Aller au contenu

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.

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.

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 doublons
df = df.drop_duplicates() # supprime les lignes strictement identiques

Les 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).

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.

0510x : autre variable observée0102030405060y : variable à complétervraie moyenne : 37,1moyenne des observées : 38,3MCAR : manque au hasardobservéemanquante0510x : autre variable observéevraie moyenne : 37,1moyenne des observées : 32,1MAR : dépend de x (observée)0510x : autre variable observéevraie moyenne : 37,1moyenne des observées : 33,9MNAR : dépend de y (la valeur cachée)
Données simulées. En MCAR, la moyenne des valeurs observées reste proche de la vraie ; en MAR et surtout en MNAR, elle est biaisée, car les grandes valeurs manquent plus souvent. En MAR, x permet de corriger ; en MNAR, rien d'observé ne le permet.

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égieQuand l’utiliserLimite
Supprimer les lignesmanquants rares et dus au hasardperte d’exemples (catastrophique sur le Titanic)
Supprimer la colonnebeaucoup de manquants (60 à 70 % et plus) et variable non essentielleperte d’information (Cabin, 77 %)
Imputation simple : moyenne, médiane, moderapide ; 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’autresplus 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 informatifajoute 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.

0102030405060âge (années)8 âges connusaprès imputationpar la moyenneécart-type 14,72 « ? » remplacéspar 27,25écart-type 13,2moyenne 27,25 (inchangée)
Les deux valeurs imputées tombent pile sur la moyenne : elle ne bouge pas, mais la dispersion diminue (écart-type de 14,7 à 13,2 ans).

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 seulement
X_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 »

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 méthode la plus robuste repose sur les quartiles. Notons Q1Q_1 le premier quartile (25 % des valeurs sont en dessous) et Q3Q_3 le troisième (75 % en dessous). L’écart interquartile vaut IQR=Q3−Q1\text{IQR} = Q_3 - Q_1. Une valeur est déclarée aberrante si elle sort de l’intervalle

[ Q1−1,5×IQR  ;  Q3+1,5×IQR ]\left[\,Q_1 - 1{,}5 \times \text{IQR} \;;\; Q_3 + 1{,}5 \times \text{IQR}\,\right]

C’est exactement la règle qui dessine les « moustaches » d’une boîte à moustaches.

050100150200250prix du billet (livres)aberrant263Q₃ + 1,5 × IQR ≈ 57,33Q₁ ≈ 8,15Q₃ ≈ 27,82IQR ≈ 19,67borne basse ≈ −21,35
La boîte va de Q₁ à Q₃ ; les moustaches s'arrêtent à la dernière valeur dans l'intervalle [Q₁ − 1,5 IQR ; Q₃ + 1,5 IQR]. Seul le billet à 263 en sort.

Une autre règle courante calcule le z-score z=(x−μ)/σz = (x - \mu)/\sigma, où μ\mu est la moyenne et σ\sigma l’écart-type, et déclare aberrante toute valeur telle que ∣z∣>3\lvert z \rvert > 3. Sur les dix billets, μ≈42,3\mu \approx 42{,}3 et σ≈74,8\sigma \approx 74{,}8, si bien que le billet à 263 n’a qu’un z-score de 2,952{,}95 : 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).

les 10 billets, triés−101234z-scoreseuil |z| = 3263 : z ≈ 2,95sous le seuil !μ ≈ 42,3 et σ ≈ 74,8, gonflés par 263μ, σ calculésavec 263μ, σ calculéssans 26305101520z-score du billet à 2632,9517,03
L'effet de masquage : le billet à 263 gonfle lui-même σ, et passe sous le seuil |z| = 3. Jugé avec la moyenne et l'écart-type des neuf autres billets, il serait à 17 écarts-types.
q1, q3 = df["Fare"].quantile([0.25, 0.75])
iqr = q3 - q1
aberrant = (df["Fare"] < q1 - 1.5 * iqr) | (df["Fare"] > q3 + 1.5 * iqr)
print(aberrant.sum(), "billets signalés")
  • 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).
  • 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.

Exercice · Choisir une stratégie par colonne

facile

Dans 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éclarer
aise = 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és
print(round(revenu[manquant].mean()), round(revenu[~manquant].mean())) # 61874 28530

Exercice · Appliquer la règle de l'écart interquartile

moyen

Voici 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 (n=8n = 8). Avec l’interpolation linéaire, le quantile d’ordre qq se lit à la position (n−1) q(n - 1)\,q en partant de 0.

  • Q1Q_1 : position 7×0,25=1,757 \times 0{,}25 = 1{,}75, entre la 2e valeur (15) et la 3e (15), donc Q1=15Q_1 = 15.
  • Q3Q_3 : position 7×0,75=5,257 \times 0{,}75 = 5{,}25, entre la 6e valeur (22) et la 7e (25), donc Q3=22+0,25×3=22,75Q_3 = 22 + 0{,}25 \times 3 = 22{,}75.
  • IQR=22,75−15=7,75\text{IQR} = 22{,}75 - 15 = 7{,}75.
  • Bornes : 15−1,5×7,75=3,37515 - 1{,}5 \times 7{,}75 = 3{,}375 et 22,75+1,5×7,75=34,37522{,}75 + 1{,}5 \times 7{,}75 = 34{,}375.

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éfaut
iqr = q3 - q1
basse, haute = q1 - 1.5 * iqr, q3 + 1.5 * iqr
print(q1, q3, iqr) # 15.0 22.75 7.75
print(basse, haute) # 3.375 34.375
print(durees[(durees < basse) | (durees > haute)].tolist()) # [70]

Exercice · Ce que l'imputation fait à la variance

difficile

Une variable a nn valeurs, dont mm connues, de moyenne xˉ\bar x et de variance s2s^2 (calculée en divisant par mm). On remplace les n−mn - m valeurs manquantes par xˉ\bar x. Montrez que la nouvelle moyenne vaut xˉ\bar x et que la nouvelle variance vaut mn s2\frac{m}{n}\,s^2. Vérifiez sur l’exemple des dix âges du chapitre.

Voir la solution

Moyenne. La somme des valeurs devient mxˉ+(n−m)xˉ=nxˉm\bar x + (n - m)\bar x = n\bar x, donc la moyenne vaut toujours xˉ\bar x.

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 ms2m s^2. Les valeurs imputées sont exactement égales à xˉ\bar x : elles ajoutent des écarts nuls. La nouvelle variance est donc

s′2=ms2+(n−m)×0n=mn s2s'^2 = \frac{m s^2 + (n - m) \times 0}{n} = \frac{m}{n}\, s^2

Vérification. Avec m=8m = 8, n=10n = 10 et s2=216,69s^2 = 216{,}69 : s′2=0,8×216,69≈173,35s'^2 = 0{,}8 \times 216{,}69 \approx 173{,}35, 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), 10
moyenne, variance = connus.mean(), connus.var() # var() divise par m
complet = np.concatenate([connus, np.full(n - m, moyenne)])
print(moyenne, complet.mean()) # 27.25 27.25
print(round(variance, 2), round(complet.var(), 2)) # 216.69 173.35
print(round(m / n * variance, 2)) # 173.35 : la formule m/n * s²