Causal Methods
CHAPTER 06
SC

Synthetic control

Constructs a weighted combination of untreated units that closely matches the treated unit's pre-treatment trajectory. The post-treatment gap between the treated unit and its synthetic counterpart is the estimated causal effect.

IDENTIFICATION SETUP
01When to use it

Designed for comparative case studies: one (or few) treated aggregate units — a state, country, or firm — and a larger pool of untreated units. It is not suited to settings with many treated units or individual-level data.

02The synthetic control

A weighted average of donor units whose weights are chosen to minimize the difference between the treated unit and the synthetic control in the pre-treatment period — on both the outcome and relevant predictors.

03What it estimates

The ATT for the treated unit at each post-treatment period. Unlike DiD, no parametric functional form for the outcome is assumed — the pre-treatment fit is the identification argument.

TREATED UNIT VS SYNTHETIC CONTROL

treatmentgap19801985198919952000yeartreated unitsynthetic control
Pre-periodSynthetic control tracks treated unit — fit is the validity check
Post-periodDivergence is the estimated treatment effect
GapTreated minus synthetic at each period — the ATT time path
Donor poolUntreated units whose weighted average forms the synthetic control
ASSUMPTIONS
Pre-treatment fitrequired

The synthetic control must closely track the treated unit in the pre-treatment period on both the outcome and relevant predictors. Poor pre-fit means the synthetic control is not a credible counterfactual.

HOW TO TEST

Inspect the pre-treatment MSPE (mean squared prediction error) and the path plot. An MSPE ratio (post/pre) much greater than 1 is evidence of a treatment effect, not evidence of fit.

Convex hull conditionrequired

The treated unit must lie within the convex hull of the donor units in the pre-treatment outcome space. If the treated unit is an outlier relative to all donors, no valid weights exist and the synthetic control will be a poor fit by construction.

HOW TO TEST

Inspect donor unit pre-treatment outcomes relative to the treated unit. If the treated unit is consistently above or below all donors, the method is not appropriate.

No spillovers to donor poolrequired

Donor units must be unaffected by the treatment. If the treatment creates general equilibrium effects that also shift the donors, the synthetic control is contaminated and the gap understates the true effect.

HOW TO TEST

Argue on substantive grounds. Exclude geographic or economic neighbors most likely to be affected. Run sensitivity analyses excluding plausibly contaminated donors.

Long pre-treatment periodrecommended

A longer pre-treatment period provides more data for weight optimization and stronger evidence that the pre-fit is not spurious. Short pre-periods increase the risk of chance pre-fit that does not reflect genuine comparability.

HOW TO TEST

As a heuristic, aim for at least 10 pre-treatment periods. Fewer than 5 makes the validity argument substantially weaker.

DATA REQUIREMENTS

Aggregate panel

Units are aggregates — states, countries, firms — not individuals. The method is designed for settings where a single or small number of units are treated and the donor pool is moderate in size (10–50 units).

Long pre-treatment period

At minimum 5–10 pre-treatment time periods for meaningful weight optimization. The longer the pre-period, the more credible the pre-fit and the stronger the identification argument.

Balanced outcome series

The outcome must be observed for all units in all periods. Missing data in the donor pool forces those units out of the convex hull and limits the flexibility of the synthetic control.

01_data_prep.R
library(tidyverse)

data <- read_csv("panel_data.csv")

# Synthetic control requires:
# - One (or few) treated unit(s)
# - Several control units (the "donor pool")
# - A long pre-treatment panel (ideally 10+ periods)
# - An outcome variable observed for all units in all periods

# Inspect pre-treatment fit potential
data |>
  filter(year < treat_year) |>
  group_by(unit, treated) |>
  summarise(mean_outcome = mean(outcome), .groups = "drop")

# Visualize raw trends — treated vs donor pool
data |>
  mutate(group = if_else(unit == "California", "treated", "donor")) |>
  ggplot(aes(x = year, y = outcome, group = unit, color = group, alpha = group)) +
  geom_line() +
  scale_alpha_manual(values = c(treated = 1, donor = 0.25)) +
  geom_vline(xintercept = treat_year, linetype = "dashed") +
  labs(title = "Pre-treatment outcome trends")
ESTIMATION

The synthetic control weights are chosen to minimize the pre-treatment mean squared prediction error (MSPE) between the treated unit and the weighted average of donors — subject to weights being non-negative and summing to one. This constrained optimization ensures the synthetic control is an interpretable weighted average, not an extrapolation.

02_synth.R
library(Synth)

# Prepare data in Synth format
data_prep <- dataprep(
  foo = as.data.frame(data),
  predictors = c("gdp", "population", "unemployment"),
  predictors.op = "mean",             # average over pre-period
  time.predictors.prior = 1980:1988,   # pre-treatment years
  special.predictors = list(
    list("outcome", 1975, "mean"),     # lagged outcome values
    list("outcome", 1980, "mean"),
    list("outcome", 1985, "mean")
  ),
  dependent = "outcome",
  unit.variable = "unit_id",
  unit.names.variable = "unit",
  time.variable = "year",
  treatment.identifier = 3,           # treated unit id
  controls.identifier = c(1,2,4:38),# donor pool ids
  time.optimize.ssr = 1980:1988,   # pre-treatment for optimization
  time.plot = 1970:2000
)

# Fit synthetic control (minimizes pre-period MSPE)
synth_out <- synth(data_prep)
synth_tables(synth_out, data_prep)    # weights and balance

# Plot: treated vs synthetic control
path.plot(
  synth.res = synth_out,
  dataprep.res = data_prep,
  Ylab = "Outcome",
  Xlab = "Year",
  Ylim = c(0, 100),
  Legend = c("Treated", "Synthetic")
)
abline(v = 1989, lty = 2)
INFERENCE — PLACEBO TESTS

Standard asymptotic inference does not apply — there is typically one treated unit. Instead, randomization inference uses placebo tests: apply the synthetic control procedure to each donor unit as if it were treated, and compare the treated unit's post-treatment gap to the distribution of placebo gaps.

In-place placebos

Apply the synthetic control to each donor unit in turn, using the remaining units as the donor pool. The treated unit's gap should be larger than most placebo gaps — its rank gives an exact p-value.

MSPE ratio test

For each unit, compute the ratio of post-period MSPE to pre-period MSPE. Units with a poor pre-fit (high pre-MSPE) are uninformative placebos — filter them out before computing the p-value.

In-time placebos

Shift the treatment date to a period before the actual intervention. If a large gap emerges at the fake treatment date, it suggests the pre-fit is not credible or the outcome was already trending.

Leave-one-out

Remove one donor unit at a time and re-estimate the synthetic control. If the gap changes substantially, the estimate is driven by that donor — inspect whether it is a plausible control.

03_inference.R
library(Synth)

# Placebo inference: apply synthetic control to each donor unit
# Treated as if it were the treated unit
# H0: treatment effect is zero — placebo effects should be small

# Run placebo tests for all control units
placebos <- vector("list", length(controls))
for (i in seq_along(controls)) {
  d_placebo <- dataprep(
    ...,
    treatment.identifier = controls[i],
    controls.identifier = c(treated_id, controls[-i])
  )
  placebos[[i]] <- synth(d_placebo)
}

# Compute MSPE ratio:
# Post-period MSPE / Pre-period MSPE for treated vs placebos
# A large ratio for the treated unit (relative to placebos) is evidence
# of a genuine treatment effect
# p-value = rank of treated MSPE ratio among all units

gaps.plot(
  synth.res = synth_out,
  dataprep.res = data_prep,
  Ylab = "Gap (treated - synthetic)",
  Xlab = "Year"
)
OUTPUT INTERPRETATION

My pre-treatment MSPE is low but the donors have large weights on a few units — is that a concern?

Yes. A synthetic control that concentrates weight on one or two donors is fragile — remove either donor and the estimate changes substantially. Run leave-one-out robustness checks. Sparse weights can be unavoidable when the treated unit is unusual, but they should be reported and interrogated.

The placebo p-value is 0.10 — is my estimate significant?

With a small donor pool (e.g. 10 units), a p-value of 0.10 means the treated unit ranks first out of 10. This is the smallest achievable p-value with that pool size — it is as significant as the method allows. Report the exact rank and pool size rather than a conventional threshold.

My synthetic control fits the pre-period well but I only have 5 pre-treatment periods — how worried should I be?

Moderately worried. A good fit over 5 periods can be achieved by chance — particularly if the donor pool is large relative to the number of pre-periods. Fewer periods means the optimization has fewer constraints and the pre-fit is less informative. Report this limitation and rely more heavily on the placebo distribution for credibility.

Can I use synthetic control with multiple treated units?

The classic Abadie-Diamond-Hainmueller method is designed for a single treated unit. For multiple treated units, consider the generalized synthetic control (gsynth package in R), which estimates interactive fixed effects models, or augmented synthetic control methods (augsynth) that add regression adjustment for improved pre-period fit.