Nettoyer un jeu de données est rarement un acte glamour mais c’est l’étape qui sépare un projet sérieux d’un brouillon de notebook. Sur une mission réelle, on consacre rarement moins de 50 % du temps total à cette phase. Données dupliquées, valeurs manquantes mal codées, types incohérents, dates au format texte, encodages qui dérapent : chaque cas a sa parade, et la discipline consiste à les traiter dans un ordre stable, documenté, et reproductible.
Pour le contexte global, voir le guide principal sur la stack data 2026. Ce tutoriel se concentre sur la procédure pas-à-pas avec pandas et missingno, depuis la première ouverture du fichier jusqu’à un DataFrame propre prêt pour la modélisation.
Prérequis
- Python 3.10 ou supérieur, environnement virtuel actif
- pandas 3.0+, numpy, missingno, matplotlib
- Avoir lu le tutoriel sur l’analyse exploratoire ou maîtriser les bases pandas
- Temps estimé : 90 minutes sur un fichier de 50 000 lignes
Étape 1 — Installer missingno et préparer l’environnement
missingno est une petite bibliothèque dédiée à la visualisation des valeurs manquantes. Elle propose quatre graphiques d’une efficacité redoutable : matrix (carte des trous ligne par ligne), bar (compte des manques par colonne), heatmap (corrélations entre patterns de manques), et dendrogram (regroupement hiérarchique des colonnes selon leurs trous communs). Aucune de ces visualisations n’est strictement indispensable — pandas seul peut tout faire — mais missingno fait gagner un temps considérable sur des fichiers à plusieurs dizaines de colonnes.
pip install pandas numpy missingno matplotlib
Après l’installation, un import rapide vérifie que tout est en place. Si missingno renvoie une erreur d’import, c’est presque toujours qu’il manque matplotlib (dépendance non déclarée comme stricte selon les versions). Pour s’assurer que tout est cohérent, lancer ces deux lignes en début de notebook :
import pandas as pd, numpy as np, missingno as msno, matplotlib.pyplot as plt
print(pd.__version__, np.__version__)
L’affichage des versions sert de point de référence pour tout problème ultérieur. Une différence de version mineure suffit à expliquer un comportement qui change entre votre poste et celui d’un collègue.
Étape 2 — Charger en types nullables
Pandas dispose désormais de types nullables natifs (Int64, Float64, boolean avec un I/F/b en majuscule) qui acceptent des valeurs manquantes sans tout convertir en flottants. Activer ces types dès le chargement évite de retomber en boucle sur le bug classique : une colonne d’identifiants entiers qui devient flottante à cause d’un seul NaN, et qu’on n’arrive plus à formater proprement ensuite.
df = pd.read_csv(
"transactions.csv",
dtype_backend="numpy_nullable",
parse_dates=["date_commande"],
na_values=["", "NA", "N/A", "?", "NULL", "null", "-"],
)
print(df.dtypes)
L’argument dtype_backend="numpy_nullable" active les types nullables pour toutes les colonnes. na_values liste les chaînes que pandas doit interpréter comme des valeurs manquantes en plus des cases vides — c’est ici qu’on attrape les N/A, ?, NULL et autres conventions maison. Sans cette liste, ces valeurs apparaîtraient comme des chaînes textuelles tout à fait valides, et l’analyse de valeurs manquantes serait silencieusement faussée.
Étape 3 — Vue d’ensemble des manques avec missingno
La première carte à afficher est msno.matrix. Elle montre une bande verticale par ligne, blanche pour les manques, noire pour les valeurs présentes. En quelques secondes, on voit si les manques se concentrent au début, à la fin, sont éparpillés, ou suivent un motif clair (par exemple, deux colonnes manquent toujours simultanément).
fig = msno.matrix(df, figsize=(12, 5), fontsize=10)
plt.tight_layout()
plt.savefig("missing_matrix.png", dpi=120)
plt.show()
# Bar : nombre de valeurs présentes par colonne
msno.bar(df, figsize=(12, 4), fontsize=10)
plt.savefig("missing_bar.png", dpi=120)
plt.show()
Si la matrice révèle un alignement parfait des trous entre deux colonnes, c’est généralement qu’elles partagent une cause commune (par exemple un formulaire optionnel non rempli). Si les trous sont aléatoirement répartis (le cas idéal des « manques complètement aléatoires », ou MCAR), les méthodes d’imputation simples conviennent. Si les trous se concentrent dans un sous-ensemble particulier (par exemple uniquement chez les nouveaux clients), on est dans le cas missing at random conditionnel à une variable observée — et il faudra en tenir compte dans la modélisation.
Étape 4 — Heatmap de corrélation des manques
La heatmap de missingno calcule, pour chaque paire de colonnes, à quel point leurs patterns de manques sont corrélés. Une valeur proche de 1 signale que quand l’une est vide, l’autre l’est aussi presque toujours. Une valeur proche de -1 signale qu’elles sont mutuellement exclusives. Cette information conditionne directement la stratégie d’imputation : si deux colonnes sont systématiquement vides ensemble, les imputer indépendamment serait absurde.
msno.heatmap(df, figsize=(10, 6), fontsize=9)
plt.tight_layout()
plt.savefig("missing_heatmap.png", dpi=120)
plt.show()
Le résultat fait ressortir les paires qui se manquent ensemble. Sur un fichier client typique, on découvre souvent que numéro de TVA et raison sociale sont corrélées à 0,95 : ce sont les colonnes B2B, vides pour les particuliers. La conclusion logique est de créer une colonne binaire « est_pro » qui résume l’information, plutôt que d’imputer indépendamment.
Étape 5 — Supprimer les doublons stricts puis fonctionnels
Les doublons existent en deux variantes. Les doublons stricts sont des lignes parfaitement identiques sur toutes les colonnes — généralement un bug d’export ou d’ETL. Les doublons fonctionnels sont des lignes identiques sur les colonnes-clés métier (par exemple même client, même produit, même date) mais qui peuvent différer sur des colonnes accessoires. Ces derniers sont plus piégeux et nécessitent une décision métier.
# Doublons stricts
n_avant = len(df)
df = df.drop_duplicates()
print(f"Doublons stricts retirés : {n_avant - len(df)}")
# Doublons fonctionnels (même ID de transaction)
doublons_fonc = df[df.duplicated(subset=["id_transaction"], keep=False)]
print(f"Lignes en doublon sur id_transaction : {len(doublons_fonc)}")
print(doublons_fonc.sort_values("id_transaction").head(10))
Pour les doublons fonctionnels, la règle dépend du métier. Si les lignes diffèrent par une date de mise à jour, on garde la plus récente (sort_values puis drop_duplicates(keep="last")). Si elles sont vraiment équivalentes, on en garde une au hasard. Dans tous les cas, on documente la décision dans le notebook — sans cela, six mois plus tard, personne ne saura comment ces lignes ont été traitées.
Étape 6 — Corriger les types fautivement détectés
Même avec les types nullables activés, certaines colonnes arrivent avec un type incorrect parce que le format d’origine est ambigu. Une colonne code postal typée Int64 perd les zéros initiaux (un code postal commençant par 0 devient un nombre à 4 chiffres). Une colonne booléen exprimée en « oui/non » arrive en chaîne de caractères. Une colonne montant avec virgule décimale française arrive en texte. Chaque cas a sa correction.
# Code postal : forcer en string puis padder
df["code_postal"] = df["code_postal"].astype("string").str.zfill(5)
# Booléen métier
df["paye"] = df["paye"].map({"oui": True, "non": False, "OUI": True, "NON": False}).astype("boolean")
# Montant avec virgule décimale
df["montant"] = (
df["montant"].astype("string")
.str.replace(" ", "", regex=False)
.str.replace(",", ".", regex=False)
.astype("Float64")
)
print(df.dtypes)
Le passage par astype("string") avant manipulation textuelle évite les surprises sur les valeurs manquantes : sans cela, les NaN peuvent provoquer des erreurs sur les méthodes str.*. Le zfill(5) rajoute les zéros initiaux. Pour le booléen, le dictionnaire de mapping doit couvrir toutes les graphies présentes dans le fichier — un print(df["paye"].unique()) avant la conversion permet de toutes les lister.
Étape 7 — Décider d’une stratégie d’imputation par colonne
Aucune stratégie unique d’imputation ne convient à toutes les colonnes. La règle se décide par variable, en fonction du métier et de la proportion de manques. Pour des manques inférieurs à 5 %, l’imputation par la médiane (numérique) ou la modalité la plus fréquente (catégorielle) suffit dans 80 % des cas. Pour des manques entre 5 et 30 %, on privilégie un modèle prédictif simple (k-plus-proches-voisins) ou une imputation par groupe métier. Au-delà de 30 %, on transforme la colonne en variable binaire « renseigné/non », ou on supprime carrément.
# Imputation simple sur l'âge (manques < 5 %)
mediane_age = df["age"].median()
df["age"] = df["age"].fillna(mediane_age)
# Imputation par modalité la plus fréquente sur le canal
mode_canal = df["canal_paiement"].mode().iloc[0]
df["canal_paiement"] = df["canal_paiement"].fillna(mode_canal)
# Création d'une variable binaire pour une colonne à >40 % de manques
df["a_email"] = df["email"].notna()
df = df.drop(columns=["email"])
L’imputation par la médiane plutôt que par la moyenne est presque toujours préférable : elle est robuste aux valeurs aberrantes et ne crée pas de biais directionnel sur des distributions asymétriques. Pour les variables catégorielles, on peut aussi créer une catégorie explicite « inconnu » plutôt que d’imputer — c’est souvent plus honnête méthodologiquement, parce que cela conserve l’information « manque » comme un signal exploitable par le modèle.
Étape 8 — Imputation par groupe métier
Pour les colonnes intermédiaires (entre 5 et 30 % de manques), une imputation conditionnelle au groupe métier donne souvent de meilleurs résultats. Imputer la valeur médiane par catégorie de produit est plus juste qu’imputer la médiane globale, parce que les distributions par catégorie diffèrent. La syntaxe pandas groupby + transform est faite pour ça.
df["prix_unitaire"] = df.groupby("categorie")["prix_unitaire"].transform(
lambda s: s.fillna(s.median())
)
# Vérifier qu'il n'y a plus de manques
print(df["prix_unitaire"].isna().sum())
Si après cette opération il reste des manques, c’est qu’une catégorie complète n’avait aucune valeur — la médiane par groupe est alors elle-même NaN. On retombe dans ce cas sur une imputation globale en filet de sécurité avec un second fillna(df["prix_unitaire"].median()). Le résultat doit toujours être vérifié par isna().sum() == 0 avant de passer à l’étape suivante.
Étape 9 — Normaliser le texte
Les colonnes textuelles cachent des duplications invisibles : « Dakar », « dakar », « DAKAR », « Dakar » (avec espace final) sont quatre modalités différentes pour pandas alors que c’est manifestement la même ville. La normalisation consiste à réduire ces variations en une forme canonique. C’est rarement parfait du premier coup mais cela divise par cinq ou dix le nombre de modalités à modéliser.
def normaliser(s: pd.Series) -> pd.Series:
return (
s.astype("string")
.str.strip()
.str.lower()
.str.normalize("NFKD")
.str.encode("ascii", errors="ignore")
.str.decode("ascii")
)
df["ville"] = normaliser(df["ville"])
print(df["ville"].value_counts().head(20))
Le pipeline retire les espaces autour, met en minuscules, puis transforme les caractères accentués en leur équivalent ASCII (« é » devient « e »). C’est utile pour les noms de villes, les libellés produits, les statuts. Le résultat doit toujours être contrôlé par un value_counts qui révèle les fautes de frappe restantes — il faudra alors corriger à la main les modalités les plus fréquentes (un replace ciblé suffit), sans chercher à tout couvrir.
Étape 10 — Sauvegarder en Parquet
Une fois le DataFrame propre, on le sauvegarde dans un format efficace. Le CSV reste universel mais perd les types et grossit en taille. Le format Parquet conserve les types pandas (y compris les nullables), compresse efficacement, et se relit en quelques secondes même sur de gros volumes. Toutes les bibliothèques modernes le lisent (DuckDB, Polars, Spark).
df.to_parquet("transactions_clean.parquet", index=False, compression="zstd")
print("Taille :", round(__import__("os").path.getsize("transactions_clean.parquet")/1024, 1), "Ko")
La compression zstd offre un excellent compromis taille/vitesse. Sur le fichier de 50 000 lignes du tutoriel, on passe typiquement de 8 Mo en CSV à 800 Ko en Parquet, soit dix fois moins. Le rechargement avec pd.read_parquet("transactions_clean.parquet") reproduit exactement les types — fini les conversions à refaire à chaque ouverture.
Étape 11 — Vérification finale
Avant de passer à l’étape modélisation, on vérifie une dernière fois que le DataFrame est exempt de défauts. Trois assertions suffisent : aucune valeur manquante restante (sauf cas explicitement choisi), pas de doublon, et types cohérents. Cette vérification peut être encapsulée dans une fonction réutilisable, voire dans des contrats Pandera ou Great Expectations pour automatiser le contrôle à chaque exécution.
assert df.duplicated().sum() == 0, "Doublons restants"
assert df["age"].isna().sum() == 0, "Manques restants sur age"
assert df["montant"].dtype == "Float64", "Type incorrect sur montant"
print(f"OK : {len(df):,} lignes, {df.shape[1]} colonnes propres")
Si une assertion échoue, on revient à l’étape concernée. Un nettoyage de qualité est un nettoyage qui passe sans erreur ces vérifications, et qui le fait à chaque réexécution sur de nouveaux fichiers du même type. C’est le moment où l’on peut envisager d’extraire la logique dans un module Python réutilisable, plutôt que de la garder dispersée dans un notebook.
Erreurs fréquentes
| Erreur | Cause | Solution |
|---|---|---|
| fillna ne remplit pas tout | Méthode appliquée sur une copie, pas inplace | Réassigner : df[« col »] = df[« col »].fillna(…) |
| Imputation par moyenne biaise les résultats | Distribution asymétrique avec outliers | Préférer la médiane systématiquement |
| str.lower() crashe sur les NaN | Méthodes str sur Series mixte | Convertir en « string » dtype d’abord |
| Code postal perd ses zéros initiaux | Détection automatique en Int64 | Forcer dtype= »string » au chargement |
| Doublons stricts subsistent après drop_duplicates | Espaces ou casse différents | Normaliser le texte avant le drop |
| Imputation crée une fuite de données | Médiane calculée sur tout le jeu | Calculer sur le train uniquement, encapsuler dans un Pipeline |
| Parquet ne se relit pas | pyarrow ou fastparquet manquant | pip install pyarrow |
Tutoriels associés
- Analyse exploratoire avec pandas et matplotlib
- Feature engineering avec scikit-learn
- 🔝 Retour au guide principal : Sciences de données : la stack pratique 2026
Ressources officielles
- pandas — Working with missing data
- missingno sur GitHub
- pandas — Nullable integer data type
- Format Apache Parquet
FAQ
Faut-il toujours imputer les valeurs manquantes ?
Non. Certains modèles (gradient boosting, arbres de décision) gèrent les NaN nativement et le manque devient une information. Imputer n’est nécessaire que pour les modèles qui exigent des valeurs complètes, typiquement les modèles linéaires et les réseaux de neurones.
Quelle différence entre dropna et fillna ?
dropna supprime les lignes ou colonnes contenant des manques. fillna les remplace par une valeur. La suppression est radicale et perd de l’information, l’imputation conserve la ligne mais introduit un biais. Le bon choix dépend du taux de manques et de l’enjeu métier.
Faut-il nettoyer dans le notebook d’exploration ou dans un module ?
Le notebook sert à explorer et décider. Une fois les règles fixées, on les extrait dans un module Python réutilisable que l’on appelle depuis le notebook et depuis les pipelines de production. Cette séparation est ce qui transforme un brouillon en code maintenable.
missingno fonctionne-t-il sur de gros DataFrames ?
Au-delà de quelques millions de lignes, les graphiques deviennent lents et illisibles. On échantillonne alors avec df.sample(100_000) avant de passer la matrice. L’analyse des manques est statistique : un échantillon représentatif suffit.