Affichez vos données sur une Google Map avec python

Affichage interactif de données géographiques : prix de l'immobilier aux environs de Genève.

real estate transactions displayed on a google map

Dans cet article, vous apprendrez comment superposer vos données à une Google Map dynamique.

Comme exemple, nous utiliserons un dataset contenant toutes les ventes immobilières de 2018 et 2019 dans le pays de Gex, aux environs de Genève. Si vous souhaitez juste voir les prix, vous trouverez une carte interactive à la fin de l'article.

Les datasets géographiques sont partout. En fait, dès que l'on mesure quelque chose à un endroit particulier dans le monde, le dataset devient géographique. Pensez au recensement, à l'immobilier, à un système distribué de senseurs IOT, aux données géologiques ou météorologiques, etc.

Pour comprendre et utiliser ces datasets, vous devez pouvoir les afficher ou les segmenter en fonction des coordonnées géographiques. Et dès que vous faites ça, leurs caractéristiques vous sautent aux yeux. Vous verrez les problèmes de vos données et pourrez les nettoyer, et vous pourrez commencer à penser aux moyens d'extraire des informations précieuses de ces datasets.

Voici donc ce que nous allons faire:

  • Installation : configuration de python pour cet exercice ;
  • Google Map API Key : obtention d'une clef d'identification permettant d'utiliser l'API Google Map depuis une application ou un site web ;
  • Préparation des données : utilisation de pandas pour lire le dataset depuis un fichier, et premiers regards sur le dataset avant affichage ;
  • Affichage d'une Google Map avec vos données : nous créerons un bel affichage interactif grâce à bokeh.

Installation

Dans l'article Interactive Visualization with Bokeh in a Jupyter Notebook, nous avons vu comment utiliser bokeh pour créer facilement des visualisations interactives. Et dans Simple Text Mining with Pandas, je vous ai donné une première introduction à pandas, qui permet de traiter et d'analyser efficacement les données, en quelques lignes de code.

Comme nous l'avons dit, nous utiliserons pandas pour le traitement des données et bokeh pour la visualisation. Nous allons donc créer un nouvel environnement Anaconda avec ces deux outils.

Tout d'abord, installez Anaconda si vous ne l'avez pas encore fait. Puis créez le nouvel environnement et activez-le :

conda create --name geovis python=3.7 
conda activate geovis

Ensuite, installez les packages dont nous aurons besoin :

conda install pandas bokeh jupyter

Obtenez une clef d'API Google Map

La clef d'API est nécessaire à la création de cartes Google Map depuis une application ou un site web comme celui-ci.

Pour l'obtenir, suivez les instructions de Google.

Avant de commencer, veuillez noter que l'API Google Map est payante. Mais Google nous offre 200 dollars par mois de crédit gratuit, ce qui est amplement suffisant pour suivre ce tuto, et même pour une utilisation raisonnée de l'API. Par exemple, cette page web ne me coûtera rien, car le traffic vers cette page est trop faible.

Après avoir créé votre clef, placez son identifiant dans une variable d'environnement, que nous lirons plus tard pour dessiner les cartes :

export GOOGLE_API_KEY=<votre_clef>

On commence par importer pandas et par préparer bokeh pour un affichage intégré au jupyter notebook :

In [1]:
import pandas as pd
from bokeh.io import output_notebook
output_notebook()
bokeh_width, bokeh_height = 500,400
Loading BokehJS ...

Ensuite, on charge nos données dans une dataframe pandas, et on imprime les premières lignes :

In [2]:
df = pd.read_csv('dvf_gex.csv')
df.head()
Out[2]:
Unnamed: 0 price area_tot area_build lon lat
0 83897 741139.0 386.0 0.0 6.072922 46.319225
1 83912 716500.0 2731.0 0.0 6.072922 46.319225
2 83927 15000.0 727.0 0.0 6.072922 46.319225
3 83957 741139.0 338.0 0.0 6.068902 46.323598
4 83997 582000.0 4643.0 0.0 6.072211 46.316697

Chaque ligne de la dataframe correspond à un transfert de propriété immobilière. Et voici une description des différentes colonnnes :

  • price : prix de vente de la propriété ;
  • area_tot : surface totale de la parcelle ;
  • area_build : surface du bâti ;
  • lon : longitude ;
  • lat : latitude ;

La première colonne est l'index de la dataframe df et la seconde colonne est l'index qui était utilisé dans la dataframe de laquelle j'ai extrait ce petit échantillon. Vous pouvez simplement les oublier.

Nous allons commencer par afficher une simple carte Google Map dynamique, puis nous améliorerons notre visualisation progressivement. Enfin, nous rajouterons nos données immobilières.

Carte Google Map Dynamique dans un Notebook Jupyter

Tout d'abord, nous devons choisir des coordonnées pour le centre de la carte. J'ai décidé d'utiliser celles de Saint-Genis-Pouilly, qui se trouve au milieu de la zone qui nous intéresse. Pour trouver les coordonnées d'un lieu, il vous suffit de chercher sur google le nom de ce lieu, suivi des mots clés "lat lon". Voici ce que j'ai obtenu :

In [3]:
lat, lon = 46.2437, 6.0251

Il nous faut ensuite lire la clef de l'API Google Map depuis la variable d'environnement (voir ci-dessus) :

In [4]:
import os 
api_key = os.environ['GOOGLE_API_KEY']

On importe les outils bokeh nécessaires, puis on crée une petite fonction pour afficher la carte :

In [5]:
from bokeh.io import show
from bokeh.plotting import gmap
from bokeh.models import GMapOptions

def plot(lat, lng, zoom=10, map_type='roadmap'):
    gmap_options = GMapOptions(lat=lat, lng=lng, 
                               map_type=map_type, zoom=zoom)
    p = gmap(api_key, gmap_options, title='Pays de Gex', 
             width=bokeh_width, height=bokeh_height)
    show(p)
    return p

Et nous appelons cette fonction :

In [6]:
p = plot(lat, lon)

Vous pouvez maintenant essayer d'appeler cette fonction avec différents arguments. Par exemple, vous pouvez utiliser des coordonnées différentes pour le centre de la carte (peut-être celles de l'endroit où vous vous trouvez?), un niveau de zoom différent, ou un autre type de carte (essayez satellite ou terrain).

Maintenant, rajoutons un marqueur pour indiquer le centre de la carte :

In [7]:
def plot(lat, lng, zoom=10, map_type='roadmap'):
    gmap_options = GMapOptions(lat=lat, lng=lng, 
                               map_type=map_type, zoom=zoom)
    p = gmap(api_key, gmap_options, title='Pays de Gex', 
             width=bokeh_width, height=bokeh_height)
    # attention, la longitude est en abscisse ;-)
    center = p.circle([lng], [lat], size=10, alpha=0.5, color='red')
    show(p)
    return p

p = plot(lat, lon, map_type='terrain')

Vous pouvez utiliser la barre d'outils sur la droite de la carte pour activer l'outil de déplacement, de zoom molette souris, et de remise à zéro.

Superposition de Données à une Google Map

Ce n'est en fait pas beaucoup plus difficile que ce que nous avons déjà fait !

Nous devons juste déclarer une ColumnDataSource bokeh pour les données que nous souhaitons superposer, à partir de notre dataframe. Une fois que c'est fait, il suffit de dire à bokeh quelles colonnes utiliser pour les coordonnées x et y.

Mais avant de faire cela, nous devons d'abord vérifier le nombre de points que nous nous apprêtons à afficher. En effet, il faut garder à l'esprit que bokeh enverra tous ces points au navigateur client. Si vous en envoyez trop, vous allez le tuer! Voyons voir :

In [8]:
df.shape
Out[8]:
(3031, 6)

Seulement 3000 points, c'est parfait. Grosso modo, vous pouvez vous permettrer d'afficher de cette façon jusqu'à 50 000 points. Si vous en avez plus, vous devrez faire appel à d'autre outils, et nous verrons cela dans un futur article.

In [9]:
from bokeh.models import ColumnDataSource

def plot(lat, lng, zoom=10, map_type='roadmap'):
    gmap_options = GMapOptions(lat=lat, lng=lng, 
                               map_type=map_type, zoom=zoom)
    p = gmap(api_key, gmap_options, title='Pays de Gex', 
             width=bokeh_width, height=bokeh_height)
    # définition de la ColumnDataSource
    source = ColumnDataSource(df)
    # regardez comment spécifier les colonnes à utiliser
    # pour x et y, et comment déclarer comme source 
    # la ColumnDataSource : 
    center = p.circle('lon', 'lat', size=4, alpha=0.2, 
                      color='yellow', source=source)
    show(p)
    return p

p = plot(lat, lon, map_type='satellite')

Bravo! Nous pouvons maintenant améliorer cet affichage pour le rendre vraiment utile.

La première chose que nous allons faire est d'ajouter un peu d'interactivité : il serait bien de pouvoir obtenir des informations sur un point donné en passant la souris dessus.

Ensuite, encoderons de l'information dans le style d'affichage des points. Pour l'instants, tous les points apparaissent en jaune, et sont même taille. Mais nous pouvons utiliser la taille et la couleur pour indiquer par exemple le prix de la propriété ou sa surface.

Bokeh HoverTool et ToolTips

Nous pouvons choisir et configurer les outils qui apparaissent en haut à droite de l'affichage. Par défaut, nous avons déplacement, zoom, et reset. Rajoutons l'outil de survol (hover) :

In [10]:
def plot(lat, lng, zoom=10, map_type='roadmap'):
    gmap_options = GMapOptions(lat=lat, lng=lng, 
                               map_type=map_type, zoom=zoom)
    # les outils sont définis ci-dessous: 
    p = gmap(api_key, gmap_options, title='Pays de Gex', 
             width=bokeh_width, height=bokeh_height,
             tools=['hover', 'reset', 'wheel_zoom', 'pan'])
    source = ColumnDataSource(df)
    center = p.circle('lon', 'lat', size=4, alpha=0.5, 
                      color='yellow', source=source)
    show(p)
    return p

p = plot(lat, lon, map_type='satellite', zoom=12)

Vous pouvez maintenant survoler un point avec votre souris, et un tootip apparaitra. Mais les informations de ce tooltip sont pour l'instant assez limitées. Nous allons améliorer cela : nous abandonnons l'outil de survol par défaut, et nous en créons un adapté à nos besoins :

In [11]:
from bokeh.models import HoverTool

def plot(lat, lng, zoom=10, map_type='roadmap'):
    gmap_options = GMapOptions(lat=lat, lng=lng, 
                               map_type=map_type, zoom=zoom)
    # création de l'outil de survol
    hover = HoverTool(
        tooltips = [
            # @price se réfère à la colonne price 
            # de la ColumnDataSource
            ('price', '@price euros'),
            ('building', '@area_build m2'), 
            ('terrain', '@area_tot m2'), 
        ]
    )
    # ci-dessous, nous avons remplacé 'hover'
    # (l'outil de survol par défaut) par le nôtre
    p = gmap(api_key, gmap_options, title='Pays de Gex', 
             width=bokeh_width, height=bokeh_height,
             tools=[hover, 'reset', 'wheel_zoom', 'pan'])
    source = ColumnDataSource(df)
    center = p.circle('lon', 'lat', size=4, alpha=0.5, 
                      color='yellow', source=source)
    show(p)
    return p

p = plot(lat, lon, map_type='satellite', zoom=12)

Et vous pouvez maintenant inspecter n'importe quel point grâce à l'outil de survol.

Taille de Marqueur Variable dans bokeh

La taille et la couleur des marqueurs est une excellente manière de transmettre immédiatement de l'information à propos du dataset. Nous pouvons décider d'affecter n'importe quelle information à ces attributs visuels.

Par exemple, j'aimerais voir quelles sont les propriétés les plus chères, et quelles sont celles qui sont parties à un prix bien trop élevé.

Je vais donc lier la taille des marqueurs au prix, et leur couleur aux prix du m2.

Commençons par le prix.

Nous définissons d'abord une colonne rayon dans notre dataframe, fonction du prix:

In [12]:
import numpy as np
df['radius'] = np.sqrt(df['price'])/200.
df.head()
Out[12]:
Unnamed: 0 price area_tot area_build lon lat radius
0 83897 741139.0 386.0 0.0 6.072922 46.319225 4.304472
1 83912 716500.0 2731.0 0.0 6.072922 46.319225 4.232316
2 83927 15000.0 727.0 0.0 6.072922 46.319225 0.612372
3 83957 741139.0 338.0 0.0 6.068902 46.323598 4.304472
4 83997 582000.0 4643.0 0.0 6.072211 46.316697 3.814446

Deux choses à noter:

  • J'ai fait en sorte que le rayon soit proportionnel à la racine carrée du prix, de façon à ce que la surface de chaque cercle soit proportionnel au prix (car la surface est égale à $\pi R^2$). Nous aurions pu faire un choix différent.
  • J'ai divisé cette valeur par 200 pour obtenir un rayon qui ne soit pas trop grand pour l'affichage (voir ci-dessous).
In [13]:
def plot(lat, lng, zoom=10, map_type='roadmap'):
    gmap_options = GMapOptions(lat=lat, lng=lng, 
                               map_type=map_type, zoom=zoom)
    hover = HoverTool(
        tooltips = [
            ('price', '@price euros'),
            ('building', '@area_build m2'), 
            ('terrain', '@area_tot m2'), 
        ]
    )
    p = gmap(api_key, gmap_options, title='Pays de Gex', 
             width=bokeh_width, height=bokeh_height,
             tools=[hover, 'reset', 'wheel_zoom', 'pan'])
    source = ColumnDataSource(df)
    # on utilise la colonne radius pour la taille des cercles:
    center = p.circle('lon', 'lat', size='radius', 
                      alpha=0.5, color='yellow', source=source)
    show(p)
    return p

p = plot(lat, lon, map_type='satellite', zoom=11)

Essayez maintenant de zoomer et de dézoomer. Vous verrez que la taille des cercles reste constante. Et donc, les cercles commencent à se superposer franchement si vous dézoomez trop. Pour corriger cela, nous devons faire un tout petit changement.

Au lieu de régler la taille des cercles (size), nous allons régler leur rayon (radius), qui s'exprime dans les unités de x et de y (longitude et latitude).

In [14]:
# Je dois changer le coefficient du rayon 
# pour que les cercles soient visibles: 
df['radius'] = np.sqrt(df['price'])/5.

def plot(lat, lng, zoom=10, map_type='roadmap'):
    gmap_options = GMapOptions(lat=lat, lng=lng, 
                               map_type=map_type, zoom=zoom)
    hover = HoverTool(
        tooltips = [
            ('price', '@price euros'),
            ('building', '@area_build m2'), 
            ('terrain', '@area_tot m2'), 
        ]
    )
    p = gmap(api_key, gmap_options, title='Pays de Gex', 
             width=bokeh_width, height=bokeh_height,
             tools=[hover, 'reset', 'wheel_zoom', 'pan'])
    source = ColumnDataSource(df)
    # nous renseignons radius au lieu de size: 
    center = p.circle('lon', 'lat', radius='radius', alpha=0.5, 
                      color='yellow', source=source)
    show(p)
    return p

p = plot(lat, lon, map_type='satellite', zoom=11)