
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 |
# From CRAN
install.packages("fixes")
# Development version
pak::pak("yo5uke/fixes")library(fixes)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 tinytablerun_es()All six estimators share the same interface.
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)
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] <- NAestimator = "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)
plot_att_gt(cs, type = "heatmap")
plot_att_gt(cs, type = "facet")
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)
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)
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)
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)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).
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()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 plotUse 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.
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")
plot_es_interactive(es)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) |
Found a bug or have a feature request? Open an issue on GitHub.