BottinBot2
Présentation du bot
En 2019, l'équipe du DHLAB a effectué une extraction de 4 Million d'adresses dans les anciens annuaires de la ville de Paris [1]. Les données sont mis sous forme d'un pandas.dataframe
dans la langue Python, utilisant la library Pandas. Les lignes sont les entrées des bottins, chaque entrée contient les informations suivantes:
index, directory, page, row, year, name, job, street, number, street_clean, street_only
La fonction de ce bot est d'automatiser la création d'articles biographiques à partir de ces données. Le BottinBot2 travail sur les données des années 1849 à 1956.
Fonctionnement du bot
Traitements des données du bottin
En premier lieu, les entrées contenant des valeurs NaN
sont éliminées.
Ensuite, trois nouvelles colonnes sont ajoutées : name_pp, job_pp et street_pp. Elles correspondent respectivement à pre_process_str(s)
, pour s
égal aux colonnes name
, job
et street_only
; où pre_process_str(s)
prends une chaîne de caractères s
et en renvoie une version qui s'est successivement vu supprimée de sa dernière éventuelle paire de parenthèse, supprimée de tout éventuel whitespace aux extrémités, supprimée de tout éventuel accent sur ses lettres, et mise en minuscules.
La similarité entre deux entrées est définie à l'aide d'une fonction score(entry1, entry2)
, qui pour deux entrées données, retourne un score de ressemblance compris entre 0 (aucune ressemblance) et 1 (identiques). Ce score est définis simplement comme le produit des trois scores de ressemblances resemblance(entr1.X, entry2.X)
, pour X
égal à name_pp, job_pp et street_pp. Cette deuxième fonction resemblance(s1, s2)
, elle, calcule la distance de Levenshtein entre les chaînes s1
et s2
normalisée par la taille la plus longue, réduit artificiellement cette distance si il a une inclusion d'une des chaînes dans l'autre, et enfin retourne 1 moins le résultat, de sorte à produire un score de ressemblance entre s1
et s2
bien compris entre 0 et 1. Le fait d'étudier le score final comme produit de ces 3 scores permet en quelque sorte d'autoriser que certaines des 3 variables soient un peu éloignées mais pas d'autres.
Le fonctionnement de l'algorithme de regroupement (coûteux en temps) regroupement_personnes(df)
est décrit par le pseudo-code ci-dessous :
def regroupement_personnes(df): Persons = [[entry] for entry in entrées de la df qui sont à l’année 1856] for year in range(1856-1, 1849 -1, -1): #Parcours déchronologique entries_year = entrées de la df qui sont à l’année year for P in Persons: P_lastentry = P[-1] Calcul des score(entry,P_lastentry) pour toute entry dans entries_year, (arrêt éventuel si on trouve un score parfait de 1) Si le meilleur de ces score est >= match_threshold: P.append( cette entry la plus ressemblante ). Suppression de cette entry la plus ressemblante de entries_year. Sinon: pass new_Persons = [[entry] for entry in entries_year] Persons += new_Persons return transformer_en_DataFrame(Persons)
L'approche anti-chronologique est justifiée par le fait que les données des bottins les plus récentes sont souvent les plus complêtes et plus consistantes avec les informations actuelles sur la ville. Le résultat de ce processus est une liste de dataFrame contenant les entrées attribuées à la même entité:
N.B. : Une exécution en parallèle de regroupement_personnes
sur des sous parties de la dataframe totale est en fait réalisée; où les quelques découpes ont été faites selon la 2ème lettre de la colonne name_pp (voir notebook Jupyter). À posteriori, comme moyen le plus sûr de faire des découpes indépendantes, il aurait simplement fallu choisir de découper selon les numéros de pages...
Cette liste est ensuite convertie en un dictionnaire à trois niveaux de clefs permettant de retrouver, à l'aide du nom, du métier et de l'adresse d'une entité, le dataFrame d'entrée correspondant.
N.B. : Pour un tableau d'une personne donnée, les valeurs de ces trois clefs name, job et street sont à chaque fois prises comme la valeur qui "ressemble le plus à toutes les autres" au sein de ce tableau propre à cette personne ; c'est-à-dire que c'est à chaque fois la valeur pour qui la moyenne des scores avec toutes les autres [pondérée par leur dégénérescence] est la meilleure. De plus, si aucune n'est plus ressemblante que les autres (c'est le cas par exemple s'il n'y a que 2 entrées), on garde la plus longue.
Le choix des clés a une importance, car ce sont elles qui permettront de former le titre des articles.
Par ailleurs, il est à noter que lors de la construction de ce dictionnaire, il peut apparaître quelques rares fois des collisions de triplets de clés (name, job, street).
Lorsque la collision ne fait pas intervenir d'années en commun, il est décidé de combiner les deux tableaux correspondants en une seule personne. (leur séparation jusqu'à présent est probablement due à un threshold de score réglé trop haut)
Par contre, lorsque la collision comporte des entrées avec années en commun, il a été décidé de ne garder que le premier D[name][job][street]
trouvé et de "jeter" tous les prochains qui donnent lieu à ces collisions..
Ces cas de collisions avec années en commun sont dus :
- parfois à une erreur de frères/sœurs pas 100% séparé (l'algorithme n'a pas bien séparé ces personnes),
- et/ou parfois à une erreur, due à un probable déménagement, où pendant quelques années deux adresses différentes perdurent ,
- ou parfois même, à une erreur de doublon qui figure réellement sur les photos du bottin.
Ecriture des articles
L'article pour chaque entité est composé d'entrées de la forme :
- year / Paris. Mention de name dans la catégorie job à l'adresse number street_clean. [url]
où les variables year,name,job,number et street_clean sont tiré des entrées du dataframe correspondant à l'individu et l'url est crée à l'aide de la fonction entry2url. Le titre de l'article dépend de l'existence d'autres entités partageant le même nom ou métier. Il y a donc trois cas:
- Il n'existe aucune autre entité ayant le même nom et le même métier, dans ce cas : le titre de l'article sera simplement le nom de l'entité e.g Klosé.
- Il existe une entité ayant le même nom mais pas le même métier, dans ce cas :
- le titre de l'article sera "nom de l'entité(métier de l'entité)" e.g Sénéchal (boulanger).
- Le titre de cette article est ensuite rajouté sur une page désambiguisation regroupant les titres des articles des entités ayant le même nom. Le titre de la page de désambiguisation est "nom de l'entité (Page d'homonymie)" e.g Sénéchal (Page d'homonymie).
- Il existe une autre entité ayant le même nom et le même métier. Dans ce cas :
- le titre de l'article sera "nom de l'entité(métier de l'entité,adresse de l'entité)" e.g Piquot (tailleur, Caumartin).
- Le titre de cette article est ensuite rajouté sur une page désambiguisation regroupant les titres des articles d'entité ayant le même nom et le même métier. Le titre de la page de désambiguisation est "nom de l'entité(métier de l'entité)". Le titre de cette page dépend de l'existence ou non d'entité avec le même nom et un métier différent :
- S'il n'y a pas d'autres entité avec le même nom et un métier différent, le titre de la page de désambiguisation est "nom de l'entité(métier de l'entité) (Page d'homonymie)" e.g Vaury jeune (boulanger) (Page d'homonymie).
- S'il y a autres entité avec le même nom et un métier différent, la titre de la page de désambiguisation est simplement "nom de l'entité(métier de l'entité)" ( e.g Piquot (tailleur)) et cet titre est rajouté sur une autre page désambiguisation regroupant les titres des articles d'entité ayant le même nom e.g Piquot (Page d'homonymie).
Discussion qualitative
Les deux problèmes principales, soient le regroupement des entrées de personnes individuelles et le problème d'homonymie, ont étés abordés.
Le problème de regroupement de personnes
L'algorithme de regroupement intelligent de personnes semble d'avoir bien fonctionné. On présente ici quelques exemples de pages où le regroupement a été bien réussie :
- Exemple déménagement : Gasnier (antiquités), noter le changement de numéro de 15. à 17. Il est quasiment sûr qu'il s'agit de la même personne.
- Exemple appellation d'un métier et déménagement : Martin (avocat), noter le changement de l'appellation du métier ainsi que le changement de numéro d'adresse. Le métier dans le titre ("avocat") est celui qui apparaît le plus dans les entrées biographiques. Il est très probable qu'il s'agit de la même personne.
- Exemple changement d'écriture du nom : Vooss, noter le changement du nom de "Vooss" à "Wooss". On y trouve également une faute de reconnaissance de text dans le métier, ainsi qu'un déménagement qui ont étés résolues par l'algorithme. Il semble toujours probable qu'il y s'agit de la même personne. Ce regroupement n'aurait pas pu être fait si les données auraient été triés alphabétiquement par la première lettre du nom.
Le problème d'homonymie
Le problème d'homonymie est résolue par l'organisation des DataFrames des personnes individuelles sous forme de dictionnaire à plusieurs niveaux ("nested dictionary") (voir en haut pour la description). Celui-ci permet de boucler une seule fois sur ce dictionnaire ainsi que les sous-dictionnaires, pouvant facilement créer les pages de personnes individuelles y contenues, mettre des titres de page appropriés selon le niveau d'homonymie, et en même temps créer les pages d'homonymies décrites précédemment.
Un défaut important est l'interaction avec des pages déjà existantes sur le wiki. Notre hypothèse que les pages sous le format "[Nom] ([métier])" ou "[Nom] ([métier],[rue])" soient uniquement créées par nous, et donc qu'il soit justifié d'effacer et recréer ces pages chaque fois qu'on fait tourner notre bot, n'a pas été vérifiée et est donc peut-être un peu naïve. Également, le choix de rajouter la biographie à partir de notre données à la fin d'un article du format "[Nom]" n'était pas le meilleure. D'un côté, le format du page n'est plus consistant, en particulier l'ordre chronologique n'est plus conservé (voir la page Adam (A.) comme exemple). Dans cet exemple particulier un peut aussi constater deux autres problèmes :
- Il ne s'agit clairement pas de la même personne, malgré le même nom. Il aurait fallu procéder par une homonymie de noms.
- Dans le processus de testing notre bot avait déjà une fois bouclé sur la personne Adam (A.). En relançant le bot, la biographie a été rajoutée une deuxième fois, car le choix a été fait de ne pas effacer la page. Ce problème semble d'être assez répandu, on a trouvé plusieurs pages avec cet erreur.
Un meilleure choix à ce stade de développement aurait été de décider de ne jamais modifier ni effacer des pages à titre déjà existant. Le seul défaut de ce choix aurait été que les pages créées par le BottinBot2 pendant la phase de test, qui sont partiellement incomplètes dans leur biographies parce que le bot avait été testé sur un dictionnaire réduit ne pas contenant des entrées de toutes les années, n'auraient pas pu être mises à jour. On espère de pouvoir reverser les changement faites par notre bot pour pouvoir le relancer en ayant fait ce choix de ne pas effacer les pages.
On donne quelque propositions pour résoudre ces problèmes d'interaction avec des pages déjà existantes:
- Convention d'écriture de pages : Dans le cas qu'une convention universelle d'écriture des pages biographiques soit établie entre les BottinBots, on pourrait s'imaginer une fonction qui, avant d'écrire un article, recherche si un article sous le titre souhaité existe, déjà, vérifie s'il peut s'agir de la même personne, et, dans ce cas, introduit les entrées bibliographiques au bon endroit.
- Modification des propres pages : Afin de pouvoir effacer ou modifier d'une manière plus flexible les pages créées par le BottinBot2, on pourrait créer une structure de données (du type
dict
oulist
) à laquelle sera rajoutée le titre d'une page une fois qu'elle est créée par le bot. Si on refait tourner notre bot, il pourrait ainsi distinguer si la page a été créée par lui même, et l'effacer et récrire sans problèmes. Alternativement, on pourrait aussi query l'utilisateur créateur de chaque page et ainsi déterminer si la page a été créée par le BottinBot2.
Code
Tout le code se trouve sur GitHub : MaximamongeissBot.