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 |
Argument mapping#
diff-diff deliberately ships no argument aliases for the R spellings. Where the
library owns a concept it uses its own name; where a name is the field’s language it
uses that. This table is the translation layer that replaces aliasing — it is the
answer to “why isn’t first_treat spelled gname”.
Where it lives names the callable that accepts the argument, since not every R
argument maps onto fit().
R package |
R argument |
diff-diff equivalent |
Where it lives |
Notes |
|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
First treated period; |
|
|
|
|
A list of column names, not a formula |
|
|
|
|
Constructor, not |
|
|
|
|
Post-fit, not a fit argument. |
|
|
|
|
|
|
|
carried by the fitted results |
results object |
Pass the results object itself; there is no coefficient/vcov argument |
|
|
inferred from the results |
results object |
Derived from the event-study surface, never passed |
|
|
|
|
Long-format column name, not a matrix |
|
|
|
|
Not a 1:1 mapping: a time-invariant treatment indicator plus the post-period list replaces the block structure |
fixest, DIDmultiplegtDYN and DIDHAD are discussed on this page in prose
rather than with paired examples, so they are deliberately absent here rather than
mapped from memory. See Migrating to 4.0 for the 4.0 renames, which change several
of the diff-diff spellings above.
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 (R's aggte() has two counterparts: the fit-time aggregate= shown here,
# deprecated in 3.9, and post-fit results.aggregate(type=), which supersedes it)
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='y', unit='unit',
time='period', dose='dose')
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.
Changes-in-Changes and QDiD are available via the qte package
(qte::CiC() / qte::QDiD(), the diff-diff parity target -
including covariates: diff-diff’s covariates= ports qte’s
xformla branch, a quantreg-based conditional CiC).
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,.conf_intinstead 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.