SpilloverDiD conley + survey + lag>0 via panel-block composition (Wave E.2 follow-up)#477
Conversation
…e E.2 follow-up) Extends the panel-aware stratified-Conley spatial sandwich (Wave E.2 cross- sectional, PR #474) to `conley_lag_cutoff > 0` by adding a within-PSU serial Bartlett HAC term (Newey-West 1987 separable form). The composition `meat = meat_spatial + meat_serial` has disjoint index sets, exactly matching the no-survey panel-block decomposition at `diff_diff.conley._compute_conley_meat`. Methodology — documented synthesis of: - Conley (1999) spatial-HAC - Newey-West (1987) serial Bartlett kernel weights `(1 - |t-s|/(L+1))` - Binder (1983) / Gerber (2026) Prop 1 stratified TSL on Wave D Gardner GMM influence functions Serial term uses per-period within-stratum centering (Binder TSL form, matching the spatial helper); panel-wide per-stratum FPC (the serial sum is a panel-level construct, so the cluster set is panel-wide); hardcoded Bartlett serial kernel regardless of `conley_kernel` (mirrors `conley.py:951-965`); panel-wide dense time codes for lag math (matches `conley.py:940` R deviation). Supported surface — requires an effective PSU: either an explicit `survey_design.psu` OR a `cluster=<col>` argument that gets injected as the effective PSU per Wave E.1's `_inject_cluster_as_psu` routing. No-effective-PSU survey designs (weights-only / strata-only WITHOUT a cluster fallback) raise `NotImplementedError` post-resolution at `SpilloverDiD.fit` per `feedback_no_silent_failures`: the pseudo-PSU = obs-index fallback would silently zero the serial sum (each pseudo-PSU appears in exactly one period). Routing the serial loop to `conley_unit` would mix IF allocators with the spatial term and is queued as a follow-up. Code changes: - New sibling helper `_compute_stratified_serial_bartlett_meat` in `diff_diff/two_stage.py` (T=1 short-circuit, three-mode singleton-stratum branching with FPC inside the multi-PSU block to avoid divide-by-zero, panel-wide mean for `lonely_psu='adjust'`, zeroed centering for singleton-active-period cells so raw scores don't leak into the serial Bartlett cross-products under unbalanced panels) - Orchestrator `_compute_stratified_conley_meat` extended with `conley_lag_cutoff` kwarg; spatial loop unchanged; serial helper called after spatial loop when `L > 0` - Dispatch in `_compute_gmm_corrected_meat` conley branch threads `conley_lag_cutoff` through - `spillover.py:2210` Wave E.2-era `NotImplementedError` gate for lag>0 + survey deleted; replaced with post-resolution fail-closed gate that fires only when `resolved_survey_fit.psu` is None AFTER cluster injection (so the documented `cluster=<col>` injection surface continues to work) Tests — 24 new methods across two classes (`TestSpilloverDiDWaveE2FollowupConleySurveyLagCutoff` and `TestSpilloverDiDWaveE2FollowupConleySurveyLagCutoffEventStudy`): - `test_a` lag=0 strict bit-identity to shipped Wave E.2 meat - `test_a2` lag=0 does NOT invoke serial helper (mock-spy) - `test_b` lag=1 invokes serial helper exactly once (mock-spy) - `test_c0` raw-vs-centered hand-check pins Binder TSL centering - `test_c1`/`test_c2` hand-computation methodology anchors at L=1 and L=2 - `test_c3` AR(1) DGP serial inflation behavioral pin (rho=0.7, > 5%) - `test_d` single-stratum lag=1 finite output - `test_e` cross-stratum independence of serial term (partition + sum) - `test_f` singleton-adjust + lag=1 no divide-by-zero - `test_f2` all-singleton-remove + lag=1 returns zero meat - `test_g` unbalanced panel + panel-wide dense time codes (hand-computed) - `test_g2` lag > T-1 well-defined - `test_h` singleton-active-period centering zeros (sparse-period regression) - `test_j` no-survey panel-block conley unchanged after gate relaxation - `test_k` replicate-weight rejection still fires - `test_l` cluster + lag=1 + survey warn-and-use-PSU - `test_m` fit-idempotency under lag=1 + survey - `test_n`/`test_n2` no-effective-PSU survey + lag>0 raises NotImplementedError - `test_n3` cluster-injected effective-PSU surface fits + matches explicit PSU - `test_r` drift goldens at lag=1 vs lag=0 (ATT invariant, SE differs) - `test_o`/`test_p`/`test_r` event-study mirror (both is_staggered branches) Existing `test_j0_panel_conley_lag_cutoff_rejected_under_survey` (Wave E.2-era gate-assertion) deleted. Docs: - REGISTRY `Variance (Wave E.2 follow-up)` subsection with documented- synthesis framing + cross-references + effective-PSU restriction - `spillover.rst` Wave E.2 follow-up stanza - CHANGELOG `[Unreleased]` bullet - `llms.txt` + `README.md` catalog entries updated - `references.rst` adds Newey-West (1987) - TODO row deleted (old deferral); new row added for the no-effective-PSU follow-up tail Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Overall Assessment ✅ Looks good Executive Summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
Execution note: this was a static diff review; I could not run the added tests in the provided environment because |
P2 (Methodology — missing PSD/finite warning on combined survey meat): mirror `_compute_conley_meat`'s finite + negative-eigenvalue guard on the combined `meat = meat_spatial + meat_serial` returned by the survey panel-block orchestrator. Both the radial 1-D Bartlett spatial kernel AND the Newey-West Bartlett serial kernel are practitioner specializations that are NOT formally PSD-guaranteed; adding two non-PSD-guaranteed terms can produce a more indefinite combined meat, so the diagnostic surface matters more on the panel-block path than the no-survey baseline. Guard fires after the saturation NaN-fail check (so genuinely-saturated meats NaN-propagate without spurious warning). P3 (Documentation — public docs missing effective-PSU restriction): README.md, diff_diff/guides/llms.txt, and docs/api/spillover.rst now mention the effective-PSU requirement for `conley_lag_cutoff > 0` (weights-only / strata-only without cluster fallback raises NotImplementedError). REGISTRY's Restrictions list already had the caveat; the public-surface docs are now consistent. P3 (Wording — single-stratum reduction overclaim): REGISTRY and CHANGELOG single-stratum reduction descriptions clarified to "Conley sandwich on within-stratum-CENTERED PSU totals" (NOT plain). The Binder TSL centering is retained at H=1 — under survey weights the per-period stratum mean is always subtracted from PSU scores before the kernel application, even when the stratum is the entire sample. The cross-sectional Wave E.2 reduction wording was already correct (centered). P3 (Documentation — test_a `full meat matrix` overclaim): tightened test_a docstring to accurately describe what is pinned (ATT AND scalar SE bit-identity via assert_array_equal), with an explicit note that full meat-matrix equality is implied — not directly asserted — because the meat matrix is not exposed on `SpilloverDiDResults`. P3 (Tech debt — code duplication, DEFERRED): the serial Bartlett kernel logic is duplicated between two_stage.py (survey path) and conley.py (no-survey path). Factoring out a shared kernel helper + shared PSD/finite guard is cosmetic and out of scope for this PR. Added a TODO.md row to track the refactor follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall Assessment ✅ Looks good Executive Summary
Methodology No findings. The new survey Code Quality No findings. Performance No findings. Maintainability
Tech Debt
Security No findings. Documentation/Tests
|
CI Codex R2 verdict: ✅ Looks good (0 P0/P1/P2). One actionable P3 (docs wording drift across 4 surfaces). Fixes: - docs/api/spillover.rst H=1 lag>0 reduction: "plain Newey-West Bartlett HAC on per-PSU score sequences" -> "Newey-West Bartlett HAC on per-period within-stratum-CENTERED per-PSU score sequences (NOT raw — Binder TSL centering is retained at H=1)". Now matches REGISTRY which was already updated in the previous push. - diff_diff/two_stage.py _compute_gmm_corrected_meat docstring: "Requires survey_design.psu set" -> "Requires an effective PSU — either explicit survey_design.psu OR cluster=<col> injected as the effective PSU per Wave E.1's _inject_cluster_as_psu routing". Aligns the inline docstring with the actual post-resolution gate at spillover.py. - CHANGELOG.md: removed "full meat matrix" overclaim. The lag=0 regression pin uses assert_array_equal on user-visible ATT + scalar SE (the meat matrix is not exposed on SpilloverDiDResults); the test_a2 mock-spy independently asserts the serial helper isn't invoked at lag=0. - tests/test_spillover.py class docstring: same "full meat matrix" wording trimmed to "ATT and scalar SE bit-identity (test_a) plus mock-spy that the serial helper isn't invoked (test_a2)". P3 informational items NOT addressed (out of scope for this PR per the recurring codex assessment): - Serial Bartlett kernel duplication between two_stage.py + conley.py (TODO.md row added in previous push; refactor would touch the no-survey panel-block path) - no-effective-PSU survey + lag>0 fail-closed restriction (TODO.md row exists; methodology derivation pending) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall Assessment ✅ Looks good Executive Summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
|
…rate CI Codex R3 verdict: ✅ Looks good (0 P0/P1/P2). One actionable P3 (lag=0 narrative wording drift across 4 surfaces). The "orchestrator early-returns" phrasing was technically inaccurate: at lag=0 the orchestrator does NOT early-return. It still runs the spatial per-period loop, the saturation NaN-fail guard, and the new combined-meat PSD/finite guard. The lag=0 guarantee is narrower: the serial helper is not INVOKED, so `meat_serial` contributes nothing to the combined meat. Fixes: - docs/api/spillover.rst H=1 lag>0 reduction note + lag=0 reduction line: "orchestrator early-returns before invoking the serial helper" -> "orchestrator skips the serial helper invocation when L = 0 so meat_serial does not contribute; the test_a2 mock-spy independently asserts the helper isn't invoked". - docs/methodology/REGISTRY.md lag=0 reduction note: clarify that the spatial loop + saturation guard + new PSD/finite guard still run; the guarantee is on user-visible ATT + scalar SE, not on the meat matrix. - tests/test_spillover.py test_a + test_a2 docstrings: reword "early-return" to "skips the serial helper invocation"; explicitly note that full meat-matrix equality is NOT asserted (only ATT + scalar SE). - CHANGELOG.md: mirrored wording fix. P3 informational items NOT addressed (both TODO-tracked): - Serial Bartlett kernel duplication between two_stage.py + conley.py - no-effective-PSU survey + lag>0 fail-closed restriction Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall Assessment ✅ Looks good Executive Summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
|
Release notes consolidate 8 PRs since 3.4.0 (2026-05-19): Public-surface variance lifts: - SpilloverDiD survey_design on HC1/CR1 via Binder TSL (Wave E.1, igerber#468) - SpilloverDiD vcov_type=conley + survey_design via stratified-Conley on PSU totals (Wave E.2, igerber#474) + lag_cutoff>0 follow-up (igerber#477) - SunAbraham vcov_type ∈ {classical, hc1, hc2, hc2_bm} (Phase 1b 1/8, igerber#472) - WLS-CR2 Bell-McCaffrey gates lifted via clubSandwich port (igerber#475) Methodology-review-tracker promotions (mostly docs/tests): - PreTrendsPower R pretrends parity goldens (PR-C, igerber#471) - HAD methodology-review-tracker promotion (igerber#473) - ContinuousDiD methodology-review-tracker promotion (igerber#476) All changes additive; bit-equal defaults preserved across the affected estimators. No new estimators (patch-level per semver convention). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Extends the panel-aware stratified-Conley spatial sandwich (Wave E.2 cross-sectional, PR #474) to
conley_lag_cutoff > 0by adding a within-PSU serial Bartlett HAC term (Newey-West 1987 separable form). The compositionmeat = meat_spatial + meat_serialhas disjoint index sets, exactly matching the no-survey panel-block decomposition atdiff_diff.conley._compute_conley_meat._compute_stratified_serial_bartlett_meatindiff_diff/two_stage.py(T=1 short-circuit, three-mode singleton-stratum branching, panel-wide FPC, panel-wide dense time codes, zeroed centering for singleton-active-period cells)_compute_stratified_conley_meatextended withconley_lag_cutoffkwarg; spatial loop unchanged; serial helper called after when L>0SpilloverDiD.fitfor no-effective-PSU + lag>0 (fires AFTER_inject_cluster_as_psuso the documentedcluster=<col>injection surface continues to work)Methodology references
Documented synthesis of:
(1 - |t-s|/(L+1))conley.py:949-965. Documented in REGISTRY ("Centering asymmetry vs no-survey reference"): the no-survey path assumesE[scores] = 0so centering is a no-op; survey-weighted Binder TSL needs explicit centering or it inflates variance by twice the squared per-period stratum mean.survey_design.psuORcluster=<col>injected as PSU per Wave E.1's_inject_cluster_as_psu). No-effective-PSU survey designs raiseNotImplementedErrorperfeedback_no_silent_failures(pseudo-PSU = obs-index fallback would silently zero the serial sum). Tracked in TODO.md.Full details in
docs/methodology/REGISTRY.mdsection "Variance (Wave E.2 follow-up -conley_lag_cutoff > 0panel-block composition via spatial + serial Bartlett HAC)".Validation
tests/test_spillover.py(24 new test methods acrossTestSpilloverDiDWaveE2FollowupConleySurveyLagCutoffandTestSpilloverDiDWaveE2FollowupConleySurveyLagCutoffEventStudy). Existingtest_j0_panel_conley_lag_cutoff_rejected_under_survey(Wave E.2-era gate assertion) deleted._scratch/wave_e2_followup_smoke.pyhand-computation anchor for the methodology composition.Security / privacy