stacked

Stacked difference-in-differences with corrective weights: for R and Stata

stacked implements the stacked difference-in-differences estimator of Wing, Hollingsworth & Freedman (2024).

Stacked DiD is designed for settings with staggered treatment adoption. It:

This ensures that a simple weighted regression can recover the treatment-effect parameter you actually set out to estimate, the average treatment effect (ATE) for the average treated unit.

Both R and Stata packages are available.

Install

# From CRAN (coming soon)
# install.packages("stacked")

# Development version from GitHub
# install.packages("remotes")
remotes::install_github("hollina/stacked", subdir = "R/stacked")
* From SSC (coming soon)
* ssc install stacked

* Development version from GitHub
net install stacked, ///
    from("https://raw.githubusercontent.com/hollina/stacked/main/Stata") replace

Why do we need corrective weights?

The problem in one example

Consider two cohorts that have different treatment effects and are wildly unequal in size.

  • 10 groups adopt in year 11 with a large positive effect (300)
  • 1,000 groups adopt in year 60 with a negative effect (−100)

The simple unit-weighted average should clearly be negative (-96 \(~ = \frac{300\times 10 -100\times 1000}{10 + 1000}\)).

But a plain stacked TWFE regression without-corrective weights returns 93. Not only is this the wrong answer it is the wrong sign.

Why does this happen?

stacked TWFE overweights the tiny early cohort, giving it a disproportionate share of the treatment variance. Our method makes simple corrective weights (Q-weights for short) that fix this problem. The corrective weights do this by load the mass in proportion to each cohort’s treated units, ensuring the correct answer is recovered.

The three panels below show the raw group means, the weights that decide the sign and the two event studies. These panels tell the whole story using simulated data.

Raw group means (presented without error simply for a legible picture):

Group-mean outcome by year: the small year-11 cohort jumps up by 300, the large year-60 cohort drops by 100, the never-treated group is flat.

Each cohort’s ATT scattered against its sub-experiment weight without and without corrective weighting. We call the stacked TWFE without corrective-weights, precision weighting.

  • Here the precision weights split the mass 50%/50%,
  • while corrective weighting correclty loads 99% on the large negative cohort

Each cohort's ATT against its sub-experiment weight: corrective weighting loads 0.99 on the large negative cohort, deciding the sign.

Event studies for both estimators. Only the event-studies from stacked TWFE using corrective weights track the true ATT.

Event-study comparison: the corrective series is flat pre-adoption and settles near -96; the unweighted series is positive after adoption.

Walk the whole argument step by step in the worked example, or move the pieces yourself in the interactive app.

Quick start

The workflow is four steps:

  1. preview the event-window trade-offs,
  2. build the stacked dataset,
  3. estimate the corrective-weighted event study and/or ATT
  4. plot it

This example uses the bundled medicaid panel (state uninsurance rates and ACA Medicaid-expansion adoption dates).

library(stacked)

data(medicaid)

# 1. Preview how event-window length (kappa) trades off against sample
to <- kappa_trade_offs(
  medicaid, "year", "state", "adopt_year",
  kappa_pre_range = 1:4, kappa_post_range = 1:4
)
to[kappa_pre <= 3 & kappa_post <= 3,
   .(kappa_pre, kappa_post, n_sub_exp, n_obs)]
   kappa_pre kappa_post n_sub_exp n_obs
1:         1          1         5   354
2:         1          2         4   400
3:         1          3         3   425
4:         2          1         5   472
5:         2          2         4   500
6:         2          3         3   510
7:         3          1         5   590
8:         3          2         4   600
9:         3          3         3   595
# 2. Build the stacked dataset (3 pre periods, 2 post periods)
stack <- build_stack(
  medicaid, "year", "state", "adopt_year",
  kappa_pre = 3, kappa_post = 2
)

# 3. Fit the Q-weighted event-study regression, clustering on state
model <- stackreg(stack, "uninsured", cluster_var = "state")

# Average post-period ATT
attr(model, "avg_post_att")$estimate
[1] -0.02187775
# 4. Plot the event study
p_med <- stack_plot(model, title = "Medicaid expansion and the uninsured rate")

Event-study plot of the effect of Medicaid expansion on the uninsured rate, showing flat pre-trends and a decline after adoption.

Event-study plot of the effect of Medicaid expansion on the uninsured rate, dark theme.

stacked use medicaid, clear   // load the bundled example panel

* 1. Preview how event-window length (kappa) trades off against sample
stacked kappa, time(year) unit(state) adopt(adopt_year) kpre(1/4) kpost(1/4)

* 2. Build the stacked dataset (3 pre periods, 2 post periods)
stacked build, time(year) unit(state) adopt(adopt_year) kpre(3) kpost(2)

* 3. Fit the Q-weighted event-study regression, clustering on state
stacked reg uninsured, cluster(state)

* 4. Plot the event study
stacked plot

Key functions

R Stata Description
kappa_trade_offs() stacked kappa Preview stack composition (and design divergence D) across event windows
build_stack() stacked build Create the stacked dataset with Q-weights
stackreg() stacked reg Fit the Q-weighted event-study (or static ATT) regression
stack_plot() stacked plot Visualize the event study
stack_levels() stacked levels Q-weighted mean outcome levels for treated and control groups
add_pscore_weights() stacked pscore Add propensity-score-adjusted weights

See the full function reference for arguments and details.

Where to next