cassionAnalyse de données

Leçon 4 sur 8

Unité · Une ligne par quoi ?

Long, large, et le groupe répété entre les deux

Pivoter parce que l'analyse a besoin de la forme, pas parce que l'export est arrivé ainsi. Le groupe répété aplati, le tableau croisé DHIS2, et la valeur de remplissage qui transforme « non rapporté » en zéro.

PythonR90 minDéfinitions d'indicateurs de l'UNICEF

Deux formes, et l’usage de chacune

Les mêmes données, deux fois.

Long — une ligne par observation, une colonne par variable. 2 736 lignes de formation sanitaire, période, antigène, doses.

Large — une ligne par unité, une colonne par mesure. 456 lignes de formation sanitaire et période, avec six colonnes d’antigènes.

Aucune n’est correcte dans l’absolu. La règle qui fonctionne vraiment :

Le long sert à calculer. Le large sert à lire.

Regroupez, filtrez, joignez et résumez sur des données longues, car chacun de ces verbes prend un nom de colonne et le format long a une colonne par concept. Pivotez vers le large en toute dernière étape avant qu’un humain le regarde, car une personne qui lit un tableau veut les six antigènes côte à côte.

L’essentiel des ennuis de cette leçon vient de l’inverse — recevoir un export large et l’analyser large, ce qui transforme « calculer la couverture de chaque antigène » d’une ligne en six.

Le groupe répété aplati

Une plateforme de collecte pose une question répétée — chaque enfant du ménage, chaque symptôme observé, chaque source d’eau utilisée — et l’exporte en colonnes numérotées.

household_id, member_1_age, member_1_sex, member_2_age, member_2_sex, member_3_age, ...

C’est l’une des formes les plus répandues dans ce secteur et elle est inexploitable telle quelle. Vous ne pouvez pas compter les membres, ni filtrer les enfants de moins de cinq ans, ni joindre à quoi que ce soit, parce que ce sur quoi vous voulez travailler est étalé en colonnes au lieu de descendre en lignes.

long = members_wide.melt(
    id_vars="household_id",
    var_name="field",
    value_name="value",
)
long[["slot", "attribute"]] = long["field"].str.extract(r"member_(\d+)_(\w+)")

members = (
    long.dropna(subset=["slot"])
    .pivot(index=["household_id", "slot"], columns="attribute", values="value")
    .reset_index()
)
members <- members_wide |>
  tidyr::pivot_longer(
    cols = tidyr::starts_with("member_"),
    names_to = c("slot", "attribute"),
    names_pattern = "member_(\\d+)_(\\w+)",
    values_to = "value"
  ) |>
  tidyr::pivot_wider(names_from = attribute, values_from = value)

Deux pivots, pas un. Le premier transforme chaque colonne numérotée en lignes ; le second retransforme les noms d’attributs en colonnes. C’est en essayant de le faire d’un seul geste qu’on se bloque, car le nom de colonne porte deux faits — quel membre et quel attribut — qu’il faut séparer avant de pouvoir utiliser l’un ou l’autre.

names_pattern en R et l’extraction par expression régulière en Python opèrent cette séparation. Mettez le motif au point face aux vrais noms de colonnes avant d’écrire quoi que ce soit d’autre ; un NA silencieux ici devient un membre qui disparaît.

Les emplacements vides, et ceux qui veulent dire quelque chose

Un formulaire prévu pour huit membres et un ménage de trois exporte cinq emplacements vides. La plupart ne sont rien. L’un d’eux ne l’est pas.

print(members["age"].isna().sum(), "empty slots")

members = members[members["age"].notna() | members["sex"].notna()]
members |> summarise(empty = sum(is.na(age) & is.na(sex)))

members <- members |> filter(!is.na(age) | !is.na(sex))

La distinction — un emplacement dont tous les attributs sont manquants est du remplissage et doit partir. Un emplacement dont l’âge manque mais dont le sexe est renseigné est un membre réel avec un âge manquant, et le supprimer retire une personne du ménage. Filtrez sur « toute la ligne est vide », jamais sur une seule colonne.

Confrontez le résultat à quelque chose que le fichier sait déjà.

counted = members.groupby("household_id").size().rename("members_found")
check = households.join(counted, on="household_id")
mismatch = check[check["members_found"] != check["household_size"]]
print(len(mismatch), "households where the roster does not match household_size")
check <- households |>
  left_join(count(members, household_id, name = "members_found"), by = "household_id") |>
  filter(members_found != household_size)

La taille de ménage déclarée et le nombre de lignes de membres doivent concorder. Là où ce n’est pas le cas, soit le dépliage a perdu quelqu’un, soit l’entretien.

Du long au large — le tableau croisé DHIS2

L’extraction vaccinale arrive en long — une ligne par formation sanitaire, période et antigène — et c’est la bonne forme pour calculer. Pour la poser devant un responsable sanitaire de district, pivotez.

wide = vax.pivot_table(
    index=["facility_id", "period"],
    columns="antigen",
    values="doses_administered",
    aggfunc="sum",
).reset_index()

print(len(vax), "->", len(wide))
wide <- vax |>
  tidyr::pivot_wider(
    id_cols = c(facility_id, period),
    names_from = antigen,
    values_from = doses_administered
  )

cat(nrow(vax), "->", nrow(wide), "\n")

2 736 lignes deviennent 456 — 38 formations sanitaires sur 12 mois — avec six colonnes d’antigènes. L’arithmétique mérite d’être vérifiée à chaque fois : 2 736 divisé par 6 antigènes fait 456, donc rien n’a été agrégé au passage. Quand la division ne tombe pas juste, la table longue avait des doublons sur la clé de pivot, et pivot_table les aura silencieusement sommés.

Le pivot de pandas lève une erreur sur les doublons ; pivot_table les agrège. Prenez pivot d’abord, précisément parce que vous voulez l’erreur. Le pivot_wider de tidyr avertit et produit des colonnes-listes, ce qui est plus laid et tout aussi informatif.

La valeur de remplissage qui invente des données

wide = vax.pivot_table(
    index=["facility_id", "period"], columns="antigen",
    values="doses_administered", aggfunc="sum", fill_value=0,
)
wide <- vax |>
  tidyr::pivot_wider(names_from = antigen, values_from = doses_administered,
                     values_fill = 0)

fill_value=0 remplit les combinaisons absentes des données longues. Pour des doses administrées c’est souvent juste — une formation sanitaire sans ligne penta3 n’a réellement administré aucune dose de penta3.

Pour un taux, c’est toujours faux. Une cellule de couverture manquante signifie « non calculée », et la remplir par zéro publie une formation sanitaire à 0 % de couverture qui n’a simplement pas rapporté. Le cours de nettoyage faisait ce constat à propos du rapportage ; ici il revient par une autre porte, comme un argument de pivot que personne ne perçoit comme une décision.

Le réflexe sûr est de la laisser manquante et de la traiter explicitement.

wide = wide.assign(reported=wide.notna().sum(axis=1))
wide <- wide |> mutate(reported = rowSums(!is.na(across(bcg:penta3))))

Le tableau large que vous ne pouvez pas filtrer

Une fois en large, une question comme « quels couples formation-mois ont eu moins de dix doses d’un antigène quelconque » demande six comparaisons reliées par ou, et ajouter un septième antigène oblige à toutes les modifier. La même question sur des données longues tient en une ligne.

low = vax[vax["doses_administered"] < 10]
low <- vax |> filter(doses_administered < 10)

C’est le test qui dit si vous avez pivoté trop tôt. Si votre opération suivante nomme plus d’une colonne contenant le même type de valeur, revenez au long.

Aller-retour, et son contrôle

Toute restructuration digne de confiance est réversible. Affirmez-le une fois, pendant que vous écrivez le code.

back = wide.melt(id_vars=["facility_id", "period"],
                 var_name="antigen", value_name="doses_administered").dropna()

assert len(back) == len(vax)
assert back["doses_administered"].sum() == vax["doses_administered"].sum()
back <- wide |>
  tidyr::pivot_longer(-c(facility_id, period),
                      names_to = "antigen", values_to = "doses_administered") |>
  filter(!is.na(doses_administered))

stopifnot(nrow(back) == nrow(vax),
          sum(back$doses_administered) == sum(vax$doses_administered))

Nombre de lignes et total. Deux lignes, et elles attrapent un antigène silencieusement perdu, un doublon qui s’est fait sommer, et une valeur de remplissage qui a inventé des lignes.

La suite

Vous savez désormais donner à une table la forme qu’exige la question. L’unité suivante porte sur les tables qu’il faut faire venir de l’extérieur — la base de population qui fournit un dénominateur de couverture, et le calendrier qui dit quelles périodes auraient dû exister.

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.

Lancer le diaporamaLire les diapositives

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.