Regularization and embedded importance
Source:vignettes/articles/regularization.Rmd
regularization.RmdThe four functions on this page each fit one model and read the
selection off that model’s structure. fs_lasso() and
fs_elastic() keep the predictors whose penalized
coefficients are not zero, fs_randomforest() ranks
predictors by permutation importance, and fs_mars() keeps
the predictors that appear in its pruned spline terms. Because every
predictor is in the model at once, each one is judged in the presence of
the others, which a univariate filter cannot do.
fs_lasso() and fs_elastic() fit linear
models, so they miss a predictor whose effect is curved, and neither
holds out any rows, so neither gives you an honest performance estimate.
fs_randomforest() and fs_mars() pick up
non-linear effects, and both report metrics on a held-out split that the
selection never saw.
For which quantities are honest and which are optimistic across all of featR, see Validation and leakage. For help choosing between these and the filter or wrapper methods, see Choosing a method.
Example data
Every example below uses the same simulated regression problem, so
you can check each method against the known answer. y
depends on x1, x2 and x3 linearly
and on x_nl through a U-shaped curve with almost no linear
trend. x1_copy is x1 plus a little noise, and
x4 to x10 are pure noise.
fs_lasso()
The lasso is linear regression with an L1 penalty on the
coefficients. The penalty shrinks coefficients toward zero and sets some
of them exactly to zero, and the predictors left with a non-zero
coefficient are the selection. fs_lasso() fits it
with glmnet::cv.glmnet(), choosing the penalty strength by
cross-validation (nfolds = 5 by default). It handles
numeric outcomes only (gaussian family); for classification, use
fs_elastic().
las <- fs_lasso(d, "y", nfolds = 5, seed = 1)
las
#> <fs_result> lasso (regression)
#> Selected 8 of 12 features
#> x2, x1, x1_copy, x3, x_nl, x4, x5, x9
#> Details: lambda_min, lambda_1se, lambda_used, coefficients, n_features (in $details)
head(las$scores, 6)
#> Variable Coefficient AbsCoefficient
#> 1 x2 -1.4237946 1.4237946
#> 2 x1 1.0857606 1.0857606
#> 3 x1_copy 0.7641129 0.7641129
#> 4 x3 0.6011092 0.6011092
#> 5 x_nl 0.2535626 0.2535626
#> 6 x4 0.0425803 0.0425803The lasso kept 8 of the 12 predictors: the four real ones,
x1_copy, and three noise columns (x4,
x5, x9). The default,
lambda = "min", selects at the penalty that minimizes
cross-validated error. That penalty is tuned for prediction, and it
usually lets some noise through. lambda = "1se" uses the
largest penalty whose error is within one standard error of the minimum,
which gives a sparser and more stable set. On this data it drops the
three noise columns:
selected(fs_lasso(d, "y", nfolds = 5, seed = 1, lambda = "1se"))
#> [1] "x2" "x1" "x1_copy" "x3" "x_nl"Both penalty values are stored in details$lambda_min and
details$lambda_1se, whichever one you select at.
scores holds each coefficient multiplied by its column’s
standard deviation, so predictors measured in different units can be
ranked against each other. The raw coefficients are in
details$coefficients. With standardize = FALSE
the two tables are identical, and you should compare them only across
predictors that share a scale.
Read the selection with three limits in mind:
- Among strongly correlated predictors, the lasso tends to keep one
and zero out the rest, so a missing predictor is not evidence that it is
unrelated to the outcome. Here
x1andx1_copyboth survive, but they split one effect between them (scores 1.09 and 0.76, which together come close to the true coefficient of 2), so neither coefficient measures it alone. - The model is linear.
x_nlhas one of the strongest effects in the data, but its score is 0.25 against 1.42 forx2, coming only from whatever linear trend this sample happens to contain. Compare the random forest and MARS results below. -
fs_lasso()reports no performance estimate. If you keep the fitted model (return_model = TRUE), its cross-validated error is the one the penalty was chosen to minimize, so it is optimistic.
Factors and character columns are expanded into dummy columns, and
the selection names those columns (colorred, not
color). A factor with fewer than two observed levels is an
error, and so is a dummy name that collides with another column (factor
g with level b next to a column called
gb). Infinite values are an error too.
Missing predictor values are an error under the default
impute = "none". impute = "mean" fills them
with column means computed on the whole data set, before
cross-validation, so the folds see each other; featR warns when you use
it. If you need imputation, do it inside your own resampling loop.
alpha defaults to 1. A value between 0 and 1 gives an
elastic-net penalty at that fixed mixing value;
fs_elastic() tunes it for you instead.
fs_elastic()
The elastic net mixes the lasso’s L1 penalty with the L2 (ridge)
penalty. The mixing parameter alpha runs from 0 (pure
ridge) to 1 (pure lasso). fs_elastic() tunes
alpha and the penalty strength lambda together
by resampling, through caret::train(method = "glmnet"),
with 5-fold cross-validation unless you pass your own
trControl. Unlike fs_lasso(), it also handles
classification.
el <- fs_elastic(d, "y", alpha_seq = c(0.25, 0.5, 1), seed = 1)
el$details$best_alpha
#> [1] 0.5
el$details$metric_value
#> [1] 2.110188
selected(el)
#> [1] "x1" "x2" "x3" "x4" "x5" "x7" "x8"
#> [8] "x9" "x1_copy" "x_nl"The winning alpha was 0.5, and 10 of the 12 predictors
survived, including five of the seven noise columns. When
alpha is below 1, the ridge part of the penalty spreads
weight across correlated predictors, so a group like x1 and
x1_copy tends to stay in together rather than being cut to
one representative. The cost, as here, is a less sparse selection.
The winner is the pair with the best resampled metric, and picking
the best of many candidates leans toward too little penalty. On
pure-noise data (100 rows, 50 predictors) the default selected 8.5
predictors on average and at least one in every run. Passing a
caret::trainControl() with
selectionFunction = "oneSE" picks the most penalized pair
within one standard error of the best, which selected 1.8 on average in
the same simulation.
The same optimism applies to details$metric_value, the
resampled RMSE of the winning pair (2.11 here). It is the best value
over the whole grid, and fs_elastic() holds out no test
rows, so treat it as optimistic and wrap the whole call in an outer
resampling loop if you need an honest estimate. In
model$results, caret’s Rsquared is the squared
correlation between predictions and observations, not
1 - SSE/SST.
The default alpha_seq is
seq(0.1, 1, by = 0.1). It leaves out alpha = 0
because ridge never sets a coefficient exactly to zero; if you add 0
yourself and it wins, every predictor is “selected” and featR warns.
scores are absolute coefficients on the scale of the
columns as supplied (NULL for an outcome with more than two
classes, where each predictor has one coefficient per class).
fs_elastic() does not rescale them the way
fs_lasso() does, so standardize the predictors first if you
want to rank by scores.
Rows with a missing outcome or any missing predictor are dropped
before fitting. Factors are expanded into k - 1 dummy columns, and the
selection names those columns. With use_pca = TRUE (and
nPCs set), the model is fit on principal components that
are recomputed inside every resample, and the selection then names
components (PC1, PC2, …) rather than your
columns.
fs_randomforest()
A random forest averages many decision trees, each grown on a
bootstrap sample with a random subset of predictors tried at each split.
fs_randomforest()
splits the data (train_ratio = 0.75 by default, set through
control), grows the forest on the training rows, and
reports permutation importance plus metrics on the held-out rows.
Permutation importance measures how much the forest’s error grows when
one predictor’s values are shuffled.
task defaults to "classification", because
a numeric column can be a class label, so pass
task = "regression" for a numeric outcome. featR warns when
a numeric target meets the default.
rf <- fs_randomforest(d, "y", task = "regression",
control = list(ntree = 300), seed = 1)
head(rf$scores, 6)
#> x2 x_nl x1 x1_copy x3 x7
#> 26.187296 26.142210 18.069593 17.606422 4.794183 1.894942
rf$details$metrics
#> $RMSE
#> [1] 1.645803
#>
#> $MAE
#> [1] 1.326286
#>
#> $R2
#> [1] 0.770357The forest ranks x_nl second, almost tied with
x2, where the linear methods gave it a small coefficient.
x1 and x1_copy split their shared credit (18.1
and 17.6), and the noise columns trail far behind, led by
x7 at 1.9. These scores are the mean increase in MSE
divided by its standard error over trees; set
control$scale_importance = FALSE for the unscaled version.
Importance can also favor predictors with many distinct values, so treat
close scores as ties.
A forest ranks predictors without selecting any. Unless you supply a
selection rule, selected is every predictor, ordered by
importance. To turn the ranking into a selection, pass a function as
control$feature_select. It receives the training rows as a
data.table and returns the columns to keep, including the outcome. The
hook below keeps a fixed list for illustration; a real rule would
compute something from dt.
top5 <- function(dt) {
keep <- c("x1", "x2", "x3", "x_nl", "x1_copy") # any rule you like
dt[, c(keep, "y"), with = FALSE]
}
rf_sel <- fs_randomforest(d, "y", task = "regression",
control = list(ntree = 300, feature_select = top5),
seed = 1)
selected(rf_sel)
#> [1] "x_nl" "x2" "x1" "x1_copy" "x3"The hook runs after the split and sees only the training rows, so the
held-out details$metrics stay honest even when the rule
looks at the outcome. The out-of-bag error in details$oob
does not: the forest is grown on the same rows the hook just selected
on, so after a hook that consults the outcome the OOB error is
optimistic. control$preprocess, by contrast, runs on the
full data before the split, so keep it to row-wise transformations.
Missing predictor values are imputed by default with the median
(numeric) or most common value (other types), learned on the training
rows only. Rows with a missing outcome are dropped with a warning. With
n_cores above 1 the forest is grown in pieces and combined,
which discards the OOB results, so details$oob is
NULL.
fs_mars()
MARS (multivariate adaptive regression splines) builds a model from
piecewise-linear hinge functions, adding terms greedily and then pruning
them back. It finds curved effects and, with degree above
1, interactions on its own. fs_mars() fits it with
the earth package through caret, tunes degree and
nprune (the number of terms kept) by repeated
cross-validation, evaluates on a held-out split
(train_ratio = 0.8), and selects the predictors that appear
in the pruned model. The task follows the outcome: numeric means
regression, factor or character means classification.
The defaults tune degree = 1:3 with 5-fold
cross-validation repeated 3 times. The example uses a smaller grid to
keep it fast.
ma <- fs_mars(d, "y", degree = 1:2, nprune = c(5, 10, 15),
number = 3, repeats = 1, seed = 1)
selected(ma)
#> [1] "x1_copy" "x_nl" "x2" "x3"
ma$details$metrics
#> $RMSE
#> [1] 0.9944926
#>
#> $MAE
#> [1] 0.8114338
#>
#> $R2
#> [1] 0.9184193
ma$details$removed_predictors
#> $nzv
#> character(0)
#>
#> $corr
#> [1] "x1"MARS found x_nl along with x2,
x3 and x1_copy, and kept none of the noise
columns. x1 is missing because of a filter that runs before
the model is fit. fs_mars() drops near-zero-variance
predictors and one member of each pair correlated above
corr_cut (default 0.95), and
details$removed_predictors records what went. Both filters
are fit on the training rows only, and neither looks at the outcome, so
which of x1 and x1_copy survives says nothing
about which matters more. Set corr_cut = 0 to turn the
correlation filter off.
The reported R2 is caret’s squared correlation between
predictions and observations, not 1 - SSE/SST, and a biased
model can still score well on it, so read it together with RMSE.
fs_randomforest() reports 1 - SSE/SST, so the
two R2 values on this page are not comparable. RMSE is on
the same scale for both (0.99 for MARS against 1.65 for the forest), but
each comes from one split of a different size, so on small data treat
the gap as rough.
Rows with any missing value are dropped before anything else, so
fs_mars() needs fairly complete data. Data with more than
sample_size rows (default 10000) are then randomly
down-sampled to that size before the split.
For classification fs_mars() needs the e1071 package,
and it up-samples the smaller classes inside each resample rather than
before cross-validation. It treats the first factor level as the
positive class for PR AUC, sensitivity and specificity, with no
override, so use stats::relevel() to change it.
fs_randomforest() uses the second level unless you set
control$positive_class, so those class-specific numbers do
not compare across the two functions (ROC AUC does). Test-set ROC and PR
AUC are reported for binary outcomes when pROC and PRROC are
installed.