Penalized synthetic control estimation

The goal of pensynth is to make it easier to perform penalized synthetic control in the spirit of Abadie & L'Hour (2021).

Features

[!NOTE] pensynth estimates unit weights, but variable weights have to be pre-specified. This is by design.

Installation

I recommend installing the latest version of pensynth from r-universe like so:

install.packages("pensynth", repos = c("https://vankesteren.r-universe.dev", "https://cloud.r-project.org"))

The package is also on CRAN, so it can be installed directly from there too.

install.packages("pensynth")

The latest development version can also be installed from this repository:

remotes::install_github("vankesteren/pensynth")

Why penalization?

Penalized synthetic control yields a smooth transition between the synthetic control method (when $\lambda = 0$) and nearest neighbour matching (when $\lambda \to \infty$).

When the treated unit is in the convex hull of the donor units (which is more likely when there are many donors) there is no unique solution for the unit weights of synthetic control. In these cases especially, the penalty can help because it prefers solutions with donors closer in covariate space.

The pensynth implementation achieves this through optimizing the following objective:

\min_{\boldsymbol{W}} \left[ \| \boldsymbol{X}_1 - \boldsymbol{X}_0 \boldsymbol{W} \|^2 + \lambda \sum_{t\in T} \sum_{d\in D} \boldsymbol{W}_{dt} \cdot \|\boldsymbol{X}_1^t - \boldsymbol{X}_0^d\|^2 \right]
\text{s.t.}
\quad \boldsymbol{W}_{\{d,t\}} \geq 0 \,\,\forall \,\,\{d,t\} \in \{D, T\},
\sum_{d\in D} \boldsymbol{W}_{\{d,t\}} = 1 \,\,\forall \,\,t \in T,

Where

The first term in the objective is the same (up to variable weights) as the original synthetic control, and the second term is the nearest neighbour matching penalty.

Example

Here, we perform pensynth with a simple simulated dataset.

library(pensynth)
set.seed(45)

# Generate some data with a 0.8SD effect
dat <- simulate_data_synth(treatment_effect = 0.8)

dataplot

# Run penalized synthetic control
# estimate lambda using pre-intervention timeseries MSE
fit <- cv_pensynth(
  X1 = dat$X1, # Treated unit covariates
  X0 = dat$X0, # Donor unit covariates
  Z1 = dat$Z1, # Treated unit hold-out values (pre-intervention outcome)
  Z0 = dat$Z0  # Donor unit hold-out values
)
plot(fit)

cvplot

We can then use the predict() method to compute the synthetic control

# Compute average treatment effect post-intervention
Y1_synth <- predict(
  object = fit, 
  newdata = dat$Y0 # Donor units post-intervention outcome
)
mean(dat$Y1 - Y1_synth)
#> [1] 0.8780154

References

Abadie, A., & L’Hour, J. (2021). A penalized synthetic control estimator for disaggregated data. Journal of the American Statistical Association, 116(536), 1817-1834.

Some of the code was inspired by (but heavily adapted from) jeremylhour/pensynth. Where this was the case, the code is commented.