R Comparison#
This guide compares diff-diff with popular R packages for DiD analysis, helping users familiar with R transition to Python.
Overview#
Feature |
diff-diff (Python) |
did (R) |
Other R |
|---|---|---|---|
Basic DiD |
✅ |
✅ |
✅ |
Staggered DiD |
✅ |
✅ |
|
Covariate adjustment |
✅ DR, IPW, Reg |
✅ DR, IPW, Reg |
✅ Varies |
Honest DiD |
✅ |
|
N/A |
Synthetic DiD |
✅ |
|
N/A |
Wild bootstrap |
✅ |
|
N/A |
Package Correspondence#
R did Package → diff-diff#
The R did package by Callaway and Sant’Anna is the gold standard for
staggered DiD. Here’s how to translate common operations:
Basic estimation:
# R (did package)
library(did)
out <- att_gt(
yname = "Y",
tname = "period",
idname = "id",
gname = "G",
data = data
)
# Python (diff-diff)
from diff_diff import CallawaySantAnna
cs = CallawaySantAnna()
results = cs.fit(
data,
outcome='Y',
time='period',
unit='id',
first_treat='G'
)
With covariates (doubly robust):
# R
out <- att_gt(
yname = "Y", tname = "period",
idname = "id", gname = "G",
xformla = ~ X1 + X2,
est_method = "dr",
data = data
)
# Python
cs = CallawaySantAnna(estimation_method='dr')
results = cs.fit(
data,
outcome='Y',
time='period',
unit='id',
first_treat='G',
covariates=['X1', 'X2']
)
Aggregations:
# R
agg_simple <- aggte(out, type = "simple")
agg_dynamic <- aggte(out, type = "dynamic")
agg_group <- aggte(out, type = "group")
# Python (unlike R's aggte(), aggregation is requested at fit time)
results = cs.fit(data, outcome='Y', time='period', unit='id',
first_treat='G', aggregate='all')
overall_att = results.overall_att # Simple aggregation
event_study = results.event_study_effects # Dynamic
by_group = results.group_effects # By cohort
R HonestDiD Package → diff-diff#
The HonestDiD package implements Rambachan & Roth (2023) sensitivity analysis:
Relative magnitudes (ΔRM):
# R
library(HonestDiD)
delta_rm_results <- createSensitivityResults_relativeMagnitudes(
betahat = beta_hat,
sigma = sigma,
numPrePeriods = 4,
numPostPeriods = 3,
Mbarvec = seq(0, 2, by = 0.5)
)
# Python
from diff_diff import HonestDiD
honest = HonestDiD(method='relative_magnitude', M=1.0)
results = honest.fit(event_study_results)
# Sensitivity analysis over M grid
sensitivity = honest.sensitivity_analysis(
event_study_results,
M_grid=[0, 0.5, 1.0, 1.5, 2.0]
)
Smoothness restrictions (ΔSD):
# R
delta_sd_results <- createSensitivityResults(
betahat = beta_hat,
sigma = sigma,
numPrePeriods = 4,
numPostPeriods = 3,
Mvec = seq(0, 0.1, by = 0.02)
)
# Python
from diff_diff import HonestDiD
honest = HonestDiD(method='smoothness', M=0.05)
results = honest.fit(event_study_results)
R synthdid Package → diff-diff#
The synthdid package implements Arkhangelsky et al. (2021):
# R
library(synthdid)
setup <- panel.matrices(data, unit = "unit", time = "time",
outcome = "Y", treatment = "treatment")
tau.hat <- synthdid_estimate(setup$Y, setup$N0, setup$T0)
# Python
from diff_diff import SyntheticDiD
# SyntheticDiD requires a time-invariant ever-treated indicator
data['ever_treated'] = data.groupby('unit')['treatment'].transform('max')
# Derive post-treatment periods from treatment timing
post_periods = sorted(data.loc[data['treatment'] == 1, 'time'].unique())
sdid = SyntheticDiD()
results = sdid.fit(
data,
outcome='Y',
unit='unit',
time='time',
treatment='ever_treated',
post_periods=post_periods
)
Heterogeneous Adoption (HAD)#
When every unit is treated at the post period (universal-rollout policies,
industry-wide regime changes) but treatment intensity varies across units,
the standard R workhorses (did, fixest, synthdid,
DIDmultiplegtDYN) assume an untreated comparison group exists and do
not apply. The dedicated R package DIDHAD (de Chaisemartin et al.,
August 2025) covers the QUG case (Design 1’, d_lower = 0) from the
same arXiv paper.
diff-diff ships HeterogeneousAdoptionDiD, which
implements de Chaisemartin, Ciccia, D’Haultfoeuille and Knau (2026,
arXiv:2405.04465v6) and adds two surfaces beyond the QUG-focused R
package: Design 1 (no QUG, d_lower > 0, targets WAS_{d_lower} under
Assumption 6 or sign-only under Assumption 5), and survey-design
integration via Binder (1983) Taylor-series linearization (sampling weights
+ optional strata / PSU / FPC). The diagnostic battery
did_had_pretest_workflow() surfaces violations of the HAD
identification assumptions (the design path is auto-detected separately by
HeterogeneousAdoptionDiD.fit() from the dose support).
import numpy as np
import pandas as pd
from diff_diff import HeterogeneousAdoptionDiD
# Build a HAD-shape panel: D=0 in pre-periods (t < F), D > 0 only at F+.
rng = np.random.default_rng(42)
G, F, T = 200, 4, 5
doses = rng.beta(0.5, 1.0, size=G)
rows = []
for g in range(G):
for t in range(1, T + 1):
y = (rng.normal()
+ (doses[g] + doses[g] ** 2) * (t >= F)
+ rng.normal(0, 0.5))
d = doses[g] if t >= F else 0.0
rows.append({'unit': g, 'period': t, 'y': y, 'dose': d})
had_data = pd.DataFrame(rows)
est = HeterogeneousAdoptionDiD()
results = est.fit(had_data, outcome_col='y', unit_col='unit',
time_col='period', dose_col='dose',
aggregate='event_study')
Key Differences#
Design Philosophy#
diff-diff: sklearn-style API with
fit()method, returning rich result objectsR packages: Function-based, returning lists or S3/S4 objects
Inference#
diff-diff: Analytical SEs by default, wild bootstrap available
R did: Multiplier bootstrap by default
Fixed Effects#
diff-diff:
absorbparameter for high-dimensional FE (within transformation)R fixest:
feolswith|notation for absorbed FE
Output Format#
diff-diff results have convenience methods:
results.summary() # Print formatted table
results.to_dict() # Dictionary representation
results.to_dataframe() # pandas DataFrame
Feature Comparison Table#
Feature |
diff-diff |
R did |
R HonestDiD |
R synthdid |
|---|---|---|---|---|
Basic 2x2 DiD |
✅ |
✅ |
❌ |
❌ |
TWFE |
✅ |
❌ |
❌ |
❌ |
Staggered DiD (CS) |
✅ |
✅ |
❌ |
❌ |
Covariate adjustment |
✅ |
✅ |
❌ |
❌ |
Doubly robust |
✅ |
✅ |
❌ |
❌ |
Group-time effects |
✅ |
✅ |
❌ |
❌ |
Event study |
✅ |
✅ |
✅ |
❌ |
Synthetic DiD |
✅ |
❌ |
❌ |
✅ |
Honest DiD (ΔRM) |
✅ |
❌ |
✅ |
❌ |
Honest DiD (ΔSD) |
✅ |
❌ |
✅ |
❌ |
Wild bootstrap |
✅ |
❌ |
❌ |
❌ |
Cluster-robust SE |
✅ |
✅ |
❌ |
✅ |
Placebo tests |
✅ |
❌ |
❌ |
✅ |
Parallel trends tests |
✅ |
✅ |
❌ |
❌ |
Bacon decomposition |
✅ |
❌ |
❌ |
❌ |
Sun-Abraham |
✅ |
❌ |
❌ |
❌ |
Imputation DiD |
✅ |
❌ |
❌ |
❌ |
Two-Stage DiD (did2s) |
✅ |
❌ |
❌ |
❌ |
Stacked DiD |
✅ |
❌ |
❌ |
❌ |
Continuous DiD |
✅ |
✅ |
❌ |
❌ |
Triple Difference (DDD) |
✅ |
❌ |
❌ |
❌ |
TROP |
✅ |
❌ |
❌ |
❌ |
Efficient DiD |
✅ |
❌ |
❌ |
❌ |
Heterogeneous adoption (HAD) |
✅ |
❌ |
❌ |
❌ |
Note
R equivalents for estimators not covered by the did, HonestDiD, or
synthdid packages: Sun-Abraham is available via fixest::sunab();
Imputation DiD via the didimputation package; Two-Stage DiD via the
did2s package; Bacon Decomposition via the bacondecomp package;
Stacked DiD requires manual implementation or the stackedev package;
Continuous DiD is available via the did package continuous extension;
Triple Difference requires manual implementation in R.
TROP and Efficient DiD have no direct R equivalents.
HeterogeneousAdoptionDiD (dCDH 2026) overlaps with the dedicated R
package DIDHAD (de Chaisemartin et al., 2025), which covers the
QUG case (Design 1’); diff-diff additionally covers Design 1 (no QUG,
WAS_{d_lower}) and survey-design integration via Binder TSL.
Migration Tips#
Column names: diff-diff uses string column names, similar to R packages
Formula interface: diff-diff supports R-style formulas for basic DiD:
formula='y ~ treated * post'Results access: Use
.att,.se,.ciinstead of$att,$seVisualization:
plot_event_study()produces matplotlib figures similar toggdid()outputMissing data: diff-diff requires complete data; use
balance_panel()ordropna()firstHeterogeneous Adoption (HAD): If you need surfaces the R
DIDHADpackage does not cover - Design 1 (no QUG,WAS_{d_lower}) or survey-design integration - reach forHeterogeneousAdoptionDiD. See the Heterogeneous Adoption (HAD) section above for the migration pattern.