cassionAnalyse de données

Retour à la leçonLeçon 2 sur 8Des jointures démontrables

Prouver que la jointure a fait ce que vous avez annoncé

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

    • Le contrôle doit coûter moins cher que le bug
    • Déclarez la relation, toujours
    • Le tableau de rapprochement
    • Protégez le nombre de lignes quand vous vouliez le préserver
    • Le taux de non-appariement est un indicateur, pas une erreur
    • Normalisez la clé avant d'accuser la jointure
    • La collision de suffixes
    • Des clés aux noms différents
    • La suite
    Notes du présentateur
    Un tableau de rapprochement pour chaque jointure — lignes appariées, les deux comptes d'anti-jointure, le nombre de lignes avant et après — plus la collision de suffixes qui écrase une colonne sans rien dire.
  2. Diapositive 2 / 27

    Le contrôle doit coûter moins cher que le bug

    • Personne ne saute les contrôles de jointure en croyant les jointures sûres.
    Notes du présentateur
    Personne ne saute les contrôles de jointure en croyant les jointures sûres. On les saute parce que vérifier prend quatre commandes, qu'il faut lire la sortie, et que la jointure a « manifestement » fonctionné. Le contrôle doit donc devenir un seul appel produisant un seul petit tableau. Écrivez-le une fois, placez-le dans le fichier que tous vos scripts importent, et le coût de la vérification passe sous celui du doute.
  3. Diapositive 3 / 27

    Déclarez la relation, toujours — En Python

    joined = attendance.merge(
        roster, on="student_id", how="left", validate="many_to_one",
    )
    Notes du présentateur
    La première ligne de défense est un argument que vous pouviez déjà passer.
  4. Diapositive 4 / 27

    Déclarez la relation, toujours — En R

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

    Déclarez la relation, toujours

    Ce que vous attendezpandas validate=dplyr relationship=
    Une ligne de chaque côtéone_to_oneone-to-one
    Plusieurs à gauche, une à droitemany_to_onemany-to-one
    Une à gauche, plusieurs à droiteone_to_manyone-to-many
    Réellement plusieurs à plusieursomettremany-to-many
    Notes du présentateur
    Quatre valeurs, et choisir entre elles vous oblige à dire ce que vous croyez.
  6. Diapositive 6 / 27

    Déclarez la relation, toujours

    • dplyr avertit d'un plusieurs-à-plusieurs inattendu même sans l'argument — pandas non
    Notes du présentateur
    dplyr avertit d'un plusieurs-à-plusieurs inattendu même sans l'argument ; pandas non. Cette différence a décidé du sort d'un certain nombre de chiffres discrètement faux, et c'est la raison pour laquelle les exemples Python de ce cours passent toujours validate=. Notez la dernière ligne. Passer many-to-many explicitement n'est pas une façon de faire taire le contrôle — c'est l'affirmation que le gonflement est voulu, et cela va de pair avec un commentaire disant quelle est la granularité obtenue.
  7. Diapositive 7 / 27

    Le tableau de rapprochement — En Python (suite)

    def reconcile(left, right, on, left_name="left", right_name="right"):
        left_keys = left[on].drop_duplicates()
        right_keys = right[on].drop_duplicates()
        merged = left_keys.merge(right_keys, on=on, how="outer", indicator=True)
        counts = merged["_merge"].value_counts()
    
        return pd.Series({
            f"{left_name} rows": len(left),
            f"{right_name} rows": len(right),
            "keys matched": int(counts.get("both", 0)),
            f"{left_name} only": int(counts.get("left_only", 0)),
            f"{right_name} only": int(counts.get("right_only", 0)),
            f"{left_name} unmatched %": round(
                100 * counts.get("left_only", 0) / max(len(left_keys), 1), 2
            ),
        })
    Notes du présentateur
    L'argument de relation vous dit que la forme était bonne. Il ne vous dit pas ce qui n'a pas trouvé de partenaire, et c'est en général la question la plus intéressante.
  8. Diapositive 8 / 27

    Le tableau de rapprochement — En Python (suite)

    
    
    print(reconcile(attendance, roster, ["student_id"], "attendance", "roster"))
  9. Diapositive 9 / 27

    Le tableau de rapprochement — En R (suite)

    reconcile <- function(left, right, on, left_name = "left", right_name = "right") {
      left_keys  <- dplyr::distinct(dplyr::select(left, dplyr::all_of(on)))
      right_keys <- dplyr::distinct(dplyr::select(right, dplyr::all_of(on)))
    
      tibble::tibble(
        measure = c(paste(left_name, "rows"), paste(right_name, "rows"),
                    "keys matched", paste(left_name, "only"), paste(right_name, "only")),
        value = c(
          nrow(left), nrow(right),
          nrow(dplyr::inner_join(left_keys, right_keys, by = on)),
          nrow(dplyr::anti_join(left_keys, right_keys, by = on)),
          nrow(dplyr::anti_join(right_keys, left_keys, by = on))
        )
      )
    }
    
  10. Diapositive 10 / 27

    Le tableau de rapprochement — En R (suite)

    reconcile(attendance, roster, "student_id", "attendance", "roster")
  11. Diapositive 11 / 27

    Le tableau de rapprochement

    mesurevaleur
    lignes de présence70 245
    lignes de liste1 200
    clés appariées1 200
    présence seulement0
    liste seulement0
    Notes du présentateur
    Remarquez qu'il rapproche les clés distinctes, pas les lignes. Comparer des nombres de lignes de part et d'autre d'une jointure un-à-plusieurs n'apprend rien ; comparer les ensembles de clés dit exactement qui est d'un côté et pas de l'autre. Voilà le tableau à coller dans une cellule au-dessus de chaque jointure. Quatre secondes de lecture, et les deux modes de défaillance silencieux deviennent impossibles à manquer.
  12. Diapositive 12 / 27

    Protégez le nombre de lignes quand vous vouliez le préserver — En Python

    def join_preserving(left, right, on, how="left", **kwargs):
        before = len(left)
        out = left.merge(right, on=on, how=how, **kwargs)
        if len(out) != before:
            raise ValueError(f"join changed row count: {before} -> {len(out)}")
        return out
  13. Diapositive 13 / 27

    Protégez le nombre de lignes quand vous vouliez le préserver — En R

    join_preserving <- function(left, right, by, ...) {
      before <- nrow(left)
      out <- dplyr::left_join(left, right, by = by, ...)
      if (nrow(out) != before) {
        stop(sprintf("join changed row count: %d -> %d", before, nrow(out)))
      }
      out
    }
    Notes du présentateur
    C'est strictement plus fort que validate=, car cela attrape aussi le cas où la clé est unique des deux côtés mais où la table de gauche a perdu des lignes — ce qui arrive dès que quelqu'un remplace how="left" par how="inner" en déboguant autre chose et oublie de revenir en arrière.
  14. Diapositive 14 / 27

    Le taux de non-appariement est un indicateur, pas une erreur — En Python

    summary = reconcile(distributions, registration, ["beneficiary_id"])
    if summary["left unmatched %"] > 5:
        print(f"WARNING: {summary['left unmatched %']}% of distribution rows "
              "have no registration record")
    Notes du présentateur
    Une jointure qui n'apparie pas 6 % d'une liste de bénéficiaires vous dit quelque chose sur l'enregistrement, pas sur votre code. Rapportez-le.
  15. Diapositive 15 / 27

    Le taux de non-appariement est un indicateur, pas une erreur — En R

    unmatched <- nrow(anti_join(distributions, registration, by = "beneficiary_id"))
    share <- unmatched / nrow(distributions)
    if (share > 0.05) warning(sprintf("%.1f%% of distributions have no registration", 100 * share))
    Notes du présentateur
    Suivez ce nombre d'un tour à l'autre et il devient une tendance de qualité des données qui a sa place dans un rapport mensuel — le même geste que la série de constats de validation du cours de nettoyage. Un taux d'appariement qui tombe de 98 % à 91 % entre deux tours signale un processus d'enregistrement qui a changé, et il est bien plus utile de le savoir en mars que de le découvrir lors d'un audit.
  16. Diapositive 16 / 27

    Normalisez la clé avant d'accuser la jointure

    • Le type. Un code de formation sanitaire lu comme entier d'un côté et comme chaîne de l'autre. 1042 n'égale jamais…
    • Espaces et casse. "FAC001 " et "fac001" font trois clés différentes.
    • Dates contre horodatages. 2024-03-01 et 2024-03-01 00:00:00 se comparent égaux dans certaines bibliothèques et…
    Notes du présentateur
    Une large part des « la jointure n'a pas marché » vient d'une clé qui ne se compare pas égale.
  17. Diapositive 17 / 27

    Normalisez la clé avant d'accuser la jointure — En Python

    def key_clean(series):
        return series.astype("string").str.strip().str.upper()
    
    
    for frame in (registers, aggregates):
        frame["facility_id"] = key_clean(frame["facility_id"])
  18. Diapositive 18 / 27

    Normalisez la clé avant d'accuser la jointure — En R

    key_clean <- function(x) toupper(stringr::str_squish(as.character(x)))
    
    registers  <- registers  |> mutate(facility_id = key_clean(facility_id))
    aggregates <- aggregates |> mutate(facility_id = key_clean(facility_id))
  19. Diapositive 19 / 27

    Normalisez la clé avant d'accuser la jointure

    • Faites-le sur les deux côtés dans la même fonction — pour qu'ils ne puissent pas diverger
    Notes du présentateur
    Faites-le sur les deux côtés dans la même fonction, pour qu'ils ne puissent pas diverger. Deux expressions de nettoyage séparées, c'est le même défaut que deux calculs d'indicateur séparés.
  20. Diapositive 20 / 27

    La collision de suffixes — En Python

    merged = registers.merge(aggregates, on="facility_id", suffixes=("_register", "_dhis2"))
    Notes du présentateur
    Les deux tables ont une colonne nommée updated_at, ou district, ou notes. La jointure n'échoue pas, elle renomme.
  21. Diapositive 21 / 27

    La collision de suffixes — En R

    merged <- registers |>
      left_join(aggregates, by = "facility_id", suffix = c("_register", "_dhis2"))
  22. Diapositive 22 / 27

    La collision de suffixes — En Python

    overlap = (set(registers.columns) & set(aggregates.columns)) - {"facility_id"}
    print("columns in both:", sorted(overlap))
    Notes du présentateur
    Les deux bibliothèques ont un défaut peu utile — _x et _y en pandas, .x et .y en dplyr — et trois jointures plus loin, plus personne ne peut dire si district_x vient du registre ou de l'agrégat. Nommez les suffixes d'après la source, à chaque fois. Cela coûte un argument et fait la différence entre une table traçable et une supposition. Pire encore, une colonne partagée dont vous ignoriez l'existence, transportée en silence par la jointure puis utilisée. Vérifiez avant de joindre.
  23. Diapositive 23 / 27

    La collision de suffixes — En R

    setdiff(intersect(names(registers), names(aggregates)), "facility_id")
  24. Diapositive 24 / 27

    Des clés aux noms différents — En Python

    merged = cases.merge(
        facilities, left_on="site_code", right_on="facility_id", how="left",
        validate="many_to_one",
    )
    Notes du présentateur
    Dites-le explicitement plutôt que de renommer une colonne pour faire marcher la jointure — le renommage survit à la jointure et égare le lecteur suivant.
  25. Diapositive 25 / 27

    Des clés aux noms différents — En R

    merged <- cases |>
      left_join(facilities, by = c("site_code" = "facility_id"),
                relationship = "many-to-one")
  26. Diapositive 26 / 27

    La suite

    • Tous les contrôles vus jusqu'ici supposent que vous savez ce qu'est une ligne de chaque table.
    Notes du présentateur
    Tous les contrôles vus jusqu'ici supposent que vous savez ce qu'est une ligne de chaque table. C'est dans cette supposition que logent les défaillances restantes — une table de ménages jointe à une table de personnes, ou un agrégat mensuel joint à un registre journalier. La leçon suivante porte sur la désignation de la granularité, et sur l'agrégation à cette granularité avant la jointure plutôt que sur la découverte après coup d'une multiplication.
  27. Diapositive 27 / 27

    La suite

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