SpilloverDiD vcov_type='conley' + survey_design= via panel-aware stratified-Conley sandwich on per-period PSU totals (Wave E.2)#474
Merged
Conversation
…tified-Conley sandwich on per-period PSU totals (Wave E.2)
Composes Conley (1999) spatial-HAC with Gerber (2026, arXiv:2605.04124)
Proposition 1 Binder TSL (the Wave E.1 foundation) and the Wave D Gardner
GMM first-stage uncertainty correction (Butts 2021 §3.1 + Gardner 2022 §4)
applied to SpilloverDiD's ring-indicator stage-2 design. No reference
software combines all three on a two-stage influence function.
Panel-aware composition (preserves the library's existing
`conley_lag_cutoff = 0` semantic at `diff_diff.conley._compute_conley_meat`
— "within-period spatial only, exclude cross-period pairs"): per-PSU
spatial centroids are panel-constant (mean of per-obs `conley_coords`
within each PSU, computed once on the full active sample). For each
period t, SpilloverDiD's per-obs Hájek-weighted Wave D IF psi_i is
aggregated to per-period PSU totals `S_psu_t[g] = sum_{i in PSU g, time t}
psi_i`; the within-stratum sandwich applies the Conley kernel between
panel-constant PSU centroids scaled by the Binder FPC factor
`(1 - f_h) * n_h/(n_h-1)`. Cross-stratum kernel weights are exactly zero
by sampling design. Total meat is `sum_t sum_h M_h_t`.
Implementation:
- New `_compute_stratified_conley_meat_from_psu_scores` helper in
`diff_diff/survey.py` (parallel to existing Binder helper; per-stratum
Conley sandwich; singleton lonely_psu="adjust" `continue` to skip FPC
parity with Binder).
- New panel-aware dispatch wrapper `_compute_stratified_conley_meat` in
`diff_diff/two_stage.py`: precomputes panel-constant centroids per
explicit PSU; per-period loop re-builds the PSU set from ACTIVE rows
in each period (handles both explicit-PSU and implicit-PSU=obs
layouts correctly without zero-padding off-period rows).
- `_compute_gmm_corrected_meat` conley branch routes to the new wrapper
when `resolved_survey is not None`; the `resolved_survey is None`
branch is bit-identical to Wave D.
- Lifts `spillover.py:2201` upfront and `two_stage.py:217` helper-level
NotImplementedError gates on conley+survey.
- Upfront gate stays for `conley_lag_cutoff > 0` (serial Bartlett HAC
composition is a separate follow-up in TODO.md).
- Saturated-design NaN-fail mirrors Wave E.1
("Wave E.2 stratified-Conley sandwich: df_survey = 0..." UserWarning).
- `cluster_ids` intentionally dropped at the dispatch boundary (after
PSU aggregation every PSU is its own cluster; threading would zero
all cross-PSU kernel pairs).
Out of scope (deferred to follow-up): `conley_lag_cutoff > 0` serial
Bartlett composition with the panel-aware stratified-Conley spatial
sandwich; replicate-weight variance (inherits Wave E.1 gate);
LinearRegression-side conley+survey at `linalg.py:2853` (separate
Bertanha-Imbens Phase 5 roadmap); DiagnosticReport routing for the
new combination (Wave F).
Tests: `TestSpilloverDiDWaveE2ConleySurveyDesign` (21 tests including
no-survey conley path bit-identical-to-Wave-D + mock-spy on dispatch;
panel-aware per-period sum invariant on orchestrator + helper
composition; multi-coord PSU + finite_mask centroid-stability
regression; hand-computation methodology anchor; single-stratum ≡ plain
Conley on PSU totals; cross-stratum independence on survey helper;
Binder vs Conley singleton-adjust FPC skip parity; lonely-PSU
sensitivity; FPC large ≡ no-FPC, FPC = n_h zeros stratum; saturated
NaN-fail with `pytest.warns(match="Wave E.2 stratified-Conley")`;
replicate-weight + non-pweight + panel-Conley-lag rejections; cluster
warn-and-use-PSU; fit idempotency; finite_mask survey-array
subsetting; no-PSU coverage — weights-only `SurveyDesign(weights=...)`,
strata-only `SurveyDesign(weights=..., strata=...)`, and a per-period
re-index unit invariant pinning that no cross-period spatial pairs leak
into the meat on implicit-PSU layouts). Plus
`TestSpilloverDiDWaveE2ConleySurveyDesignEventStudy` (3 tests:
event-study path on both `is_staggered` branches; drift goldens at
`rtol=1e-12 / atol=1e-14`). Full SpilloverDiD (250 tests) + TwoStageDiD
survey (94 tests) suite passes. Rust backend Wave E.2 tests
(`DIFF_DIFF_BACKEND=rust pytest -k WaveE2`) all pass.
Docs: REGISTRY + spillover.rst + CHANGELOG + llms.txt + README +
references.rst synthesis-framing first-draft; Wave E.1 entry's "Public
surface restrictions" bullet updated to past-tense the conley+survey
gate reference; TODO.md Wave E.2 row deleted; new follow-up row added
for the `conley_lag_cutoff > 0` serial Bartlett composition.
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
|
Owner
Author
|
/ai-review |
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall Assessment ✅ Looks good Executive Summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
|
HanomicsIMF
pushed a commit
to HanomicsIMF/diff-diff
that referenced
this pull request
May 22, 2026
…e E.2 follow-up) Extends the panel-aware stratified-Conley spatial sandwich (Wave E.2 cross- sectional, PR igerber#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>
HanomicsIMF
pushed a commit
to HanomicsIMF/diff-diff
that referenced
this pull request
May 22, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
SpilloverDiD(vcov_type="conley", survey_design=...)is now supported via a panel-aware stratified-Conley sandwich on per-period PSU totals (Wave E.2). Lifts the Wave E.1NotImplementedErroratspillover.py:2201upfront andtwo_stage.py:217helper-level.conley_lag_cutoff = 0semantic atdiff_diff/conley.py:_compute_conley_meat("within-period spatial only — exclude cross-period pairs"). For each periodt, per-obs Hájek-weighted Wave D IFpsi_iis aggregated to per-period PSU totalsS_psu_t[g] = sum_{i in PSU g, time t} psi_i; per-PSU centroids are panel-constant (mean of per-obsconley_coordswithin each PSU, computed ONCE on the full active sample); within-stratum sandwich applies the Conley kernel between PSU centroids scaled by Binder FPC(1 - f_h) * n_h/(n_h-1). Cross-stratum kernel weights are exactly zero by sampling design. Total meat issum_t sum_h M_h_t.TODO.md):conley_lag_cutoff > 0serial Bartlett HAC composition (fail-closed upfront); replicate-weight variance (inherits Wave E.1 gate); LinearRegression-sideconley + survey_designatlinalg.py:2853(separate Bertanha-Imbens Phase 5 roadmap); DiagnosticReport routing for the new combination (Wave F).Methodology references (required if estimator / math changes)
docs/methodology/REGISTRY.mdWave E.2 subsection (~L3227) anddocs/api/spillover.rstWave E.2 note block. The synthesis framing leads every documented surface from the first draft per the project's documented-synthesis convention.Validation
tests/test_spillover.py— newTestSpilloverDiDWaveE2ConleySurveyDesign(21 tests) andTestSpilloverDiDWaveE2ConleySurveyDesignEventStudy(3 tests). Coverage includes:adjustFPC skip paritypytest.warns(match="Wave E.2 stratified-Conley")finite_masksurvey-array subsettingSurveyDesign(weights=...), strata-onlySurveyDesign(weights=..., strata=...), per-period re-index unit invariantis_staggered=True/Falsebranches perfeedback_cohort_loop_trigger_cache_both_branches; drift goldens atrtol=1e-12 / atol=1e-14DIFF_DIFF_BACKEND=rust pytest -k WaveE2) all pass.Security / privacy
Generated with Claude Code