Conversation
Add gsynth callout tip, link gsynth/panelView user manuals, fix table alignment and minor formatting. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fect_nevertreated() relabelled its outgoing $method from "gsynth"
(or "cfe") to "fe" whenever cross-validation selected r.cv = 0. The
bootstrap dispatcher in fect_boot reads out$method and has branches
only for gsynth / ife / mc / cfe -- so every se = TRUE call where CV
picked the two-way-FE model crashed with:
Error: Unsupported bootstrap method: fe
(Reported by John Macdonald against gsynth; gsynth delegates SE to
fect.) The point estimate was fine because the estimation path in
fect_nevertreated handles r = 0 internally; only the SE path tripped
on the relabel.
Fix: keep method = "gsynth" (or "cfe") regardless of r.cv. The
existing "gsynth" / "cfe" branches in fect_boot's dispatcher already
route to fect_nevertreated, which estimates correctly at r = 0.
Adds tests/testthat/test-gsynth-r0-boot.R: pure two-way-FE DGP that
deterministically drives CV to r = 0, checks $method stays "gsynth"
and the se = TRUE call returns without error.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- DESCRIPTION: 2.2.0 -> 2.2.1, Date -> 2026-04-21 - NEWS.md: add 2.2.1 entry describing the bootstrap-dispatcher fix in cfff93e (preserve $method after CV when r.cv = 0) - .Rbuildignore: exclude cell-D1.rds, local build tarballs, and .Rcheck directory from the package build Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix: preserve method after CV when r.cv = 0 (gsynth/cfe boot crash)
- Add R/impute_Y0.R: dispatcher routing (method, predictive) to the correct underlying estimator (fect_nevertreated/fect_fe/fect_cfe) - Add R/valid_controls.R: control-screening helper replacing inline r.cv+1 logic in boot.R - R/boot.R: replace draw.error cascade and one.nonpara cascade with single impute_Y0 calls; add extra-FE slicing in Loop 2; update .export list; delete dead Branch B (L1131-1296 pre-refactor) - R/default.R: add hasRevs+parametric hard gate after hasRevs is computed (L1725) - Add tests/testthat/test-paraboot-dispatcher.R: unit tests for dispatcher error paths, valid_controls thresholds, and hasRevs gate Bug fixed: Loop 2 (one.nonpara) previously called fect_nevertreated unconditionally regardless of method/predictive. Now impute_Y0 dispatches correctly for ife+notyettreated, cfe+nevertreated, cfe+notyettreated combinations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add §Parametric bootstrap: valid regimes to vignettes/02-fect.Rmd:
three-gate table, Monte Carlo evidence (80.6% undercoverage for
ife+notyettreated+parametric), and migration recipe. Extend
\item{vartype} in man/fect.Rd to name all three values, state the
parametric restriction (Gates A/B/C), and recommend bootstrap as
the safe default.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds validation error when se=1, vartype="parametric", and time.component.from="notyettreated". Mirrors existing hasRevs+parametric gate (L1734-1739). Coverage-sim (2026-04-13-paraboot-coverage) showed ~20% undercoverage for ife+notyet; theoretical analysis confirms EM shrinkage bias. Gate aligns code with stated policy (Liu-Wang-Xu 2024 H1). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Convert 5 spec-listed collision tests to expect_error (Phase 3a/3b/3c, Phase 3a-F3 in test-factors-from-refactor.R; BUG-1a in test-paraboot-parity.R) - Convert 5 unexpected collisions discovered via grep audit (BUG-3a, BUG-1c, BUG-3c, BUG-4, BUG-6 in test-paraboot-parity.R) — all ife/cfe+default+parametric calls that now hit the notyettreated gate - Add test-parametric-notyet-gate.R with 7 new tests: NGP-1/2/3/4 (gate fires for ife+default, cfe+default, ife+explicit-notyet, message check) and NGN-1/2/3 (gate does NOT fire for gsynth, ife+nev, cfe+nev) - Full suite: 650/650 pass, 0 fail StatsClaw run: 2026-04-14-notyet-parametric-gate
Keep Version at 2.2.1 (same-day release). Describe the two backported fixes as additional bullets under the existing 2.2.1 section rather than opening a new version. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
backport: paraboot dispatcher fix + notyettreated parametric gate
Mirrors the NEWS.md 2.2.1 entry: r=0 boot crash, paraboot dispatcher refactor, notyet+parametric gate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 2.2.1 changelog entry contained \`r = 0\` which knitr reads as
inline R code (the \`r <expr>\` inline-eval syntax). Replaced with
math-mode \$r = 0\$ and \$r_{\text{cv}} = 0\$ so the Quarto book renders
cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ted parallel branch
- New R/cv-helpers.R: six .fect_cv_score_one_* helper functions covering all
four CV inner-loop call sites (cv.R IFE, cv.R MC, fect_nevertreated IFE,
fect_nevertreated CFE), plus .CV_PARALLEL_THRESH constant.
- R/default.R: new five-value parallel API (TRUE/FALSE/"cv"/"boot"/c("cv","boot"));
computes do_parallel_cv / do_parallel_boot; propagates to fect_cv; replaces
parallel==TRUE/FALSE gates in bootstrap setup with do_parallel_boot.
- R/cv.R: adds do_parallel_cv/do_parallel_boot params to fect_cv signature;
IFE notyettreated gains flat r×k future_lapply parallel branch gated by
Nco*TT > 20000 (auto) or explicit "cv" override; MC inner loop migrated to
helper (serial, Phase 2 will add parallel); nevertreated delegation passes
do_parallel_cv.
- R/fect_nevertreated.R: adds do_parallel_cv param; migrates all four fold-
parallel foreach bodies (IFE all_units, IFE treated_units, CFE all_units,
CFE treated_units) to use helpers; serial fallback also migrated via lapply;
parallel gate updated to honor do_parallel_cv and explicit "cv" override.
- man/fect.Rd: updated parallel parameter documentation with five-value table.
- NEWS.md: 2.2.2 entry documenting Phase 1 changes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug #1 (cv.R): parallel branch assigned r.cv from r_seq[i] (unnamed column vector element). Added names(r.cv) <- "r" after the aggregate loop so the parallel path matches the serial path's c(r=N) form. identical(fit_seq$r.cv, fit_par$r.cv) now TRUE. Bug #2 (boot.R + default.R): fect_boot() did not accept do_parallel_cv and did not forward parallel/cores/do_parallel_cv to fect_cv(). Added do_parallel_cv parameter to fect_boot signature, threaded it from default.R's fect_boot() call, and added parallel/cores/do_parallel_cv to the fect_cv() call inside boot.R. CV parallel banner now appears when se=TRUE and parallel="cv". Bug #3 (cv.R): parallel branch guard used criterion != "PC" (uppercase) but canonical form is "pc" (lowercase). Changed to criterion != "pc". criterion="pc" with parallel="cv" no longer errors with "argument is of length zero". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers all test-spec.md sections for REQ-parallel-cv-phase1:
P (numerical identity): P.1 IFE notyettreated all_units serial==parallel,
P.2 IFE notyettreated treated_units serial==parallel, P.3-P.6 nevertreated
helper-migration fixture regression (IFE/CFE × all_units/treated_units).
A (API back-compat): A.1 parallel=TRUE backward compat, A.2 parallel=FALSE
determinism, A.3 invalid parallel raises error.
S (selective switches): S.1 parallel="cv" emits CV banner/no boot banner,
S.2 parallel="boot" emits boot banner/no CV banner.
T (threshold gating): T.1 parallel=TRUE on small panel gates to serial,
T.2 parallel="cv" overrides threshold, T.3 override produces correct output.
R (plan restoration): R.1 plan restored after normal return,
R.2 plan restored even on error path.
D (reproducibility): D.1 same seed two parallel runs bit-identical.
L (LOO unaffected): L.1 uses time.component.from="nevertreated" (spec error
corrected per audit — LOO+notyettreated is invalid; mirrors G.7 approach).
E (edge cases): E.1 k=1 no error, E.2 cores=1 no error, E.3 parallel=FALSE
ignores cores, E.4 c("cv","boot") accepted, E.5 criterion="pc" no error
(Bug #3 regression guard), E.6 boot+CV no deadlock.
Also stages four nevertreated fixture RDS files (written by tester).
Full suite: FAIL 0 | WARN 0 | SKIP 0 | PASS 699 (655 prior + 44 new).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add parallel execution branch for MC notyettreated CV in R/cv.R: - Gate: cv_mc_parallel = do_parallel_cv && (Nco*TT > 20000 || explicit "cv") - Flat (lambda, fold) task list dispatched via future_lapply - Sequential master aggregation applies 1% rule; no break_check in parallel mode - Distinct variable names: use_explicit_cv_mc, old.future.plan.mc - on.exit(..., after=FALSE) ensures correct LIFO-equivalent plan restoration when method="both" (MC restore prepended, fires before IFE restore) - Remove dead variable .inter_fe_ub_local from IFE parallel block (Phase 1 review) - Extend NEWS.md Phase 1 entry with Phase 2 bullets - Add MC parallel note to man/fect.Rd parallel parameter description - Add M.1-M.6 test assertions to tests/testthat/test-cv-parallel.R - Full suite: PASS 708 / FAIL 0 / WARN 0 / SKIP 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Migrates both IFE and CFE parallel CV branches in fect_nevertreated.R from foreach %dopar% (k-fold only) to flat r×k future_lapply dispatch. Adds CFE nevertreated parallel support with the same pattern as IFE and MC. Removes doFuture::registerDoFuture() calls. Uses on.exit(after=FALSE) LIFO discipline. All eight migration regression cases pass within 1e-10. Adds 27 new test assertions (C and N sections) to test-cv-parallel.R. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…N.6 formal test - man/fect.Rd: add mc = 20000 to parallel arg threshold table (was missing; ife and cfe were listed but mc was omitted despite Phase 2 adding MC parallel) - NEWS.md: add explicit latent-bug bullet under Phase 3 entry — prior foreach %dopar% parallel CV in fect_nevertreated.R (all_units) was non-functional; migration to future_lapply(future.packages="fect") fixes it - R/cv.R dead variable (.inter_fe_ub_local): already removed in Phase 2 commit 5531166; confirmed absent — no change needed - tests/testthat/test-cv-parallel.R: add N.6 formal test_that block — plan restored after sequential IFE then CFE nevertreated parallel CV calls (was spot-check only in Phase 3 audit; now permanent, 2 expect_equal assertions) Test count: 726 + 1 = 727 PASS, 0 FAIL (E.6 pre-existing flake not triggered)
Two unrelated cleanups packaged together since both are pre-CRAN polish for
the parallel-CV feature:
- R/cv-helpers.R: drop the `fect:::` prefix on `inter_fe_ub`, `inter_fe_mc`,
and `complex_fe_ub` calls (4 sites). The `:::` was added defensively for
parallel worker visibility, but `future_lapply(future.packages = "fect")`
loads the package on workers, making internal symbols accessible without
qualifier. R CMD check --as-cran was flagging the self-references as a
NOTE; this clears it. cv-parallel test suite (73 tests) still passes.
- vignettes/02-fect.Rmd: extend the parallel-computing callout to document
the new five forms of the `parallel` argument (TRUE / FALSE / "cv" /
"boot" / c("cv", "boot")) with a behavior table.
- vignettes/03-ife-mc.Rmd: rewrite the "Parallel computing" subsection in
the cross-validation chapter. Document method-specific auto-thresholds
(IFE/MC = 20k, CFE = 60k), the (rank, fold) flat dispatch topology, the
MC `break_check` tradeoff in parallel mode, and the CV-vs-bootstrap
independent-control sugar.
- vignettes/bb-updates.Rmd: add v2.2.2 changelog entry covering the new
parallel CV (cv.R notyettreated for IFE/MC), the fect_nevertreated.R
flat-dispatch migration (with latent worker-visibility bug closure), and
the five-form parallel argument.
R CMD check --as-cran on the resulting branch: 1 WARNING, 1 NOTE — exact
pre-existing baseline (macOS clang header warn + HonestDiDFEct GitHub
Suggests note). 0 new issues. devtools::test() passes 727/727.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… changelog merge Ships parallel-CV entries under the existing v2.2.1 header (no version bump). - 03-ife-mc.Rmd: drop space in inline code `r=c(0,5)` / `k=10` so knitr does not parse it as inline R (the `r<space>` trigger). - index.qmd: add hidden setup chunk so Quarto engages the knitr engine; merge Source Code and Version into a single callout with an auto-derived "rendered against fect X.Y.Z on YYYY-MM-DD" line read from DESCRIPTION. - 09-sens.Rmd: flip hh_honest_placebo chunk to cache=TRUE. The bootstrap fit re-executed on every render; other chunks in the chapter were already cached. Cuts re-render time on this chapter to seconds. - bb-updates.Rmd, NEWS.md: merge the parallel-CV bullets into the v2.2.1 entry (date 2026-04-22). Keeps version and changelog self-consistent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1-3 handled the new parallel API in default.R / cv.R via the
do_parallel_cv / do_parallel_boot flags, but five downstream sites kept
scalar `parallel == TRUE` / `if (parallel)` checks that errored with
"the condition has length > 1" on vector input. Caught by an Alsaadi
(2025) real-workload smoke on 2026-04-22 (gap fit 374.7s serial-CV →
212.4s with parallel=c("cv","boot")+cores=18; ATT identical).
- R/boot.R: compute do_parallel_boot once, use at both fect_boot check sites
- R/fittest.R, R/permutation.R, R/did_wrapper.R, R/fect_sens.R: use
!identical(parallel, FALSE) as the generic "parallelize me" test
- tests/testthat/test-parallel-vector-form.R: regression test that exercises
parallel = c("cv","boot") through fect -> fect_boot with se = TRUE
Also bundled (all motivated by the debug session that found the bug):
- Test infra: tests/testthat.R uses LocationReporter + JunitReporter for
non-interactive captured runs; CI workflow uploads test-results.xml and
surfaces per-test results as PR annotations on the Ubuntu leg. Gated on
NOT_CRAN or GITHUB_ACTIONS so CRAN's default behavior is unchanged.
- Docs: v2.2.1 changelog (bb-updates.Rmd, NEWS.md) tightened into two
subheadings plus a bullet calling out this vector-form fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Parallel cross-validation + parametric-bootstrap fixes (v2.2.1)
…trap Changes cov.ar from 0 to TT-1L at the two res.vcov() call sites for vcov_tr and vcov_co on the unbalanced-panel code path (R/boot.R:900,904). Prior behavior zeroed out all off-diagonal entries of the empirical covariance matrix, causing rmvnorm() draws to be iid across time. On serially-correlated residuals with AR(1) coefficient rho > 0, this under-estimated ATT SEs by sqrt((1+rho)/(1-rho)). Balanced-panel path (else branch at R/boot.R:992-1003) uses direct whole-T vector resampling and is unaffected. The nonparametric "bootstrap" and "jackknife" vartypes are also unaffected (both unit-level by construction). Empirical validation on EH 2023 edu_eq (random placebo treatment, 100 reps, true ATT = 0): GSC parametric coverage moves from 0.28 to 0.99 with this fix, matching the theoretical VIF = (1+rho)/(1-rho) ~= 27.6 at residual AR(1) = 0.93. Tests: - New regression test in tests/testthat/test-cov-ar-parametric-boot.R: - T2a: unbalanced AR(1) panel, ratio of new/old SE matches theory. - T2b: balanced panel, output bitwise identical (no regression). - T2c: rho = 0 control, no systematic effect. - Full devtools::test(): 737 / 0 FAIL / 0 WARN / 0 SKIP. - R CMD check --as-cran: 0 ERROR; 1 WARN + 1 NOTE both pre-existing. Docs: - NEWS.md: bullet under existing v2.2.1 entry (version not bumped; 2.2.1 has not yet shipped to CRAN). - vignettes/bb-updates.Rmd: matching changelog entry. - vignettes/07-gsynth.Rmd: new "Parametric bootstrap on an unbalanced panel" subsection demonstrating vartype = "parametric" on turnout.ub alongside the default vartype = "bootstrap"; explains the Gaussian-on-pairwise-complete-empirical-covariance approximation, distinguishes the conditional and unconditional inferential targets of the two procedures, includes side-by-side gap plots. Scoping log, spec, test-spec, and sim-spec under statsclaw-workspace/fect/runs/2026-04-24-cov-ar-unbalanced-bootstrap-fix.md and runs/REQ-cov-ar/.
fix(boot): preserve serial correlation on unbalanced parametric bootstrap
- entropy-regularized simplex projection for bounded loadings - thread loading.bound through R/boot.R (main fit + bootstrap dispatch) - harden mirror-descent solver against NaN in fallback path - cache CV-selected gamma across bootstrap replications
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update the existing 2.3.0 rolling-CV entry to reflect the Stage 3 fold-in: both method = "ife" and method = "gsynth" are supported (the latter via the companion Y.ct.full GSC-population change in fect_nevertreated.R, see "GSC: Y.ct.full populated at control positions" entry on the Stage 2 PR). The deferred-dispatcher note is kept and revised to point users at the two-step pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the codoc-mismatch WARNING on R CMD check --as-cran:
Code: method = c("ife", "gsynth")
Docs: method = "ife"
Triggered by the Stage 3 fold-in (a8bc1bb) that broadened method
from a free-form string with `identical(method, "ife")` guard to
match.arg'd c("ife", "gsynth"). Roxygen source in R/cv-rolling.R
was already updated; only the .Rd file lagged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(boot): Phase A foreach → future_lapply, drop doFuture pollution
# Conflicts: # NEWS.md
feat(gsc): populate Y.ct.full at control positions with F*t(lambda_co)
feat(cv): rolling (forward-only) CV for rank selection
…aths fix Squash of 4 commits implementing the v2.3.0 rolling-window CV API. `r.cv.rolling()` standalone: * Rewritten as standard ML rolling-window CV: random per-unit anchors + drop-after-holdout + `cv.buffer` past-side buffer + `k` folds + per-fold unit sampling at `cv.prop`. Replaces the prior tail-only deterministic implementation that shipped on the v2.3.0 dev tip via PR #119. * Extended to `method = "cfe"`. CFE-specific args (Z, gamma, Q, Q.type, kappa, extra index columns) flow through `...` and are held fixed; rolling CV picks `r` only. `index` length check relaxed from ==2L to >=2L so extra grouping FE columns flow through unchanged. Dispatcher integration: * `cv.method = "rolling"` wired through the main `fect()` CV dispatcher for all factor-model methods (ife/cfe/mc/gsynth/both) via shared helper `.build_cv_mask_rolling()` in `R/cv-helpers.R`. Consumed by `fect_cv`, `fect_binary_cv`, `fect_nevertreated`, `fect_mspe` -- one identical per-fold sampling + drop-future implementation across all four entry points. * `fect_mspe()` accepts `cv.method = "rolling"` for AR-leakage-resistant out-of-sample model comparison. API consolidation -- `"block"` alias + soft deprecations: * New `cv.method = "block"` value as forward-looking alias for `"all_units"`. Internal dispatch unchanged; legacy values still work for one release. * `.fect_normalize_cv_method()` emits one-time `message()` on `cv.method = "all_units"` ("use 'block' instead") and `"treated_units"` (will be deprecated when `cv.units` lands in v2.4.0). Maps `"block"` -> `"all_units"` for internal dispatch. Default flips: * `cv.method` default flipped from `"all_units"` (or `"treated_units"` in `fect_nevertreated`) to `"rolling"` across all 6 entry points (cv.R, cv_binary.R, fect_nevertreated.R, fect_mspe.R, default.R x3, boot.R) + Rd files. * `cv.donut` default raised from 0 to 1 (matches `cv.buffer = 1` for rolling). * `cv.gap` -> `cv.buffer` rename (no deprecation alias since cv.gap was never shipped to CRAN). Latent fixes folded in: * PSOCK worker `.libPaths()` propagation. New `.fect_make_future_cluster()` in cv-helpers.R uses `parallelly::makeClusterPSOCK(rscript_libs = .libPaths())`, replacing 4 `future::multisession` call sites (cv.R x2, fect_nevertreated.R x2). Mirror fix for the doParallel cluster in R/boot.R via `clusterCall(.libPaths)`. Resolves "no package called 'fect'" worker init errors during Quarto book render and intermittent CRAN-check failures. * `loading.overlap` 1D plot histograms now use dark borders matching darker fill variants (were invisible against white background). * `fect_iden.Rd` LaTeX math-macro warnings cleared. Quarto book updates (6 chapters): * 03-ife-mc.Rmd: rewrote CV-method table (block/rolling default/loo); collapsed deep dive to summary; cv-strategies figure embed; K=200 evidence callout. * 04-cfe.Rmd: new "Cross-validation" subsection (dispatcher only); placed before Summary. * 06-plots.Rmd: `loading.overlap` promoted to L2 section with anchor `#sec-loading-overlap`; 1D plot bug fix; expanded discussion. * 07-gsynth.Rmd: top callout reordered (rolling first, concise); CV section condensed. * aa-cheatsheet.Rmd: new "Cross-Validation" L2 section with full param table + scoring criteria + workflow + "Why we prefer rolling" callout (K=200 evidence); Notes section moved to last. * bb-updates.Rmd: v2.3.0 entry rewritten. Tests: * New `test-rolling-via-dispatcher.R` (5+ dispatcher integration tests). * New `test-rolling-cv-methods.R` (CFE extension tests). * `test-rolling-window-cv.R` regression suite for the standalone helper. * Defensive prelude added to E.6 (state-leak fix for ordering-dependent failure that existed pre-rewrite). Squashed from: f6e49e8 (docs: clear fect_iden Rd warnings + document rolling CV + changelog), 014c33f (rewrite as standard rolling-window CV), 9489311 (consolidate to cv.buffer + k=10 default; absorb docs/v2.3.0-cleanup, PR #123), 2d30d4f (cv.method = "rolling" in fect() dispatcher; "block" alias; default flips). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CV default flips paired for stable iid recovery (across all CV entry
points: fect, fect_cv, fect_binary_cv, fect_nevertreated, fect_mspe,
r.cv.rolling, default, boot):
* cv.prop default 0.2 -> 0.1
* k default 10 -> 20
The (cv.prop = 0.1, k = 20) pair gives ~2x coverage of every eligible
unit across folds while keeping per-fold mask size moderate, sharpening
the MSPE curve enough that the 1-SE rule reliably recovers r_true on
iid panels with at-least-moderate factor signal-to-noise.
simdata regenerated with the latent factor contribution scaled by 2
(vs the original LWX 2024 DGP). Doubling raises factor signal-to-noise
from 2.7 to 10.9, making the latent factor structure clearly recoverable
by cross-validated rank selection on this dataset. Original simdata's
signal was comparable to noise, so MSPE-based selection landed within
the 1-SE band of truth without converging on it. All other DGP
components (covariates, FEs, treatment effect, error) are unchanged.
Existing user code that loads simdata will see different numerical Y.
Test fixtures regenerated under the new defaults and re-pinned with
explicit cv.prop=0.1, k=20, cv.donut=1, cv.rule="1se" arguments
(future-proof against further default flips):
* phase3-baseline-{ife,cfe}-{all,tr}-serial.rds
* nevertreated_{ife,cfe}_{all,tr}.rds (regenerated under new simdata
since att.avg depends on the new Y)
Vignette updates:
* 01-start.Rmd: simdata DGP equation reflects 2*lambda'f
* 03-ife-mc.Rmd, 07-gsynth.Rmd, aa-cheatsheet.Rmd, 04-cfe.Rmd:
parameter references updated (cv.prop=0.1, k=20)
* 03-ife-mc.Rmd: task count text updated (k=20 -> 120 tasks vs k=10 -> 60)
* aa-cheatsheet.Rmd: sub-options table updated
* bb-updates.Rmd: v2.3.0 entry rewrites the new defaults + simdata note
* simdata.Rd: full DGP, doubled-factor explanation, all column docs
Quality pipeline:
* devtools::test(): 976 PASS / 0 FAIL / 0 WARN / 0 SKIP
* Quarto book renders cleanly (13/13 chapters)
* R CMD check --as-cran: 1 NOTE (HonestDiDFEct in Suggests, pre-existing)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(cv): rolling CV in dispatcher + cv.prop/k defaults + simdata factor doubled
Resolve a long-standing surface inconsistency: when W was non-NULL, plot(fit) silently rendered W-weighted per-period ATTs while print(fit) and fit$est.att returned the unweighted versions. Same fit, two answers depending on which surface the user looked at. Reported by Sergei Schaub against fect 2.3.0; triage at Research_Hub/Packages/fect/triage/2026-04-27-schaub-weights/. Single canonical aggregation surface The fit object stores one canonical aggregation. When W is supplied, est.att, est.avg, est.att90, att.bound, att.boot, att.vcov, est.placebo, est.carryover, and the per-method off / on / avg / time / count counterparts all carry the W-weighted aggregation. The redundant *.W parallel slots (est.att.W, est.avg.W, att.W.boot, att.W.vcov, att.W.bound, att.on.W, time.on.W, count.on.W, att.avg.W, att.placebo.W, att.carryover.W, est.placebo.W, est.carryover.W, est.att.off.W, att.off.W.bound, att.off.W.vcov) are no longer attached to the fit object. print.fect() labels the obs-level row "Tr obs sample-weighted (W)" when W enters the aggregation, and reverts to "Tr obs equally weighted" otherwise. plot.fect() reads the canonical slots directly --- no auto-flip. The weight argument is deprecated (no-op + warning); removal scheduled for v2.5.0. New W.est / W.agg arguments Two new top-level arguments distinguish three weight roles: - Survey / sample weights: W = "ws" (or W.est = W.agg = "ws"). W enters both the outcome-model fit and the across-treated-obs aggregation. Backward-compatible with v2.3.0 callers passing W alone. - Weight only in the outcome-model fit: W.est = "wr" alone. The weight enters the fit but the across-treated-obs aggregation gives each treated cell weight 1. Use this when the weight reflects fit-side considerations and the estimand is the unweighted average ATT across treated cells. - Weight only in the aggregation: W.agg = "your_col" alone. Outcome model fit unweighted; weight applied only when summarizing across treated cells. Common cases: calibration weights to a target population, or post-stratification weights. In v2.3.1, W.est and W.agg (when both supplied) must point to the same column. Distinct columns for fit vs. aggregation (e.g., combined survey x IPW designs) are scheduled for v2.4.0 and currently error. Caveat for IPW users: a fect-internal IPW-for-confounding workflow is under development for v3.0 (cross-fit doubly-robust path). v2.3.1 does not include a recommended IPW workflow, and W.agg should not be used as a substitute. See statsclaw-workspace/fect/runs/REQ-cross-fit-dr/spec.md. W.role deprecation The W.role argument briefly introduced during v2.3.1 development is deprecated and will be removed in v2.5.0. It is still accepted with a one-time warning that translates to the new arguments: "both" -> W.est = W.agg = W, "estimation" -> W.est only, "aggregation" -> W.agg only. Documentation - NEWS.md, vignettes/bb-updates.Rmd: v2.3.1 entry. - man/fect.Rd: W, W.est, W.agg documented; W.role flagged as deprecated. - vignettes/03-ife-mc.Rmd: new "## Weights" section at 3.5 (after Diagnostics), describing the three roles, with a callout-warning that v3.0 cross-fit DR is the right home for IPW-for-confounding. - vignettes/aa-cheatsheet.Rmd: three rows for W, W.est, W.agg. Tests tests/testthat/test-weights-consistency.R covers all four configs (W=NULL, W="ws", W.est="wr", W.agg="ipw") plus the same-column constraint and the W.role deprecation warning. Full devtools::test(): 1014 / 0 / 0 / 0. Version bump DESCRIPTION 2.3.0 -> 2.3.1, Date 2026-04-24 -> 2026-04-27. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(weights): consistent ATT surface + W.est/W.agg per-role weights (v2.3.1)
…ght API (v2.3.2)
Visual overhaul across all 14 plot.fect() output types under
`theme.bw = TRUE` (the default): white panel, plain left-aligned title,
thin grey reference lines, dashed treatment-onset vline, pre/post lightness
contrast (grey50 / grey20), publication-sized axis text, and compact
legends. Stats annotation block moves to top-left with a symmetric 2.5%
inset and 2 × num_stat_lines top padding so it never grazes the leftmost
CI; sized at cex.text * 1.0 so it does not overpower the title.
Placebo / carryover plots render highlighted periods as a single accent
glyph (orange triangle for placebo, blue diamond for carryover, orange
triangle for carryover.rm) instead of stacked circle + accent. The
lightened-tone background rectangle is now opt-in via
`highlight.fill = TRUE` (default FALSE — keeps figures clean for print
and grayscale). The `highlight` argument additionally accepts a character
subset of `c("placebo", "carryover", "carryover.rm")` for selective
per-test-type highlighting. NULL / TRUE / FALSE behave as before.
`legacy.style = TRUE` reproduces pre-2.3.1 figures byte-identically
(bold centered title, larger axis sizes, solid vline, blue placebo
triangles, peach rectangle), independent of `theme.bw`.
`theme.bw = FALSE` is now soft-deprecated with a one-time per-session
message; removal scheduled for v2.5.0. Users who want the gray-panel
look should pass `legacy.style = TRUE`, or apply
`+ ggplot2::theme_gray()` to the returned plot.
Migrated off ggplot2 4.0's deprecated `fatten` / `lwd` arguments to the
`size` / `linewidth` aesthetics; deprecation warnings cleared.
`loadings` (ggpairs) plot's correlation panel reformatted with overall +
per-group entries; per-group label colors match the density-plot fills.
Bug fix: carryover-test plot pre-exit boundary. Legacy code took
`shift <- dim(x$est.carryover)[1]`, which was always 1 (the slot stores
the aggregate, 1 row), so the last pre-exit cell rendered at plot-x = 1
past the dashed onset line. Fix detects real `carryover.rm` from
`x$call`. (`carryover.rm` has no dedicated fit-object slot today; long
term `R/fe.R / cfe.R / mc.R` should store it explicitly.)
Quarto book: worked `highlight = ` examples in chapter 6
(carryover and carryover.rm), trim of chapter 2 §"User-supplied weights"
with deepened cross-links to chapter 3 §"Weights". 13-assertion
regression test (`test-modern-theme.R`).
R CMD check --as-cran clean (0 WARN / 0 ERROR / 1 unavoidable NOTE).
Tests: 1026 PASS / 0 FAIL / 0 WARN / 0 SKIP. Quarto book renders
end-to-end (13 chapters, 174 SVG figures).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(plot): modern visual defaults for plot.fect() + selective highlight API (v2.3.2)
… matrix-shape (v2.3.3) * PSOCK race in CV → bootstrap chain. `parallel = TRUE` after `CV = TRUE` occasionally inherited a stale `future::plan()` whose underlying cluster had been torn down. The next `clusterCall(.libPaths)` could kill a worker mid-handshake; `foreach(.errorhandling = "pass")` then returned an error object in place of a fit, and downstream `array(..., dim = ...)` crashed with "incorrect number of dimensions" (E.6 in `test-cv-parallel.R`). Bootstrap PSOCK construction now reuses the shared `.fect_make_future_cluster()` helper (which bakes `.libPaths()` into worker startup via `rscript_libs`), wrapped in a 3-attempt retry-with-backoff that shrinks worker count between attempts. If `doParallel` cluster init also fails after retries, the bootstrap degrades to sequential rather than crashing. * `diagtest()` matrix-shape edge case. The all-non-NA column filter on `pre.att.boot` and `att.boot` could collapse the matrix to a vector when only one bootstrap column survived (rare but reachable when small `nboots` interacts with NA-filled iterations from worker errors). Downstream `res_boot[pre.pos, ]` then threw "incorrect number of dimensions". Pass `drop = FALSE` at both filter sites. * `carryover.rm` is now stored explicitly on the fit object. Pre-fix, `plot.fect()` recovered `carryover.rm` from `as.list(x$call)$carryover.rm` + `eval(arg, envir = parent.frame())`. That call-parsing path silently gave the wrong K under `do.call()`, programmatic wrappers, or any call-rewriting code path. The fit object now stores `carryover.rm` directly (`R/default.R`); `R/plot.R` reads from the slot. Behaviorally a no-op for fits built via named-argument `fect(...)`; correct under any other construction. * New `tests/testthat/test-carryover-rm-slot.R` (3 tests) verifies the slot is populated, NULL'd when unused, and the plot-logic recovery path under `x$call` wipe. * New `E.7` in `tests/testthat/test-cv-parallel.R` simulates the CV → boot stale-plan PSOCK race directly: install a fresh cluster, kill its workers behind future's back, then run boot. The retry loop in `run_dopar_retry` recovers cleanly. * Quarto book chapter 2 §Other estimands gains a new subsection on post-hoc estimands derived from the imputed potential-outcome surface, with a worked APTT (Chen & Roth 2024) example using the existing fit slots. Closes documentation side of issue #126 (ajunquera). * `DESCRIPTION` bumped to 2.3.3, `Date: 2026-04-28`. NEWS.md gets a "fect 2.3.3 (development)" heading covering the three fixes plus the documentation addition. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-race fix(boot,plot,diagtest): PSOCK retry + carryover.rm slot + APTT vignette (v2.3.3)
Adds a typed dispatcher and low-level accessor for computing alternative
estimands directly from any fect imputation fit. Replaces the four
ad-hoc paths previously available (effect(), att.cumu(), the v2.3.3
ch2 §Post-hoc estimands stopgap recipe, and the W.agg = 0/1 mask
hack for window-restricted ATT) with one coherent surface.
* New estimand(fit, type, by, cells, weights, window, direction,
vartype, conf.level, ci.method) typed dispatcher. Closed enums for
type and canonical by values; user-column by reserved for future
releases. Shipped types:
"att" - per-cell mean treatment effect.
"att.cumu" - cumulative ATT (replaces effect() / att.cumu()).
"aptt" - average proportional TE on the treated (Chen and Roth
2024 QJE).
"log.att" - mean log-scale treatment effect.
Shipped by axes: "event.time", "overall" (with optional window).
cohort and calendar.time reserved.
* New imputed_outcomes(fit, cells, replicates, direction) accessor
returns a long-form data frame with documented columns
(id, time, event.time, cohort, treated, Y_obs, Y0_hat, eff,
eff_debias, W.agg, [replicate]). Use this for custom estimands the
dispatcher does not ship; pipe to dplyr / data.table for arbitrary
aggregation.
* cells = filter (NULL / logical / formula) on both functions, with
window = c(L, R) sugar over cells = ~ event.time >= L & <= R.
* New eff_debias slot on the fit object, NULL for plain imputation
estimators (FE / IFE / MC / CFE / GSC); reserved for future
doubly-robust estimators so the score eff = (Y_obs - Y0_hat) +
eff_debias can be added without breaking the long-form schema.
* Numerical-equality invariants asserted by package tests:
estimand(fit, "att", "event.time") == fit$est.att
estimand(fit, "att.cumu", "event.time") == effect(fit)$effect.est.att
estimand(fit, "att.cumu", "overall", win) == att.cumu(fit, period = win)
byte-identical for the relevant columns under default arguments.
* Soft-deprecation: effect() and att.cumu() emit a one-time-per-session
message pointing at estimand(). They continue to work byte-identically
to v2.3.x. Removal not before v3.0.0. estimand() suppresses the message
when delegating internally so users only see it on their direct calls.
* New "Alternative estimands" chapter (vignettes/04a-estimands.Rmd) in
the user manual with worked examples for each shipped type, the
cells / window / direction patterns, a custom-estimand example via
imputed_outcomes(), the keep.sims memory tradeoffs table, and the
migration table from effect() / att.cumu(). vignettes/02-fect.Rmd
§Post-hoc estimands is now a one-paragraph backref to the new chapter.
* Tests: 84 expectations across 6 new test files
(test-imputed-outcomes.R, test-estimand-att.R, test-estimand-att-cumu.R,
test-estimand-aptt.R, test-estimand-log-att.R,
test-estimand-deprecation.R) covering schema, byte-equality
invariants, error paths, and deprecation gating.
* DESCRIPTION bumped to 2.4.0, Date 2026-04-29.
Out of scope (deferred per design contract):
- by = "cohort" / "calendar.time" / user columns (canonical names
reserved; implementations to follow).
- Plot integration: existing plot(fit, type = "cumul") flows through
effect() unchanged. New plot types "aptt" / "log.att" deferred.
- DR non-linear functionals (aptt.dr, log.att.dr).
- Post-hoc linear-constraint inference.
- User-supplied closures (summarise_po(fit, fn = ...)).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(po-estimands): unified post-hoc estimand API (v2.4.0)
v2.4.1 — extend estimand() vartype enum to accept "parametric".
Audit finding: fit$eff.boot was already populated under vartype = "parametric"
when keep.sims = TRUE. The allocation at boot.R:557-566 is conditioned on
keep.sims, not vartype; the universal fill at boot.R:1756-1760 / 1871-1882
covers all vartypes. The only blocker was match.arg() at po-estimands.R:452
rejecting "parametric".
This change collapses to a one-line match.arg extension plus roxygen,
docs, and tests. Strictly additive — no fit-time machinery changed; v2.4.0
byte-equality contract intact.
Also fold in a small completeness fix on the cumulative-ATT path:
estimand(fit, "att.cumu", ...) now populates n_cells from
fit$est.att[, "count"] (event-time path: per-period count; overall
path: window-summed count) rather than NA_integer_. This lets esplot()
draw the bottom-bar treated-cell panel for cumulative plots, matching
the per-event-time level ATT behavior. ch03 + ch08 cumulative examples
updated to pass Count = "n_cells". New tests AC.6 / AC.6b lock the
invariant.
Files:
R/po-estimands.R — vartype enum + @param doc + att.cumu n_cells
man/estimand.Rd — usage + \item{vartype} match
DESCRIPTION — Version 2.4.1, Date 2026-04-29
NEWS.md — v2.4.1 entry
tests/testthat/test-estimand-parametric.R — 9 test_thats / 32 expects
tests/testthat/test-estimand-att-cumu.R — AC.6 / AC.6b for n_cells
vignettes/03-estimands.Rmd — new §Parametric variance + Count
vignettes/08-gsynth.Rmd — new §Cumulative Effects + Count
vignettes/bb-updates.Rmd — v2.4.1 heading
vignettes/index.qmd — version bump
Validation:
testthat: full suite passes (estimand-att-cumu 25/0/0/0 with new
AC.6 / AC.6b expectations; estimand-parametric 32/0/0/0;
no v2.4.0 byte-equality regressions).
R CMD check: 1 WARNING, 2 NOTEs (WARN is pre-existing toolchain
artifact: Apple clang 21 ↔ R_ext/Boolean.h:62 pragma —
verified by re-checking origin/dev at f59300a, same
status. CRAN's macOS-arm64 builder uses older clang
and does not emit it.)
Quarto book: 14 HTML / 176 SVG / 0 errors at squash time. ch08
re-rendered post-n_cells fix: chunk 84/121
(turnout-cumulative) executes cleanly.
Contract: ref/po-estimands-contract.md
Run log: statsclaw-workspace/fect/runs/2026-04-29-po-estimands-parametric-v241.md
feat(po-estimands): parametric vartype for estimand() (v2.4.1)
71a0631 to
893b84d
Compare
xuyiqing
added a commit
that referenced
this pull request
Apr 30, 2026
The PAR-1..7 tests in test-paraboot-parity.R and the P.3..6 helper-migration tests in test-cv-parallel.R compare against RDS baselines (paraboot-baseline.rds and nevertreated_*.rds) captured on macOS/Linux during the parametric-boot refactor on 2026-04-13. Windows runners produce numerically equal but bit-different results due to BLAS implementation differences, causing 22+4 = 26 identical()/expect_identical() failures.
Add skip_on_os("windows") to the 11 affected test_that blocks. Byte-equality coverage is preserved on macOS+Ubuntu (where the baselines match); Windows users get a clean R CMD check. macOS-latest passed with these checks intact, confirming the regression-detection property still holds on the OSes where bit-equality is achievable.
Surfaces now because the R-CMD-check workflow only runs on master pushes/PRs, and PR #132 (dev -> master) is the first such PR since the parity tests were added.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
162c744 to
89743a1
Compare
…E) + CI restructure Squash of release-prep work for the v2.4.1 CRAN-accepted release. Pre-squash tip retained locally on tag pre-final-squash-v241 for rollback. DOCS - NEWS.md: drop "(development)" suffix from v2.3.2 / v2.3.3 / v2.4.0 / v2.4.1 headings now that the bundled release is on CRAN. - vignettes/bb-updates.Rmd: mark v2.4.1 as "(2026-04-30) CRAN release." per the convention used for v2.2.0 (commit 07da03c). Trim v2.4.1 entry to user-visible enhancement + bug fix; drop slot-contract / match.arg / byte-equality internals. - vignettes/01-start.Rmd: new chunk after the installed-version check displays live CRAN release version (via available.packages()) and dev branch DESCRIPTION version (via GitHub raw), with tryCatch fallbacks for offline renders. Lets users compare their installed version against both reference points. - README.Rmd / README.md: bump Chiu et al. citation from "First View" to APSR 120(1): 245-266 (matches bib entry CLLX2026). Convert CRAN status / downloads badges from inline <img> tags to standard markdown image syntax. Strip trailing whitespace, drop trailing blank line. README.Rmd gains <!-- markdownlint-disable MD041 --> (YAML frontmatter + setup chunk push the H1 down; structural to Rmd). - ARCHITECTURE.md: full scriber regeneration on dev for v2.4.1. Refreshed module-structure / function-call / data-flow Mermaid graphs. Eight new R files added to the module table (cv-rolling.R, cv-rule-helpers.R, cv-helpers.R, impute_Y0.R, valid_controls.R, loading_bound.R, theme-helpers.R, plot_return.R). Two new architectural patterns: rolling-window CV (v2.3.0) and per-role weight + modern-theme split (v2.3.1/v2.3.2). Line counts updated across all existing files; total R source line count corrected to 32,047. Run log at statsclaw-workspace/fect/runs/2026-04-30-architecture-regen-v241.md. CI - .github/workflows/R-CMD-check.yaml: gate cost and strictness by event type. * pull_request to master: matrix collapses to ubuntu-latest only with NOT_CRAN="false" (CRAN-style; skip_on_cran tests are skipped). Fast feedback (~10-15 min) and matches CRAN's farm behavior so PR reviews catch CRAN-relevant issues. * push to master / workflow_dispatch: full 3-OS matrix. - ubuntu-latest, NOT_CRAN="false" (CRAN-style) - macos-latest, NOT_CRAN="true" (full suite, including byte-equality fixture tests) - windows-latest, NOT_CRAN="false" (CRAN-style) - Per-OS NOT_CRAN gating ensures the byte-equality fixture tests in test-paraboot-parity.R (PAR-1..7) and test-cv-parallel.R (P.3..6) only run on macOS, where their RDS baselines were captured (non-Apple BLAS produces bit-different results). On Ubuntu and Windows these tests skip via skip_on_cran(), matching how CRAN's win-builder and farm see them. Implementation: dynamic matrix via fromJSON of a conditional string keyed on github.event_name; per-OS not_cran field threaded through to rcmdcheck via the `env=` parameter (verified: the actual rcmdcheck signature has `env = character()`, not `env_vars=`). r-lib/actions/setup-r-dependencies@v2 unconditionally writes NOT_CRAN=true to $GITHUB_ENV during its run, so the parent R process always sees NOT_CRAN=true regardless of workflow-level env: settings; the only reliable override is via callr's env vector, which rcmdcheck forwards. callr::rcmd_safe_env() lists 4 baseline keys (CYGWIN, R_TESTS, R_BROWSER, R_PDFVIEWER); rcmdcheck appends our `env` on top, and callr passes that complete vector as the child R CMD check process env. Empirically verified: parent NOT_CRAN=true, env=c(NOT_CRAN="false") -> child sees "false". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Merge
devintomasterfor the v2.4.1 CRAN release (accepted 2026-04-30). Bundles v2.2.0 → v2.4.1 in one master-side release; master was last cut at v2.2.0 (commit731c15b, 2026-03-27).Headline additions across the bundle:
estimand()acceptsvartype = "parametric";att.cumupopulatesn_cellssoesplot(..., Count = "n_cells")works on cumulative plots (PR #130).estimand(fit, type, by, ...)typed dispatcher (att,att.cumu,aptt,log.att) +imputed_outcomes()long-form accessor; new chapter @sec-estimands; soft-deprecateseffect()andatt.cumu()(PR #129). Closes issue #126.carryover.rmslot +diagtest()drop = FALSEfix (PR #128).plot.fect()(white panel, dashed onset vline, peach highlight rectangle, glyph-only highlights withhighlight.fill = TRUEopt-in); new selective-highlight API;legacy.style = TRUEfor byte-identical pre-2.3.1 reproduction (PR #127).W.est/W.aggper-role weight arguments (PR #125).cv.method = "rolling"default,cv.prop = 0.1,k = 20;simdatafactor doubled (PR #124).This commit-level merge head is
5d9f030(the docs-only commit marking v2.4.1 as CRAN release inbb-updates.Rmdand dropping the(development)tag from NEWS headings).CRAN status
Submitted to CRAN 2026-04-29; accepted 2026-04-30. gsynth (1.4.0, sole CRAN reverse-import) verified compatible against installed fect 2.4.1 (
R CMD checkStatus OK, 0E/0W/0N).Test plan
masterafter merge.v2.4.1on the merge commit.v2.4.1referencing this PR.devfrommasterso the merge commit is reachable.🤖 Generated with Claude Code