R implementation of the synthetic control method for causal inference in comparative case studies. Estimates the effect of an intervention on a single unit (a state, country, firm, etc.) by constructing a synthetic control unit as a weighted combination of comparison units that approximates the treated unit's pre-intervention trajectory on a set of predictors. The post-intervention divergence between the treated unit and its synthetic control is the estimated treatment effect.
The method and this implementation are described in:
Abadie, A., & Gardeazabal, J. (2003). "The Economic Costs of Conflict: A Case Study of the Basque Country." American Economic Review, 93(1), 113–132.
Abadie, A., Diamond, A., & Hainmueller, J. (2010). "Synthetic Control Methods for Comparative Case Studies: Estimating the Effect of California's Tobacco Control Program." Journal of the American Statistical Association, 105(490), 493–505.
Abadie, A., Diamond, A., & Hainmueller, J. (2011). "Synth: An R Package for Synthetic Control Methods in Comparative Case Studies." Journal of Statistical Software, 42(13), 1–17.
Abadie, A., Diamond, A., & Hainmueller, J. (2014). "Comparative Politics and the Synthetic Control Method." American Journal of Political Science, 59(2), 495–510.
# From CRAN (stable)
install.packages("Synth")
# Development version from GitHub
# install.packages("remotes")
remotes::install_github("j-hai/Synth")- Project page — package overview and current release notes.
- Explainer — a self-contained tutorial on the synthetic control method for R and Stata users, with the canonical Proposition 99 example worked end-to-end.
The recommended workflow is synth_data() → synth() → synth.tab() /
path.plot() / gaps.plot(). The example below reproduces the
California Proposition 99 analysis from Abadie, Diamond, and
Hainmueller (2010) using the bundled smoking panel (39 states ×
1970–2000 × cigarette sales + four predictors).
library(Synth)
data(smoking)
# Build the input matrices from a panel data frame
dataprep.out <- synth_data(
panel = smoking,
outcome = "cigsale",
unit_col = "state_id",
time_col = "year",
treated = "California",
treatment_time = 1989,
predictors = c("lnincome", "age15to24", "retprice", "beer"),
special_predictors = list(
list("cigsale", 1988, "mean"),
list("cigsale", 1980, "mean"),
list("cigsale", 1975, "mean")
),
unit_names_col = "state_name"
)
# Construct the synthetic control unit
synth.out <- synth(dataprep.out)
# Inspect: donor weights and predictor balance
print(synth.tab(synth.res = synth.out, dataprep.res = dataprep.out))
# Plot the treated unit vs. its synthetic control
path.plot(synth.res = synth.out, dataprep.res = dataprep.out,
Ylab = "Per-capita cigarette sales (packs)",
Xlab = "Year",
Legend = c("California", "Synthetic California"))
# Plot the gap (treated − synthetic) over time
gaps.plot(synth.res = synth.out, dataprep.res = dataprep.out,
Ylab = "Gap in per-capita cigarette sales",
Xlab = "Year",
Main = "Proposition 99 (1989) gap, California − synthetic")After Proposition 99 raised cigarette taxes in 1989, California's
per-capita sales fall well below its synthetic counterpart — the
post-1988 gap is the estimated treatment effect. The synthetic
California puts most of its weight on Utah (≈34%), Nevada (≈25%),
Montana (≈20%), and Connecticut (≈11%); synth.tab() reports the
full weight vector and pre-period predictor balance.
For placebo inference around this estimate, see
vignette("inference"). The classic 13-argument dataprep()
interface still exists for advanced cases (per-predictor time
windows, custom predictor matrices); see ?dataprep. The
Basque-country application from Abadie & Gardeazabal (2003) is
available via data(basque).
synth_data()— one-line ergonomic wrapper arounddataprep()for the common case (panel data frame + treated unit name + treatment date). The 13-argdataprep()is still there for advanced cases.synth_inference()— split-conformal (Chernozhukov–Wuthrich–Zhu) and parametric Gaussian prediction intervals around the synthetic counterfactual. Returns an S3 object withprint(),plot(), andas.data.frame()methods.synth_placebos(),synth_mspe_test(),synth_mspe_plot(), with aplot()method — full in-space placebo workflow following Abadie, Diamond, and Hainmueller (2010).- Optional alternative QP backends —
quadopt = "cvxr"(CVXR + CLARABEL by default) andquadopt = "torch"(Frank-Wolfe simplex LS via thetorchpackage, GPU/MPS-capable). Inner/outer split viaquadopt_inner/quadopt_outerkeeps V-search at ipop's speed when only the final W needs the modern solver. - ggplot2 support —
autoplot.synth_inference()andautoplot.synth_placebos()(whenlibrary(ggplot2)is attached). - Cross-platform parallel placebos —
parallel = TRUEdoes the right thing on Windows (PSOCK cluster) and unix-likes (forks). - Two vignettes —
vignette("synth-quickstart")for a 5-minute intro,vignette("inference")for the inference deep dive. predictors.opnow applies to the control units too —dataprep()used to apply it to the treated unit only and always averaged the controls with the mean. Results change only whenpredictors.opis not"mean"(for example"median").- Operator checks in
dataprep()—predictors.op, and the operator of a special predictor that spans more than one period, must name a function that acceptsna.rm = TRUEand returns a single number. Anything else ("range","quantile", a misspelled name) now stops with a message that says what is wrong. - Labels follow the data —
dataprep()lists control units and periods in ascending order, andsynth.tab()matches unit names to weights by unit number, so an unsortedcontrols.identifierno longer prints weights next to the wrong units. The weights themselves do not change. - Placebo function names — development versions of 1.2-0 used
names that clash with the
SCtoolspackage. The functions are nowsynth_placebos(),synth_mspe_test(),synth_mspe_plot(), and theplot()method; the change log lists the old names.
| Question | Method | Function | Package |
|---|---|---|---|
| How surprising is the effect vs. other units? | placebo MSPE-ratio rank | synth_mspe_test() |
Synth |
| Prediction band around the counterfactual (lightweight) | split-conformal | synth_inference(method = "conformal") |
Synth |
| Prediction band assuming i.i.d. Gaussian residuals | parametric | synth_inference(method = "parametric") |
Synth |
| Period-varying intervals decomposing in/out-of-sample uncertainty | CFPT prediction intervals | scpi::scpi() |
scpi |
| Multiple / staggered treated units | augmented or generalized SC | augsynth, gsynth |
augsynth, gsynth |
| GPU autodiff implementation of the SC family | torch-native SC + synthdid + MC | trex.panel |
trex (Python) |
See NEWS.md for the full change log.
synth(verbose = FALSE)(the default) is now genuinely silent.verbose = TRUErestores the previous chatter and adds the MSPE/loss summary at the end.quadopt = "LowRankQP"(long deprecated) now errors fast instead of printing a message and crashing in undefined-variable code a few lines later.path.plot()Ylimnow pads by a fraction of the data range, so plots of negative-valued series are no longer cropped at the bottom.dataprep(): the missing-data warning loop in the X0 (control predictors) section now iterates over all rows of X0 (it previously only covered the firstlength(predictors)rows).- Several typos fixed in error/warning messages.
GPL (>= 2). See LICENSE.note.