Synthetic Control — 20 methods, one package¶
StatsPAI bundles a synthetic-control workflow with 20 estimator entry points, 6 inference strategies, automatic comparison, power analysis, sensitivity diagnostics, and report helpers. Validation strength varies by method and is reported through method-level metadata and the JSS evidence ledger. Validation status remains method-specific; broad API coverage is not a blanket parity claim.
Quick start¶
import statspai as sp
# Load the California Proposition 99 dataset (Abadie et al. 2010 style)
df = sp.synth.california_tobacco()
# Classic SCM (Abadie, Diamond & Hainmueller 2010)
res = sp.synth(
df, outcome='cigsale', unit='state', time='year',
treated_unit='California', treatment_time=1989,
method='classic',
)
print(res.summary())
sp.synth() is the unified dispatcher — switch method= to run any
of the 20 variants with the same API and return type.
The 20 methods¶
method= |
Paper | When to use |
|---|---|---|
classic |
Abadie, Diamond & Hainmueller (2010) | Baseline. One treated unit, few pre-periods, good donor fit. |
penalized / ridge |
Doudchenko & Imbens (2016) | When classic weights are unstable; ridge improves extrapolation. |
demeaned |
Ferman & Pinto (2021) | When unit fixed effects are large; demean before matching. |
unconstrained / elastic_net |
Doudchenko & Imbens (2016) | When negative/unbounded weights are theoretically justified. |
augmented / ascm |
Ben-Michael, Feller & Rothstein (2021) | When no exact pre-treatment fit exists; bias-corrects classic SCM. |
sdid |
Arkhangelsky et al. (2021) | Combines DID & SCM; robust to effect heterogeneity. |
factor / gsynth |
Xu (2017) | Multiple treated units + interactive fixed effects. |
staggered |
Ben-Michael, Feller & Rothstein (2022) | Heterogeneous adoption times. |
mc |
Athey et al. (2021) | Many missing cells; matrix completion via nuclear norm. |
discos |
Gunsilius (2023) | When treatment affects the whole distribution, not just the mean. |
multi_outcome |
Sun (2023) | Multiple correlated outcomes; borrows strength across. |
scpi |
Cattaneo, Feng & Titiunik (2021) | Need honest prediction intervals, not placebo p-values. |
bayesian |
Vives & Martinez (2024) | Want full posterior; Dirichlet prior + MCMC credible intervals. |
bsts / causal_impact |
Brodersen et al. (2015) | Structural TS with level/trend; the Google CausalImpact model. |
penscm |
Abadie & L'Hour (2021) | Many similar donors; pairwise discrepancy penalty ensures interpretability. |
fdid |
Li (2024) | Want a subset of donors, not weights — simple to communicate. |
cluster |
Rho (2024) | Large donor pool; cluster first, then SCM within best cluster. |
sparse / lasso |
Amjad, Shah & Shen (2018) | High-dimensional donors; L1 picks a handful. |
kernel |
RKHS / MMD matching | Nonlinear donor relationships. |
kernel_ridge |
Kernel ridge regression | Nonlinear, unconstrained, regularised. |
Inference strategies¶
| Strategy | Call | When |
|---|---|---|
| Placebo (in-space) | default | Always available; exchangeable donor pool. |
| Conformal | method='conformal' or conformal_synth() |
Want finite-sample coverage guarantees (Chernozhukov et al. 2021). |
| Prediction intervals | scpi() |
Account for parameter + residual + design uncertainty. |
| Bootstrap / jackknife | SDID internal | Inference for SDID. |
| Bayesian posterior | bayesian_synth() |
Full credible intervals from MCMC. |
| BSTS posterior | bsts_synth() |
Kalman filter / smoother posterior draws. |
Research workflow¶
Run all methods at once¶
comp = sp.synth_compare(df, outcome='cigsale', unit='state', time='year',
treated_unit='California', treatment_time=1989)
comp.plot() # overlay all counterfactuals
comp.summary_table() # ATT, pre-RMSPE, p-value per method
best = sp.synth_recommend(comp) # auto-pick best by pre-fit + robustness
Power analysis before you commit¶
power = sp.synth_power(df, outcome='cigsale', unit='state', time='year',
treated_unit='California', treatment_time=1989,
effect_sizes=[2, 5, 10, 15, 20], n_sims=500)
sp.synth_power_plot(power)
# What's the minimum detectable effect at 80% power?
mde = sp.synth_mde(df, outcome='cigsale', ..., target_power=0.80)
Sensitivity suite¶
sens = sp.synth_sensitivity(res) # one-shot: LOO + time + donor + RMSPE
sp.synth_sensitivity_plot(sens)
# or individual components
sp.synth_loo(res) # leave-one-donor-out
sp.synth_time_placebo(res) # backdate the treatment time
sp.synth_donor_sensitivity(res) # random donor-pool subsampling
sp.synth_rmspe_filter(res) # drop high-residual placebos
Structured report helper¶
sp.synth_report(res, format='markdown') # 'text' | 'markdown' | 'latex'
sp.synth_report_to_file(res, 'analysis.md')
Produces: abstract, method, weights, placebo distribution, sensitivity, references — directly pasteable into a paper or appendix.
Canonical datasets¶
StatsPAI ships the three canonical SCM datasets as realistic simulations (same structure as the published data; use for teaching and unit tests):
sp.synth.california_tobacco() # Prop 99 (1989)
sp.synth.german_reunification() # Abadie, Diamond & Hainmueller (2015)
sp.synth.basque_terrorism() # Abadie & Gardeazabal (2003)
For real published data, use the causaldata R package or the authors'
replication files — StatsPAI does not redistribute copyrighted data.
Choosing a method¶
If you are unsure, run sp.synth_compare(...) and inspect:
- Pre-RMSPE — lower is better fit.
- Consistency across methods — if classic/ASCM/SDID/MC agree, the answer is robust. If they disagree, inspect why.
- Placebo distribution — is the treated-unit effect extreme relative to placebos?
Then run sp.synth_sensitivity(best) to stress-test the winner.
References¶
- Abadie, Diamond & Hainmueller (2010). JASA 105(490).
- Abadie & L'Hour (2021). JASA 116(536).
- Amjad, Shah & Shen (2018). JMLR 19(22).
- Arkhangelsky, Athey, Hirshberg, Imbens & Wager (2021). AER 111(12).
- Athey, Bayati, Doudchenko, Imbens & Khosravi (2021). JASA 116(536).
- Ben-Michael, Feller & Rothstein (2021). JASA 116(536).
- Brodersen, Gallusser, Koehler, Remy & Scott (2015). AOAS 9(1).
- Cattaneo, Feng & Titiunik (2021). JASA 116(536).
- Chernozhukov, Wüthrich & Zhu (2021). JASA 116(536).
- Doudchenko & Imbens (2016). NBER WP 22791.
- Ferman & Pinto (2021). Quantitative Economics 12(4).
- Gunsilius (2023). Econometrica 91(3).
- Li (2024). Forward Difference-in-Differences.
- Rho (2024). Cluster Synthetic Control Methods.
- Sun (2023). ReStat forthcoming.
- Vives & Martinez (2024). JCGS forthcoming.
- Xu (2017). Political Analysis 25(1).
For Agents¶
Pre-conditions - panel data in long form (unit × time × outcome) - single treated unit (classic) or a treatment-timing column (staggered) - ≥ 10 donor (untreated) units with similar pre-treatment trajectories - ≥ 10 pre-treatment periods (fewer → large weight on any one year)
Identifying assumptions - Treatment effect on the treated is identified by the counterfactual implicit in the donor weights - No spillover from treated unit to donors (SUTVA) - Donor pool contains units whose outcomes plausibly track the treated counterfactual - Pre-treatment fit (RMSPE) is small relative to post-treatment effect for placebo inference
Failure modes → recovery
| Symptom | Exception | Remedy | Try next |
|---|---|---|---|
| Pre-treatment RMSPE > post-treatment effect | AssumptionWarning |
Poor pre-fit — switch to method='demeaned'/'augmented' or enlarge donor pool. | sp.synth |
| Placebo p-value ≥ 0.1 despite visible gap | AssumptionWarning |
Use inference='conformal' (valid under weak assumptions) or report ranked placebo statistic. | sp.synth |
| All weight concentrated on one donor | AssumptionWarning |
Interpolation bias risk — check method='elastic_net' or augmented SCM. | sp.synth |
| Treated unit outside donor convex hull | IdentificationFailure |
Extrapolation needed — use method='unconstrained' or 'augmented'. | sp.synth |
Alternatives (ranked)
- sp.sdid
- sp.did
- sp.matrix_completion
- sp.causal_impact
Typical minimum N: 10