Synthetic Control Methods
Simon Mueller
2026-09-06
Source:vignettes/synthetic-control.Rmd
synthetic-control.RmdIntroduction
Synthetic control methods (Abadie and Gardeazabal, 2003; Abadie, Diamond, and Hainmueller, 2010) provide a data-driven approach to construct a counterfactual for a single treated unit from a weighted combination of donor (control) units. This is particularly useful when:
- There is a single treated unit (e.g., one firm, one country)
- A traditional event study with many events is not possible
- You want to visualize the treatment effect as a gap between actual and synthetic trajectories
The EventStudy package implements synthetic control estimation with two optimization methods and includes a placebo test for inference.
Data Setup
Synthetic control requires: - Treated unit: time series of the outcome for the treated unit - Donor pool: time series of outcomes for potential control units - Treatment time: when the intervention occurs
library(EventStudy)
library(tibble)
# Treated unit
treated_data <- tibble(
time = 1:100,
outcome = cumsum(rnorm(100, mean = 0.1, sd = 0.5))
)
# Inject treatment effect after period 60
treated_data$outcome[61:100] <- treated_data$outcome[61:100] + 5
# Donor pool (long format)
donor_data <- do.call(rbind, lapply(paste0("D", 1:10), function(u) {
tibble(
unit = u,
time = 1:100,
outcome = cumsum(rnorm(100, mean = 0.1, sd = 0.5))
)
}))Creating the Task
task <- SyntheticControlTask$new(
treated_data = treated_data,
donor_data = donor_data,
treatment_time = 61
)
print(task)
#> SyntheticControlTask
#> Donors: 10
#> Periods: 100
#> Pre-treat: 60
#> Post-treat: 40
#> Treatment: 61
#> Estimated: FALSEEstimation
Two optimization methods are available:
quadprog (default)
Uses quadprog::solve.QP() for exact quadratic
programming. Requires the quadprog package.
task <- estimate_synthetic_control(task, method = "quadprog")optim (fallback)
Uses stats::optim() with a softmax reparameterization.
No extra dependencies.
task <- estimate_synthetic_control(task, method = "optim")Results
After estimation, the task contains:
# Donor weights (sum to 1, all >= 0)
task$results$weights
#> D1 D2 D3 D4 D5 ...
#> 0.312 0.000 0.188 0.000 0.500 ...
# Average treatment effect on the treated
task$results$att
#> [1] 4.87
# Pre- and post-treatment mean squared prediction error
task$results$pre_mspe
task$results$post_mspe
# Full trajectory
task$results$trajectory
#> # A tibble: 100 x 4
#> time treated synthetic gap
#> <int> <dbl> <dbl> <dbl>
#> 1 1 0.234 0.289 -0.0547
#> ...Visualization
Three plot types are available:
Trajectory Plot
Compare the treated unit to its synthetic counterpart:
plot_synthetic_control(task, type = "trajectory")Gap Plot
Visualize the treatment effect (gap between treated and synthetic):
plot_synthetic_control(task, type = "gap")Placebo Plot
After running the placebo test, overlay placebo gaps:
task <- sc_placebo_test(task)
plot_synthetic_control(task, type = "placebo")Placebo Test
The placebo test re-estimates the synthetic control for each donor unit as a pseudo-treated unit. The treated unit’s RMSPE ratio is ranked among all units to produce a p-value:
p = \frac{\text{rank of treated ratio}}{N_{donors} + 1}
where the RMSPE ratio is \frac{RMSPE_{post}}{RMSPE_{pre}}.
task <- sc_placebo_test(task)
# p-value
task$results$placebo$p_value
# RMSPE ratios for all donor units
task$results$placebo$rmspe_ratiosYou can limit the number of placebo units for faster computation:
task <- sc_placebo_test(task, n_placebo = 5)References
- Abadie, A. and 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., and Hainmueller, J. (2010). Synthetic Control Methods for Comparative Case Studies. Journal of the American Statistical Association, 105(490), 493-505.