Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
- [SunAbraham](https://diff-diff.readthedocs.io/en/stable/api/staggered.html) - Sun & Abraham (2021) interaction-weighted estimator for heterogeneity-robust event studies
- [ImputationDiD](https://diff-diff.readthedocs.io/en/stable/api/imputation.html) - Borusyak, Jaravel & Spiess (2024) imputation estimator, most efficient under homogeneous effects
- [TwoStageDiD](https://diff-diff.readthedocs.io/en/stable/api/two_stage.html) - Gardner (2022) two-stage estimator with GMM sandwich variance
- [SpilloverDiD](https://diff-diff.readthedocs.io/en/stable/api/spillover.html) - Butts (2021) ring-indicator spillover-aware DiD identifying direct effect on treated + per-ring spillover on near-control units; handles non-staggered and staggered timing; supports survey-design variance under `survey_design=` for HC1 / CR1 (Wave E.1 Binder TSL) and Conley (Wave E.2 panel-aware stratified-Conley sandwich on per-period PSU totals; `conley_lag_cutoff=0` only — serial Bartlett HAC composition queued as follow-up)
- [SpilloverDiD](https://diff-diff.readthedocs.io/en/stable/api/spillover.html) - Butts (2021) ring-indicator spillover-aware DiD identifying direct effect on treated + per-ring spillover on near-control units; handles non-staggered and staggered timing; supports survey-design variance under `survey_design=` for HC1 / CR1 (Wave E.1 Binder TSL) and Conley (Wave E.2 panel-aware stratified-Conley sandwich on per-period PSU totals; extended in Wave E.2 follow-up to `conley_lag_cutoff > 0` via panel-block composition with within-PSU serial Bartlett HAC — `lag>0` requires an effective PSU via explicit `survey_design.psu` or injected `cluster=<col>`)
- [SyntheticDiD](https://diff-diff.readthedocs.io/en/stable/api/estimators.html) - Synthetic DiD combining standard DiD and synthetic control for few treated units
- [TripleDifference](https://diff-diff.readthedocs.io/en/stable/api/triple_diff.html) - triple difference (DDD) estimator for designs requiring two criteria for treatment eligibility
- [ContinuousDiD](https://diff-diff.readthedocs.io/en/stable/api/continuous_did.html) - Callaway, Goodman-Bacon & Sant'Anna (2024) continuous treatment DiD with dose-response curves
Expand Down
3 changes: 2 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ Deferred items from PR reviews that were not addressed before merge.
| `SyntheticDiD(vcov_type="conley")` support. Currently raises `TypeError` at `__init__` because SyntheticDiD uses `variance_method ∈ {bootstrap, jackknife, placebo}` rather than the analytical sandwich that Conley plugs into. Wiring would require either reimplementing an analytical sandwich path for SyntheticDiD or designing a spatial-block bootstrap (new methodology, Politis-Romano 1994 territory). | `synthetic_did.py::SyntheticDiD` | follow-up (spillover-conley) | Low |
| `SpilloverDiD(survey_design=...)` replicate-weight variance (BRR / Fay / JK1 / JKn / SDR). Wave E.1 ships Taylor-linearization only. Per Gerber (2026) Appendix A, the IF-reweighting shortcut does NOT apply to TwoStageDiD-class estimators because `gamma_hat` is weight-sensitive; correct support requires per-replicate full re-fit of stage 1 and stage 2 (200+ LoC of test surface beyond E.1). | `spillover.py::SpilloverDiD.fit`, `survey.py::compute_replicate_refit_variance` | follow-up | Low |
| `SpilloverDiD(survey_design=...)` subpopulation preservation (Wave E.3). Wave E.1's `finite_mask` block physically removes zero-weight rows that lose stage-1 FE support, so `SurveyDesign.subpopulation()`-derived designs see `n_psu` / `df_survey` / Binder centering recomputed on the reduced fit sample rather than the full domain design. Standard domain-estimation practice (R `survey::svyrecvar` on a `subset()` design) preserves the original PSU/strata counts and treats out-of-domain rows as zero-score padding. Fix requires separating fit-sample alignment (Psi array) from design-level bookkeeping: preserve a full-design `resolved_survey` for inference metadata + zero-pad dropped zero-weight rows' IF contribution. Add `SurveyDesign.subpopulation()` regression test to lock the contract. | `spillover.py::SpilloverDiD.fit`, `two_stage.py::_compute_binder_tsl_meat` | follow-up (Wave E.3) | Medium |
| `SpilloverDiD(vcov_type="conley", conley_lag_cutoff > 0, survey_design=...)` serial Bartlett HAC composition. Wave E.2 ships the panel-aware `conley_lag_cutoff = 0` case ("within-period spatial only" — `sum_t sum_h M_h_t` per `tests/test_spillover.py::TestSpilloverDiDWaveE2ConleySurveyDesign::test_b_panel_aware_per_period_sum_invariant`) and raises `NotImplementedError` upfront at `spillover.py:fit` on `conley_lag_cutoff > 0`. The serial Bartlett component (within-unit / within-PSU temporal HAC at lag ≤ L) needs to compose with the panel-aware stratified-Conley spatial sandwich — the natural addition is `meat_serial = sum_g sum_{|t-s|<=L, t!=s} (1 - |t-s|/(L+1)) * (S_psu_t[g] - S_bar_h_t)(S_psu_s[g] - S_bar_h_s)'` per PSU, summed across all PSUs in each stratum, with appropriate Binder FPC scaling — plus a methodology call on whether to include cross-period spatial pairs in the serial term. Regression goldens vs the cross-sectional limit (lag=0, which is now the shipped path). | `spillover.py::SpilloverDiD.fit`, `two_stage.py::_compute_stratified_conley_meat` | follow-up (Wave E.2 follow-up) | Medium |
| Serial Bartlett kernel logic duplicated between `diff_diff/two_stage.py::_compute_stratified_serial_bartlett_meat` (survey path) and `diff_diff/conley.py::_compute_conley_meat` panel-block branch (no-survey path). Both compute `K[t,s] = (1 - |t-s|/(L+1)) * 1{|t-s| <= L, t != s}` over dense panel-period codes. Factor out a shared `_serial_bartlett_kernel_matrix(t_codes, L)` helper and a shared post-meat finite + PSD-warning guard so the survey and no-survey paths can't drift on diagnostics or kernel weights. Cosmetic; refactor doesn't change behavior. | `two_stage.py::_compute_stratified_serial_bartlett_meat`, `conley.py::_compute_conley_meat` | follow-up | Low |
| `SpilloverDiD(vcov_type="conley", conley_lag_cutoff > 0, survey_design=...)` no-effective-PSU serial Bartlett HAC. Wave E.2 follow-up ships the panel-block composition when an effective PSU exists (explicit `survey_design.psu` OR injected via `cluster=<col>` per `_inject_cluster_as_psu`). Weights-only / strata-only survey designs WITHOUT a cluster fallback raise `NotImplementedError` at `SpilloverDiD.fit` post-resolution because under the pseudo-PSU = obs-index fallback each pseudo-PSU appears in exactly one period — the per-PSU serial cross-period loop would silently contribute zero. Fix would either derive a unit-level serial fallback for no-PSU designs (mixes IF allocators with the pseudo-PSU spatial term — needs methodology work) or route the serial loop through `conley_unit` with explicit documentation of the IF-allocator asymmetry. Regression goldens vs the effective-PSU shipped path. | `spillover.py::SpilloverDiD.fit`, `two_stage.py::_compute_stratified_serial_bartlett_meat` | follow-up (Wave E.2 follow-up tail) | Low |
| `SpilloverDiD(ring_method="count")` extension. Currently only the nearest-treated-ring specification is exposed. Count-of-treated-in-ring (paper Section 3.2 end) is methodologically supported by Butts but re-introduces functional-form dependence; expose with an explicit kwarg gate and documentation warning. | `spillover.py::SpilloverDiD.fit` | follow-up | Low |
| `SpilloverDiD` data-driven `d_bar` selection (Butts 2021b / Butts 2023 JUE Insight cross-validation). | `spillover.py::SpilloverDiD` | follow-up | Low |
| `SpilloverDiD` T22 TVA tutorial (`docs/tutorials/22_spillover_did.ipynb`): synthetic TVA-style DGP reproducing Butts (2021) Section 4 Table 1 Panel A bias-correction direction (~40% understatement). Split from the methodology PR per user-confirmed scope split (2026-05-15). | `docs/tutorials/`, `tests/test_t22_*_drift.py` | follow-up (Wave B) | Medium |
Expand Down
2 changes: 1 addition & 1 deletion diff_diff/guides/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Full practitioner guide: call `diff_diff.get_llm_guide("practitioner")`
- [SunAbraham](https://diff-diff.readthedocs.io/en/stable/api/staggered.html): Sun & Abraham (2021) interaction-weighted estimator for heterogeneity-robust event studies
- [ImputationDiD](https://diff-diff.readthedocs.io/en/stable/api/imputation.html): Borusyak, Jaravel & Spiess (2024) imputation estimator — most efficient under homogeneous effects
- [TwoStageDiD](https://diff-diff.readthedocs.io/en/stable/api/two_stage.html): Gardner (2022) two-stage estimator with GMM sandwich variance
- [SpilloverDiD](https://diff-diff.readthedocs.io/en/stable/api/spillover.html): Butts (2021) ring-indicator spillover-aware DiD identifying direct effect on treated + per-ring spillover-on-control; reuses `conley_coords` for ring construction; handles non-staggered and staggered timing; supports `SurveyDesign(weights, strata, psu, fpc)` under `vcov_type="hc1"` with optional `cluster=<col>` for CR1 via Gerber (2026) Binder TSL (Wave E.1) and under `vcov_type="conley"` via a panel-aware stratified-Conley sandwich on per-period PSU totals (Wave E.2; `conley_lag_cutoff=0` only — serial Bartlett HAC composition queued as follow-up), both composed with the Wave D Gardner GMM correction (replicate weights queued as follow-up)
- [SpilloverDiD](https://diff-diff.readthedocs.io/en/stable/api/spillover.html): Butts (2021) ring-indicator spillover-aware DiD identifying direct effect on treated + per-ring spillover-on-control; reuses `conley_coords` for ring construction; handles non-staggered and staggered timing; supports `SurveyDesign(weights, strata, psu, fpc)` under `vcov_type="hc1"` with optional `cluster=<col>` for CR1 via Gerber (2026) Binder TSL (Wave E.1) and under `vcov_type="conley"` via a panel-aware stratified-Conley sandwich on per-period PSU totals (Wave E.2 cross-sectional `conley_lag_cutoff=0`) extended in Wave E.2 follow-up to `conley_lag_cutoff > 0` via panel-block composition with within-PSU serial Bartlett HAC (Newey-West 1987 separable form; `lag>0` requires an effective PSU via explicit `survey_design.psu` or injected `cluster=<col>`), both composed with the Wave D Gardner GMM correction (replicate weights queued as follow-up)
- [SyntheticDiD](https://diff-diff.readthedocs.io/en/stable/api/estimators.html): Synthetic DiD combining standard DiD and synthetic control methods for few treated units
- [TripleDifference](https://diff-diff.readthedocs.io/en/stable/api/triple_diff.html): Triple difference (DDD) estimator for designs requiring two criteria for treatment eligibility
- [ContinuousDiD](https://diff-diff.readthedocs.io/en/stable/api/continuous_did.html): Callaway, Goodman-Bacon & Sant'Anna (2024) continuous treatment DiD with dose-response curves
Expand Down
92 changes: 61 additions & 31 deletions diff_diff/spillover.py
Original file line number Diff line number Diff line change
Expand Up @@ -2197,37 +2197,23 @@ def fit(
# check, cluster-vs-PSU warn) runs AFTER `_validate_spillover_inputs`
# below so it sees the panel columns the validator guarantees.
#
# Wave E.2 scope-limit (upfront, before resolution / panel work):
# the panel-block Conley HAC (`conley_lag_cutoff > 0`) is NOT
# composed with the survey path in this PR. The stratified-Conley
# helper applies a cross-sectional kernel on PSU-aggregated totals;
# composing the within-unit serial Bartlett HAC with the within-
# stratum cross-PSU spatial kernel requires carrying PSU-by-time
# scores into the meat construction, which is a separate Wave E.x
# follow-up tracked in TODO.md. Reject upfront with a clear pointer
# so users running `survey_design=` + `conley_lag_cutoff > 0` get
# the error before stage-1 / 2 work (per `feedback_no_silent_failures`).
if (
survey_design is not None
and self.vcov_type == "conley"
and self.conley_lag_cutoff is not None
and self.conley_lag_cutoff > 0
):
raise NotImplementedError(
"SpilloverDiD(vcov_type='conley', conley_lag_cutoff > 0) "
"combined with survey_design= is not supported in Wave E.2. "
"The Wave E.2 stratified-Conley sandwich aggregates Psi to "
"PSU totals before applying the cross-sectional Conley "
"kernel; the panel-block decomposition (within-unit serial "
"Bartlett HAC over time) would require carrying PSU-by-time "
"scores and composing the serial kernel with the within-"
"stratum cross-PSU spatial kernel. This composition is "
"queued as a follow-up (see TODO.md). For Wave E.2, use "
"conley_lag_cutoff=0 (cross-sectional Conley) with "
"survey_design=, or use survey_design= with "
"vcov_type='hc1' (+ cluster=<col> for CR1) for the full "
"Wave E.1 path."
)
# Wave E.2 follow-up (shipped): `vcov_type='conley' + conley_lag_cutoff > 0
# + survey_design=` is supported via panel-block stratified-Conley
# sandwich (spatial Wave E.2 term + within-PSU serial Bartlett HAC)
# WHEN there is an effective PSU (explicit `survey_design.psu` OR
# injected via `cluster=<col>` per Wave E.1's `_inject_cluster_as_psu`
# routing). The orchestrator at
# `two_stage.py::_compute_stratified_conley_meat` sums the two terms
# with disjoint index sets — matches the no-survey panel-block
# decomposition at `conley.py::_compute_conley_meat` (Conley 1999
# spatial + Newey-West 1987 serial Bartlett; separable form, NOT
# Driscoll-Kraay 2D-HAC). FPC convention: per-period FPC on spatial,
# panel-wide stratum-level FPC on serial. The no-effective-PSU
# fail-closed gate is downstream at the post-resolution check (see
# the `resolved_survey_fit.psu is None` block below the cluster
# injection); the gate cannot live up here because at this point
# the user-supplied `cluster=<col>` has not yet been injected into
# the survey design as the effective PSU.
# Validate `anticipation` up front: must be a non-negative integer.
# Accepting fractional or negative values would silently shift
# treatment timing and ring exposure beyond what the estimator's
Expand Down Expand Up @@ -3100,6 +3086,50 @@ def fit(
_conley_unit_arg = None
_conley_lag_arg = None

# Wave E.2 follow-up gate (post-resolution, post-injection):
# fail-closed for `vcov_type="conley" + conley_lag_cutoff > 0` when
# the EFFECTIVE PSU is still absent after `_inject_cluster_as_psu`.
# Under no-effective-PSU survey designs (weights-only / strata-only
# WITHOUT a cluster fallback) the orchestrator falls back to
# pseudo-PSU = obs-index in `_compute_stratified_conley_meat`, but
# each pseudo-PSU appears in exactly one period, so the per-PSU
# serial cross-period loop never contributes anything (silent zero
# serial term). Routing the serial loop to `conley_unit` (the panel
# unit) instead of pseudo-PSU would mix IF allocators (PSU spatial
# vs unit serial), which violates the single-IF-allocator design
# pinned by the user-confirmed methodology in the Wave E.2 follow-up
# plan. Fail-closed per `feedback_no_silent_failures` until a
# no-effective-PSU-specific derivation is queued. Note: this fires
# AFTER `_inject_cluster_as_psu` (which runs upstream) so the
# documented `cluster=<col> + survey_design(without psu)` surface
# — which becomes an effective-PSU layout via injection — passes
# through unscathed. R2 P1 fix: original front-door gate at
# `spillover.py:2210-2242` (now removed) fired before injection
# and broke the cluster-as-PSU survey-Conley surface.
if (
resolved_survey_fit is not None
and resolved_survey_fit.psu is None
and self.vcov_type == "conley"
and self.conley_lag_cutoff is not None
and self.conley_lag_cutoff > 0
):
raise NotImplementedError(
"SpilloverDiD(vcov_type='conley', conley_lag_cutoff > 0) "
"combined with a no-effective-PSU survey_design "
"(weights-only / strata-only WITHOUT a cluster fallback) "
"is not supported in Wave E.2 follow-up. Under no-effective-"
"PSU survey designs the panel-block serial Bartlett HAC "
"would silently contribute zero (each pseudo-PSU = "
"obs-index appears in exactly one period, so the within-PSU "
"temporal sum has no cross-period pairs to accumulate). "
"Routing the serial loop to `conley_unit` would mix IF "
"allocators with the spatial term and is not derived in "
"this PR. Supply either an explicit `survey_design.psu`, "
"or `cluster=<col>` (which is injected as the effective "
"PSU per Wave E.1's `_inject_cluster_as_psu` routing), "
"or use `conley_lag_cutoff=0` (cross-sectional Wave E.2)."
)

# Derive the Wave D variance mode from the PUBLIC contract:
# - vcov_type="conley" → "conley" (Conley spatial-HAC + GMM)
# - cluster=<col> supplied → "cluster" (CR1 + GMM)
Expand Down
Loading
Loading