Clustering & Déduplication¶
🗺️ Contexte: on ingère de la donnée d’acteurs de l’économie circulaire de plusieurs sources
🔴 Problème: certains acteurs sont en doublon, polluant l’application utilisteur
🟢 Solution: faire un travail pour réduire les doublons et améliorer la cohérence des données
🤔 Comment: voir ci-dessous
⚠️ Limitations connues¶
On ne séparent pas les enfants d’un cluster existant: choix de notre part, pour l’instant on veut uniquement ajouter des nouveaux enfants. Pour changer il faudra CLUSTERING & DEDUP: re-clusteriser les enfants existants. Pour l’instant:
🟢 avantage: pas de risque d’endommager des clusters existants
🟠 inconvénient: pas d’opportunité de re-clustering les mauvais cluster existants
Pas de re-clustering enfants = pas de contexte enfant: conséquence du dessus, les enfants n’ont pas leur donnée récupérée ni normalisée, ce qui peut poser des problèmes de contexte, donc on à fait https://github.com/incubateur-ademe/quefairedemesobjets/pull/1379 en attendant
📜 Définition¶
📦 Clustering: fait de regrouper des acteurs via leur similarité
1️⃣ Déduplication: convertir un cluster en 1 seul acteur pour ne plus avoir de doublons
🎏 Etat final idéal d’un acteur: on se concentre sur les acteurs statut=ACTIF
Etat 🇫🇷 | Code 🇬🇧 | Définition | Correspond à une source en particulier | Visible sur la carte |
---|---|---|---|---|
parent(s) | parent(s) | acteur qui a 1 ou plusieurs enfants rattachés à lui | 🟠 NON on créé un parent “artificiel” pour que celui-ci vive de manière détachée de ses enfants = plus robuste à travers le temps, pas besoin de changer le parent à chaque fois que les enfants changent |
🟢 OUI |
enfant(s) | child / children | acteur rattaché à 1 parent (1 est le maximum) | 🟢 OUI C’est de cette source que vient l’acteur |
🟠 NON C’est le parent qui est affiché |
orphelin(s) | orphan(s) | acteur rattaché à 0 parent | 🟢 OUI C’est de cette source que vient l’acteur |
🟢 OUI |
➡️ Transitions d’états: scénarios¶
modèle de changement | Etat avant | Etat après | Scénario | Conséquences dans revision | Conséquences dans displayed |
---|---|---|---|---|---|
acteur_create_as_parent | Orphelin | Parent | ➕ Nouveau parent pour nouveau cluster | ➕ Parent à créer ➕ Donnée enrichie au mieux |
pareil que révision |
acteur_keep_as_parent | Parent | Parent | 1️⃣ 1 seul parent existant -> à garder | 🟰 Toujours parent du cluster ➕ Donnée enrichie au mieux |
pareil que révision |
acteur_keep_as_parent | Parent | Parent | 🎖️ 2+ parents dans cluster -> celui avec + d'enfants -> à garder | 🟰 Toujours parent du cluster ➕ Donnée enrichie au mieux |
pareil que révision |
acteur_delete_as_parent | Parent | N’existera plus | 🔴 2+ parents dans cluster -> non choisi -> à supprimer | 🛑 Devrait être automatiquement supprimé suite à la mise à jour de ces enfants (voir PR1247) | 🛑 Devrait disparaitre de displayed |
acteur_verify_in_revision | Enfant | Enfant | 🟰 Pointe déjà vers nouveau parent → rien à faire | Aucune | Aucune |
acteur_update_parent_id | Enfant | Enfant | 🔀 Pointait vers un parent qui n’a pas été choisi → à pointer vers nouveau parent | 🔀 Mettre à jour parent_id pour pointer vers nouveau parent | Aucune |
acteur_update_parent_id | Orphelin | Enfant | 🔀 à pointer vers un parent | 🔀 Mettre à jour parent_id pour pointer vers nouveau parent | 🛑 Devrait disparaitre de displayed |
Orphelin | Orphelin | Ne fais toujours pas parti d’un cluster (pas de changement) | Aucune | Aucune |
🧪 Algorithme¶
🗓️ Tentatives passées¶
Considération de https://github.com/dedupeio/dedupe: mais en voyant que le comparateur évalue 2 valeurs à la fois = complexité O(n2) au moment du runtime, et sachant nos volumes (~500K acteurs) = peur de s’orentier vers du non-vectorisé. On voit des retours utilisateurs qui vont dans ce sens (temps de matching qui explose en passant d’un échantillon de 1K à 5K).
Tentative vectorisée: tenté de faire du vectorisé de base (ex: TF-IDF pour naturellement dépriorisé le bruit / la redondance) mais n’ayant pas d’infra de compute (ex: startup état = frugale) l’idée a été abandonnée (le risque de créer des modèles qu’on ne pouvait pas gérer via Airflow).
👉🏻 Actuellement¶
Très primitif: avec de la normalisation et du TF-IDF mais qui tourne à une échelle trop réduite pour être vraiment pertinent. Manque de tolérance fuzzy.
💡 Améliorations¶
Continuer la normalisation en amont: car celle-ci fait bénéficier non seulement le clustering, mais la qualité des données sur la carte:
Etendre le scope vectorisé: par exemple à l’échelle d’un département, pour offrir un compromis pertinence vs. taille du modèle
Reconsidérer https://github.com/dedupeio/dedupe: encore une fois sur des sous-ensembles (e.g. ville) pour bénéficier de la librairie (qui offre des choses intéressantes genre distances) sans souffrir trop du O(n2)
Embeddings: à cause des représentations vraiment diverses (ex: centre de collècte des déchets vs. décheteries)
Mappings de conversion pour les cas limités/connus (ex: abbrévation des noms de voies, ex: ESP -> ESPLANADE)
Algo phonétiques pour les typos
Modèles de langues: potentiellement les modèles compact (SLMs) qui offrirait des performances supérieur à tout ce qu’on peut faire au dessus en posant la question simple (« merci de clusteriser ces échantillons »)
🚤 Performance¶
👉🏻 Actuellement¶
Mauvaises mais suffisantes pour métier: qui fait tourner l’algo sur Airflow et fait autre chose en attendant
Raison principale: les boucles et aller-retour successifs pour choisir les parents et leur données
Exemple d’un clustering de 150K acteurs qui prenait ~6 heures
💡 Améliorations¶
Réécrire les tâches en passant par des modèles DBT qui s’occupe de préparer la data = finit les boucles / aller-retour DB via python.
🔀 Schéma¶
graph TD subgraph selection["🔎 <b>selection</b>: statut=ACTIF"] direction LR parents orphans children end subgraph normalization["🧹 <b>normalization</b>"] norma["lower case, no accents etc..."] end subgraph clustering["📦 <b>clustering</b>"] similar["exact + fuzzy silimarity"] end subgraph dedup["1️⃣ <b>deduplication</b>"] parents_choose["🥇 Choose new parents"] parents_data["🗄️ Choose parents' data (from non-normalized data)"] parents_choose-->parents_data parents_data-->children_feed["⬅️ Add existing children"] children_feed-->changes_mark["📋 Define changes"] end subgraph suggestions[" <b>suggestions</b>"] propose end parents-->|displayed| normalization orphans-->|displayed| normalization normalization --> clustering children-->|revision| children_feed clustering-->dedup changes_mark-->suggestions