18  Synthetic Control in R and Stata

Published

September 12, 2021

18.1 Synthetic control

Abadie et al. have a few papers on synthetic control (https://economics.mit.edu/files/11859, https://economics.mit.edu/files/17847). The key idea is for a case study situation. A single unit, say a state/firm/country is exposed to a shock, and we are interested in the effect of that shock. For example, Card et al studied the effect of minimum wage increase for the state of New Jersey. They used diff-in-diff, with Pennsylvania as the control state. The point of synthetic control is that maybe parallel trend assumption does not hold for the control unit. Is Pennsylvania a good control for New Jersey? Does it follow the same trend as New Jersey? Maybe not. Synthetic control is to generate a more comparable control with some combination of multiple control units, which can have a much closer parallel trend as the treated unit, than using any of the control units.

Now, how to construct this synthetic control unit? Basically it is a weighted average of multiple control units. There are two sets of weights to be determined. One set for the control units, one set to determine the importance of predictors. Abadie et al (2010) propose to choose a set of \(w_i\)’s so that the resulting synthetic control best resembles the pre-intervention values for the treated unit of predictors of the outcome variable, subject to the constraints that the weights are non-negative and sum to 1. Another set of weights \(v_h\) for predictor importance is determined such that it minimizes the mean squared prediction error (MSPE) of this synthetic control with respect to the outcome in the pre-treatment period. The intuition of these weights are to pick \(w_i\)’s to make the weighted sum of each predictor as close to the treated unit’s value of predictor as close as possible, given a set of \(v_h\)’s. Then \(v_h\)’s are determined by how important each predictor is to predict the outcome. We don’t use any information post-treatment to determine the weights. But we can use pre-treatment outcome to see the relative importance of each predictor in terms of predictor outcome. We can do this by out-of-sample validation. Basically divide the pre-treatment period into training and validation periods (that’s one reason that the pre-treatment period cannot be too short). Iterate the process of choosing \(w_i\) and \(v_h\) to achieve a minimization of MSPE in the validation period.

18.2 inference

Since synthetic control is for case study situation, we have essentially one treated unit and one synthetic control. The usual inference method would not work. Abadie et al suggested a permutation based approach. The basic idea is to permute the label of treatment; in other words, suppose one control unit is now a treated unit. Then follow the same procedure to get the synthetic control for that “treated unit”; look at the effect. Now if the treatment is truly for the treated unit only, then there would not be any effect for all the control unit. Therefore we can have a distribution of treatment effects after permutation of treatment label.

18.3 an example

The canonical synthetic control is implemented in both R and Stata (https://web.stanford.edu/~jhain/synthpage.html). We use a package called “tidysynth” here to study one of the original examples, which evaluates the impact of Proposition 99 on cigarette consumption in California.

library(tidysynth)
library(dplyr)
library(tidyverse)
library(ggplot2)
library(tidyr)
data("smoking")
smoking %>% dplyr::glimpse()
Rows: 1,209
Columns: 7
$ state     <chr> "Rhode Island", "Tennessee", "Indiana", "Nevada", "Louisiana…
$ year      <dbl> 1970, 1970, 1970, 1970, 1970, 1970, 1970, 1970, 1970, 1970, …
$ cigsale   <dbl> 123.9, 99.8, 134.6, 189.5, 115.9, 108.4, 265.7, 93.8, 100.3,…
$ lnincome  <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …
$ beer      <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …
$ age15to24 <dbl> 0.1831579, 0.1780438, 0.1765159, 0.1615542, 0.1851852, 0.175…
$ retprice  <dbl> 39.3, 39.9, 30.6, 38.9, 34.3, 38.4, 31.4, 37.3, 36.7, 28.8, …
smoking_out <-
    smoking %>%
    # initial the synthetic control object
    synthetic_control(outcome = cigsale, # outcome
                    unit = state, # unit index in the panel data
                    time = year, # time index in the panel data
                    i_unit = "California", # unit where the intervention occurred
                    i_time = 1988, # time period when the intervention occurred
                    generate_placebos=T # generate placebo synthetic controls (for inference)
                    ) %>%
    # Generate the aggregate predictors used to fit the weights
    # average log income, retail price of cigarettes, and proportion of the
    # population between 15 and 24 years of age from 1980 - 1988
  generate_predictor(time_window = 1980:1988,
                     ln_income = mean(lnincome, na.rm = T),
                     ret_price = mean(retprice, na.rm = T),
                     youth = mean(age15to24, na.rm = T)) %>%
    # average beer consumption in the donor pool from 1984 - 1988
  generate_predictor(time_window = 1984:1988,
                     beer_sales = mean(beer, na.rm = T)) %>%
    # Lagged cigarette sales
  generate_predictor(time_window = 1975,
                     cigsale_1975 = cigsale) %>%
  generate_predictor(time_window = 1980,
                     cigsale_1980 = cigsale) %>%
  generate_predictor(time_window = 1988,
                     cigsale_1988 = cigsale) %>%
    # Generate the fitted weights for the synthetic control
  generate_weights(optimization_window = 1970:1988, # time to use in the optimization task
                   margin_ipop = .02,sigf_ipop = 7,bound_ipop = 6 # optimizer options
  ) %>%
  # Generate the synthetic control
  generate_control()

smoking_out %>% plot_trends()

smoking_out %>% plot_differences()

smoking_out %>% plot_weights()

smoking_out %>% grab_balance_table()
# A tibble: 7 × 4
  variable     California synthetic_California donor_sample
  <chr>             <dbl>                <dbl>        <dbl>
1 ln_income        10.1                  9.85         9.83 
2 ret_price        89.4                 89.4         87.3  
3 youth             0.174                0.174        0.173
4 beer_sales       24.3                 24.2         23.7  
5 cigsale_1975    127.                 127.         137.   
6 cigsale_1980    120.                 120.         138.   
7 cigsale_1988     90.1                 91.4        114.   
smoking_out %>% plot_placebos()

smoking_out %>% plot_placebos(prune = FALSE)

smoking_out %>% plot_mspe_ratio()

smoking_out %>% grab_significance()
# A tibble: 39 × 8
   unit_name      type  pre_mspe post_mspe mspe_ratio  rank fishers_exact_pvalue
   <chr>          <chr>    <dbl>     <dbl>      <dbl> <int>                <dbl>
 1 California     Trea…     3.17     392.      124.       1               0.0256
 2 Georgia        Donor     3.79     179.       47.2      2               0.0513
 3 Indiana        Donor    25.2      770.       30.6      3               0.0769
 4 West Virginia  Donor     9.52     284.       29.8      4               0.103 
 5 Wisconsin      Donor    11.1      268.       24.1      5               0.128 
 6 Missouri       Donor     3.03      67.8      22.4      6               0.154 
 7 Texas          Donor    14.4      277.       19.3      7               0.179 
 8 South Carolina Donor    12.6      234.       18.6      8               0.205 
 9 Virginia       Donor     9.81      96.4       9.83     9               0.231 
10 Nebraska       Donor     6.30      52.9       8.40    10               0.256 
# ℹ 29 more rows
# ℹ 1 more variable: z_score <dbl>
# A tibble: 31 × 3
   time_unit real_y synth_y
       <dbl>  <dbl>   <dbl>
 1      1970   123     117.
 2      1971   121     119.
 3      1972   124.    124.
 4      1973   124.    125.
 5      1974   127.    127.
 6      1975   127.    127.
 7      1976   128     128.
 8      1977   126.    126.
 9      1978   126.    125.
10      1979   122.    123.
# ℹ 21 more rows
smoking_out %>% grab_synthetic_control(placebo = T)
# A tibble: 1,209 × 5
   .id        .placebo time_unit real_y synth_y
   <chr>         <dbl>     <dbl>  <dbl>   <dbl>
 1 California        0      1970   123     117.
 2 California        0      1971   121     119.
 3 California        0      1972   124.    124.
 4 California        0      1973   124.    125.
 5 California        0      1974   127.    127.
 6 California        0      1975   127.    127.
 7 California        0      1976   128     128.
 8 California        0      1977   126.    126.
 9 California        0      1978   126.    125.
10 California        0      1979   122.    123.
# ℹ 1,199 more rows
smoking_out %>%
  tidyr::unnest(cols = c(.outcome))
# A tibble: 1,482 × 50
   .id        .placebo .type   time_unit California Alabama Arkansas Colorado
   <chr>         <dbl> <chr>       <dbl>      <dbl>   <dbl>    <dbl>    <dbl>
 1 California        0 treated      1970       123       NA       NA       NA
 2 California        0 treated      1971       121       NA       NA       NA
 3 California        0 treated      1972       124.      NA       NA       NA
 4 California        0 treated      1973       124.      NA       NA       NA
 5 California        0 treated      1974       127.      NA       NA       NA
 6 California        0 treated      1975       127.      NA       NA       NA
 7 California        0 treated      1976       128       NA       NA       NA
 8 California        0 treated      1977       126.      NA       NA       NA
 9 California        0 treated      1978       126.      NA       NA       NA
10 California        0 treated      1979       122.      NA       NA       NA
# ℹ 1,472 more rows
# ℹ 42 more variables: Connecticut <dbl>, Delaware <dbl>, Georgia <dbl>,
#   Idaho <dbl>, Illinois <dbl>, Indiana <dbl>, Iowa <dbl>, Kansas <dbl>,
#   Kentucky <dbl>, Louisiana <dbl>, Maine <dbl>, Minnesota <dbl>,
#   Mississippi <dbl>, Missouri <dbl>, Montana <dbl>, Nebraska <dbl>,
#   Nevada <dbl>, `New Hampshire` <dbl>, `New Mexico` <dbl>,
#   `North Carolina` <dbl>, `North Dakota` <dbl>, Ohio <dbl>, Oklahoma <dbl>, …

18.4 Generalized Synthetic Control

The idea of generalized SC is to combine both an interactive fixed effect model and synthetic control. It is basically a panel data synthetic control. It naturally takes multiple treated units and uses bootstrap to get standard errors. It generates ATT with standard errors.

Examples are here: https://yiqingxu.org/packages/gsynth/gsynth_examples.html

## for processing C++ code
require(Rcpp)
## for plotting
require(ggplot2)
require(GGally)
## for parallel computing
require(foreach)
require(future)
require(doParallel)
require(abind)
require(lfe)
library(gsynth)
## ## Syntax has been updated since v.1.2.0.
## ## Comments and suggestions -> yiqingxu@stanford.edu.
data(gsynth)
library(panelView)
## ## See bit.ly/panelview4r for more info.
## ## Report bugs -> yiqingxu@stanford.edu.
panelview(Y ~ D, data = simdata,  index = c("id","time"), pre.post = TRUE)

panelview(Y ~ D, data = simdata,  index = c("id","time"), type = "outcome")

system.time(
    out <- gsynth(Y ~ D + X1 + X2, data = simdata, index = c("id","time"), force = "two-way", CV = TRUE, r = c(0, 5), se = TRUE, inference = "parametric", nboots = 100, parallel = FALSE)
)
   user  system elapsed 
  1.375   0.019   1.402 
print(out)
Call:
gsynth(formula = Y ~ D + X1 + X2, data = simdata, index = c("id", 
    "time"), force = "two-way", r = c(0, 5), CV = TRUE, se = TRUE, 
    nboots = 100, inference = "parametric", parallel = FALSE, 
    vartype = "parametric")

Average Treatment Effect on the Treated:
     ATT.avg   S.E. CI.lower CI.upper p.value
[1,]   5.084 0.2918    4.512    5.656       0

   ~ by Period (including Pre-treatment Periods):
         ATT   S.E. CI.lower CI.upper   p.value count
-19  0.05868 0.6322  -1.1804  1.29775 9.260e-01     5
-18 -0.11741 0.5355  -1.1670  0.93213 8.264e-01     5
-17 -0.89029 0.5042  -1.8784  0.09784 7.741e-02     5
-16  1.29462 0.4938   0.3267  2.26252 8.753e-03     5
-15 -1.20350 0.6416  -2.4610  0.05400 6.068e-02     5
-14  2.21992 0.6566   0.9330  3.50684 7.225e-04     5
-13 -0.18469 0.7023  -1.5612  1.19178 7.926e-01     5
-12  0.87413 0.5328  -0.1701  1.91835 1.009e-01     5
-11 -0.04824 0.5870  -1.1987  1.10220 9.345e-01     5
-10  0.84503 0.5443  -0.2218  1.91188 1.206e-01     5
-9   0.49754 0.5817  -0.6425  1.63762 3.924e-01     5
-8  -0.38001 0.6231  -1.6013  0.84130 5.420e-01     5
-7  -1.03493 0.5347  -2.0828  0.01297 5.290e-02     5
-6  -0.81532 0.6800  -2.1481  0.51743 2.305e-01     5
-5  -1.13329 0.5284  -2.1689 -0.09772 3.196e-02     5
-4  -0.17733 0.6114  -1.3757  1.02107 7.718e-01     5
-3   0.17671 0.8013  -1.3937  1.74714 8.255e-01     5
-2  -0.14376 0.5996  -1.3189  1.03139 8.105e-01     5
-1  -1.19697 0.4487  -2.0763 -0.31761 7.633e-03     5
0    1.35910 0.9498  -0.5024  3.22060 1.524e-01     5
1    0.34558 0.5937  -0.8180  1.50917 5.605e-01     5
2    0.15709 0.7348  -1.2831  1.59730 8.307e-01     5
3    3.64127 0.4977   2.6658  4.61673 2.549e-13     5
4    2.74975 0.6414   1.4925  4.00696 1.813e-05     5
5    4.45569 0.7330   3.0191  5.89233 1.211e-09     5
6    5.67231 0.8218   4.0616  7.28304 5.121e-12     5
7    8.17039 0.5637   7.0656  9.27519 0.000e+00     5
8    6.57085 0.7545   5.0921  8.04959 0.000e+00     5
9    9.11624 0.5965   7.9472 10.28534 0.000e+00     5
10   9.96205 0.4849   9.0117 10.91240 0.000e+00     5

Coefficients for the Covariates:
    Coef    S.E. CI.lower CI.upper p.value
X1 1.454 0.04187    1.372    1.537       0
X2 3.507 0.04288    3.423    3.591       0
out$est.att
            ATT      S.E.   CI.lower    CI.upper      p.value count
-19  0.05868370 0.6321897 -1.1803854  1.29775282 9.260417e-01     5
-18 -0.11741144 0.5354905 -1.1669535  0.93213064 8.264478e-01     5
-17 -0.89029173 0.5041592 -1.8784256  0.09784213 7.741362e-02     5
-16  1.29461792 0.4938341  0.3267208  2.26251505 8.752727e-03     5
-15 -1.20349647 0.6415914 -2.4609926  0.05399966 6.068291e-02     5
-14  2.21991721 0.6566067  0.9329918  3.50684266 7.225056e-04     5
-13 -0.18468804 0.7022923 -1.5611556  1.19177952 7.925668e-01     5
-12  0.87413414 0.5327753 -0.1700862  1.91835448 1.008559e-01     5
-11 -0.04824208 0.5869693 -1.1986808  1.10219660 9.344969e-01     5
-10  0.84503247 0.5443195 -0.2218141  1.91187905 1.205530e-01     5
-9   0.49754000 0.5816863 -0.6425442  1.63762421 3.923625e-01     5
-8  -0.38001372 0.6231327 -1.6013314  0.84130399 5.419652e-01     5
-7  -1.03492639 0.5346505 -2.0828221  0.01296930 5.290366e-02     5
-6  -0.81531903 0.6799885 -2.1480720  0.51743399 2.305206e-01     5
-5  -1.13328625 0.5283589 -2.1688507 -0.09772183 3.195944e-02     5
-4  -0.17733004 0.6114381 -1.3757266  1.02106653 7.718000e-01     5
-3   0.17670638 0.8012576 -1.3937296  1.74714238 8.254535e-01     5
-2  -0.14376360 0.5995804 -1.3189195  1.03139231 8.105057e-01     5
-1  -1.19696676 0.4486581 -2.0763204 -0.31761309 7.633108e-03     5
0    1.35910371 0.9497582 -0.5023882  3.22059567 1.524303e-01     5
1    0.34558474 0.5936783 -0.8180034  1.50917289 5.604941e-01     5
2    0.15709192 0.7348156 -1.2831202  1.59730402 8.307154e-01     5
3    3.64127124 0.4976900  2.6658167  4.61672574 2.549072e-13     5
4    2.74975180 0.6414467  1.4925393  4.00696431 1.812680e-05     5
5    4.45569195 0.7329921  3.0190538  5.89233006 1.211063e-09     5
6    5.67231032 0.8218140  4.0615845  7.28303612 5.121015e-12     5
7    8.17038502 0.5636868  7.0655792  9.27519082 0.000000e+00     5
8    6.57084917 0.7544717  5.0921118  8.04958648 0.000000e+00     5
9    9.11624492 0.5964862  7.9471534 10.28533645 0.000000e+00     5
10   9.96204576 0.4848859  9.0116869 10.91240458 0.000000e+00     5
out$est.avg
      ATT.avg      S.E. CI.lower CI.upper p.value
[1,] 5.084123 0.2918341 4.512138 5.656107       0
out$est.beta
       Coef       S.E. CI.lower CI.upper p.value
X1 1.454447 0.04186583 1.372392 1.536503       0
X2 3.507197 0.04287653 3.423161 3.591234       0
system.time(
out <- gsynth(Y ~ D + X1 + X2, data = simdata, index = c("id","time"),
    force = "two-way", CV = TRUE, r = c(0, 5), se = TRUE,
    inference = "parametric", nboots = 100, parallel = TRUE, cores = 4)
)
   user  system elapsed 
  7.173   0.340  22.550 
# cumuEff was removed in newer gsynth versions (now a wrapper for fect)
# cumu1 <- cumuEff(out, cumu = TRUE, id = NULL, period = c(0,5))
# cumu1$est.catt

plot(out)


Systematic treatment: Julia.