Changelog
Source:NEWS.md
roadrunner 0.0.0.9047
krls() — autotune unification + auto-ARD dispatch
-
autotune = TRUEbecomes a hands-free dispatcher. New argumentautotune.speed = c("balanced", "quality", "fast")(default"balanced") picks between the v0.0.0.9046 scalar sigma sweep and an ARD-dispatched path that routes through the cheap-tier ARD orchestrator:-
"fast"keeps the v0.0.0.9046 scalar sigma grid behaviour byte-identically. -
"balanced"(default) dispatches through cheap-tier ARD whenncol(X) >= 20orncol(X) >= nrow(X) / 10(heuristic for high-p / sparse-signal regimes); otherwise scalar sigma only. -
"quality"always dispatches through ARD and sweeps a 6-cell grid overard.alpha in {0.5, 1, 2}xard.imp in {"avgderiv", "vsq"}, picking the winner by inner K-fold CV.
-
- New argument
autotune.warmstart = TRUEruns a cheap probe on a 15% subsample (capped at 200 rows) to test whether ARD dispatch outperforms the isotropic baseline; if the ARD probe does not improve held-out MSE by >2%, the ARD branch is dropped. Skipped whenn < 200orautotune.speed = "fast". - The prior hard rejection of
ard != "none" + autotune = TRUEis lifted. When the user pinsard = "cheap"explicitly underautotune = TRUE, autotune routes through ARD regardless ofautotune.speed. -
print.krls_rr()now reports the autotune dispatch decision + selected(alpha, imp)for ARD-dispatched fits, and the chosen sigma for scalar fits. -
$autotuneoutput schema gains$speed,$warmstart,$ard_dispatched,$ard_decision_rule,$winner_sigma,$winner_alpha,$winner_imp.$winneris preserved on the ARD path as the back-compat sentinelNA_real_.
Behaviour change
Behaviour change at v0.0.0.9047:
krls(autotune = TRUE)now dispatches through cheap-tier ARD on high-p / sparse-signal data (ncol(X) >= 20orncol(X) >= nrow(X) / 10) by default. To restore exact v0.0.0.9046 behaviour, passautotune.speed = "fast". On low-p dense data the default"balanced"mode is byte-identical to v0.0.0.9046.
Out of scope (P2c / P3 / P5)
- Gradient-based / marginal-likelihood ARD optimisation.
- HSIC pre-screen for high-p feature selection.
- Vector-sigma autotune sweep (requires C++ rewrite of
krls_autotune_inner_cpp). - Nystrom + autotune-ARD composition.
- Multi-objective tuning (R^2 + sparsity).
-
ssf_gridbeyondc(1.0); multi-threaded ARD cell grid.
roadrunner 0.0.0.9046
krls() — automatic two-pass ARD selector (cheap tier)
- New argument
ard = "cheap"enables a two-pass automatic ARD pipeline: pass 1 fits isotropic KRLS at the scale-aware sigma anchor; pass 2 refits with per-feature lengthscales derived from pass-1 marginal- effect importances vias_k = sigma_iso * (median(imp) / imp_k)^alpha, clipped to[sigma_iso / cap, sigma_iso * cap]. - New args
ard.alpha = 1.0(mapping exponent),ard.cap = 100(symmetric multiplicative ceiling),ard.imp = c("avgderiv", "vsq")(importance source). - Default
ard = "none"is byte-identical to v0.0.0.9045. - Composes with bagging (
n.boot), CV (lambda.method = "cv"), GCV, weights, and varmod. Rejects autotune + ARD and Nystrom + ARD at fit time (same constraints as manual vector sigma in P2a). - Compute cost: ~3-5x scalar isotropic. Pass 1 is a throwaway fit; its
varmod/binary/n.bootare forced off, butvcovstays on because the engine couples it toderivative = TRUE(which we need for importance extraction). The pass-1 outputs are discarded after the per-feature importance vector is read off. - Empirical lift: on grf::generate_causal_data aw3 with n_train=750, p=100 (sparse signal, mostly noise dimensions), test R^2 rises from 0.13 (isotropic) to 0.33 (ard=‘cheap’), comparable to ranger (0.34) on the same data.
roadrunner 0.0.0.9045
krls() — per-feature Gaussian bandwidth (manual ARD)
-
krls(..., sigma = c(s1, ..., sp))now accepts a length-ncol(X)vector of strictly positive lengthscales. Gaussian kernel becomes K_ij = exp(-sum_k (x_ik - x_jk)^2 / sigma_k). - Scalar sigma path is byte-identical to v0.0.0.9044 fits (FP determinism preserved via constant-vector fast detection in kernel worker).
- Marginal effects, vcov of average effects, and predict() all scale per-feature by 2/sigma_k and 4/sigma_k^2 respectively.
-
autotune = TRUE+ vector sigma errors at fit time; auto-selection of ARD lengthscales deferred to Phase 2b/2c. -
approx = "nystrom"+ vector sigma errors at fit time; ARD + Nystrom is P5 stretch goal.
roadrunner 0.0.0.9044
krls() — GCV lambda selection
-
krls(..., lambda.method = "gcv")selects the ridge penalty by minimising the closed-form generalised cross-validation criterion (Craven & Wahba 1979) on the existing eigendecomposition ofK. Brings parity withKRLSv1.5-0+. - Closed form: no extra kernel evaluation or refit; cost is one golden-section search over the same
[L, U]bracket used by LOO. - Denominator
(1 - trH/n)^2is floored at1e-8to remain finite in the interpolation limitlambda -> 0. -
lambda.method = "loo"remains the default."loo"and"cv"paths are byte-identical to v0.0.0.9043 on a fixed-seed problem.
roadrunner 0.0.0.9043
krls() speedup — Phase 2 (Nystrom low-rank approximation)
- New optional argument
approx = "nystrom"enables the Nystrom low-rank approximation. Replaces O(n^3) eigendecomposition on the full kernel with O(m^3) + O(n m^2) where m =nystrom_m(defaultceiling(sqrt(n) * 3), e.g. n=2000 -> m=135, n=5000 -> m=213). - Five new optional args:
approx,nystrom_m,landmarks,landmark_method,landmark_seed,nystrom_eps. - Default exact path is byte-identical to v0.0.0.9042 — opt-in only.
- New exported helper
get_landmarks(fit)returns landmark coordinates (original or standardized X scale). - Determinism: at fixed
landmark_seed(+seed.cvwhen autotune), Nystrom fits are byte-identical acrossautotune.nthreads.
Empirical wall-clock speedup (EMP-PHASE2, R=5 reps, paired seeds)
| n | p | approx | autotune | wall (s) | speedup | paired RMSE delta (median) |
|---|---|---|---|---|---|---|
| 2000 | 10 | exact | FALSE | 0.52 | 1.0x | - |
| 2000 | 10 | nystrom | FALSE | 0.056 | 9.27x | -0.02% |
| 5000 | 10 | exact | FALSE | 10.08 | 1.0x | - |
| 5000 | 10 | nystrom | FALSE | 0.329 | 30.64x | +4.81% |
Paired RMSE delta is the per-rep (RMSE_nystrom - RMSE_exact) / RMSE_exact on identical (X, y) draws (paired seed schedule keyed on (n, rep)); n=2000 range [-3.4%, +5.4%], n=5000 range [+4.0%, +8.9%]. Both well within the +10% target on the additive smooth DGP.
API compatibility
- Zero breaking changes.
krls()withoutapproxdefaults to"exact"and behaves identically to v0.0.0.9042. -
predict.krls_rrauto-detects Nystrom fits viafit$approxand uses the cheap cross-kernel path.
roadrunner 0.0.0.9042
krls() speedup — Phase 1 (shared distance + parallel autotune)
-
krls(..., autotune = TRUE)is now parallelised over the sigma grid via RcppParallel/TBB. The pairwise squared-distance matrix is computed once per CV fold and reused across every sigma candidate. - New optional argument
autotune.nthreads(defaultgetOption("roadrunner.nthreads", parallel::detectCores(logical=FALSE))). Passautotune.nthreads = 1for strictly sequential execution. - Determinism contract preserved: fits are byte-identical across
autotune.nthreadsvalues at fixed seed and inputs (each worker writes to a unique slot in the output vectors, so no reduction is involved).
Empirical wall-clock speedup (REQ-001 minimal grid, R=5)
| n | p | nthreads | time | speedup vs v0.0.0.9041 |
|---|---|---|---|---|
| 500 | 10 | 1 | (TBD) | 1.0x (sequential) |
| 500 | 10 | 4 | (TBD) | (filled by Task 9) |
| 1500 | 20 | 1 | (TBD) | 1.0x (sequential) |
| 1500 | 20 | 4 | (TBD) | (filled by Task 9) |
roadrunner 0.0.0.9041
krls() — scale-aware sigma anchor refined to geomean_p (REQ-20260518-003)
The default sigma anchor formula in .krls_sigma_anchor() is updated from the raw median heuristic (median(d2), introduced in v0.0.0.9040) to the geomean_p formula: sqrt(median(d2) * p) where d2 are pairwise squared Euclidean distances on the standardised training matrix and p = ncol(X).
Why: a 15-DGP head-to-head vs KRLS::krls() at n=500, p=10 (iter-0, REQ-20260518-003) showed the v0.0.0.9040 median anchor over-smoothed locally nonlinear signals, producing 8 losses and 0 ties in favour of KRLS. An anchor sweep over 6 candidate formulae (iter-1) identified geomean_p as the clear winner (14/15 DGPs). Verification with the patched anchor (iter-2) confirmed: roadrunner wins 12/15 DGPs, KRLS wins 0/15, 3 ties.
Delta table — iter-0 (median) vs iter-2 (geomean_p) mean MSE ratio (rr / KRLS):
| DGP | iter-0 ratio | iter-2 ratio | delta |
|---|---|---|---|
| additive | 1.327 | 0.896 | -0.431 |
| exp-decay | 1.681 | 0.887 | -0.795 |
| friedman1 | 1.126 | 0.959 | -0.167 |
| friedman3 | 1.084 | 0.957 | -0.127 |
| interaction | 1.471 | 0.895 | -0.576 |
| mixture | 1.140 | 0.992 (tie) | -0.148 |
| poly2 | 1.286 | 0.871 | -0.415 |
| tanh-interaction | 1.485 | 0.952 | -0.533 |
| friedman2 | 0.984 (tie) | 0.936 | -0.049 |
| sin-sum | 1.011 (tie) | 0.982 (tie) | -0.029 |
| linear | 0.872 | 0.919 | +0.047 |
| sparse | 0.865 | 0.917 | +0.052 |
| heterosked | 0.859 | 0.920 | +0.062 |
| monotone | 0.960 | 0.925 | -0.035 |
| noise | 1.000 (tie) | 1.000 (tie) | 0.000 |
At n=500, p=10 on standardised N(0,1) data the geomean_p anchor gives sigma ~ 13.5, splitting the difference between the raw median (~18–19) and KRLS’s fixed sigma = p = 10. The formula is still data-adaptive: on non-standardised or non-unit-variance inputs it will differ sensibly from both extremes.
This is a one-line R change (R/krls.R, .krls_sigma_anchor()); the C++ engine and all other logic are unchanged. At a fixed (sigma, lambda) fits remain byte-identical to all prior versions. To restore the v0.0.0.9040 median anchor, pass sigma = stats::median(as.numeric(stats::dist(scale(X)))^2).
roadrunner 0.0.0.9040
krls() — four overfitting fixes (REQ-20260518-002)
Addresses overfitting confirmed by diagnostic sweeps in REQ-20260518-001. All four changes are R-level only; the C++ engine is unchanged, so at a fixed (sigma, lambda) fits remain byte-identical to earlier versions.
Empirical improvement on signal DGPs (n=500, p=10, tune=none, R=10 replications; overfit ratio = test_MSE / train_MSE):
| DGP | Before (pre-fix) | After (post-fix) | Improvement |
|---|---|---|---|
| additive | 3.35 | 1.58 | 53% |
| interaction | 4.38 | 2.10 | 52% |
| sparse | 2.61 | 1.26 | 52% |
| linear | 2.70 | 1.37 | 49% |
| noise | 1.06 | 1.04 | — |
All improvement is attributable to Fix 1 (sigma default) and Fix 2 (lambda tolerance); Fixes 3 and 4 further stabilise autotune sigma selection.
Breaking defaults (old values restorable by explicit arguments):
Default
sigmachanged fromncol(X)to the median pairwise squared Euclidean distance on the standardised predictors (the ‘median heuristic’). Atn=500, p=10the oracle sigma is ~20 and the median heuristic anchors near that neighbourhood;sigma = ncol(X)was 10 (2x under-smoothed). Restore old behaviour withsigma = ncol(X).Default lambda tolerance changed from
1e-3 * n(n-dependent, coarse at moderate n) to1e-6(fixed, 6-digit precision). The LOO golden-section was empirically selecting lambda ~4x the argmin atsigma=20, n=500under the old tolerance. The L-bracket climb now uses multiplicative steps (x10 per step) instead of additive steps (0.05 per step) for scale-robustness. Restore old behaviour withtol = 1e-3 * nrow(X).Autotune sigma grid changed from
ncol(X) * c(0.25, 0.5, 1, 2, 4, 8)(6 points fixed atd) tosigma_anchor * c(0.125, 0.25, 0.5, 1, 2, 4, 8, 16, 32)(9 points centred on the median heuristic anchor). Restore old behaviour withautotune.grid = ncol(X) * c(0.25, 0.5, 1, 2, 4, 8).Autotune CV stabilised: default folds raised from 5 to 10; default cross-partition repeats raised from 1 to 2 (via the
ncrossargument, whose default changes from1LtoNULL); sigma selection now applies the 1-SE rule (largest sigma within 1 SE of minimum CV-MSE, biasing toward wider kernels when evidence is weak). Theautotunecomponent of the fit gains new fields:ncross,mse_per_fold,se_mse,cv.1se,sigma_1se. Restore old fold count withnfold = 5; restore old repeat count withncross = 1.
roadrunner 0.0.0.9033
New feature: krls() – Kernel Regularized Least Squares
- Adds
krls(), a from-scratch implementation of the Hainmueller and Hazlett (2014) KRLS estimator under the roadrunner roof. The algorithm mirrorsKRLS::krls()exactly (standardisation, Gaussian kernel, eigen-basis closed-form solve, golden-section LOO lambda search, marginal effects, binary first-difference handling); at matched(sigma, lambda)fits agree withKRLS::krls()to within floating-point precision (< 1e-12on coefficients, fitted values, pointwise and average marginal effects, and prediction SEs). - Engine is C++ (
src/krls.cpp) on top ofRcppArmadillo(added toLinkingTo+Imports) andRcppParallel. The Gaussian kernel and the test-vs-train kernel are built in parallel with a TBB worker. Eigendecomposition is dispatched to LAPACK viaarma::eig_symwith the divide-and-conquer driver. Marginal effects are computed via the identitydy/dx_k = -(2/sigma)*(X_k*(K c) - K diag(c) X)_k, which avoids the explicitn x ndistance matrix and reduces the average- marginal-effect variance fromO(n^3)toO(n^2)per variable via a row-sum trick. - Measured speed-up over
KRLS::krls()on simulated benchmarks (derivative = TRUE,vcov = TRUE) atnthreads = default:n=200 p=3~2x;n=500 p=3~5-6x;n=500 p=10~10x;n=1000 p=3~6x;n=1000 p=10~10x. Coefficient max-abs error vsKRLS::krls()is< 2e-13on every cell. - S3 class is
c("krls_rr", "krls")sopredict(),print(), andsummary()dispatch unambiguously to roadrunner methods even whenKRLSis loaded in the same session.inherits(fit, "krls")is preserved for downstream-compat checks. - Tests:
tests/testthat/test-krls.R(parity vsKRLS::krlsfor coefficients, fitted values, Looe, marginal effects, prediction SEs, binary first-differences; structural input-validation; predict recovers fitted values; sensible R^2). 11 tests, 28 assertions, all green.
roadrunner 0.0.0.9032
Bug fixes (statsclaw 2026-05-13 audit triage, BUG-008..BUG-013)
BUG-013 (usability, low):
print.ares()andsummary.ares()used to be silent about bagging (n.boot > 0) and autotune state, so a bagged or autotuned fit printed identically to a plain fit. Fix: add one-lineBagging: n.boot = N replicate(s)andAutotune: degree=D penalty=P nk=K fast.k=F warmstart=T/Fblocks to both printers when the corresponding components are present.summary.aresalso carries$bootand$autotunethrough to its print method. Regression test:tests/testthat/test-bug-013-print-bag-autotune.R.BUG-011 (correctness, medium):
ares.formula()silently droppedsubset = ...(it fell into...and went nowhere) and silently absorbedoffset(...)terms as ordinary predictors. Both produced wrong fits with zero indication. Fix: (a) add explicitsubsetarg toares.formulaand pass it through tomodel.framevia the lm()-stylematch.call()construction (so NSE inside model.frame doesn’t trip over thesubsetsymbol resolving to the base R function); (b) detectoffset()terms viaattr(terms, "offset")beforemodel.frameruns andstop()with an actionable message. Offset pass-through to the post-hoc GLM refit was punted to a later release (touches predict + bag + autotune compose paths). Regression test:tests/testthat/test-bug-011-formula-subset-offset.R.BUG-008 (correctness, high):
predict()used to return finite WRONG values for newdata rows containingNAwhen training usedna.action = "omit". The pre-existing warning promised “the affected rows will return NA predictions” but the code never imposedNA–NaN > 0in the C++ hinge evaluates toFALSE, collapsing each affected hinge to 0 and yielding a deterministic but wrong prediction. Fix: detect NA rows inxnewbefore the C++ basis pass, zero-fill the NaN cells, and re-imposeNAon those rows after the linear-predictor compute (including bag mean, bag SE, and theinterval = "pint"matrix path). Regression test:tests/testthat/test-bug-008-predict-na-rows.R.BUG-009 (correctness, high): bagged
predict(..., type = "link")for non-gaussian families used to returng(mean(g^{-1}(eta_b))), i.e. the link applied to the response-scale bag mean. By Jensen’s inequality this is notmean(eta_b), and the simulator audit showed divergences of hundreds of log-odds units for binomial bags at moderate signal – makingtype = "link"numerically unreliable for any downstream use. Fix: collect per-replicate linear predictorsetasand response-scale predictionsrespsseparately;type = "link"returnsrowMeans(etas),type = "response"returnsrowMeans(resps)(unchanged from the prior behaviour). Bag SE is computed on whichever scale was returned. Regression test:tests/testthat/test-bug-009-bagged-link-jensen.R.BUG-012 (correctness, medium): formula-path fits with derived terms (
I(x^2),poly(x, 2),log(x + 10),scale(x),splines::bs(x), …) used to fit successfully butpredict()would fail with “newdata is missing columns: I(x^2)” becausepredict.areslooked for the expanded column name as a literal column of newdata, not re-evaluating the originaltermsobject on newdata. Fix: whenobject$termsis non-null (formula path), usemodel.matrix(delete.response(terms), newdata, xlev = object$xlevels)to rebuild the design with derived terms re-evaluated. Falls back to the prior column-lookup path when notermsobject is stored (matrix interface).ares.formulanow also stashesxlevelson the fit. Regression test:tests/testthat/test-bug-012-predict-derived-terms.R.BUG-010 (robustness, medium): sister to BUG-004.
NAvalues in factor / character newdata columns used to fall through the OOV detector (which only handled non-NA character values), thenmodel.matrix(~ ., newdata)’s defaultna.action = na.omitsilently dropped those rows –length(predict(fit, newdata))was less thannrow(newdata). Fix: detect NA in factor / character newdata columns up front; error with a clear message naming the column(s), mirroring BUG-004’s OOV path. Regression test:tests/testthat/test-bug-010-predict-factor-na.R.
roadrunner 0.0.0.9031
Performance
- Autotune now reuses the Householder R / Qty computed during the shared forward pass instead of recomputing them inside each
mars_backward_only_cppcall. Saves the O(n*M^2) initial Householder pass at every per-cell backward replay; the cached R / Qty are byte-identical to the recomputed values, so selected basis, coefficients, and GCV are unchanged. Measured speedup on the v0.26 speed baseline (inst/sims/v0.26-speed-baseline.R, 24-cell grid x 5 reps, nthreads=4): geometric-mean 1.23x across the grid; on the autotune cells specifically 1.05-1.15x (gaussian highdim p=20: 1.09x; gamma n=1000 p=10: 1.11x; binomial n=1500 p=20: 1.15x). Determinism invariant (nthreads=1 == nthreads=N) preserved. Internal C++ entries gain optionalcompute_forward_qr(mars_fit_cpp) andR_in/Qty_in(mars_backward_only_cpp) parameters; the public R API is unchanged.
roadrunner 0.0.0.9030
New features
-
plot(fit)now produces a 4-panel diagnostic display in a 2x2 grid, modelled onstats::plot.lm(): residuals vs fitted, normal Q-Q of standardized residuals, scale-location, and residuals vs leverage with Cook’s-distance contours. Panels 4 (Cook’s distance) and 6 (Cook’s vs leverage) are also available viawhich. For binomial / poisson / gamma fits, the residual-vs-fitted panel uses deviance residuals and the hat matrix uses canonical-link IRLS working weights. The training observation weights are now stored on the fitted object ($weights).
roadrunner 0.0.0.9029
Bug fixes
Triage of 2026-05-11 adversarial audit (audit-2026-05-11/):
- BUG-001 (high): bagged GLM refits for
family = "binomial" | "poisson" | "gamma"now reuse the same bootstrap indices used to select the basis. The unseeded path used to redraw indices from the live RNG, so the post-hoc GLM coefficients were fitted on a different bootstrap sample than the basis. - BUG-002:
predict(bagged_fit)(withnewdata = NULL) now returns the bag mean, matchingpredict(bagged_fit, x_train). The trainingxis stored on the fit (out$x) to support this. - BUG-003:
varmod = "lm"prediction intervals now warn and floor at a meaningful lower bound (rather than1e-12) when extrapolation makes the predicted MAD non-positive. In-sample PIs are unchanged. - BUG-004:
predict()errors loudly on out-of-vocabulary factor or character levels innewdata, instead of silently dropping the affected rows. - BUG-005:
weightsmust now be strictly positive. Zero-weight rows used to biasGCVdownward and produce over-fitting; drop the rows fromx/yinstead. - BUG-006: documented honestly that
varmod = "lm"captures only yhat-dependent residual scale, not x-driven heteroscedasticity. - BUG-007:
family = "poisson"rejectsall(y == 0)(degenerate GLM fit); all families reject constanty(only the intercept can be fit).
roadrunner 0.0.0.9028
Package
- Renamed from
arestoroadrunner. The MARS fitter remains available asares(), with the"ares"S3 class and methods unchanged. Updatelibrary(ares)calls tolibrary(roadrunner).
ares()
-
familyaccepts"gaussian"(default),"binomial","poisson", and"gamma". The forward + backward MARS pass runs on the numeric response; the selected basis is then refit withstats::glm.fit()for non-gaussian families.predict()gainstype = c("response", "link"). -
weightsargument for observation weights. Composes with CV pruning, autotune, and bagging. -
pmethod = "cv"(and the convenience triggernfold > 0) performs K-fold cross-validated subset-size selection. Optionalncrossrepetitions, quantile-based stratification, and the 1-SE rule viacv.1se = TRUE. -
autotune = TRUEruns an inner-CV grid search over(degree, penalty, nk, fast.k).autotune.speedchooses among"balanced"(default),"quality", and"fast". A 20 % subsample warm-start short-circuits the full grid when the best-per-degree gap is decisive. -
n.boot > 0fits row-bootstrap replicates of the central model.predict()averages across replicates;se.fit = TRUEattaches the per-row bag standard deviation. -
varmod = "const" | "lm"(gaussian only) stores a residual variance model at fit time, enablingpredict(interval = "pint")for approximate prediction intervals. -
na.action = c("impute", "omit")handles missing values inx. Default"impute"median-imputes numeric columns and stores the medians for reapplication at predict time. - Factor and character columns in a data-frame
xare expanded viamodel.matrixand replayed on new data.