Exact Bell-McCaffrey CL2 block as the default leverage adjustment (A18)#121
Exact Bell-McCaffrey CL2 block as the default leverage adjustment (A18)#121fabian-s wants to merge 5 commits into
Conversation
Implemented by a codex subagent (gpt-5.6-terra, high), independently
re-verified: exact Bell-McCaffrey block B_g = I - 2 H_gg + (Ht^2)_gg with
(1-cap)^2 eigenvalue floor; cl2_adjustment = auto/exact/shortcut on pffr()
and coef.pffr() (auto: exact for G <= 100 and a dense-cost bound, visible
fallback to shortcut); diagnostics n_adjusted/min_block_eig/max_block_kappa
in fit metadata; docs + NEWS.
A18 hardening gate: 16 cells x {gaussian, poisson, binomial, Gamma} x
k {4,7} x unbalanced/influential-cluster designs - all SEs finite/positive,
3 influential Poisson/Gamma blocks hit the floor and match the uncapped
analogue to 8.8e-6, median SE ratio to shortcut 1.00-1.02
(hardening-exactcl2-results.csv). test-pffr-exactcl2.R: 19 PASS (re-run
independently); full test-pffr.R + sandwich/satterthwaite files passed.
Open: exact Bell-McCaffrey Satterthwaite df remains separate (documented).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WFfnErmWi9NCGCCJ8YwE95
Both adjustments pay the same per-cluster eigendecomposition, so the exact block's marginal cost is only the extra multiplications: timed at ~0.03 s extra at proxy 5e8 and ~0.7 s total at 5e9 (single-thread BLAS). The old cap could deny a sub-second correctness upgrade in the saturated small-G regime where Study EX measured the largest gain (+1.3pp at G=20). G <= 100 relevance bound unchanged (EX-validated range; gain < 0.15pp beyond). Boundary tests updated; 20 PASS. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01WFfnErmWi9NCGCCJ8YwE95
There was a problem hiding this comment.
Pull request overview
This PR updates refund’s CL2 sandwich covariance path to default to the exact Bell–McCaffrey leverage adjustment when it’s both relevant (small/moderate cluster counts) and computationally affordable, while preserving the historical shortcut as a fallback and user-selectable option. It also surfaces richer CL2 diagnostics in fit metadata and adds a hardening gate + tests to ensure robustness under high-leverage scenarios.
Changes:
- Add
cl2_adjustment = c("auto","exact","shortcut")topffr()andcoef.pffr(), with an auto-rule that resolves exact vs shortcut and records diagnostics insandwich_info. - Implement exact Bell–McCaffrey per-cluster block logic in
gam_sandwich_cluster_cl2()and addresolve_cl2_adjustment()to centralize selection. - Add new CI tests + a hardening script/results artifact, and update NEWS/man pages accordingly.
Reviewed changes
Copilot reviewed 9 out of 17 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/testthat/test-pffr.R | Updates existing CL2 diagnostics test to reflect new default (“exact”) and forcing shortcut for cap-warning behavior. |
| tests/testthat/test-pffr-exactcl2.R | Adds new test file covering exact-CL2 hardening gate and auto/forced adjustment selection behavior. |
| R/pffr.R | Adds cl2_adjustment argument and documents the new CL2 default behavior. |
| R/pffr-methods.R | Adds cl2_adjustment to coef.pffr() to allow recomputation/override at coefficient extraction time. |
| R/pffr-core.R | Implements exact CL2 block math, selection resolver, metadata wiring, and vcov caching keyed by CL2 adjustment. |
| NEWS.md | Documents the exact-CL2 default behavior, rule-of-thumb thresholds, and new diagnostics. |
| man/resolve_cl2_adjustment.Rd | New generated documentation for the adjustment resolver. |
| man/pffr.Rd | Updates pffr() Rd to include cl2_adjustment. |
| man/pffr_vcov.Rd | Documents cl2_adjustment passthrough for covariance recomputation. |
| man/pffr_compute_sandwich.Rd | Documents cl2_adjustment argument in sandwich computation helper. |
| man/pffr_build_metadata.Rd | Updates metadata builder Rd to include cl2_adjustment. |
| man/gam_sandwich_cluster_cl2.Rd | Documents cl2_adjustment and exact vs shortcut diagnostics on the returned covariance. |
| man/coef.pffr.Rd | Documents the new cl2_adjustment override for coef.pffr(). |
| man/apply_sandwich_correction.Rd | Documents cl2_adjustment passthrough to sandwich correction. |
| hardening-exactcl2.R | Adds a reproducible hardening script generating fixtures and comparing exact vs shortcut behavior. |
| hardening-exactcl2-results.csv | Adds committed output table from the hardening script run. |
| EXACTCL2-IMPLEMENTATION-NOTES.md | Adds implementation/design notes for exact CL2, thresholds, and testing record. |
Files not reviewed (8)
- man/apply_sandwich_correction.Rd: Generated file
- man/coef.pffr.Rd: Generated file
- man/gam_sandwich_cluster_cl2.Rd: Generated file
- man/pffr.Rd: Generated file
- man/pffr_build_metadata.Rd: Generated file
- man/pffr_compute_sandwich.Rd: Generated file
- man/pffr_vcov.Rd: Generated file
- man/resolve_cl2_adjustment.Rd: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| #' @param cl2_adjustment Leverage adjustment within \code{sandwich = "cl2"}: | ||
| #' \code{"auto"} (default) uses exact CL2 when \eqn{G \le 100} and the | ||
| #' dense-block cost proxy \eqn{G\max_g D_g p^2} is at most \eqn{5\times | ||
| #' 10^8}; it otherwise falls back to the historical shortcut. The exact block | ||
| #' is \eqn{B_g = I - 2H_{gg} + (H_t^2)_{gg}}, with eigenvalues floored at |
| \item{cl2_adjustment}{Leverage adjustment within \code{sandwich = "cl2"}: | ||
| \code{"auto"} (default) uses exact CL2 when \eqn{G \le 100} and the | ||
| dense-block cost proxy \eqn{G\max_g D_g p^2} is at most \eqn{5\times | ||
| 10^8}; it otherwise falls back to the historical shortcut. The exact block |
| Within the existing CL2 path only, `"auto"` selects exact CL2 when: | ||
|
|
||
| * `G <= 100`; and | ||
| * `G * max(D_g) * p^2 <= 5e8`. | ||
|
|
| max_block_kappa <- max( | ||
| max_block_kappa, | ||
| abs(max(ee_B$values)) / max(abs(block_min), 1e-300) | ||
| ) |
|
Follow-up closed: the Poisson G=40 confirmation cells (EX 29–30) are done (LRZ job 5329641, R=300 each, zero failures):
Slightly larger than the Gaussian gain at the same G (+0.15…+0.33pp), consistent with the rougher IRLS hat structure. Across all 30 EX cells the minimum ff delta remains positive (min t = 8.6) — the never-worse claim now covers Gaussian and Poisson through the auto-rule's relevant range. Data: 🤖 Generated with Claude Code |
…a call Independent review (+ Copilot comments 1-3, all valid): the cap raise in 33d8fad missed the pffr.R roxygen (and its generated Rd) and the notes file's Default-rule body. All now 5e9. Also pass cl2_adjustment through to pffr_build_metadata() - previously the parameter existed but the sole caller never supplied it (the field was authoritative only after apply_sandwich_correction overwrote it). Copilot comment 4 (use max(abs(eigenvalues)) for kappa) NOT actioned: B_g is a principal submatrix of the PSD matrix (I-H)^2, hence PSD (verified numerically, min block eigenvalue +0.23), so abs(max(v)) == max(abs(v)) and the diagnostic is already the true condition number. Review also verified the exact-block identity against a dense (I-H)^2 reference to 4.4e-16 incl. cross-cluster terms. Tests: 20/20. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01WFfnErmWi9NCGCCJ8YwE95
|
Independent code review completed (adversarial pass over the full diff + numerical verification; Copilot comments adjudicated). Verification highlights:
Copilot comment adjudication:
Also fixed from the review: Reviewer verdict: merge after fixes → fixes applied, 20/20 tests pass. 🤖 Generated with Claude Code |
Both are PR documentation, not package files (non-standard top-level files would also trip R CMD check); their full content is attached to PR #121 as a comment. The hardening *script* stays on the branch so the results remain regenerable. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01WFfnErmWi9NCGCCJ8YwE95
Implementation notes + hardening resultsMoved out of the package tree in Exact-CL2 implementation notesDate: 2026-07-21 Change made
where
Default ruleWithin the existing CL2 path only,
The first bound confines exact CL2 to the small-to-moderate-cluster regime Tests runAll R invocations used
Hardening output summaryThe 16-cell table covers families Gaussian, Poisson, binomial, and Gamma; Actual results:
Open questionsThe pointwise Satterthwaite degrees-of-freedom routine remains the established The requested local commits could not be created in this execution environment: Addendum 2026-07-21 (PI-side review): cost cap raised 5e8 -> 5e9Timing at the shipped cap showed the exact block's marginal cost over the Hardening results (hardening-exactcl2-results.csv)"family","k","design","G","max_cluster_size","n_adjusted","min_block_eig","max_block_kappa","max_abs_exact_nocap","median_se_ratio_to_shortcut","se_finite_positive","se_ratio_sane"
"gaussian",4,"unbalanced",5,50,0,0.453590234808712,2.20463299969789,0,1.01263947332551,TRUE,TRUE
"poisson",4,"unbalanced",5,50,0,0.348940004869249,2.82730371704937,0,1.0079082080914,TRUE,TRUE
"binomial",4,"unbalanced",5,50,0,0.552153568687061,1.81109035005941,0,1.0010220332576,TRUE,TRUE
"Gamma",4,"unbalanced",5,50,0,0.332486151450086,3.00764406468859,0,1.00903670061469,TRUE,TRUE
"gaussian",7,"unbalanced",5,50,0,0.374043657671271,2.67348471091803,0,1.0189182605098,TRUE,TRUE
"poisson",7,"unbalanced",5,50,0,0.301435120875162,3.31746346310504,0,1.00000000257308,TRUE,TRUE
"binomial",7,"unbalanced",5,50,0,0.478396994803052,2.09031413420915,0,1.00000000004754,TRUE,TRUE
"Gamma",7,"unbalanced",5,50,0,0.386030967291895,2.590465752049,0,1.00648632821506,TRUE,TRUE
"gaussian",4,"influential",5,50,0,1.39826045642e-06,709818.235108635,0,1.00791990080075,TRUE,TRUE
"poisson",4,"influential",5,50,1,3.20272329212834e-07,2982577.58727008,7.06659823389664e-06,1.01755206252338,TRUE,TRUE
"binomial",4,"influential",5,50,0,1.58358505475409e-06,623867.247044085,0,1.01039988193779,TRUE,TRUE
"Gamma",4,"influential",5,50,0,2.35791791958206e-06,423665.797661914,0,1.01294453124424,TRUE,TRUE
"gaussian",7,"influential",5,50,0,1.12859513423658e-06,882176.398770039,0,1.01074965381931,TRUE,TRUE
"poisson",7,"influential",5,50,1,7.21418514260108e-07,1385176.29210241,8.75780657148509e-07,1.01500324062661,TRUE,TRUE
"binomial",7,"influential",5,50,0,1.35355708907881e-06,738794.066446026,0,1.00000000006042,TRUE,TRUE
"Gamma",7,"influential",5,50,1,7.39633434387876e-07,1348528.03666834,4.54114080495374e-07,1.00490798665056,TRUE,TRUE🤖 Generated with Claude Code |
Non-standard top-level file; full script preserved as PR #121 documentation alongside the notes and results it generates. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01WFfnErmWi9NCGCCJ8YwE95
Hardening script (hardening-exactcl2.R)Removed from the package tree in # Scoped exact-CL2 hardening gate (DESIGN amendment A18).
#
# Run from the package root with:
# OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 nice -n 15 Rscript hardening-exactcl2.R
# The script writes hardening-exactcl2-results.csv in the package root.
make_exactcl2_fixture <- function(family_name, k, design) {
set.seed(
match(family_name, c("gaussian", "poisson", "binomial", "Gamma")) *
100 +
k +
match(design, c("unbalanced", "influential"))
)
n_curve <- 12L
n_grid <- 10L
grid <- seq(0, 1, length.out = n_grid)
xlin <- stats::rnorm(n_curve)
eta <- outer(xlin, rep(0.35, n_grid)) +
matrix(rep(0.2 * sin(2 * pi * grid), each = n_curve), n_curve, n_grid) -
0.4
Y <- switch(
family_name,
gaussian = eta + matrix(stats::rnorm(n_curve * n_grid, sd = 0.35), n_curve),
poisson = matrix(
stats::rpois(n_curve * n_grid, lambda = exp(pmin(eta, 2))),
n_curve
),
binomial = matrix(
stats::rbinom(n_curve * n_grid, 1, stats::plogis(eta)),
n_curve
),
Gamma = matrix(
stats::rgamma(n_curve * n_grid, shape = 5, scale = exp(pmin(eta, 2)) / 5),
n_curve
)
)
# Keep the response in its ordinary range, then make one covariate curve
# extreme. This isolates high leverage from a separable/outcome-saturation
# failure and exercises the shipped eigenvalue floor (especially Gaussian,
# Poisson, and Gamma at k = 9).
if (design == "influential") xlin[1] <- 3000
# Five deliberately unbalanced independent clusters, with cluster 1 made
# influential above. Their curve counts 1, 1, 2, 3, 5 yield unequal blocks.
cluster <- rep(seq_len(5), times = c(1, 1, 2, 3, 5))
list(
data = data.frame(Y = I(Y), xlin = xlin),
yind = grid,
cluster = cluster,
family = switch(
family_name,
gaussian = stats::gaussian(),
poisson = stats::poisson(),
binomial = stats::binomial(),
Gamma = stats::Gamma(link = "log")
),
k = k
)
}
run_exactcl2_hardening <- function() {
cases <- expand.grid(
family = c("gaussian", "poisson", "binomial", "Gamma"),
k = c(4L, 7L),
design = c("unbalanced", "influential"),
stringsAsFactors = FALSE
)
rows <- vector("list", nrow(cases))
for (i in seq_len(nrow(cases))) {
case <- cases[i, ]
fixture <- make_exactcl2_fixture(case$family, case$k, case$design)
fit <- suppressWarnings(suppressMessages(refund::pffr(
Y ~ xlin,
data = fixture$data,
yind = fixture$yind,
family = fixture$family,
bs.yindex = list(bs = "ps", k = fixture$k, m = c(2, 1)),
sandwich = "none"
)))
# `1 - sqrt(tol)` makes the exact floor equal tol: Study EX's
# cl2_exact_nocap comparator without duplicating the package implementation.
V_exact <- suppressWarnings(refund:::pffr_vcov(
fit,
sandwich = "cl2",
cluster = fixture$cluster,
cl2_adjustment = "exact"
))
V_nocap <- suppressWarnings(refund:::gam_sandwich_cluster_cl2(
refund:::pffr_model_based_gam(fit),
refund:::build_cluster_id(fit$pffr, cluster = fixture$cluster),
cl2_adjustment = "exact",
leverage_cap = 1 - sqrt(1e-8)
))
V_shortcut <- suppressWarnings(refund:::pffr_vcov(
fit,
sandwich = "cl2",
cluster = fixture$cluster,
cl2_adjustment = "shortcut"
))
se_exact <- sqrt(pmax(diag(V_exact), 0))
se_shortcut <- sqrt(pmax(diag(V_shortcut), 0))
positive <- is.finite(se_exact) &
is.finite(se_shortcut) &
se_exact > 0 &
se_shortcut > 0
ratio <- se_exact[positive] / se_shortcut[positive]
rows[[i]] <- data.frame(
family = case$family,
k = case$k,
design = case$design,
G = length(unique(fixture$cluster)),
max_cluster_size = max(table(fixture$cluster)) * length(fixture$yind),
n_adjusted = attr(V_exact, "n_adjusted"),
min_block_eig = attr(V_exact, "min_block_eig"),
max_block_kappa = attr(V_exact, "max_block_kappa"),
max_abs_exact_nocap = max(abs(V_exact - V_nocap)),
median_se_ratio_to_shortcut = stats::median(ratio),
se_finite_positive = all(positive),
se_ratio_sane = length(ratio) > 0 &&
min(ratio) > 1e-3 &&
max(ratio) < 1e3,
stringsAsFactors = FALSE
)
}
do.call(rbind, rows)
}
if (sys.nframe() == 0L) {
devtools::load_all(quiet = TRUE)
results <- run_exactcl2_hardening()
utils::write.csv(results, "hardening-exactcl2-results.csv", row.names = FALSE)
print(results)
stopifnot(
all(results$se_finite_positive),
all(results$se_ratio_sane),
all(is.finite(results$n_adjusted)),
any(results$n_adjusted > 0)
)
}🤖 Generated with Claude Code |
What
Makes the exact Bell–McCaffrey leverage block the default within the CL2 path (the package-level
sandwich = "auto"policy is unchanged):B_g = I − 2·H_gg + (H̃²)_gg, eigenvalues floored at(1 − leverage_cap)²; the historical shortcut(I − H_gg)^{−1/2}(cap 0.999) remains available and is the automatic fallback.cl2_adjustmentargument onpffr()andcoef.pffr():"auto"(default) /"exact"/"shortcut"."auto"selects exact whenG ≤ 100and the dense-cost proxyG · max(D_g) · p² ≤ 5e9; the resolved choice plus diagnostics (n_adjusted,min_block_eig,max_block_kappa) are recorded in the fit metadata, so a fallback is always visible.Why
Pre-registered decision A18 (PI, 2026-07-17) in the pffr-ci companion project, on correctness grounds, gated on evidence:
pffr-ci: results-summaries/studyEX-primary.csv)studyPX-exact-primary.csv)Threshold calibration
G ≤ 100: the EX-validated range; beyond it the measured gain is < 0.15pp.5e9, measured not guessed: both adjustments pay the same per-cluster eigendecomposition, so exact's marginal cost is only the extra multiplications — timed at ~0.03 s extra at proxy 5e8 and ~0.7 s total at 5e9 (single-thread BLAS). The cap therefore bounds the correctness upgrade at well under a second and never denies it in the saturated small-G regime where its benefit is largest. (First cut used 5e8; raised in the second commit with the timing evidence.)Hardening (the A18 gate)
hardening-exactcl2.R→hardening-exactcl2-results.csv(both preserved as documentation comments on this PR — script and results — and regenerable; neither ships in the package tree): 16 cells = {gaussian, poisson, binomial, Gamma} × k ∈ {4, 7} × {unbalanced (cluster sizes 1,1,2,3,5), influential-cluster} designs.n_adjusted = 1); smallest pre-floor eigenvalue 3.2e−07; exact vs uncapped analogue differ ≤ 8.8e−06 (differences confined to floored cells) — the floor handles indefinite blocks as shipped.Testing
tests/testthat/test-pffr-exactcl2.R(20 assertions incl. auto-rule boundaries) — PASS, independently re-run twice.test-pffr.R+ sandwich-storage/refit/Satterthwaite/auto-policy test files — PASS (NOT_CRAN=true).devtools::document()regenerated Rd; NEWS entry included.Out of scope / follow-ups
EXACTCL2-IMPLEMENTATION-NOTES.md).🤖 Generated with Claude Code
https://claude.ai/code/session_01WFfnErmWi9NCGCCJ8YwE95