Skip to content

Commit 1dfdc92

Browse files
authored
Merge pull request #307 from igerber/feature/dcdh-survey
Add survey support to dCDH estimator
2 parents bd2b55d + e82faab commit 1dfdc92

11 files changed

Lines changed: 1960 additions & 107 deletions

ROADMAP.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ The dynamic companion paper subsumes the AER 2020 paper: `DID_1 = DID_M`. The si
181181

182182
These are referenced by the dCDH papers but live in *separate* efforts or *separate* companion papers we don't yet have:
183183

184-
- **Survey design integration**deferred to a separate effort after all three phases ship. Phase 1 documents "no survey support" in the compatibility matrix; the separate effort revisits when Phase 3 is complete.
184+
- **Survey design integration**shipped. Supports pweight with strata/PSU/FPC via Taylor Series Linearization. Replicate weights and PSU-level bootstrap deferred.
185185
- **Fuzzy DiD** (within-cell-varying treatment, Web Appendix Section 1.7 of dynamic paper) → de Chaisemartin & D'Haultfœuille (2018), separate paper not yet reviewed
186186
- **Principled anticipation handling and trimming rules** (footnote 14 of dynamic paper) → de Chaisemartin (2021), separate paper not yet reviewed
187187
- **2SLS DiD** (referenced in AER appendix Section 3.4) → separate paper
@@ -191,11 +191,11 @@ These remain in **Future Estimators** below if/when we choose to extend.
191191
### Architectural notes (for plan and PR reviewers)
192192

193193
- **Single `ChaisemartinDHaultfoeuille` class** (alias `DCDH`). Not a family. New features land as `fit()` parameters or fields on the results dataclass. No `DCDHDynamic`, `DCDHCovariate`, etc. Matches the library's idiomatic pattern: `CallawaySantAnna`, `ImputationDiD`, and `EfficientDiD` are all single classes that evolved across many phases.
194-
- **Forward-compatible API from Phase 1.** `fit(aggregate=None, controls=None, trends_linear=None, L_max=None, ...)` accepts the Phase 2/3 parameters from day one and raises `NotImplementedError` with a clear pointer to the relevant phase until they are implemented. No signature changes between phases.
194+
- **Forward-compatible API from Phase 1.** `fit(aggregate=None, controls=None, trends_linear=None, L_max=None, ...)` accepts the Phase 2/3 parameters from day one and raised `NotImplementedError` with a clear pointer to the relevant phase until they were implemented. As of the dCDH work, Phase 2, Phase 3, and `survey_design` are all live; only `aggregate` remains gated with `NotImplementedError`. No signature changes between phases.
195195
- **Conservative CI** under Assumption 8 (independent groups), exact only under iid sampling. Documented in REGISTRY.md as a `**Note:**` deviation from "default nominal coverage." Theorem 1 of the dynamic paper.
196196
- **Cohort recentering for variance is essential.** Cohorts are defined by the triple `(D_{g,1}, F_g, S_g)`. The plug-in variance subtracts cohort-conditional means, **NOT a single grand mean**. Test fixtures must catch this — a wrong implementation silently produces a smaller, incorrect variance.
197197
- **No Rust acceleration is planned for any phase.** The estimator's hot path is groupby + BLAS-accelerated matrix-vector products, where NumPy already operates near-optimally. If profiling on large panels (`G > 100K`) reveals a bottleneck post-ship, the existing `_rust_bootstrap_weights` helper can be reused for the bootstrap loop without writing new Rust code.
198-
- **No survey design integration in any phase.** Handled as a separate effort after all three phases ship. Phase 1 documents the absence in the compatibility matrix so survey users do not silently apply survey weights and get wrong answers.
198+
- **Survey design integration shipped.** Supports pweight with strata/PSU/FPC via TSL. Replicate weights and PSU-level bootstrap deferred to a follow-up.
199199

200200
---
201201

diff_diff/chaisemartin_dhaultfoeuille.py

Lines changed: 625 additions & 82 deletions
Large diffs are not rendered by default.

diff_diff/chaisemartin_dhaultfoeuille_results.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,13 @@ class ChaisemartinDHaultfoeuilleResults:
342342
``compute_honest_did(results)`` post-hoc. Contains identified
343343
set bounds, robust confidence intervals, and breakdown analysis.
344344
survey_metadata : Any, optional
345-
Always ``None`` in Phase 1 — survey integration is deferred to a
346-
separate effort after all phases ship.
345+
Populated when ``fit(..., survey_design=sd)`` is called; ``None``
346+
otherwise. Carries the resolved survey design summary
347+
(``weight_type``, strata/PSU counts, ``df_survey``, weight range,
348+
and replicate-method info when applicable). ``df_survey`` is
349+
threaded into survey-aware inference (t-distribution at all
350+
analytical surfaces) and consumed by ``compute_honest_did()`` to
351+
produce survey-aware critical values.
347352
bootstrap_results : DCDHBootstrapResults, optional
348353
Bootstrap inference results when ``n_bootstrap > 0``.
349354
"""

diff_diff/guides/llms-full.txt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -265,12 +265,12 @@ est.fit(
265265
trends_linear: bool | None = None, # Phase 3: DID^{fd}
266266
trends_nonparam: Any | None = None, # Phase 3: DID^s
267267
honest_did: bool = False, # Phase 3: HonestDiD integration
268-
# ---- deferred (separate effort) ----
269-
survey_design: Any = None,
268+
# ---- survey support ----
269+
survey_design: SurveyDesign | None = None, # pweight + strata/PSU/FPC (TSL)
270270
) -> ChaisemartinDHaultfoeuilleResults
271271
```
272272

273-
`L_max` controls multi-horizon computation. Phase 3 parameters raise `NotImplementedError`.
273+
`L_max` controls multi-horizon computation. Phase 3 parameters (`controls`, `trends_linear`, `trends_nonparam`, `honest_did`, `heterogeneity`, `design2`) and `survey_design` are implemented; only `aggregate` remains gated with `NotImplementedError`.
274274

275275
**Usage:**
276276

@@ -322,7 +322,7 @@ print(f"sigma_fe (sign-flipping threshold): {diagnostic.sigma_fe:.3f}")
322322
- Validated against R `DIDmultiplegtDYN` v2.3.3 at horizon `l = 1` via `tests/test_chaisemartin_dhaultfoeuille_parity.py`
323323
- Phase 1 placebo SE is intentionally `NaN` with a warning. The dynamic companion paper Section 3.7.3 derives the cohort-recentered analytical variance for `DID_l` only — not for the placebo `DID_M^pl`. Phase 2 will add multiplier-bootstrap support for the placebo. Until then, the placebo point estimate is meaningful but its inference fields stay NaN-consistent **even when `n_bootstrap > 0`** (bootstrap currently covers `DID_M`, `DID_+`, and `DID_-` only)
324324
- The analytical CI is conservative under Assumption 8 (independent groups) of the dynamic companion paper, exact only under iid sampling
325-
- Survey design (`survey_design`) is not yet supported and is deferred to a separate effort after all phases ship
325+
- Survey design supported: pweight with strata/PSU/FPC via Taylor Series Linearization. Replicate weights and PSU-level bootstrap deferred
326326

327327
### SunAbraham
328328

@@ -1021,7 +1021,7 @@ Returned by `SyntheticDiD.fit()`.
10211021
| `time_weights` | `dict` | Pre-treatment time weights |
10221022
| `pre_periods` | `list` | Pre-treatment periods |
10231023
| `post_periods` | `list` | Post-treatment periods |
1024-
| `variance_method` | `str` | "bootstrap" or "placebo" |
1024+
| `variance_method` | `str` | "bootstrap", "jackknife", or "placebo" |
10251025
| `noise_level` | `float` | Estimated noise level |
10261026
| `zeta_omega` | `float` | Unit weight regularization |
10271027
| `zeta_lambda` | `float` | Time weight regularization |

diff_diff/guides/llms-practitioner.txt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ Is treatment adoption staggered (multiple cohorts, different timing)?
173173
|-- NO, simple 2x2 design:
174174
| \-- DifferenceInDifferences (DiD)
175175
|
176+
|-- Treatment switches ON and OFF (reversible / non-absorbing)?
177+
| \-- ChaisemartinDHaultfoeuille (dCDH / alias `DCDH`)
178+
| -- Only library estimator for non-absorbing treatments; supports
179+
| L_max multi-horizon, dynamic placebos, cost-benefit delta,
180+
| HonestDiD, and `survey_design=` (pweight + strata/PSU/FPC via TSL)
181+
|
176182
|-- Few treated units (< 20)?
177183
| \-- SyntheticDiD (SDiD) -- synthetic control + DiD hybrid
178184
|
@@ -260,6 +266,27 @@ results = es.fit(data, outcome='y', unit='unit_id', time='period',
260266
print(results.summary())
261267
```
262268

269+
### Reversible (non-absorbing) treatment with survey design
270+
Use `ChaisemartinDHaultfoeuille` (dCDH) when treatment switches ON and OFF.
271+
Pass `survey_design=SurveyDesign(...)` for design-based inference via Taylor
272+
Series Linearization. Only `weight_type='pweight'` is supported; replicate
273+
weights are deferred. When combined with `n_bootstrap > 0`, dCDH emits a
274+
`UserWarning` and falls back to group-level multiplier bootstrap — prefer
275+
the analytical TSL path for survey-aware inference.
276+
277+
```python
278+
from diff_diff import ChaisemartinDHaultfoeuille, SurveyDesign
279+
280+
sd = SurveyDesign(weights='pw', strata='stratum', psu='cluster', nest=True)
281+
results = ChaisemartinDHaultfoeuille().fit(
282+
data, outcome='y', group='unit_id', time='period',
283+
treatment='treated',
284+
L_max=3, # multi-horizon event study
285+
survey_design=sd, # survey-aware analytical SE (TSL)
286+
)
287+
print(results.summary())
288+
```
289+
263290
---
264291

265292
## Step 6: Sensitivity Analysis

diff_diff/honest_did.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -967,14 +967,24 @@ def _largest_consecutive_block(times, boundary_val):
967967
beta_hat = np.array(effects)
968968
sigma = np.diag(np.array(ses) ** 2)
969969

970+
# Extract survey df. For replicate designs with undefined df
971+
# (rank <= 1), use sentinel df=0 so _get_critical_value returns
972+
# NaN, matching the safe_inference contract.
973+
df_survey = None
974+
if hasattr(results, "survey_metadata") and results.survey_metadata is not None:
975+
sm = results.survey_metadata
976+
df_survey = getattr(sm, "df_survey", None)
977+
if df_survey is None and getattr(sm, "replicate_method", None) is not None:
978+
df_survey = 0 # undefined replicate df → NaN inference
979+
970980
return (
971981
beta_hat,
972982
sigma,
973983
len(pre_times),
974984
len(post_times),
975985
pre_times,
976986
post_times,
977-
None, # df_survey: dCDH has no survey support
987+
df_survey,
978988
)
979989
except ImportError:
980990
pass

docs/choosing_estimator.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -293,9 +293,9 @@ Phase 3 will add covariate adjustment.
293293

294294
.. note::
295295

296-
``ChaisemartinDHaultfoeuille`` does not yet support ``survey_design``;
297-
passing it raises ``NotImplementedError``. Survey integration is
298-
deferred to a separate effort after Phases 2 and 3 ship.
296+
``ChaisemartinDHaultfoeuille`` supports ``survey_design`` with pweight
297+
and strata/PSU/FPC via Taylor Series Linearization. Replicate weights
298+
are not yet supported.
299299

300300
Synthetic DiD
301301
~~~~~~~~~~~~~
@@ -726,10 +726,10 @@ estimation. The depth of support varies by estimator:
726726
- Full
727727
- Multiplier at PSU
728728
* - ``ChaisemartinDHaultfoeuille``
729+
- pweight only
730+
- Full (TSL)
729731
- --
730-
- --
731-
- --
732-
- --
732+
- Group-level (warning)
733733
* - ``TripleDifference``
734734
- pweight only
735735
- Full

0 commit comments

Comments
 (0)