You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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>
Copy file name to clipboardExpand all lines: docs/methodology/REGISTRY.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1565,7 +1565,7 @@ Observation weights `v_it`:
1565
1565
1566
1566
**Note (pretrends lead FE-span guard, 2026-07):** the Test-1 lead model (`_compute_lead_coefficients`) routes its lead indicators + covariates through `snap_absorbed_regressors` after the within-transform (same two-stage snap + LSMR confirmation as the `absorb=` estimators; see "Absorbed Fixed Effects"). Lead indicators are the most plausible FE-spanned regressors: when a lead's calendar period contains only that cohort's rows on Omega_0 (e.g. never-treated units unobserved there), the lead collapses to a calendar-time dummy in the span of the absorbed time FE — it now snaps to exact zero and reports a deterministic NaN coefficient with a cause-specific warning (label `lead[h]`), instead of relying on the raw rank check alone (which the truncated-MAP-iterate exposure can defeat in slow-convergence regimes). Identified leads are unchanged (snap is a no-op; suite bit-stable).
1567
1567
1568
-
**Note on v_it derivation:** The paper's Supplementary Proposition A3 gives the explicit `v_it^*` formula; it is not in the reviewed main-article PDF, so the projection is validated *empirically* against R `didimputation` (`tests/test_methodology_imputation.py::TestImputationDiDParityR`, SEs match to ~1e-10; the covariate branch — first stage `y ~ x | unit + time` on the untreated sample — is anchored separately by `TestImputationDiDCovariateParityR` on a time-varying-X panel, SEs ~2e-10). **Deviation note (superseded closed form):** the FE-only path previously used a closed form `-(w_i./n_{0,i} + w_.t/n_{0,t} - w../N_0)`, which is exact only for a *balanced* untreated set; because `Omega_0` is generically unbalanced in staggered designs (treated observations are removed), that form biased the SE (~27% on the parity panel) and was replaced by the exact projection above during the ImputationDiD methodology validation. A genuinely rank-deficient `A_0' A_0` (e.g. an unidentified period FE) routes to a dense least-squares fallback with a `UserWarning`.
1568
+
**Note on v_it derivation:** The paper's Supplementary Proposition A3 gives the explicit `v_it^*` formula; it is not in the reviewed main-article PDF, so the projection is validated *empirically* against R `didimputation` (`tests/test_methodology_imputation.py::TestImputationDiDParityR`, SEs match to ~1e-10; the covariate branch — first stage `y ~ x | unit + time` on the untreated sample — is anchored separately by `TestImputationDiDCovariateParityR` on a time-varying-X panel, SEs ~2e-10). **Deviation note (superseded closed form):** the FE-only path previously used a closed form `-(w_i./n_{0,i} + w_.t/n_{0,t} - w../N_0)`, which is exact only for a *balanced* untreated set; because `Omega_0` is generically unbalanced in staggered designs (treated observations are removed), that form biased the SE (~27% on the parity panel) and was replaced by the exact projection above during the ImputationDiD methodology validation. A genuinely rank-deficient `A_0' A_0` (e.g. an unidentified period FE) routes to a sparse LSMR least-squares fallback with a `UserWarning` (no dense materialization; see the sparse-variance-solver Note below).
1569
1569
1570
1570
Auxiliary model residuals (Equation 8):
1571
1571
- Partition `Omega_1` into groups `G_g` (default: cohort × horizon)
@@ -1613,7 +1613,7 @@ where `W_it(h) = 1[K_it = h]` are lead indicators, estimated on `Omega_0` only.
1613
1613
- **Non-constant `first_treat` within a unit:** Emits `UserWarning` identifying the count and example unit. The estimator proceeds using the first observed value per unit (via `.first()` aggregation), but results may be unreliable.
1614
1614
- **treatment_effects DataFrame weights:** `weight` column uses `1/n_valid` for finite tau_hat and 0 for NaN tau_hat, consistent with the ATT estimand (unweighted), or normalized survey weights `sw_i/sum(sw)` when `survey_design` is active.
1615
1615
- **Rank-deficient covariates in variance:** Covariates with NaN coefficients (dropped for rank deficiency in Step 1) are excluded from the variance design matrices `A_0`/`A_1`. Only covariates with finite coefficients participate in the `v_it` projection.
1616
-
- **Sparse variance solver:** the untreated projection `v_untreated = -A_0 (A_0'[W]A_0)^{-1} A_1'w` factorizes the normal-equations matrix `(A_0'[W]A_0)` once per `fit()` via `scipy.sparse.linalg.factorized` and reuses the factorization across every estimand target (overall ATT, each event-study horizon, each group, and the bootstrap precompute), solving only the target-specific RHS `A_1'w` per target -- factorize-once / solve-many (the design is target-invariant; only `weights` vary). This is **bit-identical** to the prior per-target `scipy.sparse.linalg.spsolve` for a single dense RHS (both use the SuperLU simple driver with the same defaults), built once instead of `O(targets)` times. Mirrors the TwoStageDiD GMM-sandwich `factorized` pattern. An exactly singular `(A_0'[W]A_0)` makes `factorized` raise `RuntimeError`; the build falls back to dense `lstsq` and emits a `UserWarning` once per fit (silent-failure audit axis C). A defensive per-target non-finite solve likewise routes to dense `lstsq` with a per-target `UserWarning`, so callers always know variance estimates came from the degraded path. The design is built/cached in `_build_untreated_projection` and solved per target in `_solve_untreated_v`.
1616
+
- **Sparse variance solver:** the untreated projection `v_untreated = -A_0 (A_0'[W]A_0)^{-1} A_1'w` factorizes the normal-equations matrix `(A_0'[W]A_0)` once per `fit()` via `scipy.sparse.linalg.factorized` and reuses the factorization across every estimand target (overall ATT, each event-study horizon, each group, and the bootstrap precompute), solving only the target-specific RHS `A_1'w` per target -- factorize-once / solve-many (the design is target-invariant; only `weights` vary). This is **bit-identical** to the prior per-target `scipy.sparse.linalg.spsolve` for a single dense RHS (both use the SuperLU simple driver with the same defaults), built once instead of `O(targets)` times. Mirrors the TwoStageDiD GMM-sandwich `factorized` pattern. An exactly singular `(A_0'[W]A_0)` makes `factorized` raise `RuntimeError`; the build emits a `UserWarning` once per fit (silent-failure audit axis C) and the solve routes to a **sparse LSMR least-squares fallback** (`scipy.sparse.linalg.lsmr`, `atol=btol=1e-14`) — the previous dense `lstsq(toarray())` fallback materialized the `O((U+T+K)^2)` normal matrix, an OOM risk on large panels (2026-07). Solver choice cannot change the estimator output: 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 (dense-oracle parity test). Convergence is validated fail-closed: `istop` in `{0, 1, 2, 4, 5}` counts as certified (4/5 are SciPy's machine-precision analogues of 1/2); an uncertified stop gets one retry with an uncapped condition limit, then raises internally and the variance boundary reports a **full NaN inference tuple** — raising rather than returning NaN matters because the missing-FE `nan_to_num` in the psi product would otherwise launder a NaN vector into zeros and a finite, wrong variance. A defensive per-target non-finite solve likewise routes to the LSMR fallback with a per-target `UserWarning`, so callers always know variance estimates came from the degraded path. The design is built/cached in `_build_untreated_projection` and solved per target in `_solve_untreated_v`.
1617
1617
- **Note:** Survey weights enter ImputationDiD via weighted iterative FE (Step 1), survey-weighted ATT aggregation (Step 3), and design-based variance via `compute_survey_if_variance()`. PSU clustering, stratification, and FPC are fully supported in the Theorem 3 variance path. When `resolved_survey` is present, the observation-level influence function (`v_it * epsilon_tilde_it`) is passed to `compute_survey_if_variance()` which applies the stratified PSU-level sandwich with FPC correction. Strata also enters survey df (n_PSU - n_strata) for t-distribution inference. Bootstrap + survey supported (Phase 6) via PSU-level multiplier weights.
1618
1618
- **Bootstrap inference:** Uses multiplier bootstrap on the Theorem 3 influence function: `psi_i = sum_t v_it * epsilon_tilde_it`. Cluster-level psi sums are pre-computed for each aggregation target (overall, per-horizon, per-group), then perturbed with multiplier weights (Rademacher by default; configurable via `bootstrap_weights` parameter to use Mammen or Webb weights, matching CallawaySantAnna). This is a library extension (not in the paper) consistent with CallawaySantAnna/SunAbraham bootstrap patterns.
1619
1619
- **Auxiliary residuals (Equation 8):** Implements the paper's *unit-clustered* Equation 8 aggregator, `tau_tilde_g = sum_i (sum_{t in G_g,i} v_it)(sum_{t in G_g,i} v_it * tau_hat_it) / sum_i (sum_{t in G_g,i} v_it)^2` (Borusyak-Jaravel-Spiess 2024, eq. 8, p. 3272; minimal-excess-variance derivation in Supplementary Appendix A.8): for each unit form the within-unit weight sum `a_{i,g}` and weighted-effect sum `b_{i,g}` over the unit's observations in group `g`, then combine across units. Groups partition `Omega_1` via `aux_partition` (default `"cohort_horizon"` = cohort × event-time; also `"cohort"` / `"horizon"`). Unimputable (NaN `tau_hat`) and off-target observations carry `v_it = 0` and are excluded from the aggregation — exact for finite `tau_hat` (a zero-weight row adds 0 to both `a` and `b`) and NaN-safe; a group with no contributing observations falls back to the unweighted group mean (a variance no-op, since `psi_g = sum_t v_it * eps_tilde_it = 0` there).
0 commit comments