cassionAnalyse de données

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.

PythonR75 min

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 valeurs1 = 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.

Animer cette leçon

La leçon en diaporama, la prose étant reléguée dans les notes du présentateur plutôt que projetée. Produit à partir de cette page, dont il ne peut donc pas s'écarter.

Le PDF ne requiert aucun logiciel et se projette depuis n'importe quel poste. Le fichier PowerPoint est fait pour être modifié : appliquez la charte de votre organisation, retirez une section pour une séance plus courte, ou fusionnez deux leçons en atelier.