Skip to content

feat(imputation): leave-one-out conservative variance (BJS 2024 App. A.9)#630

Open
igerber wants to merge 1 commit into
mainfrom
feature/imputation-leave-one-out
Open

feat(imputation): leave-one-out conservative variance (BJS 2024 App. A.9)#630
igerber wants to merge 1 commit into
mainfrom
feature/imputation-leave-one-out

Conversation

@igerber

@igerber igerber commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add the opt-in ImputationDiD(leave_one_out=False) parameter implementing the Borusyak-Jaravel-Spiess (2024) Supplementary Appendix A.9 leave-one-out finite-sample refinement to the conservative variance.
  • The non-LOO auxiliary aggregate tau_tilde_g (eq. 8) is built from the fitted tau_hat_it and so partially overfits to the noise epsilon_it, biasing the variance downward. leave_one_out=True recomputes each unit's group aggregate excluding that unit — implemented efficiently (A.9) by rescaling each treated auxiliary residual by 1/(1 - v_ig**2 / sum_j v_jg**2), which reproduces the direct leave-one-out at the per-unit cluster sum psi_i exactly (self-derived, machine-precision-verified identity). Yields a larger, less-downward-biased SE (Prop. A8: unbiased for an upper bound at the default unit clustering; an equal-weight K-unit group inflates residuals by exactly K/(K-1)).
  • Point estimates and the entire default path (leave_one_out=False) are byte-identical (R didimputation parity preserved). The opt-in SE inflation scales with 1/group-size: ~0.4% on large panels, up to ~13% on small/fine-partition designs.
  • Robustness: the leave-one-out denominator is the exact sum of the other units' squared weights (not D - v_ig**2, which cancels for dominated >=2-unit groups); replicate-weight survey designs raise NotImplementedError (their variance bypasses the IF path); leave_one_out is recorded on ImputationDiDResults / to_dict() / summary().

Methodology references (required if estimator / math changes)

  • Method name(s): ImputationDiD (Borusyak, Jaravel & Spiess 2024, Revisiting Event-Study Designs, ReStud 91(6)); Supplementary Appendix A.9 "Leave-Out Conservative Variance Estimation".
  • Paper / source link(s): docs/methodology/REGISTRY.md ## ImputationDiD (new leave-one-out Note, the derivation's home); docs/methodology/papers/borusyak-jaravel-spiess-2024-review.md (A.9 GAP now filled with a provenance note — sourced from arXiv 2108.12419v5 App. A.9; the REStud Supplementary Material is canonical).
  • Any intentional deviations from the source (and why): the shipped option is the tau_tilde residual rescale (as in the authors' Stata did_imputation); the stronger Prop. A8 variant that also leaves out for the covariate delta_hat is noted but not implemented. The LOO >= non-LOO / upper-bound guarantee is documented as holding at the default unit clustering; coarser cluster= / analytical survey_design= / n_bootstrap compositions apply the same rescale as a documented library extension. Default off preserves R didimputation parity.

Validation

  • Tests added/updated: tests/test_methodology_imputation.py::TestB2024AppendixA9LeaveOneOut — the exact psi-identity vs the paper's direct leave-one-out (paper-fidelity gate, atol=1e-12), K/(K-1) hand-calc, direction (LOO >= non-LOO at unit clustering), extreme-weight / positive-near-cancellation edges, effective-singleton fallback, replicate-weight fail-closed, result metadata, and @slow MC coverage. Existing _EQ8_COHORT_SE_PIN and R didimputation golden-parity tests pass unchanged (default byte-identity).
  • No CI-runnable external anchor (R didimputation omits LOO; the authors' Stata did_imputation ships it but Stata is not in CI) — the exact psi-identity anchors the implementation to the paper's own tau_tilde_it^LO definition.
  • Backtest / simulation / notebook evidence (if applicable): MC coverage test (LOO coverage >= nominal and >= non-LOO on an overfit-prone fine partition).

Security / privacy

  • Confirm no secrets/PII in this PR: Yes

🤖 Generated with Claude Code

https://claude.ai/code/session_01LHDijzf8zHXk5T8ahS2mKi

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Overall Assessment

⚠️ Needs changes — no P0 issues and no unmitigated methodology mismatch found. One P1 remains for missing direct coverage of new leave_one_out=True parameter interactions across aggregation and analytical survey paths.

Executive Summary

  • Affected method: ImputationDiD conservative variance only; point estimates and default leave_one_out=False path appear preserved.
  • The implemented A.9 residual rescale matches the Methodology Registry formula and is applied in the influence-function path used by analytical variance and bootstrap.
  • Replicate-weight survey designs fail closed with NotImplementedError, which is correct because that path bypasses the IF residual machinery.
  • The documented deviations/extensions in REGISTRY.md are not defects under the review rules.
  • P1: tests cover the core identity, singleton handling, default parity, cluster/bootstrap overall SE, and replicate fail-closed, but not aggregate="all" event-study/group outputs or analytical survey composition for leave_one_out=True.
  • Verification note: attempted focused pytest run, but pytest is not installed in this environment.

Methodology

Finding M1 — P3 Informational, No Action Required

Severity: P3
Impact: The implementation intentionally ships the tau_tilde residual rescale only, while the stronger exact-unbiased Prop. A8 variant that also leaves out covariate estimation is documented as not implemented. The Registry also documents that coarser cluster=, analytical survey_design=, and bootstrap composition are library extensions beyond the default unit-cluster guarantee. These are documented deviations/extensions, so they are not defects.
Concrete fix: None required.

Locations: docs/methodology/REGISTRY.md:L1581, diff_diff/imputation.py:L1727-L1754, diff_diff/imputation.py:L1777-L1784, diff_diff/imputation.py:L2595-L2650.

Code Quality

No findings. The parameter is validated in __init__ and at fit() time, appears in get_params(), is passed into results, and replicate survey no-op risk is handled fail-closed.

Locations: diff_diff/imputation.py:L193-L224, diff_diff/imputation.py:L373-L388, diff_diff/imputation.py:L2578-L2593, diff_diff/imputation_results.py:L264-L267, diff_diff/imputation_results.py:L500-L512.

Performance

No findings. The near-cancellation recomputation is scoped to suspect dominated rows and should not change the normal path materially.

Maintainability

No findings. The LOO logic is isolated in _leave_one_out_factor() and applied through the existing _compute_cluster_psi_sums() influence-function path.

Locations: diff_diff/imputation.py:L1368-L1412, diff_diff/imputation.py:L2595-L2650.

Tech Debt

No findings. The prior TODO for ImputationDiD LOO variance was removed, and I did not see a new untracked correctness limitation.

Security

No findings. No secrets or security-sensitive behavior were introduced in the changed code.

Documentation/Tests

Finding T1 — P1 Missing Parameter Interaction Coverage

Severity: P1
Impact: leave_one_out=True is a new public estimator parameter affecting SEs. The implementation routes through event-study, group, bootstrap, and analytical survey variance paths, but the new tests only exercise overall ATT for cluster/bootstrap composition. There is no direct assertion that aggregate="event_study", aggregate="group"/"all", or analytical survey_design= produce finite, self-consistent LOO inference fields. Per the edge-case review rule for new parameters, this leaves important parameter interactions unpinned.
Concrete fix: Add focused tests for:

  1. ImputationDiD(leave_one_out=True).fit(..., aggregate="all"), asserting overall ATT is unchanged vs non-LOO and post-treatment event-study/group SE, p-value, CI fields are finite or consistently NaN where expected.
  2. ImputationDiD(leave_one_out=True, n_bootstrap=..., seed=...).fit(..., aggregate="all"), asserting bootstrap event-study and group SE/p-value/CI fields are populated consistently.
  3. ImputationDiD(leave_one_out=True).fit(..., aggregate="all", survey_design=SurveyDesign(weights=..., psu=...)), asserting analytical survey variance composes and inference fields remain consistent.

Locations: implemented aggregation paths at diff_diff/imputation.py:L662-L705; analytical survey variance at diff_diff/imputation.py:L1462-L1490; bootstrap event/group psi paths at diff_diff/imputation_bootstrap.py:L198-L238 and diff_diff/imputation_bootstrap.py:L240-L270; current LOO composition test only checks overall SE at tests/test_methodology_imputation.py:L777-L795.

Path to Approval

  1. Add a non-slow aggregate="all" LOO test covering event-study and group outputs.
  2. Extend the LOO bootstrap test to aggregate="all" and assert event-study/group bootstrap inference fields.
  3. Add an analytical pweight/PSU survey_design= LOO test with finite overall/event/group inference.

…A.9)

Add the opt-in `leave_one_out` parameter (default False) implementing the
Borusyak-Jaravel-Spiess (2024) Supplementary Appendix A.9 finite-sample
refinement to ImputationDiD's conservative variance.

The non-LOO auxiliary aggregate tau_tilde_g (eq. 8) is built from the fitted
tau_hat_it and thus partially overfits to the noise epsilon_it, biasing the
variance downward. leave_one_out=True recomputes each unit's group aggregate
excluding that unit -- implemented efficiently (A.9) by rescaling each treated
auxiliary residual by 1/(1 - v_ig**2/sum_j v_jg**2), which reproduces the
direct leave-one-out at the per-unit cluster sum psi_i EXACTLY (self-derived,
machine-precision-verified identity). Yields a larger, less-downward-biased SE
(Prop. A8: unbiased for an upper bound at the default unit clustering; an
equal-weight K-unit group inflates residuals by exactly K/(K-1)).

Point estimates and the entire default path (leave_one_out=False) are
byte-identical, preserving R didimputation parity. Robustness:
- the leave-one-out denominator is the exact sum of the OTHER units' squared
  weights (drop-then-sum), not D - v_ig**2 after forming the ratio, so an
  extremely dominated >=2-unit group keeps its large finite factor instead of
  silently reverting to non-LOO; the singleton test counts positive-weight
  units (handles effective singletons);
- replicate-weight survey designs raise NotImplementedError (their variance
  bypasses the influence-function path where the rescale lives);
- leave_one_out is recorded on ImputationDiDResults, to_dict(), and summary().

Validated by the exact psi-identity, K/(K-1) hand-calc, direction (LOO >=
non-LOO at unit clustering), extreme-weight / near-cancellation edges, and MC
coverage; the authors' Stata did_imputation ships the same option (no
CI-runnable anchor). Fills the A.9 GAP in the BJS review doc (sourced from
arXiv 2108.12419v5). Closes the ImputationDiD LOO Actionable TODO row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHDijzf8zHXk5T8ahS2mKi
@igerber igerber force-pushed the feature/imputation-leave-one-out branch from 86cfd13 to a57a1b6 Compare July 7, 2026 01:36
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: a57a1b6dee5277da5308846b5ae6e85da0cda12c


Overall Assessment

Looks good — no unmitigated P0/P1 findings. The previous P1 on missing leave_one_out=True interaction coverage appears resolved.

Executive Summary

  • Affected method: ImputationDiD conservative variance only; point estimates and leave_one_out=False remain unchanged by the new path.
  • The A.9 residual rescale matches the new Methodology Registry contract and is routed through the shared influence-function path used by overall, event-study, group, survey TSL, and multiplier bootstrap SEs.
  • Replicate-weight survey designs fail closed with NotImplementedError, which avoids a silent no-op because that variance path bypasses the IF residual machinery.
  • The prior review’s requested aggregate="all" analytical, bootstrap, and analytical survey tests have been added.
  • Verification limitation: I could not run the tests because this environment lacks pytest and numpy.

Methodology

Finding M1 — P3 Informational, No Action Required

Severity: P3
Impact: The implementation intentionally ships the tau_tilde residual-rescale form of BJS 2024 Supp. App. A.9, while the stronger Prop. A8 leave-out-for-delta_hat variant is documented as not implemented. Coarser cluster=, analytical survey_design=, and multiplier bootstrap composition are also documented as library extensions. Under the review rules, these documented deviations/extensions are not defects.
Concrete fix: None required.

Locations: docs/methodology/REGISTRY.md:L1581, diff_diff/imputation.py:L1721-L1784, diff_diff/imputation.py:L2595-L2650.

Code Quality

No findings. leave_one_out is strict-bool validated in __init__ and at fit() time, included in get_params(), used through _compute_auxiliary_residuals_treated(), and persisted on ImputationDiDResults.

Locations: diff_diff/imputation.py:L193-L237, diff_diff/imputation.py:L298-L302, diff_diff/imputation.py:L2557-L2593, diff_diff/imputation_results.py:L153-L155.

Performance

No findings. The near-cancellation exact recomputation is limited to suspect dominated (group, unit) rows; normal groups stay on vectorized arithmetic.

Location: diff_diff/imputation.py:L2639-L2649.

Maintainability

No findings. The LOO logic is isolated in _leave_one_out_factor() and consumed by the existing _compute_cluster_psi_sums() path, so analytical, aggregation, survey, and bootstrap paths share the same variance machinery.

Locations: diff_diff/imputation.py:L1381-L1394, diff_diff/imputation.py:L1462-L1490, diff_diff/imputation_bootstrap.py:L172-L276.

Tech Debt

No findings. The prior TODO entry for ImputationDiD LOO variance was removed after implementation, and I did not find a newly introduced untracked correctness limitation.

Location: TODO.md:L28-L35.

Security

No findings. The PR does not introduce secrets, filesystem/network behavior, or security-sensitive code paths.

Documentation/Tests

No blocking findings. The prior P1 is addressed by new tests covering aggregate="all" analytical LOO, aggregate="all" bootstrap LOO, and analytical survey LOO, along with singleton fallback, replicate fail-closed behavior, metadata persistence, and factor identity tests.

Locations: tests/test_methodology_imputation.py:L621-L897, diff_diff/imputation_results.py:L264-L267, diff_diff/imputation_results.py:L497-L527.

Verification note: focused test execution was not possible here: pytest is not installed, and importing the package for a small sanity check also failed because numpy is not installed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant