nonabsdid provides three things on top of the
heterogeneity-robust DiD estimators that already exist in R:
nabs_event_study(), that runs
DCDH, PanelMatch, or any of the fect family (FE / IFE / MC)
with a common argument set.as_nabs_event_study(), that converts
each estimator’s native output into a tidy tibble with a stable
schema.nabs_event_plot(), that overlays
any combination of those tidy objects on a single ggplot2 panel, with an
optional naive TWFE reference series.Plus a one-line shortcut, nabs_event_study_simple(),
that runs all three heterogeneity-robust estimators with sensible
defaults and gives you a single overlay plot — meant for the very first
look at the data.
The estimator packages themselves (DIDmultiplegtDYN,
PanelMatch, fect, fixest) are
suggested, not required, so install only the ones you
plan to use.
res <- nabs_event_study_simple(sim,
outcome = "y", treatment = "d",
unit = "id", time = "t")
res$plotThat’s it. Window lengths are auto-picked from the panel. All available heterogeneity-robust estimators run (skipped silently if their package isn’t installed), the naive TWFE reference is overlaid in grey, and you get a single figure to inspect.
For everything below — picking specific estimators, wider control over options, comparing FE vs IFE vs MC — read on.
We simulate a panel with non-absorbing treatment so that all three
estimators have something to chew on. Half of the units turn on at
t = 10 and half of those turn back off at
t = 16.
set.seed(2026)
N <- 80; TT <- 24
sim <- expand.grid(id = seq_len(N), t = seq_len(TT))
# Treatment switches on at t=10 for ids <= N/2, and off at t=16 for ids <= N/4.
sim$d <- with(sim, as.integer(
(id <= N/2 & t >= 10 & !(id <= N/4 & t >= 16))
))
# Heterogeneous, time-varying effect: 1 for early, 0.5 for late.
sim$tau <- with(sim, ifelse(id <= N/4, 1.0, 0.5))
sim$y <- with(sim, 0.05 * id + 0.03 * t + d * tau + rnorm(nrow(sim), sd = 0.3))res_dcdh <- nabs_event_study(sim, outcome = "y", treatment = "d",
unit = "id", time = "t",
method = "DCDH", lags = 4, leads = 6)
res_pm <- nabs_event_study(sim, outcome = "y", treatment = "d",
unit = "id", time = "t",
method = "PanelMatch", lags = 4, leads = 6)
res_ife <- nabs_event_study(sim, outcome = "y", treatment = "d",
unit = "id", time = "t",
method = "IFE")Each nabs_event_study() return is a list with
tidy (an nabs_event_study_tbl),
fit (the native estimator object, for diagnostics), and
call.
If you want full control of estimator-specific options, call the underlying package and tidy the result:
fit <- DIDmultiplegtDYN::did_multiplegt_dyn(
df = sim, outcome = "y", group = "id", time = "t",
treatment = "d", effects = 6, placebo = 4,
cluster = "id"
)
tidy_dcdh <- as_nabs_event_study(fit, outcome = "y")For PanelMatch, remember to pass the placebo result via
pre_obj:
panel <- PanelMatch::PanelData(sim, "id", "t", "d", "y")
pm <- PanelMatch::PanelMatch(panel.data = panel, lag = 4, lead = 0:6,
refinement.method = "ps.match",
size.match = 10, qoi = "att",
placebo.test = TRUE,
forbid.treatment.reversal = FALSE)
pe <- PanelMatch::PanelEstimate(pm, panel.data = panel)
pl <- PanelMatch::placebo_test(pm, panel.data = panel, plot = FALSE)
tidy_pm <- as_nabs_event_study(pe, pre_obj = pl)For a visual baseline, fit a naive TWFE event study with
fixest:
ref <- naive_twfe(sim, outcome = "y", treatment = "d",
unit = "id", time = "t",
lags = 4, leads = 6)The reference is built from treatment changes (a distributed-lag specification), so it handles the on/off treatment simulated above rather than assuming a single absorbing onset. It is still intended only as a reference: it is not robust to treatment-effect heterogeneity – which is the point. Showing it next to the robust estimators makes the correction visible.
nabs_event_plot(
res_dcdh$tidy, res_pm$tidy, res_ife$tidy,
reference = ref,
xlim = c(-4, 6),
ylim = c(-1, 2),
ylab = "Effect on y"
)The reference series is drawn in grey20 (configurable
via reference_color) and dashed, so it sits visually behind
the main estimators. Pre-treatment periods get round markers;
post-treatment get triangles. Each method gets its own color pair; the
default palette is patterned after the conventions in applied
DCDH/PanelMatch papers, with red shades pre and blue/green post.
Every tidier returns a tibble with the same columns:
| column | type | description |
|---|---|---|
time |
int | Relative period (0 = treatment onset). |
estimate |
num | Point estimate. |
std.error |
num | Standard error (may be NA). |
conf.low |
num | Lower CI bound. |
conf.high |
num | Upper CI bound. |
window |
chr | "pre" if time < 0, else
"post". |
method |
chr | "DCDH", "PanelMatch", "IFE",
"TWFE", or custom. |
outcome |
chr | Outcome variable name. |
Anything you can coerce to a data frame with at least
time and estimate columns can also flow
through as_nabs_event_study(). This makes it easy to plug
in additional estimators later – write a one-line method that pulls the
right slots, and the plotting code keeps working.