Retour à la leçon·Leçon 4 sur 8·Identité et doublons
La même personne, deux fois, sous deux identifiants
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 doublon sans clé
- Commencez par les attributs qui ne devraient pas coïncider
- Regardez les faux avant les vrais
- Le blocage, et pourquoi on ne peut pas s'en passer
- Normalisez avant de comparer
- Distance d'édition et choix d'un seuil
- Notez plusieurs champs, n'enchaînez pas les filtres
- Le produit est une file de relecture, pas un tableau propre
- Ce qu'il ne faut pas automatiser
- La suite
Notes du présentateur
L'appariement d'enregistrements quand rien ne joint — blocage, normalisation, distance d'édition et score. Douze paires candidates dans le registre de dépistage, six réelles, et quatre fabriquées par les âges manquants de la leçon 2.Le doublon sans clé
- Un enfant dépisté le matin, renvoyé chez lui, puis ramené l'après-midi par un autre accompagnant est enregistré deux fois sous deux identifiants.
Notes du présentateur
Un enfant dépisté le matin, renvoyé chez lui, puis ramené l'après-midi par un autre accompagnant est enregistré deux fois sous deux identifiants. De même pour un ménage visité par deux enquêteurs dans la même rue. De même pour un bénéficiaire figurant sur trois listes de distribution sous trois graphies du même nom. Aucun de ces cas ne partage de clé.duplicated()ne les trouvera jamais. Et ils comptent — dans une charge de cas ils gonflent le numérateur, dans un taux de couverture ils gonflent le numérateur sans le dénominateur, et dans un décompte de bénéficiaires ils sont précisément ce qu'un audit cherche. Les retrouver relève de l'appariement d'enregistrements, discipline différente du dédoublonnage. Le dédoublonnage supprime des lignes identiques. L'appariement décide si deux lignes qui ne sont pas identiques décrivent la même chose — et il peut se tromper dans les deux sens, raison pour laquelle le produit de cette leçon est une liste à faire relire par un humain, et non un tableau nettoyé.Commencez par les attributs qui ne devraient pas coïncider — En Python
CANDIDATE_KEY = ["commune", "screening_date", "age_months", "sex", "muac_mm"] candidates = ( muac.groupby(CANDIDATE_KEY, dropna=False) .filter(lambda g: g["child_id"].nunique() > 1) .sort_values(CANDIDATE_KEY) ) print(candidates[["child_id"] + CANDIDATE_KEY].to_string(index=False))Notes du présentateur
Le registre de dépistage n'a ni nom ni ménage. Il a une combinaison qui devrait être quasi unique par accident — commune, date, âge, sexe et mesure.Commencez par les attributs qui ne devraient pas coïncider — En R
CANDIDATE_KEY <- c("commune", "screening_date", "age_months", "sex", "muac_mm") candidates <- muac |> group_by(across(all_of(CANDIDATE_KEY))) |> filter(n_distinct(child_id) > 1) |> ungroup() |> arrange(across(all_of(CANDIDATE_KEY)))Notes du présentateur
Douze groupes reviennent. Six sont le même enfant réinscrit sous un nouvel identifiant. Six sont deux enfants différents qui se trouvent coïncider.Regardez les faux avant les vrais
Commune Date Âge Sexe PB Identifiants Desdunes 2024-01-20 36 f 134 CH03315, CH09241 Gros-Morne 2024-06-10 manquant f 131 CH00576, CH02413 Gros-Morne 2024-06-10 manquant m 123 CH01302, CH04050 Gros-Morne 2024-06-13 manquant m -99 CH00519, CH00755 Gros-Morne 2024-06-14 manquant m 137 CH01558, CH02913 Verrettes 2024-11-09 26 f 127 CH02193, CH03129 Regardez les faux avant les vrais
- Une colonne de blocage comportant des valeurs manquantes fabrique des appariements — Excluez les lignes manquantes du…
Notes du présentateur
Quatre des douze viennent de Gros-Morne, la semaine du 10 juin, et toutes ont un âge manquant. C'est la semaine dont le formulaire était mal configuré — la non-réponse de la leçon 2 qui revient dans une autre leçon sous un autre déguisement. Voici pourquoi. Regrouper sur une colonne manquante fait s'apparier chaque ligne manquante avec toutes les autres sur cette colonne. Le bloc s'effondre : au lieu de comparer des enfants du même âge, vous comparez tous les enfants d'âge inconnu. Dans une commune qui dépiste quatre-vingt-cinq enfants en une semaine, deux garçons ayant le même PB au millimètre n'a rien de surprenant. Une colonne de blocage comportant des valeurs manquantes fabrique des appariements. Excluez les lignes manquantes du bloc ou bloquez sur autre chose — ne laissez jamais un trou compter comme un accord.Regardez les faux avant les vrais — En Python
blocked = muac[muac["age_months"].notna() & muac["muac_mm"].notna()]Regardez les faux avant les vrais — En R
blocked <- muac |> filter(!is.na(age_months), !is.na(muac_mm))Notes du présentateur
Faites-le et huit candidats subsistent — les six réinscriptions réelles et deux coïncidences authentiques à Verrettes et Saint-Marc. Huit, c'est une liste qu'un superviseur peut confronter au registre papier en une après-midi. Douze, dont quatre absurdes, c'est une liste qui apprend aux gens à ignorer les listes.Le blocage, et pourquoi on ne peut pas s'en passer — En Python
blocks = blocked.groupby(["commune", "sex"]) print(sum(len(g) * (len(g) - 1) // 2 for _, g in blocks), "pairs to compare")Notes du présentateur
Comparer chaque ligne à toutes les autres est quadratique. Sur 4 218 enregistrements cela fait 8,9 millions de paires — lent mais supportable. Sur un registre de 300 000 bénéficiaires, cela fait 45 milliards, ce qui ne l'est pas. Le blocage consiste à ne comparer que les enregistrements déjà d'accord sur quelque chose de simple et fiable — la commune, le site de distribution, le mois, la première lettre du nom de famille. Les comparaisons coûteuses n'ont ensuite lieu qu'à l'intérieur de chaque bloc.Le blocage, et pourquoi on ne peut pas s'en passer — En R
blocked |> count(commune, sex) |> summarise(pairs = sum(n * (n - 1) / 2))Notes du présentateur
L'arbitrage est explicite : un bloc trop grossier est lent, et un bloc trop fin rate les paires en désaccord sur la colonne de blocage. Deux graphies d'un nom de village placées dans des blocs différents ne seront jamais comparées. C'est pourquoi la colonne de blocage doit être celle en laquelle vous avez le plus confiance, et pourquoi bloquer sur un champ en texte libre est généralement une erreur.Normalisez avant de comparer — En Python (suite)
import re import unicodedata def normalise(series): return ( series.fillna("") .str.normalize("NFKD") .str.encode("ascii", "ignore").str.decode("ascii") .str.lower() .str.replace(r"[^a-z0-9 ]", " ", regex=True) .str.replace(r"\s+", " ", regex=True) .str.strip() )Notes du présentateur
Pour tout ce qui relève du texte, l'essentiel du travail se fait avant le calcul de la moindre distance.Normalisez avant de comparer — En Python (suite)
households["head_key"] = normalise(households["head_of_household"])Normalisez avant de comparer — En R
normalise <- function(x) { x |> tidyr::replace_na("") |> stringi::stri_trans_general("Latin-ASCII") |> tolower() |> stringr::str_replace_all("[^a-z0-9 ]", " ") |> stringr::str_squish() } households$head_key <- normalise(households$head_of_household)Normalisez avant de comparer
- Ne retirez les accents que pour la clé de comparaison, jamais dans les données conservées —
Étienneest le nom de la…
Notes du présentateur
Casse, accents, espaces doubles et ponctuation expliquent la grande majorité des noms « différents » dans les données de ce secteur. Retirez-les et une part étonnante de votre problème d'appariement approximatif devient de l'appariement exact. Ne retirez les accents que pour la clé de comparaison, jamais dans les données conservées.Étienneest le nom de la personne.etienneest un index.- Ne retirez les accents que pour la clé de comparaison, jamais dans les données conservées —
Distance d'édition et choix d'un seuil — En Python
from rapidfuzz import fuzz, process matches = process.extract( "jean baptiste pierre", households["head_key"].tolist(), scorer=fuzz.token_sort_ratio, score_cutoff=88, limit=10, )Notes du présentateur
Pour ce qui survit à la normalisation, comparez avec une distance entre chaînes. N'importe laquelle des distances usuelles convient ; ce qui compte est de choisir un seuil délibérément et de dire lequel.Distance d'édition et choix d'un seuil — En R
scores <- stringdist::stringsim( "jean baptiste pierre", households$head_key, method = "jw" ) which(scores > 0.88)Notes du présentateur
token_sort_ratioet Jaro-Winkler sont deux valeurs par défaut raisonnables ici, pour une raison qu'il vaut la peine de connaître — dans ce secteur les noms arrivent avec leurs éléments permutés (Pierre Jean BaptistecontreJean Baptiste Pierre) et avec une faute de frappe bien plus rarement dans les premiers caractères que dans les derniers. Le seuil est un curseur entre précision et rappel, pas un réglage. À 95 vous obtenez peu de paires et manquez de vrais doublons. À 80 vous en obtenez beaucoup, la plupart fausses, et le relecteur cesse de lire. Réglez-le en prenant un échantillon de cinquante paires à votre seuil proposé et en vérifiant combien sont réelles. Inscrivez le seuil retenu et le taux de justesse mesuré dans le journal de nettoyage.Notez plusieurs champs, n'enchaînez pas les filtres — En Python
def pair_score(a, b): name = fuzz.token_sort_ratio(a["head_key"], b["head_key"]) / 100 same_site = 1.0 if a["community"] == b["community"] else 0.0 size_gap = abs(a["household_size"] - b["household_size"]) size = max(0.0, 1 - size_gap / 5) return 0.6 * name + 0.25 * same_site + 0.15 * sizeNotes du présentateur
Un seul champ ne suffit jamais. Comparez-en une poignée et combinez-les, pour qu'un accord fort sur un champ compense un accord faible sur un autre.Notez plusieurs champs, n'enchaînez pas les filtres — En R
pair_score <- function(a, b) { name <- stringdist::stringsim(a$head_key, b$head_key, method = "jw") same_site <- as.numeric(a$community == b$community) size <- pmax(0, 1 - abs(a$household_size - b$household_size) / 5) 0.6 * name + 0.25 * same_site + 0.15 * size }Notes du présentateur
Les pondérations relèvent du jugement, et elles doivent être visibles plutôt qu'enfouies dans une chaîne de conditions. Un filtre enchaîné traite chaque critère comme absolu : une faute dans le nom de la communauté et la paire disparaît. Un score laisse les indices s'additionner, ce qui est de toute façon la manière dont un relecteur humain raisonne.Le produit est une file de relecture, pas un tableau propre — En Python
review = ( pairs.sort_values("score", ascending=False) .loc[:, ["id_a", "id_b", "score", "head_a", "head_b", "community", "size_gap"]] .assign(decision="", reviewer="", reviewed_on="") ) review.to_csv("outputs/review/duplicate-candidates.csv", index=False)Notes du présentateur
C'est ce qui sépare un appariement utile d'un appariement qui provoque un incident.Le produit est une file de relecture, pas un tableau propre — En R
review <- pairs |> arrange(desc(score)) |> select(id_a, id_b, score, head_a, head_b, community, size_gap) |> mutate(decision = "", reviewer = "", reviewed_on = "") readr::write_csv(review, here::here("outputs", "review", "duplicate-candidates.csv"))Le produit est une file de relecture, pas un tableau propre
Un doublon fusionné est un enregistrement détruit. Si la fusion était fausse, la preuve qu'elle l'était a disparu avec lui.
Notes du présentateur
Trois colonnes vides, et c'est là tout l'intérêt. Le fichier part chez la personne qui tient le registre, revient avec une décision sur chaque ligne, et c'est ce fichier revenu que le script de nettoyage lit — pas le score, et pas un seuil appliqué automatiquement.Ce qu'il ne faut pas automatiser
- Une fausse fusion supprime quelqu'un. Deux ménages n'en font plus qu'un ; l'un cesse de recevoir l'assistance sans…
- Une fausse scission compte double. C'est coûteux et embarrassant, et cela ne prive personne de rien.
Notes du présentateur
Dans les données de bénéficiaires, de protection et de gestion de cas, la fusion automatique n'est pas un raccourci technique à faible taux d'erreur. C'est une décision qui porte sur une personne, et les deux modes de défaillance ne sont pas symétriques. Cette asymétrie impose de sous-fusionner par défaut. Signalez, faites relire, et laissez décider la personne qui tient le registre. Lorsqu'une fusion a lieu, conservez les deux identifiants d'origine dans l'enregistrement fusionné pour pouvoir revenir en arrière — une table d'appariement aveckept_id,merged_id,score,revieweretdateà côté des données. Rien de tout cela n'est de la prudence gratuite. Les principes de gestion de l'information VBG qui régissent une partie des données de ce secteur le disent directement : le préjudice d'un enregistrement mal traité retombe sur la personne qu'il décrit, pas sur l'analyste.La suite
- Vous savez désormais quelles lignes désignent la même chose et quelles colonnes les identifient.
Notes du présentateur
Vous savez désormais quelles lignes désignent la même chose et quelles colonnes les identifient. L'unité suivante s'attaque aux valeurs elles-mêmes — les mesures qui ne peuvent pas être vraies, les codes de sortie qui contredisent leur propre mesure, et les seuils sectoriels qui transforment « cela semble faux » en une règle qu'un script applique.