🧹 Module 14 — Data Cleaning robuste
Bootcamp Data Analyst — From Zero to Hero | Niveau Intermédiaire · Module 14
🎯 Ce que tu seras capable de faire à la fin de ce module
- Nettoyer des données textuelles complexes avec des expressions régulières, en Python et en R
- Extraire plusieurs informations d'une même chaîne de texte (groupes de capture)
- Lire une erreur Python (traceback) et un avertissement R, et savoir où chercher le problème
- Connaître le piège du NA silencieux en R — très différent du crash immédiat de Python
- Isoler un bug avec une méthode simple et reproductible
- Documenter un script proprement : docstrings Python, roxygen2 en R, README
⏱️ Durée estimée : 2 heures 🛠️ Outils : Python (module
re) · R (stringr,dplyr) 📌 Prérequis : Module 13 terminé · Modules 10-12 (Python & R) du Niveau Débutant
1. Pourquoi aller au-delà de SUPPRESPACE/.str.strip()/str_trim()
Au Niveau Débutant, tu as déjà nettoyé des données : espaces parasites, valeurs manquantes. Mais les vraies données administratives et commerciales ivoiriennes posent des problèmes plus tordus :
| Donnée brute | Le problème |
|---|---|
"07 12 34 56 78", "0712345678", "+225 0712345678", "225-07-12-34-56-78" |
Un même numéro de téléphone, 4 formats différents |
"1 250 000", "1.250.000", "1250000 FCFA" |
Un même montant, séparateurs de milliers différents |
"07/12/2023", "07-12-2023", "7 décembre 2023" |
Une même date, 3 formats différents |
"Kouassi, Jean (07-12-2023)" |
Plusieurs informations mélangées dans une seule cellule texte |
.str.strip()/str_trim() ne résout aucun de ces cas. Pour tout le reste, il faut décrire un motif (pattern) — c'est exactement ce que fait une expression régulière (regex), disponible aussi bien en Python qu'en R.
2. Les expressions régulières (regex) — Python & R
Une regex est un texte qui décrit un motif à rechercher. Le langage des motifs (\d, \s, +, *, [...]...) est identique en Python et en R — seules les fonctions qui l'utilisent changent de nom.
Les briques essentielles (communes aux deux langages)
| Motif | Signifie | Exemple |
|---|---|---|
\d |
Un chiffre | \d\d\d matche "123" |
\D |
Tout sauf un chiffre | |
\s |
Un espace (space, tab...) | |
+ |
1 fois ou plus | \d+ matche "7", "12", "345678" |
* |
0 fois ou plus | |
? |
0 ou 1 fois | |
[...] |
Un caractère parmi une liste | [A-Za-z] = une lettre |
^ / $ |
Début / fin de chaîne | |
(...) |
Un groupe de capture (pour extraire un morceau précis) |
Cas 1 — Uniformiser des numéros de téléphone ivoiriens
Python :
pythonimport re
numeros = ["07 12 34 56 78", "0712345678", "+225 0712345678", "225-07-12-34-56-78"]
def uniformiser(numero):
chiffres = re.sub(r"\D", "", numero) # ne garder que les chiffres
return chiffres[-10:] # garder les 10 derniers (retire le 225 éventuel)
propres = [uniformiser(n) for n in numeros]
print(propres)
# ['0712345678', '0712345678', '0712345678', '0712345678']R (équivalent avec stringr) :
rlibrary(stringr)
numeros <- c("07 12 34 56 78", "0712345678", "+225 0712345678", "225-07-12-34-56-78")
uniformiser <- function(numero) {
chiffres <- str_remove_all(numero, "\\D") # ne garder que les chiffres
str_sub(chiffres, -10, -1) # garder les 10 derniers caractères
}
propres <- sapply(numeros, uniformiser)
print(propres)
# [1] "0712345678" "0712345678" "0712345678" "0712345678"💡 En R, une regex à l'intérieur d'une chaîne de caractères s'écrit avec un double backslash (
"\\D") — le premier backslash échappe le second pour que R transmette bien\Dau moteur de regex. Même motif qu'en Python, syntaxe d'échappement différente.
Cas 2 — Extraire un montant
Python :
pythonmontant = "1 250 000 FCFA"
chiffres = re.sub(r"[^\d]", "", montant)
print(int(chiffres))
# 1250000R :
rmontant <- "1 250 000 FCFA"
chiffres <- str_remove_all(montant, "[^0-9]")
as.numeric(chiffres)
# [1] 1250000Cas 3 — Extraire plusieurs informations d'une même chaîne (groupes de capture)
Reprenons "Kouassi, Jean (07-12-2023)" de la section 1 — comment en extraire le nom, le prénom et la date séparément ?
Python :
pythontexte = "Kouassi, Jean (07-12-2023)"
match = re.match(r"(\w+), (\w+) \((\d{2})-(\d{2})-(\d{4})\)", texte)
nom, prenom, jour, mois, annee = match.groups()
print(f"{prenom} {nom}, né(e) le {annee}-{mois}-{jour}")
# Jean Kouassi, né(e) le 2023-12-07R (avec str_match) :
rtexte <- "Kouassi, Jean (07-12-2023)"
resultat <- str_match(texte, "(\\w+), (\\w+) \\((\\d{2})-(\\d{2})-(\\d{4})\\)")
nom <- resultat[1, 2]
prenom <- resultat[1, 3]
jour <- resultat[1, 4]
mois <- resultat[1, 5]
annee <- resultat[1, 6]
cat(prenom, nom, ", né(e) le", annee, "-", mois, "-", jour, "\n")
# Jean Kouassi , né(e) le 2023 - 12 - 07🔗 Pont : chaque
(...)dans le motif devient un élément dematch.groups()en Python, ou une colonne de la matrice retournée parstr_match()en R (la colonne 1 est toujours la chaîne complète, les groupes commencent à la colonne 2).
👉 À toi de jouer
Écris une fonction (en Python ou en R) qui uniformise ces 3 dates dans un même format AAAA-MM-JJ : "07/12/2023", "07-12-2023", "7 décembre 2023". Indice : il te faudra deux motifs différents (un pour les formats numériques avec / ou -, un pour le format en toutes lettres) et un petit dictionnaire {"décembre": "12", ...} pour convertir le nom du mois.
3. Déboguer un script qui plante — Python & R
Python : un crash immédiat et explicite
pythonimport pandas as pd
df = pd.DataFrame({"salaire": ["450000", "380 000", "non renseigné"]})
df["salaire_num"] = df["salaire"].astype(int)Traceback (most recent call last):
File "script.py", line 3, in <module>
df["salaire_num"] = df["salaire"].astype(int)
ValueError: invalid literal for int() with base 10: '380 000'Trois informations essentielles, à lire de bas en haut :
- Le type d'erreur (
ValueError) et son message :int()ne sait pas convertir'380 000'(l'espace bloque la conversion) - La ligne exacte qui a planté (
line 3) - La pile d'appels au-dessus, si l'erreur vient d'une fonction imbriquée
R : le piège du NA silencieux
Le même problème en R se comporte très différemment — et c'est un piège classique :
rdf <- data.frame(salaire = c("450000", "380 000", "non renseigné"))
df$salaire_num <- as.integer(df$salaire)Warning message:
NAs introduced by coercionR ne plante pas. Il affiche un simple avertissement et continue, en remplaçant silencieusement "380 000" ET "non renseigné" par NA — sans distinguer "une vraie valeur mal formatée" d'une "valeur légitimement absente". Si tu ne lis pas tes avertissements, ton script continue avec des données fausses sans que rien ne t'alerte.
⚠️ Réflexe R indispensable :
options(warn = 2)transforme tous les avertissements en erreurs bloquantes — utile pendant le développement pour ne rater aucun avertissement silencieux comme celui-ci.
Méthode pour isoler un bug (valable dans les deux langages)
- Reproduis en petit : teste la ligne fautive sur une seule valeur (
int("380 000")ouas.integer("380 000")) plutôt que sur tout un jeu de données. - Affiche l'état intermédiaire :
df["salaire"].unique()(Python) ouunique(df$salaire)(R) révèle souvent le format inattendu. - Corrige à la source avec une regex, plutôt que de bricoler un
try/except(Python) outryCatch(R) qui cache le problème.
Python (correction) :
pythondf["salaire_num"] = df["salaire"].apply(
lambda s: int(re.sub(r"\D", "", s)) if re.search(r"\d", s) else None
)
print(df)
# salaire salaire_num
# 0 450000 450000.0
# 1 380 000 380000.0
# 2 non renseigné NaNR (correction) :
rlibrary(dplyr)
library(stringr)
df <- df %>%
mutate(salaire_num = if_else(
str_detect(salaire, "\\d"),
as.integer(str_remove_all(salaire, "\\D")),
NA_integer_
))
print(df)
# salaire salaire_num
# 1 450000 450000
# 2 380 000 380000
# 3 non renseigné NA💡 Un
try/except/tryCatchqui avale l'erreur sans rien afficher est le pire réflexe de debugging, dans les deux langages — tu perds l'information qui t'aurait dit où est le problème.
4. Documenter son travail — Python & R
Un script qui fonctionne aujourd'hui et qu'on ne comprend plus dans 3 mois n'est pas un script fini.
Docstring Python
pythondef uniformiser_numero(numero: str) -> str:
"""Uniformise un numéro de téléphone ivoirien en 10 chiffres sans préfixe.
Gère les formats : espaces, tirets, +225, préfixe 225.
Exemple : "+225 07 12 34 56 78" -> "0712345678"
"""
chiffres = re.sub(r"\D", "", numero)
return chiffres[-10:]Documentation roxygen2 en R
R utilise une convention équivalente, avec des commentaires préfixés par #' :
r#' Uniformise un numéro de téléphone ivoirien en 10 chiffres sans préfixe
#'
#' Gère les formats : espaces, tirets, +225, préfixe 225.
#'
#' @param numero Un numéro de téléphone sous forme de texte
#' @return Le numéro nettoyé, 10 chiffres, sans préfixe
#' @examples
#' uniformiser_numero("+225 07 12 34 56 78") # "0712345678"
uniformiser_numero <- function(numero) {
chiffres <- str_remove_all(numero, "\\D")
str_sub(chiffres, -10, -1)
}Commentaires utiles vs bruit (les deux langages)
python# ❌ Commentaire inutile — répète ce que le code dit déjà
total = df["montant"].sum() # calcule la somme de montant
# ✅ Commentaire utile — explique un choix non-évident
total = df["montant"].sum() # les valeurs négatives = remboursements, on les garde volontairementUn bon commentaire répond à "pourquoi ?", jamais à "quoi ?" — le code répond déjà à "quoi", en Python comme en R.
README minimal pour un projet d'analyse
markdown# Analyse RH — Bamba & Associés
## Contenu
- `nettoyage.py` / `nettoyage.R` — uniformise les numéros de téléphone et montants
- `analyse.py` / `analyse.R` — calculs et graphiques
## Comment l'exécuter
1. `pip install -r requirements.txt` (Python) ou `install.packages(c("stringr","dplyr"))` (R)
2. Exécuter le script de nettoyage puis celui d'analyse
## Données
Source : export RH interne (confidentiel, non inclus dans ce dépôt)C'est le même réflexe que le README GitHub vu au Module 13 — c'est souvent la première chose qu'un∙e collègue ou un∙e recruteur∙se lit, quel que soit le langage utilisé derrière.
5. Pont Python ↔ R — data cleaning et debugging
| Opération | Python | R |
|---|---|---|
| Chercher un motif (booléen) | re.search(pattern, texte) |
str_detect(texte, pattern) |
| Remplacer tout ce qui matche | re.sub(pattern, repl, texte) |
str_remove_all(texte, pattern) / str_replace_all(...) |
| Extraire des groupes de capture | re.match(pattern, texte).groups() |
str_match(texte, pattern) (colonnes 2+) |
| Garder les N derniers caractères | texte[-10:] |
str_sub(texte, -10, -1) |
| Convertir un texte non-numérique en entier | int("abc") → crash immédiat (ValueError) |
as.integer("abc") → NA + avertissement silencieux |
| Rendre les avertissements bloquants | (déjà bloquant par défaut) | options(warn = 2) |
| Documenter une fonction | Docstring """...""" |
Commentaires roxygen2 #' ... |
| Éviter d'avaler une erreur silencieusement | Ne jamais faire un except: pass nu |
Ne jamais faire un tryCatch(..., error = function(e) NULL) nu |
🔗 Retiens surtout la différence de philosophie : Python crashe fort et tôt (tu ne peux pas l'ignorer), R avertit et continue (tu peux facilement rater le problème si tu ne lis pas la console). Aucune des deux approches n'est "meilleure" — mais tu dois savoir laquelle tu utilises pour adapter ta vigilance.
✅ Résumé du module
| Concept | Ce qu'il faut retenir |
|---|---|
| Regex | Décrit un motif à rechercher/remplacer — même langage de motifs en Python et en R |
\d, \s, +, *, [...], (...) |
Briques de base, identiques dans les deux langages |
re.search / str_detect |
Vérifier qu'un motif existe dans un texte |
re.sub / str_remove_all |
Remplacer ce qui matche un motif |
re.match(...).groups() / str_match |
Extraire plusieurs informations d'une même chaîne |
| Traceback Python | Se lit de bas en haut : type d'erreur, message, ligne fautive — crash immédiat |
NA silencieux en R |
as.integer() sur du texte mal formaté ne plante pas, il avertit et continue — piège classique |
options(warn = 2) |
Transforme les avertissements R en erreurs bloquantes |
| Isoler un bug | Reproduire en petit, afficher l'état intermédiaire, corriger à la source |
try/except / tryCatch silencieux |
Le pire réflexe, dans les deux langages |
| Docstring / roxygen2 | Explique ce que fait une fonction et comment l'utiliser |
| Bon commentaire | Répond à "pourquoi", jamais à "quoi" |
| README | Contenu, comment exécuter, source des données |
🧠 Quiz — Vérifie ta compréhension
Q1. Quelle regex retire tout ce qui n'est pas un chiffre d'une chaîne numero en Python ?
- a)
re.sub(r"\d", "", numero) - b)
re.sub(r"\D", "", numero) - c)
re.search(r"\d+", numero)
👉 Voir la réponse
✅ b) — \D (D majuscule) signifie "tout sauf un chiffre". re.sub(r"\D", "", numero) remplace chaque caractère non-numérique par rien, ne laissant que les chiffres. La réponse a) ferait l'inverse (supprimerait les chiffres), et c) ne fait que chercher, pas remplacer.
Q2. En R, as.integer("380 000") renvoie...
- a) Une erreur qui arrête le script, comme en Python
- b)
NA, avec un simple avertissement — le script continue - c)
380000, R retire automatiquement les espaces
👉 Voir la réponse
✅ b) — Contrairement à Python qui lève une ValueError bloquante, R affiche "NAs introduced by coercion" et continue silencieusement avec un NA. C'est pour ça qu'il faut toujours lire ses avertissements en R, ou utiliser options(warn = 2) pour les transformer en erreurs.
Q3. Ton script plante avec ValueError: invalid literal for int() with base 10: '380 000'. Que fais-tu en premier ?
- a) J'entoure la ligne d'un
try/except: passpour que le script continue - b) Je teste
int("380 000")isolément pour comprendre exactement ce qui bloque - c) Je relance le script en espérant que ça passe cette fois
👉 Voir la réponse
✅ b) — Reproduire le problème en isolant la valeur fautive te montre immédiatement que c'est l'espace qui bloque int(). Le try/except: pass (a) cache l'erreur sans la résoudre — tu auras juste une valeur manquante inexpliquée plus loin.
Q4. Tu as texte <- "Kouassi, Jean (07-12-2023)" en R et le motif "(\\w+), (\\w+) \\((\\d{2})-(\\d{2})-(\\d{4})\\)". Comment récupères-tu le prénom ("Jean") après resultat <- str_match(texte, motif) ?
- a)
resultat[1, 1] - b)
resultat[1, 2] - c)
resultat[1, 3]
👉 Voir la réponse
✅ c) — La colonne 1 de str_match() est toujours la chaîne complète qui matche. La colonne 2 est le premier groupe (\\w+) (le nom, "Kouassi"), la colonne 3 est le deuxième groupe (le prénom, "Jean").
Q5. Lequel de ces commentaires est utile ?
- a)
# incrémente x de 1au-dessus dex += 1 - b)
# on exclut les stagiaires : leur contrat n'a pas de date de fin, ça fausserait la moyenneau-dessus d'un filtre - c)
# fonction qui calcule quelque chose
👉 Voir la réponse
✅ b) — Ce commentaire explique une décision non-évidente (pourquoi exclure les stagiaires) que le code seul ne révèle pas. Les options a) et c) répètent ou vagualisent ce que le code dit déjà.
➡️ Module suivant
Tu sais maintenant nettoyer des données réelles et déboguer proprement, dans les deux langages. Dans le Module 15, on approfondit pandas et on découvre DuckDB pour interroger des DataFrames en SQL.
→ Module 15 — Pandas approfondi + DuckDB