The R package autotune implements the Lasso with
data-driven tuning for linear models.
The development version of the autotune package can be
installed from GitHub using
# install.packages("devtools")
# Ensure that you have the Rcpp package installed with version >=1.0.13
# devtools::install_github("Tathagata-S/autotune")When installing from GitHub, in order to build the package from source, you need to have the appropriate R development tools installed (Rtools on Windows, or these tools on Mac).
autotune performs lasso via the
autotune_lasso() function.
We illustrate autotune lasso on simulated data using a linear model with \(s=10\), \(n=300\), \(p=500\). Reader can also specify different configurations of data generating process in the following code chunk.
set.seed(10)
n = 300
p = 500
s = 10
beta = c(rep(1, s), rep(0, p - s))
x = matrix(rnorm(n * p), ncol = p)
# Maunal sigma allocation
# y = x %*% beta + rnorm(n, sd = 1)
# Dynamic sigma allocation with snr specified
snr = 2
y = x %*% beta + rnorm(n, sd = sqrt(var(x%*%beta)/snr))Given data (x, y), run the autotune lasso as follows with default \(\alpha = 0.01\).
ptm <- proc.time()
fit.autotune <- autotune_lasso(x, y, alpha = 0.01)
proc.time() - ptm
#> user system elapsed
#> 0.013 0.003 0.016The regression coefficients \(\beta\)s, intercept, final lambda and the sequence of estimated sigmas can be extracted from the fitted autotune lasso object as follows
b.autotune <- fit.autotune$beta
intercept.autotune <- fit.autotune$a0
lambda.autotune <- fit.autotune$lambda
sigma.seq.autotune <- fit.autotune$CD.path.details$sigma_sq_seq
sigma.estimate.autotune <- fit.autotune$sigma_sqNow, autotune also supports the S3 function
coef which returns the estimated regression coefficients
\(\beta\)s along with the intercept
head(coef(fit.autotune), n = 21)
#> 21 x 1 sparse Matrix of class "dgCMatrix"
#>
#> (Intercept) -0.2506709
#> V1 0.5329995
#> V2 0.8984378
#> V3 0.6647993
#> V4 0.7491217
#> V5 0.7049080
#> V6 0.8335613
#> V7 0.8657125
#> V8 0.6522736
#> V9 0.6986307
#> V10 0.7410557
#> V11 .
#> V12 .
#> V13 .
#> V14 .
#> V15 .
#> V16 -0.1021190
#> V17 .
#> V18 .
#> V19 .
#> V20 .We contrast our solution with Cross-Validation-tuned Lasso using
cv.glmnet() in the glmnet package.
Now, we will visualize the quality of lambdas selected by autotune and CV with respect to the 10-fold CV MSE and true Relative MSE.
plot(fit.glmnet, cex.lab = 1.5, ylab = "10-fold CV MSE")
# plotting log of lambdas selected by CV
abline(v = log(c(fit.glmnet$lambda.min, fit.glmnet$lambda.1se)), lty = "dashed",col = c(rgb(0,1,0), rgb(0,0.75,0)), lwd = 3)
# plotting log of lambda selected by autotune
abline(v = log(lambda.autotune), col = "blue", lty = "dashed", lwd = 3)
legend("topleft", inset = 0.05, legend = c("Autotune LASSO",
"CV_LASSO(min)",
"CV_LASSO(1se)"),
col = c("blue", rgb(0,1,0), rgb(0,0.75,0)),
lty = "dashed", lwd = 2, cex = 1)mse_glmnet <- apply(fit.glmnet$glmnet.fit$beta, 2, function(x) mean((x- beta)^2)/mean(beta^2))
ymin = 0.95 * min(mse_glmnet)
ymax = max(1, max(mse_glmnet))
oldpar <- par(mgp = c(3.5, 1, 0), mar = c(5, 4, 2.5, 0.5) + 1.4)
plot(log(fit.glmnet$lambda), mse_glmnet,
type = 'b', col = "red", cex.lab = 2, cex=2, cex.axis = 2,
ylim = c(ymin, ymax),
ylab = "Relative MSE w.r.t. true coefficients", xlab = expression(paste("log(",lambda,")")))
# plotting log of lambdas selected by CV
abline(v = log(c(fit.glmnet$lambda.min, fit.glmnet$lambda.1se)), lty = "dashed",col = c(rgb(0,1,0), rgb(0,0.75,0)), lwd = 3)
# plotting log of lambdas selected by autotune
abline(v = log(lambda.autotune), col = "blue", lty = "dashed", lwd = 3)
legend(
"topleft", inset = 0.05,
legend = c("Autotune LASSO",
"CV_LASSO(min)",
"CV_LASSO(1se)"),
col = c("blue", rgb(0,1,0), rgb(0,0.75,0)),
lty = "dashed", lwd = 2, cex = 1)Across the lambda grid, we plot RMSE of solution path taken by different tuners.
temp <- fit.autotune$CD.path.details$lambda0
seq.lambdas.autotune <- temp * c(var(y), sigma.seq.autotune)
final.rmse.autotune <- (mean(b.autotune - beta)^2) / (mean(beta)^2)
intermediate.rmse.autotune <- approx(x = fit.glmnet$lambda, y = mse_glmnet, xout = seq.lambdas.autotune)
intermediate.rmse.autotune$y[1] <- 1
rmse.path.autotune <- c(intermediate.rmse.autotune$y, final.rmse.autotune)
oldpar <- par(mgp = c(3.5, 1, 0), mar = c(5, 4, 2.5, 0.5) + 1.4)
plot(log(fit.glmnet$lambda), mse_glmnet,
type = 'b', col = "red", cex.lab = 2, cex=2, cex.axis = 2,
ylim = c(min(ymin, 0.95 * final.rmse.autotune), ymax),
ylab = "Relative MSE w.r.t. the true coefficients", xlab = expression(paste("log(",lambda,")")))
lines(c(log(seq.lambdas.autotune), log(fit.autotune$lambda)),
rmse.path.autotune,
col = "blue",
lwd = 2)
points(log(seq.lambdas.autotune),
intermediate.rmse.autotune$y,
col = "blue",
pch = 1,
lwd = 2,
cex = 2)
points(log(fit.autotune$lambda),
final.rmse.autotune,
col = "blue",
pch = 11,
lwd = 3,
cex = 3
)scalregThe scalreg package was removed from CRAN in July
2026.
data("sp500")
attach(sp500)
scaled_lasso_benchmark <- read.csv(
system.file(
"extdata",
"sp500-scalreg-1.0.1.csv",
package = "autotune"
)
)
scaled_lasso_observed <- scaled_lasso_benchmark[
which(!scaled_lasso_benchmark$bootstrap),
,
drop = FALSE
]
n <- 190
rang <- 252 - n
X_train = sp500.percent[1:n, 3:(dim(sp500.percent)[2])]
Y_train = sp500.percent[1:n, 1]
x_train = scale(X_train)
y_train = Y_train - mean(Y_train)
X_test = sp500.percent[(1:rang) + n, 3: (dim(sp500.percent)[2])]
Y_test = sp500.percent[(1:rang) + n, 1]
x_test <- scale(X_test)
y_test <- Y_test - mean(Y_test)
# Default value of beta_iter_max is 40
ans_autotune <- autotune_lasso(x_train, y_train, beta_iter_max = 40, trace_it = TRUE)
#> Iteration: 1Iteration: 2Lambda converged, Iteration: 3Lambda converged, Iteration: 4Lambda converged, Iteration: 5Lambda converged, Iteration: 6Lambda converged, Iteration: 7Lambda converged, Iteration: 8Lambda converged, Iteration: 9Lambda converged, Iteration: 10Lambda converged, Iteration: 11Lambda converged, Iteration: 12Lambda converged, Iteration: 13Lambda converged, Iteration: 14Lambda converged, Iteration: 15Lambda converged, Iteration: 16Lambda converged, Iteration: 17Lambda converged, Iteration: 18Lambda converged, Iteration: 19Lambda converged, Iteration: 20Lambda converged, Iteration: 21Lambda converged, Iteration: 22Lambda converged, Iteration: 23Lambda converged, Iteration: 24Lambda converged, Iteration: 25Lambda converged, Iteration: 26Lambda converged, Iteration: 27Lambda converged, Iteration: 28Lambda converged, Iteration: 29Lambda converged, Iteration: 30Lambda converged, Iteration: 31Lambda converged, Iteration: 32Lambda converged, Iteration: 33Lambda converged, Iteration: 34Lambda converged, Iteration: 35Lambda converged, Iteration: 36Lambda converged, Iteration: 37Lambda converged, Iteration: 38Lambda converged, Iteration: 39Lambda converged, Iteration: 40Lambda converged, Iteration: 41Lambda converged, Iteration: 42
#>
#> No of predictors significant for sigma estimation: 2
pred_err_autotune <- mean( (y_test - x_test %*% ans_autotune$beta)^2 )
pred_err_scallas <- scaled_lasso_observed$test_mse
cv_fit <- cv.glmnet(x_train, y_train, alpha = 1, intercept = F)
pred_err_cv_min <- mean( (y_test - predict(cv_fit, newx = x_test, s = "lambda.min"))^2 )
pred_err_cv_1se <- mean( (y_test - predict(cv_fit, newx = x_test, s = "lambda.1se"))^2 )
pred_err_autotune
#> [1] 0.5001709
pred_err_scallas
#> [1] 0.712887
pred_err_cv_min
#> [1] 0.7034008
pred_err_cv_1se
#> [1] 0.6785715
scaled_lasso_observed$nonzero_coefs
#> [1] 64
sum(ans_autotune$beta != 0)
#> [1] 47
sum(coef(cv_fit, s = "lambda.min") != 0)
#> [1] 60
sum(coef(cv_fit, s = "lambda.1se") != 0)
#> [1] 55
detach(sp500)real_life_prediction_comparison <- function(x, y, x_test, y_test, scaled_lasso_result, alpha = 0.01, beta_iter_max = 40, plot_regu = TRUE, trace_it = TRUE){
ans_autotune <- autotune_lasso(x, y, alpha = alpha, beta_iter_max = beta_iter_max, trace_it = trace_it)
# ans_glmnet <- glmnet(x, y, alpha = 1, intercept = FALSE)
cv_fit <- cv.glmnet(x, y, alpha = 1, intercept = F)
vary <- var(y_test)
mse_glmnet <- apply(cv_fit$glmnet.fit$beta, 2, function(b) mean((y_test - x_test%*%b)^2)/vary)
# log_glmnet_lambdas <- log(fit.glmnet$lambda)
# log_autotune_lambdas <- log(intermediate_lambdas_used)
if(plot_regu) {
intermediate_lambdas_used <- c( c(var(y)/2, ans_autotune$CD.path.details$sigma_sq_seq) * ans_autotune$CD.path.details$lambda0)
autotune_lambdas <- c(intermediate_lambdas_used, ans_autotune$lambda)
final.rmse.autotune <- mean((y_test - x_test%*%ans_autotune$beta)^2) / vary
intermediate.rmse.autotune <- approx(x = log(cv_fit$lambda), y = mse_glmnet, xout = log(intermediate_lambdas_used), rule = 2)
intermediate.rmse.autotune$y[1] <- 1
rmse.path.autotune <- c(intermediate.rmse.autotune$y, final.rmse.autotune)
ysquaredmean <- mean(y^2)
cv_fit$cvm <- cv_fit$cvm/ysquaredmean
cv_fit$cvsd <- cv_fit$cvsd/ysquaredmean
cv_fit$cvup <- cv_fit$cvm + cv_fit$cvsd
cv_fit$cvlo <- cv_fit$cvm - cv_fit$cvsd
cv_fit_lowerlim <- min(cv_fit$cvm - cv_fit$cvsd, mse_glmnet)
cv_fit_upperlim <- max(cv_fit$cvm + cv_fit$cvsd, mse_glmnet)
ymin = 0.95 * min(cv_fit_lowerlim , min(rmse.path.autotune))
ymax = max(cv_fit_upperlim, rmse.path.autotune)
ymax_trunc = ymin + 0.8 * (ymax - ymin) # Truncate vertical lines here
ydiff = ymax - ymin
oldpar <- par(mgp = c(3.5, 1, 0), mar = c(6, 5, 4, 2) + 0.1)
on.exit(par(oldpar), add = TRUE)
par(mar = c(5, 7, 4, 2))
plot(cv_fit,
ylim = c(ymin, max(cv_fit_upperlim, rmse.path.autotune)),
cex.lab = 2,
cex.axis = 2,
ylab = expression(
"||" * (Y[test] - X[test] * hat(beta)) * "||"[2]^2
~ "/" ~
"||" * Y[test] * "||"[2]^2
))
# expression(paste(sqrt("RMSE")))
points(log(cv_fit$lambda), mse_glmnet,
type = 'b', col = "orange", lwd = 3)
lines(rep(log(ans_autotune$lambda), 2),
c(ymin, ymin + 0.92 * ydiff), col = "blue", lwd = 0.5, lty = "dashed")
lines(log(autotune_lambdas), rmse.path.autotune,
col = "blue", lwd = 1.5)
points(log(autotune_lambdas), rmse.path.autotune,
col = "blue", pch = 1, lwd = 1.5, cex = 1)
points(log(ans_autotune$lambda), final.rmse.autotune,
col = "blue", pch = 11, lwd = 2, cex = 2)
vlines <- c(
log(cv_fit$lambda.min),
log(cv_fit$lambda.1se)
)
vline_labels <- c("CV(min)", "CV(1se)")
vline_colors <- c(rgb(0,1,0), rgb(0,0.75,0))
vline_ltys <- c("dashed", "dashed")
vline_lwds <- c(3, 3)
for (i in seq_along(vlines)) {
lines(rep(vlines[i], 2),
c(ymin, ymin + (0.55 + i * 0.1) * ydiff),
col = vline_colors[i], lty = vline_ltys[i],
lwd = vline_lwds[i])
text(x = vlines[i], y = ymin + (0.57 + i * 0.1) * ydiff,
labels = vline_labels[i], srt = -30, adj = 0,
xpd = TRUE, cex = 1.4, col = vline_colors[i])
}
text(x = log(ans_autotune$lambda),
y = ymin + 0.95 * ydiff,
labels = "Autotune", srt = -30, adj = 0,
xpd = TRUE, cex = 1.4, col = "blue")
if (!is.null(cv_fit$cvsd)) {
arrows(x0 = log(cv_fit$lambda),
y0 = cv_fit$cvm - cv_fit$cvsd,
x1 = log(cv_fit$lambda),
y1 = cv_fit$cvm + cv_fit$cvsd,
angle = 90, code = 3, length = 0.05, col = "red", lwd = 1.5)
}
legend("topright",
inset = c(0.05, 0.02),
legend = c("CV Error", "Test Error"),
col = c("red", "orange"),
pt.cex = c(1.5, 2), pch = c(19, 19),
lwd = c(1, 3),
text.col = c("red", "orange"),
horiz = FALSE,
bty = "n")
}
# mse_autotune <- mean( (y_test - x_test %*% ans_autotune$beta)^2 )
# mse_cv_min <- mean( (y_test - predict(cv_fit, newx = x_test, s = "lambda.min"))^2 )
# mse_cv_1se <- mean( (y_test - predict(cv_fit, newx = x_test, s = "lambda.1se"))^2 )
oos_scallas <- scaled_lasso_result$out_of_sample_r2
oos_autotune <- 1 - mean( (y_test - x_test %*% ans_autotune$beta)^2 ) / vary
oos_cv_min <- 1 - mean( (y_test - predict(cv_fit, newx = x_test, s = "lambda.min"))^2 ) / vary
oos_cv_1se <- 1 - mean( (y_test - predict(cv_fit, newx = x_test, s = "lambda.1se"))^2 ) / vary
return(data.frame(out_of_sample_autotune = oos_autotune,
out_of_sample_scaled_lasso = oos_scallas,
out_of_sample_cv_min = oos_cv_min,
out_of_sample_cv_1se = oos_cv_1se,
nonzero_coefs_autotune = sum(ans_autotune$beta != 0),
nonzero_coefs_scaled_lasso = scaled_lasso_result$nonzero_coefs,
nonzero_coefs_cvmin = sum(coef(cv_fit, s = "lambda.min") != 0),
nonzero_coefs_cv1se = sum(coef(cv_fit, s = "lambda.1se") != 0)))
}ans <- real_life_prediction_comparison(
x_train,
y_train,
x_test,
y_test,
scaled_lasso_result = scaled_lasso_observed,
plot_regu = TRUE
)
#> Iteration: 1Iteration: 2Lambda converged, Iteration: 3Lambda converged, Iteration: 4Lambda converged, Iteration: 5Lambda converged, Iteration: 6Lambda converged, Iteration: 7Lambda converged, Iteration: 8Lambda converged, Iteration: 9Lambda converged, Iteration: 10Lambda converged, Iteration: 11Lambda converged, Iteration: 12Lambda converged, Iteration: 13Lambda converged, Iteration: 14Lambda converged, Iteration: 15Lambda converged, Iteration: 16Lambda converged, Iteration: 17Lambda converged, Iteration: 18Lambda converged, Iteration: 19Lambda converged, Iteration: 20Lambda converged, Iteration: 21Lambda converged, Iteration: 22Lambda converged, Iteration: 23Lambda converged, Iteration: 24Lambda converged, Iteration: 25Lambda converged, Iteration: 26Lambda converged, Iteration: 27Lambda converged, Iteration: 28Lambda converged, Iteration: 29Lambda converged, Iteration: 30Lambda converged, Iteration: 31Lambda converged, Iteration: 32Lambda converged, Iteration: 33Lambda converged, Iteration: 34Lambda converged, Iteration: 35Lambda converged, Iteration: 36Lambda converged, Iteration: 37Lambda converged, Iteration: 38Lambda converged, Iteration: 39Lambda converged, Iteration: 40Lambda converged, Iteration: 41Lambda converged, Iteration: 42
#>
#> No of predictors significant for sigma estimation: 2names(ans)
#> [1] "out_of_sample_autotune" "out_of_sample_scaled_lasso"
#> [3] "out_of_sample_cv_min" "out_of_sample_cv_1se"
#> [5] "nonzero_coefs_autotune" "nonzero_coefs_scaled_lasso"
#> [7] "nonzero_coefs_cvmin" "nonzero_coefs_cv1se"library(dplyr)
#>
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#>
#> filter, lag
#> The following objects are masked from 'package:base':
#>
#> intersect, setdiff, setequal, union#Number of bootstraps
B <- 20
result_list <- list()
result_list[[1]] <- data.frame(
n = n,
bootstrap = FALSE,
real_life_prediction_comparison(
x_train,
y_train,
x_test,
y_test,
scaled_lasso_result = scaled_lasso_observed,
plot_regu = FALSE,
trace_it = FALSE
)[1:4]
)
for( j in (1:B)+1) {
set.seed(j)
boot.sample <- sample(n, size = n, replace = TRUE)
boot.x <- x_train[boot.sample, ]
boot.y <- y_train[boot.sample]
scaled_lasso_result <- scaled_lasso_benchmark[
which(scaled_lasso_benchmark$seed == j),
,
drop = FALSE
]
result_list[[j]] <- c(
n = n,
bootstrap = TRUE,
real_life_prediction_comparison(
boot.x,
boot.y,
x_test,
y_test,
scaled_lasso_result = scaled_lasso_result,
plot_regu = FALSE,
trace_it = FALSE
)[1:4]
)
}
mat <- do.call(rbind, result_list)
df <- as.data.frame(mat, stringsAsFactors = FALSE)
colnames(df) <- c("n", "bootstrap", "Autotune",
"Scaled", "CVmin",
"CV1se")
df <- df %>%
mutate(
n = as.integer(n),
bootstrap = as.logical(bootstrap),
across(!c(n, bootstrap), as.numeric)
)alg_cols <- colnames(df)[3:6]
boot_only <- df %>% filter(bootstrap == TRUE)
boxlist <- lapply(alg_cols, function(cn) boot_only[[cn]])
names(boxlist) <- alg_cols
# Uncomment the following line and line 448 for saving the boxplot in your working directory
# pdf(file.path(paste0("OOS_boxplot_for_n=",df[1,1],"_B=", B,".pdf")), width = 8, height = 9)
oldpar <- par(mar = c(4, 6, 4, 2))
boxplot(boxlist, main = paste0("Bootstrapped Out-of-Sample R^2 for n = ", df[1,1]), ylab = expression(paste("Out-of-Sample ",R^2)), ylim = c(max(0.98 * min(df[alg_cols]), 0), 1),
cex.lab = 2,
cex.axis = 1.8,
cex.main = 1.5)
nonboot <- df %>% filter(bootstrap == FALSE)
if(nrow(nonboot) > 0){
points(1:4, as.numeric(nonboot[1, alg_cols]), pch = 18, col = "red", cex = 2)
}
legend(
"topright",
legend = expression(paste("Observed O.O.S. ",R^2)),
col = "red",
pch = 18,
pt.cex = 2,
cex = 2,
bty = "n"
)