From a6c6aba685c2a21174c50818aeb36e44a56dceab Mon Sep 17 00:00:00 2001 From: igerber Date: Sat, 30 May 2026 06:38:50 -0400 Subject: [PATCH 1/4] two-stage-did: thread vcov_type as narrow {hc1} contract (Phase 1b interstitial #5, final) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TwoStageDiD's variance is the Gardner/did2s two-stage GMM cluster-sandwich (always clusters; default at unit) — a structural twin of ImputationDiD, NOT the GMM×HC2-BM beast the tracker described (that was SpilloverDiD's helper). Add vcov_type="hc1" accepting only {hc1}; reject {classical,hc2,hc2_bm} (the GMM-corrected meat S_g = gamma_hat' c_g - X_2g' eps_2g folds in first-stage uncertainty, so no single hat matrix spans both stages for HC2 leverage / BM-DOF) and conley (deferred). Results gains vcov_type/cluster_name/n_clusters + to_dict(); summary() renders the unit-cluster CR1 label with bootstrap + survey suppression gates. Bootstrap n_clusters<2 NaN guard (load-bearing, post-drop perturbation count) + survey n_psu<2 defense. cluster= + replicate weights raises NotImplementedError. Docs: REGISTRY taxonomy -> N=5 + TwoStageDiD Note, llms-full signature, both autosummary RSTs, CHANGELOG, TODO (initiative complete + conley follow-up). 34 new tests; ATT/SE bit-identical vs baseline across default/cluster/bootstrap. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 1 + TODO.md | 3 +- diff_diff/guides/llms-full.txt | 1 + diff_diff/two_stage.py | 100 +++++++ diff_diff/two_stage_bootstrap.py | 108 ++++++- diff_diff/two_stage_results.py | 91 ++++++ .../_autosummary/diff_diff.TwoStageDiD.rst | 1 + .../diff_diff.TwoStageDiDResults.rst | 4 + docs/api/two_stage.rst | 1 + docs/methodology/REGISTRY.md | 11 +- tests/test_two_stage.py | 280 ++++++++++++++++++ 11 files changed, 591 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4f5a2b2..3783708a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added / Changed - **EfficientDiD `vcov_type` threading + Results metadata harmonization (Phase 1b interstitial #4, permanently narrow).** `EfficientDiD(vcov_type=...)` now accepts `{"hc1"}` only (default). Analytical-sandwich families `{classical, hc2, hc2_bm}` and `conley` are REJECTED at `__init__` / `set_params` with methodology-rooted messages — EfficientDiD uses influence-function-based variance per Chen-Sant'Anna-Xie (2025) achieving the semiparametric efficiency bound; the per-unit EIF aggregation has no single design matrix on which hat-matrix leverage or Bell-McCaffrey Satterthwaite DOF can be defined. `cluster=` (Liang-Zeger CR1 on cluster-aggregated EIF) and `survey_design=` (TSL on combined IF) paths are unchanged. **BC break on `EfficientDiDResults`:** the `cluster` field renamed to `cluster_name`; new `n_clusters` + `vcov_type` fields added; `to_dict()` method added (mirrors TripleDifferenceResults). `DiagnosticReport._pt_hausman` updated to read the renamed `cluster_name` field for the Hausman pretest replay (`diff_diff/diagnostic_report.py:2444`). `EfficientDiD.set_params(vcov_type=bad)` raises immediately rather than deferring to `fit()` — intentional eager-validation pattern matching EfficientDiD's existing handling of `pt_assumption`/`control_group` etc, diverging from `ImputationDiD`/`TripleDifference`/`CallawaySantAnna` (which use sklearn mutate-then-validate-at-use). Survey-PSU bootstrap path returns NaN SE when fewer than 2 independent PSUs are available (was ≈0 SE from BLAS roundoff). New summary block: `Variance estimator: