MethodAtlas
tutorial2 hours

Lab: Event Studies from Scratch

Implement an event study design step by step. Create event-time indicators, estimate dynamic treatment effects, visualize pre-trends and post-treatment dynamics, and learn to interpret the event study plot correctly.

Overview

Event studies extend the DiD framework by estimating separate treatment effects for each time period relative to treatment. Instead of a single "before vs. after" estimate, you trace out how the treatment effect evolves over time. The pre-treatment coefficients serve as a diagnostic for the parallel trends assumption.

What you will learn:

  • How to construct event-time (relative time) indicators
  • How to estimate a dynamic treatment effects regression
  • How to create and interpret the classic event study plot
  • How to test for pre-trends
  • Why you must omit one reference period (and which one to choose)
  • What can go wrong with event study specifications

Prerequisites: DiD (see the DiD lab), Fixed Effects.


Step 1: The Setting

Imagine a corporate governance reform that is adopted by different firms at different times. We want to estimate (a) the dynamic path of the treatment effect and (b) whether there were pre-existing trends that might invalidate the design.


Step 2: Simulate Panel Data with Staggered Treatment

library(fixest)
library(ggplot2)

set.seed(2024)
n_firms <- 200
n_periods <- 20

# Treatment timing
treatment_year <- rep(Inf, n_firms)
treated_firms <- sample(n_firms, 100)
for (i in treated_firms) {
treatment_year[i] <- sample(2005:2014, 1)
}

# Build panel
df <- data.frame()
for (i in 1:n_firms) {
firm_fe <- rnorm(1, 0, 2)
for (t in 0:(n_periods - 1)) {
  year <- 2000 + t
  year_fe <- 0.3 * t

  if (treatment_year[i] < Inf) {
    event_time <- year - treatment_year[i]
    treated <- 1
    post <- as.integer(year >= treatment_year[i])
    te <- ifelse(post, 1.0 + 0.5 * pmin(event_time, 5), 0)
  } else {
    event_time <- NA
    treated <- 0
    post <- 0
    te <- 0
  }

  y <- 10 + firm_fe + year_fe + te + rnorm(1, 0, 1.5)
  df <- rbind(df, data.frame(
    firm_id = i, year = year, treated = treated, post = post,
    event_time = event_time, performance = y,
    treatment_year = ifelse(treatment_year[i] < Inf,
                             treatment_year[i], NA)
  ))
}
}

cat("Panel:", nrow(df), "observations\n")
cat("Treated firms:", sum(treatment_year < Inf), "\n")

Expected output:

firm_idyeartreatedpostevent_timeperformance
0200010-9.08.12
0200110-8.09.45
02009110.012.87
02010111.013.54
15020050011.03

Summary statistics:

StatisticValue
Panel dimensions200 firms x 20 years = 4,000 obs
Treated firms100
Never-treated firms100
Treatment years2005–2014 (staggered)
Mean performance (pre-treatment)~10 + firm FE + time trend

Step 3: Create Event-Time Indicators

The event study regression requires dummy variables for each period relative to treatment. We normalize to one period before treatment (k=1k = -1) as the reference category.

# Create event-time indicators
# fixest makes this easy with the i() function
# Clip event time to [-5, 5]
df$event_time_clipped <- pmin(pmax(df$event_time, -5), 5)

# For the manual approach, create dummies
event_times <- c(-5, -4, -3, -2, 0, 1, 2, 3, 4, 5)

for (k in event_times) {
col_name <- paste0("et_", ifelse(k < 0, "m", ""), abs(k))
df[[col_name]] <- as.integer(df$event_time_clipped == k & df$treated == 1)
}

cat("Event-time dummies created. Reference period: k = -1\n")
Requiresfixest

Expected output:

Event-time indicatorObservations with indicator = 1
et_-5 (binned, k <= -5)~500 (treated obs 5+ years pre)
et_-4~100
et_-3~100
et_-2~100
et_-1 (reference)omitted
et_0~100
et_1~100
et_2~100
et_3~100
et_4~100
et_5 (binned, k >= 5)~500 (treated obs 5+ years post)

Step 4: Estimate the Event Study Regression

# Event study using fixest (the easiest approach)
# i(event_time_clipped, ref = -1) creates all dummies with -1 as reference
m_es <- feols(performance ~ i(event_time_clipped, treated, ref = -1) |
            firm_id + year,
            data = df[!is.na(df$event_time) | df$treated == 0, ],
            vcov = ~firm_id)

summary(m_es)

# The coefficients are the event study estimates
Requiresfixest

Expected output:

Event Time (k)CoefficientSE95% CI Lower95% CI Upper
-5-0.020.18-0.370.33
-40.050.20-0.340.44
-3-0.080.19-0.450.29
-20.030.18-0.320.38
-10.000------
01.020.190.651.39
11.480.201.091.87
22.050.211.642.46
32.520.222.092.95
43.010.232.563.46
53.480.193.113.85

The pre-treatment coefficients (k = -5 to -2) are all close to zero and statistically insignificant, consistent with parallel trends. The post-treatment coefficients grow over time, matching the true DGP of 1.0 + 0.5 * min(k, 5).


Step 5: Create the Event Study Plot

The coefficient plot is the signature visualization of the event study design.

# Event study plot using fixest
iplot(m_es,
    main = "Event Study: Governance Reform and Firm Performance",
    xlab = "Event Time (Years Relative to Treatment)",
    ylab = "Coefficient (Relative to k = -1)")

# Add reference line at treatment onset
abline(v = -0.5, lty = 2, col = "red")
Requiresfixest
Concept Check

In your event study plot, the pre-treatment coefficients (k = -5 to k = -2) are all close to zero and statistically insignificant. What can you conclude?


Step 6: Interpreting the Post-Treatment Dynamics

The post-treatment coefficients tell a story about how the treatment effect evolves over time.

# Extract and interpret post-treatment coefficients
coefs <- coeftable(m_es)
cat("Post-Treatment Dynamics:\n")
cat("The effect grows over time as the governance reform takes hold.\n\n")

# True effects for comparison
for (k in 0:5) {
true_te <- 1.0 + 0.5 * min(k, 5)
cat(sprintf("  k = %+d: True = %+.3f\n", k, true_te))
}

Expected output:

Event Time (k)Estimated EffectTrue Effect (DGP)Interpretation
k = +0~1.01.0Immediate effect at adoption
k = +1~1.51.5Effect after 1 year
k = +2~2.02.0Effect after 2 years
k = +3~2.52.5Effect after 3 years
k = +4~3.03.0Effect after 4 years
k = +5~3.53.5Effect after 5 years (plateau)

The gradually building pattern reflects a reform that takes time to produce its full impact. The DGP generates this with the formula: treatment effect = 1.0 + 0.5 * min(k, 5).


Step 7: What Can Go Wrong

Pre-trend Violation

# Simulate pre-trend violation (similar to Python code)
# When you see an upward slope in pre-treatment coefficients,
# parallel trends is violated.
# The post-treatment coefficients mix the true effect
# with the pre-existing differential trend.

cat("If pre-treatment coefficients slope upward, this indicates\n")
cat("a violation of parallel trends. The post-treatment estimates\n")
cat("are contaminated by the pre-existing trend.\n")
Concept Check

Your event study shows a clear upward slope in pre-treatment coefficients (k = -5 to k = -2). Your advisor suggests 'just detrending' by subtracting the pre-treatment trend from the post-treatment coefficients. Is this a good solution?


Step 8: Advanced Considerations

Binning Endpoint Periods

In most settings, bin distant event times to avoid sparse cells. For example, group all k5k \leq -5 into a single "k5k \leq -5" dummy and all k5k \geq 5 into "k5k \geq 5".

Joint Test for Pre-Trends

Instead of eyeballing individual pre-trend coefficients, formally test whether they are jointly zero.

# Joint test for pre-trends using fixest
# wald() function tests if specified coefficients are jointly zero
wald(m_es, "event_time_clipped::-[2-5]")

cat("p > 0.05: Cannot reject that pre-trends are jointly zero\n")
Requiresfixest

Expected output:

StatisticValue
Joint F-statistic~0.5–2.0
Degrees of freedom4 (testing et_-5, et_-4, et_-3, et_-2)
p-value> 0.05 (typically 0.3–0.8)
ConclusionCannot reject that pre-trends are jointly zero

The joint F-test confirms what the event study plot shows visually: the pre-treatment coefficients are not distinguishable from zero, consistent with the parallel trends assumption.

Aggregating Post-Treatment Effects

Sometimes you want a single summary treatment effect rather than period-by-period estimates.

# Simple summary DiD estimate
df$treat_post <- df$treated * df$post
m_did <- feols(performance ~ treat_post | firm_id + year,
             data = df, vcov = ~firm_id)
cat("Summary DiD estimate:", coef(m_did)["treat_post"], "\n")
Requiresdid

Expected output:

EstimatorEstimateInterpretation
Average post-treatment coefficient~2.25Mean of event-time coefficients at k = 0 through k = 5
Static DiD (TWFE)~2.0–2.5Single summary treatment effect averaging across all post-treatment periods

The average of the true DGP treatment effects across k = 0 to 5 is (1.0 + 1.5 + 2.0 + 2.5 + 3.0 + 3.5) / 6 = 2.25.


Step 9: Exercises

  1. Change the reference period. Re-estimate with k=2k = -2 as the reference period. How do the coefficients and plot change? Which reference period makes more sense for your setting?

  2. Test for anticipation effects. What if firms change behavior before the formal treatment date? Allow the treatment effect to begin one period before the official date and re-estimate.

  3. Staggered DiD concerns. With staggered treatment timing and heterogeneous treatment effects, the standard TWFE event study can be biased ((Sun & Abraham, 2021)). Read about this issue and think about when it matters.

  4. Placebo test. For the never-treated firms, assign a random "fake" treatment year and estimate the event study. All coefficients should be near zero.


Summary

In this lab you learned:

  • Event studies estimate separate treatment effects for each period relative to treatment
  • You must omit one reference period (typically k=1k = -1) to avoid multicollinearity
  • Flat pre-treatment coefficients are consistent with parallel trends but do not prove it
  • The post-treatment pattern reveals dynamic treatment effects (growing, stable, or fading)
  • In most settings, include firm and year fixed effects and cluster standard errors appropriately
  • Bin endpoint event times to avoid sparse cells
  • Use a joint F-test for pre-trends rather than eyeballing individual coefficients
  • Pre-trend violations are a fundamental threat — "detrending" is not a reliable fix
  • With staggered treatment, be aware of potential biases in standard TWFE event studies