Skip to content

Commit b72c0b9

Browse files
igerberclaude
andcommitted
Address local AI review P3: backward-compat test for legacy snapshots
Pre-fix _SyntheticDiDFitSnapshot objects don't carry Y_shift / Y_scale fields. After this PR those fields default to (0.0, 1.0), which makes the new normalization path a pure no-op on the legacy snapshot. Add a regression test that overwrites results_._fit_snapshot with a manually-constructed snapshot using only the pre-PR fields, then confirms both in_time_placebo() and sensitivity_to_zeta_omega() preserve schema and row count. Locks in the backward-compatibility contract so future refactors don't accidentally tighten the normalization path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 37bb13e commit b72c0b9

1 file changed

Lines changed: 57 additions & 0 deletions

File tree

tests/test_methodology_sdid.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2423,6 +2423,63 @@ def test_in_time_placebo_detectable_at_extreme_scale(self):
24232423
)
24242424

24252425

2426+
class TestDiagnosticSnapshotBackwardCompat:
2427+
"""Locks in the backward-compatibility contract for legacy
2428+
_SyntheticDiDFitSnapshot objects that pre-date Y_shift/Y_scale. Their
2429+
defaults (0.0, 1.0) must make the new normalization a pure no-op so
2430+
older cached snapshots still drive diagnostic refits unchanged."""
2431+
2432+
def test_legacy_snapshot_defaults_are_noop(self):
2433+
from diff_diff.results import _SyntheticDiDFitSnapshot
2434+
2435+
data = _make_panel(seed=42)
2436+
with warnings.catch_warnings():
2437+
warnings.simplefilter("ignore", UserWarning)
2438+
r = SyntheticDiD(variance_method="jackknife", seed=1).fit(
2439+
data, outcome="outcome", treatment="treated",
2440+
unit="unit", time="period",
2441+
post_periods=[5, 6, 7],
2442+
)
2443+
2444+
# Baseline diagnostic output with the real (fit-captured) normalization.
2445+
placebo0 = r.in_time_placebo()
2446+
sens0 = r.sensitivity_to_zeta_omega()
2447+
2448+
# Overwrite the snapshot with a legacy one built without Y_shift /
2449+
# Y_scale — the defaults must make the two diagnostic paths produce
2450+
# the same output as the fit-captured version, because the main
2451+
# fit's Y-shift/scale choice is a no-op on a small, well-scaled
2452+
# panel (Y_shift ~ 10, Y_scale ~ O(1), so (Y - shift)/scale is just
2453+
# a shifted/scaled copy of Y).
2454+
snap = r._fit_snapshot
2455+
legacy_snap = _SyntheticDiDFitSnapshot(
2456+
Y_pre_control=np.array(snap.Y_pre_control),
2457+
Y_post_control=np.array(snap.Y_post_control),
2458+
Y_pre_treated=np.array(snap.Y_pre_treated),
2459+
Y_post_treated=np.array(snap.Y_post_treated),
2460+
control_unit_ids=list(snap.control_unit_ids),
2461+
treated_unit_ids=list(snap.treated_unit_ids),
2462+
pre_periods=list(snap.pre_periods),
2463+
post_periods=list(snap.post_periods),
2464+
w_control=snap.w_control,
2465+
w_treated=snap.w_treated,
2466+
# Defaults — no Y_shift/Y_scale captured.
2467+
)
2468+
# Confirm the defaults are what we expect.
2469+
assert legacy_snap.Y_shift == 0.0
2470+
assert legacy_snap.Y_scale == 1.0
2471+
2472+
r._fit_snapshot = legacy_snap
2473+
placebo_legacy = r.in_time_placebo()
2474+
sens_legacy = r.sensitivity_to_zeta_omega()
2475+
2476+
# Shape and columns must match.
2477+
assert list(placebo_legacy.columns) == list(placebo0.columns)
2478+
assert list(sens_legacy.columns) == list(sens0.columns)
2479+
assert len(placebo_legacy) == len(placebo0)
2480+
assert len(sens_legacy) == len(sens0)
2481+
2482+
24262483
class TestHeterogeneousAndRampingScale:
24272484
"""D-4b: the existing TestScaleEquivariance suite is affine-only
24282485
(Y → a*Y + b with a single scalar a). These pathways are not covered:

0 commit comments

Comments
 (0)