Skip to content

Commit fca42fe

Browse files
igerberclaude
andcommitted
synthetic-control: address CI codex R7 — deterministic low-rate exclusion regression (P2)
Add test_single_inner_nonconvergence_excluded_from_v_ranking: monkeypatch _inner_solve_W so exactly ONE objective evaluation (the uniform-start eval) reports conv=False, then assert (a) the any-occurrence "during nested V selection" warning fires and (b) the selected V is a genuine small-MSPE fit (res.mspe_v < 1.0, not the large penalty) — i.e. the truncated candidate was EXCLUDED from the argmin, not merely warned. Complements the blanket-failure (inner_max_iter=1) test. Test-only change. (Reaches the module via importlib since the diff_diff.synthetic_control attribute is the convenience function, which shadows the submodule — same pattern as diff_diff.trop.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 08957d6 commit fca42fe

1 file changed

Lines changed: 35 additions & 0 deletions

File tree

tests/test_methodology_synthetic_control.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,41 @@ def test_inner_v_search_nonconvergence_warning():
322322
synthetic_control(df, "y", "treated", "unit", "year", seed=0, inner_max_iter=1)
323323

324324

325+
def test_single_inner_nonconvergence_excluded_from_v_ranking(monkeypatch):
326+
# A single LOW-RATE non-converged objective evaluation must be EXCLUDED from V
327+
# ranking (penalized out of the argmin), not merely warned about: force exactly one
328+
# objective eval (the uniform-start eval, max(v) < 0.9) to report conv=False and
329+
# assert (a) the any-occurrence warning fires, and (b) the selected V is a genuine
330+
# small-MSPE fit (mspe_v << penalty), i.e. the truncated candidate did not win.
331+
import importlib
332+
333+
# NB: ``diff_diff.synthetic_control`` the attribute is the convenience *function*
334+
# (it shadows the submodule, same as ``diff_diff.trop``), so reach the module via
335+
# importlib to monkeypatch its module-global _inner_solve_W.
336+
sc = importlib.import_module("diff_diff.synthetic_control")
337+
338+
df, _, _ = _make_panel()
339+
real_solve = sc._inner_solve_W
340+
state = {"failed": False}
341+
342+
def patched(X1s, X0s, v, max_iter, min_decrease):
343+
w, conv = real_solve(X1s, X0s, v, max_iter, min_decrease)
344+
if not state["failed"] and float(np.max(v)) < 0.9: # a spread V => an objective eval
345+
state["failed"] = True
346+
return w, False
347+
return w, conv
348+
349+
monkeypatch.setattr(sc, "_inner_solve_W", patched)
350+
with pytest.warns(UserWarning, match="during nested V selection"):
351+
res = synthetic_control(df, "y", "treated", "unit", "year", seed=0)
352+
353+
assert state["failed"] # the patch actually fired on an objective evaluation
354+
assert np.isfinite(res.att)
355+
# Exclusion proof: the chosen V's outer-objective MSPE is a real (small) value, not
356+
# the large penalty a truncated candidate would have carried.
357+
assert res.mspe_v is not None and res.mspe_v < 1.0
358+
359+
325360
def test_n_starts_one_runs():
326361
# n_starts=1 uses only the uniform start (short-circuits the heuristic candidates)
327362
# and still produces a valid nested fit.

0 commit comments

Comments
 (0)