import os
import duckdb
= duckdb.connect(database=":memory:")
con
con.execute(f"""
CREATE SECRET secret_ls3 (
TYPE S3,
KEY_ID '{os.environ["AWS_ACCESS_KEY_ID"]}',
SECRET '{os.environ["AWS_SECRET_ACCESS_KEY"]}',
ENDPOINT '{os.environ["AWS_S3_ENDPOINT"]}',
SESSION_TOKEN '{os.environ["AWS_SESSION_TOKEN"]}',
REGION 'us-east-1',
URL_STYLE 'path'
);
"""
)
Atelier pour découvrir la récupération de données via avec le format Parquet
Le format Parquet
est un format de données connaissant une popularité importante du fait de ses caractéristiques techniques (orientation colonne, compression, interopérabilité…), de sa nature open source et du riche écosystème associé dont les frameworks les plus proéminents sont Arrow
et DuckDB
. A ces nombreux avantages s’ajoutent une intégration native aux infrastructures cloud basées sur S3
, des extensions nombreuses pour traiter des données complexes comme les données géographiques ou, plus récemment, le portage en WASM de DuckDB
permettant de construire des applications réactives impliquant des transformations de données directement depuis le navigateur.
Regarder le replay de la session live du 09 Avril 2025:
1 Introduction
Tout au long de ce tutoriel guidé, nous allons voir comment utiliser le format Parquet
de manière la plus efficiente.
Afin de comparer les différents formats et méthodes d’utilisation, nous allons généralement comparer le temps d’exécution et l’usage mémoire d’une requête standard.
1.1 Etapes préliminaires
Au cours de cet atelier, nous aurons besoin des packages suivants:
#| output: false
library(duckdb)
library(glue)
library(DBI)
library(dplyr)
library(dbplyr)
library(mapview)
Ce tutoriel s’appuie sur des données ouvertes diffusées au format Parquet
. Pour les récupérer, vous pouvez exécuter le script suivant:
Récupérer les données
library(arrow)
library(dplyr)
library(readr)
library(fs)
# Chemin du fichier d'entrée
<- "data/RPindividus.parquet"
filename_table_individu
# Lire le fichier Parquet
<- read_parquet(filename_table_individu)
df
# Filtrer les données pour REGION == "24"
<- df %>% filter(REGION == "24")
df_filtered
# Sauvegarder en CSV
write_csv(df_filtered, "data/RPindividus_24.csv")
# Sauvegarder en Parquet
write_parquet(df_filtered, "data/RPindividus_24.parquet")
# Créer le dossier si nécessaire
dir_create("data/RPindividus")
# Sauvegarder en Parquet partitionné par REGION et DEPT
write_dataset(
df,path = "data/RPindividus",
format = "parquet",
partitioning = c("REGION", "DEPT")
)
2 Passer de CSV
à Parquet
Commençons par comparer les formats CSV
et Parquet
afin de comprendre les gains qu’apporte déjà ce format.
Le prochain chapitre propose d’utiliser le package bench
pour les comparatifs. Il est plus simple d’encapsuler dans ces benchmarks des fonctions: vous pouvez développer le code puis l’intégrer dans une fonction ad hoc.
Pour ce premier exercice, nous proposons d’utiliser Arrow
pour la lecture des fichiers. Nous verrons ultérieurement comment faire la même chose avec DuckDB
.
CSV
au Parquet
- La requête suivante permet de calculer les données pour construire une pyramide des âges sur un département donné, à partir du fichier
CSV
du recensement. Après l’avoir testée, encapsuler celle-ci dans une fonctionreq_csv
(sans argument).
<- readr::read_csv("data/RPindividus_24.csv") |>
res filter(DEPT == "36") |>
group_by(AGED, DEPT) |>
summarise(n_indiv = sum(IPONDI))
Sur le même modèle, construire une fonction
req_read_parquet
basée cette fois sur le fichierdata/RPindividus_24.parquet
chargé avec la fonction read_parquet d’Arrow
Comparer les performances (temps d’exécution et allocation mémoire) de ces deux méthodes grâce à la fonction bench::mark, à laquelle on passera les paramètres
iterations = 1
(comparaison à partir d’une seule itération) etcheck = FALSE
(autorise les outputs des deux fonctions à être différents).Arranger les données pour avoir un tableau de résultats
Proposition de correction
library(dplyr)
library(ggplot2)
library(bench)
library(stringr)
<- "./data"
path_data <- glue("{path_data}/RPindividus.parquet")
path_parquet <- glue("{path_data}/RPindividus_24.parquet")
path_parquet_subset <- glue("{path_data}/RPindividus_24.csv")
path_csv_subset
<- function(path="data/RPindividus_24.csv") {
req_csv <- readr::read_csv(path) |>
res filter(DEPT == "36") |>
group_by(AGED, DEPT) |>
summarise(n_indiv = sum(IPONDI))
return(res)
}
<- function(path="data/RPindividus_24.parquet") {
req_read_parquet <- arrow::read_parquet(path) |>
res filter(DEPT == "36") |>
group_by(AGED, DEPT) |>
summarise(n_indiv = sum(IPONDI))
return(res)
}
<- function(path="data/RPindividus_24.parquet") {
req_open_dataset <- arrow::open_dataset(path) |>
res filter(DEPT == "36") |>
group_by(AGED, DEPT) |>
summarise(n_indiv = sum(IPONDI)) |>
collect()
return(res)
}
<- function(path="data/RPindividus.parquet") {
req_open_dataset_full <- arrow::open_dataset(path) |>
res filter(DEPT == "36") |>
group_by(AGED, DEPT) |>
summarise(n_indiv = sum(IPONDI)) |>
collect()
return(res)
}
<- function(path="data/RPindividus_partitionne.parquet") {
req_open_dataset_part <- arrow::open_dataset(path, hive_style = TRUE) |>
res filter(DEPT == "36") |>
group_by(AGED, DEPT) |>
summarise(n_indiv = sum(IPONDI)) |>
collect()
return(res)
}
<- function(path){
mesurer_taille <- file.size(path)
poids <- convertir_taille(poids)
taille return(taille)
}
# Fonction pour convertir les octets en Mo ou Go
<- function(octets) {
convertir_taille if (octets >= 1024^3) {
return(paste(round(octets / 1024^3, 2), "Go"))
else if (octets >= 1024^2) {
} return(paste(round(octets / 1024^2, 2), "Mo"))
else {
} return(paste(octets, "octets"))
}
}
library(glue)
library(stringr)
library(dplyr)
library(ggplot2)
library(bench)
library(gt)
source("R/benchmark_functions.R")
<- "./data"
path_data <- glue("{path_data}/RPindividus.parquet")
path_parquet <- glue("{path_data}/RPindividus_24.parquet")
path_parquet_subset <- glue("{path_data}/RPindividus_24.csv")
path_parquet_csv
<- bench::mark(
benchmark1 req_csv = req_csv(path_csv_subset),
req_read_parquet = req_read_parquet(path_parquet_subset),
iterations = 1,
check = FALSE
)
<- mesurer_taille(path_csv_subset)
poids_csv <- mesurer_taille(path_parquet_subset)
poids_parquet
<- benchmark1 |>
benchmark1_table mutate(
value = as.numeric(str_extract(mem_alloc, "\\d+(\\.\\d+)?")),
unit = str_extract(mem_alloc, "[KMGTP]B"),
mem_alloc_kb = case_when(
== "KB" ~ value,
unit == "MB" ~ value * 1024,
unit == "GB" ~ value * 1024^2,
unit == "TB" ~ value * 1024^3,
unit == "PB" ~ value * 1024^4,
unit TRUE ~ NA_real_ # Par défaut, si l'unité n'est pas reconnue
)|>
) mutate(
median = as.numeric(median, units="seconds"),
poids = as.numeric(
gsub(" Mo", "", c(poids_csv, poids_parquet))
),method = c("CSV avec `read_csv` (`readr`)", "Parquet `read_parquet` (`arrow`)"),
|>
) mutate(
median_bar = as.numeric(median, units="seconds"),
mem_alloc = paste0(value, " ", unit),
poids_bar = poids
|>
) select(
mem_alloc_bar=mem_alloc_kb
method, poids, poids_bar, median, median_bar, mem_alloc,
)
<- gt(benchmark1_table) |>
benchmark1 ::gt_plt_bar(column=median_bar) |>
gtExtras::gt_plt_bar(column=mem_alloc_bar) |>
gtExtras::gt_plt_bar(column=poids_bar) |>
gtExtrascols_move(median_bar, after=median) |>
cols_move(mem_alloc_bar, after=mem_alloc_bar) |>
tab_spanner(md("**Temps d'exécution**<br> _(sec.)_"), starts_with("median")) |>
tab_spanner(md("**Mémoire allouée**<br> _(MB)_"), starts_with("mem_alloc")) |>
tab_spanner(md("**Poids sur disque**<br> _(Mo)_"), starts_with("poids")) |>
cols_label(everything() ~ '') |>
fmt_number(median, decimals = 2) |>
fmt_number(poids, decimals = 0) |>
fmt_markdown(method)
dir.create("./bench")
gtsave(benchmark1, "./bench/mark1.html")
3 Comprendre l’intérêt de la lazy evaluation
La partie précédente a montré un gain de temps considérable du passage de CSV
à Parquet
. Néanmoins, l’utilisation mémoire était encore très élevée alors qu’on utilise de fait qu’une infime partie du fichier.
Dans cette partie, on va voir comment utiliser la lazy evaluation et les optimisations du plan d’exécution effectuées par Arrow
pour exploiter pleinement la puissance du format Parquet
.
3.1 La lazy evaluation en pratique
Arrow
Utiliser la fonction arrow::open_dataset pour ouvrir le fichier
data/RPindividus_24.parquet
. Regarder la classe de l’objet obtenu.Afficher les 5 premières lignes de la table avec la fonction
head()
. Observer l’objet obtenu (sortie en console, classe).Faire la même chose avec
duckdb
(par le biais de l’API tidyverse et en SQL direct)Prenez ce code:
::open_dataset(path_parquet_subset) |>
arrowfilter(DEPT == "36") |>
group_by(AGED, DEPT) |>
summarise(n_indiv = sum(IPONDI))
et exécutez le. Que se passe-t-il à votre avis ?
Ajouter une étape
collect()
à la fin de cette chaîne. Comprenez-vous la différence ?Construire une fonction
req_open_dataset
sur le modèle de celles de la partie précédente, qui importe cette fois les données avec la fonction arrow::open_datasetComparer les performances (temps d’exécution et allocation mémoire) de la méthode
read_parquet
et de la méthodeopen_dataset
grâce à la fonction bench::mark
3.2 Comprendre l’optimisation permise par Parquet
et DuckDB
Pour réduire la volumétrie des données importées, il est possible de mettre en oeuvre deux stratégies:
- N’importer qu’un nombre limité de colonnes
- N’importer qu’un nombre limité de lignes
Comme cela a été évoqué dans les slides, le format Parquet
est particulièrement optimisé pour le premier besoin. C’est donc généralement la première optimisation mise en oeuvre. Pour s’en convaincre on peut regarder la taille des données importées dans deux cas:
- On utilise beaucoup de lignes mais peu de colonnes
- On utilise beaucoup de colonnes mais peu de lignes
Pour cela, nous utilisons la fonction SQL EXPLAIN ANALYZE
disponible dans duckdb
. Elle décompose le plan d’exécution de duckdb
, ce qui nous permettra de comprendre la stratégie d’optimisation. Elle permet aussi de connaître le volume de données importées lorsqu’on récupère un fichier d’internet. En effet, duckdb
est malin: plutôt que de télécharger un fichier entier pour n’en lire qu’une partie, la librairie est capable de n’importer que les blocs du fichier qui l’intéresse.
Ceci nécessite l’utilisation de l’extension httpfs
(un peu l’équivalent des library
de R
en duckdb
). Elle s’installe et s’utilise de la manière suivante
#| output: false
library(duckdb)
# url_bpe <- "https://www.insee.fr/fr/statistiques/fichier/8217525/BPE23.parquet"
<- "https://minio.lab.sspcloud.fr/lgaliana/diffusion/BPE23.parquet"
url_bpe <- dbConnect(duckdb())
con
dbExecute(
con,glue(
"INSTALL httpfs;",
"LOAD httpfs;"
) )
Demandons à DuckDB
d’exécuter la requête “beaucoup de colonnes, pas beaucoup de lignes” et regardons le plan d’exécution et les informations données par DuckDB
:
Voir le plan : “beaucoup de colonnes, pas beaucoup de lignes”
glue(
'EXPLAIN ANALYZE ',
'SELECT * FROM read_parquet("{url_bpe}") LIMIT 5'
)
<- dbGetQuery(
plan
con,glue(
'EXPLAIN ANALYZE ',
'SELECT * FROM read_parquet("{url_bpe}") LIMIT 5'
) )
print(plan)
Voir le plan : “peu de colonnes, beaucoup de lignes”
<- dbGetQuery(
plan
con,glue(
'EXPLAIN ANALYZE ',
'SELECT TYPEQU, LONGITUDE, LATITUDE FROM read_parquet("{url_bpe}") LIMIT 10000'
) )
print(plan)
La comparaison de ces plans d’exécution montre l’intérêt de faire un filtre sur les colonnes : les besoins computationnels sont drastiquement diminués. Le filtre sur les lignes n’arrive que dans un second temps, une fois les colonnes sélectionnées.
Pourquoi seulement un rapport de 1 à 4 entre le poids des deux fichiers ? C’est parce que nos requêtes comportent toute deux la variable IPONDI
(les poids à utiliser pour extrapoler l’échantillon à la population) qui est à haute précision là où beaucoup d’autres colonnes comportent un nombre réduit de modalités et sont donc peu volumineuses.
4 Le partitionnement
La lazy evaluation et les optimisations d’Arrow
apportent des gain de performance considérables. Mais on peut encore faire mieux ! Lorsqu’on sait qu’on va être amené à filter régulièrement les données selon une variable d’intérêt, on a tout intérêt à partitionner le fichier Parquet
selon cette variable.
Parquet
partitionné
- Parcourir la documentation de la fonction arrow::write_dataset pour comprendre comment spécifier la clé de partitionnement d’un fichier
Parquet
. Plusieurs méthodes sont possibles ! - Importer la table individus complète du recensement
data/RPindividus.parquet
avec la fonction arrow::open_dataset et l’exporter en une tabledata/RPindividus_partitionne.parquet
partitionnée par la région (REGION
) et le département (DEPT
) - Observer l’arborescence de fichiers de la table exportée
- Modifier la fonction
req_open_dataset
de la partie précédente pour partir de la table complète (non-partitionnée)data/RPindividus.parquet
au lieu de l’échantillon - Construire une fonction
req_open_dataset_partitionne
sur le modèle dereq_open_dataset
, qui importe cette fois les données partitionnéesdata/RPindividus_partitionne.parquet
. Ne pas oublier de spécifier le paramètrehive_style = TRUE
. - Comparer les performances (temps d’exécution et allocation mémoire) des deux méthodes grâce à la fonction bench::mark
5 DuckDB
ou Arrow
?
La réponse dépend des préférences de chacun. Les deux écosystèmes sont très bien. DuckDB
est pensé pour sa simplicité d’usage et son universalité: en principe, qu’on fasse du , , ou de la ligne de commande, on pourra utiliser le même code SQL, seule la définition de la connection changera. Arrow
propose une syntaxe un peu moins familière mais l’intégration au tidyverse
rend ce framework beaucoup plus facile d’usage que si on devait directement utiliser Arrow
.
Il existe aussi des clients plus hauts niveau pour duckdb
: c’est notamment le cas de la connection entre duckdb
et tidyverse
permise en . Ceux-ci permettent des opérations de manipulation de données plus complexes: si le SQL de DuckDB
est déjà plus simple d’usage que celui de PostGre
, on reste limité pour des opérations complexes de (dé)structuration de données: le client tidyverse
apporte les avantages de cet écosystème en plus de ceux de DuckDB
.
6 (Geo)Parquet
et DuckDB
pour les données spatiales
Nous proposons, pour illustrer l’un des atouts de DuckDB
, à savoir sa simplicité d’usage sur des problèmes complexes, de faire du traitement de données spatiales. Pour cela, nous allons faire une tâche lourde: restreindre des données spatiales à partir de leur appartenance à une zone géographique donnée. Cette dernière sera définie géométriquement comme un triangle dont deux des coins correspondent aux anciens bâtiments de l’Insee à Malakoff.
L’import des contours dont nous aurons besoin en se fait assez naturellement grâce à sf
.
6.1 Récupération des données
#| output: false
dir.create("data")
download.file("https://minio.lab.sspcloud.fr/projet-formation/nouvelles-sources/data/triangle.geojson", "data/triangle.geojson")
download.file("https://minio.lab.sspcloud.fr/projet-formation/nouvelles-sources/data/malakoff.geojson", "data/malakoff.geojson")
download.file("https://minio.lab.sspcloud.fr/projet-formation/nouvelles-sources/data/montrouge.geojson", "data/montrouge.geojson")
download.file("https://minio.lab.sspcloud.fr/projet-formation/nouvelles-sources/data/geoparquet/dvf.parquet", "data/dvf.parquet")
download.file("https://minio.lab.sspcloud.fr/projet-formation/nouvelles-sources/data/geoparquet/carreaux.parquet", "data/carreaux.parquet")
<- sf::st_read("data/triangle.geojson", quiet=TRUE)
triangle <- sf::st_read("data/malakoff.geojson", quiet=TRUE)
malakoff <- sf::st_read("data/montrouge.geojson", quiet=TRUE) montrouge
On peut visualiser la ville de Malakoff
et notre zone d’intérêt (qu’on nommera, par abus de langage, le triangle d’or de Malakoff):
mapview(malakoff) + mapview(triangle, col.regions = "#ffff00")
Nous utiliserons aussi les contours de Montrouge pour cette partie:
mapview(montrouge)
En principe, duckdb
fonctionne à la manière d’une base de données. Autrement dit, on définit une base de données et effectue des requêtes (SQL ou verbes tidyverse
) dessus. Pour créer une base de données, il suffit de faire un read_parquet
avec le chemin du fichier.
Comme il n’est pas possible de distinguer cette zone par requêtes attributaires, nous proposons de :
- Via
DuckDB
, extraire les transactions de l’ensemble de la commune de Malakoff tout en conservant leur caractère spatial (chaque transaction correspond à un point géographique, avec ses coordonnées xy). - Utiliser localement le package
sf
pour distinguer spatialement les transactions effectuées à l’intérieur ou à l’extérieur du Triangle d’Or (dont nous fournissons les contours). - Calculer la médiane des prix dans les deux sous-zones.
On extrait les transactions de Malakoff. Pour information, dans le fichier dvf.parquet
, les coordonnées spatiales sont stockées dans un format binaire spécifique (Well-Known Binary - WKB). Ce format est efficace pour le stockage et les calculs, mais n’est pas directement lisible ou interprétable par les humains.
En transformant ces géométries en une représentation texte lisible (Well-Known Text - WKT) avec ST_AsText
, on rend les données spatiales faciles à afficher, interpréter ou manipuler dans des contextes qui ne supportent pas directement les formats binaires géospatiaux.
6.2 Spatial join avec DuckDB
Pour cet exercice, nous allons utiliser les variables suivantes:
<- "92046"
cog_malakoff <- "92049" cog_montrouge
et le geoparquet pourra être interprété par duckdb
selon ce modèle:
read_parquet('data/dvf.parquet')
FROM
SELECT
XXXX,ST_AsText(geometry) AS geom_text
WHERE XXXX
La base de données se crée tout simplement de la manière suivante :
#| output: false
#| echo: true
<- dbConnect(duckdb::duckdb())
con dbExecute(con, "INSTALL spatial;")
dbExecute(con, "LOAD spatial;")
En vous inspirant du template ci-dessus, créer un dataframe
transactions_malakoff
qui recense les transactions dans cette charmante bourgade.A ce niveau, les transactions extraites sont maintenant chargées en mémoire et on les transforme dans un format qui facilite leur manipulation en R via le package
sf
.
<-
transactions_malakoff ::st_as_sf(transactions_malakoff, wkt = "geom_text", crs = 2154) |>
sfrename(geometry=geom_text)
- Nous allons créer un masque pour reconnaître les transactions qui sont situées ou non dans le triangle d’or. Utiliser la structure suivante pour créer ce masque :
<- transactions_malakoff |>
bool_mask # ... |>
::st_intersects(triangle, sparse = FALSE) sf
⚠️ il faut tenir compte des projections géographiques avant de faire l’opération d’intersection. Ce code est donc à amender à la marge pour pouvoir faire l’intersection.
Cela donne un vecteur de booléen, on peut donc identifier les transactions dans le triangle d’or ou en dehors à partir de celui-ci.
Ci-dessous le dataframe brut extrait via Duckdb (réponse 1).
#| echo: true
<- glue("
query2 FROM read_parquet('data/dvf.parquet')
SELECT
code_commune,
valeur_fonciere,
ST_AsText(geometry) AS geom_text
WHERE code_commune = '{cog_malakoff}'
")
<- dbGetQuery(con, query2)
transactions_malakoff
head(transactions_malakoff, 3)
Ci-dessous, le dataframe transformé en objet sf
et prêt pour les opérations spatiales (réponse 2) :
#| echo: true
<-
transactions_malakoff ::st_as_sf(transactions_malakoff, wkt = "geom_text", crs = 2154) |>
sfrename(geometry=geom_text)
head(transactions_malakoff, 3)
Une fois les données prêtes, on intersecte les points avec le triangle représentant le centre-ville de Malakoff (question 3)
#| echo: true
<- transactions_malakoff |>
bool_mask ::st_transform(4326) |>
sf::st_intersects(triangle, sparse = FALSE)
sf
head(bool_mask)
On peut ensuite facilement créer nos deux espaces de Malakoff :
#| echo: true
<- transactions_malakoff[bool_mask,]
in_triangle <- transactions_malakoff[!bool_mask,] out_triangle
Une fois que chaque transaction est identifiée comme étant à l’intérieur ou à l’extérieur du Triangle, le calcul de la médiane des prix est immédiat.
<- median(in_triangle$valeur_fonciere)
median_in <- median(out_triangle$valeur_fonciere)
median_out
print(glue("Médiane des prix dans le Triangle d'Or de Malakoff : ", median_in))
print(glue("Médiane des prix dans le reste de Malakoff : ", median_out))
La médiane des prix est un peu plus élevée dans le Triangle qu’en dehors. On peut aller au-delà et étudier la distribution des transactions. Bien que la taille d’échantillon soit réduite, on a ainsi une idée de la diversité des prix dans cette bucolique commune de Malakoff.
#| code-fold: true
#| code-summary: "Produire la figure sur la distribution du prix des biens"
library(ggplot2)
library(scales)
<- transactions_malakoff %>%
malakoff_identified mutate(
region = if_else(as.logical(bool_mask), "Triangle d'or", "Hors triangle d'or")
)
ggplot(
malakoff_identified,aes(y = valeur_fonciere, x = region, fill = region)
+
) geom_violin() +
scale_y_continuous(
trans = "log10",
labels = comma_format(),
breaks = scales::trans_breaks("log10", function(x) 10^x)
+
) geom_jitter(height = 0, width = 0.1) +
labs(y = "Valeur de vente (€)") +
theme_minimal()
Tout ceci ne nous dit rien de la différence entre les biens dans le triangle et en dehors de celui-ci. Nous n’avons fait aucun contrôle sur les caractéristiques des biens. Nous laissons les curieux explorer la mine d’or qu’est cette base.
7 Accéder directement à des données sur S3
En premier lieu, la bonne pratique est de définir la connexion à S3
par le biais de secrets DuckDB
:
Il suffit ensuite de faire un read_parquet
en préfixant par s3://
. La lecture se passera comme si on était en local:
= (
query "FROM read_parquet('s3://projet-formation/bonnes-pratiques/data/RPindividus.parquet') "
"SELECT IPONDI AS poids, COLUMNS('.*AGE.*')"
) con.sql(query)
On peut bien sûr faire des opérations plus complexes, par exemple calculer une pyramide des âges par département par le biais d’une requête SQL:
= con.sql("""
pyramide FROM read_parquet('s3://projet-formation/bonnes-pratiques/data/RPindividus.parquet')
SELECT
CAST(FLOOR(AGED / 10) * 10 AS INT) AS tranche_age,
CAST(SUM(IPONDI) AS INT) AS poids,
DEPT
GROUP BY tranche_age, DEPT
ORDER BY tranche_age, DEPT
""").to_df()
pyramide
Représentons celle-ci sur une carte. Récupérons le fond de carte des départements avec cartiflette
:
from cartiflette import carti_download
= carti_download(
shp_communes = ["France"],
values = 4326,
crs = "DEPARTEMENT",
borders ="topojson",
vectorfile_format=50,
simplification="FRANCE_ENTIERE_DROM_RAPPROCHES",
filter_by="EXPRESS-COG-CARTO-TERRITOIRE",
source=2022
year
)
2) shp_communes.head(
Calculons la part de chaque classe d’âge pour neutraliser l’effet taille:
= (
donnees_carte
shp_communes="INSEE_DEP", right_on="DEPT")
.merge(pyramide, left_on
)"proportion"] = donnees_carte["poids"] / donnees_carte["POPULATION"] donnees_carte[
Et voici les cartes, obtenues simplement:
from plotnine import *
map = (
"tranche_age"] == 30]) +
ggplot(donnees_carte.loc[donnees_carte[= "proportion")) +
geom_map(aes(fill +
theme_light() = "Part des trentenaires dans la population")
labs(title
)map
from plotnine import *
map = (
"tranche_age"] == 60]) +
ggplot(donnees_carte.loc[donnees_carte[= "proportion")) +
geom_map(aes(fill +
theme_light() = "Part des soixantenaires dans la population")
labs(title
)map
Données partitionnées
On peut faire la même chose avec des données partitionnées:
"SELECT * FROM read_parquet('s3://projet-formation/bonnes-pratiques/data/RPindividus/**/*.parquet', hive_partitioning = true) WHERE DEPT IN (11, 31, 34)") con.sql(
8 DuckDB WASM
DuckDB
permet aussi de faire des traitements analytiques directement dans le navigateur grâce à son implémentation WASM. C’est particulièrement pratique pour créer des applications réactives comme celle-ci :
Que ce soit avec Quarto
(comme ici) ou par le biais d’Observable
(comme là), il est donc possible de construire des applications interactives reposant sur des sites statiques, sans avoir besoin d’un serveur ou .
Références supplémentaires
Du contenu pédagogique supplémentaire sur le sujet, produit par l’Insee:
- La formation aux bonnes pratiques de l’Insee
- Un atelier de l’EHESS sur
Parquet
avec de nombreux exemples ici - Le cours de mise en production de l’ENSAE
D’autres références utiles, à consommer sans modération, sur le sujet:
- Les posts d’Eric Mauvière sur icem7.fr/
- Webinaire du CASD sur
Parquet
etDuckDB
Une vidéo sur la philosophie derrière la naissance de DuckDB
et sa relation avec MotherDuck
: