Récupérer des données avec des API depuis Python

Les API (Application Programming Interface) sont un mode d’accès aux données en expansion. Grâce aux API, l’automatisation de scripts est facilitée puisqu’il n’est plus nécessaire de stocker un fichier, et gérer ses différentes versions, mais uniquement de requêter une base et laisser au producteur de données le soin de gérer les mises à jour de la base.

Exercice
Manipulation
Auteur·rice

Lino Galiana

Date de publication

2024-11-20

Pour essayer les exemples présents dans ce tutoriel :
View on GitHub Onyxia Onyxia Open In Colab

1 Introduction : Qu’est-ce qu’une API ?

1.1 Définition

Pour expliquer le principe d’une API, nous allons reprendre le début de la fiche dédiée aux API dans la documentation collaborative sur le logiciel R (un logiciel de traitement statistique proche de Python) utilitR que je recommande de lire :

Une Application Programming Interface (ou API) est une interface de programmation qui permet d’utiliser une application existante pour restituer des données. Le terme d’API peut être paraître intimidant, mais il s’agit simplement d’une façon de mettre à disposition des données : plutôt que de laisser l’utilisateur consulter directement des bases de données (souvent volumineuses et complexes), l’API lui propose de formuler une requête qui est traitée par le serveur hébergeant la base de données, puis de recevoir des données en réponse à sa requête.

D’un point de vue informatique, une API est une porte d’entrée clairement identifiée par laquelle un logiciel offre des services à d’autres logiciels (ou utilisateurs). L’objectif d’une API est de fournir un point d’accès à une fonctionnalité qui soit facile à utiliser et qui masque les détails de la mise en oeuvre. Par exemple, l’API Sirene permet de récupérer la raison sociale d’une entreprise à partir de son identifiant Siren en interrogeant le référentiel disponible sur Internet directement depuis un script R, sans avoir à connaître tous les détails du répertoire Sirene.

À l’Insee comme ailleurs, la connexion entre les bases de données pour les nouveaux projets tend à se réaliser par des API. L’accès à des données par des API devient ainsi de plus en plus commun et est amené à devenir une compétence de base de tout utilisateur de données.

utilitR

1.2 Avantages des API

A nouveau, citons la documentation utilitR :

Les API présentent de multiples avantages :

  • Les API rendent les programmes plus reproductibles. En effet, grâce aux API, il est possible de mettre à jour facilement les données utilisées par un programme si celles-ci évoluent. Cette flexibilité accrue pour l’utilisateur évite au producteur de données d’avoir à réaliser de multiples extractions, et réduit le problème de la coexistence de versions différentes des données.
  • Grâce aux API, l’utilisateur peut extraire facilement une petite partie d’une base de données plus conséquente.
  • Les API permettent de mettre à disposition des données tout en limitant le nombre de personnes ayant accès aux bases de données elles-mêmes.
  • Grâce aux API, il est possible de proposer des services sur mesure pour les utilisateurs (par exemple, un accès spécifique pour les gros utilisateurs).

utilitR

L’utilisation accrue d’API dans le cadre de stratégies open-data est l’un des piliers des 15 feuilles de route ministérielles en matière d’ouverture, de circulation et de valorisation des données publiques.

1.3 Utilisation des API

Citons encore une fois la documentation utilitR :

Une API peut souvent être utilisée de deux façons : par une interface Web, et par l’intermédiaire d’un logiciel (R, Python…). Par ailleurs, les API peuvent être proposées avec un niveau de liberté variable pour l’utilisateur :

  • soit en libre accès (l’utilisation n’est pas contrôlée et l’utilisateur peut utiliser le service comme bon lui semble) ;
  • soit via la génération d’un compte et d’un jeton d’accès qui permettent de sécuriser l’utilisation de l’API et de limiter le nombre de requêtes.

utilitR

De nombreuses API nécessitent une authentification, c’est-à-dire un compte utilisateur afin de pouvoir accéder aux données. Dans un premier temps, nous regarderons exclusivement les API ouvertes sans restriction d’accès.
Certains exercices et exemples permettront néanmoins d’essayer des API avec restrictions d’accès.

2 Requêter une API

2.1 Principe général

L’utilisation de l’interface Web est utile dans une démarche exploratoire mais trouve rapidement ses limites, notamment lorsqu’on consulte régulièrement l’API. L’utilisateur va rapidement se rendre compte qu’il est beaucoup plus commode d’utiliser une API via un logiciel de traitement pour automatiser la consultation ou pour réaliser du téléchargement de masse. De plus, l’interface Web n’existe pas systématiquement pour toutes les API.

Le mode principal de consultation d’une API consiste à adresser une requête à cette API via un logiciel adapté (R, Python, Java…). Comme pour l’utilisation d’une fonction, l’appel d’une API comprend des paramètres qui sont détaillées dans la documentation de l’API.

utilitR

Voici les éléments importants à avoir en tête sur les requêtes (j’emprunte encore à utilitR) :

  • Le point d’entrée d’un service offert par une API se présente sous la forme d’une URL (adresse web). Chaque service proposé par une API a sa propre URL. Par exemple, dans le cas de l’OpenFood Facts, l’URL à utiliser pour obtenir des informations sur un produit particulier (l’identifiant 737628064502) est https://world.openfoodfacts.org/api/v0/product/737628064502.json
  • Cette URL doit être complétée avec différents paramètres qui précisent la requête (par exemple l’identifiant Siren). Ces paramètres viennent s’ajouter à l’URL, souvent à la suite de ?. Chaque service proposé par une API a ses propres paramètres, détaillés dans la documentation.
  • Lorsque l’utilisateur soumet sa requête, l’API lui renvoie une réponse structurée contenant l’ensemble des informations demandées. Le résultat envoyé par une API est majoritairement aux formats JSON ou XML (deux formats dans lesquels les informations sont hiérarchisées de manière emboitée). Plus rarement, certains services proposent une information sous forme plate (de type csv).

Du fait de la dimension hiérarchique des formats JSON ou XML, le résultat n’est pas toujours facile à récupérer mais Python propose d’excellents outils pour cela (meilleurs que ceux de R). Certains packages, notamment json, facilitent l’extraction de champs d’une sortie d’API. Dans certains cas, des packages spécifiques à une API ont été créés pour simplifier l’écriture d’une requête ou la récupération du résultat. Par exemple, le package pynsee propose des options qui seront retranscrites automatiquement dans l’URL de requête pour faciliter le travail sur les données Insee.

2.2 Illustration avec une API de l’Ademe pour obtenir des diagnostics énergétiques

Le diagnostic de performance énergétique (DPE) renseigne sur la performance énergétique d’un logement ou d’un bâtiment, en évaluant sa consommation d’énergie et son impact en termes d’émissions de gaz à effet de serre.

Les données des performances énergétiques des bâtiments sont mises à disposition par l’Ademe. Comme ces données sont relativement volumineuses, une API peut être utile lorsqu’on ne s’intéresse qu’à un sous-champ des données. Une documentation et un espace de test de l’API sont disponibles sur le site API GOUV1.

Supposons qu’on désire récupérer une centaine de valeurs pour la commune de Villieu-Loyes-Mollon dans l’Ain (code Insee 01450).

L’API comporte plusieurs points d’entrée. Globalement, la racine commune est :

https://koumoul.com/data-fair/api/v1/datasets/dpe-france

Ensuite, en fonction de l’API désirée, on va ajouter des éléments à cette racine. En l’occurrence, on va utiliser l’API field qui permet de récupérer des lignes en fonction d’un ou plusieurs critères (pour nous, la localisation géographique):

L’exemple donné dans la documentation technique est

GET https://koumoul.com/data-fair/api/v1/datasets/dpe-france/values/{field}

ce qui en Python se traduira par l’utilisation de la méthode get du package Request sur un url dont la structure est la suivante :

  • il commencera par https://koumoul.com/data-fair/api/v1/datasets/dpe-france/values/ ;
  • il sera ensuite suivi par des paramètres de recherche. Le champ {field} commence ainsi généralement par un ? qui permet ensuite de spécifier des paramètres sous la forme nom_parameter=value

A la lecture de la documentation, les premiers paramètres qu’on désire :

  • Le nombre de pages, ce qui nous permet d’obtenir un certain nombre d’échos. On va seulement récupérer 10 pages ce qui correspond à une centaine d’échos. On va néanmoins préciser qu’on veut 100 échos
  • Le format de sortie. On va privilégier le JSON qui est un format standard dans le monde des API. Python offre beaucoup de flexibilité grâce à l’un de ses objets de base, à savoir le dictionnaire (type dict), pour manipuler de tels fichiers
  • Le code commune des données qu’on désire obtenir. Comme on l’a évoqué, on va récupérer les données dont le code commune est 01450. D’après la doc, il convient de passer le code commune sous le format: code_insee_commune_actualise:{code_commune}. Pour éviter tout risque de mauvais formatage, on va utiliser %3A pour signifier :, %2A pour signifier * et %22 pour signifier ".
  • D’autres paramètres annexes, suggérés par la documentation

Cela nous donne ainsi un URL dont la structure est la suivante :

city_code = "01450"
size = 100
api_root = "https://koumoul.com/data-fair/api/v1/datasets/dpe-france/lines"
url_api = (
    f"{api_root}?format=json&q_mode=simple&qs=code_insee_commune_actualise"
    + "%3A%22"
    + f"{city_code}"
    + "%22"
    + f"&size={size}&select="
    + "%2A&sampling=neighbors"
)

Si vous introduisez cet URL dans votre navigateur, vous devriez aboutir sur un JSON non formaté2. En Python, on peut utiliser requests pour récupérer les données3 :

import requests
import pandas as pd

req = requests.get(url_api)
wb = req.json()

Prenons par exemple les 1000 premiers caractères du résultat, pour se donner une idée du résultat et se convaincre que notre filtre au niveau communal est bien passé :

print(req.content[:1000])

b’{“total”: 121,“next”: “https://koumoul.com/data-fair/api/v1/datasets/dpe-france/lines?format=json&q_mode=simple&qs=code_insee_commune_actualise%3A%2201450%22&size=100&select=*&sampling=neighbors&after=102719%2C912454”,“results”: [\n {“classe_consommation_energie”: “E”,“tr001_modele_dpe_type_libelle”: “Vente”,“annee_construction”: 1,“_geopoint”: “45.927488,5.230195”,“latitude”: 45.927488,“surface_thermique_lot”: 106.87,“_i”: 2,“tr002_type_batiment_description”: “Maison Individuelle”,“geo_adresse”: “Rue du Chateau 01800 Villieu-Loyes-Mollon”,“_rand”: 959550,“code_insee_commune_actualise”: “01450”,“estimation_ges”: 9,“geo_score”: 0.58,“classe_estimation_ges”: “B”,“nom_methode_dpe”: “M9thode Facture”,“tv016_departement_code”: “01”,“consommation_energie”: 286,“date_etablissement_dpe”: “2013-04-15”,“longitude”: 5.230195,“_score”: null,’

Ici, il n’est même pas nécessaire en première approche d’utiliser le package json, l’information étant déjà tabulée dans l’écho renvoyé (on a la même information pour tous les pays): On peut donc se contenter de Pandas pour transformer nos données en DataFrame et Geopandas pour convertir en données géographiques :

import pandas as pd
import geopandas as gpd


def get_dpe_from_url(url):

    req = requests.get(url)
    wb = req.json()
    df = pd.json_normalize(wb["results"])

    dpe = gpd.GeoDataFrame(
        df, geometry=gpd.points_from_xy(df.longitude, df.latitude), crs=4326
    )
    dpe = dpe.dropna(subset=["longitude", "latitude"])

    return dpe


dpe = get_dpe_from_url(url_api)
dpe.head(2)
classe_consommation_energie tr001_modele_dpe_type_libelle annee_construction _geopoint latitude surface_thermique_lot _i tr002_type_batiment_description geo_adresse _rand ... classe_estimation_ges nom_methode_dpe tv016_departement_code consommation_energie date_etablissement_dpe longitude _score _id version_methode_dpe geometry
0 E Vente 1 45.927488,5.230195 45.927488 106.87 2 Maison Individuelle Rue du Chateau 01800 Villieu-Loyes-Mollon 959550 ... B Méthode Facture 01 286.0 2013-04-15 5.230195 None HJt4TdUa1W0wZiNoQkskk NaN POINT (5.2302 45.92749)
1 G Vente 1960 45.931376,5.230461 45.931376 70.78 9 Maison Individuelle 552 Rue Royale 01800 Villieu-Loyes-Mollon 681070 ... D Méthode 3CL 01 507.0 2013-04-22 5.230461 None UhMxzza1hsUo0syBh9DxH 3CL-DPE, version 1.3 POINT (5.23046 45.93138)

2 rows × 23 columns

Essayons de représenter sur une carte ces DPE avec les années de construction des logements. Avec Folium, on obtient la carte interactive suivante :

import seaborn as sns
import folium

palette = sns.color_palette("coolwarm", 8)


def interactive_map_dpe(dpe):

    # convert to number
    dpe["color"] = [
        ord(dpe.iloc[i]["classe_consommation_energie"].lower()) - 96
        for i in range(len(dpe))
    ]
    dpe = dpe.loc[dpe["color"] <= 7]
    dpe["color"] = [palette.as_hex()[x] for x in dpe["color"]]

    center = dpe[["latitude", "longitude"]].mean().values.tolist()
    sw = dpe[["latitude", "longitude"]].min().values.tolist()
    ne = dpe[["latitude", "longitude"]].max().values.tolist()

    m = folium.Map(location=center, tiles="OpenStreetMap")

    # I can add markers one by one on the map
    for i in range(0, len(dpe)):
        folium.Marker(
            [dpe.iloc[i]["latitude"], dpe.iloc[i]["longitude"]],
            popup=f"Year of construction: {dpe.iloc[i]['annee_construction']}, <br>DPE: {dpe.iloc[i]['classe_consommation_energie']}",
            icon=folium.Icon(
                color="black", icon="home", icon_color=dpe.iloc[i]["color"]
            ),
        ).add_to(m)

    m.fit_bounds([sw, ne])

    return m


m = interactive_map_dpe(dpe)
/opt/conda/lib/python3.12/site-packages/geopandas/geodataframe.py:1819: SettingWithCopyWarning:


A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
# Display the map
m
Make this Notebook Trusted to load map: File -> Trust Notebook

2.3 Un catalogue incomplet d’API existantes

De plus en plus de sites mettent des API à disposition des développeurs et autres curieux.

Pour en citer quelques-unes très connues :

Cependant, il est intéressant de ne pas se restreindre à celles-ci dont les données ne sont pas toujours les plus intéressantes. Beaucoup de producteurs de données, privés comme publics, mettent à disposition leurs données sous forme d’API.

  • API gouv : beaucoup d’API officielles de l’Etat français et accès à de la documentation
  • Insee : https://api.insee.fr/catalogue/ et pynsee
  • Pôle Emploi : https://www.emploi-store-dev.fr/portail-developpeur-cms/home.html
  • SNCF : https://data.sncf.com/api
  • Banque Mondiale : https://datahelpdesk.worldbank.org/knowledgebase/topics/125589

3 L’API DVF : accéder à des données de transactions immobilières simplement

Important

Cette partie nécessite une mise à jour pour privilégier l’API DVF du Cerema

Le site DVF (demandes de valeurs foncières) permet de visualiser toutes les données relatives aux mutations à titre onéreux (ventes de maisons, appartements, garages…) réalisées durant les 5 dernières années.

Un site de visualisation est disponible sur https://app.dvf.etalab.gouv.fr/.

Ce site est très complet quand il s’agit de connaître le prix moyen au mètre carré d’un quartier ou de comparer des régions entre elles. L’API DVF permet d’aller plus loin afin de récupérer les résultats dans un logiciel de traitement de données. Elle a été réalisée par Christian Quest et le code source est disponible sur Github .

Les critères de recherche sont les suivants : - code_commune = code INSEE de la commune (ex: 94068) - section = section cadastrale (ex: 94068000CQ) - numero_plan = identifiant de la parcelle, (ex: 94068000CQ0110) - lat + lon + dist (optionnel): pour une recherche géographique, dist est par défaut un rayon de 500m - code_postal

Les filtres de sélection complémentaires : - nature_mutation (Vente, etc) - type_local (Maison, Appartement, Local, Dépendance)

Les requêtes sont de la forme : http://api.cquest.org/dvf?code_commune=29168.

Exercice 1 : Exploiter l’API DVF
  1. Rechercher toutes les transactions existantes dans DVF à Plogoff (code commune 29168, en Bretagne). Afficher les clés du JSON et en déduire le nombre de transactions répertoriées.
  2. N’afficher que les transactions portant sur des maisons.
  3. Utiliser l’API geo pour récupérer le découpage communal de la ville de Plogoff.
  4. Représenter l’histogramme des prix de vente.

N’hésitez pas à aller plus loin en jouant sur des variables de groupes par exemple.

Le résultat de la question 2 devrait ressembler au DataFrame suivant :

L’histogramme des prix de vente (question 4) aura l’aspect suivant :

On va faire une carte des ventes en affichant le prix de l’achat. La cartographie réactive sera présentée dans les chapitres consacrés à la visualisation de données.

Supposons que le DataFrame des ventes s’appelle ventes. Il faut d’abord le convertir en objet geopandas.

code_service_ch reference_document articles_1 articles_2 articles_3 articles_4 articles_5 numero_disposition date_mutation nature_mutation ... nombre_pieces_principales nature_culture nature_culture_speciale surface_terrain lat lon geom.type geom.coordinates geom geometry
0 None None None None None None None 000001 2017-09-29 Vente ... 0.0 None None NaN 48.037810 -4.717967 Point [-4.717967, 48.03781] NaN POINT (-4.71797 48.03781)
1 None None None None None None None 000001 2018-07-29 Vente ... 0.0 None None NaN 48.037810 -4.717967 Point [-4.717967, 48.03781] NaN POINT (-4.71797 48.03781)
2 None None None None None None None 000001 2014-10-30 Vente ... NaN T None 1240.0 48.042296 -4.709488 Point [-4.709488, 48.042296] NaN POINT (-4.70949 48.0423)
3 None None None None None None None 000001 2014-10-30 Vente ... NaN T None 630.0 48.043125 -4.706963 Point [-4.706963, 48.043125] NaN POINT (-4.70696 48.04312)
4 None None None None None None None 000001 2015-06-25 Vente ... NaN J None 78.0 48.042232 -4.705553 Point [-4.705553, 48.042232] NaN POINT (-4.70555 48.04223)
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
434 None None None None None None None 000001 2015-09-01 Vente ... NaN T None 1595.0 48.037084 -4.712427 Point [-4.712427, 48.037084] NaN POINT (-4.71243 48.03708)
435 None None None None None None None 000001 2015-09-01 Vente ... 4.0 S None 555.0 48.037312 -4.712316 Point [-4.712316, 48.037312] NaN POINT (-4.71232 48.03731)
436 None None None None None None None 000001 2015-09-01 Vente ... 0.0 S None 555.0 48.037312 -4.712316 Point [-4.712316, 48.037312] NaN POINT (-4.71232 48.03731)
437 None None None None None None None 000001 2015-09-01 Vente ... NaN T None 595.0 48.037271 -4.711856 Point [-4.711856, 48.037271] NaN POINT (-4.71186 48.03727)
438 None None None None None None None 000001 2014-10-30 Vente ... NaN L None 850.0 48.033956 -4.716009 Point [-4.716009, 48.033956] NaN POINT (-4.71601 48.03396)

431 rows × 49 columns

Avant de faire une carte, on va convertir les limites de la commune de Plogoff en geoJSON pour faciliter sa représentation avec folium (voir la doc Geopandas à ce propos):

Pour représenter graphiquement, on peut utiliser le code suivant (essayez de le comprendre et pas uniquement de l’exécuter).

Make this Notebook Trusted to load map: File -> Trust Notebook

4 Géocoder des données grâce aux API officielles

Pour pouvoir faire cet exercice

!pip install xlrd

Jusqu’à présent, nous avons travaillé sur des données où la dimension géographique était déjà présente ou relativement facile à intégrer.

Ce cas idéal ne se rencontre pas nécessairement dans la pratique. On dispose parfois de localisations plus ou moins précises et plus ou moins bien formattées pour déterminer la localisation de certains lieux.

Depuis quelques années, un service officiel de géocodage a été mis en place. Celui-ci est gratuit et permet de manière efficace de coder des adresses à partir d’une API. Cette API, connue sous le nom de la Base d’Adresses Nationale (BAN) a bénéficié de la mise en commun de données de plusieurs acteurs (collectivités locales, Poste) et de compétences d’acteurs comme Etalab. La documentation de celle-ci est disponible à l’adresse https://api.gouv.fr/les-api/base-adresse-nationale.

Pour illustrer la manière de géocoder des données avec Python, nous allons partir de la base des résultats des auto-écoles à l’examen du permis sur l’année 2018.

Ces données nécessitent un petit peu de travail pour être propres à une analyse statistique. Après avoir renommé les colonnes, nous n’allons conserver que les informations relatives au permis B (permis voiture classique) et les auto-écoles ayant présenté au moins 20 personnes à l’examen.

import pandas as pd
import xlrd
import geopandas as gpd

df = pd.read_excel(
    "https://www.data.gouv.fr/fr/datasets/r/d4b6b072-8a7d-4e04-a029-8cdbdbaf36a5",
    header=[0, 1],
)

# The Excel file has nested column names,
# we clean it
index_0 = [
    "" if df.columns[i][0].startswith("Unnamed") else df.columns[i][0]
    for i in range(len(df.columns))
]
index_1 = [df.columns[i][1] for i in range(len(df.columns))]
keep_index = [True if el in ("", "B") else False for el in index_0]
cols = [index_0[i] + " " + index_1[i].replace("+", "_") for i in range(len(df.columns))]
df.columns = cols
df = df.loc[:, keep_index]
df.columns = df.columns.str.replace("(^ |°)", "", regex=True).str.replace(" ", "_")

# We keep the subsample of interest
df = df.dropna(subset=["B_NB"])
df = df.loc[~df["B_NB"].astype(str).str.contains(r"(\%|\.)", regex=True), :]
df["B_NB"] = df["B_NB"].astype(int)
df["B_TR"] = (
    df["B_TR"].str.replace(",", ".").str.replace("%", "", regex=True).astype(float)
)
df = df.loc[df["B_NB"] > 20]
/tmp/ipykernel_1557/453192817.py:19: UserWarning:

This pattern is interpreted as a regular expression, and has match groups. To actually get the groups, use str.extract.

In this sample, the average success rate in 2018 was 58.02%

Nos informations géographiques prennent la forme suivante :

df.loc[:, ["Adresse", "CP", "Ville"]].head(5)
Adresse CP Ville
0 56 RUE CHARLES ROBIN 01000 BOURG EN BRESSE
2 7, avenue Revermont 01250 Ceyzeriat
3 72 PLACE DE LA MAIRIE 01000 SAINT-DENIS LES BOURG
4 6 RUE DU LYCEE 01000 BOURG EN BRESSE
5 9 place Edgard Quinet 01000 BOURG EN BRESSE

Autrement dit, nous disposons d’une adresse, d’un code postal et d’un nom de ville. Ces informations peuvent servir à faire une recherche sur la localisation d’une auto-école puis, éventuellement, de se restreindre à un sous-échantillon.

4.1 Utiliser l’API BAN

La documentation officielle de l’API propose un certain nombre d’exemples de manière de géolocaliser des données. Dans notre situation, deux points d’entrée paraissent intéressants:

  • L’API /search/ qui représente un point d’entrée avec des URL de la forme https://api-adresse.data.gouv.fr/search/?q=\<adresse\>&postcode=\<codepostal\>&limit=1
  • L’API /search/csv qui prend un CSV en entrée et retourne ce même CSV avec les observations géocodées. La requête prend la forme suivante, en apparence moins simple à mettre en oeuvre : curl -X POST -F data=@search.csv -F columns=adresse -F columns=postcode https://api-adresse.data.gouv.fr/search/csv/

La tentation serait forte d’utiliser la première méthode avec une boucle sur les lignes de notre DataFrame pour géocoder l’ensemble de notre jeu de données. Cela serait néanmoins une mauvaise idée car les communications entre notre session Python et les serveurs de l’API seraient beaucoup trop nombreuses pour offrir des performances satisfaisantes.

Pour vous en convaincre, vous pouvez exécuter le code suivant sur un petit échantillon de données (par exemple 100 comme ici) et remarquer que le temps d’exécution est assez important

import time

dfgeoloc = df.loc[:, ["Adresse", "CP", "Ville"]].apply(
    lambda s: s.str.lower().str.replace(",", " ")
)
dfgeoloc["url"] = (
    dfgeoloc["Adresse"] + "+" + dfgeoloc["Ville"].str.replace("-", "+")
).str.replace(" ", "+")
dfgeoloc["url"] = (
    "https://api-adresse.data.gouv.fr/search/?q="
    + dfgeoloc["url"]
    + "&postcode="
    + df["CP"]
    + "&limit=1"
)
dfgeoloc = dfgeoloc.dropna()

start_time = time.time()


def get_geoloc(i):
    print(i)
    return gpd.GeoDataFrame.from_features(
        requests.get(dfgeoloc["url"].iloc[i]).json()["features"]
    )


local = [get_geoloc(i) for i in range(len(dfgeoloc.head(10)))]
print("--- %s seconds ---" % (time.time() - start_time))

Comme l’indique la documentation, si on désire industrialiser notre processus de géocodage, on va privilégier l’API CSV.

Pour obtenir une requête CURL cohérente avec le format désiré par l’API on va à nouveau utiliser Requests mais cette fois avec des paramètres supplémentaires:

  • data va nous permettre de passer des paramètres à CURL (équivalents aux -F de la requête CURL) :
    • columns: Les colonnes utilisées pour localiser une donnée. En l’occurrence, on utilise l’adresse et la ville (car les codes postaux n’étant pas uniques, un même nom de voirie peut se trouver dans plusieurs villes partageant le même code postal) ;
    • postcode: Le code postal de la ville. Idéalement nous aurions utilisé le code Insee mais nous ne l’avons pas dans nos données ;
    • result_columns: on restreint les données échangées avec l’API aux colonnes qui nous intéressent. Cela permet d’accélérer les processus (on échange moins de données) et de réduire l’impact carbone de notre activité (moins de transferts = moins d’énergie dépensée). En l’occurrence, on ne ressort que les données géolocalisées et un score de confiance en la géolocalisation ;
  • files: permet d’envoyer un fichier via CURL.

Les données sont récupérées avec request.post. Comme il s’agit d’une chaîne de caractère, nous pouvons directement la lire avec Pandas en utilisant io.StringIO pour éviter d’écrire des données intermédiaires.

Le nombre d’échos semblant être limité, il est proposé de procéder par morceaux (ici, le jeu de données est découpé en 5 morceaux).

import requests
import io
import numpy as np
import time

params = {
    "columns": ["Adresse", "Ville"],
    "postcode": "CP",
    "result_columns": ["result_score", "latitude", "longitude"],
}

df[["Adresse", "CP", "Ville"]] = df.loc[:, ["Adresse", "CP", "Ville"]].apply(
    lambda s: s.str.lower().str.replace(",", " ")
)


def geoloc_chunk(x):
    dfgeoloc = x.loc[:, ["Adresse", "CP", "Ville"]]
    dfgeoloc.to_csv("datageocodage.csv", index=False)
    response = requests.post(
        "https://api-adresse.data.gouv.fr/search/csv/",
        data=params,
        files={"data": ("datageocodage.csv", open("datageocodage.csv", "rb"))},
    )
    geoloc = pd.read_csv(io.StringIO(response.text), dtype={"CP": "str"})
    return geoloc


start_time = time.time()
geodata = [geoloc_chunk(dd) for dd in np.array_split(df, 10)]
print("--- %s seconds ---" % (time.time() - start_time))

Cette méthode est beaucoup plus rapide et permet ainsi, une fois retourné à nos données initiales, d’avoir un jeu de données géolocalisé.

# Retour aux données initiales
geodata = pd.concat(geodata, ignore_index=True)
df_xy = df.merge(geodata, on=["Adresse", "CP", "Ville"])
df_xy = df_xy.dropna(subset=["latitude", "longitude"])

# Mise en forme pour le tooltip
df_xy["text"] = (
    df_xy["Raison_Sociale"]
    + "<br>"
    + df_xy["Adresse"]
    + "<br>"
    + df_xy["Ville"]
    + "<br>Nombre de candidats:"
    + df_xy["B_NB"].astype(str)
)
df_xy.filter(
    ["Raison_Sociale", "Adresse", "CP", "Ville", "latitude", "longitude"],
    axis="columns",
).sample(10)
Raison_Sociale Adresse CP Ville latitude longitude
1061 LAFONT 79 bd jean jaures 13300 salon de provence 43.639094 5.096217
6612 ACCES PERMIS PERPIGNAN 9 rue rempart villeneuve 66000 perpignan 42.700483 2.892332
4328 BERNARD / LOIRE OCEAN CONDUITE 11 route de pornic 44730 saint michel chef chef 47.178825 -2.147210
5741 AUTO-ECOLE MIQUIGNON 481 avenue du marechal leclerc 59500 douai 50.370840 3.088380
6988 ZZDES MINIMES 10 rue des farges 69005 lyon 45.756592 4.819971
6360 Vernet Christophe 24 avenue des paulines 63000 clermont-ferrand 45.774227 3.093986
58 LEVANT -AUTO-ECOLE- 1-3 chemin des fleurs 01210 ferney-voltaire 46.259500 6.116863
9483 AUTO-ECOLE PIERRE-BUFFIERE 50 avenue de la république 87260 pierre-buffiere 45.694690 1.359842
3290 EN RTE VERS PERMIS 19 rue des carmes 35120 dol de bretagne 48.551667 -1.750745
9543 4 C PERMIS 3 bis rue de l' hôpital 88350 liffol le grand 48.317915 5.581324

Il ne reste plus qu’à utiliser Geopandas et nous serons en mesure de faire une carte des localisations des auto-écoles :

# Transforme en geopandas pour les cartes
import geopandas as gpd

dfgeo = gpd.GeoDataFrame(
    df_xy, geometry=gpd.points_from_xy(df_xy.longitude, df_xy.latitude)
)

Nous allons représenter les stations dans l’Essonne avec un zoom initialement sur les villes de Massy et Palaiseau. Le code est le suivant :

import folium

# Représenter toutes les autoécoles de l'Essonne
df_91 = df_xy.loc[df_xy["Dept"] == "091"]

# Centrer la vue initiale sur Massy-Palaiseau
df_pal = df_xy.loc[df_xy["Ville"].isin(["massy", "palaiseau"])]
center = df_pal[["latitude", "longitude"]].mean().values.tolist()
sw = df_pal[["latitude", "longitude"]].min().values.tolist()
ne = df_pal[["latitude", "longitude"]].max().values.tolist()

m = folium.Map(location=center, tiles="OpenStreetMap")

# I can add marker one by one on the map
for i in range(0, len(df_91)):
    folium.Marker(
        [df_91.iloc[i]["latitude"], df_91.iloc[i]["longitude"]],
        popup=df_91.iloc[i]["text"],
        icon=folium.Icon(icon="car", prefix="fa"),
    ).add_to(m)

m.fit_bounds([sw, ne])
# Afficher la carte
m
Make this Notebook Trusted to load map: File -> Trust Notebook

Vous pouvez aller plus loin avec l’exercice suivant.

Exercice 2 : Quelles sont les auto-écoles les plus proches de chez moi ?

On va supposer que vous cherchez, dans un rayon donné autour d’un centre ville, les auto-écoles disponibles.

Fonction nécessaire pour cet exercice

Cet exercice nécessite une fonction pour créer un cercle autour d’un point (source ici). La voici :

from functools import partial
import pyproj
from shapely.ops import transform
from shapely.geometry import Point

proj_wgs84 = pyproj.Proj("+proj=longlat +datum=WGS84")


def geodesic_point_buffer(lat, lon, km):
    # Azimuthal equidistant projection
    aeqd_proj = "+proj=aeqd +lat_0={lat} +lon_0={lon} +x_0=0 +y_0=0"
    project = partial(
        pyproj.transform, pyproj.Proj(aeqd_proj.format(lat=lat, lon=lon)), proj_wgs84
    )
    buf = Point(0, 0).buffer(km * 1000)  # distance in metres
    return transform(project, buf).exterior.coords[:]
  1. Pour commencer, utiliser l’API Geo pour la ville de Palaiseau.
  2. Appliquer la fonction geodesic_point_buffer au centre ville de Palaiseau
  3. Ne conserver que les auto-écoles dans ce cercle et les ordonner

Si vous avez la réponse à la question 3, n’hésitez pas à la soumettre sur Github afin que je complète la correction 😉 !

ERROR 1: PROJ: proj_create_from_database: Open of /opt/conda/share/proj failed
Skipping field codesPostaux: unsupported OGR type: 5
/opt/conda/lib/python3.12/site-packages/shapely/ops.py:276: FutureWarning:

This function is deprecated. See: https://pyproj4.github.io/pyproj/stable/gotchas.html#upgrading-to-pyproj-2-from-pyproj-1

Pour se convaincre, de notre cercle constitué lors de la question 2, on peut représenter une carte. On a bien un cercle centré autour de Palaiseau :

5 Exercices supplémentaires

5.1 Découvrir l’API d’OpenFoodFacts

Pour vous aidez, vous pouvez regarder une exemple de structure du JSON ici : https://world.openfoodfacts.org/api/v0/product/3274080005003.json en particulier la catégorie nutriments.

Exercice 3 : Retrouver des produits dans l’openfood facts 🍕

Voici une liste de code-barres: 3274080005003, 5449000000996, 8002270014901, 3228857000906, 3017620421006, 8712100325953

Utiliser l’API d’openfoodfacts (l’API, pas depuis le CSV !) pour retrouver les produits correspondants et leurs caractéristiques nutritionnelles.

Le panier paraît-il équilibré ? 🍫

Récupérer l’URL d’une des images et l’afficher dans votre navigateur.

product_name code categories categories_tags nutriments.carbohydrates_100g nutriments.saturated-fat_100g nutriments.proteins_100g nutriments.sugars_100g nutriments.salt_100g nutriments.fat_100g nutriments.sodium_100g nutriments.energy_100g nutriments.calcium_100g nutriments.fiber_100g
0 Eau de source 3274080005003 Boissons et préparations de boissons,Boissons,... [en:beverages-and-beverages-preparations, en:b... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
0 Coca-Cola 5449000000996 Getränke und Getränkezubereitungen,Getränke,Ko... [en:beverages-and-beverages-preparations, en:b... 10.6 0.0 0.0 10.6 0.0 0.0 0.0 180.0000 NaN NaN
0 S. Pellegrino Water 8002270014901 Boissons et préparations de boissons,Boissons,... [en:beverages-and-beverages-preparations, en:b... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0001 0.164 NaN

For example, here is the photo of the product with the barcode 5449000000996. Do you recognize it?

Informations additionnelles

environment files have been tested on.

Latest built version: 2024-11-20

Python version used:

'3.12.6 | packaged by conda-forge | (main, Sep 30 2024, 18:08:52) [GCC 13.3.0]'
Package Version
affine 2.4.0
aiobotocore 2.15.1
aiohappyeyeballs 2.4.3
aiohttp 3.10.8
aioitertools 0.12.0
aiosignal 1.3.1
alembic 1.13.3
altair 5.4.1
aniso8601 9.0.1
annotated-types 0.7.0
appdirs 1.4.4
archspec 0.2.3
asttokens 2.4.1
attrs 24.2.0
babel 2.16.0
bcrypt 4.2.0
beautifulsoup4 4.12.3
black 24.8.0
blinker 1.8.2
blis 0.7.11
bokeh 3.5.2
boltons 24.0.0
boto3 1.35.23
botocore 1.35.23
branca 0.7.2
Brotli 1.1.0
cachetools 5.5.0
cartiflette 0.0.2
Cartopy 0.24.1
catalogue 2.0.10
cattrs 24.1.2
certifi 2024.8.30
cffi 1.17.1
charset-normalizer 3.3.2
click 8.1.7
click-plugins 1.1.1
cligj 0.7.2
cloudpathlib 0.20.0
cloudpickle 3.0.0
colorama 0.4.6
comm 0.2.2
commonmark 0.9.1
conda 24.9.1
conda-libmamba-solver 24.7.0
conda-package-handling 2.3.0
conda_package_streaming 0.10.0
confection 0.1.5
contextily 1.6.2
contourpy 1.3.0
cryptography 43.0.1
cycler 0.12.1
cymem 2.0.8
cytoolz 1.0.0
dask 2024.9.1
dask-expr 1.1.15
databricks-sdk 0.33.0
debugpy 1.8.6
decorator 5.1.1
Deprecated 1.2.14
diskcache 5.6.3
distributed 2024.9.1
distro 1.9.0
docker 7.1.0
duckdb 0.10.1
en-core-web-sm 3.7.1
entrypoints 0.4
et_xmlfile 2.0.0
exceptiongroup 1.2.2
executing 2.1.0
fastexcel 0.11.6
fastjsonschema 2.20.0
fiona 1.10.1
Flask 3.0.3
folium 0.17.0
fontawesomefree 6.6.0
fonttools 4.54.1
frozendict 2.4.4
frozenlist 1.4.1
fsspec 2023.12.2
funcy 2.0
gensim 4.3.2
geographiclib 2.0
geopandas 1.0.1
geoplot 0.5.1
geopy 2.4.1
gitdb 4.0.11
GitPython 3.1.43
google-auth 2.35.0
graphene 3.3
graphql-core 3.2.4
graphql-relay 3.2.0
graphviz 0.20.3
great-tables 0.12.0
greenlet 3.1.1
gunicorn 22.0.0
h2 4.1.0
hpack 4.0.0
htmltools 0.6.0
hyperframe 6.0.1
idna 3.10
imageio 2.36.0
importlib_metadata 8.5.0
importlib_resources 6.4.5
inflate64 1.0.0
ipykernel 6.29.5
ipython 8.28.0
itsdangerous 2.2.0
jedi 0.19.1
Jinja2 3.1.4
jmespath 1.0.1
joblib 1.4.2
jsonpatch 1.33
jsonpointer 3.0.0
jsonschema 4.23.0
jsonschema-specifications 2024.10.1
jupyter-cache 1.0.0
jupyter_client 8.6.3
jupyter_core 5.7.2
kaleido 0.2.1
kiwisolver 1.4.7
langcodes 3.5.0
language_data 1.3.0
lazy_loader 0.4
libmambapy 1.5.9
locket 1.0.0
lxml 5.3.0
lz4 4.3.3
Mako 1.3.5
mamba 1.5.9
mapclassify 2.8.1
marisa-trie 1.2.1
Markdown 3.6
markdown-it-py 3.0.0
MarkupSafe 2.1.5
matplotlib 3.9.2
matplotlib-inline 0.1.7
mdurl 0.1.2
menuinst 2.1.2
mercantile 1.2.1
mizani 0.11.4
mlflow 2.16.2
mlflow-skinny 2.16.2
msgpack 1.1.0
multidict 6.1.0
multivolumefile 0.2.3
munkres 1.1.4
murmurhash 1.0.10
mypy-extensions 1.0.0
narwhals 1.14.1
nbclient 0.10.0
nbformat 5.10.4
nest_asyncio 1.6.0
networkx 3.3
nltk 3.9.1
numexpr 2.10.1
numpy 1.26.4
opencv-python-headless 4.10.0.84
openpyxl 3.1.5
opentelemetry-api 1.16.0
opentelemetry-sdk 1.16.0
opentelemetry-semantic-conventions 0.37b0
OWSLib 0.28.1
packaging 24.1
pandas 2.2.3
paramiko 3.5.0
parso 0.8.4
partd 1.4.2
pathspec 0.12.1
patsy 0.5.6
Pebble 5.0.7
pexpect 4.9.0
pickleshare 0.7.5
pillow 10.4.0
pip 24.2
platformdirs 4.3.6
plotly 5.24.1
plotnine 0.13.6
pluggy 1.5.0
polars 1.8.2
preshed 3.0.9
prometheus_client 0.21.0
prometheus_flask_exporter 0.23.1
prompt_toolkit 3.0.48
protobuf 4.25.3
psutil 6.0.0
ptyprocess 0.7.0
pure_eval 0.2.3
py7zr 0.20.8
pyarrow 17.0.0
pyarrow-hotfix 0.6
pyasn1 0.6.1
pyasn1_modules 0.4.1
pybcj 1.0.2
pycosat 0.6.6
pycparser 2.22
pycryptodomex 3.21.0
pydantic 2.9.2
pydantic_core 2.23.4
Pygments 2.18.0
pyLDAvis 3.4.1
PyNaCl 1.5.0
pynsee 0.1.8
pyogrio 0.10.0
pyOpenSSL 24.2.1
pyparsing 3.1.4
pyppmd 1.1.0
pyproj 3.7.0
pyshp 2.3.1
PySocks 1.7.1
python-dateutil 2.9.0
python-dotenv 1.0.1
python-magic 0.4.27
pytz 2024.1
pyu2f 0.1.5
pywaffle 1.1.1
PyYAML 6.0.2
pyzmq 26.2.0
pyzstd 0.16.2
querystring_parser 1.2.4
rasterio 1.4.2
referencing 0.35.1
regex 2024.9.11
requests 2.32.3
requests-cache 1.2.1
retrying 1.3.4
rich 13.9.4
rpds-py 0.21.0
rsa 4.9
ruamel.yaml 0.18.6
ruamel.yaml.clib 0.2.8
s3fs 2023.12.2
s3transfer 0.10.2
scikit-image 0.24.0
scikit-learn 1.5.2
scipy 1.13.0
seaborn 0.13.2
setuptools 74.1.2
shapely 2.0.6
shellingham 1.5.4
six 1.16.0
smart-open 7.0.5
smmap 5.0.0
sortedcontainers 2.4.0
soupsieve 2.5
spacy 3.7.5
spacy-legacy 3.0.12
spacy-loggers 1.0.5
SQLAlchemy 2.0.35
sqlparse 0.5.1
srsly 2.4.8
stack-data 0.6.2
statsmodels 0.14.4
tabulate 0.9.0
tblib 3.0.0
tenacity 9.0.0
texttable 1.7.0
thinc 8.2.5
threadpoolctl 3.5.0
tifffile 2024.9.20
toolz 1.0.0
topojson 1.9
tornado 6.4.1
tqdm 4.66.5
traitlets 5.14.3
truststore 0.9.2
typer 0.13.1
typing_extensions 4.12.2
tzdata 2024.2
Unidecode 1.3.8
url-normalize 1.4.3
urllib3 1.26.20
wasabi 1.1.3
wcwidth 0.2.13
weasel 0.4.1
webdriver-manager 4.0.2
websocket-client 1.8.0
Werkzeug 3.0.4
wheel 0.44.0
wordcloud 1.9.3
wrapt 1.16.0
xgboost 2.1.1
xlrd 2.0.1
xyzservices 2024.9.0
yarl 1.13.1
yellowbrick 1.5
zict 3.0.0
zipp 3.20.2
zstandard 0.23.0

View file history

SHA Date Author Description
e56a219 2024-10-30 17:13:03 Lino Galiana Intro partie modélisation & typo geopandas (#571)
9d8e69c 2024-10-21 17:10:03 lgaliana update badges shortcode for all manipulation part
47a0770 2024-08-23 07:51:58 linogaliana fix API notebook
1953609 2024-08-12 16:18:19 linogaliana One button is enough
783a278 2024-08-12 11:07:18 Lino Galiana Traduction API (#538)
580cba7 2024-08-07 18:59:35 Lino Galiana Multilingual version as quarto profile (#533)
101465f 2024-08-07 13:56:35 Lino Galiana regex, webscraping and API chapters in 🇬🇧 (#532)
065b0ab 2024-07-08 11:19:43 Lino Galiana Nouveaux callout dans la partie manipulation (#513)
06d003a 2024-04-23 10:09:22 Lino Galiana Continue la restructuration des sous-parties (#492)
8c316d0 2024-04-05 19:00:59 Lino Galiana Fix cartiflette deprecated snippets (#487)
005d89b 2023-12-20 17:23:04 Lino Galiana Finalise l’affichage des statistiques Git (#478)
3fba612 2023-12-17 18:16:42 Lino Galiana Remove some badges from python (#476)
a06a268 2023-11-23 18:23:28 Antoine Palazzolo 2ème relectures chapitres ML (#457)
b68369d 2023-11-18 18:21:13 Lino Galiana Reprise du chapitre sur la classification (#455)
889a71b 2023-11-10 11:40:51 Antoine Palazzolo Modification TP 3 (#443)
04ce567 2023-10-23 19:04:01 Lino Galiana Mise en forme chapitre API (#442)
3eb0aeb 2023-10-23 11:59:24 Thomas Faria Relecture jusqu’aux API (#439)
a771183 2023-10-09 11:27:45 Antoine Palazzolo Relecture TD2 par Antoine (#418)
a63319a 2023-10-04 15:29:04 Lino Galiana Correction du TP numpy (#419)
154f09e 2023-09-26 14:59:11 Antoine Palazzolo Des typos corrigées par Antoine (#411)
3bdf3b0 2023-08-25 11:23:02 Lino Galiana Simplification de la structure 🤓 (#393)
130ed71 2023-07-18 19:37:11 Lino Galiana Restructure les titres (#374)
f0c583c 2023-07-07 14:12:22 Lino Galiana Images viz (#371)
ef28fef 2023-07-07 08:14:42 Lino Galiana Listing pour la première partie (#369)
f21a24d 2023-07-02 10:58:15 Lino Galiana Pipeline Quarto & Pages 🚀 (#365)
62aeec1 2023-06-10 17:40:39 Lino Galiana Avertissement sur la partie API (#358)
38693f6 2023-04-19 17:22:36 Lino Galiana Rebuild visualisation part (#357)
3248633 2023-02-18 13:11:52 Lino Galiana Shortcode rawhtml (#354)
3c880d5 2022-12-27 17:34:59 Lino Galiana Chapitre regex + Change les boites dans plusieurs chapitres (#339)
f5f0f9c 2022-11-02 19:19:07 Lino Galiana Relecture début partie modélisation KA (#318)
2dc82e7 2022-10-18 22:46:47 Lino Galiana Relec Kim (visualisation + API) (#302)
f10815b 2022-08-25 16:00:03 Lino Galiana Notebooks should now look more beautiful (#260)
494a85a 2022-08-05 14:49:56 Lino Galiana Images featured ✨ (#252)
d201e3c 2022-08-03 15:50:34 Lino Galiana Pimp la homepage ✨ (#249)
1239e3e 2022-06-21 14:05:15 Lino Galiana Enonces (#239)
bb38643 2022-06-08 16:59:40 Lino Galiana Répare bug leaflet (#234)
5698e30 2022-06-03 18:28:37 Lino Galiana Finalise widget (#232)
7b9f27b 2022-06-03 17:05:15 Lino Galiana Essaie régler les problèmes widgets JS (#231)
1ca1a8a 2022-05-31 11:44:23 Lino Galiana Retour du chapitre API (#228)
Retour au sommet

Notes de bas de page

  1. La documentation est également disponible ici↩︎

  2. JSON is a highly appreciated format in the field of big data because it allows stacking incomplete data. It is one of the preferred formats of the No-SQL paradigm, for which this excellent course offers a gentle introduction.↩︎

  3. Depending on the API, we either need nothing more if we directly obtain a JSON, or we may need to use a parser like BeautifulSoup otherwise. Here, the JSON can be formatted relatively easily.↩︎

Citation

BibTeX
@book{galiana2023,
  author = {Galiana, Lino},
  title = {Python pour la data science},
  date = {2023},
  url = {https://pythonds.linogaliana.fr/},
  doi = {10.5281/zenodo.8229676},
  langid = {fr}
}
Veuillez citer ce travail comme suit :
Galiana, Lino. 2023. Python pour la data science. https://doi.org/10.5281/zenodo.8229676.