Skip to content

feat(aggregation): CS post-fit aggregate() replays the multiplier bootstrap on bootstrapped fits (M-020) - #780

Merged
igerber merged 1 commit into
mainfrom
feat/cs-bootstrap-replay
Aug 18, 2026
Merged

feat(aggregation): CS post-fit aggregate() replays the multiplier bootstrap on bootstrapped fits (M-020)#780
igerber merged 1 commit into
mainfrom
feat/cs-bootstrap-replay

Conversation

@igerber

@igerber igerber commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • CallawaySantAnnaResults.aggregate('event_study')/aggregate('group') now WORK on bootstrapped fits (n_bootstrap > 0): the recompute levels REPLAY the fit-time multiplier bootstrap from a kit-retained BootstrapReplaySpec — the RNG state captured at weight-stream construction plus the run parameters BY VALUE — producing percentile se/CI and sup-t bands that match a fit-time fit(aggregate=...) aggregation to floating-point reassociation (assert_allclose ~1 ULP; the discrete percentile p-value is a count statistic compared at 2/n_bootstrap). Previously these levels raised NotImplementedError (M-020 notes amendment; retires the TODO #726 bootstrap re-aggregation row).
  • Mechanism is wholesale reuse: _run_multiplier_bootstrap gains a keyword-only _replay_bitgen_state injection; a value-bound _KitBootstrapAggregator host re-runs the SAME engine post-fit, and the fit-time percentile-override blocks are extracted verbatim into shared helpers so fit and replay cannot drift. seed=None fits replay, pickles replay, and post-fit set_params/attribute mutation cannot alter the replay.
  • Backend guard: Rust and NumPy weight generators produce DIFFERENT draws from the same bit-generator state, so the spec stamps bootstrap_chunking.effective_weight_backend() at capture (branch-aware — stratified/single-PSU survey and census-FPC generation is provably backend-independent and stamps "portable") and the replay fails closed on a mismatch or a pre-replay legacy pickle, naming the remedies.
  • Bug fix along the way: a fit with NO post-treatment cells crashed the shared group-bootstrap stats block (np.column_stack([])) — reachable pre-existing via the deprecated fit-time aggregate='group' and newly via the replay; now guarded to the supported zero-row result, with a regression test covering both routes.
  • Ripples: DiagnosticReport's ES-gated checks now RUN on bootstrapped plain CS fits (Bonferroni / diagonal-covariance fallbacks; replay warnings recorded and republished per section); practitioner_next_steps advises the post-fit route on bootstrapped CS fits. Sibling estimators' (EfficientDiD/ImputationDiD/TwoStageDiD/ContinuousDiD) bootstrap gates and the deprecated SDDD engine's fit-time override copy are untouched (SDDD unification sequenced with M-014; deferral recorded in the M-020 notes).
  • Docs: M-020 notes + code_refs, REGISTRY (CS aggregate note, relay-level re-warn scoping, a weight-backend note in the Survey-Aware Bootstrap section), REPORTING.md, all three LLM guides (the four-estimator exception block SPLIT — CS replays, siblings still raise), troubleshooting.rst, migration-4.0.md, v4-design.md, CHANGELOG; tutorials 02 (cells 19–21) and 09 (eight cells) rewritten to the post-fit route and re-executed against this branch's venv kernel.

Methodology references (required if estimator / math changes)

  • Method name(s): Callaway & Sant'Anna (2021) multiplier bootstrap (influence-function perturbation, percentile inference, sup-t simultaneous bands). The replay re-runs the existing engine bit-identically at the weight-stream level; no point-estimate, IF/WIF, weighting, or variance formula changes.
  • Paper / source link(s): Callaway & Sant'Anna (2021), "Difference-in-Differences with multiple time periods", J. Econometrics — see docs/methodology/REGISTRY.md CallawaySantAnna section (post-fit aggregate() note, rows M-020/M-117) and the Survey-Aware Bootstrap section's new weight-backend note.
  • Any intentional deviations from the source (and why): None methodological. Engineering contracts documented in REGISTRY: replay parity is assert_allclose (~1 ULP BLAS reassociation), never bit-identity; the derived container publishes no analytical vcov/df beside percentile inference; cross-backend replay fails closed because the two weight generators are not cross-reproducible from one RNG state.

Validation

  • Tests added/updated: tests/test_aggregate_contract.py (TestBootstrapReplay — ES/group/balance_e parity against the NATIVE fit-time surface, seedless idempotence, mutation immunity, pickle round-trip, relay order-independence, legacy/backend fail-closed pins, low-draws re-warn pin, empty-group zero-row regression on both routes; TestBootstrapReplayDesigns — bare-cluster PSU expansion, stratified-survey portability, FPC, repeated cross-sections, unbalanced panels, single-PSU NaN surfaces; TestBootstrapReplayConsumers — pretrends diag fallback + honest_did diagonal warning), tests/test_bootstrap_chunking.py (effective_weight_backend pins incl. the stale-extension case), tests/test_diagnostic_report.py (bootstrapped CS derives-and-runs + warning republication with a dedicated 49-draw fixture), tests/test_practitioner.py (bootstrapped-CS advice pins incl. positive aggregate('group') and no fit-time-kwarg assertions).
  • Backtest / simulation / notebook evidence (if applicable): tutorials 02 and 09 re-executed end-to-end on this branch (post-fit bootstrap route) and validated with nbmake against a kernel bound to this worktree's venv.

Security / privacy

  • Confirm no secrets/PII in this PR: Yes (the retained replay state is an RNG state dict + run parameters; the kit's identifier-minimization contract is unchanged and pinned).

…ootstrapped fits

CallawaySantAnnaResults.aggregate('event_study'/'group') on a bootstrapped
fit (n_bootstrap > 0) previously raised NotImplementedError; the recompute
levels now REPLAY the fit-time multiplier bootstrap from a kit-retained
BootstrapReplaySpec and publish percentile inference matching a fit-time
aggregation to BLAS reassociation (assert_allclose ~1 ULP on se/CI/cband;
the discrete percentile p-value compared at 2/n_bootstrap). M-020 notes
amendment; retires the TODO bootstrap re-aggregation row (#726).

Mechanism (wholesale reuse):
- _run_multiplier_bootstrap gains a keyword-only _replay_bitgen_state and
  snapshots the RNG state at weight-stream construction (nothing consumes
  the rng earlier, so the state fully determines the stream WITHIN one
  weight backend); the state + generation-branch identity ride
  CSBootstrapResults as plain __post_init__ attributes (public dataclass
  shape unchanged) into the kit's BootstrapReplaySpec (by value:
  seed=None fits replay, pickles carry it, post-fit set_params/attribute
  mutation cannot alter it).
- staggered_results gains _KitBootstrapAggregator, a value-bound host
  re-running the SAME engine with the injected state; the fit-time
  override blocks (percentile se/CI/p + t, group df_used clearing, sup-t
  cband rows) are extracted verbatim into shared staggered_bootstrap
  helpers so fit and replay cannot drift.
- Backend guard: Rust and NumPy generators produce DIFFERENT draws from
  the same bit-generator state (absolute Xoshiro row-seeding vs direct
  PCG64 stream), so the spec stamps bootstrap_chunking's new
  effective_weight_backend() at capture - branch-aware: stratified/
  single-PSU survey generation and census-FPC zero weights are provably
  backend-independent and stamp "portable" - and the replay fails closed
  on a mismatch (and on backend=None) naming both backends and the
  remedies. Legacy pickles without the spec fail closed with a refit
  message.
- The combined-IF gate in _prepare_event_study_aggregation also accepts
  precomputed-only callers (the kit-backed replay threads df=None).

Ripples:
- DiagnosticReport's ES-gated checks now RUN on bootstrapped plain CS
  fits (parallel_trends via Bonferroni fallback, pretrends_power/
  sensitivity via the diagonal-covariance fallback; replay warnings
  recorded and republished per section).
- practitioner_next_steps advises the post-fit route on bootstrapped CS
  fits (new pins; the deprecated fit-time kwarg form is gone from the
  advice bundle).
- Sibling estimators' (EfficientDiD/Imputation/TwoStage/Continuous)
  bootstrap gates and the SDDD engine's fit-time override copy are
  untouched (the SDDD twin unification is sequenced with M-014; deferral
  recorded in the M-020 notes).

Tests: TestBootstrapReplay (fit-time-vs-post-fit parity for ES/group/
balance_e against the NATIVE stored surface; seed=None idempotence;
set_params/mutation immunity; pickle round-trip; relay order
independence; legacy and backend fail-closed pins; low-draws re-warn
pin), TestBootstrapReplayDesigns (bare-cluster PSU expansion, stratified
survey portability, FPC, RCS, unbalanced panel, single-PSU NaN
surfaces), TestBootstrapReplayConsumers (pretrends diag fallback +
honest_did diagonal warning), effective_weight_backend unit tests, DR
derives-and-runs + warning-republication pins, practitioner pins.

Docs: M-020 notes + code_refs (+staggered_bootstrap.py,
+bootstrap_chunking.py), REGISTRY CS aggregate note + relay-level
re-warn scoping + weight-backend note in the Survey-Aware Bootstrap
section, REPORTING.md, llms.txt / llms-full.txt / llms-practitioner.txt
(the four-estimator exception block SPLIT: CS replays, siblings still
raise), troubleshooting.rst, migration-4.0.md warning block + CS row
blurbs, v4-design.md families count, CHANGELOG Unreleased entry,
tutorials 02 (cells 19-21) and 09 (eight cells) rewritten to the
post-fit route and re-executed. TODO row deleted; the DDD container-port
and EfficientDiD cross-references trued up.
@github-actions

Copy link
Copy Markdown

Overall assessment

Looks good — no unmitigated P0/P1 findings.

Executive summary

  • Replay reuses the existing Callaway–Sant’Anna multiplier-bootstrap engine; estimator, weighting, IF, and variance formulas are unchanged.
  • RNG state, bootstrap parameters, anticipation, and backend identity are propagated correctly.
  • Empty aggregations and invalid inference consistently return NaN/zero-row surfaces.
  • Documented diagonal-covariance fallbacks and replay performance costs are informational, not defects.

Methodology

  • Severity: P3 — informational
    Impact: Bootstrap event studies intentionally clear joint covariance and use the documented diagonal approximation in HonestDiD/PreTrendsPower. This approximation is not necessarily conservative, but is explicitly documented in docs/methodology/REGISTRY.md:L1055 and docs/methodology/REGISTRY.md:L5185.
    Concrete fix: None required.

The replay at diff_diff/staggered_results.py:L418-L479 invokes the same bootstrap and override implementations used during fitting, preserving the cited CS multiplier-bootstrap methodology.

Code Quality

No findings. Shared override helpers avoid duplicated inference logic, and all modified inference paths use safe_inference_batch().

Performance

  • Severity: P3 — informational
    Impact: Each recompute aggregation regenerates the complete multiplier stream and reruns the fused perturbation multiplication. This can be expensive for large fits.
    Concrete fix: None required; the cost and absence of memoization are documented.

Maintainability

No findings. The replay host snapshots every estimator attribute read by the bootstrap engine, and backend selection is centralized in effective_weight_backend().

Tech Debt

  • Severity: P3 — informational
    Impact: Equivalent replay support remains unavailable for sibling estimators.
    Concrete fix: None for this PR; these limitations are tracked in TODO.md:L36-L38.

Security

No findings. The added retained state contains bootstrap configuration and RNG state, while raw unit identifiers remain canonicalized in diff_diff/staggered.py:L5141-L5145.

Documentation/Tests

No PR findings. Tests cover parity, seedless replay, mutation immunity, pickling, backend mismatches, survey/cluster designs, empty groups, and NaN inference. Static parsing succeeded; runtime tests could not be executed because the review environment lacks NumPy, pandas, and pytest.

@igerber igerber added the ready-for-ci Triggers CI test workflows label Aug 18, 2026
@igerber
igerber merged commit 765d267 into main Aug 18, 2026
43 of 44 checks passed
@igerber
igerber deleted the feat/cs-bootstrap-replay branch August 18, 2026 22:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-ci Triggers CI test workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant