Retour à la leçon·Leçon 3 sur 8·Une ligne par quoi ?
Une ligne par quoi ?
Le même diaporama que les téléchargements, rendu sous forme de page. Lancez le diaporama pour le présenter en plein écran — les flèches ou un clic avancent d'une diapositive, Échap quitte.
Ce que couvre cette leçon
- Énoncez la granularité à voix haute
- La granularité décide du sens du nombre
- Développer les ménages en personnes
- Pondérer vaut généralement mieux que développer
- Agrégez à la granularité avant de joindre
- Le plusieurs-à-plusieurs que personne n'a choisi
- Des épisodes, pas des personnes
- Inscrivez la granularité dans le nom du fichier
- La suite
Notes du présentateur
La granularité d'une table décide du sens de tout nombre qu'on en tire. Ménages contre personnes, épisodes contre patients, et le plusieurs-à-plusieurs qui transforme 2 403 lignes en 13 004 sans que personne l'ait décidé.Énoncez la granularité à voix haute — En Python
# grain: one household interview wash = pd.read_csv("wash-household-survey-2024.v1.csv") # grain: one student per school day attendance = pd.read_csv("school-attendance-2024.v1.csv") # grain: one facility x month x antigen vax = pd.read_csv("vaccination-coverage-2024.v1.csv")Notes du présentateur
La granularité d'une table, c'est ce qu'est une ligne. Un entretien de ménage. Un enfant dépisté un jour donné. Un couple formation-mois-antigène. Un épisode d'admission. Cela ressemble à une formalité. C'est la phrase la plus utile que vous puissiez écrire au-dessus d'une table, car presque toute défaillance de jointure et presque toute dispute sur un dénominateur sont en réalité deux personnes qui tiennent en tête deux granularités différentes.Énoncez la granularité à voix haute — En R
# grain: one household interview wash <- read_csv("wash-household-survey-2024.v1.csv") # grain: one student per school day attendance <- read_csv("school-attendance-2024.v1.csv") # grain: one facility x month x antigen vax <- read_csv("vaccination-coverage-2024.v1.csv")Notes du présentateur
Trois commentaires, et ils préviendront plus de dégâts que toutes les assertions de ce cours. Une jointure n'est sûre que si vous savez nommer la granularité des deux côtés et celle du résultat.La granularité décide du sens du nombre — En Python
sized = wash[wash["household_size"].notna() & wash["litres_per_person_day"].notna()] by_household = (sized["litres_per_person_day"] < 15).mean() by_person = ( sized.loc[sized["litres_per_person_day"] < 15, "household_size"].sum() / sized["household_size"].sum() ) print(f"households below 15 L/p/d: {by_household:.1%}") print(f"people in those households: {by_person:.1%}")Notes du présentateur
L'enquête EAH couvre 2 403 ménages et 13 004 personnes, soit une taille moyenne de 5,52. Deux phrases parfaitement exactes sortent de ce fichier.La granularité décide du sens du nombre — En R
sized <- wash |> filter(!is.na(household_size), !is.na(litres_per_person_day)) sized |> summarise( by_household = mean(litres_per_person_day < 15), by_person = sum(household_size[litres_per_person_day < 15]) / sum(household_size) )La granularité décide du sens du nombre
- Indiquez la granularité dans le libellé —
share_of_households_below_15letshare_of_people_below_15lne peuvent pas…
Notes du présentateur
13,3 % des ménages, 13,5 % des personnes. Et sur la défécation à l'air libre, 13,4 % des ménages contre 13,9 % des personnes. Les écarts sont ici faibles, et cela mérite d'être dit franchement — sur ce fichier le choix déplace à peine le nombre. Il déplace ce que le nombre est. Sphere énonce son standard de quantité d'eau par personne, donc c'est le chiffre par personne qui répond au standard ; un chiffre par ménage répond à « combien de ménages sont concernés », ce dont a besoin un plan de distribution. Là où l'écart n'est pas faible est précisément là où cela compte le plus. Les grands ménages manquent systématiquement plus d'eau par personne, si bien que dans une enquête où la taille des ménages varie davantage, les deux chiffres se séparent — et rapporter alors le chiffre par ménage face à un standard par personne sous-estime le problème. Indiquez la granularité dans le libellé.share_of_households_below_15letshare_of_people_below_15lne peuvent pas être confondus ;water_below_minimumle peut.- Indiquez la granularité dans le libellé —
Développer les ménages en personnes — En Python
people = wash.loc[wash["household_size"].notna()].copy() people["household_size"] = people["household_size"].astype(int) people = people.loc[people.index.repeat(people["household_size"])] people["person_index"] = people.groupby("household_id").cumcount() + 1 print(len(wash), "households ->", len(people), "people")Notes du présentateur
Il arrive qu'il faille réellement une ligne par personne — pour joindre à une structure par âge et sexe, pour alimenter une pondération individuelle, ou pour calculer un dénominateur de population à partir de l'enquête elle-même.Développer les ménages en personnes — En R
people <- wash |> filter(!is.na(household_size)) |> tidyr::uncount(household_size, .remove = FALSE, .id = "person_index") cat(nrow(wash), "households ->", nrow(people), "people\n")Développer les ménages en personnes
- C'est une affirmation, pas une transformation. Chaque personne du ménage est désormais supposée avoir l'accès à…
- Rien des individus n'est réel.
person_indexest une position, pas une personne. Ne désagrégez pas dessus, et ne… - Les quarante-six ménages sans taille enregistrée disparaissent. Dites-le, ou la population développée est…
Notes du présentateur
2 403 lignes deviennent 13 004. Trois choses à retenir de cette opération.Pondérer vaut généralement mieux que développer — En Python
sized["weight"] = sized["household_size"] weighted_share = ( (sized["litres_per_person_day"] < 15) * sized["weight"] ).sum() / sized["weight"].sum()Notes du présentateur
Le développement est gourmand en mémoire et facile à mal employer. Lorsque vous n'avez besoin que de statistiques pondérées par la population, pondérez.Pondérer vaut généralement mieux que développer — En R
sized |> summarise(share = weighted.mean(litres_per_person_day < 15, w = household_size))Notes du présentateur
Même réponse, une seule table, et la pondération est visible en tant que colonne — ce qui permet à un relecteur de voir sur quoi vous avez pondéré. Une table développée cache la pondération dans le nombre de lignes.Agrégez à la granularité avant de joindre — En Python
per_student = ( attendance.assign(present=attendance["present"].map({"true": True, "false": False})) .groupby("student_id") .agg(days_marked=("present", "size"), days_present=("present", "sum")) .reset_index() ) per_student["attendance_rate"] = per_student["days_present"] / per_student["days_marked"] # grain: one student. Now the join is one-to-one. summary = roster.merge(per_student, on="student_id", how="left", validate="one_to_one")Notes du présentateur
C'est la règle opérationnelle pour laquelle toute la leçon existe. Vous avez un fichier de présence journalière et vous voulez un résumé par élève joint à la liste. Le mauvais ordre consiste à joindre d'abord et agréger ensuite — cela fonctionne, mais fait traverser la jointure à 70 245 lignes et rend la relation plusieurs-à-un alors qu'elle n'avait pas à l'être.Agrégez à la granularité avant de joindre — En R
per_student <- attendance |> summarise( days_marked = sum(present %in% c(TRUE, FALSE)), days_present = sum(present %in% TRUE), .by = student_id ) |> mutate(attendance_rate = days_present / days_marked) summary <- roster |> left_join(per_student, by = "student_id", relationship = "one-to-one")Notes du présentateur
Deux gains, et c'est le second qui compte vraiment. La jointure est désormaisone_to_one, donc le contrôle le plus fort possible s'applique. Et l'agrégation est écrite là où l'on voit son dénominateur —days_markedcompte les jours où une marque existe, ce qui n'est pas la même chose que les jours d'ouverture de l'école, et l'avant-dernière leçon est entièrement consacrée à cette distinction.Le plusieurs-à-plusieurs que personne n'a choisi — En Python
# household file: one row per household. member file: one row per person. # Both carry `community`. Joining on it instead of household_id: bad = households.merge(members, on="community", how="inner")Notes du présentateur
Un plusieurs-à-plusieurs n'est presque jamais voulu. Il survient quand la clé n'est pas celle que vous croyiez.Le plusieurs-à-plusieurs que personne n'a choisi — En R
bad <- households |> inner_join(members, by = "community")Notes du présentateur
Chaque ménage d'une communauté s'apparie désormais à chaque personne de cette communauté. Quatre cents ménages de six personnes, dans une seule communauté, produisent 400 × 2 400 lignes — neuf cent soixante mille — à partir d'un fichier dont la granularité honnête est de 2 400 personnes. L'indice est toujours le même — un nombre de lignes qui est un produit et non une somme, et un total dont l'ordre de grandeur est suspicieusement rond.validate="one_to_many"l'attrape avant que vous ayez à le remarquer.Des épisodes, pas des personnes
- La charge de cas est un décompte d'épisodes. Deux admissions du même enfant sont deux admissions, et les rapporter…
- Les enfants atteints est un décompte d'enfants distincts, plus petit.
- Le taux de guérison a des épisodes au numérateur et au dénominateur, et mélanger un numérateur d'épisodes avec un…
Notes du présentateur
Une dernière granularité à nommer, car c'est là que dérapent les chiffres des programmes de traitement. Dans un registre PCIMA, une ligne est en général un épisode d'admission, pas un enfant. Un enfant réadmis après rechute a deux lignes. Ce qui implique ceci.Des épisodes, pas des personnes — En Python
print("episodes:", len(admissions)) print("children:", admissions["child_id"].nunique()) print("readmitted:", (admissions.groupby("child_id").size() > 1).sum())Des épisodes, pas des personnes — En R
c(episodes = nrow(admissions), children = n_distinct(admissions$child_id), readmitted = sum(count(admissions, child_id)$n > 1))Notes du présentateur
Publier les deux nombres, avec leurs libellés, met fin à la discussion avant qu'elle commence.Inscrivez la granularité dans le nom du fichier — Exemple
outputs/tables/attendance_by_student.csv outputs/tables/attendance_by_school_month.csv outputs/tables/coverage_by_facility_period_antigen.csvNotes du présentateur
Une dernière petite habitude qui rapporte. Quand vous enregistrez une table intermédiaire, mettez la granularité dans son nom. Six mois plus tard,summary.csvne vous dit rien et chacun de ceux-ci vous dit tout. Cela rend aussi une mauvaise jointure évidente au moment où quelqu'un lit deux noms de fichiers côte à côte.La suite
- La granularité répond à la question de ce qu'est une ligne.
Notes du présentateur
La granularité répond à la question de ce qu'est une ligne. La leçon suivante répond à celle de ce qu'est une colonne — le groupe répété aplati qu'une plateforme de collecte exporte ensymptom_1,symptom_2,symptom_3, et le pivot qui le retransforme en lignes que l'on peut compter.