cassionAnalyse de données

Retour à la leçonLeçon 3 sur 8Une 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.

Diapositives · PDFDiapositives · PowerPoint

  1. Diapositive 1 / 21

    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é.
  2. Diapositive 2 / 21

    É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.
  3. Diapositive 3 / 21

    É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.
  4. Diapositive 4 / 21

    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.
  5. Diapositive 5 / 21

    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)
      )
  6. Diapositive 6 / 21

    La granularité décide du sens du nombre

    • Indiquez la granularité dans le libellé — share_of_households_below_15l et share_of_people_below_15l ne 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_15l et share_of_people_below_15l ne peuvent pas être confondus ; water_below_minimum le peut.
  7. Diapositive 7 / 21

    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.
  8. Diapositive 8 / 21

    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")
  9. Diapositive 9 / 21

    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_index est 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.
  10. Diapositive 10 / 21

    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.
  11. Diapositive 11 / 21

    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.
  12. Diapositive 12 / 21

    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.
  13. Diapositive 13 / 21

    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ésormais one_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_marked compte 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.
  14. Diapositive 14 / 21

    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.
  15. Diapositive 15 / 21

    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.
  16. Diapositive 16 / 21

    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.
  17. Diapositive 17 / 21

    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())
  18. Diapositive 18 / 21

    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.
  19. Diapositive 19 / 21

    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.csv
    Notes 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.csv ne 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.
  20. Diapositive 20 / 21

    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 en symptom_1, symptom_2, symptom_3, et le pivot qui le retransforme en lignes que l'on peut compter.
  21. Diapositive 21 / 21

    La suite

    Lire la leçon complète, avec le code exécutable Retour à la leçon