Retour à la leçon·Leçon 2 sur 8·Des 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.
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.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.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.Déclarez la relation, toujours — En R
joined <- attendance |> left_join(roster, by = "student_id", relationship = "many-to-one")Déclarez la relation, toujours
Ce que vous attendez pandas validate=dplyr relationship=Une ligne de chaque côté one_to_oneone-to-onePlusieurs à gauche, une à droite many_to_onemany-to-oneUne à gauche, plusieurs à droite one_to_manyone-to-manyRéellement plusieurs à plusieurs omettre many-to-manyNotes du présentateur
Quatre valeurs, et choisir entre elles vous oblige à dire ce que vous croyez.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 toujoursvalidate=. Notez la dernière ligne. Passermany-to-manyexplicitement 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.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.Le tableau de rapprochement — En Python (suite)
print(reconcile(attendance, roster, ["student_id"], "attendance", "roster"))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)) ) ) }Le tableau de rapprochement — En R (suite)
reconcile(attendance, roster, "student_id", "attendance", "roster")Le tableau de rapprochement
mesure valeur lignes de présence 70 245 lignes de liste 1 200 clés appariées 1 200 présence seulement 0 liste seulement 0 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.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 outProté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 quevalidate=, 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 remplacehow="left"parhow="inner"en déboguant autre chose et oublie de revenir en arrière.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.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.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.
1042n'égale jamais… - Espaces et casse.
"FAC001 "et"fac001"font trois clés différentes. - Dates contre horodatages.
2024-03-01et2024-03-01 00:00:00se 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.- Le type. Un code de formation sanitaire lu comme entier d'un côté et comme chaîne de l'autre.
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"])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))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.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éeupdated_at, oudistrict, ounotes. La jointure n'échoue pas, elle renomme.La collision de suffixes — En R
merged <- registers |> left_join(aggregates, by = "facility_id", suffix = c("_register", "_dhis2"))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 —_xet_yen pandas,.xet.yen dplyr — et trois jointures plus loin, plus personne ne peut dire sidistrict_xvient 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.La collision de suffixes — En R
setdiff(intersect(names(registers), names(aggregates)), "facility_id")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.Des clés aux noms différents — En R
merged <- cases |> left_join(facilities, by = c("site_code" = "facility_id"), relationship = "many-to-one")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.