Lesson 7 of 11

Synthetic Control

Lesson 6 found a hidden lever, a randomized reminder, to pull a clean effect out of confounded data. But look back at every method in this course so far: matching, weighting, difference-in-differences, instruments. Each one needed a comparison group, many treated units and many untreated ones. What do you do when exactly one unit ever got the policy, one city, one company, one country, and no single other unit is a fair stand-in for it?

The answer is one of the most elegant ideas in causal inference: if no real control fits, build one. Blend several untreated units into a synthetic twin that tracks your treated unit before the policy, then read the effect as the gap that opens after.

By the end of this lesson you will be able to:

  • Explain the synthetic control setup and why a single control unit or a plain average is not good enough
  • Build a synthetic control as a weighted blend of donor units, with the weights fit on the pre-policy period only
  • Read the treatment effect as the post-policy gap, and know why a near-zero pre-policy gap is what makes it believable
  • Judge the result with a placebo test when you have just one treated unit and no standard error to lean on

Prerequisites: Lesson 3 (why a naive before/after comparison is biased) and Lesson 6 (recovering an effect when there is no clean control). You can read base R: %*%, optim, and indexing a vector.

The setup

One city taxes soda; nowhere is a perfect copy

Meet Marisol, a mid-sized city that, on the first day of month 25, put a small tax on sugary soda. The city council wants one honest number: how many cans of soda per person, per month, did the tax actually remove? Get it right and other cities will copy the policy; get it wrong and a good idea dies or a useless one spreads.

The trouble is that Marisol is the only city that taxed. We have ten comparison cities that never did, but no single one of them is a twin: some are bigger, some drink more soda, some were already cutting back faster. And soda drinking was drifting down everywhere anyway, so we cannot just compare Marisol before and after and blame the whole change on the tax.

Each lesson runs in a fresh R session, so we build the data ourselves. Because we simulated it, we know the true answer to check against: the tax truly removes 5 cans per person per month.

RInteractive R
set.seed(1) Tpre <- 24; Tpost <- 12; Tt <- Tpre + Tpost # 36 months of data; the tax starts at month 25 pre <- 1:Tpre # the 24 months BEFORE Marisol's soda tax post <- (Tpre + 1):Tt # the 12 months AFTER it # ten comparison cities that never taxed soda: cans of soda sold per person, each month donors <- sapply(1:10, function(j) 46 + cumsum(rnorm(Tt, mean = -0.03 * j, sd = 0.7))) # Marisol moves like a blend of cities 1, 2 and 3 ... until the tax cuts 5 cans a month marisol <- as.numeric(donors %*% c(0.5, 0.3, 0.2, rep(0, 7))) + rnorm(Tt, 0, 0.4) marisol[post] <- marisol[post] - 5 # the TRUE effect we will spend the lesson recovering round(head(cbind(month = 1:Tt, marisol = marisol, city1 = donors[, 1], city2 = donors[, 2])), 1) #> month marisol city1 city2 #> [1,] 1 44.7 45.5 45.7 #> [2,] 2 46.1 45.6 45.6 #> [3,] 3 45.1 45.0 46.3 #> [4,] 4 45.9 46.1 46.7 #> [5,] 5 45.9 46.3 46.6 #> [6,] 6 45.8 45.7 46.3

  

Plot all eleven cities. Marisol is the dark line; the ten comparison cities are grey. Before the tax they weave through the same band; the question is which combination of them is Marisol's true twin.

RInteractive R
library(ggplot2) traj <- data.frame(month = rep(1:Tt, 11), cans = c(marisol, as.numeric(donors)), city = rep(c("Marisol", paste0("city", 1:10)), each = Tt), treated = rep(c(TRUE, rep(FALSE, 10)), each = Tt)) donors_lines <- subset(traj, !treated) marisol_line <- subset(traj, treated) ggplot(mapping = aes(month, cans, group = city)) + geom_vline(xintercept = 24.5, linetype = "dotted") + geom_line(data = donors_lines, colour = "grey78", linewidth = 0.5) + geom_line(data = marisol_line, colour = "#1c2c4f", linewidth = 1.2) + annotate("text", x = 24.5, y = min(traj$cans), label = "soda tax", hjust = -0.05, size = 3) + labs(x = "month", y = "cans of soda per person", title = "Marisol (dark) among ten comparison cities: which blend is its twin?")