Affichage interactif de données géographiques : prix de l'immobilier aux environs de Genève.
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:
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
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 :
import pandas as pd
from bokeh.io import output_notebook
output_notebook()
bokeh_width, bokeh_height = 500,400
Ensuite, on charge nos données dans une dataframe pandas, et on imprime les premières lignes :
df = pd.read_csv('dvf_gex.csv')
df.head()
Chaque ligne de la dataframe correspond à un transfert de propriété immobilière. Et voici une description des différentes colonnnes :
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.
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 :
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) :
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 :
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 :
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 :
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.
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 :
df.shape
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.
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.
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
) :
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 :
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.
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:
import numpy as np
df['radius'] = np.sqrt(df['price'])/200.
df.head()
Deux choses à noter:
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).
# 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)
Joli! Vous pouvez déjà voir que certaines propriétés ont été vendues pour un prix phénoménal, notamment au voisinage de l'aéroport. Le pays de Gex, c'est pratiquement Paris... Ces grosses ventes correspondent à des bâtiments ou des terrains destinés à une utilisation commerciale, peut-être un parking ou un supermarché.
Maintenant, nous allons utiliser la couleur des marqueurs pour indiquer le prix au m2 des bâtiments.
Il n'est bien sûr pas possible de calculer ce prix si la surface du bâti est nulle. Nous allons donc d'abord créer une nouvelle dataframe, après avoir éliminé toutes les lignes pour lesquelles c'est le cas. Puis nous calculons le prix au m2:
dfb = df[df['area_build']>0.].copy()
dfb['pricem2'] = dfb['price']/dfb['area_build']
dfb.head()
Ensuite, nous modifions encore notre fonction pour afficher une couleur reliée au prix du m2:
from bokeh.transform import linear_cmap
from bokeh.palettes import Plasma256 as palette
from bokeh.models import ColorBar
# je rajoute la dataframe comme paramètre, puisque
# nous allons maintenant afficher une dataframe différente:
def plot(df, 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'),
# the {0.} means that we don't want decimals
# for 1 decimal, write {0.0}
('price/m2', '@pricem2{0.}'),
('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)
# définition d'un color mapper, qui va mapper les valeurs
# de pricem2 entre 2000 et 8000 sur la palette de couleurs:
mapper = linear_cmap('pricem2', palette, 2000., 8000.)
# nous utilisons le mapper pour la couleur des cercles:
center = p.circle('lon', 'lat', radius='radius', alpha=0.6,
color=mapper, source=source)
# et nous rajoutons une échelle de couleurs sur la droite:
color_bar = ColorBar(color_mapper=mapper['transform'],
location=(0,0))
p.add_layout(color_bar, 'right')
show(p)
return p
p = plot(dfb, lat, lon, map_type='roadmap', zoom=11)
Vous pouvez maintenant immédiatement trouver les propriétés qui se sont vendues au-dessus du prix du marché. Par exemple, à Brétigny, nous trouvons une maison de 80 m2 vendue pour 705 000 euros alors que juste à côté, une maison de 142 m2 a été vendue pour "seulement" 695 000 euros.
Mais attention ! la première a 9150 m2 de terrain ! c'est donc une excellente affaire. Je ne serais pas surpris de voir ce terrain séparé en une dizaine de lots, qui seront mis en vente prochainement.
Dans cet article, vous avez appris comment :
Vous êtes maintenant prêt à analyser des données géographiques!
Par la suite, nous verrons comment:
N'hésitez pas à me donner votre avis dans les commentaires ! Je répondrai à toutes les questions.
Et si vous avez aimé cet article, vous pouvez souscrire à ma newsletter pour être prévenu lorsque j'en sortirai un nouveau. Pas plus d'un mail par semaine, promis!
Rejoignez ma mailing list pour plus de posts et du contenu exclusif: