Retour à la leçon·Leçon 3 sur 8·Identité 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.
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.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 surstudent_id, et ce faisant vous avez affirmé quelque chose — questudent_ididentifie 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.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.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"))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 200student_iddistincts. 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.Une clé est une affirmation, et elle est testable — En R
roster |> group_by(student_id) |> filter(n() > 1) |> arrange(student_id)Une clé est une affirmation, et elle est testable
student_id school_id grade feeding_programme STU0150 SCH09 3 false STU0150 SCH08 3 true STU0896 SCH01 2 true STU0896 SCH06 2 false 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.Clés composites, et celle dont vous disposez vraiment — En Python
print(is_key(attendance, ["student_id", "attendance_date"]))Notes du présentateur
student_idn'est pas une clé de la liste.student_idplusschool_iden 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.Clés composites, et celle dont vous disposez vraiment — En R
is_key(attendance, c("student_id", "attendance_date"))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.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")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.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.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.Dites ce que vous attendez, et laissez la jointure échouer — En R
joined <- attendance |> left_join(roster, by = "student_id", relationship = "many-to-one")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 mergeNotes du présentateur
Les deux échouent bruyamment sur ces données.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. Mettezvalidate=ourelationship=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.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.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.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.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()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.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 BAPTISTEetJean Baptisteen 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 unmerge(on="name")suivi de rien.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.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) }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, placezassert_keyen tête de tout script qui joint quoi que ce soit.La suite
assert_keytrouve les élèves enregistrés deux fois sous le même identifiant.
Notes du présentateur
assert_keytrouve 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.