Leçon 3 sur 8
Lire un export sans le corrompre
Inférence de types, zéros initiaux, dates, et le code de valeur manquante qui devient un nombre. Les dégâts se produisent à l'import, avant même que vous ayez regardé quoi que ce soit.
Les dégâts précèdent le regard
read_csv(chemin) tient sur une ligne et prend une demi-douzaine de décisions à
votre place. La plupart sont justes. Celles qui ne le sont pas le sont
silencieusement, et le temps que vous vous en aperceviez, la valeur corrompue
est passée dans trois synthèses et un graphique.
Cette leçon consiste à reprendre ces décisions.
Les codes de valeur manquante deviennent des nombres
Le registre PB code les mesures manquantes -99, et non par une cellule vide.
Il y a une raison à cela — une case vide sur un registre papier est ambiguë
entre « non mesuré » et « l’enquêteur a sauté la page », alors qu’une valeur
sentinelle ne l’est pas — mais un lecteur de CSV n’a aucun moyen de savoir que
-99 n’est pas une mesure.
import pandas as pd
naif = pd.read_csv("data/raw/muac-screening-artibonite-2024.v1.csv")
print(naif["muac_mm"].mean())
Ce chiffre est inférieur de plusieurs millimètres à la réalité. Pire, il est plausible : rien n’y ressemble à une erreur, et une moyenne de PB n’est pas une valeur que la plupart des lecteurs peuvent vérifier d’un coup d’œil.
Déclarez la sentinelle à la lecture et elle n’entre jamais dans un calcul :
muac = pd.read_csv(
"data/raw/muac-screening-artibonite-2024.v1.csv",
na_values={"muac_mm": ["-99"]},
)
print(muac["muac_mm"].mean())
print(muac["muac_mm"].isna().sum())
library(readr)
muac <- read_csv(
"data/raw/muac-screening-artibonite-2024.v1.csv",
na = c("", "NA", "-99")
)
mean(muac$muac_mm, na.rm = TRUE)
sum(is.na(muac$muac_mm))
Notez la différence. pandas permet de restreindre la sentinelle à une colonne,
ce qui est souhaitable : -99 est un PB manquant, mais si une colonne portait
légitimement une valeur négative, un traitement global la détruirait. Le
read_csv de R applique na à tout le fichier ; restreignez ensuite la portée
lorsque cela importe.
Traiter une sentinelle n’est pas la même chose que décider quoi faire de la valeur manquante. Cette décision relève de la leçon 6. Ici, vous vous assurez seulement que le code cesse de se faire passer pour une mesure.
Les identifiants ne sont pas des nombres
child_id s’écrit ici CH00854 et survit intact à l’import. Beaucoup de vrais
registres utilisent des identifiants purement numériques — 00854 — et un
lecteur les transformera obligeamment en l’entier 854.
Le zéro initial a disparu, la jointure vers le fichier ménage échoue exactement pour les identifiants qui en portaient un, et l’échec ressemble à des ménages manquants plutôt qu’à une erreur de type.
muac = pd.read_csv(
chemin,
dtype={"child_id": "string", "commune": "string"},
na_values={"muac_mm": ["-99"]},
)
muac <- read_csv(
chemin,
col_types = cols(
child_id = col_character(),
commune = col_character()
),
na = c("", "NA", "-99")
)
La règle se généralise : si vous ne ferez jamais d’arithmétique dessus, ce n’est pas un nombre. Codes de formation sanitaire, numéros de téléphone, identifiants de grappe, numéros de ménage et codes administratifs sont tous du texte qui s’écrit avec des chiffres.
Les dates
screening_date arrive sous la forme 2024-01-15. C’est de l’ISO 8601, c’est
sans ambiguïté, et les deux lecteurs l’interpréteront correctement. Sachez que
vous avez de la chance.
De vrais exports produisent 15/01/2024, 01/15/2024, 15-janv-24 et des
numéros de série Excel comme 45306, parfois dans la même colonne, parce que
trois personnes ont saisi sur trois machines configurées différemment.
01/02/2024 est soit le 1er février, soit le 2 janvier, et le fichier ne vous
dira pas lequel.
muac["screening_date"] = pd.to_datetime(
muac["screening_date"], format="%Y-%m-%d", errors="raise"
)
muac <- muac |>
dplyr::mutate(
screening_date = as.Date(screening_date, format = "%Y-%m-%d")
)
stopifnot(!any(is.na(muac$screening_date)))
Deux habitudes valent la peine d’être prises :
- Énoncez explicitement le format plutôt que de laisser l’analyseur le déduire. Un format déduit peut changer d’un fichier à l’autre selon la composition des valeurs.
- Utilisez
errors="raise". L’alternative,errors="coerce", convertit toute date illisible en valeur manquante — autrement dit, un fichier dans un mauvais format de date s’importe « avec succès » avec une colonne de dates entièrement vide.
Les catégories à vocabulaire fixe
outcome prend quatre valeurs et sex en prend deux. Les déclarer comme
catégorielles apporte deux choses : un objet plus léger et — plus utile — une
erreur lorsqu’une valeur hors vocabulaire apparaît.
issues = pd.CategoricalDtype(
["no-action", "referred-tsfp", "referred-otp", "referred-sc"], ordered=False
)
muac["outcome"] = muac["outcome"].astype(issues)
# Toute valeur hors vocabulaire est désormais NaN — comptez-les avant de continuer.
print(muac["outcome"].isna().sum())
muac <- muac |>
dplyr::mutate(
outcome = factor(
outcome,
levels = c("no-action", "referred-tsfp", "referred-otp", "referred-sc")
)
)
sum(is.na(muac$outcome))
C’est un vrai piège dans les deux langages : une valeur hors des niveaux déclarés devient manquante au lieu de déclencher une erreur. Comptez toujours les valeurs manquantes immédiatement après la conversion. Un passage de zéro à neuf signifie que neuf lignes portaient une valeur dont vous ignoriez l’existence, et il vaut mieux la voir maintenant que la découvrir sous forme de trou dans un tableau.
La colonne oedema de ce registre est exactement ce cas. Ennery et Desdunes
l’ont renseignée de manière incohérente au premier trimestre, avec Y et N
plutôt que true et false. Lue naïvement, ces lignes deviennent manquantes
sans un mot.
Une lecture défensive, de bout en bout
Assemblons le tout — en affirmant ce que l’on attend, pour que le script échoue sur un mauvais fichier au lieu de produire un mauvais chiffre.
import pandas as pd
CHEMIN = "data/raw/muac-screening-artibonite-2024.v1.csv"
muac = pd.read_csv(
CHEMIN,
dtype={"child_id": "string", "commune": "string", "sex": "string"},
na_values={"muac_mm": ["-99"]},
keep_default_na=True,
)
muac["screening_date"] = pd.to_datetime(
muac["screening_date"], format="%Y-%m-%d", errors="raise"
)
assert len(muac) == 4218, f"4218 lignes attendues, {len(muac)} obtenues"
assert muac["child_id"].notna().all(), "chaque ligne exige un identifiant"
print(muac.dtypes)
print(muac.isna().sum())
library(readr)
library(dplyr)
CHEMIN <- "data/raw/muac-screening-artibonite-2024.v1.csv"
muac <- read_csv(
CHEMIN,
col_types = cols(
child_id = col_character(),
commune = col_character(),
screening_date = col_date(format = "%Y-%m-%d"),
age_months = col_integer(),
sex = col_character(),
muac_mm = col_integer(),
oedema = col_character(),
outcome = col_character()
),
na = c("", "NA", "-99")
)
stopifnot(nrow(muac) == 4218)
stopifnot(!any(is.na(muac$child_id)))
glimpse(muac)
colSums(is.na(muac))
Les assertions sont la partie que l’on saute et celle qui rapporte. Un fichier qui arrive avec 4 190 lignes au lieu de 4 218 a perdu quelque chose entre le serveur et vous, et le script doit le dire plutôt que de rapporter discrètement une charge de cas plus faible.
Lire les autres formats
Les mêmes principes s’appliquent, avec d’autres noms de fonctions.
| Source | Python | R |
|---|---|---|
| CSV | pd.read_csv |
readr::read_csv |
| Excel | pd.read_excel |
readxl::read_excel |
| Stata | pd.read_stata |
haven::read_dta |
| SPSS | pd.read_spss |
haven::read_sav |
| Parquet | pd.read_parquet |
arrow::read_parquet |
Deux remarques propres au secteur. Les fichiers Stata et SPSS portent des
étiquettes de valeurs — 1 = Oui, 2 = Non — que haven conserve alors
que pandas les perd le plus souvent ; si un institut de sondage vous remet un
.dta, lisez-le en R même si vous l’analyserez en Python. Et les exports Excel
comportent fréquemment une ligne de titre fusionnée au-dessus de l’en-tête, que
les deux lecteurs prendront pour l’en-tête si vous ne leur demandez pas de la
sauter.
La suite
Le registre est maintenant en mémoire, avec ses types intacts. La leçon suivante le restructure — ainsi que l’export aplati à la CommCare sous lequel il aurait pu arriver — en une ligne par observation.