Difference-in-Differences Estimation
Statistics
Comparing Changes Over Time Between Treatment and Control
Difference-in-differences estimates causal effects by comparing outcome changes over time between treated and control groups. The parallel trends assumption ensures that without treatment, both groups would have followed the same trajectory.
-
Policy Evaluation β Assess minimum wage effects on employment across states
-
Healthcare β Evaluate insurance expansions using state-by-state rollout timing
-
Business β Measure marketing campaign impact using geographic staggered launches
The double difference β in changes, not levels β eliminates time-invariant confounders.
Difference-in-Differences (DiD) estimates causal effects by comparing changes in outcomes over time between a treatment group and a control group.
Basic DiD Formula
Parallel Trends Assumption
Testing Parallel Trends
Pre-treatment periods can provide indirect evidence:
-
Plot pre-treatment trends for both groups
-
Test whether pre-treatment differences are stable
-
If pre-treatment trends diverge, the assumption is questionable
Regression Specification
Event Study Design
Extends DiD by estimating dynamic effects β how the treatment effect evolves over time.
Staggered Adoption
Many units adopt treatment at different times. Recent research shows that two-way fixed effects (TWFE) can be biased in staggered DiD.
| Method | Problem |
|--------|---------|
| TWFE | Negative weights; can give wrong sign |
| Callaway-Sant'Anna | Robust estimator for staggered DiD |
| Sun-Abraham | Interaction-weighted estimator |
| de Chaisemartin-D'HaultfΕuille | robust DiD with heterogeneous effects |
Difference-in-Differences with Covariates
Including covariates can improve precision but is not required for identification.
Python Implementation
import numpy as np
import pandas as pd
import statsmodels.api as sm
import matplotlib.pyplot as plt
np.random.seed(42)
# Simulate DiD data
n_units = 200
n_periods = 10
treated = np.repeat(np.random.binomial(1, 0.5, n_units), n_periods)
time = np.tile(np.arange(n_periods), n_units)
post = (time >= 5).astype(int)
treatment = treated * post
# True effect = 2.0
Y = 5 + 1.5*treated + 0.3*time + 2.0*treatment + np.random.randn(n_units*n_periods)*2
df = pd.DataFrame({'Y': Y, 'treated': treated, 'time': time, 'post': post, 'treatment': treatment})
# Basic DiD
model = sm.OLS.from_formula('Y ~ treated + post + treatment', data=df).fit()
print("Basic DiD:")
print(model.summary().tables[1])
# Event study
event_coefs = []
for k in range(-4, 6):
if k == -1:
continue
df['event_k'] = ((df['time'] - 5) == k) * df['treated']
model_k = sm.OLS.from_formula('Y ~ C(time) + C(treated) + event_k', data=df).fit()
event_coefs.append({'k': k, 'coef': model_k.params['event_k'],
'se': model_k.bse['event_k']})
event_df = pd.DataFrame(event_coefs)
event_df['ci_lower'] = event_df['coef'] - 1.96*event_df['se']
event_df['ci_upper'] = event_df['coef'] + 1.96*event_df['se']
plt.figure(figsize=(8, 5))
plt.errorbar(event_df['k'], event_df['coef'],
yerr=1.96*event_df['se'], fmt='o-', capsize=4)
plt.axhline(y=0, color='gray', linestyle='--', alpha=0.5)
plt.axvline(x=0, color='red', linestyle='--', alpha=0.5)
plt.xlabel('Event Time (relative to treatment)')
plt.ylabel('Treatment Effect')
plt.title('Event Study Plot')
plt.show()
Worked Example
Key Takeaways
Related Topics
-
See Causal Inference for the potential outcomes framework
-
See Regression Discontinuity for another quasi-experimental method
-
See Panel Data Analysis for longitudinal methods