diddesign
Double Difference-in-Differences for Python
Overview
diddesign implements the Double Difference-in-Differences estimator
proposed by Egami and Yamauchi (2023, Political Analysis). The method
addresses a practical question in observational panel studies: when multiple
pre-treatment periods are available, how should an analyst exploit them to
strengthen causal inference?
The standard DID estimator requires parallel trends between the last pre-treatment period and the post-treatment period. The sequential DID (sDID) estimator requires the weaker parallel trends-in-trends assumption but uses an additional pre-treatment period. These two assumptions are logically distinct—neither implies the other. The Double DID formulates a GMM problem that combines both estimators with variance-minimizing weights. When both identification conditions hold, the combined estimate achieves lower variance than either component alone; when only one holds, the framework remains consistent under that condition.
The package extends to K-DID (Appendix E of the paper): given K ≥ 3 pre-treatment periods, higher-order transformed-outcome estimators provide additional moment conditions that permit up to (K−1)-degree polynomial time-varying confounding, combined via the same GMM weighting. A J-test moment-selection step adaptively discards misspecified components.
For staggered-adoption designs, the package computes lead-specific SA-DID, SA-sDID, and SA-Double-DID (or SA-K-DID) estimates and aggregates them into a time-weighted average treatment effect.
Identification Assumptions
The Double DID framework rests on two assumptions about the counterfactual trend structure:
| Assumption | Formal Name | Permitted Bias |
|---|---|---|
| A1 | Parallel Trends | Constant between groups |
| A2 | Parallel Trends-in-Trends | Linear change between groups |
Standard DID requires A1. Sequential DID requires A2. The Double DID requires only that at least one of A1 or A2 holds—a strictly weaker condition than either assumption alone. With K ≥ 3 pre-treatment periods, K-DID adds higher-order assumptions that accommodate polynomial confounding.
The identifying relationship is:
Extended Parallel Trends
↓ implies both
┌────┴────┐
↓ ↓
Standard Parallel
Parallel Trends-in-
Trends Trends
Installation
pip install diddesign
Visualization support (matplotlib):
pip install "diddesign[plot]"
Requirements: Python ≥ 3.12, NumPy ≥ 1.26, pandas ≥ 2.2.
Usage
The package provides three entry points:
did()fits a DID or staggered-adoption design and returns aDidResult.did_check()computes pre-treatment diagnostics and returns aDidCheckResult.DidResultprovides table-ready accessors (to_dataframe(),to_weights_frame(),to_gmm_frame(),to_latex()).
The Double DID workflow proceeds in two steps: first assess the plausibility of identification assumptions via pre-treatment diagnostics, then estimate treatment effects conditional on those diagnostics.
Step 1: Assess Pre-treatment Assumptions
from diddesign.data import data
from diddesign import did_check
df = data("malesky2014")
check = did_check(
data=df, outcome="pro4", treatment="treatment",
time="year", post="post_treat", data_type="rcs",
id_cluster="id_district", lag=[1], n_boot=50, random_seed=1234,
)
print(check.to_summary_frame())
Output:
lag estimate_raw std_error_raw eqci95_lb_std eqci95_ub_std
0 1 -0.00337 0.041026 -0.163403 0.163403
The placebo estimates test whether the DID and sDID estimators yield approximately zero effects in pre-treatment periods where no effect should exist. The equivalence confidence interval (reported in units of baseline control-group standard deviations) provides positive evidence for approximate parallel trends when it excludes substantively large deviations. No universal cutoff exists; researchers must apply domain knowledge.
Step 2: Estimate Treatment Effects
from diddesign import did
result = did(
df, outcome="pro4", treatment="treatment",
time="year", post="post_treat", data_type="rcs",
id_cluster="id_district", n_boot=200, random_seed=1234,
)
print(result.to_dataframe())
print(result.to_latex(caption="Recentralization Effect on Pro4"))
Output:
estimator lead estimate std_error ci_lo ci_hi weight
0 Double-DID 0 0.076596 0.046146 -0.013849 0.167041 NaN
1 DID 0 0.079314 0.057338 -0.033066 0.191694 1.806658
2 sDID 0 0.082684 0.089100 -0.091949 0.257317 -0.806658
\begin{table}[htbp]
\centering
\caption{Recentralization Effect on Pro4}
\begin{tabular}{llrrrr}
\hline\hline
Estimator & Lead & Estimate & Std. Error & CI Low & CI High \\
\hline
Double-DID & 0 & 0.0766$^{*}$ & 0.0461 & -0.0138 & 0.1670 \\
DID & 0 & 0.0793 & 0.0573 & -0.0331 & 0.1917 \\
sDID & 0 & 0.0827 & 0.0891 & -0.0919 & 0.2573 \\
\hline\hline
\multicolumn{6}{l}{\footnotesize Note: $^{*}$p$<$0.10, $^{**}$p$<$0.05, $^{***}$p$<$0.01} \\
\end{tabular}
\end{table}
The output reports three rows: Double-DID (the GMM-optimal combination), DID,
and sDID. The GMM weights indicate which component the data favor—when
w_DID ≈ 1, standard DID dominates; when w_sDID ≈ 1, the sequential
component dominates.
Staggered Adoption
import numpy as np
from diddesign.data import data
from diddesign import did, did_check
df = data("paglayan2019")
df["log_expenditure"] = np.log(df["pupil_expenditure"] + 1.0)
# Diagnose pre-trends across multiple lags
check = did_check(
data=df, outcome="log_expenditure", treatment="treatment",
time="year", unit_id="state", design="sa",
lag=[1, 2, 3], thres=1, n_boot=50, random_seed=1234,
)
print(check.to_summary_frame())
# SA-Double-DID estimation
result = did(
df, outcome="log_expenditure", treatment="treatment",
time="year", unit_id="state", design="sa",
thres=1, n_boot=200, random_seed=1234,
)
print(result.to_dataframe())
Output:
lag estimate_raw std_error_raw eqci95_lb_std eqci95_ub_std
0 1 -0.002669 0.009736 -0.117499 0.117499
1 2 -0.012447 0.007841 -0.151357 0.151357
2 3 0.002269 0.011331 -0.121691 0.121691
estimator lead estimate std_error ci_lo ci_hi weight
0 SA-Double-DID 0 0.011401 0.012157 -0.011430 0.033800 NaN
1 SA-DID 0 0.010984 0.012247 -0.011420 0.034097 0.843723
2 SA-sDID 0 0.013653 0.014537 -0.014634 0.042717 0.156277
K-DID with J-test Moment Selection
When three or more pre-treatment periods are available, K-DID exploits higher-order moment conditions. The J-test adaptively removes components whose identifying assumptions appear violated.
result = did(
df, outcome="log_expenditure", treatment="treatment",
time="year", unit_id="state", design="sa",
kmax=3, jtest=True, thres=1,
n_boot=200, random_seed=1234,
)
print(result.to_dataframe())
Output:
estimator lead estimate std_error ci_lo ci_hi weight
0 SA-K-DID 0 0.011685 0.012156 -0.011180 0.034105 None
1 SA-DID 0 0.010984 0.012247 -0.011420 0.034097 None
2 SA-sDID 0 0.013653 0.014537 -0.014634 0.042717 None
3 SA-kDID-3 0 0.003875 0.023613 -0.040192 0.052995 None
Covariates and Interactions
The package supports continuous covariates, factor() categorical encoding,
and x1:x2 interaction terms (or x1*x2 for main effects plus interaction):
result = did(
data, outcome="y", treatment="treat", time="time",
unit_id="unit", covariates=["x1*x2", "factor(region)"],
n_boot=50, random_seed=55,
)
Visualization
from diddesign import plot_estimates, plot_diagnostics
plot_estimates(result, check_fit=check,
title="Double DID Estimates", save="estimates.png", show=False)
plot_diagnostics(check, result=result,
title="Pre-treatment Diagnostics", save="diagnostics.png",
show=False)
Methodology
The Double DID estimator combines DID (τ̂_DID) and sequential DID (τ̂_sDID) using efficient GMM weights:
$$ \hat{\tau}{DDID} = w{DID} \cdot \hat{\tau}{DID} + w{sDID} \cdot \hat{\tau}_{sDID} $$
where the weights minimize asymptotic variance:
$$ w = \frac{\Sigma^{-1} \mathbf{1}}{\mathbf{1}' \Sigma^{-1} \mathbf{1}} $$
and Σ is the bootstrap covariance matrix of (τ̂_DID, τ̂_sDID)'. Under the extended parallel trends assumption:
$$ \text{Var}(\hat{\tau}{DDID}) \leq \min{\text{Var}(\hat{\tau}{DID}),; \text{Var}(\hat{\tau}_{sDID})} $$
Standard errors and confidence intervals are obtained via a nonparametric
bootstrap (cluster bootstrap when id_cluster is specified). The bootstrap
covariance matrix Σ̂ determines the GMM-optimal weights, and percentile or
normal-approximation intervals are reported depending on the design.
For K-DID, the K-dimensional generalization combines all K component estimators via the K×K bootstrap covariance, with the J-test providing a model-selection step that excludes moments whose overidentification statistic rejects.
API Reference
Core Functions
| Function | Purpose |
|---|---|
did() |
Fit DID or staggered-adoption design →DidResult |
did_check() |
Pre-treatment diagnostics →DidCheckResult |
summary() |
Formatted summary of fitted result |
fit(..., as_frame=True) |
Event-time plotting rows |
check(..., as_frame=True) |
Diagnostic plotting rows |
did() Parameters
did(data, *, formula=None, outcome=None, treatment=None, time,
unit_id=None, post=None, design="did", data_type="panel",
covariates=None, lead=0, thres=None, n_boot=30, se_boot=None,
level=95, id_cluster=None, random_seed=None, parallel=False,
n_cores=None, parallel_backend="thread", worker_timeout=None,
verbose=1, kmax=2, jtest=False)
| Parameter | Type | Default | Description |
|---|---|---|---|
data |
DataFrame | — | Input data (panel or repeated cross-section) |
formula |
str| None | None |
R-style formula, e.g."y ~ treat" |
outcome |
str| None | None |
Outcome column name (alternative to formula) |
treatment |
str| None | None |
Treatment indicator column |
time |
str | — | Time period column (required) |
unit_id |
str| None | None |
Unit identifier column (required for panel) |
post |
str| None | None |
Post-treatment indicator (required for RCS) |
design |
str | "did" |
"did" for standard or "sa" for staggered adoption |
data_type |
str | "panel" |
"panel" or "rcs" (repeated cross-section) |
covariates |
list[str]| None | None |
Covariate terms:"x1", "factor(x2)", "x1:x2", "x1*x2" |
lead |
int| list[int] | 0 |
Lead(s) for staggered adoption |
thres |
int| None | None |
Minimum observations threshold |
n_boot |
int | 30 |
Number of bootstrap replications |
se_boot |
bool| None | None |
Use bootstrap percentile CI |
level |
int | 95 |
Confidence level (50–99) |
id_cluster |
str| None | None |
Cluster variable for clustered bootstrap |
random_seed |
int| None | None |
Seed for reproducibility |
parallel |
bool | False |
Enable parallel bootstrap computation |
n_cores |
int| None | None |
Number of cores (default: all available) |
kmax |
int | 2 |
Maximum DID order: 2 = Double DID, ≥ 3 = K-DID |
jtest |
bool | False |
Apply J-test moment selection for K-DID |
Returns: DidResult
did_check() Parameters
did_check(*, data=None, formula=None, outcome=None, treatment=None,
time=None, unit_id=None, post=None, design="did",
covariates=None, data_type="panel", id_cluster=None,
lag=1, thres=None, n_boot=30, random_seed=None,
verbose=1)
All parameters are keyword-only. Parameters shared with did() have
identical semantics. The lag parameter specifies which pre-treatment
lag(s) to test.
Returns: DidCheckResult
Result Objects
DidResult is an immutable object returned by did(). It provides frame
accessors for downstream analysis:
result.to_dataframe() # Estimates as DataFrame
result.to_estimates_frame() # Alias
result.to_bootstrap_frame() # Bootstrap draws (iterations × components)
result.to_weights_frame() # GMM weight rows by lead
result.to_gmm_frame() # Full GMM calculation rows
result.to_k_weights_frame() # K-dimensional GMM weights (K-DID)
result.to_latex() # LaTeX table string
result.to_serialized_result() # Serializable dict for export
DidCheckResult is an immutable object returned by did_check():
check.to_summary_frame() # Placebo test summary
check.to_placebo_frame() # Placebo plotting rows
check.to_trends_frame() # Trend comparison rows
check.to_pattern_frame() # SA pattern rows
check.named_plot_rows() # Named plotting records
Each row in the GMM frame is a DidGmmRow containing the covariance entries,
weights, and GMM variance for a single lead. The diagnostic result provides
DidCheckResult.named_plot_rows() for downstream figure production.
For scripts that need a detached serialized record, DidResult and
DidCheckResult provide to_serialized_result(). New reporting code should
usually start from the frame accessors above because those preserve the table
rows used in the manuscript.
Data Loading
from diddesign.data import data
df = data("malesky2014") # Vietnam RCS (Malesky et al. 2014)
df = data("paglayan2019") # US states panel (Paglayan 2019)
Visualization Functions
All plotting functions require diddesign[plot] (matplotlib).
| Function | Input | Description |
|---|---|---|
plot_estimates(result, ...) |
DidResult |
Event-study plot with optional placebo overlay |
plot_trends(check_result, ...) |
DidCheckResult |
Pre-treatment trend comparison |
plot_placebo(check_result, ...) |
DidCheckResult |
Placebo estimate plot |
plot_pattern(check_result, ...) |
DidCheckResult |
Staggered-adoption pattern diagnostic |
plot_diagnostics(check_result, ...) |
DidCheckResult |
Multi-panel diagnostic figure |
Errors
diddesign provides structured exceptions with machine-readable error codes
(E001–E020) and diagnostic context dictionaries:
from diddesign.errors import DidValueError, ErrorCode
try:
result = did(data=df, outcome="y", treatment="bad_col", time="t", unit_id="id")
except DidValueError as e:
print(e.code) # ErrorCode.E001
print(e.context) # {'field_name': 'treatment', ...}
Full parameter documentation is available in the
Sphinx API reference.
To build docs locally: cd Docs && sphinx-build -b html . _build/html.
Citation
If you use this package in your research, please cite both the software and the methodology paper:
APA Format:
Cai, X. & Xu, W. (2026). diddesign: Python package for Double Difference-in-Differences estimation (Version 0.1.4) [Computer software]. GitHub. https://github.com/gorgeousfish/diddesign-py
Egami, N., & Yamauchi, S. (2023). Using Multiple Pretreatment Periods to Improve Difference-in-Differences and Staggered Adoption Designs. Political Analysis, 31(2), 195-212. https://doi.org/10.1017/pan.2022.8
Authors:
- Xuanyu Cai, University of Macau, xuanyuCAI@outlook.com
- Wenli Xu, City University of Macau, wlxu@cityu.edu.mo
BibTeX:
@software{diddesign2026python,
title={diddesign: Python package for Double Difference-in-Differences estimation},
author={Cai, Xuanyu and Xu, Wenli},
year={2026},
version={0.1.4},
url={https://github.com/gorgeousfish/diddesign-py}
}
@article{egami2023using,
title={Using Multiple Pretreatment Periods to Improve Difference-in-Differences and Staggered Adoption Designs},
author={Egami, Naoki and Yamauchi, Soichiro},
journal={Political Analysis},
volume={31},
number={2},
pages={195--212},
year={2023},
doi={10.1017/pan.2022.8}
}
Authors
Python Implementation:
- Xuanyu Cai, University of Macau Email: xuanyuCAI@outlook.com
- Wenli Xu, City University of Macau Email: wlxu@cityu.edu.mo
Methodology:
- Naoki Egami, Columbia University
- Soichiro Yamauchi, Harvard University
See Also
- R package by Egami & Yamauchi: DIDdesign
- Stata package: diddesign
- Paper: 10.1017/pan.2022.8
License
GPL-2.0. See LICENSE for details.
Metadata
Release files for diddesign 0.1.5
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| diddesign-0.1.5.tar.gz | 459.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| diddesign-0.1.5-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 929.5 kB
Release files / diddesign-0.1.5.tar.gz
| Download URL | diddesign-0.1.5.tar.gz |
|---|---|
| Size | 459.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
678e2aea515ed2fc3e9916bd2ed0857ec78a455274bc55e58cce983fce531b02
|
|
BLAKE2b-256 checksum How to use checksums |
e632daafdfffc252d164c74f46d17adb924737e8e9dc9a5f0b625b31760c5840
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.6
|
Release files / diddesign-0.1.5-py3-none-any.whl
| Download URL | diddesign-0.1.5-py3-none-any.whl |
|---|---|
| Size | 470.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f750748937394c8f3d99d7a6c4e0303dda15995ece16b5daee9b14a3d1d0b7a3
|
|
BLAKE2b-256 checksum How to use checksums |
77686a815f82ad41b1d418ab6fdca426ae079f67a0b366c17ac205d00750e6a8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.6
|