cassionAnalyse de données

Leçon 8 sur 8

Unité · La table que vous analysez

Une seule table, assemblée à dessein

Partir d'une ossature des unités sur lesquelles vous devez rapporter, attacher une mesure à la fois, affirmer à chaque raccord, et porter des colonnes de provenance pour qu'un relecteur puisse remonter tout chiffre jusqu'à son fichier d'origine.

PythonR75 minGestion axée sur les résultats (GAR)Définitions d'indicateurs de l'UNICEF

La table dont tout le reste se calcule

À l’issue d’un assemblage, vous voulez exactement une table : une ligne par unité d’analyse, une colonne par mesure, et rien d’autre. Chaque graphique, chaque résumé et chaque chiffre du rapport en sortent.

Cette discipline compte parce que l’alternative est ce qui se produit d’ordinaire — un notebook de onze tableaux nommés df, df2, merged, merged_final et merged_final_v2, où la table dont un graphique a été tiré est celle qui se trouvait en mémoire à ce moment-là. Personne ne peut reproduire cela, y compris la personne qui l’a écrit.

Décidez l’unité d’analyse avant d’écrire une ligne

L’unité d’analyse est ce sur quoi votre rapport formule des affirmations. Écrivez-la d’abord, car elle détermine toutes les jointures qui suivent.

# unit of analysis: one school x month
# rows: 24 schools x 3 months = 72
# unit of analysis: one school x month
# rows: 24 schools x 3 months = 72

Si vous ne pouvez pas l’énoncer en une ligne, l’analyse n’est pas prête à être assemblée. Et si deux chiffres de votre rapport ont des unités différentes — l’un par école, l’autre par enfant — ils appartiennent à deux tables, pas à une.

Construisez d’abord l’ossature

L’ossature est l’ensemble complet des unités sur lesquelles vous devez rapporter, bâti à partir de l’autorité et non des données. Chaque mesure y est ensuite attachée.

schools = roster["school_id"].drop_duplicates()
months = pd.Series(["2024-02", "2024-03", "2024-04"], name="month")

analysis = (
    pd.MultiIndex.from_product([schools, months], names=["school_id", "month"])
    .to_frame(index=False)
)
print(len(analysis), "rows in the spine")
analysis <- tidyr::expand_grid(
  school_id = unique(roster$school_id),
  month = c("2024-02", "2024-03", "2024-04")
)

L’ossature d’abord est l’habitude la plus utile de cette leçon. Un couple école-mois sans données existe désormais comme une ligne aux mesures manquantes, ce qui est visible, au lieu de ne pas exister, ce qui ne l’est pas. C’est le même argument que la grille de périodes, appliqué à l’assemblage entier.

Attachez une mesure à la fois, et affirmez à chaque raccord

def attach(base, addition, on, name):
    before = len(base)
    out = base.merge(addition, on=on, how="left", validate="one_to_one")
    if len(out) != before:
        raise ValueError(f"{name}: row count changed {before} -> {len(out)}")
    filled = out[addition.columns.difference(on)].notna().any(axis=1).mean()
    print(f"{name}: attached, {filled:.1%} of spine rows matched")
    return out


analysis = attach(analysis, attendance_by_school_month, ["school_id", "month"], "attendance")
analysis = attach(analysis, enrolment_by_school, ["school_id"], "enrolment")
analysis = attach(analysis, feeding_by_school, ["school_id"], "feeding programme")
attach_measure <- function(base, addition, by, name) {
  before <- nrow(base)
  out <- dplyr::left_join(base, addition, by = by, relationship = "one-to-one")
  if (nrow(out) != before) {
    stop(sprintf("%s: row count changed %d -> %d", name, before, nrow(out)))
  }
  message(sprintf("%s: attached", name))
  out
}

analysis <- analysis |>
  attach_measure(attendance_by_school_month, c("school_id", "month"), "attendance") |>
  attach_measure(enrolment_by_school, "school_id", "enrolment")

Deux propriétés qui valent d’être acquises. Le nombre de lignes ne peut pas changer, donc un gonflement est impossible par construction. Et le taux d’appariement est affiché à chaque attachement — une table d’inscriptions appariée à 100 % de l’ossature et une table de présence appariée à 92 % vous disent immédiatement où sont les trous, avant qu’aucun chiffre ne soit calculé.

attach() a l’air d’un cérémonial jusqu’à la première fois où il se déclenche. Il se déclenchera.

Portez la provenance

Ajoutez des colonnes disant d’où viennent les choses et sur quoi elles reposent.

analysis["source_register"] = "school-attendance-2024.v1.csv"
analysis["denominator_source"] = "roster 2024, enrolled students"
analysis["marks_definition"] = "true/false only; Y/N excluded"
analysis["extracted_on"] = "2026-07-27"
analysis <- analysis |>
  mutate(
    source_register    = "school-attendance-2024.v1.csv",
    denominator_source = "roster 2024, enrolled students",
    marks_definition   = "true/false only; Y/N excluded",
    extracted_on       = "2026-07-27"
  )

Cela paraît redondant tant que chaque ligne porte la même valeur. Cela cesse de l’être dès que deux extractions sont concaténées, qu’un deuxième district arrive, ou que quelqu’un ouvre le CSV un an plus tard sans savoir de quelle version du registre il provient. Les colonnes constantes sont bon marché ; les nombres sans étiquette ne le sont pas.

Affirmez ce qui doit être vrai de la table finie

assert analysis.duplicated(subset=["school_id", "month"]).sum() == 0
assert analysis["attendance_rate"].between(0, 1).all()
assert (analysis["days_present"] <= analysis["days_marked"]).all()
assert len(analysis) == 24 * 3

print(f"analysis table: {len(analysis)} rows, "
      f"{analysis['attendance_rate'].isna().sum()} without an attendance rate")
stopifnot(
  !any(duplicated(analysis[c("school_id", "month")])),
  all(dplyr::between(analysis$attendance_rate, 0, 1), na.rm = TRUE),
  all(analysis$days_present <= analysis$days_marked, na.rm = TRUE),
  nrow(analysis) == 24 * 3
)

Quatre assertions, correspondant chacune à une leçon de ce cours — la granularité est unique, le taux est un taux, le numérateur ne peut pas dépasser son dénominateur, et l’ossature est complète. Exécutez-les à la fin de l’assemblage, à chaque fois.

Ce qui n’a pas sa place dans la table

  • Les colonnes intermédiaires. present_x, _merge, key_clean et les six colonnes nécessaires à un seul calcul. Supprimez-les ; c’est ainsi qu’une table devient illisible.
  • Les lignes sous votre seuil de rapportage. Si vous supprimeriez une cellule de quatre cas dans un tableau publié, ne la transportez pas dans un fichier qui circule par courriel.
  • Tout ce qui est au niveau de la personne. Une table d’analyse à la granularité école-mois n’a rien à faire d’un identifiant d’élève, et le jour où elle en porte un est le jour où elle ne peut plus être partagée.

Enregistrez-la avec sa granularité dans le nom

analysis.to_csv("outputs/tables/attendance_by_school_month.csv", index=False)
readr::write_csv(analysis, here::here("outputs", "tables", "attendance_by_school_month.csv"))

Et enregistrez le script d’assemblage à côté, car la table est le produit et le script est la méthode. Si régénérer la table demande plus d’une commande, la régénérer dans trois mois n’aura pas lieu.

scripts/
  01_read_and_validate.R      contract and validation   (cleaning course)
  02_clean.R                  rules and cleaning log    (cleaning course)
  03_assemble.R               spine, joins, assertions  (this course)
  04_report.R                 tables and figures

Quatre scripts, exécutés dans l’ordre, des fichiers bruts à la table finale. C’est l’organisation que Chaînes d’analyse reproductibles développera plus loin dans le programme ; l’adopter dès maintenant ne coûte rien et évite la reconstitution.

Ce que ce cours vous laisse

Vous savez prendre plusieurs fichiers décrivant le même programme et produire une table unique que vous défendriez colonne par colonne — jointures prouvées, granularité énoncée, dénominateur sourcé, calendrier complet, et l’écart avec ce qui a été remonté consigné plutôt que discuté.

Le dernier cours de ce module, Évaluation de la qualité des données, prend ce dernier point et en fait une routine — facteurs de vérification sur un échantillon de formations sanitaires, les cinq dimensions de la qualité, et un rapport qui nomme l’action corrective, le responsable et l’échéance, ce qui transforme un écart trouvé en un écart qui se referme.

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.