Introduction : Pourquoi le Nettoyage de Données Reste Incontournable en 2026
Si vous bossez dans la data en 2026, vous connaissez probablement cette statistique qu'on cite partout : 80 % du temps d'un data scientist est consacré au nettoyage et à la préparation des données. Et malgré les progrès considérables des outils, ce chiffre reste obstinément vrai. La bonne nouvelle ? Avec la sortie de Pandas 3.0 en janvier 2026, le nettoyage de données en Python n'a jamais été aussi performant, cohérent et — osons le dire — agréable à écrire.
Allez, on plonge dedans.
Dans ce guide, on va couvrir l'ensemble du processus de nettoyage avec Pandas 3.0 — de la gestion des valeurs manquantes à la détection des outliers, en passant par la validation automatisée avec Pandera. Que vous soyez débutant ou praticien confirmé, vous trouverez ici des techniques concrètes, du code prêt à l'emploi et les bonnes pratiques actuelles.
Si vous avez déjà exploré nos guides sur les pipelines de données avec Polars et DuckDB ou sur Scikit-Learn 1.8, considérez cet article comme la pièce manquante : celle qui garantit que vos données sont propres avant d'entrer dans vos modèles ou vos dashboards.
Ce qui Change avec Pandas 3.0 pour le Nettoyage
Avant de plonger dans les techniques, prenons un moment pour comprendre ce que Pandas 3.0 apporte de nouveau. Ce n'est pas une simple mise à jour incrémentale — certains changements modifient fondamentalement la façon dont on écrit son code de nettoyage.
Copy-on-Write est Activé par Défaut
C'est LE changement le plus impactant. Le fameux SettingWithCopyWarning que vous avez vu des centaines de fois ? Il disparaît enfin. En contrepartie, l'assignation chaînée ne fonctionne plus :
# Pandas 2.x — fonctionnait (avec un warning)
df[df["age"] > 100]["age"] = 100
# Pandas 3.0 — ne modifie PAS df (silencieusement ignoré)
# Il faut utiliser .loc explicitement :
df.loc[df["age"] > 100, "age"] = 100
En pratique, ça veut dire que vos opérations de nettoyage sont plus sûres : pas de modifications accidentelles de vos DataFrames sources. Mais il faut adapter vos réflexes, et honnêtement, ça prend quelques jours d'habitude.
Le Nouveau Type str Basé sur PyArrow
Les colonnes de texte utilisent désormais par défaut le type str (basé sur PyArrow) au lieu du vieux type object. Le résultat : des opérations de nettoyage de chaînes 5 à 10 fois plus rapides et une consommation mémoire réduite de moitié.
Pour le nettoyage de données textuelles, c'est une petite révolution silencieuse.
# Pandas 2.x
>>> ser = pd.Series(["alice", "bob"])
>>> ser.dtype
dtype('object')
# Pandas 3.0
>>> ser = pd.Series(["alice", "bob"])
>>> ser.dtype
str
Syntaxe pd.col() pour des Pipelines Plus Lisibles
La nouvelle fonction pd.col() permet de référencer des colonnes de manière plus expressive dans les chaînes de méthodes. Fini les lambdas un peu cryptiques :
import pandas as pd
# Ancien style avec lambda
df_clean = df.assign(revenu=lambda x: x["revenu"].clip(lower=0, upper=500_000))
# Nouveau style Pandas 3.0
df_clean = df.assign(revenu=pd.col("revenu").clip(lower=0, upper=500_000))
Cohérence des Types Nullable et Résolution Datetime
Les types nullable (Int64, Float64, boolean) utilisent désormais pd.NA de manière cohérente — fini le mélange entre NaN, None et pd.NaT qui rendait le debugging si pénible. De plus, la résolution par défaut des datetime passe à la microseconde au lieu de la nanoseconde, ce qui évite les fameuses erreurs de débordement sur les dates antérieures à 1678 ou postérieures à 2262.
Gestion des Valeurs Manquantes : Les Fondamentaux et Au-Delà
Les valeurs manquantes sont le problème numéro un du nettoyage de données. C'est aussi, d'expérience, celui qui génère le plus de bugs subtils en production. Voyons comment les traiter méthodiquement avec Pandas 3.0.
Détection des Valeurs Manquantes
Commençons par un jeu de données réaliste :
import pandas as pd
import numpy as np
# Simuler un dataset de clients e-commerce
data = {
"client_id": [1, 2, 3, 4, 5, 6, 7, 8],
"nom": ["Alice Dupont", "Bob Martin", "n/a", "Diana Leroy",
"Émile Faure", "--", "Grace Ndiaye", "Hugo Blanc"],
"age": [34, np.nan, 28, 45, None, 31, "na", 52],
"email": ["[email protected]", "[email protected]", None, "[email protected]",
"emile@mail", "N/A", "[email protected]", ""],
"montant_achat": [150.0, 230.5, None, 89.0, 320.0, 45.0, np.nan, 180.0],
"date_inscription": ["2024-01-15", "2024-03-22", "2024-05-10", None,
"2024-07-01", "2024-08-", "2025-01-12", "2025-03-05"]
}
df = pd.DataFrame(data)
# Première inspection
print(df.info())
print(df.isna().sum())
La méthode isna() (ou son alias isnull()) détecte les NaN et None, mais elle ne détecte pas les valeurs manquantes déguisées comme "n/a", "--" ou "". Et croyez-moi, dans les données réelles, c'est presque toujours là que se cachent les problèmes.
Traiter les Valeurs Manquantes Déguisées
En pratique, les données brutes contiennent souvent des représentations non standard des valeurs manquantes. J'ai même vu des "N/D", des "???", et une fois un magnifique "demander à Jean-Pierre". Voici comment gérer les cas les plus courants :
# Définir toutes les variantes de "manquant"
valeurs_manquantes = ["n/a", "na", "N/A", "--", "", "null", "NULL", "none"]
# Idéal : traiter à la lecture du fichier
df = pd.read_csv("clients.csv", na_values=valeurs_manquantes)
# Ou remplacer après coup dans un DataFrame existant
df = df.replace(valeurs_manquantes, pd.NA)
# Vérifier le résultat
print(df.isna().sum())
Stratégies de Suppression avec dropna()
Supprimer des lignes est parfois la bonne approche, mais il faut le faire intelligemment :
# Supprimer les lignes où TOUTES les valeurs sont manquantes
df_clean = df.dropna(how="all")
# Garder uniquement les lignes ayant au moins 4 valeurs non-nulles
df_clean = df.dropna(thresh=4)
# Supprimer uniquement si les colonnes critiques sont manquantes
df_clean = df.dropna(subset=["client_id", "email"])
Stratégies de Remplacement avec fillna()
Le remplacement est souvent préférable à la suppression. Le choix de la stratégie dépend du type de données :
# Numérique : remplir par la médiane (robuste aux outliers)
df["montant_achat"] = df["montant_achat"].fillna(df["montant_achat"].median())
# Catégoriel : remplir par le mode (valeur la plus fréquente)
df["ville"] = df["ville"].fillna(df["ville"].mode()[0])
# Séries temporelles : propagation avant puis arrière
df["temperature"] = df["temperature"].ffill().bfill()
# Remplacement conditionnel par groupe
df["age"] = df["age"].fillna(
df.groupby("categorie_client")["age"].transform("median")
)
Imputation Avancée avec Scikit-Learn
Pour aller plus loin, scikit-learn offre des imputers sophistiqués qui s'intègrent directement dans vos pipelines ML :
from sklearn.impute import SimpleImputer, KNNImputer
# Imputation par la médiane
imputer_median = SimpleImputer(strategy="median")
df[["age", "montant_achat"]] = imputer_median.fit_transform(
df[["age", "montant_achat"]]
)
# Imputation par K plus proches voisins (plus intelligent)
imputer_knn = KNNImputer(n_neighbors=5)
df[["age", "montant_achat", "anciennete"]] = imputer_knn.fit_transform(
df[["age", "montant_achat", "anciennete"]]
)
Le KNNImputer est particulièrement efficace car il utilise la similarité entre observations pour estimer les valeurs manquantes — bien plus pertinent qu'une simple moyenne globale. Personnellement, c'est mon choix par défaut dès que le dataset est assez grand.
Suppression des Doublons et Normalisation des Types
Identifier et Supprimer les Doublons
Les doublons, c'est le genre de problème sournois qui peut fausser toute votre analyse sans que vous vous en rendiez compte. Voici comment les traiter proprement :
# Compter les doublons exacts
print(f"Doublons exacts : {df.duplicated().sum()}")
# Doublons sur des colonnes spécifiques (plus courant)
print(f"Doublons email : {df.duplicated(subset=['email']).sum()}")
# Garder la dernière occurrence
df_clean = df.drop_duplicates(subset=["email"], keep="last")
# Doublons approximatifs : normaliser d'abord, puis dédupliquer
df["nom_normalise"] = (
df["nom"]
.str.lower()
.str.strip()
.str.replace(r"\s+", " ", regex=True)
)
df_clean = df.drop_duplicates(subset=["nom_normalise"], keep="first")
Normalisation des Types de Données
Un DataFrame propre, c'est un DataFrame où chaque colonne a le bon type. C'est un détail qui a l'air anodin, mais c'est crucial pour la performance et la fiabilité de vos analyses :
# Conversion numérique (les erreurs deviennent NaN)
df["age"] = pd.to_numeric(df["age"], errors="coerce")
df["code_postal"] = df["code_postal"].astype("str")
# Conversion datetime avec gestion des erreurs
df["date_inscription"] = pd.to_datetime(
df["date_inscription"],
errors="coerce", # les dates invalides deviennent NaT
format="mixed" # accepte plusieurs formats
)
# Pandas 3.0 : le paramètre copy est déprécié dans astype()
# Grâce à Copy-on-Write, c'est géré automatiquement
df["categorie"] = df["categorie"].astype("category")
# Vérifier les types après conversion
print(df.dtypes)
Détection et Traitement des Outliers
Les outliers — ces valeurs aberrantes qui s'écartent fortement du reste des données — peuvent considérablement fausser vos analyses et vos modèles. Mais attention : un outlier n'est pas toujours une erreur. C'est un point sur lequel on insiste souvent, et pour cause.
Méthode IQR (Interquartile Range)
La méthode IQR est robuste et fonctionne bien pour des distributions pas nécessairement normales. C'est celle qu'on utilise le plus souvent en pratique :
def detecter_outliers_iqr(df, colonne, facteur=1.5):
"""Détecte les outliers par la méthode IQR."""
Q1 = df[colonne].quantile(0.25)
Q3 = df[colonne].quantile(0.75)
IQR = Q3 - Q1
borne_inf = Q1 - facteur * IQR
borne_sup = Q3 + facteur * IQR
outliers = df[(df[colonne] < borne_inf) | (df[colonne] > borne_sup)]
print(f"Colonne '{colonne}' : {len(outliers)} outliers détectés")
print(f" Bornes : [{borne_inf:.2f}, {borne_sup:.2f}]")
return outliers
# Application
outliers_montant = detecter_outliers_iqr(df, "montant_achat")
outliers_age = detecter_outliers_iqr(df, "age")
Méthode Z-Score
Le Z-score fonctionne bien quand vos données suivent approximativement une distribution normale :
from scipy import stats
import numpy as np
def detecter_outliers_zscore(df, colonne, seuil=3):
"""Détecte les outliers par Z-score."""
z_scores = stats.zscore(df[colonne].dropna())
outliers_mask = np.abs(z_scores) > seuil
n_outliers = outliers_mask.sum()
print(f"Colonne '{colonne}' : {n_outliers} outliers (|z| > {seuil})")
return df[colonne].dropna()[outliers_mask]
outliers = detecter_outliers_zscore(df, "montant_achat", seuil=2.5)
Capping (Clipping) : Traiter sans Supprimer
Plutôt que de supprimer les outliers, vous pouvez les ramener dans une plage acceptable. C'est souvent l'approche la plus pragmatique — et franchement, celle que je recommande dans la majorité des cas :
# Clipping aux percentiles 1% et 99%
borne_inf = df["montant_achat"].quantile(0.01)
borne_sup = df["montant_achat"].quantile(0.99)
df["montant_achat"] = df["montant_achat"].clip(lower=borne_inf, upper=borne_sup)
Détection Visuelle avec Box Plots
La visualisation reste votre meilleur allié pour comprendre vos outliers avant de décider quoi en faire. Ne sous-estimez jamais un bon vieux box plot :
import matplotlib.pyplot as plt
import seaborn as sns
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
colonnes_numeriques = ["age", "montant_achat", "anciennete_jours"]
for ax, col in zip(axes, colonnes_numeriques):
sns.boxplot(data=df, y=col, ax=ax, color="steelblue")
ax.set_title(f"Distribution de {col}")
plt.tight_layout()
plt.savefig("outliers_boxplots.png", dpi=150)
plt.show()
Quand NE PAS Supprimer les Outliers
Un point crucial souvent négligé : tous les outliers ne sont pas des erreurs. Avant de supprimer ou de transformer une valeur, posez-vous ces questions :
- Est-ce une erreur de saisie ? Un âge de 350 ans, oui clairement. Un achat de 15 000 € dans un e-commerce de luxe, probablement pas.
- Est-ce un événement rare mais réel ? Les fraudes, les pannes exceptionnelles, les pics de ventes du Black Friday — ce sont des outliers légitimes.
- Quel est l'impact sur votre modèle ? Si vous construisez un modèle de détection de fraude, les outliers sont votre signal. Les supprimer reviendrait à jeter exactement ce que vous cherchez.
Documentez toujours votre décision et le raisonnement derrière. Votre futur vous (ou votre collègue) vous remerciera.
Nettoyage de Texte avec le Nouveau Type str de Pandas 3.0
Le nettoyage de texte est souvent la partie la plus fastidieuse du travail. Mais avec le nouveau type str de Pandas 3.0 (basé sur PyArrow), ces opérations sont désormais nettement plus rapides. Voyons ce que ça donne concrètement.
Opérations de Base
# Nettoyage classique de colonnes texte
df["nom"] = (
df["nom"]
.str.strip() # Supprimer espaces début/fin
.str.lower() # Tout en minuscules
.str.replace(r"\s+", " ", regex=True) # Espaces multiples → un seul
.str.title() # Première lettre en majuscule
)
# Nettoyage d'emails
df["email"] = (
df["email"]
.str.strip()
.str.lower()
.str.replace(r"\s", "", regex=True) # Supprimer tout espace
)
# Validation basique d'email avec regex
pattern_email = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
df["email_valide"] = df["email"].str.match(pattern_email, na=False)
Exemple Pratique : Nettoyer une Colonne de Noms
On a tous eu ce moment où on ouvre un export CRM et les noms ressemblent à tout sauf à des noms. Voici un pipeline de nettoyage concret :
import pandas as pd
# Données réalistes exportées d'un vieux CRM
noms_bruts = pd.Series([
" DUPONT, Alice ", "martin bob", "Léa BERNARD",
"M. Jean-Pierre DUVAL", "ndiaye, Grace (Mme)",
" hugo blanc jr.", "Dr. Sophie Moreau-Petit"
])
def nettoyer_nom(serie):
"""Pipeline de nettoyage pour une colonne de noms."""
return (
serie
.str.strip()
.str.replace(r"(M\.|Mme|Dr\.|Jr\.|\(.*?\))", "", regex=True)
.str.replace(r",", " ", regex=False)
.str.replace(r"\s+", " ", regex=True)
.str.strip()
.str.title()
)
noms_propres = nettoyer_nom(noms_bruts)
print(noms_propres)
Performance : object vs str (PyArrow)
La différence de performance est vraiment significative, surtout sur de gros volumes. Voici un benchmark rapide :
import pandas as pd
import time
# Générer 1 million de chaînes
n = 1_000_000
noms = pd.Series([" Alice Dupont "] * n)
# Type object (ancien comportement)
noms_object = noms.astype("object")
start = time.time()
_ = noms_object.str.strip().str.lower().str.replace(" ", "_", regex=False)
temps_object = time.time() - start
# Type str/PyArrow (Pandas 3.0 par défaut)
noms_arrow = noms.astype("str")
start = time.time()
_ = noms_arrow.str.strip().str.lower().str.replace(" ", "_", regex=False)
temps_arrow = time.time() - start
print(f"Type object : {temps_object:.2f}s")
print(f"Type str : {temps_arrow:.2f}s")
print(f"Gain : {temps_object / temps_arrow:.1f}x plus rapide")
Sur un million de lignes, attendez-vous à un gain de 5 à 8x selon les opérations. Pour des datasets volumineux, ça transforme littéralement des minutes en secondes.
Validation des Données avec Pandera
Le nettoyage, c'est bien. Mais comment garantir que vos données respectent vos règles métier après le nettoyage — et qu'elles continueront à les respecter en production ? C'est là que Pandera entre en jeu.
Pandera (v0.29.0, janvier 2026) est une bibliothèque de validation de données pour DataFrames Pandas. Pensez-y comme des tests unitaires, mais pour vos données. Si vous n'en avez jamais entendu parler, vous allez adorer.
pip install "pandera[io]"
Validation par Schéma
import pandera as pa
schema = pa.DataFrameSchema({
"client_id": pa.Column(int, pa.Check.gt(0), unique=True, nullable=False),
"nom": pa.Column(str, pa.Check.str_length(min_value=2), nullable=False),
"age": pa.Column(
float,
[pa.Check.ge(18), pa.Check.lt(120)],
nullable=True
),
"email": pa.Column(
str,
pa.Check.str_matches(r"^[^@]+@[^@]+\.[^@]+$"),
nullable=False
),
"montant_achat": pa.Column(
float,
[pa.Check.ge(0), pa.Check.lt(100_000)],
nullable=False
),
"date_inscription": pa.Column(pa.DateTime, nullable=False),
})
# Valider le DataFrame
try:
df_valide = schema.validate(df)
print("Validation réussie !")
except pa.errors.SchemaError as e:
print(f"Erreur de validation : {e}")
Validation par Classe (DataFrameModel)
Pour des schémas plus complexes, l'approche orientée objet est plus lisible et maintenable. C'est celle que je privilégie sur les projets d'une certaine taille :
import pandera as pa
from pandera.typing import Series
class SchemaClient(pa.DataFrameModel):
client_id: Series[int] = pa.Field(gt=0, unique=True)
nom: Series[str] = pa.Field(str_length={"min_value": 2})
age: Series[float] = pa.Field(ge=18, lt=120, nullable=True)
email: Series[str] = pa.Field(regex=r"^[^@]+@[^@]+\.[^@]+$")
montant_achat: Series[float] = pa.Field(ge=0, lt=100_000)
date_inscription: Series[pa.DateTime]
class Config:
strict = True # Pas de colonnes inattendues
coerce = True # Convertir les types automatiquement
@pa.check("nom")
def nom_contient_espace(cls, serie):
"""Le nom doit contenir au moins un prénom et un nom."""
return serie.str.contains(r"\s", regex=True)
# Utilisation
df_valide = SchemaClient.validate(df)
Validation Lazy pour des Rapports Complets
Par défaut, Pandera s'arrête à la première erreur. Pas super pratique quand on débugue un dataset de 100 000 lignes. En mode lazy, il collecte toutes les erreurs d'un coup :
try:
df_valide = schema.validate(df, lazy=True)
except pa.errors.SchemaErrors as e:
# Rapport complet de toutes les erreurs
print(e.failure_cases)
# Exporter les erreurs pour analyse
erreurs = e.failure_cases
erreurs.to_csv("rapport_erreurs_validation.csv", index=False)
print(f"{len(erreurs)} erreurs de validation détectées.")
Ce rapport vous donne pour chaque erreur : la colonne concernée, le check échoué, la valeur fautive et l'index de la ligne. Extrêmement pratique pour diagnostiquer les problèmes dans un gros dataset.
Pipeline Automatisé de Nettoyage : Tout Assembler
Bon, maintenant qu'on a vu chaque technique individuellement, assemblons le tout dans un pipeline automatisé, réutilisable et traçable. C'est là que tout prend sens.
import pandas as pd
import numpy as np
import pandera as pa
import logging
# Configuration du logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(message)s")
logger = logging.getLogger(__name__)
# --- Schéma de validation final ---
schema_propre = pa.DataFrameSchema({
"client_id": pa.Column(int, pa.Check.gt(0), unique=True, nullable=False),
"nom": pa.Column(str, pa.Check.str_length(min_value=2), nullable=False),
"age": pa.Column(float, [pa.Check.ge(18), pa.Check.lt(120)], nullable=False),
"email": pa.Column(
str, pa.Check.str_matches(r"^[^@]+@[^@]+\.[^@]+$"), nullable=False
),
"montant_achat": pa.Column(
float, [pa.Check.ge(0), pa.Check.lt(100_000)], nullable=False
),
"date_inscription": pa.Column(pa.DateTime, nullable=False),
})
def charger_et_standardiser(chemin, na_values=None):
"""Étape 1 : Chargement avec détection des valeurs manquantes."""
na_vals = na_values or ["n/a", "na", "N/A", "--", "", "null", "NULL"]
df = pd.read_csv(chemin, na_values=na_vals)
logger.info(f"Chargé {len(df)} lignes, {df.columns.size} colonnes")
return df
def nettoyer_texte(df):
"""Étape 2 : Nettoyage des colonnes textuelles."""
df = df.copy()
df["nom"] = (
df["nom"].str.strip()
.str.replace(r"\s+", " ", regex=True)
.str.title()
)
df["email"] = df["email"].str.strip().str.lower()
logger.info("Colonnes texte nettoyées")
return df
def convertir_types(df):
"""Étape 3 : Conversion et normalisation des types."""
df = df.copy()
df["age"] = pd.to_numeric(df["age"], errors="coerce")
df["montant_achat"] = pd.to_numeric(df["montant_achat"], errors="coerce")
df["date_inscription"] = pd.to_datetime(
df["date_inscription"], errors="coerce", format="mixed"
)
logger.info("Types convertis")
return df
def traiter_manquants(df):
"""Étape 4 : Imputation des valeurs manquantes."""
n_avant = df.isna().sum().sum()
df = df.copy()
df = df.dropna(subset=["client_id", "email"])
df["age"] = df["age"].fillna(df["age"].median())
df["montant_achat"] = df["montant_achat"].fillna(
df["montant_achat"].median()
)
df = df.dropna(subset=["nom", "date_inscription"])
n_apres = df.isna().sum().sum()
logger.info(f"Valeurs manquantes : {n_avant} → {n_apres}")
return df
def supprimer_doublons(df):
"""Étape 5 : Déduplication."""
n_avant = len(df)
df = df.drop_duplicates(subset=["client_id"], keep="last")
df = df.drop_duplicates(subset=["email"], keep="last")
logger.info(f"Doublons supprimés : {n_avant} → {len(df)} lignes")
return df
def traiter_outliers(df):
"""Étape 6 : Capping des outliers."""
df = df.copy()
for col in ["age", "montant_achat"]:
q01 = df[col].quantile(0.01)
q99 = df[col].quantile(0.99)
n_out = ((df[col] < q01) | (df[col] > q99)).sum()
df[col] = df[col].clip(lower=q01, upper=q99)
logger.info(f" {col} : {n_out} outliers clippés")
return df
def filtrer_emails_invalides(df):
"""Étape 7 : Supprimer les emails invalides."""
pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
mask = df["email"].str.match(pattern, na=False)
logger.info(f"Emails invalides supprimés : {(~mask).sum()}")
return df[mask]
# --- Pipeline principal ---
def pipeline_nettoyage(chemin_csv):
"""Pipeline complet de nettoyage de données clients."""
logger.info("DÉMARRAGE DU PIPELINE DE NETTOYAGE")
df = (
charger_et_standardiser(chemin_csv)
.pipe(nettoyer_texte)
.pipe(convertir_types)
.pipe(traiter_manquants)
.pipe(supprimer_doublons)
.pipe(traiter_outliers)
.pipe(filtrer_emails_invalides)
)
# Validation finale avec Pandera
try:
df = schema_propre.validate(df, lazy=True)
logger.info("VALIDATION FINALE : SUCCÈS")
except pa.errors.SchemaErrors as e:
logger.error(f"VALIDATION : {len(e.failure_cases)} ERREURS")
raise
logger.info(f"Pipeline terminé : {len(df)} lignes propres")
return df
# Exécution
df_propre = pipeline_nettoyage("clients_bruts.csv")
df_propre.to_parquet("clients_propres.parquet", index=False)
Notez l'utilisation de .pipe() qui permet de chaîner les fonctions de manière lisible. Chaque étape est indépendante, testable et journalisée. C'est exactement le type de pipeline que vous pouvez intégrer dans vos workflows de data engineering — et que vous devriez utiliser systématiquement plutôt que de bricoler dans un notebook sans structure.
Bonnes Pratiques et Erreurs Courantes
Après pas mal d'années à nettoyer des données (et à corriger des nettoyages mal faits), voici les leçons qu'on considère essentielles :
- Toujours explorer avant de nettoyer. Utilisez
df.describe(),df.info(),df.head(20)et des visualisations avant de toucher à quoi que ce soit. On ne peut pas nettoyer ce qu'on ne comprend pas. - Documentez chaque décision de nettoyage. Pourquoi la médiane plutôt que la moyenne ? Pourquoi ce seuil d'outliers ? Un commentaire ou un log aujourd'hui vous évitera des heures de debugging dans six mois.
- Validez à chaque étape, pas seulement à la fin. Avec Pandera, vous pouvez définir des schémas intermédiaires pour attraper les problèmes au plus tôt.
- Ne sur-nettoyez pas. Chaque transformation supprime potentiellement de l'information. Un outlier réel contient un signal. Une catégorie rare peut être prédictive. C'est tentant de tout lisser, mais résistez.
- Adaptez-vous à Copy-on-Write. En Pandas 3.0, oubliez l'assignation chaînée (
df[mask]["col"] = val). Utilisez systématiquement.loc,.assign()ou le chaînage avec.pipe(). - Testez avec Pandera en production. Votre pipeline de nettoyage est du code comme un autre : il mérite des tests. Un schéma Pandera à l'entrée et à la sortie, c'est votre filet de sécurité.
- Sauvegardez en Parquet, pas en CSV. Le format Parquet préserve les types, est plus compact et beaucoup plus rapide à lire. En 2026, c'est le standard — il n'y a plus vraiment de raison d'utiliser CSV pour du stockage intermédiaire.
FAQ — Questions Fréquentes
Comment gérer les valeurs manquantes dans un DataFrame Pandas ?
Pandas offre plusieurs approches. La détection se fait avec df.isna() ou df.isnull(). Ensuite, vous pouvez supprimer les lignes avec dropna() (en utilisant thresh et subset pour un contrôle fin) ou remplacer les valeurs avec fillna(). Pour des données numériques, la médiane est souvent préférable à la moyenne car elle résiste mieux aux outliers. Pour une imputation plus sophistiquée, utilisez KNNImputer de scikit-learn. En Pandas 3.0, les types nullable garantissent un comportement cohérent avec pd.NA.
Quelle est la différence entre dropna() et fillna() ?
dropna() supprime les lignes (ou colonnes) contenant des valeurs manquantes — vous perdez des données, mais le résultat est complet. fillna() remplace les valeurs manquantes par une valeur de substitution (moyenne, médiane, constante). Vous conservez toutes vos lignes, mais vous introduisez des valeurs estimées. En règle générale, utilisez dropna() quand le pourcentage de manquants est faible (moins de 5 %) et fillna() quand supprimer trop de lignes biaiserait votre analyse.
Comment détecter les outliers avec Python ?
Les deux méthodes les plus courantes sont la méthode IQR (Interquartile Range) et le Z-score. L'IQR est plus robuste et ne suppose pas de distribution normale : tout ce qui sort de l'intervalle [Q1 - 1.5×IQR, Q3 + 1.5×IQR] est considéré comme outlier. Le Z-score mesure l'écart à la moyenne en nombre d'écarts-types et convient aux données approximativement normales. Complétez toujours par une visualisation (box plots, scatter plots) avant de prendre une décision.
Qu'est-ce que Copy-on-Write dans Pandas 3.0 ?
Copy-on-Write (CoW) est un mécanisme d'optimisation mémoire activé par défaut dans Pandas 3.0. Quand vous faites df2 = df[["col1", "col2"]], Pandas ne copie pas immédiatement les données — il partage la mémoire. La copie n'a lieu que quand l'un des deux DataFrames est modifié. L'avantage : moins de mémoire utilisée et la disparition du SettingWithCopyWarning. L'inconvénient : l'assignation chaînée (df[mask]["col"] = val) ne fonctionne plus. Utilisez .loc à la place.
Comment valider automatiquement un DataFrame avec Pandera ?
Pandera vous permet de définir un schéma décrivant la structure attendue de votre DataFrame : types de colonnes, plages de valeurs, contraintes d'unicité, patterns regex. Vous pouvez utiliser l'API fonctionnelle (DataFrameSchema) ou l'API orientée objet (DataFrameModel). Appelez schema.validate(df) pour vérifier la conformité. Avec lazy=True, Pandera collecte toutes les erreurs au lieu de s'arrêter à la première, ce qui produit un rapport complet. Intégrez cette validation à l'entrée et à la sortie de vos pipelines pour garantir la qualité en production.