Skip to content

[Mirror] CS bootstrap event-study covariance - CI-review mirror of PR #839 - #846

Closed
igerber wants to merge 4 commits into
mainfrom
fix/cs-bootstrap-es-vcov-mirror
Closed

igerber wants to merge 4 commits into
mainfrom
fix/cs-bootstrap-es-vcov-mirror

Conversation

@igerber

@igerber igerber commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • CI-review mirror of fix(staggered): carry the bootstrap event-study covariance instead of clearing it #839 (ilgrad's fix for [Bug]: HonestDiD on a bootstrapped CallawaySantAnna event study drops the cross-horizon covariance; Δ^SD(0) covers 68% at 95% #836), same tree as the contributor's branch plus two maintainer follow-up commits. Fork PRs cannot receive CI AI review (fork-skip security gate), so this same-repo mirror exists to run review rounds against the identical tree.
  • Do not merge - fix(staggered): carry the bootstrap event-study covariance instead of clearing it #839 is the merge vehicle and preserves the contributor's authorship. Once this mirror is review-clean, the follow-up commits are pushed back to the contributor's branch (maintainerCanModify is on) and fix(staggered): carry the bootstrap event-study covariance instead of clearing it #839 merges; this PR then closes without merging. Endgame check: tree-hash equality between the two heads.
  • Contributor's commits (70afb12, 63aad7d): bootstrapped CallawaySantAnna fits (and DMLDiD replays) carry the covariance (ddof=1) of the event-study multiplier-bootstrap draws as event_study_vcov instead of clearing it. Its diagonal is the bootstrap se**2 by construction (the SEs are np.std(draws, ddof=1) of the same matrix), so HonestDiD, PreTrendsPower and DiagnosticReport read a full covariance consistent with the reported SEs instead of diag(se**2). Not stored (diagonal fallback kept) when n_bootstrap <= n_horizons (rank-deficient by construction), a draw is non-finite, or fewer than 2 PSUs.
  • Maintainer follow-up 52238be: the native HonestDiD.fit(results) route assigned event_study_vcov unvalidated while the aggregate('event_study') container route ran _validate_vcov_subblock(..., allow_singular=False); with fewer PSUs than horizons the native route returned finite bounds from a singular matrix where the container raised. Pre-existing on analytical fits; now both routes validate. Also fixes the stale llms-practitioner.txt line.
  • Maintainer follow-up 6482556: docs/v4-deprecations.yaml M-020 note still described vcov=None on bootstrap replay.

Problem (#836)

Issue reproducer and an independent Monte Carlo (Rust backend, 100 units, 500 panels, Delta^SD(M=0) interval for the average post-period effect, nominal 95%):

arm main this branch
n_bootstrap=0 93.2% 93.2%
n_bootstrap=199 67.2% (vcov None) 92.6% (vcov on 500/500)

Bootstrap vs analytical event-study correlations agree to 0.06 at 999 draws.

Methodology references (required if estimator / math changes)

  • Method name(s): Callaway & Sant'Anna (2021) multiplier bootstrap for the event-study aggregation; Rambachan & Roth (2023) HonestDiD; Roth (2022) pre-trends power.
  • Paper / source link(s): https://doi.org/10.1016/j.jeconom.2020.12.001, https://doi.org/10.1093/restud/rdad018, https://doi.org/10.1257/aeri.20210236
  • Any intentional deviations from the source (and why): R's honest_did.AGGTEobj builds Sigma from the influence-function matrix even when did reports bootstrap SEs; here Sigma on a bootstrapped fit is the covariance of the bootstrap draws, so its diagonal matches the reported SEs. Documented in REGISTRY (new CallawaySantAnna **Note:**, updated HonestDiD **Note (deviation from R):**, PreTrendsPower Sigma_22 note, post-fit aggregate() note).

Validation

  • Tests added/updated: tests/test_staggered.py (draw-covariance contract across varying/universal/anticipation bases; n_bootstrap <= n_horizons not stored), tests/test_honest_did.py (bootstrap vcov carried; native/container parity; below-draw-limit diagonal fallback; NEW singular few-PSU case raises on both routes for analytical and bootstrap; survey-df test moved from 4 to 10 nested PSUs because its old design was intrinsically singular - survey ES vcov rank <= df = 2 over 6 horizons), tests/test_aggregate_contract.py (replay vcov parity with fit time; consumers read the full covariance), tests/test_diagnostic_report.py (bootstrapped CS now takes joint_wald_event_study), tests/test_dml_did.py (replay carries the covariance).
  • Local runs (Rust backend): every test file feeding results into HonestDiD/PreTrendsPower/DiagnosticReport (1797 passed, 1 skipped); the 17 pre-merge-resolved suites (1636 passed); ledger/docs/changelog suites (545 passed). Pure-Python backend on the bootstrap classes: 55 passed. ruff, black, mypy clean; premerge_scan.py 0 findings.
  • Backtest / simulation / notebook evidence (if applicable): coverage table above (the issue's 1000-panel table reproduces within Monte Carlo error).

Security / privacy

  • Confirm no secrets/PII in this PR: Yes

Changelog

  • changelog.d/ fragment added (or N/A - no user-visible change): Yes - changelog.d/20261005-cs-bootstrap-event-study-vcov.md (Fixed + Behavioral Changes).

ilgrad and others added 4 commits October 5, 2026 15:07
… clearing it

With n_bootstrap > 0, CallawaySantAnna cleared event_study_vcov at fit time
and in the post-fit aggregate("event_study") replay, so HonestDiD fell back
to diag(se**2) and its Delta^SD intervals under-covered (#836). The clearing
kept the analytical VCV away from percentile-bootstrap SEs, but the
multiplier bootstrap already holds a consistent covariance: the event-study
SEs are np.std(draws, ddof=1) of es_boot_matrix, so the covariance of the
same draws has se**2 on its diagonal by construction.

CSBootstrapResults now carries that covariance over the horizons with a
finite bootstrap SE, and fit() and the replay (CallawaySantAnna and DMLDiD)
hand it on. It is not stored when n_bootstrap does not exceed the number of
horizons (B draws give rank <= B - 1), when a draw is non-finite, or with
fewer than two PSUs; consumers then keep the diagonal fallback. PreTrendsPower
and the DiagnosticReport parallel-trends check now read the same matrix.
With fewer PSUs than event-study horizons the bootstrap covariance is
singular, as the analytical one is, so HonestDiD's container route now raises
its singular-covariance ValueError where it used to fall back to the diagonal.
State it under Behavioral Changes and in the CallawaySantAnna REGISTRY note.
…e the container route

HonestDiD.fit(results) on a CallawaySantAnnaResults object assigned the
stored event_study_vcov unvalidated, while the aggregate('event_study')
container route ran _validate_vcov_subblock(..., allow_singular=False).
With fewer PSUs than horizons the stored matrix (analytical or bootstrap)
is singular, so the native route returned finite bounds where the
container route raised. Run the same check on the native route.

- tests: singular few-PSU regression on both routes (analytical and
  bootstrap); the survey-df test moves from 4 PSUs (df=2, rank-2 VCV
  over 6 horizons - singular) to 10 nested PSUs (df=8).
- docs: REGISTRY CS/HonestDiD notes, changelog fragment, and the
  practitioner guide line that still described the diagonal fallback on
  bootstrapped CS containers.
@github-actions

Copy link
Copy Markdown

Overall assessment

✅ Looks good

Executive summary

  • No unmitigated P0/P1 findings.
  • Bootstrap covariance is computed from the same draws and ddof=1 convention as reported SEs.
  • Rambachan–Roth’s nonsingularity assumption is now enforced consistently on both HonestDiD routes.
  • The deviation from R is explicitly documented in the Methodology Registry.
  • Changelog and targeted edge-case tests are included.

Methodology

  • Severity: P3 — Documented deviation from R
    • Impact: Bootstrap CS/DMLDiD surfaces use bootstrap-draw covariance, whereas R’s honest_did.AGGTEobj uses the analytical influence-function covariance. This is explicitly documented at docs/methodology/REGISTRY.md:L1097-L1120 and docs/methodology/REGISTRY.md:L6482-L6483; under the review policy it is informational, not a defect.
    • Concrete fix: None required.

The implementation is internally consistent: covariance and SE use the same draws and ddof=1 at diff_diff/staggered_bootstrap.py:L693-L717. The HonestDiD validation at diff_diff/honest_did.py:L1460-L1470 enforces the finite, symmetric, matching-diagonal, nonsingular covariance required by Rambachan–Roth Assumption 3. Supplying the full pre-period block also aligns with Roth’s (\Sigma_{22}) contract.

Code Quality

No findings. Fit-time and replay paths share the same bootstrap result fields and override helpers.

Performance

No findings. The added covariance calculation is small relative to the existing multiplier-bootstrap draw generation.

Maintainability

No findings. Covariance provenance and indexing are explicit, and replay propagation is centralized at diff_diff/staggered_results.py:L481-L498.

Tech Debt

No untracked issues introduced. The documented low-draw and unavailable-covariance fallbacks remain explicit.

Security

No findings or apparent secrets/PII.

Documentation/Tests

No findings. Tests cover base-period and anticipation variants, insufficient draws, native/container parity, singular few-PSU designs, DML replay, and DiagnosticReport routing. A changelog.d/ fragment documents the behavior change.

Test execution was unavailable because pytest is not installed in the review environment; assessment is based on static inspection.

@igerber igerber closed this Oct 14, 2026
@igerber
igerber deleted the fix/cs-bootstrap-es-vcov-mirror branch October 14, 2026 19:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants