Skip to content

feat(v4): StackedDiD post-fit aggregate() + honest/pretrends container admission (2(b) PR-2, M-024) - #747

Merged
igerber merged 4 commits into
mainfrom
feat/v4-stacked-aggregate-shim
Aug 3, 2026
Merged

feat(v4): StackedDiD post-fit aggregate() + honest/pretrends container admission (2(b) PR-2, M-024)#747
igerber merged 4 commits into
mainfrom
feat/v4-stacked-aggregate-shim

Conversation

@igerber

@igerber igerber commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Summary

  • StackedDiD's event-study surface is now ALWAYS computed at fit (v4 program 2(b) PR-2; ledger row M-024 flips to shimmed): the pooled regression always included the event-time interactions, so only the extraction and (under hc2_bm) the per-event Bell-McCaffrey contrast DOFs were fit-time-gated - both gates are removed. Plain fits gain summary()'s event-study table, a total to_dataframe('event_study'), plot_event_study rendering, and DiagnosticReport's heterogeneity (always) / parallel_trends (kappa_pre >= 2) checks.
  • StackedDiD.fit(aggregate=) deprecates via the shared NOT_SUPPLIED sentinel (warn-and-still-work; group/all/unknown-value ValueErrors persist after the warning; the stacked_did() wrapper forwards the shim). StackedDiDResults adopts AggregationMixin: aggregate('event_study') is a pure builder view carrying base_period='universal' + singleton reference_event_times provenance; aggregate('simple') relays the stored overall inference bit-exactly (target='att', n = n_treated_units/'units' treated-only scope, df = inference_df).
  • compute_honest_did / compute_pretrends_power admit Stacked-sourced containers (second M-093 pre-cut amendment): kappa_pre >= 2 required; honest additionally needs a non-singular pre-period covariance; analytical fits carry df_survey=None (normal-theory FLCI critical values - deliberate, REGISTRY Note); withheld-inference rows (finite se, non-finite p - hc2_bm BM-DOF fail-close, replicate-undefined) admit with a source-scoped warning in both consumers; the seven producer-derived guard messages derive the producer from surface.source with producer-conditional remedies.
  • Cross-producer container hardening: EventStudyResults.__post_init__ now copies vcov/vcov_index (the post-fit view aliased the producer's stored matrix; int index labels keep their dtype). The practitioner "Check sub-experiment balance" step moves off the colliding heterogeneity step key.
  • Ledger/design/docs: M-024 shimmed (phase 5), M-093 amended, v4-design §5/§6 amended (container copy contract + view-relay exception), REGISTRY M-024/admission Notes + HonestDiD/PreTrendsPower mirrors, REPORTING.md covariance-routing updates, variance-conventions tail-df pin (309.0,)*3 + regenerated table, guides/README/API/choosing/notebook migrated to the post-fit route, ~35 deprecated call sites swept.

Methodology references (required if estimator / math changes)

  • Method name(s): Stacked DiD (trimmed aggregate ATT), Rambachan-Roth honest sensitivity bounds, Roth pre-trends power
  • Paper / source link(s): Wing, Freedman & Hollingsworth (2024, NBER WP 32054); Rambachan & Roth (2023, REStud); Roth (2022, AER: Insights)
  • Any intentional deviations from the source (and why): documented under REGISTRY.md ## StackedDiD Note (post-fit aggregate() - row M-024) and Note (container admission - rows M-024/M-093) - the widened hc2_bm noise-floor carve-out (fail-closed + warned), normal-theory FLCI critical values on analytical Stacked containers (the overall post-average contrast df is not a design-level cluster df), and the admit-plus-warn treatment of withheld-inference rows (bounds/power consume only beta/sigma, which remain valid).

Validation

  • Tests added/updated: tests/test_aggregate_contract.py (TestStackedShim/TestStackedAggregate incl. BM m=1-vs-batched DOF parity and in-process overall-inference reconstruction at rtol 1e-14), tests/test_event_study_consumers.py (TestStackedContainerAdmission: end-to-end honest/pretrends, source-flip parity, guard-message reach tests, withheld-inference warning trio, singular-covariance split, rank-drop gap pin, provenance ladder), tests/test_stacked_did.py (pin flips + sweep + summary/plot/legacy-pickle pins), tests/test_variance_conventions.py (tail-df pin + regenerated committed table), tests/test_diagnostic_report.py (two-check flip pins), tests/test_business_report.py (BR schema + next_steps retention), tests/test_practitioner.py (distinct step-key pin), tests/test_guides.py (fit-block sentinel-line pin), plus mechanical aggregate= sweeps in tests/test_methodology_stacked_did.py, tests/test_event_study_surface.py, tests/test_survey_estimator_validation.py.
  • Backtest / simulation / notebook evidence (if applicable): docs/tutorials/13_stacked_did.ipynb migrated to the post-fit route and re-executed clean via nbmake. Perf (median plain fit, 120x12 panel, 30 reps): hc1 3.43 -> 3.84 ms, hc2_bm 14.41 -> 16.48 ms - the deliberate always-computed-surface cost; numerical parity with the pre-change paths pinned at rtol 1e-14.

Security / privacy

  • Confirm no secrets/PII in this PR: Yes

…r admission (2(b) PR-2, M-024)

The event-study surface is always computed at fit (both fit-time gates
removed, incl. per-event hc2_bm BM DOFs); fit(aggregate=) deprecates via
the NOT_SUPPLIED sentinel (warn-and-still-work; value validation kept).
StackedDiDResults adopts AggregationMixin: aggregate('event_study') is a
pure builder view with base_period="universal" + singleton
reference_event_times provenance; aggregate('simple') relays the stored
overall inference bit-exactly (target="att", n=n_treated_units/units,
df=inference_df). compute_honest_did/compute_pretrends_power admit
Stacked-sourced containers (kappa_pre >= 2; honest needs a non-singular
pre-covariance); the seven container guard messages derive the producer
from surface.source with producer-conditional remedies; withheld-inference
rows admit with a source-scoped warning in both consumers.
EventStudyResults.__post_init__ now copies vcov/vcov_index (cross-producer
de-aliasing; int index dtype preserved). DiagnosticReport's heterogeneity
+ parallel_trends (kappa_pre >= 2) checks now run on plain fits with a
per-type PT skip remediation; the practitioner balance step moves off the
colliding "heterogeneity" step key. Ledger: M-024 -> shimmed (phase 5),
M-093 second pre-cut amendment; v4-design §5/§6 amended (view-relay
exception + container copy contract); REGISTRY M-024/admission Notes;
variance-conventions tail-df pin (309,)*3 + regenerated table; guides/
README/API/choosing/notebook migrated to the post-fit route.
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Overall assessment

⚠️ Needs changes — one unmitigated P1 methodology/edge-case finding.

Executive summary

  • StackedDiD, HonestDiD, and PreTrendsPower are affected.
  • P1: Stacked containers can falsely claim a common observed reference period on gapped panels.
  • BM batching and analytical normal-theory FLCI are documented deviations, not defects.
  • No security or material code-quality issues found.

Methodology

P1 — Common-reference provenance is not verified

  • Location: diff_diff/stacked_did_results.py:L193-L208, diff_diff/stacked_did.py:L938-L946, diff_diff/stacked_did.py:L1164-L1176
  • Impact: reference_event_times always claims (-1 - anticipation,), while trimming checks only panel bounds and the fit always synthesizes that reference row. On a gapped/ragged panel with no observations at the omitted event time, rank handling may choose an observed horizon as the effective baseline. HonestDiD/PreTrendsPower can then accept coefficients as satisfying Rambachan–Roth’s common δ₀ = 0 normalization and return sensitivity/power results for the wrong normalization.
  • Concrete fix: Before fitting or constructing the container, require treated and control support at the reference event time for every retained sub-experiment. Fail closed or trim unsupported sub-experiments; do not emit singleton reference provenance unless verified.

P3 — Documented methodology choices

  • Location: docs/methodology/REGISTRY.md:L2084-L2085
  • Impact: Analytical Stacked containers use normal-theory HonestDiD critical values, and BM contrasts now share a batch-relative numerical guard.
  • Concrete fix: None. Both choices are explicitly documented and fail closed where applicable.

Code Quality

No findings.

Performance

P3 — Deliberate fit-time overhead

  • Impact: Always extracting the event-study surface and BM DOFs increases fit time.
  • Concrete fix: None required; the tradeoff is documented and benchmarked.

Maintainability

No findings.

Tech Debt

P3 — Tracked practitioner-key collisions

  • Location: TODO.md:L24
  • Impact: Similar collisions remain for other estimators, but the StackedDiD collision is fixed.
  • Concrete fix: Follow the existing TODO row; no action required in this PR.

Security

No findings or exposed secrets identified.

Documentation/Tests

P2 — Covariance requirement is understated

  • Location: docs/methodology/REGISTRY.md:L2085, diff_diff/honest_did.py:L838-L844
  • Impact: Documentation says HonestDiD requires a nonsingular pre-period covariance, but code correctly checks the full retained pre/post covariance, as Rambachan–Roth Assumption 3 requires.
  • Concrete fix: Change the Registry and docstrings to “full retained event-study covariance.”

Path to Approval

  1. Validate observed common-reference support before assigning Stacked provenance or admitting the container.
  2. Add gapped-panel tests where a - 1 - anticipation is absent, asserting both consumers fail closed rather than returning results.

…nce-wording fix (CI review R1)

P1 (verified by execution): on a gapped panel where every retained
cohort's calendar period a-1-anticipation is absent, the omitted
reference cell is empty, the design rank-drops, and QR pivoting silently
re-normalizes against an arbitrary horizon while the surface synthesizes
the e=-1-k reference row and the container certifies
reference_event_times/base_period="universal" - a fabricated delta_0=0
claim HonestDiD/PreTrendsPower would trust. fit() now raises a
fail-closed ValueError when the reference cell lacks treated or control
rows stack-wide (both cells are load-bearing: either absence collinears
a design block). Partial support - some cohorts' own ref period absent
while the pooled omitted category stays populated - still fits, verified
by execution: the coefficient-vector normalization is real and the
per-cohort raggedness is the documented ragged-window estimand
composition, so no legitimate fit changes. Gapped-panel tests pin both
sides; REGISTRY/CHANGELOG document the guard.

P2: the singularity requirement wording understated the check - honest's
container branch validates the FULL retained event-study covariance
(pre+post sub-block, Rambachan-Roth Assumption 3), not just the
pre-period block; reworded in the docstrings, REGISTRY, ledger notes,
and CHANGELOG.
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: a4d6910b490d810a29e724cf7163e24bed14a49d


Overall assessment

⚠️ Needs changes — one prior P1 is only partially resolved.

Executive summary

  • StackedDiD, HonestDiD, and PreTrendsPower are affected.
  • The new reference guard checks raw rows, not positive-weight WLS support.
  • The prior covariance-documentation finding is resolved.
  • Normal-theory FLCI and BM batching are documented choices.
  • No security issues identified.

Methodology

P1 — Reference support ignores effective weights

  • Location: diff_diff/stacked_did.py:L651-L668, diff_diff/stacked_did.py:L701-L715, diff_diff/stacked_did.py:L1462-L1476, diff_diff/stacked_did_results.py:L193-L208
  • Impact: The prior finding is only partially resolved. Wing et al.’s Equation 3 is a Q-weighted WLS regression, but the guard checks raw row presence before composing weights. Under aggregate weighting, a control-only reference cell receives Q-weight zero when its sub-experiment has no treated reference rows. Survey weights can similarly zero an entire reference cell. The guard passes, yet the effective WLS baseline is empty and rank handling can re-normalize the model while the container still claims a common reference. HonestDiD/PreTrendsPower may then consume incorrectly normalized coefficients.
  • Concrete fix: Run the guard after composed_weights is built and require positive total weight for both treated and control reference cells.

P3 — Documented methodology choices

  • Location: docs/methodology/REGISTRY.md:L2084-L2085
  • Impact: Analytical Stacked containers use normal-theory HonestDiD critical values, and BM contrasts use the documented batch-relative guard.
  • Concrete fix: None required.

Code Quality

No additional findings.

Performance

P3 — Deliberate fit-time overhead

  • Impact: Always materializing the event-study surface adds documented fit cost.
  • Concrete fix: None required.

Maintainability

No findings.

Tech Debt

P3 — Tracked practitioner-key collisions

  • Location: TODO.md:L24
  • Impact: Similar collisions remain for other estimators.
  • Concrete fix: Follow the existing TODO row.

Security

No findings or exposed secrets identified.

Documentation/Tests

No separate findings. The covariance wording now correctly describes the full retained covariance. The P1 fix needs positive-weight reference-support tests.

Path to Approval

  1. Validate treated and control reference support using composed_weights > 0.
  2. Add a ragged aggregate-weighting test where raw reference rows exist but all control reference Q-weight is zero.
  3. Add a survey-weight test where one reference cell has only zero-weight observations.

…not raw rows (CI review R2)

Verified by execution: Equation 3 is Q-weighted WLS, so a reference cell
can exist in raw rows yet carry zero effective mass - (a) under
aggregate weighting, a sub-experiment's control rows get Q=0 at any
event time where that sub-experiment has no treated rows, so a ragged
panel whose only control-reference rows sit in such sub-experiments
passed the raw-row guard while the effective control baseline was empty
(reproduced: rank drop, distorted surface, container still certifying
reference_event_times=(-1,)); (b) survey pweights can zero a cell the
same way. The guard now runs after weight composition and requires
positive total composed weight in BOTH reference cells, with the causes
enumerated in the error. Two new tests pin the zero-Q ragged case and
the survey-zeroed case; the stack-wide-missing and partial-support
scenarios keep their prior behavior (re-verified). REGISTRY/CHANGELOG
descriptions updated to the weighted formulation.
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 1b847304da11c767d9bbcb132ccfc3f2d03211db


Overall assessment

Looks good — the prior P1 is resolved; no unmitigated P0/P1 findings.

Executive summary

  • Affected methods: StackedDiD, HonestDiD, and PreTrendsPower.
  • The reference guard now correctly evaluates composed Q/survey weights.
  • Both treated and control reference cells must have positive effective mass.
  • Regression tests cover zero-Q and zero-survey-weight cases.
  • Documented inference deviations remain P3 informational.

Methodology

P3 — Prior reference-support finding resolved

  • Location: diff_diff/stacked_did.py:L668-L727
  • Impact: The guard now follows Wing et al.’s Q-weighted WLS support semantics and prevents fabricated reference normalization.
  • Concrete fix: None required.

P3 — Documented methodology choices

  • Location: docs/methodology/REGISTRY.md:L2084-L2085
  • Impact: Always-materialized event studies, normal-theory Stacked HonestDiD FLCIs, withheld-inference admission, and BM batching are explicitly documented deviations/choices.
  • Concrete fix: None required.

Code Quality

No findings.

Performance

P3 — Deliberate fit-time overhead

  • Impact: Always constructing the event-study surface adds documented computation.
  • Concrete fix: None required.

Maintainability

No findings.

Tech Debt

P3 — Tracked practitioner-key collisions

  • Location: TODO.md:L24
  • Impact: Similar collisions in other estimators remain explicitly tracked.
  • Concrete fix: None required for this PR.

Security

No findings or exposed secrets identified.

Documentation/Tests

No findings. Effective-weight tests cover aggregate Q-weight and survey-weight failures at tests/test_stacked_did.py:L2187-L2263.

Runtime tests could not be executed because the review environment lacks pytest/numpy; changed Python files passed AST parsing.

@igerber igerber added the ready-for-ci Triggers CI test workflows label Aug 3, 2026
…ue tolerance

Two test-only fixes for the PR CI matrix:

- test_plot_event_study_renders_on_plain_fit failed on every leg with
  ModuleNotFoundError: matplotlib is an optional dependency the CI test
  env does not install. Guard with pytest.importorskip per the existing
  convention (test_visualization.py, test_bacon.py,
  test_event_study_consumers.py).

- test_bm_dof_batch_parity_and_overall_reconstruction failed on the
  Linux x86 legs only: batched-vs-m=1 BLAS kernels differ at ~1 ULP in
  the Satterthwaite dof (OpenBLAS reassociation), and the overall
  contrast's deep-tail p-value (~1e-54 at t~29) amplifies that relative
  noise by ~t^2 (d ln p ~ -t dt), landing at 1.5e-14 - just over the
  uniform rtol=1e-14. Split the assertion: t/CI stay at rtol=1e-14, the
  p-value alone loosens to rtol=1e-12 with the amplification documented
  inline. macOS Accelerate and Windows were bit-stable; no library code
  changes.
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 1c28e7f729b78b8476b19ed12c9f83b3a4a52982


Overall assessment

Looks good — the prior P1 is resolved; no unmitigated P0/P1 findings.

Executive summary

  • Affected methods: StackedDiD, HonestDiD, and PreTrendsPower.
  • The reference-support guard now checks final composed WLS weight for both treated and control cells.
  • Consumer admission preserves common-reference normalization and full covariance ordering.
  • Inference deviations are explicitly documented in the Methodology Registry.
  • Regression coverage includes Q-weight, survey-weight, covariance, anticipation, and NaN fail-closed paths.

Methodology

P3 — Prior reference-support finding resolved

  • Location: diff_diff/stacked_did.py:L684-L727
  • Impact: Empty effective reference cells now fail closed before rank handling can fabricate the reported normalization.
  • Concrete fix: None required.

P3 — Documented methodology choices

  • Location: docs/methodology/REGISTRY.md:L2084-L2085
  • Impact: Always-materialized surfaces, batched BM DOFs, normal-theory Stacked HonestDiD FLCIs, and withheld-inference admission are explicitly documented deviations or implementation choices.
  • Concrete fix: None required.

Code Quality

No findings. New inference continues to use safe_inference() and preserves all-or-nothing NaN output.

Performance

P3 — Deliberate fit-time overhead

  • Impact: Plain fits now extract event-study results and calculate per-event BM DOFs.
  • Concrete fix: None; this is the documented cost of total post-fit aggregation.

Maintainability

No findings. Aggregation is isolated in StackedDiDResults, and covariance arrays are defensively copied at diff_diff/results_base.py:L434-L456.

Tech Debt

P3 — Tracked practitioner-key collisions

  • Location: TODO.md:L24
  • Impact: Similar collisions in other estimators remain explicitly tracked.
  • Concrete fix: None required for this PR.

Security

No findings or exposed secrets identified.

Documentation/Tests

P3 — Coverage is comprehensive

  • Location: tests/test_stacked_did.py:L2119-L2263, tests/test_event_study_consumers.py:L1543-L1919
  • Impact: Tests cover the prior finding, consumer admission, inference failure, covariance, and anticipation interactions.
  • Concrete fix: None required.

Runtime tests could not run because NumPy is unavailable in the review environment. All 19 affected Python files and all 25 notebook cells passed AST parsing.

@igerber
igerber merged commit 0abc754 into main Aug 3, 2026
31 checks passed
@igerber
igerber deleted the feat/v4-stacked-aggregate-shim branch August 3, 2026 23:47
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