The Difference-in-Differences (DiD) method is a powerful statistical technique for estimating causal effects of interventions, policies, or treatments. By comparing changes over time between a treatment group and a control group, DiD allows you to isolate the true impact of an intervention while controlling for confounding factors.
💡 Key Assumption: The parallel trends assumption is critical for DiD. It requires that in the absence of treatment, the treatment and control groups would have followed the same trend over time. This calculator provides tests and visualizations to help you assess this assumption.
Ready to estimate causal effects? to see how DiD works, or check the required data formats to prepare your own data and discover the true impact of your intervention.
Enter the mean outcomes for each group (treatment/control) in each time period (before/after). Standard deviations are optional but recommended for statistical inference.
Difference-in-Differences (DiD) is a quasi-experimental method used to estimate the causal effect of a treatment or intervention. It compares the change in outcomes over time between a treatment group (exposed to the intervention) and a control group (not exposed), effectively "differencing out" time-invariant confounders and common trends.
Basic DiD Estimate:
Where T = Treatment group, C = Control group
Regression Specification:
is the DiD estimate (the treatment effect)
The critical assumption for DiD is that in the absence of treatment, the treatment and control groups would have followed parallel trends. This means:
This calculator accepts three different data formats. Choose the format that matches your data structure:
Use this when you have already calculated group means. You need 4 rows: control/treatment × before/after.
group,period,outcome,n_observations
control,before,50.2,100
control,after,52.1,100
treatment,before,48.5,120
treatment,after,55.3,120Use this when you have individual observations with binary treatment and time indicators.
id,outcome,treatment,post,age,gender
1,45.2,0,0,35,M
2,48.1,0,1,36,M
3,52.3,1,0,40,F
4,58.7,1,1,41,FUse this for panel data with multiple time periods and potentially staggered treatment timing.
state,year,outcome,treated
CA,2010,100,0
CA,2011,105,0
CA,2012,110,1
TX,2010,95,0
TX,2011,98,0
TX,2012,99,0library(tidyverse)
library(fixest) # For DiD with fixed effects
# Example: Effect of minimum wage increase on employment
# Treatment: New Jersey (raised minimum wage in 1992)
# Control: Pennsylvania (no change)
# Fixed sample data (same as Python for comparison)
employment_values <- c(
# PA before (Control, Period 0) - 20 observations
19.8, 20.2, 19.5, 20.8, 19.9, 20.1, 20.0, 19.7, 20.3, 20.5,
19.6, 20.4, 19.9, 20.0, 20.2, 19.8, 20.1, 20.3, 19.7, 20.0,
# PA after (Control, Period 1) - 20 observations
20.5, 20.8, 20.2, 21.0, 20.3, 20.7, 20.4, 20.1, 20.9, 21.1,
20.0, 20.6, 20.5, 20.3, 20.8, 20.2, 20.7, 20.9, 20.4, 20.6,
# NJ before (Treatment, Period 0) - 20 observations
20.3, 20.7, 20.1, 21.2, 20.5, 20.8, 20.4, 20.2, 21.0, 21.3,
20.0, 21.1, 20.6, 20.4, 20.9, 20.3, 20.7, 21.0, 20.2, 20.5,
# NJ after (Treatment, Period 1) - 20 observations
21.5, 22.1, 21.8, 22.5, 21.9, 22.3, 21.7, 21.4, 22.2, 22.6,
21.3, 22.4, 21.8, 21.6, 22.0, 21.5, 22.1, 22.3, 21.7, 21.9
)
data <- tibble(
state = c(rep("PA", 40), rep("NJ", 40)),
period = rep(c(rep(0, 20), rep(1, 20)), 2),
treated = c(rep(0, 40), rep(1, 40)),
employment = employment_values
)
# Create interaction term
data <- data %>%
mutate(treat_x_post = treated * period)
# Method 1: Basic DiD regression
did_model <- lm(employment ~ treated + period + treat_x_post, data = data)
summary(did_model)
# Method 2: Using fixest (more efficient for large datasets)
did_fe <- feols(employment ~ treat_x_post | state + period, data = data)
summary(did_fe)
# Visualize parallel trends
data %>%
group_by(treated, period) %>%
summarise(mean_employment = mean(employment), .groups = "drop") %>%
ggplot(aes(x = period, y = mean_employment,
color = factor(treated), group = treated)) +
geom_line(size = 1.2) +
geom_point(size = 3) +
geom_vline(xintercept = 0.5, linetype = "dashed", alpha = 0.5) +
scale_color_manual(values = c("blue", "red"),
labels = c("Control (PA)", "Treatment (NJ)")) +
labs(title = "Parallel Trends: Employment Over Time",
x = "Time Period (0 = Before, 1 = After)",
y = "Average Employment",
color = "Group") +
theme_minimal()
# Extract DiD estimate
did_estimate <- coef(did_model)["treat_x_post"]
did_se <- summary(did_model)$coefficients["treat_x_post", "Std. Error"]
did_pvalue <- summary(did_model)$coefficients["treat_x_post", "Pr(>|t|)"]
cat(sprintf("DiD Estimate: %.3f (SE = %.3f, p = %.3f)",
did_estimate, did_se, did_pvalue))import pandas as pd
import numpy as np
import statsmodels.formula.api as smf
import matplotlib.pyplot as plt
import seaborn as sns
# Example: Effect of minimum wage increase on employment
# Fixed sample data (same as R for comparison)
employment_values = [
# PA before (Control, Period 0) - 20 observations
19.8, 20.2, 19.5, 20.8, 19.9, 20.1, 20.0, 19.7, 20.3, 20.5,
19.6, 20.4, 19.9, 20.0, 20.2, 19.8, 20.1, 20.3, 19.7, 20.0,
# PA after (Control, Period 1) - 20 observations
20.5, 20.8, 20.2, 21.0, 20.3, 20.7, 20.4, 20.1, 20.9, 21.1,
20.0, 20.6, 20.5, 20.3, 20.8, 20.2, 20.7, 20.9, 20.4, 20.6,
# NJ before (Treatment, Period 0) - 20 observations
20.3, 20.7, 20.1, 21.2, 20.5, 20.8, 20.4, 20.2, 21.0, 21.3,
20.0, 21.1, 20.6, 20.4, 20.9, 20.3, 20.7, 21.0, 20.2, 20.5,
# NJ after (Treatment, Period 1) - 20 observations
21.5, 22.1, 21.8, 22.5, 21.9, 22.3, 21.7, 21.4, 22.2, 22.6,
21.3, 22.4, 21.8, 21.6, 22.0, 21.5, 22.1, 22.3, 21.7, 21.9
]
data = pd.DataFrame({
'state': ['PA']*40 + ['NJ']*40,
'period': [0]*20 + [1]*20 + [0]*20 + [1]*20,
'treated': [0]*40 + [1]*40,
'employment': employment_values
})
# Create interaction term
data['treat_x_post'] = data['treated'] * data['period']
# Method 1: Basic DiD regression
model = smf.ols('employment ~ treated + period + treat_x_post', data=data)
results = model.fit()
print(results.summary())
# Method 2: With clustered standard errors (by state)
results_robust = model.fit(cov_type='cluster',
cov_kwds={'groups': data['state']})
print("DiD with Clustered SE:")
print(results_robust.summary())
# Calculate group means
group_means = data.groupby(['treated', 'period'])['employment'].mean()
print("Group Means:")
print(group_means)
# Calculate DiD manually
control_change = group_means[0, 1] - group_means[0, 0]
treatment_change = group_means[1, 1] - group_means[1, 0]
did_estimate = treatment_change - control_change
print(f"Manual DiD Estimate: {did_estimate:.3f}")
# Visualize parallel trends
plt.figure(figsize=(10, 6))
for treated in [0, 1]:
subset = data[data['treated'] == treated]
means = subset.groupby('period')['employment'].mean()
label = 'Treatment (NJ)' if treated == 1 else 'Control (PA)'
color = 'red' if treated == 1 else 'blue'
linestyle = '-' if treated == 1 else '--'
plt.plot(means.index, means.values, marker='o',
label=label, color=color, linestyle=linestyle, linewidth=2)
plt.axvline(x=0.5, color='gray', linestyle=':', alpha=0.5, label='Treatment Time')
plt.xlabel('Time Period (0 = Before, 1 = After)')
plt.ylabel('Average Employment')
plt.title('Parallel Trends: Employment Over Time')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Extract DiD results
did_coef = results.params['treat_x_post']
did_se = results.bse['treat_x_post']
did_pvalue = results.pvalues['treat_x_post']
did_ci = results.conf_int().loc['treat_x_post']
print(f"DiD Estimate: {did_coef:.3f}")
print(f"Standard Error: {did_se:.3f}")
print(f"p-value: {did_pvalue:.4f}")
print(f"95% CI: [{did_ci[0]:.3f}, {did_ci[1]:.3f}]")