Multiple-pre-treatment difference-in-differences with inspectable pandas result objects.
Project description
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 git+https://github.com/gorgeousfish/diddesign-py.git
Visualization support (matplotlib):
pip install "diddesign[plot] @ git+https://github.com/gorgeousfish/diddesign-py.git"
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:
Xu, W. (2026). diddesign: Python package for Double Difference-in-Differences estimation (Version 0.1.0) [Computer software]. GitHub. https://github.com/gorgeousfish/diddesign
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
BibTeX:
@software{diddesign2026python,
title={diddesign: Python package for Double Difference-in-Differences estimation},
author={Wenli Xu},
year={2026},
version={0.1.0},
url={https://github.com/gorgeousfish/diddesign}
}
@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, City 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.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file diddesign-0.1.3.tar.gz.
File metadata
- Download URL: diddesign-0.1.3.tar.gz
- Upload date:
- Size: 458.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ef56d9386b500cadf6f1f27b3274bcc4e88f24aa994c7fcf7d08bb8149695903
|
|
| MD5 |
2053c3385cc3a1bb65f3185a22fbed1f
|
|
| BLAKE2b-256 |
aed63fb9c132a80744cbaf28cd7b44f79450eb7a60fae223e6c324746483e5c8
|
File details
Details for the file diddesign-0.1.3-py3-none-any.whl.
File metadata
- Download URL: diddesign-0.1.3-py3-none-any.whl
- Upload date:
- Size: 470.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
69b32f4e1f865a58083fa1d99ae8553a799a6b1f64ef5a262e5282c5c72227d3
|
|
| MD5 |
9f497fb42372a188babf49247a07e8a4
|
|
| BLAKE2b-256 |
3a981f9e28825995bfa4d1cb0758b8d3277daedc03668313a1994afd9fdc89f4
|