Articulation de différents langages (R, JavaScript et Python) pour la géovisualisation avec Quarto

Atelier Sageo, Québec, Juin 2023

Auteur·rice
Affiliation

Timothée Giraud, Nicolas Lambert, Matthieu Viry, Ronan Ysebaert

Présentation

Dans un contexte d’essor de carnets interactifs (notebooks), l’objet de cet atelier consiste à montrer les possibilités offertes par Quarto, un système de publication scientifique et technique open source basé sur Pandoc, qui permet d’articuler différents langages pour réaliser des géovisualisations reproductibles dans un seul et même document. Cet atelier proposera aux participants une prise en main des principales fonctionnalités offertes par Quarto, puis une mise en œuvre de chaînes de traitements de l’information géographique reproductibles avec R, Python et Observable JavaScript dans cet environnement.

Déroulé de la demi-journée

  • Installation des logiciels nécessaires à la mise en place de l’environnement de développement (Quarto, R, Python, RStudio, etc.).
  • Présentation et prise en main des principales fonctionnalités des notebooks Quarto. Présentation des écosystèmes spatiaux de R et Python, et, d’un jeu de données commun sur lequel s’appuiera l’ensemble de l’atelier.
  • Traitement des données vectorielles : Mise en pratique d’une chaîne de traitements reproductible avec R, notamment grâce aux packages sf et mapsf.
  • Traitement des données raster : Mise en pratique d’une chaîne de traitements reproductible avec Python (bibliothèques geopandas, rasterio, xarray, matplotlib, etc.).
  • Création de visualisations cartographiques interactives avec ojs (bibliothèques bertin, Plot, Gridviz, Deck.gl).
  • Export du document quarto produit pendant l’atelier (templates de différentes revues scientifiques, slides, https://quartopub.com/, etc.).

Données atelier

Vecteur

Raster

Télécharger les données de l’exercice

Toutes les données mobilisées dans cet atelier son stockées dans un projet RStudio. Vous devez le télécharger puis le décompresser sur votre machine. Il vous sera ensuite possible de tester l’ensemble des manipulations proposées dans ce document au sein du projet quebec.
Télécharger le projet

Préparation des données

Dans cet atelier nous allons utiliser des données portant sur les municipalités du Québec. Dans un premier temps nous avons préparé et nettoyé les données avec le logiciel R. Les différentes étapes de préparation sont décrites dans le script suivant.

Code
library(sf)
library(mapsf)
library(readxl)
library(rmapshaper)

# Communes
mun <- st_read("data-raw/munic_s.shp", quiet = TRUE)
# Transformation conique équivalente d'ALbers
mun <- st_transform(mun, 6623)
# Suppression des surfaces en eau
mun <- mun[!is.na(mun$MUS_CO_GEO),]
# Selection des variables pertinentes
mun <- mun[, c("MUS_CO_GEO", "MUS_NM_MUN", "MUS_CO_MRC", "MUS_NM_MRC", "MUS_NM_REG")]
# Simplifier les contours des polygones
mun <- ms_simplify(mun, )

# Agregation des multipolygones
mun <- aggregate(x = mun[, -1], 
                 by = list(MUS_CO_GEO = mun$MUS_CO_GEO),
                 FUN = head, 1)
mun <- st_cast(mun, "MULTIPOLYGON")

# Import données population et nettoyage
df <- read_xlsx("data-raw/stats_socio_eco/Pop-MUN-total.xlsx", 
                skip = 3, n_max = 1292,
                col_types = c(rep("text", 2),"numeric",rep("text", 4), 
                              rep("numeric", 22))) 
df <- as.data.frame(df)
df <- df[,c(1:3, 8:29)]
col_pop <- paste0("POP_", seq(2001, 2022, 1))
colnames(df) <- c("NOM", "STATUT", "MUS_CO_GEO", col_pop)
df$MUS_CO_GEO <- formatC(df$MUS_CO_GEO, width = 5, flag = "0", format = "d")

mun <- merge(mun, df, by = "MUS_CO_GEO", all.x = T)


# Import données socio-éco et nettoyage
df2 <- data.frame(read_xlsx("data-raw/stats_socio_eco/Tableau.xlsx", 
                            skip = 4, n_max = 1163))
df2 <- df2[-c(1:2),]
df2 <- df2[,c(2:4,6,8,10)]
colnames(df2) <- c("NOM", "REG", "VIT_ECO", "REV_MED", "TX_2564", "TAAM")
df2$id <- paste0(df2$NOM, "_", df2$REG)

df2$VIT_ECO <- as.numeric(df2$VIT_ECO)
df2$REV_MED <- as.numeric(df2$REV_MED)
df2$TX_2564 <- as.numeric(df2$TX_2564)
df2$TAAM <- as.numeric(df2$TAAM)
mun$id <- paste0(mun$NOM, " (", mun$STATUT, ")_", mun$MUS_NM_REG)
mun <- merge(mun, df2, by = "id", all.x = TRUE)
mun <- mun[, c(2:6, 8:30, 33:36)]



# agregation de municipalité en Municipalité régionale de comté (MRC)
mun <- st_set_precision(mun, 1)
mrc <- aggregate(mun[, c("MUS_NM_MRC","MUS_NM_REG")], 
                 by = list(MUS_CO_MRC = mun$MUS_CO_MRC),
                 FUN = head, 1)
mrc <- st_cast(mrc, "MULTIPOLYGON")
mun <- st_set_precision(mun, 0)

st_write(st_transform(mrc, 4326), "data/mrc.geojson")
st_write(st_transform(mun, 4326), "data/munic.geojson")

Ce script abouti à la création de deux fichiers :

  • mrc.geojson est un fond de carte des Municipalité Régionales de Comté.
  • munic.geojson et un fond de carte des municipalités contenant des données socio-démographiques.

R

L’écosystème spatial de R

Le logiciel R (R Core Team, 2023) permet depuis longtemps de traiter et représenter les données spatiales.
Le principal package permettant de traiter les données vectorielles avec R est le package sf (Pebesma, 2018).
Les packages stars (Pebesma et Bivand, 2023) et terra (Hijmans, 2023) quand à eux sont les principaux packages dédiés au traitement des données raster.

Les développements actuels s’appuient sur ce socle et forment un écosystème robuste qui offre aux utilisateurs la plupart des fonctionnalités autrefois réservées aux Systèmes d’Information Géographique.
Le package mapsf (Giraud, 2022a) par exemple s’appuie sur le package sf pour créer des cartes thématiques. D’autres packages sont dédiés à la cartographie interactive, aux statistiques spatiales, l’interaction avec des API de cartographie, la création de cartogrammes etc.

Le package sf

Le package sf a été publié fin 2016 par Edzer Pebesma. Ce package permet l’import, l’export, la manipulation et l’affichage de données spatiales vectorielles. Pour cela sf s’appuie sur une série de bibliothèques spatiales : GDAL (GDAL/OGR contributors, 2022) et PROJ (PROJ contributors, 2021) pour les opérations d’import, d’export et de projection, et GEOS (GEOS contributors, 2021) pour les opérations de géotraitement (buffer, intersection…).
Ce package propose des objets simples (suivant le standard simple feature) dont la manipulation est assez aisée. Une attention particulière a été portée à la compatibilité du package avec la syntaxe pipe (|> ou %>%) et les opérateurs du tidyverse (Wickham et al., 2019).

Pebesma et Bivand (2023)
Historique

Le package sf est venu remplacer les packages sp (Pebesma et Bivand, 2005), rgeos (Bivand et Rundel, 2023) et rgdal (Bivand et al., 2023) en combinant leurs fonctionnalités dans un package unique plus ergonomique.

Format des objets spatiaux sf

Les objets sf sont des data.frame dont l’une des colonnes contient des géométries.
Ce format est très pratique dans la mesure ou les données et les géométries sont intrinsèquement liées dans un même objet.

Vignette décrivant le format simple feature
library(sf)
mtq <- st_read(dsn = "data/mtq.gpkg", layer = "mtq")
plot(st_geometry(mtq))

plot(mtq)

mtq_c <- st_centroid(mtq)
plot(st_geometry(mtq))
plot(st_geometry(mtq_c), cex = 1.2, col = "red", pch = 20, add = TRUE)

mat <- st_distance(x = mtq_c, y = mtq_c)
mat[1:5, 1:5]
Units: [m]
          [,1]     [,2]      [,3]      [,4]      [,5]
[1,]     0.000 35316.29  3019.561 12247.132 17181.321
[2,] 35316.293     0.00 38303.361 25478.399 18597.357
[3,]  3019.561 38303.36     0.000 15099.121 20200.611
[4,] 12247.132 25478.40 15099.121     0.000  7149.189
[5,] 17181.321 18597.36 20200.611  7149.189     0.000
mtq_u <- st_union(mtq)
plot(st_geometry(mtq), col = "lightblue")
plot(st_geometry(mtq_u), lwd = 2, border = "red", add = TRUE)

mtq_b <- st_buffer(x = mtq_u, dist = 5000)
plot(st_geometry(mtq_b), col = "grey", lwd = 2, border = "red")
plot(st_geometry(mtq), col = "lightblue", add = TRUE)
plot(st_geometry(mtq_u), lwd = 2, add = TRUE)

mtq_z <- st_intersection(x = mtq, y = p)
plot(st_geometry(mtq))
plot(st_geometry(mtq_z), col = "red", border = "green", add = TRUE)
plot(st_geometry(mtq_z), lwd = 2)

mtq_c |> 
  st_union() |> 
  st_voronoi() |> 
  st_collection_extract("POLYGON") |> 
  st_intersection(mtq_u) |> 
  st_sf() |> 
  st_join(mtq_c, st_intersects) |>
  st_cast("MULTIPOLYGON") |>
  st_geometry() |>
  plot(col = "ivory4")

Le package mapsf

mapsf permet de créer la plupart des types de carte utilisés habituellement en cartographie statistique.
Pour chaque type de carte, plusieurs paramètres permettent de personnaliser la représentation cartographique. Ces paramètres sont les mêmes que ceux que l’on retrouve dans les logiciels de SIG ou de cartographie usuels.
Associées aux fonctions de représentation des données d’autres fonctions sont dédiées à l’habillage cartographique, à la création de cartons ou à l’export des cartes.
mapsf est le successeur de cartography (Giraud et Lambert, 2017), il offre les mêmes fonctionnalités principales tout en étant plus léger et plus ergonomique.

Chaîne de traitement typique

library(mapsf)
# Import the sample dataset
mtq <- mf_get_mtq()
# Plot the base map
mf_map(x = mtq)
# Plot proportional symbols
mf_map(x = mtq, var = "POP", type = "prop")
# Plot a map layout
mf_layout(title = "Population in Martinique", 
          credits = "T. Giraud; Sources: INSEE & IGN, 2018")

# Export a map with a theme and extra margins 
mf_export(x = mtq, filename = "fig/mtq.svg",  width = 5, 
          theme = "green", expandBB = c(0,0,0,.3)) 
# Plot a shadow
mf_shadow(mtq, col = "grey10", add = TRUE)
# Plot a choropleth map
mf_map(x = mtq, var = "MED", type = "choro",
       pal = "Dark Mint", 
       breaks = "quantile", 
       nbreaks = 6, 
       leg_title = "Median Income\n(euros)", 
       leg_val_rnd = -2, 
       add = TRUE)
# Start an inset map
mf_inset_on(x = "worldmap", pos = "right")
# Plot mtq position on a worldmap
mf_worldmap(mtq, col = "#0E3F5C")
# Close the inset
mf_inset_off()
# Plot a title
mf_title("Wealth in Martinique, 2015")
# Plot credits
mf_credits("T. Giraud\nSources: INSEE & IGN, 2018")
# Plot a scale bar
mf_scale(size = 5)
# Plot a north arrow
mf_arrow('topleft')
dev.off()

Note

D’autres packages peuvent être utilisés pour réaliser des cartes thématiques.
Le package ggplot2 (Wickham, 2016), en association avec le package ggspatial (Dunnington, 2023), permet par exemple d’afficher des objets spatiaux et de réaliser des cartes thématiques simples.
Le package tmap (Tennekes, 2018) est dédié à la création de cartes thématiques, il utilise une syntaxe proche de celle de ggplot2 (enchaînement d’instructions combinées avec le signe +).
La documentation et les tutoriels pour utiliser ces deux packages sont facilement accessibles sur le web.

Ressources

Geocomputation with R - Lovelace et al. (2019)

Spatial Data Science with applications in R - Pebesma et Bivand (2023)

Spatial Data Science with R and “terra” - Robert J. Hijmans

Progress in the R ecosystem for representing and handling spatial data - Bivand (2021)

Cartographie avec R - Giraud et Pecout (2023a)

Géomatique avec R - Giraud et Pecout (2023b)

CRAN Task View: Analysis of Spatial Data

R Special Interest Group on using Geographical data and Mapping (mailing list)

Exemples d’analyse sur les données du Québec

Import et préparation des données

Nous utilisons le package sf pour importer 2 fichiers geojson.
Il est ensuite possible de changer la projection des couches avec la fonction st_transform().
Nous pouvons aussi créer une couche des régions du Québec en agrégeant les Municipalité Régionales de Comté.

# Import des données
library(sf)
mun_raw <- st_read("data/munic.geojson", quiet= TRUE)
mrc_raw <- st_read("data/mrc.geojson", quiet = TRUE)
# Projection NAD83 / Quebec Albers
epsg_6623 <- "EPSG:6623"
mun <- st_transform(mun_raw, epsg_6623)
mrc <- st_transform(mrc_raw, epsg_6623)
# Création d'une couche des régions
reg <- aggregate(x = mrc[,'MUS_NM_REG'], 
                 by = list(REG = mrc$MUS_NM_REG), 
                 FUN = head, 1)

Première cartographie simple

Sur cette première carte nous allons simplement afficher les différentes couches en les superposant.
Nous utilisons le package mapsf qui permet aussi de créer une couche d’étiquettes avec la fonction mf_label().

library(mapsf)
mf_map(mun, border = "white")
mf_map(mrc, col = NA, border = "grey70", lwd = 1, add = TRUE)
mf_map(reg, col = NA, border = "black", lwd = 1.2, add = TRUE)
mf_label(reg, var = "MUS_NM_REG", halo = TRUE, overlap = FALSE)
mf_scale(size = 100)
mf_title("Le Québec")
mf_credits(txt = "Ministère des Ressources naturelles et des Forêts, 2023", 
           bg = "#ffffffCC")

Où sont les restaurants dans la ville de Québec?

Nous voulons savoir dans quelles zones de la ville de Québec se trouvent les restaurants.
Pour connaître la position des restaurants dans la ville nous allons extraire des informations la base de donnés cartographique libre OpenStreetMap avec le package osmdata (Padgham et al., 2017). Le package osmdata utilise l’API du service Overpass turbo pour extraire des données vectorielles de la base de données OpenStreetMap.

Nous devons tout d’abord définir une emprise spatiale à partir de laquelle nous pourrons extraire les informations sur les restaurants.
Après avoir démarré les packages nécessaires, nous allons tout d’abord sélectionner la MRC de Québec dans la couche des MRC.

library(sf)
library(osmdata)
nom_mrc <- "Québec"
target <- mrc[mrc$MUS_NM_MRC == nom_mrc, ]

Nous allons ensuite définir une bounding box correspondant à l’emprise de la MRC. Le package osmdata fonctionne avec des objets en WGS84 nous avons donc besoin de reprojeter les données.

# Reprojection en WGS84
target_4326 <- st_transform(target, "EPSG:4326")
# Création de la bounding box
bbox <- st_bbox(target_4326)

Nous pouvons maintenant définir ce que nous voulons extraire de la base de donnée OSM. Une connaissance de la structuration de la base de données OSM est nécessaire (Wiki OpenStreetMap). Nous utilisons le système de clef/valeur pour construire la requête.

# Initialisation de la requête à envoyer
q <- opq(bbox = st_bbox(bbox))
# Choix des objets à extraire
req <- add_osm_feature(opq = q, 
                       key = 'amenity', 
                       value = "restaurant")

La fonction osmdata_sf() permet d’envoyer la requête et de recevoir les résultats sous forme de data.frame sf.

# Reception de la requete
res <- osmdata_sf(q = req)
res
Object of class 'osmdata' with:
                 $bbox : 46.7152971489633,-71.5896138558637,46.9818615959766,-71.1448958492775
        $overpass_call : The call submitted to the overpass API
                 $meta : metadata including timestamp and version numbers
           $osm_points : 'sf' Simple Features Collection with 859 points
            $osm_lines : NULL
         $osm_polygons : 'sf' Simple Features Collection with 61 polygons
       $osm_multilines : NULL
    $osm_multipolygons : NULL

Ce résultat contient des restaurants représentés par de points (osm_points) et d’autre représentés par des polygones (osm_polygons). La couche des points contient également les points qui composent les polygones de la couche de polygones. La fonction unique_osmdata() permet de filtrer ces “doublons”.

# Reduction du resultats 
# (les points composant les polygones sont supprimés)
res <- unique_osmdata(res)
res
Object of class 'osmdata' with:
                 $bbox : 46.7152971489633,-71.5896138558637,46.9818615959766,-71.1448958492775
        $overpass_call : The call submitted to the overpass API
                 $meta : metadata including timestamp and version numbers
           $osm_points : 'sf' Simple Features Collection with 407 points
         $osm_polygons : 'sf' Simple Features Collection with 61 polygons
       $osm_multilines : NULL
    $osm_multipolygons : NULL

L’étape suivante consiste à extraire les points et les polygones, puis à transformer les polygones en points en utilisant leurs centroides. Cela permet de constituer une couche de points complète et cohérente.

# Extraction des points
resto_point <- res$osm_points
# Extraction des polygones
resto_poly <- res$osm_polygons
# Transformation des polygones en points (centroides)
resto_poly_point <- st_centroid(resto_poly)
# Listes des champs à conserver
chps <- c("osm_id", "name", "cuisine")
# Rassembler les 2 couches de points
resto <- rbind(resto_point[, chps], resto_poly_point[, chps])
resto
Simple feature collection with 468 features and 3 fields
Geometry type: POINT
Dimension:     XY
Bounding box:  xmin: -71.54201 ymin: 46.71551 xmax: -71.14547 ymax: 46.93504
Geodetic CRS:  WGS 84
First 10 features:
             osm_id                       name    cuisine
291667452 291667452                  Chez Cora       <NA>
295803248 295803248                  Normandin       <NA>
345886369 345886369                Chez Victor     burger
386267989 386267989                     Ly-Hai vietnamese
393559700 393559700                     Mike's    italian
400282934 400282934            Le Rideau Rouge     burger
400283204 400283204 Petits Creux & Grands Crus     french
400283356 400283356                 Pizzédelic      pizza
400292015 400292015                  Piazzetta      pizza
400292075 400292075                   Graffiti       <NA>
                            geometry
291667452 POINT (-71.19642 46.85348)
295803248 POINT (-71.41531 46.85531)
345886369 POINT (-71.22581 46.80766)
386267989  POINT (-71.2281 46.80561)
393559700 POINT (-71.35824 46.83807)
400282934 POINT (-71.22516 46.80328)
400283204  POINT (-71.2253 46.80341)
400283356 POINT (-71.22521 46.80333)
400292015 POINT (-71.22463 46.80285)
400292075 POINT (-71.22447 46.80271)

Maintenant que nous avons cette couche de restaurants nous allons pouvoir l’afficher.
Pour cela nous pouvons utiliser un fond de carte issues de tuiles raster grâce au package maptiles (Giraud, 2023). Nous allons d’abord reprojeter les différentes couches d’information en Web/Mercator (EPSG:3857) car les tuiles raster sont fournies dans cette projection .

target <- st_transform(target, "EPSG:3857")
resto <- st_transform(resto, "EPSG:3857")

Nous pouvons maintenant télécharger les tuiles raster grace à la fonction get_tiles().

library(maptiles)
tiles <- get_tiles(x = target, 
                   provider = "CartoDB.PositronNoLabels",
                   project = FALSE, 
                   crop = TRUE, 
                   zoom = 11)

Nous créons ensuite la carte avec mapsf.

library(mapsf)
mf_theme(mar = c(0,0,0,0), inner = TRUE)
mf_export(x = tiles,
          filename = "fig/restaurants01.png",
          width = ncol(tiles), height = nrow(tiles))
mf_raster(tiles, add = TRUE)
mf_map(target, col = NA, lwd = 2, add = TRUE)
mf_map(resto, col = "red", add = TRUE)
mf_title("Les restaurants à Québec")
mf_credits("© OpenStreetMap contributors © CARTO", bg = "white")
dev.off()

Nous allons maintenant agréger les restaurants dans une grille régulière pour tenter rendre plus lisible l’organisation spatiale des restaurants à Québec.

# Création d'une grille régulière (carreaux : 2km * 2km)
grid <- st_make_grid(target, cellsize = 2000)
grid <- st_sf(id = 1:length(grid), geom = grid)

# Comptage des points dans les polygones
grid$n <- lengths(st_intersects(grid, resto, sparse = TRUE))

# Cartographie
mf_export(x = tiles,
          filename = "fig/restaurants02.png",
          width = ncol(tiles), height = nrow(tiles))
mf_raster(tiles, add = TRUE)
mf_map(target, col = NA, lwd = 2, add = TRUE)
mf_map(x = grid[grid$n >0 , ], 
       var = "n", 
       type = "choro", 
       breaks = 'geom', 
       nbreak = 6, 
       alpha= .75, 
       border = NA, 
       pal = "Burg", 
       leg_frame = TRUE, 
       leg_pos = "left", 
       leg_title = "Nombre de restaurants",
       leg_val_rnd = 0,
       add = TRUE)
mf_title("Les restaurants à Québec")
mf_credits("© OpenStreetMap contributors © CARTO", bg = "white")
dev.off()

Nous pouvons aussi utiliser une méthode de lissage telle que la méthode de lissage par noyaux (KDE) grâce au package spatstat (Baddeley et al., 2015).

library(spatstat)
library(terra)
library(tanaka)
p <- as.ppp(st_coordinates(resto), W = as.owin(st_bbox(target)))
# portée du lissage 500m
ds <- density.ppp(p, sigma = 500, eps = c(100, 100), positive = TRUE)
# transformation en raster terra et passage en unité par km²
rasdens <- rast(ds) * 1000 * 1000
# ajout des information de projection
crs(rasdens) <- 'EPSG:3857'
# affichage par défaut
plot(rasdens)

Il est ensuite possible de représenter les densités calculées avec les packages tanaka (Giraud, 2022b) et mapsf.

# Calcul des polygones de contour en fonction de bornes de classe
iso <- tanaka_contour(rasdens, breaks = c(1,2,5,10,20,25))
# choix d'une palette de couleur
cols <- mf_get_pal(n = 5, palette = "Teal", rev = TRUE)
# selection des région ou la densité est >= 1
iso <- iso[-1, ]
# Cartographie
mf_export(x = tiles,
          filename = "fig/restaurants03.png",
          width = ncol(tiles), height = nrow(tiles))
mf_raster(tiles, add = TRUE)
mf_map(target, add = T, col = NA, lwd = 2)
tanaka(iso, 
       legend.title = "Nombre de\nrestaurants\nau km²\n(KDE, sigma=500)",
       legend.pos = "topright", 
       shift = 65,
       col = cols,
       add = TRUE)
mf_title("Les restaurants à Québec")
mf_credits("© OpenStreetMap contributors © CARTO",bg = "white")
dev.off()
png 
  2 

À vous de jouer

  • Vous devez étudier la répartition des restaurants dans une autre MRC, par exemple celle de Montréal.

  • Vous devez étudier la répartition des bars et pubs à Québec (dans OpenStreetMap key = “amenity”, value = “pub” et value = “bar”).

Pour cela vous devrez construire un document quarto (au format de votre choix) et adapter quelques passages du code présenté précédemment.

Python

Le langage Python

  • Python : un langage polyvalent, interprété et multi-paradigme

  • De plus en plus utilisé pour la science des données

  • Un écosystème robuste pour différents domaines d’application scientifiques

Écosystème pour le géospatial

Données vectorielles

Le package GeoPandas

Un projet open source pour faciliter le travail avec des données géospatiales vectorielles en Python. GeoPandas étend Pandas pour permettre de disposer d’un type de colonne géométrique et pour permettre d’effectuer des opérations spatiales. Les opérations géométriques sont réalisées avec shapely, les accès en lecture / écriture aux fichiers utilisent fiona et la visualisation utilise matplotlib.

Format des GeoDataFrame
  • Utilisation :

Avec la fonction read_file :

import geopandas as gpd

nybb = gpd.read_file(file_path)

Avec la méthode plot des GeoDataFrame :

nybb.plot()

Affichage :

print(nybb.crs)
EPSG:2263

Transformation :

nybb_geo = nybb.to_crs('EPSG:4326')

En utilisant la bibliothèque contextily :

import contextily as cx

ax = nybb.to_crs('EPSG:3857').plot()
cx.add_basemap(ax, source='https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png')
ax.set_axis_off()
ax

Simplement en utilisant l’attribut centroid des GeoDataFrame :

nybb.centroid.plot()

Simplement en utilisant l’attribut boundary des GeoDataFrame :

nybb.boundary.plot()

En utilisant la méthode buffer des GeoDataFrame :

buff_nybb = nybb.buffer(3000)
ax = buff_nybb.plot(color="red")
nybb.plot(ax=ax, color="aliceblue")

En utilisant la méthode dissolve des GeoDataFrame :

# On peut utiliser dissolve(by="nom_colonne") si on veut aggréger selon les valeurs d'une colonne
agg = nybb.dissolve()
agg.plot()

En utilisant la méthode intersection des GeoDataFrame :

from shapely import wkt

point = wkt.loads('Point(998769.1146889535 174169.7607268664)')

intersecting = nybb.intersection(point.buffer(35000))
/home/tim/env-quarto/lib/python3.9/site-packages/shapely/set_operations.py:133: RuntimeWarning: invalid value encountered in intersection
  return lib.intersection(a, b, **kwargs)


ax = nybb.plot()
ax = intersecting.plot(ax=ax, color="red")
ax

Données raster

  • Rasterio (Gillies et al., 2013) :
    • lecture / écriture de raster (wrapper de haut niveau autour de GDAL)
    • données représentées sous forme d’array NumPy
    • reprojection
    • resampling
    • virtual files
    • etc.
  • Rasterstats :
    • résumer des données raster sur la base de géométries vectorielles
    • extraction de valeurs à un point précis
  • xarray (Hoyer et al., s. d.) et rioxarray (Snow, Brochart, et al., 2023):
    • xarray pour travailler avec des tableaux multidimensionnels étiquetés
    • rioxarray pour ouvrir des jeux des données raster avec rasterio, les stocker dans le format de xarray, et avoir accès à différentes fonctionnalités de rasterio
    • moins “rustique” que d’utiliser rasterio seul / dépend de l’usage souhaité

Le package Rasterio

Exemple d’utilisation :

import rasterio as rio

with rio.open('./data/MODIS_ARRAY.nc') as f:
    # Métadonnées :
    metadata = f.meta
    # Lire toutes les bandes :
    data = f.read()
    # Ou f.read(1) pour lire seulement la première bande
  • Un dictionnaire de métadonnées + un tableau NumPy contenant la (ou les) bande(s) :
print(metadata)
{'driver': 'netCDF', 'dtype': 'int16', 'nodata': -28672.0, 'width': 200, 'height': 200, 'count': 1, 'crs': CRS.from_wkt('PROJCS["unknown",GEOGCS["unknown",DATUM["unknown",SPHEROID["unknown",6371007.181,0]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]]],PROJECTION["Sinusoidal"],PARAMETER["longitude_of_center",0],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH]]'), 'transform': Affine(231.6563582639561, 0.0, -7274009.649486291,
       0.0, -231.65635826375018, 5050108.61015275)}
print(data)
[[[-28672 -28672 -28672 ...    656    656    554]
  [-28672 -28672 -28672 ...    694    694    642]
  [-28672 -28672 -28672 ...    456    575    642]
  ...
  [   993    817    817 ...    471    479    498]
  [   893    893    816 ...    479    479    469]
  [   816    816    832 ...    515    469    485]]]

Les package xarray et rioxarray

Exemple d’utilisation :

import rioxarray
import xarray

xds = xarray.open_dataarray("./data/MODIS_ARRAY.nc")
  • Un objet de type xarray.DataArray ou xarray.Dataset qui contient les différentes méta-données et les données :
xds
<xarray.DataArray (y: 200, x: 200)>
[40000 values with dtype=float32]
Coordinates:
  * y        (y) float64 5.05e+06 5.05e+06 5.05e+06 ... 5.004e+06 5.004e+06
  * x        (x) float64 -7.274e+06 -7.274e+06 ... -7.228e+06 -7.228e+06
Attributes:
    crs:        +a=6371007.181 +b=6371007.181 +lon_0=0 +no_defs +proj=sinu +u...
    res:        [231.65635826 231.65635826]
    is_tiled:   0
    nodata:     -28672.0
    transform:  [ 2.31656358e+02  0.00000000e+00 -7.27400965e+06  0.00000000e...
filled = xds.rio.interpolate_na()

En utilisant la méthode plot des objets xarray.DataArray (ici après sélection d’un subset des données) :

import matplotlib.pyplot as plt

fig = plt.figure(figsize=(15, 5))
ax1, ax2 = (fig.add_subplot(131), fig.add_subplot(133))

xds.isel(x=slice(0, 20), y=slice(0, 20)).plot(ax=ax1)
filled.isel(x=slice(0, 20), y=slice(0, 20)).plot(ax=ax2)

  • Affichage :
xds.rio.crs
CRS.from_wkt('PROJCS["unknown",GEOGCS["unknown",DATUM["unknown",SPHEROID["unknown",6371007.181,0]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]]],PROJECTION["Sinusoidal"],PARAMETER["longitude_of_center",0],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH]]')
  • Transformation :
xds = xds.rio.reproject("EPSG:6623")
  • Découpage selon l’emprise des données contenues dans un GeoDataFrame (ici gdf) :
clipped = xds.rio.clip(gdf.geometry.values, gdf.crs, drop=False, invert=True)

Analyse spatiale

Écosystème de bibliothèques pour l’analyse spatiale PySAL (Python Spatial Analysis Library, Rey et Anselin (2007)) :

  • Explorer:
    • Analyse exploratoire des données spatiales (package esda)
    • Analyse de la dynamique des données spatiales longitudinales (package giddy)
    • Mesure des inégalités dans l’espace et dans le temps (package inequality)
    • Analyse statistique de motifs ponctuels planaires (package pointpats)
    • Mesure de la ségrégation dans le temps et dans l’espace (package segregation)
    • Morphométrie urbaine (package momepy)
  • Modéliser:
    • Régression géographiquement pondérée à plusieurs échelles (package mgwr)
    • Modèles linéaires généralisés épars (package spglm)
    • Modèles d’interaction spatiale (package spint)
    • Modèles de régression spatiale (package spreg)
    • Modèles de composantes de variance à corrélation spatiale à plusieurs niveaux (package spvcm)
    • Areal interpolation et cartographie dasymétrique (package tobler)
    • Accessibilité spatiale (package access)
    • Optimisation spatiale (package spopt)

Autres packages utiles …

  • Binding Python pour GRASS + Intégration dans les notebooks Jupyter
  • Iris - Analyse et visualisation de données des sciences de la Terre.

  • Pour la cartographie : cartopy ; geoviews et geoplot basés sur cartopy ; PyGMT (selon les usages - pas de solution aussi aboutie que mapsf pour les cartes réalisées en SHS).

Ressources Python Géospatial

Interaction R ⇄ Python dans Quarto

  • Définition de variables dans un chunk R :
a <- 42
b <- list(1, 2, 3)
c <- c(12, 13, 14)
  • Récupération depuis un chunk Python :

en utilisant la variable r, un point, et le nom de la variable R à récupérer

print(r.a)
42.0
print(r.b)
[1.0, 2.0, 3.0]
print(r.c)
[12.0, 13.0, 14.0]
  • Définition de variables dans un chunk Python :
a = 42
b = [1, 2, 3]
  • Récupération depuis un chunk R :

en utilisant la variable py, un dollar $, et le nom de la variable Python à récupérer

print(py$a)
[1] 42
print(py$b)
[1] 1 2 3

Interaction R ⇄ Python dans Quarto (suite)

  • Il est possible d’échanger des types plus complexes (data.frameDataFrame pandas, tableau numpy, etc.)
  • Depuis un chunk R :
df <- data.frame(
   emp_id = c(1:5),
   emp_name = c("Rick","Dan","Michelle","Ryan","Jane"),
   salary = c(623.3,515.2,611.0,729.0,843.25),
   start_date = as.Date(c("2012-01-01", "2013-09-23", "2014-11-15", "2014-05-11", "2015-03-27")),
   stringsAsFactors = FALSE
)
  • Récupération depuis un chunk Python :
df = r.df
print(df.head())
   emp_id  emp_name  salary  start_date
0       1      Rick  623.30  2012-01-01
1       2       Dan  515.20  2013-09-23
2       3  Michelle  611.00  2014-11-15
3       4      Ryan  729.00  2014-05-11
4       5      Jane  843.25  2015-03-27
  • Depuis un chunk Python :
import pandas as pd
import numpy as np

df2 = pd.DataFrame({
  "emp_id": list(range(5)),
  "emp_name": ["Rick", "Dan", "Michelle", "Ryan", "Jane"],
  "salary": [623.3, 515.2, 611.0, 729.0, 843.25],
  "start_date": pd.to_datetime(["2012-01-01", "2013-09-23", "2014-11-15", "2014-05-11", "2015-03-27"]),
})

arr = np.array([[12, 47], [34, 90], [23, 19]])
  • Utilisation depuis un chunk R :
head(py$df2)
  emp_id emp_name salary          start_date
1      0     Rick 623.30 2012-01-01 01:00:00
2      1      Dan 515.20 2013-09-23 02:00:00
3      2 Michelle 611.00 2014-11-15 01:00:00
4      3     Ryan 729.00 2014-05-11 02:00:00
5      4     Jane 843.25 2015-03-27 01:00:00
print(py$arr)
     [,1] [,2]
[1,]   12   47
[2,]   34   90
[3,]   23   19

Interaction R ⇄ Python dans Quarto (suite)

Et pour les objets spatiaux ? (sf / geopandas)

  • Si la conversion data.frameDataFrame est transparente, il n’en est pas de même pour les dataframes avec une composante spatiale (les objets sf sont convertis en DataFrameau lieu de GeoDataFrame : perte de la dimension spatiale, attribut crs non transféré et colonne geometry qui contient désormais seulement des listes de coordonnées).

  • L’écriture d’une petite function permet de résoudre ça :

from shapely.geometry import shape
import geopandas as gpd

def restore_geodataframe(df, crs, geom_type):
  geom = df.geometry.apply(lambda coords: shape({ "type": geom_type, "coordinates": coords }))
  gdf = gpd.GeoDataFrame(df.drop(['geometry'], axis=1), geometry=geom)
  gdf.set_crs(crs, inplace=True)
  return gdf

Ainsi, les objets sf pourront être convertis en GeoDataFrame ainsi :

mrc = restore_geodataframe(r.mrc, r.epsg_6623, "MultiPolygon")
mrc.head()
  MUS_CO_MRC                                    MUS_NM_MRC                       MUS_NM_REG                                           geometry
0         01  Communauté maritime des Îles-de-la-Madeleine  Gaspésie - Îles-de-la-Madeleine  MULTIPOLYGON (((505936.000 400484.000, 503752....
1         02                               Le Rocher-Percé  Gaspésie - Îles-de-la-Madeleine  MULTIPOLYGON (((288282.000 493474.000, 281197....
2         03                              La Côte-de-Gaspé  Gaspésie - Îles-de-la-Madeleine  MULTIPOLYGON (((252570.000 528170.000, 252646....
3         04                             La Haute-Gaspésie  Gaspésie - Îles-de-la-Madeleine  MULTIPOLYGON (((120775.000 563455.000, 122763....
4         05                                   Bonaventure  Gaspésie - Îles-de-la-Madeleine  MULTIPOLYGON (((163436.000 501024.000, 187515....

Python, Quarto et interactivité

  • Utilisation des widgets Jupyter possible (seulement si utilisation de l’engin de rendu jupyter, pas avec knitr - i.e. l’inverse des htmlwidgets en R qui ne fonctionne que si knitr est utilisé).

  • Exemple :

Exemples d’analyse sur les données du Québec

Objectifs : Récupérer les données préparées avec R (MRC et localisation des restaurants) pour les croiser avec des données d’occupation du sol au format raster.

  • Les imports nécessaires :
import numpy as np
import pandas as pd
import geopandas as gpd
import rioxarray
import xarray

from matplotlib import pyplot as plt
from rasterio.plot import show
from rasterstats import zonal_stats, point_query
  • Récupération des jeux de données et de quelques autres infos (SRC, etc.) depuis l’environnement R :
# Le SCR des données
dst_crs = r.epsg_6623

# On récupère le jeu de données "mrc" (municipalités régionales de comté)
# et le jeu de données "resto"
mrc = restore_geodataframe(r.mrc, dst_crs, "MultiPolygon")
resto = restore_geodataframe(r.resto, "EPSG:3857", "Point")
resto.to_crs(dst_crs, inplace=True)
reg = mrc.dissolve(by="MUS_NM_REG")

sel = r.nom_mrc # Québec

# Chemin du jeu de données raster
# et du fichier contenant les descriptions des catégories
lc_fp = './data/T01_PROVINCE.tif'
lc_categories_fp = './data/correspondance_raster_CL_COUV.dbf'
  • Sélection des entités correspondantes et affichage, relativement au reste de la province :
extract = mrc[mrc.MUS_NM_MRC == sel]

ax = mrc.plot(color="lightblue", edgecolor="grey", alpha=0.5, figsize=(16, 12))
ax = reg.plot(ax=ax, color=None, edgecolor="orange", alpha=0.8)
ax = extract.plot(ax=ax, color="red")
ax.set_axis_off()
ax

  • Ouverture et reprojection d’un jeu de données raster (avec rioxarray) :
xds = rioxarray.open_rasterio(lc_fp)
xds = xds.rio.reproject(dst_crs)
xds
<xarray.DataArray (band: 1, y: 23726, x: 28690)>
array([[[255, 255, 255, ..., 255, 255, 255],
        [255, 255, 255, ..., 255, 255, 255],
        [255, 255, 255, ..., 255, 255, 255],
        ...,
        [255, 255, 255, ..., 255, 255, 255],
        [255, 255, 255, ..., 255, 255, 255],
        [255, 255, 255, ..., 255, 255, 255]]], dtype=uint8)
Coordinates:
  * x            (x) float64 -8.306e+05 -8.305e+05 ... 6.038e+05 6.039e+05
  * y            (y) float64 1.303e+06 1.303e+06 ... 1.173e+05 1.172e+05
  * band         (band) int64 1
    spatial_ref  int64 0
Attributes:
    AREA_OR_POINT:           Area
    RepresentationType:      THEMATIC
    STATISTICS_COVARIANCES:  5.53386940676013
    STATISTICS_MAXIMUM:      11
    STATISTICS_MEAN:         6.4740178066355
    STATISTICS_MINIMUM:      1
    STATISTICS_SKIPFACTORX:  1
    STATISTICS_SKIPFACTORY:  1
    STATISTICS_STDDEV:       2.3524177789585
    scale_factor:            1.0
    add_offset:              0.0
    _FillValue:              255
  • Affichons le raster reprojeté et l’emprise de la ville de Québec :
# Remplacer les '255' par '0'
xds.data[xds.data == 255] = 0

# Étendue des données
bounds = xds.rio.bounds()
extent = [bounds[0], bounds[2], bounds[1], bounds[3]]

# Création d'une figure vide
fig, ax = plt.subplots(figsize=(16, 12))
# Affichage du raster (attribut 'data' de l'objet xds)
show(xds.data[0], ax=ax, extent=extent, cmap="Set3")
# Affichage de la ville de Québec en rouge par dessus
extract.plot(ax=ax, color='red', edgecolor='red', linewidth=2)
# On enlève l'affichage des axes
ax.set_axis_off()
ax

  • Ouverture du fichier DBF qui contient les correspondances entre les codes et les noms des catégories d’utilisation des sols :
categories = pd.DataFrame(
  gpd.read_file(lc_categories_fp, encoding='utf-8')[['ID', 'CL_COUV', 'Descriptio']])
categories.head(11)
      ID CL_COUV                             Descriptio
0    1.0  010000                 Surfaces artificielles
1    2.0  020000                       Terres agricoles
2    3.0  060000             Milieux humides forestiers
3    4.0  070000  Milieux humides herbacés ou arbustifs
4    5.0  100000        Plans et cours d’eau intérieure
5    6.0  110101    Forêts de conifères à couvert fermé
6    7.0  110102     Forêts de feuillus à couvert fermé
7    8.0  110103          Forêts mixtes à couvert fermé
8    9.0  110200                Forêts à couvert ouvert
9   10.0  000000                         Pas de données
10  11.0  000001               En attente de traitement


# On va convertir cette DataFrame en un dictionnaire pour l'utiliser
# comme un mapping ID -> Description
categories = {
  int(k): v['Descriptio']
  for k,v in categories[['ID', 'Descriptio']].set_index('ID').to_dict(orient='index').items()
}
# Bien qu'il y ait une catégorie 'Pas de données' (ID 10)
# certaines cellules ne comportent pas de valeur (nodata, ici avec la valeur 0)
categories[0] = 'Pas de données'
categories
{1: 'Surfaces artificielles', 2: 'Terres agricoles', 3: 'Milieux humides forestiers', 4: 'Milieux\xa0humides\xa0herbacés\xa0ou\xa0arbustifs', 5: 'Plans et cours d’eau intérieure', 6: 'Forêts de conifères à couvert fermé', 7: 'Forêts\xa0de\xa0feuillus\xa0à\xa0couvert\xa0fermé', 8: 'Forêts\xa0mixtes\xa0à\xa0couvert\xa0fermé', 9: 'Forêts à couvert ouvert', 10: 'Pas de données', 11: 'En attente de traitement', 12: 'Surfaces artificielles vers terres agricoles', 13: 'Surfaces artificielles vers milieux humides forestiers', 14: 'Surfaces artificielles vers milieux\xa0humides\xa0herbacés\xa0ou\xa0arbustifs', 15: 'Surfaces artificielles vers plans et cours d’eau intérieure', 16: 'Surfaces artificielles vers forêts de conifères à couvert fermé', 17: 'Surfaces artificielles vers forêts\xa0de\xa0feuillus\xa0à\xa0couvert\xa0fermé', 18: 'Surfaces artificielles vers forêts\xa0mixtes\xa0à\xa0couvert\xa0fermé', 19: 'Surfaces artificielles vers forêts à couvert ouvert', 20: 'Surfaces artificielles vers pas de données', 21: 'Surfaces artificielles vers en attente de traitement', 22: 'Terres agricoles vers surfaces artificielles', 23: 'Terres agricoles vers milieux humides forestiers', 24: 'Terres agricoles vers milieux\xa0humides\xa0herbacés\xa0ou\xa0arbustifs', 25: 'Terres agricoles vers plans et cours d’eau intérieure', 26: 'Terres agricoles vers forêts de conifères à couvert fermé', 27: 'Terres agricoles vers forêts\xa0de\xa0feuillus\xa0à\xa0couvert\xa0fermé', 28: 'Terres agricoles vers forêts\xa0mixtes\xa0à\xa0couvert\xa0fermé', 29: 'Terres agricoles vers forêts à couvert ouvert', 30: 'Terres agricoles vers pas de données', 31: 'Terres agricoles vers en attente de traitement', 32: 'Milieux humides forestiers vers surfaces artificielles', 33: 'Milieux humides forestiers vers terres agricoles', 34: 'Milieux humides forestiers vers milieux\xa0humides\xa0herbacés\xa0ou\xa0arbustifs', 35: 'Milieux humides forestiers vers plans et cours d’eau intérieure', 36: 'Milieux humides forestiers vers forêts de conifères à couvert fermé', 37: 'Milieux humides forestiers vers forêts\xa0de\xa0feuillus\xa0à\xa0couvert\xa0fermé', 38: 'Milieux humides forestiers vers forêts\xa0mixtes\xa0à\xa0couvert\xa0fermé', 39: 'Milieux humides forestiers vers forêts à couvert ouvert', 40: 'Milieux humides forestiers vers pas de données', 41: 'Milieux humides forestiers vers en attente de traitement', 42: 'Milieux\xa0humides\xa0herbacés\xa0ou\xa0arbustifs vers surfaces artificielles', 43: 'Milieux\xa0humides\xa0herbacés\xa0ou\xa0arbustifs vers terres agricoles', 44: 'Milieux\xa0humides\xa0herbacés\xa0ou\xa0arbustifs vers milieux humides forestiers', 45: 'Milieux\xa0humides\xa0herbacés\xa0ou\xa0arbustifs vers plans et cours d’eau intérieure', 46: 'Milieux\xa0humides\xa0herbacés\xa0ou\xa0arbustifs vers forêts de conifères à couvert fermé', 47: 'Milieux\xa0humides\xa0herbacés\xa0ou\xa0arbustifs vers forêts\xa0de\xa0feuillus\xa0à\xa0couvert\xa0fermé', 48: 'Milieux\xa0humides\xa0herbacés\xa0ou\xa0arbustifs vers forêts\xa0mixtes\xa0à\xa0couvert\xa0fermé', 49: 'Milieux\xa0humides\xa0herbacés\xa0ou\xa0arbustifs vers forêts à couvert ouvert', 50: 'Milieux\xa0humides\xa0herbacés\xa0ou\xa0arbustifs vers pas de données', 51: 'Milieux\xa0humides\xa0herbacés\xa0ou\xa0arbustifs vers en attente de traitement', 52: 'Plans et cours d’eau intérieure vers surfaces artificielles', 53: 'Plans et cours d’eau intérieure vers terres agricoles', 54: 'Plans et cours d’eau intérieure vers milieux humides forestiers', 55: 'Plans et cours d’eau intérieure vers milieux\xa0humides\xa0herbacés\xa0ou\xa0arbustifs', 56: 'Plans et cours d’eau intérieure vers forêts de conifères à couvert fermé', 57: 'Plans et cours d’eau intérieure vers forêts\xa0de\xa0feuillus\xa0à\xa0couvert\xa0fermé', 58: 'Plans et cours d’eau intérieure vers forêts\xa0mixtes\xa0à\xa0couvert\xa0fermé', 59: 'Plans et cours d’eau intérieure vers forêts à couvert ouvert', 60: 'Plans et cours d’eau intérieure vers pas de données', 61: 'Plans et cours d’eau intérieure vers en attente de traitement', 62: 'Forêts de conifères à couvert fermé vers surfaces artificielles', 63: 'Forêts de conifères à couvert fermé vers terres agricoles', 64: 'Forêts de conifères à couvert fermé vers milieux humides forestiers', 65: 'Forêts de conifères à couvert fermé vers milieux\xa0humides\xa0herbacés\xa0ou\xa0arbustifs', 66: 'Forêts de conifères à couvert fermé vers plans et cours d’eau intérieure', 67: 'Forêts de conifères à couvert fermé vers forêts\xa0de\xa0feuillus\xa0à\xa0couvert\xa0fermé', 68: 'Forêts de conifères à couvert fermé vers forêts\xa0mixtes\xa0à\xa0couvert\xa0fermé', 69: 'Forêts de conifères à couvert fermé vers forêts à couvert ouvert', 70: 'Forêts de conifères à couvert fermé vers pas de données', 71: 'Forêts de conifères à couvert fermé vers en attente de traitement', 72: 'Forêts\xa0de\xa0feuillus\xa0à\xa0couvert\xa0fermé vers surfaces artificielles', 73: 'Forêts\xa0de\xa0feuillus\xa0à\xa0couvert\xa0fermé vers terres agricoles', 74: 'Forêts\xa0de\xa0feuillus\xa0à\xa0couvert\xa0fermé vers milieux humides forestiers', 75: 'Forêts\xa0de\xa0feuillus\xa0à\xa0couvert\xa0fermé vers milieux\xa0humides\xa0herbacés\xa0ou\xa0arbustifs', 76: 'Forêts\xa0de\xa0feuillus\xa0à\xa0couvert\xa0fermé vers plans et cours d’eau intérieure', 77: 'Forêts\xa0de\xa0feuillus\xa0à\xa0couvert\xa0fermé vers forêts de conifères à couvert fermé', 78: 'Forêts\xa0de\xa0feuillus\xa0à\xa0couvert\xa0fermé vers forêts\xa0mixtes\xa0à\xa0couvert\xa0fermé', 79: 'Forêts\xa0de\xa0feuillus\xa0à\xa0couvert\xa0fermé vers forêts à couvert ouvert', 80: 'Forêts\xa0de\xa0feuillus\xa0à\xa0couvert\xa0fermé vers pas de données', 81: 'Forêts\xa0de\xa0feuillus\xa0à\xa0couvert\xa0fermé vers en attente de traitement', 82: 'Forêts\xa0mixtes\xa0à\xa0couvert\xa0fermé vers surfaces artificielles', 83: 'Forêts\xa0mixtes\xa0à\xa0couvert\xa0fermé vers terres agricoles', 84: 'Forêts\xa0mixtes\xa0à\xa0couvert\xa0fermé vers milieux humides forestiers', 85: 'Forêts\xa0mixtes\xa0à\xa0couvert\xa0fermé vers milieux\xa0humides\xa0herbacés\xa0ou\xa0arbustifs', 86: 'Forêts\xa0mixtes\xa0à\xa0couvert\xa0fermé vers plans et cours d’eau intérieure', 87: 'Forêts\xa0mixtes\xa0à\xa0couvert\xa0fermé vers forêts de conifères à couvert fermé', 88: 'Forêts\xa0mixtes\xa0à\xa0couvert\xa0fermé vers forêts\xa0de\xa0feuillus\xa0à\xa0couvert\xa0fermé', 89: 'Forêts\xa0mixtes\xa0à\xa0couvert\xa0fermé vers forêts à couvert ouvert', 90: 'Forêts\xa0mixtes\xa0à\xa0couvert\xa0fermé vers pas de données', 91: 'Forêts\xa0mixtes\xa0à\xa0couvert\xa0fermé vers en attente de traitement', 92: 'Forêts à couvert ouvert vers surfaces artificielles', 93: 'Forêts à couvert ouvert vers terres agricoles', 94: 'Forêts à couvert ouvert vers milieux humides forestiers', 95: 'Forêts à couvert ouvert vers milieux\xa0humides\xa0herbacés\xa0ou\xa0arbustifs', 96: 'Forêts à couvert ouvert vers plans et cours d’eau intérieure', 97: 'Forêts à couvert ouvert vers forêts de conifères à couvert fermé', 98: 'Forêts à couvert ouvert vers forêts\xa0de\xa0feuillus\xa0à\xa0couvert\xa0fermé', 99: 'Forêts à couvert ouvert vers forêts\xa0mixtes\xa0à\xa0couvert\xa0fermé', 100: 'Forêts à couvert ouvert vers pas de données', 101: 'Forêts à couvert ouvert vers en attente de traitement', 102: 'Pas de données vers surfaces artificielles', 103: 'Pas de données vers terres agricoles', 104: 'Pas de données vers milieux humides forestiers', 105: 'Pas de données vers milieux\xa0humides\xa0herbacés\xa0ou\xa0arbustifs', 106: 'Pas de données vers plans et cours d’eau intérieure', 107: 'Pas de données vers forêts de conifères à couvert fermé', 108: 'Pas de données vers forêts\xa0de\xa0feuillus\xa0à\xa0couvert\xa0fermé', 109: 'Pas de données vers forêts\xa0mixtes\xa0à\xa0couvert\xa0fermé', 110: 'Pas de données vers forêts à couvert ouvert', 111: 'Pas de données vers en attente de traitement', 112: 'En attente de traitement vers surfaces artificielles', 113: 'En attente de traitement vers terres agricoles', 114: 'En attente de traitement vers milieux humides forestiers', 115: 'En attente de traitement vers milieux\xa0humides\xa0herbacés\xa0ou\xa0arbustifs', 116: 'En attente de traitement vers plans et cours d’eau intérieure', 117: 'En attente de traitement vers forêts de conifères à couvert fermé', 118: 'En attente de traitement vers forêts\xa0de\xa0feuillus\xa0à\xa0couvert\xa0fermé', 119: 'En attente de traitement vers forêts\xa0mixtes\xa0à\xa0couvert\xa0fermé', 120: 'En attente de traitement vers forêts à couvert ouvert', 121: 'En attente de traitement vers pas de données', 0: 'Pas de données'}
  • Calcul des statistiques zonales (combien de cellules de chaque type dans la sélection) :
stats = zonal_stats(extract, xds.data[0], affine=xds.rio.transform(), categorical=True, nodata=0)
stats
[{1: 95228, 2: 25829, 3: 4574, 4: 778, 5: 5490, 6: 4124, 7: 43887, 8: 44969, 9: 62, 10: 47}]
  • Traçons un histogramme de ce résultat, en utilisant les noms appropriés des catégories d’utilisation des sols :
# Trier les valeurs par le nombre de cellule
sorted_values = sorted(stats[0].items(), key = lambda x: x[1], reverse=True)

# Utilise les noms de catégories du DataFrame `categories`...
x = [categories[int(k)] for k, _ in sorted_values]

# ...et les valeurs que nous venons de calculer avec la fonction `zonal_stats`
height = [v for _, v in sorted_values]

fig, ax = plt.subplots(figsize=(8, 8))
bar_container = ax.bar(x, height)
ax.set_xticklabels(
    ax.get_xticklabels(),
    rotation=45,
    horizontalalignment='right'
)
fig.tight_layout()
ax

  • Dans quel type d’occupation du sol se trouvent les restaurants précédemment récupérés ?
geoms = resto['geometry'].tolist()

resto["lc"] = [categories[int(x)] for x in point_query(vectors=geoms, raster=xds.data[0], nodata=0, affine=xds.rio.transform())]
resto.head()
              osm_id         name     cuisine                        geometry                      lc
291667452  291667452    Chez Cora          NA  POINT (-205284.124 320601.265)  Surfaces artificielles
295803248  295803248    Normandin          NA  POINT (-221931.609 321452.378)  Surfaces artificielles
345886369  345886369  Chez Victor      burger  POINT (-207712.728 315587.610)  Surfaces artificielles
386267989  386267989       Ly-Hai  vietnamese  POINT (-207895.478 315365.940)  Surfaces artificielles
393559700  393559700       Mike's     italian  POINT (-217665.483 319360.818)  Surfaces artificielles
  • Quels restaurants ne sont pas situés dans une zone artificialisée ?
selection = resto[(resto.lc != 'Surfaces artificielles') & (resto.lc != 'Pas de données')]
selection
                  osm_id                        name                          cuisine                        geometry                                     lc
2395695353    2395695353                  Yuzu sushi                            sushi  POINT (-222424.372 321631.681)             Milieux humides forestiers
3089591298    3089591298                     Café 47                               NA  POINT (-211438.971 309903.333)             Milieux humides forestiers
3222747115    3222747115                  Yuzu Sushi                               NA  POINT (-213715.133 318403.631)     Forêts de feuillus à couvert fermé
3224167888    3224167888                   Le Subtil                               NA  POINT (-212550.597 319033.410)     Forêts de feuillus à couvert fermé
3257077054    3257077054            Ben & Florentine                               NA  POINT (-221125.085 321891.351)             Milieux humides forestiers
3801921158    3801921158               Le Commandant                          italian  POINT (-218914.348 315176.377)          Forêts mixtes à couvert fermé
4726892021    4726892021                        Pidz                            pizza  POINT (-229413.591 325273.771)        Plans et cours d’eau intérieure
5099196285    5099196285         Le Bistro Évolution                               NA  POINT (-210863.848 307329.858)          Forêts mixtes à couvert fermé
5485394052    5485394052  érablière du chemin du Roy  maple;ice_cream;quebec;canadian  POINT (-222402.680 308460.059)             Milieux humides forestiers
5485401715    5485401715                   Quai 1636                  regional;french  POINT (-216802.399 309187.074)                       Terres agricoles
6643475285    6643475285                     La cage                               NA  POINT (-203039.864 313768.119)  Milieux humides herbacés ou arbustifs
7816042184    7816042184                 Chez Victor                               NA  POINT (-210673.674 307668.201)                       Terres agricoles
10878153356  10878153356                      Pacini                          italian  POINT (-213819.005 318251.978)    Forêts de conifères à couvert fermé
10878153357  10878153357                  Yuzu sushi                            sushi  POINT (-213801.047 318254.188)     Forêts de feuillus à couvert fermé
189982784      189982784              Resto-Pub Ryna                               NA  POINT (-211598.938 306756.046)     Forêts de feuillus à couvert fermé
998906150      998906150                 Benny & Co.         chicken;barbecue;poutine  POINT (-213983.922 325706.559)                       Terres agricoles
ax = extract.plot(color=None, edgecolor="black")
ax = selection.plot(ax=ax, marker='x', color='red', markersize=8)
ax.set_axis_off()
ax

À vous de jouer 🚀

Objectif : Comparer l’occupation du sol de la MRC de Trois-Rivières à l’occupation du sol de la MRC de Montréal.

  • Vous cherchez à obtenir :
    • un diagramme en barre (combinant le résultat des statistiques zonales pour la MRC de Trois-Rivières et pour la MRC de Montréal),
    • un résumé (pourquoi pas en HTML !) de la surface de chaque type d’occupation du sol, comme celui présenté en exemple ci-dessous, pour Trois-Rivières et Montréal.

Exemple de tableau de résumé
  • Au début de votre document, vous devez insérer un chunk de code R pour préciser la localisation de votre environnement virtuel Python - comme nous utilisons knitr, c’est ce qui va permettre à reticulate de savoir où chercher les packages Python qui ont été installés pour cet atelier :
```{r}
#| echo: false
library(reticulate)
use_virtualenv("~/env-quarto")
```

Besoin d’aide ?

  • Créer un chunk ‘r’ pour dfinir la localisation de l’environnement virtuel
  • Créer les chunks ‘python’ correspondants aux actions suivantes :
    • Ouvrir le jeu de données raster
    • Ouvrir le jeu de données vecteur
    • S’assurer qu’ils sont dans le même système de coordonnées de référence et effectuer la transformation si ce n’est pas le cas
    • Obtenir les statistiques zonales pour la MRC de Trois-Rivières
    • Obtenir les statistiques zonales pour la MRC de Montréal
    • Produire le diagramme et le tableau synthétisant ces informations
import rioxarray
import xarray
import pandas as pd
import geopandas as gpd
import rasterstats
import matplotlib.pyplot as plt
xds = xarray.open_dataarray("/chemin/vers/le/fichier")
gdf = gpd.read_file('/chemin/vers/le/fichier')

En utilisant les paramètres décrivant la matrice de transformation de géoréférencement du jeu de données raster en question (cette transformation fait correspondre les coordonnées ligne/colonne des pixels aux coordonnées du système de référence de l’ensemble de données) :

transform = xds.rio.transform()

Puis en utilisant la première et la cinquième valeur qui représentent respectivement la largeur et la hauteur du pixel :

area_pixel = transform[0] * abs(transform[4]) # la hauteur est généralement négative

Par exemple en stockant du Markdown dans une chaîne de caractère et en le transformant en HTML avec IPython.display.Markdown :

from IPython.display import Markdown

rendu = '''
## Résumé

| Catégorie | Superficie |
|---------|----------|
| Aahah | 21.34 |
'''

Markdown(rendu)

Observable JavaScript

Qu’est-ce que l’ojs ?

L’Observable javascript {ojs} est un ensemble d’améliorations apportées à vanilla JavaScript créé par Mike Bostock (également auteur de D3 (Bostock et al., 2011)). Observable JS se distingue par son exécution réactive, qui convient particulièrement bien à l’exploration et à l’analyse interactives des données.


Observable est aussi une startup fondée par Mike Bostock et Melody Meckfessel, qui propose une plateforme 100% en ligne (https://observablehq.com) pour concevoir, partager et diffuser des visualisations de données.

Observable c’est donc aussi C’est aussi une plateforme web qui héberge des notebooks computationnels sur la visualisation de données.

Objectifs

👉 Construire un langage et un écosystème dédié à la visualisation de données sur le web

👉 Fédérer et faire collaborer une communauté

Observable dans Quarto

Le runtime d’Observable est Open Source et est utilisé dans Quarto.

Dans Quarto, toutes les instructions s’écrivent dans des chunks {ojs}

```{ojs}

```

On peut paramétrer l’affiche et/ou l’execution du code avec //| echo et //| eval.

```{ojs}
//| echo: false
//| eval: true
```

NB : Contrairement aux chunks R et Python, le code s’execute just in time dans le navigateur web une fois le notebook généré. Seul l’export html est donc pris en compte.

Principes

Le langage JavaScript n’a pas du été conçu pour l’analyse de données. Mais, gros point fort, on a tous un navigateur web et JavaScript tourne sur toutes les machines. Le but : bâtir une évolution du langage JavaScript pour en faire un langage adapté à l’analyse et la visualisation de données sur le web.

l’ojs ressemble donc à du JavaScript, mais il y a quelques différences.

👉 Des bibliothèques préchargées 📊

OJS c’est du JavaScript + plein de bibliothèques pré chargées :

Symbol Name Version
_ Lodash 4.17.21
aq Arquero
Arrow Apache Arrow 4.0.1
d3 D3.js
dot Graphviz 0.2.1
htl Hypertext Literal
Inputs Observable Inputs
L Leaflet
mermaid Mermaid 9.1.6
Plot Observable Plot
SQLite SQL.js 1.7.0
topojson TopoJSON Client 3.1.0
vl Vega, Vega-Lite 5.22.1, 5.2.0

👉 Des cellules 🙋

Chaque début de ligne identifie une cellule ojs. Chaque ligne définit une variable et une cellule qui doit être unique sur l’ensemble du document.

En début de cellule, on n’utilisera pas les déclarations de variables avec let, const ou var.

```{ojs}
a = 10 // une cellule
b = 20 // une cellule
```
```{ojs}
c = a + b // une cellule
```

A l’intérieur d’un bloc de code, on écrit du vanilla JavaScript.

```{ojs}
d = { 
  let a = 10 
  let b = 30
  return a + b 
} // Une cellule
```

👉 Des promesses sont résolues ✅

Dans ojs, les promesses sont résolues d’une cellule à l’autre. Cela permet d’enlever de la complexité liée à la conception asynchrone du JavaScript. Ca va être très pratique quand on charge un jeu de données par exemple.

👉 L’ordre des cellules n’a pas d’importance 🤯

On peut donc écrire ça

```{ojs}
result = val1 * val2
```

avant ça

```{ojs}
val1 = 10
val2 = 20
```

Le lien entre les cellules se fait de façon topologique et non de haut en bas.

👉 Tout est réactif 🔥

Dès lors qu’une cellule est modifiée, les cellules qui en dépendent sont modifiées également.

Pour modifier la valeur d’une cellule, on utilise des Inputs (des composants proposés par OJS pour explorer les données) avec le mot clé viewof

viewof x = Inputs.range([0, 255], {step: 1})
viewof y = Inputs.text({value: 30})
viewof z = Inputs.radio([10, 100, 1000], {value: 10})
x * y * z

Ce système est pratique pour modifier des visualisations de données.

Ici, un exemple avec un simple cercle dessiné en svg.

viewof color = Inputs.color({ label: "Couleur", value: "#4ab58c" })
viewof radius = Inputs.range([1, 50], { label: "Rayon", step: 1, value: 100 })
viewof position = Inputs.range([50, 950], { label: "Position", step: 1, value: 50 })
html`<svg viewBox="0 0 1000 100" xmlns="http://www.w3.org/2000/svg">
  <circle cx=${position} cy="50" r=${radius} fill="${color}"/>
</svg>`

L’écosystème spatial de l’ojs

L’écosystème spatial de JavaScript est moins étoffé que ce qu’on peut trouver en Python ou en R. Mais on trouve quand même des choses intéressantes qui permettent de faire de la géomatique directement dans le navigateur web. Voici quelques exemples.

Les fonctionnalités spatiales de d3.js

d3-geo est le module spatial de D3. Il permet de gérer les projections cartographiques, les formes sphériques et la trigonométrie sphérique. Voir aussi d3-geo-projection. d3-delaunay est une bibliothèque rapide pour calculer le diagramme de Voronoï d’un ensemble de points bidimensionnels. Elle est basée sur Delaunator, une bibliothèque rapide pour calculer la triangulation de Delaunay en utilisant des algorithmes de balayage. Le diagramme de Voronoï est construit en reliant les circonférences des triangles adjacents dans la triangulation de Delaunay. d3-geo-voronoi est une adaptation de d3-delaunay sur la sphère.

TopoJSON est une extension de GeoJSON. La librairie permet de réaliser des opérations SIG basées sur la topologie (agrégation, voisins, simplification, etc.)

Les bibliothèques SIG

turf est une bibliothèque JavaScript pour l’analyse spatiale (de type SIG). Elle comprend les opérations spatiales traditionnelles, des fonctions d’aide pour la création de données GeoJSON, ainsi que des outils de classification des données et de statistiques. L’avantage de turf est d’être modulaire. On ne charge que ce dont on a besoin.

spl.js est une implémentation de SpatiaLite (et de ses amis : sqlite, geos, proj, rttopo) pour le navigateur.

geotoolbox est un outil javascript pour les géographes basé sur d3geo, topojson et jsts. Il permet de traiter simplement les propriétés geojson et fournit plusieurs opérations SIG utiles pour la cartographie thématique.

Gdals3.js est un portage des applications Gdal (gdal_translate, ogr2ogr, gdal_rasterize, gdalwarp, gdaltransform) vers Webassembly. Il permet de convertir des données géospatiales matricielles et vectorielles dans divers formats et systèmes de coordonnées.

proj4js est une bibliothèque JavaScript permettant de transformer les coordonnées d’un système de coordonnées à un autre, y compris les transformations de datum

Dataviz / cartographie

plot est la bibliothèque de visualisation de données développée par Observable (Mike Bostock et Philippe Rivière). Au départ pensée pour la visualisation de données statistiques, elle permet aujourd’hui de réaliser des cartes. Plot est disponible par défaut dans les cellules {ojs}.

vega-lite est un outil de visualisation mettant en œuvre une grammaire graphique, similaire à ggplot2. Cette bibliothèque est disponible par défaut dans les cellules {ojs}.

bertin est une bibliothèque écrite en JavaScript qui permet de réaliser des cartes thématiques pour le web. Sa conception vise à permettre aux utilisateurs de créer rapidement des cartes thématiques interactives sans forcement connaître le langage JavaScript.

Gridviz est une bibliothèque JavaScript basée sur WebGL développée par Julien Gaffuri et Joe Davies. Elle permet de visualiser des données carroyées (ou tout ensemble de données tabulaires avec une position x/y) dans le navigateur dans une grande variété de styles cartographiques.

go-cart-wasm est une bibliothèque JS/WASM permettant de réaliser des cartogrammes basés sur le flux (comme décrit dans “Gastner, Seguy, and More (2018)).

Importer des données

Pour importer des données en {ojs}, on utilise l’instruction FileAttachment()

Pour un fichier JSON

mrc = FileAttachment("data/mrc.geojson").json()
munic = FileAttachment("data/munic.geojson").json()

Pour un fichier csv

FileAttachment("data.csv").csv({typed: true})

On peut aussi récupérer des données depuis R ou Python avec l’instruction ojs_define

NB

Souvenez vous que le code JavaScript s’execute dans le navigateur web. Il s’execute donc après le code R et Python.

ojs_define fonctionne bien pour des simples variables ou des data frames. Mais ne renvoie pas un geoJSON correctement formaté à partir d’un spatial dataframe.

chunck r

ojs_define(test = mun)

Chuck ojs

test

Une solution consiste à utiliser le package geojsonsf.

Chunck r

library("geojsonsf")
ojs_define(mun = sf_geojson(st_transform(mun, 4326)))

mun existe desormais dans l’univers ojs et est correctement formaté. Mais c’est une chaîne de caractères. On utilisera donc JSON.parse pour l’utiliser

Chunk ojs

JSON.parse(mun)

Visualiser les données statistiques

Pour regarder la table attributaire, on utilise Inputs.table()

Inputs.table(munic.features.map(d => d.properties))

Avec {ojs}, on va aussi pouvoir faire des graphiques avec la bibliothèque plot. Pour cela, on fait quelques manipulations de données.

Tout d’abord, on récupère les noms des 20 communes les plus peuplées, classées par ordre décroissant de population.

communes = munic.features
  .map((d) => d.properties)
  .sort((a, b) => d3.descending(a.POP_2022, b.POP_2022))
  .map((d) => d.MUS_NM_MUN)
  .slice(0, 20)

On met en forme les données.

data = {
  let arr = [];
  communes.forEach((d) => {
    let tmp = munic.features
      .map((d) => d.properties)
      .find((x) => x.MUS_NM_MUN == d);
    for (let i = 2001; i <= 2022; i++) {
      arr.push({ year: i, pop: tmp["POP_" + i], com: d });
    }
  });
  return arr;
}

Puis on réalise le graphique avec Plot en proposant de sélectionner une commune.

NB

Plot est directement disponible dans {ojs}. Mais si on souhaite une autre version, on peut l’appeler.

Plot = require("@observablehq/plot@0.6.7")

La version 0.6.7 implémente les infobulles.

viewof commune = Inputs.select(communes, { label: "Commune" })
Plot.plot({
  grid: true,
  x: {
    label: "Années →",
    tickFormat: ""
  },
  y: {
    label: `↑ Population (${commune})`
  },
  color: { legend: true },
  marginLeft: 60,
  marks: [
    Plot.lineY(data.filter(d => d.com == commune), {
      x: "year",
      y: "pop",
      stroke: "red",
      strokeWidth: 2
    })
  ]
})

On peut également mettre toutes les courbes sur un graphique

Plot.plot({
  grid: true,
  x: {
    label: "Années →",
    tickFormat: ""
  },
  y: {
    label: `↑ Population`
  },
  color: { legend: true },
  marginLeft: 60,
  marks: [
    Plot.lineY(data, {
      x: "year",
      y: "pop",
      stroke: "com",
      strokeWidth: 2,
      tip: true
    })
  ]
})

Ou en normalisant

Plot.plot({
  grid: true,
  x: {
    label: "Années →",
    tickFormat: ""
  },
  y: {
    label: `↑ Population`
  },
  marginLeft: 60,
  marks: [
    Plot.lineY(data, Plot.normalizeY("first",{
      x: "year",
      y: "pop",
      stroke: "com",
      strokeWidth: 2,
      tip: true
    }))
  ]
})

Visualiser les géométries

Comme leaflet (Agafonkin, 2015) est disponible dans les cellules {ojs}, on peut directement afficher les géométries.

{
  const container = yield htl.html`<div style="height: 500px;">`;
  const map = L.map(container);
  const layer = L.geoJSON(mrc).addTo(map);
  map.fitBounds(layer.getBounds(), { maxZoom: 9 });
  L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
    attribution:
      "© <a href=https://www.openstreetmap.org/copyright>OpenStreetMap</a> contributors"
  }).addTo(map);
}

On peut également afficher la carte simplement avec la bibliothèque bertin.

bertin = require("bertin")
bertin.quickdraw(mrc)

Un peu de SIG

La bibiothèque geotoolbox permet de faire facilement differentes operations SIG.

geo = require("geotoolbox")
  • Simplification
viewof simpl = Inputs.range([0.0001, 0.2], { label: "Simplification",  value: 0.1 })
mrc2 = geo.simplify(mrc, { k: simpl })
  • Centroid
ctr = geo.centroid(mrc)
  • Buffer
quebec = geo.filter(mrc, (d) => d.MUS_CO_MRC == "23")
buff = geo.buffer(quebec, { dist: distance})
ctr2 = geo.clip(ctr, {clip: buff})

Cartographie thématique

Pour créer des cartes thématiques, on peut utiliser la bibliothèque bertin (Lambert et al., 2023).

La fonction draw permet de réaliser n’importe quel type de carte.

Par défaut, la couche s’affiche simplement.

bertin.draw({layers:[{geojson: munic, fill: "#d18de0"}]})

On peut ajouter l’attrubut tooltip pour afficher des infobulles.

bertin.draw({
  layers:[
    { 
      geojson: munic, fill: "#d18de0",
      tooltip: "$MUS_NM_MUN"
    }
  ]
})

Cartographie thématique

A présent, réalisons une carte de la population en 2022. Pour cela, on utilise le type bubble.

Par défaut, ça donne ceci :

bertin.draw({
    layers: [
      {
        type: "bubble",
        geojson: munic, 
        values: "POP_2022", 
        fill:"red"
      },
      {
        geojson: munic,
        fill: "#CCC"
      }, 
  ]
})

Et en paramétrant un peu, on obtient ceci.

bertin.draw({
  params: { extent: [[-79, 44.5], [-66, 51]]},
  layers: [
    { type: "header", text: "Population en 2022", anchor: "start" }, // Titre
    { type: "minimap", x: 10, y: 10, width: 160 }, 
    {
      id : "bub", //  identifiant de la couche
      type: "bubble", // couche de symboles proportionnels
      geojson: munic, 
      values: "POP_2022", // variable à cartographier 
      k: 50, // Rayon du plus gros cercle
      leg_x: 740,
      leg_y: 350,
      leg_round: 0,
      leg_title: "Nombre d'habitants",
      fill: "red",
      fillOpacity:0.5,
      tooltip: ["$MUS_NM_MUN", d => d.properties["POP_2022"] + " habitants"] // infobulle
    },
    {geojson: munic, fill:"#CCC"}, // Couche des communes
    {type:"scalebar"}
  ]
})

On peut facilement combiner cette couche de cercles avec un une carte choroplèthe.

bertin.draw({
  params: { extent: [[-79, 44.5], [-66, 51]]},
  layers: [
    { type: "header", text: "Population en 2022", anchor: "start" }, // Titre
    { type: "minimap", x: 10, y: 10, width: 160 }, 
    {
      id : "bub", 
      type: "bubble", 
      geojson: munic, 
      values: "POP_2022", 
      k: 50, 
      leg_x: 690,
      leg_y: 370,
      leg_round: 0,
      leg_title: "Nombre d'habitants",
      fill: "white",
      fillOpacity:0.2,
      stroke:"black",
      strokeWidth:1.4,
      tooltip: ["$MUS_NM_MUN", d => d.properties["POP_2022"] + " habitants"] 
    },
    { 
      geojson: munic,
      fill: {
            type: "choro",
            values: "VIT_ECO", 
            colors: "RdYlBu",
            leg_x:870,
            leg_y:315,
            leg_title: "Indice de\nvitalité\néconomique",
            leg_round: 2
        }
    }, // Couche des communes
    {type:"scalebar"}
  ]
})

On peut aussi colorier les cercles

bertin.draw({
  params: { extent: [[-79, 44.5], [-66, 51]]},
  layers: [
    { type: "header", text: "Population en 2022", anchor: "start" }, // Titre
    { type: "minimap", x: 10, y: 10, width: 160 }, 
    {
      id : "bub", 
      type: "bubble", 
      geojson: munic, 
      values: "POP_2022", 
      k: 50, 
      leg_x: 690,
      leg_y: 370,
      leg_round: 0,
      leg_title: "Nombre d'habitants",
      fill: {
            type: "choro",
            values: "VIT_ECO", 
            colors: "RdYlBu",
            leg_x:870,
            leg_y:315,
            leg_title: "Indice de\nvitalité\néconomique",
            leg_round: 2
        },
      tooltip: ["$MUS_NM_MUN", d => d.properties["POP_2022"] + " habitants"] 
    },
    { 
      geojson: munic,
      fill: "#CCC"
    }, // Couche des communes
    {type:"scalebar"}
  ]
})

On peut utiliser les Inputs pour tester des visualisations, explorer les données ou proposer des interactions à un utilisateur.

viewof symbology = Inputs.radio(["bubble", "square", "spikes", "smooth", "dotdensity"], { label: "symbologie",   value: "bubble"})
viewof r = Inputs.range([10, 100], { label: "taille", step: 1, value: 50, disabled : ["bubble", "square", "spikes"].includes(symbology) ? false: true})
viewof toggle = Inputs.toggle({label: "dorling", value: false, disabled : ["bubble", "square", "dotdensity"].includes(symbology) ? false: true})
viewof thresholds = Inputs.range([5, 50], { label: "thresholds", step: 1, value: 23, disabled : symbology == "smooth" ? false : true})
viewof bandwidth = Inputs.range([5, 50], { label: "bandwidth", step: 1, value: 27, disabled : symbology == "smooth" ? false : true })
viewof filterpop = Inputs.range([1000, 100000], { label: "POP >= ", step: 1, value: 1000})
// Carte
bertin.draw({
    params: { extent: [[-79, 44.5], [-66, 51]]},
    layers: [
      {
        type: symbology,
        geojson: geo.filter(munic, (d) => d.POP_2022 >= filterpop), 
        values: "POP_2022", 
        fill: symbology == "smooth" ? "RdYlGn" : "red",
        fillOpacity:0.7,
        dorling: toggle,
        k: r,
        thresholds,
        bandwidth,
        symbol_size:25,
        w:15,
        symbol_shift : toggle ? 1 : 0,
        stroke : symbology == "spikes" ? "red" : "white",
        tooltip: ["$MUS_NM_MUN", d => d.properties["POP_2022"] + " habitants"] 
      },
      {
        geojson: munic,
        fill: "#CCC",
        stroke : toggle ? "#CCC" : "white"
      }, 
  ]
})

Références

Documentation Exemples

À vous de jouer

faire des cartes avec {ojs}
Objectif

Réaliser comme ci-dessous, une carte choroplèthe sur une région du Québec que l’on peut selectionner dans une liste déroulante.

A vous de jouer

  • Chargez la bibliothèque bertin
  • Importez la couche munic.geojson

Pour importer des données au format JSON en Observable JavaScript, on utilise l’instruction FileAttachment()

  • Afficher le fond de carte avec la fonction draw

La fonction draw prend en entrée un objet qui a la structure suivante :

bertin.draw({
  layers: [{...}]
})

Si la carte ne s’affiche pas correctement, utiliser l’attribut rewind:true

  • Afficher les données attributaires.

On peut afficher les données avec l’instruction. Inputs.table() Mais auparavant, il faut récupérer les propriétés du geoJSON comme ceci :

munic.features.map((d) => d.properties)
  • Créer une liste déroulante pour choisir parmi 3 régions à étudier

Par exemple :

mesregions = ["Estrie","Capitale-Nationale","Bas-Saint-Laurent"]
viewof myregion =  Inputs.select(mesregions, {label: "Choisissez une région"})
  • Créez un nouveau fond de carte contenant uniquement les municipalités de la région définie par la liste déroulante.

Pour créer ce sous jeu de données, on peut utiliser la fonction subset de la bibliothèque bertin.

myreg = bertin.properties.subset({
    geojson: munic,
    field: "MUS_NM_REG",
    selection:   [myregion]
})
  • Afficher le nouveau fond de carte
  • Définisser la couleur de fond, l’épaisseur des traits, la couleur des contours.
  • Réaliser une carte chorplèthe à partir d’une variable disponible. Par exemple, le taux d’actifs (TX_2564).

Définissez l’attribut fill par un objet commençant par {type: choro, values: ...}

  • Changez les couleurs et la méthode de discretisation

La documentation est disponible ici

bertin = require("bertin")
munic = FileAttachment("munic.geojson").json()
viewof myregion = Inputs.select(
  ["Estrie", "Capitale-Nationale", "Bas-Saint-Laurent"],
  { label: "Choisissez une région" }
)
myreg = bertin.properties.subset({
    geojson: munic,
    field: "MUS_NM_REG",
    selection:   [myregion]
})
bertin.draw({
  layers: [
    { type: "header", text: `Les actifs (${myregion})` },
    { type: "scalebar" },
    {
      geojson: myreg,
      rewind: true,
      fill: {
        type: "choro",
        values: "TX_2564",
        colors: "RdYlGn",
        leg_x: 2,
        leg_y: 2,
        leg_title: "Taux d'actifs",
        leg_round: 1
      },
      tooltip: [
        "$MUS_NM_MUN",
        (d) => "Taux d'actifs :" + d.properties.TX_2564 + "%"
      ]
    }
  ]
})

Les références

Agafonkin, V. (2015). Leaflet javascript libary.
Baddeley, A., Rubak, E. et Turner, R. (2015). Spatial Point Patterns: Methodology and Applications with R. Chapman; Hall/CRC Press. https://www.routledge.com/Spatial-Point-Patterns-Methodology-and-Applications-with-R/Baddeley-Rubak-Turner/p/book/9781482210200/
Bivand, R. (2021). Progress in the R ecosystem for representing and handling spatial data. Journal of Geographical Systems, 23(4), 515‑546. https://doi.org/10.1007/s10109-020-00336-0
Bivand, R., Keitt, T. et Rowlingson, B. (2023). rgdal: Bindings for the ’Geospatial’ Data Abstraction Library. https://CRAN.R-project.org/package=rgdal
Bivand, R. et Rundel, C. (2023). rgeos: Interface to Geometry Engine - Open Source (’GEOS’). https://CRAN.R-project.org/package=rgeos
Bostock, M., Ogievetsky, V. et Heer, J. (2011). D³ Data-Driven Documents. IEEE Transactions on Visualization and Computer Graphics, 17(12), 2301‑2309. https://doi.org/10.1109/TVCG.2011.185
Dorman, M., Graser, A., Nowosad, J. et Lovelace, R. (2022). Geocomputation with Python. CRC Press. https://py.geocompx.org/
Dunnington, D. (2023). ggspatial: Spatial Data Framework for ggplot2. https://CRAN.R-project.org/package=ggspatial
GDAL/OGR contributors. (2022). GDAL/OGR Geospatial Data Abstraction software Library. Open Source Geospatial Foundation. https://doi.org/10.5281/zenodo.5884351
GEOS contributors. (2021). GEOS coordinate transformation software library. Open Source Geospatial Foundation. https://libgeos.org/
Gillies, S. et al. (2013). Rasterio: geospatial raster I/O for Python programmers. Mapbox. https://github.com/rasterio/rasterio
Gillies, S., Buffat, R., Arnott, J., Taves, M. W., Wurster, K., Snow, A. D., Cochran, M., Sales de Andrade, E. et Perry, M. (2023, janvier). Fiona. https://github.com/Toblerity/Fiona
Gillies, S., Wel, C. van der, Van den Bossche, J., Taves, M. W., Arnott, J., Ward, B. C., et al. (2023, janvier). Shapely. https://doi.org/10.5281/zenodo.5597138
Giraud, T. (2022a). mapsf: Thematic Cartography. https://CRAN.R-project.org/package=mapsf
Giraud, T. (2022b). tanaka: Design Shaded Contour Lines (or Tanaka) Maps. https://CRAN.R-project.org/package=tanaka
Giraud, T. (2023). maptiles: Download and Display Map Tiles. https://CRAN.R-project.org/package=maptiles
Giraud, T. et Lambert, N. (2017). Reproducible Cartography. In M. Peterson (dir.), Advances in Cartography and GIScience. ICACI 2017. Lecture Notes in Geoinformation and Cartography. (p. 173‑183). Springer. https://doi.org/10.1007/978-3-319-57336-6_13
Giraud, T. et Pecout, H. (2023a). Cartographie avec R. https://doi.org/10.5281/zenodo.7528161
Giraud, T. et Pecout, H. (2023b). Géomatique avec R. https://doi.org/10.5281/zenodo.7528145
Hijmans, R. J. (2023). terra: Spatial Data Analysis. https://CRAN.R-project.org/package=terra
Hoyer, S., Roos, M., Joseph, H., Magin, J., Cherian, D., Fitzgerald, C., Hauser, M., Fujii, K., Maussion, F., Imperiale, G., Clark, S., Kleeman, A., Nicholas, T., Kluyver, T., Westling, J., Munroe, J., Amici, A., Barghini, A., Banihirwe, A., Bell, R., et al.Wolfram, P. J. (s. d.). xarray. https://doi.org/10.5281/zenodo.598201
Jordahl, K., Bossche, J. V. den, Fleischmann, M., Wasserman, J., McBride, J., Gerard, J., Tratner, J., Perry, M., Badaracco, A. G., Farmer, C., Hjelle, G. A., Snow, A. D., Cochran, M., Gillies, S., Culbertson, L., Bartos, M., Eubank, N., maxalbert, Bilogur, A., Rey, S., et al.Leblanc, F. (2020, juillet). geopandas/geopandas: v0.8.1. Zenodo. https://doi.org/10.5281/zenodo.3946761
Lambert, N., Giraud, T., Viry, M. et Ysebaert, R. (2023). Cartographier pour le web avec la bibliothèque JavaScript « bertin ». SAGEO.
Lovelace, R., Nowosad, J. et Muenchow, J. (2019). Geocomputation with R. CRC Press. https://r.geocompx.org/
Padgham, M., Rudis, B., Lovelace, R. et Salmon, M. (2017). osmdata. The Journal of Open Source Software, 2(14). https://doi.org/10.21105/joss.00305
Pebesma, E. (2018). Simple Features for R: Standardized Support for Spatial Vector Data. The R Journal, 10(1), 439‑446. https://doi.org/10.32614/RJ-2018-009
Pebesma, E. et Bivand, R. (2005). Classes and methods for spatial data in R. R News, 5(2), 9‑13. https://CRAN.R-project.org/doc/Rnews/
Pebesma, E. et Bivand, R. (2023). Spatial Data Science: With applications in R (p. 352). Chapman and Hall/CRC. https://r-spatial.org/book/
PROJ contributors. (2021). PROJ coordinate transformation software library. Open Source Geospatial Foundation. https://proj.org/
R Core Team. (2023). R: A Language and Environment for Statistical Computing. R Foundation for Statistical Computing. https://www.R-project.org/
Rey, S. J. et Anselin, L. (2007). PySAL: A Python Library of Spatial Analytical Methods. The Review of Regional Studies, 37(1), 5‑27.
Snow, A. D., Brochart, D., Raspaud, M., Taves, M., Bell, R., RichardScottOZ, Chegini, T., stefank0, Amici, A., Braun, R. et al., et. (2023). corteva/rioxarray: 0.14.1 Release. https://doi.org/10.5281/zenodo.7829704
Snow, A. D., Whitaker, J., Cochran, M., Miara, I., Bossche, J. V. den, Mayo, C., Cochrane, P., Lucas, G., Kloe, J. de, Karney, C. et al., et. (2023). pyproj4/pyproj: 3.5.0 Release. https://doi.org/10.5281/zenodo.7776548
Tennekes, M. (2018). tmap: Thematic Maps in R. Journal of Statistical Software, 84(6), 1‑39. https://doi.org/10.18637/jss.v084.i06
The pandas development team. (s. d.). pandas-dev/pandas: Pandas. https://github.com/pandas-dev/pandas
Wickham, H. (2016). ggplot2: Elegant Graphics for Data Analysis. Springer-Verlag New York. https://ggplot2.tidyverse.org
Wickham, H., Averick, M., Bryan, J., Chang, W., McGowan, L. D., François, R., Grolemund, G., Hayes, A., Henry, L., Hester, J., Kuhn, M., Pedersen, T. L., Miller, E., Bache, S. M., Müller, K., Ooms, J., Robinson, D., Seidel, D. P., Spinu, V., Takahashi, K., et al.Yutani, H. (2019). Welcome to the tidyverse. Journal of Open Source Software, 4(43), 1686. https://doi.org/10.21105/joss.01686