En structurant le nettoyage avec des pipelines Pandas lisibles, typés et réutilisables. Le vrai sujet, c’est d’arrêter les modifications bricolées qui créent des warnings, consomment trop de RAM et rendent les traitements impossibles à relire trois semaines après.
Pourquoi chaîner les méthodes Pandas ?
Je chaîne les méthodes Pandas parce que je veux nettoyer mes données sans transformer mon notebook en champ de mines. Le style classique marche au début, puis on ajoute une variable temporaire, puis une copie, puis un filtre, puis une affectation, et un matin Pandas vous sort un SettingWithCopyWarning. Ce warning veut dire en gros : “Je ne suis pas sûr que vous modifiez le DataFrame original ou une vue temporaire”. Pas idéal quand vous préparez des données pour un reporting ou un modèle IA.
Le style impératif ressemble souvent à ça :
df_sales = ventes.copy()
df_sales["order_date"] = pd.to_datetime(df_sales["order_date"], errors="coerce")
df_sales["item_code"] = df_sales["item_code"].str.strip().str.upper()
df_sales["total_revenue"] = df_sales["quantity"] * df_sales["unit_price"]
df_sales = df_sales[df_sales["order_date"].notna()]
df_sales = df_sales[df_sales["quantity"] > 0]
df_sales = df_sales[df_sales["unit_price"] >= 0]
df_sales = df_sales.rename(columns={
"order_date": "date_commande",
"item_code": "code_article"
})
Ce n’est pas horrible. Mais dès que le nettoyage grossit, on perd le fil. Chez un client, le vrai gain n’était pas la ligne de code en moins. C’était le fait de pouvoir isoler une étape, la tester, la déplacer, sans casser le reste.
Comme on dit à Brive, un bon plan de marquage vaut mieux qu’un bon reporting ! Si besoin, consultez moi - faites appel à un super consultant en tracking client et server side.
Avec les parenthèses, je lis la transformation de haut en bas. Chaque méthode prend le DataFrame courant, le transforme, puis le passe à l’étape suivante.
def drop_invalid_sales(df):
return df.dropna(subset=["order_date", "item_code"])
clean_sales = (
ventes
.assign(
order_date=lambda d: pd.to_datetime(d["order_date"], errors="coerce"),
item_code=lambda d: d["item_code"].str.strip().str.upper(),
total_revenue=lambda d: d["quantity"] * d["unit_price"]
)
.query("quantity > 0 and unit_price >= 0")
.pipe(drop_invalid_sales)
.rename(columns={
"order_date": "date_commande",
"item_code": "code_article"
})
)
.assign() me sert à créer ou modifier des colonnes proprement. Les lambda reçoivent le DataFrame courant, donc je peux enchaîner sans créer dix variables intermédiaires.
.query() rend les filtres plus lisibles. Au lieu d’empiler des crochets partout, j’écris presque une phrase : quantité positive, prix positif ou nul.
.pipe() est pratique quand une étape mérite une fonction dédiée. Je peux brancher une règle métier, un nettoyage réutilisable, ou une validation plus complexe, sans casser la lecture du pipeline.
| Style impératif | Pipeline déclaratif |
| Beaucoup de variables intermédiaires | Une transformation lisible de haut en bas |
| Risque de copies implicites | Moins d’ambiguïté sur le DataFrame courant |
| Plus exposé au SettingWithCopyWarning | Étapes plus faciles à isoler et tester |
| Le nettoyage devient vite dispersé | Le nettoyage reste reproductible |
Comment gagner en mémoire et vitesse ?
Une fois que mon pipeline de nettoyage tient debout, je regarde toujours les types. Pas pour faire joli. Pour éviter que Pandas garde des colonnes texte en object alors qu’elles pourraient être beaucoup plus légères.
Le cas classique, c’est une colonne avec peu de valeurs différentes. Un statut, un pays, une catégorie produit, un segment client. Si j’ai 2 millions de lignes avec seulement 5 statuts possibles, category est souvent un très bon choix. Pandas stocke les valeurs une fois, puis utilise des codes internes. Ça prend moins de mémoire, et certaines opérations deviennent plus rapides.
Je commence toujours par mesurer, sinon on optimise à l’aveugle.
df.memory_usage(deep=True).sort_values(ascending=False)
Je reste prudent. Une colonne comme customer_id, order_id ou un commentaire libre n’est pas un bon candidat. Si presque chaque ligne a une valeur différente, category peut ne rien apporter, voire compliquer les choses.
Avant de convertir, je nettoie les textes avec les accesseurs vectorisés .str. C’est plus lisible et souvent plus adapté que des boucles Python ou des apply écrits trop vite. J’ai vu des fichiers clients passer de plusieurs minutes à quelques secondes juste en remplaçant des boucles ligne par ligne par du .str.
memory_before = df.memory_usage(deep=True).sum()
df['item_code'] = (
df['item_code']
.astype('string')
.str.strip()
.str.upper()
.str.replace(' ', '', regex=False)
.str.replace('-', '', regex=False)
)
df['status'] = (
df['status']
.astype('string')
.str.strip()
.str.lower()
.str.replace('in progress', 'pending', regex=False)
)
df['status'] = df['status'].astype('category')
memory_after = df.memory_usage(deep=True).sum()
print(memory_before, memory_after)
Je peux aussi utiliser .str.contains() pour repérer des valeurs à corriger, par exemple des codes article qui contiennent un préfixe bizarre. Mais attention, ces méthodes ne remplacent pas une vraie règle métier. Elles nettoient la forme. Elles ne décident pas ce qu’un statut veut dire dans votre activité.
Ma règle simple est celle-ci.
| Type ou méthode | Quand je l’utilise |
| object | Quand la colonne contient du texte très variable, des identifiants uniques, ou des valeurs mixtes à inspecter. |
| category | Quand la colonne a peu de valeurs différentes, comme un statut, un pays, une catégorie ou un segment. |
| .str | Quand je veux nettoyer du texte proprement avec strip, lower, upper, replace ou contains, sans boucle Python inutile. |
Comment imputer par groupe proprement ?
Quand je vois une colonne avec des valeurs manquantes, je me méfie toujours du réflexe “on met la médiane partout et c’est réglé”. Ça marche parfois, mais dans beaucoup de jeux de données, les groupes n’ont pas du tout le même comportement. Un prix moyen en électronique n’a rien à voir avec un prix moyen en papeterie. Un délai de livraison pour un client premium peut être très différent d’un client standard.
Dans ces cas-là, j’utilise groupby().transform(). L’idée est simple : on calcule une statistique par groupe, mais Pandas renvoie une série de même longueur que le DataFrame, alignée sur l’index d’origine. Donc pas besoin de faire une agrégation puis un merge manuel derrière. C’est plus propre, et surtout ça évite pas mal d’erreurs silencieuses.
df['price_imputed'] = df['price'].isna()
df['price'] = df['price'].fillna(
df.groupby('product_category')['price'].transform('median')
)
Ici, chaque prix manquant est remplacé par la médiane de sa product_category. Si la ligne est dans “electronics”, elle récupère la médiane des prix “electronics”. Pas celle de tout le dataset. C’est souvent beaucoup plus réaliste.
On peut aussi grouper sur deux colonnes quand le contexte est plus précis. Par exemple une médiane de délai par segment client et catégorie produit.
df['delivery_delay_imputed'] = df['delivery_delay'].isna()
df['delivery_delay'] = df['delivery_delay'].fillna(
df.groupby(['customer_segment', 'product_category'])['delivery_delay']
.transform('median')
)
Le petit piège, c’est le groupe entièrement vide. Si aucun délai n’est connu pour un couple segment + catégorie, la médiane du groupe reste manquante. Dans ce cas, je mets souvent une stratégie de repli simple avec la médiane globale.
group_median = df.groupby('product_category')['price'].transform('median')
global_median = df['price'].median()
df['price_imputed'] = df['price'].isna()
df['price'] = df['price'].fillna(group_median).fillna(global_median)
Je préfère toujours marquer les lignes imputées quand la donnée sert à une décision business. Une valeur remplacée reste une estimation. Et si 40% des prix d’une catégorie sont manquants, le problème n’est peut-être pas statistique. C’est peut-être un souci de collecte, d’intégration ou de process métier. Imputer sans regarder peut juste rendre le problème invisible.
| Méthode | Quand je l’utilise | Risque |
| Imputation globale | Quand les groupes sont homogènes ou peu importants | Écrase les différences entre populations |
| Imputation par groupe | Quand la valeur dépend clairement du contexte de la ligne | Peut échouer si certains groupes sont vides |
| Suppression des lignes | Quand il y a peu de valeurs manquantes et aucun biais évident | Peut supprimer une population entière sans qu’on s’en rende compte |
Et si votre nettoyage Pandas devenait enfin lisible ?
Le nettoyage de données avec Pandas devient beaucoup plus fiable quand on arrête d’empiler des mutations au hasard. Je préfère construire un pipeline clair avec .assign(), .query() et .pipe(), puis optimiser les colonnes qui coûtent cher avec category et les méthodes .str vectorisées. Pour les valeurs manquantes, .transform() permet de rester proche du contexte de chaque ligne, au lieu d’écraser les différences entre groupes. Ce n’est pas juste plus propre sur le papier. C’est du code qu’on relit, qu’on teste, qu’on transmet et qu’on peut faire évoluer sans peur. Le bénéfice pour vous est simple : moins de bugs, moins de RAM gaspillée, plus de confiance dans vos données.
FAQ
- Pourquoi Pandas affiche un SettingWithCopyWarning ?
Pandas l’affiche quand il n’est pas sûr que vous modifiez une vraie copie ou une vue d’un DataFrame. C’est typiquement le genre de problème qui arrive avec du code impératif découpé en petites mutations. Un pipeline avec .assign(), .query() et .pipe() limite ce flou et rend les transformations plus explicites. - À quoi sert .assign() dans un pipeline Pandas ?
.assign() sert à créer ou modifier des colonnes sans sortir du chaînage. J’aime bien l’utiliser avec des lambdas, parce que chaque calcul part du DataFrame courant. Ça évite les variables temporaires partout et ça rend les dépendances entre colonnes plus lisibles. - Quand faut-il utiliser le type category dans Pandas ?
Le type category est utile pour les colonnes texte avec peu de valeurs différentes, comme un statut, un segment ou une catégorie produit. Il peut réduire l’usage mémoire et parfois accélérer certains traitements. Je l’évite pour les identifiants très uniques ou les champs libres, où le gain est rarement intéressant. - Pourquoi utiliser .str au lieu d’une boucle Python ?
Les accesseurs .str de Pandas permettent d’appliquer des opérations de texte de façon vectorisée et lisible. Pour nettoyer des espaces, uniformiser la casse ou remplacer des caractères, c’est souvent plus propre qu’une boucle ou un apply bricolé. Et surtout, le pipeline reste facile à relire. - Pourquoi .transform() est pratique pour imputer des valeurs manquantes ?
.transform() renvoie une série alignée sur le DataFrame d’origine. C’est parfait pour remplacer une valeur manquante par la médiane ou la moyenne de son groupe, sans faire d’agrégation séparée puis de merge. On garde le contexte métier de la ligne, ce qui donne souvent une imputation plus juste.
A propos de l’auteur
Je suis Franck Scandolera, expert et formateur en Tracking avancé server-side, Analytics Engineering, automatisation No/Low Code avec n8n, intégration de l’IA en entreprise et SEO/GEO. Avec mon agence webAnalyste et mon organisme Formations Analytics, j’accompagne des équipes qui veulent rendre leurs données plus propres, plus exploitables et plus utiles au business. J’ai travaillé avec des clients comme Logis Hôtel, Yelloh Village, BazarChic, la Fédération Française de Football ou Texdecor. Si vous voulez fiabiliser vos pipelines data, vos automatisations ou vos usages IA, contactez-moi.
⭐ Data Analyst, Analytics Engineer et expert dans l’automatisation IA ⭐
Ref clients : Logis Hôtel, Yelloh Village, BazarChic, Fédération Football Français, Texdecor…
Mon terrain de jeu :
Data Analyst & Analytics engineering : tracking propre RGPD, entrepôt de données (GTM server, BigQuery…), modèles (dbt/Dataform), dashboards décisionnels (Looker, SQL, Python).
Automatisation IA des taches Data, Marketing, RH, compta etc : conception de workflows intelligents robustes (n8n, Make, App Script, scraping) connectés aux API de vos outils et LLM (OpenAI, Mistral, Claude…).
Engineering IA pour créer des applications et agent IA sur mesure : intégration de LLM (OpenAI, Mistral…), RAG, assistants métier, génération de documents complexes, APIs, backends Node.js/Python.





