cassionAnalyse de données

Retour à la leçonLeçon 3 sur 8Identité et doublons

Prouvez la clé avant de joindre

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 / 28

    Ce que couvre cette leçon

    • L'hypothèse que rien ne vérifie
    • Une clé est une affirmation, et elle est testable
    • Clés composites, et celle dont vous disposez vraiment
    • Ce qu'une clé cassée fait à une jointure
    • Dites ce que vous attendez, et laissez la jointure échouer
    • Protégez le nombre de lignes quand la jointure est l'objectif
    • Les lignes d'un côté et pas de l'autre
    • Des colonnes qui n'étaient pas faites pour être des clés
    • Affirmez, ne regardez pas
    • La suite
    Notes du présentateur
    Toute jointure formule une hypothèse d'unicité que rien ne vérifie. La tester, voir deux lignes de liste dupliquées gonfler de 120 lignes une table de 70 245, et apprendre à affirmer le nombre de lignes plutôt qu'à le regarder.
  2. Diapositive 2 / 28

    L'hypothèse que rien ne vérifie

    • Vous avez une liste d'élèves et un fichier de présence journalière.
    Notes du présentateur
    Vous avez une liste d'élèves et un fichier de présence journalière. Vous les joignez sur student_id, et ce faisant vous avez affirmé quelque chose — que student_id identifie exactement une ligne de la liste. Rien dans aucun des deux fichiers ne le dit. Aucune erreur n'est levée si c'est faux. Ce qui se produit à la place, c'est que la jointure produit discrètement plus de lignes qu'elle n'en avait, que tous les décomptes en aval sont gonflés d'une quantité invisible, et que le taux de présence obtenu est faux d'un écart trop petit pour avoir l'air faux. Cette leçon consiste à tester cette hypothèse avant de la formuler.
  3. Diapositive 3 / 28

    Une clé est une affirmation, et elle est testable — En Python

    def is_key(df, columns):
        subset = df[columns]
        return {
            "rows": len(df),
            "distinct": len(subset.drop_duplicates()),
            "missing_any": int(subset.isna().any(axis=1).sum()),
            "is_key": len(subset.drop_duplicates()) == len(df)
                      and not subset.isna().any(axis=None),
        }
    
    
    print(is_key(roster, ["student_id"]))
    print(is_key(attendance, ["student_id", "attendance_date"]))
    Notes du présentateur
    L'affirmation a deux volets — unique et complète. Une colonne comportant des doublons n'est pas une clé. Une colonne comportant des valeurs manquantes n'en est pas une non plus, car une valeur manquante n'identifie rien.
  4. Diapositive 4 / 28

    Une clé est une affirmation, et elle est testable — En R

    is_key <- function(df, columns) {
      subset <- dplyr::select(df, dplyr::all_of(columns))
      list(
        rows        = nrow(df),
        distinct    = nrow(dplyr::distinct(subset)),
        missing_any = sum(!stats::complete.cases(subset)),
        is_key      = nrow(dplyr::distinct(subset)) == nrow(df) &&
                      !any(is.na(subset))
      )
    }
    
    is_key(roster, "student_id")
    is_key(attendance, c("student_id", "attendance_date"))
  5. Diapositive 5 / 28

    Une clé est une affirmation, et elle est testable — En Python

    repeated = roster[roster["student_id"].duplicated(keep=False)]
    print(repeated.sort_values("student_id"))
    Notes du présentateur
    Sur cette liste — 1 202 lignes, 1 200 student_id distincts. Ce n'est pas une clé, à deux lignes près. Deux lignes sur 1 202 font 0,17 % et cela n'a l'air de rien. Regardez ce qu'elles sont.
  6. Diapositive 6 / 28

    Une clé est une affirmation, et elle est testable — En R

    roster |>
      group_by(student_id) |>
      filter(n() > 1) |>
      arrange(student_id)
  7. Diapositive 7 / 28

    Une clé est une affirmation, et elle est testable

    student_idschool_idgradefeeding_programme
    STU0150SCH093false
    STU0150SCH083true
    STU0896SCH012true
    STU0896SCH062false
    Notes du présentateur
    Deux élèves ont changé d'école sans jamais être radiés de la première. Chacun figure désormais une fois dans une école avec cantine et une fois dans une école sans — et l'analyse phare de ce jeu de données porte précisément sur l'effet de l'alimentation scolaire sur la présence. Ces deux élèves apporteront tout leur historique de présence aux deux bras de la comparaison. Telle est la forme générale de ce défaut. Le doublon est rarement fortuit ; il est en général la trace de quelque chose qui a eu lieu — un transfert, une réinscription, un formulaire envoyé deux fois — et il atterrit en général exactement dans la variable sur laquelle porte l'analyse.
  8. Diapositive 8 / 28

    Clés composites, et celle dont vous disposez vraiment — En Python

    print(is_key(attendance, ["student_id", "attendance_date"]))
    Notes du présentateur
    student_id n'est pas une clé de la liste. student_id plus school_id en est une, puisque les deux lignes diffèrent par l'école. Savoir si c'est la clé que vous voulez est une autre question — si l'analyse suppose « une ligne par élève », une clé composite qui admet deux lignes par élève a documenté le problème, pas résolu. Le fichier de présence relève du cas plus courant, où la clé naturelle est composite dès le départ.
  9. Diapositive 9 / 28

    Clés composites, et celle dont vous disposez vraiment — En R

    is_key(attendance, c("student_id", "attendance_date"))
  10. Diapositive 10 / 28

    Clés composites, et celle dont vous disposez vraiment — En Python

    ROSTER_KEY = ["student_id"]          # intended; violated by 2 rows, see cleaning log
    ATTENDANCE_KEY = ["student_id", "attendance_date"]
    Notes du présentateur
    70 245 lignes, 70 245 couples distincts. Voilà une clé. Une ligne par élève et par jour de classe, exactement ce que le fichier prétend être. Prenez l'habitude d'écrire la clé dans le code, à côté de la lecture.
  11. Diapositive 11 / 28

    Clés composites, et celle dont vous disposez vraiment — En R

    ROSTER_KEY <- "student_id"           # intended; violated by 2 rows, see cleaning log
    ATTENDANCE_KEY <- c("student_id", "attendance_date")
  12. Diapositive 12 / 28

    Ce qu'une clé cassée fait à une jointure — En Python

    joined = attendance.merge(roster, on="student_id", how="left")
    print(len(attendance), "->", len(joined))
    Notes du présentateur
    L'arithmétique mérite d'être faite une fois à la main, car elle explique tous les gonflements que vous rencontrerez. Une jointure apparie chaque ligne de gauche à toutes les lignes de droite partageant la clé. Si un élève a 60 lignes de présence et 1 ligne de liste, la jointure produit 60 lignes. Si cet élève a 2 lignes de liste, elle en produit 120.
  13. Diapositive 13 / 28

    Ce qu'une clé cassée fait à une jointure — En R

    joined <- attendance |> left_join(roster, by = "student_id")
    cat(nrow(attendance), "->", nrow(joined), "\n")
    Notes du présentateur
    70 245 devient 70 365. Cent vingt lignes de plus — deux élèves, soixante jours de classe chacun, comptés deux fois. Une jointure à gauche qui ajoute des lignes est une contradiction dans les termes : « jointure à gauche » se lit d'ordinaire « garder la table de gauche et y accrocher des colonnes », et cette lecture n'est vraie que si le côté droit est unique sur la clé. Ce n'est pas un défaut de la jointure. C'est la jointure qui vous dit la vérité sur vos données, sous une forme que personne ne regarde.
  14. Diapositive 14 / 28

    Dites ce que vous attendez, et laissez la jointure échouer — En Python

    joined = attendance.merge(
        roster,
        on="student_id",
        how="left",
        validate="many_to_one",   # many attendance rows, one roster row
    )
    Notes du présentateur
    Les deux langages vérifient la relation pour vous. Servez-vous-en.
  15. Diapositive 15 / 28

    Dites ce que vous attendez, et laissez la jointure échouer — En R

    joined <- attendance |>
      left_join(roster, by = "student_id", relationship = "many-to-one")
  16. Diapositive 16 / 28

    Dites ce que vous attendez, et laissez la jointure échouer — Exemple

    MergeError: Merge keys are not unique in right dataset; not a many-to-one merge
    Notes du présentateur
    Les deux échouent bruyamment sur ces données.
  17. Diapositive 17 / 28

    Dites ce que vous attendez, et laissez la jointure échouer — Exemple

    Error in `left_join()`:
    ! Each row in `x` must match at most 1 row in `y`.
    i Row 1 of `x` matches multiple rows in `y`.
    Notes du présentateur
    Une erreur qu'il faut traiter aujourd'hui vaut mieux qu'un dénominateur de présence discrètement gonflé qu'on traitera en réunion de revue au mois de novembre. Mettez validate= ou relationship= sur chaque jointure que vous écrivez. Cela coûte un argument et transforme la classe de bugs silencieux la plus coûteuse en trace d'erreur.
  18. Diapositive 18 / 28

    Protégez le nombre de lignes quand la jointure est l'objectif — En Python

    before = len(attendance)
    joined = attendance.merge(roster, on="student_id", how="left")
    assert len(joined) == before, f"join changed row count: {before} -> {len(joined)}"
    Notes du présentateur
    Là où l'argument de relation ne convient pas — un plusieurs-à-plusieurs qui en est réellement un — affirmez directement le décompte.
  19. Diapositive 19 / 28

    Protégez le nombre de lignes quand la jointure est l'objectif — En R

    before <- nrow(attendance)
    joined <- attendance |> left_join(roster, by = "student_id")
    stopifnot(nrow(joined) == before)
    Notes du présentateur
    L'affirmation fait trois mots de plus que la jointure. Écrivez-la à chaque fois.
  20. Diapositive 20 / 28

    Les lignes d'un côté et pas de l'autre — En Python

    left_only = set(attendance["student_id"]) - set(roster["student_id"])
    right_only = set(roster["student_id"]) - set(attendance["student_id"])
    print(f"{len(left_only)} students with attendance and no roster row")
    print(f"{len(right_only)} students on the roster with no attendance")
    Notes du présentateur
    Une clé peut être unique et malgré tout ne pas s'apparier. Vérifiez les deux sens avant d'accepter le résultat d'une jointure.
  21. Diapositive 21 / 28

    Les lignes d'un côté et pas de l'autre — En R

    setdiff(attendance$student_id, roster$student_id) |> length()
    setdiff(roster$student_id, attendance$student_id) |> length()
  22. Diapositive 22 / 28

    Les lignes d'un côté et pas de l'autre

    • De la présence sans ligne de liste — un élève marqué présent sans être inscrit. Soit la liste est incomplète, soit…
    • Des lignes de liste sans aucune présence — des élèves inscrits jamais marqués, dans un sens ni dans l'autre. Dans…
    Notes du présentateur
    Ici les deux valent zéro, ce qui est la réponse souhaitée et qu'on n'obtient presque jamais. Quand ce n'est pas zéro, les deux sens veulent dire des choses totalement différentes. Une jointure interne fait disparaître les deux groupes et rapporte un chiffre propre. C'est pourquoi le contrôle précède la jointure et ne la suit pas.
  23. Diapositive 23 / 28

    Des colonnes qui n'étaient pas faites pour être des clés

    • Les doublons créent de fausses fusions. Deux ménages dont le chef s'appelle Jean Baptiste deviennent un seul ménage.
    • Les variantes créent de fausses scissions. Jean Baptiste, JEAN BAPTISTE et Jean Baptiste en deviennent trois.
    Notes du présentateur
    Noms, numéros de téléphone et noms de chefs de ménage servent constamment de clés, parce qu'ils sont la seule chose que deux fichiers ont en commun. Ce ne sont pas des clés, et le mode de défaillance est asymétrique. Si un nom est réellement le seul lien dont vous disposez, il s'agit d'un problème d'appariement d'enregistrements, pas d'une jointure, et la leçon suivante lui est entièrement consacrée. Ce qu'il ne faut surtout pas faire est un merge(on="name") suivi de rien.
  24. Diapositive 24 / 28

    Affirmez, ne regardez pas — En Python

    def assert_key(df, columns, name):
        duplicated = df.duplicated(subset=columns, keep=False)
        if duplicated.any():
            offenders = df.loc[duplicated, columns].drop_duplicates()
            raise ValueError(
                f"{name}: {duplicated.sum()} rows violate the key {columns}\n"
                f"{offenders.head(10)}"
            )
    Notes du présentateur
    Le motif que cette leçon enseigne réellement est plus petit que chacun de ses contrôles. Chacun d'eux peut s'écrire comme un affichage qu'on lit une fois, ou comme une assertion qui s'exécute sur chaque export à venir. Seule la seconde survit.
  25. Diapositive 25 / 28

    Affirmez, ne regardez pas — En R

    assert_key <- function(df, columns, name) {
      dup <- duplicated(df[columns]) | duplicated(df[columns], fromLast = TRUE)
      if (any(dup)) {
        stop(sprintf("%s: %d rows violate the key %s", name, sum(dup),
                     paste(columns, collapse = " + ")))
      }
      invisible(df)
    }
  26. Diapositive 26 / 28

    Affirmez, ne regardez pas

    Un contrôle exécuté une fois vous renseigne sur le fichier que vous aviez. Un contrôle exécuté à la lecture vous renseigne sur le fichier que vous avez.
    Notes du présentateur
    L'unité 4 transforme ce motif en une suite de validation. Pour l'instant, placez assert_key en tête de tout script qui joint quoi que ce soit.
  27. Diapositive 27 / 28

    La suite

    • assert_key trouve les élèves enregistrés deux fois sous le même identifiant.
    Notes du présentateur
    assert_key trouve les élèves enregistrés deux fois sous le même identifiant. Il ne peut pas trouver l'enfant réinscrit sous un nouvel identifiant, le ménage interrogé deux fois par deux enquêteurs, ni le bénéficiaire figurant sur trois listes de distribution avec trois graphies de son nom. Ceux-là ne partagent aucune clé, et la leçon suivante montre comment les retrouver sans fusionner deux personnes qui se ressemblent simplement.
  28. Diapositive 28 / 28

    La suite

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