ModernDiD is a scalable, GPU-accelerated difference-in-differences library for Python. It consolidates modern DiD estimators from leading econometric research and various R and Stata packages into a single framework with a consistent API. Runs on a single machine, NVIDIA GPUs, and distributed Spark and Dask clusters.
- DiD Estimators - Staggered DiD, Doubly Robust DiD, Continuous DiD, Triple DiD, Intertemporal DiD, Honest DiD.
- Dataframe agnostic - Pass any Arrow-compatible DataFrame such as polars, pandas, pyarrow, duckdb, and more powered by narwhals.
- Distributed computing - Scale to billions of observations across Spark and Dask clusters. Pass a distributed DataFrame and the backend activates transparently.
- Fast computation - Polars for internal data wrangling, NumPy vectorization, Numba JIT compilation, and threaded parallel compute.
- GPU acceleration - Optional CuPy-accelerated estimation on NVIDIA GPUs, with multi-GPU scaling in distributed environments.
- Native plots - Built-in visualizations powered by plotnine, returning standard
ggplotobjects you can customize with the full grammar of graphics. - Robust inference - Analytical standard errors, bootstrap (weighted and multiplier), and simultaneous confidence bands.
For detailed documentation, including user guides and API reference, see moderndid.readthedocs.io.
uv pip install moderndid # Core estimators (did, drdid, didinter, didtriple)Some estimators and features require additional dependencies that are not installed by default. Extras are additive and build on the base install, so you always get the core estimators (att_gt, drdid, did_multiplegt, ddd) plus whatever extras you specify:
didcont- Continuous treatment DiD (cont_did)didhonest- Sensitivity analysis (honest_did)plots- Batteries-included plotsnumba- Faster bootstrap inferencespark- Distributed estimation via PySparkdask- Distributed estimation via Daskgpu- GPU-accelerated estimation (requires CUDA)
uv pip install "moderndid[all]" # All extras except gpu
uv pip install "moderndid[didcont,plots]" # Combine specific extras
uv pip install "moderndid[gpu,spark]" # GPU + distributedTo install the latest development version directly from GitHub:
uv pip install "moderndid[all] @ git+https://github.com/jordandeklerk/moderndid.git"Tip
When a package manager like uv or pip cannot resolve a dependency required by an extra, it may silently fall back to an older version of ModernDiD where that extra does not exist, rather than raising an error.
The gpu extra is the most likely to trigger this, since it depends on cupy-cuda12x (Linux and Windows only) and rmm-cu12 (Linux only), both of which require NVIDIA CUDA. If you see a warning like The package moderndid==0.0.3 does not have an extra named 'gpu', this is what happened. To use the gpu extra, install on a machine with NVIDIA CUDA drivers, or pin the version to get a clear error instead of a silent downgrade.
uv pip install "moderndid[gpu]>=0.1.0"This example uses county-level teen employment data to estimate the effect of minimum wage increases. States adopted higher minimum wages at different times (2004, 2006, or 2007), making this a staggered adoption design.
The att_gt() function estimates the average treatment effect for each group g (defined by when units were first treated) at each time period t. We use the doubly robust estimator, which combines outcome regression and propensity score weighting to provide consistent estimates if either model is correctly specified.
import moderndid as did
data = did.load_mpdta()
attgt_result = did.att_gt(
data=data,
yname="lemp",
tname="year",
idname="countyreal",
gname="first.treat",
est_method="dr",
)
print(attgt_result)==============================================================================
Group-Time Average Treatment Effects
==============================================================================
┌───────┬──────┬──────────┬────────────┬────────────────────────────┐
│ Group │ Time │ ATT(g,t) │ Std. Error │ [95% Pointwise Conf. Band] │
├───────┼──────┼──────────┼────────────┼────────────────────────────┤
│ 2004 │ 2004 │ -0.0105 │ 0.0233 │ [-0.0561, 0.0351] │
│ 2004 │ 2005 │ -0.0704 │ 0.0310 │ [-0.1312, -0.0097] * │
│ 2004 │ 2006 │ -0.1373 │ 0.0364 │ [-0.2087, -0.0658] * │
│ 2004 │ 2007 │ -0.1008 │ 0.0344 │ [-0.1682, -0.0335] * │
│ 2006 │ 2004 │ 0.0065 │ 0.0233 │ [-0.0392, 0.0522] │
│ 2006 │ 2005 │ -0.0028 │ 0.0196 │ [-0.0411, 0.0356] │
│ 2006 │ 2006 │ -0.0046 │ 0.0178 │ [-0.0394, 0.0302] │
│ 2006 │ 2007 │ -0.0412 │ 0.0202 │ [-0.0809, -0.0016] * │
│ 2007 │ 2004 │ 0.0305 │ 0.0150 │ [ 0.0010, 0.0600] * │
│ 2007 │ 2005 │ -0.0027 │ 0.0164 │ [-0.0349, 0.0294] │
│ 2007 │ 2006 │ -0.0311 │ 0.0179 │ [-0.0661, 0.0040] │
│ 2007 │ 2007 │ -0.0261 │ 0.0167 │ [-0.0587, 0.0066] │
└───────┴──────┴──────────┴────────────┴────────────────────────────┘
------------------------------------------------------------------------------
Signif. codes: '*' confidence band does not cover 0
P-value for pre-test of parallel trends assumption: 0.1681
------------------------------------------------------------------------------
Data Info
------------------------------------------------------------------------------
Control Group: Never Treated
Anticipation Periods: 0
------------------------------------------------------------------------------
Estimation Details
------------------------------------------------------------------------------
Estimation Method: Doubly Robust
------------------------------------------------------------------------------
Inference
------------------------------------------------------------------------------
Significance level: 0.05
Analytical standard errors
==============================================================================
Reference: Callaway and Sant'Anna (2021)
ModernDiD provides "batteries-included" plotting functions (plot_event_study, plot_gt, plot_agg, and more) as well as data converters for building custom figures with plotnine. Since all plot functions return ggplot objects, you can restyle them with the full grammar of graphics:
from plotnine import element_text, labs, theme, theme_gray
p = did.plot_gt(attgt_result, ncol=3)
p = (p
+ labs(
x="Year",
y="ATT (Log Employment)",
title="Minimum Wage Effects on Teen Employment",
subtitle="Group-time average treatment effects by treatment cohort",
)
+ theme_gray()
+ theme(
legend_position="bottom",
strip_text=element_text(size=11, weight="bold"),
)
)While group-time effects are useful, they can be difficult to summarize when there are many groups and time periods. The aggte function aggregates these into more interpretable summaries. Setting type="dynamic" produces an event study that shows how effects evolve relative to treatment timing:
event_study = did.aggte(attgt_result, type="dynamic")
print(event_study)==============================================================================
Aggregate Treatment Effects (Event Study)
==============================================================================
Overall summary of ATT's based on event-study/dynamic aggregation:
┌─────────┬────────────┬────────────────────────┐
│ ATT │ Std. Error │ [95% Conf. Interval] │
├─────────┼────────────┼────────────────────────┤
│ -0.0772 │ 0.0200 │ [ -0.1164, -0.0381] * │
└─────────┴────────────┴────────────────────────┘
Dynamic Effects:
┌────────────┬──────────┬────────────┬────────────────────────────┐
│ Event time │ Estimate │ Std. Error │ [95% Pointwise Conf. Band] │
├────────────┼──────────┼────────────┼────────────────────────────┤
│ -3 │ 0.0305 │ 0.0150 │ [-0.0078, 0.0688] │
│ -2 │ -0.0006 │ 0.0133 │ [-0.0344, 0.0333] │
│ -1 │ -0.0245 │ 0.0142 │ [-0.0607, 0.0118] │
│ 0 │ -0.0199 │ 0.0118 │ [-0.0501, 0.0102] │
│ 1 │ -0.0510 │ 0.0169 │ [-0.0940, -0.0079] * │
│ 2 │ -0.1373 │ 0.0364 │ [-0.2301, -0.0444] * │
│ 3 │ -0.1008 │ 0.0344 │ [-0.1883, -0.0133] * │
└────────────┴──────────┴────────────┴────────────────────────────┘
------------------------------------------------------------------------------
Signif. codes: '*' confidence band does not cover 0
------------------------------------------------------------------------------
Data Info
------------------------------------------------------------------------------
Control Group: Never Treated
Anticipation Periods: 0
------------------------------------------------------------------------------
Estimation Details
------------------------------------------------------------------------------
Estimation Method: Doubly Robust
------------------------------------------------------------------------------
Inference
------------------------------------------------------------------------------
Significance level: 0.05
Analytical standard errors
==============================================================================
Reference: Callaway and Sant'Anna (2021)
Event time 0 is the on-impact effect, negative event times are pre-treatment periods, and positive event times are post-treatment periods. Pre-treatment effects near zero support the parallel trends assumption, while post-treatment effects show how the impact evolves over time.
Data converters make it easy to overlay estimates from different estimators. The figure below compares the Callaway and Sant'Anna estimates against a standard TWFE event study estimated with pyfixest. See the Plotting Guide for the full code and more examples.
All estimators share a unified interface for core arguments. Pass any Arrow PyCapsule-compatible DataFrame (polars, pandas, pyarrow, duckdb, and others) and estimation works the same way:
result = did.att_gt(data, yname="y", tname="t", idname="id", gname="g", ...)
result = did.ddd(data, yname="y", tname="t", idname="id", gname="g", pname="p", ...)
result = did.cont_did(data, yname="y", tname="t", idname="id", gname="g", dname="dose", ...)
result = did.drdid(data, yname="y", tname="t", idname="id", treatname="treat", ...)
result = did.did_multiplegt(data, yname="y", tname="t", idname="id", dname="treat", ...)Distributed Computing. Pass a Spark or Dask DataFrame and the distributed backend activates automatically. See the Distributed guide.
from pyspark.sql import SparkSession
spark = SparkSession.builder.master("local[*]").getOrCreate()
result = did.att_gt(data=spark.read.parquet("panel.parquet"), # Spark dataframe
yname="y",
tname="t",
idname="id",
gname="g")GPU Acceleration. Pass backend="cupy" to offload estimation to NVIDIA GPUs. See the GPU guide and benchmarks.
result = did.att_gt(data,
yname="lemp",
tname="year",
idname="countyreal",
gname="first.treat",
backend="cupy") # GPU backendBuilt-in datasets from published studies are included for testing and reproducing results. All loaders return Arrow-compatible DataFrames that work directly with any estimator.
did.load_mpdta() # County teen employment
did.load_nsw() # NSW job training program
did.load_ehec() # Medicaid expansion
did.load_engel() # Household expenditure
did.load_favara_imbs() # Bank lending
did.load_cai2016() # Crop insuranceSynthetic data generators are also available for simulations and benchmarking.
did.gen_did_scalable() # Staggered DiD panel
did.gen_cont_did_data() # Continuous treatment DiD
did.gen_ddd_2periods() # Two-period triple DiD
did.gen_ddd_mult_periods() # Staggered triple DiD
did.gen_ddd_scalable() # Large-scale triple DiDmoderndid.didml— Machine learning approaches to DiD (Hatamyar et al., 2023)moderndid.drdidweak— Robust to weak overlap (Ma et al., 2023)moderndid.didcomp— Compositional changes in repeated cross-sections (Sant'Anna & Xu, 2025)moderndid.didimpute— Imputation-based estimators (Borusyak, Jaravel, & Spiess, 2024)moderndid.didbacon— Goodman-Bacon decomposition (Goodman-Bacon, 2019)moderndid.didlocal— Local projections DiD (Dube et al., 2025)moderndid.did2s— Two-stage DiD (Gardner, 2021)moderndid.etwfe— Extended two-way fixed effects (Wooldridge, 2021; Wooldridge, 2023)moderndid.functional— Specification tests (Roth & Sant'Anna, 2023)
ModernDiD would not be possible without the researchers who developed the underlying econometric methods and implemented them in various R and Stata packages. See our Acknowledgements page for a full list of the software, packages, and papers that have influenced this project.
If you use ModernDiD in your research, please cite it as:
@software{moderndid,
author = {{The ModernDiD Authors}},
title = {{ModernDiD: Scalable, GPU-Accelerated Difference-in-Differences for Python}},
year = {2025},
url = {https://github.com/jordandeklerk/moderndid}
}


