Régression logistique pénalisée en R : lasso, ridge et elastic net avec glmnet

Réduire les coefficients, sélectionner les prédicteurs et construire un classifieur parcimonieux — réglé honnêtement par validation croisée

Un guide pratique de la régression logistique pénalisée (régularisée) en R pour la classification binaire : lasso (L1), ridge (L2) et elastic net. Découvrez comment la pénalité réduit les coefficients, pourquoi le lasso en met certains exactement à zéro (sélection de variables) et comment régler lambda par validation croisée. Traité de deux façons — le workflow tidymodels moderne avec parsnip + glmnet, et la route classique glmnet::cv.glmnet family = “binomial” — et comparé à la régression logistique simple via l’exactitude et l’AUC ROC. Sur les données PimaIndiansDiabetes2.

Date de publication

25 juin 2026

Modifié

7 juillet 2026

AstucePoints clés
  • La régression logistique pénalisée (alias régression logistique régularisée) ajoute une pénalité sur la taille des coefficients à la régression logistique ordinaire. Elle réduit les prédicteurs faibles vers zéro — le remède au trop-plein de variables, aux prédicteurs corrélés et au surajustement dans un classifieur binaire.
  • Le lasso (L1) réduit et met certains coefficients exactement à zéro — il réalise donc une sélection de variables et vous remet un modèle parcimonieux et interprétable. Le ridge (L2) réduit chaque coefficient mais les conserve tous. L’elastic net mélange les deux (le mixture entre 0 et 1).
  • L’ampleur de la réduction tient à un seul paramètre de réglage, lambda (penalty). λ = 0 donne la régression logistique simple ; à mesure que λ croît, les coefficients se réduisent vers zéro. Vous choisissez λ par validation croisée.
  • Standardisez d’abord les prédicteurs — les pénalités agissent sur la taille des coefficients, donc les prédicteurs doivent partager une même échelle (step_normalize(), ou le standardize = TRUE intégré de glmnet).
  • Deux routes, une même idée : le workflow tidymodels moderne (logistic_reg(penalty = tune(), mixture =) |> set_engine("glmnet")) réglé par tune_grid(), ou le classique glmnet::cv.glmnet(..., family = "binomial") — qui vous remet lambda.min (erreur CV la plus faible) et lambda.1se (le modèle le plus simple à une erreur standard du minimum).
  • Sur les données de diabète Pima, le lasso égale ou dépasse la régression logistique simple avec moins de prédicteurs — exactement le rôle de la régularisation : même exactitude, modèle plus simple.
Obtenez le livre — Machine Learning Essentials (PDF)

Introduction

Vous avez un jeu de données clinique et une issue oui/non à prédire — un patient est-il testé positif au diabète ? — à partir d’une poignée de mesures (glucose, IMC, âge, grossesses, etc.). La régression logistique ordinaire est l’outil naturel, mais elle a un point faible : lorsque les prédicteurs sont corrélés ou nombreux, les estimations des coefficients deviennent instables et le modèle surajuste — il s’ajuste au bruit d’entraînement et classe mal les nouveaux patients.

La régression logistique pénalisée corrige cela en ajoutant une pénalité sur la taille des coefficients à l’ajustement logistique. Les grands coefficients coûtent désormais au modèle, qui les réduit donc vers zéro, ne gardant que le signal qui mérite sa place. C’est la régularisation, et elle fait deux choses à la fois : elle stabilise les estimations et, dans le cas du lasso, sélectionne les variables automatiquement en poussant les coefficients les plus faibles à exactement zéro.

Cette leçon est l’extension prédictive de la régression logistique. Si votre but est d’interpréter un modèle logistique — lire les odds ratio, les p-value, vérifier les hypothèses — commencez par la régression logistique dans le pilier Biostatistique. Ici, le but est la prédiction : construire un classifieur qui généralise, et sélectionner les prédicteurs qui comptent. Nous procédons de deux façons — le workflow tidymodels moderne (parsnip + glmnet) et la route classique glmnet::cv.glmnet() — et comparons à la régression logistique simple. Pour le jumeau de cette leçon côté régression (issue continue), voir la régression pénalisée.

NoteQuand y recourir

Utilisez la régression logistique pénalisée lorsque votre modèle de classification a beaucoup de prédicteurs, des prédicteurs corrélés, ou qu’il surajuste (excellent à l’entraînement, médiocre sur de nouvelles données). Recourez au lasso lorsque vous soupçonnez que seuls quelques prédicteurs comptent vraiment (vous voulez un classifieur parcimonieux et interprétable) ; au ridge lorsque de nombreux prédicteurs contribuent chacun un peu ; et à l’elastic net lorsque les prédicteurs sont corrélés et que vous voulez quand même une sélection.

Les données

Nous utilisons le jeu de données PimaIndiansDiabetes2 du package mlbench : 768 femmes d’origine pima amérindienne, avec pour but de prédire diabetes (une issue binaire pos/neg) à partir de huit prédicteurs cliniques — nombre de grossesses (pregnant), glucose plasmatique, pressure artérielle, épaisseur du pli cutané au triceps, insulin sérique, indice de masse corporelle (mass), la fonction pedigree de diabète et l’age.

Cette version comporte des valeurs manquantes (notées NA là où une mesure est invraisemblable, p. ex. un glucose nul). Les modèles pénalisés ont besoin de cas complets, donc — en suivant l’analyse source — nous supprimons les lignes avec NA, ce qui laisse 392 enregistrements complets. (Pour un modèle qui les conserve, vous imputeriez plutôt avec une étape de recipe comme step_impute_median() ; voir l’ ingénierie des caractéristiques avec les recipes.)

Nous séparons ensuite en un jeu d’entraînement (80 %, pour l’ajustement et le réglage) et un jeu de test (20 %, mis de côté pour juger honnêtement le classifieur final). Nous stratifions sur diabetes afin que les deux jeux gardent le même équilibre positif/négatif, et fixons une graine pour la reproductibilité :

library(rsample)
data("PimaIndiansDiabetes2", package = "mlbench")

# Drop rows with missing measurements (penalized models need complete cases)
pima <- na.omit(PimaIndiansDiabetes2)

set.seed(123)
split      <- initial_split(pima, prop = 0.80, strata = diabetes)
train_data <- training(split)
test_data  <- testing(split)

c(complete = nrow(pima), train = nrow(train_data), test = nrow(test_data))
complete    train     test 
     392      313       79 
# Class balance is preserved by the stratified split
prop.table(table(train_data$diabetes))

      neg       pos 
0.6677316 0.3322684 
head(pima, 4)
  pregnant glucose pressure triceps insulin mass pedigree age diabetes
4        1      89       66      23      94 28.1    0.167  21      neg
5        0     137       40      35     168 43.1    2.288  33      pos
7        3      78       50      32      88 31.0    0.248  26      pos
9        2     197       70      45     543 30.5    0.158  53      pos

Environ 313 patients pour apprendre, 79 mis de côté pour faire office de juge, et 8 prédicteurs à réduire. Environ un tiers des patients sont positifs au diabète — un problème de classification modérément déséquilibré mais exploitable.

Le workflow tidymodels : lasso pour la classification

Dans tidymodels, la seule chose qui change par rapport à un modèle pénalisé de régression est la spécification du modèle : parsnip::logistic_reg() au lieu de linear_reg() (la régression logistique n’est que du glmnet avec family = "binomial" sous le capot). Elle prend les deux mêmes réglages :

  • mixture = 1lasso pur (pénalité L1) — réduit et sélectionne
  • mixture = 0ridge pur (pénalité L2) — réduit, conserve tous les prédicteurs
  • 0 < mixture < 1elastic net (p. ex. 0.5 est un mélange moitié-moitié)

Le moteur est "glmnet". Les méthodes pénalisées exigent des prédicteurs standardisés (la pénalité agit sur la taille des coefficients, donc les prédicteurs doivent partager une même échelle), aussi la recipe les normalise d’abord.

Régler la pénalité par validation croisée

Nous ne devinons pas lambda — nous laissons la validation croisée le choisir. Marquez penalty = tune(), construisez une grille de valeurs candidates, et notez chacune par une CV à 10 plis sur le jeu d’entraînement, stratifiée sur l’issue. mixture = 1 en fait un lasso. Nous notons via l’AUC ROC (une mesure sans seuil de la qualité avec laquelle le modèle classe les positifs au-dessus des négatifs) et l’exactitude :

library(rsample)
library(recipes)
library(parsnip)
library(workflows)
library(tune)
library(dials)
library(yardstick)
data("PimaIndiansDiabetes2", package = "mlbench")
pima <- na.omit(PimaIndiansDiabetes2)

set.seed(123)
split      <- initial_split(pima, prop = 0.80, strata = diabetes)
train_data <- training(split)

# Standardize predictors — penalized methods require a common scale
rec <- recipe(diabetes ~ ., data = train_data) |>
  step_normalize(all_numeric_predictors())

# Lasso: mixture = 1, penalty left to be tuned
lasso_spec <- logistic_reg(penalty = tune(), mixture = 1) |>
  set_engine("glmnet")

lasso_wf <- workflow() |>
  add_recipe(rec) |>
  add_model(lasso_spec)

# 10-fold CV on the training set, over a grid of lambda values
set.seed(123)
folds <- vfold_cv(train_data, v = 10, strata = diabetes)
grid  <- grid_regular(penalty(range = c(-4, 0)), levels = 50)   # 10^-4 to 10^0

set.seed(123)
lasso_tuned <- tune_grid(
  lasso_wf, resamples = folds, grid = grid,
  metrics = metric_set(roc_auc, accuracy)
)

# The lambda with the highest cross-validated ROC AUC
best_lambda <- select_best(lasso_tuned, metric = "roc_auc")
best_lambda
# A tibble: 1 × 2
  penalty .config         
    <dbl> <chr>           
1  0.0133 pre0_mod27_post0

tune_grid() a validé par croisement chaque lambda candidat ; select_best() renvoie celui à la meilleure performance CV. Ici, la pénalité retenue est modeste — juste assez pour retirer le prédicteur le plus faible ou deux tout en conservant le pouvoir discriminant du modèle.

La courbe de CV : AUC en fonction de lambda

Le graphique le plus utile en modélisation pénalisée est la performance validée par croisement en fonction de lambda. Il montre tout le compromis biais–variance en une seule image :

library(ggplot2)

# Collect the CV metric for every candidate lambda
lasso_metrics <- collect_metrics(lasso_tuned)
auc_curve     <- subset(lasso_metrics, .metric == "roc_auc")

ggplot(auc_curve, aes(x = penalty, y = mean)) +
  geom_line(color = "#3a86d4", linewidth = 1) +
  geom_point(color = "#3a86d4", size = 1.5) +
  geom_vline(xintercept = best_lambda$penalty, linetype = "dashed", color = "gray40") +
  scale_x_log10() +
  labs(
    x = expression("Penalty " * lambda * " (log scale)"),
    y = "Cross-validated ROC AUC",
    title = "Choosing the penalty by cross-validation",
    subtitle = "AUC is high for light penalties, then falls as lambda over-shrinks (dashed = best lambda)"
  ) +
  theme_minimal()

Cross-validated ROC AUC plotted against the lasso penalty lambda on a log scale for the Pima diabetes data: AUC is high and flat for small penalties, then falls as the penalty grows large and shrinks the coefficients too far.

Lisez-le de gauche à droite. Pour un lambda faible (à gauche), la pénalité mord à peine — le modèle est proche de la régression logistique simple et l’AUC est élevée. À mesure que lambda croît (à droite), les coefficients sont forcés vers zéro ; passé un certain point, le modèle est trop contraint (sous-ajusté) et l’AUC chute. La ligne pointillée marque le gagnant de la validation croisée — le point d’équilibre entre surajustement (trop peu de pénalité) et sous-ajustement (trop).

Le chemin des coefficients : observez le lasso sélectionner les prédicteurs

L’autre graphique emblématique est le chemin des coefficients — chaque coefficient tracé à mesure que lambda augmente. C’est là que le comportement caractéristique du lasso est visible : les coefficients ne font pas que se réduire, ils atteignent exactement zéro et y restent. Un prédicteur dont le chemin est tombé à zéro a été écarté du modèle.

library(glmnet)
library(rsample)
data("PimaIndiansDiabetes2", package = "mlbench")
pima <- na.omit(PimaIndiansDiabetes2)

set.seed(123)
split      <- initial_split(pima, prop = 0.80, strata = diabetes)
train_data <- training(split)

# glmnet wants a numeric predictor matrix + a numeric/factor outcome
x <- model.matrix(diabetes ~ ., train_data)[, -1]
y <- ifelse(train_data$diabetes == "pos", 1, 0)

# Fit lasso (alpha = 1) for a binary outcome over a sequence of lambdas
lasso_full <- glmnet(x, y, alpha = 1, family = "binomial", standardize = TRUE)

plot(lasso_full, xvar = "lambda", label = TRUE)
title("Lasso coefficient paths (Pima diabetes)", line = 2.5)

Lasso coefficient-path plot for the logistic model on the Pima diabetes data: each coloured line is one predictor's standardized coefficient against minus-log-lambda, so the penalty is heaviest at the left where all coefficients are zero and lightest at the right; predictors switch on one by one as the penalty relaxes, illustrating automatic variable selection.

Chaque courbe est un prédicteur. L’axe des x est \(-\log(\lambda)\), donc la pénalité est la plus forte à gauche et la plus faible à droite. En lisant de gauche à droite (pénalité forte → faible) : à forte pénalité (tout à gauche), chaque coefficient est écrasé à zéro ; à mesure que la pénalité se relâche, les prédicteurs « s’activent » un à un à peu près dans leur ordre d’importance — pedigree, puis glucose et mass (IMC), comme on s’y attend cliniquement. Le nombre le long de l’axe supérieur indique combien de prédicteurs restent non nuls à ce lambda. Voilà la sélection de variables qui se produit en continu — la fonctionnalité phare que le lasso possède et le ridge non.

NoteLe ridge a une autre allure — il n’atteint jamais zéro

Relancez le chemin avec alpha = 0 (ridge) et les lignes glissent ensemble vers zéro mais ne le touchent jamais — le ridge conserve chaque prédicteur, simplement plus petit. Cette unique différence visuelle résume toute l’histoire : le lasso sélectionne, le ridge se contente de réduire. L’elastic net (0 < alpha < 1) se situe entre les deux.

Finaliser et noter sur le jeu de test

Nous choisissons le meilleur lambda, finalisons le workflow, et last_fit() — ajuster sur l’ensemble des données d’entraînement et noter une seule fois sur le jeu de test intact. La table de coefficients tidy montre quels prédicteurs ont survécu :

library(rsample)
library(recipes)
library(parsnip)
library(workflows)
library(tune)
library(dials)
library(yardstick)
library(broom)
data("PimaIndiansDiabetes2", package = "mlbench")
pima <- na.omit(PimaIndiansDiabetes2)

set.seed(123)
split      <- initial_split(pima, prop = 0.80, strata = diabetes)
train_data <- training(split)

rec        <- recipe(diabetes ~ ., data = train_data) |> step_normalize(all_numeric_predictors())
lasso_spec <- logistic_reg(penalty = tune(), mixture = 1) |> set_engine("glmnet")
lasso_wf   <- workflow() |> add_recipe(rec) |> add_model(lasso_spec)

set.seed(123); folds <- vfold_cv(train_data, v = 10, strata = diabetes)
grid <- grid_regular(penalty(range = c(-4, 0)), levels = 50)
set.seed(123)
lasso_tuned <- tune_grid(lasso_wf, resamples = folds, grid = grid,
                         metrics = metric_set(roc_auc, accuracy))
best_lambda <- select_best(lasso_tuned, metric = "roc_auc")

# Finalize with the chosen lambda, fit on train, score ONCE on test
final_fit <- finalize_workflow(lasso_wf, best_lambda) |> last_fit(split)
collect_metrics(final_fit)
# A tibble: 3 × 4
  .metric     .estimator .estimate .config        
  <chr>       <chr>          <dbl> <chr>          
1 accuracy    binary         0.785 pre0_mod0_post0
2 roc_auc     binary         0.840 pre0_mod0_post0
3 brier_class binary         0.156 pre0_mod0_post0

Lisez les chiffres (la performance honnête, hors échantillon, de votre classifieur) :

  • Exactitude ≈ 0.78 — le modèle prédit correctement le diabète environ 78 % du temps sur des patients qu’il n’a jamais vus lors de l’ajustement. Utile, mais à lire avec l’AUC, car l’exactitude peut flatter un modèle sur une issue déséquilibrée.
  • AUC ROC ≈ 0.84 — la probabilité que le modèle attribue à un positif aléatoire un score plus élevé qu’à un négatif aléatoire. 0.5 est un tirage à pile ou face, 1.0 est parfait ; 0.84 est un classifieur réellement bon. Comme l’AUC est sans seuil, elle juge le classement du modèle, pas un seuil arbitraire de 0.5.

Et les coefficients survivants (sur l’échelle standardisée, donc directement comparables en taille) :

library(broom)

# The coefficients of the finalized lasso model
tidy(extract_fit_parsnip(final_fit$.workflow[[1]]))
# A tibble: 9 × 3
  term        estimate penalty
  <chr>          <dbl>   <dbl>
1 (Intercept)  -0.960   0.0133
2 pregnant      0.219   0.0133
3 glucose       1.04    0.0133
4 pressure      0       0.0133
5 triceps       0.109   0.0133
6 insulin       0.0284  0.0133
7 mass          0.452   0.0133
8 pedigree      0.331   0.0133
9 age           0.274   0.0133

Un coefficient d’exactement 0 signifie que le lasso a écarté ce prédicteur. Les plus grands coefficients — glucose, mass (IMC), pedigree, age — sont les prédicteurs sur lesquels le classifieur s’appuie le plus, ce qui correspond à l’histoire clinique. Comme chaque coefficient est un effet en log-odds, vous pouvez lire son signe : positif signifie que des valeurs plus élevées poussent la prédiction vers diabète-positif. La section suivante montre la route classique, où la sélection de variables est encore plus marquée.

La route classique : glmnet::cv.glmnet

La plupart des utilisateurs de R découvrent la régression logistique pénalisée directement via glmnet. cv.glmnet() avec family = "binomial" fait la validation croisée pour vous et renvoie deux lambdas utiles. C’est la route qui vaut la peine d’être connue car elle rend explicite le choix entre lambda.min et lambda.1se :

library(glmnet)
library(rsample)
data("PimaIndiansDiabetes2", package = "mlbench")
pima <- na.omit(PimaIndiansDiabetes2)

set.seed(123)
split      <- initial_split(pima, prop = 0.80, strata = diabetes)
train_data <- training(split)
test_data  <- testing(split)

# glmnet works on a numeric matrix + a binary outcome
x <- model.matrix(diabetes ~ ., train_data)[, -1]
y <- ifelse(train_data$diabetes == "pos", 1, 0)

# Lasso (alpha = 1) for a binary outcome, with built-in 10-fold cross-validation
set.seed(123)
cv_lasso <- cv.glmnet(x, y, alpha = 1, family = "binomial")   # standardize = TRUE by default

c(lambda.min = cv_lasso$lambda.min, lambda.1se = cv_lasso$lambda.1se)
 lambda.min  lambda.1se 
0.008711219 0.067447803 

Deux lambdas, deux philosophies :

  • lambda.min — le lambda à l’erreur validée par croisement la plus faible. Prédit le mieux, mais conserve plus de prédicteurs.
  • lambda.1se — le plus grand lambda dont l’erreur reste à une erreur standard du minimum. Un peu plus d’erreur, mais un modèle plus simple, pénalisé plus agressivement (plus de zéros). Le choix conventionnel quand vous valorisez l’interprétabilité et la robustesse.

Regardez les coefficients à lambda.1se — la pénalisation comme sélection de variables, au grand jour :

library(glmnet)
library(rsample)
data("PimaIndiansDiabetes2", package = "mlbench")
pima <- na.omit(PimaIndiansDiabetes2)

set.seed(123)
split      <- initial_split(pima, prop = 0.80, strata = diabetes)
train_data <- training(split)
x <- model.matrix(diabetes ~ ., train_data)[, -1]
y <- ifelse(train_data$diabetes == "pos", 1, 0)
set.seed(123); cv_lasso <- cv.glmnet(x, y, alpha = 1, family = "binomial")

# Coefficients at the simpler 1-SE model: dots are predictors dropped (= zero)
coef(cv_lasso, s = "lambda.1se")
9 x 1 sparse Matrix of class "dgCMatrix"
               lambda.1se
(Intercept) -5.2246868597
pregnant     .           
glucose      0.0247421454
pressure     .           
triceps      0.0007391438
insulin      .           
mass         0.0249294143
pedigree     0.0303655000
age          0.0176041112

Plusieurs prédicteurs affichent un . : leur coefficient est exactement zéro, donc le lasso les a écartés. Seule une poignée — menée par glucose, mass et pedigree — conserve un coefficient non nul. De huit prédicteurs aux quelques-uns qui portent réellement le signal du diabète, automatiquement. Ce modèle plus parcimonieux est plus facile à expliquer et généralise souvent tout aussi bien.

Lasso contre régression logistique simple : le modèle plus simple perd-il en exactitude ?

L’intérêt de la régularisation est un modèle plus simple qui prédit au moins aussi bien. Prouvons-le : notons le lasso (aux deux lambdas) contre la régression logistique simple avec les huit prédicteurs, sur le même jeu de test mis de côté :

library(glmnet)
library(rsample)
data("PimaIndiansDiabetes2", package = "mlbench")
pima <- na.omit(PimaIndiansDiabetes2)

set.seed(123)
split      <- initial_split(pima, prop = 0.80, strata = diabetes)
train_data <- training(split)
test_data  <- testing(split)
x      <- model.matrix(diabetes ~ ., train_data)[, -1]
y      <- ifelse(train_data$diabetes == "pos", 1, 0)
x_test <- model.matrix(diabetes ~ ., test_data)[, -1]
set.seed(123); cv_lasso <- cv.glmnet(x, y, alpha = 1, family = "binomial")

# Accuracy = share of correct pos/neg calls at the 0.5 probability cutoff
accuracy_at <- function(prob) {
  pred <- ifelse(prob > 0.5, "pos", "neg")
  mean(pred == test_data$diabetes)
}

# Lasso predictions at each lambda (type = "response" → probabilities)
p_min <- as.vector(predict(cv_lasso, newx = x_test, s = "lambda.min", type = "response"))
p_1se <- as.vector(predict(cv_lasso, newx = x_test, s = "lambda.1se", type = "response"))

# Plain logistic regression with ALL predictors
full_model <- glm(diabetes ~ ., data = train_data, family = binomial)
p_full     <- predict(full_model, test_data, type = "response")

data.frame(
  model    = c("Lasso (lambda.min)", "Lasso (lambda.1se)", "Full logistic (all 8)"),
  accuracy = c(accuracy_at(p_min), accuracy_at(p_1se), accuracy_at(p_full))
)
                  model  accuracy
1    Lasso (lambda.min) 0.7848101
2    Lasso (lambda.1se) 0.7721519
3 Full logistic (all 8) 0.7594937

Les modèles lasso égalent ou dépassent la régression logistique complète — avec moins de prédicteurs. lambda.min conserve quelques variables de plus pour la meilleure exactitude brute ; lambda.1se est le modèle le plus parcimonieux et ne coûte qu’une pincée d’exactitude en échange d’un classifieur bien plus simple. Voilà la décision centrale de la régression pénalisée : combien d’exactitude (le cas échéant) êtes-vous prêt à échanger contre de la simplicité ? Ici, vous n’en échangez presque aucune — ce qui est toute la raison de régulariser. Toutes choses égales par ailleurs, préférez le modèle plus simple.

Ridge et elastic net : changez un seul argument

Tout ce qui précède était du lasso (mixture = 1 / alpha = 1). Les deux autres méthodes changent un seul argument — le workflow est par ailleurs identique :

library(rsample)
library(recipes)
library(parsnip)
library(workflows)
library(tune)
library(dials)
library(yardstick)
data("PimaIndiansDiabetes2", package = "mlbench")
pima <- na.omit(PimaIndiansDiabetes2)

set.seed(123)
split      <- initial_split(pima, prop = 0.80, strata = diabetes)
train_data <- training(split)
rec        <- recipe(diabetes ~ ., data = train_data) |> step_normalize(all_numeric_predictors())
set.seed(123); folds <- vfold_cv(train_data, v = 10, strata = diabetes)
grid <- grid_regular(penalty(range = c(-4, 0)), levels = 50)

# Helper: tune a penalized logistic model at a given mixture, return test-set AUC
score_penalized <- function(mixture) {
  spec <- logistic_reg(penalty = tune(), mixture = mixture) |> set_engine("glmnet")
  wf   <- workflow() |> add_recipe(rec) |> add_model(spec)
  set.seed(123)
  tuned <- tune_grid(wf, resamples = folds, grid = grid,
                     metrics = metric_set(roc_auc, accuracy))
  best  <- select_best(tuned, metric = "roc_auc")
  m     <- collect_metrics(finalize_workflow(wf, best) |> last_fit(split))
  data.frame(accuracy = m$.estimate[m$.metric == "accuracy"],
             roc_auc  = m$.estimate[m$.metric == "roc_auc"])
}

rbind(
  Ridge         = score_penalized(0),     # L2 only — keeps all predictors
  `Elastic net` = score_penalized(0.5),   # L1 + L2 blend
  Lasso         = score_penalized(1)      # L1 only — selects predictors
)
             accuracy   roc_auc
Ridge       0.7468354 0.8338171
Elastic net 0.7594937 0.8432511
Lasso       0.7848101 0.8396226

Sur les données Pima, les trois sont proches en exactitude et en AUC — fréquent, et en soi un constat utile. Quand les performances sont à égalité, préférez le modèle plus simple : lasso ou elastic net (qui ont écarté des prédicteurs) plutôt que le ridge (qui les conserve tous les huit). Toutes choses égales par ailleurs, optez pour le modèle plus simple.

NoteLequel devrais-je utiliser ?
  • Lasso quand vous pensez que seuls quelques prédicteurs comptent — il donne un classifieur parcimonieux et interprétable.
  • Ridge quand de nombreux prédicteurs contribuent chacun un peu et que vous voulez tous les conserver (il gère aussi élégamment les prédicteurs corrélés en répartissant la charge entre eux).
  • Elastic net quand les prédicteurs sont corrélés et que vous voulez quand même une sélection — il regroupe les prédicteurs corrélés au lieu d’en choisir un arbitrairement (la faiblesse du lasso). Vous hésitez ? Réglez aussi mixture et laissez la validation croisée choisir le mélange.

La régression logistique ordinaire choisit les coefficients β pour maximiser la vraisemblance des étiquettes 0/1 observées — de façon équivalente, pour minimiser la log-vraisemblance négative (la déviance). La régression logistique pénalisée ajoute une pénalité sur la taille des coefficients :

\[ \hat{\beta} = \arg\min_{\beta} \; \big[ -\ell(\beta) \big] + \lambda \, P(\beta), \]

\(\ell(\beta)\) est la log-vraisemblance, λ ≥ 0 contrôle la force de la pénalité, et \(P(\beta)\) est le terme de pénalité. Les trois méthodes ne diffèrent que par \(P\) :

\[ P_{ridge} = \sum_{j} \beta_j^2 \quad (\ell_2), \qquad P_{lasso} = \sum_{j} \lvert \beta_j \rvert \quad (\ell_1), \]

\[ P_{enet} = \alpha \sum_{j} \lvert \beta_j \rvert + (1-\alpha)\sum_{j} \beta_j^2 . \]

La pénalité L2 (ridge) est lisse, donc elle réduit les coefficients mais ne les force jamais à exactement zéro. La pénalité L1 (lasso) a un angle en zéro, et c’est cet angle qui lui permet de mettre des coefficients exactement à zéro — la raison géométrique pour laquelle le lasso sélectionne des variables et le ridge non. L’elastic net mélange les deux avec \(\alpha\) (= mixture) : \(\alpha = 1\) donne le lasso, \(\alpha = 0\) donne le ridge. Dans tous les cas, \(\lambda\) (= penalty) est le bouton choisi par validation croisée ; λ = 0 retrouve la régression logistique simple. Comme la pénalité compare les tailles des coefficients, les prédicteurs doivent d’abord être standardisés — sinon un prédicteur exprimé en petites unités serait pénalisé du simple fait de son échelle.

Vous venez de caret ? La correspondance

Si vous avez appris la régression logistique pénalisée avec le train(method = "glmnet") de caret, les idées se transposent directement — seuls les verbes changent. La seule différence avec un modèle pénalisé de régression est logistic_reg() (avec family = "binomial" sous le capot) au lieu de linear_reg() :

caret tidymodels
train(..., method = "glmnet", family = "binomial") logistic_reg(penalty = tune(), mixture = tune()) \|> set_engine("glmnet")
tuneGrid = expand.grid(alpha = 1, lambda = ...) mixture = 1 (lasso) + a penalty() grid
tuneGrid = expand.grid(alpha = 0, lambda = ...) mixture = 0 (ridge) + a penalty() grid
trControl = trainControl("cv", number = 10) vfold_cv(v = 10) + tune_grid()
metric = "ROC" metric_set(roc_auc, accuracy)
model$bestTune select_best(tuned, metric = "roc_auc")

alpha dans glmnet/caret est mixture dans parsnip ; lambda est penalty. Tout le reste est le même arc séparation → recipe → réglage → finalisation que vous connaissez déjà du workflow tidymodels.

🟢 Avec un agent IA

Vous avez un modèle de classification avec trop de prédicteurs ou des prédicteurs corrélés ? Demandez à Prova « ajuste une régression logistique lasso à ces données, règle la pénalité par validation croisée, et dis-moi quels prédicteurs elle garde » — elle répond avec du code tidymodels + glmnet que vous pouvez exécuter sur vos propres données, puis vous aide à lire le chemin des coefficients, l’AUC, et à choisir entre lambda.min et lambda.1se. The runtime is the judge. Demander à Prova →

Problèmes courants

  • Vous avez oublié de standardiser les prédicteurs. Les pénalités agissent sur la taille des coefficients, donc un prédicteur exprimé en grandes unités est injustement moins réduit. Utilisez toujours step_normalize(all_numeric_predictors()) dans la recipe (ou fiez-vous au standardize = TRUE par défaut de glmnet).
  • glmnet renvoie une erreur sur des facteurs ou une issue non numérique. glmnet() a besoin d’une matrice de modèle numérique pour x et d’une issue 0/1 (ou facteur) pour y. Construisez la matrice avec model.matrix(y ~ ., data)[, -1] et codez l’issue avec ifelse(y == "pos", 1, 0). Passez family = "binomial" pour une issue binaire. Dans une recipe tidymodels, ajoutez step_dummy(all_nominal_predictors()) pour les prédicteurs catégoriels.
  • Vos données ont des valeurs manquantes. na.omit() supprime les lignes incomplètes (utilisé ici), mais vous perdez des données. Préférez imputer dans la recipe — step_impute_median(all_numeric_predictors()) — afin qu’aucune ligne ne soit jetée et que l’imputation soit apprise uniquement sur l’entraînement.
  • L’exactitude paraît correcte mais le modèle rate les positifs. Sur une issue déséquilibrée, l’exactitude peut être élevée alors que le rappel sur la classe rare est médiocre. Lisez l’AUC ROC (sans seuil) en parallèle de l’exactitude, et envisagez un seuil de probabilité différent que 0.5.
  • Deux exécutions donnent des lambdas légèrement différents. La répartition des plis de la validation croisée est aléatoire. Appelez set.seed() avant vfold_cv() / cv.glmnet() pour un résultat reproductible.

Questions fréquentes

Utilisez glmnet avec family = "binomial". Pour le lasso : cv.glmnet(x, y, alpha = 1, family = "binomial") pour choisir la pénalité par validation croisée, puis glmnet(x, y, alpha = 1, family = "binomial", lambda = cv$lambda.min) pour le modèle final — où x est une model.matrix numérique et y une issue 0/1. Dans tidymodels : logistic_reg(penalty = tune(), mixture = 1) |> set_engine("glmnet") dans un workflow avec step_normalize(), réglé par tune_grid().

Le lasso, c’est glmnet avec alpha = 1 (ou mixture = 1 dans tidymodels) et family = "binomial". cv.glmnet(x, y, alpha = 1, family = "binomial") valide par croisement la pénalité ; coef() à lambda.1se montre quels prédicteurs le lasso a gardés et lesquels il a mis exactement à zéro. Contrairement au ridge, le lasso réalise une sélection de variables — il écarte les prédicteurs faibles du classifieur.

Le ridge, c’est glmnet avec alpha = 0 (ou mixture = 0 dans tidymodels) et family = "binomial" : cv.glmnet(x, y, alpha = 0, family = "binomial") pour régler la pénalité, puis glmnet(..., alpha = 0, lambda = cv$lambda.min). Le ridge réduit chaque coefficient vers zéro mais conserve tous les prédicteurs — utile quand vous voulez garder chaque variable tout en domptant les prédicteurs corrélés et le surajustement.

glmnet ajuste une régression logistique pénalisée quand vous passez family = "binomial". glmnet(x, y, alpha, lambda, family = "binomial") ajuste le ridge (alpha = 0), le lasso (alpha = 1) ou l’elastic net (entre les deux) sur une séquence de pénalités ; cv.glmnet() ajoute une validation croisée intégrée et renvoie lambda.min (erreur CV la plus faible) et lambda.1se (le modèle le plus simple à une erreur standard). Il a besoin d’une matrice de prédicteurs numérique et standardise les prédicteurs par défaut.

La régression logistique simple (avec tous les prédicteurs) peut surajuster lorsque les prédicteurs sont nombreux ou corrélés. La régression logistique pénalisée réduit les coefficients, prédisant généralement au moins aussi bien sur de nouvelles données avec moins de prédicteurs — un modèle plus simple et plus robuste. Sur les données de diabète Pima, le lasso égale ou dépasse le modèle logistique complet. Utilisez la pénalisation pour la prédiction et la sélection de variables ; utilisez la régression logistique simple (en Biostatistique) quand vous devez interpréter l’odds ratio et la p-value de chaque coefficient.

Testez vos connaissances

À l’aide des données PimaIndiansDiabetes2 (mlbench, avec na.omit()), construisez un modèle logistique ridge (mixture = 0) à la façon tidymodels : séparez 80/20 en stratifiant sur diabetes, normalisez les prédicteurs, réglez penalty par validation croisée à 10 plis sur l’AUC ROC, et rapportez l’exactitude et l’AUC du jeu de test. Comment se compare-t-il au lasso de la leçon — et le ridge écarte-t-il des prédicteurs ?

La structure est identique au workflow lasso — changez seulement mixture = 1 en mixture = 0. Vous aurez besoin de rsample, recipes, parsnip, workflows, tune, dials et yardstick. Pour vérifier les prédicteurs écartés, regardez tidy() de l’ajustement finalisé — le ridge les gardera tous (aucun zéro).

library(rsample); library(recipes); library(parsnip)
library(workflows); library(tune); library(dials); library(yardstick)
data("PimaIndiansDiabetes2", package = "mlbench")
pima <- na.omit(PimaIndiansDiabetes2)

set.seed(123)
split      <- initial_split(pima, prop = 0.80, strata = diabetes)
train_data <- training(split)

rec        <- recipe(diabetes ~ ., data = train_data) |> step_normalize(all_numeric_predictors())
ridge_spec <- logistic_reg(penalty = tune(), mixture = 0) |> set_engine("glmnet")
ridge_wf   <- workflow() |> add_recipe(rec) |> add_model(ridge_spec)

set.seed(123); folds <- vfold_cv(train_data, v = 10, strata = diabetes)
grid <- grid_regular(penalty(range = c(-4, 0)), levels = 50)
set.seed(123)
tuned <- tune_grid(ridge_wf, resamples = folds, grid = grid,
                   metrics = metric_set(roc_auc, accuracy))
best  <- select_best(tuned, metric = "roc_auc")

collect_metrics(finalize_workflow(ridge_wf, best) |> last_fit(split))

Vous obtiendrez une exactitude et une AUC proches de celles du lasso — essentiellement la même performance. La différence clé est dans les coefficients : le ridge conserve les huit prédicteurs (aucun mis à zéro), tandis que le lasso en a écarté plusieurs. Quand les performances sont à égalité, le modèle lasso/elastic-net est le plus simple, et le préférable.

Vérification rapide. Vous ajustez une régression logistique lasso et une ridge sur les mêmes données et elles classent aussi bien l’une que l’autre (même AUC), mais le modèle lasso a trois coefficients exactement nuls tandis que le modèle ridge n’en a aucun. Quel modèle livreriez-vous, et pourquoi ?

Livrez le modèle lasso. Quand deux classifieurs performent aussi bien, préférez le plus simple : les trois zéros du lasso signifient qu’il utilise moins de prédicteurs, donc il est plus facile à expliquer, moins coûteux en collecte de données et moins susceptible de surajuster par la suite. Le ridge conserve chaque prédicteur, ce qui ajoute de la complexité pour aucun gain de performance ici. Toutes choses égales par ailleurs, optez pour le modèle plus simple.

Conclusion

Vous avez construit une régression logistique pénalisée de bout en bout sur les données PimaIndiansDiabetes2 : le lasso (L1) réduit et met certains coefficients exactement à zéro, réalisant une sélection de variables ; le ridge (L2) réduit chaque coefficient mais les conserve tous ; l’elastic net mélange les deux. Vous avez réglé la pénalité lambda honnêtement par validation croisée — à la fois de façon moderne (logistic_reg(penalty = tune(), mixture =) |> set_engine("glmnet") + tune_grid()) et par la route classique cv.glmnet(..., family = "binomial") — lu la courbe de CV et le chemin des coefficients, mesuré l’exactitude et l’AUC ROC, et montré que le lasso parcimonieux égale ou dépasse la régression logistique simple. La règle pratique : standardisez d’abord, laissez la validation croisée choisir lambda, et quand les performances sont à égalité, livrez le modèle plus simple.

Ensuite, voyez comment choisir le seuil de probabilité et lire la matrice de confusion complète dans évaluation des modèles de classification, ou revisitez le workflow sur lequel cette leçon s’appuie dans construire des modèles avec tidymodels.

Leçons connexes

Vous préférez un livre ? Machine Learning Essentials est disponible en PDF téléchargeable — toutes les leçons de cette série, hors ligne et à vous.

Obtenir le livre (PDF) →Les leçons restent gratuites en ligne.
Cette page vous a-t-elle été utile ?

Prouvez que vous savez le faire. Maîtrisez toute la série Classification en R — suivez votre parcours, construisez des projets et obtenez un certificat.

Commencer gratuitement →

Passez à Pro — Prova illimité sur vos propres données et un certificat vérifiable qui atteste la compétence.

dès 15 $/mois facturé annuellement

Passer à Pro →

✓ Vous êtes Pro — continuez. The runtime is the judge.

Recevez les nouvelles leçons R & Python par e-mail

Pratique, reproductible, sans spam. Désinscription à tout moment.

Double opt-in. Nous ne partageons jamais votre e-mail.

Partager cette pageXLinkedInRedditHN
Note

Cette leçon est reproductible : chaque coefficient, métrique et figure a été produit par le code montré — copiez n’importe quel bloc et exécutez-le pour les reproduire. The runtime is the judge.

Références

  • James, G., Witten, D., Hastie, T., & Tibshirani, R. (2014). An Introduction to Statistical Learning, with Applications in R. Springer. (Chapitre 6 — Sélection de modèle linéaire et régularisation ; chapitre 4 — Classification.)
  • Friedman, J., Hastie, T., & Tibshirani, R. (2010). Regularization Paths for Generalized Linear Models via Coordinate Descent. Journal of Statistical Software, 33(1). (Le package glmnet.)
  • Kuhn, M., & Silge, J. Tidy Modeling with R. Disponible gratuitement en ligne (CC BY-NC-SA).

Réutilisation

Citation

BibTeX
@online{2026,
  author = {},
  title = {Régression logistique pénalisée en R : lasso, ridge et
    elastic net avec glmnet},
  date = {2026-06-25},
  url = {https://www.datanovia.com/learn/machine-learning/classification/penalized-logistic-regression},
  langid = {fr}
}
Veuillez citer ce travail comme suit :
“Régression logistique pénalisée en R : lasso, ridge et elastic net avec glmnet.” 2026. June 25. https://www.datanovia.com/learn/machine-learning/classification/penalized-logistic-regression.