Skip to content

feat(v4): item 7c of the 4.0 cut - SpilloverDiD df_convention knob (M-154) - #881

Merged
igerber merged 1 commit into
mainfrom
feat/v4-pr7c-m154-spillover-df-convention
Oct 30, 2026
Merged

igerber merged 1 commit into
mainfrom
feat/v4-pr7c-m154-spillover-df-convention

Conversation

@igerber

@igerber igerber commented Oct 30, 2026

Copy link
Copy Markdown
Owner

Summary

  • Item 7c of the 4.0 cut: SpilloverDiD gains the shared three-value df_convention knob with the 4.0 default "cluster" ([M-154]). p-values and CIs on its clustered lanes - the unit auto-cluster from 7b and an explicit cluster= - now use the Stata/fixest t(G-1) reference like every other auto-clustered default; point estimates, SEs and t-statistics never move. "residual" restores the 3.x / 7b residual-df inference bit-for-bit; "normal" is deliberate z.
  • The df ladder is the pure helper spillover._resolve_inference_df, called once per fit and feeding every safe_inference site: a survey_design df keeps precedence under every value; a saturated stage-2 sample keeps the warn + df=0 all-NaN sentinel under every value; otherwise utils.resolve_tail_df with G forced to None on every Conley fit (an explicit product-kernel cluster= included, the SunAbraham precedent) and on the cluster=False HC1 lane, so "cluster" falls back to the residual df there while "normal" still selects z. Validated at __init__, through set_params and at fit().
  • SpilloverDiDResults carries df_convention / inference_df (also in to_dict()); the hc2/hc2_bm rejection text no longer claims a "generic residual df".
  • Ledger: M-154 -> done (M-080's note updated). REGISTRY df_convention knob roster gains SpilloverDiD (out-of-scope list drops it) and the SpilloverDiD df Note describes the ladder; variance-conventions roster / L3 / shared-DGP / tail-df prose, migration guide, llms-full.txt, docs/api/spillover.rst, docs/doc-deps.yaml, spec section 7 (item 7 complete) and TODO.md updated. Tutorial 23 section 5 names the reference distribution (markdown only - the notebook prints estimates and SEs); re-validated with nbmake.

Methodology references (required if estimator / math changes)

  • Method name(s): SpilloverDiD (Butts 2021 ring-indicator spillover DiD, Gardner 2022 two-stage) - analytical tail-df convention only; no estimator math changes.
  • Paper / source link(s): Stata/fixest cluster df convention t(G-1) (REGISTRY TwoWayFixedEffects clustered-CR1 inference-df note, the library-wide df_convention roster); Butts (2023) arXiv:2105.03737; Gardner (2022) arXiv:2207.05943.
  • Any intentional deviations from the source (and why): None new. "cluster" falls back to the residual df on every Conley fit (the combined Conley+cluster product kernel has no documented G-1 reference - the SunAbraham precedent; "normal" still selects z there) and the survey df keeps precedence under every value; both documented in the REGISTRY SpilloverDiD "inference df" Note.

Validation

  • Tests added/updated: new tests/test_v4_spillover_df_convention.py (M-154 test_ref: knob surface, default-lane t(G-1) on the headline and ring rows, an explicit coarser cluster column, residual / normal, the cluster=False / Conley lanes, survey-df precedence, event-study mode under all three values, the df ladder's saturation / survey / knob branches); tests/test_variance_conventions.py gains the spillover_cluster_unit / spillover_cluster_unit_default_df rows (tail df 358 / 59; committed table regenerated); tests/test_t23_spillover_tva_drift.py pins the tutorial's default fit at t(199) and the HC1 / Conley fits on the residual df; tests/test_spillover.py key-set pin, tests/test_v4_cluster_policy.py and tests/test_v4_matrix.py ledger pins updated.
  • Backtest / simulation / notebook evidence (if applicable): docs/tutorials/23_spillover_tva.ipynb re-validated under the Rust backend with nbmake (stores no outputs). Full default suite 15470 passed / 0 failed; mypy clean; py3.9 compile clean.

Security / privacy

  • Confirm no secrets/PII in this PR: Yes

Changelog

  • changelog.d/ fragment added (or N/A - no user-visible change): Yes (changelog.d/20261029-v4-pr7c-m154-spillover-df-convention.md)

…-154)

SpilloverDiD gains the shared three-value df_convention knob with the
4.0 default "cluster": p-values and confidence intervals on its clustered
lanes - the unit auto-cluster that 7b introduced and an explicit
cluster= - now use the Stata/fixest t(G-1) reference like every other
auto-clustered default, with G the effective cluster count reported as
n_clusters. Point estimates, standard errors and t-statistics never
move. "residual" restores the 3.x / 7b residual-df inference
(n_obs_eff - k_effective) bit-for-bit; "normal" is deliberate z.

The ladder is the pure helper spillover._resolve_inference_df, called
once per fit and feeding every safe_inference site (aggregate tau_total,
per-ring delta_j, event-study per-cell tau_k / delta_jk and the scalar
lincom att): a survey_design df keeps precedence under every knob value;
a saturated stage-2 sample keeps the warn + df=0 all-NaN sentinel under
every value (resolve_tail_df is never consulted there); otherwise
utils.resolve_tail_df with G forced to None (residual df) on EVERY
Conley fit - an explicit product-kernel cluster= included, the
SunAbraham precedent - and on the cluster=False HC1 lane. The knob is
validated at __init__, through set_params and again at fit();
SpilloverDiDResults carries df_convention and inference_df (None under z
and under the NaN sentinel); the hc2/hc2_bm rejection text no longer
claims a "generic residual df".

Tests: new tests/test_v4_spillover_df_convention.py (M-154 test_ref)
pins the knob surface, the default-lane t(G-1) on the headline and the
ring rows, an explicit coarser cluster column (G=10), the residual and
normal values, the inert cluster=False / Conley lanes (explicit cluster
included), survey-df precedence on weights-only and PSU+strata designs,
event-study mode under all three values and the df ladder's saturation /
survey / knob branches directly on the helper. The variance-conventions
matrix gains spillover_cluster_unit (explicit residual, tail df 358) and
its _default_df twin (tail df 59) on a deterministic coordinate overlay
of the shared panel; the committed table is regenerated. The t23 drift
suite pins the tutorial's default fit at t(199) and the HC1 / Conley
fits on the residual df; test_spillover's exact get_params key set gains
df_convention. Tutorial 23 section 5 names the reference distribution
(no code cell changes - the notebook prints estimates and SEs only).

Ledger: M-154 -> done (M-080's note updated); spec section 7 / item 7
complete; REGISTRY knob roster gains SpilloverDiD (out-of-scope list
drops it) and the SpilloverDiD df Note describes the ladder; the
variance-conventions roster, L3 entry, shared-DGP paragraph and tail-df
landscape, the migration guide, llms-full.txt, docs/api/spillover.rst,
docs/doc-deps.yaml and TODO.md updated.
@github-actions

Copy link
Copy Markdown

Overall assessment

✅ Looks good — no unmitigated P0 or P1 findings in the supplied diff.

Executive summary

  • The affected method is SpilloverDiD; changes concern analytical reference distributions, leaving estimator and sandwich calculations unchanged.
  • The df ladder matches the Registry, including survey precedence, Conley fallback, and saturation handling.
  • Constructor validation, parameter round-tripping, result metadata, and all four inference sites propagate the new convention.
  • A changelog fragment is present.
  • Syntax and isolated resolver checks passed. Full numerical tests could not run because pytest, NumPy, SciPy, and pandas are unavailable.

Methodology

  • P3 — Documented inference convention; informational.
    Location: diff_diff/spillover.py:L1085-L1156; docs/methodology/REGISTRY.md:L7080-L7100.
    Impact: Clustered fits use t(G−1) by default; Conley and unclustered fits retain residual df under "cluster". Survey df keeps precedence, and invalid df produces joint NaN inference through safe_inference(). These choices match the explicit Registry Note.
    Concrete fix: None required.

Source cross-check used the repository’s reviews of Butts §3.1/§5 and Gardner §3.3, plus the Registry’s Stata/fixest convention. Live source retrieval was unavailable. No undocumented methodology mismatch was identified.

Code Quality

No findings. The shared BaseEstimator supplies parameter propagation, and no new inline inference calculations or partial NaN guards were introduced.

Performance

No findings. Cluster counting is reused for inference and metadata; the change introduces no material additional computation.

Maintainability

No findings. A single resolver supplies aggregate, per-ring, event-study, and scalar lincom inference.

Tech Debt

  • P3 — Tracked df metadata limitation; informational.
    Location: TODO.md:L102; docs/methodology/REGISTRY.md:L7095-L7099.
    Impact: Event-study adapters still have the documented inference_df/df_inference naming gap.
    Concrete fix: Address through the existing df-provenance backlog item; no action required for this PR.

Security

No findings in the supplied changes.

Documentation/Tests

No actionable findings. The fragment, migration guide, API documentation, Registry, and tutorial describe the new behavior.

Eight changed Python files passed in-memory compilation. Additionally, 864 isolated checks of the actual resolver bodies passed using a scalar isfinite shim, including saturation and survey precedence. These checks do not substitute for running the estimator tests with its scientific dependencies.

@igerber igerber added the ready-for-ci Triggers CI test workflows label Oct 30, 2026
@igerber
igerber merged commit d46a5fd into main Oct 30, 2026
43 of 44 checks passed
@igerber
igerber deleted the feat/v4-pr7c-m154-spillover-df-convention branch October 30, 2026 12:03
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