teach.pascalyim.com
Sommaire

ML · Chapitre 2

Machine learning 2 — Régression

Lancer sur Kaggle

Après avoir mis en place les outils Python nécessaires à la manipulation de données dans le premier chapitre, nous abordons maintenant le premier grand problème classique de l'apprentissage supervisé : la régression. Étant donné un ensemble d'observations (xi,yi)(x_i, y_i) pour lesquelles on connaît à la fois les variables explicatives et la valeur d'une variable cible numérique, comment construire un modèle capable de prédire cette valeur cible pour de nouvelles observations ?

Nous commencerons par la régression linéaire des moindres carrés, en l'implémentant nous-mêmes pour bien comprendre ce qui se passe sous le capot, avant d'utiliser scikit-learn pour passer à plusieurs variables. Nous verrons ensuite comment évaluer correctement un modèle au moyen de métriques d'erreur et de procédures de validation, puis comment introduire de la non-linéarité en élargissant la base de variables (régression polynomiale) ou en changeant complètement de paradigme (k plus proches voisins). Le chapitre se conclut sur les questions de prétraitement : normalisation des données et utilisation des pipelines, indispensables dès qu'on combine plusieurs étapes.

L'objectif est double : maîtriser un premier ensemble d'outils opérationnels pour traiter un problème de régression sur un dataset tabulaire, et acquérir les bons réflexes méthodologiques (séparation entraînement/test, validation croisée, prévention du data leakage) qui resteront valables pour tous les algorithmes étudiés dans la suite du cours.

La régression linéaire des moindres carrés

Principe et formulation

Le cas le plus simple est celui de la régression linéaire simple, où l'on dispose d'une seule variable explicative xx et d'une variable cible yy. On cherche une droite

y^=ax+b\hat{y} = a x + b

qui approxime au mieux les couples (xi,yi)(x_i, y_i) observés. Pour chaque point, l'écart entre la valeur réelle et la valeur prédite par la droite, appelé résidu, vaut ei=yi(axi+b)e_i = y_i - (a x_i + b). La méthode des moindres carrés consiste à choisir aa et bb de manière à minimiser la somme des carrés de ces résidus :

mina,b  J(a,b)=i=1n(yi(axi+b))2.\min_{a,b} \; J(a,b) = \sum_{i=1}^{n} \bigl(y_i - (a x_i + b)\bigr)^2.

Pourquoi le carré plutôt que la valeur absolue ? D'une part, la fonction JJ devient quadratique en aa et bb, donc dérivable partout, et son minimum se calcule explicitement. D'autre part, le carré pénalise plus fortement les grands écarts, ce qui donne un estimateur mathématiquement très riche, lié de près aux notions de variance et de covariance.

Calcul des coefficients optimaux

La fonction J(a,b)J(a,b) est convexe et atteint son minimum lorsque ses dérivées partielles s'annulent simultanément. La dérivée par rapport à bb s'écrit

Jb=2i=1n(yiaxib).\frac{\partial J}{\partial b} = -2 \sum_{i=1}^{n} \bigl(y_i - a x_i - b\bigr).

L'annuler conduit à yi=axi+nb\sum y_i = a \sum x_i + n b, soit après division par nn :

b=yˉaxˉ.b = \bar{y} - a \bar{x}.

La droite de régression passe toujours par le point moyen (xˉ,yˉ)(\bar{x}, \bar{y}) du nuage de points. Cette propriété géométrique simple est une conséquence directe de la condition d'optimalité.

La dérivée par rapport à aa donne, après injection de l'expression de bb et réorganisation des termes :

a=i(xixˉ)(yiyˉ)i(xixˉ)2=Cov(x,y)Var(x).a = \frac{\sum_{i} (x_i - \bar{x})(y_i - \bar{y})}{\sum_{i} (x_i - \bar{x})^2} = \frac{\mathrm{Cov}(x,y)}{\mathrm{Var}(x)}.

Le numérateur mesure la covariance entre xx et yy — la manière dont elles varient ensemble — tandis que le dénominateur est la variance de xx. La pente est d'autant plus élevée que xx explique bien les variations de yy. Géométriquement, la droite obtenue minimise la somme des distances verticales au carré et équilibre les résidus autour de zéro.

Implémentation manuelle

Avant d'utiliser scikit-learn, il est instructif d'écrire soi-même une régression linéaire à partir des deux formules ci-dessus. La classe ci-dessous suit la convention scikit-learn d'un constructeur sans paramètres, d'une méthode fit qui apprend les coefficients, et d'une méthode predict qui les applique :

class MyLinearRegression: def __init__(self): self.a = None self.b = None def fit(self, x, y): x = np.array(x) y = np.array(y) x_mean = np.mean(x) y_mean = np.mean(y) numerator = np.sum((x - x_mean) * (y - y_mean)) denominator = np.sum((x - x_mean) ** 2) self.a = numerator / denominator self.b = y_mean - self.a * x_mean def predict(self, x): x = np.array(x) return self.a * x + self.b

Une fois la classe écrite, son utilisation tient en quelques lignes : on lit le dataset co2_mini, on extrait les colonnes consumption et co2, on entraîne le modèle puis on calcule les prédictions sur les mêmes points pour les comparer aux valeurs réelles.

df = pd.read_csv('/kaggle/input/datasets/pyim59/mini-datasets/co2_mini.csv') x = df['consumption'] y = df['co2'] model = MyLinearRegression() model.fit(x, y) y_hat = model.predict(x)

À ce stade, model.a contient la pente apprise et model.b l'ordonnée à l'origine. Le vecteur y_hat rassemble les valeurs prédites par la droite pour chacune des observations.

Visualiser le modèle avec Plotly

Pour juger visuellement de la qualité de l'ajustement, on superpose la droite prédite au nuage de points. Plotly distingue deux notions : la figure, qui est le conteneur principal (axes, titre, zone d'affichage), et les traces, qui sont les jeux de données affichés. Un appel à px.scatter crée une figure avec une seule trace, et la méthode add_scatter permet d'ajouter une trace supplémentaire à la figure existante, sans en créer une nouvelle.

fig = px.scatter(x=x, y=y) fig.add_scatter( x=x, y=y_hat, name="Valeurs prédites" ) fig.show()

L'intérêt est que les deux traces partagent les mêmes axes et la même échelle : la comparaison visuelle entre observations et prédictions est immédiate.

Analyser les résidus

L'examen des résidus ri=yiy^ir_i = y_i - \hat{y}_i est un outil de diagnostic essentiel. Un résidu positif signifie que le modèle sous-estime la valeur réelle ; un résidu négatif qu'il la surestime. Tracer les résidus en fonction de xx permet de vérifier la qualité de l'ajustement : pour un bon modèle linéaire, on s'attend à des résidus répartis aléatoirement autour de zéro, sans structure visible et avec une dispersion homogène.

residus = y - y_hat fig = px.scatter( x=x, y=residus, labels={"x": "Consommation", "y": "Résidu"}, title="Résidus de la régression linéaire" ) fig.add_hline(y=0, line_color="red") fig.show()

À l'inverse, un motif visible (courbure, élargissement systématique de la dispersion, points aberrants) suggère qu'une relation non linéaire, des outliers ou une variance non constante échappent au modèle. C'est précisément ce type de signal qui motivera, plus loin, le passage à la régression polynomiale ou aux kk plus proches voisins.

Mesurer la qualité d'un modèle : les métriques

Disposer de prédictions ne suffit pas, encore faut-il quantifier l'erreur que l'on commet. Une métrique résume cette erreur en un nombre, et plusieurs métriques coexistent parce qu'elles ne mettent pas l'accent sur les mêmes aspects.

MAE, MSE et RMSE

L'erreur absolue moyenne (MAE) est la moyenne des écarts en valeur absolue :

MAE=1niyiy^i.\mathrm{MAE} = \frac{1}{n}\sum_i |y_i - \hat{y}_i|.

Elle s'exprime dans les mêmes unités que la variable cible et reste relativement robuste aux gros écarts. L'erreur quadratique moyenne (MSE) élève chaque écart au carré :

MSE=1ni(yiy^i)2,\mathrm{MSE} = \frac{1}{n}\sum_i (y_i - \hat{y}_i)^2,

ce qui pénalise très fortement les grandes erreurs. La racine de la MSE, ou RMSE, ramène à l'unité de yy tout en conservant cette sensibilité au carré :

RMSE=MSE.\mathrm{RMSE} = \sqrt{\mathrm{MSE}}.

Si MAE et RMSE sont proches, c'est que les erreurs sont assez compactes. Si la RMSE est beaucoup plus grande que la MAE, c'est qu'il existe quelques points avec de très grosses erreurs : la RMSE est un excellent détecteur d'outliers.

On rencontre aussi la MAPE (Mean Absolute Percentage Error), qui exprime l'erreur en pourcentage de la valeur cible et permet de comparer des problèmes d'ordres de grandeur différents.

Le coefficient de détermination R2R^2

Le R2R^2 mesure la part de la variance de yy que le modèle parvient à expliquer, en se comparant à un modèle de référence très simple : prédire toujours la moyenne yˉ\bar{y}. En notant la somme des carrés des résidus SSres=(yiy^i)2\mathrm{SS}_{\text{res}} = \sum (y_i - \hat{y}_i)^2 et la somme des carrés totale SStot=(yiyˉ)2\mathrm{SS}_{\text{tot}} = \sum (y_i - \bar{y})^2, on définit

R2=1SSresSStot.R^2 = 1 - \frac{\mathrm{SS}_{\text{res}}}{\mathrm{SS}_{\text{tot}}}.

Un R2=1R^2 = 1 correspond à un modèle parfait, un R2=0R^2 = 0 à un modèle équivalent à la prédiction constante par la moyenne, et un R2R^2 négatif à un modèle pire que cette baseline. Le R2R^2 est donc à interpréter comme un gain relatif sur un modèle naïf, complémentaire des métriques d'erreur absolue. Attention toutefois : un R2R^2 élevé sur les données d'entraînement ne garantit ni la qualité prédictive sur de nouvelles données, ni l'absence de sur-apprentissage. Il faut toujours croiser plusieurs métriques et ne pas oublier d'examiner les résidus.

Implémentation et utilisation

On peut implémenter ces métriques en quelques lignes avec NumPy, ce qui aide à comprendre exactement ce qu'elles calculent :

def MAE(y, y_hat): return np.mean(np.abs(np.asarray(y_hat) - np.asarray(y))) def MSE(y, y_hat): return np.mean((np.asarray(y_hat) - np.asarray(y)) ** 2) def RMSE(y, y_hat): return np.sqrt(MSE(y, y_hat)) def MAPE(y, y_hat, eps=1e-12): y = np.asarray(y); y_hat = np.asarray(y_hat) return np.mean(np.abs((y_hat - y) / (np.abs(y) + eps))) def R2(y, y_hat): y = np.asarray(y); y_hat = np.asarray(y_hat) ss_res = np.sum((y - y_hat) ** 2) ss_tot = np.sum((y - np.mean(y)) ** 2) return 1 - ss_res / ss_tot

En pratique, scikit-learn fournit les versions optimisées correspondantes (mean_absolute_error, mean_squared_error, mean_absolute_percentage_error, r2_score) que l'on importe depuis sklearn.metrics dès qu'on travaille avec des modèles réels.

La régression linéaire multiple

Du cas simple au cas multiple

Lorsque la variable cible dépend de plusieurs variables explicatives, on étend la formulation à pp variables. Le modèle s'écrit

y^=β0+β1x1+β2x2++βpxp,\hat{y} = \beta_0 + \beta_1 x_1 + \beta_2 x_2 + \dots + \beta_p x_p,

β0\beta_0 joue le rôle de l'ordonnée à l'origine et où chaque βj\beta_j mesure l'effet de la variable xjx_j toutes choses égales par ailleurs. Avec deux variables, par exemple Length et Weight pour prédire Rings sur le dataset Abalone, le modèle décrit géométriquement un plan dans un espace à trois dimensions plutôt qu'une simple droite.

Le dataset abalone_mini

L'abalone est un mollusque (un ormeau) dont la coquille présente des anneaux dont le nombre permet d'estimer l'âge. Le dataset rassemble pour chaque individu plusieurs mesures physiques (Length, Diameter, Height, Weight) et le nombre d'anneaux Rings. L'objectif standard est de prédire Rings à partir des mesures physiques, ce qui en fait un cas d'école parfait pour la régression multiple.

Pour visualiser simultanément trois variables, Plotly propose scatter_3d, qui place chaque observation comme un point dans un espace tridimensionnel interactif. L'option opacity rend les points semi-transparents, ce qui améliore la lisibilité dans les zones denses :

px.scatter_3d( df, x='Length', y='Weight', z='Rings', title='Abalone: Length, Weight vs Rings', opacity=0.2 )

Le graphique permet de pivoter, zoomer et survoler les points, ce qui est particulièrement utile pour repérer la forme générale du nuage avant de modéliser.

Régression linéaire avec scikit-learn

La classe LinearRegression de sklearn.linear_model implémente la méthode des moindres carrés ordinaires (OLS) pour un nombre quelconque de variables explicatives. La convention scikit-learn impose deux choses : X doit être un tableau à deux dimensions, même s'il n'y a qu'une seule variable, et y doit être unidimensionnel.

from sklearn.linear_model import LinearRegression from sklearn.metrics import ( mean_absolute_error, mean_squared_error, mean_absolute_percentage_error, r2_score ) df = pd.read_csv('/kaggle/input/datasets/pyim59/mini-datasets/abalone_mini.csv') X = df[['Length', 'Weight']] y = df['Rings'] model = LinearRegression() model.fit(X, y) y_hat = model.predict(X)

Après l'appel à fit, l'attribut model.coef_ contient les coefficients β1,,βp\beta_1, \dots, \beta_p et model.intercept_ contient β0\beta_0. La méthode predict applique le plan ajusté à de nouvelles observations. Pour visualiser le résultat en 3D, on superpose au nuage des valeurs réelles une nouvelle trace contenant les valeurs prédites, que l'on distingue typiquement par une couleur :

fig = px.scatter_3d( x=df.Length, y=df.Weight, z=df.Rings, title='Abalone: réel (Rings) vs prédiction (plan)', opacity=0.25 ) fig.add_scatter3d( x=df.Length, y=df.Weight, z=y_hat, name='Prédit (LinearRegression)', mode='markers', marker=dict(color='red', size=5) ) fig.show()

Les points rouges forment un plan dans l'espace : c'est exactement ce que produit la régression linéaire à deux variables.

Cas général à nn variables et écriture matricielle

Pour nn variables, le modèle s'écrit y^=b+j=1najxj\hat{y} = b + \sum_{j=1}^{n} a_j x_j. En empilant les observations dans une matrice XX de taille m×nm \times n et la cible dans un vecteur yy de taille mm, on obtient la forme matricielle compacte y^=Xa+b\hat{y} = X a + b. En absorbant bb dans une colonne de 1 ajoutée à XX, la solution des moindres carrés admet une expression fermée appelée équation normale :

a=(XTX)1XTy,a = (X^T X)^{-1} X^T y,

valable lorsque XTXX^T X est inversible. Cette formule généralise directement celle obtenue à une variable. En pratique, scikit-learn n'inverse jamais cette matrice naïvement : il utilise des solveurs numériques plus stables, capables de gérer les cas où XTXX^T X est mal conditionnée ou les jeux de données très volumineux.

Il devient alors trivial d'utiliser toutes les variables disponibles. Pour prédire Rings à partir de toutes les autres caractéristiques, il suffit de retirer la colonne cible avec drop :

X = df.drop(columns=['Rings']) y = df['Rings'] model = LinearRegression() model.fit(X, y) y_hat = model.predict(X)

L'enrichissement du modèle améliore généralement les métriques sur les données d'entraînement, mais cela soulève immédiatement une question méthodologique fondamentale : ces gains se traduiront-ils sur de nouvelles données ?

Évaluer correctement : entraînement, test et validation croisée

Pourquoi séparer les données

Évaluer un modèle sur les données qui ont servi à l'entraîner conduit presque toujours à une estimation optimiste et trompeuse de sa qualité. La situation est analogue à un examen dont les questions seraient strictement identiques à celles répétées en cours : une bonne note traduit alors essentiellement une mémorisation, pas une compréhension. De la même manière, un modèle qui a vu un point pendant l'apprentissage peut très bien y produire une prédiction parfaite sans pour autant savoir généraliser.

L'ensemble de test sert précisément à mesurer la capacité du modèle à se comporter sur des données qu'il n'a jamais vues, et donc à détecter un éventuel sur-apprentissage. La fonction train_test_split de sklearn.model_selection mélange les données puis les sépare en deux ensembles disjoints :

from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) model = LinearRegression() model.fit(X_train, y_train) y_hat = model.predict(X_test)

Le paramètre test_size=0.2 réserve 20 % des données à l'évaluation et random_state=42 fixe la graine aléatoire pour rendre la séparation reproductible. Les métriques doivent ensuite être calculées sur l'ensemble de test, qui simule des données jamais vues. Comparer ces métriques avec celles obtenues sur l'ensemble d'entraînement met en évidence un éventuel écart caractéristique du sur-apprentissage.

L'instabilité du tirage unique et la validation croisée

Si l'on omet le random_state, chaque exécution produit une nouvelle séparation aléatoire et donc des métriques différentes — parfois sensiblement — alors que ni le modèle ni les données n'ont changé. Un seul tirage train/test peut tomber, par chance, sur une partition trop favorable ou trop défavorable, et la performance mesurée est ainsi à la fois instable et peu représentative.

La validation croisée apporte une réponse robuste à ce problème. Dans la version la plus utilisée, dite k-fold, on découpe le dataset en kk plis de taille comparable. Pour chaque itération, un pli sert de jeu de test et les k1k-1 autres servent à entraîner le modèle ; l'opération est répétée kk fois en faisant tourner le rôle du pli de test. On obtient au final kk scores, dont la moyenne donne une estimation globale de la performance et l'écart-type une mesure de stabilité. Avec k=5k = 5, chaque observation est utilisée quatre fois pour l'entraînement et une fois pour l'évaluation : toutes les données contribuent à l'évaluation.

Scikit-learn fournit la fonction cross_val_score qui automatise tout ce processus :

from sklearn.model_selection import cross_val_score scores = cross_val_score( model, X, y, cv=5, scoring="r2" ) print(scores.mean(), scores.std())

Le paramètre cv fixe le nombre de plis et scoring choisit la métrique. Un faible écart-type entre les plis indique une performance stable. Pour les régressions, les valeurs courantes de scoring sont "r2", "neg_mean_absolute_error", "neg_mean_squared_error", "neg_root_mean_squared_error" et "neg_mean_absolute_percentage_error".

Pourquoi les scores d'erreur sont-ils négatifs ? Par convention, cross_val_score cherche toujours à maximiser un score. Comme les métriques d'erreur (MAE, MSE, RMSE, MAPE) doivent être minimisées, scikit-learn les renvoie en négatif. Pour retrouver l'erreur réelle, on prend simplement l'opposé : mae = -scores.

Régression polynomiale et modèles non linéaires

La régression linéaire suppose une relation y^=ax+b\hat{y} = a x + b, ce qui n'est pas toujours réaliste. Un examen des résidus mettant en évidence une courbure, par exemple lors de la prédiction du poids d'un abalone à partir de sa seule longueur, suggère que la relation est intrinsèquement non linéaire. Une manière simple d'introduire de la non-linéarité consiste à enrichir la base de variables avec des puissances de la variable explicative :

y^=β0+β1x+β2x2.\hat{y} = \beta_0 + \beta_1 x + \beta_2 x^2.

On parle alors de régression polynomiale, mais le modèle reste linéaire par rapport aux coefficients βj\beta_j — c'est ce qui permet de continuer à utiliser LinearRegression exactement comme avant. Concrètement, on crée explicitement la nouvelle colonne dans le DataFrame :

df['Length2'] = df['Length']**2 X = df[['Length', 'Length2']] y = df['Weight'] model = LinearRegression() model.fit(X, y) y_hat = model.predict(X)

Le modèle a maintenant accès à un espace plus riche et peut épouser la courbure observée. On peut bien sûr étendre l'idée à des polynômes de degré supérieur en ajoutant Length3, Length4, etc., en gardant à l'esprit que monter le degré augmente le risque de sur-apprentissage : le modèle finit par mémoriser le bruit plutôt que la tendance.

Scikit-learn fournit pour cela la classe PolynomialFeatures, qui construit automatiquement toutes les puissances et combinaisons jusqu'à un degré donné, ce qui devient indispensable dès qu'il y a plusieurs variables.

Les kk plus proches voisins (k-NN)

Une approche locale, non paramétrique

Le k-Nearest Neighbors Regressor adopte un point de vue radicalement différent. Plutôt que d'apprendre une formule globale reliant X à y, il prédit pour une nouvelle observation xx en s'appuyant uniquement sur les observations les plus proches du jeu d'entraînement. Le procédé tient en trois étapes : on calcule la distance entre xx et toutes les observations d'entraînement, on identifie les kk plus proches — les voisins — et on prend la moyenne de leurs valeurs cibles :

y^(x)=1kiNk(x)yi,\hat{y}(x) = \frac{1}{k} \sum_{i \in \mathcal{N}_k(x)} y_i,

Nk(x)\mathcal{N}_k(x) désigne l'ensemble des kk plus proches voisins de xx. Aucune équation n'est apprise globalement : le modèle s'adapte localement, ce qui le rend très flexible mais aussi très dépendant de la notion de distance employée.

Le rôle de la distance

La similarité entre observations est mesurée par une distance dans l'espace des variables. Scikit-learn utilise par défaut la distance de Minkowski, qui s'écrit, pour deux observations xx et xx' à pp dimensions :

d(x,x)=(j=1pxjxjp)1/p.d(x, x') = \left( \sum_{j=1}^{p} |x_j - x'_j|^p \right)^{1/p}.

Le cas p=2p=2 redonne la distance euclidienne classique, j(xjxj)2\sqrt{\sum_j (x_j - x'_j)^2}, et le cas p=1p=1 la distance de Manhattan, jxjxj\sum_j |x_j - x'_j|. On peut aussi pondérer les voisins selon leur proximité, en remplaçant la moyenne simple par une moyenne pondérée du type wi=1/d(x,xi)w_i = 1/d(x, x_i) : les voisins les plus proches ont alors plus d'influence.

Implémentation et paramètres

L'utilisation reproduit exactement l'interface des autres modèles scikit-learn, ce qui est l'un des grands attraits de la bibliothèque :

from sklearn.neighbors import KNeighborsRegressor model = KNeighborsRegressor(n_neighbors=5) model.fit(X_train, y_train) y_hat = model.predict(X_test)

Les paramètres importants sont peu nombreux mais déterminants. Le nombre de voisins n_neighbors règle la flexibilité du modèle : un petit kk produit un modèle très souple mais sensible au bruit, tandis qu'un grand kk lisse les prédictions au risque de sous-apprentissage. La distance utilisée se choisit via metric (Minkowski par défaut), et la pondération via weights ("uniform" ou "distance").

Le k-NN n'a pas de phase d'entraînement coûteuse — fit se contente de mémoriser les données — mais sa prédiction demande de calculer toutes les distances, ce qui peut devenir lourd sur de grands datasets.

Le dataset house_mini et la sensibilité à l'échelle

Présentation du dataset

Le dataset house_mini est une version simplifiée de KC House Data, qui regroupe des informations sur des ventes de logements dans le comté de King (État de Washington). Chaque ligne décrit un logement, avec sa cible price (le prix de vente) et plusieurs variables explicatives : bedrooms et bathrooms (nombres de chambres et de salles de bain), sqft_living (surface habitable en pieds carrés), sqft_lot (surface totale du terrain), floors (nombre d'étages) et zipcode (code postal, variable catégorielle encodée numériquement).

Une exploration rapide avec df.shape, df.info(), df.describe() et un px.violin sur la distribution du prix révèle des ordres de grandeur très différents entre variables : sqft_living peut varier de quelques centaines à plusieurs milliers, tandis que bathrooms reste typiquement entre 1 et 5.

Le piège de l'échelle pour le k-NN

Cette hétérogénéité d'échelle a une conséquence dramatique pour les méthodes basées sur des distances comme le k-NN. La distance euclidienne entre deux observations est dominée par les variables ayant les plus grandes valeurs numériques. Concrètement, une différence de 500 sur sqft_living écrase complètement une différence de 1 sur bathrooms, même si cette dernière est souvent plus informative pour le prix. Les voisins sélectionnés finissent par ne refléter qu'une seule variable — la plus grande en valeur — et la notion même de « voisin le plus proche » perd son sens. Le modèle peut alors produire des prédictions instables et des performances très dégradées par rapport à ce que l'on obtiendrait avec des variables comparables.

La régression linéaire est en revanche insensible à un changement d'échelle : multiplier une variable par 1000 multiplie simplement le coefficient correspondant par 1/1000. Le k-NN, le SVR, les réseaux de neurones et plus généralement toutes les méthodes basées sur des distances ou des produits scalaires nécessitent une normalisation préalable.

Standardisation et autres scalers

La standardisation transforme chaque variable selon

xscaled=xμσ,x_{\text{scaled}} = \frac{x - \mu}{\sigma},

μ\mu et σ\sigma sont la moyenne et l'écart-type de la variable. Après standardisation, chaque variable a une moyenne nulle et une variance unitaire, et toutes contribuent à parts comparables à la distance.

Scikit-learn implémente cette transformation dans la classe StandardScaler :

from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)

La distinction entre fit_transform et transform est cruciale : fit_transform calcule la moyenne et l'écart-type uniquement sur les données d'entraînement puis applique la transformation, tandis que transform réutilise ces mêmes paramètres sur le test. Toute autre démarche (par exemple fit_transform sur l'ensemble complet avant la séparation) introduit une fuite d'information : les statistiques utilisées pour normaliser le train auraient été contaminées par les valeurs du test.

Pour repasser dans l'espace original, par exemple pour interpréter ou afficher des résultats, on dispose de scaler.inverse_transform, qui applique x=xscaledσ+μx = x_{\text{scaled}} \cdot \sigma + \mu.

Scikit-learn propose plusieurs autres scalers dans le module sklearn.preprocessing. Le MinMaxScaler ramène toutes les variables dans un intervalle borné, généralement [0,1][0, 1], ce qui est utile lorsqu'on souhaite préserver les rapports d'origine. Le RobustScaler utilise la médiane et l'intervalle interquartile à la place de la moyenne et de l'écart-type, et résiste mieux aux valeurs aberrantes. Le choix du scaler dépend de la distribution des variables et de la sensibilité du modèle aux outliers.

Les pipelines de scikit-learn

Pourquoi un pipeline ?

Lorsque la chaîne de traitement comporte plusieurs étapes — par exemple un scaler suivi d'un modèle — il est tentant d'enchaîner les fit_transform, transform et fit à la main. C'est faisable, mais source d'erreurs : on peut oublier d'utiliser le même scaler entre train et test, normaliser avant la séparation et provoquer un data leakage, ou se tromper en validation croisée où chaque pli devrait avoir sa propre normalisation.

Un pipeline scikit-learn encapsule plusieurs étapes dans un seul objet exécuté automatiquement dans le bon ordre. La normalisation est alors apprise sur le train réel à chaque étape (y compris à chaque pli de la validation croisée) et appliquée correctement au test correspondant.

Construction et utilisation

La classe Pipeline prend une liste de couples (nom, transformateur ou estimateur). Le dernier élément doit être un modèle ; les autres sont des transformateurs.

from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.neighbors import KNeighborsRegressor pipe = Pipeline([ ("scaler", StandardScaler()), ("model", KNeighborsRegressor(n_neighbors=5)) ]) pipe.fit(X_train, y_train) y_hat = pipe.predict(X_test)

Lors de pipe.fit, le StandardScaler apprend ses paramètres sur X_train et transforme les données, puis le k-NN s'entraîne sur les données transformées. Lors de pipe.predict, X_test est transformé avec les paramètres appris sur X_train, puis le k-NN produit ses prédictions. On accède aux objets internes via pipe.named_steps["scaler"] et pipe.named_steps["model"], ce qui est utile pour inspecter ou récupérer le modèle entraîné.

Les pipelines fonctionnent directement avec cross_val_score : à chaque pli, la normalisation est ajustée sur le train du pli, puis appliquée au test du pli, ce qui garantit une évaluation méthodologiquement correcte sans aucun effort supplémentaire.

Exercices

Exercice 1 — Implémenter une régression linéaire simple

L'objectif est d'implémenter manuellement une régression linéaire simple, sans utiliser scikit-learn, afin de comprendre le calcul des coefficients. On considère un modèle de la forme y^=ax+b\hat{y} = a x + bxx est la variable explicative, yy la variable cible, aa la pente et bb l'ordonnée à l'origine.

Écrire une classe MyLinearRegression contenant un constructeur __init__, une méthode fit(x, y) qui calcule les coefficients aa et bb, et une méthode predict(x) qui renvoie les valeurs prédites y^\hat{y}. On n'utilisera que Python et NumPy. x et y peuvent être des listes, des Series Pandas ou des tableaux NumPy. Les coefficients seront calculés selon les formules

a=i(xixˉ)(yiyˉ)i(xixˉ)2,b=yˉaxˉ.a = \frac{\sum_i (x_i - \bar{x})(y_i - \bar{y})}{\sum_i (x_i - \bar{x})^2}, \qquad b = \bar{y} - a \bar{x}.

L'interface attendue est :

model = MyLinearRegression() model.fit(x, y) y_hat = model.predict(x)

Tester ensuite la classe sur le dataset co2_mini, en utilisant consumption comme variable explicative et co2 comme cible.

Exercice 2 — Régression linéaire simple sur Abalone

Sur le dataset Abalone, prédire Rings à partir de la seule variable Length à l'aide d'une régression linéaire scikit-learn. Visualiser sur un même graphique les valeurs réelles et les valeurs prédites, puis tracer les résidus.

Exercice 3 — Régression linéaire multiple à toutes les variables

Toujours sur Abalone, prédire Rings à partir de toutes les autres caractéristiques. On utilisera drop pour éliminer la colonne Rings du DataFrame avant de construire X. Comparer les métriques (MAE, RMSE, MAPE, R2R^2) à celles obtenues avec une seule variable. La prédiction est-elle améliorée ?

Exercice 4 — Train/test sur Abalone

Reprendre la régression sur toutes les variables d'Abalone en utilisant cette fois un découpage train/test avec train_test_split. Calculer les métriques sur l'ensemble de test. Refaire l'expérience plusieurs fois sans fixer random_state : que constate-t-on ?

Exercice 5 — Validation croisée sur la MAPE

Évaluer la régression linéaire sur Abalone par validation croisée à 5 plis, en utilisant la MAPE comme métrique. On rappelle que la valeur de scoring correspondante est "neg_mean_absolute_percentage_error" et qu'il faut prendre l'opposé du résultat pour obtenir l'erreur réelle.

Exercice 6 — Régression linéaire pour le poids d'un abalone

On souhaite maintenant prédire le poids (Weight) d'un abalone à partir de sa longueur (Length). Charger le dataset Abalone, entraîner une régression linéaire pour prédire Weight à partir de Length, calculer les valeurs prédites, puis visualiser sur un même graphique les valeurs réelles et les valeurs prédites. Tracer également les résidus. Qu'observe-t-on ?

Exercice 7 — Régression polynomiale

On souhaite améliorer la prédiction du poids d'un abalone à partir de sa longueur en introduisant des variables polynomiales construites manuellement. Charger le dataset Abalone, ajouter au DataFrame une colonne Length2 correspondant au carré de Length, puis construire la matrice X à partir des colonnes Length et Length2. Entraîner un modèle LinearRegression pour prédire Weight, calculer les valeurs prédites, et réaliser deux visualisations :

  • une visualisation 3D avec Length en axe xx, Length2 en axe yy et Weight (réel et prédit) en axe zz ;
  • une visualisation 2D Length versus Weight réel et prédit.

Exercice 8 — k-NN sur Abalone

Prédire le poids (Weight) d'un abalone à partir de sa longueur (Length) à l'aide d'un k-NN. Visualiser les valeurs réelles et prédites. Refaire ensuite l'exercice en prédisant Rings à partir de toutes les autres caractéristiques avec un k-NN, en utilisant un découpage train/test.

Exercice 9 — Exploration de house_mini

Explorer le dataset house_mini : dimensions, statistiques descriptives, distribution de la variable cible price, graphiques pertinents.

df = pd.read_csv('/kaggle/input/datasets/pyim59/mini-datasets/house_mini.csv') df.head() df.shape, df.info(), df.describe() px.violin(df.price)

Exercice 10 — Régression linéaire sur house_mini

Prédire le prix d'un logement à l'aide d'une régression linéaire. On effectuera la séparation train/test, on calculera les métriques sur le test, puis on visualisera par exemple sqft_living versus price réel et prédit, ainsi que les résidus. Commenter les remarques que suscite la qualité de l'ajustement.

Exercice 11 — k-NN sur house_mini

Reprendre l'exercice précédent en remplaçant la régression linéaire par un KNeighborsRegressor (sans normalisation pour l'instant). Comparer les métriques et commenter.

Exercice 12 — Normaliser les données

Reprendre l'exercice du k-NN sur house_mini, mais en normalisant cette fois X_train et X_test avec un StandardScaler (en pensant à n'ajuster le scaler que sur le train). Comparer les performances obtenues à celles sans normalisation. Quelles conclusions en tirez-vous sur la sensibilité du k-NN à l'échelle des variables ?

Exercice 13 — Encapsuler dans un pipeline

Adapter l'exercice précédent en utilisant un Pipeline regroupant le StandardScaler et le KNeighborsRegressor. Vérifier que les performances obtenues sont cohérentes avec celles de l'exercice 12, et apprécier la simplification du code.

Exercice 14 — Comparaison par validation croisée

Sur house_mini, comparer la performance de la régression linéaire et du pipeline k-NN (StandardScaler + KNeighborsRegressor(n_neighbors=5)) au moyen d'une validation croisée à 10 plis, en utilisant la MAPE comme métrique. Quel modèle l'emporte ?

Pour aller plus loin

Pour approfondir la régression linéaire et ses variantes régularisées (Ridge, Lasso, ElasticNet), la documentation officielle de scikit-learn rassemble toutes les classes du module linear_model ainsi qu'un guide détaillé : sklearn.linear_model.LinearRegression et le guide utilisateur « Linear Models ». La construction automatique de variables polynomiales s'effectue via PolynomialFeatures, souvent combinée à LinearRegression au sein d'un pipeline. Le k-NN est documenté dans KNeighborsRegressor et le guide « Nearest Neighbors ». Pour le prétraitement, voir StandardScaler, MinMaxScaler et RobustScaler, ainsi que le guide Preprocessing data. Enfin, la validation croisée et les pipelines sont couverts en détail dans les guides Cross-validation et Pipelines and composite estimators, avec les classes cross_val_score et Pipeline comme points d'entrée principaux.