Major release: the package API is reborn with modern noun-style function
names, base plot() methods for every result class, and a fixest-optional
estimation core. All pre-1.0.0 functions keep working (with a deprecation
warning) and return numerically identical results.
event_study(), did() and att() take the model as a formula,
outcome ~ treatment + covariates | fixed effects, as in
fixest::feols(). The same formula works in all three, so switching
between a single DiD coefficient, an event study, and aggregated ATTs means
swapping the function name. formula and data may come in either order
(df |> event_study(y ~ D | id + year, time = year) works).y ~ D | id + year. Each unit's treatment start is the first period
with D = 1; a single start date gives the universal design, several
the staggered one. D must stay on once it starts, and rows with a
missing D are dropped with a warning. With time_transform = TRUE the
start is read on the time index.NA = never
treated): y ~ x | id + year, timing = g, with only covariates on the
right-hand side (y ~ 1 | ... when there are none).y ~ treated | id + year, timing = 2005. The staggered estimators and
att() now accept this form too, and did() also accepts a timing
column.time is required by event_study() and att(). unit is taken from
the fixed effects when they are time plus exactly one other column, and
a message reports the unit and time used; print() now shows them for
every es_result, did_result and att_result (new unit_var /
time_var attributes on es_result and att_result)."cs", "sa", "bjs" and att() (the argument
interface ignores them silently). "cs", "bjs" and "flex" accept unit
(group) and time fixed effects silently and warn about any others.outcome, treatment, fe, covariates) keeps
working in fixes 1.x with a deprecation warning that prints the
equivalent formula call; fixes 2.0.0 is planned to accept only the
formula. These arguments must now be named: positional calls such as
att(df, y, year, g, unit = id) stop with an error pointing to the
formula. run_es(), run_did() and calc_att() still give exactly one
deprecation warning.event_study() replaces run_es(). Argument modernization:
conf.level -> conf_level, B -> boot_reps, alpha -> boot_alpha;
method = "sunab" is retired in favour of estimator = "sa" (the
built-in Sun-Abraham implementation); staggered now defaults to NULL
and is inferred -- timing naming a column of data means staggered,
a scalar means universal timing.att() replaces calc_att(). The reserved-but-ignored arguments
(treatment, fe, covariates, cluster, weights, vcov,
vcov_args) are dropped; a unit identifier is required (unit, or read
from the fixed effects of the formula); conf.level -> conf_level.did() replaces run_did() (conf.level -> conf_level).att_gt() extracts the Callaway-Sant'Anna ATT(g,t) table as a real
att_gt_result object with its own plot()/autoplot() methods.contamination_weights() replaces compute_contamination_weights().honest_sensitivity() keeps its name; Mvec, gridPoints,
numPrePeriods, numPostPeriods are renamed to m_grid, grid_points,
n_pre_periods, n_post_periods (old names still accepted with a
deprecation warning).es_result objects gain broom::tidy() / broom::glance() methods and
an estimator attribute (shown by print()).plot() method returning a ggplot:
plot(<es_result>), plot(<att_result>) (new point-range chart),
plot(att_gt(x)), plot(<honest_result>), and
plot(<sa_contamination_weights>).plot(x, interactive = TRUE) returns the plotly chart formerly provided
by plot_es_interactive() (requires the suggested plotly package).plot_es(), plot_es_interactive(), plot_att_gt(), plot_honest(),
and plot_contamination_weights() are deprecated wrappers over the same
engines. The duplicate autoplot.es_result registration was removed.type = "ribbon"
gives the previous line-and-band display.plot() method. Error bars and point
ranges (plot.es_result(), plot.att_result()) are black; ribbon
displays (type = "ribbon", the interactive plot, the "facet" display
of plot.att_gt_result()) use a muted steel blue with a lighter band.
color, fill and alpha default to NULL and resolve from that
palette, so passing them explicitly works exactly as before.plot.es_result(type = "errorbar") gains errorbar_color, which colours
the bars on their own; it follows color unless given.vline_val still overrides it.plot() / autoplot() on an es_result gain time_axis. The default
"relative" keeps the event-time axis; "calendar" plots the original
time values instead (e.g. 2010, or a Date), labels the axis with the
time column's name, and moves the dashed reference line with it. Available whenever every treated unit adopts in the same period
(universal timing, or a single-cohort staggered design); with several
cohorts one relative period maps to several calendar periods, so it errors
with an explanation. es_result objects carry the new ref_time,
time_var, time_levels and time_transform attributes that back it.ci_level now errors when the result was not estimated at that confidence
level, naming the levels that are available. Previously the plot silently
fell back to the 95% band, or failed with an opaque error when no 95% band
existed.alpha is now honoured by the ribbons of show_simultaneous = TRUE
plots, which previously used hard-coded opacities.did() now run on the package's
internal C++ fixed-effects OLS engine (weighted least squares, Student-t
inference, and fit statistics added), reproducing the previous
fixest-based results to numerical agreement (oracle tests at 1e-8).run_es(method = "sunab") (legacy path), multiway clustering, non-empty
vcov_args, vcov types beyond iid/hetero/HC1, and fixed-effects or
cluster specifications beyond plain columns (e.g. ~ id^year). These
paths raise an informative error when fixest is not installed.did() is on par
or faster. On very large panels (~1M rows) the classic event-study path
is still slower than fixest::feols() with the same design (about 1.1 s
against 0.7 s for 50,000 units over 20 periods, fixest on 8 threads).estimator = "cs" in event_study() and att()) now balances the
panel, as did::att_gt() does by default. Rows with a missing or
non-finite outcome, time, or unit are dropped, then every unit not
observed in all periods is dropped; both steps warn. Previously a single
missing outcome turned every ATT(g, t) that touched it — and with it the
whole event-study curve — into NA, and units missing some periods were
kept wherever both periods of a comparison were observed. Estimates now
match did::att_gt() on panels with missing values, and N, N_units,
N_treated and N_nevertreated describe the balanced sample. Panels that
are already balanced and complete give the same results as before.timing must be constant within each unit. event_study() with
estimator = "cs", "sa", "bjs" or "twm", att(), and
contamination_weights() now stop with an error naming the offending
units when a unit carries more than one timing value, including a mix of
NA and a date. Such data previously ran, with cohort membership taken
from whichever row came first, so results depended on the row order.fixest::sunab() in that case too.NA, which silently
removed all of the cohort's post-treatment cells (or stopped estimation
when every cohort had one). Rows with missing covariates are now dropped
as fixest::feols() drops them, including rows in the reference period,
which used to be kept. Covariates are centred on the estimation sample,
so results equal those from the same data with those rows removed.contamination_weights() returned NA weights when only one TWFE
event-time coefficient was estimated; they are now computed.all.vars(), so covariates = ~ log(x)
silently estimated with x. Covariate terms are now evaluated with
model.matrix conventions, as in the TWFE path (log(x), I(x^2),
factor(region) dummies, x1:x2), in the formula, in the covariates
argument, and in the deprecated run_es(). Results with plain numeric
covariates are unchanged.N_treated
was the number of rows with a known event time (every row under
universal timing), and N_nevertreated was 0 under universal timing.
Both now count units, as for the other estimators: units with
treatment = 1 and a known event time, and all other units. They are
NA when no unit identifier is available.tests/testthat.R was missing, so R CMD check (and the R-CMD-check CI
jobs) never ran the test suite; it now does.event_study(estimator = "cs") and att(estimator = "cs")
summed the squared SEs of ATT(g,t) as if they were independent. They now
use the influence function of each aggregate, including the estimation of
the cohort weights, and match did::aggte(). att() SEs were up to half
as large as did's (overstating significance), and event-study SEs up to
22% larger. Point estimates are unchanged.event_study(estimator = "bjs") left out the projection of the treated
weights on the fixed effects (Borusyak et al. 2024, Theorem 3), so it was
20-30% too small; att(estimator = "bjs") used the naive spread of the
unit-time effects and was less than half of the correct value. Both now
match didimputation::did_imputation(), clustered by unit (or by
cluster in event_study()). Point estimates are unchanged.bootstrap = TRUE, the simultaneous bands for the CS event study use
the same influence functions as the analytic SEs.[y | X], residuals and the VCOV
"meat" in C++ without intermediate copies, and skips its NA and singleton
checks on complete data. On one machine (20 periods, never-treated units
plus three cohorts): classic event study on 1,000,000 rows about 1.1 s;
estimator = "sa" on 100,000 rows 0.3 s; "sa", "twm" and "bjs" on
300,000 rows (row order shuffled) 1.0, 1.0 and 0.2 s; "cs" on 300,000
rows 0.1 s without and 0.7 s with the bootstrap.contamination_weights() solves all auxiliary regressions in one pass
over the demeaned design instead of one regression per cohort-period cell
(30,000 rows in 0.07 s).did_result (from did()/run_did()) no longer stores the raw fixest
model in $model. It now carries coeftable, vcov, fit_stats, and
df.t; broom::tidy()/broom::glance()/print() output is unchanged.run_es(), calc_att(), run_did(), plot_es(),
plot_es_interactive(), plot_att_gt(), plot_honest(),
compute_contamination_weights(), and plot_contamination_weights()
are deprecated (lifecycle warning once per session) but fully functional,
returning results identical to their successors.run_did() reported nrow(data) instead of the estimation sample size.
attr(., "N") and N_treated (and the printed "N = … obs") counted all
rows of data, including those fixest::feols() drops for missing values,
disagreeing with nobs(model), broom::glance(), and run_es() (which
already report the fitted sample). N, N_units, and N_treated now
describe the estimation sample, recovered from the model's obs_selection.
On data without missing values the numbers are unchanged.calc_att() results can now be tabulated. att_result gains
broom::tidy() and broom::glance() methods, so aggregated ATTs (overall,
by-cohort, or by-time; CS or BJS) can be passed to
modelsummary::modelsummary() and rendered with tinytable. Several
estimators — e.g. run_did() TWFE, CS, and BJS — can be stacked side by side
in one table. The term column is derived from the aggregation type
("ATT", "Cohort <g>", "Time <t>") so rows align across estimators.
run_es(..., rel_time = <col>): pass a pre-built event-time column (time
relative to treatment) — e.g. the Time_to_Treatment from
paneltools/fect's get.cohort() — and run_es() uses it verbatim as the
event-study factor (i(rel_time, treatment, ref = baseline)) instead of
computing relative time from time/timing. This normalises the previous
time = event_time, timing = 0 idiom and, unlike that idiom, keeps
never-treated controls (NA rel_time) in the estimation sample. Requires
estimator = "twfe" and method = "classic"; the default calendar-difference
behaviour is unchanged. See the new Details section of ?run_es for the
three relative-time conventions (calendar difference, within-unit rank,
pre-built) and when they diverge in sparse/unbalanced panels.
g-1 is not observed (a real hazard in sparse panels, where a cohort can
vanish from the by-cohort and event-study aggregates), and exposes them as
attr(result, "dropped_cohorts") on both run_es() and calc_att().attr(result, "n_unimputed") (and n_treated_obs): the
number of treated observations whose counterfactual could not be imputed and
were excluded from aggregation, on both run_es() and calc_att().The long-term plan of trimming the fixest dependency (see CLAUDE.md)
begins in this release. Two internal engines now replace fixest::feols()
in most estimators; all user-facing APIs, output columns, and numerics are
unchanged — every change here is verified against the previous
fixest-based results to numerical agreement (tolerance = 1e-6, typically
much closer).
src/fe_solver.cpp) instead of
fixest::feols(y ~ 0 | unit + time) + fixest::fixef(). It reproduces
fixest's estimation sample exactly, including the iterative removal of
singleton fixed-effect levels (dropping one singleton can create another).compute_contamination_weights() now run their
saturated cohort × period regressions through a new internal FE-OLS engine
(R/fe_ols.R, src/demean_kway.cpp, src/fe_ols_kernels.cpp): k-way
fixed-effects demeaning, keep-first collinearity dropping, and iid /
heteroskedasticity-robust (HC1) / one-way cluster-robust standard errors
that reproduce fixest's default small-sample corrections exactly
(ssc(adj = TRUE, cluster.adj = TRUE), nested-fixed-effect accounting).
Requests the internal engine cannot reproduce exactly — multi-way
clustering, custom vcov_args, or fixed-effect specifications that are not
plain columns — are transparently delegated to fixest, so every code
path keeps returning identical results.fixest's multi-threaded speed on saturated designs with hundreds of
cohort × period columns. Thread count defaults to half the detected
physical cores and can be set with options(fixes.nthreads = <n>).fixest remains the estimation engine for classic TWFE / method = "sunab" in run_es(), for run_did(), and as the fallback described
above; it is still a required (Imports) dependency. parallel (base R)
is now also imported, for core-count detection only.CS estimator: character / factor unit IDs produced silently wrong results
(run_es(estimator = "cs") and calc_att(estimator = "cs")): unit IDs were
coerced with as.integer(), which turns every character ID into NA — all
units collapsed into one inside the Rcpp kernel and the ATT(g,t) table was
meaningless, with no error raised. Unit IDs are now densely re-encoded with
match(); character, factor, and arbitrary numeric IDs give results
identical to integer IDs.
CS estimator: a cohort with timing == 0 leaked into the control group:
0L was the internal never-treated sentinel, so a genuine cohort first
treated at time 0 (e.g., event-time-centred data) was silently used as
never-treated controls. time/timing are now shifted by a common internal
offset so the sentinel can never collide; results are invariant to common
shifts of the time axis.
Classic staggered TWFE dropped the entire never-treated control group
(estimator = "twfe", staggered = TRUE): the relative-time variable is
NA for never-treated units and fixest::feols() removes rows with NA
regressors, so every control row was excluded from the estimation sample
(visible only as a fixest NOTE). Those rows are now assigned the (excluded)
baseline level — their event dummies are identically zero because the i()
interaction is multiplied by treatment = 0 — so the control group is
correctly retained. N_treated / N_nevertreated metadata still reflect
the original NA pattern.
method = "sunab" dropped the entire never-treated control group:
fixest::sunab() has no NA-cohort convention and drops those rows.
Never-treated units are now recoded internally to a cohort far beyond the
sample period (the fixest::base_stagg convention) and retained as
controls. Verified numerically identical to a direct
fixest::feols(y ~ sunab(...)) call using the same recoding.
Rcpp (unit, time) lookup key: replaced the multiplicative hash (which
could collide for time values above 1,000,003, e.g. Unix timestamps) with a
collision-free 64-bit (unit << 32) | time key in
src/compute_att_gt.cpp.
g - 1 base-period assumption), instead of the generic
"No ATT(g,t) estimates were computed".g - 1 - anticipation) is not observed.time / timing values now raise an error
instead of being silently truncated by as.integer() (new shared helper
.assert_integerish()).baseline reference period is
not feasible for any cohort, in which case no period would be excluded and
the regression is unnormalised.match() on keys instead of per-cohort scans).tests/testthat/test-robustness.R (19 assertions) covering all of the
above: ID-type invariance, time-shift invariance, control-group retention
(classic TWFE and sunab vs. direct fixest reference), grid-spacing errors,
fractional-time rejection, and BJS singleton imputation.run_es() — diagnostic warning for ambiguous timing = NA (classic TWFE,
staggered = TRUE only): a warning() is now emitted when any unit has
treatment = 1 in at least one row but timing = NA. Such units are silently
absorbed as never-treated controls — correct when NA is intentional, but
dangerous when NA represents genuinely missing treatment timing. The warning
names up to 5 affected unit IDs (when unit is supplied) for quick diagnosis.
@param timing documentation clarified: all staggered estimators (cs, sa,
bjs, twm, flex, classic with staggered = TRUE) share the same NA = never treated convention as did::att_gt() and fixest::sunab(). The roxygen
documentation now states this explicitly and notes the risk of silent
misclassification when NA represents missing data rather than "never treated".
honest_sensitivity() — honest robust inference (Rambachan & Roth 2023)
(R/honest_did.R): sensitivity analysis for event-study / DiD
designs when parallel trends may be violated. Instead of assuming parallel
trends holds exactly, it reports confidence sets for a post-treatment effect
under progressively weaker restrictions on the difference in trends, and a
breakdown value (the largest restriction at which the effect is still
significant):
res <- run_es(df, outcome = y, treatment = treat, time = year, timing = 6,
fe = ~ id + year)
h <- honest_sensitivity(res, type = "relative_magnitude",
Mvec = c(0, 0.5, 1, 1.5, 2))
plot_honest(h)
"relative_magnitude" (\eqn{\Delta^{RM}(\bar M)}) and
"smoothness" (\eqn{\Delta^{SD}(M)}).run_es() es_result (estimator "twfe": classic or
method = "sunab", which now carry the event-study coefficient covariance
via the new es_vcov attribute), or from a raw betahat / sigma pair for
any estimator.plot_honest() / autoplot() draw the "top-down" sensitivity plot;
print.honest_result() reports the breakdown value."Conditional"): \eqn{\Delta^{SD}} (single post period) matches to machine
precision; \eqn{\Delta^{RM}} matches to grid resolution. Heavy numeric
helpers (lpSolveAPI, Rglpk, TruncatedNormal, Matrix, pracma) are in
Suggests and gated with requireNamespace().Estimator-core consolidation (behaviour-preserving): factored the
boilerplate duplicated across run_es(), calc_att(), run_did(), and the
five estimator backends into shared helpers in
R/utils-internal.R — .resolve_col() (NSE column
resolution), .add_ci_columns() (CI construction), .validate_panel_cols(),
.compute_cohort_sizes(), .model_vcov_full(), and .aggregate_iw(). The
calc_att() aggregation loops were vectorised. All existing tests pass
unchanged.
run_did() — basic TWFE DiD estimator (R/run_did.R):
New function for classic two-way fixed-effects difference-in-differences.
Accepts a pre-built binary treatment indicator D_it and returns a
did_result S3 object with full modelsummary / tinytable compatibility:
df$D <- as.integer(df$treated & df$year >= 2006)
result <- run_did(df, outcome = y, treatment = D, fe = ~ id + year)
print(result)
modelsummary::modelsummary(result) # tinytable output (modelsummary >= 2.0)
run_es() and calc_att() — specify variables
by name, not by formula construction.fe auto-inferred as ~ unit + time when both unit and time are
supplied and fe = NULL.tidy.did_result() delegates to broom::tidy.fixest() for full
regression table (all regressors); glance.did_result() delegates to
broom::glance.fixest() for model-level stats (within.r.squared,
nobs, AIC, etc.).print.did_result() shows a clean header: estimator, sample sizes,
FE spec, VCOV type.log(y) supported.modelsummary and tinytable added to Suggests.calc_att() — ATT aggregation function (R/calc_att.R): New
function that separates "calculating ATT" from "running an event study".
While run_es() produces a full dynamic event-study curve (effects by
relative time), calc_att() yields aggregated ATT estimates with a clear,
minimal interface:
calc_att(data, outcome = y, time = year, timing = g, unit = id,
estimator = "cs",
aggregation = "simple") # overall ATT
calc_att(..., aggregation = "by_cohort") # ATT per cohort
calc_att(..., aggregation = "by_time") # ATT per calendar period
"cs" (Callaway-Sant'Anna 2021) and "bjs" (Borusyak
et al. 2024). SA, TWM, FLEX are event-study-only estimators and are not
supported (use run_es()).did::aggte() for point estimates:
"simple": exposure-weighted average over all post-treatment (g,t) pairs."by_cohort": mean ATT(g,t) over post-treatment periods per cohort."by_time": cohort-size-weighted ATT per calendar period.did::aggte SEs, which use influence-function variance
accounting for within-cohort time correlation. Cluster-robust SE for BJS
aggregations planned for v0.12.att_result S3 object (data.frame subclass) with columns
group, estimate, std.error, statistic, p.value,
conf_low_XX/conf_high_XX. Attributes: aggregation, estimator,
N, N_units, N_treated, control_group (CS only), att_gt (raw
CS ATT(g,t) table), tau_it (BJS unit-time effects table).print.att_result() S3 method provides a concise summary.tests/testthat/test-att.R:
did::aggte() at tolerance 1e-4mean(tau_it$tau_hat) at tolerance 1e-12conf.level support verifiedrun_es.R refactoring: Extracted .make_tidy() and .es_finalize() private
helpers that centralise the repeated tidy-construction / baseline-row / range-filter /
attr-stamping pattern that was previously duplicated across the cs, sa, bjs,
twm, and flex branches (~360 lines removed). No API change; all 593 tests pass.inst/profile/benchmark.R with profile_estimator()
and benchmark_all() utilities. Run interactively to generate flamegraph HTML and
wall-clock summary across estimators before starting Phase 3 Rcpp work.src/compute_att_gt.cpp (shipped in v0.8.1 but
missing from the Rcpp Acceleration Roadmap), and updated Package Structure to list all
13 R source files, 5 Rcpp files, and 16 test files that now exist.test-twm.R Test 16 — trends=TRUE with exactly 2 pre-treatment periods succeeds
without warning (boundary condition of the ≥ 2 requirement).test-twm.R Test 17 — explicit lead_range/lag_range correctly trims TWM output
and preserves estimates in the shared window (exercises .es_finalize() filter).test-flex.R Test 12 — explicit lead_range/lag_range trims FLEX output correctly.test-flex.R Test 13 — FLEX succeeds with an all-treated panel (no never-treated
groups; N_nevertreated == 0).test-sa.R — explicit lead_range/lag_range trims SA output and preserves
estimates within the shared window.run_es(estimator = "twm", trends = TRUE): Cohort-specific linear trend
detrending (Wooldridge 2025, Section 8). Adds d_g * t regressors so each
cohort's counterfactual trend can deviate linearly from the common time
trend. Implemented as post-treatment-only cells + trend columns; requires
= 2 pre-treatment periods per cohort. Output shows
relative_time >= 0only (pre-trend testing is deferred to the no-trend model).
run_es(estimator = "twm", covariates = ~ x1 + x2): Full Wooldridge (2025)
Procedure 5.1 covariate interactions. Adds treatment-cell × cohort-demeaned
covariate terms (ẋ_{ig} = x_i - x̄_g) and i(time, x_j) conditional
parallel-trends controls (Eqs. 5.2-5.3).run_es(estimator = "flex", covariates = ~ x1 + x2): Full Deb et al. (2024)
Eq. 3.1 covariate interactions for RCS data. Cell-level centering
X_{i,t} - X̄_{g,t} (Eq. 2.11) with i(time, x_j) and i(group, x_j)
conditional PT controls.src/indicator_matrix.cpp — build_indicator_matrix_cpp(): Replaces the
pure-R nested for-loop that fills the 0/1 indicator matrix in the SA, TWM,
and FLEX estimators. Single shared Rcpp utility, O(N×K) integer fill with
no R allocation overhead per column.src/iw_aggregation.cpp — aggregate_iw_cpp(): Replaces the R event-time
aggregation loop and the t(w) %*% V_sub %*% w quadratic-form VCOV step in
SA, TWM, and FLEX. Uses RcppArmadillo arma::mat::submat() for O(1) VCOV
block extraction and arma::as_scalar(w.t() * V_sub * w) for the quadratic
form.src/cov_demeaning.cpp — build_cov_interactions_cpp(): Replaces the
pure-R covariate demeaning loops in TWM (cohort-level centering) and FLEX
(group×time cell-level centering) with a single std::unordered_map-based
O(N) grouping pass followed by column-wise mean subtraction, then builds the
N × (K×p) treatment-cell × centred-covariate interaction matrix in C++.src/bootstrap_cs.cpp — bootstrap_cs_cpp(): Replaces the R-level
apply/sweep calls in the CS multiplier bootstrap with RcppArmadillo DGEMM
for both the pilot and main stages. Uses R::runif() for Mammen weight
generation (column-major fill order matches R's matrix()) so results are
numerically identical to the pure-R path under the same set.seed().src/Makevars and src/Makevars.win linking $(LAPACK_LIBS) $(BLAS_LIBS) $(FLIBS) for cross-platform RcppArmadillo support.RcppArmadillo added to LinkingTo in DESCRIPTION.run_es(estimator = "twm"): Wooldridge (2025) Two-Way
Mundlak (TWM) estimator for panel data. Implements
Procedure 5.1 via POLS on cohort x calendar-time
treatment cells with two-way FE. Algebraically
identical to estimator = "sa" in the no-covariate
base case (verified numerically at tolerance 1e-10).
The trends = TRUE extension (cohort-specific linear
trend detrending, Wooldridge 2025 Section 8) is
reserved for v0.9.1.run_es(estimator = "flex", group = <group_id>):
Deb, Norton, Wooldridge & Zabel (2024) FLEX estimator
for repeated cross-section (RCS) data. Uses group
x calendar-time OLS with group+time FE (no unit FE).
Algebraically equivalent to the multi-step imputation
estimator (Proposition 2.1). New group argument
identifies the treatment group (R_{ig}) each
individual belongs to. Default clustering is by group.
Covariate interactions deferred to v0.9.1.papers/Deb et al. (2024).txt,
papers/Woodridge (2025).txt.run_es(estimator = "cs"): Callaway-Sant'Anna (2021)
estimator. Computes group-time ATT(g,t) via the
unconditional DiD estimand (eq. 2.8) with
never-treated or not-yet-treated control groups.
Supports control_group = "nevertreated" (default)
or "notyettreated". The full ATT(g,t) matrix is
stored as the att_gt attribute of the result.run_es(estimator = "sa"): Sun-Abraham (2021)
interaction-weighted estimator. Aggregates cohort x
relative-time interactions by cohort share weights.
Numerically identical to fixest::sunab() to
machine precision.run_es(estimator = "bjs"): Borusyak, Jaravel &
Spiess (2024) imputation estimator. Fits TWFE on
untreated observations only, imputes counterfactuals
for treated observations, and averages treatment
effects by horizon. Handles singleton unit fixed
effects via closed-form recovery.run_es(bootstrap = TRUE): Multiplier bootstrap for
simultaneous confidence bands (Algorithm 1, Callaway
& Sant'Anna 2021). Adds conf_low_sim /
conf_high_sim columns to the result and stores the
(g,t)-level bootstrap object as
attr(result, "bootstrap"). Controlled by B
(draws, default 999) and boot_seed arguments.
Available only when estimator = "cs".plot_att_gt(): Visualize the full ATT(g,t) matrix
from CS results as a heatmap (type = "heatmap") or
cohort-faceted time series (type = "facet").
Requires estimator = "cs" result as input.
When bootstrap = TRUE was used, the heatmap adds
a subtitle with the simultaneous critical value and
open-diamond markers for simultaneously significant
cells; the facet adds a lighter simultaneous CI
ribbon.plot_es(show_simultaneous = TRUE): Overlays the
simultaneous bootstrap CI (lighter band, alpha 0.15)
alongside the pointwise CI (alpha 0.3) with a
two-entry legend. Errors informatively if bootstrap
was not run.plot_es_interactive(show_simultaneous = TRUE):
Adds a second lighter ribbon trace for the
simultaneous CI and extends the hover tooltip with
simultaneous CI bounds.run_es() bootstrap path: base::merge() silently
dropped all custom attributes (e.g. att_gt,
N_units, lead_range) from the es_result object.
Attributes are now saved and restored around the
merge, so plot_att_gt() works correctly after a
bootstrap = TRUE run.did and didimputation to Suggests for
numerical agreement tests against reference
implementations.cluster in run_es() did not produce clustered standard errorsvcov = "HC1" was being applied after model estimation, overriding the clustered SE from feols()cluster is specified and vcov is left at its default ("HC1"), the model's clustered standard errors are usedcluster is set, pass vcov = "HC1" together with cluster = NULLclassic and sunab methodsrun_es() results and equivalent direct fixest::feols() callsLead/lag range filtering now enforced:
lead_range and lag_range parameters were ignored, causing all estimated coefficients to be returned regardless of specified ranges[-lead_range, lag_range]classic and sunab methodssunab() namespace error resolved:
'sunab' is not an exported object from 'namespace:fixest' error when using method = "sunab"sunab function from fixest packageBaseline row now included in sunab results:
baseline parameter (default: -1) now applies to both classic and sunab methodsestimate = 0, std.error = 0, and is_baseline = TRUEbaseline is used for both methodsImproved term column formatting:
"-9", "0", "3")"year::-9") for better readabilityModernized code with native pipe operator:
%>%) with base R pipe (|>) throughout the package|>)relative_time was NA for all coefficients except baseline when using non-staggered treatment timingfixest::i() now correctly corresponds to the baseline parametertiming = 5 and baseline = -1, period 4 (not period 5) is now used as the referenceplot_es_interactive() for creating interactive event study plotses_result objects from run_es()sapply() approach for significant performance gainsplotly package (optional, suggested dependency)run_es() and plot_es() now support returning and visualizing multiple confidence levels (e.g., 90%, 95%, 99%) in a single analysis.plot_es() adds ci_level selection, more theme options (theme_style), and improved ribbon/error bar display.NA in timing):
timing are now retained as never-treated controls (staggered only).weights input:
~ popwt), bare names (popwt), or character strings ("popwt").cluster input handling:
unit is supplied without time_transform = TRUE.Added support for staggered treatment timing:
staggered = TRUE option allows timing to vary by unit (e.g., treatment year column).NA in timing are safely retained as untreated.Added support for observation weights:
weights argument (e.g., ~ popwt) to run weighted regressions.Automatic lead/lag range detection:
lead_range or lag_range is NULL, the function computes the maximum feasible range from the data.unit is specified without time_transform = TRUE.plot_es():
ggplot2::scale_x_continuous() with integer breaks spaced by 1, aligned to the relative_time range.Date:
time_transform = TRUE to automatically convert the time variable into a unit-level sequential index (1, 2, 3, ...) for event study estimation.unit argument to specify the panel unit identifier required when time_transform = TRUE.Date class in the time variable and converts it automatically to numeric if time_transform = FALSE.unit is missing or time is of unsupported type.@examples in the function documentation to include Date-based examples.time_transform usage.README.md to describe irregular time handling and demonstrate new use cases.time_transform, unit handling, and Date conversion edge cases.lead1, lag0) already exist in the dataset to prevent accidental overwriting.lead_range, lag_range, and interval) has fewer than 10 rows, helping users identify overly narrow estimation windows.treatment variable: it is now coerced to logical using as.logical() to support both binary numeric (0/1) and logical (TRUE/FALSE) formats.fe argument (e.g., ~ id + year) were combined using model_formula | fe_text, which caused evaluation errors during tests.as.formula() to ensure compatibility with fixest::feols().run_es():
~ x1 + x2).fe and cluster arguments must now be specified using a one-sided formula (e.g., ~ id + year).cluster is still accepted.fe_var argument now supports additive notation (firm_id + year) instead of character vectors.plot_es() efficiency and documentation.fe notation.This version introduced several enhancements and refinements to improve usability and maintainability.
outcome_var, treated_var, and time_var are now processed using rlang::ensym() for better robustness.fe_var and cluster_var handling improved for more reliable column referencing.plot_es() function:
relative_time, estimate, etc.) are present.baseline handling could lead to incorrect sorting of lead/lag terms.This is the first release of the fixes package, providing tools for estimating and visualizing event study models with fixed effects.
run_es(): A function to estimate event study models using fixest::feols(), generating lead and lag variables automatically.
fe_var as character vector).cluster_var.interval argument.plot_es(): A function to visualize event study results with ggplot2.
type = "ribbon", default).type = "errorbar").fixest::feols().interval argument).c("firm_id", "year"))."state_id").fe_var.