# The Mathematics of Drilling Intercepts

- toc: true
- branch: master
- badges: true
- comments: true

## Introduction

Drilling intercepts are a prominent feature of junior mining news releases. They are closely monitored by the mining investment community, and a particularly good intercept can raise the prospects for a project.

As an example, consider this November 10 2020 release from Freegold Ventures:

> *Freegold Intercepts 3.78 g/t Au Over 119 Metres Including 131.5 g/t Over 3 Metres Within 573 Metres of 1.21 g/t Au at Golden Summit*

The market responded with a 3% boost in the share price the next trading day, so clearly this was regarded as a positive signal for the company's prospects. [(This is typical: capital markets tend to treat any news of this sort as good news.)](https://doi.org/10.1177%2F0312896212473401)

The implications for the economic, geological, and engineering variables surrounding the project are much less clear. *Is this a good geological result? Is it a good engineering result?* Intercepts are *highlights*: incomplete data, collected and released selectively, so is it even possible to make an informed judgement using these numbers?

To complicate things even further, the selectively reported drilling intercepts are usually released in a rather complex manner, which can make it difficult to distinguish between truly good numbers and deceptively good results.  Drilling intercepts are discussed at great length in other sources (here and here and here) but we'll take a mathematical perspective and develop a model that describes nested intercept configurations of arbitrary complexity.

We'll take Great Bear Resources for an extended example. Great Bear Resources is a Canadian junior mining company whose stock gained substantially on announcement of very high grade intercepts at their Dixie project in Ontario. At time of writing, GBR is trading at a $886 million CAD market cap (which is not very bearish at all!)

GBR Price Today by TradingView

```
#hide

from google.colab import drive
drive.mount('/content/drive')

!pip install anytree

from anytree import Node, RenderTree

from anytree import Node, RenderTree, AsciiStyle, LevelOrderIter

```

    Mounted at /content/drive

Here we open up the spreadsheet of drilling results (available on their website), and then filter on `Drill Hole` to consider a single hole:

```
import pandas as pd

intercepts = pd.read_excel('drive/My Drive/Projects/posts/data/Great_Bear/lp_drill_hole_composites_all.xlsx')
intercepts['Record'] = intercepts.index

dh = intercepts[intercepts['Drill Hole'] == 'BR-022']

dh
```

      Drill Hole
      Unnamed: 1
      From (m)
      To (m)
      Width (m)
      Gold (g/t)
      Record

      95
      BR-022
      including
      110.0
      116.10
      6.10
      2.62
      95

      96
      BR-022
      and including
      111.4
      113.10
      1.70
      7.92
      96

      97
      BR-022
      and
      274.0
      299.00
      25.00
      0.19
      97

      98
      BR-022
      and
      432.9
      439.00
      6.10
      4.05
      98

      99
      BR-022
      including
      432.9
      435.70
      2.80
      8.18
      99

      100
      BR-022
      and
      445.0
      452.00
      7.00
      0.41
      100

      101
      BR-022
      and
      461.6
      512.00
      50.40
      1.78
      101

      102
      BR-022
      including
      471.0
      512.00
      41.00
      2.09
      102

      103
      BR-022
      and including
      471.0
      478.00
      7.00
      2.37
      103

      104
      BR-022
      and including
      490.0
      491.00
      1.00
      8.15
      104

      105
      BR-022
      and including
      505.2
      508.75
      3.55
      14.90
      105

      106
      BR-022
      and including
      506.2
      506.70
      0.50
      100.48
      106

      107
      BR-022
      and
      600.0
      620.00
      20.00
      0.52
      107

This is how intercepts are typically presented: a table with a `From` field describing where they started measuring, a `To` field describing where they stopped, and a `Grade` field (called `Gold` here) that tells us how enriched that interval is with the valuable stuff. `From` and `To` are typically measured downhole from the drill collar.

It's easy to establish a basic understanding of how these tables are read, and many experienced mining investors immediately recognize these grades as very high. The rest of us might need to rely on statistics, since we don't have the benefit of many years' experience with drilling results.

Of course it is first necessary to determine the true assay values for each separate interval from top to bottom. Unfortunately, each row is not independent - some of the intercepts are contained in others, and the subinterval gold is INCLUDED in the parent interval calculation! So we can't just use the `Gold (g/t)` field directly, since intercepts are reported with these "highlights", or higher grade sections within the longer interval.

Sometimes this convention is used unethically to suggest larger intervals of enrichment than truly exist. This is called "grade smearing" and the method of residual grade calculation applied here will detect any such attempt to disguise poor results.

At first it may seem like the correct interpretation of these intervals is to imagine them intervals stacked on top of one another, but this is very misleading. We can easily visualize this to see the error:

```
#hide_input

import altair as alt

y_axis = alt.Axis(
    title='Intercept ID',
    offset=5,
    ticks=False,
    domain=False
)

alt.Chart(dh).mark_bar().encode(
    alt.X('From (m):Q',
        scale=alt.Scale(zero=False)),
    x2='To (m):Q',
    y=alt.Y('Drill Hole:N', axis=y_axis),
    color=alt.Color('Gold (g/t):Q', scale=alt.Scale(scheme="inferno")),
    tooltip=[
        alt.Tooltip('Width (m):Q', title='Width'),
        alt.Tooltip('Gold (g/t):Q', title='Gold Grade')
    ]
).properties(width=800, height=100).configure(background='#D9E9F0')
```

- We only have the total grade, INCLUDING the high-grade, child subintervals. Considering it in that way ignores the fact that the high-grade intervals are included in the wider, lower-grade intervals, inflating the grade measured over that length. This has enormous implications for the *continuity* of the mineralization, which determines the feasibility of the project.

In order to eliminate this effect we'll need to do some math with the intercepts. This visualization attempts to show this hierarchical, branching structure:

```
#hide_input

import altair as alt

y_axis = alt.Axis(
    title='Interval ID',
    offset=5,
    ticks=False,
    domain=False
)

alt.Chart(dh).mark_bar().encode(
    alt.X('From (m):Q',
        scale=alt.Scale(zero=False)),
    x2='To (m):Q',
    y=alt.Y('Record:N', axis=y_axis),
    color=alt.Color('Gold (g/t):Q', scale=alt.Scale(scheme="inferno")),
    tooltip=[
        alt.Tooltip('Width (m):Q', title='Width'),
        alt.Tooltip('Gold (g/t):Q', title='Gold Grade')
    ]
).properties(width=800, height=400).configure(background='#D9E9F0')
```

Plotted side by side, the intercepts show the parent-child overlapping relationship and capture the complexity of the problem.

Parent intervals can have no child intervals, a single child interval, or several child intervals. Child intervals themselves can have no child intervals, a single child interval, or several child intervals. Clearly there is a whole class of related problems we could solve with a general solution to this problem.

So far we have treated the `From` and `To` fields in isolation, and we can use a cool feature of Pandas to convert them to intervals:

```
dh['Interval'] = dh.apply(lambda x: pd.Interval(x['From (m)'], x['To (m)']), axis=1)

dh
```

    /usr/local/lib/python3.6/dist-packages/ipykernel_launcher.py:1: SettingWithCopyWarning:
    A value is trying to be set on a copy of a slice from a DataFrame.
    Try using .loc[row_indexer,col_indexer] = value instead

    See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
      """Entry point for launching an IPython kernel.

So the motivation here was to create `Interval` objects to use them with the `pd.Overlaps` function and then model the overlap relationship among the different intervals:

```
import itertools
import numpy as np

cross_interval = itertools.product(dh.Interval,dh.Interval)

overlap_matrix = np.array([interval[0].overlaps(interval[1]) for interval in cross_interval])

intersect_matrix = np.array([interval[0].intersect(interval[1]) for interval in cross_interval])

ns = int(np.sqrt(overlap_matrix.shape[0]))

```

```
#hide_input

import matplotlib.pyplot as plt

plt.rcParams.update({'font.size': 16})

fig, ax = plt.subplots(1,1, figsize=(10,11))

img = ax.imshow(overlap_matrix.reshape(ns,ns), cmap='hot', interpolation='nearest')

x_label_list = dh.Record.values

ax.set_xticks(np.arange(0, ns, 1))
ax.set_yticks(np.arange(0, ns, 1))

text = ax.set_xticklabels(x_label_list)
text = ax.set_yticklabels(x_label_list)

```

Here we see the overlaps: if a pixel is white, it means that the interval on the x-axis and the interval on the y-axis overlap.

Overlap is symmetric: so each 'child' overlaps with its parent and vice versa. It should become clear that we are actually interested in the "contains" relationship, which is not symmetric and will help us identify parent intervals and child intervals and start reducing the intervals.

Fortunately this is also supported in Python:

```
#hide

from sympy import Interval

dh['Interval_obj'] = dh.apply(lambda x: Interval(x['From (m)'], x['To (m)']), axis=1)

cross_interval = itertools.product(dh.Interval_obj,dh.Interval_obj)
contain_matrix = np.array([interval[0].is_proper_superset(interval[1]) for interval in cross_interval])

```

    /usr/local/lib/python3.6/dist-packages/ipykernel_launcher.py:5: SettingWithCopyWarning:

    A value is trying to be set on a copy of a slice from a DataFrame.
    Try using .loc[row_indexer,col_indexer] = value instead

    See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy

```
#hide_input

import matplotlib.pyplot as plt

plt.rcParams.update({'font.size': 16})

fig, ax = plt.subplots(1,1, figsize=(10,11))

img = ax.imshow(contain_matrix.reshape(ns,ns), cmap='hot', interpolation='nearest')

x_label_list = dh.Record.values

ax.set_xticks(np.arange(0, ns, 1))
ax.set_yticks(np.arange(0, ns, 1))

text = ax.set_xticklabels(x_label_list)
text = ax.set_yticklabels(x_label_list)
```

Now we can pull out a tree

Of the machine-intelligible formats, a **tree data structure** is clearly the most suited to representing the intervals.

```
#hide
from anytree import Node, RenderTree, AsciiStyle, PreOrderIter

interval_pairs = list(itertools.product(dh.Record.values, dh.Record.values))
parent_child = list(contain_matrix)

root = Node("DH")

all_str_to_node = {str(dh['Record'].values[i]) : Node(str(dh['Record'].values[i]), parent = root) for i, column in enumerate(contain_matrix.reshape(ns,ns).T)}

#[all_str_to_node[str(dh_101['Record'].values[i])].parent := root for i, column in enumerate(contain_matrix.reshape(14,14).T) if ~np.any(column)]

contain_matrix_sq = contain_matrix.reshape(ns,ns)

#for i, col in enumerate(contain_matrix_sq.T):
#  print(col)
#  if ~np.any(col):
#    #all_str_to_node[str(dh_101['Record'].values[i])].parent = root
#    print("Root: {}".format(dh_101['Record'].values[i]))
#  else:
#    print("Parent: {}".format(dh_101['Record'].values[::-1][np.argmax(col[::-1])]))
#    print("Child: {}".format(dh_101['Record'].values[i]))

```

```
for i, col in enumerate(contain_matrix_sq.T):

  if ~np.any(col):

    all_str_to_node[str(dh['Record'].values[i])].parent = root

  else:

    all_str_to_node[str(dh['Record'].values[i])].parent = all_str_to_node[str(dh['Record'].values[::-1][np.argmax(col[::-1])])]

print(RenderTree(root, style=AsciiStyle()).by_attr())
```

    DH
    |-- 95
    |   +-- 96
    |-- 97
    |-- 98
    |   +-- 99
    |-- 100
    |-- 101
    |   +-- 102
    |       |-- 103
    |       |-- 104
    |       +-- 105
    |           +-- 106
    +-- 107

Now we are really getting somewhere- we can actually start looking at the global picture (since we now know which intervals are not "child" intervals)

These are the direct children. We can go ahead and plot them and have a totally accurate picture of the log:

```
#hide_input

dh_prime = dh[dh.Record.isin((child.name for child in root.children))]

y_axis = alt.Axis(
    title='Intercept ID',
    offset=5,
    ticks=False,
    domain=False
)

reqd_cols = ['From (m)', 'To (m)', 'Gold (g/t)', 'Width (m)']

alt.Chart(dh_prime[reqd_cols]).mark_bar().encode(
    alt.X('From (m):Q',
        scale=alt.Scale(zero=False)),
    x2='To (m):Q',
    y=alt.Y('Drill Hole:N', axis=y_axis),
    color=alt.Color('Gold (g/t):Q', scale=alt.Scale(scheme="inferno")),
    tooltip=[
        alt.Tooltip('Width (m):Q', title='Width'),
        alt.Tooltip('Gold (g/t):Q', title='Gold Grade')
    ]
).properties(width=800, height=100).configure(background='#D9E9F0')
```

```
dh_prime.dtypes
```

    Drill Hole                 object
    Unnamed: 1                 object
    From (m)                  float64
    To (m)                    float64
    Width (m)                 float64
    Gold (g/t)                float64
    Record                      int64
    Interval        interval[float64]
    Interval_obj               object
    dtype: object

While that is correct, it is not complete: we have left out all of the additional information provided by the smaller sub-intervals!

In order to incorporate that we will have to remove them from the parent intervals and determine the residual grade (whatever is left once we pull out the gold contained in the subinterval)

```
((119) * (3.78) - (3) * (131.5)) / (119 - 3)
```

    0.47689655172413786

As an example of this kind of calculation, a simpler set of intervals from a  Freegold Ventures press release:

> *Freegold Intercepts 3.78 g/t Au Over 119 Metres Including 131.5 g/t Over 3 Metres Within 573 Metres of 1.21 g/t Au at Golden Summit*

We know the gold grade over the whole 119 meters, and the gold grade over 3 meters, but what is the gold grade over the $119 - 3 = 116 m$?

The solution is a simple weighted average calculation, like compositing over a drillhole:

$\frac{119 \times 3.78-3 \times 131.5}{119-3} = 0.477 g/t$

Credit to https://twitter.com/BrentCo77759016/status/1326183861722599424 and

So now we have to do this, but with every single subinterval until we get the residual grade at every point along the drillhole

Fortunately, the tree data structure we selected has specialized methods that make a traversal very simple.

```
levelord_nodes = [(node.name, node.children) for node in LevelOrderIter(root)]

levelord_nodes
```

    [('DH',
      (Node('/DH/95'),
       Node('/DH/97'),
       Node('/DH/98'),
       Node('/DH/100'),
       Node('/DH/101'),
       Node('/DH/107'))),
     ('95', (Node('/DH/95/96'),)),
     ('97', ()),
     ('98', (Node('/DH/98/99'),)),
     ('100', ()),
     ('101', (Node('/DH/101/102'),)),
     ('107', ()),
     ('96', ()),
     ('99', ()),
     ('102',
      (Node('/DH/101/102/103'), Node('/DH/101/102/104'), Node('/DH/101/102/105'))),
     ('103', ()),
     ('104', ()),
     ('105', (Node('/DH/101/102/105/106'),)),
     ('106', ())]

```
nn_np_loi = [(node.name, node.parent) for node in LevelOrderIter(root)]
```

```
all_str_to_node
```

    {'100': Node('/DH/100'),
     '101': Node('/DH/101'),
     '102': Node('/DH/101/102'),
     '103': Node('/DH/101/102/103'),
     '104': Node('/DH/101/102/104'),
     '105': Node('/DH/101/102/105'),
     '106': Node('/DH/101/102/105/106'),
     '107': Node('/DH/107'),
     '95': Node('/DH/95'),
     '96': Node('/DH/95/96'),
     '97': Node('/DH/97'),
     '98': Node('/DH/98'),
     '99': Node('/DH/98/99')}

```
for node, parent in nn_np_loi[::-1][:-1]:
  print(node)
  for child in all_str_to_node[node].children:
    print(child)
```

    106
    105
    Node('/DH/101/102/105/106')
    104
    103
    102
    Node('/DH/101/102/103')
    Node('/DH/101/102/104')
    Node('/DH/101/102/105')
    99
    96
    107
    101
    Node('/DH/101/102')
    100
    98
    Node('/DH/98/99')
    97
    95
    Node('/DH/95/96')

```
dh
```

      Drill Hole
      Unnamed: 1
      From (m)
      To (m)
      Width (m)
      Gold (g/t)
      Record
      Interval
      Interval_obj

      95
      BR-022
      including
      110.0
      116.10
      6.10
      2.62
      95
      (110.0, 116.1]
      Interval(110.000000000000, 116.100000000000)

      96
      BR-022
      and including
      111.4
      113.10
      1.70
      7.92
      96
      (111.4, 113.1]
      Interval(111.400000000000, 113.100000000000)

      97
      BR-022
      and
      274.0
      299.00
      25.00
      0.19
      97
      (274.0, 299.0]
      Interval(274.000000000000, 299.000000000000)

      98
      BR-022
      and
      432.9
      439.00
      6.10
      4.05
      98
      (432.9, 439.0]
      Interval(432.900000000000, 439.000000000000)

      99
      BR-022
      including
      432.9
      435.70
      2.80
      8.18
      99
      (432.9, 435.7]
      Interval(432.900000000000, 435.700000000000)

      100
      BR-022
      and
      445.0
      452.00
      7.00
      0.41
      100
      (445.0, 452.0]
      Interval(445.000000000000, 452.000000000000)

      101
      BR-022
      and
      461.6
      512.00
      50.40
      1.78
      101
      (461.6, 512.0]
      Interval(461.600000000000, 512.000000000000)

      102
      BR-022
      including
      471.0
      512.00
      41.00
      2.09
      102
      (471.0, 512.0]
      Interval(471.000000000000, 512.000000000000)

      103
      BR-022
      and including
      471.0
      478.00
      7.00
      2.37
      103
      (471.0, 478.0]
      Interval(471.000000000000, 478.000000000000)

      104
      BR-022
      and including
      490.0
      491.00
      1.00
      8.15
      104
      (490.0, 491.0]
      Interval(490.000000000000, 491.000000000000)

      105
      BR-022
      and including
      505.2
      508.75
      3.55
      14.90
      105
      (505.2, 508.75]
      Interval(505.200000000000, 508.750000000000)

      106
      BR-022
      and including
      506.2
      506.70
      0.50
      100.48
      106
      (506.2, 506.7]
      Interval(506.200000000000, 506.700000000000)

      107
      BR-022
      and
      600.0
      620.00
      20.00
      0.52
      107
      (600.0, 620.0]
      Interval(600.000000000000, 620.000000000000)

```
cross_interval = itertools.product(dh.Interval_obj,dh.Interval_obj)

intersect_matrix = np.array([interval[0].intersect(interval[1]) for interval in cross_interval])

intersect_matrix
```

    array([Interval(110.000000000000, 116.100000000000),
           Interval(111.400000000000, 113.100000000000), EmptySet(),
           EmptySet(), EmptySet(), EmptySet(), EmptySet(), EmptySet(),
           EmptySet(), EmptySet(), EmptySet(), EmptySet(), EmptySet(),
           Interval(111.400000000000, 113.100000000000),
           Interval(111.400000000000, 113.100000000000), EmptySet(),
           EmptySet(), EmptySet(), EmptySet(), EmptySet(), EmptySet(),
           EmptySet(), EmptySet(), EmptySet(), EmptySet(), EmptySet(),
           EmptySet(), EmptySet(),
           Interval(274.000000000000, 299.000000000000), EmptySet(),
           EmptySet(), EmptySet(), EmptySet(), EmptySet(), EmptySet(),
           EmptySet(), EmptySet(), EmptySet(), EmptySet(), EmptySet(),
           EmptySet(), EmptySet(),
           Interval(432.900000000000, 439.000000000000),
           Interval(432.900000000000, 435.700000000000), EmptySet(),
           EmptySet(), EmptySet(), EmptySet(), EmptySet(), EmptySet(),
           EmptySet(), EmptySet(), EmptySet(), EmptySet(), EmptySet(),
           Interval(432.900000000000, 435.700000000000),
           Interval(432.900000000000, 435.700000000000), EmptySet(),
           EmptySet(), EmptySet(), EmptySet(), EmptySet(), EmptySet(),
           EmptySet(), EmptySet(), EmptySet(), EmptySet(), EmptySet(),
           EmptySet(), EmptySet(),
           Interval(445.000000000000, 452.000000000000), EmptySet(),
           EmptySet(), EmptySet(), EmptySet(), EmptySet(), EmptySet(),
           EmptySet(), EmptySet(), EmptySet(), EmptySet(), EmptySet(),
           EmptySet(), EmptySet(),
           Interval(461.600000000000, 512.000000000000),
           Interval(471.000000000000, 512.000000000000),
           Interval(471.000000000000, 478.000000000000),
           Interval(490.000000000000, 491.000000000000),
           Interval(505.200000000000, 508.750000000000),
           Interval(506.200000000000, 506.700000000000), EmptySet(),
           EmptySet(), EmptySet(), EmptySet(), EmptySet(), EmptySet(),
           EmptySet(), Interval(471.000000000000, 512.000000000000),
           Interval(471.000000000000, 512.000000000000),
           Interval(471.000000000000, 478.000000000000),
           Interval(490.000000000000, 491.000000000000),
           Interval(505.200000000000, 508.750000000000),
           Interval(506.200000000000, 506.700000000000), EmptySet(),
           EmptySet(), EmptySet(), EmptySet(), EmptySet(), EmptySet(),
           EmptySet(), Interval(471.000000000000, 478.000000000000),
           Interval(471.000000000000, 478.000000000000),
           Interval(471.000000000000, 478.000000000000), EmptySet(),
           EmptySet(), EmptySet(), EmptySet(), EmptySet(), EmptySet(),
           EmptySet(), EmptySet(), EmptySet(), EmptySet(),
           Interval(490.000000000000, 491.000000000000),
           Interval(490.000000000000, 491.000000000000), EmptySet(),
           Interval(490.000000000000, 491.000000000000), EmptySet(),
           EmptySet(), EmptySet(), EmptySet(), EmptySet(), EmptySet(),
           EmptySet(), EmptySet(), EmptySet(),
           Interval(505.200000000000, 508.750000000000),
           Interval(505.200000000000, 508.750000000000), EmptySet(),
           EmptySet(), Interval(505.200000000000, 508.750000000000),
           Interval(506.200000000000, 506.700000000000), EmptySet(),
           EmptySet(), EmptySet(), EmptySet(), EmptySet(), EmptySet(),
           EmptySet(), Interval(506.200000000000, 506.700000000000),
           Interval(506.200000000000, 506.700000000000), EmptySet(),
           EmptySet(), Interval(506.200000000000, 506.700000000000),
           Interval(506.200000000000, 506.700000000000), EmptySet(),
           EmptySet(), EmptySet(), EmptySet(), EmptySet(), EmptySet(),
           EmptySet(), EmptySet(), EmptySet(), EmptySet(), EmptySet(),
           EmptySet(), EmptySet(),
           Interval(600.000000000000, 620.000000000000)], dtype=object)

```
dh['grade_len'] = dh['Gold (g/t)'] * dh['Width (m)']
```

    /usr/local/lib/python3.6/dist-packages/ipykernel_launcher.py:1: SettingWithCopyWarning:
    A value is trying to be set on a copy of a slice from a DataFrame.
    Try using .loc[row_indexer,col_indexer] = value instead

    See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
      """Entry point for launching an IPython kernel.

```
dh
```

      Drill Hole
      Unnamed: 1
      From (m)
      To (m)
      Width (m)
      Gold (g/t)
      Record
      Interval
      Interval_obj
      grade_len

      95
      BR-022
      including
      110.0
      116.10
      6.10
      2.62
      95
      (110.0, 116.1]
      Interval(110.000000000000, 116.100000000000)
      15.982

      96
      BR-022
      and including
      111.4
      113.10
      1.70
      7.92
      96
      (111.4, 113.1]
      Interval(111.400000000000, 113.100000000000)
      13.464

      97
      BR-022
      and
      274.0
      299.00
      25.00
      0.19
      97
      (274.0, 299.0]
      Interval(274.000000000000, 299.000000000000)
      4.750

      98
      BR-022
      and
      432.9
      439.00
      6.10
      4.05
      98
      (432.9, 439.0]
      Interval(432.900000000000, 439.000000000000)
      24.705

      99
      BR-022
      including
      432.9
      435.70
      2.80
      8.18
      99
      (432.9, 435.7]
      Interval(432.900000000000, 435.700000000000)
      22.904

      100
      BR-022
      and
      445.0
      452.00
      7.00
      0.41
      100
      (445.0, 452.0]
      Interval(445.000000000000, 452.000000000000)
      2.870

      101
      BR-022
      and
      461.6
      512.00
      50.40
      1.78
      101
      (461.6, 512.0]
      Interval(461.600000000000, 512.000000000000)
      89.712

      102
      BR-022
      including
      471.0
      512.00
      41.00
      2.09
      102
      (471.0, 512.0]
      Interval(471.000000000000, 512.000000000000)
      85.690

      103
      BR-022
      and including
      471.0
      478.00
      7.00
      2.37
      103
      (471.0, 478.0]
      Interval(471.000000000000, 478.000000000000)
      16.590

      104
      BR-022
      and including
      490.0
      491.00
      1.00
      8.15
      104
      (490.0, 491.0]
      Interval(490.000000000000, 491.000000000000)
      8.150

      105
      BR-022
      and including
      505.2
      508.75
      3.55
      14.90
      105
      (505.2, 508.75]
      Interval(505.200000000000, 508.750000000000)
      52.895

      106
      BR-022
      and including
      506.2
      506.70
      0.50
      100.48
      106
      (506.2, 506.7]
      Interval(506.200000000000, 506.700000000000)
      50.240

      107
      BR-022
      and
      600.0
      620.00
      20.00
      0.52
      107
      (600.0, 620.0]
      Interval(600.000000000000, 620.000000000000)
      10.400

```
from sympy import Union
import functools

resid_grades, resid_len = {}, {}

for node in levelord_nodes[1:]:

  parent_interval = dh[dh['Record'] == float(node[0])]
  child_names = [child.name for child in node[1]]
  child_intervals = [dh[dh['Record'] == float(child)] for child in child_names]

  new_interval_obj = parent_interval.Interval_obj.values[0] - Union([child.Interval_obj.values[0] for child in child_intervals])

  l_child_int = Union([intv['Interval_obj'].values[0] for intv in child_intervals])._measure

  lg_child_int = [dh.loc[int(child_name)]['grade_len'] for child_name in child_names]

  lg_total_int = parent_interval.grade_len.values[0]

  residual_grade = (lg_total_int - sum(lg_child_int)) / (new_interval_obj._measure)

  resid_grades[node[0]] = residual_grade
  resid_len[node[0]] = new_interval_obj

  print("Interval:")
  print(node[0])

  print("Length x Grade:")
  print(lg_total_int - sum(lg_child_int))

  print("Residual Grade:")
  print(residual_grade)

```

    Interval:
    95
    Length x Grade:
    2.517999999999999
    Residual Grade:
    0.572272727272726
    Interval:
    97
    Length x Grade:
    4.75
    Residual Grade:
    0.190000000000000
    Interval:
    98
    Length x Grade:
    1.801000000000002
    Residual Grade:
    0.545757575757574
    Interval:
    100
    Length x Grade:
    2.8699999999999997
    Residual Grade:
    0.410000000000000
    Interval:
    101
    Length x Grade:
    4.022000000000006
    Residual Grade:
    0.427872340425534
    Interval:
    107
    Length x Grade:
    10.4
    Residual Grade:
    0.520000000000000
    Interval:
    96
    Length x Grade:
    13.464
    Residual Grade:
    7.92000000000005
    Interval:
    99
    Length x Grade:
    22.903999999999996
    Residual Grade:
    8.17999999999997
    Interval:
    102
    Length x Grade:
    8.055000000000007
    Residual Grade:
    0.273514431239389
    Interval:
    103
    Length x Grade:
    16.59
    Residual Grade:
    2.37000000000000
    Interval:
    104
    Length x Grade:
    8.15
    Residual Grade:
    8.15000000000000
    Interval:
    105
    Length x Grade:
    2.654999999999994
    Residual Grade:
    0.870491803278683
    Interval:
    106
    Length x Grade:
    50.24
    Residual Grade:
    100.480000000000

Check these solutions: 95 should be easy to validate

includes 96

95 extends from 110.00 m to 116.10 m (l = 6.10 m)

96 is the interval from 111.40 to 113.10 m (l = 1.70 m)

Larger interval

(6.10 m)(2.62 g/t) = (1.7 m)(7.92 g/t) + (4.4 m)(x g/t)

Rearranging terms, we get:

$x = \frac{(6.10 m)(2.62 g/t) - (1.7 m)(7.92 g/t)}{ 4.4 m }$

So the residual grade is 0.5723 g/t, which matches the value found above!

```
((6.10 * 2.62) - (1.7)*(7.92)) / (4.4)
```

    0.5722727272727269

```
resid_len
```

    {'100': Interval(445.000000000000, 452.000000000000),
     '101': Interval.Ropen(461.600000000000, 471.000000000000),
     '102': Union(Interval.open(478.000000000000, 490.000000000000), Interval.open(491.000000000000, 505.200000000000), Interval.Lopen(508.750000000000, 512.000000000000)),
     '103': Interval(471.000000000000, 478.000000000000),
     '104': Interval(490.000000000000, 491.000000000000),
     '105': Union(Interval.Ropen(505.200000000000, 506.200000000000), Interval.Lopen(506.700000000000, 508.750000000000)),
     '106': Interval(506.200000000000, 506.700000000000),
     '107': Interval(600.000000000000, 620.000000000000),
     '95': Union(Interval.Ropen(110.000000000000, 111.400000000000), Interval.Lopen(113.100000000000, 116.100000000000)),
     '96': Interval(111.400000000000, 113.100000000000),
     '97': Interval(274.000000000000, 299.000000000000),
     '98': Interval.Lopen(435.700000000000, 439.000000000000),
     '99': Interval(432.900000000000, 435.700000000000)}

```
Interval(445.000000000000, 452.000000000000).end
```

    452.000000000000

```
dh['Record'].astype(str).map(resid_len)
dh['Record'].astype(str).map(resid_grades)
```

    95     0.572272727272726
    96      7.92000000000005
    97     0.190000000000000
    98     0.545757575757574
    99      8.17999999999997
    100    0.410000000000000
    101    0.427872340425534
    102    0.273514431239389
    103     2.37000000000000
    104     8.15000000000000
    105    0.870491803278683
    106     100.480000000000
    107    0.520000000000000
    Name: Record, dtype: object

TODO: Need to split up the non-contiguous segments so that you can actually plot them

Idea: use .args attribute
Problem: This is defined for Interval objects as well and it gets us something we don't want

```
dh['Record'].astype(str).map(resid_len)
```

    95     Union(Interval.Ropen(110.000000000000, 111.400...
    96          Interval(111.400000000000, 113.100000000000)
    97          Interval(274.000000000000, 299.000000000000)
    98     Interval.Lopen(435.700000000000, 439.000000000...
    99          Interval(432.900000000000, 435.700000000000)
    100         Interval(445.000000000000, 452.000000000000)
    101    Interval.Ropen(461.600000000000, 471.000000000...
    102    Union(Interval.open(478.000000000000, 490.0000...
    103         Interval(471.000000000000, 478.000000000000)
    104         Interval(490.000000000000, 491.000000000000)
    105    Union(Interval.Ropen(505.200000000000, 506.200...
    106         Interval(506.200000000000, 506.700000000000)
    107         Interval(600.000000000000, 620.000000000000)
    Name: Record, dtype: object

```

def args_extended(interval):
  if type(interval) == Union:
    return interval.args
  else:
    return interval

remaining_interval_grade = pd.DataFrame({'interval' : dh['Record'].astype(str).map(resid_len), 'grade' : dh['Record'].astype(str).map(resid_grades)})

remaining_interval_grade['split_intervals'] = remaining_interval_grade.interval.apply(args_extended)

rig_exploded = remaining_interval_grade.explode('split_intervals')
```

```
rig_exploded
```

      interval
      grade
      split_intervals

      95
      Union(Interval.Ropen(110.000000000000, 111.400...
      0.572272727272726
      Interval.Ropen(110.000000000000, 111.400000000...

      95
      Union(Interval.Ropen(110.000000000000, 111.400...
      0.572272727272726
      Interval.Lopen(113.100000000000, 116.100000000...

      96
      Interval(111.400000000000, 113.100000000000)
      7.92000000000005
      Interval(111.400000000000, 113.100000000000)

      97
      Interval(274.000000000000, 299.000000000000)
      0.190000000000000
      Interval(274.000000000000, 299.000000000000)

      98
      Interval.Lopen(435.700000000000, 439.000000000...
      0.545757575757574
      Interval.Lopen(435.700000000000, 439.000000000...

      99
      Interval(432.900000000000, 435.700000000000)
      8.17999999999997
      Interval(432.900000000000, 435.700000000000)

      100
      Interval(445.000000000000, 452.000000000000)
      0.410000000000000
      Interval(445.000000000000, 452.000000000000)

      101
      Interval.Ropen(461.600000000000, 471.000000000...
      0.427872340425534
      Interval.Ropen(461.600000000000, 471.000000000...

      102
      Union(Interval.open(478.000000000000, 490.0000...
      0.273514431239389
      Interval.open(478.000000000000, 490.000000000000)

      102
      Union(Interval.open(478.000000000000, 490.0000...
      0.273514431239389
      Interval.open(491.000000000000, 505.200000000000)

      102
      Union(Interval.open(478.000000000000, 490.0000...
      0.273514431239389
      Interval.Lopen(508.750000000000, 512.000000000...

      103
      Interval(471.000000000000, 478.000000000000)
      2.37000000000000
      Interval(471.000000000000, 478.000000000000)

      104
      Interval(490.000000000000, 491.000000000000)
      8.15000000000000
      Interval(490.000000000000, 491.000000000000)

      105
      Union(Interval.Ropen(505.200000000000, 506.200...
      0.870491803278683
      Interval.Ropen(505.200000000000, 506.200000000...

      105
      Union(Interval.Ropen(505.200000000000, 506.200...
      0.870491803278683
      Interval.Lopen(506.700000000000, 508.750000000...

      106
      Interval(506.200000000000, 506.700000000000)
      100.480000000000
      Interval(506.200000000000, 506.700000000000)

      107
      Interval(600.000000000000, 620.000000000000)
      0.520000000000000
      Interval(600.000000000000, 620.000000000000)

```
rig_exploded['From'] = rig_exploded.split_intervals.apply(lambda x: x.start).astype(float)
rig_exploded['To'] = rig_exploded.split_intervals.apply(lambda x: x.end).astype(float)
rig_exploded['Width'] = (rig_exploded.To - rig_exploded.From).astype(float)

rig_exploded['grade'] = rig_exploded.grade.astype(float)

rig_exploded['drillhole'] = 'BR-022'
```

```
rig_exploded[['From', 'To', 'grade', 'Width']]
```

      From
      To
      grade
      Width

      95
      110.00
      111.40
      0.572273
      1.40

      95
      113.10
      116.10
      0.572273
      3.00

      96
      111.40
      113.10
      7.920000
      1.70

      97
      274.00
      299.00
      0.190000
      25.00

      98
      435.70
      439.00
      0.545758
      3.30

      99
      432.90
      435.70
      8.180000
      2.80

      100
      445.00
      452.00
      0.410000
      7.00

      101
      461.60
      471.00
      0.427872
      9.40

      102
      478.00
      490.00
      0.273514
      12.00

      102
      491.00
      505.20
      0.273514
      14.20

      102
      508.75
      512.00
      0.273514
      3.25

      103
      471.00
      478.00
      2.370000
      7.00

      104
      490.00
      491.00
      8.150000
      1.00

      105
      505.20
      506.20
      0.870492
      1.00

      105
      506.70
      508.75
      0.870492
      2.05

      106
      506.20
      506.70
      100.480000
      0.50

      107
      600.00
      620.00
      0.520000
      20.00

```

y_axis = alt.Axis(
    title='Intercept ID',
    offset=5,
    ticks=False,
    domain=False
)

reqd_cols = ['From', 'To', 'grade', 'Width', 'drillhole']

alt.Chart(rig_exploded[reqd_cols]).mark_bar().encode(
    alt.X('From:Q',
        scale=alt.Scale(zero=False)),
    x2='To:Q',
    y=alt.Y('drillhole:N', axis=y_axis),
    color=alt.Color('grade:Q', scale=alt.Scale(scheme="inferno")),
    tooltip=[
        alt.Tooltip('Width:Q', title='Width'),
        alt.Tooltip('grade:Q', title='Gold Grade')
    ]
).properties(width=800, height=100).configure(background='#D9E9F0').interactive()
```


## Thiết Kế Hồi Quy Gián Đoạn

Dù không ngẫm nghĩ nhiều về nó nhưng chúng ta không khỏi ấn tượng bởi tính liên tục của tự nhiên. Bạn không thể trồng cây mà  nụ không nở trước, bạn không thể dịch chuyển tức thời từ nơi này sang nơi khác, vết thương cần thời gian để chữa lành. Ngay cả trong lĩnh vực xã hội, sự liên tục dường như là một chuẩn mực. Bạn không thể phát triển doanh nghiệp chỉ trong một ngày, cần phải có sự kiên định và chăm chỉ để tích luỹ của cải và phải mất đến nhiều năm để bạn nắm được cách hồi quy tuyến tính hoạt động. Trong những trường hợp thường thấy, tự nhiên rất có tính liên kết và không thay đổi nhiều.

```
Làm sao đem hết xác hồn,
Hòa mình với Đạo chẳng còn lìa xa.
```
\- Đạo Đức Kinh, Lão Tử.

Có nghĩa là **khi chúng ta nhìn thấy những thay đổi đột ngột, có thể chúng là nhân tạo**, hay nói cách khác là những tình huống do con người tạo ra. Những sự kiện này thường đi kèm với những giả tưởng đối với những chuyện thông thường: nếu một điều kỳ lạ xảy ra, nó cho chúng ta một số hiểu biết về những gì có thể đã xảy ra nếu tự nhiên vận hành theo một hướng khác. Khám phá những biến đổi nhân tạo này là cốt lõi của mô hình Thiết Kế Hồi Quy Gián Đoạn.

Thiết lập cơ bản có dạng như sau. Hãy tưởng tượng bạn có một biến can thiệp \\(T\\) và các kết quả tiềm năng \\(Y_0\\) và \\(Y_1\\). Can thiệp T là một hàm gián đoạn của biến chỉ định quan sát được \\(R\\) sao cho

$
D_i = \mathcal{1}\{R_i>c\}
$

Nói cách khác, can thiệp có giá trị 0 khi \\(R\\) dưới ngưỡng quyết định \\(c\\) và có giá trị 1 đối với trường hợp ngược lại. Điều này có nghĩa là chúng ta phải quan sát \\(Y_1\\) khi \\(R>c\\) và \\(Y_0\\) khi \\(R<c\\). Để hiểu rõ được điều này, hãy nghĩ về các kết quả tiềm năng như 2 chức năng mà ta không thể quan sát hoàn toàn. Cả \\(Y_0(R)\\) và \\(Y_1(R)\\) đều ở đó, chúng ta chỉ là không thể thấy chúng. Ngưỡng hoạt động như một công tắc cho phép ta nhìn thấy một trong hai chức năng đó chứ không bao giờ thấy được cả hai chức năng cùng một lúc, giống như trong hình dưới đây:

Ý tưởng của hồi quy gián đoạn là so sánh kết quả ngay trên và ngay dưới ngưỡng quyết định để xác định tác động can thiệp tại ngưỡng quyết định. Đây được gọi là thiết kế **RD sắc  **, vì xác suất nhận được can thiệp tăng từ 0 đến 1 ngay tại ngưỡn quyết định quyết định quyết địnhg quyết định, nhưng chúng ta cũng có thể tìm hiểu về **thiết kế RD mờ**, trong đó xác suất cũng tăng, nhưng ít cực đoan hơn.

## Có Phải Rượu Đang Giết Bạn?

Một câu hỏi về chính sách công rất liên quan là độ tuổi uống rượu tối thiểu là bao nhiêu. Hầu hết ở các quốc gia, bao gồm cả Brazil, là 18 tuổi trở lên, nhưng ở Mỹ (hầu hết các bang), hiện tại là 21. Vậy, có phải tại Mỹ đang quá thận trọng và họ nên giảm độ tuổi uống rượu tối thiểu? Hay là các quốc gia khác nên tăng độ tuổi uống rượu hợp pháp tại đất nước của mình?

Một cách để xem xét câu hỏi này là từ [góc độ tỷ lệ tử vong (Carpenter và Dobkin, 2009)](https://www.aeaweb.org/articles?id=10.1257/app.1.1.164). Từ quan điểm chính sách công, người ta có thể tranh luận rằng chúng ta nên giảm tỷ lệ tử vong càng nhiều càng tốt. Nếu uống rượu làm gia tăng đáng kể tỷ lệ tử vong, chúng ta nên tránh hạ độ tuổi uống rượu tối thiểu. Điều này sẽ phù hợp với mục tiêu giảm tử vong gây ra bởi uống rượu.

Để ước lượng tác động của rượu, chúng ta có thể sử dụng thực tế là độ tuổi uống rượu hợp pháp gây ra sự gián đoạn đối với tự nhiên. Tại Mỹ, những người dưới 21 tuổi không uống rượu (hoặc uống ít hơn nhiều) trong khi những người trên 21 tuổi lại uống. Điều này có nghĩa là xác suất uống rượu tăng vọt ở độ tuổi 21 và đó là điều mà chúng ta có thể khám phá với RDD.

```python
import warnings
warnings.filterwarnings('ignore')

import pandas as pd
import numpy as np
from matplotlib import style
from matplotlib import pyplot as plt
import seaborn as sns
import statsmodels.formula.api as smf

%matplotlib inline

style.use("fivethirtyeight")
```

Để làm được như vậy, chúng ta có thể lấy một số dữ liệu tử vong tổng hợp theo độ tuổi. Mỗi hàng là một độ tuổi trung bình của một nhóm người và tỷ lệ tử vong trung bình gây ra bởi tất cả các nguyên nhân (`all`), do tai nạn giao thông (`mva`) và do tự tử (`suicide`).

```python
drinking = pd.read_csv("./data/drinking.csv")
drinking.head()[["agecell", "all", "mva", "suicide"]]
```

      agecell
      all
      mva
      suicide

      0
      19.068493
      92.825400
      35.829327
      11.203714

      1
      19.150684
      95.100740
      35.639256
      12.193368

      2
      19.232876
      92.144295
      34.205650
      11.715812

      3
      19.315070
      88.427760
      32.278957
      11.275010

      4
      19.397260
      88.704940
      32.650967
      10.984314

Để dễ hình dung, cũng như vì một lí do quan trọng không kém ở phần sau, chúng ta sẽ tập trung biến chỉ định `agecell` tại ngưỡng quyết định ở tuổi 21.

```python
drinking["agecell"] -= 21
```

Nếu chúng ta vẽ biểu đồ của nhiều biến kết quả (`all`,`mva`, `suicide`) với biến chỉ định trên trục x, chúng ta thấy được rằng tỷ lệ tử vong cũng tăng lên khi ta vượt qua độ tuổi uống rượu hợp pháp.

```python
plt.figure(figsize=(8,8))
ax = plt.subplot(3,1,1)
drinking.plot.scatter(x="agecell", y="all", ax=ax)
plt.title("Death Cause by Age (Centered at 0)")

ax = plt.subplot(3,1,2, sharex=ax)
drinking.plot.scatter(x="agecell", y="mva", ax=ax)

ax = plt.subplot(3,1,3, sharex=ax)
drinking.plot.scatter(x="agecell", y="suicide", ax=ax);

```

Có một số dấu hiệu, nhưng chúng ta cần nhiều hơn thế. Chính xác thì việc uống rượu bia tác động lên tỷ lệ tử vong tại ngưỡng quyết định nào? Và sai số chuẩn cho ước lượng đó là bao nhiêu?

## Ước lượng RDD

Giả thiết chủ đạo mà RDD dựa vào là sự liên tục của kết quả tiềm năng tại ngưỡng quyết định. Nói một cách chính xác thì giới hạn của các kết quả tiềm năng khi biến chỉ định tiệm cận ngưỡng quyết định từ bên phải và bên trái phải như nhau.

$$
\lim_{r \to c^-} E[Y_{ti}|R_i=r] = \lim_{r \to c^+} E[Y_{ti}|R_i=r]
$$

Nếu điều kiện này thoả mãn, chúng ta có thể tìm được tác động nhân quả tại ngưỡng quyết định

\begin{align}
\lim_{r \to c^+} E[Y_{ti}|R_i=r] - \lim_{r \to c^-} E[Y_{ti}|R_i=r]=&\lim_{r \to c^+} E[Y_{1i}|R_i=r] - \lim_{r \to c^-} E[Y_{0i}|R_i=r] \\
=& E[Y_{1i}|R_i=r] - E[Y_{0i}|R_i=r] \\
=& E[Y_{1i} - Y_{0i}|R_i=r]
\end{align}

Theo một cách nào đó thì đây chính là một dạng của Tác Động Can Thiệp Bình Quân Cục Bộ (LATE), vì chúng ta chỉ có thể biết nó tại ngưỡng quyết định. Trong thiết lập này, chúng ta có thể coi RDD như một thử nghiệm ngẫu nhiên cục bộ. Đối với những người tại ngưỡng quyết định, can thiệp có thể diễn ra một cách tình cờ theo cả hai hướng, một số người nằm dưới ngưỡng quyết định, và một số người lại nằm trên. Trong ví dụ của chúng ta, tại cùng một thời điểm, một số người chỉ vừa qua độ tuổi 21 và một số người sắp tròn 21 tuổi. Những thứ quyết định điều này là liệu có ai đó được sinh ra sau đó vài ngày hay không, điều này khá ngẫu nhiên. Vì lý do này, RDD cho thấy một câu chuyện về nhân quả rất hấp dẫn. Nó không phải là tiêu chuẩn vàng như RCT, nhưng nó cũng gần như thế.

Bây giờ, để ước lượng tác động can thiệp tại ngưỡng quyết định, tất cả những gì chúng ta cần làm là ước lượng cả hai giới hạn trong công thức trên và so sánh chúng. Cách đơn giản nhất để làm điều đó là chạy một mô hình hồi quy tuyến tính

Để cho nó hoạt động, chúng ta cho biến giả trên ngưỡng quyết định tác động lên biến chỉ định

$
y_i = \beta_0 + \beta_1 r_i + \beta_2 \mathcal{1}\{r_i>c\} + \beta_3 \mathcal{1}\{r_i>c\} r_i
$

Điều mà ta đang làm là áp một hồi quy tuyến tính trên ngưỡng quyết định và một hồi quy khác ở dưới ngưỡng quyết định đó. Tham số \\(\beta_0\\) là hệ số chặn của hồi quy dưới ngưỡng quyết định và \\(\beta_0+\beta_2\\) là hệ số chặn của hồi quy trên ngưỡng quyết định.

Và đây là lúc thủ thuật đặt gốc tọa độ 0 tại ngưỡng chỉ định can thiệp phát huy tác dụng. Sau bước tiền xử lý này, ngưỡng quyết định sẽ trở thành 0. Điều này khiến hệ số chặn \\(\beta_0\\) là giá trị được dự đoán ở ngưỡng quyết định, cho hồi quy bên dưới nó. Nói cách khác, \\(\beta_0=\lim_{r \to c^-} E[Y_{ti}|R_i=r]\\). Với lập luận tương tự, \\(\beta_0+\beta_2\\) là giới hạn của kết quả ở trên. Nghĩa là

$
\lim_{r \to c^+} E[Y_{ti}|R_i=r] - \lim_{r \to c^-} E[Y_{ti}|R_i=r]=\beta_2=E[ATE|R=c]
$

Điều này được thể hiện qua đoạn code dưới đây trong trường hợp chúng ta ước lượng tác động của việc uống rượu đến tỷ lệ tử vong gây ra bởi mọi nguyên nhân ở độ tuổi 21.

```python
rdd_df = drinking.assign(threshold=(drinking["agecell"] > 0).astype(int))

model = smf.wls("all~agecell*threshold", rdd_df).fit()

model.summary().tables[1]
```

          	             coef	     std err	      t	      P>|t|	  [0.025	    0.975]

  Intercept	            93.6184	     0.932	   100.399	  0.000	    91.739	    95.498

  agecell	               0.8270	     0.819	     1.010	  0.318	    -0.823	     2.477

  threshold	             7.6627	     1.319	     5.811	  0.000	     5.005	    10.320

  agecell:threshold	    -3.6034	     1.158	    -3.111	  0.003	    -5.937	    -1.269

Mô hình này cho chúng ta biết rằng tỷ lệ tử vong tăng 7.6627 điểm khi uống rượu. Một cách nói khác là rượu làm tăng 8% nguy cơ tử vong gây ra bởi mọi nguyên nhân ((7.6627+93.6184)/93.6184). Lưu ý rằng điều này cũng cung cấp cho chúng ta các sai số chuẩn cho ước lượng của tác động nhân quả. Trong trường hợp này, tác động có ý nghĩa thống kê, vì trị số p có giá trị dưới 0.01.

Nếu chúng ta muốn xác minh mô hình này bằng đồ thị, ta có thể biểu thị các giá trị dự đoán trên dữ liệu mà chúng ta có. Bạn có thể thấy rằng chúng ta có 2 mô hình hồi quy: một cho những người có độ tuổi trên 21 và một cho độ tuổi dưới 21.

```python
ax = drinking.plot.scatter(x="agecell", y="all", color="C0")
drinking.assign(predictions=model.fittedvalues).plot(x="agecell", y="predictions", ax=ax, color="C1")
plt.title("Regression Discontinuity");
```

Nếu ta làm tương tự cho những tác nhân khác, ta sẽ có như sau

```python
plt.figure(figsize=(8,8))

for p, cause in enumerate(["all", "mva", "suicide"], 1):
    ax = plt.subplot(3,1,p)
    drinking.plot.scatter(x="agecell", y=cause, ax=ax)
    m = smf.wls(f"{cause}~agecell*threshold", rdd_df).fit()
    ate_pct = 100*((m.params["threshold"] + m.params["Intercept"])/m.params["Intercept"] - 1)
    drinking.assign(predictions=m.fittedvalues).plot(x="agecell", y="predictions", ax=ax, color="C1")
    plt.title(f"Impact of Alcohol on Death: {np.round(ate_pct, 2)}%")

plt.tight_layout()
```

RDD cho thấy uống rượu làm tăng 15% nguy cơ tử vong do tự tử và tai nạn xe hơi, đây là một con số khá lớn. Những kết quả này là những lý lẽ thuyết phục để không hạ độ tuổi uống rượu tối thiểu nếu chúng ta muốn giảm thiểu tỷ lệ tử vong.

### Tỷ Trọng Kernel

Hồi Quy Gián Đoạn phần lớn dựa vào các đặc điểm ngoại suy của hồi quy tuyến tính. Vì chúng ta đang xem xét các giá trị tại điểm đầu và cuối của 2 đường hồi quy, tốt hơn hết chúng ta nên xác định đúng các giới hạn đó. Những gì có thể xảy ra là hồi quy có thể đã tập trung quá nhiều vào việc khớp các điểm dữ liệu khác để đổi lại tỷ lệ khớp đáng thất vọng tại ngưỡng quyết định. Nếu điều này xảy ra, rất có thể chúng ta đã đo lường sai tác động can thiệp.

Một cách để giải quyết vấn đề trên là gán trọng số cao hơn cho các điểm nằm gần ngưỡng quyết định hơn. Có nhiều cách để làm được điều này, nhưng một cách phổ biến là điều chỉnh lại trọng số cho các mẫu thông qua **hàm tam giác kernel**

$
K(R, c, h) = \mathcal{1}\{|R-c| \leq h\} * \bigg(1-\frac{|R-c|}{h}\bigg)
$

Phần đầu tiên của kernel là một hàm chỉ số cho biết chúng ta đã gần ngưỡng quyết định hay chưa. Gần như thế nào? Điều này được xác định bởi tham số dải tần suất \\(h\\). Phần thứ hai của kernel là một hàm tỷ trọng. Khi chúng ta rời xa ngưỡng quyết định, trọng số ngày càng nhỏ hơn. Các trọng số được chia cho dải tần suất. Nếu dải tần suất lớn, trọng số sẽ giảm dần với tốc độ chậm. Nếu dải tần suất nhỏ, trọng số sẽ nhanh chóng tiến về 0.

Để dễ hiểu hơn, ta sẽ biểu diễn các tỷ trọng đối với kernel được áp dụng trong bài toán này. Tôi đã thiết lập dải tần suất tại đây là 1, có nghĩa là chúng ta sẽ chỉ xem xét dữ liệu từ những người không quá 22 tuổi và không dưới 20 tuổi.

```python
def kernel(R, c, h):
    indicator = (np.abs(R-c) <= h).astype(float)
    return indicator * (1 - np.abs(R-c)/h)
```

```python
plt.plot(drinking["agecell"], kernel(drinking["agecell"], c=0, h=1))
plt.xlabel("agecell")
plt.ylabel("Weight")
plt.title("Kernel Weight by Age");
```

Nếu chúng ta áp dụng những trọng số này cho bài toán đầu tiên, tác động của rượu sẽ lớn hơn, ít nhất là đối với trường hợp tử vong gây ra bởi mọi nguyên nhân. Kết quả tăng vọt từ 7.6627 lên 9.7004. Kết quả vẫn có ý nghĩa thống kê. Ngoài ra, hãy lưu ý rằng tôi đang sử dụng `wls` thay vì `ols`

```python
model = smf.wls("all~agecell*threshold", rdd_df,
                weights=kernel(drinking["agecell"], c=0, h=1)).fit()

model.summary().tables[1]
```

          	             coef	     std err	      t	      P>|t|	  [0.025	    0.975]

  Intercept	            93.2002	     0.731	   127.429	  0.000	    91.726	    94.674

  agecell	               0.4109	     1.789	     0.230	  0.819	    -3.196	     4.017

  threshold	             9.7004	     1.034	     9.378	  0.000	     7.616	    11.785

  agecell:threshold	    -7.1759	     2.531	    -2.835	  0.007	   -12.276	    -2.075

```python
ax = drinking.plot.scatter(x="agecell", y="all", color="C0")
drinking.assign(predictions=model.fittedvalues).plot(x="agecell", y="predictions", ax=ax, color="C1")
plt.title("Regression Discontinuity (Local Regression)");
```

Và đây là những gì ta có được đối với tỷ lệ tử vong gây ra bởi các tác nhân khác. Lưu ý rằng hồi quy tại những điểm trên ngưỡng quyết định có xu hướng giảm lớn hơn vì nó không xem xét các điểm nằm xa ngưỡng quyết định.

```python
plt.figure(figsize=(8,8))
weights = kernel(drinking["agecell"], c=0, h=1)

for p, cause in enumerate(["all", "mva", "suicide"], 1):
    ax = plt.subplot(3,1,p)
    drinking.plot.scatter(x="agecell", y=cause, ax=ax)
    m = smf.wls(f"{cause}~agecell*threshold", rdd_df, weights=weights).fit()
    ate_pct = 100*((m.params["threshold"] + m.params["Intercept"])/m.params["Intercept"] - 1)
    drinking.assign(predictions=m.fittedvalues).plot(x="agecell", y="predictions", ax=ax, color="C1")
    plt.title(f"Impact of Alcohol on Death: {np.round(ate_pct, 2)}%")

plt.tight_layout()
```

Ngoại trừ tác nhân tự sát, có vẻ như việc thêm tỷ trọng kernel đã làm cho tác động tiêu cực của rượu lớn hơn. Một lần nữa, nếu chúng ta muốn giảm thiểu tỷ lệ tử vong, chúng ta KHÔNG nên khuyến nghị hạ độ tuổi uống rượu hợp pháp, bởi vì rõ ràng rượu có tác động đến tỷ lệ tử vong.

Trường hợp đơn giản này bao quát những gì xảy ra khi thiết kế hồi quy gián đoạn vận hành một cách hoàn hảo. Tiếp theo, chúng ta sẽ tìm hiểu một số chẩn đoán mà ta nên chạy để kiểm tra xem chúng ta có thể tin tưởng RDD đến mức nào và cùng nói về một chủ đề đã rất đỗi quen thuộc: tác động của giáo dục đối với thu nhập.

## Hiệu Ứng Da Cừu và RDD Mờ

Khi nói đến tác động của giáo dục đối với thu nhập, có hai quan điểm chính trong kinh tế học. Lập luận đầu tiên được biết đến rộng rãi rằng giáo dục làm tăng vốn nhân lưc, tăng năng suất và do đó, tăng thu nhập. Theo quan điểm này, giáo dục thực sự thay đổi bạn theo hướng tốt hơn. Một quan điểm khác cho rằng giáo dục chỉ đơn giản là một cơ chế phát tín hiệu. Nó chỉ đẩy bạn đến với các bài kiểm tra và bài tập khó nhằn, và nếu bạn có thể làm được chúng, nó phát tín hiệu cho thị trường rằng bạn là một nhân viên giỏi. Theo quan điểm này, giáo dục không làm cho bạn năng suất hơn. Nó chỉ cho thị trường biết bạn luôn làm việc hiệu quả như thế nào. Điều quan trọng ở đây là bằng tốt nghiệp. Nếu bạn có nó, bạn sẽ được trả lương cao hơn. Chúng ta gọi đây là **hiệu ứng da cừu**, bởi vì trước đây bằng cấp được in bằng da cừu.

Để kiểm định giả thuyết này, [Clark và Martorell](https://faculty.smu.edu/millimet/classes/eco7321/papers/clark%20martorell%202014.pdf) đã sử dụng hồi quy gián đoạn để đo lường tác động của việc tốt nghiệp lớp 12 đối với thu nhập. Để làm được điều đó, họ đã phải cân nhắc một số biến chỉ định, theo đó nếu học sinh vượt qua ngưỡng quyết định đó thì tốt nghiệp và ngược lại. Họ đã tìm thấy dữ liệu như vậy trong hệ thống giáo dục Texas.

Để tốt nghiệp ở Texas, học sinh phải vượt qua một kỳ thi. Các kỳ kiểm tra bắt đầu từ năm lớp 10 và học sinh có thể làm nhiều lần, nhưng cuối cùng, các bạn trẻ phải đối mặt với kỳ thi cuối vào cuối năm lớp 12. Ý tưởng là lấy dữ liệu từ những học sinh đã tham gia các kỳ thi cuối đó và so sánh những học sinh thi trượt cận điểm đỗ với những bạn thi đậu cận điểm đỗ. Những học sinh này sẽ có vốn nhân lực rất tương đồng, nhưng khác biệt về tín hiệu chứng chỉ. Cụ thể, những học sinh vừa đủ điểm đỗ, sẽ nhận được bằng tốt nghiệp.

```python
sheepskin = pd.read_csv("./data/sheepskin.csv")[["avgearnings", "minscore", "receivehsd", "n"]]
sheepskin.head()
```

      avgearnings
      minscore
      receivehsd
      n

      0
      11845.086
      -30.0
      0.416667
      12

      1
      9205.679
      -29.0
      0.387097
      31

      2
      8407.745
      -28.0
      0.318182
      44

      3
      11114.087
      -27.0
      0.377778
      45

      4
      10814.624
      -26.0
      0.306667
      75

Một lần nữa, dữ liệu được nhóm theo biến chỉ định. Nó không chỉ chứa biến chỉ định (`minscore`, với 0 là giá trị trung tâm) và kết quả (`avgearnings`), mà nó còn có xác suất nhận bằng tốt nghiệp theo ô điểm đó và kích thước của ô điểm (`n`). Vì vậy, ví dụ, trong số 12 học sinh có kết quả -30 dưới ngưỡng quyết định điểm, chỉ có 5 người có thể nhận được bằng tốt nghiệp.

Điều này có nghĩa là có sự giảm sút trong chỉ định can thiệp. Một số học sinh có số điểm dưới ngưỡng quyết định đỗ vẫn có thể xoay xở để lấy được bằng tốt nghiệp bằng mọi cách. Tại đây, hồi quy gián đoạn có dạng **mờ**, thay vì sắc . Lưu ý rằng xác suất nhận được bằng tốt nghiệp không tăng vọt từ 0 lên 1 tại ngưỡng quyết định mà nó nhảy từ khoảng 50% lên 90%.

```python
sheepskin.plot.scatter(x="minscore", y="receivehsd", figsize=(10,5))
plt.xlabel("Điểm thi so với Ngưỡng quyết định")
plt.ylabel("Tỉ lệ nhận bằng")
plt.title("Bài thi cuối cùng");
```

Chúng ta có thể nghĩ về RD mờ như một loại không tuân thủ. Việc vượt qua ngưỡng quyết định này sẽ khiến mọi học sinh nhận được bằng tốt nghiệp, nhưng một số học sinh, những người luôn không nhận, không nhận được bằng. Tương tự như vậy, những bạn có điểm dưới ngưỡng quyết định sẽ không nhận được bằng tốt nghiệp, nhưng một số học sinh, những nguời luôn nhận, vẫn xoay xở để lấy được bằng.

Cũng như kết quả tiềm năng, chúng ta có trạng thái can thiệp tiềm năng trong tình huống này. \\(T_1\\) là can thiệp mà mọi người nhận được nếu họ vượt quá ngưỡng quyết định. \\(T_0\\) là can thiệp mà mọi người nhận được nếu họ ở dưới ngưỡng quyết định. Như bạn đã thấy, chúng ta có thể xem **ngưỡng quyết định như một Biến Công Cụ**. Cũng giống như trong IV, yếu tố không tuân thủ làm cho tác động can thiệp mà chúng ta ước lượng bị thiên lệch về 0.

Xác suất để can thiệp nhỏ hơn 1, thậm chí ở trên ngưỡng quyết định, làm cho kết quả chúng ta quan sát được thấp hơn kết quả tiềm năng thực \\(Y_1\\). Tương tự, kết quả mà chúng ta quan sát dưới ngưỡng quyết định cao hơn kết quả tiềm năng thực \\(Y_0\\). Điều này làm cho tác động can thiệp tại ngưỡng quyết định có vẻ như nhỏ hơn so với thực tế và chúng tôi sẽ phải sử dụng kỹ thuật IV để điều chỉnh.

Cũng như trước đây chúng ta đặt giả thiết rằng kết quả tiềm năng sẽ liên tục, bây giờ chúng ta cũng làm tương tự cho can thiệp tiềm năng. Ngoài ra, chúng ta cần đặt giả thiết cho đơn điệu, giống như trong IV. Nó nói rằng \\(T_{i1}>T_{i0} \ \forall i\\). Điều này có nghĩa là việc vượt qua ngưỡng quyết định từ trái sang phải chỉ làm tăng khả năng nhận được bằng tốt nghiệp (hoặc không có người luôn không nhận). Với 2 giả thiết này, chúng ta có Mô Hình Ước Lượng Wald cho LATE.

$$
\dfrac{\lim_{r \to c^+} E[Y_i|R_i=r] - \lim_{r \to c^-} E[Y_i|R_i=r]}{\lim_{r \to c^+} E[T_i|R_i=r] - \lim_{r \to c^-} E[T_i|R_i=r]} = E[Y_{1i} - Y_{0i} | T_{1i} > T_{0i}, R_i=c]
$$

Lưu ý rằng đây là một ước lượng cục bộ theo hai nghĩa. Đầu tiên, nó là cục bộ vì nó chỉ cho thấy tác động can thiệp tại ngưỡng quyết định \\(c\\). Đây là RD cục bộ. Thứ hai, nó là cục bộ vì nó chỉ ước lượng tác động can thiệp cho người tuân thủ. Đây là IV cục bộ.

Để ước lượng, chúng ta sẽ sử dụng 2 hồi quy tuyến tính. Tử số có thể được ước lượng theo cách chúng ta đã làm trước đó. Để có được mẫu số, chúng ta chỉ cần thay kết quả bằng can thiệp. Nhưng trước tiên, hãy bàn về kiểm định giả thiết mà chúng ta cần chạy để đảm bảo rằng ước lượng RDD có thể tin tưởng được.

### McCrary Test

Một thứ có thể phá vỡ lập luận RDD của chúng ta là nếu mọi người chi phối vị trí của họ tại ngưỡng quyết định. Nó có thể xảy ra trong ví dụ về da cừu nếu học sinh có điểm số ngay dưới ngưỡng quyết định tìm ra cách để nâng điểm bài kiểm tra lên một chút. Một ví dụ khác là khi bạn cần có thu nhập dưới một mức nhất định để nhận được trợ cấp chính phủ. Một số gia đình có thể cố tình giảm thu nhập chỉ để đủ điều kiện tham gia gói trợ cấp.

Trong các tình huống này, chúng ta có xu hướng thấy một hiện tượng gọi là tập hợp mật độ của biến chỉ định. Nghĩa là chúng ta sẽ có rất nhiều đối tượng ngay trên hoặc ngay dưới ngưỡng quyết định. Để kiểm tra điều này, chúng ta có thể vẽ biểu đồ hàm mật độ của biến chỉ định và xem liệu có bất kỳ đột biến nào xung quanh ngưỡng quyết định hay không. Đối với trường hợp của chúng ta, mật độ được biểu thị bởi cột `n` trong dữ liệu.

```python
plt.figure(figsize=(8,8))

ax = plt.subplot(2,1,1)
sheepskin.plot.bar(x="minscore", y="n", ax=ax)
plt.title("Kiểm định McCrary")
plt.ylabel("Độ trơn tại ngưỡng quyết định")

ax = plt.subplot(2,1,2, sharex=ax)
sheepskin.replace({1877:1977, 1874:2277}).plot.bar(x="minscore", y="n", ax=ax)
plt.xlabel("Điểm thi so với ngưỡng quyết định")
plt.ylabel("Đột biến tại ngưỡng quyết định");
```

Biểu đồ đầu tiên cho thấy mật độ dữ liệu trông như thế nào. Như chúng ta có thể thấy, không có đột biến nào xung quanh ngưỡng quyết định, có nghĩa là không có hội tụ. Học sinh không thao túng điểm số của chúng tại ngưỡng quyết định. Chỉ nhằm mục đích minh họa, biểu đồ thứ hai cho thấy tập hợp sẽ như thế nào nếu học sinh có thể thao túng điểm số tại ngưỡng quyết định. Chúng ta sẽ thấy mật độ của các ô ngay trên ngưỡng quyết định tăng đột biến, vì nhiều học sinh có số điểm tại đó. Nhằm giải quyết vấn đề này, chúng ta có thể quay lại để ước lượng hiệu ứng da cừu.

Như tôi đã nói trước đây, tử số của Mô hình ước lượng wald có thể được ước lượng giống với cách ta đã làm trong RDD sắc . Tại đây, chúng ta sẽ sử dụng trọng số của kernel với dải tần suất là 15. Vì chúng ta cũng có kích thước của ô điểm, chúng ta sẽ nhân kernel với kích thước mẫu để có được trọng số cuối cùng cho ô điểm.

```python
sheepsking_rdd = sheepskin.assign(threshold=(sheepskin["minscore"]>0).astype(int))
model = smf.wls("avgearnings~minscore*threshold",
                sheepsking_rdd,
                weights=kernel(sheepsking_rdd["minscore"], c=0, h=15)*sheepsking_rdd["n"]).fit()

model.summary().tables[1]
```

           	             coef	     std err	      t	      P>|t|	  [0.025	    0.975]

  Intercept	           1.399e+04	    83.678	   167.181	  0.000	  1.38e+04	  1.42e+04

  minscore	             181.6636	    16.389	    11.084	  0.000	   148.588	   214.739

  threshold	            -97.7571	   145.723	    -0.671	  0.506	  -391.839	   196.325

  minscore:threshold	    18.1955	    30.311	     0.600	  0.552	   -42.975	    79.366

Nó cho chúng ta biết rằng tác động của bằng tốt nghiệp là -97,7571, nhưng con số này không mang ý nghĩa thống kê (trị số p là 0,5). Nếu chúng ta biểu diễn các kết quả, ta có một đường chạy liên tục qua ngưỡng quyết định. Những người có nhiều năm học vấn hơn thực sự kiếm được nhiều tiền hơn, nhưng không có đột biến nào ở thời điểm họ nhận được bằng tốt nghiệp lớp 12. Đây là một lập luận ủng hộ quan điểm rằng giáo dục tăng thu nhập bằng cách giúp cho mọi người làm việc hiệu quả hơn, bởi vì không có sự xuất hiện của hiệu ứng da cừu.

```python
ax = sheepskin.plot.scatter(x="minscore", y="avgearnings", color="C0")
sheepskin.assign(predictions=model.fittedvalues).plot(x="minscore", y="predictions", ax=ax, color="C1", figsize=(8,5))
plt.xlabel("Test Scores Relative to Cutoff")
plt.ylabel("Average Earnings")
plt.title("Last-chance Exams");
```

Tuy nhiên, như chúng ta đã biết từ cách hoạt động của thiên lệch bất tuân, kết quả này thiên lệch về 0. Để chỉnh sửa, chúng ta cần chia tỷ trọng ở giai đoạn đầu và dùng Mô hình ước lượng wald. Thật không may, Python không có sẵn gói công cụ nào đủ tốt để giải quyết vấn đề này, vì vậy chúng ta sẽ phải thực hiện chúng thủ công và sử dụng bootstrap để đo được sai số chuẩn.

Đoạn code dưới đây ước lượng tử số của Mô hình ước lượng wald giống như chúng ta đã làm trước đây và nó cũng xây dựng mẫu số bằng cách thay thế biến mục tiêu bằng biến can thiệp `receivehsd`. Bước cuối cùng chỉ cần chia tử số cho mẫu số.

```python
def wald_rdd(data):
    weights=kernel(data["minscore"], c=0, h=15)*data["n"]
    denominator = smf.wls("receivehsd~minscore*threshold", data, weights=weights).fit()
    numerator = smf.wls("avgearnings~minscore*threshold", data, weights=weights).fit()
    return numerator.params["threshold"]/denominator.params["threshold"]
```

```python
from joblib import Parallel, delayed

np.random.seed(45)
bootstrap_sample = 1000
ates = Parallel(n_jobs=4)(delayed(wald_rdd)(sheepsking_rdd.sample(frac=1, replace=True))
                          for _ in range(bootstrap_sample))
ates = np.array(ates)
```

Với các mẫu bootstrap, chúng ta có thể biểu diễn phân phối của các ATE và xem thử khoảng tin cậy 95% nằm ở đâu.

```python
sns.distplot(ates, kde=False)
plt.vlines(ates.mean()-1.96*ates.std(), 0, 100, linestyles="dotted")
plt.vlines(ates.mean()+1.96*ates.std(), 0, 100, linestyles="dotted", label="95% CI")
plt.title("ATE Bootstrap Distribution")
plt.xlim([-10000, 10000])
plt.legend();
```

Bạn thấy đấy, ngay cả khi chúng ta chia tỷ trọng tác động ngay giai đoạn đầu, nó vẫn không khác 0 về mặt thống kê. Điều này có nghĩa là giáo dục không làm tăng thu nhập đơn giản chỉ bởi một hiệu ứng da cừu đơn thuần, mà do nó giúp gia tăng năng suất của con người.

## Ý tưởng chủ đạo

Chúng ta đã học cách tận dụng sự gián đoạn nhân tạo để ước lượng tác động nhân quả. Ý tưởng là chúng ta sẽ có một số ngưỡng quyết định nhân tạo khiến xác suất của sự can thiệp nhảy vọt. Một ví dụ mà chúng ta đã thấy là độ tuổi làm cho xác suất uống rượu tăng vọt ở độ tuổi 21, chúng ta có thể sử dụng điều này để ước lượng tác động của việc uống rượu. Thực tế chỉ ra rằng khi ở gần với ngưỡng quyết định, chúng ta có một phương pháp gần với thử nghiệm ngẫu nhiên. Các đối tượng ở rất gần ngưỡng quyết định có thể đã đi theo một trong hai hướng và những gì có thể xác định nơi chúng thuộc về là ngẫu nhiên. Với điều này, chúng ta có thể so sánh những đối tượng cận trên và cận dưới ngưỡng quyết định để có được tác động can thiệp. Chúng ta đã biết cách làm điều đó với hồi quy tuyến tính theo tỷ trọng bằng kernel và nó thậm chí còn cung cấp sai số chuẩn cho ATE mà chúng ta không cần phải mất tí công sức nào.

Sau đó, chúng ta xem xét điều gì sẽ xảy ra trong thiết kế RD mờ, khi có sự xuất hiện của việc không tuân thủ. Chúng ta thấy rằng cách ta có thể tiếp cận tình huống cũng giống như cách chúng ta đã làm với IV.

## Tài liệu tham khảo

Tôi muốn dành loạt bài viết này để vinh danh Joshua Angrist, Alberto Abadie and Christopher Walters vì khóa học Kinh tế lượng tuyệt cú mèo của họ. Phần lớn ý tưởng trong loạt bài này được lấy từ các bài giảng của họ được tổ chức bởi Hiệp hội Kinh tế Mĩ.  Theo dõi các bài giảng này là những gì tôi làm trong suốt năm 2020 khó nhằn.
* [Kinh tế lượng với dữ liệu chéo](https://www.aeaweb.org/conference/cont-ed/2017-webcasts)
* [Luyện chưởng Kinh tế lượng Gần như Vô hại](https://www.aeaweb.org/conference/cont-ed/2020-webcasts)

Tôi cũng muốn giới thiệu cuốn sách lý thú của Angrist. Chúng cho tôi thấy Kinh tế lượng, hoặc 'Lượng theo cách họ gọi không chỉ vô cùng hữu ích mà còn rất vui.

* [Kinh tế lượng Gần như Vô hại](https://www.mostlyharmlesseconometrics.com/)
* [Luyện chưởng 'Lượng](https://www.masteringmetrics.com/)

Tài liệu tham khảo cuối cùng của tôi là cuốn sách của Miguel Hernan and Jamie Robins. Nó là người bạn đồng hành tin cậy với tôi khi trả lời những câu hỏi nhân quả khó nhằn.

* [Sách Suy Luận Nhân Quả](https://www.hsph.harvard.edu/miguel-hernan/causal-inference-book/)

# Bảng Từ Viết tắt
|Viết tắt| Tiếng Anh | Tiếng Việt |
| --- | --- | --- |
|IV|Instrumental Variable|Biến Công cụ|
|RD|Regression Discontinuity|Hồi quy Gián đoạn|
|RDD|Regression Discontinuity Design|Thiết kế Hồi quy Gián đoạn|

# Bảng Thuật ngữ
| Thuật ngữ | Tiếng Anh |
| --- | --- |
|biến can thiệp|treatment variable|
|biến công cụ|instrumental-variable, instrumental variable, instrument, instrument variable|
|biến giả|dummy, dummy variable|
|biến kết quả|outcome variable|
|bootstrap|bootstrap|
|can thiệp tiềm năng|potential treatment|
|chỉ định can thiệp|treatment assignment|
|code|code|
|dải tần suất|bandwidth|
|giá trị dự đoán|predicted value|
|giả thuyết|hypothesis|
|hiệu ứng da cừu|sheepskin effect|
|hàm gián đoạn|discontinuous function|
|hàm mật độ|density function|
|hàm tam giác kernel|triangular kernel|
|hàm tỷ trọng|weighting function|
|hệ số chặn|intercept|
|hồi quy|regression, regress|
|hồi quy gián đoạn|regression discontinuity|
|hồi quy tuyến tính|linear regression|
|kiểm định giả thiết|sanity check|
|kích thước mẫu|sample size|
|kết quả tiềm năng|potential outcome|
|mô hình hồi quy|regression model|
|mô hình ước lượng wald|wald estimator|
|mẫu|sample|
|mật độ|density|
|mật độ dữ liệu|data density|
|ngưỡng|threshold|
|phân phối|distribution|
|sai số chuẩn|standard error|
|tham số|parameter|
|tham số dải tần suất|bandwidth parameter|
|thiên lệch|bias|
|thiên lệch bất tuân|non compliance bias|
|thiết kế hồi quy gián đoạn|regression discontinuity design|
|thử nghiệm ngẫu nhiên|randomised trial|
|thử nghiệm ngẫu nhiên cục bộ|local randomized trial|
|trị số p|p-value|
|tác động can thiệp|treatment effect, treatment impact|
|tác động can thiệp bình quân cục bộ|local average treatment effect|
|xác suất|probability|
|điểm dữ liệu|data point|
|ước lượng cục bộ|local estimate|


# Molecular Dynamics

##### Syma Khalid

```
from IPython.core.display import HTML
css_file = '../ipython_notebook_styles/ngcmstyle.css'
HTML(open(css_file, "r").read())
```

<link href='http://fonts.googleapis.com/css?family=Open+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Arvo:400,700,400italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=PT+Mono' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Shadows+Into+Light' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Nixie+One' rel='stylesheet' type='text/css'>
<style>

@font-face {
    font-family: "Computer Modern";
    src: url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf');
}

#notebook_panel { /* main background */
    background: rgb(245,245,245);
}

div.cell { /* set cell width */
    width: 1000px;
}

div #notebook { /* centre the content */
    background: #fff; /* white background for content */
    width: 1200px;
    margin: auto;
    padding-left: 0em;
}

#notebook li { /* More space between bullet points */
margin-top:0.8em;
}

/* draw border around running cells */
div.cell.border-box-sizing.code_cell.running {
    border: 1px solid #111;
}

/* Put a solid color box around each cell and its output, visually linking them*/
div.cell.code_cell {
    background-color: rgb(256,256,256);
    border-radius: 0px;
    padding: 0.5em;
    margin-left:1em;
    margin-top: 1em;
}

div.text_cell_render{
    font-family: 'Open Sans' sans-serif;
    line-height: 140%;
    font-size: 125%;
    font-weight: 400;
    width:900px;
    margin-left:auto;
    margin-right:auto;
}

/* Formatting for header cells */
.text_cell_render h1 {
    font-family: 'Arvo', serif;
    font-style:regular;
    font-weight: 400;
    font-size: 45pt;
    line-height: 100%;
    color: rgb(0,51,102);
    margin-bottom: 0.5em;
    margin-top: 0.5em;
    display: block;
}
.text_cell_render h2 {
    font-family: 'Arvo', serif;
    font-weight: 400;
    font-size: 30pt;
    line-height: 100%;
    color: rgb(0,51,102);
    margin-bottom: 0.1em;
    margin-top: 0.3em;
    display: block;
}

.text_cell_render h3 {
    font-family: 'Arvo', serif;
    margin-top:16px;
	font-size: 22pt;
    font-weight: 600;
    margin-bottom: 3px;
    font-style: regular;
    color: rgb(102,102,0);
}

.text_cell_render h4 {    /*Use this for captions*/
    font-family: 'Arvo', serif;
    font-size: 14pt;
    text-align: center;
    margin-top: 0em;
    margin-bottom: 2em;
    font-style: regular;
}

.text_cell_render h5 {  /*Use this for small titles*/
    font-family: 'Arvo', sans-serif;
    font-weight: 400;
    font-size: 16pt;
    color: rgb(163,0,0);
    font-style: italic;
    margin-bottom: .1em;
    margin-top: 0.8em;
    display: block;
}

.text_cell_render h6 { /*use this for copyright note*/
    font-family: 'PT Mono', sans-serif;
    font-weight: 300;
    font-size: 9pt;
    line-height: 100%;
    color: grey;
    margin-bottom: 1px;
    margin-top: 1px;
}

.CodeMirror{
        font-family: "PT Mono";
        font-size: 90%;
}

</style>

## Velocity Verlet algorithm

Considers both the atomic positions and velocities at the same time.

Divide the procedure into three steps:

$$
\begin{align}
  v(t + \delta t / 2) & = v(t) = \frac{a(t) \, \delta t}{2}, \\
  r(t + \delta t) & = r(t) + v(t + \delta t / 2) \, \delta t \\
  v(t + \delta t) & = v(t + \delta t / 2) + \frac{a(t + \delta t) \, \delta t}{2}.
\end{align}
$$

Only set of positions, forces and velocities need to be carried at any one time.

##### Advantages

* Simple to code
* Relatively modest memory requirements
* Velocities explictly calculated, thus temperature is reliably calculated
* Energy well conserved.

The algorithm of choice for most popular MD codes.

## Temperature

Temperature specifies thermodynamic state - important concept in MD:

$$
\begin{equation}
  T(t) = \sum_{i=1}^N \frac{m_i v_i^2(t)}{K_B (3 N -3)}
\end{equation}
$$

$N$ is the number of particles and $K_B$ the Boltzmann constant.

## Timesteps

How to choose $\delta t$? Must strike balance between conserving energy and efficient simulation.

High frequency motions can be problematic; get around this by "freezing out" - constraining bonds to their equilibrium values.

For biomolecules $\delta t = 2$fs is often used.

## System setup

Need coordinates for the molecules to start.

Usual to get structure from x-ray crystallography or NMR (for biological systems).

Embed structure in an environment that mimics experimental conditions.

Set system up carefully, taking care to remove any "bad contacts" - eg, use energy miinimisation.

## Periodic Boundaries

Only small numbers of molecules in a cell (max 150k as of 2014). Molecules on the surface experience different forces to bulk molecules, so to avoid this use periodic boundaries.

If a particle leaves the central box one of its images will enter the box through the opposite face.

The number of particles in the central box (which is actually simulated) remains constant.

## System Equilibriation

The equivalent of a "well-stirred" solution in synthetic chemistry.

When doing a simulation in the **NPT** (Number of particles, Pressure, Temperature: these are *forced* to remain constant) ensemble, then we monitor the system volume (ie, the volume of the box).

Similarly, when using an **NVT** (Volume...) ensemble, we monitor the system pressure.

Often start by equilibriating in the NVT ensemble, then switching to the NPT ensemble.

Can study the systematic drift in the structure by monitoring the root-mean-square deviation

## Timescale limitations

One of the major limitations: timescale that can be simulated.

State-of-the-art: simulations max 1-10 $\mu$s of "real time".

Problem as conformation changes can take ms, s, to hours or longer.

Additional problem: encounters between two molecules can lead to changes on diffusion timescale of ms or longer.

## Enhanced Sampling Methods

Many different methods to tackle the timescale limitations. Focus on Metadynamics.

Assume system can be described by a few selected degrees of freedom, called *collective variables* (CV).

Sampling facilitated by addition of a history-dependent bias potential that acts on the CVs.

Calculate the location of the system in the space determined by the CVs, add a positive Gaussian to discourage the system from revisiting that location (as it has already been sampled).

How to choose CVs?

They are a function of the microscopic coordinates; not unique.

* Should distinguish between initial and final state and also describe key intermediates (but none of these are necessarilly known!).
* Should include all slow modes of the systesm.
* Should be limited in number.

Once full energy landscape sampled, the *free-energy* is a constant as a function of the CVs. Can construct the true free-energy by substracting the Gaussians added (big additional benefit).

How many Gaussians to add, and the width, are tuneable parameters (plenty of trial and error).

## Energy minimisation

Method employed to bring system to its (local) energy minimum. To find the minimum points

* map the complete *hypersurface* (potential energy wrt all possible parameters) and thereby find all possible values
* want approach that keeps computational cost realistic
* tyipcally a $3N-5$ or $3N-6$ dimensional problem.

Various algorithms that work.

* No single algorithm always works
* Algorithms "go downhill", locating the nearest minimum (not necessarily the *global* minimum).

Two general categories:

* Those that use derivative information
* Those that don't.

Focus on one example of a derivative method.

### First Derivative Method - Steepest Descent.

Function must be continuous and differentiable. First order methods use the gradients. Clearly related to the force $\vec{F} = -\nabla V$.

Imagine standing on top of a hill and looking for the steepest slope that takes you down.

* At each interation the search direction is taken as the negative gradient of the function.
* Negative goes downhill.
* How far to go? Use *line search*.

* Locate minimum along the specified direction.
* Bracket the minimum. Then use Bisection or similar to find the minimum.
* From this point, recalculate the search direction; must be perpendicular to the first.

**Advantages**:

* Quickly removes worst steric clashes
* Rapidly brings bond lengths and angles to ideal values

**Disadvantages**

* Inefficient in valleys and near the minimum

```

```

FEEG6016 Simulation and Modelling/2014/Molecular Dynamics Lecture 2.ipynb