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")
install.packages("Synth", repos = c('https://j-hai.r-universe.dev', 'https://cloud.r-project.org'))
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 around dataprep()
for the common case (panel data frame + treated unit name + treatment
date). The 13-arg dataprep() 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 with print(), plot(), and
as.data.frame() methods.synth_placebos(), synth_mspe_test(), synth_mspe_plot(),
with a plot() method — full in-space placebo workflow following
Abadie, Diamond, and Hainmueller (2010).quadopt = "cvxr" (CVXR +
CLARABEL by default) and quadopt = "torch" (Frank-Wolfe simplex LS via the torch
package, GPU/MPS-capable). Inner/outer split via quadopt_inner /
quadopt_outer keeps V-search at ipop's speed when only the final W
needs the modern solver.autoplot.synth_inference() and
autoplot.synth_placebos() (when library(ggplot2) is attached).parallel = TRUE does the
right thing on Windows (PSOCK cluster) and unix-likes (forks).vignette("synth-quickstart") for a 5-minute
intro, vignette("inference") for the inference deep dive.predictors.op now 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 when
predictors.op is not "mean" (for example "median").dataprep() — predictors.op, and the
operator of a special predictor that spans more than one period,
must name a function that accepts na.rm = TRUE and returns a
single number. Anything else ("range", "quantile", a misspelled
name) now stops with a message that says what is wrong.dataprep() lists control units and
periods in ascending order, and synth.tab() matches unit names to
weights by unit number, so an unsorted controls.identifier no
longer prints weights next to the wrong units. The weights
themselves do not change.SCtools package. The functions are now
synth_placebos(), synth_mspe_test(), synth_mspe_plot(), and
the plot() 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 = TRUE restores 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() Ylim now 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 first length(predictors) rows).GPL (>= 2). See LICENSE.note.