Bug fix (att_gt()): with control_group = "notyettreated" and no never-treated group, the presence of already-treated units (treated in or before the first period, possibly via anticipation) no longer affects the ATT(g,t) of the other groups. The last-treated cohort, which serves as the not-yet-treated comparison group in this design, was being deleted from the data along with the always-treated units — biasing the remaining estimates or turning them into NA — and is now retained. control_group = "nevertreated" was unaffected.
Bug fix (fix_weights = "varying" standard errors): on a balanced panel, fix_weights = "varying" reported standard errors (analytic and bootstrap, and all aggte() aggregations) that were exactly 2× too large. Point estimates were correct. The repeated-cross-section influence function is normalized over the 2·n_units stacked observations; folding the pre- and post-period halves to the unit level was missing the corresponding 1/2. Fixed in both code paths; verified against a Monte Carlo (the corrected SE now matches the empirical sampling standard deviation, and equals the panel-estimator SE when weights are time-invariant). Repeated cross sections and unbalanced panels were unaffected.
aggte() now fails gracefully instead of with cryptic errors when an aggregation has an empty or all-NA selection: type = "calendar" with na.rm = TRUE drops calendar periods whose post-treatment cells are all NA (analogous to the existing type = "group" guard) rather than erroring; and type = "simple"/"dynamic" with a min_e/max_e window that excludes every post-treatment period now return a clear message instead of an internal "report this as a bug"/"non-numeric argument" error.
att_gt() now rejects a negative gname up front with a clear message in both faster_mode = TRUE and FALSE. Previously negative cohort codes (out of spec — gname must be 0 for never-treated or a positive treatment time) were silently accepted on the fast path but errored on the slow path.
The parallel multiplier bootstrap (bstrap = TRUE, pl = TRUE, cores > 1, when the influence function has more than 2500 rows — i.e. more than 2500 units, or more than 2500 clusters when clustered standard errors are used) is now reproducible under a fixed set.seed(). It previously used the default Mersenne-Twister RNG inside parallel::mclapply(), whose forked workers re-seed non-deterministically, so bootstrap standard errors and uniform-band critical values drifted between identical-seed runs. It now uses L'Ecuyer-CMRG parallel RNG streams (and restores the caller's RNG kind on exit). Point estimates were always unaffected.
Fixed a crash in the parallel bootstrap when biters < cores (with pl = TRUE, cores > 1, and more than 2500 influence-function rows — units, or clusters under clustered inference): the per-core work split could produce a negative chunk size. The split is now always non-negative.
aggte(type = "calendar") now warns when min_e, max_e, or balance_e is supplied, since these event-study options do not apply to calendar-time aggregation (the unrestricted, correct calendar effects are returned, as before).
This is a large release that consolidates all development since 2.3.0. Headline changes: a substantially faster engine (group-time ATTs are roughly 2.5-3x faster in common settings, and the conditional pre-test is several times faster and far lighter on memory), first-class clustered and unbalanced-panel inference, support for transformation and factor covariates, a point-estimates-only mode, and a long list of correctness fixes. Numerical results are unchanged up to floating-point precision except where a bug fix is explicitly noted.
DRDID (>= 1.3.0), which provides faster and more robust 2x2 DiD estimators (used internally for every group-time ATT) and guards against silently-incorrect standard errors on ill-conditioned (near-singular) designs.New compute_inffunc argument in att_gt() (default TRUE). Set compute_inffunc = FALSE for a point-estimates-only run: it returns the group-time ATT point estimates (identical to a full run) without influence functions, standard errors, uniform bands, or the pre-test. This is faster and uses much less memory (no \eqn{n \times k} influence-function matrix is ever formed or bootstrapped), which helps for quick exploration or very large datasets. The result cannot be passed to aggte() (which now errors with a clear message); bstrap and cband are set to FALSE automatically.
Covariate formulas (xformla) may now use transformations and other model-matrix terms, e.g. ~ I(X^2), ~ log(X), ~ poly(X, 2), ~ X1 * X2. These previously errored because pre-processing stored the evaluated model frame (losing the raw variable and creating matrix-valued columns) instead of the raw covariates. Pre-processing now keeps the raw covariates so the design matrix can be rebuilt, and drops rows whose evaluated design is non-finite (e.g. log() of a non-positive value).
Factor covariates now produce exactly the same estimates, standard errors, and warning messages as adding their dummy columns by hand. Previously the faster_mode = FALSE path applied droplevels() within each 2x2 comparison, so a factor level absent from a comparison changed the design (and could error with "contrasts can be applied only to factors with 2 or more levels"). The design matrix is now built once over the full sample, with global factor levels, and row-subset per cell.
New fix_weights argument in att_gt() for explicit control over how time-varying sampling weights are resolved in each 2x2 comparison: NULL (default, prior behavior), "varying" (per-observation weights via the RC estimators), "base_period" (fix at g-1), or "first_period". See ?att_gt. A runtime message points users to it when time-varying weights are detected in balanced panel data.
att_gt() accepts ... to forward additional arguments to a custom est_method function.
Added nobs() S3 methods for MP and AGGTEobj objects (number of unique cross-sectional units), and statistic (t-statistic) and p.value (pointwise, two-sided) columns to tidy() output for both classes, following broom conventions.
The influence-function matrix returned in MP$inffunc now carries the unit ids as rownames (the idname values; an internal observation index for repeated cross sections), and its row-order contract is documented in ?att_gt and ?MP. The row ORDER is mode-specific -- faster_mode = FALSE sorts units by id while faster_mode = TRUE uses an internal (period, cohort, id) ordering -- so external consumers of the influence functions (e.g. sensitivity analyses or custom cluster aggregation) must align rows by rowname, never by position. Values are unchanged; only the labels are new.
att_gt(faster_mode = TRUE) (the default) is about 2.5-3x faster on common problems, with identical results. Each (g,t) cell previously built and extracted a data.table even though the cohort vectors are already materialized in the pre-computed tensors; the per-cell cohort is now a plain list of vectors passed straight to the DRDID estimators (panel, repeated cross-section, and unbalanced-panel paths), and the unbalanced-panel influence-function aggregation applies a sparse unit-aggregation operator built once per call (bit-identical to the previous per-cell rowsum()/data.table group-by, but without re-hashing the row-to-unit map for every (g,t) cell — about 2x faster end-to-end on large unbalanced panels). Earlier (g,t) work also feeds in: cumulative cohort sizes, cached pre-treatment periods, pre-computed cohort and period-membership masks on the repeated-cross-section / unbalanced-panel path, and sparse-triplet influence-matrix construction. One intentional edge-case change in the unbalanced-panel aggregation: NA/NaN entries in a 2x2 influence function (only reachable with a custom est_method that returns a finite ATT alongside a partially-NA influence function) now propagate to that unit's aggregated influence value, so the affected (g,t) standard error is NA, matching faster_mode = FALSE; previously the fast path silently zeroed them, understating the standard error.
The per-(g,t) overlap-check propensity logit (fit for every dr/ipw cell to detect propensity-score overlap violations) now uses fastglm's low-level entry point (fastglmPure) instead of the fastglm() wrapper, skipping its per-call input coercion and family/deviance bookkeeping. The fitted values -- and therefore the overlap decision -- are bit-identical.
Two further per-cell guard speedups, in both faster_mode paths, with bit-identical estimates, standard errors, influence functions, and warnings. (1) For intercept-only designs (xformla = NULL/~1, the default) the overlap and regression-feasibility guards use their closed forms -- an unweighted intercept-only logit fits every unit at mean(D), and the control-unit Gram matrix is the scalar control count -- skipping the per-cell logit fit entirely; within 1e-6 of the 0.999 cutoff the real fit is still used, so knife-edge decisions are unchanged. (2) For panel data with control_group = "nevertreated" and non-varying weights, the guard booleans are computed once per (group, covariate-period, weight-period) instead of being refit for every post-treatment cell of a group (the guards' inputs are bit-identical across those cells); failed cells still warn once per cell, and options(did.disable_check_cache = TRUE) restores the per-cell checks. Together these make a default no-covariate att_gt() run roughly 10-15% faster, with similar gains on covariate runs using never-treated controls.
att_gt(faster_mode = FALSE) builds the covariate design matrix once and assembles each 2x2 cell directly from precomputed per-period blocks (outcomes, weights, design) indexed by position, instead of rebuilding model.matrix() and reshaping (get_wide_data()) the long data for every cell. Bit-identical, with about half the transient allocation; options(did.disable_precompute = TRUE) restores the original per-cell assembly (the once-built design matrix is used either way, so the option is a debugging escape hatch for the cell assembly only, with identical results). The repeated-cross-section / unbalanced-panel slow path gets the same treatment: each (g,t) cell is assembled positionally from per-period row indices and plain column vectors precomputed once per call, replacing two full data.table subsets, a full-data %in%, and a droplevels() per cell (about 1.7x faster end-to-end on both the repeated-cross-section and unbalanced-panel paths, bit-identical, behind the same escape hatch); the fix_weights = "base_period"/"first_period" weight lookup likewise uses per-period vectors instead of a per-cell full-table subset. The repeated-cross-section / unbalanced influence-function aggregation now uses rowsum() instead of stats::aggregate() (about 40x faster on that step). faster_mode = TRUE and faster_mode = FALSE remain identical to numerical precision for every supported option.
Pre-processing for faster_mode = TRUE is leaner, with identical outputs: only the columns the call references (id/time/group/outcome/weights/cluster plus the raw xformla variables) are copied out of the input data, so wide data frames no longer pay a full-table copy (cutting the transient memory peak by roughly the size of the unused columns); the balanced-panel checks use an arithmetic row-count test instead of full by-unit groupings (the grouping now only runs when unbalanced units actually need to be identified); guaranteed no-op complete-case passes are short-circuited behind anyNA(); the temporary asif_never_treated/treated_first_period columns are replaced by local vectors; and the period/crosstable count tables are derived without re-grouping the unit-level table.
The conditional pre-test (conditional_did_pretest()) is several times faster end-to-end and far lighter on memory. Its multiplier bootstrap (test.mboot()) previously looped over biters draws, each multiplying the full n x k x nX influence array by fresh weights -- O(n^2 k) work and an O(n^2 k) transient allocation per draw (over 1 GB per draw at a few thousand units). It is now a single tiled matrix contraction (100x+ faster on that step, with the per-draw gigabyte allocations eliminated), numerically identical to the old loop up to floating-point summation order; the indicator() weighting function is also vectorized.
Internal speedups with identical results: vectorized the multiplier-bootstrap post-processing (mboot()), removed a duplicated n x k matrix construction in the aggregation estimated-weight influence term (wif()), preallocated the sparse influence-function assembly, and removed redundant work in pre-processing and simulation.
Clustered standard errors are now available without the bootstrap. With clustervars set and bstrap = FALSE, att_gt() and aggte() report cluster-robust standard errors computed analytically from the cluster sums of the influence function, at every aggregation level (group-time, simple, group, dynamic, calendar), and the pre-test Wald statistic is reported under clustering.
The cluster-robust multiplier bootstrap (mboot) now follows Callaway & Sant'Anna (2021), Remark 10: it draws one multiplier per cluster and aggregates the influence function to cluster sums. Identical to before for equal-sized clusters; correct cluster-sum aggregation for unbalanced clusters and repeated cross sections.
Clustered inference (bootstrap and analytical) is supported for panel data, unbalanced panels, and repeated cross sections, and is identical under faster_mode = TRUE and faster_mode = FALSE. For repeated cross sections without an idname, the internal observation id is used to align the cluster identifiers with the influence function (idname itself is required whenever panel = TRUE; see below).
aggte() no longer silently ignores a clustervars request it cannot honor (the aggregation can only use the cluster information att_gt() retained). It now warns -- including when an override names a different variable than att_gt() clustered on -- and falls back to non-clustered standard errors, instead of silently returning the i.i.d. error or crashing in mboot().
Fixed two faster_mode = TRUE clustered-standard-error bugs on unbalanced panels so the fast path reproduces the slow path: (1) the analytical clustered SE silently fell back to the i.i.d. error because the stored per-unit cluster vector was observation-length and no longer aligned with the influence function; and (2) in aggte(), the estimated-weight influence term was added in id-sorted order while the influence function is in first-appearance order, misattributing it and giving a wrong aggregated SE (point estimates were unaffected). Balanced panels and repeated cross sections were unaffected.
Fixed the conditional parallel-trends pre-test (conditional_did_pretest()), which had silently broken under R >= 4.0 and spuriously rejected almost always whenever there was more than one pre-treatment ATT(g,t) cell. The observed Cramér-von Mises statistic was left in (n_gt x nX) orientation while its bootstrap null distribution is (nX x n_gt), scaling the observed statistic by n / n_gt and driving the p-value to ~0. The root cause was ifelse(class(J) == "matrix", ...): class() of a matrix became length-2 (c("matrix","array")) in R 4.0, so ifelse() evaluated both branches and the no-transpose branch always won. The orientation is now selected with is.matrix().
aggte(type = "group", na.rm = TRUE) with a finite max_e no longer errors ("No valid att_gt() estimates found ...") when a group's only non-missing ATT(g,t) lies past max_e; the group filter now applies the same max_e window. The default max_e = Inf is unchanged.
Duplicated (idname, tname) rows (the same unit observed more than once in a period, a common long-format data-prep mistake) are now rejected with a clear error in both code paths. Previously only faster_mode = TRUE caught this; faster_mode = FALSE silently produced incorrect estimates.
A weightsname column with negative values or a non-positive mean is now rejected with a clear, identical error in both code paths, instead of silently producing NA/NaN estimates.
Fixed faster_mode = TRUE vs FALSE ATT disagreement when sampling weights (weightsname) vary across time: the fast path was always using first-period weights and now uses the same period's weights as the slow path.
Fixed influence-function aggregation for fix_weights = "varying" on balanced panels (now aggregates by unit id with rowsum() rather than assuming stacked order), and a length mismatch for fix_weights = "base_period"/"first_period" on unbalanced panels after weight-based unit dropping.
Fixed glance.MP() returning NULL for ngroup/ntime under faster_mode = TRUE.
Fixed an aggte() crash ("Error in get(gname): invalid first argument") when the group column is literally named gname and dreamerr >= 1.5.0 is installed (data.table's get() was intercepted; replaced with set()).
aggte() no longer modifies the data stored inside the MP object by reference: under faster_mode = TRUE it previously recoded the never-treated gname value Inf to 0 in MP$DIDparams$data as a side effect. The input object is now left untouched; all results are unchanged.
Fixed groups treated after the last observed period but within the anticipation window being coerced to never-treated (contaminating the control group with anticipation effects), and a data-filter inconsistency for always-treated units when anticipation > 0. Added an informative message clarifying that never-treated units are unaffected by anticipation.
When internal 2x2 estimation fails for a specific (g,t) cell (e.g. a singular design), att_gt() now warns and sets that cell's ATT to NA instead of crashing, in both faster_mode = TRUE and FALSE (#185, #190).
pl = TRUE on Windows now warns and falls back to sequential processing instead of crashing (#176).
Misspelled yname, idname, tname, gname, weightsname, or clustervars now produce a clear message listing the missing columns (#203).
Column names reserved for internal use by did (.w, .rowid, .G, .C, post, asif_never_treated, treated_first_period) are now rejected with a clear error when used as yname, tname, idname, gname, weightsname, or clustervars, or referenced in xformla. Previously they could silently collide with the columns did creates internally; rename such columns before calling att_gt().
control_group and base_period must now exactly match one of their documented values, in both modes. Previously faster_mode = TRUE accepted partial and case-insensitive abbreviations (e.g. control_group = "never"), and faster_mode = FALSE silently treated any unrecognized base_period value as "varying".
An invalid est_method (an unrecognized string or an unquoted variable) now errors clearly instead of silently defaulting to "dr" (#194).
fix_weights = "base_period"/"first_period" are blocked for repeated cross sections (panel = FALSE); fix_weights = "varying" is blocked with a custom est_method function (whose signature differs from the internal RC path). Both with clear messages.
anticipation must now be a non-negative number in both modes. Previously only faster_mode = TRUE enforced this; the faster_mode = FALSE path silently accepted negative values (shifting the base period later than the treatment period), which was undocumented and inconsistent across modes.
panel = TRUE (the default) without idname now errors with "Must provide idname when panel = TRUE. Set panel = FALSE for repeated cross sections." Previously this failed with a cryptic internal data.table error (faster_mode = TRUE) or a misleading "All observations dropped while converting data to balanced panel" message (faster_mode = FALSE).
A non-numeric outcome variable (yname) is now rejected up front with a clear message in both code paths (logical 0/1 outcomes remain allowed). Previously a character or factor outcome "ran" to completion with all-NA ATTs and misleading per-cell warnings, and a list-column outcome failed with a cryptic complete.cases() error.
The per-(g,t) regression-feasibility check now reports the real cause when it fails: "Covariate matrix for control units is singular or numerically ill-conditioned ... consider centering/rescaling covariates or removing collinear terms" instead of the misleading "Not enough control units ... to run specified regression" (which fired even with thousands of control units, e.g. for a quadratic in a year-scale covariate). The check itself is unchanged (and now uses crossprod()); affected cells still return NA with a warning.
alp must now be a single number strictly between 0 and 1 (e.g. alp = 1.5 previously inverted the confidence bands silently or errored deep inside quantile()), and biters must be a single positive whole number when bstrap = TRUE (a negative value previously crashed inside the bootstrap's linear-algebra code with no hint about the cause).
Cleaner failed-cell warnings under faster_mode = FALSE: each failed (g,t) cell now warns exactly once with the same text as faster_mode = TRUE ("overlap condition violated for group g in time period t"). Previously the slow path warned twice per failed overlap/rank check -- the diagnostic plus a wrapper warning leaking the internal sentinel ("... : overlap. The ATT for this cell will be set to NA."). Genuine estimator errors are still surfaced by the wrapper warning. Additionally, when the Wald pre-test is unavailable, the warning now distinguishes "pre-treatment ATT(g,t) estimates exist but all have missing/zero variance" from "no pre-treatment cells exist at all" (the latter previously mis-diagnosed the former as "all groups are first treated early in the panel").
The documented clustervars contract -- at most one cluster variable beyond idname, and it must be time-invariant within unit -- is now enforced up front in both code paths, with one shared, plainly-worded error message (also used by mboot()). Previously faster_mode = FALSE with bstrap = FALSE (the analytical clustered-SE path) accepted extra cluster variables and silently clustered on the first one only; a time-varying cluster variable on that path triggered a fallback warning advising bstrap = TRUE, advice that then errored in mboot() for the very same input; and the faster_mode = TRUE error exposed internal argument names ("args$clustervars must be ... a character scalar"), contradicting the documented vector interface.
Per-cell empty-cell warnings under faster_mode = FALSE now name the period that is actually empty. Under base_period = "universal" (and for post-treatment cells under "varying"), the repeated-cross-section path warned "No units in group g in time period t" with the cell's current period -- a period where the group does have observations -- instead of the empty base period; faster_mode = TRUE already reported the base period correctly.
Aligned two pre-processing warnings across modes: the faster_mode = TRUE "no never-treated group" warning now also discloses that data from periods on/after the last cohort's treatment date is filtered out (both modes always dropped those periods; only the slow path said so), and the faster_mode = FALSE balanced-panel coercion warning now reports the number of dropped units ("k units are missing in some periods. Converting to balanced panel by dropping them.", same text as faster_mode = TRUE) instead of mislabeling the unit count as "observations" (an undercount of the rows actually removed).
Reduced namespace pollution: replaced blanket import(stats), import(utils), and import(BMisc) with selective importFrom() calls. did no longer re-exports stats::filter/stats::lag (which previously masked dplyr::filter/dplyr::lag). R CMD check passes with 0 code-related NOTEs.
Replaced fragile ifelse(cond, x <- a, x <- b) side-effect idioms (which relied on R's branch-evaluation order) with if/else, and get()/:= with set() inside data.table loops, throughout; behavior is unchanged.
Substantially expanded the test suite: glance(), ggdid(), error handling, edge cases, all aggregation types, systematic faster_mode consistency across dozens of parameter combinations, and JEL replication tests. The suite now runs with 0 warnings (previously 66+). Added a GitHub Action to auto-bump the dev version in DESCRIPTION on PR merge.
Expanded weightsname documentation (how time-varying weights are handled for balanced panels vs. repeated cross sections / unbalanced panels); grammar and typo fixes across docs, vignettes, and error messages; corrected the mpdta data documentation.
Replaced deprecated BMisc function names (getListElement, rhs.vars) with their snake_case equivalents (get_list_element, rhs_vars).
Code improvements that make the package faster and more memory efficient
Improved automated testing and regression testing
Check if data is balanced if panel = TRUE and allow_unbalanced_panel = TRUE. If it is, disable allow_unbalanced_panel and proceed with panel data setup. This is different from the previous behavior, which would always proceed as if panel = FALSE.
Significantly reduced the number of recursive package dependencies, enabling faster installation times and a smaller build footprint.
Added wrapper function for HonestDiD package
Fix bug for setups where gname is not contained in tname (but is in the tname range)
Fix bug for including too many groups with universal base period in pre-treatment periods
Bug fix for anticipation using notyettreated comparison group
Bug fixes related to unbalanced panel and clustered standard errors
Bug fixes for conditional_did_pretest
Even faster bootstrap code (thanks Kyle Butts)
Updated version requirement for BMisc package
Bug fix for unbalanced panel and repeated cross sections in pre-treatment periods using universal base period
Code is substantially faster/more memory efficient
Support for universal base period
Major improvements to unit testing
Completely removed mp.spatt and mp.spatt.test functions (which were the original names for att_gt)
Simulation/testing code now exported
Removed some slow running checks
Multiplier bootstrap code is now written in C++
Improvements to error handling, added some additional warning messages, removed some unnecessary warning messages
Bug fixes for NA standard errors that occur with very small groups
Improved plots
Maximum event time for event studies
Compute critical value for simultaneous confidence bands even when some standard error is zero (set these to NA)
Improved codes for unbalanced panel data: faster and more memory efficient
Correct estimates of P(G=g|Eventually treated) with unbalanced panel data. This affects aggte objects with unbalanced panel data
Bug fixes for summary aggte objects
Allow clustering for unbalanced panel data
Fixed error in calendar-type aggregation within aggte function (point estimates were not being weighted by group-size; now they are).
Additional error handling
Big improvement on code base / functionality / testing
Deprecated mp.spatt function and replaced it with att_gt function
Calling att_gt is similar to calling mp.spatt; instead of formula for outcome of the form y~treat, now just pass the name of the outcome variable
Deprecated mp.spatt function and replaced it with conditional_did_pretest function
New est_method parameter. Can call any function for 2x2 DID in the DRDID package (default is now doubly robust estimation, but inverse probability weights and regression estimators are also supported) as well as provide custom 2x2 DID estimators
Bug fixes for including groups that are already treated in the first period
Allow for user to select control group -- either never treated or not yet treated
Add functionality for uniform confidence bands for all aggregated treatment effect parameters
Introduced dynamic effects in pre-treatment periods. These allow for users to report event study plots that are common that include pre-treatment periods and are common in applied work. The event study plots in the did package are robust to selective treatment timing (unlike standard regression event study plots)
Support for using repeated cross sections data instead of panel data is much improved
Support for using sampling weights is much improved
Big improvement to website, vignettes, and code documentation
Code for dealing with unbalanced panels
Allow for event studies to be computed over subsets of event times
Allow for treatment anticipation via anticipation argument
Improved ways to summarize aggregated treatment effect parameters
Fixed bug related to needing new version of BMisc
Fixed bug related to plotting with no pre-treatment periods
Improved ways to easily plot aggregated treatment effect parameters
Added some error handling for some cases with small group sizes, and fixed some cryptic error messages
Fixes handling for data being in format besides data.frame (e.g. tibble)
Add warnings about small group sizes which are a very common source of trouble
fixed issues between BMisc and formula.tools