Manipuler des données géospatiales en Python

Les bibliothèques classiques permettant de manipuler des données géospatiales s'intègrent très bien aux IDE scientifiques (Spyder, Jupyter, etc.) et permettent de visualiser rapidement et interactivement des géométries ou un extrait d'une couche de données (attributs, géométries).

Les principales bibliothèques à connaitre sont les suivantes :

  • shapely : bindings GEOS pour Python (équivalent R : rgeos)
  • geopandas : manipulation de données vecteur sous la forme lignes/colonnes (avec pandas) (équivalent R : sf)
  • rasterio : manipulation de données raster (équivalent R : raster)
  • Rtree : création/interrogation d'un index spatial, bindings de libspatialindex (équivalent R : rtree de rgeos )

D'autres bibliothèques géospatiales sont également utilisables en Python :

  • GDAL/ogr : bindings autogénérés (SWIG), complets, performants, mais pas très "pythoniques"
  • PyQGIS : API Python du logiciel SIG QGIS (dans QGIS ou en standalone)
  • ArcPy: API Python du logicial SIG ArcGIS (dans ArcGIS ou en standalone)
  • Grass : API Python de la solution GRASS

Se familiariser avec les types géométriques de base : Shapely

La bibliothèque Shapely se présente comme des bindings de haut niveau autour de la bibliothèque GEOS.

In [3]:
from shapely.geometry import Point, LineString, Polygon
In [4]:
#### Création d'un point à partir de coordonnées :
pt1 = Point(1.0, 1.0)
pt2 = Point(3.1, 2.4)

# On peut utiliser 3 coordonnées :
pt_3d = Point(1.0, 1.0, 3.0)

# Accéder aux coordonnées d'un objet de type Point :
coords = pt1.coords[0]
print("Coordonnées sous forme d'un tuple avec pt.coords[0] : {}".format(coords))

# Accéder seulement à la coordonnée en 'x' ou en 'y' :
pt_x = pt1.x
pt_y = pt1.y
print("pt.x -> {}\npt.y -> {}\n".format(pt_x, pt_y))

# La représentation de ces geometry (lors de l'appel de print par exemple)
# utilise la notation WKT :
print(pt1)
Coordonnées sous forme d'un tuple avec pt.coords[0] : (1.0, 1.0)
pt.x -> 1.0
pt.y -> 1.0

POINT (1 1)
In [5]:
# Dans Spyder comme dans Jupyter on pourra afficher une représentation graphique
# de la géométrie en inscrivant le nom de la variable qui la contient dans l'interpréteur :
pt1
Out[5]:
In [6]:
# Création d'une ligne à partir de plusieurs Point :
line = LineString([pt1, pt2])

# Création d'une ligne à partir de coordonnées : 
line = LineString([(1.0, 1.0), (3.1, 2.4), (5.8, 4.5)])

# Accéder à la séquence de coordonnées de la ligne
# (sous forme d'une liste de tuples) :
list_coords = list(line.coords)
print("Sous forme d'une liste de tuples :\n{}\n".format(list_coords))

# Accéder seulement aux valeurs en 'x' :
list_xs = list(line.coords.xy[0])
print("Récupération des 'x' de notre séquence de points :\n{}\n".format(list_xs))

# Accéder seulement aux valeurs en 'y' :
list_ys = list(line.coords.xy[1])
print("Récupération des 'y' de notre séquence de points :\n{}\n".format(list_ys))
Sous forme d'une liste de tuples :
[(1.0, 1.0), (3.1, 2.4), (5.8, 4.5)]

Récupération des 'x' de notre séquence de points :
[1.0, 3.1, 5.8]

Récupération des 'y' de notre séquence de points :
[1.0, 2.4, 4.5]

In [7]:
# Création d'un polygone à partir d'un seul anneau 
# (l'anneau externe)
poly1 = Polygon([(1., 1.), (1.2, 1.), (1.2, 1.2), (1., 1.2), (1., 1.)])

# Création d'un polygone à partir de plusieurs anneaux
# (l'anneau externe et un anneau représentant un trou)
poly_with_hole = Polygon(
    [(10., 10.), (12., 10.), (12., 12.), (10., 12.), (10., 10.)],
    [[(11., 10.5), (10.5, 11.5), (11.5, 11.5), (11., 10.5)]])

print('Polygon:')
poly1

print('Polygon with hole:')
poly_with_hole
Polygon:
Out[7]:
Polygon with hole:
Out[7]:
In [8]:
# Imports relatifs aux géométries
# multi-parties
from shapely.geometry import (
    MultiPoint, MultiLineString,
    MultiPolygon, GeometryCollection,
)
In [9]:
multipoint1 = MultiPoint([
    Point(1.1, 1.2),
    Point(8.0, 2.0),
    Point(9.5, 9.5),
])

multipoint2 = MultiPoint([(1.1, 1.2), (8.0, 2.0), (9.5, 9.5)])

multiline = MultiLineString([
    LineString([(12.1, 5.6), (4.5, 3.0)]),
    LineString([(117.1, 12.6), (30.5, 46.0)]),
    LineString([(0., 0.), (3., 3.)])
])

multipoly = MultiPolygon([poly1.buffer(2, 1), poly_with_hole])

collec = GeometryCollection([
    Point(5., 5.),
    LineString([(4.2, 4.3), (3., 3.), (5., 3.), (5., 0.,)]),
    multipoly,
])

collec
Out[9]:

Les méthodes associées à ces objets :

In [10]:
# Des méthodes correspondants à des prédicats spatiaux :
poly1.contains(pt1)
multiline.intersects(poly1)
multipoint1.equals(multipoint2)

# Des propriétés :
poly1.boundary.is_closed
poly1.is_closed
poly1.area
poly1.intersects(poly_with_hole)
poly_with_hole.area
Out[10]:
False
Out[10]:
True
Out[10]:
True
Out[10]:
True
Out[10]:
False
Out[10]:
0.03999999999999998
Out[10]:
False
Out[10]:
3.5
In [11]:
# Des méthodes qui permettent d'obtenir de nouvelles géométries :
pt1.buffer(10)
multiline.intersection(poly1)

# Des... propriétés qui permettent d'obtenir de nouvelles géométries :
multipoint1.convex_hull
poly_with_hole.boundary
Out[11]:
Out[11]:
Out[11]:
Out[11]:

Ce sont ces objets qu'on va manipuler dans la colonne geometry avec geopandas par la suite. On retrouvera les attributs de ces objets sur cette colonne, avec pour signification d'appliquer l'opération à la géométrie de chaque entité.

geopandas

Ouverture d'une couche de données avec geopandas :

In [12]:
# Import nécessaires:
# Geopandas et les bibliothèques vues précédemment :
import geopandas as gpd
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np


gdf = gpd.read_file('data/COURS_D_EAU.shp')

# L'objet est de type `GeoDataFrame` et
# possède les méthodes des `DataFrame` ainsi que d'autres :
gdf.head()
Out[12]:
ID CODE_HYDRO TOPONYME IMPORTANCE DATE_CREAT DATE_MAJ DATE_APP DATE_CONF SOURCE ID_SOURCE STATUT MAREE PERMANENT STATUT_TOP COMMENT geometry
0 COURDEAU0000000013199566 None le Gier 4 2006-08-18 12:13:50 2018-07-06 14:53:45 None None None None None None None Validé None (LINESTRING (823137.8 6477831.7, 823134.4 6477...
1 COURDEAU0000000013199499 None le Garon 5 2006-08-18 12:13:50 2018-07-06 14:53:45 None None None None None None None Validé None (LINESTRING (821856.7 6512492.6, 821865.9 6512...
2 COURDEAU0000000012012955 None la Valencize 5 2006-08-16 12:14:40 2018-07-06 14:53:45 None None None None None None None Validé None LINESTRING (828022.2 6484036.4, 828031.8 64840...
3 COURDEAU0000000013199545 None le Mornantet 5 2006-08-18 12:13:50 2018-07-06 14:53:45 None None None None None None None Validé None (LINESTRING (828867.2 6504625.2, 828874 650462...
4 COURDEAU0000002000804457 06C0000002000804457 le Rhône 5 2017-01-24 16:06:18 2018-02-23 12:57:41 None None None None Validé None None Collecté None (LINESTRING (839675.7 6315441.7, 839443.9 6314...
In [13]:
# Attribut propre aux `GeoDataFrame` :
gdf.crs
Out[13]:
{'proj': 'lcc',
 'lat_1': 49,
 'lat_2': 44,
 'lat_0': 46.5,
 'lon_0': 3,
 'x_0': 700000,
 'y_0': 6600000,
 'ellps': 'GRS80',
 'units': 'm',
 'no_defs': True}
In [14]:
# Attributs dont le comportement a été modifié par rapport aux DataFrame :
gdf.plot()
Out[14]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f2cad857198>

On dispose d'une colonne geometry. Lors de l'affichage de la table comme précédemment on a vu sa représentation au format WKT mais en interne il s'agit de géometries de la bibliothèque shapely.

Comme avec pandas, on peut sélectionner les colonnes avec la notation entre crochets et sélectionner des entités avec les méthodes .loc, .iloc ou en vérifiant une condition.

In [15]:
# Affichage de seulement quelques colonnes :
gdf[['TOPONYME', 'IMPORTANCE']].head()
Out[15]:
TOPONYME IMPORTANCE
0 le Gier 4
1 le Garon 5
2 la Valencize 5
3 le Mornantet 5
4 le Rhône 5
In [16]:
# Affichage de seulement quelques colonnes
# pour les cours d'eau dont l'importance est supérieure ou égale à 4:
gdf[gdf.IMPORTANCE >= 4][['TOPONYME', 'geometry']].head()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-16-8a7aa4eb613b> in <module>
      1 # Affichage de seulement quelques colonnes
      2 # pour les cours d'eau dont l'importance est supérieure ou égale à 4:
----> 3 gdf[gdf.IMPORTANCE >= 4][['TOPONYME', 'geometry']].head()

/usr/local/lib/python3.6/dist-packages/pandas/core/ops.py in wrapper(self, other, axis)
   1764 
   1765             with np.errstate(all='ignore'):
-> 1766                 res = na_op(values, other)
   1767             if is_scalar(res):
   1768                 raise TypeError('Could not compare {typ} type with Series'

/usr/local/lib/python3.6/dist-packages/pandas/core/ops.py in na_op(x, y)
   1623 
   1624         if is_object_dtype(x.dtype):
-> 1625             result = _comp_method_OBJECT_ARRAY(op, x, y)
   1626 
   1627         elif is_datetimelike_v_numeric(x, y):

/usr/local/lib/python3.6/dist-packages/pandas/core/ops.py in _comp_method_OBJECT_ARRAY(op, x, y)
   1601         result = libops.vec_compare(x, y, op)
   1602     else:
-> 1603         result = libops.scalar_compare(x, y, op)
   1604     return result
   1605 

pandas/_libs/ops.pyx in pandas._libs.ops.scalar_compare()

TypeError: '>=' not supported between instances of 'str' and 'int'

Lecture du message d'erreur et analyse du pbm ...

In [17]:
# Conversion explicite de type
gdf.IMPORTANCE = gdf.IMPORTANCE.astype(int)

# Retour à l'opération demandée
gdf[gdf.IMPORTANCE >= 4][['TOPONYME', 'geometry']].head()
Out[17]:
TOPONYME geometry
0 le Gier (LINESTRING (823137.8 6477831.7, 823134.4 6477...
1 le Garon (LINESTRING (821856.7 6512492.6, 821865.9 6512...
2 la Valencize LINESTRING (828022.2 6484036.4, 828031.8 64840...
3 le Mornantet (LINESTRING (828867.2 6504625.2, 828874 650462...
4 le Rhône (LINESTRING (839675.7 6315441.7, 839443.9 6314...
In [18]:
# Obtention d'une Serie contenant les longueurs de cours d'eau 
lengths = gdf.geometry.length
lengths.sort_values(ascending=False).head() # <- tri effectué et ID conservés
Out[18]:
4      402446.133223
131    288093.954219
44     192641.606705
318    192590.027719
859    130477.443500
dtype: float64
In [19]:
# Utilisons ces indexes triés pour appeler notre GeoDataFrame
# et obtenir les toponymes triés par la taille du cours d'eau :
gdf.loc[lengths.sort_values(ascending=False).index][['TOPONYME']]
Out[19]:
TOPONYME
4 le Rhône
131 l'Isère
44 le Rhône
318 l'Ain
859 le Drac
907 le Buëch
920 la Romanche
296 la Bourbre
365 l'Albarine
98 la Galaure
744 le Guiers
51 la Varèze
207 Canal de la Bourne
392 la Bourne
0 le Gier
1023 la Bonne
178 l'Herbasse
132 la Gère
85 le Dolon
681 la Gresse
1386 le Vénéon
1288 la Séveraisse
850 l'Ebron
1 le Garon
481 la Vernaisson
1259 le Bréda
1437 le Gélon
1612 l'Arvan
84 la Sanne
1291 l'Eau d'Olle
... ...
33 le Verdier
72 Ruisseau de Saint-Antoine
1294 Ruisseau Battiards
68 Ruisseau de Vernat
809 Ruisseau des Communaux
375 Ruisseau de Tourterelle
523 Ruisseau de Riverolle
264 Ruisseau des Groubes
42 Ruisseau de Priolet
332 Ruisseau le Rousset
80 Ruisseau de Marcet
61 Ruisseau de Changuinet
379 Ruisseau de l'Éperon
432 la Bordèche
1142 Ruisseau de la Combe du Coing
1463 Canal du Gelon
630 Ruisseau la Galise
1481 Ruisseau du Maître
402 la Baïse
82 la Chanal
1144 Ruisseau de Rivaillon
1518 Ruisseau de la Grande Montagne
1521 Ruisseau de Cohardin
1080 Ruisseau de Prémol
1442 le Joudron
774 Canal du Ruisseau du Pontet
57 le Solon
140 Canal de Pulivès
1304 Ruisseau du Bon de Loge
584 le Rays

1748 rows × 1 columns

In [20]:
gdf[gdf.TOPONYME == 'Canal du Furon'].index
Out[20]:
Int64Index([735], dtype='int64')
In [21]:
geom = gdf.loc[(735, 'geometry')]
In [22]:
print(type(geom))
geom
<class 'shapely.geometry.linestring.LineString'>
Out[22]:
In [23]:
print('Longueur: {:.1f}m'.format(geom.length))
Longueur: 21416.3m

Ouverture d'un fichier raster avec rasterio :

In [24]:
import rasterio as rio

mnt = rio.open('data/N245E395.tif')
print(mnt.meta)
{'driver': 'GTiff', 'dtype': 'float32', 'nodata': -3.4028234663852886e+38, 'width': 2000, 'height': 2000, 'count': 1, 'crs': CRS({'proj': 'laea', 'lat_0': 52, 'lon_0': 10, 'x_0': 4321000, 'y_0': 3210000, 'ellps': 'GRS80', 'towgs84': '0,0,0,0,0,0,0', 'units': 'm', 'no_defs': True}), 'transform': Affine(25.0, 0.0, 3950000.0,
       0.0, -25.0, 2500000.0)}

La projection ne correspond pas, on va reprojeter le fichier raster

In [25]:
from rasterio.warp import calculate_default_transform, reproject, Resampling
from rasterio.plot import show


dst_crs = 'EPSG:2154' # Lambert 93

transform, width, height = calculate_default_transform(
    mnt.crs, dst_crs, mnt.width, mnt.height, *mnt.bounds)
kwargs = mnt.meta.copy()
kwargs.update({
    'crs': dst_crs,
    'transform': transform,
    'width': width,
    'height': height
})

with rio.open('data/N245E395_L93.tif', 'w', **kwargs) as dst:
    reproject(
        source=rio.band(mnt, 1),
        destination=rio.band(dst, 1),
        src_transform=mnt.transform,
        src_crs=mnt.crs,
        dst_transform=transform,
        dst_crs=dst_crs,
        resampling=Resampling.nearest,
    )

mnt.close()
In [26]:
mnt = rio.open('data/N245E395_L93.tif')
show(mnt)
Out[26]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f2cad599ba8>

Les deux bibliothèques donnent accès à leurs éléments (géométries d'une part et valeurs du raster d'autre part) sous forme d'objet Python. On va donc facilement pouvoir interagir entre les objets de ces bibliothèques.

Exemple : Création du profil en long d'un cours d'eau à partir du tracé précédement extrait et du mnt reprojeté.

La procédure est relativement simple à expliquer :

  • définir une distance entre nos points de relevé (ici on prend arbitrairement 50 points)
  • créer des points le long du cours d'eau en utilisant cette distance
  • récupérer l'altitude de chacun de ces points
  • créer un graphique avec en absisse la distance à la source et en ordonnées l'altitude
In [27]:
# geom est la géométrie récupérée plus tôt
length = geom.length

# combien de points :
nb_points = 50 

# quelle distance entre les points :
step = geom.length / nb_points

dists = []
coords = []
for dist in range(0, int(length), int(step)):
    pt = geom.interpolate(dist) # type(pt) -> shapely.geometry.Point
    # récupération des coordonnées du points sous forme d'un tuple :
    c = pt.coords[0]
    coords.append(c)
    dists.append(dist)

result = []
for i, value in enumerate(mnt.sample(coords)):
    result.append((dists[i], value[0]))

On peut maintenant utiliser ces valeurs pour créer un graphique avec matplotlib:

In [28]:
plt.plot([i[0] for i in result], [i[1] for i in result])
plt.title('Profil en long du Canal du Furon')
plt.xlabel('Distance à la source')
plt.ylabel('Altitude')
plt.show()
Out[28]:
[<matplotlib.lines.Line2D at 0x7f2c9d94d048>]
Out[28]:
Text(0.5,1,'Profil en long du Canal du Furon')
Out[28]:
Text(0.5,0,'Distance à la source')
Out[28]:
Text(0,0.5,'Altitude')

Et si on voulait créer une couche de points (où chaque entité serait un de ces points), avec un champ contenant la distance à la source, un champ contenant le toponyme du cours d'eau et un champ contenant l'altitude du point ?

Cette couche va être créée sous forme d'un nouvel objet geopandas et va ensuite être sauvegardée au format GeoJSON.

In [29]:
result = {}
result['geometry'] = []
result['distance_source'] = []
result['altitude'] = []

for dist in range(0, int(length), int(step)):
    pt = geom.interpolate(dist)
    result['geometry'].append(pt) 
    result['distance_source'].append(dist)
    altitude = list(mnt.sample(pt.coords))[0][0]
    result['altitude'].append(altitude)

# Comme avec pandas on peut utiliser un dictionnaire dont les clés sont
# les nom de colonnes et les valeurs associées représentent
# les valeurs des entités de ces colonnes
new_gdf = gpd.GeoDataFrame(result, crs=gdf.crs)

# Toutes nos nouvelles entités vont prendre cette valeur
new_gdf['TOPONYME_COURS_DEAU'] = 'Canal du Furon'
new_gdf.head()
Out[29]:
geometry distance_source altitude TOPONYME_COURS_DEAU
0 POINT (904717.4 6447571.2) 0 1423.304810 Canal du Furon
1 POINT (904696.944100579 6447996.848641824) 428 1354.772339 Canal du Furon
2 POINT (904755.9720668748 6448415.456500354) 856 1316.247681 Canal du Furon
3 POINT (904703.2014671427 6448834.856722699) 1284 1288.363037 Canal du Furon
4 POINT (904552.2122605438 6449229.864593551) 1712 1219.130859 Canal du Furon
In [30]:
result = []

for dist in range(0, int(length), int(step)):
    pt = geom.interpolate(dist)
    result.append({
        "geometry": pt,
        "distance_source": dist,
        "altitude": list(mnt.sample(pt.coords))[0][0],
    })

# Cette fois nous avons utilisé une liste
# contenant un dictionnaire pour chaque entité
# ... le résultat est le même qu'avec la méthode prédécente
new_gdf = gpd.GeoDataFrame(result, crs=gdf.crs)

# Toutes nos nouvelles entités vont prendre cette valeur
new_gdf['TOPONYME_COURS_DEAU'] = 'Canal du Furon'
new_gdf.head()
Out[30]:
altitude distance_source geometry TOPONYME_COURS_DEAU
0 1423.304810 0 POINT (904717.4 6447571.2) Canal du Furon
1 1354.772339 428 POINT (904696.944100579 6447996.848641824) Canal du Furon
2 1316.247681 856 POINT (904755.9720668748 6448415.456500354) Canal du Furon
3 1288.363037 1284 POINT (904703.2014671427 6448834.856722699) Canal du Furon
4 1219.130859 1712 POINT (904552.2122605438 6449229.864593551) Canal du Furon
In [31]:
new_gdf.plot()
Out[31]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f2cad599198>

Enregistrement du résultat

Transformation des géometries en coordonnées géographiques (EPSG:4326) avant d'enregistrer le fichier au format GeoJSON :

In [33]:
new_gdf.to_crs(epsg=4326).to_file('data/points_canal_furon.geojson', driver='GeoJSON')
In [34]:
%cat data/points_canal_furon.geojson
{
"type": "FeatureCollection",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
"features": [
{ "type": "Feature", "properties": { "altitude": 1423.3048095703125, "distance_source": 0, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.602936388562156, 45.097041248809695 ] } },
{ "type": "Feature", "properties": { "altitude": 1354.7723388671875, "distance_source": 428, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.602854817807447, 45.100877769687337 ] } },
{ "type": "Feature", "properties": { "altitude": 1316.2476806640625, "distance_source": 856, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.60378031054364, 45.104627343059519 ] } },
{ "type": "Feature", "properties": { "altitude": 1288.363037109375, "distance_source": 1284, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.603285507285376, 45.108417233222696 ] } },
{ "type": "Feature", "properties": { "altitude": 1219.130859375, "distance_source": 1712, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.601532157195259, 45.112016753315864 ] } },
{ "type": "Feature", "properties": { "altitude": 1175.6884765625, "distance_source": 2140, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.601070658466197, 45.11581345206455 ] } },
{ "type": "Feature", "properties": { "altitude": 1128.1978759765625, "distance_source": 2568, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.602875015792644, 45.119419528471319 ] } },
{ "type": "Feature", "properties": { "altitude": 1114.665771484375, "distance_source": 2996, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.603318600708223, 45.122799392830103 ] } },
{ "type": "Feature", "properties": { "altitude": 1063.9599609375, "distance_source": 3424, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.600993324093486, 45.125765092317025 ] } },
{ "type": "Feature", "properties": { "altitude": 1028.2977294921875, "distance_source": 3852, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.596714861215382, 45.127787527323726 ] } },
{ "type": "Feature", "properties": { "altitude": 1009.0530395507812, "distance_source": 4280, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.592514319284061, 45.129088181552241 ] } },
{ "type": "Feature", "properties": { "altitude": 983.6806640625, "distance_source": 4708, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.588682946240334, 45.131060046928347 ] } },
{ "type": "Feature", "properties": { "altitude": 970.5863037109375, "distance_source": 5136, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.587298981190341, 45.134015004202716 ] } },
{ "type": "Feature", "properties": { "altitude": 965.86419677734375, "distance_source": 5564, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.587601804540015, 45.137549358350377 ] } },
{ "type": "Feature", "properties": { "altitude": 961.939453125, "distance_source": 5992, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.589307549857744, 45.14042518783868 ] } },
{ "type": "Feature", "properties": { "altitude": 969.871826171875, "distance_source": 6420, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.592913627728215, 45.141984603742358 ] } },
{ "type": "Feature", "properties": { "altitude": 976.03668212890625, "distance_source": 6848, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.597445920273993, 45.143742685679051 ] } },
{ "type": "Feature", "properties": { "altitude": 971.39031982421875, "distance_source": 7276, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.599646103684858, 45.146560456559598 ] } },
{ "type": "Feature", "properties": { "altitude": 967.7244873046875, "distance_source": 7704, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.600427204302824, 45.149846695838534 ] } },
{ "type": "Feature", "properties": { "altitude": 937.7781982421875, "distance_source": 8132, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.603545080638023, 45.152644680689647 ] } },
{ "type": "Feature", "properties": { "altitude": 903.19818115234375, "distance_source": 8560, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.606450670072399, 45.155564354564106 ] } },
{ "type": "Feature", "properties": { "altitude": 887.65020751953125, "distance_source": 8988, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.608563605707576, 45.158904918695171 ] } },
{ "type": "Feature", "properties": { "altitude": 902.82696533203125, "distance_source": 9416, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.609622889418532, 45.162068349970873 ] } },
{ "type": "Feature", "properties": { "altitude": 887.63275146484375, "distance_source": 9844, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.61150269903735, 45.165494295432453 ] } },
{ "type": "Feature", "properties": { "altitude": 885.61968994140625, "distance_source": 10272, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.613121697272172, 45.168901529458537 ] } },
{ "type": "Feature", "properties": { "altitude": 872.78839111328125, "distance_source": 10700, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.614942999845164, 45.172356807222897 ] } },
{ "type": "Feature", "properties": { "altitude": 872.81927490234375, "distance_source": 11128, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.618119315036401, 45.174805688763286 ] } },
{ "type": "Feature", "properties": { "altitude": 848.47894287109375, "distance_source": 11556, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.619627475553519, 45.177934243368981 ] } },
{ "type": "Feature", "properties": { "altitude": 841.46453857421875, "distance_source": 11984, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.620750085917474, 45.1814856253404 ] } },
{ "type": "Feature", "properties": { "altitude": 830.4658203125, "distance_source": 12412, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.622142283331967, 45.184387228716318 ] } },
{ "type": "Feature", "properties": { "altitude": 775.163818359375, "distance_source": 12840, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.624230750879395, 45.187675083696647 ] } },
{ "type": "Feature", "properties": { "altitude": 730.93505859375, "distance_source": 13268, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.627799486106804, 45.190200343474167 ] } },
{ "type": "Feature", "properties": { "altitude": 695.1026611328125, "distance_source": 13696, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.627787607221253, 45.194013715198366 ] } },
{ "type": "Feature", "properties": { "altitude": 661.1397705078125, "distance_source": 14124, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.629921048860592, 45.197433685869086 ] } },
{ "type": "Feature", "properties": { "altitude": 585.79681396484375, "distance_source": 14552, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.633707783632583, 45.199823332486567 ] } },
{ "type": "Feature", "properties": { "altitude": 557.097412109375, "distance_source": 14980, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.636353649814095, 45.202538534219968 ] } },
{ "type": "Feature", "properties": { "altitude": 537.7073974609375, "distance_source": 15408, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.639967476597039, 45.204792161149598 ] } },
{ "type": "Feature", "properties": { "altitude": 488.54095458984375, "distance_source": 15836, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.642411860700116, 45.2079031996944 ] } },
{ "type": "Feature", "properties": { "altitude": 387.07159423828125, "distance_source": 16264, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.646745904349822, 45.209285620326256 ] } },
{ "type": "Feature", "properties": { "altitude": 317.86752319335938, "distance_source": 16692, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.651977536327467, 45.20901998038741 ] } },
{ "type": "Feature", "properties": { "altitude": 219.61807250976562, "distance_source": 17120, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.657256211070005, 45.208327406623717 ] } },
{ "type": "Feature", "properties": { "altitude": 208.73480224609375, "distance_source": 17548, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.661253451782798, 45.209378293241429 ] } },
{ "type": "Feature", "properties": { "altitude": 205.32588195800781, "distance_source": 17976, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.663819145162079, 45.212297457256589 ] } },
{ "type": "Feature", "properties": { "altitude": 207.84251403808594, "distance_source": 18404, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.666641892173243, 45.214817054624511 ] } },
{ "type": "Feature", "properties": { "altitude": 203.32449340820312, "distance_source": 18832, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.665419281842449, 45.218486262219521 ] } },
{ "type": "Feature", "properties": { "altitude": 203.68572998046875, "distance_source": 19260, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.666680148985379, 45.22171955209356 ] } },
{ "type": "Feature", "properties": { "altitude": 202.93037414550781, "distance_source": 19688, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.664108748457322, 45.225117643505079 ] } },
{ "type": "Feature", "properties": { "altitude": 202.04118347167969, "distance_source": 20116, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.661516805145573, 45.228508329692723 ] } },
{ "type": "Feature", "properties": { "altitude": 201.22537231445312, "distance_source": 20544, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.658927656717651, 45.231900129762046 ] } },
{ "type": "Feature", "properties": { "altitude": 202.91989135742188, "distance_source": 20972, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.656485254040554, 45.235219131549286 ] } },
{ "type": "Feature", "properties": { "altitude": 198.85018920898438, "distance_source": 21400, "TOPONYME_COURS_DEAU": "Canal du Furon" }, "geometry": { "type": "Point", "coordinates": [ 5.656687409215286, 45.238664339673768 ] } }
]
}

Plus de manipulations géospatiales avec geopandas

On a vu que geopandas permettait de manipuler les données sous la même forme que pandas (lignes / colonnes). Cette possibilité va pouvoir être utilisée pour appliquer des opérations à l'ensemble de la colonne geometry.

On peut par exemple créer un buffer autour de chaque point de la manière suivante :

In [35]:
# On peut appeler la méthode sur l'ensemble de l'objet ou seulement sur sa colonne geometry, le résultat est le même
# Dans les deux cas c'est une GeoSeries (une colonne et un index) qui est renvoyée :

new_gdf.buffer(100).head()

print('new_gdf.buffer(100) == new_gdf.geometry.buffer(100) -> {}'
      .format(np.all(new_gdf.buffer(10) == new_gdf.geometry.buffer(10))))
Out[35]:
0    POLYGON ((904817.4 6447571.2, 904816.918472667...
1    POLYGON ((904796.944100579 6447996.848641824, ...
2    POLYGON ((904855.9720668748 6448415.456500354,...
3    POLYGON ((904803.2014671427 6448834.856722699,...
4    POLYGON ((904652.2122605438 6449229.864593551,...
dtype: object
new_gdf.buffer(100) == new_gdf.geometry.buffer(100) -> True

On peut utiliser cette colonne pour remplacer la colonne de géométries qui contient nos points :

In [36]:
new_gdf.geometry = new_gdf.geometry.buffer(100)
In [37]:
new_gdf.head()
new_gdf.tail()
Out[37]:
altitude distance_source geometry TOPONYME_COURS_DEAU
0 1423.304810 0 POLYGON ((904817.4 6447571.2, 904816.918472667... Canal du Furon
1 1354.772339 428 POLYGON ((904796.944100579 6447996.848641824, ... Canal du Furon
2 1316.247681 856 POLYGON ((904855.9720668748 6448415.456500354,... Canal du Furon
3 1288.363037 1284 POLYGON ((904803.2014671427 6448834.856722699,... Canal du Furon
4 1219.130859 1712 POLYGON ((904652.2122605438 6449229.864593551,... Canal du Furon
Out[37]:
altitude distance_source geometry TOPONYME_COURS_DEAU
46 202.930374 19688 POLYGON ((909146.8996321146 6461947.770264197,... Canal du Furon
47 202.041183 20116 POLYGON ((908930.9023242079 6462317.26292893, ... Canal du Furon
48 201.225372 20544 POLYGON ((908715.1445422065 6462686.893849844,... Canal du Furon
49 202.919891 20972 POLYGON ((908511.1915707089 6463048.839004138,... Canal du Furon
50 198.850189 21400 POLYGON ((908514.1746335384 6463431.775548101,... Canal du Furon

Plus de manipulations géospatiales avec geopandas (suite)

In [38]:
# Ouverture de la couche des communes de la région Auvergne-Rhône-Alpes
comm = gpd.read_file('data/COMMUNE_auvergne_rhone_alpes.shp')

# Le système de coordonnées est le même que nos cours d'eau
assert comm.crs == gdf.crs
In [39]:
# Aggrégation / fusion des communes en départements
# On choisit les colonnes à conserver, la colonne à utiliser
# pour l'aggrégation, et une fonction d'aggrégation si besoin
dep = comm[['geometry', 'NOM_DEPT', 'SUPERFICIE', 'POPULATION']].dissolve(by='NOM_DEPT', aggfunc='sum')
dep.reset_index(drop=False, inplace=True)

# Affichage du résultat
dep.head(4)
dep.plot()
Out[39]:
NOM_DEPT geometry SUPERFICIE POPULATION
0 AIN POLYGON ((858243.4999999506 6525232.500001915,... 563065 619497
1 ALLIER POLYGON ((652774.0000000136 6581522.800001746,... 732174 343431
2 ARDECHE POLYGON ((802759.7999999614 6358170.900002372,... 556048 320379
3 CANTAL POLYGON ((696976.8000000011 6395040.500002271,... 568153 147035
Out[39]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f2c9c07c588>
In [40]:
geom_furon = gdf.loc[gdf.TOPONYME == 'Canal du Furon', 'geometry'].values[0]

# Quelles sont les communes traversées par le Canal du Furon ?
comm[comm.intersects(geom_furon.buffer(1000))]
Out[40]:
ID_GEOFLA CODE_COM INSEE_COM NOM_COM STATUT X_CHF_LIEU Y_CHF_LIEU X_CENTROID Y_CENTROID Z_MOYEN SUPERFICIE POPULATION CODE_ARR CODE_DEPT NOM_DEPT CODE_REG NOM_REG geometry
699 COMMUNE00000000000006293 170 38170 FONTANIL-CORNILLON Commune simple 909029 6465315 908960 6465457 329 536 2780 1 38 ISERE 84 AUVERGNE-RHONE-ALPES POLYGON ((907028.2999999302 6465155.500002086,...
900 COMMUNE00000000000008005 474 38474 SASSENAGE Commune simple 909188 6459715 907910 6460927 434 1329 11705 1 38 ISERE 84 AUVERGNE-RHONE-ALPES POLYGON ((911008.8999999284 6459848.200002102,...
1260 COMMUNE00000000000010930 205 38205 LANS-EN-VERCORS Commune simple 903471 6450984 903301 6448974 1194 3914 2613 1 38 ISERE 84 AUVERGNE-RHONE-ALPES POLYGON ((902581.4999999308 6453999.000002118,...
2203 COMMUNE00000000000019696 382 38382 SAINT-EGREVE Commune simple 910499 6462717 910635 6462909 345 1086 15996 1 38 ISERE 84 AUVERGNE-RHONE-ALPES POLYGON ((910420.6999999286 6460691.5000021, 9...
2415 COMMUNE00000000000021454 153 38153 ENGINS Commune simple 905491 6456924 905165 6458778 1301 2091 496 1 38 ISERE 84 AUVERGNE-RHONE-ALPES POLYGON ((903819.699999931 6462879.200002093, ...
3184 COMMUNE00000000000027701 281 38281 NOYAREY Commune simple 906419 6463915 905506 6463960 602 1701 2269 1 38 ISERE 84 AUVERGNE-RHONE-ALPES POLYGON ((907028.2999999302 6465155.500002086,...
3297 COMMUNE00000000000028626 185 38185 GRENOBLE Préfecture de département 914070 6457870 913890 6457937 216 1854 160215 1 38 ISERE 84 AUVERGNE-RHONE-ALPES POLYGON ((911008.8999999284 6459848.200002102,...
3680 COMMUNE00000000000031965 433 38433 SAINT-NIZIER-DU-MOUCHEROTTE Commune simple 906593 6455847 906956 6455498 1216 1136 1108 1 38 ISERE 84 AUVERGNE-RHONE-ALPES POLYGON ((905111.3999999298 6453557.70000212, ...
In [41]:
# Calculer la densité de population sur la couche des communes :
comm['DENSITE_POPULATION'] = comm['POPULATION'] / comm['SUPERFICIE']

comm.plot('DENSITE_POPULATION', scheme='Quantiles', k=8, figsize=(16,16), legend=True)
Out[41]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f2ccaf7eeb8>
Attention geopandas ne dispose toutefois pas de 'vrais' fonctionnalités 'carto' et il existe des bibliothèques qui offrent une API dédiée à la cartographie en Python.
Sans se plonger dans la cartographie thématique, on note qu'il est facilement possible des faire des cartes choroplètes (avec geopandas, ou folium ensuite par exemple) ou des cartes de symboles proportionnels avec quelques opérations supplémentaires.

Affichage de nos 50 buffer sur un fond Leaflet :

In [42]:
import folium
from pyproj import Proj, transform

# On va récupérer les coordonnées du centroide du cours d'eau pour centrer la carte
# (on doit transformer ces coordonnées du Lambert 93 vers des coordonnées en latitude-longitude)
c = geom.centroid.xy
x,y = transform(Proj(init="epsg:2154"), Proj(init="epsg:4326"), c[0][0], c[1][0])

# On instancie un objet Map avec le fond de notre choix
# et on lui ajoute direction nos entitées polygonales:
map_example = folium.Map(
    location=(y, x),
    tiles = "Stamen Toner",
    zoom_start = 11).add_child(
        folium.features.GeoJson(new_gdf.to_crs(epsg='4326').to_json()))
In [43]:
# Export du document HTML
map_example.save('tmp/plot_data.html')
# Affichage dans IPython 
IFrame('tmp/plot_data.html', width="700px", height="400px")
Out[43]:

Dans la partie suivante nous verrons comment créer une page réactive utilisant des données coté serveur ainsi qu'une solution rapide pour exposer une API réalisant des calculs personnalisés par exemple.

Bien que ça ne soit pas le cas dans les exemples suivants, il est possible d'intégrer des données géo-spatiales dans développements web comme ceux qui vont suivre.

Partie suivante ...

7. Publication sur le web