Changelog
Source:NEWS.md
roadrunner 0.0.0.9063
meep() — isotonic propensity calibration (on by default)
Cross-fitted binomial nuisances (the propensity score, and any binary-outcome nuisance) are now post-processed through pooled isotonic calibration (causal isotonic calibration; van der Laan, Carone, Luedtke & van der Laan 2023). The calibration map is monotone, so it leaves the learner’s ranking — and hence ROC/AUC — unchanged while putting the probabilities on a properly calibrated scale, which improves the finite-sample coverage of downstream DML / AIPW estimators. Controlled by the new calibrate argument ("isotonic" default, "none" to disable) with truncation via calibrate_bounds. The fitted calibrators are stored and re-applied by predict.meep() on new data.
Other recent meep() additions
-
plda()is now a default learner for binomial nuisances; default learners are selected automatically per nuisance family. -
extra.learnersadds optional external learners to the stack —"forest"(ranger) and"BART"(dbarts) — kept inSuggests, not dependencies. -
plot.meep()gives quick per-learner / stack diagnostics: ROC curves for binary nuisances, OOF R² and observed-vs-predicted for continuous ones.
roadrunner 0.0.0.9059
bgam() — component-wise P-spline gradient boosting
bgam() fits a smooth, strictly additive model via component-wise functional gradient boosting (Bühlmann & Yu 2003; Schmid & Hothorn 2008). At each of mstop iterations the algorithm selects the single penalised B-spline base-learner — one per predictor — that most reduces the current loss, adds a shrinkage-scaled ridge update for that component only, and repeats. The result is a smooth GAM with built-in variable selection: predictors that are never selected retain a zero coefficient vector and their selection frequency ($selection_frequency) serves as a natural importance metric. bgam() fills the smooth-additive-model-with-variable-selection gap in the roadrunner lineup that neither ares() (piecewise-linear, interaction-capable) nor krls() (global kernel, no variable selection) covers natively. It scales as O(n · p · K) per iteration and is particularly effective at n = 1000+, p moderately large, smooth signal settings.
Supported families: "gaussian" (squared-error loss) and "binomial" (logistic / binomial deviance, binary response).
Key arguments: mstop (boosting iterations), nu (shrinkage, default 0.1), nknots (interior knots per predictor, default 20), degree (B-spline degree, default 3), dpen (penalty order, default 2), df_target (target effective df per base-learner, default 4), unpenalized (predictors entered as linear instead of spline), weights (per-observation weights), n.boot (bagging replicates), autotune (k-fold CV mstop selection, default TRUE).
meep() integration: bgam is an opt-in learner; activate via meep(X, y, treatment = D, learners = c("ares", "krls", "bgam")). Pass extra arguments through bgam_args = list(...). bgam is family-agnostic: gaussian path for continuous nuisances, logistic path for propensity scores. Hyperparameters frozen by tune = "once": mstop, nu, nknots, degree, dpen.
Parity oracle: mboost::gamboost() (Hofner et al. 2014). mboost is listed under Suggests and is never a hard dependency; parity tests guard with skip_if_not_installed("mboost"). Loss trajectories agree to cor > 0.99 at matched mstop and nu; eta-scale predictions agree to cor > 0.85 for gaussian and cor > 0.80 for binomial (implementations diverge on knot placement and lambda calibration — see test-spec for rationale).
Trimmed simulation (inst/sims/results/bgam-trimmed-0.0.0.9059.md): 4-cell × 50-rep Gaussian DGP (y = sin(2πx1) + 0.5·x2² + N(0, 0.5²)) shows bgam tracks ares within Monte Carlo SE on the small-n / high-p cell where the additive smooth signal is best exploited, and both learners reduce misspecified OLS RMSE by 30–60%. Full 16-cell × 200-rep run (with heteroskedastic and binomial DGPs) is deferred to a follow-up.
roadrunner 0.0.0.9058
meep() krls learner: curve-fitter defaults
meep() now invokes its krls learner with vcov = FALSE and derivative = FALSE by default. The meep ensemble only consumes the out-of-fold prediction matrix from each nuisance fit; the SE block (coefficient + fitted-value covariance) and the marginal-effects block (pointwise + average derivatives + variance of average derivatives) were computed on every fold of every nuisance and then discarded.
Per-fit cost share at n = 1500 (v9057 single-fit profile): vcov block ~15% (skipped under vcov = FALSE) derivative block ~23% (skipped under derivative = FALSE)
End-to-end measurement (meep(learners = "krls", folds = 5), 12 threads, Friedman-1-like DGP with binary treatment):
n = 800 : 9.5s -> 9.1s 1.04x (4% saved) n = 1500 : 53.6s -> 51.7s 1.04x (4% saved)
OOF prediction matrices are bit-identical between the two paths (max|delta| == 0 on both y_hat_oof and d_hat_oof); the overall meep speedup is modest because meep wall is autotune-dominated (per-cell eig in the inner CV), but the saved work is real, the krls fits in fit$folds[[k]]$models$krls are now ~2 * n^2 + n * p doubles lighter, and any downstream uncertainty quantification for DML / CF nuisance fits should be derived from the residuals / OOF predictions rather than the individual fit-level SEs.
Opt back in for vcov + derivative via krls_args:
The ares / ols / logreg learner specs are unchanged.
132 meep tests pass; lint clean.
roadrunner 0.0.0.9057
krls() single-fit: closed-form spectral vcov
The unweighted (weights = NULL) krls(..., vcov = TRUE) path now computes coefficient and fitted-value covariances directly from the eigen-decomposition that was already produced for the lambda search, using the spectral identity K = V diag(d) V^T:
vcovmatc = V diag(w) V^T = tcrossprod(V scaled by sqrt(w))
vcovmatyhat = V diag(d^2 * w) V^T = tcrossprod(V scaled by d sqrt(w))where w = sigma2 / (d + lambda)^2. This replaces two R-level O(n^3) dgemm calls (V %*% (t(V) * w) and crossprod(K, vcovmatc %*% K)) with two O(n^3 / 2) dsyrk calls via tcrossprod; no K matrix is touched on this path. Outputs match the v9056 formulation to within ~1e-15 (machine epsilon) and are symmetric by construction.
Measurements (16-core MKL, Friedman-1, 12 threads, inst/sims/v0.26-krls-speed-baseline.R cells, cumulative vs v9055):
| cell | v9055 | v9057 | speedup |
|---|---|---|---|
| n=400 gcv | 0.033s | 0.026s | 1.27x |
| n=1500 gcv | 0.403s | 0.353s | 1.14x |
| n=1500 default | 0.534s | 0.498s | 1.07x |
| n=400 autotune | 1.456s | 0.758s | 1.92x |
| n=800 autotune | 8.235s | 5.526s | 1.49x |
| n=1500 autotune | 50.87s | 37.92s | 1.34x |
Geometric-mean speedup across all 9 baseline cells: +20.7% vs v9055.
The weighted (!is.null(weights)) path retains the v9056 two-gemm formulation because the spectral simplification assumes V diagonalises the kernel used in the K-times-K reduction; in the weighted path V diagonalises K_w = D K D (not K), and the residual V^T D^-1 V terms do not collapse. The non-Gaussian / logistic / Nystrom paths are unaffected.
AUDIT-D thread-determinism is preserved (nthreads = 1 and nthreads = 4 produce bit-identical K, coeffs, fitted, vcov.c, vcov.fitted, avgderivatives). 639 krls tests pass with ARES_FULL_TESTS = 1; lint clean.
roadrunner 0.0.0.9056
krls() autotune: progressive sigma-grid pruning
krls(..., autotune = TRUE) now drops clearly-losing sigmas from the inner cross-validation as soon as evidence accumulates. After prune_warmup folds (default 2), any sigma whose mean held-out MSE exceeds prune_factor x the running best (default 1.5) is removed from the remaining fold evaluations, unless it sits within prune_keep_neighbors grid positions of the running best (default 1, prevents collateral damage near the minimum on flat MSE landscapes).
Measured on a 16-core MKL box (Friedman-1, autotune.speed = "fast", 12 worker threads), comparing v0.0.0.9056 vs v0.0.0.9055 over the inst/sims/v0.26-krls-speed-baseline.R grid:
| cell | v9055 | v9056 | speedup |
|---|---|---|---|
| n=400 autotune | 1.46s | 0.80s | 1.82x |
| n=800 autotune | 8.23s | 5.50s | 1.50x |
| n=1500 autotune | 50.87s | 38.23s | 1.33x |
Selected sigma, lambda, coeffs, and held-out RMSE are bit-identical to v9055 on every cell tested. The pruning gate is use_inner_cpp-only (Gaussian + ls + unweighted + no user bracket), so the legacy serial fallback and non-Gaussian / weighted / logistic paths are unchanged.
New options (all with safe defaults):
-
roadrunner.krls.autotune.prune_factor(default1.5). Set toInfto disable pruning entirely and recover v9055 behaviour byte-for-byte. -
roadrunner.krls.autotune.prune_warmup(default2L). Minimum folds populated before pruning may activate. -
roadrunner.krls.autotune.prune_keep_neighbors(default1L). Window around the running-best sigma that is protected from pruning.
fit$autotune gains prune_factor, prune_warmup, prune_keep_neighbors, prune_active, pruned_n, and prune_log for inspection.
roadrunner 0.0.0.9055
Tier A audit bug fixes (8 fixes)
This release closes eight bugs identified by the 2026-05-22 audit. Full plan: docs/superpowers/audits/2026-05-22-package-audit/01-tier-A-fix-plan.md.
-
BUG-014 (
ares):pmethod = "cv"crashed with"subscript out of bounds"when a small fold’s forward pass collapsed to an intercept-only model (M_full == 1). The C++ engine populates$path.subsets/$path.coefsonly whenM > 1, so those lists were empty for the M=1 case. The CV loop now guards the index against list length and scores the intercept-only prediction directly from$coefficients, letting the fold contribute a valid size-1 MSE. -
BUG-015 (
predict.ares): when the fit carried$factor_info(or a$terms),model.matrix()ran throughmodel.frame()with the globalna.action(defaultna.omit), silently droppingnewdatarows that hadNAin numeric columns. The returned vector was shorter thannrow(newdata), violating the predict-length contract. Both branches now build the model frame withna.action = na.passso NAs flow through to the downstream median-impute / NA-row-mask path. -
BUG-016 (
krls):predict(weighted_krls_fit, type = "variance")returned ~0 across all rows. The fit stashed(V, dvals)from the weighted kernelK_w = D K D(D = diag(sqrt(w))), but the predict-side helper builtK_starfrom the unweighted kernel. The helper now column-scalesK_starbysqrt(w_tr)before the quad-form, implementingV[f*] = K**(unwt) - K* D (K_w + lam I)^-1 D K*'. -
BUG-017 (
krls):predict(bagged_krls_fit, newdata = NULL)returned$fitted– the central fit – instead of the bag mean across replicates. The non-NULL branch already does the bagging, so the NULL-newdata fast-path was inconsistent with the predict-on-newdata behaviour. The fix routes bagged NULL-newdata calls through the non-NULL branch (withnewdata = object$X). -
BUG-018 (
meep):predict.meep(nuisance = "mu0" | "mu1")returned all-NA for any weightedmeep()fit. The arm-restricted refit insidepredict.meeppassedobject$weights(length n) without slicing to the treatment-arm subset, so the base learner erroredlength(weights) != length(y), the error was swallowed, and the prediction collapsed to NA. Weights are now sliced to the arm rows. -
BUG-019 (
meep): a factor or character outcome with three or more levels was silently coerced to integer codes and fit as a regression. Multi-class outcomes are out of scope;meep()now rejects them upfront with a clear message, mirroring the treatment-validation pattern. -
BUG-020 (
logreg):null.deviancedisagreed withstats::glm()for no-intercept formulas (y ~ 0 + x). The C++ engine always used the weighted mean of y as the null mu, but the canonical null model under no intercept iseta = 0 -> mu = 0.5. The engine now takes ahas_interceptflag (R-side detected as"(Intercept)" %in% colnames(X)orintercept = TRUE), and usesmu0 = 0.5when false. -
BUG-021 (
plda): whennfold > min(table(y)), the stratified fold builder placed each class only into folds1..length(class), so some folds received zero test observations and biased the CV error toward 0 (selecting the wrong lambda).plda()now rejects upfront with a clear message recommending the largest valid nfold.
roadrunner 0.0.0.9054
meep() — ols() and logreg() are now default learners
meep()’s default learners is now c("ares", "krls", "ols", "logreg") (previously c("ares", "krls")). The two linear learners are applied per nuisance by family: ols is fitted only for gaussian (continuous) nuisances and logreg only for binomial (binary) ones, while ares and krls remain family-agnostic and apply everywhere.
- Because the Y-model and the D-model can differ in family, applicability is resolved per nuisance. A gaussian outcome with a binary treatment fits
olsforoutcome/mu0/mu1andlogregfortreatment. - A learner that does not apply to a nuisance is skipped cleanly: its OOF column stays all-
NA(the ensemble already excludes all-NAcolumns) and it is not recorded as a fold failure. - Narrowing
learnersto names that are all family-incompatible with a nuisance (for examplelearners = "ols"with a binary outcome) is now an error naming the nuisance, its family, and the cause. - Custom list-learners are never family-filtered.
This changes default meep() output: the default OOF prediction matrices now carry ols/logreg columns and the ensemble weights are recomputed over the larger learner set.
roadrunner 0.0.0.9053
ols() and logreg() — unregularized linear fitters
Two new user-facing functions, ols() and logreg(): fast, low-dependency unregularized linear fitters with C++ (Rcpp/Armadillo) engines, classical and HC robust standard errors, and the package’s standard base-R-style API.
-
ols(x, y, weights, n.boot, varmod, ...)— ordinary and weighted least squares via a weighted economy-QR solve. Matrix and formula interfaces. Returns an S3 object of class"ols". A rank-deficient design matrix is rejected with a clear error rather than silently dropping columns. -
logreg(x, y, weights, n.boot, ...)— binary logistic regression by iteratively reweighted least squares (Fisher scoring). Matrix and formula interfaces; accepts a 0/1 numeric, logical, or two-level-factor response. Returns an S3 object of class"logreg". Non-convergence (the signature of quasi-complete separation) is reported with a warning. -
Robust SEs — classical and HC0–HC3 sandwich covariances; surfaced in
summary()andpredict()via arobustargument. -
Bagging —
n.boot > 0refits on bootstrap-weight resamples; baggedpredict()returns the bag mean. Serial loop, deterministic across thread counts. -
Prediction intervals —
ols()supportsvarmod = "const" | "lm"for heteroskedastic gaussian prediction intervals (sharedares()variance-model helper). -
S3 methods —
predict,print,summary,plotfor both classes. -
meep()integration —"ols"and"logreg"are opt-inmeep()learners (ols= gaussian,logreg= binomial). The defaultlearnersstaysc("ares", "krls"). Having no hyperparameters,tuneis a no-op for them: every cross-fitting fold is a plain refit.
roadrunner 0.0.0.9052
plda() — Penalized Linear Discriminant Analysis
New user-facing function plda(): penalized Fisher’s linear discriminant analysis (Witten & Tibshirani 2011) with L1 and fused-lasso penalties, multi-class support, built-in CV autotune, and parallel CV fold execution.
-
plda(x, y, K, lambda, penalty, autotune, ...)— matrix and formula interfaces. Returns an S3 object of class"plda"carrying the discriminant matrix, class means, within-class standard deviations, and (whenautotune = TRUE) the full CV result. -
Penalties —
penalty = "L1"(default) applies coordinate-wise soft thresholding;penalty = "fused"adds a fused-lasso difference penalty (Condat 2013 total-variation prox) controlled bylambda2. -
Multi-class — supports
G >= 2classes;Kdiscriminant vectors up toG - 1. -
Autotune —
autotune = TRUE(default) cross-validateslambdaandKover a log-spaced grid; parallel CV fold loop via TBB with the roadrunner determinism invariant (byte-identical results for anynthreads). -
S3 methods —
predict.plda(class, posterior, projection),print.plda,summary.plda,plot.plda(2-D discriminant projection).
roadrunner 0.0.0.9051
meep() — Phase Q-DML: cross-fitted causal ensemble
New user-facing function meep(): a pure-R orchestration layer over the package’s ares() (MARS) and krls() (Kernel Regularized Least Squares) fitters that produces honest, cross-fitted (out-of-fold) nuisance estimates for Double Machine Learning and causal-forest workflows. meep() does not estimate a treatment effect itself — it returns the cross-fitted predictions and residuals, which are meant to be handed downstream to grf::causal_forest() (via Y.hat / W.hat) or to a DML estimator.
-
meep(X, y, treatment = NULL, ...)— a grf-style matrix/vector interface. Returns an S3 object of class"meep"carryingy_hat_oof,d_hat_oof,mu0_hat_oof/mu1_hat_oof,y_resid,d_resid, thefoldsvector, the per-nuisance OOF prediction matrices, the ensemble weights, and per learner-by-nuisance OOF performance. -
Stacking ensemble —
ensemble = "stack"fits non-negative least squares on the honest OOF prediction matrix (leakage is already purged column-wise, so no second nesting layer is needed)."average"and"best"are cheaper fallbacks. The NNLS solver is hand-rolled (Lawson–Hanson active-set) — no new package dependencies. -
tune = c("once", "per_fold", "none")—"once"(default) autotunes each learner on the full data, freezes the hyperparameters, and refits per fold;"per_fold"autotunes inside every fold;"none"is the fast path that callsares()/krls()with their own defaults and never invokes autotune. -
Family auto-detection — a binary outcome routes to
ares(family = "binomial")+krls(loss = "logistic"); a binary treatment is modelled as a propensity score. Multi-valued treatments are rejected with a clear error. -
Cluster-robust folds — the
clusterargument keeps whole clusters within a fold. -
Graceful degradation — a learner that errors on a fold is dropped for those rows, the ensemble weights are renormalized over the survivors per row, and the event is logged in
$fold_failures. -
predict.meep(),print.meep(), andsummary.meep()(with an overlap diagnostic for binary treatments) S3 methods. -
grfadded toSuggestsfor the gated integration test.
roadrunner 0.0.0.9050
krls() — Phase Q5: true logistic-loss IRLS path (B1)
Closes the biggest functional gap vs lukesonnet/krls2: real logistic ridge regression in the kernel basis, optimised via IRLS on penalised binomial deviance. Default loss = "ls" remains byte-identical to v0.0.0.9049 (back-compat snapshot in test-krls-ard-kernel.R continues to pass to ULP tolerance, and loss = "ls" fits are byte-equal to v9049 to tolerance = 0).
loss = c("ls", "logistic")new argument onkrls.default(). Default"ls"is the existing kernel ridge LS path."logistic"optimisesminimise -2 sum(y log p + (1-y) log(1-p)) + lambda c' K cwherep = plogis(K c), via a Cholesky-backed Newton step in coefficient space (option ii in the spec): each iter solves(K W K + lambda I) c = K W zwithW = p(1-p)and working responsez = eta + (y - p) / W. ~5-10 iters typical at default tolerance1e-6; cap 50. Step-halving (up to 5 per iter) on penalised-deviance increase. Numerical guards: W floored at1e-8, eta clipped to[-30, 30], perfect-separation detection (||c||_inf > 1e6OR penalised dev< 1e-10) warns and returns the last finite iterate.-
Compose rules.
loss = "logistic"requires binary y in{0, 1}. Combinations that don’t make sense (or are deferred) error early:-
+ approx = "nystrom"— deferred (large-n use case). -
+ lambda.method = "gcv"— Wahba 2-D extension; deferred. -
+ lambda.method = "mll"— Laplace approx; deferred to Q9. -
+ varmod != "none"— residual variance model is LS-specific. -
predict(type = "variance")— Q6 will derive the IRLS-aware posterior variance; current Q5 errors.
Composes fine with:
whichkernel = "linear"/"poly*",ard = "cheap",autotune = TRUE(deviance scoring; force R fallback because the Gaussian-specialised C++ inner is LS-specific),n.boot > 0(bagged averaging on the LINK scale, sigmoid at end for Jensen),lambda.method = "loo"(closed-form CT-2008), andlambda.method = "cv"(fold-deviance scoring),derivative = TRUE(link-scale marginal effects by default),weights(case weights baked into IRLSW = w * p(1-p)). -
CT-2008 closed-form LOO under logistic. Cawley & Talbot 2008 leave-one-out approximation, computed in coefficient space from a single
H = K (K W K + lambda I)^{-1} Ksolve. The diagonalH_diagis stashed on the fit (one extra n x n solve per converged IRLS).cv_loo_devreplacesLooeunder logistic.Predict gains
type = "class"(hard 0/1 prediction). For logistic fits:type = "link"returns eta;type = "response"andtype = "prob"return calibrated probabilities;type = "class"returnsas.integer(p > 0.5). For binary LS fits,type = "class"uses the existing plogis-on-LS shortcut.Print / summary. Logistic fits print a
"loss: logistic (IRLS, k iter, converged)"line, use McFadden pseudo-R-squared in place of LS R-squared, and append a"(link scale)"suffix on the average marginal effects header.
Internal
- New C++ exports:
krls_irls_logistic_cpp(IRLS solver returning coeffs, eta, p, W, deviance, iter, converged, H_diag),krls_logistic_loo_loss_cpp(CT-2008 closed-form LOO deviance). - New R helpers:
.krls_logistic_lambdasearch,.krls_logistic_fit_block,.krls_bag_logistic. -
.krls_autotune_scalar()acceptsis_logistic = FALSE; when TRUE, per-cell IRLS fits + deviance scoring replace the LS solve. -
.krls_run_cheap_ard()acceptslossand forwards through the pass-1 isotropic fit so ARD under logistic works end-to-end. -
slim_krls()stripseta_fittedandH_diagalong with the existing heavy intermediates.slim_krls(keep_predict = FALSE)retainsloss,deviance,converged,iter,cv_loo_devfor the inspection-grade summary. -
var.avgderivativesunder logistic isNA(full IRLS-aware derivation deferred to Q6) with a one-shot warning.
Tests
-
test-krls-logistic.R(~250 LoC, 13 tests): binary-y validation; IRLS convergence (< 20 iter); glmnet deviance match (skip_if_not_installed("glmnet")); Brier improvement vs LS+plogis; CT-2008 LOO matches explicit refit (n = 20 toy, tol 5e-3); CV under logistic; autotune + logistic; bagging + logistic; predict type consistency; link-scale marginal effects; compose rejections (Nystrom, GCV, MLL, varmod); ARD cheap + logistic on sparse signal lifts AUC; back-compat —loss = "ls"is byte-identical to v9049 totolerance = 0.
roadrunner 0.0.0.9049
krls() — Phase Q2: polynomial kernels + GP variance + MLL
Three bundled feature items implemented behind a single polymorphic C++ kernel dispatch. Gaussian default fits are byte-identical to v0.0.0.9048; non-Gaussian kernels open up K_ij = x_i' x_j (linear) and K_ij = (x_i' x_j + poly_c)^d for d in 1..4 (polynomial).
A1 —
whichkernel = c("gaussian", "linear", "poly1..4")+poly_c. Default"gaussian"is unchanged."linear"is the inner-product kernel;"poly1..4"are inhomogeneous polynomial kernels with offsetpoly_c(default1.0). Non-Gaussian kernels are incompatible withapprox = "nystrom",ard != "none", and vectorsigma— all error at fit time with clear messages. Marginal effects are computed via a closed-form dgemm route (D = d * H * XwithH = (X X' + c)^(d-1)for poly, constant per column for linear).var.avgderivativesis currentlyNAfor non-Gaussian kernels (full derivation deferred to Phase Q6); a one-shot warning surfaces this at fit time. Autotune dispatches: Gaussian sweepssigma(9-pt anchor, existing); poly sweepspoly_c(defaultc(0, 0.25, 0.5, 1, 2, 4, 8)); linear collapses to lambda-only (errors if user suppliesautotune.grid). Non- Gaussian autotune runs through the R fallback at the scalar autotune helper rather than the Gaussian-specialisedkrls_autotune_inner_cpp— expect a 5-10x wall-clock regression vs Gaussian autotune at matching grid sizes; this is acceptable for Q2 scope. The fit list gains$whichkernel,$kernel_type,$poly_c.A6 —
predict(..., type = "variance"). Newtypevalue returns the GP posterior varianceK** - K* (K + lambda I)^{-1} K*'per row ofnewdatavia the newkrls_posterior_var_cpphelper. Closed-form computation in the eigen-basis of the fit’s kernel: reuses cacheddvals+V(now stashed on the fit at v9049). Newunscalearg (defaultFALSE): whenTRUE, multiplies the returned variance byvar(y_train)so it sits on the raw y scale. Incompatible withapprox = "nystrom"(would require the m-length Phi spectrum — deferred). Bagged fits average per- replicate posterior variances. Matches a brute-forceKss - diag(K* (K + lI)^{-1} K*')reference totol = 1e-8on n=50. FP-floored at 0.A11 —
lambda.method = "mll". Closed-form Type-II marginal log-likelihood as the lambda-selection objective. Pure-R loss.krls_mll_loss(dvals, Vty, lambda, n)reuses the same eigen- basis cache as LOO/GCV. Incompatible withapprox = "nystrom". Composes with autotune over sigma (Gaussian) and with non-Gaussianwhichkernel. Sigma-by-MLL selection is deferred to Q8.
Internal
- New C++ exports:
krls_posterior_var_cpp(closed-form GP variance in eigen-basis). Existing kernel exports (krls_kernel_cpp,krls_kernel_pred_cpp,krls_deriv_cpp,krls_avg_deriv_var_cpp) gain optionalkernel_type+kernel_carguments with default(0, 1.0)(Gaussian, back-compat). - New R helpers:
.krls_mll_loss(),.krls_predict_variance(). -
.krls_autotune_scalar()acceptskernel_type+poly_c; when non-Gaussian, forces the R-side per-cell fallback (the Gaussian-specialised C++ inner is preserved untouched). -
slim_krls()strips the newdvalsfield along with the existing heavy intermediates.slim_krls(keep_predict=FALSE)retains$whichkernel,$kernel_type,$poly_cfor the inspection-grade summary. -
predict.krls_rr()accepts newtype = "variance"andunscalearguments; legacy fits (nokernel_type/poly_cfield) default to Gaussian via fallback at predict time.
Tests
-
test-krls-kernels-poly.R(~150 LoC, 17 tests + 2 slow): linear kernel byte-equal vs hand-builtXX'; poly2 byte-equal vs(XX'+c)^2; linear deriv constant per column; poly2 deriv matches finite difference totol = 1e-4; validation gates; autotune poly_c sweep; bagging + linear; Gaussian back-compat seal. -
test-krls-posterior-var.R(~120 LoC, 8 tests + 1 slow): closed- form matches brute force totol = 1e-8; training-point var =1 - diag(H); monotonic increase with distance from training (1D toy);unscalearg; Nystrom errors; bagged averages; poly2 posterior var diagonal; FP floor at 0. -
test-krls-mll.R(~95 LoC, 6 tests + 1 slow): closed-form matches hand-computed grid argmin; MLL ≠ LOO chosen lambda on real data; Nystrom + MLL errors; composes with autotune; composes with poly2.
krls() — Phase Q1 parity batch
Four small parity items, all R-side (no C++ changes), batched together to avoid four trivial release cycles.
-
A3 —
Neffective(effective df). Fits now carry$Neffective, the trace of the smoother hat matrixH = K (K + lambda I)^{-1}. Computed in the eigen-basis assum(d / (d + lambda)), wheredare the kernel eigenvalues used for the solve. The Nystrom path uses the m-lengthPhi' Phispectrum so the formula generalises tosum(Sigma2 / (Sigma2 + lambda)).summary(fit)prints an"Effective df: <value>"line near the existingR^2line.Neffective -> 0aslambda -> Infand-> n(or the rank ofK) aslambda -> 0. -
A7 —
subsetonkrls.default(). The matrix / data-frame default method now acceptssubset = NULL(a no-op, back-compatible) or a logical / integer vector selecting rows ofX.yandweightsare sliced in lockstep before any downstream processing. Mirrors the formula method’ssubsetsemantics. -
A8 —
predict(..., type = "prob")for binary fits.predict.krls_rr()gains atype = c("response", "link", "prob")argument."response"(default) preserves the prior behaviour byte-for-byte."prob"is valid only when the fit is binary (yhas exactly two unique values, tracked on the fit via the new$binary_yfield) and returnsplogis(yhat). The docstring is explicit: this is a calibration shortcut, not a true posterior probability; the underlying fit remains least-squares loss."link"is a synonym for"response"reserved for the future logistic-loss extension. -
A9 —
slim_krls()/unslim_krls(). New exported helpers for cheap serialisation.slim_krls(fit, keep_predict = TRUE)(default) strips the heavyK,V,Vsq,Vty,vcov.c,vcov.fittedintermediates while preserving everythingpredict()needs; saved size typically drops by >50% atn >= 200.slim_krls(fit, keep_predict = FALSE)strips further to an inspection-grade summary (coeffs,R2,Looe, marginal effects, sigma, lambda,Neffective);predict()then errors with a clear message.unslim_krls()is a no-op placeholder — refitkrls()with the stored$callto rebuild the dropped fields.
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.