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:

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=True option applies a J-test moment-selection step that drops misspecified components. Requires panel data with at least kmax pre-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"). The thres parameter 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 least kmax pre-treatment periods and higher-order parallel-trends assumptions are plausible.

  • With kmax>=3, set jtest=True to 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 None for 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). With kmax=3 or higher, additional moment conditions enter the GMM problem. Increase kmax only when the panel has enough pre-treatment periods and the higher-order parallel-trends assumptions are plausible.

jtest (bool, default False):

When kmax>=3, setting jtest=True applies 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=1 to exclude empty cohorts only.

se_boot (bool or None):

Controls inference method. With None (default for design="did"), bootstrap identifies the weight matrix and standard errors while confidence intervals use the efficient-GMM asymptotic formula. With True, percentile bootstrap intervals replace asymptotic ones. Staggered-adoption designs require se_boot=True (set automatically).

lead (list of int or None):

For staggered-adoption designs, specifies which leads (periods since treatment) to estimate. None estimates 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. Set se_boot=True for percentile bootstrap intervals.

Staggered adoption (design="sa"):

Bootstrap confidence intervals are required. Passing se_boot=False is 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:

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.