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 6 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 <noreply@anthropic.com>
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 <noreply@anthropic.com> 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 <noreply@anthropic.com> 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 <noreply@anthropic.com> 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 <noreply@anthropic.com> 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 |
Study LB recorded Poisson replicates whose cluster-robust interval widths were numerically exploded -- up to 1e133 -- while the fit was recorded as a success, so no failure gate saw them. Forensics: those replicates are ones where the simulated response ran many orders of magnitude above its nominal mean, leaving a fit whose model-based bread is no longer the inverse of the same weighted cross-product. The penalized hat then violates bounds that hold exactly (0 <= h_ii <= 1, 0 <= eigen(H_gg) <= 1): per-cluster hat eigenvalues up to 1.8e131 were recorded. CR1 explodes there too, so this is not a defect of the leverage adjustment. gam_sandwich_cluster_cl2() now monitors those invariants -- both monitors are free, h_ii falls out of T_g and the block eigenvalues are computed anyway -- and warns when they break by more than round-off, exposing max_obs_leverage, min_block_eig_rel and hat_invariant_violation as attributes. It warns rather than substituting: capping H_gg silently turns an impossible leverage into the largest legitimate one, so the shortcut answers garbage with its maximal inflation while the exact block, PSD by construction, deflates. Both are bounded, neither is meaningful, and falling back to CR1 would only swap in a covariance built from the same broken bread. Measured on the new fixture at max(Y) = 8.3e17: CR1 5.2e85, shortcut 1.7e87, exact 8.7e8 -- the exact path is far less explosive, but not trustworthy either. Also restores tests/testthat/test-pffr-exactcl2.R, which has been unable to run since 276ba71 removed the top-level script it source()d; its fixtures now live in tests/testthat/helper-exactcl2.R, together with the new P-LB5 fixture. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FaZwk9uigjyiVkuTTigSaH
|
New commit f8a8fa6 — hat-invariant detector (follow-up hardening from the locked-benchmark P-LB5 finding). The locked confirmatory benchmark recorded a handful of Poisson replicates whose cluster/CL2 interval widths exploded (up to ~1e30 in the recorded metrics) while evading the failure gates. Forensics traced every one of them to numerically degenerate fits: the simulated response ran many orders of magnitude above its nominal mean (an uncapped latent-Gaussian mechanism), and on such fits the penalized hat matrix breaks bounds that hold exactly in exact arithmetic ( What this commit adds to
The A18 hardening gate re-run passes byte-identically; a new 24-cell gate covering this regime passes (script and results archived in the paper repo: |
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