Signal non-convergence in FE imputation alternating-projection solvers#314
Signal non-convergence in FE imputation alternating-projection solvers#314
Conversation
TwoStageDiD and ImputationDiD each run two iterative alternating-projection solvers (_iterative_fe, _iterative_demean) whose convergence loop exited silently on max_iter exhaustion, returning the current iterate as if converged. This matches the silent-failure pattern audited under axis B of the silent-failures initiative (findings #2-#5). Adds a shared warn_if_not_converged helper in diff_diff.utils and calls it from all four alternating-projection loops on non-convergence. Pattern mirrors the existing logistic + Poisson IRLS convergence warnings in linalg.py (lines 1329-1376). Warning-only: no new public parameter, no behavior change on inputs that already converge. Updates REGISTRY.md entries for ImputationDiD and TwoStageDiD with Note labels describing the new signal. Axis-B regression-lint baseline: 10 silent range(max_iter) loops -> 6 remaining (Frank-Wolfe and TROP addressed in follow-up PRs). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Overall assessment✅ Looks good Executive summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
Review note: this was a static review only. I could not execute the tests in this environment because |
Mirrors the positive + negative coverage already present for _iterative_fe. Ensures an accidental always-warn regression on the alternating-projection demean path would fail the test suite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
/ai-review |
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall Assessment Needs changes. Executive Summary
Methodology
Code Quality
Performance No findings. Maintainability No findings. The shared helper in diff_diff/utils.py:L68 is a maintainability improvement for the four modified estimator loops. Tech Debt No separate finding. TODO.md:L51 does not already track the Security No findings. Documentation/Tests No unmitigated findings. The prior re-review concern about missing convergent Path to Approval
|
…ction AI review on PR #314 surfaced two more silent alternating-projection loops in diff_diff.utils.within_transform (previously hard-coded range(100) with tol=1e-8). The weighted path is exercised on every WooldridgeDiD fit via wooldridge.py:667, so leaving it silent would have left this PR as a "partial pattern fix" for axis B. - Expose max_iter and tol as optional kwargs (defaults preserved: 100, 1e-8). - Track non-converged variables across the per-variable inner loops and emit a single consolidated UserWarning naming which variables failed, reusing the warn_if_not_converged helper already added in this PR. - Add a positive warning test and a convergent negative control in TestWithinTransformationAlgebra. Axis-B regression-lint baseline: 10 -> 4 silent range(max_iter) loops remaining (Frank-Wolfe, TROP global outer+inner, TROP local). 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
|
Both parameters apply only when weights is not None. 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
|
…tors AI review surfaced that the shared within_transform weighted path also affects WooldridgeDiD and survey-weighted TwoWayFixedEffects, SunAbraham, and BaconDecomposition, but the prior notes only covered ImputationDiD and TwoStageDiD. Adds one shared note under 'Absorbed Fixed Effects with Survey Weights' covering the cross-estimator behavior, plus a per-estimator note in the WooldridgeDiD section (since its path always routes through the weighted helper, regardless of whether survey weights are provided). 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
|
…mean The warning behavior added in this PR also fires from the iterative demeaning helper (covariate residualization in both estimators, plus the ImputationDiD pre-trend test). Earlier notes named only the FE solver. 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 No unmitigated P0/P1 findings. Executive Summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
|
Absorbs #312 (sdid scale fix), #313 (roadmap refresh + dCDH docstring rewrite), and #314 (within_transform convergence warnings) from main. Conflicts resolved in: - diff_diff/chaisemartin_dhaultfoeuille.py: took main's comprehensive Phase 1-3 feature list in the class docstring but merged in the PR #311 group-vs-PSU bootstrap-clustering framing and the replicate-weight survey-support line. Kept the PR #311 'user-specified cluster= not supported + automatic PSU-level under survey_design' wording for the cluster= parameter docstring (strictly more accurate than main's 'always clusters at the group level' text). - diff_diff/guides/llms-full.txt: kept main's more detailed placebo SE contract paragraph (which already distinguishes single-period NaN from multi-horizon analytical/bootstrap) and appended the sup-t / shared-weights / cross-horizon coverage details from the PR #311 update. Kept the PR #311 survey_design signature comment that mentions TSL + replicate + PSU bootstrap. Full regression across touched areas: 336 + 324 passing.
… remove deprecated SyntheticDiD params Package four merged PRs (#312 SDID catastrophic cancellation at extreme Y scale, #313 roadmap refresh, #314 FE imputation non-convergence signaling, #315 Frank-Wolfe SC weight solver non-convergence signaling) as 3.1.2. Also remove the SyntheticDiD(lambda_reg=...) and SyntheticDiD(zeta=...) kwargs, which have been deprecated with DeprecationWarning since v2.3.1 (2026-02-10) in favor of zeta_omega / zeta_lambda; their warning messages announced removal in v3.1. Passing the old kwargs now raises TypeError at __init__ and ValueError: Unknown parameter at set_params. Internal ridge-regression helpers that accept a lambda_reg parameter (compute_synthetic_weights, rank_control_units, Rust FFI bindings) are unaffected. Version strings bumped in diff_diff/__init__.py, pyproject.toml, rust/Cargo.toml, and diff_diff/guides/llms-full.txt. CHANGELOG populated with Fixed / Changed / Removed sections and comparison-link footer. TODO.md's "Deprecated Code" entry removed now that the task is done. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per CI review feedback (#316): removing public kwargs under a patch version violates Semantic Versioning, which CHANGELOG.md explicitly claims to adhere to. Restore lambda_reg and zeta handling in SyntheticDiD.__init__ and set_params as warning-only, and bump the removal target in the DeprecationWarning text from "v3.1" to "v4.0.0". The 3.1.2 release now carries only the four fix/doc PRs (#312 SDID scale, #313 roadmap, #314 FE imputation convergence, #315 Frank-Wolfe convergence) with no breaking changes. - diff_diff/synthetic_did.py: restore deprecated kwargs + warnings (v4.0.0 text) - tests/test_methodology_sdid.py: restore TestDeprecatedParams class + set_params deprecation test - tests/test_estimators.py: restore test_deprecated_params - CHANGELOG.md: drop Removed section; add Changed entry documenting the v3.1 -> v4.0.0 bump in the removal target - TODO.md: restore Deprecated Code section with v4.0.0 removal target and SemVer rationale 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
warn_if_not_convergedhelper indiff_diff.utils(reusable across axis-B fixes).convergedflag through the four alternating-projection loops inTwoStageDiD._iterative_fe,TwoStageDiD._iterative_demean,ImputationDiD._iterative_fe,ImputationDiD._iterative_demean; call the helper after the loop.linalg.py:1329-1376.Methodology references (required if estimator / math changes)
didimputationanddid2sdo not emit a non-convergence signal either, but this library already emits convergence warnings in its other IRLS paths; extending that convention to alternating projection is an additive signal, not a methodology change.**Note:**labels describing the new signal.Validation
tests/test_imputation.py::TestImputationEdgeCases(3) andtests/test_two_stage.py::TestSilentWarningAudit(3) exercise non-convergence under tighttol/max_iterand assert the warning fires, plus one negative-control test per estimator.test_imputation.py+test_two_stage.py(150 pre-existing + 6 new). Zero regressions.Security / privacy