cdid estimates
group-time average treatment effects (att_gt()) and their
aggregations (aggte()) in panel data with missing outcomes
using the chained difference-in-differences of Bellégo, Benatia and
Dortet-Bernadet (2025) (https://doi.org/10.1016/j.jeconom.2024.105783). It
builds on the did interface of Callaway and Sant’Anna
(2021) and uses chained estimation by default for panel data
(mode = "chained"), while mode = "ordinary"
delegates unchanged to did.
We simulate a self-contained long panel in base R with \(N = 900\) units across \(T = 5\) periods (1 to
5), a binary covariate x, a never-treated
control group (g = 0), and a cohort first treated in period
3 (g = 3). Treatment cohort membership depends
on x through a binary logit, and the latent outcome
combines a unit fixed effect \(\alpha_i\), a common trend plus an
x-specific trend, independent normal noise, and a
homogeneous post-treatment effect \(1 + 0.25(t
- 3)\) for \(g = 3\) at \(t \ge 3\) (population target values
0 at pre-treatment period 2, and
1.00, 1.25, 1.50 at periods
3, 4, 5, with post-treatment
average 1.25).
Each unit is randomly assigned, independently of (g, x)
and outcome shocks, to one of four consecutive observation windows
{1, 2}, {2, 3}, {3, 4}, or
{4, 5}. Every unit therefore has two observed outcomes and
three NA outcomes, and no unit is observed at all five
dates. We retain all long panel rows (with y = NA outside
the unit’s window) and pass the full unit table units as
frame.
library(cdid)
set.seed(20261029)
n_units <- 900
n_periods <- 5
units <- data.frame(
id = seq_len(n_units),
x = rbinom(n_units, size = 1, prob = 0.5)
)
units$g <- ifelse(
runif(n_units) < plogis(-0.2 + 0.4 * units$x),
3,
0
)
unit_alpha <- rnorm(n_units)
window_start <- sample.int(n_periods - 1L, size = n_units, replace = TRUE)
panel_data <- data.frame(
id = rep(units$id, each = n_periods),
time = rep(seq_len(n_periods), times = n_units),
g = rep(units$g, each = n_periods),
x = rep(units$x, each = n_periods)
)
treated_post <- panel_data$g == 3 & panel_data$time >= 3
true_effect <- ifelse(treated_post, 1 + 0.25 * (panel_data$time - 3), 0)
y_latent <- rep(unit_alpha, each = n_periods) +
(0.4 + 0.2 * panel_data$x) * panel_data$time +
true_effect +
rnorm(nrow(panel_data), sd = 0.5)
in_window <- panel_data$time == rep(window_start, each = n_periods) |
panel_data$time == rep(window_start + 1L, each = n_periods)
panel_data$y <- ifelse(in_window, y_latent, NA_real_)We verify that (id, time) pairs are unique and count
observed outcomes per unit across the unit table units:
In chained mode, data must have unique
(idname, tname) rows, finite numeric calendar dates
tname, numeric outcomes yname that are finite
when observed and kept as NA when missing (never replaced
by 0), and a constant known treatment cohort
gname per unit (0 for never treated). No unit
is required to have a complete history or an observed outcome at the
cohort baseline date (\(t = 2\)). The
unit table frame defines the study population \(F\) of size \(N =
|F|\): units observed at two or more dates (\(A\)) supply within-unit changes \(\Delta Y_{s,t} = Y_t - Y_s\), while
singletons (one observed outcome) and units with no observed outcomes
supply no differences but remain in \(F\) for population counts, cohort shares,
active selection or imputation models, and influence functions for all
study units.
With the default selection = "none", no
observation-probability or imputation model is fitted. Instead, for each
cohort and each date pair \((s, t)\)
used by the estimator, the subset of units observed at both \(s\) and \(t\) is assumed representative of its cohort
in the study population with respect to the mean outcome change: \[E[Y_t - Y_s \mid G = g, S_{s,t} = 1] = E[Y_t -
Y_s \mid G = g].\] This is a population expectation condition in
the study population; it does not require individual units to have
identical outcome changes, realized sample means to agree across
observation windows, or arbitrary missingness patterns to identify every
target. Observation may depend on the time-invariant unit fixed effect
\(\alpha_i\) removed by within-unit
differencing, provided it does not select on the mean change.
Identification and inference also require: no treatment anticipation
before \(g\)
(anticipation = 0, so \(t =
2\) is cohort 3’s reference date); conditional parallel trends
from the reference period (pt_assumption = "post"); overlap
in the active comparison risk sets; for doubly robust estimation
(est_method = "dr"), either a correctly specified treatment
propensity logit or a correctly specified control outcome-change
regression given xformla; and catalog support connecting
each target date to the reference date with eligible controls.
In this design, the untreated conditional mean change from \(s\) to \(t\) is \((t -
s)(0.4 + 0.2x)\) in both cohorts, so conditional parallel trends
and the control regressions hold under either outcome-pooling choice. By
default, outcome_pooling = "separate" lets each comparison
have its own outcome-regression slopes. Set
outcome_pooling = "pooled" to share slopes per calendar
step when that restriction is credible. The treatment logit is also
correctly specified by ~ x, and the independently assigned
observation windows preserve the cohort mean changes in population.
We fit chained doubly robust estimation with
xformla = ~ x and selection = "none",
combining the overlapping adjacent-period comparisons by feasible
two-step GMM (gmm_weighting = "optimal"). Here we set
bstrap = FALSE and cband = FALSE to compute
fast analytical standard errors and pointwise normal confidence
intervals; by default (bstrap = TRUE, cband = TRUE),
cdid computes multiplier-bootstrap standard errors and
simultaneous confidence bands.
fit <- cdid::att_gt(
yname = "y",
tname = "time",
idname = "id",
gname = "g",
xformla = ~ x,
data = panel_data,
frame = units,
control_group = "nevertreated",
est_method = "dr",
mode = "chained",
selection = "none",
pt_assumption = "post",
bstrap = FALSE,
cband = FALSE
)
summary(fit)
#>
#> cdid chained difference-in-differences (chained doubly robust GMM): cdid_MP
#> cdid 1.2.0.1 result built on did 2.5.1 (tested: did 2.5.1); did's MP methods tabulate the estimates below.
#> Chained estimators: Bellego C., Benatia D. and V. Dortet-Bernadet (2025). "The Chained Difference-in-Differences." Journal of Econometrics 248, 105783. doi:10.1016/j.jeconom.2024.105783.
#> Ordinary estimators: Callaway B. and P. H. C. Sant'Anna (2021). "Difference-in-Differences with Multiple Time Periods." Journal of Econometrics 225(2), 200-230. doi:10.1016/j.jeconom.2020.12.001.
#>
#> Call:
#> cdid::att_gt(yname = "y", tname = "time", idname = "id", gname = "g",
#> xformla = ~x, data = panel_data, frame = units, control_group = "nevertreated",
#> est_method = "dr", mode = "chained", selection = "none",
#> pt_assumption = "post", bstrap = FALSE, cband = FALSE)
#>
#> Group-Time Average Treatment Effects:
#> Group Time ATT(g,t) Std. Error [95% Pointwise Conf. Band]
#> 3 2 0.0496 0.0917 -0.1302 0.2293
#> 3 3 0.7934 0.0932 0.6107 0.9761 *
#> 3 4 1.2238 0.1356 0.9579 1.4896 *
#> 3 5 1.4039 0.1659 1.0788 1.7290 *
#> ---
#> Signif. codes: `*' confidence band does not cover 0
#>
#> P-value for pre-test of parallel trends assumption: 0.588958075147391
#> Control Group: Never Treated, Anticipation Periods: 0
#> Estimation Method: Doubly Robust
#>
#> Chained support: 4 of 4 requested cells available.
#> Units: 900 in the unit table, 0 observed at all 5 dates, 900 with at least two observed outcomes.
#> Observation correction: none (selection = "none").
#> Parallel trends: PT-post; base_period = "varying".
#> GMM weighting: optimal (two-step); covariance regularization: not applied (moment covariance well conditioned).
#> Inference: analytic standard errors, unit-level; pointwise intervals.
#> Parallel-trends pre-test: W = 0.291974, df = 1, p = 0.589 (Wald test of the pretrend cell).Compare the estimates with the population targets: 0 for
the pre-treatment comparison reported at period 2, and
1.00, 1.25, 1.50 at periods
3, 4, 5. Realized estimates vary
across samples; one draw does not establish bias, coverage, or relative
efficiency. tidy() exposes cell-level estimates,
availability and support reasons; glance() gives a one-row
fit summary:
cells <- cdid::tidy(fit)
cells[, c("target.id", "role", "estimate", "std.error",
"conf.low", "conf.high", "available",
"support.status", "support.reason")]
#> target.id role estimate std.error conf.low conf.high available
#> 1 pre(g=3,t=2) pre 0.04955105 0.09170237 -0.1301823 0.2292844 TRUE
#> 2 ATT(g=3,t=3) post 0.79343581 0.09322092 0.6107262 0.9761455 TRUE
#> 3 ATT(g=3,t=4) post 1.22375601 0.13563479 0.9579167 1.4895953 TRUE
#> 4 ATT(g=3,t=5) post 1.40388077 0.16587612 1.0787695 1.7289920 TRUE
#> support.status
#> 1 supported_direct_pretrend
#> 2 estimable
#> 3 estimable
#> 4 estimable
#> support.reason
#> 1 direct auxiliary comparison 1->2 with controls {Inf}
#> 2 level anchored to the cohort reference through retained comparison rows (prop:graph)
#> 3 level anchored to the cohort reference through retained comparison rows (prop:graph)
#> 4 level anchored to the cohort reference through retained comparison rows (prop:graph)
cdid::glance(fit)
#> nobs ngroup ntime control.group est.method estimator
#> 1 900 1 5 nevertreated dr chained doubly robust GMM
#> n.requested n.available band.family inference
#> 1 4 4 pointwise_normal_over_supported_ids analytic
#> clustered
#> 1 FALSEcdid::aggte() aggregates the retained
cdid_MP result without refitting and inherits its inference
settings unless overridden. We compute the overall post-treatment
average (type = "simple") and event-time estimates
(type = "dynamic"), inspect the tidy dynamic table, and
plot the event study with cdid::ggdid():
agg_simple <- cdid::aggte(fit, type = "simple")
agg_dynamic <- cdid::aggte(fit, type = "dynamic")
summary(agg_simple)
#>
#> cdid chained difference-in-differences (chained doubly robust GMM): cdid_AGGTEobj
#> cdid 1.2.0.1 result built on did 2.5.1 (tested: did 2.5.1); did's AGGTEobj methods tabulate the estimates below.
#> Chained estimators: Bellego C., Benatia D. and V. Dortet-Bernadet (2025). "The Chained Difference-in-Differences." Journal of Econometrics 248, 105783. doi:10.1016/j.jeconom.2024.105783.
#> Ordinary estimators: Callaway B. and P. H. C. Sant'Anna (2021). "Difference-in-Differences with Multiple Time Periods." Journal of Econometrics 225(2), 200-230. doi:10.1016/j.jeconom.2020.12.001.
#>
#> Call:
#> cdid::aggte(fit, type = "simple")
#>
#> ATT Std. Error [ 95% Conf. Int.]
#> 1.1404 0.1184 0.9083 1.3724 *
#>
#>
#> ---
#> Signif. codes: `*' confidence band does not cover 0
#>
#> Control Group: Never Treated, Anticipation Periods: 0
#> Estimation Method: Doubly Robust
#>
#> Chained simple aggregation: 1 of 1 intended aggregate(s) available; na.rm = FALSE.
#> Parallel trends: PT-post; base_period = "varying".
#> Inference: analytic standard errors, unit-level; pointwise intervals.
cdid::tidy(agg_dynamic)[, c("label", "estimate",
"std.error", "conf.low", "conf.high",
"available", "reason")]
#> label estimate std.error conf.low conf.high available reason
#> 1 event_-1 0.04955105 0.09170237 -0.1301823 0.2292844 TRUE intended_target
#> 2 event_0 0.79343581 0.09322092 0.6107262 0.9761455 TRUE intended_target
#> 3 event_1 1.22375601 0.13563479 0.9579167 1.4895953 TRUE intended_target
#> 4 event_2 1.40388077 0.16587612 1.0787695 1.7289920 TRUE intended_target
cdid::ggdid(agg_dynamic)Beyond the baseline specification above, cdid
supports:
est_method):
"dr" (default), "reg" (separate control
outcome-change regressions by default, with
outcome_pooling = "pooled" available), "ipw",
or custom did-style IPW callbacks.selection = "conditional_mtar"): observation
weighting (selection_est_method = "ipw" with
selection_model = "pair_logit" or
"independent_period_logit"), actual-change imputation
(selection_est_method = "imputation"), or their augmented
combination (selection_est_method = "dr" with treatment
IPW, OR or DR), with configurable slope pooling
(selection_pooling, imputation_pooling),
separate selection formulas (selection_xformla), and subset
fitting universes (selection_universe).pt_assumption = "all" with pt_start),
reference-date covariates and fix_weights, clustered
inference (clustervars), and the chained conditional
pretrend test (cdid::conditional_did_pretest()).See the installed help pages (?cdid::att_gt,
?cdid::aggte, ?cdid::conditional_did_pretest,
?cdid::reexports) and the repository documentation at https://github.com/dvbn/cdid, especially
docs/help/prepare-data.md,
docs/help/investigate-missingness.md,
docs/methods/missing-outcomes.md, and the companion
notebook.