Retour à la leçon·Leçon 6 sur 8·Faire coïncider les pièces
Les lignes qui n'ont jamais été écrites
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
- Une ligne absente n'a aucune valeur à avoir manquante
- Construire la grille
- Où sont passées les 1 755
- Absent n'est pas absent
- Une grille complète ne prouve pas que tout le monde a rapporté
- Des clés de période qui se trient
- Aligner un registre journalier sur un agrégat mensuel
- La suite
Notes du présentateur
Une ligne absente n'a aucune valeur à avoir manquante. Construire la grille que les données auraient dû remplir, retrouver où sont passées 1 755 lignes de présence, et comprendre pourquoi une grille complète ne prouve pas que tout le monde a rapporté.Une ligne absente n'a aucune valeur à avoir manquante
- Tous les contrôles du cours de nettoyage regardaient des valeurs — cases vides, sentinelles, nombres implausibles.
Notes du présentateur
Tous les contrôles du cours de nettoyage regardaient des valeurs — cases vides, sentinelles, nombres implausibles. Tous partagent une hypothèse : que la ligne est là. La défaillance traitée ici est d'une autre nature. Une école fermée n'a aucune ligne de présence. Une formation sanitaire qui n'a pas rapporté n'a aucune ligne dans l'extraction. Un mois sans distribution n'a rien du tout. Il n'y a aucune case vide à compter, aucunNAà détecter, et tous les résumés que vous calculez portent sur les périodes qui se trouvent avoir été enregistrées. Le remède a toujours la même forme — construire la grille des périodes qui devraient exister, y joindre les données, et laisser les trous apparaître sous forme de lignes.Construire la grille — En Python
students = roster["student_id"].drop_duplicates() school_days = attendance["attendance_date"].drop_duplicates() grid = pd.MultiIndex.from_product( [students, school_days], names=["student_id", "attendance_date"] ).to_frame(index=False) complete = grid.merge(attendance, on=["student_id", "attendance_date"], how="left") print(f"grid {len(grid):,}, actual {len(attendance):,}, " f"missing {len(grid) - len(attendance):,}")Construire la grille — En R
grid <- tidyr::expand_grid( student_id = unique(roster$student_id), attendance_date = unique(attendance$attendance_date) ) complete <- grid |> left_join(attendance, by = c("student_id", "attendance_date")) cat(sprintf("grid %d, actual %d, missing %d\n", nrow(grid), nrow(attendance), nrow(grid) - nrow(attendance)))Construire la grille — En R
complete <- attendance |> tidyr::complete(student_id, attendance_date)Notes du présentateur
1 200 élèves par 60 jours de classe font une grille de 72 000. Le fichier en contient 70 245. 1 755 lignes n'ont jamais été écrites. dplyr offre un chemin plus court lorsque la trame est déjà les données.complete()remplit le produit cartésien des valeurs qu'il trouve, ce qui est juste quand toutes les combinaisons devraient exister et faux quand un élève a rejoint l'école en cours de trimestre — il n'inventera pas de jours pour un élève que le fichier ne mentionne pas avant mars. Construisez la grille explicitement quand l'univers est défini hors des données, ce qui est le cas habituel.Où sont passées les 1 755 — En Python
missing = complete[complete["present"].isna()] by_school = ( missing.merge(roster[["student_id", "school_id"]], on="student_id") .groupby("school_id") .agg(missing_rows=("present", "size"), days=("attendance_date", "nunique")) ) print(by_school)Où sont passées les 1 755 — En R
complete |> filter(is.na(present)) |> left_join(select(roster, student_id, school_id), by = "student_id") |> summarise(missing_rows = n(), days = n_distinct(attendance_date), .by = school_id)Où sont passées les 1 755
school_id lignes manquantes jours SCH07 915 15 SCH18 840 15 Notes du présentateur
Deux écoles, quinze jours chacune, et 61 × 15 + 56 × 15 = 1 755. La grille rend compte de chacune des lignes manquantes, et sous une forme qui nomme la cause : deux écoles ont été fermées les mêmes quinze jours de mars. C'est le bénéfice. Avant la grille, les lignes manquantes étaient invisibles. Après, ce sont deux écoles et une plage de dates — une grève, une inondation, une période d'examens, quelque chose qu'un collègue peut confirmer en un coup de téléphone.Absent n'est pas absent — En Python
naive = complete["present"].fillna(False) print("attendance rate treating gaps as absences:", naive.mean())Notes du présentateur
Vient maintenant la décision, et c'est toute la raison d'être de cette leçon.Absent n'est pas absent — En R
complete |> summarise(rate = mean(coalesce(present, FALSE)))Absent n'est pas absent — En Python
open_days = attendance.merge(roster[["student_id", "school_id"]], on="student_id") school_calendar = open_days[["school_id", "attendance_date"]].drop_duplicates() grid = ( roster[["student_id", "school_id"]] .merge(school_calendar, on="school_id") ) print(len(grid), "student-days the schools were actually open")Notes du présentateur
Remplir les trous par « absent » fait ressembler SCH07 et SCH18 à une urgence d'abandon scolaire, puisqu'un quart de leur trimestre est désormais enregistré comme tous les enfants manquants tous les jours. Le traitement correct est l'inverse : ces jours n'étaient pas des jours de classe pour ces écoles, et ils n'ont leur place ni au numérateur ni au dénominateur.Absent n'est pas absent — En R
school_calendar <- attendance |> left_join(select(roster, student_id, school_id), by = "student_id") |> distinct(school_id, attendance_date) grid <- roster |> select(student_id, school_id) |> left_join(school_calendar, by = "school_id", relationship = "many-to-many")Absent n'est pas absent
- Construisez la grille par école, pas par district — L'univers des périodes est une propriété de l'unité de rapportage,…
Notes du présentateur
Construisez la grille par école, pas par district. L'univers des périodes est une propriété de l'unité de rapportage, et supposer un calendrier unique et partagé est la manière dont une fermeture devient une absence.Absent n'est pas absent
Décider si un trou est un zéro, une valeur manquante ou une période qui ne devrait pas exister n'est pas une question technique. C'est l'analyse, et cela a sa place dans le journal avec son motif.
Une grille complète ne prouve pas que tout le monde a rapporté — En Python
expected = ( vax["facility_id"].nunique() * vax["period"].nunique() * vax["antigen"].nunique() ) print(expected, "expected rows;", len(vax), "actual")Notes du présentateur
L'extraction vaccinale est le contre-exemple, et il compte parce qu'il ressemble au bon cas.Une grille complète ne prouve pas que tout le monde a rapporté — En R
c(expected = n_distinct(vax$facility_id) * n_distinct(vax$period) * n_distinct(vax$antigen), actual = nrow(vax))Une grille complète ne prouve pas que tout le monde a rapporté — En Python
coverage = ( vax[vax["report_submitted"]] .groupby(["period", "antigen"]) .agg(doses=("doses_administered", "sum"), target=("target_population", "sum")) ) reporting = vax.groupby(["period", "antigen"])["report_submitted"].mean()Notes du présentateur
38 × 12 × 6 = 2 736, et le fichier compte 2 736 lignes. La grille est parfaite. Et 642 de ces lignes portentreport_submittedàfalseet zéro dose. Le contrôle de grille passe donc, et les données restent pleines de trous — ce sont simplement des trous entourés d'une ligne. Deux contrôles distincts, et il vous faut les deux : chaque ligne attendue est-elle présente, et chaque ligne présente contient-elle un rapport.Une grille complète ne prouve pas que tout le monde a rapporté — En R
vax |> summarise( reporting_rate = mean(report_submitted), doses = sum(doses_administered[report_submitted]), target = sum(target_population[report_submitted]), .by = c(period, antigen) )Des clés de période qui se trient — En Python
vax["period"] = pd.to_datetime(vax["period"]) vax["month"] = vax["period"].dt.strftime("%Y-%m") # 2024-08, sorts correctlyNotes du présentateur
Un petit point mécanique qui cause des ennuis disproportionnés.Des clés de période qui se trient — En R
vax <- vax |> mutate(month = format(period, "%Y-%m"))Des clés de période qui se trient
- Le mois doit porter son année — Une grille de douze mois indexée sur
monthseul fusionne silencieusement août 2023 et…
Notes du présentateur
Employez des clés de période ISO —2024-08,2024-Q3,2024-W32— partout où une période sert de clé.Aug,Augustet08/2024se trient mal, et08/2024est ambigu entre deux conventions toutes deux d'usage quotidien dans ce secteur. Le mois doit porter son année. Une grille de douze mois indexée surmonthseul fusionne silencieusement août 2023 et août 2024 dès que deux années de données se retrouvent dans le même dossier.- Le mois doit porter son année — Une grille de douze mois indexée sur
Aligner un registre journalier sur un agrégat mensuel — En Python
monthly = ( attendance.assign(month=attendance["attendance_date"].dt.strftime("%Y-%m")) .merge(roster[["student_id", "school_id"]], on="student_id") .groupby(["school_id", "month"]) .agg(marks=("present", "size"), present=("present", "sum")) .reset_index() )Notes du présentateur
La dernière forme de cette leçon, et celle sur laquelle la suivante s'appuie. Pour comparer un registre ligne à ligne à une remontée mensuelle, agrégez le registre à la granularité de l'agrégat — jamais l'inverse.Aligner un registre journalier sur un agrégat mensuel — En R
monthly <- attendance |> mutate(month = format(attendance_date, "%Y-%m")) |> left_join(select(roster, student_id, school_id), by = "student_id") |> summarise(marks = n(), present = sum(present %in% TRUE), .by = c(school_id, month))Aligner un registre journalier sur un agrégat mensuel — En Python
last = monthly["month"].max() print(f"excluding partial period {last}") monthly = monthly[monthly["month"] < last]Notes du présentateur
Deux choses à remarquer, car ce sont deux choix. Le mois auquel appartient un enregistrement vient de la date de l'événement, non de la date de soumission du formulaire — une remontée tardive appartient toujours au mois qu'elle décrit. Et le dernier mois du fichier est très souvent partiel, si bien qu'un graphique de tendance qui l'inclut paraît toujours en baisse.Aligner un registre journalier sur un agrégat mensuel — En R
monthly <- monthly |> filter(month < max(month))Notes du présentateur
Écartez-le ou signalez-le, mais ne le tracez jamais en silence à côté de mois complets.La suite
- Vous disposez maintenant d'un registre agrégé à la même granularité que la remontée mensuelle.
Notes du présentateur
Vous disposez maintenant d'un registre agrégé à la même granularité que la remontée mensuelle. La leçon suivante met les deux côte à côte, cherche pourquoi ils divergent, et transforme l'écart en un tableau publiable plutôt qu'en une discussion à gagner.