library(tidyverse); library(MatchIt); library(cobalt); library(estimatr); library(causalsens)
data("lalonde.psid", package = "causalsens")
data("lalonde.exp", package = "causalsens")
# Experimental benchmark from lalonde.exp (the gold standard we want to recover)
fit_exp <- # ___ YOUR CODE: lm_robust(re78 ~ treat, data = lalonde.exp, se_type = "HC1")
# Persist a CSV so the Python tab can read the same sample
write.csv(lalonde.psid, "lalonde.csv", row.names = FALSE)
covars <- c("age", "education", "black", "hispanic", "married", "nodegree",
"re74", "re75", "u74", "u75")
formula_match <- as.formula(paste("treat ~", paste(covars, collapse = " + ")))
# (a) Naive OLS — observational diff-in-means with HC1 SE
fit_naive <- # ___ YOUR CODE
# (b) Adjusted OLS — include all 10 covariates linearly, HC1 SE
fit_adj <- # ___ YOUR CODE
# (c) Exact matching on the four binary covariates only (method = "exact")
m_exact <- # ___ YOUR CODE: matchit(treat ~ black + hispanic + married + nodegree, ...)
# (d) Mahalanobis 1:1 matching with replacement
m_maha <- # ___ YOUR CODE: matchit(formula_match, method = "nearest",
# distance = "mahalanobis", replace = TRUE)
# (e) Coarsened Exact Matching with default coarsening
m_cem <- # ___ YOUR CODE: matchit(formula_match, method = "cem")
# (f) PSM — 1:1, logistic distance, caliper 0.2 * SD(logit PS), with replacement
m_psm <- # ___ YOUR CODE
# For each matched object, estimate the ATT as the weighted mean-difference on the
# matched sample (re78 ~ treat, weights = weights). For the SE use matched-set
# cluster-robust SEs — cluster by `subclass` (without replacement), or by `subclass`
# and the reused `id` via get_matches() (with replacement) — NOT plain HC1. Assemble
# all six estimates into a single results tibble with method, ATT, SE, and 95% CI.
# Bonus — Route 1 (Abadie-Imbens): also compute the AI analytic SE via
# Matching::Match(..., BiasAdjust = TRUE) for Mahalanobis and PSM, and compare
# the two routes. (CEM is not in the `Matching` package.)
# Produce three Love plots (Mahalanobis, CEM, PSM) via cobalt::love.plot,
# annotated with the headline result for each method.9 Session 6: Observed Confounding
9.1 Lecture Slides
9.2 Overview
This session is the foundation of Part III: Identification Under (Un-)Observed Confounding. We move from S04–S05’s experimental setting — where the analyst controls treatment assignment — to observational data, where treatment \(D\) is correlated with everything that predicts \(Y\). The pivot rests on a single assumption: conditional independence, \((Y(0), Y(1)) \perp D \mid \mathbf{X}\), given an adjustment set \(\mathbf{X}\) that closes every backdoor path.
The session walks through three estimator families that exploit this assumption — matching, inverse probability weighting (IPW), and augmented IPW (AIPW) — culminating in the doubly robust property: AIPW is consistent if either the outcome model or the propensity model is correctly specified. We then pivot to the question every honest observational analyst must answer: how strong would an unobserved confounder have to be to overturn the estimate? This motivates the modern toolkit of Rosenbaum bounds, E-values (VanderWeele and Ding 2017), and sensemakr (Cinelli and Hazlett 2020). The PBL completes the loop with a six-step workflow on the Lalonde NSW dataset and a controlled DGP where the truth is known.
Three forward-pointers: S07 (instrumental variables, when conditional independence fails), S08 (double machine learning, when \(\mathbf{X}\) is high-dimensional), S09 (treatment effect heterogeneity, when \(\tau\) varies with \(\mathbf{X}\)).
9.3 Lecture Notes
9.3.1 Identification under observed confounding
9.3.1.1 The conditional independence assumption — five names for one idea
The defining assumption of this session is
\[(Y(0), Y(1)) \perp D \mid \mathbf{X}\]
read as “potential outcomes are independent of treatment, given the adjustment set \(\mathbf{X}\)”. It is known by five names across disciplines: conditional independence (statistics), unconfoundedness (econometrics), ignorability (Bayesian inference), conditional exchangeability (epidemiology), and selection-on-observables (labour economics / sociology). All five describe the same condition: every variable that drives both \(D\) and \(Y\) is in \(\mathbf{X}\).
In S04, randomization made this hold unconditionally — the experimenter picked \(D\) uniformly at random, so \(D \perp (Y(0), Y(1))\) by construction. In S06, the conditional version places the burden on the analyst’s domain knowledge: the DAG that defines “the right \(\mathbf{X}\)” is a theoretical claim, not a data product.
9.3.1.2 Backdoor adjustment and the “good and bad controls” taxonomy
By S02’s backdoor criterion, an adjustment set \(\mathbf{X}\) is sufficient if it blocks every backdoor path from \(D\) to \(Y\) and contains no descendants of \(D\). Cinelli, Forney, and Pearl (Cinelli, Forney, and Pearl 2024) enumerate eight canonical configurations, sorted into three families:
- Good controls (M1–M3): classic confounders, indirect ancestors, and precision variables. Including them reduces bias or variance.
- Bad controls (M4–M6): post-treatment variables, colliders on \(D\) and \(Y\), and pre-treatment colliders of two unmeasured confounders (“M-bias”). Including them creates bias.
- Neutral / risky (M7–M8): pure instruments, which amplify any remaining unobserved-confounder bias, and pre-treatment variables of unknown structure, where the default is to include unless M6/M7 is suspected.
The pedagogical point: “throw everything at the model” is wrong. The analyst’s job is to draw a DAG, derive an adjustment set, and justify the variables included.
9.3.1.3 Positivity — every unit must have a chance of either treatment
The second identification assumption is positivity (also called overlap or common support):
\[0 < P(D = 1 \mid \mathbf{X} = \mathbf{x}) < 1 \quad \text{for every } \mathbf{x} \in \text{supp}(\mathbf{X}).\]
If positivity fails for some \(\mathbf{x}\), any estimator extrapolates beyond the data at that point. Two responses: trim the sample to the overlap region (e.g., propensity score \(\in [0.05, 0.95]\), Crump et al. 2009), or redefine the estimand via overlap weights (Li, Morgan, and Zaslavsky 2018). Both are now standard.
9.3.1.4 Identification — three roads to the same ATE
Under conditional independence and positivity, the ATE is identified from observed data in three algebraically equivalent ways:
- Adjustment formula (a.k.a. the g-formula): \(\tau = \mathbb{E}_X\!\left[\mathbb{E}(Y \mid D=1, X) - \mathbb{E}(Y \mid D=0, X)\right]\) — motivates the outcome-regression and matching families.
- IPW: \(\tau = \mathbb{E}\!\left[\dfrac{D \, Y}{e(X)} - \dfrac{(1-D) Y}{1 - e(X)}\right]\) — motivates inverse-probability weighting.
- AIPW: the two combined with a residual correction — yields a doubly robust estimator (AIPW).
In finite samples, the three expressions diverge — and the divergence is informative about which assumption is wrong.
9.3.2 Matching
9.3.2.1 The matching idea — build a synthetic RCT
Matching reads the adjustment formula literally: for each treated unit at \(\mathbf{X}_i\), find its nearest untreated twin under some distance metric, then average the paired differences. The target is the ATT, \(\tau_{\text{ATT}} = \mathbb{E}\!\left[\mathbb{E}(Y \mid D=1, X) - \mathbb{E}(Y \mid D=0, X) \mid D=1\right]\) — the conditional contrast averaged over the treated distribution of \(X\). The sample estimator (1:1, no replacement) is
\[\hat\tau_{\text{ATT}} = \frac{1}{n_1}\sum_{i: D_i = 1} \bigl(Y_i - Y_{m(i)}\bigr)\]
where \(m(i)\) is the index of the matched untreated unit. Three design knobs define the estimator: the distance (exact / Mahalanobis / propensity / CEM-coarsened), the matching ratio (1:1 / 1:M / variable-M with caliper), and replacement (yes / no). For the ATE rather than the ATT, also match each untreated unit to its nearest treated twin and combine by arm shares: \(\hat\tau_{\text{ATE}} = (n_1/n)\hat\tau_{\text{ATT}} + (n_0/n)\hat\tau_{\text{ATU}}\). The bias–variance direction of each knob: a caliper (drop matches with distance \(> c\); rule of thumb \(c = 0.2 \times\) SD of the logit propensity score) and 1:M matching trade bias against variance, and matching with replacement lowers bias at the cost of variance. The modern default is 1:M (\(M = 4\)) with replacement and a caliper of \(0.2\), reporting the number of unique controls used and the share of treated units matched.
9.3.2.2 Exact, Mahalanobis, and CEM
- Exact matching pairs \(i, j\) only if \(\mathbf{X}_i = \mathbf{X}_j\). Zero bias from the matching step; impossible in practice for continuous or high-dimensional \(\mathbf{X}\) (curse of dimensionality).
- Mahalanobis distance \(d_M(\mathbf{X}_i, \mathbf{X}_j) = \sqrt{(\mathbf{X}_i - \mathbf{X}_j)^\top \hat\Sigma_X^{-1} (\mathbf{X}_i - \mathbf{X}_j)}\) is scale-invariant and decorrelates. Pairs are not exact but are “balanced”. Default for continuous and ordinal \(\mathbf{X}\).
- Coarsened Exact Matching (CEM) (Iacus, King, and Porro 2012) bins each continuous covariate into a small number of strata, then does exact matching on the bins. Treated units in bins without untreated units (and vice versa) are pruned. The Monotonic Imbalance Bounding (MIB) property: bias is bounded before the data are touched, by the coarsening width — the analyst sets the maximum imbalance up front rather than diagnosing it ex post.
CEM is the modern recommendation for matching when sample size permits the pruning. Its price is sample loss: aggressive coarsening prunes more units; the estimand effectively becomes the ATT on the overlap region.
9.3.2.3 Propensity score matching and its discontents
The propensity score \(e(\mathbf{X}) = P(D = 1 \mid \mathbf{X})\) is a one-dimensional summary of the adjustment set. By the Rosenbaum–Rubin (1983) balancing-score theorem (Rosenbaum and Rubin 1983), conditioning on \(e(\mathbf{X})\) achieves the same identification as conditioning on the full vector \(\mathbf{X}\). Pre-2019 textbooks present propensity score matching (PSM) as the canonical matching method: estimate \(\hat e\) by logistic regression, match treated and untreated on \(\hat e\), estimate the ATT.
King & Nielsen (2019) (King and Nielsen 2019) published the modern critique: PSM can increase imbalance relative to no matching, because matching on \(\hat e\) approximates random sampling within strata of \(\hat e\) — analogous to a completely-randomized RCT rather than a blocked one. CEM and Mahalanobis approximate a fully blocked design and produce sample-by-sample covariate balance. The operational consensus today: prefer covariate-space matching (CEM or Mahalanobis) over PSM, choosing between them by checking both balance and retained-sample representativeness — CEM can over-prune (as the Ex 1 Lalonde lab shows, where it retains only 51 of 185 treated and lands far from the experimental benchmark), in which case Mahalanobis is preferred; reserve the propensity score for weighting (IPW) and AIPW (AIPW).
9.3.2.4 Balance diagnostics and the Love plot
After matching, the analyst must verify that the matched sample is balanced on every covariate in \(\mathbf{X}\). The standard metric is the standardized mean difference (SMD): \(\text{SMD}_k = (\bar X_{k,1} - \bar X_{k,0}) / \sqrt{(s_{k,1}^2 + s_{k,0}^2)/2}\) for covariate \(k\). Rule of thumb: \(|\text{SMD}_k| < 0.10\) means balance achieved (Stuart 2010). The Love plot visualizes raw vs. matched SMDs for every covariate side by side — cobalt::love.plot() in R.
Additional metrics for the same plot: variance ratio \(s_{k,1}^2 / s_{k,0}^2 \in [0.5, 2]\); K-S statistic for distribution-level checks; L1 distance for multidimensional histogram comparison (CEM’s native metric).
9.3.2.5 Inference after matching
The point estimate is mechanical (average paired difference). Standard errors are not:
- The naive t-test treats matched pairs as i.i.d. — wrong (matching introduces dependence through shared controls).
- The naive bootstrap of the full matching procedure is not generally consistent for matching SEs [Abadie & Imbens 2008].
- Abadie–Imbens (2006) (Abadie and Imbens 2006) analytic SEs account for the matching procedure and handle matching with replacement; implemented in the
Matchingpackage (Matching::Match()).MatchItitself returns no SEs. - Regression on the matched sample with
weights, plus matched-set cluster-robust SEs — not plain HC1, which treats the matched sample as i.i.d. and is inconsistent for matching SEs (errs in either direction) [Abadie & Spiess 2022]. Cluster bysubclasswithout replacement; bysubclassand the reused-unitid(viaget_matches()) with replacement. (Adding the covariates to the regression additionally regresses out residual imbalance — a related, not identical, alternative to the Abadie–Imbens estimator.)
The two routes target the same estimand and a careful analyst reports both as a robustness check: they agree on the qualitative story but differ in the exact finite-sample numbers, with the Abadie–Imbens variance the more conservative (it carries the full matching + conditional-variance uncertainty). The Ex 1 lab computes both for the with-replacement methods (Mahalanobis, PSM).
9.3.3 Inverse Probability Weighting (IPW)
9.3.3.1 The Horvitz–Thompson theorem
Under conditional independence and positivity, the ATE satisfies
\[\tau = \mathbb{E}\!\left[\frac{D \, Y}{e(\mathbf{X})} - \frac{(1 - D) \, Y}{1 - e(\mathbf{X})}\right]\]
— the Horvitz–Thompson (HT) estimator (Hirano, Imbens, and Ridder 2003). The proof is two lines of iterated expectation + conditional independence. The sample estimator plugs in \(\hat e(\mathbf{X}_i)\):
\[\hat\tau^{\text{HT}} = \frac{1}{n}\sum_{i=1}^n \left[\frac{D_i Y_i}{\hat e(\mathbf{X}_i)} - \frac{(1-D_i) Y_i}{1 - \hat e(\mathbf{X}_i)}\right].\]
9.3.3.2 Hájek normalization and diagnostics
The HT estimator is unbiased but high-variance. The Hájek (normalized) IPW divides by the sum of weights within each arm:
\[\hat\tau^{\text{Hájek}} = \frac{\sum_i D_i Y_i / \hat e_i}{\sum_i D_i / \hat e_i} - \frac{\sum_i (1-D_i) Y_i / (1-\hat e_i)}{\sum_i (1-D_i) / (1-\hat e_i)}.\]
Hájek is biased \(O(1/n)\) but lower-variance and bounded by \([\min Y, \max Y]\). It is the default in every IPW software package.
Diagnostics: report the effective sample size \(\text{ESS} = (\sum w_i)^2 / \sum w_i^2\), the maximum weight, and the propensity-score histogram for the two arms. Trim units with \(\hat e \notin [\alpha, 1-\alpha]\) (Crump et al. 2009) or use overlap weights \(\propto e(1-e)\) (Li, Morgan, Zaslavsky 2018) for a smoother alternative.
9.3.3.3 Modern balancing weights — enforce balance, don’t diagnose it
The classical workflow fits a propensity model, then checks balance ex post. The modern alternative enforces balance as a constraint on the weights:
- Entropy balancing (Hainmueller 2012): minimum-KL-divergence weights subject to exact moment-balance constraints.
- Covariate balancing propensity score (CBPS) (Imai and Ratkovic 2014): fit \(\hat e\) under a balancing objective rather than maximum likelihood.
- Stable balancing weights (SBW) (Zubizarreta 2015): minimum-variance weights subject to user-tolerated imbalance.
Athey & Imbens (2017) (Athey and Imbens 2017) recommend these as the modern default for applied causal inference.
9.3.4 Augmented IPW (AIPW)
9.3.4.1 The AIPW estimator
The two ingredients are the outcome regression (OR) \(\mu_d(\mathbf{x}) = \mathbb{E}(Y \mid D = d, \mathbf{X} = \mathbf{x})\) and the propensity score \(e(\mathbf{x})\). The AIPW estimator (Robins, Rotnitzky, and Zhao 1994) is
\[\hat\tau_{\text{AIPW}} = \frac{1}{n}\sum_{i=1}^n \!\left[\hat\mu_1(\mathbf{X}_i) - \hat\mu_0(\mathbf{X}_i) + \frac{D_i (Y_i - \hat\mu_1(\mathbf{X}_i))}{\hat e(\mathbf{X}_i)} - \frac{(1-D_i)(Y_i - \hat\mu_0(\mathbf{X}_i))}{1 - \hat e(\mathbf{X}_i)}\right].\]
Two readings of the same formula: (a) the OR difference, augmented by an IPW correction on the residuals; (b) the IPW estimator, with a correction that removes its dependence on \(\hat e\) where the OR model fits.
9.3.4.2 Double robustness
\(\hat\tau_{\text{AIPW}} \xrightarrow{p} \tau\) if either \(\hat\mu_d \xrightarrow{p} \mu_d\) (OR correct) or \(\hat e \xrightarrow{p} e\) (PS correct). The bias decomposes as the product of the two model errors — small if either factor is small. AIPW is also semiparametrically efficient (Hahn 1998) when both are correctly specified, and asymptotically normal: a standard error follows from the influence-function variance \(\widehat{\text{Var}}(\hat\tau_{\text{AIPW}}) = \frac{1}{n^2}\sum_i (\hat\psi_i - \hat\tau_{\text{AIPW}})^2\), where \(\hat\psi_i\) is the per-unit AIPW score (the bracketed term in the estimator), or equivalently from the bootstrap — this is what the Exercise 3 confidence intervals use.
The failure mode: if both models are wrong and not coincidentally offsetting, AIPW is biased. Exercise 2 demonstrates this on a controlled DGP (true ATE = 2.0): double robustness rescues the estimate when only the OR (scenario B) or only the PS (scenario C) is misspecified, and fails when both are wrong (scenario D).
9.3.4.3 Forward-pointer to S08 — Double Machine Learning
Classical AIPW uses parametric nuisances (logistic PS + linear OR). Three modern moves: replace each with ML estimators, use cross-fitting (split-sample fit/evaluate to remove overfit bias), and use Neyman-orthogonal score functions for valid \(\sqrt{n}\)-CIs. Together = Double Machine Learning [Chernozhukov et al. 2018], the topic of S08.
9.3.5 Sensitivity analysis
Conditional independence is untestable from the data: by definition, the unobserved confounder is absent from the dataset. Sensitivity analysis flips the question from “is the assumption right?” to “how strong would the violation have to be?”
9.3.5.1 Rosenbaum bounds
For matching estimators, Rosenbaum (2002) (Rosenbaum 2002) parametrizes hidden bias on the odds-ratio scale: within a matched pair \((i, j)\) with \(\mathbf{X}_i = \mathbf{X}_j\), the odds ratio of treatment due to an unobserved \(U\) lies in \([1/\Gamma, \Gamma]\). The sensitivity output is the largest \(\Gamma\) at which the matched-pair test (Wilcoxon signed-rank, say) still rejects. \(\Gamma = 1\) is no hidden bias; \(\Gamma \geq 2\) is strong robustness; \(\Gamma \geq 3\) is highly robust.
9.3.5.2 E-values
VanderWeele & Ding (2017) (VanderWeele and Ding 2017) define the E-value as the minimum risk-ratio strength that an unobserved \(U\) would need to have with both \(D\) and \(Y\) to fully explain away an observed effect:
\[E = \widehat{\text{RR}} + \sqrt{\widehat{\text{RR}}\bigl(\widehat{\text{RR}} - 1\bigr)} \quad \text{for } \widehat{\text{RR}} > 1.\]
For continuous outcomes, convert via \(\widehat{\text{RR}} \approx \exp(0.91 \cdot d)\) where \(d\) is the standardized mean difference. The E-value’s appeal: no model required, computable in one line, interpretable on the risk-ratio scale.
9.3.5.3 sensemakr
Cinelli & Hazlett (2020) (Cinelli and Hazlett 2020) build a sensitivity framework for linear regression (and AIPW with linear OR) parametrized by partial \(R^2\). Two summary statistics:
- Robustness Value (RV): the value of \(R^2_{Y \sim U \mid D, X} = R^2_{D \sim U \mid X}\) at which the point estimate would be reduced to a threshold (default: zero).
- RV-α: the value at which the \(t\)-statistic no longer rejects at level \(\alpha\).
The headline innovation is benchmarking: compare RV against the partial \(R^2\) of named observed covariates (“\(U\) would have to be at least \(k\) times stronger than age”). This makes the abstract sensitivity number actionable.
9.3.5.4 The reporting standard
For any observational estimate, report: (1) point estimate + CI; (2) robustness value; (3) at least one benchmark covariate with bound multiplier; (4) ovb_contour_plot; (5) one paragraph on the assumed lower bound on \(U\)’s strength and the operational conclusion.
9.3.6 The observed-confounding workflow
The six-step pipeline:
- Articulate the DAG — derive an adjustment set; flag good/bad controls.
- Check overlap — propensity histogram; trim or use overlap weights.
- Choose estimator — CEM / Mahalanobis / IPW / AIPW.
- Verify balance — Love plot + SMD table for every covariate.
- Stress-test — Rosenbaum bounds + E-value + sensemakr.
- Report — DAG + estimand + estimator + balance + RV.
Lock steps 1–3 before unblinding outcome data. This is the operational discipline that distinguishes a defensible analysis from a fishing expedition.
Why locking matters — the garden of forking paths. An observational analysis has many defensible choices (adjustment set, matching method, caliper width, trimming threshold, outcome model, subsample); with \(K\) binary choices there are \(2^K\) analytical paths, and the best-looking one will be biased (Gelman & Loken 2014). Pre-registration lists each decision and its value before \(Y\) is unblinded. A minimal pre-analysis plan for S06 work: (1) the DAG with the adjustment set highlighted; (2) the estimator and nuisance models with explicit functional form; (3) the trimming/caliper rule; (4) the inference method (sandwich / bootstrap); (5) the sensitivity benchmarks (which observed covariates). Pre-registration is now required in many medical and social-science journals, becoming standard in tech-company observational analyses, and supported for observational designs on OSF and EGAP (the AEA RCT Registry remains RCT-only).
9.4 Coding Lab
Four exercises walk through the observed-confounding pipeline end-to-end. Total budget ≈ 135 minutes:
| # | Topic | Slide stack | Time |
|---|---|---|---|
| 1 | Matching family on Lalonde (six estimators, three Love plots) | Matching | ~40 min |
| 2 | IPW: estimation, diagnostics, and balancing weights | IPW | ~40 min |
| 3 | AIPW and double robustness (four-scenario 2×2) | AIPW | ~30 min |
| 4 | Sensitivity analysis on the AIPW estimate | Sensitivity | ~25 min |
Exercise 1 runs six matching estimators side-by-side on Lalonde NSW, reproduces King–Nielsen (2019) in miniature, and asks which estimator recovers the experimental benchmark. Exercise 2 walks the full IPW workflow on a controlled DGP (known true ATE = 2.0): estimate (Horvitz–Thompson and Hájek), check balance on the weighted sample, then stress the DGP until positivity nearly fails and apply three remedies (trimming, overlap weights, entropy balancing) — contrasting fit-then-check IPW against enforce-by-design balancing weights. Exercise 3 reuses that DGP to demonstrate AIPW and the doubly-robust property across a four-scenario (OR × PS correctness) design, and shows how a misspecified propensity model is caught by the balance check. Exercise 4 runs sensitivity analysis on the Exercise 3 AIPW estimate after withholding the true confounder, using both E-values and sensemakr.
Each exercise has solutions in collapsed code blocks (both R and Python). Open the relevant panel-tabset, work on the exercise, then expand the solution to compare.
9.4.1 Exercise 1 — Matching family on Lalonde (~40 min)
The Lalonde National Supported Work (NSW) demonstration (1976–1977) is the canonical matching benchmark (Dehejia and Wahba 2002). The experimental treatment was a job-training program; the outcome is real earnings in 1978. We use two paired datasets shipped together in the causalsens R package:
lalonde.exp— the experimental sample (185 treated + 260 NSW controls). The unbiased ATT here is the gold-standard benchmark: \(\hat\tau_{\text{exp}} = \$1{,}794\) (HC1 SE \(\approx \$671\)).lalonde.psid— the observational variant. The 185 NSW treated units are paired with 2,490 full PSID controls — a sample chosen specifically because it does not match the treated population on covariates. PSID respondents are a representative slice of the US workforce; NSW participants are disadvantaged job-training enrollees. The naive earnings gap is therefore enormous and negative.
The full-PSID control sample is what makes the demonstration vivid: matching methods have $17,000 of selection bias to close, and the failure modes of each method become clearly visible.
Task: estimate the ATT on the observational lalonde.psid data using six methods, compute balance diagnostics on each, and reflect on which method converges to the experimental benchmark of $1,794.
9.4.1.1 Scaffold
import numpy as np, pandas as pd
import statsmodels.formula.api as smf
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from scipy.spatial import cKDTree
# Read the same lalonde.psid data the R tab wrote out
lalonde = pd.read_csv("lalonde.csv")
X_cols = ["age", "education", "black", "hispanic", "married", "nodegree",
"re74", "re75", "u74", "u75"]
# (a) Naive OLS
m_naive = # ___ YOUR CODE: smf.ols("re78 ~ treat", data=lalonde).fit(cov_type="HC1")
# (b) Adjusted OLS — include all 10 covariates linearly
m_adj = # ___ YOUR CODE
# (f) Hand-rolled 1:1 PSM:
# 1. StandardScaler on X (binary + dollar-scale features mix badly)
# 2. LogisticRegression(treat ~ X) → predict propensity ps
# 3. logit_ps = log(ps / (1 - ps))
# 4. scipy.spatial.cKDTree on untreated logit_ps; query for each treated unit's nearest neighbour
# 5. ATT = mean(Y[treated] - Y[matched_untreated])
# ___ YOUR CODE: implement steps 1–5; assemble a 3-row results DataFrame
# (Naive, Adjusted, hand-rolled PSM) with method, ATT, SE.9.4.2 Exercise 2 — IPW: estimation, diagnostics, and balancing weights (~40 min)
The propensity-score weighting workflow has three moves, in order: estimate the effect (Horvitz–Thompson and Hájek IPW), check balance on the weighted sample (did the weights actually equalize the covariates?), and — when standard IPW turns out fragile — reach for the modern balancing-weight alternatives. We run all three on a controlled DGP with a known true ATE = 2.0, then stress the DGP until positivity nearly fails and compare three remedies.
9.4.2.1 Scaffold
set.seed(20260601L)
n <- 5000
# Controlled DGP: two true confounders (X1, X2), noise covariate (X3),
# post-treatment M (used in Ex 3). True ATE = 2.0.
df <- tibble(
X1 = rnorm(n), X2 = rnorm(n), X3 = rnorm(n),
D = rbinom(n, 1, plogis(-0.5 + 0.8 * X1 - 0.6 * X2)),
Y = 1.0 + 2.0 * D + 1.5 * X1 - 1.0 * X2 + 0.5 * D * X1 + rnorm(n, sd = 1.2),
M = D + 0.3 * X1 + rnorm(n, sd = 0.3)
)
# (1) ESTIMATE — fit the correct PS, then Horvitz-Thompson and Hájek IPW.
e_hat <- # ___ YOUR CODE: predict(glm(D ~ X1 + X2, df, family = binomial()), type="response")
ht <- # ___ YOUR CODE: mean(D*Y/e_hat) - mean((1-D)*Y/(1-e_hat))
hajek <- # ___ YOUR CODE: self-normalized Hájek form
# (2) CHECK BALANCE — did the weights equalize X1, X2? The weighted Love plot.
# w <- WeightIt::weightit(D ~ X1 + X2 + X3, data = df, estimand = "ATE")
# cobalt::love.plot(w, stats = "mean.diffs", abs = TRUE, thresholds = c(m = 0.1))
# (3) DIAGNOSE under stress — rebuild with steep selection so positivity nearly fails.
# df_stress: D ~ plogis(-0.5 + 2.4*X1 - 1.8*X2). Compute ESS/n, max(w),
# out-of-[0.05,0.95] count; plot the mirrored PS histogram.
# (4) REMEDIES — standard Hájek, trim, overlap weights (ATO), entropy balancing.
# Then a weighted Love plot contrasting raw vs IPW vs entropy-balanced.import numpy as np, pandas as pd
import statsmodels.api as sm, statsmodels.formula.api as smf
from scipy.optimize import minimize
from scipy.special import logsumexp
rng = np.random.default_rng(20260601)
n = 5000
X1, X2, X3 = rng.standard_normal(n), rng.standard_normal(n), rng.standard_normal(n)
D = rng.binomial(1, 1 / (1 + np.exp(-(-0.5 + 0.8 * X1 - 0.6 * X2))))
Y = (1.0 + 2.0 * D + 1.5 * X1 - 1.0 * X2 + 0.5 * D * X1
+ rng.standard_normal(n) * 1.2)
M = D + 0.3 * X1 + rng.standard_normal(n) * 0.3
df = pd.DataFrame({"X1": X1, "X2": X2, "X3": X3, "D": D, "Y": Y, "M": M})
# (1) ESTIMATE — HT and Hájek IPW with the correct PS.
# (2) CHECK BALANCE — weighted SMD of X1, X2, X3 (treated vs control) under IPW weights.
# (3) DIAGNOSE under stress — ESS/n, max(w), out-of-range count.
# (4) REMEDIES — Hájek, trim, overlap weights, hand-rolled entropy balancing; then
# compare weighted SMDs raw vs IPW vs entropy-balanced.9.4.3 Exercise 3 — AIPW and double robustness (~30 min)
We reuse the controlled DGP from Exercise 2 (true ATE = 2.0) to see the doubly-robust property cleanly: AIPW augments the IPW estimator with an outcome-regression correction, and is consistent if either the propensity model or the outcome model is right. We compare three estimators (IPW, OR-alone, AIPW) across four DGP-specified scenarios for model correctness.
9.4.3.1 Scaffold
# Reuses `df` from Exercise 2. Helper: fit PS via glm, fit two outcome
# regressions (D=1, D=0), and return Horvitz-Thompson, Hájek, and AIPW.
ipw_aipw <- function(df, ps_form, or_form) {
ps_fit <- # ___ YOUR CODE: glm(ps_form, data = df, family = binomial())
e_hat <- # ___ YOUR CODE: predict(ps_fit, type = "response")
keep <- # ___ YOUR CODE: trim e_hat to [0.02, 0.98]
d <- df[keep, ]; e_hat <- e_hat[keep]; D <- d$D; Y <- d$Y
ht <- # ___ YOUR CODE
hj <- # ___ YOUR CODE: self-normalized Hájek form
or1 <- # ___ YOUR CODE: lm(or_form, data = d[d$D == 1, ])
or0 <- # ___ YOUR CODE
mu1 <- predict(or1, newdata = d); mu0 <- predict(or0, newdata = d)
aipw <- # ___ YOUR CODE: mean(mu1 - mu0 + D*(Y-mu1)/e_hat - (1-D)*(Y-mu0)/(1-e_hat))
tibble(HT = ht, Hajek = hj, AIPW = aipw, n_kept = sum(keep))
}
# Run the four scenarios with:
# ps_correct = D ~ X1 + X2; ps_wrong = D ~ X3;
# or_correct = Y ~ X1 + X2 + I(X1^2); or_wrong = Y ~ X3.
# Then: a per-scenario balance check (weighted SMD) — confirm balance FAILS when
# the PS is misspecified; and a PS-overlap histogram on the correct specification.# Reuses `df` from Exercise 2.
def ipw_aipw(df, ps_form, or_form):
ps_fit = # ___ YOUR CODE
e_hat = # ___ YOUR CODE
keep = # ___ YOUR CODE: trim at (0.02, 0.98)
d, e_hat = df[keep].reset_index(drop=True), e_hat[keep].reset_index(drop=True)
D, Y = d["D"].values, d["Y"].values
ht = # ___ YOUR CODE
hj = # ___ YOUR CODE
or1 = # ___ YOUR CODE: smf.ols(or_form, data=d[d.D == 1]).fit()
or0 = # ___ YOUR CODE
mu1, mu0 = or1.predict(d), or0.predict(d)
aipw = # ___ YOUR CODE
return {"HT": ht, "Hajek": hj, "AIPW": aipw, "n_kept": int(keep.sum())}
# Build the 4-scenario table; then the per-scenario weighted-SMD balance check.9.4.4 Exercise 4 — Sensitivity analysis on the AIPW estimate (~25 min)
We now play the role of an analyst who built the AIPW estimate in Exercise 3 but did not have access to one of the true confounders (\(X_2\)). We compute two sensitivity quantities — the E-value and the sensemakr robustness value — and judge whether the analysis is robust. (Rosenbaum bounds apply to matched-pair estimators, not to this AIPW/OLS estimate.)
9.4.4.1 Scaffold
# (a) Restricted-adjustment AIPW on the controlled DGP (df, from Ex 2), withholding X2.
ps_restricted <- # ___ YOUR CODE: D ~ X1
or_restricted <- # ___ YOUR CODE: Y ~ X1
restricted <- # ___ YOUR CODE: ipw_aipw(df, ps_restricted, or_restricted)
# Print the restricted-AIPW point estimate.
# (b) Linear OLS for sensemakr (which needs an lm fit).
fit_for_sens <- # ___ YOUR CODE: lm(Y ~ D + X1, data = df)
# (c) sensemakr: robustness value + benchmark against X1 at kd = 1, 2, 3.
# Produce summary() and ovb_contour_plot().
library(sensemakr)
s <- # ___ YOUR CODE: sensemakr(fit_for_sens, treatment = "D", benchmark_covariates = "X1", kd = c(1, 2, 3))
# summary(s); ovb_contour_plot(s, sensitivity.of = "estimate")
# (d) E-value (VanderWeele-Ding 2017) via 0.91 * d_smd approximation.
library(EValue)
beta_hat <- coef(fit_for_sens)["D"]
sd_resid <- sigma(fit_for_sens)
ci <- confint(fit_for_sens)["D", ]
d_smd <- # ___ YOUR CODE: beta_hat / sd_resid
rr_hat <- # ___ YOUR CODE: exp(0.91 * d_smd)
# Compute lower / upper RR bounds analogously; call evalues.RR() and print.import numpy as np
import statsmodels.formula.api as smf
# Restricted-adjustment OLS (no X2)
fit_for_sens = # ___ YOUR CODE: smf.ols("Y ~ D + X1", data=df).fit()
# Restricted-adjustment AIPW (reuse the ipw_aipw helper from Ex 3 and df from Ex 2)
restricted = # ___ YOUR CODE: ipw_aipw(df, "D ~ X1", "Y ~ X1")
# E-value via the 0.91 * d approximation (no package needed)
beta_hat = # ___ YOUR CODE: fit_for_sens.params["D"]
sd_resid = # ___ YOUR CODE: np.sqrt(fit_for_sens.scale)
rr_hat = # ___ YOUR CODE: np.exp(0.91 * beta_hat / sd_resid)
# e_value(rr) = rr + sqrt(rr * (rr - 1)) for rr >= 1
# PySensemakr port for the robustness value + contour (pip install PySensemakr)
try:
import sensemakr as smkr
s = # ___ YOUR CODE: smkr.Sensemakr(model=fit_for_sens, treatment="D",
# benchmark_covariates=["X1"], kd=[1, 2, 3])
# print(s.summary()); s.plot(plot_type="contour", sensitivity_of="estimate")
except ImportError:
print("[PySensemakr not available; read the R sensemakr output instead]")9.5 References & Resources
9.5.1 Academic literature
The full curated literature menu — 24 academic refs + 9 practitioner resources, every entry DOI-verified on 2026-05-20 — lives at literature/discovered/06_observed_confounding.md. Highlights below.
Foundational
- Rosenbaum & Rubin (1983), Biometrika — the central role of the propensity score (Rosenbaum and Rubin 1983)
- Dehejia & Wahba (2002), REStat — the canonical Lalonde reanalysis (Dehejia and Wahba 2002)
- Imbens (2004), REStat — review of nonparametric ATE under exogeneity
Matching
- Abadie & Imbens (2006), Econometrica — matching SEs (Abadie and Imbens 2006)
- Stuart (2010), Statistical Science — assignable single-paper overview (Stuart 2010)
- Iacus, King, Porro (2012), Political Analysis — CEM (Iacus, King, and Porro 2012)
- King & Nielsen (2019), Political Analysis — the PSM critique (King and Nielsen 2019)
Weighting
- Hirano, Imbens, Ridder (2003), Econometrica — IPW efficiency (Hirano, Imbens, and Ridder 2003)
- Hainmueller (2012), Political Analysis — entropy balancing (Hainmueller 2012)
- Imai & Ratkovic (2014), JRSS-B — CBPS (Imai and Ratkovic 2014)
- Zubizarreta (2015), JASA — stable balancing weights (Zubizarreta 2015)
AIPW and double robustness
- Robins, Rotnitzky, Zhao (1994), JASA — AIPW origin (Robins, Rotnitzky, and Zhao 1994)
Sensitivity
- Rosenbaum (2002), Springer — Observational Studies (2e), Rosenbaum bounds (Rosenbaum 2002)
- VanderWeele & Ding (2017), Annals of Internal Medicine — E-values (VanderWeele and Ding 2017)
- Cinelli & Hazlett (2020), JRSS-B — sensemakr (Cinelli and Hazlett 2020)
Reviews and overlap
- Athey & Imbens (2017), JEP — modern applied causal inference (Athey and Imbens 2017)
- Crump, Hotz, Imbens, Mitnik (2009), Biometrika — overlap trimming (Crump et al. 2009)
- Li, Morgan, Zaslavsky (2018), JASA — overlap weights (Li, Morgan, and Zaslavsky 2018)
- Cinelli, Forney, Pearl (2024), Sociological Methods & Research — good and bad controls (Cinelli, Forney, and Pearl 2024)
9.5.2 Practitioner resources
- Hernán & Robins, Causal Inference: What If — free PDF, chapters 12–15 cover observed confounding and IPW.
- Facure, Causal Inference for the Brave and True — practical PSM/IPW chapters with Python code.
MatchItvignette: https://kosukeimai.github.io/MatchIt/articles/MatchIt.htmlWeightItvignette: https://ngreifer.github.io/WeightIt/cobaltvignette: https://ngreifer.github.io/cobalt/sensemakrR vignette: https://carloscinelli.com/sensemakr/articles/sensemakr.html- PySensemakr quickstart: https://pysensemakr.readthedocs.io/en/latest/notebooks/quickstart.html
- DoWhy end-to-end pipeline: https://www.pywhy.org/dowhy/
- EconML observational estimators: https://econml.azurewebsites.net/
- DoubleML observational + DML: https://docs.doubleml.org/stable/index.html
9.5.3 Bilingual package summary
| Topic | R packages | Python packages |
|---|---|---|
| Matching | MatchIt, Matching, optmatch, cem |
psmpy, causalinference, dowhy |
| Balance diagnostics | cobalt, MatchIt |
dowhy refute, causallib |
| IPW / weighting | WeightIt, CBPS, ebal, survey, sbw |
dowhy, causallib, econml |
| AIPW / DR | drgee, AIPW, WeightIt (with OLS) |
econml.dr.LinearDRLearner, doubleml |
| Sensitivity | sensemakr, EValue, rbounds, sensitivitymv |
PySensemakr |










