fix(imputation): LSMR singular-variance fallback — no dense normal-matrix materialization#657
Open
igerber wants to merge 2 commits into
Open
fix(imputation): LSMR singular-variance fallback — no dense normal-matrix materialization#657igerber wants to merge 2 commits into
igerber wants to merge 2 commits into
Conversation
Overall Assessment✅ Looks good. No unmitigated P0/P1 findings. Executive Summary
MethodologyFinding M1 — P3 informational
Code QualityFinding C1 — P3 informational
PerformanceNo findings.
MaintainabilityNo blocking findings.
Tech DebtFinding T1 — P3 informational, tracked
SecurityNo findings.
Documentation/TestsFinding D1 — P3 informational
|
…trix materialization The sparse-factorization fallback called np.linalg.lstsq((A0'[W]A0).toarray(), ...) — an O((U+T+K)^2) dense materialization and OOM risk on large panels (triggered only when the sparse factorization fails, e.g. rank-deficient Omega_0). Both fallback sites now solve via scipy.sparse.linalg.lsmr with no densification. Solver choice provably cannot change the estimator: least-squares solutions of the singular system differ only by null(sqrt(W) A_0) components, which the downstream projection v = -[W_0] A_0 z annihilates — locked by a dense-lstsq-oracle parity test on a genuinely singular system plus a no-densify spy test through the full fit. Warning text updated (tests adjusted). The analogous TwoStageDiD dense fallbacks are multi-RHS with coefficient-level consumers where the invariance argument does not transfer — tracked as a new TODO row rather than blindly swapped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ted parity oracle
M1: REGISTRY ImputationDiD passages (v_it derivation note + sparse
variance solver note) now describe the sparse LSMR fallback (certified
istop set {0,1,2,4,5}, uncertified -> full NaN inference tuple, dense
lstsq removed) instead of the pre-#657 dense-lstsq contract.
C1: five stale 'dense lstsq' comments/docstrings in imputation.py
updated to the LSMR contract.
D1: weighted singular-system parity test added — null-space components
of (A_0'[W]A_0) live in null(sqrt(W) A_0), so the WEIGHTED projection
W_0 A_0 z (what the weighted estimator consumes) must match the dense
oracle even where the unweighted projection need not; includes
zero-weight (subpopulation) rows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3fd25bb to
6b45c59
Compare
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
np.linalg.lstsq((A₀'[W]A₀).toarray(), …)— anO((U+T+K)²)dense materialization, triggered when the sparse factorization fails (e.g. rank-deficient Ω₀). Both fallback sites now solve viascipy.sparse.linalg.lsmrwith no dense materialization (locked by a spy test that poisons.toarraythrough a full fit).null(√W·A₀)components, which the downstream projectionv = −[W₀]A₀zannihilates — locked by a dense-lstsq-oracle parity test on a genuinely singular system.istop ∈ {0,1,2,4,5}counts as certified (4/5 are SciPy's machine-precision analogues of 1/2 — round-3 P1); an uncertified stop gets one retry with an uncapped condition limit, then raises a private_LSMRUnconvergedErrorcaught at the variance boundary and converted to a full NaN inference tuple. Raising rather than returning NaN matters: the missing-FEnan_to_numin the psi product would launder a NaN vector into zeros and a finite, wrong variance (round-2 P0) — a fit-level regression test forcesistop=7end-to-end and asserts NaN SE/t/p/CI with a finite point estimate.TwoStageDiDdense fallbacks are multi-RHS with coefficient-level consumers, where the null-space-invariance argument does not transfer — tracked as a new TODO row with the analysis needed, not blindly swapped.Methodology references (required if estimator / math changes)
atol=1e-14, uncertified solves fail closed to NaNValidation
tests/test_imputation.py::TestLSMRFallbackParity(5 tests: singular-oracle parity, no-densify spy, unconverged raise, fit-level NaN inference, istop-4/5 acceptance); warning-text assertions updated in 2 files. Suites green: 179 imputation + methodology-imputation; two_stage untouched (126).Security / privacy
Generated with Claude Code