Nettoyez vos données
L’exploration du chapitre précédent a laissé une liste de problèmes : des colonnes inutiles, 177 âges manquants, des cabines presque toujours absentes, et quelques billets hors de prix. Que faire de tout cela ? Supprimer, remplacer, garder tel quel ? Il n’y a pas de réponse universelle, mais il y a une méthode.
Dans ce chapitre, vous allez apprendre à nettoyer un jeu de données dans l’ordre : retirer ce qui ne sert à rien, éliminer les doublons, traiter les valeurs manquantes, puis les valeurs aberrantes. Le fil rouge reste le Titanic, complété par un petit extrait de dix passagers que vous pourrez recalculer à la main.
Retirez ce qui ne sert à rien
Section intitulée « Retirez ce qui ne sert à rien »Le filtrage d’attributs est le premier geste. Il prend deux formes.
La suppression retire les colonnes qui n’ont aucun lien avec la cible. Sur le Titanic, PassengerId est un simple numéro de ligne ; Name et Ticket sont (presque) uniques pour chaque passager. Les garder, c’est offrir au modèle un moyen de « reconnaître » chaque passager par cœur, sans rien apprendre de général.
La sélection garde les variables les plus pertinentes parmi celles qui restent. Elle réduit la dimension, améliore souvent les performances, simplifie l’interprétation et peut réduire le coût de collecte. Nous y consacrerons le chapitre 4.
Éliminez les doublons et corrigez les formats
Section intitulée « Éliminez les doublons et corrigez les formats »Une même ligne peut apparaître plusieurs fois : fichier fusionné deux fois, export relancé, erreur d’un système amont. Les doublons faussent les comptages et, en apprentissage, peuvent se retrouver à la fois dans le jeu d’apprentissage et dans le jeu de test. Le réflexe est de comparer le nombre de lignes au nombre de valeurs distinctes d’un identifiant censé être unique.
En TP, sur un fichier de photos géolocalisées de Rennes, nous avions 29 541 lignes mais seulement 4 148 identifiants de photo distincts : environ 86 % des lignes étaient des copies. Un simple drop_duplicates() a ramené le fichier à 4 148 lignes. Nous reprendrons ce cas en détail au chapitre 10.
print(len(df), df["PassengerId"].nunique()) # s'ils diffèrent, il y a des doublonsdf = df.drop_duplicates() # supprime les lignes strictement identiquesLes problèmes de format sont plus sournois : des dates stockées comme du texte ("06/06/2020"), des unités mélangées (des prix en euros et en francs), des caractères mal décodés (« é » à la place de « é »), des catégories écrites de plusieurs façons (« Rennes », « RENNES », « rennes »). On les corrige avec pd.to_datetime, des conversions explicites et une normalisation du texte (minuscules, suppression des accents).
Comprenez pourquoi des valeurs manquent
Section intitulée « Comprenez pourquoi des valeurs manquent »Avant de choisir quoi faire d’une valeur manquante, demandez-vous pourquoi elle manque. On distingue classiquement trois mécanismes :
- MCAR (missing completely at random) : le manque est dû au pur hasard, sans lien avec les données. Par exemple, une page de registre perdue.
- MAR (missing at random) : le manque dépend d’autres variables observées. Par exemple, l’âge est plus souvent inconnu pour les passagers de 3e classe, moins bien enregistrés.
- MNAR (missing not at random) : le manque dépend de la valeur manquante elle-même. Par exemple, dans une enquête, les personnes aux revenus les plus élevés sont celles qui refusent le plus de donner leur revenu.
Pourquoi est-ce important ? Parce que supprimer les lignes incomplètes ou remplacer par la moyenne ne donne des résultats sans biais qu’en MCAR. En MAR, on peut corriger en s’appuyant sur les autres variables. En MNAR, aucune méthode automatique ne suffit : il faut connaître le domaine.
Choisissez une stratégie pour les valeurs manquantes
Section intitulée « Choisissez une stratégie pour les valeurs manquantes »Voici les options, de la plus brutale à la plus fine :
| Stratégie | Quand l’utiliser | Limite |
|---|---|---|
| Supprimer les lignes | manquants rares et dus au hasard | perte d’exemples (catastrophique sur le Titanic) |
| Supprimer la colonne | beaucoup de manquants (60 à 70 % et plus) et variable non essentielle | perte d’information (Cabin, 77 %) |
| Imputation simple : moyenne, médiane, mode | rapide ; le mode pour une variable catégorielle | écrase la variance, crée un pic artificiel |
| Imputation par un modèle (régression, k plus proches voisins) | la variable manquante est liée à d’autres | plus coûteuse, biaisée si le manque n’est pas aléatoire |
| Indicateur de manquant, ou catégorie « inconnu » | le fait même de manquer est informatif | ajoute des colonnes |
Sur le Titanic, cela donne trois décisions différentes : Embarked (2 manquants) s’impute par le mode, le port le plus fréquent ; Cabin (77 %) se supprime, ou se réduit à un indicateur « cabine connue » ; Age (20 %) mérite mieux qu’une moyenne, par exemple une imputation par régression à partir de la classe et du titre extrait du nom.
Pourquoi se méfier de l’imputation par la moyenne ? Faisons le calcul sur un petit extrait.
Avec 20 % d’âges imputés par la moyenne, l’histogramme de l’âge présente un pic artificiel à 29-30 ans, et toutes les corrélations avec l’âge sont affaiblies. C’est acceptable pour un premier modèle rapide, pas pour une analyse fine.
En scikit-learn, l’imputation est un objet qui apprend la valeur de remplacement :
from sklearn.impute import SimpleImputer
imputeur = SimpleImputer(strategy="median", add_indicator=True)imputeur.fit(X_app[["Age"]]) # la médiane est calculée sur l'apprentissage seulementX_app_age = imputeur.transform(X_app[["Age"]])X_test_age = imputeur.transform(X_test[["Age"]]) # même médiane appliquée au test# add_indicator=True ajoute une colonne 0/1 « l'âge manquait »Détectez et traitez les valeurs aberrantes
Section intitulée « Détectez et traitez les valeurs aberrantes »Une valeur aberrante (outlier) est une valeur très éloignée des autres. Elle peut être une erreur (un âge de 250 ans, une température de −999 codant une panne de capteur) ou un cas rare mais réel (un billet de première classe exceptionnellement cher, une transaction frauduleuse). Toute la difficulté est de faire la différence.
La règle de l’écart interquartile
Section intitulée « La règle de l’écart interquartile »La méthode la plus robuste repose sur les quartiles. Notons le premier quartile (25 % des valeurs sont en dessous) et le troisième (75 % en dessous). L’écart interquartile vaut . Une valeur est déclarée aberrante si elle sort de l’intervalle
C’est exactement la règle qui dessine les « moustaches » d’une boîte à moustaches.
Le z-score et son point faible
Section intitulée « Le z-score et son point faible »Une autre règle courante calcule le z-score , où est la moyenne et l’écart-type, et déclare aberrante toute valeur telle que . Sur les dix billets, et , si bien que le billet à 263 n’a qu’un z-score de : il passe sous le seuil.
Que s’est-il passé ? La valeur aberrante a elle-même gonflé la moyenne et, surtout, l’écart-type qui sert à la juger. C’est l’effet de masquage. Le z-score suppose de plus une distribution à peu près normale, ce que les prix ne sont pas. Les quartiles, eux, ne bougent presque pas en présence d’un extrême : ils sont robustes. Pour de gros volumes ou plusieurs variables à la fois, on utilise des modèles dédiés comme Isolation Forest ou le facteur local d’anomalie (LOF).
q1, q3 = df["Fare"].quantile([0.25, 0.75])iqr = q3 - q1aberrant = (df["Fare"] < q1 - 1.5 * iqr) | (df["Fare"] > q3 + 1.5 * iqr)print(aberrant.sum(), "billets signalés")Que faire d’une valeur aberrante ?
Section intitulée « Que faire d’une valeur aberrante ? »- Si c’est une erreur avérée, on la corrige quand c’est possible, sinon on la traite comme une valeur manquante.
- Si c’est un cas réel, on la garde, et l’on choisit des méthodes qui la supportent : écrêtage aux percentiles (winsorisation), transformation logarithmique (chapitre 3), statistiques robustes (médiane plutôt que moyenne), mise à l’échelle robuste (
RobustScaler).
En résumé
Section intitulée « En résumé »- On supprime les attributs sans lien avec la cible (identifiants), après avoir vérifié qu’on ne peut rien en extraire d’utile.
- Les doublons se détectent en comparant le nombre de lignes au nombre d’identifiants distincts ; en TP, 29 541 lignes cachaient 4 148 photos.
- Les valeurs manquantes peuvent être MCAR, MAR ou MNAR ; supprimer ou imputer par la moyenne n’est sans biais qu’en MCAR.
- L’imputation par la moyenne conserve la moyenne mais réduit la variance (multipliée par la part de valeurs connues).
- La règle de l’écart interquartile détecte les valeurs aberrantes de façon robuste ; le z-score peut être masqué par la valeur extrême elle-même.
- Une valeur aberrante réelle se garde et se traite par écrêtage, logarithme ou méthodes robustes.
Exercices
Section intitulée « Exercices »Exercice · Choisir une stratégie par colonne
facileDans un fichier de 5 000 clients d’une banque, vous observez : telephone_fixe manquant à 82 % ; ville manquante pour 12 clients ; revenu manquant à 15 %, surtout chez les clients les plus aisés, qui refusent de le déclarer. Proposez une stratégie pour chaque colonne.
Voir la solution
telephone_fixe(82 %) : trop de manquants pour imputer quoi que ce soit de fiable. On supprime la colonne, ou on la remplace par un indicateur « a déclaré un fixe » si l’on pense que ce fait est informatif (clients plus âgés, par exemple).ville(12 clients) : très peu de manquants. On peut imputer par le mode (la ville la plus fréquente), ajouter une catégorie « inconnue », ou supprimer ces 12 lignes sans grande perte.revenu(15 %) : le manque dépend de la valeur elle-même, c’est du MNAR. Une imputation par la moyenne sous-estimerait systématiquement les revenus des clients aisés. Il vaut mieux ajouter un indicateur de manquant (qui porte ici une vraie information), et, si l’on impute, le faire avec prudence en s’appuyant sur d’autres variables (patrimoine, produits détenus).
En code
import numpy as np
rng = np.random.default_rng(0)revenu = rng.lognormal(mean=10.3, sigma=0.5, size=5000) # 5 000 revenus annuels simulés
# MNAR : parmi les 20 % les plus aisés, 3 clients sur 4 refusent de déclareraise = revenu > np.quantile(revenu, 0.8)manquant = aise & (rng.random(5000) < 0.75)print(round(manquant.mean(), 3)) # 0.151 : 15 % de manquants
impute = np.where(manquant, revenu[~manquant].mean(), revenu)print(round(revenu.mean()), round(impute.mean())) # 33578 28530 : moyenne sous-estimée
# L'indicateur de manquant porte l'information : il repère les clients aisésprint(round(revenu[manquant].mean()), round(revenu[~manquant].mean())) # 61874 28530set.seed(0)revenu <- rlnorm(5000, meanlog = 10.3, sdlog = 0.5) # 5 000 revenus annuels simulés
# MNAR : parmi les 20 % les plus aisés, 3 clients sur 4 refusent de déclareraise <- revenu > quantile(revenu, 0.8)manquant <- aise & (runif(5000) < 0.75)round(mean(manquant), 3) # 0.151 : 15 % de manquants
impute <- ifelse(manquant, mean(revenu[!manquant]), revenu)round(c(vraie = mean(revenu), imputee = mean(impute))) # 33759 28722 : moyenne sous-estimée
# L'indicateur de manquant porte l'information : il repère les clients aisésround(c(manquants = mean(revenu[manquant]), declares = mean(revenu[!manquant])))# 62041 28722Exercice · Appliquer la règle de l'écart interquartile
moyenVoici les durées (en minutes) de huit consultations : 12, 15, 15, 18, 20, 22, 25, 70. En utilisant l’interpolation linéaire pour les quartiles (méthode par défaut de pandas), déterminez les valeurs aberrantes.
Voir la solution
Les valeurs sont déjà triées (). Avec l’interpolation linéaire, le quantile d’ordre se lit à la position en partant de 0.
- : position , entre la 2e valeur (15) et la 3e (15), donc .
- : position , entre la 6e valeur (22) et la 7e (25), donc .
- .
- Bornes : et .
Seule la consultation de 70 minutes est signalée. Reste à savoir si c’est une erreur (un rendez-vous jamais clôturé dans le logiciel) ou une consultation réellement longue : la règle détecte, elle ne décide pas.
En code
import pandas as pd
durees = pd.Series([12, 15, 15, 18, 20, 22, 25, 70])q1, q3 = durees.quantile([0.25, 0.75]) # interpolation linéaire par défautiqr = q3 - q1basse, haute = q1 - 1.5 * iqr, q3 + 1.5 * iqrprint(q1, q3, iqr) # 15.0 22.75 7.75print(basse, haute) # 3.375 34.375print(durees[(durees < basse) | (durees > haute)].tolist()) # [70]durees <- c(12, 15, 15, 18, 20, 22, 25, 70)q <- quantile(durees, c(0.25, 0.75)) # type 7 par défaut : interpolation linéaire, comme pandasiqr <- q[2] - q[1]bornes <- c(q[1] - 1.5 * iqr, q[2] + 1.5 * iqr)q; iqr # 15 22.75 7.75bornes # 3.375 34.375durees[durees < bornes[1] | durees > bornes[2]] # 70Exercice · Ce que l'imputation fait à la variance
difficileUne variable a valeurs, dont connues, de moyenne et de variance (calculée en divisant par ). On remplace les valeurs manquantes par . Montrez que la nouvelle moyenne vaut et que la nouvelle variance vaut . Vérifiez sur l’exemple des dix âges du chapitre.
Voir la solution
Moyenne. La somme des valeurs devient , donc la moyenne vaut toujours .
Variance. Comme la moyenne n’a pas changé, les écarts des valeurs connues à la moyenne sont les mêmes qu’avant, et leur somme des carrés vaut . Les valeurs imputées sont exactement égales à : elles ajoutent des écarts nuls. La nouvelle variance est donc
Vérification. Avec , et : , ce qui correspond bien au calcul direct. Plus la part de manquants est grande, plus la variance est écrasée : avec 50 % de valeurs imputées, elle serait divisée par deux.
En code
import numpy as np
connus = np.array([22, 38, 26, 35, 54, 2, 27, 14], dtype=float)m, n = len(connus), 10moyenne, variance = connus.mean(), connus.var() # var() divise par mcomplet = np.concatenate([connus, np.full(n - m, moyenne)])
print(moyenne, complet.mean()) # 27.25 27.25print(round(variance, 2), round(complet.var(), 2)) # 216.69 173.35print(round(m / n * variance, 2)) # 173.35 : la formule m/n * s²connus <- c(22, 38, 26, 35, 54, 2, 27, 14)m <- length(connus); n <- 10var_n <- function(x) mean((x - mean(x))^2) # variance divisée par n (var() divise par n - 1)complet <- c(connus, rep(mean(connus), n - m))
c(mean(connus), mean(complet)) # 27.25 27.25round(c(var_n(connus), var_n(complet)), 2) # 216.69 173.35round(m / n * var_n(connus), 2) # 173.35 : la formule m/n * s²