moderndid.ddd#
- moderndid.ddd(data, yname, tname, idname=None, gname=None, pname=None, xformla=None, control_group='nevertreated', base_period='universal', est_method='dr', weightsname=None, boot=False, boot_type='multiplier', biters=1000, cluster=None, alpha=0.05, trim_level=0.995, panel=True, allow_unbalanced_panel=False, random_state=None, n_jobs=1, n_partitions=None, max_cohorts=None, backend=None)[source]#
Compute the doubly robust Triple Difference-in-Differences estimator for the ATT.
Implements triple difference-in-differences (DDD) estimation following [1]. DDD extends standard DiD by incorporating a partition variable \(Q\) that identifies eligible units within treatment-enabling groups \(S\), allowing for violations of traditional DiD parallel trends as long as these violations are stable across groups.
Let \(S_i\) denote the period when treatment is enabled for unit \(i\)’s group, and \(Q_i \in \{0,1\}\) indicate eligibility within that group. The group-time average treatment effect measures the effect among eligible units in group \(g\) at time \(t\)
\[ATT(g,t) = \mathbb{E}[Y_{i,t}(g) - Y_{i,t}(\infty) \mid S_i = g, Q_i = 1].\]Identification relies on a DDD conditional parallel trends assumption that allows for differential trends between eligible and ineligible units, provided these differentials are stable across treatment-enabling groups. For groups \(g\) and \(g'\) where \(g' > \max\{g,t\}\)
\[\begin{split}&\mathbb{E}[\Delta Y(\infty) \mid S=g, Q=1, X] - \mathbb{E}[\Delta Y(\infty) \mid S=g, Q=0, X] \\ &= \mathbb{E}[\Delta Y(\infty) \mid S=g', Q=1, X] - \mathbb{E}[\Delta Y(\infty) \mid S=g', Q=0, X],\end{split}\]where \(\Delta Y(\infty) = Y_t(\infty) - Y_{t-1}(\infty)\) denotes the change in untreated potential outcomes. This assumption does not impose standard DiD parallel trends within or across groups, making DDD appealing when such assumptions are implausible.
- Parameters:
- data
DataFrame Data in long format. Accepts any object implementing the Arrow PyCapsule Interface (
__arrow_c_stream__), including polars, pandas, pyarrow Table, and cudf DataFrames.- yname
str Name of outcome variable column.
- tname
str Name of time period column.
- idname
str, optional Name of unit identifier column. Required for panel data. For repeated cross-section data (panel=False), this can be omitted and a row index will be used automatically.
- gname
str Name of treatment group column. For 2-period data, this should be 0 for never-treated and a positive value for treated units. For multi-period data, this is the first period when treatment is enabled for the unit’s group (use 0 or np.inf for never-treated units).
- pname
str Name of partition/eligibility column (1=eligible, 0=ineligible). This identifies which units within a treatment group are actually eligible to receive the treatment effect.
- xformla
str, optional Formula for covariates in the form “~ x1 + x2 + x3”. If None, only an intercept is used.
- control_group{“nevertreated”, “notyettreated”}, default=”nevertreated”
Which units to use as controls in multi-period settings. This parameter is ignored for 2-period data.
- base_period{“universal”, “varying”}, default=”universal”
Base period selection for multi-period settings. This parameter is ignored for 2-period data.
- est_method{“dr”, “reg”, “ipw”}, default=”dr”
Estimation method: doubly robust, regression, or IPW.
- weightsname
str, optional Name of the column containing observation weights.
- bootbool, default=False
Whether to use bootstrap for inference.
- boot_type{“multiplier”, “weighted”}, default=”multiplier”
Type of bootstrap for 2-period data (only used if boot=True). Multi-period data always uses multiplier bootstrap.
- biters
int, default=1000 Number of bootstrap repetitions (only used if boot=True).
- cluster
str, optional Name of the clustering variable for clustered standard errors. Currently only supported for 2-period data with bootstrap.
- alpha
float, default=0.05 Significance level for confidence intervals.
- trim_level
float, default=0.995 Trimming level for propensity scores. Only used for repeated cross-section data (panel=False).
- panelbool, default=True
Whether the data is panel data (True) or repeated cross-section data (False). Panel data has the same units observed across time periods. Repeated cross-section data has different samples in each period.
- allow_unbalanced_panelbool, default=False
If True and panel=True, allows unbalanced panel data. For multi-period settings, estimation stays in panel mode (preserving panel efficiency) while handling units that appear in different subsets of periods. For 2-period settings, unbalanced data falls back to repeated cross-section mode. If the panel is unbalanced and this is False, an error will be raised.
- random_state
int,Generator, optional Random seed for reproducibility of bootstrap.
- n_jobs
int, default=1 Number of parallel jobs for group-time estimation in multi-period settings. 1 = sequential (default), -1 = all cores, >1 = that many workers. Ignored for 2-period data.
- n_partitions
intorNone, default=None Number of Dask partitions per cell. Only used when
datais a Dask DataFrame; ignored for non-Dask inputs.- max_cohorts
intorNone, default=None Maximum number of treatment cohorts to process in parallel when using the Dask distributed backend. Each cohort’s group-time cells are computed concurrently within a thread, so this controls how many cohorts share the cluster simultaneously. Higher values increase throughput but require more memory on workers to hold the per-cohort wide-pivoted DataFrames. When
None, defaults to the number of Dask workers. Ignored for non-Dask inputs.For best performance, set this equal to the total number of treatment cohorts so that all cohorts run concurrently. Reduce the value if the cluster runs out of memory.
- backend{“numpy”, “cupy”} or
None, default=None Array backend to use for this call only. When set, the backend is activated for the duration of this call and reverted automatically when the call returns.
None(the default) uses whatever backend is currently active (seeset_backend). Ignored whendatais a Dask DataFrame.
- data
- Returns:
DDDPanelResult,DDDRCResult,DDDMultiPeriodResult, orDDDMultiPeriodRCResultFor 2-period panel data (panel=True), returns DDDPanelResult containing:
att: The DDD point estimate
se: Standard error
uci, lci: Confidence interval bounds
boots: Bootstrap draws (if requested)
att_inf_func: Influence function
did_atts: Individual DiD ATT estimates
subgroup_counts: Number of units per subgroup
args: Estimation arguments
For 2-period repeated cross-section data (panel=False), returns DDDRCResult with the same structure.
For multi-period panel data, returns DDDMultiPeriodResult containing:
att: Array of ATT(g,t) point estimates
se: Standard errors for each ATT(g,t)
uci, lci: Confidence interval bounds
groups, times: Treatment cohort and time for each estimate
glist, tlist: Unique cohorts and periods
inf_func_mat: Influence function matrix
n: Number of units
args: Estimation arguments
For multi-period repeated cross-section data, returns DDDMultiPeriodRCResult with the same structure.
See also
ddd_panelTwo-period DDD estimator for panel data.
ddd_rcTwo-period DDD estimator for repeated cross-section data.
ddd_mpMulti-period DDD estimator for staggered adoption with panel data.
ddd_mp_rcMulti-period DDD estimator for staggered adoption with RCS data.
agg_dddAggregate group-time DDD effects.
Notes
The DDD estimator identifies treatment effects in settings where units must satisfy two criteria to be treated: belonging to a group that enables treatment (e.g., a state that passes a policy) and being in an eligible partition (e.g., women eligible for maternity benefits). This allows for violations of standard DiD parallel trends assumptions, as long as these violations are stable across groups.
When
est_method="dr"(the default), the function implements doubly robust DDD estimators that combine outcome regression and inverse probability weighting. These estimators are consistent if either the outcome model or the propensity score model is correctly specified.References
[1]Ortiz-Villavicencio, M., & Sant’Anna, P. H. C. (2025). Better Understanding Triple Differences Estimators. arXiv preprint arXiv:2505.09942. https://arxiv.org/abs/2505.09942
Examples
We can generate synthetic data for a 2-period DDD setup using the
gen_ddd_2periodsfunction. The data contains treatment status (state), eligibility within treatment groups (partition), and covariates.In [1]: import numpy as np ...: from moderndid import ddd, gen_ddd_2periods ...: ...: dgp = gen_ddd_2periods(n=1000, dgp_type=1, random_state=42) ...: df = dgp["data"] ...: df.head() ...: Out[1]: shape: (5, 10) ┌─────┬───────┬───────────┬──────┬───┬───────────┬───────────┬───────────┬─────────┐ │ id ┆ state ┆ partition ┆ time ┆ … ┆ cov2 ┆ cov3 ┆ cov4 ┆ cluster │ │ --- ┆ --- ┆ --- ┆ --- ┆ ┆ --- ┆ --- ┆ --- ┆ --- │ │ i64 ┆ i64 ┆ i64 ┆ i64 ┆ ┆ f64 ┆ f64 ┆ f64 ┆ i64 │ ╞═════╪═══════╪═══════════╪══════╪═══╪═══════════╪═══════════╪═══════════╪═════════╡ │ 1 ┆ 0 ┆ 0 ┆ 1 ┆ … ┆ -0.046451 ┆ -0.196832 ┆ 1.104606 ┆ 7 │ │ 1 ┆ 0 ┆ 0 ┆ 2 ┆ … ┆ -0.046451 ┆ -0.196832 ┆ 1.104606 ┆ 7 │ │ 2 ┆ 0 ┆ 1 ┆ 1 ┆ … ┆ -0.994815 ┆ 0.638861 ┆ -0.281919 ┆ 50 │ │ 2 ┆ 0 ┆ 1 ┆ 2 ┆ … ┆ -0.994815 ┆ 0.638861 ┆ -0.281919 ┆ 50 │ │ 3 ┆ 1 ┆ 0 ┆ 1 ┆ … ┆ -0.245423 ┆ 0.258435 ┆ 2.013509 ┆ 25 │ └─────┴───────┴───────────┴──────┴───┴───────────┴───────────┴───────────┴─────────┘
Now we can compute the DDD estimate using the doubly robust estimator. The
pnameparameter identifies which units within a treatment group are eligible to receive treatment, which is the key distinction from standard DiD.In [2]: result = ddd( ...: data=df, ...: yname="y", ...: tname="time", ...: idname="id", ...: gname="state", ...: pname="partition", ...: xformla="~ cov1 + cov2 + cov3 + cov4", ...: est_method="dr", ...: ) ...: result ...: Out[2]: ============================================================================== Triple Difference-in-Differences (DDD) Estimation ============================================================================== DR-DDD estimation for the ATT: ┌────────┬────────────┬──────────┬────────────────────────┐ │ ATT │ Std. Error │ Pr(>|t|) │ [95% Conf. Interval] │ ├────────┼────────────┼──────────┼────────────────────────┤ │ 0.3940 │ 0.1915 │ 0.0397 │ [ 0.0185, 0.7694] * │ └────────┴────────────┴──────────┴────────────────────────┘ ------------------------------------------------------------------------------ Signif. codes: '*' confidence interval does not cover 0 ------------------------------------------------------------------------------ Data Info ------------------------------------------------------------------------------ Panel Data: 2 periods Outcome variable: y Qualification variable: partition No. of units at each subgroup: treated-and-eligible: 224 treated-but-ineligible: 256 eligible-but-untreated: 258 untreated-and-ineligible: 262 ------------------------------------------------------------------------------ Estimation Details ------------------------------------------------------------------------------ Outcome regression: OLS Propensity score: Logistic regression (MLE) ------------------------------------------------------------------------------ Inference ------------------------------------------------------------------------------ Significance level: 0.05 Analytical standard errors ============================================================================== See Ortiz-Villavicencio and Sant'Anna (2025) for details.
The function automatically detects multi-period data with staggered treatment adoption. When there are more than two time periods or treatment cohorts, it returns group-time ATT estimates that can be aggregated using
agg_ddd.In [3]: from moderndid import gen_ddd_mult_periods ...: ...: dgp_mp = gen_ddd_mult_periods(n=500, dgp_type=1, random_state=42) ...: result_mp = ddd( ...: data=dgp_mp["data"], ...: yname="y", ...: tname="time", ...: idname="id", ...: gname="group", ...: pname="partition", ...: control_group="nevertreated", ...: base_period="varying", ...: est_method="dr", ...: ) ...: result_mp ...: Out[3]: ============================================================================== Triple Difference-in-Differences (DDD) Estimation Multi-Period / Staggered Treatment Adoption ============================================================================== DR-DDD estimation for ATT(g,t): ┌───────┬──────┬──────────┬────────────┬───────────────────────┐ │ Group │ Time │ ATT(g,t) │ Std. Error │ [95% Conf. Int.] │ ├───────┼──────┼──────────┼────────────┼───────────────────────┤ │ 2 │ 2 │ -6.5255 │ 10.4750 │ [-27.0561, 14.0050] │ │ 2 │ 3 │ -14.0093 │ 20.9401 │ [-55.0512, 27.0325] │ │ 3 │ 2 │ 0.9262 │ 10.8543 │ [-20.3478, 22.2001] │ │ 3 │ 3 │ 25.0984 │ 10.8614 │ [ 3.8105, 46.3864] * │ └───────┴──────┴──────────┴────────────┴───────────────────────┘ ------------------------------------------------------------------------------ Signif. codes: '*' confidence interval does not cover 0 ------------------------------------------------------------------------------ Data Info ------------------------------------------------------------------------------ Panel Data Outcome variable: y Qualification variable: partition Control group: Never Treated Base period: varying No. of units per treatment group: Units never enabling treatment: 97 Units enabling treatment at period 2: 173 Units enabling treatment at period 3: 230 ------------------------------------------------------------------------------ Estimation Details ------------------------------------------------------------------------------ Outcome regression: OLS Propensity score: Logistic regression (MLE) ------------------------------------------------------------------------------ Inference ------------------------------------------------------------------------------ Significance level: 0.05 Analytical standard errors ============================================================================== See Ortiz-Villavicencio and Sant'Anna (2025) for details.
The function also supports repeated cross-section data where different units are sampled in each time period. Set
panel=Falseto use this mode.In [4]: dgp_rcs = gen_ddd_2periods(n=2000, dgp_type=1, panel=False, random_state=42) ...: result_rcs = ddd( ...: data=dgp_rcs["data"], ...: yname="y", ...: tname="time", ...: gname="state", ...: pname="partition", ...: xformla="~ cov1 + cov2 + cov3 + cov4", ...: est_method="dr", ...: panel=False, ...: ) ...: result_rcs ...: Out[4]: ============================================================================== Triple Difference-in-Differences (DDD) Estimation Repeated Cross-Section Data ============================================================================== DR-DDD estimation for the ATT: ┌─────────┬────────────┬──────────┬────────────────────────┐ │ ATT │ Std. Error │ Pr(>|t|) │ [95% Conf. Interval] │ ├─────────┼────────────┼──────────┼────────────────────────┤ │ -0.0443 │ 0.1860 │ 0.8116 │ [ -0.4089, 0.3202] │ └─────────┴────────────┴──────────┴────────────────────────┘ ------------------------------------------------------------------------------ Signif. codes: '*' confidence interval does not cover 0 ------------------------------------------------------------------------------ Data Info ------------------------------------------------------------------------------ Repeated Cross-Section Data: 2 periods Outcome variable: y Qualification variable: partition No. of observations at each subgroup: treated-and-eligible: 944 treated-but-ineligible: 1020 eligible-but-untreated: 1004 untreated-and-ineligible: 1032 ------------------------------------------------------------------------------ Estimation Details ------------------------------------------------------------------------------ Outcome regression: OLS (4 cell-specific models) Propensity score: Logistic regression (MLE) ------------------------------------------------------------------------------ Inference ------------------------------------------------------------------------------ Significance level: 0.05 Analytical standard errors ============================================================================== See Ortiz-Villavicencio and Sant'Anna (2025) for details.
For multi-period repeated cross-section data with staggered treatment adoption, set
panel=Falsewith multiple time periods.In [5]: dgp_mp_rcs = gen_ddd_mult_periods(n=500, dgp_type=1, panel=False, random_state=42) ...: result_mp_rcs = ddd( ...: data=dgp_mp_rcs["data"], ...: yname="y", ...: tname="time", ...: gname="group", ...: pname="partition", ...: control_group="notyettreated", ...: base_period="universal", ...: est_method="dr", ...: panel=False, ...: ) ...: result_mp_rcs ...: Out[5]: ============================================================================== Triple Difference-in-Differences (DDD) Estimation Multi-Period / Staggered Treatment Adoption (Repeated Cross-Section) ============================================================================== DR-DDD estimation for ATT(g,t): ┌───────┬──────┬───────────┬────────────┬─────────────────────────┐ │ Group │ Time │ ATT(g,t) │ Std. Error │ [95% Conf. Int.] │ ├───────┼──────┼───────────┼────────────┼─────────────────────────┤ │ 2 │ 1 │ 0.0000 │ NA │ NA │ │ 2 │ 2 │ -53.4664 │ 42.1653 │ [-136.1089, 29.1762] │ │ 2 │ 3 │ -152.9206 │ 62.0955 │ [-274.6256, -31.2156] * │ │ 3 │ 1 │ 46.8342 │ 54.0600 │ [-59.1214, 152.7899] │ │ 3 │ 2 │ 0.0000 │ NA │ NA │ │ 3 │ 3 │ 17.6324 │ 66.0984 │ [-111.9181, 147.1829] │ └───────┴──────┴───────────┴────────────┴─────────────────────────┘ ------------------------------------------------------------------------------ Signif. codes: '*' confidence interval does not cover 0 ------------------------------------------------------------------------------ Data Info ------------------------------------------------------------------------------ Repeated Cross-Section Data Outcome variable: y Qualification variable: partition Control group: Not Yet Treated (GMM-based) Base period: universal Number of observations: 1500 Time periods: 3 (1 to 3) Treatment cohorts: 2 ------------------------------------------------------------------------------ Estimation Details ------------------------------------------------------------------------------ Outcome regression: OLS (4 cell-specific models per comparison) Propensity score: Logistic regression (MLE) ------------------------------------------------------------------------------ Inference ------------------------------------------------------------------------------ Significance level: 0.05 Analytical standard errors ============================================================================== See Ortiz-Villavicencio and Sant'Anna (2025) for details.