User Guide
Installation
From PyPI:
pip install diddesign
For visualization support (matplotlib):
pip install "diddesign[plot]"
For editable development installs (repository contributors only):
python3 -m pip install -e diddesign-py
Requirements: Python >= 3.12, NumPy >= 1.26, pandas >= 2.2.
Quick Start
The package exposes two primary functions:
diddesign.did()— estimate treatment effects.diddesign.did_check()— pre-treatment placebo diagnostics.
A typical workflow diagnoses pre-treatment assumptions first, then estimates:
from diddesign.data import data
from diddesign import did, did_check, summary
df = data("malesky2014")
# Step 1: pre-treatment placebo diagnostic
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())
lag estimate_raw std_error_raw eqci95_lb_std eqci95_ub_std
1 -0.00337 0.041026 -0.163403 0.163403
# Step 2: estimate treatment effects
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(summary(result, as_frame=True))
estimator lead estimate std.error statistic p_value ci.low ci.high
Double-DID 0 0.076596 0.046146 1.659849 0.096945 -0.013849 0.167041
DID 0 0.079314 0.057338 1.383284 0.166578 -0.033066 0.191694
sDID 0 0.082684 0.089100 0.927990 0.353413 -0.091949 0.257317
Data Requirements
The estimator accepts two data structures through the data_type parameter.
- Panel data (
data_type="panel"): Repeated observations of the same units over time. Each row is a unit-time pair. Required columns:
outcome: the dependent variable.treatment: binary (0/1) treatment indicator.time: integer or ordered time period.unit_id: unit identifier (string or integer).
- Repeated cross-sections (
data_type="rcs"): Different units sampled in each period. Required columns:
outcome: the dependent variable.treatment: binary (0/1) treatment indicator.time: integer or ordered time period.post: binary (0/1) indicator for post-treatment periods.id_cluster(optional): cluster identifier for clustered bootstrap.
Designs
The package supports three estimation designs through the design and
kmax parameters.
- Standard Double DID (
design="did",kmax=2): Two pre-treatment periods. Combines DID and sequential DID via efficient GMM weights. This is the default. Accepts both panel and RCS data.
- K-DID (
design="did",kmax>=3): Three or more pre-treatment periods. Higher-order moment conditions accommodate polynomial confounding. The
jtest=Trueoption applies a J-test moment-selection step that drops misspecified components. Requires panel data with at leastkmaxpre-treatment periods.- Staggered Adoption (
design="sa"): Units adopt treatment at different times. Computes lead-specific SA-DID, SA-sDID, and SA-Double-DID (or SA-K-DID) estimates, aggregated by treatment-timing weights. Requires panel data (
data_type="panel"). Thethresparameter sets the minimum number of treated units per adoption cohort.
Design Choice Guide
Use design="did" when:
Treatment occurs at a common time for all treated units.
Works with panel or repeated cross-section data.
Use design="sa" when:
Units adopt treatment at different times (staggered adoption).
Requires panel data (
data_type="panel").
kmax guidelines:
kmax=2(default): recommended for most applications.kmax>=3: only when the panel has at leastkmaxpre-treatment periods and higher-order parallel-trends assumptions are plausible.With
kmax>=3, setjtest=Trueto adaptively remove misspecified moment conditions.
Parameter Selection Guide
The following guidance helps choose key parameters for applied work.
n_boot(integer, required):Number of bootstrap replications for inference. For exploratory analysis, 50–200 replications provide quick feedback. For publication-quality inference, use at least 1000. Larger values reduce bootstrap noise in standard errors and confidence intervals at the cost of computation time.
random_seed(integer or None):Seed for the bootstrap random number generator. Set an integer value for reproducibility. Omit or pass
Nonefor non-reproducible runs.kmax(integer, default 2):Maximum number of pre-treatment periods to exploit. With
kmax=2, the estimator uses DID and sequential DID (the standard Double DID). Withkmax=3or higher, additional moment conditions enter the GMM problem. Increasekmaxonly when the panel has enough pre-treatment periods and the higher-order parallel-trends assumptions are plausible.jtest(bool, default False):When
kmax>=3, settingjtest=Trueapplies a J-test moment-selection procedure that drops components whose identifying assumptions appear violated. This guards against bias from misspecified higher-order conditions.thres(integer, default 0):Minimum number of treated units in an adoption cohort for staggered-adoption designs. Cohorts below this threshold are excluded. Set
thres=1to exclude empty cohorts only.se_boot(bool or None):Controls inference method. With
None(default fordesign="did"), bootstrap identifies the weight matrix and standard errors while confidence intervals use the efficient-GMM asymptotic formula. WithTrue, percentile bootstrap intervals replace asymptotic ones. Staggered-adoption designs requirese_boot=True(set automatically).lead(list of int or None):For staggered-adoption designs, specifies which leads (periods since treatment) to estimate.
Noneestimates all available leads.lag(list of int):For
did_check, specifies which pre-treatment lags to test. Each lag defines a placebo treatment timing.
Result Objects
diddesign.DidResult is returned by did(). It is immutable and
provides pandas DataFrame accessors:
result.to_dataframe() # All estimate rows
result.to_estimates_frame() # Alias for to_dataframe()
result.to_bootstrap_frame() # Bootstrap draws (iterations x 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 output
result.to_serialized_result() # Serializable dict for export
diddesign.DidCheckResult is returned by did_check():
check.to_summary_frame() # Placebo test summary rows
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
Reading Returned Rows
The returned DataFrames follow a deliberate reading order before reporting.
For Double DID, read the estimate frame first, then the weight and GMM frames:
estimates = result.to_estimates_frame()
weights = result.to_weights_frame()
gmm_rows = result.to_gmm_frame()
# Component estimates
component_rows = estimates.loc[estimates["estimator"].isin(["DID", "sDID"])]
# Combined Double-DID row
combined_row = estimates.loc[estimates["estimator"] == "Double-DID"].iloc[0]
The GMM weights show how DID and sDID were combined. A w_did of 1.81 and
w_sdid of -0.81 indicates the GMM-optimal combination extrapolates beyond
a convex average. This occurs when the covariance structure of component
estimators makes one component informationally redundant for the combined
estimate.
For staggered-adoption designs, read by lead. Each lead has its own SA-DID, SA-sDID, and SA-Double-DID rows, plus a lead-specific weight row:
# Filter by lead
lead0 = estimates.loc[estimates["lead"] == 0]
weights_lead0 = weights.loc[weights["lead"] == 0]
Interpreting Results
The summary() function returns a frame with test statistics and p-values:
from diddesign import summary
print(summary(result, as_frame=True))
Key columns:
estimate: the point estimate of the treatment effect (ATT).
std.error: bootstrap or asymptotic standard error.
statistic: z-statistic (estimate / std.error).
p_value: two-sided p-value.
ci.low, ci.high: 95% confidence interval bounds.
The Double-DID row is the GMM-optimal combination. The DID and sDID rows are the individual component estimators. Compare them to understand whether the combined estimate derives mainly from one component or represents a genuine efficiency improvement.
For pre-treatment diagnostics, interpret the equivalence confidence interval
(eqci95_lb_std, eqci95_ub_std) as the range of standardized placebo
effects consistent with parallel trends. A placebo estimate near zero with a
narrow equivalence interval supports the identifying assumption.
Formula Interface
An alternative to specifying column names individually:
from diddesign import did
result = did(
df, formula="pro4 ~ treatment + post_treat | lnarea + lnpopden",
time="year", data_type="rcs",
id_cluster="id_district", n_boot=50, random_seed=1234,
)
Panel formulas use outcome ~ treatment | covariates.
Repeated cross-section formulas use outcome ~ treatment + post | covariates.
Supported covariate terms: bare column names (continuous), factor(column)
for categorical encoding, x1:x2 for interactions, and x1*x2 for main
effects plus interaction.
Covariates
Pass covariates as a list of term strings:
result = did(
df, outcome="pro4", treatment="treatment",
time="year", post="post_treat", data_type="rcs",
id_cluster="id_district",
covariates=["lnarea", "lnpopden"],
n_boot=50, random_seed=1234,
)
print(result.to_dataframe())
estimator lead estimate std_error ci_lo ci_hi weight
Double-DID 0 0.083374 0.042437 0.000199 0.166549 NaN
DID 0 0.086286 0.054630 -0.020786 0.193359 1.864198
sDID 0 0.089656 0.085487 -0.077895 0.257207 -0.864198
Supported covariate specifications:
"x1"— continuous covariate."factor(region)"— categorical dummy encoding."x1:x2"— interaction of x1 and x2."x1*x2"— main effects plus interaction.
Covariates cannot reuse outcome, treatment, time, unit id, or post columns.
Inference
- Standard DID (
design="did"): By default (
se_boot=None), bootstrap identifies the GMM weight matrix and component standard errors, while confidence intervals use the efficient-GMM asymptotic formula. Setse_boot=Truefor percentile bootstrap intervals.- Staggered adoption (
design="sa"): Bootstrap confidence intervals are required. Passing
se_boot=Falseis rejected.
The n_boot parameter controls bootstrap replications (minimum 2). The
random_seed parameter ensures reproducibility (integer in [0, 2^32 - 1]
or None).
Visualization
Plotting requires the optional diddesign[plot] dependency (matplotlib).
Functions consume the plotting rows returned by fit() and check():
from diddesign import fit, plot_estimates, plot_diagnostics
# Render figures directly from result objects
plot_estimates(result, title="Double DID Estimates", save="fig.png", show=False)
plot_diagnostics(check, result=result, save="diag.png", show=False)
# Or prepare plotting rows for custom use
fit_rows = fit(result, as_frame=True)
print(fit_rows)
Available plot functions:
diddesign.plot_estimates()— event-study plot with optional placebo overlay.diddesign.plot_trends()— pre-treatment trend comparison.diddesign.plot_placebo()— placebo estimate plot.diddesign.plot_pattern()— staggered-adoption pattern diagnostic.diddesign.plot_diagnostics()— multi-panel diagnostic figure.
Built-in Datasets
from diddesign.data import data
df = data("malesky2014") # Vietnam RCS (Malesky et al. 2014), 6269 x 42
df = data("paglayan2019") # US states panel (Paglayan 2019), 2058 x 9
malesky2014: Repeated cross-section data on Vietnamese communes, originally
used to study the effects of administrative recentralization on local public
goods provision (Malesky, Nguyen, and Tran 2014). Key columns: pro4
(outcome), treatment, year, post_treat, id_district (cluster).
paglayan2019: Balanced panel of US states from 1959 to 2000, used to study
the effects of collective bargaining laws on teacher salaries and school
expenditure (Paglayan 2019). Key columns: pupil_expenditure,
teacher_salary, treatment, year, state (unit id).
See diddesign.data for full dataset documentation.
Error Handling
The estimators validate inputs and raise informative exceptions:
DidDataError: column missing, wrong type, or panel structure violated.DataContractError: design contract violation (e.g., treatment not absorbing in SA design).
from diddesign import did, DidDataError
try:
result = did(data=df, outcome="y", treatment="treat", time="t")
except DidDataError as e:
print(f"Data issue: {e}")
Bootstrap Inference for Staggered Adoption
For staggered-adoption designs, bootstrap confidence intervals are required
because the aggregation across treatment timings does not admit a simple
asymptotic formula. Set se_boot=True to request percentile bootstrap
confidence intervals. Python or NumPy boolean scalars are accepted and
numeric or string flags are rejected with an informative error message.
Passing se_boot=False with design="sa" is rejected so that
the inference choice remains explicit.
Summary Statistic Edge Cases
A zero standard error with a non-zero estimate is reported as an infinite signed statistic with zero p-value, correctly reflecting the extreme z-score rather than producing undefined arithmetic.
Conversely, the unidentified 0 / 0 case is reported as None for both
statistic and p-value, instead of exposing NaN in public summary rows.
Rows whose standard error is unavailable remain in the summary output with
their estimator, lead, and point estimate intact. Only
std.error, statistic, p_value, ci.low, and ci.high are
reported as None.
Plotting Row Requirements
The fit() function requires available Double-DID estimate rows with
standard errors to produce plotting rows. Diagnostic overlays require
finite raw placebo estimates with non-negative raw standard errors.
For a single-lead result, fit() requires a Double-DID row with a standard
error. For multi-lead did() results, fit() emits one Double-DID
plotting row per lead that actually has both a Double-DID estimate and
standard error. For multi-lead did() results, fit emits one
Double-DID plotting row per lead with an available Double-DID estimate and
standard error, and keeps time_to_treat equal to the lead. Each row
carries the lead-specific estimate and standard error.
Staggered-adoption pattern diagnostics cannot be overlaid onto a non-SA result; attempting this results in silently dropping the treatment-pattern rows from the output frame.
Formula Validation
The formula argument must be a string conforming to the two-sided R-style
syntax. is_panel must be a Python or NumPy boolean scalar; truthy
integers and strings are rejected.
The formula parser accepts bare column names and factor(column) markers
on the right-hand side. Direct DidFormulaSpec objects enforce the same
identifier, covariate-term, and interaction-term validation rules;
unsupported terms are rejected with informative error messages.
The same covariate-term validation applies when terms are passed directly
through covariates=; direct covariates must be a sequence of terms, not
a bare string.
Duplicate covariate column names are rejected before did() and
did_check() proceed, including mixed encodings such as x plus
factor(x).
Covariates also cannot reuse outcome, treatment, time, unit id, or post role columns; those role columns are reserved for the DID design itself.
Time Label Validation
Time labels must be all numeric or all string labels. Numeric time labels
may use Python or numpy real scalars (int, float, numpy integer/float types).
NaN and infinite time values are rejected before panel, repeated
cross-section, or staggered-adoption estimators derive treatment timing.
Boolean Numeric Validation
Manual did_check diagnostic and plotting rows also treat booleans as
invalid numeric values, rather than coercing True or False into
1.0 or 0.0.
GMM matrix entries must be finite numeric values, not booleans.
Random Seed Validation
random_seed must be None or an integer between 0 and 2**32 - 1;
booleans, floats, strings, negative values, and out-of-range values are
rejected before any bootstrap sampling starts.
Design Role and Cluster Validation
design and data_type must be strings from the supported public choices
("did", "sa" for design; "panel", "rcs" for data_type).
outcome, treatment, time, unit_id, and post must be
non-empty column name strings when required or provided. Those design-role
columns must be distinct; any overlap is rejected before any estimator or
diagnostic design matrix is built.
id_cluster must be None or a non-empty column name string; blank
strings and non-string values are rejected to prevent silently changing the
cluster mode. Explicit cluster columns may match unit_id (cluster at the
unit level), but they may not reuse outcome, treatment, time, or post
columns, because those columns define the estimand rather than an
independent clustering variable.
Bootstrap and Sequence Validation
n_boot must be an integer greater than or equal to 2 for both did()
and data-driven did_check(); booleans, floats, strings, and missing
values are rejected before any bootstrap or diagnostic resampling starts.
lead sequences for did() and lag sequences for data-driven
did_check() must contain at least one non-negative integer.
GMM Availability Consistency
GMM rows require double_did_available to match the availability of both
component weights and a positive GMM variance; the result cannot mark a
complete weighting calculation as unavailable.
double_did_available flags accept Python or NumPy boolean scalars;
numeric and string flag values are rejected.
double_did_available_leads must exactly match the estimate leads for
which both component weights exist. The global double_did_available flag
must match whether every estimate lead has a Double-DID row; attempts to
under-report or over-report Double-DID availability are rejected.
identified_leads must match the estimate leads that passed identification.
filtered_leads must match unidentified_leads.
requested_leads must equal the disjoint union of identified and filtered
leads. Singular alias fields such as requested_lead and
identified_lead must match their plural fields.
Negative GMM Weights
Double-DID efficient-GMM weights are not constrained to be convex weights; negative component weights are retained when the bootstrap covariance matrix is finite, symmetric positive definite and the normalized weights sum to one. These weights define the Double-DID point estimate and efficient-GMM standard error.
Public summary(), fit(), to_estimates_frame(), and serializers
expose that resulting Double-DID row directly, rather than reinterpreting it
as a convex average.
weights_by_lead must match the normalized row-sum weights implied by
W_by_lead; the result cannot expose mutually inconsistent GMM weights
and inverse covariance matrices. This check applies to both the basic DID
design and the staggered-adoption SA design.
Boolean as_frame Validation
The as_frame option on summary(), fit(), and check() must be
a Python or NumPy boolean scalar; truthy strings, integers, and other
non-boolean values are rejected to prevent silently switching the return type.
Estimator Filter Validation
The estimator filter must be a supported estimator string or a non-empty
sequence of supported estimator strings; dictionaries, generators, empty
sequences, and mixed-type sequences are rejected.
Sequential DID Transformed Outcomes
Sequential DID transformed outcomes follow Appendix C.3 of Egami and Yamauchi (2023). For both panel and repeated cross-section data, sDID uses group-specific lagged means to construct the transformed outcome: the current outcome minus the treated group’s lagged mean for treated observations, and the current outcome minus the control group’s lagged mean for control observations.
SA Component Timing Weights
SA time-average components use treatment-timing weights to aggregate across adoption cohorts. When a lead/timing path is unavailable for one component but available for another, SA-DID and SA-sDID drop unavailable paths independently and renormalize their own timing weights, rather than dropping the whole timing path from both components.
SA Placebo Timing Weights
SA placebo diagnostics also keep raw and standardized channels separate when computing timing-weighted averages. If a timing path has a raw placebo estimate but no standardized estimate, the raw channel keeps that path and renormalizes raw timing weights while the standardized channel drops only the unavailable standardized path.
Dataset Registry
The root data/ registry (see data/README.md) documents all real-world
datasets available in the repository with provenance, cleaning rules, and
package-copy paths.