cassionAnalyse de données

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

Diapositives · PDFDiapositives · PowerPoint

  1. Diapositive 1 / 27

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

    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, aucun NA à 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.
  3. Diapositive 3 / 27

    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):,}")
  4. Diapositive 4 / 27

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

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

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

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

    Où sont passées les 1 755

    school_idlignes manquantesjours
    SCH0791515
    SCH1884015
    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.
  9. Diapositive 9 / 27

    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.
  10. Diapositive 10 / 27

    Absent n'est pas absent — En R

    complete |> summarise(rate = mean(coalesce(present, FALSE)))
  11. Diapositive 11 / 27

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

    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")
  13. Diapositive 13 / 27

    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.
  14. Diapositive 14 / 27

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

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

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

    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 portent report_submitted à false et 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.
  18. Diapositive 18 / 27

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

    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 correctly
    Notes du présentateur
    Un petit point mécanique qui cause des ennuis disproportionnés.
  20. Diapositive 20 / 27

    Des clés de période qui se trient — En R

    vax <- vax |> mutate(month = format(period, "%Y-%m"))
  21. Diapositive 21 / 27

    Des clés de période qui se trient

    • Le mois doit porter son année — Une grille de douze mois indexée sur month seul 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, August et 08/2024 se trient mal, et 08/2024 est 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 sur month seul 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.
  22. Diapositive 22 / 27

    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.
  23. Diapositive 23 / 27

    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))
  24. Diapositive 24 / 27

    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.
  25. Diapositive 25 / 27

    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.
  26. Diapositive 26 / 27

    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.
  27. Diapositive 27 / 27

    La suite

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