Signal silent sparse -> dense lstsq fallback in ImputationDiD and TwoStageDiD#319
Signal silent sparse -> dense lstsq fallback in ImputationDiD and TwoStageDiD#319
Conversation
…StageDiD Addresses axis-C findings #8, #9, and #10 from the silent-failures audit: three sites where a sparse factorization failure silently fell back to dense lstsq without any user-facing signal. - diff_diff/imputation.py:1516 (variance path: scipy.sparse.linalg.spsolve on (A_0' W A_0) z = A_1' w). Bare `except Exception` was swallowing the root cause before dense lstsq. Now emits a UserWarning identifying the exception type and explaining the fallback implication. - diff_diff/two_stage.py:1647 (GMM sandwich: sparse_factorized on X'_{10} W X_{10} for Stage 1 normal equations). `except RuntimeError` was silent; now emits a UserWarning. - diff_diff/two_stage_bootstrap.py:134 (bootstrap path: same pattern as above). `except RuntimeError` was silent; now emits a UserWarning. All three are single-call sites (per fit, or per aggregation level, or per bootstrap replicate at most a handful of times) so no aggregation wrapper pattern is needed — one warning per fallback event is appropriate. REGISTRY.md updated under ImputationDiD and TwoStageDiD. New tests (3): monkey-patch the sparse entry point to raise a RuntimeError, run .fit(), assert the UserWarning fires with the expected message prefix. Works against both the variance and bootstrap surfaces. Axis-C baseline: 3 major silent-fallback sites (imputation, two_stage, two_stage_bootstrap) -> 0 remaining in these files. PowerAnalysis simulation counter (finding #11) and ContinuousDiD B-spline (#12) still open as separate follow-ups. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Overall Assessment Looks good. Executive Summary
Methodology No findings. The affected methods are the ImputationDiD variance solve in diff_diff/imputation.py:L1516-L1531, the TwoStageDiD GMM sandwich solve in diff_diff/two_stage.py:L1647-L1668, and the TwoStage bootstrap solve in diff_diff/two_stage_bootstrap.py:L134-L154. Cross-checking those sites against the estimator docstrings and the cited Borusyak-Jaravel-Spiess and Gardner/did2s methodology indicates this PR only adds Code Quality No findings. The edits are limited to Performance No findings. The fallback still uses the same dense solve path as before, so the PR does not introduce a new runtime or asymptotic performance regression in the reviewed code. Maintainability No findings. The three warning sites are mirrored by corresponding registry updates and targeted tests, so the new behavior is documented where users and maintainers will look first. Tech Debt
Security No findings. I did not see any new secret-handling, data-exposure, or unsafe input-surface changes in the diff. Documentation/Tests
|
AI review on PR #319 flagged that my new TwoStage warning tests only verify that the UserWarning fires but not that the dense fallback still produces finite, usable SEs. A future control-flow regression could keep the warning while breaking the degraded path. Mirror the assertion shape used in the pre-existing ImputationDiD test_sparse_solver_dense_fallback: after the warned .fit(), assert overall_se is finite and > 0 for both the GMM sandwich and bootstrap paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
/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
|
…odo config Packages five merged PRs since v3.1.2 as patch release 3.1.3: - #311 Replicate-weight variance and PSU-level bootstrap for dCDH — new variance_method="replicate" (BRR / Fay / JK1 / JKn / SDR) and PSU-level multiplier bootstrap, with df-aware inference and group-level PSU map. - #321 Zenodo DOI auto-minting config — .zenodo.json + top-level LICENSE so the next GitHub Release mints a concept + versioned DOI automatically. - #319 Silent sparse->dense lstsq fallback signaling in ImputationDiD and TwoStageDiD — emits ConvergenceWarning instead of switching paths silently. - #317 Non-convergence signaling in TROP alternating-minimization solvers, including LOOCV and bootstrap aggregation. Top-level warning aggregation. - #320 /bump-version skill now updates CITATION.cff; single RELEASE_DATE resolved upfront and threaded through all date-bearing files. Version strings bumped in diff_diff/__init__.py, pyproject.toml, rust/Cargo.toml, diff_diff/guides/llms-full.txt, and CITATION.cff (version: 3.1.3, date-released: 2026-04-18). CHANGELOG populated with Added / Fixed / Changed sections and comparison-link footer. Per project SemVer convention, minor bumps are reserved for new estimators or new module-level API; additive extensions to existing estimators (like PR #311's new variance_method values) are patch-level. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI review on #322 flagged that the 3.1.3 entries for PR #319 (sparse->dense lstsq fallback) and PR #317 (TROP non-convergence) claimed ConvergenceWarning, but the actual implementations emit UserWarning (imputation.py, two_stage.py, utils.py, trop.py, and the REGISTRY.md contract all use UserWarning). Users filtering warnings by category would be misled. Same factual error was in the 3.1.2 entries I wrote in PR #316 for PR #314 and PR #315. Fixing both entries in this PR — CHANGELOG is a living doc and the warning-category drift is actionable-ly misleading. No code or test changes; CHANGELOG-only edit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
UserWarningwith the original exception type and a note about the degraded-path implication.diff_diff/imputation.py:1516— variance path,scipy.sparse.linalg.spsolveon(A_0' W A_0) z = A_1' w. Previous bareexcept Exceptionwas swallowing the root cause.diff_diff/two_stage.py:1647— GMM sandwich Stage 1 normal-equations viasparse_factorized.diff_diff/two_stage_bootstrap.py:134— bootstrap path, same pattern..fit()(per aggregation level for the main variance path; per bootstrap preparation stage for bootstrap), so one warning per fallback event is appropriate and the axis-B aggregation wrapper pattern is not needed here.gamma_hat/zis identical to the silent path; only the signal is new.Methodology references
Sparse variance solverbullet now notes the warning-on-fallback; the TwoStageDiD section gains a new**Note:**describing the signal on both the GMM sandwich and bootstrap fallback paths.Validation
unittest.mock.patchto forcespsolve/sparse_factorizedto raiseRuntimeError, then run.fit()and assert the expectedUserWarningfires:tests/test_imputation.py::TestImputationVariance::test_sparse_solver_dense_fallback_emits_warningtests/test_two_stage.py::TestTwoStageDiDVariance::test_sparse_factorized_dense_fallback_emits_warningtests/test_two_stage.py::TestTwoStageDiDVariance::test_sparse_factorized_bootstrap_dense_fallback_emits_warningtest_sparse_solver_dense_fallback(which verifies the fallback produces finite SE) continues to pass.Security / privacy