Skip to content

Commit 6218e45

Browse files
igerberclaude
andcommitted
Address P3 findings for clean land
- Fix CS registry: clarify analytical vs bootstrap SE paths - Update ImputationDiD/TwoStageDiD docstrings: survey bootstrap supported - Add CS full-design aggregate='group' regression test (250 tests) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4c63569 commit 6218e45

4 files changed

Lines changed: 22 additions & 4 deletions

File tree

diff_diff/imputation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ def fit(
208208
pweight only (aweight/fweight raise ValueError). FPC raises
209209
NotImplementedError. PSU is used as cluster variable for Theorem 3
210210
variance. Strata enters survey df for t-distribution inference.
211-
Requires analytical inference (n_bootstrap=0).
211+
Both analytical (n_bootstrap=0) and bootstrap inference are supported.
212212
213213
Returns
214214
-------
@@ -1973,7 +1973,7 @@ def imputation_did(
19731973
pweight only (aweight/fweight raise ValueError). FPC raises
19741974
NotImplementedError. PSU is used as cluster variable for Theorem 3
19751975
variance. Strata enters survey df for t-distribution inference.
1976-
Requires analytical inference (n_bootstrap=0).
1976+
Both analytical (n_bootstrap=0) and bootstrap inference are supported.
19771977
**kwargs
19781978
Additional keyword arguments passed to ImputationDiD constructor.
19791979

diff_diff/two_stage.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ def fit(
204204
pweight only (aweight/fweight raise ValueError). FPC raises
205205
NotImplementedError. PSU is used as cluster variable for Theorem 3
206206
variance. Strata enters survey df for t-distribution inference.
207-
Requires analytical inference (n_bootstrap=0).
207+
Both analytical (n_bootstrap=0) and bootstrap inference are supported.
208208
209209
Returns
210210
-------

docs/methodology/REGISTRY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ The multiplier bootstrap uses random weights w_i with E[w]=0 and Var(w)=1:
418418
not-yet-treated cohorts serve as controls for each other (requires ≥2 cohorts)
419419
- **Note:** CallawaySantAnna survey support: weights, strata, PSU, and FPC are all supported. Analytical (`n_bootstrap=0`): aggregated SEs use design-based variance via `compute_survey_if_variance()`. Bootstrap (`n_bootstrap>0`): PSU-level multiplier weights replace analytical SEs for aggregated quantities. Regression method supports covariates; IPW/DR support no-covariate only (covariates+IPW/DR raises NotImplementedError — DRDID nuisance IF not yet implemented). Survey weights compose with IPW weights multiplicatively. WIF in aggregation matches R's did::wif() formula. Per-unit survey weights are extracted via `groupby(unit).first()` from the panel-normalized pweight array; on unbalanced panels the pweight normalization (`w * n_obs / sum(w)`) preserves relative unit weights since all IF/WIF formulas use weight ratios (`sw_i / sum(sw)`) where the normalization constant cancels. Scale-invariance tests pass on both balanced and unbalanced panels.
420420
- **Note (deviation from R):** CallawaySantAnna survey reg+covariates per-cell SE uses a conservative plug-in IF based on WLS residuals. The treated IF is `inf_treated_i = (sw_i/sum(sw_treated)) * (resid_i - ATT)` (normalized by treated weight sum, matching unweighted `(resid-ATT)/n_t`). The control IF is `inf_control_i = -(sw_i/sum(sw_control)) * wls_resid_i` (normalized by control weight sum, matching unweighted `-resid/n_c`). SE is computed as `sqrt(sum(sw_t_norm * (resid_t - ATT)^2) + sum(sw_c_norm * resid_c^2))`, the weighted analogue of the unweighted `sqrt(var_t/n_t + var_c/n_c)`. This omits the semiparametrically efficient nuisance correction from DRDID's `reg_did_panel` — WLS residuals are orthogonal to the weighted design matrix by construction, so the first-order IF term is asymptotically valid but may be conservative. SEs pass weight-scale-invariance tests. The efficient DRDID correction is deferred to future work.
421-
- **Note (deviation from R):** Per-cell ATT(g,t) SEs under survey weights use influence-function-based variance (matching R's `did::att_gt` analytical SE path) rather than full Taylor-series linearization. When strata/PSU/FPC are present, aggregated SEs are computed via PSU-level multiplier bootstrap (see bootstrap + survey note above) rather than analytical Taylor-series linearization on the combined IF/WIF.
421+
- **Note (deviation from R):** Per-cell ATT(g,t) SEs under survey weights use influence-function-based variance (matching R's `did::att_gt` analytical SE path) rather than full Taylor-series linearization. When strata/PSU/FPC are present, analytical aggregated SEs (`n_bootstrap=0`) use `compute_survey_if_variance()` on the combined IF/WIF; bootstrap aggregated SEs (`n_bootstrap>0`) use PSU-level multiplier weights.
422422

423423
**Reference implementation(s):**
424424
- R: `did::att_gt()` (Callaway & Sant'Anna's official package)

tests/test_survey_phase4.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,24 @@ def test_aggregate_group_with_survey(self, staggered_survey_data, survey_design_
880880
assert result.group_effects is not None
881881
assert len(result.group_effects) > 0
882882

883+
def test_aggregate_group_full_design(self, staggered_survey_data):
884+
"""aggregate='group' with full design uses design-based SEs."""
885+
sd_full = SurveyDesign(weights="weight", strata="stratum", psu="psu")
886+
result = CallawaySantAnna(estimation_method="reg").fit(
887+
staggered_survey_data,
888+
"outcome",
889+
"unit",
890+
"period",
891+
"first_treat",
892+
aggregate="group",
893+
survey_design=sd_full,
894+
)
895+
assert result.group_effects is not None
896+
assert len(result.group_effects) > 0
897+
for g, info in result.group_effects.items():
898+
assert np.isfinite(info["effect"])
899+
assert np.isfinite(info["se"])
900+
883901
def test_aggregate_all_with_survey(self, staggered_survey_data, survey_design_weights_only):
884902
"""aggregate='all' works with weights-only survey design."""
885903
result = CallawaySantAnna(estimation_method="reg").fit(

0 commit comments

Comments
 (0)