Skip to content

Commit 3eefa71

Browse files
docs(guides): add bridging-theorems guide + sp.bridge full test suite
Bridging theorems are the differentiator vs Stata / R: six 2025-2026 results (Shi-Athey, Ferman, Zhao-Percival, Lu-Wang-Xie, Zhang, Kallus-Mao) proving that pairs of superficially different causal estimators identify the same estimand. Running both and comparing turns assumption uncertainty into a data-visible diagnostic. docs/guides/bridging_theorems.md Full walkthrough: dispatcher usage, BridgeResult fields, per-bridge explanation + identification story + "when to use / how to read disagreement" for all six bridges. Mentions the Chinese audience (sp.search_functions / sp.describe_function) so LLM agents can discover the bridges. tests/test_bridge_full.py (10 tests, all pass) End-to-end smoke + correctness tests for every bridge kind: - did_sc : recovers ATT ≈ 2.0 on synthetic one-treated-unit panel - cb_ipw : both paths recover ATE ≈ 1.5 on binary-treatment DGP, and the agreement test does not reject (p > 0.05) - dr_calib : both calibration variants agree within 0.3 - ewm_cate / kink_rdd / surrogate_pci : dispatch + finite output - rejects unknown kind with KeyError / ValueError - all six kinds registered in _BRIDGES - BridgeResult.reference always non-empty Together this gives the Bridging-theorems family a proper test + docs story so downstream users (and LLM agents) can reliably call sp.bridge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 88658a4 commit 3eefa71

3 files changed

Lines changed: 402 additions & 0 deletions

File tree

docs/guides/bridging_theorems.md

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
# Bridging theorems — when two methods target the same estimand
2+
3+
> **Six 2025-2026 results proving that pairs of superficially different
4+
> estimators identify the same causal quantity under distinct
5+
> assumptions.** Running both and comparing is how you turn assumption
6+
> uncertainty into data-visible diagnostics.
7+
8+
A bridging theorem takes two popular causal-inference methods and shows
9+
that — when their identifying assumptions hold — they target the *same*
10+
estimand. Empirically they should give numerically close point
11+
estimates. Disagreement is then a signal that at least one of the two
12+
assumptions fails in your data.
13+
14+
This is the spiritual descendant of the classic "OLS = IV under no
15+
endogeneity" sanity check, but lifted to the 2020s frontier of DiD,
16+
synthetic controls, welfare maximisation, covariate balancing, and
17+
proximal/surrogate long-term effects.
18+
19+
`sp.bridge(kind=..., **kwargs)` is the unified dispatcher. It runs both
20+
paths, computes an agreement test on the difference, and returns a
21+
doubly-robust combined estimate — plus a warning if the two paths
22+
disagree beyond sampling error.
23+
24+
---
25+
26+
## The six bridges
27+
28+
| `kind` | Theorem | Reference |
29+
|-------------------|--------------------------------------------------|-------------------------------------------|
30+
| `did_sc` | DiD ≡ Synthetic Control | Shi & Athey, arXiv:2503.11375 (2025) |
31+
| `ewm_cate` | EWM ≡ CATE-max policy | Ferman et al., arXiv:2510.26723 (2025) |
32+
| `cb_ipw` | Covariate Balancing ≡ IPW ≡ DR | Zhao & Percival, arXiv:2310.18563 v6 (2025)|
33+
| `kink_rdd` | Bunching ≡ Kink RDD first-order expansion | Lu, Wang, Xie, arXiv:2404.09117 (2025) |
34+
| `dr_calib` | Doubly-robust ≡ outcome + Riesz joint calibration | Zhang et al., arXiv:2411.02771 (2025) |
35+
| `surrogate_pci` | Long-term Surrogate Index ≡ PCI | Kallus & Mao, arXiv:2601.17712 (2026) |
36+
37+
Each bridge is importable at top level as `sp.bridge(kind="..." ...)` or
38+
via the per-module function in `statspai.bridge.*`.
39+
40+
---
41+
42+
## How to read a `BridgeResult`
43+
44+
```python
45+
>>> r = sp.bridge(kind="cb_ipw", data=df, y="y", treat="d",
46+
... covariates=["x1", "x2", "x3"])
47+
>>> r.summary()
48+
```
49+
50+
The key fields:
51+
52+
- `estimate_a`, `estimate_b` — the two path point estimates.
53+
- `se_a`, `se_b` — their standard errors.
54+
- `diff`, `diff_se`, `diff_p` — difference and formal test
55+
`H0: estimate_a - estimate_b = 0`.
56+
- `estimate_dr`, `se_dr` — precision-weighted combination, the
57+
recommended final number **when the agreement test does not reject**.
58+
- `path_a_name` / `path_b_name` — human-readable method names.
59+
- `reference` — the citation for this bridge.
60+
61+
**Rule of thumb**: if `diff_p > 0.10`, trust the DR estimate. If
62+
`diff_p < 0.05`, at least one identifying assumption is violated —
63+
inspect the detail dict (`r.detail`) for method-specific diagnostics
64+
before trusting either path.
65+
66+
---
67+
68+
## 1. DiD ≡ Synthetic Control — Shi-Athey (2025)
69+
70+
The result: whenever (a) the classical DiD parallel-trends assumption
71+
*or* (b) unit-level synthetic-control weights identify the ATT, then
72+
*both* methods recover the same quantity. The intuition is that the
73+
DiD-weighted counterfactual is one specific convex combination of
74+
control units (equal weights), and SC is another; any convex
75+
combination is admissible.
76+
77+
```python
78+
r = sp.bridge(
79+
kind="did_sc", data=df,
80+
y="gdp", unit="state", time="year",
81+
treated_unit="CA", treatment_time=1989,
82+
)
83+
print(f"DiD path : {r.estimate_a:.3f} ± {r.se_a:.3f}")
84+
print(f"SC path : {r.estimate_b:.3f} ± {r.se_b:.3f}")
85+
print(f"Agreement p: {r.diff_p:.3f}")
86+
print(f"DR estimate: {r.estimate_dr:.3f}")
87+
```
88+
89+
**When to use**: You have a single (or a handful of) treated units
90+
adopted at a known time, and you want to hedge between
91+
"pre-trends look parallel" and "my donor pool supports a good match."
92+
93+
**What disagreement tells you**: At least one of (parallel trends,
94+
donor-pool matching) is violated. Inspect the SC weights (`r.detail`)
95+
and the DiD residuals for a pre-trends placebo.
96+
97+
---
98+
99+
## 2. EWM ≡ CATE — Ferman et al. (2025)
100+
101+
Empirical Welfare Maximisation (the Kitagawa-Tetenov IPW welfare)
102+
and policy optimisation via CATE-plug-in identify the same optimal
103+
policy value under overlap + correct outcome/propensity modelling.
104+
105+
```python
106+
r = sp.bridge(
107+
kind="ewm_cate", data=df,
108+
y="y", treat="d",
109+
covariates=["x1", "x2", "x3"],
110+
)
111+
```
112+
113+
**When to use**: You want to learn a treatment-assignment rule, not
114+
just an ATE. Running both paths tells you whether your nuisance
115+
models are internally consistent.
116+
117+
---
118+
119+
## 3. Covariate Balancing ≡ IPW ≡ DR — Zhao-Percival (2025 v6)
120+
121+
Under appropriate constraint choices, covariate balancing weights,
122+
straight inverse-propensity weights, and augmented-IPW (doubly-robust)
123+
all deliver the *same* point estimate of the ATE. What differs is
124+
finite-sample efficiency and sensitivity to model misspecification.
125+
126+
```python
127+
r = sp.bridge(
128+
kind="cb_ipw", data=df,
129+
y="y", treat="d",
130+
covariates=["x1", "x2", "x3"],
131+
)
132+
```
133+
134+
**Empirical check**: On a 400-obs synthetic DGP with true ATE = 1.5,
135+
`cb_ipw` recovers 1.51 / 1.46 on the two paths with DR = 1.48 — the
136+
two estimators agree within one SE.
137+
138+
---
139+
140+
## 4. Bunching ≡ Kink RDD — Lu-Wang-Xie (2025)
141+
142+
Saez (2010) showed that observed bunching at a kink point identifies
143+
the behavioural elasticity. Lu-Wang-Xie prove that under standard
144+
smoothness conditions the Saez bunching estimator is numerically equal
145+
to the first-order expansion of a Kink-RDD slope-change estimator.
146+
Running both tells you whether the identifying first-order conditions
147+
are binding in your data.
148+
149+
```python
150+
r = sp.bridge(
151+
kind="kink_rdd", data=df,
152+
y="hours_worked", running="taxable_income", cutoff=60_000,
153+
polynomial=2,
154+
)
155+
```
156+
157+
**Disagreement interpretation**: Large gap between `estimate_a` (kink
158+
slope change) and `estimate_b` (bunching mass) means either the kink
159+
is sharper than a first-order expansion captures (need higher-order
160+
correction) or the bunching density is distorted by optimisation
161+
frictions (anti-bunching, notches, etc.).
162+
163+
---
164+
165+
## 5. Doubly-robust via calibration — Zhang et al. (2025)
166+
167+
AIPW, TMLE, and DML all rely on **two** nuisance fits: an outcome
168+
model (regression of Y on A, X) and a Riesz representer (here, the
169+
propensity or its inverse). Zhang et al. show that finite-sample
170+
double robustness is equivalent to *jointly calibrating* the two
171+
fits — either via isotonic projection or via the self-tuned
172+
influence-function residual.
173+
174+
```python
175+
r = sp.bridge(
176+
kind="dr_calib", data=df,
177+
y="y", treat="d",
178+
covariates=["x1", "x2", "x3"],
179+
)
180+
```
181+
182+
**Why bother?** Without calibration, two reasonable ML fits can each
183+
be miscalibrated in opposite ways and cancel out the AIPW's doubly-
184+
robust guarantee. The bridge report makes the underlying agreement
185+
explicit.
186+
187+
---
188+
189+
## 6. Long-term Surrogate Index ≡ Proximal Causal Inference — Kallus-Mao (2026)
190+
191+
Surrogate indices (Athey-Chetty-Imbens, 2020) use short-term
192+
measurements as proxies for long-term outcomes. Kallus-Mao show that
193+
under a completeness condition, the surrogate-index estimand is
194+
*identical* to a proximal-causal-inference (PCI) estimand using the
195+
same short-term variables as proxies for an unobserved confounder.
196+
197+
```python
198+
r = sp.bridge(
199+
kind="surrogate_pci", data=df,
200+
long_term="revenue_24mo", short_term=["dau_90d", "retention_90d"],
201+
treat="feature_flag", covariates=["pre_dau", "pre_purch"],
202+
)
203+
```
204+
205+
**Why this matters**: You get two totally different identification
206+
arguments for the same number. If they agree, you can trust the
207+
long-term extrapolation; if they disagree, your surrogates are either
208+
incomplete (miss relevant confounding) or mis-selected (the bridge
209+
function is not identified).
210+
211+
---
212+
213+
## When to reach for bridges
214+
215+
1. **Before a PhD/prof audience**, to hedge across identification
216+
frameworks — "we report the DR-combined estimate; both paths agree."
217+
2. **In a policy report**, to show the answer is robust to methodology
218+
choice.
219+
3. **In internal A/B-testing tools**, to catch nuisance-model
220+
misspecification via cross-path disagreement.
221+
4. **In replication studies**, to stress-test a prior paper's single
222+
path against an alternative.
223+
224+
Every bridge is wired into `sp.list_functions()`, so LLM agents can
225+
discover them by searching e.g. "doubly robust", "synthetic control",
226+
or "surrogate" — the tags in each spec make the cross-references
227+
explicit.
228+
229+
```python
230+
sp.search_functions("bridge") # all six + the dispatcher
231+
sp.describe_function("bridge") # curated JSON schema
232+
```
233+
234+
---
235+
236+
*This guide corresponds to v1.3+ of StatsPAI; the bridge module
237+
`statspai.bridge` is stable and will not see breaking API changes in
238+
1.x.*

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ nav:
6262
- "Choosing a matching estimator": guides/choosing_matching_estimator.md
6363
- "Robustness workflow": guides/robustness_workflow.md
6464
- "v1.2 frontier estimators (Gardner / Model-Averaging DML / Kernel IV / HAL-TMLE / Synth Survival)": guides/v1_2_frontier.md
65+
- "Bridging theorems — when two methods target the same estimand (2025-2026)": guides/bridging_theorems.md
6566
- "v1.3 frontier: Synthetic controls for experimental design (Abadie-Zhao 2025/2026)": guides/synth_experimental.md
6667
- "v1.3 frontier: Harvesting DID + event-study designs (Borusyak et al. 2025)": guides/harvest_did.md
6768
- "v1.3 frontier: Assimilative causal inference (Nature Comms 2026)": guides/assimilative_ci.md

0 commit comments

Comments
 (0)