Skip to content

Commit 531013e

Browse files
igerberclaude
andcommitted
Add survey support (pweight + strata/PSU/FPC) to dCDH estimator
- Weighted cell aggregation in _validate_and_aggregate_to_cells() - Survey resolution via _resolve_survey_for_fit() with pweight-only and group-constant validation - IF expansion from group to observation level for TSL variance - Survey-aware SE at all call sites (overall, joiners, leavers, multi-horizon, placebos) via _compute_se() dispatcher - Bootstrap + survey warning (PSU-level deferred) - 12 new tests in test_survey_dcdh.py - Documentation updates: REGISTRY.md, ROADMAP.md, llms-full.txt, choosing_estimator.rst Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent dbff8a5 commit 531013e

8 files changed

Lines changed: 632 additions & 52 deletions

File tree

ROADMAP.md

Lines changed: 2 additions & 2 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
@@ -195,7 +195,7 @@ These remain in **Future Estimators** below if/when we choose to extend.
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: 226 additions & 37 deletions
Large diffs are not rendered by default.

diff_diff/survey.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,47 @@ def _validate_unit_constant_survey(data, unit_col, survey_design):
911911
)
912912

913913

914+
def _validate_group_constant_survey(data, group_col, survey_design):
915+
"""Validate that survey design columns are constant within groups.
916+
917+
The dCDH estimator aggregates to ``(group, time)`` cells and then
918+
works at the group level. Survey columns (weights, strata, PSU)
919+
must not vary within groups for the IF expansion and survey variance
920+
to be well-defined.
921+
922+
Parameters
923+
----------
924+
data : pd.DataFrame
925+
Input data (pre-aggregation).
926+
group_col : str
927+
Group identifier column name.
928+
survey_design : SurveyDesign
929+
Survey design specification (uses attribute names, not resolved arrays).
930+
931+
Raises
932+
------
933+
ValueError
934+
If any survey column varies within groups.
935+
"""
936+
cols_to_check = [
937+
survey_design.weights,
938+
survey_design.strata,
939+
survey_design.psu,
940+
survey_design.fpc,
941+
]
942+
for col in cols_to_check:
943+
if col is not None and col in data.columns:
944+
n_unique = data.groupby(group_col)[col].nunique()
945+
varying_groups = n_unique[n_unique > 1]
946+
if len(varying_groups) > 0:
947+
raise ValueError(
948+
f"Survey column '{col}' varies within groups "
949+
f"(found {len(varying_groups)} groups with multiple values). "
950+
f"dCDH survey support requires survey design columns to be "
951+
f"constant within groups."
952+
)
953+
954+
914955
def _resolve_pweight_only(resolved_survey, estimator_name):
915956
"""Guard: reject non-pweight and strata/PSU/FPC for pweight-only estimators.
916957

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

docs/llms-full.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -265,8 +265,8 @@ 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

@@ -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

docs/methodology/REGISTRY.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -627,7 +627,7 @@ Alternative: Multiplier bootstrap clustered at group via the `n_bootstrap` param
627627

628628
**Requirements checklist:**
629629
- [x] Single class `ChaisemartinDHaultfoeuille` (alias `DCDH`); not a family
630-
- [x] Forward-compat `fit()` signature with `NotImplementedError` gates for remaining parameters (`aggregate`, `survey_design`); Phase 3 gates lifted for `controls`, `trends_linear`, `trends_nonparam`, `honest_did`
630+
- [x] Forward-compat `fit()` signature with `NotImplementedError` gate for `aggregate`; survey_design now supported (pweight + strata/PSU/FPC via TSL); Phase 3 gates lifted for `controls`, `trends_linear`, `trends_nonparam`, `honest_did`
631631
- [x] `DID_M` point estimate with cohort-recentered analytical SE
632632
- [x] Joiners-only `DID_+` and leavers-only `DID_-` decompositions with their own inference
633633
- [x] Single-lag placebo `DID_M^pl` (point estimate; SE deferred to Phase 2)
@@ -648,6 +648,8 @@ Alternative: Multiplier bootstrap clustered at group via the `n_bootstrap` param
648648
- [x] Heterogeneity testing via saturated OLS (Web Appendix Section 1.5, Lemma 7)
649649
- [x] Design-2 switch-in/switch-out descriptive wrapper (Web Appendix Section 1.6)
650650
- [x] HonestDiD (Rambachan-Roth 2023) integration on placebo + event study surface
651+
- [x] Survey design support: pweight with strata/PSU/FPC via Taylor Series Linearization. Replicate weights and PSU-level bootstrap deferred.
652+
- **Note:** Survey IF expansion (`psi_i = U[g] * (w_i / W_g)`) is a library extension not in the dCDH papers. The paper's plug-in variance assumes iid sampling; the TSL variance accounts for complex survey design by expanding group-level influence functions to observation level proportionally to survey weights, then applying the standard Binder (1983) stratified PSU variance formula.
651653

652654
---
653655

tests/test_chaisemartin_dhaultfoeuille.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -385,15 +385,21 @@ def test_honest_did_requires_lmax(self, data):
385385
honest_did=True,
386386
)
387387

388-
def test_survey_design_raises_not_implemented(self, data):
389-
with pytest.raises(NotImplementedError, match="separate effort"):
388+
def test_survey_design_rejects_fweight(self, data):
389+
"""Survey support requires pweight; fweight rejected."""
390+
from diff_diff import SurveyDesign
391+
392+
data = data.copy()
393+
data["pw"] = 1.0
394+
sd = SurveyDesign(weights="pw", weight_type="fweight")
395+
with pytest.raises(ValueError, match="pweight"):
390396
self._est().fit(
391397
data,
392398
outcome="outcome",
393399
group="group",
394400
time="period",
395401
treatment="treatment",
396-
survey_design=object(),
402+
survey_design=sd,
397403
)
398404

399405
def test_cluster_parameter_raises_not_implemented(self, data):

0 commit comments

Comments
 (0)