Act I
A lottery or a coin flip decides who should be treated — that is the clean experiment of Chapter 1.
But police officers override their orders, patients skip their pills, and lottery winners never enroll. What is left of the experiment?
Whoever decides to deviate from the plan does so for a reason — and that reason may also drive the outcome.
The random nudge — a lottery win, an assignment form.
What people actually received — the action taken, the school attended.
What we care about — repeat violence, test scores, years of schooling.
IV uses the randomness in \(Z\) to learn how \(D\) affects \(Y\).
The method is built from the left column; its interpretation depends on the right.
Act II
Non-compliance is the gap between the two columns — and only the left one is guaranteed random.
| Assigned response | What the officer should do | Cases |
|---|---|---|
| Arrest | Take the suspect into custody | 92 |
| Advise | Counsel or mediate the couple | 108 |
| Separate | Send the suspect away for 8 hours | 114 |
The goal: learn which response best prevents future domestic violence.
| Form said | Arrest | Advise | Separate | Obeyed |
|---|---|---|---|---|
| Arrest | 91 | 0 | 1 | 98.9% |
| Advise | 19 | 84 | 5 | 77.8% |
| Separate | 26 | 5 | 83 | 72.8% |
Rows: the randomly assigned response · columns: what the officer actually did.
When officers deviated, they almost always deviated toward arrest.
pd.crosstab shows who followed the formimport pandas as pd # pandas → load and tabulate
# every course dataset lives in the repo's data/ folder on GitHub
GITHUB_DATA_URL = "https://raw.githubusercontent.com/cmg777/intro2causal/main/data/"
mdve = pd.read_csv(GITHUB_DATA_URL + "ch3/mdve_clean.csv") # 314 MDVE cases
# rows = what the form said, columns = what officers did; shares within each row
pd.crosstab(mdve["assigned"], mdve["delivered"], normalize="index").round(3)We see the first domino fall and the last one fall; IV infers the push in between.
\[\phi = E[D_i \mid Z_i = 1] - E[D_i \mid Z_i = 0]\]
\[\rho = E[Y_i \mid Z_i = 1] - E[Y_i \mid Z_i = 0]\]
\(\phi\): how far the nudge moves treatment. \(\rho\): how far it moves the outcome. Both are clean, because \(Z\) is random.
Both comparisons inherit the experiment’s randomization — neither uses the treatment people chose.
\[\lambda_{LATE} = \frac{\rho}{\phi} = \frac{\text{effect of } Z \text{ on } Y}{\text{effect of } Z \text{ on } D}\]
If the nudge moves only part of the group, its effect on \(Y\) is diluted by the same fraction — so we scale it back up.
LATE = reduced form ÷ first stage: the Wald ratio.
Instruments are “external factors” that move one part of a system while leaving the rest alone.
Two of the three conditions leave fingerprints we can inspect.
No data test can rule out a side channel — exclusion must be argued, not proven.
| Type | If \(Z = 0\) | If \(Z = 1\) | What IV learns about them |
|---|---|---|---|
| Complier | untreated | treated | Their effect = LATE |
| Always-taker | treated | treated | Nothing (unmoved) |
| Never-taker | untreated | untreated | Nothing (unmoved) |
| Defier | treated | untreated | Ruled out by assumption |
Only people whose treatment responds to the instrument contribute to the IV estimate.
| Crossover direction | Cases | Rate |
|---|---|---|
| Told to coddle → arrested | 45 of 222 | 20.3% |
| Told to arrest → coddled | 1 of 92 | 1.1% |
Officers bent the rules toward arrest, almost never away from it — consistent with no defiers.
Before generalizing an IV estimate, ask: who are the compliers here?
| Variable | Code name | Equals 1 when… |
|---|---|---|
| Instrument \(Z\) | z_coddle |
form said advise/separate |
| Treatment \(D\) | d_coddle |
officer advised/separated |
| Outcome \(Y\) | (published) | repeat violence occurred |
One binary instrument and one binary treatment fit the standard IV formula.
| Share actually coddled | |
|---|---|
| Form said coddle (\(Z = 1\)) | 0.797 |
| Form said arrest (\(Z = 0\)) | 0.011 |
| First stage \(\phi\) (difference) | 0.786 |
A first stage this large means the random form had real power over what officers did.
pf.feols call estimates the first stage and its strength!pip install -q pyfixest # pyfixest is not preinstalled in Colab
import pandas as pd # pandas → load tables
import pyfixest as pf # pyfixest → regressions with robust SEs
GITHUB_DATA_URL = "https://raw.githubusercontent.com/cmg777/intro2causal/main/data/"
mdve = pd.read_csv(GITHUB_DATA_URL + "ch3/mdve_clean.csv") # 314 MDVE cases
mdve["z_coddle"] = (mdve["assigned"] != "Arrest").astype(int) # Z: form said coddle
mdve["d_coddle"] = (mdve["delivered"] != "Arrest").astype(int) # D: officer coddled
fs = pf.feols("d_coddle ~ z_coddle", data=mdve, vcov="hetero") # first stage, robust SEs
fs.summary() # z_coddle 0.786 (SE 0.029), t ≈ 27 → F = t² ≈ 727| Step | Value |
|---|---|
| First stage \(\phi\) (from the data) | 0.786 |
| Reduced form \(\rho\) (published rates: 0.211 − 0.097) | 0.114 |
| LATE \(= \rho / \phi\) | 0.145 |
| Naive gap by treatment received | 0.087 |
IV finds a bigger harm from coddling than the naive 8.7-point gap — selection bias was hiding it.
import pandas as pd # pandas → load tables
# every course dataset lives in the repo's data/ folder on GitHub
GITHUB_DATA_URL = "https://raw.githubusercontent.com/cmg777/intro2causal/main/data/"
mdve = pd.read_csv(GITHUB_DATA_URL + "ch3/mdve_clean.csv") # 314 MDVE cases
z = mdve["assigned"] != "Arrest" # Z: form said coddle (True/False)
d = mdve["delivered"] != "Arrest" # D: officer actually coddled
phi = d[z].mean() - d[~z].mean() # first stage: 0.797 − 0.011 = 0.786
rho = 0.211 - 0.097 # reduced form: published repeat-violence rates
print(rho / phi) # LATE ≈ 0.145 → +14.5 points for compliersValidity check: lottery winners and losers looked alike at baseline — the balance check passes.
A school lottery is an experiment with built-in non-compliance: some winners decline, some losers get in later.
| IV component | Estimate |
|---|---|
| First stage: lottery win → attended KIPP | 0.741 |
| Reduced form: lottery win → math score | +0.355 SD |
| LATE: attending KIPP → math score | +0.48 SD |
Dividing 0.355 by 0.741 converts the effect of winning into the effect of attending.
+0.48 SD
in math — about half a standard deviation in a single year
The naive slope may just be telling us about the parents, not about family size.
Both instruments move family size for reasons unrelated to how educated the parents are.
| Estimate | Effect of one more sibling on schooling |
|---|---|
| OLS (naive) | −0.15 years |
| Reduced form, each instrument | about zero |
| 2SLS, both instruments | +0.24 (SE 0.13) |
Once family size changes by chance, the education penalty disappears — the OLS slope was selection bias.
Stage 1 — predict treatment from the instrument:
\[D_i = \alpha_1 + \phi Z_i + \gamma_1 X_i + e_{1i}\]
Stage 2 — regress the outcome on the predicted treatment:
\[Y_i = \alpha_2 + \lambda_{2SLS} \hat{D}_i + \gamma_2 X_i + e_{2i}\]
\(\hat{D}_i\) keeps only the part of treatment driven by the instrument; \(X_i\) are optional controls.
With one binary instrument and no controls, 2SLS reproduces the Wald ratio exactly.
feols calls by hand: right \(\hat\lambda\), wrong SEAlways run 2SLS as one IV call: pf.feols("y ~ 1 | d ~ z").
pf.feols runs 2SLS in one formula and beats OLS on confounded data!pip install -q pyfixest # pyfixest is not preinstalled in Colab
import numpy as np, pandas as pd, pyfixest as pf # simulate · tabulate · regress
rng, n = np.random.default_rng(42), 10_000 # fixed seed → same numbers every run
df = pd.DataFrame({"u": rng.normal(size=n), # u: hidden confounder (motivation)
"z": rng.integers(0, 2, size=n)}) # z: coin-flip instrument (a lottery)
df["d"] = (1.5*df.z + df.u + rng.normal(size=n) > 0.75).astype(int) # d: nudged by z, chosen on u
df["y"] = 0.5*df.d + df.u + rng.normal(size=n) # y: the TRUE effect of d is 0.5
print(pf.feols("y ~ d", data=df, vcov="hetero").coef()["d"]) # OLS ≈ 1.48: biased by u
print(pf.feols("y ~ 1 | d ~ z", data=df, vcov="hetero").coef()["d"]) # 2SLS ≈ 0.48: near the truthMDVE first stage: \(t \approx 27\), so \(F = t^2 \approx 727\) — nowhere near weak.
Check the first stage before trusting any IV estimate.
| RCT (Chapter 1) | OLS with controls | IV / 2SLS | |
|---|---|---|---|
| Needs | Random assignment of treatment | Confounders all observed | A valid instrument |
| Handles | All confounders | Only observed confounders | Unobserved confounders |
| Estimates | ATE (everyone) | Conditional association | LATE (compliers) |
| Weakness | Costly, often impractical | Fails with hidden confounders | Needs a strong, valid instrument |
IV trades breadth for credibility: a clean answer, but only for the compliers.
Act III
Objection. The lottery is random, so the IV estimate is the average effect of treatment in the whole population.
Response. IV only sees people whose treatment the instrument changed. KIPP’s +0.48 SD is the effect for lottery compliers; always-takers and never-takers may respond quite differently.
Objection. The first stage is small, but we have millions of observations — precision will take care of it.
Response. Weak-instrument bias does not shrink as the sample grows. With a first-stage \(F\) below 10, 2SLS can sit close to the biased OLS answer. The cure is a stronger instrument.
pf.feols) and check the first-stage F