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:
dataDataFrame

Data in long format. Accepts any object implementing the Arrow PyCapsule Interface (__arrow_c_stream__), including polars, pandas, pyarrow Table, and cudf DataFrames.

ynamestr

Name of outcome variable column.

tnamestr

Name of time period column.

idnamestr, 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.

gnamestr

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).

pnamestr

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.

xformlastr, 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.

weightsnamestr, 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.

bitersint, default=1000

Number of bootstrap repetitions (only used if boot=True).

clusterstr, optional

Name of the clustering variable for clustered standard errors. Currently only supported for 2-period data with bootstrap.

alphafloat, default=0.05

Significance level for confidence intervals.

trim_levelfloat, 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_stateint, Generator, optional

Random seed for reproducibility of bootstrap.

n_jobsint, 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_partitionsint or None, default=None

Number of Dask partitions per cell. Only used when data is a Dask DataFrame; ignored for non-Dask inputs.

max_cohortsint or None, 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 (see set_backend). Ignored when data is a Dask DataFrame.

Returns:
DDDPanelResult, DDDRCResult, DDDMultiPeriodResult, or DDDMultiPeriodRCResult

For 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_panel

Two-period DDD estimator for panel data.

ddd_rc

Two-period DDD estimator for repeated cross-section data.

ddd_mp

Multi-period DDD estimator for staggered adoption with panel data.

ddd_mp_rc

Multi-period DDD estimator for staggered adoption with RCS data.

agg_ddd

Aggregate 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_2periods function. 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 pname parameter 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=False to 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=False with 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.