Skip to content

feat(synthetic_control): ADH-2015 §4 tail diagnostics (regression_weights + sparse_synthetic_control)#608

Merged
igerber merged 1 commit into
mainfrom
feature/scm-adh2015-tail-diagnostics
Jul 3, 2026
Merged

feat(synthetic_control): ADH-2015 §4 tail diagnostics (regression_weights + sparse_synthetic_control)#608
igerber merged 1 commit into
mainfrom
feature/scm-adh2015-tail-diagnostics

Conversation

@igerber

@igerber igerber commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add two opt-in ADH-2015 §4 robustness diagnostics to SyntheticControl, closing the last two §4 checklist items:
    • regression_weights() — the implied donor weights W^reg = X0a'(X0a X0a')^{-1} X1a of the regression counterfactual (intercept-augmented, so they sum to 1 at full row rank), flagging donors an OLS counterfactual would push outside [0, 1] — the extrapolation the simplex-constrained synthetic control avoids. Pure linear algebra (no refit), computed as the min-norm least-squares solution with a rank-deficient warning.
    • sparse_synthetic_control() — exhaustive C(J, l) size-l donor-subset search holding V fixed at the baseline fit (captured on the fit snapshot), reporting how fit / ATT degrade as the synthetic is forced sparse. Default sizes=[1,2,3] skips over-cap sizes with a warning; an explicitly requested oversized l raises (max_subsets guard).
  • Both surface under estimator_native_diagnostics (not_run → populated) and leave the analytical inference contract unchanged (se/t_stat/p_value/conf_int stay NaN). No behavior change to any existing fit — the only fit-path addition is capturing the (X1s, X0s, V) triple on the snapshot (nulled on pickle and in in-time-truncated snapshots).

Methodology references (required if estimator / math changes)

  • Method name(s): SyntheticControl — Abadie-Diamond-Hainmueller (2015) §4 robustness diagnostics (regression-weight extrapolation; sparse-SC subset search).
  • Paper / source link(s): Abadie, Diamond & Hainmueller (2015), Comparative Politics and the Synthetic Control Method, AJPS 59(2):495–510 — regression-vs-SC extrapolation (pp. 498–499), sparsity-vs-fit (pp. 506–507, footnote 20). Documented in docs/methodology/REGISTRY.md §SyntheticControl; paper review in docs/methodology/papers/abadie-diamond-hainmueller-2015-review.md.
  • Any intentional deviations from the source (and why): regression_weights() computes W^reg as a min-norm least-squares solution — identical to ADH's closed-form Gram inverse at full row rank, but well-defined and better-conditioned when the intercept-augmented predictor matrix is rank-deficient (k+1 > J or collinear predictors); flagged with a UserWarning and documented as a REGISTRY Note (sum-to-one and row-scaling invariance both hold only at full row rank). No R anchor exists (R Synth has neither diagnostic) — validated by a numpy oracle of the W^reg formula and by self-consistency (the exhaustive sparse winner and its weights match an independent brute-force search using the same fixed baseline V).

Validation

  • Tests added/updated: tests/test_methodology_synthetic_control.py (+19 tests) — W^reg numpy oracle, full-rank row-scaling invariance, rank-deficient min-norm warning, sparse brute-force self-consistency (proving V is held fixed, not re-searched), v_method="cv" validation-window capture, max_subsets / non-integer-sizes / empty-sizes input validation, fail-closed on unpickled / non-converged / too-few-donors, and DiagnosticReport surfacing. Existing SyntheticControl + DiagnosticReport suites pass unchanged.
  • Backtest / simulation / notebook evidence (if applicable): N/A (opt-in diagnostics; no tutorial-facing change).

Security / privacy

  • Confirm no secrets/PII in this PR: Yes

Generated with Claude Code

…_weights + sparse_synthetic_control)

Close the last two Abadie-Diamond-Hainmueller (2015) §4 checklist items with two
opt-in SyntheticControlResults methods:

- regression_weights(): implied regression-counterfactual donor weights
  W^reg = X0a'(X0a X0a')^{-1} X1a (intercept-augmented, min-norm least-squares),
  flagging donors an OLS counterfactual would push outside [0,1] -- the extrapolation
  the simplex-constrained synthetic control avoids. Pure linear algebra, no refit;
  min-norm + rank-deficient warning when k+1 > J.
- sparse_synthetic_control(): exhaustive C(J,l) size-l donor subset search holding V
  FIXED at the baseline fit (captured on the fit snapshot as fit_X1s/fit_X0s/fit_v),
  reporting how fit/ATT degrade as the synthetic is forced sparse. Default sizes=[1,2,3]
  skips over-cap sizes with a warning; an explicitly requested oversized l raises
  (max_subsets guard).

Both surface under estimator_native_diagnostics (not_run -> populated) and leave the
analytical inference contract unchanged (se/t_stat/p_value/conf_int stay NaN). No
behavior change to any existing fit. Docs + paper-review checklist + 13 tests included.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHDijzf8zHXk5T8ahS2mKi
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

PR Review Report

Overall Assessment

Looks good — no unmitigated P0/P1 findings.

Executive Summary

  • The affected method is SyntheticControl, specifically ADH-2015 §4 opt-in diagnostics: regression_weights() and sparse_synthetic_control().
  • Methodology matches the new REGISTRY.md entries: W^reg uses intercept augmentation and sparse search holds baseline V fixed.
  • The rank-deficient W^reg min-norm behavior is explicitly documented as a REGISTRY.md Note, so it is not a defect.
  • No new inline inference or partial NaN-guard anti-patterns found; analytical SCM inference remains NaN.
  • Edge cases for too-few donors, unpickled results, invalid sizes, oversized subset search, and all-failed sparse subsets are handled.
  • I could not run pytest because pytest is not installed in this environment; static AST parsing passed.

Methodology

Finding 1

Severity: P3 informational
Location: diff_diff/synthetic_control.py:L2419-L2451, docs/methodology/REGISTRY.md:L2195-L2196
Impact: regression_weights() uses a min-norm least-squares solution when the intercept-augmented predictor matrix is not full row rank, rather than the literal Gram inverse. This is a documented deviation/extension and emits a warning.
Concrete fix: None required. The behavior is documented with a REGISTRY.md Note and covered by tests.

Code Quality

No findings.

Performance

No findings. The combinatorial sparse search has a max_subsets guard and default oversize skips.

Maintainability

No findings.

Tech Debt

No findings. The previous TODO item for these ADH-2015 tail diagnostics was removed because this PR implements it.

Security

No findings. No secrets or sensitive literals were introduced in the changed files reviewed.

Documentation/Tests

Finding 1

Severity: P3 informational
Location: tests/test_methodology_synthetic_control.py:L4281-L4565
Impact: The PR adds focused tests for the new diagnostics, including formula oracle checks, rank-deficient warnings, fixed-V sparse search, serialization fail-closed behavior, invalid inputs, and DiagnosticReport surfacing. I could not execute them locally because pytest is unavailable in the review environment.
Concrete fix: None for the PR. CI should run the added test suite. Static AST parsing of changed Python files succeeded.

@igerber igerber added the ready-for-ci Triggers CI test workflows label Jul 3, 2026
@igerber igerber merged commit 0eb4d33 into main Jul 3, 2026
33 of 34 checks passed
@igerber igerber deleted the feature/scm-adh2015-tail-diagnostics branch July 3, 2026 19:25
@igerber igerber mentioned this pull request Jul 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-ci Triggers CI test workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant