Apprendre à utiliser des fichiers au format Parquet avec R
20/02/2025
Ce support ne couvre pas tous les aspects des traitements qu’il est possible de réaliser avec les fichiers au format Parquet mais il constitue une base sur laquelle s’appuyer si vous rencontrez des fichiers Parquet sur Cerise ou ailleurs.
Un nouveau format de données …
read_parquet("data/output.parquet", as_data_frame = FALSE)$schema
# > Table
# 3 rows x 4 columns
# $colonne1 <double not null>
# $colonne2 <string not null>
# $colonne3 <bool not null>
# $colonne4 <date32[day] not null>
read_parquet("data/output.parquet", as_data_frame = FALSE)$schema$metadata
# $auteur
# [1] "DEMESIS/BQIS"
# $description
# [1] "Table test de formation"
# $date_creation
# [1] "2025-01-22"
Des fichiers moins volumineux qu’en csv 500 Mo en Parquet vs 5 Go en csv
Des requêtes plus rapides et efficaces
Seulement les données nécessaires sont lues, pas tout le fichier
Des données conformes à la mise à disposition par le producteur (par exemple, plus de problème de codes communes…)
=> Un format très efficace pour l’analyse de données mais peu adapté à l’ajout de données en continu ou à la modification fréquente de données existantes.
L’Insee diffuse des données du recensement de la population au format Parquet
Voir le guide d’utilisation joint pour manipuler ces données
Premières diffusions sur data.gouv avec les bureaux de vote, les demandes de valeurs foncières, indicateurs pénaux…)
Prévisualisations des fichiers Parquet possibles avec le nouvel explorateur de données du SSP Cloud ou avec avec l’outil ParquetViewer.
library(arrow) # Le package arrow est nécessaire pour travailler avec des fichiers parquet
library(dplyr) # Pour utiliser dplyr
library(tictoc) # Pour le benchmark
Pour l’exemple, nous allons prendre une table des exploitations du RA 2020 d’une centaine de MO qui contient 416 478 lignes et 255 colonnes.
Le résultat obtenu est un objet directement utilisable dans R (ici un data.frame).
Il est possible de sélectionner les colonnes que l’on souhaite importer dans R directement dans la fonction read_parquet
:
Voyons l’écart avec la lecture d’un fichier rds :
=> Le temps nécessaire au chargement de la table est d’environ 6 secondes !
L’écart est significatif rien que sur la lecture (X 6).
RA2020 est un data.frame : on peut donc utiliser la syntaxe dplyr :
resultat <- RA2020 |>
filter(SIEGE_REG == "93") |>
group_by(SIEGE_DEP) |>
summarise(total_SAU = sum(SAU_TOT, na.rm = TRUE))
# A tibble: 6 × 2
SIEGE_DEP total_SAU
<chr> <dbl>
1 04 158946.
2 05 91979.
3 06 41141.
4 13 145713.
5 83 77785.
6 84 112888.
Voici ci-dessous la syntaxe recommandée pour requêter un fichier parquet volumineux :
# Établir la connexion aux données
RA2020 <- open_dataset("data/RA2020_exploitations.parquet") |>
filter(SIEGE_REG == "93") |>
group_by(SIEGE_DEP) |>
summarise(total_SAU = sum(SAU_TOT, na.rm = TRUE)) |>
collect()
=> Avec cette syntaxe, la requête va automatiquement utiliser les variables du fichier Parquet dont elle a besoin (en l’occurrence SIEGE_REG, SIEGE_DEP et SAU_TOT) et minimiser l’occupation de la mémoire vive.
Revenons dans le détail sur cette syntaxe…
open_dataset()
(1/4)
Comme la fonction read_parquet()
, la fonction open_dataset()
permet de lire des données stockées en format Parquet.
Le résultat obtenu avec la fonction open_dataset()
n’est plus un data.frame mais un Arrow Table qui est une structure de données spécifique.
open_dataset()
(2/4)La fonction open_dataset()
crée un objet qui apparaît dans Values.
L’affichage dans la console d’un Arrow Table affiche uniquement les métadonnées.
open_dataset()
(3/4)Pour afficher le contenu d’un Arrow Table, il faut d’abord le convertir en data.frame avec la fonction collect()
.
RA2020 <- RA2020 |> collect()
class(RA2020)
> [1] "data.frame"
# L'opération ci-dessus est à éviter pour des tables volumineuses, si besoin de visualiser la table, on préfèrera :
extrait_RA2020 <- RA2020 |> slice_head(n = 100) |> collect()
Toutefois rien ne presse car la grande différence entre manipuler un data.frame et un Arrow Table tient au moteur d’exécution :
Si on manipule un data.frame avec la syntaxe de dplyr, alors c’est le moteur d’exécution de dplyr qui fait les calculs
Si on manipule un Arrow Table avec la syntaxe de dplyr, alors c’est le moteur d’exécution d’arrow (nommé acero) qui fait les calculs. Et le moteur d’exécution d’arrow est beaucoup plus efficace et rapide
open_dataset()
(4/4)
Il est recommandé de privilégier la fonction open_dataset()
à la fonction read_parquet()
pour au moins 2 raisons :
open_dataset()
crée une connexion au fichier Parquet mais elle n’importe pas les données contenues dans ce fichier => une consommation de RAM moins importante !
open_dataset()
peut se connecter à un fichier Parquet unique mais aussi à des fichiers Parquets partitionnés (voir plus loin)
Pour obtenir des informations générales sur le fichier (par exemple titre, auteur, date…), il faut utiliser la fonction read_parquet()
avec l’argument as_data_frame = FALSE
pour pouvoir accéder aux métadonnées globales via $schema$metadata
.
Le package {nanoparquet}
permet aussi d’obtenir d’autres infos facilement depuis des fichiers parquet :
library(nanoparquet)
parquet_info("data/RA2020_exploitations.parquet")
# > # A data frame: 1 × 7
file_name num_cols num_rows num_row_groups file_size parquet_version created_by
<chr> <int> <dbl> <int> <dbl> <int> <chr>
1 data/RA2020_exploitations.parquet 255 416478 1 39896331 2 parquet-cpp-arrow version 9.0.0
nanoparquet::parquet_column_types("data/output.parquet")
# A data frame: 4 × 6
# file_name name type r_type repetition_type logical_type
# * <chr> <chr> <chr> <chr> <chr> <I<list>>
# 1 test.parquet colonne1 DOUBLE double REQUIRED <NULL>
# 2 test.parquet colonne2 BYTE_ARRAY character REQUIRED <STRING>
# 3 test.parquet colonne3 BOOLEAN logical REQUIRED <NULL>
# 4 test.parquet colonne4 INT32 Date REQUIRED <DATE>
Attention, Parquet propose 2 niveaux de type :
- Le bas niveau
- Le logical type
La fonction parquet_column_types()
retourne les types de bas niveau dans la colonne type
et les logical_type dans la colonne logical_type
.
=> On obtient des informations sur le schéma, les row_groups, les column chunks…
Cela signifie qu’arrow se contente de mémoriser les instructions, sans faire aucun calcul tant que l’utilisateur ne le demande pas explicitement.
Il existe 2 fonctions pour déclencher l’évaluation d’un traitement arrow mais qui présente des différences :
collect()
qui renvoie le résultat du traitement sous la forme d’un data.frame/tibblecompute()
qui renvoie le résultat du traitement sous la forme d’un Arrow Table.La grande différence entre manipuler un tibble et manipuler un Arrow Table tient au moteur d’exécution :
Dans les traitements intermédiaires, on privilégiera la fonction compute()
pour pouvoir utiliser le plus possible le moteur acero.
SAU_DEP <- RA2020 |>
group_by(SIEGE_DEP) |>
summarise(total_SAU = sum(SAU_TOT, na.rm = TRUE))
class(SAU_DEP)
> [1] "arrow_dplyr_query"
resultats <- SAU_DEP |>
filter(SIEGE_DEP == "13") |>
collect()
> # A tibble: 1 × 2
SIEGE_DEP total_SAU
<chr> <dbl>
1 13 145713.
Dans l’exemple ci-dessus, la première étape ne réalise aucun calcul par elle-même, car elle ne comprend ni collect() ni compute(). L’objet SAU_DEP
n’est pas une table et ne contient pas de données, il contient simplement une requête (query) décrivant les opérations à mener sur la table du RA.
arrow analyse la requête avant de l’exécuter, et optimise le traitement pour minimiser le travail.
Dans notre exemple, arrow repère que la requête ne porte en fait que sur le département 13, et commence donc par filtrer les données sur le département avant de sommer la SAU les équipements, de façon à ne conserver que le minimum de données nécessaires et à ne réaliser que le minimum de calculs.
L’évaluation/exécution différée est très puissante mais présente des limites.
On serait tentés d’écrire un traitement entier en mode lazy (sans aucun compute()
ni collect()
dans les étapes intermédiaires) et de faire un unique compute()
ou collect()
tout à la fin du traitement afin que toutes les opérations soient optimisées en une seule étape.
Malheureusement, le moteur acero a ses limites notamment sur des traitements trop complexes (ce qui génère des plantages de sessions R).
QUELQUES CONSEILS POUR ÉLABORER LA BONNE STRATÉGIE AVEC L’ÉVALUATION DIFFÉRÉE :
compute()
)La liste des fonctions du tidyverse supportées par acero est disponible sur cette page.
Il y a (encore) quelques grands absents, notamment :
pivot_wider()
et pivot_longer()
n’ont pas d’équivalent avec acero.
les empilements de plusieurs tables avec une seule fonction (bind_rows()
dans dplyr).
Avec des Arrow Tables, il faut appeler plusieurs fois ces fonctions (en l’occurence union()
. Par exemple :
res <- RA2020 |>
group_by(SIEGE_REG) |>
mutate(total_SAU = sum(SAU_TOT)) |>
collect()
> Error: window functions not currently supported in Arrow
Call collect() first to pull data into R.
Remarque : le code ci-dessus fonctionne par contre en remplaçant le mutate()
par un summarise()
.
Plusieurs solutions existent :
collect()
et poursuivre le traitement avec le moteur d’exécution de dplyr (avec des performances moins importantes).Exemple pour le point 2 issu d’utilitr :
resultats <- bpe_ens_2018_arrow |>
group_by(DEP) |>
summarise(
nb_boulangeries = sum(NB_EQUIP * (TYPEQU == "B203")),
nb_poissonneries = sum(NB_EQUIP * (TYPEQU == "B206"))
) |>
compute()
> ! NotImplemented: Function 'multiply_checked' has no kernel matching input types (double, bool); pulling data into R
L’erreur vient de l’opération sum(NB_EQUIP * (TYPEQU == “B203”)) : arrow ne parvient pas à faire la multiplication entre NB_EQUIP (un nombre réel) et (TYPEQU == “B203”) (un booléen).
=> La solution est très simple: il suffit de convertir (TYPEQU == “B203”) en nombre entier avec la fonction as.integer() qui est supportée par acero.
Le code suivant peut alors être entièrement exécuté par acero:
Le package arrow présente 3 avantages majeurs :
Performances élevées : arrow est très efficace et très rapide pour la manipulation de données tabulaires (nettement plus performant que dplyr par exemple)
Usage réduit des ressources : arrow est conçu pour ne charger en mémoire que le minimum de données. Cela permet de réduire considérablement les besoins en mémoire, même lorsque les données sont volumineuses
Facilité d’apprentissage grâce aux approches dplyr et SQL: arrow peut être utilisé avec les verbes de dplyr (select, mutate, etc.) et/ou avec le langage SQL grâce à DuckDB (voir plus loin).
Exercice 1 (premiers contacts avec un fichier parquet + rappels sur les fonctions)
Consulter les types de colonne de ce fichier
Ouvrir le fichier parquet situé sous ~/CERISE/03-Espace-de-Diffusion/030_Structures_exploitations/3020_Recensements/RA_2020/01_BASES DIFFUSION RA2020/RA_2020_parquet/RA2020_EXPLOITATIONS_240112.parquet
Consulter les 100 premières lignes de ce fichier
Récupérer dans un vecteur trié les codes régions des lieux principaux de production (SIEGE_REG)
Récupérer dans un vecteur trié les libellés régions des lieux principaux de production (SIEGE_LIB_REG)
Ecrire une fonction calculs_RA() qui - pour une région et une table donnée en entrée - conserve uniquement les lignes correspondantes selon la colonne SIEGE_REG, puis groupe la table par SIEGE_DEP et calcule la surface totale SAU (SAU_TOT), la surface totale de céréales (CEREALES_SUR_TOT) et la surface totale d’oléagineux (OLEAG_SUR_TOT) et enfin la part de la surface des céréales dans la SAU totale et la part de la surface des oléagineux dans la SAU totale.
Utiliser ensuite la fonction calculs_RA() pour calculer ces indicateurs sur l’ensemble des régions présentes dans la table du RA2020 et stocker les résultats dans des fichiers Excel sous votre espace personnel.
TIPS : pensez à utiliser {purrr} et {openxlsx} par exemple.
Exercice 2 (collect()
vs compute()
)
data_a <- tibble(
id = rep(1:1000000, each = 10),
annee = rep(2016:2025, times = 1000000),
a = sample(letters, 10000000, replace = TRUE)
)
data_b <- tibble(
id = rep(1:1000000, each = 10),
annee = rep(2016:2025, times = 1000000),
b = runif(10000000, 1, 100)
)
data_c <- tibble(
lettres = sample(letters, 10000000, replace = TRUE),
classe = sample(c("pommes","poires","melon","fraise"), 10000000, replace = TRUE)
)
write_parquet(data_a, "data_a.parquet")
write_parquet(data_b, "data_b.parquet")
write_parquet(data_c, "data_c.parquet")
rm(data_a)
rm(data_b)
rm(data_c)
gc()
Exercice 2 (collect()
vs compute()
)
collect()
Charger les fichiers parquet data_a
et data_b
sous forme de data.frame
Créer la table etape1
en réalisant une jointure à gauche de data_a
avec data_b
.
Charger le fichier parquet data_c
sous forme de data.frame
Filtrer la table etape1
sur les années supérieures à 2020 puis faire la somme de la colonne b
selon la colonne a
Ajouter le colonne classe
issue de la table data_c
dans le tableau final.
compute()
Réaliser les mêmes traitements que A) avec des compute() et réduire le temps d’exécution.
En tant que responsable de sources, vous pouvez être amenés à écrire et déposer des fichiers Parquet, par exemple sous Cerise.
Pour cela, on utilise la fonction write_parquet()
.
Un 1er exemple simple à partir d’un fichier rds:
# Lecture du fichier rds
msa_ns <- readRDS("data/msa_ns_src_2023.rds")
# Écriture des données en format Parquet
write_parquet(x = msa_ns, sink = "data/msa_ns_src_2023.parquet")
Un autre exemple un peu plus compliqué à partir de fichier csv contenu dans un zip sur internet :
# Chargement des packages
library(arrow)
library(readr)
# Téléchargement du fichier zip
download.file("https://www.insee.fr/fr/statistiques/fichier/2540004/dpt2021_csv.zip", destfile = "data/dpt2021_csv.zip")
# Décompression du fichier zip
unzip("data/dpt2021_csv.zip", exdir = "data")
# Lecture du fichier CSV
dpt2021 <- read_delim(file = "data/dpt2021.csv")
# Écriture des données en format Parquet
write_parquet(x = dpt2021, sink = "data/dpt2021.parquet"))
Pourquoi partitionner ?
Par définition, il n’est pas possible de charger seulement quelques lignes d’un fichier Parquet : on importe nécessairement des colonnes entières.
Lorsque le fichier Parquet est partitionné, arrow est capable de filtrer les lignes à importer à l’aide de clés departitionnement, ce qui permet d’accélérer l’importation des données.
Le partitionnement permet de travailler sur des fichiers Parquet de plus petite taille et donc de consommer moins de mémoire vive.
Ça veut dire quoi partitionné ?
Partitionner un fichier revient à le “découper” selon une clé de partionnement (une ou plusieurs variables)
En pratique, l’ensemble des données sera stockée dans plusieurs fichiers au format Parquet.
Voici par exemple comment se présente un fichier Parquet partitionné selon les régions :
Pour écrire des fichiers Parquet partitionnés, on utilise la fonction write_dataset()
.
Partitionnons notre fichier issu de la MSA par type d’exploitation et sexe :
write_dataset(
dataset = msa_ns,
path = "data/msa_ns",
partitioning = c("TYPE_EXP","SEXE"), # les variables de partitionnement
format = "parquet"
)
Voici un aperçu de l’arborescence créée (:
En tant que responsable de sources par exemple, vous pouvez forcer le typage des colonnes d’un fichier Parquet.
Pour cela, assurez-vous que les colonnes du data.frame R sont au bon type.
Si ce n’est pas le cas, utilisez les fonctions de conversion as.character()
, as.integer()
, as.Date()
…
#Créez un DataFrame R en spécifiant le type de chaque colonne
df <- data.frame(
colonne1 = as.integer(c(1, 2, 3)),
colonne2 = as.character(c("A", "B", "C")),
colonne3 = as.logical(c(TRUE, FALSE, TRUE)),
colonne4 = as.numeric(35.43, 29.93, 17.02))
write_parquet(table, "output.parquet")
Une autre façon de forcer le typage des colonnes est d’utiliser un schema
. Cependant, cela apporte de la complexité au code pas forcément utile.
En cas de besoin, un exemple est disponible sur cette page.
A partir d’un data.frame R et avant l’écriture du fichier Parquet correspondant, il est possible d’ajouter des métadonnées générales sur un fichier Parquet.
Soit le data.frame suivant :
Avec uniquement le package {arrow}, on peut ajouter des métadonnées comme ceci :
Avec le package {nanoparquet}, le code est plus court.
Attention ici, la fonction write_parquet()
utilisée est issue du package {nanoparquet} et non {arrow} (qui ne propose pas l’argument metadata
et dont les noms des autres arguments sont différents).
Le package R parquetize permet de faciliter la conversion de données au format Parquet.
Plusieurs formats supportés csv, json, rds, fst, SAS, SPSS, Stata, sqlite…
Propose des solutions de contournement pour les fichiers très volumineux.
Un exemple issu de la documentation :
Conversion from a local rds file to a partitioned parquet file :: 12
rds_to_parquet(
path_to_file = system.file("extdata","iris.rds",package = "parquetize"),
path_to_parquet = tempfile(fileext = ".parquet"),
partition = "yes",
partitioning = c("Species")
)
#> Reading data...
#> Writing data...
#> ✔ Data are available in parquet dataset under /tmp/RtmptNiaDm/file1897441ca0c0.parquet
#> Writing data...
#> Reading data...
La fonction open_dataset()
permet d’ouvrir une connexion vers un fichier Parquet partitionné.
L’utilisation de la fonction open_dataset()
est similaire au cas dans lequel on travaille avec un seul fichier Parquet.
Il y a toutefois 2 différences :
Un exemple avec les données de la MSA :
# Établir la connexion au fichier Parquet partitionné
donnees_msa <- open_dataset(
"data/msa_ns", # Ici, on met le chemin d'un répertoire
hive_style = TRUE,
partitioning = arrow::schema(TYPE_EXP = arrow::utf8(), SEXE = arrow::utf8()) # Les variables de partitionnement
)
# Définir la requête
resultats_msa <- donnees_msa |>
filter(TYPE_EXP == "2" & SEXE == "1") |> # Ici, on filtre selon les clés de partitionnement
select(DEPT, RC_CHEF) |>
collect()
Ce qui donne :
Afin de tirer au mieux profit du partitionnement, il est conseillé de filtrer les données de préférence selon les variables de partitionnement (dans notre exemple, TYP_EXP
et SEXE
).
Il est fortement recommandé de spécifier le type des variables de partitionnement avec l’argument partitioning.
Cela évite des erreurs typiques: le code du département est interprété à tort comme un nombre et aboutit à une erreur à cause de la Corse…
L’argument partitioning
s’utilise en construisant un schéma qui précise le type de chacune des variables de partitionnement.
Voir cette page pour la liste des types supportés.
arrow
Il est recommandé de définir les deux options suivantes au début de votre script.
Cela autorise arrow à utiliser plusieurs processeurs à la fois, ce qui accélère les traitements :
DuckDB est un projet open-source qui propose un moteur SQL optimisé pour réaliser des travaux d’analyse statistique sur des bases de données.
Plusieurs avantages :
Un moteur portable utilisable avec plusieurs langages (R, Python, Javascript…) et plusieurs OS (Windows, Linux, MacOS…)
Une installation et une utilisation très facile
Un moteur SQL capable d’utiliser des données au format Parquet sans les charger complètement en mémoire.
Note
Il faut bien distinguer le projet DuckDB du package R duckdb qui propose simplement une façon d’utiliser Duckdb avec R.
Du point de vue d’un statisticien utilisant R, le package duckdb permet de faire trois choses :
Installation de duckdb
Il suffit d’installer le package duckdb, qui contient à la fois DuckDB et une interface pour que R puisse s’y connecter.
Bonne nouvelle sur la version de Cerise mis à disposition en 2025, le package duckdb sera installé par défaut ! 🎉
Pour utiliser duckdb, il n’est pas nécessaire de connaître le langage SQL car il est possible d’utiliser duckdb avec la syntaxe dplyr.
duckdb est une base de données distante et s’utilise comme telle : il faut ouvrir une connexion, puis “charger” les données dans la base de données pour les manipuler.
Plusieurs remarques :
Cette commande crée une nouvelle base de données duckdb dans la mémoire vive.
Cette base de données ne contient aucune donnée lorsqu’elle est créée. L’objet con
apparaît dans l’onglet Data de l’environnement RStudio, mais la liste des tables n’y est pas directement accessible
À la fin du traitement ou du programme, on ferme la connexion avec le code ci-dessous. L’option shutdown
est importante : elle permet de fermer complètement la session duckdb et de libérer la mémoire utilisée.
Important
Si on n’utilise pas cette option, il arrive souvent que des connexions à moitié ouvertes continuent à consommer des ressources !!!
La fonction duckdb_register()
permet de charger dans duckdb des données présentes en mémoire dans la session R.
Cette méthode a l’avantage de ne pas recopier les données: elle se contente d’établir un lien logique entre la base de données duckdb et un objet de la session R.
L’objet créé dans la base est une vue dans le catalogue “temp”. La durée d’existence de cette vue est le temps de la connexion.
Pour vérifier que le chargement des données a bien fonctionné, la fonction tbl()
permet d’accéder à une table de la base de données grâce à son nom (entre double quotes).
con |> tbl("RA2020_duckdb")
# Source: table<RA2020_duckdb> [?? x 255]
# Database: DuckDB v1.1.0 [damien.dotta@Windows 10 x64:R 4.3.0/:memory:]
NOM_DOSSIER TYPE_QUESTIONNAIRE SEUIL_IFS CHAMP_GEO COEF_F NUMSTRATE STRATE SIEGENAT SIEGE_CODE_COM SIEGE_LIEUDIT
<chr> <chr> <chr> <chr> <dbl> <chr> <chr> <chr> <chr> <chr>
1 AAAAAAA 2 1 1 1.22 2500000004 25000ERICA 10 14406 NA
2 BBBBBBB 2 1 1 10.3 2105161843 2105161843 10 51303 NA
3 CCCCCCC 3 1 2 1 0101030406 EXH_GEO 10 97407 CHEMIN DES ANGLAIS
Pour charger des données situés sur Cerise par exemple, la fonction tbl()
peut aussi être directement utilisée en renseignant le chemin du fichier Parquet.
con %>% tbl("read_parquet('data/RA2020_exploitations.parquet')")
# Ou plus succinct (pas besoin de read_parquet() si pas besoin d'y passer des arguments)
con %>% tbl('data/RA2020_exploitations.parquet')
# Source: SQL [?? x 255]
# Database: DuckDB v1.1.0 [damien.dotta@Windows 10 x64:R 4.3.0/:memory:]
NOM_DOSSIER TYPE_QUESTIONNAIRE SEUIL_IFS CHAMP_GEO COEF_F NUMSTRATE STRATE SIEGENAT SIEGE_CODE_COM SIEGE_LIEUDIT
<chr> <chr> <chr> <chr> <dbl> <chr> <chr> <chr> <chr> <chr>
1 AAAAAAA 2 1 1 1.22 2500000004 25000ERICA 10 14406 NA
2 BBBBBBB 2 1 1 10.3 2105161843 2105161843 10 51303 NA
3 CCCCCCC 3 1 2 1 0101030406 EXH_GEO 10 97407 CHEMIN DES ANGLAIS
Avec des fichiers partitionnés, on peut utiliser la syntaxe suivante.
**/*.parquet
est un motif qui indique que vous souhaitez lire, dans tous les sous-dossiers quelque soit le niveau (**
), l’ensemble des fichiers parquets (*.parquet
) qui s’y trouvent.
con %>% tbl('data/msa_ns/**/*.parquet')
# Source: SQL [?? x 293]
# Database: DuckDB v1.1.0 [damien.dotta@Windows 10 x64:R 4.3.0/:memory:]
A_IDENT DEPT RESID CANTON COMMUNE DOM_FISC SUP NAF SMI SITU_FAM NAIS_JO NAIS_MO NAIS_AN AF_MAJ AF_MIN AF_AVA
<chr> <chr> <chr> <chr> <chr> <dbl> <chr> <chr> <dbl> <dbl> <chr> <chr> <chr> <dbl> <dbl> <dbl>
1 XXXXXXXXXXXXXX… 11 11 10 288 1 0 NA 0 1 25 05 1969 0 0 0
2 YYYYYYYYYYYYYY… 11 11 01 281 1 2903 NA 0 1 30 01 2000 0 0 0
3 ZZZZZZZZZZZZZZ… 11 11 16 269 1 368 NA 0 1 07 01 2000 0 0 0
La fonction dbListTables()
est utile pour afficher les noms des tables présentes dans une base de données.
Une illustration pour mieux comprendre :
# On se déconnecte
DBI::dbDisconnect(con, shutdown = TRUE)
# On crée une nouvelle connexion
con <- DBI::dbConnect(drv = duckdb::duckdb())
# Affichage de la liste des tables
dbListTables(con)
# > character(0)
con |> duckdb::duckdb_register(
name = "iris_duckdb",
df = iris)
con %>% tbl("read_parquet('data/RA2020_exploitations.parquet')")
=> Question : combien de tables va maintenant renvoyer la fonction dbListTables()
?
Réponse… UNE SEULE !
# Affichage de la liste des tables
dbListTables(con)
# > "iris_duckdb"
# On se déconnecte
DBI::dbDisconnect(con, shutdown = TRUE)
En effet, lorsqu’on utilise la fonction tbl()
, celle-ci ne charge pas les données ni dans la mémoire de R ni dans celle de DuckDB.
La fonction dbListFields()
est utile pour afficher les noms des colonnes d’une table présente dans une base de données.
# On crée une nouvelle connexion
con <- DBI::dbConnect(drv = duckdb::duckdb())
con |> duckdb::duckdb_register(
name = "iris_duckdb",
df = iris)
con |> DBI::dbListFields("iris_duckdb")
# > [1] "Sepal.Length" "Sepal.Width" "Petal.Length" "Petal.Width" "Species"
# On se déconnecte
DBI::dbDisconnect(con, shutdown = TRUE)
En complément des instructions déjà vues dans la partie 2 de la formation, on peut accéder aux logical_types des colonnes d’un fichier Parquet en utilisant la requête suivante :
con <- DBI::dbConnect(drv = duckdb::duckdb())
dbGetQuery(con, "DESCRIBE FROM read_parquet('data/output.parquet')")
# column_name column_type null key default extra
# 1 colonne1 DOUBLE YES <NA> <NA> <NA>
# 2 colonne2 VARCHAR YES <NA> <NA> <NA>
# 3 colonne3 BOOLEAN YES <NA> <NA> <NA>
# 4 colonne4 DATE YES <NA> <NA> <NA>
La requête suivant est très utile pour accéder aux méta-donnnées d’un fichier Parquet avec duckdb :
Remarque : La fonction dbGetQuery() du package DBI permet de récupérer un data.frame dont le contenu est le résultat d’une requête.
La fonction parquet_metadata()
peut également être utilisée dans une requête écrite avec duckdb :
con <- DBI::dbConnect(drv = duckdb::duckdb())
dbGetQuery(con, "SELECT * FROM parquet_metadata('data/fr_immp_transactions.parquet')")
=> On retrouve des informations très intéressantes sur le mode de compression utilisé lors de l’écriture du fichier Parquet (SNAPPY), des statistiques sur les row groups (min, max), l’encodage…
Le package R duckdb a été écrit de façon à pouvoir manipuler les données avec la syntaxe de dplyr, c’est très pratique !
Comme avec le package {arrow} on utilise cette syntaxe avec les fonctions collect()
ou compute()
.
# On crée une nouvelle connexion
con <- DBI::dbConnect(drv = duckdb::duckdb())
# Etablissement de la connexion au fichier Parquet
RA2020_dataset <- con %>% tbl('data/RA2020_exploitations.parquet')
# Traitement avec dplyr et un collect()
resultat <- RA2020_dataset |>
filter(SIEGE_REG == "93") |>
group_by(SIEGE_DEP) |>
summarise(total_SAU = sum(SAU_TOT, na.rm = TRUE)) |>
collect()
# On se déconnecte
DBI::dbDisconnect(con, shutdown = TRUE)
Quand on manipule des objets avec duckdb, on construit des requêtes SQL.
Le package duckdb se contente de traduire le code dplyr en SQL.
La fonction show_query()
permet de consulter la requête SQL qui a été exécutée par duckdb.
# Traitement avec dplyr et un collect()
resultat <- RA2020_dataset |>
filter(SIEGE_REG == "93") |>
group_by(SIEGE_DEP) |>
summarise(total_SAU = sum(SAU_TOT, na.rm = TRUE)) |>
show_query()
# > <SQL>
SELECT SIEGE_DEP, SUM(SAU_TOT) AS total_SAU
FROM "data/RA2020_exploitations.parquet"
WHERE (SIEGE_REG = '93')
GROUP BY SIEGE_DEP
Si vous avez des connaissances en SQL, il est bien sûr tout à fait possible de requêter une base DuckDB avec ce langage.
La requête est alors passée dans la fonction DBI::dbGetQuery()
:
# On crée une nouvelle connexion
con <- DBI::dbConnect(drv = duckdb::duckdb())
chemin_donnees <- 'C:/Users/damien.dotta/DEMESIS/Formations_R/Formation_R_perfectionnement/formation-R-perf-06-parquet/data'
# Execution de la requete
resultatSQL <- DBI::dbGetQuery(
con,
paste0("SELECT SIEGE_DEP, SUM(SAU_TOT) AS total_SAU
FROM '", file.path(chemin_donnees,'RA2020_exploitations.parquet'),"'
WHERE (SIEGE_REG = '93')
GROUP BY SIEGE_DEP"))
# resultatSQL est un data.frame directement utilisable dans R
Lorsque le traitement est long, vous devez le découper et stocker quelque part vos résulats intermédiaires de manière à ne pas tout recalculer entièrement à chaque fois.
# On crée une nouvelle connexion
con <- DBI::dbConnect(drv = duckdb::duckdb())
# Etablissement de la connexion au fichier Parquet
RA2020_dataset <- con %>% tbl('data/RA2020_exploitations.parquet')
# A la fin du traitement, on écrit un fichier parquet intermédiaire
RA2020_dataset |>
filter(SIEGE_REG == "93") |>
# Conversion dans un format compatible avec arrow
arrow::to_arrow() |>
arrow::write_parquet("data/table_intermediaire.parquet")
# Reprise des traitements + tard avec le moteur SQL de duckdb
final <- arrow::open_dataset("data/table_intermediaire.parquet") |>
# Conversion dans un format compatible avec duckdb
arrow::to_duckdb(con) |>
group_by(SIEGE_DEP) |>
summarise(total_SAU = sum(SAU_TOT, na.rm = TRUE)) |>
collect()
# On se déconnecte
DBI::dbDisconnect(con, shutdown = TRUE)
# On crée une nouvelle connexion
# + création d'une BDD vide avec le paramètre dbdir
con <- DBI::dbConnect(drv = duckdb::duckdb(),
dbdir = "data/BDD_formation.db")
# Etablissement de la connexion au fichier Parquet
RA2020_dataset <- con %>% tbl('data/RA2020_exploitations.parquet')
# Traitement avec dplyr et un compute()
RA2020_dataset |>
filter(SIEGE_REG == "93") |>
compute(name = "resultat_interm",
temporary = FALSE)
# suppression des tables créés
DBI::dbRemoveTable(con, "resultat_interm")
# Affichage de la liste des tables
dbListTables(con)
# [1] "resultat_interm"
# Reprise des traitements + tard avec le moteur SQL de duckdb
final2 <- con |> tbl("resultat_interm") |>
group_by(SIEGE_DEP) |>
summarise(total_SAU = sum(SAU_TOT, na.rm = TRUE)) |>
collect()
# On se déconnecte
DBI::dbDisconnect(con, shutdown = TRUE)
Lors d’une connexion à une base, des paramètres de dbConnect()
peuvent être très utiles lorsqu’on travaille sur un espace comme Cerise où les ressources sont partagées.
Par exemple :
Reprenons la base BDD_formation.db
créée précédemment.
# On crée une nouvelle connexion
con <- DBI::dbConnect(drv = duckdb::duckdb(),
dbdir = "data/BDD_formation.db")
# Affichage de la liste des tables
dbListTables(con)
# > [1] "resultat_interm"
Ajoutons-y une vue :
Un exemple de code pour requêter directement la base Parquet des bureaux de vote directement depuis Cerise !!!
Configuration générale :
# Installation d'une version récente de duckdb
install.packages("duckdb", repos = "https://packagemanager.posit.co/cran/__linux__/centos8/latest")
library(duckdb) ; library(glue)
cnx <- dbConnect(duckdb())
# Vérification de la version du package
dbGetQuery(cnx, "select version()")
dbExecute(cnx, "
SET http_proxy='http://rie.proxy.national.agri:8080';
SET http_proxy_username='${AGRICOLL_U}';
SET http_proxy_password='${AGRICOLL_P}'")
# Installation et chargement de l'extension nécessaire httpfs
dbExecute(cnx, "INSTALL httpfs")
dbExecute(cnx, "LOAD httpfs")
Requête :
Le résultat de la requête :
column_name column_type null key default extra
1 code_commune_ref VARCHAR YES <NA> <NA> <NA>
2 reconstitution_code_commune VARCHAR YES <NA> <NA> <NA>
3 id_brut_bv_reu VARCHAR YES <NA> <NA> <NA>
4 id VARCHAR YES <NA> <NA> <NA>
5 geo_adresse VARCHAR YES <NA> <NA> <NA>
6 geo_type VARCHAR YES <NA> <NA> <NA>
7 geo_score DOUBLE YES <NA> <NA> <NA>
8 longitude DOUBLE YES <NA> <NA> <NA>
9 latitude DOUBLE YES <NA> <NA> <NA>
10 api_line VARCHAR YES <NA> <NA> <NA>
11 nb_bv_commune UINTEGER YES <NA> <NA> <NA>
12 nb_adresses UINTEGER YES <NA> <NA> <NA>
Autre exemple pour requêter la table Parquet des unités légales de SIRENE :
Requête à passer après configuration du proxy et chargement de l’extension https :
cnx <- dbConnect(duckdb())
dataset <- "https://static.data.gouv.fr/resources/base-sirene-des-entreprises-et-de-leurs-etablissements-siren-siret/20231214-131836/stockunitelegale-utf8.parquet"
req <- glue_sql("
CREATE TABLE extrait_siren AS
FROM {dataset}
WHERE SIREN in ('345149587','388467441')
", .con = cnx)
dbSendQuery(cnx, req)
recup_siren <- dbReadTable(cnx,"extrait_siren")
dbDisconnect(cnx, shutdown = TRUE)
Le résultat de la requête :
# A tibble: 2 × 34
siren statutDiffusionUniteLegale unitePurgeeUniteLegale dateCreationUniteLeg…¹ sigleUniteLegale sexeUniteLegale prenom1UniteLegale prenom2UniteLegale prenom3UniteLegale
<chr> <chr> <lgl> <date> <chr> <chr> <chr> <chr> <chr>
1 345149587 O NA 1985-01-01 INALCA NA NA NA NA
2 388467441 O NA 1992-09-01 NA NA NA NA NA
# ℹ abbreviated name: ¹dateCreationUniteLegale
# ℹ 25 more variables: prenom4UniteLegale <chr>, prenomUsuelUniteLegale <chr>, pseudonymeUniteLegale <chr>, identifiantAssociationUniteLegale <chr>,
# trancheEffectifsUniteLegale <chr>, anneeEffectifsUniteLegale <dbl>, dateDernierTraitementUniteLegale <dttm>, nombrePeriodesUniteLegale <dbl>, categorieEntreprise <chr>,
# anneeCategorieEntreprise <dbl>, dateDebut <date>, etatAdministratifUniteLegale <chr>, nomUniteLegale <chr>, nomUsageUniteLegale <chr>, denominationUniteLegale <chr>,
# denominationUsuelle1UniteLegale <chr>, denominationUsuelle2UniteLegale <chr>, denominationUsuelle3UniteLegale <chr>, categorieJuridiqueUniteLegale <dbl>,
# activitePrincipaleUniteLegale <chr>, nomenclatureActivitePrincipaleUniteLegale <chr>, nicSiegeUniteLegale <chr>, economieSocialeSolidaireUniteLegale <chr>,
# societeMissionUniteLegale <chr>, caractereEmployeurUniteLegale <chr>
Exercice 3 (manipulation avec duckdb)
Avec duckdb, se connecter au fichier du RA2020 au format Parquet (cf. exercice 1)
Filtrer la table du RA2020 EXPLOITATIONS sur la région de votre choix (SIEGE_REG) puis calculer la moyenne de la SAU (SAU_TOT) en fonction de l’orientation nomenclature agrégée (OTEFDA_COEF17).
Réaliser le même traitement que précédemment mais sur une région différente.
Fusionner les 2 tables créées aux questions précédentes de manière à avoir une table qui se présente sous la forme suivante :
- Re-faites l’exercice avec SQL (ou la syntaxe dplyr)
Tableau repris de la documentation utilitr.
Je souhaite… | arrow | duckdb |
---|---|---|
Optimiser mes traitements pour des données volumineuses | ✔️ | ✔️ |
Travailler sur un fichier .parquet ou .csv sans le charger entièrement en mémoire | ✔️ | ✔️ |
Utiliser la syntaxe dplyr pour traiter mes données |
✔️ | ✔️ |
Utiliser du langage SQL pour traiter mes données | ❌ | ✔️ |
Joindre des tables très volumineuses (plus de 4 Go) | ❌ | ✔️ |
Utiliser des fonctions fenêtres | ❌ | ✔️ |
Utiliser des fonctions statistiques qui n’existent pas dans arrow | ❌ | ✔️ |
Écrire un fichier .parquet | ✔️ | ✔️ |
{arrow} et {duckdb} partagent de nombreux concepts. Voici quelques différences :
{duckdb} comprend parfaitement SQL. Si vous êtes à l’aise avec ce langage, vous ne serez pas dépaysés.
Le projet duckdb est très récent. Il y a régulièrement des évolutions qui sont souvent des extensions ou des optimisations, et parfois la résolution de bugs. arrow est un projet plus ancien et plus mature.
La couverture fonctionnelle des fonctions standards de R est meilleure sur {duckdb} que sur {arrow}.
Il est préférable d’utiliser {duckdb} pour les jointures de tables volumineuses.
De même, les fonctions pivot_wider()
, pivot_longer()
et les windows_function
existent nativement dans duckdb mais pas dans arrow. Par exemple :
Pour ceux qui veulent aller plus loin :
arrow
? C’est par iciNote interne écrite par le DEMESIS : voir ici
2 Comment utiliser/interroger un fichier parquet ?