Tunes an elastic-net model over a grid of alpha and lambda values with
caret::train(method = "glmnet") and selects the predictors whose
coefficient is non-zero at the winning pair. Handles regression (numeric
outcome) and classification (factor, character or logical outcome).
Usage
fs_elastic(
data,
target,
alpha_seq = seq(0.1, 1, by = 0.1),
lambda_seq = NULL,
trControl = NULL,
metric = NULL,
use_pca = FALSE,
nPCs = NULL,
seed = NULL,
verbose = FALSE,
n_cores = 1L
)Arguments
- data
A data frame (or data.table) containing the target and the candidate predictors, with unique, non-empty column names.
- target
Single string naming the outcome column in
data.- alpha_seq
Numeric vector of alpha values to tune over, each in
[0, 1]. Defaultseq(0.1, 1, by = 0.1), which leaves out 0 (ridge) because ridge cannot zero a coefficient.- lambda_seq
Numeric vector of non-negative lambda values to tune over, or
NULL(default) to use glmnet's own path at each alpha (see "Tuning grid and metrics").- trControl
Optional
caret::trainControl()object; a partial list or a string is an error. IfNULL(default), 5-fold CV with an NA-safe summary function is used. Whenuse_pca = TRUE,pcaComp = nPCsis added to itspreProcOptions. ItsallowParallelis always overwritten withn_cores > 1, so a registered foreach backend is used only whenn_cores > 1.- metric
Optional character. Performance metric to optimize. If
NULL(default),"RMSE"for regression and"Accuracy"for classification. If caret does not find the name in the summary output, it warns and falls back to its default, anddetails$metric_namereports the metric actually used.- use_pca
Logical; default
FALSE. Whether to project the predictors onto principal components inside each resample.- nPCs
Integer >= 1. Number of principal components to retain when
use_pca = TRUE; must be strictly less thanmin(nrow, ncol)of the predictor matrix. DefaultNULL, which is an error whenuse_pca = TRUEand ignored otherwise.- seed
Optional whole number; default
NULL, which leaves the RNG alone. When given, the seed is set for this call only and the previous RNG state is restored on exit. The resampling folds are drawn in the main R process and the glmnet fits are deterministic, so a seededn_cores > 1run gives the same result as a seeded sequential one. WhentrControl$seedsis unset (NA, thetrainControl()default), caret draws one seed per resample from this seed in the main process and sets it on each worker, so seeded runs that use worker-side randomness (for examplesampling = "up") are reproducible with anyn_cores. A user-suppliedtrControl$seedstakes precedence.- verbose
Logical; default
FALSE. Print progress messages.- n_cores
Integer >= 1; default 1 (sequential, and no cluster is created). Number of workers for parallel training; values above the detected core count are capped. Values above 1 need the 'foreach' and 'doParallel' packages.
Value
An fs_result object with:
- selected
Columns (or components, when
use_pca = TRUE) whose coefficient at the chosen alpha/lambda is non-zero; for multinomial fits, the union across classes.- scores
Named numeric vector of absolute coefficients at the chosen alpha/lambda, one entry per column the model saw; columns shrunk to zero are kept with a score of 0.
NULLfor multinomial fits, where a predictor has one coefficient per class and no single score exists.- method
"elastic_net".- task
"regression"or"classification".- model
The
caret::trainobject.- details
List of
coef(coefficients at the winning lambda, intercept included: a sparse matrix, or a list of them for multinomial fits),best_alphaandbest_lambda(the winning tuning pair),metric_name(the metric caret optimized, which differs frommetricwhen caret fell back to its default),metric_value(its resampled value for the winning pair,NAwhen the metric is absent from caret's results table; optimistic, see Details),use_pca, andn_features(number of columns the model saw, which isnPCswhenuse_pca = TRUE).- call
The matched call.
Details
The elastic net penalizes coefficients with a mix of an L1 (lasso) and an
L2 (ridge) penalty: alpha sets the mix and lambda the overall
strength. Both are chosen by resampling, and every column with a non-zero
coefficient at the winning pair is reported. When the winning alpha is
below 1, the ridge part spreads weight across correlated predictors, so a
group of collinear columns tends to survive together instead of being
reduced to one representative as fs_lasso() tends to do.
alpha = 0 is pure ridge, which never sets a coefficient to exactly zero
and so cannot select. The default alpha_seq starts at 0.1 for that
reason. If you include 0 and it wins, every column is selected and a
warning says so.
The winning pair is the one with the best resampled metric (caret's
default selectionFunction = "best"). With hundreds of alpha/lambda
candidates that choice is noisy and leans towards too little penalty: on
pure-noise data (100 rows, 50 predictors) it selected 8.5 predictors on
average and at least one in every run. A trControl with
selectionFunction = "oneSE" picks the most penalized pair within one
standard error of the best and was much sparser in the same simulation
(1.8 predictors on average).
For the same reason details$metric_value, the best value over the whole
grid, is an optimistic estimate of performance. fs_elastic() holds out
no test rows and reports no honest performance estimate; that needs an
outer resampling loop around the whole call.
How the design matrix is built
Every column of data except target is a candidate predictor. The
formula is built for you (non-syntactic names are backticked), and the
predictors go through stats::model.matrix() with the intercept column
removed, so a k-level factor or character column contributes k - 1 dummy
columns. selected, scores and details$coef therefore name
design-matrix columns rather than the original columns. Non-syntactic
names are reported as you wrote them, without the backticks
model.matrix() adds. Above 500,000 cells the matrix is converted to a
sparse matrix unless use_pca = TRUE.
These inputs are errors:
fewer than two design-matrix columns (
glmnetcannot fit a single-columnx), so a single numeric predictor fails;a factor or character predictor with fewer than two distinct values;
a dummy name that collides with another column (factor
gwith levelbnext to a column calledgb);Inf or -Inf in the target or any predictor;
a predictor column that is constant, or constant up to floating-point round-off (range at most
64 * .Machine$double.epstimes its largest absolute value), checked the same way for dense and sparse input.
Missing values and the outcome type
Rows with a missing response or a missing value in any predictor are
dropped before fitting; if no rows remain, that is an error. The task is
then inferred from the remaining rows. A class that occurred only in
dropped rows is removed with a message, and a class with fewer than two
complete rows is an error. When rows are dropped, a user-supplied
trControl carrying index, indexOut or indexFinal is an error,
because those row numbers no longer match.
A logical response is converted to a two-level factor with a message. A numeric response with only two distinct values is still treated as regression, with a warning telling you to convert it to a factor if you meant classification.
Scores
scores are absolute coefficients on the scale of the columns the model
saw, not standardized ones. They rank predictors fairly only when those
columns are on comparable scales; unlike fs_lasso(), this function does
not rescale them for you. Coefficients are taken at exactly the winning
lambda (the path is refitted if that lambda is not one of the final
model's knots), not interpolated between knots.
Principal components inside resampling
With use_pca = TRUE, caret is asked for
preProcess = c("center", "scale", "pca") with pcaComp = nPCs in
trControl$preProcOptions. Centering, scaling and the component loadings
are therefore refitted on the training part of every resample, and the
held-out fold never contributes to them. The model is fitted on
components, so selected, scores and details$coef are named PC1,
PC2, and so on, rather than after the original columns. nPCs must be
smaller than the number of predictors and than the number of rows each
resample trains on.
Tuning grid and metrics
lambda_seq = NULL (the default) tunes over the lambda path that
glmnet::glmnet() itself proposes at each alpha (up to 50 values per
alpha), which is scaled to the data, instead of a fixed sequence that
spends most of its fits on irrelevant lambdas. Only the candidate values
come from the full data, as in caret's own default glmnet grid; which pair
wins is still decided by resampling. With use_pca = TRUE the path is
computed on the original predictors, so it only approximates the scale of
the components; pass lambda_seq if you need control over it.
The resampled metrics in model$results and details$metric_value are
caret's. For regression they are RMSE, Rsquared and MAE. Rsquared
is the squared Pearson correlation between held-out predictions and
observations (caret::postResample()), not 1 - SSE/SST, so it is not
comparable with an R2 defined as 1 - SSE/SST (for example
fs_randomforest()'s). It lies in [0, 1], except that with the default
trControl = NULL featR's summary function records a fold where it is
undefined (constant predictions, typical at the large-lambda end of the
path) as -Inf, so such grid rows show -Inf in model$results. For
classification the metrics are Accuracy and Cohen's Kappa, which need
the suggested package 'e1071'. featR checks for it before fitting when
the default summary function is used; a custom summaryFunction in
trControl is responsible for its own dependencies.
See also
The Regularization and embedded importance guide, which runs this function on a worked example.
Examples
if (requireNamespace("caret", quietly = TRUE) &&
requireNamespace("glmnet", quietly = TRUE) &&
requireNamespace("Matrix", quietly = TRUE)) {
# x1 and x2 drive y; x3 is noise
df <- data.frame(
x1 = seq(-2, 2, length.out = 60),
x2 = rep(c(-1, 0, 1), 20),
x3 = cos(seq_len(60))
)
df$y <- 2 * df$x1 - df$x2 + 0.1 * cos(seq_len(60) * 3)
res <- fs_elastic(df, "y", alpha_seq = c(0.5, 1), seed = 1)
print(selected(res))
print(res$scores)
print(res$details$best_alpha)
}
#> Loading required package: ggplot2
#> Loading required package: lattice
#> [1] "x1" "x2"
#> x1 x2 x3
#> 1.9816363 0.9732464 0.0000000
#> [1] 1