fix(staggered): scale-equilibrate CS / StaggeredTripleDiff covariate OR fits#570
Conversation
050b016 to
35dca44
Compare
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall Assessment:
|
…OR fits CallawaySantAnna (_compute_all_att_gt_covariate_reg, _doubly_robust) and StaggeredTripleDifference (_compute_or) fit their covariate outcome- regression nuisance via an estimator-local cho_solve(X'X) cache fast path (with bare scipy.linalg.lstsq(cond=1e-7) fallbacks) that bypassed the shared scale-equilibrated solver. Route them through solve_ols (column-equilibrated SVD/gelsd, return_vcov=False), matching TripleDifference._fit_predict_mu and R's lm()/QR; zero only dropped-column NaN coefficients for prediction (np.where(np.isnan...)) so the numerical-failure -> NaN-cell safety path is preserved. Drop the now-dead cho-cache plumbing. A covariate correlated with another regressor at a very large scale (e.g. a large constant offset, near-collinear with the intercept) previously perturbed the point-estimate ATT (and the IF SE that follows it) because forming the normal equations squares the condition number; the equilibrated SVD is offset-invariant to ~1e-11 where the prior solve drifted (~4e-6 at offset 1e6). Pure orthogonal ill-scaling was already safe (diagonal X'X), so the practical impact is confined to ill-conditioned / correlated covariate designs. Not bit-identical (cho/normal-equations -> SVD); well-scaled designs move only ~1e-12. Tests: reg-anchored scale-invariance tests for CS and StaggeredTripleDiff (covariate + 1e6 offset -> ATT(g,t) unchanged); a new with_covariates_dr CS golden parity test (att+SE vs R `did`, ~1e-3); re-pointed two brittle nan_cell tests from scipy.linalg.lstsq to the solve_ols seam. REGISTRY scale-invariance/scope notes flipped to "fixed"; CHANGELOG; removed the TODO scale-equilibration row. No change to estimands, identifying assumptions, the no-covariate path, or the propensity-score fits. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
35dca44 to
e252f16
Compare

Summary
CallawaySantAnna(
_compute_all_att_gt_covariate_reg,_doubly_robust) andStaggeredTripleDifference(
_compute_or) through the shared scale-equilibratedsolve_ols(column-equilibratedSVD/gelsd,
return_vcov=False), replacing the prior estimator-localcho_solve(X'X)cache fastpath (+ bare
scipy.linalg.lstsq(cond=1e-7)fallbacks) that bypassed it. MatchesTripleDifference._fit_predict_muand R'slm()/QR. Dropped the now-dead cho-cache plumbing.Dropped-column NaN coefficients are zeroed for prediction (
np.where(np.isnan...)), preservingthe numerical-failure → NaN-cell safety path.
large scale (e.g. a large constant offset, near-collinear with the intercept) previously
perturbed the point-estimate ATT — and the influence-function SE that follows it — because
forming the normal equations squares the condition number. The equilibrated SVD is
offset-invariant to ~1e-11 where the prior solve drifted (~4e-6 at an offset of 1e6, growing with
scale). Pure orthogonal ill-scaling was already safe (diagonal
X'X), so the practical impactis confined to ill-conditioned / correlated covariate designs — this is a robustness + R-fidelity
improvement, not a "fixes visibly-wrong ATTs" change. Not bit-identical (cho/normal-equations
→ SVD); well-scaled designs move only ~1e-12.
CallawaySantAnna/StaggeredTripleDifferenceOR-nuisance scale-equilibration rowin
TODO.md.Methodology references (required if estimator / math changes)
CallawaySantAnna(Callaway & Sant'Anna 2021) +StaggeredTripleDifferencedoubly-robust / regression-adjustment outcome-regression nuisance solve.
docs/methodology/REGISTRY.md— the CallawaySantAnna andStaggeredTripleDifference scale-invariance / scope Notes are updated to reflect the fix. R
reference:
did::att_gt/ base Rlm()(QR decomposition), the same orthogonalization family asthe SVD used here.
documented scale-robust standard already used by
TripleDifferenceand matches R's QR. No changeto estimands, identifying assumptions, IF-variance formulas, aggregation, the no-covariate path,
or the propensity-score (logit) fits.
Validation
tests/test_methodology_callaway.py(CSregscale-invariance),tests/test_methodology_staggered_triple_diff.py(StaggeredTripleDiffregscale-invariance),tests/test_csdid_ported.py(newwith_covariates_drgolden att + SE R-didparity, thepreviously-unasserted covariate scenario),
tests/test_staggered.py(twonan_cellmock testsre-pointed from
scipy.linalg.lstsqto thesolve_olsseam).the new code vs ~4e-6 drift under the old, captured via
git stash); no-regression onwell-scaled designs (max|dev|
6.66e-16across 96 values); negligible perf (≈0.008 s for aregfit on a 1500×10 covariate panel — the OR solve is small, so dropping the cho-cache costsnothing); CS covariate R-
didparity (dratt9.5e-4/ SE1.4e-4;reg/ipwatt ~1e-11,unchanged). 455 affected-suite tests pass; source
black/ruffclean;mypy0 new errors vsbaseline.
Security / privacy
🤖 Generated with Claude Code