Leçon 4 sur 8
Obtenir un tableau ordonné
Restructurer les groupes répétés aplatis en une ligne par observation, joindre une liste de membres à son ménage, et prouver que la jointure n'a pas modifié votre charge de cas en silence.
La règle
Une ligne par observation, une colonne par variable, une table par type d’objet. La règle est ancienne et elle fait encore l’essentiel du travail.
Il vaut la peine d’en dire la raison, car « ordonné » évoque le rangement plutôt que l’ingénierie. Dans un tableau ordonné, chaque opération souhaitée — filtrer, grouper, joindre, représenter — est la même opération quelle que soit la variable traitée. Dans un tableau désordonné, chaque variable exige du code sur mesure, et c’est dans ce code sur mesure que vivent les erreurs.
À quoi ressemble le désordre dans ce secteur
Trois formes couvrent la quasi-totalité des cas.
Groupes répétés aplatis. CommCare exporte une ligne par soumission, donc une visite de ménage ayant dépisté trois enfants devient :
form_id,commune,child_1_id,child_1_muac,child_2_id,child_2_muac,child_3_id,child_3_muac
F0012,Gonaives,CH00854,133,CH00855,118,,
F0013,Verrettes,CH01439,143,,,,
Une ligne est un formulaire. Vous voulez une ligne par enfant. Le nombre de colonnes change en outre d’un export à l’autre, ce qui signifie que tout code nommant explicitement les colonnes cassera le mois prochain.
Des valeurs dans les en-têtes de colonnes. Un tableau croisé DHIS2 dispose les périodes en haut :
org_unit,janv-2024,févr-2024,mars-2024
Gonaives,412,388,401
Le mois est une donnée — c’est une valeur d’une variable nommée période — mais
il loge dans un en-tête.
Plusieurs objets dans une même table. Un export d’enquête où les caractéristiques du ménage sont répétées sur chaque ligne de membre. Rien de grave, jusqu’à ce que quelqu’un calcule une taille moyenne de ménage et obtienne un chiffre pondéré par la taille des ménages.
Format long et format large
Passer de l’un à l’autre est la compétence mécanique la plus utile de cette formation.
import pandas as pd
large = pd.read_csv("data/raw/commcare-screening-export.csv")
long = large.melt(
id_vars=["form_id", "commune"],
var_name="champ",
value_name="valeur",
)
# child_1_muac -> enfant 1, champ muac
parties = long["champ"].str.extract(r"^child_(?P<rang>\d+)_(?P<attribut>.+)$")
long = pd.concat([long, parties], axis=1).dropna(subset=["rang"])
enfants = (
long.pivot(index=["form_id", "commune", "rang"], columns="attribut", values="valeur")
.reset_index()
.dropna(subset=["id"])
)
print(enfants.head())
library(dplyr)
library(tidyr)
library(readr)
large <- read_csv("data/raw/commcare-screening-export.csv")
enfants <- large |>
pivot_longer(
cols = starts_with("child_"),
names_to = c("rang", "attribut"),
names_pattern = "child_(\\d+)_(.+)",
values_to = "valeur",
values_transform = as.character
) |>
pivot_wider(names_from = attribut, values_from = valeur) |>
filter(!is.na(id))
enfants
Deux détails portent l’essentiel :
- Repérez le rang par une expression régulière, jamais en énumérant les colonnes. La liste change d’un export à l’autre ; le motif, non.
- Supprimez les rangs vides. Un formulaire ayant dépisté un seul enfant produit malgré tout des colonnes pour les rangs deux et trois, remplies de rien. Les conserver gonfle votre charge de cas de lignes qui ne sont pas des enfants.
Vérifiez ensuite l’arithmétique. Si l’export comptait 1 600 formulaires et que le plus large portait trois enfants, la restructuration produit 4 800 lignes candidates, dont seules les vraies survivent à la suppression. Ce nombre survivant est votre charge de cas, et il doit correspondre à ce que le programme pense avoir dépisté.
Joindre une liste à son parent
KoboToolbox place les répétitions dans leur propre feuille, reliée par
_parent_index.
menages = pd.read_excel("data/raw/kobo-export.xlsx", sheet_name="household")
membres = pd.read_excel("data/raw/kobo-export.xlsx", sheet_name="member")
fusion = membres.merge(
menages[["_index", "commune", "interview_date"]],
left_on="_parent_index",
right_on="_index",
how="left",
validate="many_to_one",
indicator=True,
)
print(fusion["_merge"].value_counts())
library(readxl)
menages <- read_excel("data/raw/kobo-export.xlsx", sheet = "household")
membres <- read_excel("data/raw/kobo-export.xlsx", sheet = "member")
fusion <- membres |>
left_join(
menages |> select(`_index`, commune, interview_date),
by = join_by(`_parent_index` == `_index`),
relationship = "many-to-one",
unmatched = "error"
)
nrow(fusion) == nrow(membres)
validate="many_to_one" sous pandas et relationship = "many-to-one" sous
dplyr sont les arguments importants, et presque personne ne les emploie. Ils
déclenchent une erreur si la relation que vous avez affirmée n’est pas celle des
données — c’est-à-dire exactement la défaillance qui multiplie sinon votre
nombre de lignes et se découvre trois étapes plus loin sous la forme d’une
charge de cas surestimée de 14 %.
Prouvez que la jointure a fait ce que vous aviez annoncé
Trois vérifications, à chaque fois. Elles prennent une minute et ont détecté plus d’erreurs qu’aucune autre technique de cette formation.
avant = len(membres)
apres = len(fusion)
assert apres == avant, f"la jointure a changé le nombre de lignes : {avant} -> {apres}"
orphelins = (fusion["_merge"] == "left_only").sum()
assert orphelins == 0, f"{orphelins} membres sans ménage"
assert fusion["child_id"].is_unique, "identifiants dupliqués après jointure"
stopifnot(nrow(fusion) == nrow(membres))
stopifnot(!any(is.na(fusion$commune)))
stopifnot(!any(duplicated(fusion$child_id)))
La vérification du nombre de lignes est celle à intégrer. Une jointure à gauche ne peut que laisser le nombre de lignes inchangé ou l’augmenter. S’il a augmenté, la table de droite comportait des clés dupliquées, et chaque clé dupliquée a discrètement multiplié ses lignes. Dans un tableau de charge de cas, c’est une surestimation que vous rapporterez à un bailleur.
Une jointure qui modifie votre nombre de lignes n’est pas un problème de données à corriger en aval. C’est le signe que vous avez mal compris l’une des deux tables, et la correction se situe en amont de la jointure.
Le registre est déjà ordonné
Le registre de dépistage PB utilisé par cette formation comporte une ligne par enfant, ce qui permet de le travailler directement :
muac.head()
head(muac)
C’est délibéré. La restructuration est une compétence réelle et vous en aurez besoin, mais ce n’est pas là que se situent les décisions intéressantes. Ces décisions commencent à la leçon suivante, lorsque vous regarderez ce que contiennent réellement ces 4 218 lignes.
La suite
Le tableau a la bonne forme. La leçon suivante établit ce qui ne va pas dedans — méthodiquement, avec une non-réponse ventilée par site et par semaine, car une non-réponse qui se concentre est un problème d’une tout autre nature qu’une non-réponse dispersée.