Classification de la présence d’une maladie cardiaque

Analyse exploratoire, modèles linéaires et méthodes d’ensemble

Author

Mehdi Khamassi

Published

July 15, 2026

1 Introduction

Les maladies cardiovasculaires constituent un enjeu important de santé publique. L’objectif de cette étude est de construire un modèle de classification binaire permettant d’estimer la présence d’une maladie cardiaque à partir de variables cliniques.

La variable cible est HeartDisease :

  • 0 : absence de maladie cardiaque ;
  • 1 : présence de maladie cardiaque.

1.1 Objectifs de l’étude

Cette étude vise à :

  1. analyser la structure et la qualité des données ;
  2. étudier les relations entre les variables et la cible ;
  3. construire une régression logistique non pénalisée ;
  4. évaluer ses performances prédictives ;
  5. préparer la comparaison avec Ridge, Lasso et Elastic Net ;
  6. interpréter les coefficients et les erreurs du modèle.

2 Organisation du rapport

Le rapport suit une progression en six étapes : audit et exploration des données, préparation des variables, estimation de modèles logistiques, évaluation de modèles fondés sur les arbres, comparaison globale, puis conclusion et perspectives.

3 Acquisition des données

Le jeu de données est téléchargé depuis Kaggle uniquement lorsqu’il n’est pas déjà présent dans le dossier local data.

Afficher le code
from pathlib import Path
import kagglehub

DATASET_HANDLE = "fedesoriano/heart-failure-prediction"
DATA_DIRECTORY = Path("data")
DATA_FILENAME = "heart.csv"


def download_dataset_if_not_exists(
    dataset_handle: str,
    output_dir: str | Path,
    filename: str,
) -> Path:
    """Télécharge le fichier Kaggle s’il n’existe pas localement."""

    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    expected_path = output_dir / filename

    if expected_path.exists():
        print(f"Fichier local utilisé : {expected_path}")
        return expected_path

    downloaded_path = Path(
        kagglehub.dataset_download(
            dataset_handle,
            path=filename,
            output_dir=str(output_dir),
        )
    )

    if not downloaded_path.exists():
        raise FileNotFoundError(
            f"Le fichier téléchargé est introuvable : {downloaded_path}"
        )

    print(f"Fichier téléchargé : {downloaded_path}")
    return downloaded_path


data_path = download_dataset_if_not_exists(
    dataset_handle=DATASET_HANDLE,
    output_dir=DATA_DIRECTORY,
    filename=DATA_FILENAME,
)
Fichier local utilisé : data/heart.csv

4 Environnement et configuration de l’étude

Afficher le code
import sys
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

from sklearn.model_selection import ( train_test_split, cross_validate 
)
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
    accuracy_score,
    balanced_accuracy_score,
    precision_score,
    recall_score,
    f1_score,
    roc_auc_score,
    confusion_matrix,
    classification_report,
    ConfusionMatrixDisplay,
    RocCurveDisplay,
    PrecisionRecallDisplay,
    average_precision_score,
)


RANDOM_STATE = 42
TARGET = "HeartDisease"

pd.set_option("display.max_columns", None)

print("Python :", sys.version)
print("Exécutable :", sys.executable)
Python : 3.12.3 (v3.12.3:f6650f9ad7, Apr  9 2024, 08:18:47) [Clang 13.0.0 (clang-1300.0.29.30)]
Exécutable : /Users/mehdikhamassi/Desktop/PRO/training_model/.venv/bin/python3

5 Chargement des données

Afficher le code
data = pd.read_csv(data_path)

display(data.head())
Age Sex ChestPainType RestingBP Cholesterol FastingBS RestingECG MaxHR ExerciseAngina Oldpeak ST_Slope HeartDisease
0 40 M ATA 140 289 0 Normal 172 N 0.0 Up 0
1 49 F NAP 160 180 0 Normal 156 N 1.0 Flat 1
2 37 M ATA 130 283 0 ST 98 N 0.0 Up 0
3 48 F ASY 138 214 0 Normal 108 Y 1.5 Flat 1
4 54 M NAP 150 195 0 Normal 122 N 0.0 Up 0

6 Audit et qualité des données

6.1 Dimensions et types

Afficher le code
n_rows, n_columns = data.shape

print(f"Nombre d’observations : {n_rows}")
print(f"Nombre de variables : {n_columns}")

data.info()
Nombre d’observations : 918
Nombre de variables : 12
<class 'pandas.DataFrame'>
RangeIndex: 918 entries, 0 to 917
Data columns (total 12 columns):
 #   Column          Non-Null Count  Dtype  
---  ------          --------------  -----  
 0   Age             918 non-null    int64  
 1   Sex             918 non-null    str    
 2   ChestPainType   918 non-null    str    
 3   RestingBP       918 non-null    int64  
 4   Cholesterol     918 non-null    int64  
 5   FastingBS       918 non-null    int64  
 6   RestingECG      918 non-null    str    
 7   MaxHR           918 non-null    int64  
 8   ExerciseAngina  918 non-null    str    
 9   Oldpeak         918 non-null    float64
 10  ST_Slope        918 non-null    str    
 11  HeartDisease    918 non-null    int64  
dtypes: float64(1), int64(6), str(5)
memory usage: 86.2 KB
NoteInterprétation
  • Nous disposons de 918 observations au total.
  • Le jeu de données comprend 6 variables qualitatives et 5 variables quantitatives.
  • Nous cherchons à prédire la variable binaire HeartDisease.

6.2 Valeurs manquantes

Afficher le code
missing_summary = (
    data.isna()
    .sum()
    .to_frame("nombre")
    .assign(
        pourcentage=lambda table:
            100 * table["nombre"] / len(data)
    )
    .sort_values("nombre", ascending=False)
)

display(missing_summary.round(2))
nombre pourcentage
Age 0 0.0
Sex 0 0.0
ChestPainType 0 0.0
RestingBP 0 0.0
Cholesterol 0 0.0
FastingBS 0 0.0
RestingECG 0 0.0
MaxHR 0 0.0
ExerciseAngina 0 0.0
Oldpeak 0 0.0
ST_Slope 0 0.0
HeartDisease 0 0.0
NoteInterprétation
  • Aucune donnée manquante.

6.3 Doublons

Afficher le code
n_duplicates = data.duplicated().sum()

print(f"Nombre de doublons : {n_duplicates}")
Nombre de doublons : 0
NoteInterprétation
  • Aucun doublon n’a été détecté.

6.4 Statistiques descriptives

Afficher le code
display(data.describe().T)
display(data.describe(include="all").T)
count mean std min 25% 50% 75% max
Age 918.0 53.510893 9.432617 28.0 47.00 54.0 60.0 77.0
RestingBP 918.0 132.396514 18.514154 0.0 120.00 130.0 140.0 200.0
Cholesterol 918.0 198.799564 109.384145 0.0 173.25 223.0 267.0 603.0
FastingBS 918.0 0.233115 0.423046 0.0 0.00 0.0 0.0 1.0
MaxHR 918.0 136.809368 25.460334 60.0 120.00 138.0 156.0 202.0
Oldpeak 918.0 0.887364 1.066570 -2.6 0.00 0.6 1.5 6.2
HeartDisease 918.0 0.553377 0.497414 0.0 0.00 1.0 1.0 1.0
count unique top freq mean std min 25% 50% 75% max
Age 918.0 NaN NaN NaN 53.510893 9.432617 28.0 47.0 54.0 60.0 77.0
Sex 918 2 M 725 NaN NaN NaN NaN NaN NaN NaN
ChestPainType 918 4 ASY 496 NaN NaN NaN NaN NaN NaN NaN
RestingBP 918.0 NaN NaN NaN 132.396514 18.514154 0.0 120.0 130.0 140.0 200.0
Cholesterol 918.0 NaN NaN NaN 198.799564 109.384145 0.0 173.25 223.0 267.0 603.0
FastingBS 918.0 NaN NaN NaN 0.233115 0.423046 0.0 0.0 0.0 0.0 1.0
RestingECG 918 3 Normal 552 NaN NaN NaN NaN NaN NaN NaN
MaxHR 918.0 NaN NaN NaN 136.809368 25.460334 60.0 120.0 138.0 156.0 202.0
ExerciseAngina 918 2 N 547 NaN NaN NaN NaN NaN NaN NaN
Oldpeak 918.0 NaN NaN NaN 0.887364 1.06657 -2.6 0.0 0.6 1.5 6.2
ST_Slope 918 3 Flat 460 NaN NaN NaN NaN NaN NaN NaN
HeartDisease 918.0 NaN NaN NaN 0.553377 0.497414 0.0 0.0 1.0 1.0 1.0

6.5 Nombre de valeurs uniques

Afficher le code
unique_summary = (
    data.nunique(dropna=False)
    .sort_values()
    .to_frame("nombre_valeurs_uniques")
)

display(unique_summary)
nombre_valeurs_uniques
Sex 2
FastingBS 2
ExerciseAngina 2
HeartDisease 2
RestingECG 3
ST_Slope 3
ChestPainType 4
Age 50
Oldpeak 53
RestingBP 67
MaxHR 119
Cholesterol 222

7 Analyse exploratoire des données

7.1 Nature des variables

Afficher le code
numeric_features = [
    "Age",
    "RestingBP",
    "Cholesterol",
    "MaxHR",
    "Oldpeak",
]

categorical_features = [
    "Sex",
    "ChestPainType",
    "FastingBS",
    "RestingECG",
    "ExerciseAngina",
    "ST_Slope",
]

print("Variables numériques :", numeric_features)
print("Variables catégorielles :", categorical_features)
Variables numériques : ['Age', 'RestingBP', 'Cholesterol', 'MaxHR', 'Oldpeak']
Variables catégorielles : ['Sex', 'ChestPainType', 'FastingBS', 'RestingECG', 'ExerciseAngina', 'ST_Slope']

FastingBS est stockée sous forme numérique, mais représente une variable catégorielle binaire.

7.2 Distribution de la variable cible

Afficher le code
target_summary = pd.DataFrame(
    {
        "effectif": data[TARGET].value_counts().sort_index(),
        "proportion": (
            data[TARGET]
            .value_counts(normalize=True)
            .sort_index()
        ),
    }
)

display(target_summary.round(3))

ax = target_summary["effectif"].plot(
    kind="bar",
    figsize=(7, 4),
)

ax.set_title("Distribution de la variable cible")
ax.set_xlabel("Classe")
ax.set_ylabel("Effectif")
ax.set_xticklabels(["Absence", "Présence"], rotation=0)

plt.tight_layout()
plt.show()
effectif proportion
HeartDisease
0 410 0.447
1 508 0.553
(a) Distribution de la variable cible HeartDisease
(b)
Figure 1

La proportion de la classe \(k\) est calculée par :

\[ \widehat{p}_k = \frac{n_k}{n}, \]

\(n_k\) désigne le nombre d’observations de la classe \(k\) et \(n\) le nombre total d’observations.

NoteInterprétation

La classe majoritaire est HeartDisease = 1, avec environ 55,3 % des observations, contre 44,7 % pour la classe HeartDisease = 0.

La répartition peut être considérée comme relativement équilibrée, car les deux classes sont bien représentées et le rapport entre leurs effectifs reste proche de 1. Le déséquilibre est donc modéré et ne nécessite pas, à ce stade, de méthode de rééchantillonnage comme SMOTE ou le sous-échantillonnage.

Une séparation stratifiée reste néanmoins utilisée afin de conserver ces proportions dans les jeux d’apprentissage et de test.

7.3 Distribution des variables catégorielles

Afficher le code
for column in categorical_features:
    counts = (
        data[column]
        .value_counts(dropna=False)
        .sort_values(ascending=False)
    )

    ax = counts.plot(
        kind="bar",
        figsize=(7, 4),
    )

    ax.set_title(f"Distribution de {column}")
    ax.set_xlabel(column)
    ax.set_ylabel("Nombre d’observations")

    plt.xticks(rotation=45, ha="right")
    plt.tight_layout()
    plt.show()

7.4 Variables catégorielles selon la cible

Afficher le code
for column in categorical_features:
    cross_table = pd.crosstab(
        data[column],
        data[TARGET],
        normalize="index",
    )

    ax = cross_table.plot(
        kind="bar",
        stacked=True,
        figsize=(7, 4),
    )

    ax.set_title(f"Proportion de HeartDisease selon {column}")
    ax.set_xlabel(column)
    ax.set_ylabel("Proportion")
    ax.legend(["Absence", "Présence"], title="HeartDisease")

    plt.xticks(rotation=45, ha="right")
    plt.tight_layout()
    plt.show()

NoteInterprétation

7.4.1 Interprétation de ChestPainType

  • La modalité ASY semble être la plus fortement associée à la présence d’une maladie cardiaque : la proportion d’individus appartenant à la classe HeartDisease = 1 y est nettement plus élevée.

  • À l’inverse, la modalité ATA semble être la moins associée à la présence d’une maladie cardiaque, avec une proportion plus importante d’observations appartenant à la classe HeartDisease = 0.

  • Les modalités NAP et TA présentent une répartition plus intermédiaire entre les deux classes. Leur association avec la cible paraît donc moins marquée que pour ASY et ATA.

  • Ces observations décrivent une association dans l’échantillon. Elles ne permettent pas, à elles seules, de conclure à une relation causale. Il faudra confirmer leur importance dans le modèle de classification et, si besoin, par un test d’association comme le khi-deux.

7.4.2 Interprétation de ExerciseAngina

La présence d’une angine provoquée par l’exercice semble fortement associée à la présence d’une maladie cardiaque. Environ 90 % des individus pour lesquels ExerciseAngina = Y appartiennent à la classe HeartDisease = 1, contre approximativement 30 % lorsque ExerciseAngina = N.

Cette différence suggère que ExerciseAngina possède un pouvoir discriminant important pour la classification. Il s’agit toutefois d’une association observée dans l’échantillon et non d’une preuve de causalité.

7.4.3 Interprétation de ST_Slope

La variable ST_Slope présente une association particulièrement marquée avec la cible. Environ 90 % des individus appartenant aux modalités Flat et Down présentent une maladie cardiaque, contre seulement environ 10 % pour la modalité Up.

Ces résultats suggèrent que ST_Slope pourrait avoir un pouvoir discriminant important dans le modèle de classification. La modalité Up est davantage associée à l’absence de maladie cardiaque, tandis que Flat et Down sont fortement associées à sa présence.

Cette interprétation doit toutefois tenir compte des effectifs propres à chaque modalité, notamment pour Down, et ne constitue pas une preuve de causalité.

7.4.4 Interprétation de Sex

Dans le jeu de données étudié, la proportion d’individus présentant une maladie cardiaque est plus élevée chez les hommes (Sex = M) que chez les femmes (Sex = F). La modalité masculine semble donc davantage associée à la classe HeartDisease = 1.

La variable Sex pourrait ainsi fournir une information utile au modèle de classification. Cette conclusion doit néanmoins être nuancée, car les hommes sont nettement plus représentés que les femmes dans l’échantillon. Les résultats décrivent une association statistique dans les données et ne permettent pas d’établir une relation causale.

7.5 Distribution des variables numériques

Afficher le code
for column in numeric_features:
    ax = data[column].plot(
        kind="hist",
        bins=25,
        edgecolor="black",
        figsize=(7, 4),
    )

    ax.set_title(f"Distribution de {column}")
    ax.set_xlabel(column)
    ax.set_ylabel("Fréquence")

    plt.tight_layout()
    plt.show()

7.6 Détection graphique des valeurs atypiques

Afficher le code
for column in numeric_features:
    data.boxplot(
        column=column,
        vert=False,
        figsize=(7, 3),
    )

    plt.title(f"Boîte à moustaches de {column}")
    plt.xlabel(column)
    plt.tight_layout()
    plt.show()

WarningValeurs atypiques

Une valeur atypique ne doit pas être supprimée automatiquement. Elle peut être rare mais valide.

7.7 Variables numériques selon la cible

Afficher le code
for column in numeric_features:
    data.boxplot(
        column=column,
        by=TARGET,
        figsize=(7, 4),
    )

    plt.title(f"{column} selon HeartDisease")
    plt.suptitle("")
    plt.xlabel("HeartDisease")
    plt.ylabel(column)
    plt.tight_layout()
    plt.show()

7.8 Pairplot des variables numériques

Afficher le code
pairplot_columns = numeric_features + [TARGET]

sns.pairplot(
    data[pairplot_columns],
    hue=TARGET,
    corner=True,
    diag_kind="hist",
)

plt.show()

7.9 Matrice de corrélation des variables numériques

Afficher le code
correlation_matrix = (
    data[numeric_features]
    .corr(method="pearson")
)

display(correlation_matrix.round(2))

plt.figure(figsize=(9, 7))

sns.heatmap(
    correlation_matrix,
    annot=True,
    cmap="coolwarm",
    fmt=".2f",
    linewidths=0.5,
    center=0,
)

plt.title("Matrice de corrélation de Pearson")
plt.tight_layout()
plt.show()
Age RestingBP Cholesterol MaxHR Oldpeak
Age 1.00 0.25 -0.10 -0.38 0.26
RestingBP 0.25 1.00 0.10 -0.11 0.16
Cholesterol -0.10 0.10 1.00 0.24 0.05
MaxHR -0.38 -0.11 0.24 1.00 -0.16
Oldpeak 0.26 0.16 0.05 -0.16 1.00

Le coefficient de corrélation de Pearson entre deux variables \(X\) et \(Y\) est défini par :

\[ r_{XY} = \frac{ \sum_{i=1}^{n}(x_i-\bar{x})(y_i-\bar{y}) }{ \sqrt{\sum_{i=1}^{n}(x_i-\bar{x})^2} \sqrt{\sum_{i=1}^{n}(y_i-\bar{y})^2} }. \]

NoteInterprétation

Une corrélation mesure une association linéaire. Elle ne démontre pas une relation causale.

7.10 Corrélation des variables numériques avec la cible

Afficher le code
correlation_columns = numeric_features + [TARGET]

target_correlation_matrix = data[
    correlation_columns
].corr(method="pearson")

target_correlations = (
    target_correlation_matrix[TARGET]
    .drop(TARGET)
    .sort_values()
)

display(
    target_correlations
    .round(3)
    .to_frame("corrélation")
)

ax = target_correlations.plot(
    kind="barh",
    figsize=(8, 5),
)

ax.axvline(0, linewidth=1)
ax.set_title("Corrélations avec HeartDisease")
ax.set_xlabel("Coefficient de corrélation de Pearson")
ax.set_ylabel("Variable")

plt.tight_layout()
plt.show()
corrélation
MaxHR -0.400
Cholesterol -0.233
RestingBP 0.108
Age 0.282
Oldpeak 0.404

8 Préparation des données

8.1 Variables explicatives et variable cible

Afficher le code
X = data.drop(columns=[TARGET])
y = data[TARGET].astype(int)

print("Dimensions de X :", X.shape)
print("Dimensions de y :", y.shape)
Dimensions de X : (918, 11)
Dimensions de y : (918,)

8.2 Séparation apprentissage-test

Afficher le code
X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.20,
    random_state=RANDOM_STATE,
    stratify=y,
)

Le jeu de données est divisé en deux sous-ensembles disjoints :

\[ \mathcal{D} = \mathcal{D}_{\mathrm{train}} \cup \mathcal{D}_{\mathrm{test}}, \]

avec :

\[ \mathcal{D}_{\mathrm{train}} \cap \mathcal{D}_{\mathrm{test}} = \varnothing. \]

8.3 Vérification de la stratification

Afficher le code
class_distribution = pd.DataFrame(
    {
        "jeu_complet":
            y.value_counts(normalize=True).sort_index(),
        "apprentissage":
            y_train.value_counts(normalize=True).sort_index(),
        "test":
            y_test.value_counts(normalize=True).sort_index(),
    }
)

display(class_distribution.round(3))
jeu_complet apprentissage test
HeartDisease
0 0.447 0.447 0.446
1 0.553 0.553 0.554
NoteInterprétation

Les proportions sont très proches dans les trois colonnes.

8.4 Encodage des variables catégorielles

Afficher le code
encoder = OneHotEncoder(
    sparse_output=False,
    handle_unknown="ignore",
    drop="first",
)

X_train_categorical_encoded = encoder.fit_transform(
    X_train[categorical_features]
)

X_test_categorical_encoded = encoder.transform(
    X_test[categorical_features]
)

encoded_feature_names = encoder.get_feature_names_out(
    categorical_features
)

X_train_categorical_encoded = pd.DataFrame(
    X_train_categorical_encoded,
    columns=encoded_feature_names,
    index=X_train.index,
)

X_test_categorical_encoded = pd.DataFrame(
    X_test_categorical_encoded,
    columns=encoded_feature_names,
    index=X_test.index,
)

Pour une variable catégorielle \(C\) possédant les modalités \(c_1,\ldots,c_K\), l’indicatrice associée à la modalité \(c_k\) est :

\[ I_{ik} = \begin{cases} 1 & \text{si l’observation } i \text{ appartient à } c_k,\\ 0 & \text{sinon.} \end{cases} \]

Une modalité est supprimée et sert de référence.

8.5 Standardisation des variables numériques

Afficher le code
scaler = StandardScaler()

X_train_numeric_scaled = scaler.fit_transform(
    X_train[numeric_features]
)

X_test_numeric_scaled = scaler.transform(
    X_test[numeric_features]
)

X_train_numeric_scaled = pd.DataFrame(
    X_train_numeric_scaled,
    columns=numeric_features,
    index=X_train.index,
)

X_test_numeric_scaled = pd.DataFrame(
    X_test_numeric_scaled,
    columns=numeric_features,
    index=X_test.index,
)

Chaque variable numérique est standardisée selon :

\[ z_{ij} = \frac{x_{ij}-\widehat{\mu}_j} {\widehat{\sigma}_j}, \]

\(\widehat{\mu}_j\) et \(\widehat{\sigma}_j\) sont estimés uniquement sur le jeu d’apprentissage.

8.6 Assemblage des données préparées

Afficher le code
X_train_ready = pd.concat(
    [
        X_train_numeric_scaled,
        X_train_categorical_encoded,
    ],
    axis=1,
)

X_test_ready = pd.concat(
    [
        X_test_numeric_scaled,
        X_test_categorical_encoded,
    ],
    axis=1,
)

print("Train :", X_train_ready.shape)
print("Test :", X_test_ready.shape)

display(X_train_ready.head())
Train : (734, 15)
Test : (184, 15)
Age RestingBP Cholesterol MaxHR Oldpeak Sex_M ChestPainType_ATA ChestPainType_NAP ChestPainType_TA FastingBS_1 RestingECG_Normal RestingECG_ST ExerciseAngina_Y ST_Slope_Flat ST_Slope_Up
485 0.970012 0.339016 0.127137 -0.324520 0.317046 1.0 1.0 0.0 0.0 1.0 0.0 1.0 1.0 1.0 0.0
486 0.122028 -1.266031 0.099443 1.689837 -0.440356 1.0 1.0 0.0 0.0 1.0 0.0 1.0 0.0 0.0 1.0
117 0.546020 -0.159102 1.244113 -0.247045 0.601071 0.0 0.0 0.0 0.0 1.0 0.0 1.0 1.0 1.0 0.0
361 -0.725956 1.501291 -1.876035 -0.479470 -0.819056 1.0 0.0 0.0 0.0 0.0 1.0 0.0 1.0 1.0 0.0
296 -0.407962 0.671094 -1.876035 0.101594 -0.156330 1.0 0.0 0.0 0.0 1.0 1.0 0.0 1.0 1.0 0.0

9 Régression logistique non pénalisée

9.1 Formulation mathématique

Pour une observation \(x_i\), la probabilité de la classe positive est :

\[ p_i = P(Y_i=1\mid X_i=x_i) = \frac{1}{ 1+\exp\left( -\left(\beta_0+x_i^\top\beta\right) \right) }. \]

Le logarithme des odds est linéaire :

\[ \log\left( \frac{p_i}{1-p_i} \right) = \beta_0+x_i^\top\beta. \]

9.2 Estimation du modèle

Afficher le code
model_logistic = LogisticRegression(
    penalty=None,
    max_iter=5000,
)

model_logistic.fit(
    X_train_ready,
    y_train,
)
LogisticRegression(max_iter=5000, penalty=None)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

9.3 Coefficients estimés

Afficher le code
coefficient_table = pd.DataFrame(
    {
        "variable": X_train_ready.columns,
        "coefficient": model_logistic.coef_[0],
    }
)

coefficient_table["odds_ratio"] = np.exp(
    coefficient_table["coefficient"]
)

coefficient_table["coefficient_absolu"] = (
    coefficient_table["coefficient"].abs()
)

coefficient_table = coefficient_table.sort_values(
    "coefficient_absolu",
    ascending=False,
)

print(
    f"Intercept : {model_logistic.intercept_[0]:.4f}"
)

display(
    coefficient_table[
        [
            "variable",
            "coefficient",
            "odds_ratio",
        ]
    ].round(4)
)
Intercept : 0.0699
variable coefficient odds_ratio
7 ChestPainType_NAP -1.9389 0.1439
6 ChestPainType_ATA -1.7063 0.1815
14 ST_Slope_Up -1.5476 0.2128
8 ChestPainType_TA -1.5116 0.2205
5 Sex_M 1.2401 3.4559
9 FastingBS_1 1.0548 2.8714
13 ST_Slope_Flat 0.9814 2.6683
12 ExerciseAngina_Y 0.9036 2.4686
2 Cholesterol -0.5095 0.6008
11 RestingECG_ST -0.4249 0.6538
10 RestingECG_Normal -0.4081 0.6649
4 Oldpeak 0.2681 1.3075
3 MaxHR -0.1729 0.8412
1 RestingBP 0.0462 1.0473
0 Age 0.0354 1.0360

L’odds ratio associé à la variable \(X_j\) est :

\[ OR_j=e^{\beta_j}. \]

  • \(OR_j>1\) : augmentation des odds de la classe positive ;
  • \(OR_j<1\) : diminution des odds ;
  • \(OR_j=1\) : absence d’effet linéaire estimé.
NoteInterprétation des coefficients

Les coefficients de la régression logistique s’interprètent sur l’échelle du logarithme des odds. L’odds ratio associé à une variable est obtenu par :

\[ OR_j = e^{\beta_j}. \]

Un coefficient positif est associé à une augmentation des odds de HeartDisease = 1, tandis qu’un coefficient négatif est associé à une diminution des odds, toutes les autres variables étant maintenues constantes.

9.3.1 Intercept

L’intercept est égal à 0.0699, soit un odds ratio d’environ :

\[ e^{0.0699} \approx 1.07. \]

Il représente les odds de base lorsque toutes les variables numériques standardisées valent zéro et que toutes les variables catégorielles sont dans leur modalité de référence. Son interprétation clinique directe reste limitée, car ce profil de référence est principalement une construction mathématique.

9.3.2 Variables catégorielles associées à une augmentation des odds

La modalité Sex_M possède un coefficient de 1.2401, correspondant à un odds ratio de 3.4559. À caractéristiques égales, les odds prédites de maladie cardiaque sont donc environ 3,46 fois plus élevées pour les hommes que pour les femmes, qui constituent ici la modalité de référence.

La modalité FastingBS_1 présente un odds ratio de 2.8714. Les individus ayant FastingBS = 1 ont donc des odds prédites de maladie cardiaque environ 2,87 fois plus élevées que ceux ayant FastingBS = 0, toutes choses égales par ailleurs.

La modalité ST_Slope_Flat est associée à un odds ratio de 2.6683. Par rapport à la modalité de référence de ST_Slope, les odds prédites de maladie cardiaque sont multipliées par environ 2,67.

La modalité ExerciseAngina_Y possède un odds ratio de 2.4686. La présence d’une angine provoquée par l’exercice est donc associée à des odds prédites environ 2,47 fois plus élevées que l’absence d’angine à l’effort.

9.3.3 Variables catégorielles associées à une diminution des odds

Les modalités ChestPainType_NAP, ChestPainType_ATA et ChestPainType_TA possèdent des coefficients fortement négatifs, avec des odds ratios respectifs de 0.1439, 0.1815 et 0.2205.

Par rapport à la modalité de référence de ChestPainType, qui est probablement ASY, ces modalités sont associées à des odds nettement plus faibles de maladie cardiaque :

  • NAP : réduction d’environ 85,6 % des odds ;
  • ATA : réduction d’environ 81,9 % des odds ;
  • TA : réduction d’environ 77,9 % des odds.

La modalité ST_Slope_Up présente un odds ratio de 0.2128. Par rapport à la modalité de référence de ST_Slope, les odds prédites sont réduites d’environ 78,7 %.

Les modalités RestingECG_ST et RestingECG_Normal sont également associées à une diminution des odds par rapport à la modalité de référence, avec des odds ratios respectifs de 0.6538 et 0.6649.

9.3.4 Variables numériques

Les variables numériques ont été standardisées. Leur interprétation correspond donc à une augmentation d’un écart-type de la variable, et non d’une unité dans son échelle originale.

Oldpeak possède un coefficient positif de 0.2681, soit un odds ratio de 1.3075. Une augmentation d’un écart-type de Oldpeak est associée à une augmentation d’environ 30,8 % des odds prédites de maladie cardiaque.

Cholesterol présente un coefficient négatif de -0.5095, soit un odds ratio de 0.6008. Une augmentation d’un écart-type du cholestérol est associée à une diminution d’environ 39,9 % des odds prédites. Ce résultat peut sembler contre-intuitif et doit être interprété avec prudence, notamment en raison de la présence possible de valeurs nulles codant des données manquantes.

MaxHR possède un odds ratio de 0.8412, ce qui correspond à une diminution d’environ 15,9 % des odds pour une augmentation d’un écart-type.

RestingBP et Age ont des coefficients proches de zéro, avec des odds ratios respectifs de 1.0473 et 1.0360. Leur contribution linéaire au modèle semble donc relativement faible une fois les autres variables prises en compte.

9.3.5 Variables les plus influentes

En valeur absolue, les coefficients les plus importants sont associés à :

  • ChestPainType_NAP ;
  • ChestPainType_ATA ;
  • ST_Slope_Up ;
  • ChestPainType_TA ;
  • Sex_M ;
  • FastingBS_1.

Ces variables semblent avoir le plus fort effet sur le logit prédit du modèle.

9.3.6 Modalités de référence

Avec OneHotEncoder(drop="first"), une modalité de chaque variable catégorielle est omise et sert de référence. D’après les colonnes présentes, les références semblent être :

  • Sex : F ;
  • ChestPainType : ASY ;
  • FastingBS : 0 ;
  • RestingECG : probablement LVH ;
  • ExerciseAngina : N ;
  • ST_Slope : probablement Down.

Les coefficients des modalités encodées doivent toujours être interprétés par rapport à ces catégories de référence.

9.3.7 Prudence d’interprétation

Ces coefficients décrivent des associations conditionnelles dans le jeu de données. Ils ne prouvent pas une relation causale. Leur stabilité devra être évaluée avec une validation croisée et comparée aux modèles régularisés Ridge, Lasso et Elastic Net.

9.4 Évaluation prédictive

9.4.1 Prédictions sur le jeu de test

Afficher le code
y_pred = model_logistic.predict(
    X_test_ready
)

y_proba = model_logistic.predict_proba(
    X_test_ready
)[:, 1]

Avec un seuil de \(0{,}5\) :

\[ \widehat{Y}_i = \begin{cases} 1 & \text{si } \widehat{p}_i \geq 0{,}5,\\ 0 & \text{sinon.} \end{cases} \]

9.4.2 Tableau des métriques

Afficher le code
tn, fp, fn, tp = confusion_matrix(
    y_test,
    y_pred,
).ravel()

sensitivity = tp / (tp + fn)
specificity = tn / (tn + fp)

logistic_metrics = pd.Series(
    {
        "Accuracy":
            accuracy_score(y_test, y_pred),
        "Balanced accuracy":
            balanced_accuracy_score(y_test, y_pred),
        "Précision":
            precision_score(y_test, y_pred),
        "Sensibilité / Recall":
            sensitivity,
        "Spécificité":
            specificity,
        "F1-score":
            f1_score(y_test, y_pred),
        "ROC-AUC":
            roc_auc_score(y_test, y_proba),
        "Average Precision":
            average_precision_score(y_test, y_proba),
    },
    name="Régression logistique",
)

display(
    logistic_metrics
    .to_frame("score")
    .round(3)
)
score
Accuracy 0.886
Balanced accuracy 0.880
Précision 0.872
Sensibilité / Recall 0.931
Spécificité 0.829
F1-score 0.900
ROC-AUC 0.929
Average Precision 0.936

À partir des vrais positifs \(TP\), vrais négatifs \(TN\), faux positifs \(FP\) et faux négatifs \(FN\) :

\[ \operatorname{Accuracy} = \frac{TP+TN} {TP+TN+FP+FN}. \]

\[ \operatorname{Precision} = \frac{TP} {TP+FP}. \]

\[ \operatorname{Recall} = \frac{TP} {TP+FN}. \]

\[ \operatorname{Specificity} = \frac{TN} {TN+FP}. \]

\[ F_1 = 2 \frac{ \operatorname{Precision}\times\operatorname{Recall} }{ \operatorname{Precision}+\operatorname{Recall} }. \]

\[ \operatorname{BalancedAccuracy} = \frac{ \operatorname{Recall}+\operatorname{Specificity} }{2}. \]

ImportantInterprétation performance du premier modèle

La régression logistique non pénalisée obtient de bonnes performances sur le jeu de test. Elle classe correctement 88,6 % des observations et présente une balanced accuracy de 88,0 %, ce qui indique des performances relativement homogènes entre les deux classes.

Le modèle détecte 93,1 % des individus appartenant réellement à la classe HeartDisease = 1. D’après la matrice de confusion, il identifie environ 95 des 102 cas positifs et produit seulement 7 faux négatifs. Cette forte sensibilité est particulièrement intéressante dans une logique de dépistage.

La précision de 87,2 % signifie que la majorité des individus classés comme positifs le sont effectivement. Le modèle produit néanmoins davantage de faux positifs que de faux négatifs, ce qui traduit un choix implicite favorable à la détection des cas positifs.

La spécificité atteint 82,9 % : le modèle reconnaît correctement la majorité des observations négatives, mais ses performances sont légèrement meilleures pour la détection de la présence de maladie que pour la confirmation de son absence.

Le score F1 de 0,900 confirme un très bon compromis entre précision et rappel. La ROC-AUC de 0,929 et l’Average Precision de 0,936 montrent également une forte capacité de discrimination sur l’ensemble des seuils de décision.

Ces résultats sont prometteurs, mais ils proviennent d’une seule séparation apprentissage-test. Ils devront être confirmés par validation croisée, puis comparés aux régressions logistiques Ridge, Lasso et Elastic Net. Le jeu de test ne devra pas être utilisé à répétition pour choisir les hyperparamètres.

9.4.3 Rapport de classification

Afficher le code
print(
    classification_report(
        y_test,
        y_pred,
        target_names=[
            "Absence de maladie",
            "Présence de maladie",
        ],
        digits=3,
    )
)
                     precision    recall  f1-score   support

 Absence de maladie      0.907     0.829     0.866        82
Présence de maladie      0.872     0.931     0.900       102

           accuracy                          0.886       184
          macro avg      0.889     0.880     0.883       184
       weighted avg      0.887     0.886     0.885       184

9.4.4 Matrice de confusion

Afficher le code
ConfusionMatrixDisplay.from_predictions(
    y_test,
    y_pred,
    display_labels=[
        "Absence",
        "Présence",
    ],
)

plt.title("Matrice de confusion — Régression logistique")
plt.tight_layout()
plt.show()
Figure 2: Matrice de confusion de la régression logistique non pénalisée

La matrice de confusion est :

\[ \begin{pmatrix} TN & FP\\ FN & TP \end{pmatrix}. \]

ImportantInterprétation
  • vrais positifs : np.int64(95) ;
  • vrais négatifs : np.int64(68) ;
  • faux positifs : np.int64(14) ;
  • faux négatifs : np.int64(7).

Le modèle identifie correctement 95 individus présentant une maladie cardiaque et 68 individus n’en présentant pas. Il classe toutefois 14 individus sains comme malades, ce qui correspond aux faux positifs, et ne détecte pas 7 individus réellement malades, ce qui correspond aux faux négatifs.

Le nombre relativement faible de faux négatifs explique la sensibilité élevée du modèle, égale à environ 93,1 %.

Les faux négatifs sont particulièrement importants dans un contexte de dépistage.

9.4.5 Courbe ROC

Afficher le code
roc_auc = roc_auc_score(
    y_test,
    y_proba,
)

RocCurveDisplay.from_predictions(
    y_test,
    y_proba,
    name=f"Régression logistique — AUC = {roc_auc:.3f}",
)

plt.plot(
    [0, 1],
    [0, 1],
    linestyle="--",
    label="Modèle aléatoire",
)

plt.title("Courbe ROC")
plt.xlabel("Taux de faux positifs")
plt.ylabel("Taux de vrais positifs")
plt.legend()
plt.tight_layout()
plt.show()
Figure 3: Courbe ROC de la régression logistique non pénalisée

Pour un seuil \(t\) :

\[ TPR(t) = \frac{TP(t)} {TP(t)+FN(t)}, \]

et :

\[ FPR(t) = \frac{FP(t)} {FP(t)+TN(t)}. \]

L’aire sous la courbe ROC est :

\[ AUC = \int_0^1 TPR(FPR)\,d(FPR). \]

NoteInterprétation de l’AUC

Une AUC proche de \(0{,}5\) correspond à une capacité de discrimination proche du hasard. Une valeur proche de \(1\) indique une meilleure discrimination.

9.4.6 Courbe précision-rappel

Afficher le code
average_precision = average_precision_score(
    y_test,
    y_proba,
)

PrecisionRecallDisplay.from_predictions(
    y_test,
    y_proba,
    name=f"AP = {average_precision:.3f}",
)

plt.title("Courbe précision-rappel")
plt.xlabel("Rappel")
plt.ylabel("Précision")
plt.legend()
plt.tight_layout()
plt.show()
Figure 4: Courbe précision-rappel de la régression logistique

9.5 Limites du premier modèle

La régression logistique non pénalisée constitue une référence interprétable, mais elle peut présenter plusieurs limites :

  • sensibilité à la multicolinéarité ;
  • coefficients potentiellement instables ;
  • risque de surajustement ;
  • relation supposée linéaire entre les variables et le logit ;
  • seuil fixé arbitrairement à \(0{,}5\) ;
  • absence de validation croisée à cette étape ;
  • absence de validation externe sur une autre population.

Les modèles Ridge, Lasso et Elastic Net seront étudiés dans les sections suivantes.

10 Régression logistique Ridge

10.1 Principe de la régularisation L2

La régression logistique Ridge ajoute une pénalité quadratique à la fonction de perte :

\[ \widehat{\beta}^{\,\text{Ridge}} = \arg\min_{\beta} \left[ -\ell(\beta) + \lambda \sum_{j=1}^{p}\beta_j^2 \right]. \]

La pénalité réduit l’amplitude des coefficients sans les annuler généralement complètement. Elle peut améliorer la stabilité du modèle lorsque les variables sont corrélées ou lorsque le modèle non pénalisé présente une variance élevée.

Dans scikit-learn, la force de la régularisation est contrôlée par \(C\) :

\[ C \propto \frac{1}{\lambda}. \]

Ainsi :

  • une petite valeur de \(C\) correspond à une forte régularisation ;
  • une grande valeur de \(C\) correspond à une faible régularisation.

10.2 Validation croisée stratifiée

La valeur de \(C\) est choisie uniquement à partir du jeu d’apprentissage au moyen d’une validation croisée stratifiée à cinq plis. Chaque pli conserve approximativement la proportion des classes. :contentReferenceoaicite:1

Afficher le code
from sklearn.model_selection import (
    StratifiedKFold,
    GridSearchCV,
)

cv = StratifiedKFold(
    n_splits=5,
    shuffle=True,
    random_state=RANDOM_STATE,
)

10.3 Définition du modèle Ridge

Afficher le code
model_ridge_base = LogisticRegression(
    C=1.0,
    l1_ratio=0,
    solver="lbfgs",
    max_iter=5000,
)

l1_ratio=0 correspond à une pénalisation L2 pure. Le solveur lbfgs est compatible avec cette pénalisation. :contentReferenceoaicite:2

10.4 Recherche de la meilleure valeur de \(C\)

Afficher le code
ridge_parameter_grid = {
    "C": np.logspace(-4, 4, 17),
}

ridge_grid_search = GridSearchCV(
    estimator=model_ridge_base,
    param_grid=ridge_parameter_grid,
    scoring={
        "roc_auc": "roc_auc",
        "accuracy": "accuracy",
        "balanced_accuracy": "balanced_accuracy",
        "precision": "precision",
        "recall": "recall",
        "f1": "f1",
    },
    refit="roc_auc",
    cv=cv,
    n_jobs=-1,
    return_train_score=True,
)

ridge_grid_search.fit(
    X_train_ready,
    y_train,
)
GridSearchCV(cv=StratifiedKFold(n_splits=5, random_state=42, shuffle=True),
             estimator=LogisticRegression(l1_ratio=0, max_iter=5000), n_jobs=-1,
             param_grid={'C': array([1.00000000e-04, 3.16227766e-04, 1.00000000e-03, 3.16227766e-03,
       1.00000000e-02, 3.16227766e-02, 1.00000000e-01, 3.16227766e-01,
       1.00000000e+00, 3.16227766e+00, 1.00000000e+01, 3.16227766e+01,
       1.00000000e+02, 3.16227766e+02, 1.00000000e+03, 3.16227766e+03,
       1.00000000e+04])},
             refit='roc_auc', return_train_score=True,
             scoring={'accuracy': 'accuracy',
                      'balanced_accuracy': 'balanced_accuracy', 'f1': 'f1',
                      'precision': 'precision', 'recall': 'recall',
                      'roc_auc': 'roc_auc'})
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

Avec refit="roc_auc", la combinaison obtenant la meilleure ROC-AUC moyenne est réentraînée sur l’ensemble du jeu d’apprentissage et devient accessible dans best_estimator_. :contentReferenceoaicite:3

10.5 Meilleur hyperparamètre

Afficher le code
best_ridge_c = ridge_grid_search.best_params_["C"]
best_ridge_cv_auc = ridge_grid_search.best_score_

print(f"Meilleure valeur de C : {best_ridge_c:.6g}")
print(
    "ROC-AUC moyenne en validation croisée : "
    f"{best_ridge_cv_auc:.3f}"
)

model_ridge = ridge_grid_search.best_estimator_
Meilleure valeur de C : 3.16228
ROC-AUC moyenne en validation croisée : 0.925
NoteInterprétation

À compléter après exécution :

  • meilleure valeur de \(C\) ;
  • intensité correspondante de la régularisation ;
  • ROC-AUC moyenne obtenue en validation croisée ;
  • comparaison avec les résultats du modèle non pénalisé.

Une petite valeur de \(C\) indique que la validation croisée préfère une régularisation forte. Une valeur très élevée suggère que la pénalisation apporte peu d’amélioration.

10.6 Résultats complets de la validation croisée

Afficher le code
ridge_cv_results = pd.DataFrame(
    ridge_grid_search.cv_results_
)

ridge_cv_summary = (
    ridge_cv_results[
        [
            "param_C",
            "mean_train_roc_auc",
            "mean_test_roc_auc",
            "std_test_roc_auc",
            "mean_test_accuracy",
            "mean_test_balanced_accuracy",
            "mean_test_precision",
            "mean_test_recall",
            "mean_test_f1",
            "rank_test_roc_auc",
        ]
    ]
    .sort_values("param_C")
    .reset_index(drop=True)
)

display(ridge_cv_summary.round(4))
param_C mean_train_roc_auc mean_test_roc_auc std_test_roc_auc mean_test_accuracy mean_test_balanced_accuracy mean_test_precision mean_test_recall mean_test_f1 rank_test_roc_auc
0 0.0001 0.8830 0.8766 0.0406 0.5531 0.5000 0.5531 1.0000 0.7123 17
1 0.0003 0.8845 0.8777 0.0403 0.5545 0.5015 0.5539 1.0000 0.7129 16
2 0.0010 0.8881 0.8812 0.0401 0.7398 0.7176 0.7014 0.9287 0.7984 15
3 0.0032 0.8962 0.8893 0.0372 0.8039 0.7948 0.7918 0.8819 0.8333 14
4 0.0100 0.9074 0.9011 0.0360 0.8311 0.8258 0.8305 0.8770 0.8523 13
5 0.0316 0.9187 0.9130 0.0359 0.8502 0.8459 0.8516 0.8868 0.8683 12
6 0.1000 0.9271 0.9207 0.0350 0.8502 0.8459 0.8511 0.8868 0.8680 11
7 0.3162 0.9319 0.9237 0.0363 0.8516 0.8483 0.8583 0.8794 0.8681 10
8 1.0000 0.9334 0.9243 0.0370 0.8516 0.8483 0.8588 0.8794 0.8683 4
9 3.1623 0.9336 0.9246 0.0368 0.8529 0.8498 0.8604 0.8794 0.8693 1
10 10.0000 0.9336 0.9244 0.0370 0.8502 0.8470 0.8580 0.8769 0.8669 3
11 31.6228 0.9336 0.9245 0.0373 0.8488 0.8455 0.8560 0.8769 0.8659 2
12 100.0000 0.9336 0.9243 0.0374 0.8475 0.8442 0.8557 0.8745 0.8645 7
13 316.2278 0.9336 0.9242 0.0374 0.8475 0.8442 0.8557 0.8745 0.8645 9
14 1000.0000 0.9336 0.9243 0.0374 0.8475 0.8442 0.8557 0.8745 0.8645 5
15 3162.2777 0.9336 0.9243 0.0374 0.8475 0.8442 0.8557 0.8745 0.8645 5
16 10000.0000 0.9336 0.9242 0.0374 0.8475 0.8442 0.8557 0.8745 0.8645 8

10.7 Courbe de validation de \(C\)

Afficher le code
ridge_cv_plot = ridge_cv_summary.copy()

ridge_cv_plot["param_C"] = ridge_cv_plot[
    "param_C"
].astype(float)

plt.figure(figsize=(8, 5))

plt.semilogx(
    ridge_cv_plot["param_C"],
    ridge_cv_plot["mean_train_roc_auc"],
    marker="o",
    label="Apprentissage",
)

plt.semilogx(
    ridge_cv_plot["param_C"],
    ridge_cv_plot["mean_test_roc_auc"],
    marker="o",
    label="Validation",
)

plt.axvline(
    best_ridge_c,
    linestyle="--",
    label=f"Meilleur C = {best_ridge_c:.4g}",
)

plt.xlabel("C")
plt.ylabel("ROC-AUC moyenne")
plt.title("Choix de la force de régularisation Ridge")
plt.legend()
plt.tight_layout()
plt.show()
Figure 5: Performance du modèle Ridge selon la valeur de C
NoteInterprétation de la courbe

La ROC-AUC moyenne de validation augmente fortement lorsque \(C\) passe de valeurs très faibles à des valeurs intermédiaires. Pour les petites valeurs de \(C\), la régularisation est très forte et les performances sont nettement plus faibles, ce qui suggère un sous-ajustement.

La meilleure valeur sélectionnée est :

\[ C \approx 3{,}162. \]

À cette valeur, la ROC-AUC moyenne de validation atteint environ \(0{,}925\). Au-delà, la performance de validation se stabilise puis diminue très légèrement. Une valeur de \(C\) plus grande, correspondant à une régularisation plus faible, n’apporte donc pas d’amélioration notable.

Les courbes d’apprentissage et de validation restent proches sur l’ensemble de la zone optimale. L’écart est d’environ un point de ROC-AUC, ce qui ne suggère pas de surajustement important. La performance d’apprentissage est naturellement un peu supérieure à celle de validation, mais l’écart demeure faible et stable.

Le modèle préfère ainsi une régularisation L2 relativement faible à modérée. Une régularisation trop forte dégrade clairement les performances, tandis qu’une régularisation presque nulle n’améliore plus la capacité de généralisation.

Le choix de \(C \approx 3{,}162\) constitue donc un compromis pertinent entre stabilité des coefficients et performance prédictive.

10.8 Coefficients du modèle Ridge

Afficher le code
ridge_coefficient_table = pd.DataFrame(
    {
        "variable": X_train_ready.columns,
        "coefficient_ridge": model_ridge.coef_[0],
    }
)

ridge_coefficient_table["odds_ratio_ridge"] = np.exp(
    ridge_coefficient_table["coefficient_ridge"]
)

ridge_coefficient_table["coefficient_absolu"] = (
    ridge_coefficient_table["coefficient_ridge"].abs()
)

ridge_coefficient_table = ridge_coefficient_table.sort_values(
    "coefficient_absolu",
    ascending=False,
)

print(
    f"Intercept Ridge : {model_ridge.intercept_[0]:.4f}"
)

display(
    ridge_coefficient_table[
        [
            "variable",
            "coefficient_ridge",
            "odds_ratio_ridge",
        ]
    ].round(4)
)
Intercept Ridge : 0.0179
variable coefficient_ridge odds_ratio_ridge
7 ChestPainType_NAP -1.8432 0.1583
6 ChestPainType_ATA -1.6170 0.1985
14 ST_Slope_Up -1.4862 0.2262
8 ChestPainType_TA -1.3696 0.2542
5 Sex_M 1.1915 3.2919
9 FastingBS_1 1.0093 2.7438
13 ST_Slope_Flat 0.9864 2.6815
12 ExerciseAngina_Y 0.9051 2.4721
2 Cholesterol -0.5019 0.6054
11 RestingECG_ST -0.3833 0.6816
10 RestingECG_Normal -0.3825 0.6821
4 Oldpeak 0.2756 1.3173
3 MaxHR -0.1831 0.8327
1 RestingBP 0.0420 1.0429
0 Age 0.0351 1.0358

10.9 Comparaison avec les coefficients non pénalisés

Afficher le code
coefficient_comparison = (
    coefficient_table[
        [
            "variable",
            "coefficient",
        ]
    ]
    .rename(
        columns={
            "coefficient": "non_penalise",
        }
    )
    .merge(
        ridge_coefficient_table[
            [
                "variable",
                "coefficient_ridge",
            ]
        ],
        on="variable",
        how="inner",
    )
)

coefficient_comparison["reduction_absolue"] = (
    coefficient_comparison["non_penalise"].abs()
    -
    coefficient_comparison["coefficient_ridge"].abs()
)

coefficient_comparison["taux_reduction"] = (
    1
    -
    coefficient_comparison["coefficient_ridge"].abs()
    / coefficient_comparison["non_penalise"].abs()
)

display(
    coefficient_comparison
    .sort_values(
        "coefficient_ridge",
        key=lambda values: values.abs(),
        ascending=False,
    )
    .round(4)
)
variable non_penalise coefficient_ridge reduction_absolue taux_reduction
0 ChestPainType_NAP -1.9389 -1.8432 0.0956 0.0493
1 ChestPainType_ATA -1.7063 -1.6170 0.0894 0.0524
2 ST_Slope_Up -1.5476 -1.4862 0.0614 0.0397
3 ChestPainType_TA -1.5116 -1.3696 0.1420 0.0939
4 Sex_M 1.2401 1.1915 0.0486 0.0392
5 FastingBS_1 1.0548 1.0093 0.0455 0.0431
6 ST_Slope_Flat 0.9814 0.9864 -0.0049 -0.0050
7 ExerciseAngina_Y 0.9036 0.9051 -0.0014 -0.0016
8 Cholesterol -0.5095 -0.5019 0.0076 0.0149
9 RestingECG_ST -0.4249 -0.3833 0.0416 0.0979
10 RestingECG_Normal -0.4081 -0.3825 0.0256 0.0627
11 Oldpeak 0.2681 0.2756 -0.0075 -0.0278
12 MaxHR -0.1729 -0.1831 -0.0102 -0.0589
13 RestingBP 0.0462 0.0420 0.0042 0.0907
14 Age 0.0354 0.0351 0.0003 0.0074
Afficher le code
coefficient_plot = (
    coefficient_comparison
    .set_index("variable")[
        [
            "non_penalise",
            "coefficient_ridge",
        ]
    ]
    .sort_values("non_penalise")
)

coefficient_plot.plot(
    kind="barh",
    figsize=(10, 8),
)

plt.axvline(0, linewidth=1)
plt.xlabel("Coefficient estimé")
plt.ylabel("Variable")
plt.title(
    "Comparaison des coefficients : "
    "non pénalisé et Ridge"
)
plt.tight_layout()
plt.show()

NoteInterprétation des coefficients Ridge

La pénalisation Ridge modifie relativement peu les coefficients du modèle non pénalisé. Leurs signes restent tous identiques, ce qui montre que la direction des associations estimées est stable après régularisation.

Les réductions les plus importantes concernent principalement :

  • ChestPainType_TA, dont le coefficient diminue d’environ 9,4 % ;
  • RestingECG_ST, avec une diminution d’environ 9,8 % ;
  • RestingBP, avec une réduction relative d’environ 9,1 %, mais sur un coefficient initial déjà très faible ;
  • RestingECG_Normal, avec une diminution d’environ 6,3 % ;
  • ChestPainType_ATA et ChestPainType_NAP, avec des réductions proches de 5 %.

Malgré cette contraction, les variables les plus influentes du modèle non pénalisé restent également les plus importantes dans le modèle Ridge. C’est notamment le cas de ChestPainType_NAP, ChestPainType_ATA, ST_Slope_Up, ChestPainType_TA, Sex_M et FastingBS_1.

Certaines estimations augmentent très légèrement en valeur absolue, par exemple ST_Slope_Flat, ExerciseAngina_Y, Oldpeak et MaxHR. Cela ne contredit pas le principe de Ridge : la pénalisation agit simultanément sur l’ensemble des coefficients, et la réestimation conjointe peut conduire à de petites augmentations individuelles lorsque les variables sont corrélées.

Globalement, Ridge produit donc une version légèrement plus stable et plus régularisée du premier modèle, sans modifier fortement son interprétation. Aucun coefficient n’est annulé, ce qui est cohérent avec la pénalisation L2 : Ridge réduit l’amplitude des coefficients, mais ne réalise pas de sélection stricte de variables.

Les odds ratios restent ainsi proches de ceux du modèle non pénalisé. Les conclusions principales concernant les variables associées à une hausse ou à une baisse des odds de HeartDisease = 1 demeurent inchangées.

Ces coefficients représentent des associations conditionnelles dans l’échantillon et ne doivent pas être interprétés comme des effets causaux.

10.10 Prédictions sur le jeu de test

Afficher le code
y_pred_ridge = model_ridge.predict(
    X_test_ready
)

y_proba_ridge = model_ridge.predict_proba(
    X_test_ready
)[:, 1]

Ces prédictions utilisent le seuil de décision par défaut de \(0{,}5\). Le jeu de test n’a pas été utilisé pour choisir \(C\).

10.11 Indicateurs du modèle Ridge

Afficher le code
tn_ridge, fp_ridge, fn_ridge, tp_ridge = (
    confusion_matrix(
        y_test,
        y_pred_ridge,
    ).ravel()
)

sensitivity_ridge = (
    tp_ridge
    / (tp_ridge + fn_ridge)
)

specificity_ridge = (
    tn_ridge
    / (tn_ridge + fp_ridge)
)

ridge_metrics = pd.Series(
    {
        "Accuracy":
            accuracy_score(y_test, y_pred_ridge),
        "Balanced accuracy":
            balanced_accuracy_score(
                y_test,
                y_pred_ridge,
            ),
        "Précision":
            precision_score(
                y_test,
                y_pred_ridge,
            ),
        "Sensibilité / Recall":
            sensitivity_ridge,
        "Spécificité":
            specificity_ridge,
        "F1-score":
            f1_score(y_test, y_pred_ridge),
        "ROC-AUC":
            roc_auc_score(
                y_test,
                y_proba_ridge,
            ),
        "Average Precision":
            average_precision_score(
                y_test,
                y_proba_ridge,
            ),
    },
    name="Régression Ridge",
)

display(
    ridge_metrics
    .to_frame("score")
    .round(3)
)
score
Accuracy 0.886
Balanced accuracy 0.880
Précision 0.872
Sensibilité / Recall 0.931
Spécificité 0.829
F1-score 0.900
ROC-AUC 0.930
Average Precision 0.937

10.11.1 Rapport de classification

Afficher le code
print(
    classification_report(
        y_test,
        y_pred_ridge,
        target_names=[
            "Absence de maladie",
            "Présence de maladie",
        ],
        digits=3,
    )
)
                     precision    recall  f1-score   support

 Absence de maladie      0.907     0.829     0.866        82
Présence de maladie      0.872     0.931     0.900       102

           accuracy                          0.886       184
          macro avg      0.889     0.880     0.883       184
       weighted avg      0.887     0.886     0.885       184

10.11.2 Matrice de confusion

Afficher le code
ConfusionMatrixDisplay.from_predictions(
    y_test,
    y_pred_ridge,
    display_labels=[
        "Absence",
        "Présence",
    ],
)

plt.title(
    "Matrice de confusion — Régression Ridge"
)
plt.tight_layout()
plt.show()
Figure 6: Matrice de confusion de la régression logistique Ridge
ImportantInterprétation de la matrice de confusion
  • vrais positifs : np.int64(95) ;
  • vrais négatifs : np.int64(68) ;
  • faux positifs : np.int64(14) ;
  • faux négatifs : np.int64(7).

10.11.3 Courbe ROC

Afficher le code
ridge_roc_auc = roc_auc_score(
    y_test,
    y_proba_ridge,
)

RocCurveDisplay.from_predictions(
    y_test,
    y_proba_ridge,
    name=(
        "Ridge — "
        f"AUC = {ridge_roc_auc:.3f}"
    ),
)

plt.plot(
    [0, 1],
    [0, 1],
    linestyle="--",
    label="Modèle aléatoire",
)

plt.title("Courbe ROC — Ridge")
plt.xlabel("Taux de faux positifs")
plt.ylabel("Taux de vrais positifs")
plt.legend()
plt.tight_layout()
plt.show()
Figure 7: Courbe ROC de la régression logistique Ridge

10.11.4 Courbe précision-rappel

Afficher le code
ridge_average_precision = (
    average_precision_score(
        y_test,
        y_proba_ridge,
    )
)

PrecisionRecallDisplay.from_predictions(
    y_test,
    y_proba_ridge,
    name=(
        "Ridge — "
        f"AP = {ridge_average_precision:.3f}"
    ),
)

plt.title(
    "Courbe précision-rappel — Ridge"
)
plt.xlabel("Rappel")
plt.ylabel("Précision")
plt.legend()
plt.tight_layout()
plt.show()
Figure 8: Courbe précision-rappel de la régression logistique Ridge

10.11.5 Comparaison avec le premier modèle

Afficher le code
model_comparison = pd.concat(
    [
        logistic_metrics.rename(
            "Non pénalisée"
        ),
        ridge_metrics.rename(
            "Ridge"
        ),
    ],
    axis=1,
)

model_comparison["Différence Ridge"] = (
    model_comparison["Ridge"]
    -
    model_comparison["Non pénalisée"]
)

display(model_comparison.round(3))
Non pénalisée Ridge Différence Ridge
Accuracy 0.886 0.886 0.000
Balanced accuracy 0.880 0.880 0.000
Précision 0.872 0.872 0.000
Sensibilité / Recall 0.931 0.931 0.000
Spécificité 0.829 0.829 0.000
F1-score 0.900 0.900 0.000
ROC-AUC 0.929 0.930 0.001
Average Precision 0.936 0.937 0.001
ImportantComparaison entre la régression simple et Ridge

La régression logistique Ridge produit exactement la même matrice de confusion que le modèle non pénalisé au seuil de décision de \(0{,}5\) :

  • 68 vrais négatifs ;
  • 14 faux positifs ;
  • 7 faux négatifs ;
  • 95 vrais positifs.

Les principales métriques de classification sont donc identiques : l’accuracy est de 88,6 %, la sensibilité de 93,1 %, la spécificité de 82,9 % et le score F1 de 0,900.

Ridge améliore très légèrement la ROC-AUC, qui passe de 0,929 à 0,930, ainsi que l’Average Precision, qui passe de 0,936 à 0,937. Ces écarts de 0,001 sont toutefois très faibles et ne permettent pas de conclure à une amélioration prédictive substantielle sur ce jeu de test.

La régularisation L2 a principalement pour effet de réduire légèrement l’amplitude des coefficients et d’améliorer leur stabilité, sans modifier les prédictions binaires obtenues au seuil de 0,5.

À ce stade, Ridge ne semble donc pas supérieur au modèle non pénalisé en termes de classification. Son intérêt réside surtout dans la régularisation des coefficients et dans une meilleure robustesse potentielle face à la multicolinéarité ou à de nouveaux échantillons.

11 Régression logistique Lasso

11.1 Principe de la régularisation L1

La régression logistique Lasso ajoute à la fonction de perte une pénalité proportionnelle à la somme des valeurs absolues des coefficients :

\[ \widehat{\beta}^{\,\text{Lasso}} = \arg\min_{\beta} \left[ -\ell(\beta) + \lambda \sum_{j=1}^{p} |\beta_j| \right]. \]

Contrairement à Ridge, la pénalisation Lasso peut ramener certains coefficients exactement à zéro. Elle réalise ainsi une forme de sélection automatique des variables.

Dans scikit-learn, la relation entre \(C\) et la force de régularisation est inverse :

\[ C \propto \frac{1}{\lambda}. \]

Ainsi :

  • un petit \(C\) correspond à une forte régularisation ;
  • un grand \(C\) correspond à une faible régularisation ;
  • une forte régularisation augmente généralement le nombre de coefficients nuls.

11.2 Modèle Lasso de base

Afficher le code
model_lasso_base = LogisticRegression(
    C=1.0,
    l1_ratio=1,
    solver="saga",
    max_iter=10000,
    random_state=RANDOM_STATE,
)

l1_ratio=1 définit une pénalisation L1 pure. Le solveur saga est utilisé car il prend en charge la pénalisation L1. :contentReferenceoaicite:1

11.3 Recherche de la meilleure valeur de \(C\)

La valeur de \(C\) est choisie par validation croisée stratifiée uniquement sur le jeu d’apprentissage. Le jeu de test reste réservé à l’évaluation finale. GridSearchCV teste les paramètres dans les plis de validation puis réentraîne automatiquement le meilleur estimateur lorsque refit est activé. :contentReferenceoaicite:2

Afficher le code
lasso_parameter_grid = {
    "C": np.logspace(-4, 4, 17),
}

lasso_grid_search = GridSearchCV(
    estimator=model_lasso_base,
    param_grid=lasso_parameter_grid,
    scoring={
        "roc_auc": "roc_auc",
        "accuracy": "accuracy",
        "balanced_accuracy": "balanced_accuracy",
        "precision": "precision",
        "recall": "recall",
        "f1": "f1",
    },
    refit="roc_auc",
    cv=cv,
    n_jobs=-1,
    return_train_score=True,
)

lasso_grid_search.fit(
    X_train_ready,
    y_train,
)
GridSearchCV(cv=StratifiedKFold(n_splits=5, random_state=42, shuffle=True),
             estimator=LogisticRegression(l1_ratio=1, max_iter=10000,
                                          random_state=42, solver='saga'),
             n_jobs=-1,
             param_grid={'C': array([1.00000000e-04, 3.16227766e-04, 1.00000000e-03, 3.16227766e-03,
       1.00000000e-02, 3.16227766e-02, 1.00000000e-01, 3.16227766e-01,
       1.00000000e+00, 3.16227766e+00, 1.00000000e+01, 3.16227766e+01,
       1.00000000e+02, 3.16227766e+02, 1.00000000e+03, 3.16227766e+03,
       1.00000000e+04])},
             refit='roc_auc', return_train_score=True,
             scoring={'accuracy': 'accuracy',
                      'balanced_accuracy': 'balanced_accuracy', 'f1': 'f1',
                      'precision': 'precision', 'recall': 'recall',
                      'roc_auc': 'roc_auc'})
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

11.4 Meilleur hyperparamètre

Afficher le code
best_lasso_c = lasso_grid_search.best_params_["C"]
best_lasso_cv_auc = lasso_grid_search.best_score_

model_lasso = lasso_grid_search.best_estimator_

print(f"Meilleure valeur de C : {best_lasso_c:.6g}")
print(
    "ROC-AUC moyenne en validation croisée : "
    f"{best_lasso_cv_auc:.3f}"
)
Meilleure valeur de C : 1
ROC-AUC moyenne en validation croisée : 0.925
NoteInterprétation

La meilleure valeur sélectionnée par validation croisée est :

\[ C = 1. \]

Cette valeur correspond à une régularisation L1 d’intensité intermédiaire : le modèle n’est ni fortement contraint, comme ce serait le cas avec une très petite valeur de \(C\), ni presque non pénalisé, comme avec une valeur très élevée.

La ROC-AUC moyenne obtenue en validation croisée est de :

\[ 0{,}925. \]

Cette performance est très proche de celle observée pour le modèle Ridge, dont la ROC-AUC moyenne était également voisine de \(0{,}925\). À ce stade, les deux régularisations présentent donc une capacité de discrimination comparable sur les données d’apprentissage.

Le choix de \(C=1\) suggère que la validation croisée accepte une régularisation L1 modérée. Le modèle Lasso peut ainsi réduire certains coefficients, voire en annuler certains, sans dégrader fortement la performance prédictive.

Le nombre exact de variables éliminées doit être déterminé à partir des coefficients estimés. Si plusieurs coefficients sont nuls, Lasso fournit un modèle plus parcimonieux que Ridge. Si aucun coefficient n’est nul, cela signifie que cette intensité de régularisation ne suffit pas à effectuer une sélection stricte des variables.

11.5 Résultats de la validation croisée

Afficher le code
lasso_cv_results = pd.DataFrame(
    lasso_grid_search.cv_results_
)

lasso_cv_summary = (
    lasso_cv_results[
        [
            "param_C",
            "mean_train_roc_auc",
            "mean_test_roc_auc",
            "std_test_roc_auc",
            "mean_test_accuracy",
            "mean_test_balanced_accuracy",
            "mean_test_precision",
            "mean_test_recall",
            "mean_test_f1",
            "rank_test_roc_auc",
        ]
    ]
    .sort_values("param_C")
    .reset_index(drop=True)
)

display(lasso_cv_summary.round(4))
param_C mean_train_roc_auc mean_test_roc_auc std_test_roc_auc mean_test_accuracy mean_test_balanced_accuracy mean_test_precision mean_test_recall mean_test_f1 rank_test_roc_auc
0 0.0001 0.5000 0.5000 0.0000 0.5531 0.5000 0.5531 1.0000 0.7123 14
1 0.0003 0.5000 0.5000 0.0000 0.5531 0.5000 0.5531 1.0000 0.7123 14
2 0.0010 0.5000 0.5000 0.0000 0.5531 0.5000 0.5531 1.0000 0.7123 14
3 0.0032 0.5000 0.5000 0.0000 0.5531 0.5000 0.5531 1.0000 0.7123 14
4 0.0100 0.7863 0.7764 0.0475 0.5845 0.5382 0.5738 0.9729 0.7215 13
5 0.0316 0.8892 0.8837 0.0320 0.8366 0.8313 0.8355 0.8819 0.8571 12
6 0.1000 0.9213 0.9170 0.0332 0.8434 0.8398 0.8496 0.8745 0.8613 11
7 0.3162 0.9312 0.9241 0.0335 0.8570 0.8532 0.8594 0.8893 0.8735 10
8 1.0000 0.9333 0.9252 0.0361 0.8502 0.8470 0.8584 0.8769 0.8669 1
9 3.1623 0.9335 0.9245 0.0367 0.8529 0.8501 0.8622 0.8769 0.8690 2
10 10.0000 0.9336 0.9245 0.0371 0.8515 0.8486 0.8602 0.8769 0.8680 3
11 31.6228 0.9336 0.9244 0.0374 0.8475 0.8442 0.8557 0.8745 0.8645 4
12 100.0000 0.9336 0.9242 0.0374 0.8475 0.8442 0.8557 0.8745 0.8645 9
13 316.2278 0.9336 0.9243 0.0374 0.8475 0.8442 0.8557 0.8745 0.8645 7
14 1000.0000 0.9336 0.9243 0.0374 0.8475 0.8442 0.8557 0.8745 0.8645 7
15 3162.2777 0.9336 0.9243 0.0374 0.8475 0.8442 0.8557 0.8745 0.8645 5
16 10000.0000 0.9336 0.9243 0.0374 0.8475 0.8442 0.8557 0.8745 0.8645 5

11.6 Courbe de validation de \(C\)

Afficher le code
lasso_cv_plot = lasso_cv_summary.copy()

lasso_cv_plot["param_C"] = (
    lasso_cv_plot["param_C"]
    .astype(float)
)

plt.figure(figsize=(8, 5))

plt.semilogx(
    lasso_cv_plot["param_C"],
    lasso_cv_plot["mean_train_roc_auc"],
    marker="o",
    label="Apprentissage",
)

plt.semilogx(
    lasso_cv_plot["param_C"],
    lasso_cv_plot["mean_test_roc_auc"],
    marker="o",
    label="Validation",
)

plt.axvline(
    best_lasso_c,
    linestyle="--",
    label=f"Meilleur C = {best_lasso_c:.4g}",
)

plt.xlabel("C")
plt.ylabel("ROC-AUC moyenne")
plt.title("Choix de la force de régularisation Lasso")
plt.legend()
plt.tight_layout()
plt.show()
Figure 9: Performance du modèle Lasso selon la valeur de C
NoteInterprétation de la courbe

La ROC-AUC de validation est maximale pour :

\[ C = 1. \]

Pour les très petites valeurs de \(C\), approximativement inférieures à \(10^{-2}\), la régularisation L1 est très forte. Les performances restent proches de \(0{,}5\), ce qui indique que le modèle est alors presque incapable de discriminer les deux classes. Cette zone correspond à un sous-ajustement important : les coefficients sont trop fortement contraints, voire annulés.

Lorsque \(C\) augmente entre environ \(10^{-2}\) et \(10^{-1}\), la ROC-AUC progresse rapidement. Le modèle récupère progressivement suffisamment de variables et de flexibilité pour améliorer fortement sa capacité de classification.

À partir de \(C \approx 0{,}1\), la courbe entre dans un plateau autour de \(0{,}92\) à \(0{,}925\). Le maximum obtenu à \(C=1\) est donc peu marqué : plusieurs valeurs proches produisent des performances de validation presque identiques.

Les courbes d’apprentissage et de validation sont très proches sur toute la zone optimale. Leur faible écart ne met pas en évidence de surajustement important. Pour les grandes valeurs de \(C\), les performances restent stables au lieu de se dégrader, ce qui indique que la diminution de la régularisation n’entraîne pas ici de surapprentissage visible.

Le choix de \(C=1\) constitue ainsi un compromis raisonnable entre performance prédictive et parcimonie. Toutefois, comme la zone optimale forme un plateau, le choix exact de \(C\) est peu sensible : des valeurs voisines, comme \(0{,}3\), \(1\) ou \(3\), donneraient probablement des performances très similaires.

11.6.1 Nombre de coefficients sélectionnés selon \(C\)

Le graphique suivant montre comment la régularisation L1 élimine progressivement les variables.

Afficher le code
lasso_path_rows = []

for c_value in np.logspace(-4, 4, 17):
    current_model = LogisticRegression(
        C=c_value,
        l1_ratio=1,
        solver="saga",
        max_iter=10000,
        random_state=RANDOM_STATE,
    )

    current_model.fit(
        X_train_ready,
        y_train,
    )

    current_coefficients = current_model.coef_[0]

    lasso_path_rows.append(
        {
            "C": c_value,
            "nombre_coefficients_non_nuls":
                np.count_nonzero(
                    np.abs(current_coefficients) > 1e-8
                ),
            "nombre_coefficients_nuls":
                np.sum(
                    np.abs(current_coefficients) <= 1e-8
                ),
        }
    )

lasso_sparsity = pd.DataFrame(
    lasso_path_rows
)

display(lasso_sparsity)
C nombre_coefficients_non_nuls nombre_coefficients_nuls
0 0.000100 0 15
1 0.000316 0 15
2 0.001000 0 15
3 0.003162 0 15
4 0.010000 3 12
5 0.031623 5 10
6 0.100000 10 5
7 0.316228 13 2
8 1.000000 15 0
9 3.162278 15 0
10 10.000000 15 0
11 31.622777 15 0
12 100.000000 15 0
13 316.227766 15 0
14 1000.000000 15 0
15 3162.277660 15 0
16 10000.000000 15 0
Afficher le code
plt.figure(figsize=(8, 5))

plt.semilogx(
    lasso_sparsity["C"],
    lasso_sparsity["nombre_coefficients_non_nuls"],
    marker="o",
)

plt.axvline(
    best_lasso_c,
    linestyle="--",
    label=f"Meilleur C = {best_lasso_c:.4g}",
)

plt.xlabel("C")
plt.ylabel("Nombre de coefficients non nuls")
plt.title("Parcimonie du modèle Lasso")
plt.legend()
plt.tight_layout()
plt.show()
Figure 10: Nombre de coefficients conservés selon la valeur de C
NoteInterprétation de la parcimonie

Lorsque \(C\) est faible, la régularisation est forte et davantage de coefficients sont ramenés à zéro. Lorsque \(C\) augmente, le modèle conserve progressivement plus de variables.

Le meilleur compromis doit conserver une bonne performance de validation tout en utilisant un nombre raisonnable de variables.

11.6.2 Coefficients du meilleur modèle Lasso

Afficher le code
lasso_coefficient_table = pd.DataFrame(
    {
        "variable": X_train_ready.columns,
        "coefficient_lasso": model_lasso.coef_[0],
    }
)

lasso_coefficient_table["odds_ratio_lasso"] = np.exp(
    lasso_coefficient_table["coefficient_lasso"]
)

lasso_coefficient_table["coefficient_absolu"] = (
    lasso_coefficient_table["coefficient_lasso"]
    .abs()
)

lasso_coefficient_table["selectionnee"] = (
    lasso_coefficient_table["coefficient_absolu"]
    > 1e-8
)

lasso_coefficient_table = (
    lasso_coefficient_table
    .sort_values(
        "coefficient_absolu",
        ascending=False,
    )
)

print(
    f"Intercept Lasso : "
    f"{model_lasso.intercept_[0]:.4f}"
)

display(
    lasso_coefficient_table[
        [
            "variable",
            "coefficient_lasso",
            "odds_ratio_lasso",
            "selectionnee",
        ]
    ].round(4)
)
Intercept Lasso : 0.0085
variable coefficient_lasso odds_ratio_lasso selectionnee
7 ChestPainType_NAP -1.7525 0.1733 True
14 ST_Slope_Up -1.5602 0.2101 True
6 ChestPainType_ATA -1.5533 0.2115 True
8 ChestPainType_TA -1.2048 0.2998 True
5 Sex_M 1.1206 3.0667 True
9 FastingBS_1 0.9211 2.5120 True
12 ExerciseAngina_Y 0.8818 2.4151 True
13 ST_Slope_Flat 0.8655 2.3762 True
2 Cholesterol -0.4754 0.6216 True
4 Oldpeak 0.2644 1.3026 True
10 RestingECG_Normal -0.2258 0.7979 True
3 MaxHR -0.1667 0.8464 True
11 RestingECG_ST -0.1506 0.8602 True
0 Age 0.0432 1.0441 True
1 RestingBP 0.0259 1.0262 True

11.6.3 Variables conservées et éliminées

Afficher le code
selected_lasso_features = (
    lasso_coefficient_table.loc[
        lasso_coefficient_table["selectionnee"],
        "variable",
    ]
    .tolist()
)

removed_lasso_features = (
    lasso_coefficient_table.loc[
        ~lasso_coefficient_table["selectionnee"],
        "variable",
    ]
    .tolist()
)

print(
    "Nombre de variables conservées :",
    len(selected_lasso_features),
)

print(
    "Nombre de variables éliminées :",
    len(removed_lasso_features),
)

print("\nVariables conservées :")
print(selected_lasso_features)

print("\nVariables éliminées :")
print(removed_lasso_features)
Nombre de variables conservées : 15
Nombre de variables éliminées : 0

Variables conservées :
['ChestPainType_NAP', 'ST_Slope_Up', 'ChestPainType_ATA', 'ChestPainType_TA', 'Sex_M', 'FastingBS_1', 'ExerciseAngina_Y', 'ST_Slope_Flat', 'Cholesterol', 'Oldpeak', 'RestingECG_Normal', 'MaxHR', 'RestingECG_ST', 'Age', 'RestingBP']

Variables éliminées :
[]
ImportantInterprétation de la sélection

Toutes les variables sont conservées.

11.6.4 Comparaison des coefficients des trois modèles

Afficher le code
coefficient_comparison_three = (
    coefficient_table[
        [
            "variable",
            "coefficient",
        ]
    ]
    .rename(
        columns={
            "coefficient": "non_penalise",
        }
    )
    .merge(
        ridge_coefficient_table[
            [
                "variable",
                "coefficient_ridge",
            ]
        ],
        on="variable",
        how="inner",
    )
    .merge(
        lasso_coefficient_table[
            [
                "variable",
                "coefficient_lasso",
            ]
        ],
        on="variable",
        how="inner",
    )
)

display(
    coefficient_comparison_three
    .sort_values(
        "coefficient_lasso",
        key=lambda values: values.abs(),
        ascending=False,
    )
    .round(4)
)
variable non_penalise coefficient_ridge coefficient_lasso
0 ChestPainType_NAP -1.9389 -1.8432 -1.7525
2 ST_Slope_Up -1.5476 -1.4862 -1.5602
1 ChestPainType_ATA -1.7063 -1.6170 -1.5533
3 ChestPainType_TA -1.5116 -1.3696 -1.2048
4 Sex_M 1.2401 1.1915 1.1206
5 FastingBS_1 1.0548 1.0093 0.9211
7 ExerciseAngina_Y 0.9036 0.9051 0.8818
6 ST_Slope_Flat 0.9814 0.9864 0.8655
8 Cholesterol -0.5095 -0.5019 -0.4754
11 Oldpeak 0.2681 0.2756 0.2644
10 RestingECG_Normal -0.4081 -0.3825 -0.2258
12 MaxHR -0.1729 -0.1831 -0.1667
9 RestingECG_ST -0.4249 -0.3833 -0.1506
14 Age 0.0354 0.0351 0.0432
13 RestingBP 0.0462 0.0420 0.0259
Afficher le code
coefficient_comparison_three_plot = (
    coefficient_comparison_three
    .set_index("variable")
    .sort_values("non_penalise")
)

coefficient_comparison_three_plot.plot(
    kind="barh",
    figsize=(11, 9),
)

plt.axvline(0, linewidth=1)
plt.xlabel("Coefficient estimé")
plt.ylabel("Variable")
plt.title(
    "Comparaison des coefficients : "
    "non pénalisé, Ridge et Lasso"
)
plt.tight_layout()
plt.show()

NoteInterprétation comparative

Les trois modèles conservent globalement les mêmes signes de coefficients, ce qui indique une bonne stabilité de la direction des associations estimées.

La régression Ridge réduit légèrement l’amplitude de la plupart des coefficients, sans en annuler. La régression Lasso produit généralement une contraction plus forte, en particulier pour ChestPainType_TA, RestingECG_Normal, RestingECG_ST, FastingBS_1 et ST_Slope_Flat.

Les variables conservant les coefficients les plus importants dans les trois modèles sont :

  • ChestPainType_NAP ;
  • ST_Slope_Up ;
  • ChestPainType_ATA ;
  • ChestPainType_TA ;
  • Sex_M ;
  • FastingBS_1.

Ces variables semblent donc robustement associées à la prédiction de HeartDisease = 1.

Le modèle Lasso ne ramène ici aucun coefficient exactement à zéro. Il ne réalise donc pas de sélection stricte de variables avec la valeur optimale \(C=1\), mais il produit tout de même un modèle plus parcimonieux en réduisant davantage certains effets.

Quelques coefficients augmentent légèrement avec Lasso, notamment ST_Slope_Up et Age. Cela est possible car tous les coefficients sont réestimés conjointement : la pénalisation ne réduit pas nécessairement chaque coefficient de façon monotone.

Globalement, les trois modèles conduisent à des conclusions similaires. Ridge apporte surtout de la stabilité, tandis que Lasso simplifie davantage les coefficients sans modifier profondément l’interprétation du modèle.

11.6.5 Prédictions sur le jeu de test

Afficher le code
y_pred_lasso = model_lasso.predict(
    X_test_ready
)

y_proba_lasso = model_lasso.predict_proba(
    X_test_ready
)[:, 1]

Ces prédictions utilisent le seuil par défaut de \(0{,}5\).

11.6.6 Indicateurs du modèle Lasso

Afficher le code
tn_lasso, fp_lasso, fn_lasso, tp_lasso = (
    confusion_matrix(
        y_test,
        y_pred_lasso,
    ).ravel()
)

sensitivity_lasso = (
    tp_lasso
    / (tp_lasso + fn_lasso)
)

specificity_lasso = (
    tn_lasso
    / (tn_lasso + fp_lasso)
)

lasso_metrics = pd.Series(
    {
        "Accuracy":
            accuracy_score(
                y_test,
                y_pred_lasso,
            ),
        "Balanced accuracy":
            balanced_accuracy_score(
                y_test,
                y_pred_lasso,
            ),
        "Précision":
            precision_score(
                y_test,
                y_pred_lasso,
            ),
        "Sensibilité / Recall":
            sensitivity_lasso,
        "Spécificité":
            specificity_lasso,
        "F1-score":
            f1_score(
                y_test,
                y_pred_lasso,
            ),
        "ROC-AUC":
            roc_auc_score(
                y_test,
                y_proba_lasso,
            ),
        "Average Precision":
            average_precision_score(
                y_test,
                y_proba_lasso,
            ),
    },
    name="Régression Lasso",
)

display(
    lasso_metrics
    .to_frame("score")
    .round(3)
)
score
Accuracy 0.886
Balanced accuracy 0.880
Précision 0.872
Sensibilité / Recall 0.931
Spécificité 0.829
F1-score 0.900
ROC-AUC 0.929
Average Precision 0.936

11.6.7 Rapport de classification

Afficher le code
print(
    classification_report(
        y_test,
        y_pred_lasso,
        target_names=[
            "Absence de maladie",
            "Présence de maladie",
        ],
        digits=3,
    )
)
                     precision    recall  f1-score   support

 Absence de maladie      0.907     0.829     0.866        82
Présence de maladie      0.872     0.931     0.900       102

           accuracy                          0.886       184
          macro avg      0.889     0.880     0.883       184
       weighted avg      0.887     0.886     0.885       184

11.6.8 Matrice de confusion

Afficher le code
ConfusionMatrixDisplay.from_predictions(
    y_test,
    y_pred_lasso,
    display_labels=[
        "Absence",
        "Présence",
    ],
)

plt.title(
    "Matrice de confusion — Régression Lasso"
)
plt.tight_layout()
plt.show()
Figure 11: Matrice de confusion de la régression logistique Lasso
ImportantInterprétation de la matrice de confusion
  • vrais positifs : np.int64(95) ;
  • vrais négatifs : np.int64(68) ;
  • faux positifs : np.int64(14) ;
  • faux négatifs : np.int64(7).

La régression logistique Lasso obtient les mêmes performances de classification que le modèle non pénalisé au seuil de décision de \(0{,}5\).

L’accuracy est de 88,6 %, la sensibilité de 93,1 %, la spécificité de 82,9 % et le score F1 de 0,900. Le modèle conserve donc une forte capacité à détecter les observations appartenant à la classe HeartDisease = 1, tout en produisant relativement peu de faux négatifs.

La ROC-AUC de 0,929 et l’Average Precision de 0,936 indiquent également une bonne capacité de discrimination sur l’ensemble des seuils.

Cependant, avec la valeur optimale \(C=1\), Lasso ne ramène aucun coefficient exactement à zéro : les 15 variables transformées sont conservées. Il ne réalise donc pas de sélection stricte de variables dans cette configuration.

Lasso réduit néanmoins l’amplitude de plusieurs coefficients, mais cette contraction ne modifie pas les prédictions finales sur le jeu de test. Son apport principal est donc ici la régularisation des coefficients, et non une amélioration des performances ou une simplification effective du modèle.

11.6.9 Courbe ROC

Afficher le code
lasso_roc_auc = roc_auc_score(
    y_test,
    y_proba_lasso,
)

RocCurveDisplay.from_predictions(
    y_test,
    y_proba_lasso,
    name=(
        "Lasso — "
        f"AUC = {lasso_roc_auc:.3f}"
    ),
)

plt.plot(
    [0, 1],
    [0, 1],
    linestyle="--",
    label="Modèle aléatoire",
)

plt.title("Courbe ROC — Lasso")
plt.xlabel("Taux de faux positifs")
plt.ylabel("Taux de vrais positifs")
plt.legend()
plt.tight_layout()
plt.show()
Figure 12: Courbe ROC de la régression logistique Lasso

11.6.10 Courbe précision-rappel

Afficher le code
lasso_average_precision = (
    average_precision_score(
        y_test,
        y_proba_lasso,
    )
)

PrecisionRecallDisplay.from_predictions(
    y_test,
    y_proba_lasso,
    name=(
        "Lasso — "
        f"AP = {lasso_average_precision:.3f}"
    ),
)

plt.title(
    "Courbe précision-rappel — Lasso"
)
plt.xlabel("Rappel")
plt.ylabel("Précision")
plt.legend()
plt.tight_layout()
plt.show()
Figure 13: Courbe précision-rappel de la régression logistique Lasso

11.6.11 Comparaison des trois modèles

Afficher le code
model_comparison = pd.concat(
    [
        logistic_metrics.rename(
            "Non pénalisée"
        ),
        ridge_metrics.rename(
            "Ridge"
        ),
        lasso_metrics.rename(
            "Lasso"
        ),
    ],
    axis=1,
)

model_comparison[
    "Différence Lasso vs non pénalisée"
] = (
    model_comparison["Lasso"]
    -
    model_comparison["Non pénalisée"]
)

model_comparison[
    "Différence Lasso vs Ridge"
] = (
    model_comparison["Lasso"]
    -
    model_comparison["Ridge"]
)

display(model_comparison.round(3))
Non pénalisée Ridge Lasso Différence Lasso vs non pénalisée Différence Lasso vs Ridge
Accuracy 0.886 0.886 0.886 0.000 0.000
Balanced accuracy 0.880 0.880 0.880 0.000 0.000
Précision 0.872 0.872 0.872 0.000 0.000
Sensibilité / Recall 0.931 0.931 0.931 0.000 0.000
Spécificité 0.829 0.829 0.829 0.000 0.000
F1-score 0.900 0.900 0.900 0.000 0.000
ROC-AUC 0.929 0.930 0.929 0.000 -0.001
Average Precision 0.936 0.937 0.936 -0.001 -0.001
ImportantConclusion du modèle Lasso

La régression logistique Lasso conserve exactement les mêmes performances de classification que les modèles non pénalisé et Ridge au seuil de décision de \(0{,}5\).

L’accuracy reste égale à 88,6 %, la sensibilité à 93,1 %, la spécificité à 82,9 % et le score F1 à 0,900. Le nombre de faux négatifs n’augmente donc pas : le modèle continue de détecter correctement 95 cas positifs sur 102 et n’en manque que 7.

La ROC-AUC de Lasso atteint 0,929, soit une valeur identique à celle du modèle non pénalisé et inférieure de seulement 0,001 à celle de Ridge. L’Average Precision suit la même tendance, avec un écart également négligeable de 0,001 par rapport à Ridge.

Avec la valeur optimale \(C=1\), aucun coefficient n’est toutefois ramené exactement à zéro. Les 15 variables transformées sont conservées. Lasso ne produit donc pas ici un modèle plus simple au sens d’une sélection stricte de variables.

En conclusion, Lasso régularise légèrement les coefficients sans dégrader les performances, mais il n’apporte ni gain prédictif net ni amélioration notable de l’interprétabilité par rapport aux deux modèles précédents. Ridge conserve un très léger avantage sur les métriques calculées à partir des probabilités, mais cet écart reste trop faible pour être considéré comme substantiel.

12 Régression logistique Elastic Net

12.1 Principe de la régularisation Elastic Net

Elastic Net combine une pénalisation L1, utilisée par Lasso, et une pénalisation L2, utilisée par Ridge :

\[ \widehat{\beta}^{\,\text{Elastic Net}} = \arg\min_{\beta} \left[ -\ell(\beta) + \lambda \left( \alpha\sum_{j=1}^{p}|\beta_j| + \frac{1-\alpha}{2} \sum_{j=1}^{p}\beta_j^2 \right) \right]. \]

Le paramètre \(\alpha\), représenté par l1_ratio dans scikit-learn, contrôle le mélange entre les deux pénalités :

  • l1_ratio = 0 : pénalisation Ridge pure ;
  • l1_ratio = 1 : pénalisation Lasso pure ;
  • 0 < l1_ratio < 1 : combinaison Elastic Net.

Le paramètre \(C\) est inversement lié à la force globale de régularisation :

\[ C \propto \frac{1}{\lambda}. \]

Elastic Net peut donc à la fois réduire l’amplitude des coefficients et, lorsque la composante L1 est suffisamment forte, ramener certains coefficients à zéro.

12.2 Définition du modèle Elastic Net

Afficher le code
model_elastic_base = LogisticRegression(
    C=1.0,
    l1_ratio=0.5,
    solver="saga",
    max_iter=10000,
    random_state=RANDOM_STATE,
)

12.3 Recherche conjointe de \(C\) et de l1_ratio

Deux hyperparamètres doivent être optimisés :

  • \(C\), qui contrôle la force globale de régularisation ;
  • l1_ratio, qui contrôle le compromis entre L1 et L2.
Afficher le code
elastic_parameter_grid = {
    "C": np.logspace(-3, 3, 13),
    "l1_ratio": [
        0.1,
        0.25,
        0.5,
        0.75,
        0.9,
    ],
}
Afficher le code
elastic_grid_search = GridSearchCV(
    estimator=model_elastic_base,
    param_grid=elastic_parameter_grid,
    scoring={
        "roc_auc": "roc_auc",
        "accuracy": "accuracy",
        "balanced_accuracy": "balanced_accuracy",
        "precision": "precision",
        "recall": "recall",
        "f1": "f1",
    },
    refit="roc_auc",
    cv=cv,
    n_jobs=-1,
    return_train_score=True,
)

elastic_grid_search.fit(
    X_train_ready,
    y_train,
)
GridSearchCV(cv=StratifiedKFold(n_splits=5, random_state=42, shuffle=True),
             estimator=LogisticRegression(l1_ratio=0.5, max_iter=10000,
                                          random_state=42, solver='saga'),
             n_jobs=-1,
             param_grid={'C': array([1.00000000e-03, 3.16227766e-03, 1.00000000e-02, 3.16227766e-02,
       1.00000000e-01, 3.16227766e-01, 1.00000000e+00, 3.16227766e+00,
       1.00000000e+01, 3.16227766e+01, 1.00000000e+02, 3.16227766e+02,
       1.00000000e+03]),
                         'l1_ratio': [0.1, 0.25, 0.5, 0.75, 0.9]},
             refit='roc_auc', return_train_score=True,
             scoring={'accuracy': 'accuracy',
                      'balanced_accuracy': 'balanced_accuracy', 'f1': 'f1',
                      'precision': 'precision', 'recall': 'recall',
                      'roc_auc': 'roc_auc'})
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

Avec refit="roc_auc", la meilleure combinaison est choisie selon la ROC-AUC moyenne de validation, puis réentraînée sur l’ensemble du jeu d’apprentissage. :contentReferenceoaicite:1

12.4 Meilleurs hyperparamètres

Afficher le code
best_elastic_c = (
    elastic_grid_search.best_params_["C"]
)

best_elastic_l1_ratio = (
    elastic_grid_search.best_params_["l1_ratio"]
)

best_elastic_cv_auc = (
    elastic_grid_search.best_score_
)

model_elastic = (
    elastic_grid_search.best_estimator_
)

print(
    f"Meilleure valeur de C : "
    f"{best_elastic_c:.6g}"
)

print(
    f"Meilleur l1_ratio : "
    f"{best_elastic_l1_ratio:.2f}"
)

print(
    "ROC-AUC moyenne en validation croisée : "
    f"{best_elastic_cv_auc:.3f}"
)
Meilleure valeur de C : 1
Meilleur l1_ratio : 0.90
ROC-AUC moyenne en validation croisée : 0.925
NoteInterprétation

La meilleure combinaison d’hyperparamètres retenue par validation croisée est :

\[ C = 1 \qquad \text{et} \qquad \texttt{l1\_ratio} = 0{,}90. \]

La valeur \(C=1\) correspond à une intensité de régularisation intermédiaire. Le modèle n’est donc ni fortement contraint, ni presque non pénalisé.

La valeur l1_ratio = 0.90 indique que la pénalisation Elastic Net est très majoritairement composée de L1 et seulement faiblement de L2. Le modèle se comporte donc davantage comme une régression Lasso que comme une régression Ridge.

La composante L1 représente environ 90 % du mélange de pénalisation, tandis que la composante L2 en représente environ 10 %. Elastic Net privilégie ainsi la réduction forte, voire l’annulation, de certains coefficients, tout en conservant une petite composante Ridge susceptible d’améliorer leur stabilité.

La ROC-AUC moyenne obtenue en validation croisée est de :

\[ 0{,}925. \]

Cette performance est pratiquement identique à celles obtenues avec Ridge et Lasso. Elastic Net n’apporte donc pas, à ce stade, d’amélioration nette de la capacité de discrimination.

Le choix d’un l1_ratio élevé suggère que les données favorisent davantage le comportement parcimonieux de Lasso que la contraction uniforme des coefficients propre à Ridge. Il faudra toutefois vérifier si le modèle ramène réellement certains coefficients à zéro. Si aucun coefficient n’est annulé, Elastic Net se comportera surtout comme une version légèrement stabilisée de Lasso, sans sélection stricte de variables.

12.5 Résultats complets de la validation croisée

Afficher le code
elastic_cv_results = pd.DataFrame(
    elastic_grid_search.cv_results_
)

elastic_cv_summary = (
    elastic_cv_results[
        [
            "param_C",
            "param_l1_ratio",
            "mean_train_roc_auc",
            "mean_test_roc_auc",
            "std_test_roc_auc",
            "mean_test_accuracy",
            "mean_test_balanced_accuracy",
            "mean_test_precision",
            "mean_test_recall",
            "mean_test_f1",
            "rank_test_roc_auc",
        ]
    ]
    .sort_values(
        [
            "rank_test_roc_auc",
            "param_C",
            "param_l1_ratio",
        ]
    )
    .reset_index(drop=True)
)

display(
    elastic_cv_summary.head(20).round(4)
)
param_C param_l1_ratio mean_train_roc_auc mean_test_roc_auc std_test_roc_auc mean_test_accuracy mean_test_balanced_accuracy mean_test_precision mean_test_recall mean_test_f1 rank_test_roc_auc
0 1.0000 0.90 0.9334 0.9253 0.0359 0.8502 0.8470 0.8584 0.8769 0.8669 1
1 1.0000 0.75 0.9334 0.9252 0.0359 0.8515 0.8483 0.8587 0.8794 0.8683 2
2 1.0000 0.50 0.9334 0.9251 0.0360 0.8529 0.8498 0.8608 0.8794 0.8694 3
3 3.1623 0.50 0.9335 0.9247 0.0369 0.8543 0.8513 0.8625 0.8794 0.8704 4
4 3.1623 0.10 0.9336 0.9247 0.0369 0.8529 0.8498 0.8604 0.8794 0.8693 5
5 1.0000 0.10 0.9334 0.9246 0.0368 0.8529 0.8498 0.8608 0.8794 0.8694 6
6 1.0000 0.25 0.9334 0.9246 0.0366 0.8529 0.8498 0.8608 0.8794 0.8694 7
7 3.1623 0.25 0.9336 0.9246 0.0371 0.8529 0.8498 0.8604 0.8794 0.8693 8
8 10.0000 0.75 0.9336 0.9245 0.0370 0.8502 0.8470 0.8582 0.8769 0.8670 9
9 31.6228 0.10 0.9336 0.9245 0.0373 0.8488 0.8455 0.8560 0.8769 0.8659 10
10 0.3162 0.90 0.9313 0.9245 0.0337 0.8570 0.8532 0.8594 0.8893 0.8735 11
11 3.1623 0.90 0.9335 0.9245 0.0370 0.8529 0.8501 0.8622 0.8769 0.8690 12
12 10.0000 0.90 0.9336 0.9245 0.0370 0.8502 0.8470 0.8582 0.8769 0.8670 13
13 10.0000 0.50 0.9336 0.9245 0.0370 0.8502 0.8470 0.8582 0.8769 0.8670 14
14 31.6228 0.25 0.9336 0.9244 0.0373 0.8488 0.8455 0.8560 0.8769 0.8659 15
15 31.6228 0.50 0.9336 0.9244 0.0373 0.8475 0.8442 0.8557 0.8745 0.8645 15
16 3.1623 0.75 0.9335 0.9244 0.0372 0.8529 0.8501 0.8622 0.8769 0.8690 17
17 31.6228 0.75 0.9336 0.9244 0.0374 0.8475 0.8442 0.8557 0.8745 0.8645 18
18 31.6228 0.90 0.9336 0.9244 0.0373 0.8475 0.8442 0.8557 0.8745 0.8645 19
19 10.0000 0.25 0.9336 0.9243 0.0370 0.8515 0.8486 0.8602 0.8769 0.8680 20

12.6 Tableau des ROC-AUC selon les hyperparamètres

Afficher le code
elastic_auc_pivot = (
    elastic_cv_results
    .pivot_table(
        index="param_l1_ratio",
        columns="param_C",
        values="mean_test_roc_auc",
    )
)

display(elastic_auc_pivot.round(4))
param_C 0.001000 0.003162 0.010000 0.031623 0.100000 0.316228 1.000000 3.162278 10.000000 31.622777 100.000000 316.227766 1000.000000
param_l1_ratio
0.10 0.785 0.8750 0.8963 0.9115 0.9209 0.9236 0.9246 0.9247 0.9243 0.9245 0.9243 0.9243 0.9243
0.25 0.500 0.8249 0.8836 0.9087 0.9207 0.9233 0.9246 0.9246 0.9243 0.9244 0.9243 0.9243 0.9243
0.50 0.500 0.5000 0.8688 0.8986 0.9201 0.9239 0.9251 0.9247 0.9245 0.9244 0.9242 0.9242 0.9243
0.75 0.500 0.5000 0.8330 0.8898 0.9197 0.9243 0.9252 0.9244 0.9245 0.9244 0.9242 0.9242 0.9243
0.90 0.500 0.5000 0.7938 0.8878 0.9185 0.9245 0.9253 0.9245 0.9245 0.9244 0.9242 0.9243 0.9243

12.7 Carte de validation

Afficher le code
plt.figure(figsize=(12, 5))

sns.heatmap(
    elastic_auc_pivot,
    annot=True,
    fmt=".3f",
    cmap="viridis",
)

plt.title(
    "ROC-AUC moyenne — Elastic Net"
)
plt.xlabel("C")
plt.ylabel("l1_ratio")
plt.tight_layout()
plt.show()
Figure 14: ROC-AUC de validation selon C et l1_ratio
NoteInterprétation de la carte

La meilleure combinaison d’hyperparamètres est obtenue pour :

\[ C = 1 \qquad \text{et} \qquad \texttt{l1\_ratio} = 0{,}90, \]

avec une ROC-AUC moyenne de validation égale à environ \(0{,}9253\).

Le l1_ratio élevé indique que la solution optimale est plus proche de Lasso que de Ridge. La composante L1 domine donc nettement la pénalisation, tandis que la composante L2 joue seulement un rôle complémentaire de stabilisation.

La carte montre toutefois que la zone optimale n’est pas limitée à une seule combinaison précise. Pour des valeurs de \(C\) comprises approximativement entre \(0{,}3\) et \(3\), plusieurs valeurs de l1_ratio produisent des ROC-AUC très proches, généralement comprises entre \(0{,}924\) et \(0{,}925\).

Le maximum est donc situé sur un plateau plutôt que sur un pic isolé. Cela signifie que les performances du modèle sont peu sensibles à de petites variations de \(C\) ou de l1_ratio dans cette zone.

Pour les très petites valeurs de \(C\), la régularisation est trop forte, surtout lorsque l1_ratio est élevé. La ROC-AUC peut alors tomber jusqu’à \(0{,}5\), ce qui correspond à un modèle sans capacité de discrimination et traduit un sous-ajustement important.

À l’inverse, lorsque \(C\) devient grand, les performances se stabilisent autour de \(0{,}924\). Une régularisation très faible n’apporte donc pas d’amélioration supplémentaire.

En conclusion, la combinaison \(C=1\) et l1_ratio=0.90 constitue un choix raisonnable, mais elle n’est pas nettement supérieure aux autres combinaisons proches. Elastic Net présente ici une zone de performance stable, avec un comportement proche de Lasso.

12.8 Courbes de validation selon \(C\)

Afficher le code
plt.figure(figsize=(9, 6))

for ratio in sorted(
    elastic_cv_results["param_l1_ratio"].unique()
):
    ratio_results = elastic_cv_results[
        elastic_cv_results[
            "param_l1_ratio"
        ] == ratio
    ].copy()

    ratio_results = ratio_results.sort_values(
        "param_C"
    )

    plt.semilogx(
        ratio_results["param_C"].astype(float),
        ratio_results["mean_test_roc_auc"],
        marker="o",
        label=f"l1_ratio = {ratio}",
    )

plt.axvline(
    best_elastic_c,
    linestyle="--",
    label=(
        f"Meilleur C = "
        f"{best_elastic_c:.4g}"
    ),
)

plt.xlabel("C")
plt.ylabel("ROC-AUC moyenne de validation")
plt.title(
    "Choix des hyperparamètres Elastic Net"
)
plt.legend()
plt.tight_layout()
plt.show()
Figure 15: Performance Elastic Net selon C et l1_ratio

12.9 Coefficients du meilleur modèle

Afficher le code
elastic_coefficient_table = pd.DataFrame(
    {
        "variable":
            X_train_ready.columns,
        "coefficient_elastic":
            model_elastic.coef_[0],
    }
)

elastic_coefficient_table[
    "odds_ratio_elastic"
] = np.exp(
    elastic_coefficient_table[
        "coefficient_elastic"
    ]
)

elastic_coefficient_table[
    "coefficient_absolu"
] = (
    elastic_coefficient_table[
        "coefficient_elastic"
    ].abs()
)

elastic_coefficient_table[
    "selectionnee"
] = (
    elastic_coefficient_table[
        "coefficient_absolu"
    ] > 1e-8
)

elastic_coefficient_table = (
    elastic_coefficient_table
    .sort_values(
        "coefficient_absolu",
        ascending=False,
    )
)

print(
    "Intercept Elastic Net : "
    f"{model_elastic.intercept_[0]:.4f}"
)

display(
    elastic_coefficient_table[
        [
            "variable",
            "coefficient_elastic",
            "odds_ratio_elastic",
            "selectionnee",
        ]
    ].round(4)
)
Intercept Elastic Net : -0.0033
variable coefficient_elastic odds_ratio_elastic selectionnee
7 ChestPainType_NAP -1.7442 0.1748 True
6 ChestPainType_ATA -1.5427 0.2138 True
14 ST_Slope_Up -1.5362 0.2152 True
8 ChestPainType_TA -1.1969 0.3021 True
5 Sex_M 1.1192 3.0623 True
9 FastingBS_1 0.9225 2.5155 True
12 ExerciseAngina_Y 0.8842 2.4211 True
13 ST_Slope_Flat 0.8836 2.4196 True
2 Cholesterol -0.4768 0.6207 True
4 Oldpeak 0.2675 1.3067 True
10 RestingECG_Normal -0.2391 0.7873 True
3 MaxHR -0.1707 0.8431 True
11 RestingECG_ST -0.1697 0.8439 True
0 Age 0.0422 1.0431 True
1 RestingBP 0.0268 1.0271 True

12.10 Variables conservées et éliminées

Afficher le code
selected_elastic_features = (
    elastic_coefficient_table.loc[
        elastic_coefficient_table[
            "selectionnee"
        ],
        "variable",
    ]
    .tolist()
)

removed_elastic_features = (
    elastic_coefficient_table.loc[
        ~elastic_coefficient_table[
            "selectionnee"
        ],
        "variable",
    ]
    .tolist()
)

print(
    "Nombre de variables conservées :",
    len(selected_elastic_features),
)

print(
    "Nombre de variables éliminées :",
    len(removed_elastic_features),
)

print("\nVariables conservées :")
print(selected_elastic_features)

print("\nVariables éliminées :")
print(removed_elastic_features)
Nombre de variables conservées : 15
Nombre de variables éliminées : 0

Variables conservées :
['ChestPainType_NAP', 'ChestPainType_ATA', 'ST_Slope_Up', 'ChestPainType_TA', 'Sex_M', 'FastingBS_1', 'ExerciseAngina_Y', 'ST_Slope_Flat', 'Cholesterol', 'Oldpeak', 'RestingECG_Normal', 'MaxHR', 'RestingECG_ST', 'Age', 'RestingBP']

Variables éliminées :
[]
NoteInterprétation des coefficients

Le modèle Elastic Net conserve les 15 variables transformées : aucun coefficient n’est ramené exactement à zéro. Malgré un l1_ratio élevé de \(0{,}90\), la régularisation choisie n’est donc pas assez forte pour produire une sélection stricte de variables.

Les coefficients les plus importants en valeur absolue concernent :

  • ChestPainType_NAP ;
  • ChestPainType_ATA ;
  • ST_Slope_Up ;
  • ChestPainType_TA ;
  • Sex_M ;
  • FastingBS_1.

Ces variables restent les principales contributrices à la prédiction, comme dans les modèles non pénalisé, Ridge et Lasso.

Les signes des coefficients sont également stables par rapport aux modèles précédents. Les modalités Sex_M, FastingBS_1, ExerciseAngina_Y, ST_Slope_Flat, Oldpeak et Age restent associées à une augmentation des odds de HeartDisease = 1. À l’inverse, les modalités de ChestPainType, ST_Slope_Up, Cholesterol, MaxHR et certaines modalités de RestingECG restent associées à une diminution des odds.

Les valeurs des coefficients Elastic Net sont très proches de celles obtenues avec Lasso. Cela est cohérent avec le meilleur l1_ratio = 0.90, qui donne une pénalisation dominée par la composante L1. La faible composante L2 apporte néanmoins une légère stabilisation des coefficients.

Par exemple :

  • Sex_M possède un odds ratio d’environ 3,06 ;
  • FastingBS_1 multiplie les odds par environ 2,52 ;
  • ExerciseAngina_Y et ST_Slope_Flat les multiplient par environ 2,42 ;
  • ChestPainType_NAP présente un odds ratio d’environ 0,17 par rapport à la catégorie de référence.

Globalement, Elastic Net modifie surtout l’amplitude des coefficients sans changer leur hiérarchie ni leur interprétation générale. Il se comporte ici comme une version légèrement stabilisée de Lasso, sans véritable sélection de variables.

Ces coefficients décrivent des associations conditionnelles dans l’échantillon et ne doivent pas être interprétés comme des effets causaux.

12.11 Comparaison des coefficients des quatre modèles

Afficher le code
coefficient_comparison_four = (
    coefficient_table[
        [
            "variable",
            "coefficient",
        ]
    ]
    .rename(
        columns={
            "coefficient": "non_penalise",
        }
    )
    .merge(
        ridge_coefficient_table[
            [
                "variable",
                "coefficient_ridge",
            ]
        ],
        on="variable",
        how="inner",
    )
    .merge(
        lasso_coefficient_table[
            [
                "variable",
                "coefficient_lasso",
            ]
        ],
        on="variable",
        how="inner",
    )
    .merge(
        elastic_coefficient_table[
            [
                "variable",
                "coefficient_elastic",
            ]
        ],
        on="variable",
        how="inner",
    )
)

display(
    coefficient_comparison_four
    .sort_values(
        "coefficient_elastic",
        key=lambda values: values.abs(),
        ascending=False,
    )
    .round(4)
)
variable non_penalise coefficient_ridge coefficient_lasso coefficient_elastic
0 ChestPainType_NAP -1.9389 -1.8432 -1.7525 -1.7442
1 ChestPainType_ATA -1.7063 -1.6170 -1.5533 -1.5427
2 ST_Slope_Up -1.5476 -1.4862 -1.5602 -1.5362
3 ChestPainType_TA -1.5116 -1.3696 -1.2048 -1.1969
4 Sex_M 1.2401 1.1915 1.1206 1.1192
5 FastingBS_1 1.0548 1.0093 0.9211 0.9225
7 ExerciseAngina_Y 0.9036 0.9051 0.8818 0.8842
6 ST_Slope_Flat 0.9814 0.9864 0.8655 0.8836
8 Cholesterol -0.5095 -0.5019 -0.4754 -0.4768
11 Oldpeak 0.2681 0.2756 0.2644 0.2675
10 RestingECG_Normal -0.4081 -0.3825 -0.2258 -0.2391
12 MaxHR -0.1729 -0.1831 -0.1667 -0.1707
9 RestingECG_ST -0.4249 -0.3833 -0.1506 -0.1697
14 Age 0.0354 0.0351 0.0432 0.0422
13 RestingBP 0.0462 0.0420 0.0259 0.0268
Afficher le code
coefficient_comparison_four_plot = (
    coefficient_comparison_four
    .set_index("variable")
    .sort_values("non_penalise")
)

coefficient_comparison_four_plot.plot(
    kind="barh",
    figsize=(12, 10),
)

plt.axvline(0, linewidth=1)
plt.xlabel("Coefficient estimé")
plt.ylabel("Variable")
plt.title(
    "Comparaison des coefficients des quatre modèles"
)
plt.tight_layout()
plt.show()

NoteInterprétation comparative

Les coefficients d’Elastic Net sont très proches de ceux obtenus avec Lasso. Cela est cohérent avec la valeur optimale l1_ratio = 0.90, qui donne une pénalisation très majoritairement L1 et seulement faiblement L2.

Elastic Net ressemble donc davantage au modèle Lasso qu’au modèle Ridge.

Les variables les plus fortement réduites par rapport au modèle non pénalisé sont notamment :

  • ChestPainType_TA ;
  • RestingECG_ST ;
  • RestingECG_Normal ;
  • FastingBS_1 ;
  • ST_Slope_Flat ;
  • Sex_M.

Par exemple, le coefficient de ChestPainType_TA passe de -1.5116 dans le modèle non pénalisé à -1.1969 avec Elastic Net. De même, RestingECG_ST passe de -0.4249 à -0.1697.

Aucun coefficient n’est toutefois ramené exactement à zéro. Les 15 variables transformées sont conservées. Dans cette configuration, Elastic Net ne réalise donc pas de sélection stricte de variables.

La hiérarchie générale des variables reste stable entre les quatre modèles. Les coefficients les plus importants en valeur absolue concernent toujours :

  • ChestPainType_NAP ;
  • ChestPainType_ATA ;
  • ST_Slope_Up ;
  • ChestPainType_TA ;
  • Sex_M ;
  • FastingBS_1.

Les signes restent également identiques, ce qui montre que la direction des associations estimées est robuste à la régularisation.

Globalement, Elastic Net combine une contraction des coefficients proche de Lasso avec une légère stabilisation apportée par la composante Ridge. Il ne modifie cependant ni la structure générale du modèle, ni la sélection des variables.

12.12 Prédictions sur le jeu de test

Afficher le code
y_pred_elastic = model_elastic.predict(
    X_test_ready
)

y_proba_elastic = (
    model_elastic.predict_proba(
        X_test_ready
    )[:, 1]
)

Les classes sont calculées avec le seuil par défaut de \(0{,}5\).

12.13 Indicateurs du modèle Elastic Net

Afficher le code
tn_elastic, fp_elastic, fn_elastic, tp_elastic = (
    confusion_matrix(
        y_test,
        y_pred_elastic,
    ).ravel()
)

sensitivity_elastic = (
    tp_elastic
    / (tp_elastic + fn_elastic)
)

specificity_elastic = (
    tn_elastic
    / (tn_elastic + fp_elastic)
)

elastic_metrics = pd.Series(
    {
        "Accuracy":
            accuracy_score(
                y_test,
                y_pred_elastic,
            ),
        "Balanced accuracy":
            balanced_accuracy_score(
                y_test,
                y_pred_elastic,
            ),
        "Précision":
            precision_score(
                y_test,
                y_pred_elastic,
            ),
        "Sensibilité / Recall":
            sensitivity_elastic,
        "Spécificité":
            specificity_elastic,
        "F1-score":
            f1_score(
                y_test,
                y_pred_elastic,
            ),
        "ROC-AUC":
            roc_auc_score(
                y_test,
                y_proba_elastic,
            ),
        "Average Precision":
            average_precision_score(
                y_test,
                y_proba_elastic,
            ),
    },
    name="Elastic Net",
)

display(
    elastic_metrics
    .to_frame("score")
    .round(3)
)
score
Accuracy 0.886
Balanced accuracy 0.880
Précision 0.872
Sensibilité / Recall 0.931
Spécificité 0.829
F1-score 0.900
ROC-AUC 0.929
Average Precision 0.936

12.14 Rapport de classification

Afficher le code
print(
    classification_report(
        y_test,
        y_pred_elastic,
        target_names=[
            "Absence de maladie",
            "Présence de maladie",
        ],
        digits=3,
    )
)
                     precision    recall  f1-score   support

 Absence de maladie      0.907     0.829     0.866        82
Présence de maladie      0.872     0.931     0.900       102

           accuracy                          0.886       184
          macro avg      0.889     0.880     0.883       184
       weighted avg      0.887     0.886     0.885       184

12.15 Matrice de confusion

Afficher le code
ConfusionMatrixDisplay.from_predictions(
    y_test,
    y_pred_elastic,
    display_labels=[
        "Absence",
        "Présence",
    ],
)

plt.title(
    "Matrice de confusion — Elastic Net"
)
plt.tight_layout()
plt.show()
Figure 16: Matrice de confusion de la régression logistique Elastic Net
ImportantInterprétation de la matrice de confusion

La matrice de confusion du modèle Elastic Net contient :

  • vrais positifs : 95 ;
  • vrais négatifs : 68 ;
  • faux positifs : 14 ;
  • faux négatifs : 7.

Le modèle détecte donc correctement 95 des 102 observations appartenant réellement à la classe HeartDisease = 1. Il ne manque que 7 cas positifs, ce qui explique la sensibilité élevée de 93,1 %.

Parmi les 82 observations réellement négatives, 68 sont correctement classées, tandis que 14 sont prédites à tort comme positives. La spécificité est ainsi de 82,9 %.

Elastic Net privilégie légèrement la détection des cas positifs : il produit moins de faux négatifs que de faux positifs. Ce comportement peut être pertinent dans une logique de dépistage, où manquer un cas positif peut être plus problématique qu’orienter un cas négatif vers un examen complémentaire.

La matrice de confusion est exactement identique à celles obtenues avec la régression non pénalisée, Ridge et Lasso. Au seuil de décision de \(0{,}5\), les quatre modèles classent donc les mêmes observations.

Elastic Net n’apporte ainsi aucune amélioration des prédictions binaires sur le jeu de test. Son intérêt éventuel réside plutôt dans la régularisation et la stabilité des coefficients que dans une amélioration de la classification.

12.16 Courbe ROC

Afficher le code
elastic_roc_auc = roc_auc_score(
    y_test,
    y_proba_elastic,
)

RocCurveDisplay.from_predictions(
    y_test,
    y_proba_elastic,
    name=(
        "Elastic Net — "
        f"AUC = {elastic_roc_auc:.3f}"
    ),
)

plt.plot(
    [0, 1],
    [0, 1],
    linestyle="--",
    label="Modèle aléatoire",
)

plt.title("Courbe ROC — Elastic Net")
plt.xlabel("Taux de faux positifs")
plt.ylabel("Taux de vrais positifs")
plt.legend()
plt.tight_layout()
plt.show()
Figure 17: Courbe ROC de la régression logistique Elastic Net

12.17 Courbe précision-rappel

Afficher le code
elastic_average_precision = (
    average_precision_score(
        y_test,
        y_proba_elastic,
    )
)

PrecisionRecallDisplay.from_predictions(
    y_test,
    y_proba_elastic,
    name=(
        "Elastic Net — "
        f"AP = "
        f"{elastic_average_precision:.3f}"
    ),
)

plt.title(
    "Courbe précision-rappel — Elastic Net"
)
plt.xlabel("Rappel")
plt.ylabel("Précision")
plt.legend()
plt.tight_layout()
plt.show()
Figure 18: Courbe précision-rappel de la régression Elastic Net

12.18 Comparaison des quatre régressions logistiques

Afficher le code
final_model_comparison = pd.concat(
    [
        logistic_metrics.rename(
            "Non pénalisée"
        ),
        ridge_metrics.rename(
            "Ridge"
        ),
        lasso_metrics.rename(
            "Lasso"
        ),
        elastic_metrics.rename(
            "Elastic Net"
        ),
    ],
    axis=1,
)

final_model_comparison[
    "Différence Elastic vs non pénalisée"
] = (
    final_model_comparison["Elastic Net"]
    -
    final_model_comparison["Non pénalisée"]
)

final_model_comparison[
    "Différence Elastic vs Ridge"
] = (
    final_model_comparison["Elastic Net"]
    -
    final_model_comparison["Ridge"]
)

final_model_comparison[
    "Différence Elastic vs Lasso"
] = (
    final_model_comparison["Elastic Net"]
    -
    final_model_comparison["Lasso"]
)

display(
    final_model_comparison.round(3)
)
Non pénalisée Ridge Lasso Elastic Net Différence Elastic vs non pénalisée Différence Elastic vs Ridge Différence Elastic vs Lasso
Accuracy 0.886 0.886 0.886 0.886 0.0 0.000 0.0
Balanced accuracy 0.880 0.880 0.880 0.880 0.0 0.000 0.0
Précision 0.872 0.872 0.872 0.872 0.0 0.000 0.0
Sensibilité / Recall 0.931 0.931 0.931 0.931 0.0 0.000 0.0
Spécificité 0.829 0.829 0.829 0.829 0.0 0.000 0.0
F1-score 0.900 0.900 0.900 0.900 0.0 0.000 0.0
ROC-AUC 0.929 0.930 0.929 0.929 0.0 -0.001 0.0
Average Precision 0.936 0.937 0.936 0.936 -0.0 -0.001 0.0
ImportantConclusion du modèle Elastic Net

Le modèle Elastic Net retient la combinaison suivante :

\[ C = 1 \qquad \text{et} \qquad \texttt{l1\_ratio} = 0{,}90. \]

Cette configuration correspond à une régularisation dominée par la composante L1. Le modèle se comporte donc davantage comme Lasso que comme Ridge, tout en conservant une faible composante L2 destinée à stabiliser les coefficients.

Sur le jeu de test, Elastic Net n’améliore pas les performances de classification. L’accuracy, la balanced accuracy, la précision, la sensibilité, la spécificité et le score F1 sont strictement identiques à ceux des trois modèles précédents.

La matrice de confusion reste également inchangée :

  • 95 vrais positifs ;
  • 68 vrais négatifs ;
  • 14 faux positifs ;
  • 7 faux négatifs.

La sensibilité demeure donc élevée, à 93,1 %, et le nombre de faux négatifs n’augmente pas.

La ROC-AUC atteint 0,929 et l’Average Precision 0,936. Ces résultats sont identiques à ceux du modèle non pénalisé et de Lasso, et inférieurs de seulement 0,001 à ceux de Ridge. Cet écart est trop faible pour conclure à une supériorité réelle de Ridge.

Elastic Net ne ramène par ailleurs aucun coefficient exactement à zéro : les 15 variables transformées sont conservées. Il ne produit donc pas de modèle plus parcimonieux dans cette configuration.

En conclusion, Elastic Net conserve de bonnes performances et des coefficients stables, mais il n’apporte ni amélioration prédictive nette, ni sélection de variables. Son comportement est très proche de Lasso, ce qui est cohérent avec la valeur élevée de l1_ratio.

Parmi les quatre modèles, aucun ne se distingue clairement par ses performances de classification. Ridge présente un avantage très léger sur la ROC-AUC et l’Average Precision, tandis que les autres modèles donnent des résultats pratiquement équivalents.

13 Arbre de décision

13.1 Principe du modèle

Un arbre de décision divise récursivement l’espace des variables explicatives en régions de plus en plus homogènes.

À chaque nœud, le modèle recherche une variable \(X_j\) et un seuil \(s\) permettant de séparer les observations en deux groupes :

\[ R_1(j,s)=\{X \mid X_j \leq s\}, \]

et :

\[ R_2(j,s)=\{X \mid X_j > s\}. \]

Pour une classification binaire, l’impureté de Gini d’un nœud \(m\) est :

\[ G_m = 1-\sum_{k=1}^{K}p_{mk}^{\,2}, \]

\(p_{mk}\) est la proportion d’observations de la classe \(k\) dans le nœud \(m\).

Dans le cas binaire :

\[ G_m = 1-p_m^2-(1-p_m)^2. \]

Une impureté proche de zéro indique que le nœud contient presque uniquement des observations d’une même classe.

13.2 Données utilisées

Les arbres de décision ne nécessitent pas de standardisation. Nous utilisons donc les variables numériques dans leurs unités d’origine, accompagnées des variables catégorielles encodées.

Afficher le code
X_train_encoded = pd.concat(
    [
        X_train[numeric_features],
        X_train_categorical_encoded,
    ],
    axis=1,
)

X_test_encoded = pd.concat(
    [
        X_test[numeric_features],
        X_test_categorical_encoded,
    ],
    axis=1,
)
NoteRemarque

X_train_encoded contient :

  • les variables numériques non standardisées ;
  • les variables catégorielles issues du one-hot encoding.

Cela facilitera l’interprétation des seuils utilisés par l’arbre.

13.3 Modèle de base

Afficher le code
from sklearn.tree import DecisionTreeClassifier

tree_model_base = DecisionTreeClassifier(
    criterion="gini",
    random_state=RANDOM_STATE,
)

Un arbre sans contrainte peut apprendre des règles très spécifiques au jeu d’apprentissage. La validation croisée servira donc à contrôler sa complexité.

13.4 Recherche des hyperparamètres

Les principaux hyperparamètres étudiés sont :

  • max_depth : profondeur maximale de l’arbre ;
  • min_samples_split : nombre minimal d’observations pour diviser un nœud ;
  • min_samples_leaf : nombre minimal d’observations dans une feuille ;
  • max_features : nombre de variables examinées à chaque division ;
  • ccp_alpha : intensité de l’élagage par coût-complexité.
Afficher le code
tree_parameter_grid = {
    "criterion": [
        "gini",
        "entropy",
        "log_loss",
    ],
    "max_depth": [
        2,
        3,
        4,
        5,
        6,
        8,
        None,
    ],
    "min_samples_split": [
        2,
        5,
        10,
        20,
    ],
    "min_samples_leaf": [
        1,
        2,
        5,
        10,
        20,
    ],
    "ccp_alpha": [
        0.0,
        0.001,
        0.005,
        0.01,
        0.02,
    ],
}
Afficher le code
tree_grid_search = GridSearchCV(
    estimator=tree_model_base,
    param_grid=tree_parameter_grid,
    scoring={
        "roc_auc": "roc_auc",
        "accuracy": "accuracy",
        "balanced_accuracy": "balanced_accuracy",
        "precision": "precision",
        "recall": "recall",
        "f1": "f1",
    },
    refit="roc_auc",
    cv=cv,
    n_jobs=-1,
    return_train_score=True,
)

tree_grid_search.fit(
    X_train_encoded,
    y_train,
)
GridSearchCV(cv=StratifiedKFold(n_splits=5, random_state=42, shuffle=True),
             estimator=DecisionTreeClassifier(random_state=42), n_jobs=-1,
             param_grid={'ccp_alpha': [0.0, 0.001, 0.005, 0.01, 0.02],
                         'criterion': ['gini', 'entropy', 'log_loss'],
                         'max_depth': [2, 3, 4, 5, 6, 8, None],
                         'min_samples_leaf': [1, 2, 5, 10, 20],
                         'min_samples_split': [2, 5, 10, 20]},
             refit='roc_auc', return_train_score=True,
             scoring={'accuracy': 'accuracy',
                      'balanced_accuracy': 'balanced_accuracy', 'f1': 'f1',
                      'precision': 'precision', 'recall': 'recall',
                      'roc_auc': 'roc_auc'})
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

GridSearchCV évalue chaque combinaison par validation croisée, puis réentraîne automatiquement la meilleure configuration sur tout le jeu d’apprentissage lorsque refit est activé. :contentReferenceoaicite:1

13.5 Meilleurs hyperparamètres

Afficher le code
best_tree_parameters = tree_grid_search.best_params_
best_tree_cv_auc = tree_grid_search.best_score_

tree_model = tree_grid_search.best_estimator_

print("Meilleurs hyperparamètres :")
display(best_tree_parameters)

print(
    "ROC-AUC moyenne en validation croisée : "
    f"{best_tree_cv_auc:.3f}"
)

print("Profondeur finale :", tree_model.get_depth())
print("Nombre de feuilles :", tree_model.get_n_leaves())
Meilleurs hyperparamètres :
{'ccp_alpha': 0.0,
 'criterion': 'gini',
 'max_depth': 5,
 'min_samples_leaf': 10,
 'min_samples_split': 2}
ROC-AUC moyenne en validation croisée : 0.908
Profondeur finale : 5
Nombre de feuilles : 21
NoteInterprétation

La validation croisée retient un arbre utilisant le critère d’impureté de Gini. À chaque nœud, le modèle choisit donc la séparation qui réduit le plus l’hétérogénéité des classes.

La profondeur optimale est égale à 5. L’arbre reste ainsi d’une complexité modérée : il peut représenter plusieurs interactions et relations non linéaires, tout en évitant de construire des règles excessivement profondes et spécifiques au jeu d’apprentissage.

La valeur min_samples_leaf = 10 impose qu’une feuille contienne au moins 10 observations. Cette contrainte limite les décisions fondées sur de très petits groupes et contribue à améliorer la stabilité du modèle.

La valeur min_samples_split = 2 signifie qu’un nœud peut être divisé dès qu’il contient au moins deux observations. Dans ce modèle, cette valeur est cependant encadrée par la contrainte plus forte imposée sur la taille minimale des feuilles.

La valeur optimale ccp_alpha = 0 indique que la validation croisée ne retient pas d’élagage supplémentaire par coût-complexité. La complexité de l’arbre est déjà suffisamment contrôlée par la profondeur maximale et la taille minimale des feuilles.

La ROC-AUC moyenne obtenue en validation croisée est de 0,908. Cette valeur indique une bonne capacité de discrimination, mais elle reste inférieure aux ROC-AUC moyennes proches de 0,925 obtenues avec les régressions logistiques régularisées.

L’arbre final possède une profondeur de 5 et 21 feuilles. Il est donc assez riche pour identifier plusieurs profils de patients, mais demeure encore visualisable et interprétable.

Globalement, la validation croisée favorise un arbre de complexité intermédiaire. Un arbre plus simple risquerait de sous-ajuster les données, tandis qu’un arbre plus profond pourrait apprendre des règles trop spécifiques et moins bien se généraliser.

13.6 Résultats de validation croisée

Afficher le code
tree_cv_results = pd.DataFrame(
    tree_grid_search.cv_results_
)

tree_cv_summary = (
    tree_cv_results[
        [
            "param_criterion",
            "param_max_depth",
            "param_min_samples_split",
            "param_min_samples_leaf",
            "param_ccp_alpha",
            "mean_train_roc_auc",
            "mean_test_roc_auc",
            "std_test_roc_auc",
            "mean_test_accuracy",
            "mean_test_balanced_accuracy",
            "mean_test_precision",
            "mean_test_recall",
            "mean_test_f1",
            "rank_test_roc_auc",
        ]
    ]
    .sort_values("rank_test_roc_auc")
    .reset_index(drop=True)
)

display(tree_cv_summary.head(20).round(4))
param_criterion param_max_depth param_min_samples_split param_min_samples_leaf param_ccp_alpha mean_train_roc_auc mean_test_roc_auc std_test_roc_auc mean_test_accuracy mean_test_balanced_accuracy mean_test_precision mean_test_recall mean_test_f1 rank_test_roc_auc
0 gini 5 20 10 0.000 0.9461 0.9078 0.0270 0.8338 0.8322 0.8521 0.8474 0.8491 1
1 gini 5 10 10 0.000 0.9461 0.9078 0.0270 0.8338 0.8322 0.8521 0.8474 0.8491 1
2 gini 5 5 10 0.000 0.9461 0.9078 0.0270 0.8338 0.8322 0.8521 0.8474 0.8491 1
3 gini 5 2 10 0.000 0.9461 0.9078 0.0270 0.8338 0.8322 0.8521 0.8474 0.8491 1
4 gini 4 2 10 0.000 0.9329 0.9062 0.0199 0.8406 0.8367 0.8452 0.8746 0.8585 5
5 gini 4 5 10 0.000 0.9329 0.9062 0.0199 0.8406 0.8367 0.8452 0.8746 0.8585 5
6 gini 4 10 10 0.000 0.9329 0.9062 0.0199 0.8406 0.8367 0.8452 0.8746 0.8585 5
7 gini 4 20 10 0.000 0.9329 0.9062 0.0199 0.8406 0.8367 0.8452 0.8746 0.8585 5
8 gini 5 10 10 0.001 0.9429 0.9055 0.0230 0.8338 0.8322 0.8521 0.8474 0.8491 9
9 gini 5 20 10 0.001 0.9429 0.9055 0.0230 0.8338 0.8322 0.8521 0.8474 0.8491 9
10 gini 5 2 10 0.001 0.9429 0.9055 0.0230 0.8338 0.8322 0.8521 0.8474 0.8491 9
11 gini 5 5 10 0.001 0.9429 0.9055 0.0230 0.8338 0.8322 0.8521 0.8474 0.8491 9
12 gini 4 20 10 0.001 0.9323 0.9049 0.0187 0.8406 0.8367 0.8452 0.8746 0.8585 13
13 gini 4 10 10 0.001 0.9323 0.9049 0.0187 0.8406 0.8367 0.8452 0.8746 0.8585 13
14 gini 4 5 10 0.001 0.9323 0.9049 0.0187 0.8406 0.8367 0.8452 0.8746 0.8585 13
15 gini 4 2 10 0.001 0.9323 0.9049 0.0187 0.8406 0.8367 0.8452 0.8746 0.8585 13
16 gini 8 10 10 0.000 0.9582 0.9041 0.0240 0.8175 0.8157 0.8366 0.8326 0.8343 17
17 gini 8 20 10 0.000 0.9582 0.9041 0.0240 0.8175 0.8157 0.8366 0.8326 0.8343 17
18 gini 8 2 10 0.000 0.9582 0.9041 0.0240 0.8175 0.8157 0.8366 0.8326 0.8343 17
19 gini 8 5 10 0.000 0.9582 0.9041 0.0240 0.8175 0.8157 0.8366 0.8326 0.8343 17

13.7 Effet de la profondeur

Pour isoler visuellement l’effet de la profondeur, nous entraînons des arbres avec les autres paramètres fixés à ceux du meilleur modèle.

Afficher le code
tree_depth_rows = []

depth_values = [
    1,
    2,
    3,
    4,
    5,
    6,
    8,
    10,
    12,
    None,
]

for depth in depth_values:
    current_tree = DecisionTreeClassifier(
        criterion=best_tree_parameters[
            "criterion"
        ],
        max_depth=depth,
        min_samples_split=best_tree_parameters[
            "min_samples_split"
        ],
        min_samples_leaf=best_tree_parameters[
            "min_samples_leaf"
        ],
        ccp_alpha=best_tree_parameters[
            "ccp_alpha"
        ],
        random_state=RANDOM_STATE,
    )

    current_scores = cross_validate(
        current_tree,
        X_train_encoded,
        y_train,
        cv=cv,
        scoring="roc_auc",
        return_train_score=True,
        n_jobs=-1,
    )

    tree_depth_rows.append(
        {
            "profondeur": (
                "Aucune limite"
                if depth is None
                else str(depth)
            ),
            "profondeur_numerique": (
                14 if depth is None else depth
            ),
            "roc_auc_train":
                current_scores[
                    "train_score"
                ].mean(),
            "roc_auc_validation":
                current_scores[
                    "test_score"
                ].mean(),
            "ecart_type_validation":
                current_scores[
                    "test_score"
                ].std(),
        }
    )

tree_depth_results = pd.DataFrame(
    tree_depth_rows
)

display(tree_depth_results.round(4))
profondeur profondeur_numerique roc_auc_train roc_auc_validation ecart_type_validation
0 1 1 0.8228 0.8229 0.0420
1 2 2 0.8721 0.8595 0.0303
2 3 3 0.9132 0.8923 0.0166
3 4 4 0.9329 0.9062 0.0199
4 5 5 0.9461 0.9078 0.0270
5 6 6 0.9542 0.9023 0.0241
6 8 8 0.9582 0.9041 0.0240
7 10 10 0.9585 0.9003 0.0215
8 12 12 0.9585 0.9003 0.0215
9 Aucune limite 14 0.9585 0.9003 0.0215
Afficher le code
plt.figure(figsize=(8, 5))

plt.plot(
    tree_depth_results[
        "profondeur_numerique"
    ],
    tree_depth_results[
        "roc_auc_train"
    ],
    marker="o",
    label="Apprentissage",
)

plt.plot(
    tree_depth_results[
        "profondeur_numerique"
    ],
    tree_depth_results[
        "roc_auc_validation"
    ],
    marker="o",
    label="Validation",
)

plt.xlabel("Profondeur maximale")
plt.ylabel("ROC-AUC moyenne")
plt.title(
    "Influence de la profondeur de l’arbre"
)
plt.legend()
plt.tight_layout()
plt.show()
Figure 19: ROC-AUC selon la profondeur de l’arbre
NoteInterprétation de la complexité

Pour les profondeurs faibles, comprises entre 1 et 3, les ROC-AUC d’apprentissage et de validation restent relativement proches, mais leurs valeurs sont encore limitées. Cela indique que l’arbre est trop simple pour capturer toute la structure des données : il présente alors un risque de sous-ajustement.

La ROC-AUC de validation augmente nettement jusqu’à une profondeur de 4 à 5. Elle atteint son maximum autour de la profondeur 5, avec une valeur proche de 0,908. Cette profondeur représente donc le meilleur compromis observé entre capacité d’apprentissage et généralisation.

À partir d’une profondeur de 6, la ROC-AUC d’apprentissage continue d’augmenter et se rapproche progressivement de 0,96. En revanche, la ROC-AUC de validation diminue légèrement puis se stabilise autour de 0,90.

L’écart entre les deux courbes devient ainsi plus important lorsque la profondeur augmente. L’arbre apprend davantage les particularités du jeu d’apprentissage, sans améliorer ses performances sur les données de validation. Ce comportement traduit un début de surajustement.

La profondeur 5 retenue par validation croisée est donc cohérente avec le graphique. Elle se situe juste avant que l’écart entre apprentissage et validation ne devienne plus marqué.

En conclusion :

  • les profondeurs 1 à 3 conduisent à un modèle trop simple ;
  • les profondeurs 4 à 5 offrent le meilleur compromis ;
  • les profondeurs supérieures augmentent surtout la performance d’apprentissage ;
  • une profondeur élevée n’apporte pas de gain de généralisation et accroît le risque de surajustement.

13.8 Visualisation de l’arbre optimal

Afficher le code
from sklearn.tree import plot_tree

plt.figure(figsize=(22, 12))

plot_tree(
    tree_model,
    feature_names=X_train_encoded.columns,
    class_names=[
        "Absence",
        "Présence",
    ],
    filled=True,
    rounded=True,
    proportion=True,
    precision=3,
    max_depth=4,
    fontsize=8,
)

plt.title(
    "Arbre de décision optimal "
    "— quatre premiers niveaux"
)

plt.tight_layout()
plt.show()

max_depth=4 dans plot_tree limite seulement l’affichage. Il ne modifie pas le modèle entraîné.

13.8.1 Lecture d’un nœud

Chaque nœud affiche notamment :

  • la règle de séparation ;
  • l’impureté ;
  • la proportion d’observations ;
  • la distribution des classes ;
  • la classe majoritaire prédite.
NoteInterprétation de l’arbre

La première variable utilisée à la racine est ST_Slope_Up. Cela signifie que, parmi toutes les variables disponibles, cette modalité produit la meilleure séparation initiale entre les patients avec et sans maladie cardiaque.

La règle de la racine est :

\[ \texttt{ST\_Slope\_Up} \leq 0{,}5. \]

Comme cette variable est binaire, la branche gauche correspond aux patients pour lesquels ST_Slope_Up = 0, tandis que la branche droite correspond aux patients pour lesquels ST_Slope_Up = 1.

Cette première séparation est cohérente avec la régression logistique : ST_Slope_Up possédait un coefficient fortement négatif. Sa présence était donc associée à une probabilité plus faible de maladie cardiaque.

Parmi les patients ne présentant pas la modalité ST_Slope_Up, la deuxième variable importante est MaxHR, avec un seuil d’environ 142,5. Une fréquence cardiaque maximale faible est davantage associée à la classe « présence de maladie », tandis qu’une valeur plus élevée conduit à des profils plus hétérogènes.

Dans la branche caractérisée par MaxHR <= 142.5, le sexe intervient ensuite. Les patients masculins sont fréquemment orientés vers des feuilles prédisant la présence de maladie. Ce résultat est cohérent avec le coefficient positif de Sex_M observé dans les modèles logistiques.

Pour les patientes, l’absence d’angine provoquée par l’exercice conduit vers une feuille prédisant plutôt l’absence de maladie. En revanche, lorsque ExerciseAngina_Y = 1, l’arbre prédit la présence de maladie, quelle que soit ensuite la valeur d’Oldpeak dans les branches affichées.

Chez les hommes, ChestPainType_NAP constitue une autre variable de séparation importante. Les règles sont ensuite précisées par des seuils de Cholesterol et d’Oldpeak. Même si certaines divisions supplémentaires ne changent pas la classe finale, elles modifient la proportion estimée de patients malades dans les feuilles.

Dans l’autre branche principale, correspondant à ST_Slope_Up = 1, Cholesterol est utilisé très tôt dans l’arbre, avec un seuil proche de 42,5. Ce seuil très faible doit être interprété avec prudence. Dans ce jeu de données, certaines valeurs de cholestérol égales ou proches de zéro peuvent représenter des données manquantes codées numériquement plutôt qu’un niveau physiologique réel.

Les variables ExerciseAngina_Y, Age, Oldpeak, MaxHR, ChestPainType_NAP et FastingBS_1 apparaissent également dans les premiers niveaux. L’arbre met donc en évidence des interactions : l’effet d’une variable dépend du chemin déjà suivi et du profil défini par les variables précédentes.

Les règles les plus importantes se concentrent dans les trois premiers niveaux de l’arbre. Les divisions plus profondes servent surtout à affiner les prédictions au sein de sous-groupes plus petits.

Globalement, la structure de l’arbre est cohérente avec les modèles logistiques et l’analyse exploratoire. Les mêmes variables principales réapparaissent, notamment ST_Slope_Up, MaxHR, Sex_M, ExerciseAngina_Y, Oldpeak et les modalités de ChestPainType.

La différence essentielle est que la régression logistique estime un effet global pour chaque variable, tandis que l’arbre représente des règles conditionnelles et des interactions. Il montre par exemple que l’importance de MaxHR ou de ExerciseAngina_Y dépend d’abord de la valeur de ST_Slope_Up.

13.9 Règles textuelles de l’arbre

Afficher le code
from sklearn.tree import export_text

tree_rules = export_text(
    tree_model,
    feature_names=list(
        X_train_encoded.columns
    ),
    max_depth=5,
)

print(tree_rules)
|--- ST_Slope_Up <= 0.50
|   |--- MaxHR <= 142.50
|   |   |--- Sex_M <= 0.50
|   |   |   |--- ExerciseAngina_Y <= 0.50
|   |   |   |   |--- class: 0
|   |   |   |--- ExerciseAngina_Y >  0.50
|   |   |   |   |--- Oldpeak <= 1.10
|   |   |   |   |   |--- class: 1
|   |   |   |   |--- Oldpeak >  1.10
|   |   |   |   |   |--- class: 1
|   |   |--- Sex_M >  0.50
|   |   |   |--- ChestPainType_NAP <= 0.50
|   |   |   |   |--- Cholesterol <= 222.50
|   |   |   |   |   |--- class: 1
|   |   |   |   |--- Cholesterol >  222.50
|   |   |   |   |   |--- class: 1
|   |   |   |--- ChestPainType_NAP >  0.50
|   |   |   |   |--- Oldpeak <= 0.80
|   |   |   |   |   |--- class: 1
|   |   |   |   |--- Oldpeak >  0.80
|   |   |   |   |   |--- class: 1
|   |--- MaxHR >  142.50
|   |   |--- ChestPainType_NAP <= 0.50
|   |   |   |--- Cholesterol <= 191.00
|   |   |   |   |--- class: 1
|   |   |   |--- Cholesterol >  191.00
|   |   |   |   |--- Cholesterol <= 227.50
|   |   |   |   |   |--- class: 0
|   |   |   |   |--- Cholesterol >  227.50
|   |   |   |   |   |--- class: 1
|   |   |--- ChestPainType_NAP >  0.50
|   |   |   |--- RestingBP <= 122.50
|   |   |   |   |--- class: 0
|   |   |   |--- RestingBP >  122.50
|   |   |   |   |--- class: 1
|--- ST_Slope_Up >  0.50
|   |--- Cholesterol <= 42.50
|   |   |--- FastingBS_1 <= 0.50
|   |   |   |--- class: 0
|   |   |--- FastingBS_1 >  0.50
|   |   |   |--- class: 1
|   |--- Cholesterol >  42.50
|   |   |--- ExerciseAngina_Y <= 0.50
|   |   |   |--- Age <= 56.50
|   |   |   |   |--- RestingBP <= 119.00
|   |   |   |   |   |--- class: 0
|   |   |   |   |--- RestingBP >  119.00
|   |   |   |   |   |--- class: 0
|   |   |   |--- Age >  56.50
|   |   |   |   |--- Cholesterol <= 231.00
|   |   |   |   |   |--- class: 0
|   |   |   |   |--- Cholesterol >  231.00
|   |   |   |   |   |--- class: 0
|   |   |--- ExerciseAngina_Y >  0.50
|   |   |   |--- Oldpeak <= 0.70
|   |   |   |   |--- MaxHR <= 157.50
|   |   |   |   |   |--- class: 0
|   |   |   |   |--- MaxHR >  157.50
|   |   |   |   |   |--- class: 0
|   |   |   |--- Oldpeak >  0.70
|   |   |   |   |--- class: 1

Cette représentation permet de lire précisément les premières règles de classification.

13.10 Importance des variables

L’importance d’une variable mesure la réduction totale pondérée de l’impureté obtenue grâce aux divisions utilisant cette variable.

Afficher le code
tree_importance = pd.DataFrame(
    {
        "variable":
            X_train_encoded.columns,
        "importance":
            tree_model.feature_importances_,
    }
)

tree_importance = (
    tree_importance
    .sort_values(
        "importance",
        ascending=False,
    )
    .reset_index(drop=True)
)

display(tree_importance.round(4))
variable importance
0 ST_Slope_Up 0.6679
1 Cholesterol 0.1167
2 MaxHR 0.0457
3 ExerciseAngina_Y 0.0384
4 Sex_M 0.0299
5 ChestPainType_NAP 0.0295
6 Oldpeak 0.0263
7 FastingBS_1 0.0228
8 Age 0.0129
9 RestingBP 0.0099
10 ChestPainType_ATA 0.0000
11 ChestPainType_TA 0.0000
12 RestingECG_Normal 0.0000
13 RestingECG_ST 0.0000
14 ST_Slope_Flat 0.0000
Afficher le code
tree_importance_plot = (
    tree_importance
    .sort_values("importance")
)

tree_importance_plot.plot(
    x="variable",
    y="importance",
    kind="barh",
    figsize=(9, 7),
    legend=False,
)

plt.xlabel("Importance fondée sur l’impureté")
plt.ylabel("Variable")
plt.title(
    "Importance des variables — Arbre de décision"
)
plt.tight_layout()
plt.show()
Figure 20: Importance des variables dans l’arbre de décision
WarningLimite de l’importance fondée sur l’impureté

Une importance élevée ne démontre pas une relation causale. Cette mesure peut aussi favoriser certaines variables possédant de nombreux seuils possibles.

Elle sera complétée plus tard par l’importance par permutation.

13.11 Importance par permutation

L’importance par permutation mesure la diminution de performance obtenue lorsque les valeurs d’une variable sont mélangées aléatoirement.

Afficher le code
from sklearn.inspection import permutation_importance

tree_permutation = permutation_importance(
    tree_model,
    X_test_encoded,
    y_test,
    scoring="roc_auc",
    n_repeats=30,
    random_state=RANDOM_STATE,
    n_jobs=-1,
)

tree_permutation_importance = pd.DataFrame(
    {
        "variable":
            X_test_encoded.columns,
        "importance_moyenne":
            tree_permutation.importances_mean,
        "ecart_type":
            tree_permutation.importances_std,
    }
)

tree_permutation_importance = (
    tree_permutation_importance
    .sort_values(
        "importance_moyenne",
        ascending=False,
    )
)

display(
    tree_permutation_importance.round(4)
)
variable importance_moyenne ecart_type
14 ST_Slope_Up 0.1432 0.0253
2 Cholesterol 0.0523 0.0208
9 FastingBS_1 0.0248 0.0086
3 MaxHR 0.0203 0.0109
12 ExerciseAngina_Y 0.0168 0.0088
5 Sex_M 0.0164 0.0073
0 Age 0.0065 0.0036
4 Oldpeak 0.0045 0.0042
1 RestingBP 0.0042 0.0048
7 ChestPainType_NAP 0.0026 0.0077
6 ChestPainType_ATA 0.0000 0.0000
8 ChestPainType_TA 0.0000 0.0000
10 RestingECG_Normal 0.0000 0.0000
11 RestingECG_ST 0.0000 0.0000
13 ST_Slope_Flat 0.0000 0.0000
Afficher le code
tree_permutation_plot = (
    tree_permutation_importance
    .sort_values("importance_moyenne")
)

plt.figure(figsize=(9, 7))

plt.barh(
    tree_permutation_plot["variable"],
    tree_permutation_plot[
        "importance_moyenne"
    ],
    xerr=tree_permutation_plot[
        "ecart_type"
    ],
)

plt.axvline(0, linewidth=1)
plt.xlabel(
    "Diminution moyenne de la ROC-AUC"
)
plt.ylabel("Variable")
plt.title(
    "Importance par permutation — Arbre"
)
plt.tight_layout()
plt.show()

NoteInterprétation des importances

Les deux méthodes d’importance identifient ST_Slope_Up comme la variable la plus influente de l’arbre.

Selon l’importance fondée sur la réduction de l’impureté, ST_Slope_Up représente environ 66,8 % de l’importance totale. Cette domination s’explique notamment par sa position à la racine de l’arbre : elle intervient dès la première séparation et affecte donc un grand nombre d’observations.

Les variables suivantes les plus utilisées par l’arbre sont :

  • Cholesterol, avec une importance d’environ 11,7 % ;
  • MaxHR, avec environ 4,6 % ;
  • ExerciseAngina_Y, avec environ 3,8 % ;
  • Sex_M et ChestPainType_NAP, avec environ 3 % chacune ;
  • Oldpeak et FastingBS_1, avec des contributions plus faibles.

L’importance par permutation confirme globalement cette hiérarchie. La permutation de ST_Slope_Up entraîne la plus forte diminution moyenne de la ROC-AUC, soit environ 0,143. Cette variable apporte donc une information prédictive majeure sur le jeu de test.

La permutation de Cholesterol diminue ensuite la ROC-AUC d’environ 0,052. FastingBS_1, MaxHR, ExerciseAngina_Y et Sex_M ont également une importance positive, mais plus modérée.

Les deux méthodes sont donc cohérentes sur les variables principales :

  • ST_Slope_Up est nettement dominante ;
  • Cholesterol occupe la deuxième position ;
  • MaxHR, ExerciseAngina_Y, Sex_M et FastingBS_1 complètent le groupe des variables les plus utiles.

Certaines différences apparaissent néanmoins. Par exemple, ChestPainType_NAP possède une importance fondée sur l’impureté d’environ 0,030, mais son importance par permutation est très faible et son écart-type est supérieur à sa moyenne. Son effet prédictif sur le jeu de test paraît donc peu stable ou redondant avec d’autres variables.

De même, RestingBP, Age et Oldpeak ont des importances par permutation faibles, parfois proches de leur variabilité estimée. Leur contribution supplémentaire au pouvoir discriminant de l’arbre semble donc limitée.

Les variables suivantes ont une importance nulle dans les deux analyses :

  • ChestPainType_ATA ;
  • ChestPainType_TA ;
  • RestingECG_Normal ;
  • RestingECG_ST ;
  • ST_Slope_Flat.

Elles ne sont pas utilisées dans les divisions de l’arbre optimal. Leur importance nulle ne signifie toutefois pas qu’elles sont totalement indépendantes de la maladie, mais qu’elles n’apportent pas d’amélioration supplémentaire dans la structure retenue par cet arbre.

La comparaison avec les régressions logistiques montre une cohérence partielle. ST_Slope_Up, Sex_M, FastingBS_1, ExerciseAngina_Y, MaxHR, Oldpeak et certaines modalités de ChestPainType figuraient déjà parmi les variables importantes des modèles logistiques.

En revanche, l’arbre accorde une place beaucoup plus importante à Cholesterol, notamment en raison de son utilisation dans plusieurs règles de séparation. Cette importance doit être interprétée avec prudence, car certaines valeurs très faibles ou nulles peuvent correspondre à des données manquantes codées numériquement.

En conclusion, les deux méthodes confirment que l’arbre repose principalement sur ST_Slope_Up, puis sur Cholesterol et un groupe secondaire composé de MaxHR, FastingBS_1, ExerciseAngina_Y et Sex_M. La concentration de l’importance sur une seule variable rend le modèle facile à interpréter, mais peut aussi le rendre plus sensible aux variations de cette variable.

13.12 Prédictions sur le jeu de test

Afficher le code
y_pred_tree = tree_model.predict(
    X_test_encoded
)

y_proba_tree = tree_model.predict_proba(
    X_test_encoded
)[:, 1]

Les classes utilisent le seuil de décision par défaut de \(0{,}5\).

13.13 Indicateurs du modèle

Afficher le code
tn_tree, fp_tree, fn_tree, tp_tree = (
    confusion_matrix(
        y_test,
        y_pred_tree,
    ).ravel()
)

sensitivity_tree = (
    tp_tree
    / (tp_tree + fn_tree)
)

specificity_tree = (
    tn_tree
    / (tn_tree + fp_tree)
)

tree_metrics = pd.Series(
    {
        "Accuracy":
            accuracy_score(
                y_test,
                y_pred_tree,
            ),
        "Balanced accuracy":
            balanced_accuracy_score(
                y_test,
                y_pred_tree,
            ),
        "Précision":
            precision_score(
                y_test,
                y_pred_tree,
            ),
        "Sensibilité / Recall":
            sensitivity_tree,
        "Spécificité":
            specificity_tree,
        "F1-score":
            f1_score(
                y_test,
                y_pred_tree,
            ),
        "ROC-AUC":
            roc_auc_score(
                y_test,
                y_proba_tree,
            ),
        "Average Precision":
            average_precision_score(
                y_test,
                y_proba_tree,
            ),
    },
    name="Arbre de décision",
)

display(
    tree_metrics
    .to_frame("score")
    .round(3)
)
score
Accuracy 0.832
Balanced accuracy 0.829
Précision 0.845
Sensibilité / Recall 0.853
Spécificité 0.805
F1-score 0.849
ROC-AUC 0.876
Average Precision 0.853

13.14 Rapport de classification

Afficher le code
print(
    classification_report(
        y_test,
        y_pred_tree,
        target_names=[
            "Absence de maladie",
            "Présence de maladie",
        ],
        digits=3,
    )
)
                     precision    recall  f1-score   support

 Absence de maladie      0.815     0.805     0.810        82
Présence de maladie      0.845     0.853     0.849       102

           accuracy                          0.832       184
          macro avg      0.830     0.829     0.829       184
       weighted avg      0.831     0.832     0.831       184

13.15 Matrice de confusion

Afficher le code
ConfusionMatrixDisplay.from_predictions(
    y_test,
    y_pred_tree,
    display_labels=[
        "Absence",
        "Présence",
    ],
)

plt.title(
    "Matrice de confusion — Arbre de décision"
)
plt.tight_layout()
plt.show()
Figure 21: Matrice de confusion de l’arbre de décision
ImportantInterprétation de la matrice de confusion
  • vrais positifs : np.int64(87) ;
  • vrais négatifs : np.int64(66) ;
  • faux positifs : np.int64(16) ;
  • faux négatifs : np.int64(15).

Le modèle identifie correctement 87 des 102 patients présentant réellement une maladie cardiaque. Sa sensibilité est donc de 85,3 %.

En revanche, 15 patients malades sont classés à tort comme non malades. Ce nombre de faux négatifs est nettement supérieur à celui obtenu avec les modèles logistiques, qui n’en produisaient que 7.

Parmi les 82 patients sans maladie cardiaque, 66 sont correctement classés et 16 sont considérés à tort comme positifs. La spécificité atteint ainsi 80,5 %.

Les performances globales de l’arbre sont :

  • accuracy : 83,2 % ;
  • balanced accuracy : 82,9 % ;
  • précision : 84,5 % ;
  • sensibilité : 85,3 % ;
  • spécificité : 80,5 % ;
  • F1-score : 0,849 ;
  • ROC-AUC : 0,876 ;
  • Average Precision : 0,853.

Ces résultats sont inférieurs à ceux des modèles logistiques. Par rapport à la régression logistique, l’arbre produit 8 faux négatifs supplémentaires et 2 faux positifs supplémentaires.

La baisse de sensibilité est particulièrement importante dans une logique de dépistage, car elle signifie qu’une proportion plus élevée de patients malades ne serait pas détectée par le modèle.

L’arbre reste relativement équilibré entre les deux classes, comme l’indique la balanced accuracy de 82,9 %. Toutefois, sa capacité de discrimination, mesurée par la ROC-AUC, est sensiblement plus faible que celle des modèles logistiques, dont la ROC-AUC était proche de 0,929.

En conclusion, l’arbre de décision offre une représentation interprétable sous forme de règles, mais cette simplicité s’accompagne ici d’une dégradation claire des performances prédictives. Il ne constitue donc pas, à ce stade, une meilleure alternative aux modèles logistiques.

13.16 Courbe ROC

Afficher le code
tree_roc_auc = roc_auc_score(
    y_test,
    y_proba_tree,
)

RocCurveDisplay.from_predictions(
    y_test,
    y_proba_tree,
    name=(
        "Arbre de décision — "
        f"AUC = {tree_roc_auc:.3f}"
    ),
)

plt.plot(
    [0, 1],
    [0, 1],
    linestyle="--",
    label="Modèle aléatoire",
)

plt.title(
    "Courbe ROC — Arbre de décision"
)
plt.xlabel("Taux de faux positifs")
plt.ylabel("Taux de vrais positifs")
plt.legend()
plt.tight_layout()
plt.show()
Figure 22: Courbe ROC de l’arbre de décision

La courbe ROC d’un arbre peut paraître moins lisse que celle d’une régression logistique, car l’arbre produit souvent un nombre limité de probabilités distinctes, correspondant aux proportions observées dans ses feuilles.

13.17 Courbe précision-rappel

Afficher le code
tree_average_precision = (
    average_precision_score(
        y_test,
        y_proba_tree,
    )
)

PrecisionRecallDisplay.from_predictions(
    y_test,
    y_proba_tree,
    name=(
        "Arbre de décision — "
        f"AP = {tree_average_precision:.3f}"
    ),
)

plt.title(
    "Courbe précision-rappel "
    "— Arbre de décision"
)
plt.xlabel("Rappel")
plt.ylabel("Précision")
plt.legend()
plt.tight_layout()
plt.show()
Figure 23: Courbe précision-rappel de l’arbre de décision

13.18 Comparaison avec les modèles logistiques

Afficher le code
model_comparison_with_tree = pd.concat(
    [
        logistic_metrics.rename(
            "Non pénalisée"
        ),
        ridge_metrics.rename(
            "Ridge"
        ),
        lasso_metrics.rename(
            "Lasso"
        ),
        elastic_metrics.rename(
            "Elastic Net"
        ),
        tree_metrics.rename(
            "Arbre de décision"
        ),
    ],
    axis=1,
)

model_comparison_with_tree[
    "Différence arbre vs logistique"
] = (
    model_comparison_with_tree[
        "Arbre de décision"
    ]
    -
    model_comparison_with_tree[
        "Non pénalisée"
    ]
)

display(
    model_comparison_with_tree.round(3)
)
Non pénalisée Ridge Lasso Elastic Net Arbre de décision Différence arbre vs logistique
Accuracy 0.886 0.886 0.886 0.886 0.832 -0.054
Balanced accuracy 0.880 0.880 0.880 0.880 0.829 -0.051
Précision 0.872 0.872 0.872 0.872 0.845 -0.027
Sensibilité / Recall 0.931 0.931 0.931 0.931 0.853 -0.078
Spécificité 0.829 0.829 0.829 0.829 0.805 -0.024
F1-score 0.900 0.900 0.900 0.900 0.849 -0.052
ROC-AUC 0.929 0.930 0.929 0.929 0.876 -0.053
Average Precision 0.936 0.937 0.936 0.936 0.853 -0.083
ImportantConclusion de l’arbre de décision

L’arbre est moins performant que nos modeleslogistiques.

14 Forêt aléatoire

14.1 Principe du modèle

Une forêt aléatoire combine plusieurs arbres de décision.

Chaque arbre est entraîné :

  1. sur un échantillon bootstrap du jeu d’apprentissage ;
  2. en utilisant seulement une partie aléatoire des variables à chaque séparation.

Pour une classification binaire, la prédiction finale correspond au vote majoritaire des arbres. La probabilité prédite correspond à la moyenne des probabilités produites par les différents arbres.

Cette méthode vise à réduire la variance d’un arbre individuel tout en conservant sa capacité à modéliser des relations non linéaires et des interactions entre variables.

14.2 Données utilisées

Comme les arbres de décision, les forêts aléatoires ne nécessitent pas de standardisation. Nous utilisons donc les données encodées dans leurs unités originales.

Afficher le code
print(
    "Dimensions du jeu d’apprentissage :",
    X_train_encoded.shape,
)

print(
    "Dimensions du jeu de test :",
    X_test_encoded.shape,
)
Dimensions du jeu d’apprentissage : (734, 15)
Dimensions du jeu de test : (184, 15)

14.3 Importation du modèle

Afficher le code
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import RandomizedSearchCV

14.4 Modèle de base

Afficher le code
random_forest_base = RandomForestClassifier(
    random_state=RANDOM_STATE,
    n_jobs=-1,
)

14.5 Recherche des hyperparamètres

Les principaux hyperparamètres étudiés sont :

  • n_estimators : nombre d’arbres ;
  • max_depth : profondeur maximale de chaque arbre ;
  • min_samples_split : nombre minimal d’observations nécessaire pour diviser un nœud ;
  • min_samples_leaf : nombre minimal d’observations dans une feuille ;
  • max_features : nombre de variables examinées à chaque séparation ;
  • bootstrap : utilisation ou non d’échantillons bootstrap ;
  • class_weight : pondération éventuelle des classes.

Une recherche aléatoire est utilisée afin d’explorer un nombre important de combinaisons sans tester exhaustivement toutes les possibilités.

Afficher le code
random_forest_parameter_grid = {
    "n_estimators": [
        200,
        300,
        500,
        700,
        1000,
    ],
    "criterion": [
        "gini",
        "entropy",
        "log_loss",
    ],
    "max_depth": [
        3,
        4,
        5,
        6,
        8,
        10,
        12,
        None,
    ],
    "min_samples_split": [
        2,
        5,
        10,
        20,
    ],
    "min_samples_leaf": [
        1,
        2,
        5,
        10,
        20,
    ],
    "max_features": [
        "sqrt",
        "log2",
        0.5,
        None,
    ],
    "bootstrap": [
        True,
    ],
    "class_weight": [
        None,
        "balanced",
        "balanced_subsample",
    ],
}
Afficher le code
random_forest_search = RandomizedSearchCV(
    estimator=random_forest_base,
    param_distributions=(
        random_forest_parameter_grid
    ),
    n_iter=100,
    scoring={
        "roc_auc": "roc_auc",
        "accuracy": "accuracy",
        "balanced_accuracy":
            "balanced_accuracy",
        "precision": "precision",
        "recall": "recall",
        "f1": "f1",
    },
    refit="roc_auc",
    cv=cv,
    random_state=RANDOM_STATE,
    n_jobs=-1,
    return_train_score=True,
    verbose=1,
)

random_forest_search.fit(
    X_train_encoded,
    y_train,
)
Fitting 5 folds for each of 100 candidates, totalling 500 fits
RandomizedSearchCV(cv=StratifiedKFold(n_splits=5, random_state=42, shuffle=True),
                   estimator=RandomForestClassifier(n_jobs=-1, random_state=42),
                   n_iter=100, n_jobs=-1,
                   param_distributions={'bootstrap': [True],
                                        'class_weight': [None, 'balanced',
                                                         'balanced_subsample'],
                                        'criterion': ['gini', 'entropy',
                                                      'log_loss'],
                                        'max_depth': [3, 4, 5, 6, 8, 10, 12,
                                                      None],
                                        'max_features': ['sqrt', 'log2', 0.5,
                                                         None],
                                        'min_samples_leaf': [1, 2, 5, 10, 20],
                                        'min_samples_split': [2, 5, 10, 20],
                                        'n_estimators': [200, 300, 500, 700,
                                                         1000]},
                   random_state=42, refit='roc_auc', return_train_score=True,
                   scoring={'accuracy': 'accuracy',
                            'balanced_accuracy': 'balanced_accuracy',
                            'f1': 'f1', 'precision': 'precision',
                            'recall': 'recall', 'roc_auc': 'roc_auc'},
                   verbose=1)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

RandomizedSearchCV teste un nombre défini de combinaisons tirées dans l’espace des hyperparamètres, puis réentraîne la meilleure configuration lorsque refit est activé. :contentReferenceoaicite:1

14.6 Meilleurs hyperparamètres

Afficher le code
best_random_forest_parameters = (
    random_forest_search.best_params_
)

best_random_forest_cv_auc = (
    random_forest_search.best_score_
)

random_forest_model = (
    random_forest_search.best_estimator_
)

print("Meilleurs hyperparamètres :")

display(
    best_random_forest_parameters
)

print(
    "ROC-AUC moyenne en validation croisée : "
    f"{best_random_forest_cv_auc:.3f}"
)
Meilleurs hyperparamètres :
{'n_estimators': 700,
 'min_samples_split': 10,
 'min_samples_leaf': 2,
 'max_features': 'sqrt',
 'max_depth': 10,
 'criterion': 'log_loss',
 'class_weight': None,
 'bootstrap': True}
ROC-AUC moyenne en validation croisée : 0.932
NoteInterprétation

La meilleure configuration de la forêt aléatoire contient 700 arbres. Ce nombre relativement élevé permet de stabiliser les prédictions par agrégation. Au-delà d’un certain nombre d’arbres, le gain prédictif devient généralement faible, mais la variance des résultats peut encore légèrement diminuer.

La profondeur maximale retenue est de 10. Les arbres individuels sont donc plus complexes que l’arbre de décision unique précédemment sélectionné, dont la profondeur était limitée à 5. Cette complexité est toutefois compensée par l’agrégation de nombreux arbres et par la sélection aléatoire des variables.

La valeur min_samples_leaf = 2 autorise des feuilles relativement petites. En parallèle, min_samples_split = 10 impose qu’un nœud contienne au moins 10 observations avant de pouvoir être divisé. Cette combinaison permet aux arbres de représenter des structures assez fines tout en limitant certaines divisions fondées sur de très petits groupes.

La valeur max_features = "sqrt" signifie qu’à chaque séparation, chaque arbre n’examine qu’environ la racine carrée du nombre total de variables. Avec 15 variables explicatives, cela correspond à environ :

\[ \sqrt{15} \approx 3{,}9, \]

soit approximativement 4 variables candidates à chaque nœud. Cette randomisation réduit la corrélation entre les arbres et améliore l’intérêt de leur agrégation.

Le critère retenu est log_loss. Les séparations sont donc choisies afin d’améliorer la qualité probabiliste des prédictions, plutôt qu’en utilisant directement l’impureté de Gini.

La forêt utilise des échantillons bootstrap, puisque bootstrap = True. Chaque arbre est entraîné sur un échantillon obtenu par tirage avec remise à partir du jeu d’apprentissage.

Aucune pondération particulière des classes n’est sélectionnée :

class_weight = None

La validation croisée n’a donc pas jugé nécessaire de compenser artificiellement le léger déséquilibre entre les classes. Cela reste cohérent avec la répartition relativement équilibrée du jeu de données.

La ROC-AUC moyenne obtenue en validation croisée est de :

\[ 0{,}9325. \]

Cette performance est nettement supérieure à celle de l’arbre de décision unique, qui obtenait environ \(0{,}908\). Elle dépasse également légèrement les modèles logistiques, dont la ROC-AUC moyenne de validation était proche de \(0{,}925\). Cet avantage reste toutefois modéré et doit être confirmé sur le jeu de test.

14.7 Résultats de validation croisée

Afficher le code
random_forest_cv_results = pd.DataFrame(
    random_forest_search.cv_results_
)

random_forest_cv_summary = (
    random_forest_cv_results[
        [
            "param_n_estimators",
            "param_criterion",
            "param_max_depth",
            "param_min_samples_split",
            "param_min_samples_leaf",
            "param_max_features",
            "param_class_weight",
            "mean_train_roc_auc",
            "mean_test_roc_auc",
            "std_test_roc_auc",
            "mean_test_accuracy",
            "mean_test_balanced_accuracy",
            "mean_test_precision",
            "mean_test_recall",
            "mean_test_f1",
            "rank_test_roc_auc",
        ]
    ]
    .sort_values(
        "rank_test_roc_auc"
    )
    .reset_index(drop=True)
)

display(
    random_forest_cv_summary
    .head(20)
    .round(4)
)
param_n_estimators param_criterion param_max_depth param_min_samples_split param_min_samples_leaf param_max_features param_class_weight mean_train_roc_auc mean_test_roc_auc std_test_roc_auc mean_test_accuracy mean_test_balanced_accuracy mean_test_precision mean_test_recall mean_test_f1 rank_test_roc_auc
0 700 log_loss 10 10 2 sqrt NaN 0.9855 0.9325 0.0230 0.8624 0.8573 0.8569 0.9065 0.8800 1
1 700 gini 12 10 2 sqrt NaN 0.9844 0.9323 0.0221 0.8611 0.8558 0.8548 0.9065 0.8789 2
2 300 log_loss 12 10 2 sqrt balanced 0.9860 0.9314 0.0239 0.8624 0.8587 0.8658 0.8942 0.8784 3
3 700 gini 10 10 5 sqrt balanced 0.9733 0.9312 0.0223 0.8652 0.8609 0.8637 0.9016 0.8813 4
4 300 entropy 8 2 5 log2 balanced 0.9734 0.9311 0.0232 0.8652 0.8606 0.8619 0.9040 0.8816 5
5 700 log_loss 12 20 2 sqrt NaN 0.9725 0.9310 0.0238 0.8597 0.8537 0.8496 0.9114 0.8786 6
6 500 log_loss None 2 5 log2 balanced 0.9752 0.9308 0.0237 0.8638 0.8597 0.8636 0.8991 0.8799 7
7 700 entropy 8 2 2 sqrt balanced 0.9890 0.9306 0.0234 0.8652 0.8615 0.8679 0.8966 0.8808 8
8 1000 entropy 6 10 5 sqrt balanced_subsample 0.9668 0.9306 0.0225 0.8611 0.8566 0.8597 0.8991 0.8778 9
9 300 entropy 10 10 1 log2 NaN 0.9884 0.9303 0.0244 0.8570 0.8512 0.8490 0.9065 0.8758 10
10 1000 log_loss 6 5 2 sqrt NaN 0.9758 0.9302 0.0236 0.8556 0.8500 0.8487 0.9040 0.8744 11
11 1000 gini 8 20 2 log2 balanced_subsample 0.9686 0.9301 0.0231 0.8638 0.8591 0.8609 0.9040 0.8808 12
12 700 entropy 8 20 1 log2 balanced_subsample 0.9719 0.9300 0.0236 0.8652 0.8603 0.8618 0.9065 0.8824 13
13 300 entropy 6 5 2 sqrt NaN 0.9757 0.9296 0.0241 0.8597 0.8539 0.8509 0.9089 0.8781 14
14 700 entropy 5 10 2 log2 balanced_subsample 0.9638 0.9295 0.0232 0.8611 0.8563 0.8583 0.9016 0.8784 15
15 700 gini None 20 1 log2 balanced_subsample 0.9732 0.9294 0.0231 0.8638 0.8591 0.8613 0.9040 0.8810 16
16 500 entropy 8 2 1 log2 balanced 0.9952 0.9292 0.0229 0.8638 0.8599 0.8668 0.8966 0.8798 17
17 200 gini 6 5 1 sqrt balanced 0.9774 0.9291 0.0227 0.8638 0.8594 0.8628 0.9016 0.8805 18
18 300 log_loss 5 10 2 log2 balanced_subsample 0.9634 0.9290 0.0248 0.8638 0.8591 0.8601 0.9040 0.8807 19
19 200 log_loss 12 2 2 sqrt balanced_subsample 0.9957 0.9288 0.0221 0.8611 0.8564 0.8580 0.9016 0.8782 20
NoteInterprétation de la validation croisée

La meilleure configuration atteint une ROC-AUC moyenne d’apprentissage de \(0{,}9855\) et une ROC-AUC moyenne de validation de \(0{,}9325\).

L’écart entre les deux scores est donc d’environ :

\[ 0{,}9855 - 0{,}9325 = 0{,}053. \]

Cet écart montre que la forêt s’ajuste très fortement aux données d’apprentissage. Il existe donc un certain niveau de surajustement, mais celui-ci reste partiellement contrôlé puisque la ROC-AUC de validation demeure élevée.

L’écart-type de la ROC-AUC de validation est égal à \(0{,}023\). Les performances varient donc légèrement selon les plis, mais restent globalement stables. La dispersion est même plus faible que celle observée avec plusieurs modèles logistiques, dont l’écart-type était proche de \(0{,}036\).

Les meilleures configurations sont très proches les unes des autres. Les deux premières obtiennent respectivement :

  • \(0{,}9325\) avec le critère log_loss ;
  • \(0{,}9323\) avec le critère gini.

L’écart de \(0{,}0002\) est négligeable. Le rang exact entre ces deux configurations ne doit donc pas être surinterprété.

Plus généralement, les vingt meilleures combinaisons présentent des ROC-AUC moyennes comprises approximativement entre \(0{,}9288\) et \(0{,}9325\). La recherche met ainsi en évidence un plateau de bonnes performances plutôt qu’une configuration nettement supérieure à toutes les autres.

Plusieurs choix d’hyperparamètres produisent des résultats similaires :

  • entre 300 et 1 000 arbres ;
  • des profondeurs comprises entre 6 et 12, voire sans limite ;
  • les critères gini, entropy et log_loss ;
  • avec ou sans pondération des classes ;
  • avec sqrt ou log2 pour max_features.

Cela montre que la forêt est relativement robuste au choix précis des hyperparamètres dans cette zone.

Les configurations les plus complexes obtiennent parfois des ROC-AUC d’apprentissage proches de \(0{,}995\), sans amélioration correspondante de la validation. Par exemple, une configuration atteint \(0{,}9957\) en apprentissage mais seulement \(0{,}9288\) en validation. Cela confirme que l’augmentation de la complexité peut renforcer le surajustement sans améliorer la généralisation.

La meilleure configuration n’est d’ailleurs pas celle qui maximise l’accuracy ou le F1-score moyen. Elle est sélectionnée parce que GridSearchCV ou RandomizedSearchCV utilise la ROC-AUC comme métrique de référence. Certaines configurations légèrement moins bien classées en ROC-AUC présentent une accuracy ou un F1-score un peu supérieurs.

En conclusion, la forêt aléatoire offre une ROC-AUC de validation élevée et relativement stable. Elle surajuste davantage que les modèles logistiques, mais elle améliore leur ROC-AUC moyenne de validation. La présence d’un plateau de performances suggère que le modèle ne dépend pas fortement d’une combinaison unique d’hyperparamètres.

14.8 Score Out-of-Bag

Lorsque bootstrap=True, les observations non sélectionnées pour entraîner un arbre peuvent être utilisées pour évaluer cet arbre. Elles sont appelées observations out-of-bag.

Nous réentraînons la meilleure configuration avec oob_score=True.

Afficher le code
random_forest_oob = RandomForestClassifier(
    **best_random_forest_parameters,
    oob_score=True,
    random_state=RANDOM_STATE,
    n_jobs=-1,
)

random_forest_oob.fit(
    X_train_encoded,
    y_train,
)

print(
    "Accuracy Out-of-Bag : "
    f"{random_forest_oob.oob_score_:.3f}"
)
Accuracy Out-of-Bag : 0.862

Le score OOB constitue une estimation interne de la capacité de généralisation. Il ne remplace toutefois ni la validation croisée ni le jeu de test.

14.9 Influence du nombre d’arbres

Afficher le code
forest_estimators_rows = []

estimators_values = [
    10,
    25,
    50,
    100,
    200,
    300,
    500,
    700,
    1000,
]

for number_of_trees in estimators_values:
    current_forest = RandomForestClassifier(
        **{
            **best_random_forest_parameters,
            "n_estimators": number_of_trees,
        },
        random_state=RANDOM_STATE,
        n_jobs=-1,
    )

    current_scores = cross_validate(
        current_forest,
        X_train_encoded,
        y_train,
        cv=cv,
        scoring="roc_auc",
        return_train_score=True,
        n_jobs=-1,
    )

    forest_estimators_rows.append(
        {
            "n_estimators":
                number_of_trees,
            "roc_auc_train":
                current_scores[
                    "train_score"
                ].mean(),
            "roc_auc_validation":
                current_scores[
                    "test_score"
                ].mean(),
            "ecart_type_validation":
                current_scores[
                    "test_score"
                ].std(),
        }
    )

forest_estimators_results = pd.DataFrame(
    forest_estimators_rows
)

display(
    forest_estimators_results.round(4)
)
n_estimators roc_auc_train roc_auc_validation ecart_type_validation
0 10 0.9803 0.9232 0.0270
1 25 0.9839 0.9304 0.0243
2 50 0.9848 0.9309 0.0248
3 100 0.9853 0.9315 0.0238
4 200 0.9856 0.9312 0.0247
5 300 0.9855 0.9317 0.0231
6 500 0.9855 0.9327 0.0228
7 700 0.9855 0.9325 0.0230
8 1000 0.9854 0.9324 0.0231
Afficher le code
plt.figure(figsize=(8, 5))

plt.plot(
    forest_estimators_results[
        "n_estimators"
    ],
    forest_estimators_results[
        "roc_auc_train"
    ],
    marker="o",
    label="Apprentissage",
)

plt.plot(
    forest_estimators_results[
        "n_estimators"
    ],
    forest_estimators_results[
        "roc_auc_validation"
    ],
    marker="o",
    label="Validation",
)

plt.xlabel("Nombre d’arbres")
plt.ylabel("ROC-AUC moyenne")
plt.title(
    "Influence du nombre d’arbres"
)
plt.legend()
plt.tight_layout()
plt.show()
Figure 24: ROC-AUC selon le nombre d’arbres
NoteInterprétation du nombre d’arbres

La ROC-AUC de validation augmente rapidement lorsque le nombre d’arbres passe de 10 à environ 50–100 arbres.

Avec seulement 10 arbres, la performance de validation est proche de 0,923. Elle atteint ensuite environ 0,930 avec 25 à 50 arbres, puis se stabilise autour de 0,931–0,933.

À partir d’environ 100 à 200 arbres, l’ajout de nouveaux arbres apporte donc très peu de gain prédictif. La courbe de validation forme un plateau, ce qui indique que les estimations de la forêt sont déjà suffisamment stabilisées.

La ROC-AUC d’apprentissage suit le même comportement. Elle augmente rapidement au début, puis se stabilise autour de 0,985. L’écart entre apprentissage et validation reste ensuite quasiment constant lorsque le nombre d’arbres augmente.

L’ajout d’arbres supplémentaires ne semble donc pas accentuer le surajustement. Il améliore surtout la stabilité de l’agrégation, au prix d’un temps de calcul et d’une consommation mémoire plus élevés.

Le choix de 700 arbres est cohérent avec la recherche aléatoire, mais il n’apporte probablement qu’un gain très faible par rapport à une forêt de 300 ou 500 arbres. Les performances observées à 500, 700 et 1 000 arbres sont pratiquement identiques.

En conclusion :

  • le gain principal est obtenu avec les premiers 50 à 100 arbres ;
  • la ROC-AUC se stabilise clairement après environ 200 arbres ;
  • 700 arbres garantissent une prédiction stable ;
  • une forêt de 300 à 500 arbres pourrait constituer une alternative plus économique avec une performance presque identique.

14.10 Importance fondée sur l’impureté

Afficher le code
random_forest_importance = pd.DataFrame(
    {
        "variable":
            X_train_encoded.columns,
        "importance":
            random_forest_model
            .feature_importances_,
    }
)

random_forest_importance = (
    random_forest_importance
    .sort_values(
        "importance",
        ascending=False,
    )
    .reset_index(drop=True)
)

display(
    random_forest_importance.round(4)
)
variable importance
0 ST_Slope_Up 0.1814
1 ST_Slope_Flat 0.1284
2 Oldpeak 0.1112
3 Cholesterol 0.0987
4 MaxHR 0.0965
5 ExerciseAngina_Y 0.0914
6 Age 0.0636
7 RestingBP 0.0550
8 ChestPainType_ATA 0.0485
9 Sex_M 0.0380
10 ChestPainType_NAP 0.0357
11 FastingBS_1 0.0243
12 RestingECG_Normal 0.0120
13 ChestPainType_TA 0.0077
14 RestingECG_ST 0.0077
Afficher le code
random_forest_importance_plot = (
    random_forest_importance
    .sort_values("importance")
)

plt.figure(figsize=(9, 7))

plt.barh(
    random_forest_importance_plot[
        "variable"
    ],
    random_forest_importance_plot[
        "importance"
    ],
)

plt.xlabel(
    "Importance fondée sur l’impureté"
)
plt.ylabel("Variable")
plt.title(
    "Importance des variables "
    "— Forêt aléatoire"
)
plt.tight_layout()
plt.show()
Figure 25: Importance des variables dans la forêt aléatoire

14.11 Importance par permutation

Afficher le code
random_forest_permutation = (
    permutation_importance(
        random_forest_model,
        X_test_encoded,
        y_test,
        scoring="roc_auc",
        n_repeats=30,
        random_state=RANDOM_STATE,
        n_jobs=-1,
    )
)

random_forest_permutation_importance = (
    pd.DataFrame(
        {
            "variable":
                X_test_encoded.columns,
            "importance_moyenne":
                random_forest_permutation
                .importances_mean,
            "ecart_type":
                random_forest_permutation
                .importances_std,
        }
    )
    .sort_values(
        "importance_moyenne",
        ascending=False,
    )
    .reset_index(drop=True)
)

display(
    random_forest_permutation_importance
    .round(4)
)
variable importance_moyenne ecart_type
0 Cholesterol 0.0208 0.0060
1 ST_Slope_Up 0.0197 0.0114
2 Sex_M 0.0191 0.0057
3 ExerciseAngina_Y 0.0143 0.0052
4 ChestPainType_ATA 0.0125 0.0043
5 FastingBS_1 0.0089 0.0023
6 Oldpeak 0.0085 0.0062
7 MaxHR 0.0082 0.0058
8 RestingBP 0.0069 0.0025
9 ST_Slope_Flat 0.0047 0.0072
10 RestingECG_Normal 0.0007 0.0012
11 ChestPainType_TA 0.0006 0.0006
12 Age 0.0003 0.0026
13 ChestPainType_NAP 0.0003 0.0039
14 RestingECG_ST 0.0001 0.0005
Afficher le code
random_forest_permutation_plot = (
    random_forest_permutation_importance
    .sort_values("importance_moyenne")
)

plt.figure(figsize=(9, 7))

plt.barh(
    random_forest_permutation_plot[
        "variable"
    ],
    random_forest_permutation_plot[
        "importance_moyenne"
    ],
    xerr=random_forest_permutation_plot[
        "ecart_type"
    ],
)

plt.axvline(
    0,
    linewidth=1,
)

plt.xlabel(
    "Diminution moyenne de la ROC-AUC"
)
plt.ylabel("Variable")
plt.title(
    "Importance par permutation "
    "— Forêt aléatoire"
)
plt.tight_layout()
plt.show()
Figure 26: Importance par permutation dans la forêt aléatoire

L’importance par permutation mesure la diminution d’un score lorsque les valeurs d’une variable sont mélangées. Elle doit être interprétée sur un modèle dont les performances prédictives sont satisfaisantes. :contentReferenceoaicite:2

NoteInterprétation des importances

L’importance fondée sur la réduction de l’impureté est beaucoup plus répartie dans la forêt aléatoire que dans l’arbre de décision unique.

Les variables les plus importantes selon cette première mesure sont :

  • ST_Slope_Up, avec une importance de 0,1814 ;
  • ST_Slope_Flat, avec 0,1284 ;
  • Oldpeak, avec 0,1112 ;
  • Cholesterol, avec 0,0987 ;
  • MaxHR, avec 0,0965 ;
  • ExerciseAngina_Y, avec 0,0914.

Dans l’arbre unique, ST_Slope_Up représentait à elle seule environ 66,8 % de l’importance totale. Dans la forêt, son importance diminue à environ 18,1 %, car les différents arbres utilisent des variables et des seuils différents. La prédiction dépend donc moins fortement d’une seule variable.

L’importance par permutation produit une hiérarchie différente. Les variables dont la permutation dégrade le plus la ROC-AUC sont :

  • Cholesterol, avec une diminution moyenne de 0,0208 ;
  • ST_Slope_Up, avec 0,0197 ;
  • Sex_M, avec 0,0191 ;
  • ExerciseAngina_Y, avec 0,0143 ;
  • ChestPainType_ATA, avec 0,0125 ;
  • FastingBS_1, avec 0,0089.

Ces variables semblent apporter l’information prédictive la plus difficile à remplacer par les autres variables du modèle.

Les deux méthodes sont cohérentes pour plusieurs variables importantes, notamment ST_Slope_Up, Cholesterol, ExerciseAngina_Y, Oldpeak et MaxHR. Elles confirment que ces caractéristiques participent de manière importante aux prédictions de la forêt.

Certaines différences sont néanmoins importantes. Par exemple, ST_Slope_Flat est classée deuxième selon la réduction de l’impureté, mais sa permutation ne réduit la ROC-AUC que de 0,0047, avec un écart-type de 0,0072. Son importance prédictive propre apparaît donc faible ou instable.

De même, Age possède une importance fondée sur l’impureté de 0,0636, mais son importance par permutation est presque nulle. La forêt utilise donc régulièrement cette variable pour construire des divisions, sans que sa suppression individuelle détériore nettement les performances.

Ce phénomène peut s’expliquer par la redondance entre variables. Lorsque deux variables contiennent des informations proches, la forêt peut utiliser l’une ou l’autre selon les arbres. La permutation d’une seule variable a alors un effet limité, car les autres peuvent partiellement la remplacer.

À l’inverse, Sex_M et ChestPainType_ATA ont une importance fondée sur l’impureté relativement modérée, mais une importance par permutation élevée. Même si elles sont moins souvent utilisées dans les arbres, elles fournissent une information complémentaire utile à la discrimination.

Les variables situées en bas du classement par permutation, telles que RestingECG_Normal, ChestPainType_TA, Age, ChestPainType_NAP et RestingECG_ST, ont une contribution marginale au score obtenu sur le jeu de test. Leurs diminutions moyennes de ROC-AUC sont proches de zéro et parfois inférieures à leur écart-type.

La hiérarchie obtenue reste globalement cohérente avec les modèles logistiques. Les variables ST_Slope_Up, Sex_M, ExerciseAngina_Y, FastingBS_1, Oldpeak, MaxHR et les modalités de ChestPainType présentaient déjà des coefficients importants.

La forêt accorde cependant une place plus importante à Cholesterol et RestingBP. Cette différence peut provenir de sa capacité à exploiter des seuils et des interactions non linéaires, alors que la régression logistique estime principalement des effets linéaires globaux.

En conclusion, la forêt aléatoire repose sur un ensemble plus diversifié de variables que l’arbre unique. L’importance par permutation suggère que les variables les plus robustement utiles sont surtout Cholesterol, ST_Slope_Up, Sex_M, ExerciseAngina_Y, ChestPainType_ATA et FastingBS_1.

14.12 Prédictions sur le jeu de test

Afficher le code
y_pred_random_forest = (
    random_forest_model.predict(
        X_test_encoded
    )
)

y_proba_random_forest = (
    random_forest_model.predict_proba(
        X_test_encoded
    )[:, 1]
)

14.13 Indicateurs du modèle

Afficher le code
(
    tn_random_forest,
    fp_random_forest,
    fn_random_forest,
    tp_random_forest,
) = confusion_matrix(
    y_test,
    y_pred_random_forest,
).ravel()

sensitivity_random_forest = (
    tp_random_forest
    /
    (
        tp_random_forest
        + fn_random_forest
    )
)

specificity_random_forest = (
    tn_random_forest
    /
    (
        tn_random_forest
        + fp_random_forest
    )
)

random_forest_metrics = pd.Series(
    {
        "Accuracy":
            accuracy_score(
                y_test,
                y_pred_random_forest,
            ),
        "Balanced accuracy":
            balanced_accuracy_score(
                y_test,
                y_pred_random_forest,
            ),
        "Précision":
            precision_score(
                y_test,
                y_pred_random_forest,
            ),
        "Sensibilité / Recall":
            sensitivity_random_forest,
        "Spécificité":
            specificity_random_forest,
        "F1-score":
            f1_score(
                y_test,
                y_pred_random_forest,
            ),
        "ROC-AUC":
            roc_auc_score(
                y_test,
                y_proba_random_forest,
            ),
        "Average Precision":
            average_precision_score(
                y_test,
                y_proba_random_forest,
            ),
    },
    name="Forêt aléatoire",
)

display(
    random_forest_metrics
    .to_frame("score")
    .round(3)
)
score
Accuracy 0.880
Balanced accuracy 0.875
Précision 0.870
Sensibilité / Recall 0.922
Spécificité 0.829
F1-score 0.895
ROC-AUC 0.931
Average Precision 0.930

14.14 Rapport de classification

Afficher le code
print(
    classification_report(
        y_test,
        y_pred_random_forest,
        target_names=[
            "Absence de maladie",
            "Présence de maladie",
        ],
        digits=3,
    )
)
                     precision    recall  f1-score   support

 Absence de maladie      0.895     0.829     0.861        82
Présence de maladie      0.870     0.922     0.895       102

           accuracy                          0.880       184
          macro avg      0.883     0.875     0.878       184
       weighted avg      0.881     0.880     0.880       184

14.15 Matrice de confusion

Afficher le code
ConfusionMatrixDisplay.from_predictions(
    y_test,
    y_pred_random_forest,
    display_labels=[
        "Absence",
        "Présence",
    ],
)

plt.title(
    "Matrice de confusion "
    "— Forêt aléatoire"
)
plt.tight_layout()
plt.show()
Figure 27: Matrice de confusion de la forêt aléatoire
ImportantInterprétation de la matrice de confusion
  • vrais positifs : np.int64(94) ;
  • vrais négatifs : np.int64(68) ;
  • faux positifs : np.int64(14) ;
  • faux négatifs : np.int64(8).

La forêt identifie correctement 94 des 102 patients présentant réellement une maladie cardiaque. Sa sensibilité atteint ainsi 92,2 %.

Elle produit 8 faux négatifs, contre 15 pour l’arbre de décision. L’agrégation de plusieurs arbres améliore donc nettement la détection des patients malades par rapport à l’arbre unique.

Parmi les 82 patients sans maladie, 68 sont correctement classés et 14 sont prédits à tort comme positifs. La spécificité est donc de 82,9 %.

Les performances globales de la forêt sont :

  • accuracy : 88,0 % ;
  • balanced accuracy : 87,5 % ;
  • précision : 87,0 % ;
  • sensibilité : 92,2 % ;
  • spécificité : 82,9 % ;
  • F1-score : 0,895 ;
  • ROC-AUC : 0,931 ;
  • Average Precision : 0,930.

La forêt aléatoire améliore clairement l’arbre de décision sur l’ensemble des indicateurs. Elle réduit notamment le nombre de faux négatifs de 15 à 8 et augmente la ROC-AUC de 0,876 à 0,931.

Par rapport aux modèles logistiques, les résultats sont très proches. Les modèles logistiques produisaient 7 faux négatifs, 14 faux positifs et une sensibilité de 93,1 %. La forêt produit donc un faux négatif supplémentaire et une sensibilité légèrement plus faible, à 92,2 %.

La ROC-AUC de la forêt, égale à 0,931, est toutefois légèrement supérieure aux valeurs obtenues par la plupart des régressions logistiques, comprises entre 0,929 et 0,930. Cet écart reste très faible et ne suffit pas, à lui seul, à conclure à une supériorité nette.

L’Average Precision de 0,930 est en revanche légèrement inférieure à celle des modèles logistiques, qui atteignaient environ 0,936 à 0,937.

En conclusion, la forêt aléatoire constitue une amélioration importante par rapport à l’arbre unique. Elle atteint des performances comparables aux modèles logistiques et présente une très bonne capacité de discrimination. Néanmoins, au seuil de 0,5, elle ne réduit pas le nombre de faux négatifs par rapport aux régressions logistiques et reste moins simple à interpréter.

14.16 Courbe ROC

Afficher le code
random_forest_roc_auc = (
    roc_auc_score(
        y_test,
        y_proba_random_forest,
    )
)

RocCurveDisplay.from_predictions(
    y_test,
    y_proba_random_forest,
    name=(
        "Forêt aléatoire — "
        f"AUC = "
        f"{random_forest_roc_auc:.3f}"
    ),
)

plt.plot(
    [0, 1],
    [0, 1],
    linestyle="--",
    label="Modèle aléatoire",
)

plt.title(
    "Courbe ROC — Forêt aléatoire"
)
plt.xlabel("Taux de faux positifs")
plt.ylabel("Taux de vrais positifs")
plt.legend()
plt.tight_layout()
plt.show()
Figure 28: Courbe ROC de la forêt aléatoire

14.17 Courbe précision-rappel

Afficher le code
random_forest_average_precision = (
    average_precision_score(
        y_test,
        y_proba_random_forest,
    )
)

PrecisionRecallDisplay.from_predictions(
    y_test,
    y_proba_random_forest,
    name=(
        "Forêt aléatoire — "
        f"AP = "
        f"{random_forest_average_precision:.3f}"
    ),
)

plt.title(
    "Courbe précision-rappel "
    "— Forêt aléatoire"
)
plt.xlabel("Rappel")
plt.ylabel("Précision")
plt.legend()
plt.tight_layout()
plt.show()
Figure 29: Courbe précision-rappel de la forêt aléatoire

14.18 Comparaison avec les modèles précédents

Afficher le code
model_comparison_with_random_forest = (
    pd.concat(
        [
            logistic_metrics.rename(
                "Non pénalisée"
            ),
            ridge_metrics.rename(
                "Ridge"
            ),
            lasso_metrics.rename(
                "Lasso"
            ),
            elastic_metrics.rename(
                "Elastic Net"
            ),
            tree_metrics.rename(
                "Arbre de décision"
            ),
            random_forest_metrics.rename(
                "Forêt aléatoire"
            ),
        ],
        axis=1,
    )
)

model_comparison_with_random_forest[
    "Différence forêt vs logistique"
] = (
    model_comparison_with_random_forest[
        "Forêt aléatoire"
    ]
    -
    model_comparison_with_random_forest[
        "Non pénalisée"
    ]
)

model_comparison_with_random_forest[
    "Différence forêt vs arbre"
] = (
    model_comparison_with_random_forest[
        "Forêt aléatoire"
    ]
    -
    model_comparison_with_random_forest[
        "Arbre de décision"
    ]
)

display(
    model_comparison_with_random_forest
    .round(3)
)
Non pénalisée Ridge Lasso Elastic Net Arbre de décision Forêt aléatoire Différence forêt vs logistique Différence forêt vs arbre
Accuracy 0.886 0.886 0.886 0.886 0.832 0.880 -0.005 0.049
Balanced accuracy 0.880 0.880 0.880 0.880 0.829 0.875 -0.005 0.047
Précision 0.872 0.872 0.872 0.872 0.845 0.870 -0.001 0.026
Sensibilité / Recall 0.931 0.931 0.931 0.931 0.853 0.922 -0.010 0.069
Spécificité 0.829 0.829 0.829 0.829 0.805 0.829 0.000 0.024
F1-score 0.900 0.900 0.900 0.900 0.849 0.895 -0.005 0.046
ROC-AUC 0.929 0.930 0.929 0.929 0.876 0.931 0.002 0.055
Average Precision 0.936 0.937 0.936 0.936 0.853 0.930 -0.006 0.077
ImportantConclusion de la forêt aléatoire

La forêt aléatoire améliore nettement les performances obtenues avec l’arbre de décision unique.

En validation croisée, elle atteint une ROC-AUC moyenne de \(0{,}9325\), contre environ \(0{,}908\) pour l’arbre. L’agrégation de plusieurs arbres permet donc de réduire la variabilité du modèle et d’améliorer sa capacité de généralisation.

Sur le jeu de test, la forêt obtient une ROC-AUC de \(0{,}931\), contre \(0{,}876\) pour l’arbre de décision. Elle améliore également l’accuracy, la balanced accuracy, la sensibilité et le score F1.

Le nombre de faux négatifs diminue fortement :

  • arbre de décision : 15 faux négatifs ;
  • forêt aléatoire : 8 faux négatifs.

La forêt détecte donc beaucoup mieux les patients malades que l’arbre unique. Elle conserve toutefois un faux négatif de plus que les modèles logistiques, qui en produisaient 7.

Les performances de la forêt sont globalement comparables à celles des régressions logistiques. Sa ROC-AUC de test est légèrement supérieure, mais l’écart reste très faible. Son Average Precision est en revanche légèrement inférieure. Il n’est donc pas possible d’affirmer qu’elle est nettement supérieure aux modèles logistiques.

La ROC-AUC d’apprentissage, proche de \(0{,}986\), reste supérieure à la ROC-AUC de validation de \(0{,}933\). Cet écart d’environ \(0{,}053\) indique un certain surajustement. Celui-ci reste cependant maîtrisé, car les performances de validation sont élevées, relativement stables et confirmées sur le jeu de test.

Les importances sont beaucoup plus réparties que dans l’arbre unique. ST_Slope_Up reste une variable importante, mais elle ne concentre plus à elle seule la majorité de l’importance. La forêt exploite également Cholesterol, Oldpeak, MaxHR, ExerciseAngina_Y, Sex_M, ChestPainType_ATA et FastingBS_1.

Cette répartition montre que la forêt utilise plusieurs chemins et plusieurs combinaisons de variables. Elle est ainsi moins dépendante d’une seule règle de décision et plus robuste qu’un arbre isolé.

La perte d’interprétabilité par rapport à l’arbre unique est donc compensée par un gain prédictif important. En revanche, par rapport aux régressions logistiques, le gain de performance reste faible alors que le modèle devient plus complexe à expliquer.

En conclusion, la forêt aléatoire constitue un meilleur compromis que l’arbre de décision entre non-linéarité, stabilité et performance. Elle permet de modéliser des interactions complexes tout en obtenant des résultats proches, voire légèrement supérieurs, à ceux des modèles logistiques.

Cependant, si la priorité est l’interprétabilité et la simplicité, la régression logistique Ridge reste très compétitive. Si la priorité est la capacité à modéliser des relations non linéaires, la forêt aléatoire constitue à ce stade le modèle le plus intéressant de l’étude.

15 Gradient Boosting

15.1 Principe du modèle

Le Gradient Boosting construit progressivement un modèle composé de plusieurs arbres de décision.

Contrairement à la forêt aléatoire, dans laquelle les arbres sont entraînés indépendamment, les arbres du Gradient Boosting sont construits séquentiellement.

Chaque nouvel arbre tente de corriger les erreurs résiduelles commises par l’ensemble des arbres précédents.

Pour une observation \(x\), le modèle construit une fonction additive :

\[ F_M(x) = F_0(x) + \sum_{m=1}^{M} \eta h_m(x), \]

où :

  • \(M\) représente le nombre d’arbres ;
  • \(h_m(x)\) représente le nouvel arbre ajouté à l’étape \(m\) ;
  • \(\eta\) représente le taux d’apprentissage learning_rate.

Une faible valeur de learning_rate réduit la contribution de chaque arbre et nécessite généralement un nombre d’arbres plus élevé.

15.2 Données utilisées

Les arbres ne nécessitent pas de standardisation. Nous utilisons donc les variables numériques dans leurs unités originales et les variables catégorielles encodées.

Afficher le code
print(
    "Dimensions du jeu d’apprentissage :",
    X_train_encoded.shape,
)

print(
    "Dimensions du jeu de test :",
    X_test_encoded.shape,
)
Dimensions du jeu d’apprentissage : (734, 15)
Dimensions du jeu de test : (184, 15)

15.3 Importation du modèle

Afficher le code
from sklearn.ensemble import (
    GradientBoostingClassifier,
)

from sklearn.model_selection import (
    RandomizedSearchCV,
    cross_validate,
)

15.4 Modèle de base

Afficher le code
gradient_boosting_base = (
    GradientBoostingClassifier(
        random_state=RANDOM_STATE,
    )
)

15.5 Recherche des hyperparamètres

Les principaux hyperparamètres étudiés sont :

  • n_estimators : nombre d’arbres ajoutés séquentiellement ;
  • learning_rate : contribution de chaque arbre ;
  • max_depth : profondeur des arbres individuels ;
  • min_samples_split : nombre minimal d’observations pour diviser un nœud ;
  • min_samples_leaf : nombre minimal d’observations dans une feuille ;
  • subsample : proportion du jeu d’apprentissage utilisée pour chaque arbre ;
  • max_features : nombre de variables examinées lors des divisions.
Afficher le code
gradient_boosting_parameter_grid = {
    "n_estimators": [
        50,
        100,
        150,
        200,
        300,
        500,
    ],
    "learning_rate": [
        0.01,
        0.03,
        0.05,
        0.1,
        0.2,
    ],
    "max_depth": [
        1,
        2,
        3,
        4,
        5,
    ],
    "min_samples_split": [
        2,
        5,
        10,
        20,
    ],
    "min_samples_leaf": [
        1,
        2,
        5,
        10,
        20,
    ],
    "subsample": [
        0.6,
        0.8,
        1.0,
    ],
    "max_features": [
        None,
        "sqrt",
        "log2",
    ],
}

Une recherche aléatoire est utilisée afin d’explorer plusieurs combinaisons sans tester exhaustivement tout l’espace des hyperparamètres.

Afficher le code
gradient_boosting_search = (
    RandomizedSearchCV(
        estimator=gradient_boosting_base,
        param_distributions=(
            gradient_boosting_parameter_grid
        ),
        n_iter=100,
        scoring={
            "roc_auc": "roc_auc",
            "accuracy": "accuracy",
            "balanced_accuracy":
                "balanced_accuracy",
            "precision": "precision",
            "recall": "recall",
            "f1": "f1",
        },
        refit="roc_auc",
        cv=cv,
        random_state=RANDOM_STATE,
        n_jobs=-1,
        return_train_score=True,
        verbose=1,
    )
)

gradient_boosting_search.fit(
    X_train_encoded,
    y_train,
)
Fitting 5 folds for each of 100 candidates, totalling 500 fits
RandomizedSearchCV(cv=StratifiedKFold(n_splits=5, random_state=42, shuffle=True),
                   estimator=GradientBoostingClassifier(random_state=42),
                   n_iter=100, n_jobs=-1,
                   param_distributions={'learning_rate': [0.01, 0.03, 0.05, 0.1,
                                                          0.2],
                                        'max_depth': [1, 2, 3, 4, 5],
                                        'max_features': [None, 'sqrt', 'log2'],
                                        'min_samples_leaf': [1, 2, 5, 10, 20],
                                        'min_samples_split': [2, 5, 10, 20],
                                        'n_estimators': [50, 100, 150, 200, 300,
                                                         500],
                                        'subsample': [0.6, 0.8, 1.0]},
                   random_state=42, refit='roc_auc', return_train_score=True,
                   scoring={'accuracy': 'accuracy',
                            'balanced_accuracy': 'balanced_accuracy',
                            'f1': 'f1', 'precision': 'precision',
                            'recall': 'recall', 'roc_auc': 'roc_auc'},
                   verbose=1)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

RandomizedSearchCV évalue un nombre déterminé de configurations, puis réentraîne la meilleure configuration sur l’ensemble du jeu d’apprentissage lorsque refit est activé.

15.6 Meilleurs hyperparamètres

Afficher le code
best_gradient_boosting_parameters = (
    gradient_boosting_search.best_params_
)

best_gradient_boosting_cv_auc = (
    gradient_boosting_search.best_score_
)

gradient_boosting_model = (
    gradient_boosting_search.best_estimator_
)

print("Meilleurs hyperparamètres :")

display(
    best_gradient_boosting_parameters
)

print(
    "ROC-AUC moyenne en validation croisée : "
    f"{best_gradient_boosting_cv_auc:.3f}"
)
Meilleurs hyperparamètres :
{'subsample': 0.6,
 'n_estimators': 50,
 'min_samples_split': 5,
 'min_samples_leaf': 5,
 'max_features': 'sqrt',
 'max_depth': 5,
 'learning_rate': 0.1}
ROC-AUC moyenne en validation croisée : 0.934
NoteInterprétation

La meilleure configuration du Gradient Boosting retient 50 arbres avec un taux d’apprentissage égal à 0.1.

Cette combinaison correspond à une progression relativement rapide : chaque arbre contribue de manière assez importante à la correction des erreurs. Le modèle n’a donc pas besoin d’un très grand nombre d’arbres pour atteindre sa meilleure performance en validation croisée.

Les arbres individuels ont une profondeur maximale de 5. Ils peuvent ainsi modéliser des interactions non linéaires relativement complexes entre les variables, tout en restant moins profonds que des arbres totalement libres.

La valeur min_samples_leaf = 5 impose qu’une feuille contienne au moins cinq observations. Cette contrainte limite la création de sous-groupes trop petits et contribue à contrôler le surajustement.

La valeur min_samples_split = 5 signifie qu’un nœud doit contenir au moins cinq observations avant de pouvoir être divisé. En pratique, la contrainte sur la taille minimale des feuilles joue également un rôle important dans le contrôle de la complexité.

Le paramètre subsample = 0.6 indique que chaque nouvel arbre est entraîné sur un échantillon aléatoire contenant 60 % des observations du jeu d’apprentissage. Le modèle correspond donc à un Gradient Boosting stochastique.

Cette sous-utilisation volontaire des observations à chaque étape introduit de la diversité entre les arbres et peut réduire la variance du modèle. Elle joue un rôle comparable à une forme de régularisation.

La valeur max_features = "sqrt" signifie qu’à chaque séparation, l’arbre n’examine qu’environ la racine carrée du nombre total de variables. Avec 15 variables explicatives, cela représente approximativement quatre variables candidates par division.

Cette sélection aléatoire des variables apporte une régularisation supplémentaire et limite la dépendance du modèle à quelques prédicteurs dominants.

La ROC-AUC moyenne obtenue en validation croisée est de :

\[ 0{,}934. \]

Cette valeur est légèrement supérieure à celle de la forêt aléatoire, qui atteignait environ \(0{,}9325\), et à celles des modèles logistiques, proches de \(0{,}925\).

Le gain par rapport à la forêt aléatoire est toutefois faible, de l’ordre de :

\[ 0{,}934 - 0{,}9325 = 0{,}0015. \]

Cet écart est trop faible pour conclure immédiatement à une supériorité nette du Gradient Boosting. Il faudra vérifier l’écart-type de validation, les performances sur le jeu de test et le nombre de faux négatifs.

En conclusion, la validation croisée retient un modèle composé d’un nombre modéré d’arbres relativement profonds, avec une régularisation obtenue grâce à l’utilisation de seulement 60 % des observations et d’un sous-ensemble des variables à chaque étape.

À ce stade, le Gradient Boosting présente la meilleure ROC-AUC moyenne de validation de l’étude, mais son avantage sur la forêt aléatoire reste très modeste.

15.7 Résultats de validation croisée

Afficher le code
gradient_boosting_cv_results = pd.DataFrame(
    gradient_boosting_search.cv_results_
)

gradient_boosting_cv_summary = (
    gradient_boosting_cv_results[
        [
            "param_n_estimators",
            "param_learning_rate",
            "param_max_depth",
            "param_min_samples_split",
            "param_min_samples_leaf",
            "param_subsample",
            "param_max_features",
            "mean_train_roc_auc",
            "mean_test_roc_auc",
            "std_test_roc_auc",
            "mean_test_accuracy",
            "mean_test_balanced_accuracy",
            "mean_test_precision",
            "mean_test_recall",
            "mean_test_f1",
            "rank_test_roc_auc",
        ]
    ]
    .sort_values("rank_test_roc_auc")
    .reset_index(drop=True)
)

display(
    gradient_boosting_cv_summary
    .head(20)
    .round(4)
)
param_n_estimators param_learning_rate param_max_depth param_min_samples_split param_min_samples_leaf param_subsample param_max_features mean_train_roc_auc mean_test_roc_auc std_test_roc_auc mean_test_accuracy mean_test_balanced_accuracy mean_test_precision mean_test_recall mean_test_f1 rank_test_roc_auc
0 50 0.10 5 5 5 0.6 sqrt 0.9888 0.9335 0.0263 0.8651 0.8620 0.8710 0.8917 0.8802 1
1 150 0.03 5 10 2 0.8 log2 0.9930 0.9333 0.0244 0.8638 0.8594 0.8620 0.9016 0.8805 2
2 300 0.01 4 10 20 0.8 log2 0.9609 0.9331 0.0242 0.8720 0.8670 0.8652 0.9139 0.8882 3
3 150 0.03 5 10 5 0.8 log2 0.9907 0.9330 0.0258 0.8638 0.8605 0.8694 0.8917 0.8791 4
4 150 0.03 4 10 20 1.0 log2 0.9701 0.9328 0.0254 0.8665 0.8626 0.8669 0.8991 0.8822 5
5 200 0.03 5 2 5 0.6 sqrt 0.9935 0.9325 0.0277 0.8652 0.8620 0.8702 0.8917 0.8800 6
6 50 0.05 5 5 20 0.8 sqrt 0.9619 0.9322 0.0241 0.8651 0.8600 0.8583 0.9090 0.8822 7
7 200 0.05 5 5 20 0.6 sqrt 0.9878 0.9321 0.0273 0.8692 0.8663 0.8747 0.8942 0.8834 8
8 200 0.03 5 5 5 1.0 sqrt 0.9966 0.9318 0.0226 0.8665 0.8632 0.8709 0.8941 0.8813 9
9 100 0.05 4 5 1 1.0 sqrt 0.9862 0.9317 0.0231 0.8638 0.8596 0.8645 0.8990 0.8803 10
10 200 0.03 3 2 5 0.6 log2 0.9701 0.9315 0.0302 0.8638 0.8596 0.8633 0.8991 0.8801 11
11 50 0.20 3 10 5 0.6 sqrt 0.9788 0.9314 0.0318 0.8747 0.8723 0.8830 0.8942 0.8879 12
12 50 0.01 5 20 10 0.8 log2 0.9579 0.9313 0.0222 0.8542 0.8449 0.8288 0.9336 0.8772 13
13 150 0.03 3 10 1 0.6 log2 0.9657 0.9312 0.0275 0.8652 0.8606 0.8625 0.9040 0.8819 14
14 150 0.10 3 2 10 0.6 sqrt 0.9876 0.9312 0.0355 0.8679 0.8671 0.8870 0.8744 0.8796 15
15 500 0.01 3 10 2 1.0 log2 0.9690 0.9312 0.0260 0.8652 0.8608 0.8640 0.9015 0.8815 16
16 100 0.05 5 10 5 0.6 log2 0.9896 0.9311 0.0257 0.8706 0.8675 0.8748 0.8966 0.8848 17
17 150 0.01 5 20 20 1.0 log2 0.9599 0.9309 0.0223 0.8597 0.8531 0.8461 0.9164 0.8790 18
18 50 0.20 2 10 10 1.0 log2 0.9582 0.9308 0.0303 0.8651 0.8612 0.8651 0.8991 0.8811 19
19 500 0.03 3 5 10 0.6 log2 0.9880 0.9308 0.0328 0.8747 0.8727 0.8841 0.8917 0.8874 20
NoteInterprétation de la validation croisée

La meilleure configuration atteint une ROC-AUC moyenne d’apprentissage de \(0{,}9888\) et une ROC-AUC moyenne de validation de \(0{,}9335\).

L’écart entre ces deux performances est donc d’environ :

\[ 0{,}9888 - 0{,}9335 = 0{,}0553. \]

Cet écart indique que le Gradient Boosting s’ajuste très fortement aux données d’apprentissage. Un certain surajustement est donc présent, mais la ROC-AUC de validation reste élevée, ce qui montre que le modèle conserve une bonne capacité de généralisation.

L’écart-type de validation est égal à \(0{,}0263\). Les performances varient donc modérément selon les plis de validation croisée. Cette variabilité reste acceptable et proche de celle observée avec la forêt aléatoire.

Les meilleures configurations sont extrêmement proches. Les trois premières obtiennent respectivement des ROC-AUC moyennes de :

  • \(0{,}9335\) ;
  • \(0{,}9333\) ;
  • \(0{,}9331\).

L’écart entre la première et la troisième configuration n’est que de \(0{,}0004\). Il est donc trop faible pour considérer que la première combinaison est réellement supérieure aux suivantes.

La recherche met ainsi en évidence un plateau de performances. Plusieurs combinaisons de n_estimators, learning_rate, max_depth et subsample produisent pratiquement la même capacité de discrimination.

Deux stratégies principales apparaissent parmi les meilleures configurations :

  • un taux d’apprentissage relativement élevé, comme 0.10, associé à seulement 50 arbres ;
  • un taux d’apprentissage plus faible, comme 0.03 ou 0.01, associé à 150 ou 300 arbres.

Ces résultats illustrent le compromis classique entre learning_rate et n_estimators : lorsqu’un arbre contribue moins fortement, il faut davantage d’étapes de boosting pour obtenir une performance comparable.

Les profondeurs optimales sont généralement comprises entre 4 et 5. Des arbres de cette profondeur peuvent représenter des interactions complexes, mais ils augmentent également le risque de surajustement. Les contraintes sur min_samples_leaf, min_samples_split, subsample et max_features permettent de limiter ce risque.

Certaines configurations obtiennent une ROC-AUC d’apprentissage supérieure à \(0{,}99\) sans amélioration de la validation. Par exemple, une configuration atteint \(0{,}9966\) en apprentissage pour seulement \(0{,}9318\) en validation. Cela montre qu’une augmentation de la complexité peut surtout améliorer l’ajustement aux données d’entraînement sans améliorer la généralisation.

La meilleure configuration selon la ROC-AUC n’est pas celle qui maximise les autres métriques. Par exemple, la troisième configuration atteint une accuracy moyenne de \(0{,}8720\), une sensibilité de \(0{,}9139\) et un F1-score de \(0{,}8882\), légèrement supérieurs à ceux de la configuration classée première.

Ce résultat est normal, car la sélection finale est réalisée avec la ROC-AUC comme critère de référence. Le choix du modèle dépendrait donc légèrement de la métrique prioritaire.

En conclusion, le Gradient Boosting présente une ROC-AUC de validation élevée et relativement stable. Il existe un certain surajustement, mais plusieurs configurations régularisées atteignent des performances très proches. La configuration retenue constitue un bon compromis, sans être nettement supérieure aux autres solutions situées sur le même plateau.

15.8 Évolution des performances selon le nombre d’arbres

Le modèle permet d’obtenir les prédictions produites après l’ajout successif de chaque arbre.

Afficher le code
from sklearn.metrics import roc_auc_score

gradient_boosting_stages = []

for stage_number, (
    train_probability,
    test_probability,
) in enumerate(
    zip(
        gradient_boosting_model
        .staged_predict_proba(
            X_train_encoded
        ),
        gradient_boosting_model
        .staged_predict_proba(
            X_test_encoded
        ),
    ),
    start=1,
):
    gradient_boosting_stages.append(
        {
            "nombre_arbres": stage_number,
            "roc_auc_train":
                roc_auc_score(
                    y_train,
                    train_probability[:, 1],
                ),
            "roc_auc_test":
                roc_auc_score(
                    y_test,
                    test_probability[:, 1],
                ),
        }
    )

gradient_boosting_stage_results = (
    pd.DataFrame(
        gradient_boosting_stages
    )
)

display(
    gradient_boosting_stage_results
    .tail()
    .round(4)
)
nombre_arbres roc_auc_train roc_auc_test
45 46 0.9850 0.9394
46 47 0.9858 0.9394
47 48 0.9863 0.9394
48 49 0.9870 0.9412
49 50 0.9876 0.9407

Cette courbe utilise le jeu de test uniquement pour une visualisation rétrospective. Le nombre d’arbres reste sélectionné par validation croisée, et non à partir du maximum observé sur le test.

Afficher le code
plt.figure(figsize=(9, 5))

plt.plot(
    gradient_boosting_stage_results[
        "nombre_arbres"
    ],
    gradient_boosting_stage_results[
        "roc_auc_train"
    ],
    label="Apprentissage",
)

plt.plot(
    gradient_boosting_stage_results[
        "nombre_arbres"
    ],
    gradient_boosting_stage_results[
        "roc_auc_test"
    ],
    label="Test",
)

plt.xlabel("Nombre d’arbres")
plt.ylabel("ROC-AUC")
plt.title(
    "Évolution des performances "
    "du Gradient Boosting"
)
plt.legend()
plt.tight_layout()
plt.show()
Figure 30: Évolution de la ROC-AUC selon le nombre d’arbres
NoteInterprétation de la progression

À compléter après exécution :

  • nombre d’arbres à partir duquel le score se stabilise ;
  • progression de la ROC-AUC d’apprentissage ;
  • stabilisation ou diminution de la ROC-AUC sur le test ;
  • présence éventuelle d’un surajustement tardif ;
  • cohérence avec le nombre d’arbres sélectionné en validation croisée.

L’ajout d’arbres est utile tant que la performance de généralisation augmente. Lorsque seule la performance d’apprentissage continue d’augmenter, les arbres supplémentaires risquent surtout d’accentuer le surajustement.

15.9 Importance fondée sur l’impureté

Afficher le code
gradient_boosting_importance = (
    pd.DataFrame(
        {
            "variable":
                X_train_encoded.columns,
            "importance":
                gradient_boosting_model
                .feature_importances_,
        }
    )
    .sort_values(
        "importance",
        ascending=False,
    )
    .reset_index(drop=True)
)

display(
    gradient_boosting_importance
    .round(4)
)
variable importance
0 ST_Slope_Up 0.2446
1 ExerciseAngina_Y 0.1250
2 Cholesterol 0.1048
3 MaxHR 0.0861
4 Oldpeak 0.0810
5 RestingBP 0.0574
6 ChestPainType_ATA 0.0566
7 Age 0.0508
8 Sex_M 0.0462
9 ST_Slope_Flat 0.0447
10 ChestPainType_NAP 0.0427
11 FastingBS_1 0.0229
12 ChestPainType_TA 0.0139
13 RestingECG_Normal 0.0131
14 RestingECG_ST 0.0101
Afficher le code
gradient_boosting_importance_plot = (
    gradient_boosting_importance
    .sort_values("importance")
)

plt.figure(figsize=(9, 7))

plt.barh(
    gradient_boosting_importance_plot[
        "variable"
    ],
    gradient_boosting_importance_plot[
        "importance"
    ],
)

plt.xlabel(
    "Importance fondée sur l’impureté"
)
plt.ylabel("Variable")
plt.title(
    "Importance des variables "
    "— Gradient Boosting"
)
plt.tight_layout()
plt.show()
Figure 31: Importance des variables dans le Gradient Boosting

15.10 Importance par permutation

Afficher le code
from sklearn.inspection import (
    permutation_importance,
)

gradient_boosting_permutation = (
    permutation_importance(
        gradient_boosting_model,
        X_test_encoded,
        y_test,
        scoring="roc_auc",
        n_repeats=30,
        random_state=RANDOM_STATE,
        n_jobs=-1,
    )
)

gradient_boosting_permutation_importance = (
    pd.DataFrame(
        {
            "variable":
                X_test_encoded.columns,
            "importance_moyenne":
                gradient_boosting_permutation
                .importances_mean,
            "ecart_type":
                gradient_boosting_permutation
                .importances_std,
        }
    )
    .sort_values(
        "importance_moyenne",
        ascending=False,
    )
    .reset_index(drop=True)
)

display(
    gradient_boosting_permutation_importance
    .round(4)
)
variable importance_moyenne ecart_type
0 ST_Slope_Up 0.0395 0.0108
1 Cholesterol 0.0206 0.0077
2 Sex_M 0.0203 0.0063
3 ChestPainType_ATA 0.0129 0.0057
4 FastingBS_1 0.0122 0.0043
5 ExerciseAngina_Y 0.0094 0.0046
6 Oldpeak 0.0088 0.0048
7 ST_Slope_Flat 0.0063 0.0045
8 RestingBP 0.0063 0.0026
9 ChestPainType_NAP 0.0049 0.0064
10 MaxHR 0.0033 0.0056
11 RestingECG_Normal 0.0029 0.0021
12 ChestPainType_TA 0.0009 0.0007
13 Age 0.0009 0.0025
14 RestingECG_ST 0.0001 0.0007
Afficher le code
gradient_boosting_permutation_plot = (
    gradient_boosting_permutation_importance
    .sort_values("importance_moyenne")
)

plt.figure(figsize=(9, 7))

plt.barh(
    gradient_boosting_permutation_plot[
        "variable"
    ],
    gradient_boosting_permutation_plot[
        "importance_moyenne"
    ],
    xerr=(
        gradient_boosting_permutation_plot[
            "ecart_type"
        ]
    ),
)

plt.axvline(
    0,
    linewidth=1,
)

plt.xlabel(
    "Diminution moyenne de la ROC-AUC"
)
plt.ylabel("Variable")
plt.title(
    "Importance par permutation "
    "— Gradient Boosting"
)
plt.tight_layout()
plt.show()
Figure 32: Importance par permutation du Gradient Boosting

L’importance par permutation mesure la diminution du score après mélange aléatoire des valeurs d’une variable. Une importance faible peut aussi apparaître lorsque plusieurs variables contiennent des informations redondantes. :contentReferenceoaicite:1

NoteInterprétation des importances

Selon l’importance fondée sur la réduction de l’impureté, la variable la plus importante du Gradient Boosting est ST_Slope_Up, avec une importance de \(0{,}2446\).

Elle est suivie par :

  • ExerciseAngina_Y, avec \(0{,}1250\) ;
  • Cholesterol, avec \(0{,}1048\) ;
  • MaxHR, avec \(0{,}0861\) ;
  • Oldpeak, avec \(0{,}0810\) ;
  • RestingBP, avec \(0{,}0574\) ;
  • ChestPainType_ATA, avec \(0{,}0566\).

Le modèle concentre donc une part importante de ses décisions sur ST_Slope_Up, mais de manière moins extrême que l’arbre de décision unique, dans lequel cette variable représentait environ 66,8 % de l’importance totale.

Les importances sont également un peu plus concentrées que dans la forêt aléatoire. Le Gradient Boosting réutilise en effet successivement les variables les plus utiles afin de corriger les erreurs des arbres précédents.

L’importance par permutation confirme que ST_Slope_Up est la variable la plus déterminante. Sa permutation diminue la ROC-AUC d’environ \(0{,}0395\), avec un écart-type de \(0{,}0108\).

Les variables suivantes dont la permutation dégrade le plus les performances sont :

  • Cholesterol, avec une diminution moyenne de \(0{,}0206\) ;
  • Sex_M, avec \(0{,}0203\) ;
  • ChestPainType_ATA, avec \(0{,}0129\) ;
  • FastingBS_1, avec \(0{,}0122\) ;
  • ExerciseAngina_Y, avec \(0{,}0094\) ;
  • Oldpeak, avec \(0{,}0088\).

Les deux méthodes sont donc cohérentes pour plusieurs variables importantes, notamment ST_Slope_Up, Cholesterol, ExerciseAngina_Y, Oldpeak et ChestPainType_ATA.

Certaines différences apparaissent toutefois. Par exemple, MaxHR possède une importance relativement élevée fondée sur l’impureté, mais son importance par permutation est faible et inférieure à son écart-type. Cela suggère que son information est partiellement redondante avec celle d’autres variables.

De même, Age est régulièrement utilisée dans les arbres, mais sa permutation n’entraîne presque aucune baisse de ROC-AUC. Elle contribue donc à certaines règles locales sans être indispensable à la performance globale.

À l’inverse, Sex_M possède une importance d’impureté relativement modérée, mais une importance par permutation élevée. Cette variable apporte donc une information complémentaire importante, même si elle n’est pas utilisée aussi fréquemment que d’autres dans les divisions.

ChestPainType_NAP présente également une importance par permutation faible et instable, puisque son écart-type est supérieur à sa moyenne. Son rôle prédictif propre paraît donc limité ou facilement remplacé par d’autres variables.

La comparaison avec les modèles précédents montre une forte cohérence. Les variables ST_Slope_Up, Sex_M, ExerciseAngina_Y, FastingBS_1, Oldpeak, MaxHR, Cholesterol et les modalités de ChestPainType figuraient déjà parmi les variables importantes des régressions logistiques, de l’arbre et de la forêt aléatoire.

Le Gradient Boosting accorde toutefois une place particulièrement importante à ST_Slope_Up et à ExerciseAngina_Y. Il semble donc exploiter fortement les interactions entre les résultats liés à l’effort, la pente du segment ST et les autres caractéristiques cliniques.

En conclusion, le Gradient Boosting repose principalement sur ST_Slope_Up, puis sur Cholesterol, Sex_M, ExerciseAngina_Y, ChestPainType_ATA, FastingBS_1 et Oldpeak. Les importances sont moins dominées par une seule variable que dans l’arbre unique, mais restent plus concentrées que dans la forêt aléatoire.

15.11 Prédictions sur le jeu de test

Afficher le code
y_pred_gradient_boosting = (
    gradient_boosting_model.predict(
        X_test_encoded
    )
)

y_proba_gradient_boosting = (
    gradient_boosting_model
    .predict_proba(
        X_test_encoded
    )[:, 1]
)

15.12 Indicateurs du modèle

Afficher le code
(
    tn_gradient_boosting,
    fp_gradient_boosting,
    fn_gradient_boosting,
    tp_gradient_boosting,
) = confusion_matrix(
    y_test,
    y_pred_gradient_boosting,
).ravel()

sensitivity_gradient_boosting = (
    tp_gradient_boosting
    /
    (
        tp_gradient_boosting
        + fn_gradient_boosting
    )
)

specificity_gradient_boosting = (
    tn_gradient_boosting
    /
    (
        tn_gradient_boosting
        + fp_gradient_boosting
    )
)

gradient_boosting_metrics = pd.Series(
    {
        "Accuracy":
            accuracy_score(
                y_test,
                y_pred_gradient_boosting,
            ),
        "Balanced accuracy":
            balanced_accuracy_score(
                y_test,
                y_pred_gradient_boosting,
            ),
        "Précision":
            precision_score(
                y_test,
                y_pred_gradient_boosting,
            ),
        "Sensibilité / Recall":
            sensitivity_gradient_boosting,
        "Spécificité":
            specificity_gradient_boosting,
        "F1-score":
            f1_score(
                y_test,
                y_pred_gradient_boosting,
            ),
        "ROC-AUC":
            roc_auc_score(
                y_test,
                y_proba_gradient_boosting,
            ),
        "Average Precision":
            average_precision_score(
                y_test,
                y_proba_gradient_boosting,
            ),
    },
    name="Gradient Boosting",
)

display(
    gradient_boosting_metrics
    .to_frame("score")
    .round(3)
)
score
Accuracy 0.897
Balanced accuracy 0.893
Précision 0.888
Sensibilité / Recall 0.931
Spécificité 0.854
F1-score 0.909
ROC-AUC 0.941
Average Precision 0.943

15.13 Rapport de classification

Afficher le code
print(
    classification_report(
        y_test,
        y_pred_gradient_boosting,
        target_names=[
            "Absence de maladie",
            "Présence de maladie",
        ],
        digits=3,
    )
)
                     precision    recall  f1-score   support

 Absence de maladie      0.909     0.854     0.881        82
Présence de maladie      0.888     0.931     0.909       102

           accuracy                          0.897       184
          macro avg      0.898     0.893     0.895       184
       weighted avg      0.897     0.897     0.896       184

15.14 Matrice de confusion

Afficher le code
ConfusionMatrixDisplay.from_predictions(
    y_test,
    y_pred_gradient_boosting,
    display_labels=[
        "Absence",
        "Présence",
    ],
)

plt.title(
    "Matrice de confusion "
    "— Gradient Boosting"
)
plt.tight_layout()
plt.show()
Figure 33: Matrice de confusion du Gradient Boosting
ImportantInterprétation de la matrice de confusion
  • vrais positifs : np.int64(95) ;
  • vrais négatifs : np.int64(70) ;
  • faux positifs : np.int64(12) ;
  • faux négatifs : np.int64(7).

Le modèle identifie correctement 95 des 102 patients présentant réellement une maladie cardiaque. Sa sensibilité atteint donc 93,1 %.

Seulement 7 patients malades sont classés à tort comme non malades. Le nombre de faux négatifs est ainsi identique à celui des modèles logistiques et inférieur à celui de la forêt aléatoire, qui en produisait 8.

Parmi les 82 patients sans maladie, 70 sont correctement classés et 12 sont prédits à tort comme positifs. La spécificité atteint ainsi 85,4 %, soit une amélioration par rapport aux modèles logistiques et à la forêt aléatoire, qui obtenaient une spécificité de 82,9 %.

Les performances globales sont :

  • accuracy : 89,7 % ;
  • balanced accuracy : 89,3 % ;
  • précision : 88,8 % ;
  • sensibilité : 93,1 % ;
  • spécificité : 85,4 % ;
  • F1-score : 0,909 ;
  • ROC-AUC : 0,941 ;
  • Average Precision : 0,943.

Ces résultats sont les meilleurs obtenus jusqu’à présent sur le jeu de test. Le Gradient Boosting conserve la même capacité de détection des cas positifs que les régressions logistiques, tout en réduisant le nombre de faux positifs de 14 à 12.

Par rapport à la forêt aléatoire, il produit :

  • un faux négatif de moins ;
  • deux faux positifs de moins ;
  • une accuracy supérieure ;
  • une ROC-AUC supérieure, passant de 0,931 à 0,941 ;
  • une Average Precision supérieure, passant de 0,930 à 0,943.

La balanced accuracy de 89,3 % montre également que les performances restent bien équilibrées entre les deux classes.

En conclusion, le Gradient Boosting offre ici le meilleur compromis prédictif au seuil de 0,5. Il améliore la discrimination globale, la précision, la spécificité et le F1-score, sans dégrader la sensibilité ni augmenter le nombre de faux négatifs.

15.15 Courbe ROC

Afficher le code
gradient_boosting_roc_auc = (
    roc_auc_score(
        y_test,
        y_proba_gradient_boosting,
    )
)

RocCurveDisplay.from_predictions(
    y_test,
    y_proba_gradient_boosting,
    name=(
        "Gradient Boosting — "
        f"AUC = "
        f"{gradient_boosting_roc_auc:.3f}"
    ),
)

plt.plot(
    [0, 1],
    [0, 1],
    linestyle="--",
    label="Modèle aléatoire",
)

plt.title(
    "Courbe ROC — Gradient Boosting"
)
plt.xlabel("Taux de faux positifs")
plt.ylabel("Taux de vrais positifs")
plt.legend()
plt.tight_layout()
plt.show()
Figure 34: Courbe ROC du Gradient Boosting

15.16 Courbe précision-rappel

Afficher le code
gradient_boosting_average_precision = (
    average_precision_score(
        y_test,
        y_proba_gradient_boosting,
    )
)

PrecisionRecallDisplay.from_predictions(
    y_test,
    y_proba_gradient_boosting,
    name=(
        "Gradient Boosting — "
        f"AP = "
        f"{gradient_boosting_average_precision:.3f}"
    ),
)

plt.title(
    "Courbe précision-rappel "
    "— Gradient Boosting"
)
plt.xlabel("Rappel")
plt.ylabel("Précision")
plt.legend()
plt.tight_layout()
plt.show()
Figure 35: Courbe précision-rappel du Gradient Boosting

15.17 Comparaison avec les modèles précédents

Afficher le code
model_comparison_with_boosting = pd.concat(
    [
        logistic_metrics.rename(
            "Non pénalisée"
        ),
        ridge_metrics.rename(
            "Ridge"
        ),
        lasso_metrics.rename(
            "Lasso"
        ),
        elastic_metrics.rename(
            "Elastic Net"
        ),
        tree_metrics.rename(
            "Arbre de décision"
        ),
        random_forest_metrics.rename(
            "Forêt aléatoire"
        ),
        gradient_boosting_metrics.rename(
            "Gradient Boosting"
        ),
    ],
    axis=1,
)

model_comparison_with_boosting[
    "Différence boosting vs logistique"
] = (
    model_comparison_with_boosting[
        "Gradient Boosting"
    ]
    -
    model_comparison_with_boosting[
        "Non pénalisée"
    ]
)

model_comparison_with_boosting[
    "Différence boosting vs forêt"
] = (
    model_comparison_with_boosting[
        "Gradient Boosting"
    ]
    -
    model_comparison_with_boosting[
        "Forêt aléatoire"
    ]
)

display(
    model_comparison_with_boosting
    .round(3)
)
Non pénalisée Ridge Lasso Elastic Net Arbre de décision Forêt aléatoire Gradient Boosting Différence boosting vs logistique Différence boosting vs forêt
Accuracy 0.886 0.886 0.886 0.886 0.832 0.880 0.897 0.011 0.016
Balanced accuracy 0.880 0.880 0.880 0.880 0.829 0.875 0.893 0.012 0.017
Précision 0.872 0.872 0.872 0.872 0.845 0.870 0.888 0.016 0.017
Sensibilité / Recall 0.931 0.931 0.931 0.931 0.853 0.922 0.931 0.000 0.010
Spécificité 0.829 0.829 0.829 0.829 0.805 0.829 0.854 0.024 0.024
F1-score 0.900 0.900 0.900 0.900 0.849 0.895 0.909 0.009 0.014
ROC-AUC 0.929 0.930 0.929 0.929 0.876 0.931 0.941 0.012 0.010
Average Precision 0.936 0.937 0.936 0.936 0.853 0.930 0.943 0.007 0.013
ImportantConclusion du Gradient Boosting

Le Gradient Boosting obtient les meilleures performances globales parmi les modèles évalués.

Sur le jeu de test, il atteint une accuracy de \(0{,}897\), contre \(0{,}886\) pour les régressions logistiques, \(0{,}880\) pour la forêt aléatoire et \(0{,}832\) pour l’arbre de décision.

La balanced accuracy s’élève à \(0{,}893\). Elle dépasse de \(0{,}012\) celle des modèles logistiques et de \(0{,}017\) celle de la forêt aléatoire. Cette amélioration indique que le modèle offre de meilleures performances moyennes sur les deux classes.

La sensibilité reste égale à \(0{,}931\), soit le même niveau que les régressions logistiques. Le modèle conserve donc leur bonne capacité à détecter les patients malades. Il produit 7 faux négatifs, contre 8 pour la forêt aléatoire et 15 pour l’arbre de décision.

L’amélioration provient principalement de la spécificité, qui passe de \(0{,}829\) avec les modèles logistiques et la forêt à \(0{,}854\). Le Gradient Boosting réduit ainsi le nombre de faux positifs de 14 à 12 sans augmenter le nombre de faux négatifs.

La précision atteint \(0{,}888\) et le F1-score \(0{,}909\), soit les meilleures valeurs observées dans cette étude. Le modèle réalise donc un meilleur compromis entre la précision des prédictions positives et la détection des cas réellement positifs.

La ROC-AUC atteint \(0{,}941\), soit :

  • \(0{,}012\) de plus que la régression logistique non pénalisée ;
  • \(0{,}011\) de plus que Ridge ;
  • \(0{,}010\) de plus que la forêt aléatoire ;
  • \(0{,}065\) de plus que l’arbre de décision.

L’Average Precision atteint également sa valeur maximale, avec \(0{,}943\). Le Gradient Boosting produit donc le meilleur classement probabiliste des observations, aussi bien sur l’ensemble des seuils ROC que pour la classe positive.

Ces gains restent modérés par rapport aux modèles logistiques et à la forêt aléatoire. Ils ne suffisent pas, sur un seul jeu de test, à démontrer une supériorité certaine. Ils sont toutefois cohérents avec la validation croisée, dans laquelle le Gradient Boosting obtenait également la meilleure ROC-AUC moyenne, proche de \(0{,}934\).

Le modèle présente un certain surajustement, puisque sa ROC-AUC d’apprentissage est nettement supérieure à celle de validation. Néanmoins, les performances de validation restent élevées et les résultats sur le jeu de test confirment une bonne généralisation.

En conclusion, le Gradient Boosting constitue à ce stade le modèle prédictif le plus performant de l’étude. Il améliore la discrimination, la précision et la spécificité, tout en conservant la sensibilité élevée des régressions logistiques.

Le choix final dépend toutefois de l’objectif :

  • pour maximiser la performance prédictive, le Gradient Boosting est le meilleur candidat ;
  • pour privilégier l’interprétabilité et la simplicité, la régression logistique Ridge reste une solution très compétitive ;
  • pour modéliser des relations non linéaires avec une méthode plus robuste qu’un arbre unique, la forêt aléatoire reste une alternative pertinente.

16 Conclusion générale

Cette étude avait pour objectif de construire et de comparer plusieurs modèles de classification capables de prédire la présence d’une maladie cardiaque à partir de caractéristiques cliniques et démographiques.

Le jeu de données comportait 918 observations et une variable cible binaire, HeartDisease. La classe positive représentait environ 55 % des observations, ce qui correspond à un déséquilibre modéré mais non extrême.

Après préparation des données, plusieurs familles de modèles ont été étudiées :

  • régression logistique non pénalisée ;
  • régression logistique Ridge ;
  • régression logistique Lasso ;
  • régression logistique Elastic Net ;
  • arbre de décision ;
  • forêt aléatoire ;
  • Gradient Boosting.

16.1 Résultats des modèles logistiques

Les quatre régressions logistiques ont obtenu des performances presque identiques sur le jeu de test.

Elles atteignent notamment :

  • une accuracy de 88,6 % ;
  • une sensibilité de 93,1 % ;
  • une spécificité de 82,9 % ;
  • un F1-score de 0,900 ;
  • une ROC-AUC comprise entre 0,929 et 0,930.

Leur matrice de confusion est identique, avec 95 vrais positifs, 68 vrais négatifs, 14 faux positifs et 7 faux négatifs.

Ridge présente un très léger avantage sur la ROC-AUC et l’Average Precision, mais les écarts sont trop faibles pour conclure à une supériorité nette.

Lasso et Elastic Net n’ont annulé aucun coefficient. Dans cette étude, la régularisation n’a donc pas permis de sélectionner un sous-ensemble plus réduit de variables.

Les principaux facteurs associés aux prédictions des modèles logistiques sont notamment :

  • ST_Slope_Up ;
  • les modalités de ChestPainType ;
  • Sex_M ;
  • FastingBS_1 ;
  • ExerciseAngina_Y ;
  • ST_Slope_Flat ;
  • Cholesterol ;
  • Oldpeak.

Les signes et la hiérarchie générale des coefficients sont restés stables entre les différentes formes de régularisation.

16.2 Résultats de l’arbre de décision

L’arbre optimal possède une profondeur de 5 et 21 feuilles. Il permet de représenter directement des règles non linéaires et des interactions entre les variables.

La première séparation repose sur ST_Slope_Up, suivie notamment de MaxHR, Cholesterol, Sex_M et ExerciseAngina_Y.

Cette interprétabilité s’accompagne toutefois d’une baisse importante des performances :

  • accuracy de 83,2 % ;
  • sensibilité de 85,3 % ;
  • ROC-AUC de 0,876 ;
  • 15 faux négatifs.

L’arbre unique est donc moins performant que les modèles logistiques et présente un risque plus élevé de manquer des patients réellement malades.

16.3 Résultats de la forêt aléatoire

La forêt aléatoire améliore nettement les résultats de l’arbre unique.

Elle obtient :

  • une ROC-AUC moyenne de validation de 0,933 ;
  • une accuracy de test de 88,0 % ;
  • une sensibilité de 92,2 % ;
  • une ROC-AUC de test de 0,931 ;
  • 8 faux négatifs.

Ses performances sont proches de celles des modèles logistiques. Elle modélise davantage de relations non linéaires et d’interactions, mais elle reste plus difficile à expliquer.

Les importances des variables sont également plus réparties que dans l’arbre unique. Les variables les plus utiles comprennent notamment ST_Slope_Up, Cholesterol, Sex_M, ExerciseAngina_Y, Oldpeak, MaxHR et ChestPainType_ATA.

16.4 Résultats du Gradient Boosting

Le Gradient Boosting obtient les meilleures performances globales de l’étude.

Sa configuration optimale utilise :

  • 50 arbres ;
  • un taux d’apprentissage de 0,1 ;
  • une profondeur maximale de 5 ;
  • un sous-échantillonnage de 60 % ;
  • environ quatre variables examinées à chaque séparation.

Il atteint une ROC-AUC moyenne de validation d’environ 0,934, soit la meilleure valeur parmi les modèles évalués.

Sur le jeu de test, il obtient :

  • une accuracy de 89,7 % ;
  • une balanced accuracy de 89,3 % ;
  • une précision de 88,8 % ;
  • une sensibilité de 93,1 % ;
  • une spécificité de 85,4 % ;
  • un F1-score de 0,909 ;
  • une ROC-AUC de 0,941 ;
  • une Average Precision de 0,943.

Sa matrice de confusion contient 95 vrais positifs, 70 vrais négatifs, 12 faux positifs et 7 faux négatifs.

Le Gradient Boosting conserve donc la sensibilité élevée des modèles logistiques tout en réduisant le nombre de faux positifs. Il produit les meilleures valeurs de ROC-AUC, d’Average Precision, d’accuracy et de F1-score.

16.5 Comparaison finale

Les résultats mettent en évidence trois conclusions principales.

Premièrement, les modèles logistiques constituent des solutions très solides. Ils sont simples, interprétables, stables et présentent une excellente sensibilité. Leur performance reste proche de celle des méthodes d’ensemble.

Deuxièmement, l’arbre de décision unique est utile pour comprendre les règles et les interactions, mais il n’est pas suffisamment performant pour être retenu comme modèle prédictif final.

Troisièmement, les méthodes d’ensemble améliorent nettement l’arbre unique. La forêt aléatoire atteint les performances des modèles logistiques, tandis que le Gradient Boosting les dépasse légèrement sur la plupart des indicateurs.

16.6 Modèle retenu

Le Gradient Boosting constitue le meilleur candidat lorsque l’objectif principal est de maximiser les performances prédictives.

Il présente le meilleur compromis entre :

  • discrimination globale ;
  • sensibilité ;
  • spécificité ;
  • précision ;
  • nombre de faux négatifs ;
  • stabilité en validation croisée.

Son avantage reste cependant modéré par rapport aux modèles logistiques. La différence de performance ne doit donc pas être interprétée comme une supériorité absolue, notamment parce que l’évaluation repose sur un jeu de test de taille limitée.

Dans un contexte où l’explicabilité est prioritaire, la régression logistique Ridge reste une excellente alternative. Elle offre des performances très proches, avec une interprétation directe des coefficients et des odds ratios.

16.7 Limites de l’étude

Plusieurs limites doivent être prises en compte.

Le nombre d’observations reste relativement modeste. Les estimations de performance peuvent donc varier selon le découpage des données.

Certaines variables, notamment Cholesterol et RestingBP, contiennent des valeurs nulles ou très faibles susceptibles de représenter des données manquantes codées numériquement. Leur traitement pourrait modifier les résultats.

La sélection des hyperparamètres a été réalisée par validation croisée, mais le jeu de test a servi à comparer plusieurs modèles. Une validation externe ou une validation croisée imbriquée permettrait d’obtenir une estimation encore plus rigoureuse.

Les probabilités prédites n’ont pas encore été étudiées en profondeur du point de vue de leur calibration. Un modèle performant en ROC-AUC ne produit pas nécessairement des probabilités parfaitement fiables.

Enfin, les relations identifiées sont prédictives et associatives. Elles ne permettent pas d’établir des relations causales entre les caractéristiques cliniques et la maladie cardiaque.

16.8 Perspectives

Pour prolonger cette étude, plusieurs analyses pourraient être ajoutées :

  • validation croisée imbriquée ;
  • analyse de la calibration et du score de Brier ;
  • optimisation du seuil de décision selon le coût des faux négatifs ;
  • interprétation locale avec SHAP ;
  • comparaison avec XGBoost, LightGBM ou CatBoost ;
  • traitement explicite des valeurs manquantes codées par zéro ;
  • validation sur un jeu de données externe ;
  • estimation d’intervalles de confiance par bootstrap.

16.9 Conclusion finale

Cette étude montre que plusieurs approches permettent de prédire efficacement la présence d’une maladie cardiaque.

Les régressions logistiques offrent le meilleur niveau d’interprétabilité avec des performances élevées. Les méthodes d’ensemble permettent de modéliser des relations plus complexes et améliorent les résultats de l’arbre de décision unique.

Parmi les modèles étudiés, le Gradient Boosting obtient les meilleures performances globales. Il est donc retenu comme modèle prédictif final, tandis que la régression logistique Ridge constitue le meilleur modèle de référence interprétable.

Ces résultats devront néanmoins être confirmés par une validation externe et ne doivent pas être utilisés seuls pour prendre une décision médicale.