The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.

fixes

CRAN status R-CMD-check

Overview

fixes is an R package for difference-in-differences estimation in panel data. It covers the three stages of a modern DiD workflow:

Stage Function What it does
1. Event study run_es() Dynamic treatment effects by relative time (6 estimators)
2. ATT aggregation calc_att() Aggregated ATT — overall, by cohort, by calendar time
3. Basic DiD run_did() Single-coefficient TWFE DiD with modelsummary support
4. Sensitivity honest_sensitivity() Robust inference under violations of parallel trends (Rambachan & Roth 2023)
Visualisation plot_es() Static ggplot2 event study plot
Visualisation plot_att_gt() ATT(g,t) heatmap / facet plot (CS estimator)
Visualisation plot_es_interactive() Interactive plotly plot with hover tooltips
Visualisation plot_honest() Sensitivity plot of robust CIs vs. restriction size

Estimators (selected via the estimator argument in run_es() and calc_att()):

estimator Reference Best for
"twfe" Classic TWFE Universal treatment timing
"cs" Callaway & Sant’Anna (2021) Staggered adoption
"sa" Sun & Abraham (2021) Staggered adoption
"bjs" Borusyak, Jaravel & Spiess (2024) Staggered adoption
"twm" Wooldridge (2025) Staggered adoption; optional cohort trends
"flex" Deb, Norton, Wooldridge & Zabel (2024) Repeated cross-section data

Installation

# From CRAN
install.packages("fixes")

# Development version
pak::pak("yo5uke/fixes")

Quick start

library(fixes)

Basic DiD — run_did()

For a simple two-way FE DiD with a single treatment coefficient, use run_did(). Output is fully compatible with modelsummary::modelsummary() and tinytable::tt().

There are two equivalent ways to specify the treatment:

# Option A: supply a pre-built D_it indicator
df$D <- as.integer(df$treated & df$year >= 2006)
res <- run_did(df, outcome = y, treatment = D, fe = ~ id + year)
# Option B: let run_did() construct D_it from group indicator + timing
res <- run_did(df, outcome = y, treatment = treated,
               time = year, timing = 2006,
               fe = ~ id + year)

Both options produce a did_result object:

df <- fixest::base_did

# Build a universal-timing DiD dataset
df$D <- as.integer(df$treat == 1 & df$period >= 5)

res <- run_did(
  data    = df,
  outcome = y,
  treatment = D,
  fe      = ~ id + period,
  cluster = ~ id
)

print(res)
## DiD Estimation  [TWFE]
## N = 1080 obs | 330 treated obs
## FE: id + period 
## VCOV: cluster | Cluster: id 
## 
##   term estimate std.error statistic  p.value
## 1    D      4.5     0.544      8.27 3.94e-13

run_did() integrates with the broom and modelsummary ecosystems:

broom::tidy(res)          # all coefficients (treatment + any covariates)
broom::glance(res)        # nobs, within R², AIC, ...
modelsummary::modelsummary(res)   # regression table via tinytable

Event study — run_es()

All six estimators share the same interface.

Classic TWFE (single treatment date)

Use run_es() with a fixed event date. Here we use fixest::base_did, a balanced panel where all units are treated at period 5.

es <- run_es(
  data      = df,
  outcome   = y,
  treatment = treat,
  time      = period,
  timing    = 5,
  fe        = ~ id + period,
  cluster   = ~ id,
  baseline  = -1
)

print(es)
## Event Study Result (fixes)
##   N: 1080  | Units: NA  | Treated units: 1080  | Never-treated: NA 
##   FE: id + period
##   VCOV: cluster  | Cluster: id 
##   Method: classic  | lead_range: 4  lag_range: 5  baseline: -1
plot_es(es)


Staggered adoption

When units adopt treatment at different times, the classic TWFE estimator can be biased. fixes provides modern alternatives.

Setup: fixest::base_stagg — never-treated units have NA timing.

df_stagg <- fixest::base_stagg
df_stagg$timing <- df_stagg$year_treated
df_stagg$timing[df_stagg$year_treated == 10000] <- NA

Callaway & Sant’Anna (2021) — estimator = "cs"

cs <- run_es(
  data          = df_stagg,
  outcome       = y,
  time          = year,
  timing        = timing,
  unit          = id,
  staggered     = TRUE,
  estimator     = "cs",
  control_group = "nevertreated"
)

print(cs)
## Event Study Result (fixes)
##   N: 950  | Units: 95  | Treated units: 45  | Never-treated: 50 
##   FE: 
##   VCOV: analytic  | Cluster: - 
##   Method: classic  | lead_range: 9  lag_range: 8  baseline: -1
plot_es(cs)

Visualise the full ATT(g,t) matrix

plot_att_gt(cs, type = "heatmap")

plot_att_gt(cs, type = "facet")


Sun & Abraham (2021) — estimator = "sa"

sa <- run_es(
  data      = df_stagg,
  outcome   = y,
  treatment = treated,
  time      = year,
  timing    = timing,
  unit      = id,
  fe        = ~ id + year,
  staggered = TRUE,
  estimator = "sa",
  cluster   = ~ id
)

print(sa)
## Event Study Result (fixes)
##   N: 950  | Units: 95  | Treated units: 45  | Never-treated: 50 
##   FE: id + year
##   VCOV: HC1  | Cluster: id 
##   Method: classic  | lead_range: 9  lag_range: 8  baseline: -1
plot_es(sa)


Borusyak, Jaravel & Spiess (2024) — estimator = "bjs"

bjs <- run_es(
  data      = df_stagg,
  outcome   = y,
  time      = year,
  timing    = timing,
  unit      = id,
  staggered = TRUE,
  estimator = "bjs"
)

print(bjs)
## Event Study Result (fixes)
##   N: 950  | Units: 95  | Treated units: 45  | Never-treated: 50 
##   FE: id + year
##   VCOV: bjs_conservative  | Cluster: - 
##   Method: classic  | lead_range: 1  lag_range: 8  baseline: -1
plot_es(bjs)


Wooldridge (2025) Two-Way Mundlak — estimator = "twm"

Algebraically equivalent to Sun-Abraham in the base case. trends = TRUE adds cohort-specific linear trend regressors to absorb differential pre-trends (output shows relative_time ≥ 0 only).

twm <- run_es(
  data      = df_stagg,
  outcome   = y,
  time      = year,
  timing    = timing,
  unit      = id,
  fe        = ~ id + year,
  staggered = TRUE,
  estimator = "twm"
)

print(twm)
## Event Study Result (fixes)
##   N: 950  | Units: 95  | Treated units: 45  | Never-treated: 50 
##   FE: id + year
##   VCOV: HC1  | Cluster: - 
##   Method: classic  | lead_range: 9  lag_range: 8  baseline: -1
plot_es(twm)


Deb, Norton, Wooldridge & Zabel (2024) FLEX — estimator = "flex"

Designed for repeated cross-section data (different individuals each period). Requires a group argument identifying the treatment group each observation belongs to.

flex <- run_es(
  data      = df_rcs,
  outcome   = y,
  time      = year,
  timing    = timing,
  group     = group_id,
  staggered = TRUE,
  estimator = "flex"
)

plot_es(flex)

ATT aggregation — calc_att()

After estimating run_es() with a staggered estimator, calc_att() computes a single aggregated ATT — or one per cohort / per calendar period.

# Overall ATT
att_simple <- calc_att(
  data        = df_stagg,
  outcome     = y,
  time        = year,
  timing      = timing,
  unit        = id,
  estimator   = "cs",
  aggregation = "simple"
)

print(att_simple)
## ATT Estimation  [estimator: CS | aggregation: Simple (overall)]
## N = 950 obs | 95 units | 45 treated
## 
##   group estimate std.error statistic  p.value conf_low_95 conf_high_95
## 1    NA   -0.755     0.226     -3.35 0.000813        -1.2       -0.313
# Per-cohort ATT
calc_att(df_stagg, y, year, timing, unit = id,
         estimator = "cs", aggregation = "by_cohort")

# Per-calendar-period ATT
calc_att(df_stagg, y, year, timing, unit = id,
         estimator = "cs", aggregation = "by_time")

Supported estimators for calc_att(): "cs" (Callaway-Sant’Anna 2021) and "bjs" (Borusyak et al. 2024).


Bootstrap simultaneous confidence bands

Pointwise CIs control error rates one period at a time. When you plot 15 pre- and post-treatment estimates, the joint false-positive rate may exceed 5 %. Simultaneous bands (Callaway & Sant’Anna 2021, Corollary 1) provide joint coverage across the entire event-study curve.

cs_boot <- run_es(
  data          = df_stagg,
  outcome       = y,
  time          = year,
  timing        = timing,
  unit          = id,
  staggered     = TRUE,
  estimator     = "cs",
  control_group = "nevertreated",
  bootstrap     = TRUE,
  B             = 999,
  boot_seed     = 42
)
# Lighter outer band = simultaneous CI; darker inner band = pointwise CI
plot_es(cs_boot, show_simultaneous = TRUE)
plot_es_interactive(cs_boot, show_simultaneous = TRUE)

Honest sensitivity analysis — honest_sensitivity()

Event-study estimates rely on the parallel trends assumption. Rather than testing pre-trends and hoping for the best, honest_sensitivity() implements Rambachan & Roth (2023): it reports confidence sets for a post-treatment effect under progressively weaker restrictions on how different post-treatment violations of parallel trends can be from the pre-trends, plus a breakdown value — the largest violation at which the effect is still significant.

res <- run_es(df, outcome = y, treatment = treat, time = year, timing = 6,
              fe = ~ id + year)

# Relative-magnitude restriction: post violation <= Mbar x max pre violation
h <- honest_sensitivity(res, type = "relative_magnitude",
                        Mvec = c(0, 0.5, 1, 1.5, 2))
print(h)          # robust CIs per Mbar + the original (parallel-trends) CI
plot_honest(h)    # "top-down" sensitivity plot

Use type = "smoothness" for the (bounded second difference) restriction. Inference uses the Andrews-Roth-Pakes conditional test — a pure-R reimplementation validated against the HonestDiD package. For estimators other than "twfe", pass betahat and sigma directly. The numeric helpers (lpSolveAPI, Rglpk, TruncatedNormal, Matrix, pracma) are optional Suggests.


Plotting options

plot_es() works with results from any estimator.

plot_es(es, type = "errorbar")

es_multi <- run_es(
  data      = df,
  outcome   = y,
  treatment = treat,
  time      = period,
  timing    = 5,
  fe        = ~ id + period,
  cluster   = ~ id,
  conf.level = c(0.90, 0.95, 0.99)
)
plot_es(es_multi, ci_level = 0.90, theme_style = "minimal")

Interactive plots

plot_es_interactive(es)

Key arguments

run_did()

Argument Default Description
data Data frame (panel)
outcome Outcome variable (unquoted; expressions like log(y) OK)
treatment Binary D_it indicator, or group dummy when timing is set
timing NULL Scalar treatment period; auto-constructs D_it = treatment*(time>=timing)
fe NULL FE formula ~ id + year; auto-inferred from unit + time if omitted
unit NULL Unit identifier (for FE inference and sample-size metadata)
time NULL Time variable (for FE inference and timing-based D_it construction)
covariates NULL Additional controls, e.g. ~ x1 + x2
cluster NULL Clustering: formula ~ id, column name, or vector
conf.level 0.95 CI level(s); vector allowed
vcov "HC1" VCOV type; cluster-robust SE used automatically when cluster is set

run_es()

Argument Default Description
data Data frame (panel or RCS)
outcome Outcome variable (unquoted)
treatment NULL 0/1 treatment dummy ("twfe" only)
time Time variable (numeric)
timing Treatment date (scalar for "twfe", column for others; NA = never treated)
unit NULL Unit ID (required for "cs", "sa", "bjs", "twm")
fe NULL Fixed effects formula, e.g. ~ id + year
estimator "twfe" "twfe", "cs", "sa", "bjs", "twm", or "flex"
staggered FALSE Set TRUE for unit-varying treatment timing
group NULL FLEX only: treatment group identifier
trends FALSE TWM only: cohort-specific linear trends
covariates NULL Controls (supported for "twm" and "flex")
control_group "nevertreated" CS only: "nevertreated" or "notyettreated"
cluster NULL Clustering formula, e.g. ~ id
baseline -1 Reference period
conf.level 0.95 CI level(s); vector allowed
vcov "HC1" VCOV type
bootstrap FALSE CS only: multiplier bootstrap for simultaneous CIs
B 999 Bootstrap draws
boot_seed NULL Bootstrap RNG seed

calc_att()

Argument Default Description
data Data frame (panel)
outcome Outcome variable (unquoted)
time Calendar time variable
timing First treatment period per unit; NA = never treated
unit Unit identifier (required)
estimator "cs" "cs" or "bjs"
aggregation "simple" "simple", "by_cohort", or "by_time"
control_group "nevertreated" CS only
conf.level 0.95 CI level(s)

Contributing

Found a bug or have a feature request? Open an issue on GitHub.

These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.