Skip to content

Commit 64119a0

Browse files
igerberclaude
andcommitted
two-stage-did: explicit vcov_type on convenience fn + full NaN-contract tests (codex P3)
- two_stage_did(): expose vcov_type="hc1" explicitly (was hidden behind **kwargs) and forward it, matching the imputation_did/efficient_did sibling wrappers — the convenience API surface, generated signature, and IDE help now show the param. - Degenerate-bootstrap tests now assert the FULL public NaN-propagation contract (overall t_stat/p_value/conf_int + every event-study/group inference field) via a shared _assert_full_bootstrap_nan helper, not just overall_se, so a partial regression in _build_nan_bootstrap_results can't slip through. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 489dae4 commit 64119a0

2 files changed

Lines changed: 46 additions & 11 deletions

File tree

diff_diff/two_stage.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3361,6 +3361,7 @@ def two_stage_did(
33613361
aggregate: Optional[str] = None,
33623362
balance_e: Optional[int] = None,
33633363
survey_design: object = None,
3364+
vcov_type: str = "hc1",
33643365
**kwargs,
33653366
) -> TwoStageDiDResults:
33663367
"""
@@ -3392,6 +3393,11 @@ def two_stage_did(
33923393
PSU, and FPC for design-based GMM sandwich variance. Strata enters
33933394
survey df for t-distribution inference.
33943395
Both analytical (n_bootstrap=0) and bootstrap inference are supported.
3396+
vcov_type : str, default="hc1"
3397+
Variance estimator family; permanently narrow to ``{"hc1"}`` (the Gardner
3398+
2022 two-stage GMM cluster-sandwich). Analytical-sandwich families
3399+
``{"classical", "hc2", "hc2_bm"}`` and ``"conley"`` are rejected. See
3400+
:class:`TwoStageDiD`.
33953401
**kwargs
33963402
Additional keyword arguments passed to TwoStageDiD constructor.
33973403
@@ -3408,7 +3414,7 @@ def two_stage_did(
34083414
... 'first_treat', aggregate='event_study')
34093415
>>> results.print_summary()
34103416
"""
3411-
est = TwoStageDiD(**kwargs)
3417+
est = TwoStageDiD(vcov_type=vcov_type, **kwargs)
34123418
return est.fit(
34133419
data,
34143420
outcome=outcome,

tests/test_two_stage.py

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1979,6 +1979,43 @@ def _eq(a, b):
19791979
assert _eq(e0[k][f], e1[k][f]), f"{attr}[{k}][{f}] differs"
19801980

19811981

1982+
def _assert_full_bootstrap_nan(r):
1983+
"""Assert the FULL public NaN-propagation contract under a degenerate
1984+
bootstrap (n_clusters<2 / n_psu<2): every overall + per-horizon + per-group
1985+
inference field is NaN, not just the SE (REGISTRY NaN-inference contract).
1986+
"""
1987+
# Overall inference fields
1988+
assert np.isnan(r.overall_se)
1989+
assert np.isnan(r.overall_t_stat)
1990+
assert np.isnan(r.overall_p_value)
1991+
assert all(np.isnan(x) for x in r.overall_conf_int)
1992+
assert np.isnan(r.coef_var)
1993+
# Bootstrap payload
1994+
b = r.bootstrap_results
1995+
assert np.isnan(b.overall_att_se)
1996+
assert np.isnan(b.overall_att_p_value)
1997+
assert all(np.isnan(x) for x in b.overall_att_ci)
1998+
if b.event_study_ses:
1999+
assert all(np.isnan(v) for v in b.event_study_ses.values())
2000+
if b.group_ses:
2001+
assert all(np.isnan(v) for v in b.group_ses.values())
2002+
# Per-horizon event-study inference fields (skip reference-period markers,
2003+
# which carry n_obs == 0 and are not real effects).
2004+
for eff in (r.event_study_effects or {}).values():
2005+
if eff.get("n_obs", 1) == 0:
2006+
continue
2007+
assert np.isnan(eff["se"])
2008+
assert np.isnan(eff["t_stat"])
2009+
assert np.isnan(eff["p_value"])
2010+
assert all(np.isnan(x) for x in eff["conf_int"])
2011+
# Per-group inference fields
2012+
for eff in (r.group_effects or {}).values():
2013+
assert np.isnan(eff["se"])
2014+
assert np.isnan(eff["t_stat"])
2015+
assert np.isnan(eff["p_value"])
2016+
assert all(np.isnan(x) for x in eff["conf_int"])
2017+
2018+
19822019
class TestTwoStageDiDVcovType:
19832020
"""Phase 1b interstitial #5 (final): vcov_type input contract on TwoStageDiD.
19842021
@@ -2164,14 +2201,7 @@ def test_bootstrap_single_cluster_returns_nan(self):
21642201
first_treat="first_treat",
21652202
aggregate="all",
21662203
)
2167-
assert np.isnan(r.overall_se)
2168-
assert np.isnan(r.coef_var)
2169-
assert np.isnan(r.bootstrap_results.overall_att_se)
2170-
assert all(np.isnan(x) for x in r.bootstrap_results.overall_att_ci)
2171-
if r.bootstrap_results.event_study_ses:
2172-
assert all(np.isnan(v) for v in r.bootstrap_results.event_study_ses.values())
2173-
if r.bootstrap_results.group_ses:
2174-
assert all(np.isnan(v) for v in r.bootstrap_results.group_ses.values())
2204+
_assert_full_bootstrap_nan(r)
21752205

21762206
def test_bootstrap_single_psu_survey_returns_nan(self):
21772207
data, _ = _add_survey_cols(generate_test_data(n_units=80, seed=17))
@@ -2188,8 +2218,7 @@ def test_bootstrap_single_psu_survey_returns_nan(self):
21882218
survey_design=design,
21892219
aggregate="event_study",
21902220
)
2191-
assert np.isnan(r.overall_se)
2192-
assert np.isnan(r.bootstrap_results.overall_att_se)
2221+
_assert_full_bootstrap_nan(r)
21932222

21942223
# ---- metadata reflects the post-drop fit sample (codex P2) ----
21952224

0 commit comments

Comments
 (0)