Scikit-Learn 1.8 en 2026 : Pipelines ML, GPU et Python Free-Threaded — Le Guide Pratique

Explorez scikit-learn 1.8 en pratique : support GPU via Array API, Python 3.14 sans GIL, pipelines ML modernes, calibration par température et optimisations de performances. Exemples de code inclus.

Introduction : Scikit-Learn en 2026, Plus Puissant que Jamais

Scikit-learn, c'est un peu le couteau suisse du machine learning en Python. Ça fait plus de quinze ans que la bibliothèque est là, et franchement, elle n'a jamais été aussi pertinente qu'aujourd'hui. Avec la version 1.8, on passe un vrai cap : support natif du GPU via l'Array API, compatibilité avec Python 3.14 free-threaded (adieu le GIL !), de nouvelles méthodes de calibration, et des optimisations de performances qui font vraiment plaisir.

Si vous utilisez déjà scikit-learn — ou si vous cherchez à moderniser vos workflows ML — ce guide est fait pour vous. On va explorer ensemble chaque nouveauté majeure, construire des pipelines complets et voir comment en tirer parti dans vos projets réels.

Que vous soyez data scientist, ingénieur ML ou analyste de données, vous trouverez ici des exemples concrets et immédiatement applicables. Allez, on commence par l'installation.

Installation et Configuration de l'Environnement

L'installation de scikit-learn 1.8, c'est assez classique : pip ou conda, comme d'habitude. Si vous voulez profiter du support GPU (et honnêtement, pourquoi s'en priver ?), il faudra aussi installer PyTorch ou CuPy à côté.

# Installation de scikit-learn 1.8
pip install --upgrade scikit-learn

# Pour le support GPU via PyTorch
pip install torch

# Ou via CuPy (alternative pour GPU NVIDIA)
pip install cupy-cuda12x

# Installation des dépendances complémentaires
pip install pandas numpy matplotlib joblib

Vérifions que tout est bien en place :

import sklearn
import numpy as np
import pandas as pd

print(f"scikit-learn version : {sklearn.__version__}")
print(f"NumPy version : {np.__version__}")
print(f"Pandas version : {pd.__version__}")

# Vérifier le support Python
import sys
print(f"Python version : {sys.version}")

Pour activer le support Array API dans scikit-learn 1.8, il faut configurer une variable d'environnement avant l'importation. C'est un détail facile à oublier, alors notez-le bien :

import os
os.environ["SCIPY_ARRAY_API"] = "1"

import sklearn
sklearn.set_config(array_api_dispatch=True)

L'Array API : Scikit-Learn sur GPU

C'est probablement la nouveauté la plus excitante de cette version. Et je pèse mes mots.

L'adoption progressive du standard Python Array API permet désormais d'utiliser directement des tableaux PyTorch et CuPy comme entrées. Concrètement, ça veut dire que les calculs peuvent tourner sur GPU sans modification majeure de votre code existant. Plutôt cool, non ?

Qu'est-ce que l'Array API ?

L'Array API est une spécification qui définit une interface standardisée pour les bibliothèques de manipulation de tableaux. Au lieu d'être limité aux tableaux NumPy (qui tournent uniquement sur CPU), scikit-learn peut maintenant travailler avec n'importe quel tableau compatible Array API — y compris ceux qui résident sur GPU.

En pratique, vous transférez vos données sur GPU, vous les passez directement à un estimateur scikit-learn, et celui-ci les traite sur le GPU. Pas de copie inutile entre CPU et GPU. Simple et efficace.

Estimateurs Supportés dans la Version 1.8

Voici la liste des estimateurs et fonctions qui supportent actuellement l'Array API :

  • Prétraitement : StandardScaler, PolynomialFeatures
  • Modèles linéaires : RidgeCV, RidgeClassifierCV
  • Modèles de mélange : GaussianMixture
  • Calibration : CalibratedClassifierCV

La liste s'étoffe à chaque version. Et bien sûr, les estimateurs déjà supportés dans les versions antérieures (comme PCA, LinearDiscriminantAnalysis, KMeans) restent compatibles.

Exemple Pratique : Pipeline GPU avec PyTorch

Passons aux choses sérieuses avec un exemple concret. On va envoyer des données sur GPU et entraîner un pipeline scikit-learn :

import os
os.environ["SCIPY_ARRAY_API"] = "1"

import numpy as np
import torch
import sklearn
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler, FunctionTransformer
from sklearn.linear_model import RidgeClassifierCV
from sklearn.calibration import CalibratedClassifierCV
from sklearn.model_selection import cross_validate
from sklearn.datasets import make_classification

# Générer un jeu de données
X, y = make_classification(
    n_samples=5000,
    n_features=50,
    n_informative=20,
    n_classes=3,
    random_state=42
)

# Fonction de transfert CPU -> GPU
def to_gpu_tensor(X):
    if isinstance(X, np.ndarray):
        return torch.tensor(X.astype(np.float32), device="cuda")
    return X

# Pipeline avec transfert GPU
gpu_pipeline = make_pipeline(
    FunctionTransformer(to_gpu_tensor),
    StandardScaler(),
    CalibratedClassifierCV(
        RidgeClassifierCV(alphas=np.logspace(-3, 3, 10)),
        method="temperature"
    ),
)

# Exécution avec dispatch Array API activé
with sklearn.config_context(array_api_dispatch=True):
    results = cross_validate(
        gpu_pipeline, X, y,
        cv=5,
        scoring="accuracy",
        return_train_score=True
    )

print(f"Score moyen (test) : {results['test_score'].mean():.4f}")
print(f"Score moyen (train) : {results['train_score'].mean():.4f}")
print(f"Temps moyen d'entraînement : {results['fit_time'].mean():.3f}s")

Ce pipeline transfère automatiquement les données vers le GPU, applique une normalisation, puis entraîne un classificateur Ridge avec calibration par température — le tout sur GPU. Sur des jeux de données volumineux, j'ai personnellement observé des accélérations de l'ordre de 10x par rapport au CPU. Ça vaut le coup d'essayer.

Considérations Importantes pour le GPU

Quelques points à garder en tête :

  • Le transfert de données entre CPU et GPU a un coût. Regroupez vos opérations GPU pour minimiser ces allers-retours.
  • Tous les estimateurs ne supportent pas encore l'Array API. Vérifiez la documentation avant de vous lancer.
  • PyTorch et CuPy sont les backends les mieux supportés pour le moment.
  • Pour de petits jeux de données, le CPU reste souvent plus rapide — le surcoût de transfert annule le gain.

Python Free-Threaded : Adieu le GIL

Ah, le GIL. Si vous faites du Python depuis un moment, vous connaissez forcément cette frustration. Le Global Interpreter Lock empêche l'exécution simultanée de plusieurs threads Python, et c'est un frein historique pour les performances multi-thread.

Bonne nouvelle : avec Python 3.13, CPython a introduit un mode expérimental sans GIL. Et Python 3.14 officialise ce mode « free-threaded » comme fonctionnalité de première classe. Scikit-learn 1.8 est entièrement compatible avec ce nouveau mode.

Impact sur les Performances de Scikit-Learn

Concrètement, les opérations parallèles de scikit-learn peuvent maintenant utiliser de vrais threads au lieu de processus séparés. Voici ce que ça change :

  • Moins de mémoire utilisée : les threads partagent la mémoire, contrairement aux processus qui doivent dupliquer les données.
  • Communication plus rapide : pas besoin de sérialiser/désérialiser les données entre processus.
  • Gains réels en multi-thread : Python 3.14 free-threaded affiche des gains de l'ordre de 3x pour le code CPU-intensif, avec seulement 5-10% de surcoût sur le code mono-thread.

Utilisation avec Joblib

Scikit-learn utilise joblib pour la parallélisation. Avec Python free-threaded, on peut enfin utiliser le backend threading de manière efficace :

import joblib
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import make_classification

# Générer des données
X, y = make_classification(n_samples=10000, n_features=30, random_state=42)

# Définir la grille de paramètres
param_grid = {
    'n_estimators': [50, 100, 200],
    'max_depth': [5, 10, 20, None],
    'min_samples_split': [2, 5, 10]
}

clf = RandomForestClassifier(random_state=42)
grid_search = GridSearchCV(clf, param_grid=param_grid, cv=5, n_jobs=4)

# Avec Python free-threaded, le backend threading est plus efficace
with joblib.parallel_config(backend="threading"):
    grid_search.fit(X, y)

print(f"Meilleurs paramètres : {grid_search.best_params_}")
print(f"Meilleur score : {grid_search.best_score_:.4f}")

Le passage au backend threading avec Python 3.14 free-threaded élimine la copie des données entre processus. C'est particulièrement appréciable pour les recherches d'hyperparamètres et la validation croisée sur de gros jeux de données — là où les données sont volumineuses et les itérations nombreuses.

Calibration par Température : Des Probabilités Fiables en Multiclasse

La calibration des probabilités, c'est un sujet souvent négligé dans le machine learning. Et pourtant, c'est crucial en production.

Un modèle bien calibré produit des probabilités qui correspondent à la réalité : si le modèle prédit 70% de chance pour une classe, cette classe devrait effectivement apparaître dans environ 70% des cas similaires. Ça a l'air évident dit comme ça, mais beaucoup de modèles sont très mal calibrés par défaut.

Le Problème de la Calibration Multiclasse

Les méthodes classiques (sigmoid et isotonique) fonctionnent dans un schéma one-vs-rest pour les problèmes multiclasses. Un paramètre de calibration est ajusté pour chaque classe indépendamment, ce qui peut mener à des probabilités incohérentes. La calibration par température résout ce problème élégamment : un seul paramètre libre, partagé entre toutes les classes.

Implémentation avec Scikit-Learn 1.8

import numpy as np
from sklearn.calibration import CalibratedClassifierCV, calibration_curve
from sklearn.datasets import make_classification
from sklearn.naive_bayes import GaussianNB
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt

# Créer un problème multiclasse
X, y = make_classification(
    n_samples=5000,
    n_classes=4,
    n_informative=10,
    n_redundant=2,
    random_state=42
)

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42
)

# Entraîner le classificateur de base
base_clf = GaussianNB()
base_clf.fit(X_train, y_train)

# Calibration par température (nouveau dans 1.8)
temp_calibrated = CalibratedClassifierCV(
    base_clf,
    method="temperature",
    ensemble=False
)
temp_calibrated.fit(X_train, y_train)

# Calibration sigmoid (pour comparaison)
sigmoid_calibrated = CalibratedClassifierCV(
    base_clf,
    method="sigmoid",
    ensemble=False
)
sigmoid_calibrated.fit(X_train, y_train)

# Comparer les résultats
from sklearn.metrics import log_loss

y_proba_base = base_clf.predict_proba(X_test)
y_proba_temp = temp_calibrated.predict_proba(X_test)
y_proba_sigmoid = sigmoid_calibrated.predict_proba(X_test)

print(f"Log-loss (non calibré) : {log_loss(y_test, y_proba_base):.4f}")
print(f"Log-loss (température) : {log_loss(y_test, y_proba_temp):.4f}")
print(f"Log-loss (sigmoid) : {log_loss(y_test, y_proba_sigmoid):.4f}")

La calibration par température est particulièrement adaptée aux réseaux de neurones et aux modèles trop confiants dans leurs prédictions (ce qui arrive plus souvent qu'on ne le croit). En production, des probabilités bien calibrées sont essentielles pour les systèmes de prise de décision, le classement par confiance et l'estimation de l'incertitude.

Pipelines ML Modernes avec ColumnTransformer

Les pipelines, c'est vraiment le cœur d'un workflow ML robuste et reproductible. Et scikit-learn 1.8 pousse le ColumnTransformer encore plus loin, avec un meilleur support de Polars DataFrames et des performances optimisées.

Voyons comment construire des pipelines professionnels de bout en bout.

Architecture d'un Pipeline Complet

Un pipeline ML typique comprend plusieurs étapes : prétraitement des variables numériques, traitement des variables catégorielles, ingénierie des caractéristiques, et enfin le modèle prédictif. Le ColumnTransformer permet de définir des transformations différentes pour chaque groupe de colonnes — et ça, c'est ce qui le rend si puissant.

import pandas as pd
import numpy as np
from sklearn.compose import ColumnTransformer, make_column_selector
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import (
    StandardScaler, OneHotEncoder, OrdinalEncoder,
    PolynomialFeatures, FunctionTransformer
)
from sklearn.impute import SimpleImputer
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import cross_val_score

# Simuler un jeu de données réaliste
np.random.seed(42)
n = 2000

data = pd.DataFrame({
    'age': np.random.normal(40, 12, n).astype(int),
    'revenu': np.random.lognormal(10.5, 0.8, n),
    'nb_achats': np.random.poisson(5, n),
    'anciennete_mois': np.random.exponential(24, n).astype(int),
    'categorie_client': np.random.choice(
        ['Premium', 'Standard', 'Basique'], n,
        p=[0.2, 0.5, 0.3]
    ),
    'region': np.random.choice(
        ['Nord', 'Sud', 'Est', 'Ouest', 'Centre'], n
    ),
    'canal_acquisition': np.random.choice(
        ['Web', 'Mobile', 'Boutique', 'Partenaire'], n
    ),
})

# Introduire quelques valeurs manquantes
mask = np.random.random(n) < 0.05
data.loc[mask, 'revenu'] = np.nan
mask = np.random.random(n) < 0.03
data.loc[mask, 'anciennete_mois'] = np.nan

# Variable cible : le client va-t-il se désabonner ?
y = (
    (data['nb_achats'] < 3).astype(int) +
    (data['anciennete_mois'] < 6).astype(int) +
    (data['categorie_client'] == 'Basique').astype(int) +
    np.random.binomial(1, 0.1, n)
) >= 2
y = y.astype(int)

print(f"Taille du jeu de données : {data.shape}")
print(f"Taux de désabonnement : {y.mean():.2%}")
print(f"\nValeurs manquantes :\n{data.isnull().sum()}")

Construction du Pipeline avec ColumnTransformer

# Définir les colonnes par type
cols_numeriques = ['age', 'revenu', 'nb_achats', 'anciennete_mois']
cols_categoriques_nominales = ['region', 'canal_acquisition']
cols_categoriques_ordinales = ['categorie_client']

# Pipeline pour les variables numériques
pipeline_numerique = Pipeline([
    ('imputation', SimpleImputer(strategy='median')),
    ('normalisation', StandardScaler()),
])

# Pipeline pour les variables catégorielles nominales
pipeline_nominal = Pipeline([
    ('imputation', SimpleImputer(strategy='most_frequent')),
    ('encodage', OneHotEncoder(
        handle_unknown='ignore',
        sparse_output=False,
        drop='if_binary'
    )),
])

# Pipeline pour les variables catégorielles ordinales
pipeline_ordinal = Pipeline([
    ('imputation', SimpleImputer(strategy='most_frequent')),
    ('encodage', OrdinalEncoder(
        categories=[['Basique', 'Standard', 'Premium']]
    )),
])

# Assembler le tout avec ColumnTransformer
preprocesseur = ColumnTransformer(
    transformers=[
        ('num', pipeline_numerique, cols_numeriques),
        ('nom', pipeline_nominal, cols_categoriques_nominales),
        ('ord', pipeline_ordinal, cols_categoriques_ordinales),
    ],
    remainder='drop',
    verbose_feature_names_out=True
)

# Pipeline complet avec modèle
pipeline_complet = Pipeline([
    ('pretraitement', preprocesseur),
    ('selection', SelectKBest(f_classif, k=10)),
    ('modele', GradientBoostingClassifier(
        n_estimators=100,
        max_depth=5,
        learning_rate=0.1,
        random_state=42
    ))
])

# Évaluation par validation croisée
scores = cross_val_score(pipeline_complet, data, y, cv=5, scoring='roc_auc')
print(f"\nROC AUC moyen : {scores.mean():.4f} (+/- {scores.std():.4f})")

L'avantage majeur ici, c'est que tout le prétraitement est encapsulé dans le pipeline. Ça garantit zéro fuite de données (data leakage) pendant la validation croisée. Et le même pipeline peut être sérialisé et déployé en production tel quel.

Sélection Automatique des Colonnes

Scikit-learn propose aussi make_column_selector pour sélectionner automatiquement les colonnes selon leur type. C'est pratique quand votre DataFrame évolue régulièrement :

from sklearn.compose import make_column_selector

# Sélection automatique par type de données
preprocesseur_auto = ColumnTransformer(
    transformers=[
        ('num', pipeline_numerique,
         make_column_selector(dtype_include=np.number)),
        ('cat', pipeline_nominal,
         make_column_selector(dtype_include=object)),
    ],
    remainder='drop'
)

# Le pipeline s'adapte automatiquement aux types de colonnes
pipeline_auto = Pipeline([
    ('pretraitement', preprocesseur_auto),
    ('modele', GradientBoostingClassifier(random_state=42))
])

scores_auto = cross_val_score(pipeline_auto, data, y, cv=5, scoring='roc_auc')
print(f"ROC AUC (auto) : {scores_auto.mean():.4f}")

Ingénierie des Caractéristiques Avancée

L'ingénierie des caractéristiques (ou feature engineering, pour les anglophones), c'est souvent ce qui fait la différence entre un modèle médiocre et un modèle qui performe vraiment bien. Mon expérience m'a montré que passer du temps sur cette étape rapporte beaucoup plus qu'affiner les hyperparamètres pendant des heures.

Scikit-learn 1.8 fournit des outils puissants pour créer et transformer des caractéristiques de manière systématique.

Transformations Polynomiales et Interactions

Le PolynomialFeatures, maintenant compatible Array API, permet de créer automatiquement des caractéristiques polynomiales et des termes d'interaction :

from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.model_selection import cross_val_score
import numpy as np

# Exemple : créer des interactions entre caractéristiques
X_num = data[cols_numeriques].fillna(0).values

# Caractéristiques polynomiales de degré 2
poly = PolynomialFeatures(degree=2, interaction_only=True, include_bias=False)
X_poly = poly.fit_transform(X_num)

print(f"Caractéristiques originales : {X_num.shape[1]}")
print(f"Après interactions : {X_poly.shape[1]}")
print(f"Noms des caractéristiques : {poly.get_feature_names_out()[:10]}...")

# Pipeline avec interactions
pipeline_interactions = make_pipeline(
    SimpleImputer(strategy='median'),
    PolynomialFeatures(degree=2, interaction_only=True, include_bias=False),
    StandardScaler(),
    LogisticRegression(max_iter=1000, random_state=42)
)

scores_poly = cross_val_score(
    pipeline_interactions,
    data[cols_numeriques], y,
    cv=5, scoring='roc_auc'
)
print(f"\nROC AUC avec interactions : {scores_poly.mean():.4f}")

Transformations Personnalisées avec FunctionTransformer

Le FunctionTransformer est un de ces outils qu'on sous-estime souvent. Il permet d'intégrer n'importe quelle transformation personnalisée dans un pipeline, ce qui est incroyablement flexible :

from sklearn.preprocessing import FunctionTransformer
import numpy as np

# Transformation logarithmique pour les distributions asymétriques
def log_transform(X):
    return np.log1p(np.abs(X))

# Extraction de caractéristiques temporelles
def features_temporelles(df):
    result = df.copy()
    if 'anciennete_mois' in df.columns:
        result['anciennete_annees'] = df['anciennete_mois'] / 12
        result['est_nouveau'] = (df['anciennete_mois'] < 3).astype(int)
        result['est_fidele'] = (df['anciennete_mois'] > 24).astype(int)
    if 'nb_achats' in df.columns and 'anciennete_mois' in df.columns:
        result['frequence_achat'] = (
            df['nb_achats'] / df['anciennete_mois'].clip(lower=1)
        )
    return result

# Intégrer dans un pipeline
from sklearn.compose import ColumnTransformer

enrichissement = FunctionTransformer(
    features_temporelles,
    validate=False
)

pipeline_enrichi = Pipeline([
    ('enrichissement', enrichissement),
    ('pretraitement', ColumnTransformer([
        ('num', Pipeline([
            ('imputer', SimpleImputer(strategy='median')),
            ('scaler', StandardScaler()),
        ]), make_column_selector(dtype_include=np.number)),
        ('cat', Pipeline([
            ('imputer', SimpleImputer(strategy='most_frequent')),
            ('encoder', OneHotEncoder(handle_unknown='ignore',
                                      sparse_output=False)),
        ]), make_column_selector(dtype_include=object)),
    ])),
    ('modele', GradientBoostingClassifier(random_state=42))
])

scores_enrichi = cross_val_score(pipeline_enrichi, data, y, cv=5, scoring='roc_auc')
print(f"ROC AUC avec enrichissement : {scores_enrichi.mean():.4f}")

Optimisations de Performances dans Scikit-Learn 1.8

La version 1.8 apporte des améliorations de performances significatives sur plusieurs fronts. Et le plus beau, c'est que ces optimisations sont transparentes — vous en profitez automatiquement en mettant à jour la bibliothèque. Pas besoin de changer une seule ligne de code.

Gap Safe Screening pour les Modèles L1

Les modèles avec pénalité L1 (Lasso, ElasticNet et variantes) bénéficient d'une accélération massive grâce aux règles de screening « gap safe ». Pour faire simple, cette technique permet au solveur de mettre à zéro les coefficients de certaines caractéristiques très tôt dans l'optimisation, évitant ainsi des calculs inutiles.

from sklearn.datasets import make_regression
from sklearn.linear_model import LassoCV, ElasticNetCV
import time

# Générer un jeu de données avec beaucoup de caractéristiques
X_reg, y_reg = make_regression(
    n_samples=5000,
    n_features=10000,
    n_informative=100,
    noise=10,
    random_state=42
)

# LassoCV avec gap safe screening (automatique dans 1.8)
lasso = LassoCV(cv=5, random_state=42)

debut = time.time()
lasso.fit(X_reg, y_reg)
duree = time.time() - debut

print(f"Temps d'entraînement LassoCV : {duree:.1f} secondes")
print(f"Nombre de caractéristiques sélectionnées : "
      f"{np.sum(lasso.coef_ != 0)}")
print(f"Alpha optimal : {lasso.alpha_:.6f}")
print(f"Score R² : {lasso.score(X_reg, y_reg):.4f}")

# ElasticNetCV
enet = ElasticNetCV(cv=5, l1_ratio=[0.1, 0.5, 0.7, 0.9], random_state=42)

debut = time.time()
enet.fit(X_reg, y_reg)
duree = time.time() - debut

print(f"\nTemps d'entraînement ElasticNetCV : {duree:.1f} secondes")
print(f"Nombre de caractéristiques sélectionnées : "
      f"{np.sum(enet.coef_ != 0)}")
print(f"Meilleur l1_ratio : {enet.l1_ratio_}")

Avec 10 000 caractéristiques, le gain peut atteindre 5 à 10x par rapport aux versions précédentes. Ça rend ces modèles enfin viables pour des jeux de données à très haute dimensionnalité — typiquement en génomique ou en NLP avec des features textuelles.

DecisionTreeRegressor Optimisé

L'arbre de décision avec le critère absolute_error passe d'une complexité O(n²) à O(n log n). C'est le genre d'amélioration algorithmique qui change tout :

from sklearn.tree import DecisionTreeRegressor
from sklearn.datasets import make_regression
import time

# Jeu de données conséquent
X_large, y_large = make_regression(
    n_samples=100_000,
    n_features=10,
    random_state=42
)

# Entraînement avec absolute_error (maintenant très rapide)
tree = DecisionTreeRegressor(
    criterion="absolute_error",
    max_depth=10,
    random_state=42
)

debut = time.time()
tree.fit(X_large, y_large)
duree = time.time() - debut

print(f"Temps d'entraînement (100K échantillons) : {duree:.2f} secondes")
print(f"Profondeur de l'arbre : {tree.get_depth()}")
print(f"Nombre de feuilles : {tree.get_n_leaves()}")
print(f"Score R² : {tree.score(X_large, y_large):.4f}")

Cette optimisation rend enfin le critère absolute_error (plus robuste aux valeurs aberrantes que l'erreur quadratique) utilisable sur des millions d'échantillons. Avant, c'était tout simplement impraticable.

ClassicalMDS : Nouvel Estimateur pour la Réduction de Dimension

Petite nouveauté qui mérite qu'on s'y attarde : scikit-learn 1.8 introduit ClassicalMDS (aussi connu sous le nom d'Analyse en Coordonnées Principales). C'est une méthode de réduction de dimension qui trouve une solution analytique exacte via la décomposition en valeurs propres.

from sklearn.manifold import ClassicalMDS
from sklearn.datasets import make_s_curve
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
import numpy as np

# Générer une courbe en S (un classique pour la réduction de dimension)
X_scurve, color = make_s_curve(n_samples=1500, random_state=42)

# Classical MDS
cmds = ClassicalMDS(n_components=2, random_state=42)
X_cmds = cmds.fit_transform(X_scurve)

# PCA pour comparaison
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scurve)

# Visualisation
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

axes[0].scatter(X_cmds[:, 0], X_cmds[:, 1], c=color, cmap='viridis',
                s=15, alpha=0.7)
axes[0].set_title('Classical MDS')
axes[0].set_xlabel('Composante 1')
axes[0].set_ylabel('Composante 2')

axes[1].scatter(X_pca[:, 0], X_pca[:, 1], c=color, cmap='viridis',
                s=15, alpha=0.7)
axes[1].set_title('PCA')
axes[1].set_xlabel('Composante 1')
axes[1].set_ylabel('Composante 2')

plt.tight_layout()
plt.savefig('comparaison_cmds_pca.png', dpi=150)
plt.show()

Classical MDS est particulièrement utile quand vous travaillez avec des matrices de distance plutôt que des données brutes. C'est un complément naturel à PCA : tandis que PCA minimise la perte de variance, Classical MDS préserve les distances entre points. Selon votre cas d'usage, l'un ou l'autre sera plus adapté.

Recherche d'Hyperparamètres Avancée

Avec toutes ces améliorations de performances, la recherche d'hyperparamètres devient nettement plus accessible. Voici un exemple complet qui combine pipeline, ColumnTransformer et recherche randomisée :

from sklearn.model_selection import RandomizedSearchCV
from sklearn.ensemble import (
    GradientBoostingClassifier,
    RandomForestClassifier
)
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from scipy.stats import randint, uniform

# Construire le pipeline de prétraitement
preprocesseur = ColumnTransformer([
    ('num', Pipeline([
        ('imputer', SimpleImputer(strategy='median')),
        ('scaler', StandardScaler()),
    ]), cols_numeriques),
    ('cat', Pipeline([
        ('imputer', SimpleImputer(strategy='most_frequent')),
        ('encoder', OneHotEncoder(handle_unknown='ignore',
                                  sparse_output=False)),
    ]), cols_categoriques_nominales + cols_categoriques_ordinales),
])

# Pipeline avec un modèle placeholder
pipeline = Pipeline([
    ('pretraitement', preprocesseur),
    ('modele', GradientBoostingClassifier(random_state=42))
])

# Grille de recherche pour le Gradient Boosting
param_distributions = {
    'modele__n_estimators': randint(50, 300),
    'modele__max_depth': randint(3, 15),
    'modele__learning_rate': uniform(0.01, 0.3),
    'modele__min_samples_split': randint(2, 20),
    'modele__subsample': uniform(0.6, 0.4),
}

# Recherche randomisée
recherche = RandomizedSearchCV(
    pipeline,
    param_distributions=param_distributions,
    n_iter=50,
    cv=5,
    scoring='roc_auc',
    random_state=42,
    n_jobs=-1,
    verbose=1
)

recherche.fit(data, y)

print(f"\nMeilleurs paramètres : {recherche.best_params_}")
print(f"Meilleur ROC AUC : {recherche.best_score_:.4f}")

# Analyser les résultats
import pandas as pd
resultats = pd.DataFrame(recherche.cv_results_)
top_5 = resultats.nsmallest(5, 'rank_test_score')[
    ['params', 'mean_test_score', 'std_test_score', 'rank_test_score']
]
print(f"\nTop 5 configurations :")
print(top_5.to_string())

Pipeline de Production Complet : De la Donnée Brute au Déploiement

Pour finir en beauté, construisons un pipeline complet prêt pour la production. Ce pipeline intègre toutes les bonnes pratiques qu'on a vues : prétraitement robuste, ingénierie des caractéristiques, sélection de modèle et sérialisation.

import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer, make_column_selector
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import (
    StandardScaler, OneHotEncoder, FunctionTransformer
)
from sklearn.impute import SimpleImputer
from sklearn.feature_selection import SelectFromModel
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import cross_val_score, train_test_split
from sklearn.metrics import (
    classification_report, roc_auc_score, confusion_matrix
)
import joblib

# 1. Préparation des données
X_train, X_test, y_train, y_test = train_test_split(
    data, y, test_size=0.2, stratify=y, random_state=42
)

# 2. Fonctions d'enrichissement
def enrichir_donnees(df):
    result = df.copy()
    if 'nb_achats' in df.columns and 'anciennete_mois' in df.columns:
        anciennete = df['anciennete_mois'].fillna(1).clip(lower=1)
        result['frequence_achat'] = df['nb_achats'] / anciennete
    if 'revenu' in df.columns and 'nb_achats' in df.columns:
        result['revenu_par_achat'] = (
            df['revenu'].fillna(0) / df['nb_achats'].clip(lower=1)
        )
    if 'age' in df.columns:
        result['tranche_age'] = pd.cut(
            df['age'],
            bins=[0, 25, 35, 50, 65, 100],
            labels=['18-25', '26-35', '36-50', '51-65', '65+']
        ).astype(str)
    return result

# 3. Pipeline complet
pipeline_production = Pipeline([
    # Étape 1 : Enrichissement des données
    ('enrichissement', FunctionTransformer(enrichir_donnees, validate=False)),

    # Étape 2 : Prétraitement par type de colonne
    ('pretraitement', ColumnTransformer([
        ('num', Pipeline([
            ('imputer', SimpleImputer(strategy='median')),
            ('scaler', StandardScaler()),
        ]), make_column_selector(dtype_include=np.number)),
        ('cat', Pipeline([
            ('imputer', SimpleImputer(strategy='constant',
                                       fill_value='inconnu')),
            ('encoder', OneHotEncoder(
                handle_unknown='ignore',
                sparse_output=False,
                min_frequency=0.02
            )),
        ]), make_column_selector(dtype_include=object)),
    ])),

    # Étape 3 : Sélection de caractéristiques
    ('selection', SelectFromModel(
        GradientBoostingClassifier(
            n_estimators=50, max_depth=3, random_state=42
        ),
        threshold='median'
    )),

    # Étape 4 : Modèle final
    ('modele', GradientBoostingClassifier(
        n_estimators=200,
        max_depth=5,
        learning_rate=0.1,
        subsample=0.8,
        random_state=42
    ))
])

# 4. Entraînement et évaluation
pipeline_production.fit(X_train, y_train)

# Prédictions
y_pred = pipeline_production.predict(X_test)
y_proba = pipeline_production.predict_proba(X_test)[:, 1]

# Métriques
print("Rapport de classification :")
print(classification_report(y_test, y_pred))
print(f"ROC AUC : {roc_auc_score(y_test, y_proba):.4f}")

# Validation croisée sur l'ensemble d'entraînement
scores_cv = cross_val_score(
    pipeline_production, X_train, y_train,
    cv=5, scoring='roc_auc'
)
print(f"\nROC AUC (CV 5-fold) : {scores_cv.mean():.4f} "
      f"(+/- {scores_cv.std():.4f})")

# 5. Sérialisation pour le déploiement
joblib.dump(pipeline_production, 'pipeline_churn_v1.joblib')
print("\nPipeline sauvegardé : pipeline_churn_v1.joblib")

# 6. Chargement et utilisation en production
pipeline_charge = joblib.load('pipeline_churn_v1.joblib')
nouvelles_predictions = pipeline_charge.predict_proba(X_test[:5])
print(f"\nPrédictions sur 5 nouveaux clients :")
for i, proba in enumerate(nouvelles_predictions[:, 1]):
    print(f"  Client {i+1} : {proba:.1%} risque de désabonnement")

Bonnes Pratiques et Conseils pour 2026

Avant de se quitter, voici les bonnes pratiques que je recommande pour tirer le meilleur parti de scikit-learn 1.8 :

1. Toujours Utiliser des Pipelines

Sérieusement, ne faites jamais de prétraitement en dehors d'un pipeline. Ça garantit la reproductibilité, évite les fuites de données pendant la validation croisée, et simplifie le déploiement. Un pipeline encapsule tout le workflow en un seul objet sérialisable. Il n'y a aucune bonne raison de s'en passer.

2. Exploiter le GPU Judicieusement

Le support GPU via l'Array API est puissant, mais ce n'est pas toujours nécessaire. Réservez-le pour :

  • Des jeux de données volumineux (plus de 100 000 échantillons)
  • Des opérations de prétraitement coûteuses (normalisation, PCA sur de grands espaces)
  • Des recherches d'hyperparamètres intensives

3. Profiter de Python Free-Threaded

Si vous êtes sur Python 3.14, testez systématiquement le backend threading de joblib. Les gains sont particulièrement nets pour :

  • La validation croisée (cross_val_score, GridSearchCV)
  • Les modèles d'ensemble (RandomForest, BaggingClassifier)
  • Tout ce qui utilise n_jobs > 1

4. Calibrer vos Modèles en Production

Si vos probabilités servent à prendre des décisions métier, calibrez toujours votre modèle. La calibration par température est le nouveau standard pour les problèmes multiclasses — c'est simple, efficace, et ça ne coûte quasiment rien en temps de calcul.

5. Versionner vos Pipelines

Utilisez joblib pour sauvegarder vos pipelines, et pensez à inclure des métadonnées : version de scikit-learn, date d'entraînement, métriques de performance. Votre futur vous (et votre équipe) vous remerciera.

import joblib
import sklearn
from datetime import datetime

# Sauvegarder avec métadonnées
metadata = {
    'pipeline': pipeline_production,
    'sklearn_version': sklearn.__version__,
    'date_entrainement': datetime.utcnow().isoformat(),
    'metriques': {
        'roc_auc_cv': float(scores_cv.mean()),
        'roc_auc_test': float(roc_auc_score(y_test, y_proba)),
    },
    'colonnes_attendues': list(data.columns),
}

joblib.dump(metadata, 'pipeline_churn_v1_metadata.joblib')
print("Pipeline et métadonnées sauvegardés avec succès")

Conclusion

Scikit-learn 1.8 marque une étape vraiment importante dans l'évolution de la bibliothèque. Le support GPU via l'Array API ouvre la porte à des performances qu'on n'aurait pas imaginées il y a quelques années, la compatibilité avec Python 3.14 free-threaded repousse les limites du parallélisme, et les nouvelles méthodes comme la calibration par température enrichissent sérieusement la boîte à outils du praticien ML.

Ce qui me plaît le plus, c'est que ces avancées restent fidèles à la philosophie de scikit-learn : une API cohérente, une documentation exemplaire, et une intégration transparente dans l'écosystème Python. Que vous construisiez un prototype dans un notebook Jupyter ou un pipeline de production distribué, scikit-learn 1.8 vous accompagne.

Les pipelines modernes que nous avons construits dans ce guide — combinant ColumnTransformer, ingénierie de caractéristiques, sélection de modèle et calibration — représentent l'état de l'art des bonnes pratiques ML en 2026. N'hésitez pas à les adopter dans vos projets : vous verrez la différence en termes de reproductibilité, de maintenabilité et de performances.

À propos de l'auteur Editorial Team

Our team of expert writers and editors.