Allez vers la page précédente - étapes

Fusionner les bases de données

Une fois que l'on a pris connaissance des données que l'on souhaite obtenir des différentes bases de données, il est possible de créer un serveur local afin de stocker les données. Cela permet aussi d'aligner les bases de données pour avoir un vocabulaire contrôlé. Pour cela, nous avons fait le choix d'utiliser GraphDB pour sa facilité d'utilisation et ses fonctionnalités. L'importation des données se fait en deux étapes, d'abord nous importons les instances et ensuite leur(s) propriété(s).

Importation des instances de Wikidata

Nous avons fait le choix d'importer en premier les données provenant de Wikidata car c'est le plus gros silos avec plus de 130 000 instances. Il est important d'importer à la fois les économistes et les juristes afin de ne pas avoir des doublons pour les personnes qui serait dans les deux populations. Ensuite, nous donnons à chaque instance un URN unique afin que lorsque l'on ajoute d'autres bases de données, l'URN correspond à une personne réelle si elle est présente sur plusieurs bases de données. Pour cela il faut utiliser la clause UUID qui présente un URN sous la forme: “urn:uuid:b9302fb5-642e-4d3b-af19-29a8f6d894c9”.

Préalablement, il est aussi possible dans Wikidata de fusionner des pages (et leur URI) qui correspondent à une même personne (la méthode est sur cette page).

La requête se présente de la façon suivante:

PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>
PREFIX wd: <http://www.wikidata.org/entity/>
PREFIX wdt: <http://www.wikidata.org/prop/direct/>
PREFIX ome: <https://ontome.net/class/>
INSERT {
### La clause INSERT créé des triplets et les insére dans un graphe
GRAPH <http://economists_jurists.org/import_wikidata> {
### Avec cette clause, nous précisons que cette dans ce graphe
### que nous voulons nos instances, cela permettra par la suite
### de différencier les bases de données
#CONSTRUCT{
### la clause CONSTRUCT est utile pour voir les triplets
### que cela crée avant de les mettre dans un graphe. (elle est ici désactivée)
?URI <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> ome:21 .
### Nous précisons ici que les instances sont de type personne
### ("ome:21" correspond à la classe personne dans ontoME)
?URI owl:sameAs ?person .
### avec la propriété sameAs, nous indiquons que les URI créés avec
### la clause UUID correspond aux instances de Wikidata.
}
}
WHERE
{ SERVICE <https://query.wikidata.org/sparql>
### La SERVICE service permet d'aller chercher les données Wikidata sur GrapheDB.
{ SELECT DISTINCT ?person
WHERE
{ { ?person wdt:P106 wd:Q188094 ; # économiste ("economist)
wdt:P569 ?birthDate
### 'birthDate' est la seule propriété qu'il faut conserver
### afin d'être à l'intérieur de nos limites chronologiques
FILTER ( ?birthDate >= "1770-01-01"^^xsd:dateTime )
}
UNION
{ ?person wdt:P106 wd:Q185351 ; # juriste ("jurist")
wdt:P569 ?birthDate
FILTER ( ?birthDate >= "1770-01-01"^^xsd:dateTime )
}
UNION
{?person wdt:P106 wd:Q40348; # avocat ("lawyer")
wdt:P569 ?birthDate
FILTER ( ?birthDate >= "1770-01-01"^^xsd:dateTime )
}
UNION
{?person wdt:P106 wd:Q16533; # juge ("judge")
wdt:P569 ?birthDate
FILTER ( ?birthDate >= "1770-01-01"^^xsd:dateTime )
}
UNION
{?person wdt:P106 wd:Q758225; # avocat ("attorney at law")
wdt:P569 ?birthDate
FILTER ( ?birthDate >= "1770-01-01"^^xsd:dateTime )
}
UNION
{?person wdt:P106 wd:Q380075; # avocat("advocate")
wdt:P569 ?birthDate
FILTER ( ?birthDate >= "1770-01-01"^^xsd:dateTime )
}
UNION
{?person wdt:P106 wd:Q16009129; # magistrat ("magistrate")
wdt:P569 ?birthDate
FILTER ( ?birthDate >= "1770-01-01"^^xsd:dateTime )
}
UNION
{?person wdt:P106 wd:Q791243; # avocat en France ("legal advocate in France")
wdt:P569 ?birthDate
FILTER ( ?birthDate >= "1770-01-01"^^xsd:dateTime )
}
}
ORDER BY ?person
}
BIND(uuid() AS ?URI)
### La clause UUID permet de créer un URN unique que
### nous pouvons lier avec la clause BIND aux instance.
# LIMIT 10
### Lors de l'utilisation de la clause CONSTRUCT il vaut mieux n'afficher qu'un petit nombre de résultats.
}

Importation des propriétés de Wikidata

Un fois que l'on a importé les instances, il est désormais possible d'importer les propriétés une à une. Importer les propriétés individuellement permet d'éviter les doublons d'instance et c'est aussi plus facilement modulable. Dans Wikidata, nous faisons le choix d'importer les propriétés suivantes qui nous paraissent intéressantes:

Nom

Genre

Date de naissance

Date de mort

Lieu de naissance

Lieu de mort

Nationalité

École(s) fréquenté(s)

Poste(s) occupé(s)

URI VIAF

URI BnF Data

Nous voulions ajouté à cela les personnes qui les ont influencées et celles qu'elles ont influencées, ainsi que les écoles de pensées auxquelles elles ont appartenu, mais le nombre de personnes ayant ces propriétés est inférieur à 1% donc cela n'est pas pertinent.

La méthode pour importer les propriétés est assez similaire à celle des instances. Elle se construit de la façon suivante:

PREFIX owl: <http://www.w3.org/2002/07/owl#>
PREFIX wdt: <http://www.wikidata.org/prop/direct/>
PREFIX ome: <https://ontome.net/class/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX wd: <http://www.wikidata.org/entity/>
INSERT {
GRAPH <http://economists_jurists.org/import_wikidata> {
# CONSTRUCT {
?URI rdfs:label ?label . ### Cela insert les étiquettes des personnes et les lie aux URI
}
}
WHERE
{ ?URI owl:sameAs ?person ### Cela indique que les instances sont égales aux URI
{ SERVICE <https://query.wikidata.org/sparql>
{ SELECT DISTINCT ?person ?label
WHERE
{ ?person rdfs:label ?label
{ { ?person wdt:P106 wd:Q188094 ;
wdt:P569 ?birthDate ;
rdfs:label ?label
FILTER ( ?birthDate >= "1770-01-01"^^xsd:dateTime )
}
UNION
{ ?person wdt:P106 wd:Q185351 ;
wdt:P569 ?birthDate ;
rdfs:label ?label
FILTER ( ?birthDate >= "1770-01-01"^^xsd:dateTime )
}
}
FILTER ( LANG(?label) IN ("en", "fr") ) #
### nous avons fait le choix de ne garder que les étiquettes en français et en anglais.
}
}
}
}
#LIMIT 10

Importation des instances de BnF Data

Nous importons ensuite les instances de BnF Data. La méthode est assez similaire à celle de Wikidata, mais elle demande quelques étapes préliminaires. Avant d'attribuer aux instances un URN, il faut d'abord aligner les instances de BnF Data avec celle de Wikidata. Pour cela plusieurs méthodes sont disponibles. Tout d'abord, il est possible le lier les instances de Wikidata qui renvoient vers une instances de BnF Data. [mettre la méthode quand je l'aurais trouvé]. Ensuite, il est possible de lier ces instances avec un identifiant VIAF qu'elles ont en commun. Nous procédons de la façon suivante:

PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
PREFIX egr: <http://rdvocab.info/ElementsGr2/>
PREFIX owl: <http://www.w3.org/2002/07/owl#>
PREFIX ome: <https://ontome.net/class/>
PREFIX wdt: <http://www.wikidata.org/prop/direct/>
INSERT {
GRAPH <http://economists_jurists.org/import_bnf_data>
# CONSTRUCT
{
?URI owl:sameAs ?person_bnf .
### cela lie les instances ayant un identifiant VIAF avec celles
### qui sont déjà présentes dans le serveur qui ont un identifiant VIAF commmun.
}
}
WHERE
{ SELECT DISTINCT ?URI ?person_bnf
WHERE
{ ?URI wdt:P214 ?uri_viaf # la requête cherche les identifiants VIAF dans la base de données locales
{ SERVICE <https://data.bnf.fr/sparql>
{ # La sous-requête cherche les identifiants VIAF dans BnF Data
{ ?person_bnf egr:biographicalInformation ?bio ;
egr:dateOfBirth ?bd ;
owl:sameAs ?uri_viaf
FILTER REGEX(?uri_viaf, "viaf.org", "i")
BIND(STRBEFORE(STRAFTER(STR(?bd), "http://data.bnf.fr/date/"), "/") AS ?bd1)
FILTER ( ( ( ( ( REGEX(?bio, "juriste", "i") || REGEX(?bio, "professeur de droit", "i") ) || REGEX(?bio, "docteur en droit", "i") ) || REGEX(?bio, "avocat", "i") ) || REGEX(?bio, "juge", "i") ) || REGEX(?bio, "magistrat", "i") )
FILTER ( ?bd1 > "1770" )
}
UNION
{ ?person_bnf egr:biographicalInformation ?bio ;
egr:dateOfBirth ?bd ;
owl:sameAs ?uri_viaf
FILTER REGEX(?uri_viaf, "viaf.org", "i")
BIND(STRBEFORE(STRAFTER(STR(?bd), "http://data.bnf.fr/date/"), "/") AS ?bd1)
FILTER ( ( ( REGEX(?bio, "économiste") || REGEX(?bio, "Economiste") ) || REGEX(?bio, "professeur d'économie", "i") ) || REGEX(?bio, "docteur en économie", "i") )
FILTER ( ?bd1 > "1770" )
}
}
}
}
}
# LIMIT 10

Enfin la dernière méthode qui permet de connaître si des personnes sont dans plus bases de données, c'est le Recordlinkage. Nous la présentons en détail ci-dessous.

Recordlinkage

Cette méthode permet de calculer la proximité entre deux chaînes de caractères. Elle est répétée entre chaque ligne pour les variables choisies. Nous avons choisis d'utiliser cette méthode sur le nom de la personne, sa date de naissance et sa date de décès. Les deux derniers ont l'avantage d'avoir un format strict (YYYY-MM-DD), donc améliore considérablement les scores.

Pour réaliser cela, plusieurs bibliothèques python existent. Deux serons présentées ici, et elles sont présentées dans ce très bon article recordlinking posté par Chris Moffitt.

fuzzymatcher

Tout d'abord, la bibliothèque fuzzymatcher ( la documentation est très succincte; télécharger avec conda ou pip ) permet de le faire. Mais elle a le gros désavantage de ne pas fonctionner avec des nombres ou des dates (ou plutôt elle est analyse comme des chaînes de caractère).

On commence par choisir les colonnes pour lesquels ont veut utiliser la méthode:

left_on=["name_bnf", "placeOfBirth_bnf","placeOfDeath_bnf"]
right_on=["name_dbp", "placeOfBirth_dbp","placeOfDeath_dbp"]

Ensuite, on applique la méthode à proprement parler:

matched_results = fuzzymatcher.fuzzy_left_join(BnF_Data,
DBpedia,
left_on,
right_on,
left_id_col='uri_bnf',
right_id_col='uri_dbp')

Après, il faut expliciter quels colonnes souhaite-on voir apparaître (même celles sur lesquelles, le calcul ne se fait pas):

cols_bnf_dbp= ["best_match_score","id_bnf","uri_bnf","viaf_bnf", "name_bnf", "dateBirth_bnf", "dateDeath_bnf","placeOfBirth_bnf","placeOfDeath_bnf", "bio_bnf", "id_dbp","uri_dbp", "viaf_dbp", "name_dbp","birthDate_dbp","deathDate_dbp", "placeOfBirth_dbp","placeOfDeath_dbp"]

Enfin on affiche les résultats:

best_match_bnf_dbp=matched_results[cols_bnf_dbp].sort_values(by=["best_match_score"], ascending=False).head(10)
best_match_bnf_dbp

recordlinkage

La seconde méthode semble beaucoup pertinente au vu des données que nous disposons. La bibliothèque recordlinkage (télécharger avec conda ou pip permet aussi de calculer la proximité entre des chaînes de caractères. Mais cette dernière à l'avantage de fonctionner avec tous types de données (cf. cette page pour voir les types).

La méthode de cette bibliothèque est assez semblable à la précédente. on commence par index les deux jeux de données:

import recordlinkage
indexer = recordlinkage.Index()
indexer.sortedneighbourhood(left_on='name_bnf', right_on='name_dbp')
candidates = indexer.index(BnF_Data, DBpedia)
print(len(candidates))

Il est possible de le faire sur l'ensemble des données, mais au regard volume et de la puissance de calcul nécessaire, nous le faisons que pour une seule variable. À nous choisissons celle qui donne le plus grand nombre candidat potentiel. Il est aussi de possible de réaliser la même méthode sur un seul jeu de données en mettant deux fois la même variable et le même jeu de données. Cela nous a été notamment très utilisé pour déceler des personnes présentes deux fois sous un URI différent.

Ensuite, il faut réaliser les comparaisons: compare = recordlinkage.Compare()

compare.string('name_bnf',
'name_dbp',
threshold=0.85,
label='name_bnf_dbp')
compare.exact('dateBirth_bnf',
'birthDate_dbp',
label='birthDate_bnf_dbp')
compare.exact('dateDeath_bnf',
'deathDate_dbp',
label='deathDate_bnf_dbp')
features = compare.compute(candidates, BnF_Data, DBpedia)

Pour les dates de naissance et de mort, nous avons fait le choix de prendre les valeurs exacts afin de contrer des éventuels différences de formats (ex: dans BnF Data certaines dates sont écrites ainsi: “18..” la bibliothèque n'arrive pas à les interpréter et ne sort pas de résultats).

Il faut ensuite de fusionner les données prenant les comparaisons avec un haut score. Ce dernier indique sur combien de variable un score élevé à été trouvé et les additionnent:

potential_matches = features[features.sum(axis=1) > 1].reset_index()
potential_matches['Score'] = potential_matches.loc[:, 'name_bnf_dbp':'deathDate_bnf_dbp'].sum(axis=1)
potential_matches

L'ensemble de la méthode est disponible sur un carnet sur Github. Vous pouvez aussi directement accéder aux requêtes en téléchargeant la base de données et en l'ouvrant avec un logiciel de requêtage de base de données comme DBeaver (lien vers le téléchargement, Mac Os, Windows et Linux).