run_did()reportednrow(data)instead of the estimation sample size.attr(., "N")andN_treated(and the printed "N = … obs") counted all rows ofdata, including thosefixest::feols()drops for missing values, disagreeing withnobs(model),broom::glance(), andrun_es()(which already report the fitted sample).N,N_units, andN_treatednow describe the estimation sample, recovered from the model'sobs_selection. On data without missing values the numbers are unchanged.
-
calc_att()results can now be tabulated.att_resultgainsbroom::tidy()andbroom::glance()methods, so aggregated ATTs (overall, by-cohort, or by-time; CS or BJS) can be passed tomodelsummary::modelsummary()and rendered withtinytable. Several estimators — e.g.run_did()TWFE, CS, and BJS — can be stacked side by side in one table. Thetermcolumn 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. theTime_to_Treatmentfrompaneltools/fect'sget.cohort()— andrun_es()uses it verbatim as the event-study factor (i(rel_time, treatment, ref = baseline)) instead of computing relative time fromtime/timing. This normalises the previoustime = event_time, timing = 0idiom and, unlike that idiom, keeps never-treated controls (NArel_time) in the estimation sample. Requiresestimator = "twfe"andmethod = "classic"; the default calendar-difference behaviour is unchanged. See the new Details section of?run_esfor the three relative-time conventions (calendar difference, within-unit rank, pre-built) and when they diverge in sparse/unbalanced panels.
- CS now names cohorts that are silently dropped because their base period
g-1is not observed (a real hazard in sparse panels, where a cohort can vanish from the by-cohort and event-study aggregates), and exposes them asattr(result, "dropped_cohorts")on bothrun_es()andcalc_att(). - SA warns when the saturated design is degenerate (a large share of cohort×period interactions dropped as collinear, or implausibly large CATT standard errors), which is easy to miss with sparse/irregular time.
- BJS now reports
attr(result, "n_unimputed")(andn_treated_obs): the number of treated observations whose counterfactual could not be imputed and were excluded from aggregation, on bothrun_es()andcalc_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).
- BJS Step 1 (fixed-effects nuisance regression) is now estimated by an
internal alternating-projections solver (
src/fe_solver.cpp) instead offixest::feols(y ~ 0 | unit + time)+fixest::fixef(). It reproducesfixest's estimation sample exactly, including the iterative removal of singleton fixed-effect levels (dropping one singleton can create another). - SA, TWM, FLEX, and
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 reproducefixest'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, customvcov_args, or fixed-effect specifications that are not plain columns — are transparently delegated tofixest, so every code path keeps returning identical results. - The regressions are parallelised with OpenMP (falls back to sequential
execution where OpenMP is unavailable) and are within a few percent of
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 withoptions(fixes.nthreads = <n>). fixestremains the estimation engine for classic TWFE /method = "sunab"inrun_es(), forrun_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")andcalc_att(estimator = "cs")): unit IDs were coerced withas.integer(), which turns every character ID intoNA— 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 withmatch(); character, factor, and arbitrary numeric IDs give results identical to integer IDs. -
CS estimator: a cohort with
timing == 0leaked into the control group:0Lwas 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/timingare 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 isNAfor never-treated units andfixest::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 thei()interaction is multiplied bytreatment = 0— so the control group is correctly retained.N_treated/N_nevertreatedmetadata 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 (thefixest::base_staggconvention) and retained as controls. Verified numerically identical to a directfixest::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) | timekey insrc/compute_att_gt.cpp.
- CS: clear, actionable error when the time grid does not advance in steps of
1 (the
g - 1base-period assumption), instead of the generic "No ATT(g,t) estimates were computed". - CS: warning that names cohorts skipped because their base period
(
g - 1 - anticipation) is not observed. - CS / SA / TWM / FLEX: fractional
time/timingvalues now raise an error instead of being silently truncated byas.integer()(new shared helper.assert_integerish()). - SA / TWM / FLEX: warning when the requested
baselinereference period is not feasible for any cohort, in which case no period would be excluded and the regression is unnormalised.
- Verified on a 400,000-row staggered panel (20,000 units × 20 periods): every estimator completes in roughly 1–3 s (CS bootstrap with B = 999 ≈ 3 s), peak memory ≈ 1.3 GB.
- BJS: singleton-unit FE recovery vectorised (was O(#singletons × N), which could take minutes on large unbalanced panels).
- CS: event-study aggregation and bootstrap influence-function bookkeeping
vectorised (
match()on keys instead of per-cohort scans).
- New
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 ambiguoustiming = NA(classic TWFE,staggered = TRUEonly): awarning()is now emitted when any unit hastreatment = 1in at least one row buttiming = NA. Such units are silently absorbed as never-treated controls — correct whenNAis intentional, but dangerous whenNArepresents genuinely missing treatment timing. The warning names up to 5 affected unit IDs (whenunitis supplied) for quick diagnosis. -
@param timingdocumentation clarified: all staggered estimators (cs, sa, bjs, twm, flex, classic withstaggered = TRUE) share the sameNA = never treatedconvention asdid::att_gt()andfixest::sunab(). The roxygen documentation now states this explicitly and notes the risk of silent misclassification whenNArepresents 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)
- Restriction families:
"relative_magnitude"(\eqn{\Delta^{RM}(\bar M)}) and"smoothness"(\eqn{\Delta^{SD}(M)}). - Inference via the Andrews-Roth-Pakes (ARP) conditional moment-inequality test — a pure-R reimplementation (no dependency on the HonestDiD package).
- Works directly from a
run_es()es_result(estimator"twfe": classic ormethod = "sunab", which now carry the event-study coefficient covariance via the newes_vcovattribute), or from a rawbetahat/sigmapair for any estimator. plot_honest()/autoplot()draw the "top-down" sensitivity plot;print.honest_result()reports the breakdown value.- Validated against the HonestDiD reference package (method
"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 inSuggestsand gated withrequireNamespace().
- Restriction families:
-
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(). Thecalc_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 indicatorD_itand returns adid_resultS3 object with fullmodelsummary/tinytablecompatibility: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)
- NSE API consistent with
run_es()andcalc_att()— specify variables by name, not by formula construction. feauto-inferred as~ unit + timewhen bothunitandtimeare supplied andfe = NULL.tidy.did_result()delegates tobroom::tidy.fixest()for full regression table (all regressors);glance.did_result()delegates tobroom::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.- Outcome expressions like
log(y)supported. modelsummaryandtinytableadded to Suggests.
- NSE API consistent with
-
calc_att()— ATT aggregation function (R/calc_att.R): New function that separates "calculating ATT" from "running an event study". Whilerun_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
- Estimators:
"cs"(Callaway-Sant'Anna 2021) and"bjs"(Borusyak et al. 2024). SA, TWM, FLEX are event-study-only estimators and are not supported (userun_es()). - Aggregation formulas (CS) match
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.
- SEs: independence assumption (diagonal delta-method). Systematically
smaller than
did::aggteSEs, which use influence-function variance accounting for within-cohort time correlation. Cluster-robust SE for BJS aggregations planned for v0.12. - Returns an
att_resultS3 object (data.frame subclass) with columnsgroup,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.
- Estimators:
- 48 new tests in
tests/testthat/test-att.R:- Return type and column contract for all aggregation types
- CS point estimates match
did::aggte()at tolerance 1e-4 - BJS estimates match manual
mean(tau_it$tau_hat)at tolerance 1e-12 - Multiple
conf.levelsupport verified
run_es.Rrefactoring: 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 thecs,sa,bjs,twm, andflexbranches (~360 lines removed). No API change; all 593 tests pass.- profvis benchmark harness: Added
inst/profile/benchmark.Rwithprofile_estimator()andbenchmark_all()utilities. Run interactively to generate flamegraph HTML and wall-clock summary across estimators before starting Phase 3 Rcpp work. - CLAUDE.md documentation: Added
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. - Edge-case tests (19 new, 593 total):
test-twm.RTest 16 —trends=TRUEwith exactly 2 pre-treatment periods succeeds without warning (boundary condition of the ≥ 2 requirement).test-twm.RTest 17 — explicitlead_range/lag_rangecorrectly trims TWM output and preserves estimates in the shared window (exercises.es_finalize()filter).test-flex.RTest 12 — explicitlead_range/lag_rangetrims FLEX output correctly.test-flex.RTest 13 — FLEX succeeds with an all-treated panel (no never-treated groups;N_nevertreated == 0).test-sa.R— explicitlead_range/lag_rangetrims SA output and preserves estimates within the shared window.
run_es(estimator = "twm", trends = TRUE): Cohort-specific linear trend detrending (Wooldridge 2025, Section 8). Addsd_g * tregressors 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) andi(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 centeringX_{i,t} - X̄_{g,t}(Eq. 2.11) withi(time, x_j)andi(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 thet(w) %*% V_sub %*% wquadratic-form VCOV step in SA, TWM, and FLEX. Uses RcppArmadilloarma::mat::submat()for O(1) VCOV block extraction andarma::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 singlestd::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-levelapply/sweepcalls in the CS multiplier bootstrap with RcppArmadillo DGEMM for both the pilot and main stages. UsesR::runif()for Mammen weight generation (column-major fill order matches R'smatrix()) so results are numerically identical to the pure-R path under the sameset.seed().- Added
src/Makevarsandsrc/Makevars.winlinking$(LAPACK_LIBS) $(BLAS_LIBS) $(FLIBS)for cross-platform RcppArmadillo support. RcppArmadilloadded toLinkingToin DESCRIPTION.
- 574 tests passing (47 new over v0.9.0: trends 4, TWM-covariates 3, FLEX-covariates 3, Rcpp-indicator 4, Rcpp-IW 3, Rcpp-covdemeaning 6, Rcpp-bootstrap 12, plus sub-assertions).
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 toestimator = "sa"in the no-covariate base case (verified numerically at tolerance 1e-10). Thetrends = TRUEextension (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). Newgroupargument identifies the treatment group (R_{ig}) each individual belongs to. Default clustering is by group. Covariate interactions deferred to v0.9.1.- Reference PDFs converted to TXT:
papers/Deb et al. (2024).txt,papers/Woodridge (2025).txt.
- 517 tests passing (41 new: TWM 21, FLEX 20).
- Both new estimators: pre-allocated integer indicator
matrix construction (same strategy as SA optimisation)
- feols() + quadratic-form VCOV. No new dependencies.
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. Supportscontrol_group = "nevertreated"(default) or"notyettreated". The full ATT(g,t) matrix is stored as theatt_gtattribute 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 tofixest::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). Addsconf_low_sim/conf_high_simcolumns to the result and stores the (g,t)-level bootstrap object asattr(result, "bootstrap"). Controlled byB(draws, default 999) andboot_seedarguments. Available only whenestimator = "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"). Requiresestimator = "cs"result as input. Whenbootstrap = TRUEwas 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 thees_resultobject. Attributes are now saved and restored around the merge, soplot_att_gt()works correctly after abootstrap = TRUErun.
- Added
didanddidimputationto Suggests for numerical agreement tests against reference implementations. - 195 tests passing across all estimators, visualization functions, and bootstrap routines.
- Clustered standard errors no longer silently overridden by HC1:
- Fixed a bug where specifying
clusterinrun_es()did not produce clustered standard errors - The default
vcov = "HC1"was being applied after model estimation, overriding the clustered SE fromfeols() - Now, when
clusteris specified andvcovis left at its default ("HC1"), the model's clustered standard errors are used - To explicitly request HC1 SEs even when
clusteris set, passvcov = "HC1"together withcluster = NULL - Applies to both
classicandsunabmethods - This resolves discrepancies between
run_es()results and equivalent directfixest::feols()calls
- Fixed a bug where specifying
-
Lead/lag range filtering now enforced:
- Fixed bug where
lead_rangeandlag_rangeparameters were ignored, causing all estimated coefficients to be returned regardless of specified ranges - Results now correctly filtered to only include coefficients within
[-lead_range, lag_range] - Applies to both
classicandsunabmethods
- Fixed bug where
-
sunab() namespace error resolved:
- Fixed
'sunab' is not an exported object from 'namespace:fixest'error when usingmethod = "sunab" - Formula environment now correctly configured to access
sunabfunction from fixest package - sunab() calls now work identically to direct fixest::feols() usage
- Fixed
-
Baseline row now included in sunab results:
- The
baselineparameter (default: -1) now applies to bothclassicandsunabmethods - Baseline period added to results with
estimate = 0,std.error = 0, andis_baseline = TRUE - Provides consistent behavior across both estimation methods
- Updated documentation to clarify that
baselineis used for both methods
- The
-
Improved term column formatting:
- Term values now display as clean numeric strings (e.g.,
"-9","0","3") - Removed complex fixest-specific notation (e.g.,
"year::-9") for better readability - Consistent formatting across both classic and sunab methods
- Term values now display as clean numeric strings (e.g.,
-
Modernized code with native pipe operator:
- Replaced tidyverse pipe (
%>%) with base R pipe (|>) throughout the package - Requires R >= 4.1.0 (already enforced in DESCRIPTION)
- Cleaner code with no functional changes
- Replaced tidyverse pipe (
- Comprehensive test suite overhaul:
- Added 25+ test cases covering all bug fixes and enhancements
- Separate test suites for classic, staggered, and sunab methods
- Tests for baseline row inclusion, term formatting, and range filtering
- Integration tests with fixest built-in datasets (base_did, base_stagg)
- Tests use native pipe operator (
|>)
- Fully backward compatible with previous versions
- All changes maintain existing API and functionality
- Enhanced robustness and user experience
- Critical fix for relative_time calculation in non-staggered designs:
- Fixed bug where
relative_timewas NA for all coefficients except baseline when using non-staggered treatment timing - The reference period in
fixest::i()now correctly corresponds to thebaselineparameter - For example, with
timing = 5andbaseline = -1, period 4 (not period 5) is now used as the reference - This ensures proper event study estimation with the intended baseline period
- Fixed bug where
- Interactive plotting with plotly:
- New function
plot_es_interactive()for creating interactive event study plots - Hover tooltips display: relative time, point estimate, confidence intervals, standard error, and p-value
- Supports customizable confidence levels, colors, and styling options
- Optional ribbon or point-only display modes
- Fully compatible with
es_resultobjects fromrun_es()
- New function
- Optimized relative_time extraction:
- Vectorized term parsing replaces previous
sapply()approach for significant performance gains - Faster processing of large event study results
- Vectorized term parsing replaces previous
- Enhanced error reporting:
- More informative warnings when term parsing fails
- Better debugging information for malformed coefficient names
- Fully backward compatible with previous versions
- Requires R >= 4.1.0
- Interactive plotting requires
plotlypackage (optional, suggested dependency)
- Multiple Confidence Intervals:
run_es()andplot_es()now support returning and visualizing multiple confidence levels (e.g., 90%, 95%, 99%) in a single analysis. - Enhanced Plotting Options:
plot_es()addsci_levelselection, more theme options (theme_style), and improved ribbon/error bar display. - Expanded Error Handling:
More informative error reporting for all argument validation and model mis-specification. - Documentation Overhaul:
Updated README, vignette, and roxygen docs to reflect all new features and usage patterns.
- Input Validation:
Stricter checks for all arguments and clearer, friendlier error messages. - Staggered Timing:
More robust handling of partial or missing treatment timing. - Code Refactoring:
Internal codebase cleaned for maintainability and reliability.
- Fixed: Confidence intervals always correctly labeled and included in output.
- Fixed: Dummy variables not created for untreated/never-treated units in staggered settings.
- Fixed: CRAN warnings by declaring all global variables used in tidy evaluation.
- Fully backward compatible with previous versions.
- Minimum required R version remains 4.1.0.
- Support for untreated units (
NAin timing):- Units with missing
timingare now retained as never-treated controls (staggered only).
- Units with missing
- Flexible
weightsinput:- Accepts formulas (
~ popwt), bare names (popwt), or character strings ("popwt").
- Accepts formulas (
- Improved
clusterinput handling:- Now supports formula, character vector, and bare names consistently.
- Better validation and error messages across all inputs.
- Warning added if
unitis supplied withouttime_transform = TRUE. - Documentation switched to Markdown-style lists for better readability.
- Ensured untreated units don’t receive event dummies.
- Improved internal dummy creation and baseline handling.
- Fully backward compatible with previous versions.
-
Added support for staggered treatment timing:
- New
staggered = TRUEoption allowstimingto vary by unit (e.g., treatment year column). - Units with
NAintimingare safely retained as untreated.
- New
-
Added support for observation weights:
- Use the
weightsargument (e.g.,~ popwt) to run weighted regressions.
- Use the
-
Automatic lead/lag range detection:
- If
lead_rangeorlag_rangeisNULL, the function computes the maximum feasible range from the data.
- If
- Safer handling of collinearity and untreated units.
- Warning is issued when
unitis specified withouttime_transform = TRUE. - Input validation and internal structure cleaned up for better robustness.
- Backward compatible with previous versions.
- Improved default x-axis scaling in
plot_es():- The x-axis now uses
ggplot2::scale_x_continuous()with integer breaks spaced by 1, aligned to therelative_timerange. - This provides more readable and consistent event time labeling in plots.
- The x-axis now uses
- Added support for irregular and non-numeric time variables such as
Date:- Introduced
time_transform = TRUEto automatically convert thetimevariable into a unit-level sequential index (1, 2, 3, ...) for event study estimation. - Added
unitargument to specify the panel unit identifier required whentime_transform = TRUE. - This enables seamless analysis of panel data with monthly, quarterly, or irregular time formats.
- Introduced
- Updated input validation:
- Now accepts
Dateclass in thetimevariable and converts it automatically to numeric iftime_transform = FALSE. - Improved error messages and warnings for better user feedback when
unitis missing ortimeis of unsupported type.
- Now accepts
- Enhanced documentation:
- Updated
@examplesin the function documentation to includeDate-based examples. - Extended vignette to illustrate
time_transformusage. - Expanded
README.mdto describe irregular time handling and demonstrate new use cases.
- Updated
- Strengthened test coverage:
- Added new unit tests for
time_transform,unithandling, andDateconversion edge cases.
- Added new unit tests for
- No breaking changes.
- This is a backward-compatible minor release introducing significant new functionality for irregular time formats.
- Added a warning when lead/lag dummy variable names (e.g.,
lead1,lag0) already exist in the dataset to prevent accidental overwriting. - Added a warning when filtered data (based on
lead_range,lag_range, andinterval) has fewer than 10 rows, helping users identify overly narrow estimation windows. - Improved handling of the
treatmentvariable: it is now coerced to logical usingas.logical()to support both binary numeric (0/1) and logical (TRUE/FALSE) formats. - Fixed internal bug in model formula construction:
- Previously, fixed effects specified via the
feargument (e.g.,~ id + year) were combined usingmodel_formula | fe_text, which caused evaluation errors during tests. - Now, the full model formula is safely constructed as a string and parsed with
as.formula()to ensure compatibility withfixest::feols().
- Previously, fixed effects specified via the
- No breaking changes. This is a backward-compatible patch release with internal robustness improvements and enhanced error handling.
- Support for covariates in
run_es():- Covariates must now be specified as a one-sided formula (e.g.,
~ x1 + x2).
- Covariates must now be specified as a one-sided formula (e.g.,
- Fixed Effects and Clustering Interface Updated:
feandclusterarguments must now be specified using a one-sided formula (e.g.,~ id + year).- Character vector input for
clusteris still accepted. - Improved internal handling and validation of fixed effects and clustering variables.
- Improved error messages for invalid or missing variable names.
fe_varargument now supports additive notation (firm_id + year) instead of character vectors.- Improved
plot_es()efficiency and documentation.
- Fixed cluster variable handling to correctly reference column names.
- Updated test cases to support new
fenotation. - Improved package documentation.
This version introduced several enhancements and refinements to improve usability and maintainability.
- Refactored variable name handling:
outcome_var,treated_var, andtime_varare now processed usingrlang::ensym()for better robustness.fe_varandcluster_varhandling improved for more reliable column referencing.
- More informative error messages when variables are missing in the dataset.
- Enhanced baseline term handling in regression models to prevent incorrect factor levels.
- Improved
plot_es()function:- Added validation checks to ensure required columns (
relative_time,estimate, etc.) are present. - Adjusted confidence interval calculations to avoid missing values in error bars.
- Added validation checks to ensure required columns (
- Addressed an issue where
baselinehandling could lead to incorrect sorting of lead/lag terms. - Resolved a minor inconsistency in fixed effects variable name parsing.
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 usingfixest::feols(), generating lead and lag variables automatically.- Supports fixed effects (
fe_varas character vector). - Allows clustered standard errors via
cluster_var. - Handles time scaling through the
intervalargument.
- Supports fixed effects (
plot_es(): A function to visualize event study results with ggplot2.- Supports ribbon-style confidence intervals (
type = "ribbon", default). - Allows error bar visualization (
type = "errorbar"). - Customizable plot elements including colors, line styles, and reference lines.
- Supports ribbon-style confidence intervals (
- Fixed effects regression model using
fixest::feols(). - Automated creation of lead/lag dummy variables based on treatment timing.
- Baseline period exclusion to avoid multicollinearity.
- Support for custom time intervals (
intervalargument).
- Fixed effects must be specified as a character vector (
c("firm_id", "year")). - Clustered standard errors require variable names as character strings (
"state_id"). - No direct support for additive notation in
fe_var.