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
Author

Lino Galiana

Published

2024-04-27

La partie utilisant l’API DVF n’est plus à jour, elle sera mise à jour prochainement.

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

1.1 Définition

Pour expliquer le principe d’une API, je vais reprendre le début de la fiche dédiée dans la documentation collaborative 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 energé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 terme 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 :

code_commune = "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"{code_commune}"
    + "%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 pandas
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.23020 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 in 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 marker 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"Année de 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/mamba/lib/python3.11/site-packages/geopandas/geodataframe.py:1443: 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
# Afficher la carte
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

⚠️ 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.

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.

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).

# Afficher la carte
m

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],
)

# Le Excel a des noms de colonne emboitées,
# on nettoie
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(" ", "_")

# On garde le sous-échantillon d'intérêt
df = df.dropna(subset=["B_NB"])
df = df.loc[~df["B_NB"].astype(str).str.contains("(\%|\.)"), :]
df["B_NB"] = df["B_NB"].astype(int)
df["B_TR"] = df["B_TR"].str.replace(",", ".").str.replace("%", "").astype(float)
df = df.loc[df["B_NB"] > 20]
/tmp/ipykernel_4110/3216706257.py:19: UserWarning:

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

Sur cet échantillon, le taux de réussite moyen était, en 2018, de 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
7341 ZZZ FLASH A.E. 20 rue gambetta 71230 saint vallier 46.642323 4.371436
4465 AUTO-MOTO-ECOLE J CAYROL 21 rue des maquisards 46100 figeac 44.610088 2.030264
8286 Espace Permis Auto Ecole 157 ter avenue du général de gaulle 77270 villparisis 48.950274 2.603094
983 AUTO ECOLE MARIE CLAIRE rue romain rolland 13110 port de bouc 43.410340 4.991963
10512 S F P R LA PASSERELLE 1 place hector berlioz 94190 villeneuve st georges 48.745889 2.446784
880 Top Conduite 90 c avenue de mazargues 13008 marseille 43.260046 5.393932
3706 EVOLUTION AUTO ECOLE place de la bascule 38440 chatonnay 45.487638 5.202870
994 ECF GARDANNE 8 place gueydan 13120 gardanne 43.454325 5.468314
9761 EBR 38 rue berlioz 91240 st michel sur orge 48.636346 2.321267
3453 ECOLE DE CONDUITE GRANDCLERC 15 rue jean lurcat 36700 chatillon sur indre 46.986832 1.168192

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])

Ce qui permet d’obtenir la carte:

# Afficher la carte
m
Make this Notebook Trusted to load map: File -> Trust Notebook

Vous pouvez aller plus loin avec l’exercice suivant.

ERROR 1: PROJ: proj_create_from_database: Open of /opt/mamba/share/proj failed
/opt/mamba/lib/python3.11/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.

Voici par exemple la photo du produit ayant le code-barre 5449000000996. Vous le reconnaissez ?

Informations additionnelles

environment files have been tested on.

Latest built version: 2024-04-27

Python version used:

'3.11.6 | packaged by conda-forge | (main, Oct  3 2023, 10:40:35) [GCC 12.3.0]'
Package Version
affine 2.4.0
aiobotocore 2.12.2
aiohttp 3.9.3
aioitertools 0.11.0
aiosignal 1.3.1
alembic 1.13.1
aniso8601 9.0.1
annotated-types 0.6.0
appdirs 1.4.4
archspec 0.2.3
astroid 3.1.0
asttokens 2.4.1
attrs 23.2.0
Babel 2.14.0
bcrypt 4.1.2
beautifulsoup4 4.12.3
black 24.4.2
blinker 1.7.0
blis 0.7.11
bokeh 3.4.0
boltons 23.1.1
boto3 1.34.51
botocore 1.34.51
branca 0.7.1
Brotli 1.1.0
cachetools 5.3.3
cartiflette 0.0.2
Cartopy 0.23.0
catalogue 2.0.10
cattrs 23.2.3
certifi 2024.2.2
cffi 1.16.0
charset-normalizer 3.3.2
chromedriver-autoinstaller 0.6.4
click 8.1.7
click-plugins 1.1.1
cligj 0.7.2
cloudpathlib 0.16.0
cloudpickle 3.0.0
colorama 0.4.6
comm 0.2.2
commonmark 0.9.1
conda 24.3.0
conda-libmamba-solver 24.1.0
conda-package-handling 2.2.0
conda_package_streaming 0.9.0
confection 0.1.4
contextily 1.6.0
contourpy 1.2.1
cryptography 42.0.5
cycler 0.12.1
cymem 2.0.8
cytoolz 0.12.3
dask 2024.4.1
dask-expr 1.0.10
debugpy 1.8.1
decorator 5.1.1
dill 0.3.8
distributed 2024.4.1
distro 1.9.0
docker 7.0.0
duckdb 0.10.1
en-core-web-sm 3.7.1
entrypoints 0.4
et-xmlfile 1.1.0
exceptiongroup 1.2.0
executing 2.0.1
fastjsonschema 2.19.1
fiona 1.9.6
flake8 7.0.0
Flask 3.0.2
folium 0.16.0
fontawesomefree 6.5.1
fonttools 4.51.0
frozenlist 1.4.1
fsspec 2023.12.2
GDAL 3.8.4
gensim 4.3.2
geographiclib 2.0
geopandas 0.12.2
geoplot 0.5.1
geopy 2.4.1
gitdb 4.0.11
GitPython 3.1.43
google-auth 2.29.0
graphene 3.3
graphql-core 3.2.3
graphql-relay 3.2.0
graphviz 0.20.3
great-tables 0.5.0
greenlet 3.0.3
gunicorn 21.2.0
h11 0.14.0
htmltools 0.5.1
hvac 2.1.0
idna 3.6
imageio 2.34.1
importlib_metadata 7.1.0
importlib_resources 6.4.0
inflate64 1.0.0
ipykernel 6.29.3
ipython 8.22.2
ipywidgets 8.1.2
isort 5.13.2
itsdangerous 2.1.2
jedi 0.19.1
Jinja2 3.1.3
jmespath 1.0.1
joblib 1.3.2
jsonpatch 1.33
jsonpointer 2.4
jsonschema 4.21.1
jsonschema-specifications 2023.12.1
jupyter-cache 1.0.0
jupyter_client 8.6.1
jupyter_core 5.7.2
jupyterlab_widgets 3.0.10
kaleido 0.2.1
kiwisolver 1.4.5
kubernetes 29.0.0
langcodes 3.4.0
language_data 1.2.0
lazy_loader 0.4
libmambapy 1.5.7
llvmlite 0.42.0
locket 1.0.0
lxml 5.2.1
lz4 4.3.3
Mako 1.3.2
mamba 1.5.7
mapclassify 2.6.1
marisa-trie 1.1.0
Markdown 3.6
MarkupSafe 2.1.5
matplotlib 3.8.3
matplotlib-inline 0.1.6
mccabe 0.7.0
menuinst 2.0.2
mercantile 1.2.1
mizani 0.11.2
mlflow 2.11.3
mlflow-skinny 2.11.3
msgpack 1.0.7
multidict 6.0.5
multivolumefile 0.2.3
munkres 1.1.4
murmurhash 1.0.10
mypy 1.9.0
mypy-extensions 1.0.0
nbclient 0.10.0
nbformat 5.10.4
nest_asyncio 1.6.0
networkx 3.3
nltk 3.8.1
numba 0.59.1
numpy 1.26.4
oauthlib 3.2.2
opencv-python-headless 4.9.0.80
openpyxl 3.1.2
outcome 1.3.0.post0
OWSLib 0.28.1
packaging 23.2
pandas 2.2.1
paramiko 3.4.0
parso 0.8.4
partd 1.4.1
pathspec 0.12.1
patsy 0.5.6
Pebble 5.0.7
pexpect 4.9.0
pickleshare 0.7.5
pillow 10.3.0
pip 24.0
pkgutil_resolve_name 1.3.10
platformdirs 4.2.0
plotly 5.19.0
plotnine 0.13.5
pluggy 1.4.0
polars 0.20.18
preshed 3.0.9
prometheus_client 0.20.0
prometheus-flask-exporter 0.23.0
prompt-toolkit 3.0.42
protobuf 4.25.3
psutil 5.9.8
ptyprocess 0.7.0
pure-eval 0.2.2
py7zr 0.20.8
pyarrow 15.0.0
pyarrow-hotfix 0.6
pyasn1 0.5.1
pyasn1-modules 0.3.0
pybcj 1.0.2
pycodestyle 2.11.1
pycosat 0.6.6
pycparser 2.21
pycryptodomex 3.20.0
pydantic 2.7.1
pydantic_core 2.18.2
pyflakes 3.2.0
Pygments 2.17.2
PyJWT 2.8.0
pylint 3.1.0
PyNaCl 1.5.0
pynsee 0.1.7
pyOpenSSL 24.0.0
pyparsing 3.1.2
pyppmd 1.1.0
pyproj 3.6.1
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.0
PyYAML 6.0.1
pyzmq 25.1.2
pyzstd 0.15.10
QtPy 2.4.1
querystring-parser 1.2.4
rasterio 1.3.10
referencing 0.34.0
regex 2023.12.25
requests 2.31.0
requests-cache 1.2.0
requests-oauthlib 2.0.0
rpds-py 0.18.0
rsa 4.9
Rtree 1.2.0
ruamel.yaml 0.18.6
ruamel.yaml.clib 0.2.8
s3fs 2023.12.2
s3transfer 0.10.1
scikit-image 0.23.2
scikit-learn 1.4.1.post1
scipy 1.13.0
seaborn 0.13.2
selenium 4.20.0
setuptools 69.2.0
shapely 2.0.3
six 1.16.0
smart-open 6.4.0
smmap 5.0.0
sniffio 1.3.1
snuggs 1.4.7
sortedcontainers 2.4.0
soupsieve 2.5
spacy 3.7.4
spacy-legacy 3.0.12
spacy-loggers 1.0.5
SQLAlchemy 2.0.29
sqlparse 0.4.4
srsly 2.4.8
stack-data 0.6.2
statsmodels 0.14.1
tabulate 0.9.0
tblib 3.0.0
tenacity 8.2.3
texttable 1.7.0
thinc 8.2.3
threadpoolctl 3.4.0
tifffile 2024.4.24
tomli 2.0.1
tomlkit 0.12.4
toolz 0.12.1
topojson 1.8
tornado 6.4
tqdm 4.66.2
traitlets 5.14.2
trio 0.25.0
trio-websocket 0.11.1
truststore 0.8.0
typer 0.9.4
typing_extensions 4.11.0
tzdata 2024.1
Unidecode 1.3.8
url-normalize 1.4.3
urllib3 1.26.18
wasabi 1.1.2
wcwidth 0.2.13
weasel 0.3.4
webcolors 1.13
webdriver-manager 4.0.1
websocket-client 1.7.0
Werkzeug 3.0.2
wheel 0.43.0
widgetsnbextension 4.0.10
wordcloud 1.9.3
wrapt 1.16.0
wsproto 1.2.0
xgboost 2.0.3
xlrd 2.0.1
xyzservices 2024.4.0
yarl 1.9.4
yellowbrick 1.5
zict 3.0.0
zipp 3.17.0
zstandard 0.22.0

View file history

SHA Date Author Description
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)
Back to top

Footnotes

  1. La documentation est également disponible ici↩︎

  2. Le JSON est un format très apprécié dans le domaine du big data car il permet d’empiler des données qui ne sont pas complètes. Il s’agit d’un des formats privilégiés du paradigme No-SQL pour lequel cet excellent cours propose plus de détails.↩︎

  3. Suivant les API, nous avons soit besoin de rien de plus si nous parvenons directement à obtenir un json, soit devoir utiliser un parser comme BeautifulSoup dans le cas contraire. Ici, le JSON peut être formaté relativement aisément.↩︎

Citation

BibTeX citation:
@book{galiana2023,
  author = {Galiana, Lino},
  title = {Python Pour La Data Science},
  date = {2023},
  url = {https://pythonds.linogaliana.fr/},
  doi = {10.5281/zenodo.8229676},
  langid = {en}
}
For attribution, please cite this work as:
Galiana, Lino. 2023. Python Pour La Data Science. https://doi.org/10.5281/zenodo.8229676.