Truncated History robustness check#
When to use this diagnostic#
A synthetic control fits the treated unit over a chosen pre-treatment window. How far back that window reaches is a modelling choice, and a credible effect should not depend on it. The Truncated History (TH) framework of Spoelstra, Stolp, Golsteyn, Cornelisz and van Klaveren (2025) makes that dependence visible: it re-estimates the effect on truncated pre-treatment windows and profiles how the ATT moves with the pretreatment horizon. A stable profile supports a causal reading; an unstable one says a single point estimate is fragile and an interval is the more honest summary.
Use it as a routine robustness check after any synthetic-control or difference-in-differences fit – it is the pretreatment-horizon analogue of the in-space placebo and leave-one-out checks, and it is especially useful for telling a genuine null apart from a meaningful effect when the two are hard to distinguish.
How it works#
mlsynth.truncated_history() re-runs an estimator on truncated panels and
collects, per window, the ATT, the pre-treatment MSPE, and whatever inference the
estimator reports (an in-space placebo p-value, a standard error). It is
estimator-agnostic: it accepts any mlsynth estimator that returns the standard
result object, so the same call profiles VanillaSC, SDID, PDA and the
rest. The truncation modes are:
"left"drops the earliest pre-periods one at a time – the left-Truncated History check, which targets over-reliance on distant pretreatment data;"right"drops the latest pre-periods (the in-time placebo direction);"loo"and"l2o"leave one or two pre-periods out;"random"drops random subsets of pre-periods, repeatedly.
The returned mlsynth.TruncatedHistoryResult carries the full-sample ATT,
the per-window profile, the ATT interval across windows, and a heuristic
stable verdict (the ATT keeps its sign and its spread stays small relative to
its mean).
Example#
import pandas as pd
from mlsynth import truncated_history, SDID
# California Proposition 99 (treated from 1989)
df = pd.read_csv("basedata/P99data.csv").rename(columns={"cigsale": "y"})
df["treat"] = ((df["state"] == "California") & (df["year"] >= 1989)).astype(int)
cfg = {"df": df, "outcome": "y", "treat": "treat",
"unitid": "state", "time": "year"}
res = truncated_history(SDID, cfg, mode="left")
print(res.att_full, res.stable) # -15.6, True
for w in res.profile[:4]:
print(w.label, round(w.att, 1)) # 1971-1988 -16.3, 1972-1988 -16.8, ...
print(res.stability_note)
Verification#
The left-TH profile reproduces Table 1 of Spoelstra et al. (2025) on California’s
tobacco program. Run through mlsynth’s SDID, the ATTs match the paper to the
decimal across the reported windows (full sample \(-15.6\); 1971–1988
\(-16.3\); 1972–1988 \(-16.7\); 1974–1988 \(-17.2\)), and the
profile is flagged stable – the paper’s finding that SDID is robust to the
pretreatment horizon. This is pinned in
mlsynth/tests/test_truncated_history.py and as the durable benchmark case
th_prop99.
Core API#
- mlsynth.truncated_history(estimator: Any, config: Dict[str, Any], *, mode: str = 'left', min_pre: int = 2, n_random: int = 20, seed: int = 0, stability_tol: float = 0.25) TruncatedHistoryResult#
Run the Truncated History robustness check on an mlsynth estimator.
- Parameters:
estimator – An mlsynth estimator class (anything callable as
estimator(config)whose.fit()returns aBaseEstimatorResults), e.g.VanillaSCorSDID.config – The estimator’s config dict, including
dfand thetime/treat/unitid/outcomekeys. The panel is truncated per window and the estimator re-run;display_graphsis forced off.mode – One of
"left","right","loo","l2o","random".min_pre – Minimum number of pre-treatment periods any truncated window must retain.
n_random – Number of random draws for
mode="random".seed – RNG seed for
mode="random".stability_tol – The ATT is flagged
stablewhen it keeps its sign across all windows and its spread(max-min)is at moststability_toltimes the mean magnitude.
- Returns:
TruncatedHistoryResult – The full-sample ATT, the per-window profile, and a stability verdict.
- class mlsynth.TruncatedHistoryResult(*, mode: str, att_full: float, profile: List[TruncatedHistoryWindow], att_min: float, att_max: float, att_mean: float, stable: bool, stability_note: str)#
The TH stability profile across truncated pre-treatment windows.
- model_config: ClassVar[ConfigDict] = {'frozen': True}#
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class mlsynth.TruncatedHistoryWindow(*, label: str, n_pre_periods: int, att: float, pre_mspe: float | None = None, p_value: float | None = None, standard_error: float | None = None)#
One truncated-window re-estimate.
- model_config: ClassVar[ConfigDict] = {'frozen': True}#
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].