#RuesDeFrance : une histoire de code

Se confronter à 7 millions d’entrées… les malaxer, les masser, les mélanger etc. Et en tirer le meilleur pour en faire quelque chose de joliment structuré. Comment filtrer de la donnée et l’enrichir avec Wikidata? Réponse avec des librairies Python de datascience (Pandas & Numpy); une pincée de regex et un cas de scraping (BeautifulSoup).

Pour un rappel des objectifs du projet #RuesDeFrance, je vous invite vers cet article. Les sources utilisées viennent d’un jeu de fichiers csv hébergé sur site www.lesruesdefrance.com ; et de Wikidata.

Nous abordons ici la partie technique : comment récupérer une liste propre de toutes les rues de France ? Comment enrichir les rues aux noms de personnalités avec les données de Wikidata (genre, occupation etc.) ? Il existe pour ces deux questions plusieurs approches. Je détaille les miennes ci-dessous avec un codage Python.

Etape 1 : récupérer une liste propre de toutes les rues de France

Tout est dans le titre. Il s’agit de récupérer des données éclatées dans plusieurs fichiers qui cumulent plus de 7 millions de lignes. Et d’harmoniser l’ensemble pour obtenir une belle information bien calibrée.

Phase 1.1 : importer les données à partir des url sources.

L’objectif premier est de télécharger ces fichiers en local sur Google Colab.

"""
Opération : récupérer les données sur les url sources
Source : https://www.lesruesdefrance.com
Output : 101 fichiers csv enregistrés en local pour plus de 7 millions d'entrées cumulées
"""

#Création d'une liste avec l'url des fichiers à télécharger
liste1 = list(range(1,19+1))
liste2 = list(range(21,29+1))
liste2.append('2A')
liste2.append('2B')
liste3 = list(range(30,95+1))
liste4 = list(range(971,974+1))
liste = liste1 + liste2 + liste3 + liste4
liste.append(976)
liste[0]='01'
liste[1]='02'
liste[2]='03'
liste[3]='04'
liste[4]='05'
liste[5]='06'
liste[6]='07'
liste[7]='08'
liste[8]='09'

liste_url = []
i = 0
while i < len(liste):
      url = f"https://www.lesruesdefrance.com/exportsql/liste_rue_par_dep_{liste[i]}.csv"
      response = requests.get(url)
      liste_url.append(url)
      i = i + 1

#Import des fichiers dans Google Colab
i = 0
while i < len(liste_url):
    r = requests.get(liste_url[i], stream=True)
    with open("fichier%s.csv" % i, "wb") as f:
        r.raw.decode_content = True
        shutil.copyfileobj(r.raw, f)
    i += 1
    time.sleep(0.5)

Phase 1.2 : nettoyer et filtrer les données de chaque fichier csv

Maintenant qu’on a ces fichiers… l’idée est d’harmoniser l’ensemble. Et il y a du boulot ! Entre les fautes d’orthographe, les diminutifs, les apostrophes etc. il a fallu construire une belle moulinette. Au final, on obtient nos 101 fichiers filtrés sur les 786 000 rues que comptent le territoire français.

"""
Opération : nettoyage et filtrage des fichiers un à un
Output : 101 fichiers csv propres, filtrés sur les rues, pour 786 000 entrées cumulées
"""

#Création de fichiers uniformisés centrés sur le rues
i = 0
while i <= 100:
  df_fichier = pd.read_csv('/content/fichier%s.csv' % i, sep=';')
  df_fichier['LIBVOIE'] = df_fichier['LIBVOIE'].str.replace(r'L\'', 'L ').astype(str)
  df_fichier['LIBVOIE'] = df_fichier['LIBVOIE'].str.replace(r'D\'', 'D ').astype(str)
  df_fichier['LIBVOIE'] = df_fichier['LIBVOIE'].str.replace(r'S\'', 'S ').astype(str)
  """... etc ..."""
  df_fichier['LIBVOIE'] = df_fichier['LIBVOIE'].str.replace(r' -', ' ').astype(str)
  df_fichier['LIBVOIE'] = df_fichier['LIBVOIE'].str.replace(r'-', ' ').astype(str)
  df_fichier['LIBVOIE'] = df_fichier['LIBVOIE'].str.replace(r'  ', ' ').astype(str)
  
  #Filtrage sur les rues
  df_fichier = df_fichier[df_fichier.nature_voie.str.contains('RUE')]
  
  #Remise à zéro de l'index
  df_fichier = df_fichier.reset_index()
  
  #Elimination des colonnes inutiles
  df_fichier =df_fichier.drop(['index', """...etc ...""",  'Unnamed: 9'], axis=1)
  
  #Elimination des espaces en début/fin de chaîne en passant par une liste
  list_of_single_column = df_fichier['LIBVOIE'].tolist()
  y = 0
  while y < len(list_of_single_column):
      list_of_single_column[y] = list_of_single_column[y].strip()
      y = y+1
  
  #Réenregistrement des fichiers
  df_fichier = pd.DataFrame(list_of_single_column, columns = ['RUES'])
  returnValue = df_fichier.to_csv('fichier%s.csv' % i, index = False)
  i = i+1

Phase 1.3 : concaténer les fichiers dans un dataframe

La suite est logique… l’idée est de concaténer les données de nos fichiers dans un seul et même fichier/dataframe.

"""
Opération : concaténer les fichiers dans un dataframe
Output : 1 fichier csv et 1 dataframe de 777 000 entrées
"""

#Création d'un unique fichier csv
path = "/content/"
file_list = [path + f for f in os.listdir(path) if f.startswith('fichier')]
csv_list = []

for file in sorted(file_list):
    csv_list.append(pd.read_csv(file, sep=';', encoding='latin-1', error_bad_lines=False).assign(File_Name = os.path.basename(file)))

csv_merged = pd.concat(csv_list, ignore_index=True)
csv_merged.to_csv(path + 'fichiertotal.csv', index=False)

#Création du dataframe
df = pd.read_csv('/content/fichiertotal.csv')

Etape 2 : enrichir les données avec Wikidata

Voilà la partie technique la plus intéressante ! Le croisement de données entre notre fichier de rues et les informations tirées de Wikidata. Mon idéal était d’utiliser mes données de noms de rue sous forme de liste Python, et d’utiliser cette liste dans des requêtes SPARQL. Je ne sais pas si cette technique est possible… en tout cas je n’ai pas trouvé la parada pour combiner l’utilisation des deux langages. Je suis donc passé par du scraping en utilisant la librairie BeautifulSoup. Peut-être Selenium aurait mieux fait l’affaire, pour lancer une requête filtrée sur Q5 (l’identifiant « humain » des entrées Wikidata) en bouclant ma liste dans l’url de leur page de recherche. Bref… BeautifulSoup a fait l’affaire, avec quelques limites propres à l’algorithme de recherche Wikidata.

Phase 2.1 : sélection des noms de rues à plus de 5 occurrences

Pourquoi limiter à sélection ? Pour une question de traitement… le croisement de données prend du temps. Il faut environ 1 heure pour scraper les infos à partir d’un fichier de 13 000 entrées. Je n’avais pas envie de me lancer dans une croisade de plusieurs heures avec un fichier plus long, d’autant plus que l’utilité de l’ensemble du fichier est limitée : Wikidata ne connait pas toutes les personnes mentionnées dans les rues françaises… loin de là ! Les personnes qui ont au moins cinq occurrences de rues à leur nom ont plus de chance d’être intégrés dans la base que Monsieur Truquemuche… héro local de Bouzenville-le-Cru, célèbre pour l’unique nom de rue de son village de naissance.

"""
Opération : sélection des noms de rues à plus de 5 occurrences
Output : création d'un df et d'une liste avec les 13 600 noms des rues identifiées
"""

#Création du dataframe filtré sur les noms à plus de 5 occurrences
df2 = df['RUES'].value_counts().rename_axis('rues').reset_index(name='nombre')
df_mask=df2['nombre']>=5
df2 = df2[df_mask]
returnValue = df2.to_csv('fichiertotal_occ_f5.csv', index = False)

#Transformation du dataframe en liste
list_of_single_column_rue = list(df2['rues'])

Phase 2.2 : création de listes de données pour chaque entrée humaine

Voilà la longue partie de croisement de données… qui passe par la création de listes de données en filtrant le contenu de ces listes aux conditions que nous avons bien affaires à un humain. J’ai voulu connaitre le code Wikidata, le genre, l’occupation et l’origine des personnalités reconnues. Et récupérer une courte description (en anglais) de pourquoi ils sont célèbres.

"""
Opération : création de listes de données Wikidata pour chaque entrées humaines
Source : https://www.wikidata.org/
Output : 4 listes qui comprennent des données sur le genre, l'origine, l'occupation et la description des personnalités qui ont au moins cinq rues à leur nom.
La liste "liste_enrichi_type" détermine si le nom de rue correspond à un humain
La liste "liste_enri_wikicode" donne l'identifiant Wikidata de chaque entrée
"""

#Création des listes
liste_enri_wikicode = []
liste_enri_type = []
liste_enri_genre = []
liste_enri_occupation = []
liste_enri_origine = []
liste_enri_description = []

#Lancement de la requête Wikidata pour chaque entrée
url = "https://www.wikidata.org/w/api.php"
i = 0
while i < len(list_of_single_column_rue):
  query = list_of_single_column_rue[i]
  params = {
      "action" : "wbsearchentities",
      "language" : "en", 
      "format" : "json", 
      "search" : query,
      "limit" : "1"
  }
  data = requests.get(url, params=params)
  wikicondition = data.json()

  #Si une entrée Wikidata est reconnue
  if len(wikicondition) == 4:
    #Récupération de l'ID Wikidata
    wikicode = data.json()["search"][0]["id"]
    page = f"https://www.wikidata.org/wiki/{wikicode}"
    r = requests.get(page)
    soup = BeautifulSoup(r.content, "html.parser")
    #Vérification que l'ID correspond à une occurrence humaine
    instance = 'human'
    text_instance = soup.find_all('a', text = instance)
    if not text_instance:
      type = "None"
    else:
      type = "Humain"
    
    #Vérification de l'existence de données sur le genre et l'occupation
    facteur_genre = 'sex or gender'
    facteur_occupation = 'occupation'
    facteur_origine = 'country of citizenship'
    text_facteur_genre = soup.find('a', text = facteur_genre)
    text_facteur_occupation = soup.find('a', text = facteur_occupation)
    text_facteur_origine = soup.find('a', text = facteur_origine)

    #Si les conditions sont non vérifiées alors valeurs = "None"
    if not text_facteur_genre or not text_facteur_occupation:
      genre = "None"
      occupation = "None"
      origine = "None"
      dc = "None"
    
   #Si les conditions qui identifient l'humain sont vraies
    else:
      if type == "Humain":
        #Alors extraction du genre
        gender = 'female'
        text_gender = soup.find_all('a', text = gender) 
        if not text_gender:
          genre = "Homme"
        else:
          genre = "Femme"
        #Alors extraction de l'occupation
        occupation = soup.find('div', {'data-property-id': 'P106'}).get_text()
        occupation = re.search(r'\n\noccupation(.*?)\n', occupation).group(1)
        #Alors extraction de l'origine
        origine = soup.find('div', {'data-property-id': 'P27'}).get_text()
        origine = re.search(r'\n\ncountry of citizenship\n(.*?)\n', origine).group(1)
        #Alors extraction de la description
        dc = soup.find('span', {'class': 'wikibase-descriptionview-text'},).get_text()

      #Si on a affaire à à un humanoïde au autre... (pour éviter les erreurs !)
      else :
        genre = "None"
        occupation = "None"
        origine = "None"
        dc = "None"
  
  #Si aucune entrée Wikidata n'est reconnue alors valeurs = 0 ou "None"
  else:

    wikicode = 0
    type = "None"
    genre = "None"
    occupation = "None"
    origine = "None"
    dc = "None"

  #Alimentation des listes avec les valeurs retenues
  liste_enri_wikicode.append(wikicode)
  liste_enri_type.append(type)
  liste_enri_genre.append(genre)
  liste_enri_occupation.append(occupation)
  liste_enri_origine.append(origine)
  liste_enri_description.append(description)

  i = i+1

Phase 2.3 : concaténation des données Wikidata avec les noms de rue

Par un basculement de listes à dataframes on obtient un fichier enrichi avec les informations trouvées lorsqu’il s’agit de personnalités.

"""
Objectif : concaténation des données Wikidata avec les noms de rues
Output : un fichier csv et un df de 13 600 noms de rues, enrichi des données Wikidata pour les personnalités qui figurent dans la liste 
"""

#Transformation des listes Wikidata en dataframe
df_wikicode = pd.DataFrame(liste_enri_wikicode, columns = ['wikicode'])
df_type = pd.DataFrame(liste_enri_type, columns = ['type'])
df_genre = pd.DataFrame(liste_enri_genre, columns = ['genre'])
df_occupation = pd.DataFrame(liste_enri_occupation, columns = ['occupation'])
df_origine = pd.DataFrame(liste_enri_origine, columns = ['origine'])
df_description = pd.DataFrame(liste_enri_description, columns = ['description'])

#Fusion des dataframes et enregistrement de la donnée en fichier csv
df_enrichi = pd.concat([df2, df_wikicode, df_type, df_genre, df_occupation, df_origine, df_description], axis=1, join='inner')
returnValue = df_enrichi.to_csv('fichiertotal_occ_f5_wiki.csv', index = False)

Phase 2.4 : filtrage et dédoublonnage codé des entrées humaines

L’opération de dédoublonnage (x2!) peut commencer. Certaines personnalités ont plusieurs noms de rues à leur nom ! Pensez à « Charles de Gaulle » et « Général de Gaulle »… à « Pasteur » et à « Louis Pasteur » etc. Les doublons à ce stade sont différentes, sont encore légions. Voilà les subtilités de traitement… qui même après ce nettoyage, ne peut faire l’économie d’une vérification directement dans le fichier.

"""
Objectif : filtrage et dédoublonnage codé des entrées humaines
Outpout : un fichier csv et un df de 1 600 noms de rues, centré sur les personnalités avec les caractéristiques propres à chaque individu.
"""

#Transformation du dataframe et liste classée par ordre alphabétique de l'ID Wikidata
df_pers = pd.read_csv('/content/fichiertotal_occ_f5_wiki.csv')
df_pers = df_pers[['rues','nombre', 'wikicode', 'type', 'genre', 'occupation','origine','description']]
df_pers = df_pers[(df_pers.type != 'None') & (df_pers.description != 'None')]
df_pers = df_pers.sort_values(by=['wikicode'], ascending=True)

#Transformation de chaque colonne du dataframe en listes
losc_rue_pers = list(df_pers['rues'])
losc_nombre_pers = list(df_pers['nombre'])
losc_wiki_pers = list(df_pers['wikicode'])
losc_genre_pers = list(df_pers['genre'])
losc_occupation_pers = list(df_pers['occupation'])
losc_origine_pers = list(df_pers['origine'])
losc_description_pers = list(df_pers['description'])

#Lancement du filtrage pour fusionner les doubles et les triples
y = 0
while y <3:
  i = 0
  while i < (len(losc_rue_pers) - 1):
    if losc_wiki_pers[i] == losc_wiki_pers[i+1]:
      losc_nombre_pers[i] = losc_nombre_pers[i] + losc_nombre_pers[i+1]
      
      losc_rue_pers.pop(i+1)
      losc_nombre_pers.pop(i+1)
      losc_wiki_pers.pop(i+1)
      losc_genre_pers.pop(i+1)
      losc_occupation_pers.pop(i+1)
      losc_origine_pers.pop(i+1)
      losc_description_pers.pop(i+1)
    i=i+1
  y = y+1

#Transformation des listes en dataframe
df_rues_pers = pd.DataFrame(losc_rue_pers, columns = ['rues'])
df_nombre_pers = pd.DataFrame(losc_nombre_pers, columns = ['nombre'])
df_wiki_pers = pd.DataFrame(losc_wiki_pers, columns = ['wikicode'])
df_genre_pers = pd.DataFrame(losc_genre_pers, columns = ['genre'])
df_occupation_pers = pd.DataFrame(losc_occupation_pers, columns = ['occupation'])
df_origine_pers = pd.DataFrame(losc_origine_pers, columns = ['origine'])
df_description_pers = pd.DataFrame(losc_description_pers, columns = ['description'])

#Fusion des dataframes et enregistrement de la donnée en fichier CSV
df_pers_filtre = pd.concat([df_rues_pers, df_nombre_pers, df_wiki_pers, df_genre_pers, df_occupation_pers, df_origine_pers, df_description_pers], axis=1, join='inner')
df_pers_filtre = df_pers_filtre.sort_values(by=['nombre'], ascending=False)
df_pers_filtre = df_pers_filtre.reset_index()
df_pers_filtre = df_pers_filtre[['rues','nombre', 'wikicode', 'genre', 'occupation','origine','description']]
returnValue = df_pers_filtre.to_csv('fichiertotal_occ_f5_wikipers.csv', index = False)

Au terme de ce processus, on obtient un fichier de 1 639 personnalités, avec leurs caractéristiques (genres, origines etc.) et le nombre de rues à leurs noms. Le filtrage/vérification, les mains dans le cambouis, directement dans le fichier permet d’affiner les coquilles propres au croisement de données (perso. j’ai voulu vérifier les infos sur personnalités qui ont plus de 100 rues à leur nom). Et de préparer l’analyse.

Notez que ce processus peut-être répété à l’identique pour les avenues et/ou les ponts, les impasses etc. et même les lieu-dit ! N’empêche le lieu-dit « Charles de Gaulle »… j’y crois moyen.

Maintenant place à l’analyse.

Les autres articles du projet #RuesDeFrance

#RuesDeFrance : un projet data. Quel est l’intérêt et quels sont les objectifs du projet ?

#RuesDeFrance : analyse des occurrences de rues. Combien de rues portent x fois le même nom ?

#RuesDeFrance : le Top 100. Quelles sont les 100 noms de rues les plus répandues en France ?

#RuesDeFrance : analyse des personnalités. Qui sont les personnalités qui portent un nom de rue ?