Getting started with nonabsdid

library(nonabsdid)

nonabsdid provides three things on top of the heterogeneity-robust DiD estimators that already exist in R:

  1. A unified front door, nabs_event_study(), that runs DCDH, PanelMatch, or any of the fect family (FE / IFE / MC) with a common argument set.
  2. An S3 generic, as_nabs_event_study(), that converts each estimator’s native output into a tidy tibble with a stable schema.
  3. A plotting function, 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.

A finished plot looks like this — several heterogeneity-robust estimators and the naive TWFE reference on one panel:

Comparison of heterogeneity-robust estimators vs naive TWFE

The 30-second version

res <- nabs_event_study_simple(sim,
                               outcome = "y", treatment = "d",
                               unit = "id", time = "t")
res$plot

That’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.

A simulated panel

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

Three estimators, three lines of code

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.

Or call the estimators directly

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)

Naive TWFE reference

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 the leads and lags of the treatment (a distributed-lag specification in levels), defined relative to a treatment change rather than to a single absorbing onset, 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.

Plot

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

Default prepost_color overlay of three estimators vs naive TWFE

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.

Plot styles

nabs_event_plot() offers two ways to encode the pre/post distinction, plus an option to join point estimates with a thin line. With style = "method_shape", color encodes the method only and the pre/post split is carried by marker shape (hollow circles pre, filled triangles post), which reads cleanly in grayscale:

method_shape style: color by method, shape by pre/post

nabs_event_plot(res_dcdh$tidy, res_pm$tidy, res_ife$tidy, reference = ref,
                style = "method_shape")

Set connect = TRUE (works with either style) to join each series’ point estimates with a thin line through the full path:

method_shape style with connected point estimates

nabs_event_plot(res_dcdh$tidy, res_pm$tidy, res_ife$tidy, reference = ref,
                style = "method_shape", connect = TRUE)

Schema

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.