Changelog

v2.4.7

Development version, not yet on CRAN.

Version 2.4.6 was a development version on GitHub and was not released on CRAN; its changes are listed under v2.4.6 below and are part of this release.

Correctness fixes that change results (the full list, with issue numbers, is in NEWS.md):

  • estimand() now reads each bootstrap replicate through its own units. Its SEs for the overall ATT, APTT, log-ATT and the placebo and carryover series were too small unless the treated units came first in the panel and adopted at one time. imputed_outcomes(replicates = TRUE) now returns each replicate’s own cells, so the row count varies by replicate.
  • effect() and att.cumu() (and estimand("att.cumu")) now use the variance type stored on the fit. Parametric fits could get intervals centred near zero.
  • The cumulative ATT of att.cumu(), effect() and estimand("att.cumu") is now the running sum of the per-period ATTs. It used to be count-weighted, which gives a different number when the number of treated units changes over event time; att.cumu(weighted = TRUE) still returns it.
  • For jackknife fits, att.cumu(), effect() and estimand("att.cumu") now give the jackknife standard error of the cumulative ATT, computed as for the fit’s own standard errors, with a normal interval. att.cumu() reported one about \(\sqrt{N-1}\) times too small for \(N\) units, and effect() one \(\sqrt{N/(N-1)}\) times too large.
  • For parametric fits, effect() (and so estimand(fit, "att.cumu", "event.time")) now builds its intervals from the normal critical value, as att.cumu() does. It used a \(t\) critical value, so its intervals were slightly wider.
  • For fits made with W or W.agg, estimand() and effect() now use those weights, as fit$att and fit$att.avg do. estimand(fit, "att", "overall") now equals fit$att.avg, with the standard error of fit$est.avg.
  • For jackknife fits, the standard error of estimand(fit, "att", "overall") now equals that of fit$est.avg. It came from the leave-one-out averages of the unit-level ATTs, a different quantity.
  • fect_mspe() now refits a gsynth fit with gsynth(), so it scores the fit’s own model. It refitted fect’s default fixed-effects model.
  • For parametric fits with ci.method = "basic", the \(p\)-values of the effects now compare the estimate with the draws, which are simulated with no effect. They compared the draws with zero, which gave \(p\)-values near 1 whatever the estimate. The cohort effects of parametric fits with group get the same \(p\)-values under "basic".
  • Parametric SEs no longer depend on normalize; weights now work with the parametric bootstrap.
  • The bootstrap now resamples groups of one unit correctly. With one treated unit, every draw contains it; before, most draws lacked it and were dropped without a message. One reversal unit, and two never-treated units under vartype = "parametric", are also drawn correctly.
  • criterion = "pc" now picks the r with the lowest PC for method = "gsynth" and never-treated "cfe".
  • W.agg alone no longer enters the model fit when CV = TRUE.
  • wgt.implied now follows Xu (2017): \(\Lambda_{co}(\Lambda_{co}^\top\Lambda_{co})^{-1}\Lambda_{tr}^\top\), an \(N_{co} \times N_{tr}\) matrix labeled by unit ID. The simplex weights have the same layout.
  • loading.bound = "simplex" is now applied with cross-validation, without SEs for method = "gsynth", in the parametric bootstrap, and in the loo refits. The loo refits also keep time.component.from and para.error.
  • method = "ife" with time.component.from = "nevertreated" now fits the never-treated model with se = TRUE, as it did with se = FALSE, and stops where that model cannot be estimated.
  • method = "cfe" with time.component.from = "nevertreated" now takes its bootstrap and jackknife SEs, and the SEs of its loo placebos, from the never-treated model, as it does its point estimate. They came from the not-yet-treated model.
  • With parallel = FALSE, the same seed now gives the same cross-validation folds with and without se.
  • A factor time index is used in its level order, not sorted as text; if its levels are numbers out of numeric order, fect() warns. A character time index that holds numbers is read as numbers.
  • Exactly collinear covariates, and covariates absorbed by the fixed effects, are dropped with a warning (coefficient NA). Collinear covariates used to change the estimates silently.
  • The first row of att.cumu() now follows the rule of its other rows; it was copied from fit$est.att. So att.cumu() and effect() report the same interval and \(p\)-value in every row.
  • The cohort effects of fits with group (est.group.att) now follow ci.method. The normal and basic intervals and \(p\)-values were swapped.
  • For bootstrap fits with ci.method = "basic", the \(p\)-values now go with the basic interval: \(p\) is below \(\alpha\) exactly when the \((1 - \alpha)\) interval excludes zero. They were percentile \(p\)-values. The switch-off effects of subgroups get the normal \(p\)-value under ci.method = "normal".
  • att.avg.unit now averages each treated unit over its observed treated cells. Units with a missing outcome in any period were left out, and the value was NaN when every treated unit had one.
  • With normalize = TRUE, every slot of the fit is on the outcome’s scale, so the option changes only the speed of estimation. Before, fit$Y.dat, fit$est.cm, the stored covariates and data.long stayed divided by sd(Y); sigma2.fect, and so the default equivalence threshold, est$VNT, the gsynth IC and the CV tables were put back with the wrong power; and a lambda given to method = "mc" penalized the normalized outcome, so the estimates changed. A given lambda now means the same model with or without normalize.
  • method = "cfe" reads each unit’s Z and kappa group, and each period’s Q and gamma group, from that unit’s or period’s own rows. It read them from the first period and the first unit of the filled panel, where an absent row holds 0, so one missing row there changed the model and relabeling the units changed the estimates. A Z or kappa that varies within a unit, or a Q or gamma that varies within a period, now stops with a message.
  • For parametric fits with ci.method = "basic", the \(p\)-values of the effects and the coefficients now go with the basic interval, as for bootstrap fits. They agreed with it only up to the resolution of the draws.
  • The rolling cross-validation keeps a training floor of max(min.T0, 2 * (r.max + 1)) periods for every held-out unit, so no unit trains its loadings on a handful of periods and no held-out cell goes unscored; the default cv.rule is now "min", because fold errors are heavy-tailed and the one-standard-error rule under-selected factors in every design of a simulation study. The chosen r can change; fits return the fold standard errors as CV.out.se.
  • fect(CV = TRUE) with method = "ife" or "mc" returns the chosen model refit at tol, equal to the CV = FALSE fit at r.cv or lambda.cv; it returned the fit made inside the cross-validation loop at a looser tolerance.
  • Extra fixed-effect labels (index[3:]) are read per unit-period; on an unbalanced panel an absent row no longer forms a phantom group.
  • Rows a cross-validation loop did not score are NA, and outcomes on a large scale no longer stop or change the chosen r.
  • permute = TRUE runs again for method = "fe", "ife" and "mc"; every permutation used to be dropped.
  • method = "cfe" warns before iterating when a Q column is not identified from the untreated periods, naming the column, the EM rate and a remedy; the B-spline example on sim_trend was replaced by a quadratic trend for that reason.
  • fect_mspe() and r.cv.rolling() now score the model’s full prediction at each held-out cell, with the covariates and, for never-treated fits, the intercept and the fixed effects. Scores change, and r.cv.rolling() can choose a different r. When held-out units have few periods before the held-out block, a larger min.T0 gives a steadier choice. A held-out cell with a missing covariate is left out of the score; ?fect_mspe describes the limits for CFE fits with extra fixed effects in index.
  • fect_mspe() now refits a fit made with Y, D and X column names with the fit’s own arguments when the call passes them as variables, such as min.T0 = min.T0 or W = W. It used its own argument of the same name.

Input checks. Formula terms must be bare column names (log(Y), factor(g), X1:X2 now stop); X cannot be combined with a formula, repeat a name, or name the outcome or treatment; covariates must be numeric; a character time index must hold numbers; cl must be one column and, for the cluster bootstrap, constant within units with at least two clusters; vartype = "parametric" needs at least two never-treated units; cm = TRUE needs time.component.from = "notyettreated"; interFE() stops when its intercept, or its unit and time effects together, absorb a covariate; binary = TRUE stops at once, since binary outcome models are not supported in this version; estimand(fit, "att.cumu", ...) with cells stops and points to window (it ignored cells). Each stop says what to do instead.

New. plot(fit, type = "gap", id = ...) shows the gaps of the chosen treated units, each unit’s observed outcome minus its predicted untreated outcome; before, it ignored id and showed the average over all treated units. With one unit, the plot shows its gaps by time relative to its treatment onset; with several, their unweighted average at each relative time. On fits with parametric bootstrap draws (vartype = "parametric", keep.sims = TRUE), it adds a band around the gaps; other fits show point estimates only. One unit’s gap in one period is a noisy estimate of its effect, which is not identified. A control unit, or an id not in the data, stops with a message naming it. See Gaps of chosen units.

Other fixes. method = "ife" and "mc" no longer stop with an Armadillo index error (“size out of bounds”, “index out of bounds”) when a singular value decomposition fails to converge, which can happen on very unbalanced panels: the decomposition retries with LAPACK’s standard routine and otherwise stops with a message that names the cause; the cluster bootstrap works with clusters of unequal size and keep.sims = TRUE; failed bootstrap replicates are dropped and counted instead of stopping the run; cross-validation of r no longer crashes when r exceeds the number of never-treated units; counterfactual plots work without SEs; effect() works for one unit; method = "cfe" with never-treated fitting works with one treated unit; att.cumu() works without SEs; imputed_outcomes() reports the aggregation weights of weighted fits, one row per treated cell; fect_mspe() keeps working on fits made with Y, D and X column names; dloo with never-treated fitting stops at once, with a message that says why; clearer messages when cross-validation is skipped or no treated observation is left; cm = TRUE stores est.cm also without SEs and after cross-validation; fect_mspe() finds a fit’s data and arguments where it is called, so a data frame local to a function works, and a data frame named like a function (such as df) too; att.cumu() and estimand("att.cumu") work on a window of one event time; weighted jackknife fits with placebo or carryover tests no longer stop when the model has covariates; on jackknife fits, estimand() uses ci.method = "normal" by default for every type instead of stopping; estimand() messages name the cause of a stop; vartype = "parametric" warns that cl is ignored, and print() no longer shows “Cluster SE” for parametric and jackknife fits; a cells formula in estimand() and imputed_outcomes() can use variables of the function where it was written; plot(type = "factors") works with a Date or character time index; fit$remove.id lists the removed units whenever units are removed; the IFE cross-validation table under criterion = "pc" labels its columns correctly; interFE() names the fixed effect that absorbs a covariate.

v2.4.6

Development version on GitHub, not released on CRAN. Its changes are part of 2.4.7.

  • New dloo and dloo.adjust flags on fect(), contributed by Anton Strezhnev: double leave-one-out pre-trend placebos, computed without re-fitting. With covariates, the covariate coefficients are re-estimated on each cohort’s pre-treatment comparison sample. They require staggered adoption, a balanced pre-treatment panel, method = "fe", and the default time.component.from = "notyettreated". dloo.adjust = TRUE applies Ziyi Liu’s correction.
  • r now defaults to NULL. For method = "ife" or "gsynth", leaving out r now selects the number of factors by cross-validation over 0 to 5, instead of fitting zero factors.
  • Cross-validation fixes. Each can change the selected r or lambda for some existing calls:
    • Calls that name variables as strings, such as fect(Y = "y", D = "d", ...), now use the same defaults as formula calls (rolling cross-validation, nlambda = 10). Set cv.method = "block" to get the old folds.
    • seed now fixes the folds when se = FALSE. Before, it had no effect there.
    • With se = TRUE, cross-validation now uses cv.rule, cv.buffer, cv.donut, min.T0 and proportion as given. method = "gsynth" now respects cv.rule (#146).
    • method = "cfe" with time.component.from = "nevertreated" now respects cv.rule. With the default "1se" it can select a smaller r; cv.rule = "1pct" usually gives the old selection.
  • Fix: the leave-one-out equivalence (TOST) test now uses the same pre-treatment window as the F test.
  • cv.donut is now checked against cv.nobs for block cross-validation. A value that left no held-out period to score now stops with a clear message instead of an internal error.
  • User manual: the IFE leave-one-out example now sets r to 2 (thanks to Siyang Zhu, UPF, for the report). The Inference chapter’s migration example now passes data = data.

v2.4.5

(2026-05-30) CRAN release.

  • New group.fe argument on fect(): absorb additive fixed effects at a coarsening of the unit identifier (e.g., state FE on county-level data when treatment varies at the state level).
  • Fix: method = "cfe" with force = "time" / "unit" previously errored with Index out of bounds: [index='alpha'] / [index='xi'].
  • Removed the vestigial public sfe argument and the orphan internal R/polynomial.R (no live code reached either).

v2.4.4

(2026-05-19)

  • fect() now returns $sample, a logical T x N matrix (same dims as $Y.dat) marking cells the estimator used in any part of the procedure (main fit, placebo / carryover / balance tests). Compatible with panelView::panelview(sample = ...).

v2.4.3

(2026-05-14)

  • Fix future.globals.maxSize overrun in parallel bootstrap: quiet_nonpara wrapper no longer captures fect_boot()’s full frame.
  • Raise future.globals.maxSize to 2 GiB locally inside the parallel block.

v2.4.2

(2026-05-02)

  • New test = c("none", "placebo", "carryover") argument on estimand() closes issue #131.
  • New ci.method = c("normal", "basic") argument on fect(). Default "normal" is byte-equivalent to the v2.4.1 default. "basic" is the reflected pivot CI (Davison-Hinkley 1997 §5.2.1; boot::boot.ci(type = "basic")). All CIs in fect’s est.* slots use the requested method uniformly. Legacy quantile.CI is soft-deprecated — both legacy values still work (mapped to ci.method with a one-time warning).
  • New ci.method values "bc", "bca", "normal" on estimand(); per-type defaults updated. att.cumu default is "basic" (the reflected pivot CI recommended by Davison-Hinkley 1997 §5.2.1 and used by boot::boot.ci(type = "basic")); the raw-quantile "percentile" option is preserved for replication. The full 5-method surface lives on estimand(); fect’s built-in CI machinery covers normal / basic only.
  • New para.error argument for vartype = "parametric" selects the residual-error model: "auto", "ar", "empirical", "wild".
  • Tighter EM convergence defaults: tol from 1e-3 to 1e-5, max.iteration from 1000 to 5000, plus a warning() when EM hits max.iteration without converging. IFE/CFE point estimates shift a few percent (up to 40% on factor-heavy CFE) closer to the EM’s converged minimum. Pass tol = 1e-3, max.iteration = 1000 explicitly to reproduce pre-v2.4.2 numerical output.
  • estimand() warns when a tail-quantile CI method ("basic", "percentile", "bc", "bca") is requested on a fit with fewer than 1000 bootstrap replicates, recommending refit at nboots = 1000.
  • vartype = "jackknife" with Nco > 1000 emits a fit-time warning recommending vartype = "bootstrap" for tractability.
  • Various bug fixes.

v2.4.1

(2026-04-30) CRAN release.

Enhancement. estimand()’s vartype argument now accepts "parametric" in addition to "bootstrap", "jackknife", and "none". Works for all four type values when the fit was produced with fect(..., vartype = "parametric", keep.sims = TRUE).

Bug fix. estimand(fit, "att.cumu", ...) now populates n_cells on both event-time and overall paths, so esplot(..., Count = "n_cells") draws the bottom-bar treated-cell panel for cumulative plots.

v2.4.0

(2026-04-29)

New: post-hoc estimands API. Two new public functions and one new dedicated chapter in the user manual.

  • estimand(fit, type, by, ...) typed dispatcher returns a tidy data frame with consistent columns regardless of estimand type. Shipped types:
    • "att" — default per-event-time ATT; byte-identical to fit$est.att.
    • "att.cumu" — cumulative ATT; replaces effect().
    • "aptt" — average proportional treatment effect on the treated (Chen and Roth 2024 QJE).
    • "log.att" — mean log-scale treatment effect.
  • imputed_outcomes(fit, ...) low-level accessor returns the cell-level potential-outcome surface as a long-form data frame for custom estimands. Pipe to dplyr / data.table for arbitrary aggregation.
  • cells = filter (logical / formula) on both functions, with window = c(L, R) sugar on estimand() for the common event-time-window case.
  • direction = c("on", "off") selects the event-time grid for reversal panels.
  • New chapter on post-hoc estimands in the user manual walks through all six worked examples plus the migration table.

Soft-deprecation. effect() and att.cumu() emit a one-time-per-session message pointing at estimand(). They continue to work byte-identically; removal not before v3.0.0. Numerical equality with estimand() is asserted by package tests.

Internal slot reservation. eff_debias slot on the fit object is now reserved (NULL for plain imputation estimators); populated by future doubly-robust estimators so DR scores can be added without breaking the long-form schema.

v2.3.3

(2026-04-28)

Bug fixes.

  • diagtest() no longer crashes with “incorrect number of dimensions” when only one bootstrap column survives the all-non-NA filter (drop = FALSE at both filter sites in R/diagtest.R).
  • Bootstrap PSOCK cluster init now uses the shared rscript_libs = .libPaths() helper, wrapped in a 3-attempt retry-with-backoff. Eliminates the intermittent PSOCK race that surfaced in mixed CV+boot test runs; falls back to sequential bootstrap if doParallel also fails.
  • carryover.rm is now stored on the fit object. plot.fect() reads it from the slot rather than re-parsing x$call; correct under do.call(), programmatic wrappers, and any call-rewriting code path.

Documentation. Chapter 2 §Other estimands gains a worked example for post-hoc estimands derived from the imputed potential-outcome surface, showing APTT (Chen and Roth 2024) with bootstrap CIs from the existing fit slots.

v2.3.2

(2026-04-28)

Modern visual defaults for plot.fect() (visual breaking change):

  • All 14 plot types now render with a publication-ready recipe: white panel, plain left-aligned title, dashed treatment-onset vline, peach highlight rectangle behind placebo / carryover periods, pre/post lightness contrast, and compact legends.
  • Pass legacy.style = TRUE for byte-identical pre-2.3.1 reproduction. theme.bw = FALSE is soft-deprecated (removal in v2.5.0).
  • Placebo and carryover periods now render as a single accent glyph (orange triangle / blue diamond) per period, sized so the glyph reads as the same visual weight as the surrounding circles. Background rectangle behind each highlight period is opt-in via highlight.fill = TRUE; default is glyph only, which keeps figures clean for print and grayscale.
  • highlight argument now also accepts a character subset of c("placebo", "carryover", "carryover.rm") for per-test-type selective highlighting (e.g., highlight = "placebo" on a fit with both tests renders carryover periods as plain circles).
  • Stats-annotation block (placebo / carryover / F / equivalence p-values) sits at the top-left panel corner with symmetric inset and enough top padding to never graze the leftmost CI. Sized so it does not overpower the title.
  • Internal: migrated off the ggplot2 4.0 deprecated fatten / lwd arguments to the size / linewidth aesthetics.

v2.3.1

(2026-04-27)

Consistent ATT surface when W is supplied. fit$est.att, fit$est.avg, plot(fit), and print(fit) now all report the same W-weighted aggregation. The redundant *.W parallel slots are no longer attached to the fit object. To see the unweighted view, refit with W = NULL.

New W.est and W.agg arguments. Both default to NULL and fall back to W. Use W.est alone when the weight should enter the outcome-model fit only, or W.agg alone when it should enter the aggregation only. A clean fect-internal solution for inverse-probability weights for confounding adjustment is under development for v3.0.

Deprecations. The weight argument to plot.fect() is now a no-op (the canonical slots are already W-weighted when W is supplied). It emits a one-time warning and is slated for removal in v2.5.0.

v2.3.0

(2026-04-25)

Rolling-window cross-validation:

  • New cv.method = "rolling" in the main fect() CV dispatcher (also exposed as standalone r.cv.rolling()) implements the standard time-series rolling-window CV design adapted to panel data. Recommended default for serially correlated panels; closes the AR-leakage channel that causes block CV to over-select r (often pegging at r.max). Supports method in {"ife", "gsynth", "cfe", "mc", "both"}. New parameter cv.buffer (default 1, past-side buffer). Empirical validation: on a Xu (2017) DGP with \(r_{\text{true}} = 2\) and AR(1) \(\rho = 0.8\) (K = 200 reps), rolling CV recovers truth in 56% vs block CV’s 15% (mean r.cv = 1.58 vs 3.34).

1-SE rule is the new default for r selection (Hastie/Tibshirani/Friedman 2009 §7.10):

  • cv.rule = "1se" (default) picks the smallest r within one fold-SE of the minimum-CV-error r. Legacy cv.rule = "1pct" remains available for byte-identical reproducibility of pre-2.3.0 fits.
  • Default flip: existing CV = TRUE calls will produce different r.cv on the same data. Set cv.rule = "1pct" to recover prior behavior.

Bounded factor loadings for GSC:

  • New loading.bound = "simplex" (default "none") constrains treated-unit loadings to the convex hull of control loadings via an entropy-regularized simplex projection. Companion arguments: gamma.loading (regularization strength; NULL triggers 5-fold CV) and gamma.loading.grid. Diagnostic outputs: wgt.implied (row-stochastic simplex weights; since v2.4.7 stored as an \(N_{co} \times N_{tr}\) matrix whose columns sum to one), loading.proj.resid (extrapolation flag per treated unit). Currently method = "ife"/"gsynth" with time.component.from = "nevertreated" only.
  • Caveat: percentile-bootstrap intervals may under-cover when the constraint binds; boundary-corrected inference is deferred.
  • Companion plot: plot(..., type = "loading.overlap") shows treated loadings vs the control convex hull (\(r \geq 2\)) or mirror histogram with control-range band (\(r = 1\)).

Other changes:

  • cv.method default flipped from "all_units" (or "treated_units" in fect_nevertreated) to "rolling" across all CV entry points. Existing block-CV behavior remains available via the new cv.method = "block" value (renamed from "all_units"); the legacy values "all_units" and "treated_units" are still accepted but emit a one-time deprecation message and will be removed in v2.4.0 (replaced by the unified (cv.method, cv.units) API).
  • GSC path now populates Y.ct.full[, co] from \(F \hat\lambda_{co}\) at masked control positions (was NA); enables rolling CV scoring for method = "gsynth". ATT/gap/est.avg unchanged.
  • CV default flips paired for stable iid recovery: cv.prop = 0.1 and k = 20 across all CV entry points (fect, fect_cv, fect_binary_cv, fect_nevertreated, fect_mspe, r.cv.rolling). The (cv.prop = 0.1, k = 20) pair gives ~2× coverage of every eligible unit across folds while keeping per-fold mask size moderate. Sharpens the MSPE curve enough that the 1-SE rule recovers r_true reliably on iid panels with at-least-moderate factor signal-to-noise.
  • cv.donut default raised from 0 to 1 to match cv.buffer = 1 for rolling CV.
  • simdata regenerated with the latent factor contribution scaled by 2 (vs the original Liu-Wang-Xu 2024 DGP). The doubling raises factor signal-to-noise from 2.7 to 10.9, which makes the latent factor structure clearly recoverable by cross-validated rank selection on this dataset (the original simdata’s signal was comparable to noise, so MSPE-based rank selection landed within the 1-SE band of the truth without converging on it). All other DGP components (covariates, fixed effects, treatment effect, error) are unchanged. Existing user code that loads simdata will see different numerical Y values.
  • r.cv.rolling() extended to method = "cfe". CFE-specific args (Z, gamma, Q, Q.type, kappa, extra index columns) are forwarded via ... and held fixed; rolling CV varies only r.
  • fect_mspe() accepts cv.method = "rolling" for AR-leakage-resistant model comparison.
  • PSOCK worker .libPaths() propagation fix: parallelly::makeClusterPSOCK(rscript_libs = .libPaths()) used wherever future::plan(future::multisession, ...) previously appeared. Resolves “no package called ‘fect’” errors during Quarto book render and intermittent CRAN-check failures.
  • Phase A bootstrap (R/boot.R::draw.error) migrated from foreach %dopar% to future.apply::future_lapply; removes a stale doFuture::registerDoFuture() re-registration that caused an ~8× slowdown on variant (iii) parametric bootstraps in multi-fit sessions.

v2.2.1

(2026-04-22)

Parametric-bootstrap fixes (se = TRUE, vartype = "parametric"):

  • Parametric bootstrap on unbalanced panels now correctly reflects within-unit serial correlation. Prior versions used a diagonal residual covariance in the Gaussian draw, under-estimating ATT standard errors on serially correlated data by ≈ \(\sqrt{(1+\rho)/(1-\rho)}\) at AR(1) coefficient \(\rho\). Balanced panels and vartype ∈ {"bootstrap", "jackknife"} are unaffected.
  • Fixed Unsupported bootstrap method: fe crash when method = "gsynth" or "cfe" and CV selected \(r_{\text{cv}} = 0\). Reported against the gsynth wrapper.
  • Fixed one.nonpara dispatcher so ife+notyettreated, cfe+nevertreated, and cfe+notyettreated bootstraps route to the correct Loop-2 estimator (previously all three produced incorrect SEs).
  • Added hard gate that errors on ife + notyettreated + parametric — a coverage simulation showed ~80% vs 95% nominal. Use time.component.from = "nevertreated" or a non-parametric vartype.

Parallel cross-validation:

  • IFE, MC, and CFE cross-validation now run in parallel via future_lapply, dispatching (r, fold) (or (lambda, fold)) flat across workers. Auto-engages above \(N_{co} \times T \geq 20{,}000\) for IFE/MC and \(\geq 60{,}000\) for CFE.
  • The parallel argument now accepts five forms: TRUE, FALSE, "cv", "boot", c("cv", "boot"). Scalar forms are backward-compatible; string forms bypass the auto-threshold.
  • MC-only tradeoff: break_check short-circuit applies in serial mode only; parallel MC evaluates all candidate lambdas. Prefer parallel = FALSE for MC if the search typically terminates early.
  • Internal: extracted (rank, fold) logic into R/cv-helpers.R; fect_nevertreated.R parallel CV migrated from foreach %dopar% (fold-only) to future_lapply (flat r×k), which also resolves a latent worker-visibility issue in the prior foreach path.
  • Fix: vector parallel = c("cv", "boot") no longer errors in fect_boot, fit_test, permutation, fect_sens, or did_wrapper — five sites had legacy scalar parallel == TRUE / if (parallel) checks.

v2.2.0

(2026-03-27) CRAN release.

  • Added time.component.from parameter: "notyettreated" (default) or "nevertreated" controls which units provide the time-varying model components (time fixed effects, latent factors, and temporal dynamics). Replaces method = "gsynth" with method = "ife", time.component.from = "nevertreated".
  • CFE estimator now supports time.component.from = "nevertreated", enabling full CFE model components (additional FEs, \(Z/\gamma\), \(Q/\kappa\), latent factors) with never-treated estimation.
  • Added fect_mspe() for out-of-sample model comparison (MSPE, RMSE, MAD) across specifications.
  • Unified cross-validation: replaced cv.treat + mask.method with single cv.method parameter; added cv.sample k-fold CV for fect_nevertreated.
  • Improved EM convergence conditioning: R-level centering + C++ component-wise convergence (up to 2000x better component accuracy).
  • Removed deprecated methods: polynomial, bspline, cfe_old. Renamed internal fect_gsynth to fect_nevertreated.
  • Plot system overhaul: pre/post color distinction, highlight shapes (triangles for placebo, diamonds for carryover), new esplot() parameters (xangle, yangle, xbreaks, ybreaks, legendOff).
  • Fixed parallel bootstrap .export lists for CFE + nevertreated; fixed fect_mspe + CV=TRUE interaction; fixed start0 coloring; fixed .as_mask() dimension bug.
  • Renamed datasets for clarity: simdata1 → sim_base, simdata2 → sim_trend, simgsynth → sim_gsynth. Added sim_region and sim_linear. simdata kept for backward compatibility.
  • Fixed ggplot2 deprecation warnings: size → linewidth in all geom_hline, geom_line, and bound line aesthetics.
  • Fixed GGally::ggpairs loadings plot warnings by applying color/fill scales inside sub-panel functions.
  • Exit plot legends now read “Under treatment” / “Out of treatment” instead of “Pre-treatment” / “Post-treatment”.
  • CRAN compliance: resolved codoc mismatches in esplot.Rd, fect_mspe.Rd, plot.fect.Rd; removed duplicate data source (fect.RData); added globalVariables declarations.
  • 590 tests (up from 131), including 98 book-derived behavioral tests and 36 CFE bifurcation tests.

v2.1.1

  • Added fect_mspe() for evaluating out-of-sample prediction accuracy across model specifications.
  • Optimized CFE estimator performance: memory recycling, sparse data handling, reduced C++ deep copies.
  • Fixed sigma2 normalization crash and division-by-zero guards in bootstrap.
  • Fixed bootstrap NA and multicollinearity abort issues.
  • Fixed gsynth weight calculation and r=0 grand mean issue.
  • Fixed Q.type routing in CFE and handling of time periods with all units under control.
  • Switched parallel backend from future to parallelly.
  • Added CFE tutorial chapter to Quarto book.
  • Added codetools and tail imports required by new functions.

v2.1.0

(2025-10-27)

  • Added CFE (Complex Fixed Effects) estimator as method = "cfe": additional group FEs via index, time-invariant covariates with time-varying coefficients (Z/gamma), unit-specific loadings on known time bases (Q.type/Q/kappa), and latent factors (r).
  • Added B-spline option to Q.type for flexible nonlinear time trend approximation.
  • Added return.data = TRUE option to plot() for extracting plot data.
  • Added doRNG for reproducible parallel bootstrap results.
  • Switched parallel backend from future to parallelly.
  • Aligned gsynth bootstrap values and standard errors with the original gsynth package.
  • Reduced memory usage by removing unnecessary C++ deep copies.
  • Added min.T0 restrictions to prevent estimation with too few control observations.
  • Fixed various R CMD check issues for CRAN compliance.

v2.0.5

(2025-08-25) CRAN release.

  • Fixed various bugs due to changes in dependencies
  • Added limit to cores by default in parallel computing
  • Add new plot type = "hte"
  • Added R-CMD-check for merging

v2.0.2

(2025-05-07)

  • Fixed various bugs
  • Updated counterfactual plot
  • Added sensitivity analysis
  • Added did_wrapper() and esplot()
  • Added various plotting options
  • Improved documentation

v2.0.0

(2025-01-17) CRAN release.

  • Changed to new syntax
  • Fixed various bugs
  • Added get_cohort()
  • Merged in gsynth

v1.0.0

  • First CRAN version
  • Fixed bugs

v0.6.5

  • Replace fastplm with fixest for fixed effects estimation
  • Added plots for heterogeneous treatment effects
  • Fixed bugs

v0.4.1

  • Added a NEWS.md file to track changes to the package.