-
Notifications
You must be signed in to change notification settings - Fork 54
/
Fiche_import_fichiers_parquet.qmd
465 lines (317 loc) · 28.6 KB
/
Fiche_import_fichiers_parquet.qmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
# Lire et écrire des fichiers Parquet {#importparquet}
## Tâches concernées et recommandations
- L'utilisateur souhaite importer et exploiter dans `R` des données stockées au format **Parquet**.
- L'utilisateur souhaite convertir des données au format **Parquet**.
::: {.callout-important}
## Tâche concernée et recommandation
- Il est recommandé d'utiliser le format **Parquet** pour stocker des données volumineuses, car il est plus compact que le format csv. Le **package** [`arrow`](https://arrow.apache.org/docs/r/) permet de lire, d'écrire simplement les fichiers au format **Parquet** avec `R`;
- Deux approches sont recommandées pour manipuler des données volumineuses stockées en format Parquet:
- les _packages_ `arrow` et `dplyr` si vous maîtrisez la syntaxe _tidyverse_;
- les _packages_ `DBI` et `duckdb` si vous maîtrisez le langage SQL;
- Il est essentiel de travailler avec la dernière version d'`arrow`, de `duckdb` et de `R` car les _packages_ `arrow` et `duckdb` sont en cours de développement;
- Il est préférable d'utiliser la fonction `open_dataset` pour accéder à des données stockées en format Parquet (plutôt que la fonction `read_parquet`);
- Il est recommandé de partitionner les fichiers **Parquet** lorsque les données sont volumineuses et lorsque les données peuvent être partitionnées selon une variable cohérente avec l’usage des données (département, secteur, année...);
- Lorsqu'on importe des données volumineuses, il est recommandé de sélectionner les observations (avec `filter`) et les variables (avec `select`) pour limiter la consommation de mémoire vive.
:::
Note: cette fiche n'a pas vocation à être exhaustive sur le format Parquet, mais plutôt à lister les points saillants à retenir lorsqu'un statisticien souhaite travailler avec ce format de fichier.
## Qu'est-ce que Parquet et pourquoi s'en servir?
### Qu'est-ce que le format Parquet?
**Parquet** est un format de stockage de données, au même titre que les fichiers CSV, RDS, FST... Ce format n'est pas nouveau (création en 2013), mais il a gagné en popularité dans le monde de la _data science_ au cours des dernières années, notamment grâce au projet _open-source_ [Apache arrow](https://arrow.apache.org/).
Le format Parquet présente plusieurs avantages cruciaux qui en font un concurrent direct du format csv:
- il compresse efficacement les données, ce qui le rend très adapté au stockage de données volumineuses;
- il est conçu pour être indépendant d'un logiciel: on peut lire des fichiers Parquet avec `R`, Python, C++, JavaScript, Java...
- il est conçu pour que les données puissent être chargées très rapidement en mémoire.
### Caractéristiques du format Parquet
Le format Parquet présente trois caractéristiques importantes du point de l'utilisateur:
- __Parquet stocke les données en un format binaire__. Cela signifie qu'un fichier Parquet n'est pas lisible par un humain: contrairement au format `csv`, on ne peut pas ouvrir un fichier Parquet avec Excel, LibreOffice ou Notepad pour jeter un coup d'oeil au contenu.
- Parquet repose sur un **stockage orienté colonne**. Ainsi seront stockées dans un premier temps toutes les données de la première colonne de la table, puis seulement dans un second temps les données de la deuxième colonne et ainsi de suite... [Le blog d'upsolver](https://www.upsolver.com/blog/apache-parquet-why-use) fournit une illustration pour bien visualiser la différence :
```{r, echo = FALSE, fig.cap = "Différence entre le stockage orienté ligne et colonne"}
knitr::include_graphics("../pics/parquet/stockage_colonne.png")
```
- **Un fichier Parquet contient à la fois les données et des métadonnées**. Ces métadonnées écrites à la fin du fichier enregistrent une description du fichier (appelé **schéma**). Ces métadonnées contiennent notamment le type de chaque colonne (entier/réel/caractère) et quelques statistiques (min, max). Ce sont ces métadonnées qui font en sorte que la lecture des données Parquet soit optimisée et sans risque d’altération (voir [ici](https://parquet.apache.org/docs/file-format/metadata/) pour en savoir plus).
- **Un fichier Parquet est composé de groupe de lignes (row group)** contenant également des métadonnées similaires à celles du fichier. La taille idéale d'un row group est de l'ordre de 30 000 à 1 000 000.
Dans un contexte analytique, cette organisation des données génère plusieurs avantages dont les principaux sont:
- **Un gain de vitesse lors de la lecture des données pour un usage statistique**: `R` peut extraire directement les colonnes demandées sans avoir à scanner toutes les lignes comme ce serait le cas avec un fichier `csv` ;
- **La possibilité d'avoir un haut niveau de compression**. Le taux de compression moyen par rapport au format `csv` est souvent compris entre 5 et 10. Pour des fichiers volumineux il est même possible d'avoir des taux de compression bien supérieurs.
Inversement, le format Parquet présente deux contraintes inhabituelles pour les utilisateurs des autres formats (CSV, SAS, FST...):
- Il n'est pas possible d'importer uniquement les 100 premières lignes d'un fichier Parquet (comme on peut facilement le faire pour un fichier CSV); en revanche, il est possible d'afficher les 100 premières lignes d'un fichier Parquet avec la commande: `open_dataset(mon_fichier_parquet) %>% head(100)`;
- Il n'est pas possible d'ouvrir un fichier Parquet avec Excel, LibreOffice ou Notepad.
Pour en savoir plus notamment sur la comparaison entre les formats Parquet et csv, consultez
[le chapitre sur le sujet](https://pythonds.linogaliana.fr/reads3/#le-format-parquet) dans le cours de l'ENSAE _"Python pour la data science"_.
Grâce aux travaux du projet Arrow, **les fichiers aux format Parquet sont inter-opérables** c'est-à-dire qu'ils peuvent être lus par plusieurs langages informatiques : [C](https://arrow.apache.org/docs/c_glib/), [C++](https://arrow.apache.org/docs/cpp/), [C#](https://github.com/apache/arrow/blob/main/csharp/README.md), [Go](https://godoc.org/github.com/apache/arrow/go/arrow), [Java](https://arrow.apache.org/docs/java/), [JavaScript](https://arrow.apache.org/docs/js/), [Julia](https://arrow.juliadata.org/stable/), [MATLAB](https://github.com/apache/arrow/blob/main/matlab/README.md), [Python](https://arrow.apache.org/docs/python/), [Ruby](https://github.com/apache/arrow/blob/main/ruby/README.md), [Rust](https://docs.rs/crate/arrow/) et bien entendu [R](https://arrow.apache.org/docs/r/). Le format Parquet est donc particulièrement adapté aux chaînes de traitement qui font appel à plusieurs langages (exemples: manipulation de données avec `R` puis _machine learning_ avec Python).
S'il est très efficace pour l'analyse de données, **Parquet est en revanche peu adapté à l'ajout de données en continu ou à la modification fréquente de données existantes**.
Pour cette utilisation, le statisticien privilégiera un système de gestion de base de données comme par exemple [`PostgreSQL`](https://www.postgresql.org/).
## Écrire des fichiers Parquet
### Données peu volumineuses: écrire un seul fichier Parquet
Les tables Parquet sont encore loin d'être majoritaires dans les liens de téléchargement notamment face au format csv. C'est la raison pour laquelle, nous allons dans cette section dérouler **le processus pour obtenir un fichier Parquet à partir d'un fichier csv.**
Dans un premier temps, on importe le fichier plat avec la fonction **fread()** du _package_ **data.table**, conformément aux recommandations de [la fiche sur les imports de fichiers plats](https://book.utilitr.org/03_fiches_thematiques/fiche_import_fichiers_plats). On obtient un objet `data.table` en mémoire. Dans un second temps, on exporte ces données en format Parquet avec la fonction `write_parquet()` du _package_ `arrow`.
```{r, eval=FALSE}
library(data.table)
library(magrittr)
library(arrow)
# Création du dossier "Data_parquet"
dir.create("Data_parquet")
# Téléchargement du fichier zip
download.file("https://www.insee.fr/fr/statistiques/fichier/2540004/dpt2021_csv.zip",
destfile = "Data_parquet/dpt2021_csv.zip")
# Décompression du fichier zip
unzip("Data_parquet/dpt2021_csv.zip", exdir = "Data_parquet")
# Lecture du fichier CSV
dpt2021 <- fread("Data_parquet/dpt2021.csv")
# Écriture des données en format Parquet
write_parquet(
x = dpt2021,
sink = "Data_parquet/dpt2021.parquet"
)
```
À l'issue de cette conversion, on peut noter que **le fichier Parquet créé occupe un espace de stockage 10 fois moins important que le fichier csv initial (7,4 Mo contre 76,3 Mo) !**
Pour les exemples qui suivent dans cette fiche, on utilise un fichier de [la Base Permanente des Équipements de l'Insee](https://www.insee.fr/fr/statistiques/3568629) que l'on va convertir au format **Parquet**.
Vous pouvez télécharger ce fichier avec le package [`doremifasol`](https://inseefrlab.github.io/DoReMIFaSol/index.html) et plus particulièrement la fonction [`telechargerDonnees()`](https://inseefrlab.github.io/DoReMIFaSol/reference/telechargerDonnees.html) :
```{r, eval=FALSE}
# remotes::install_github("InseeFrLab/doremifasol", build_vignettes = TRUE)
library(doremifasol)
library(arrow)
# Téléchargement des données de la BPE
donnees_BPE <- telechargerDonnees("BPE_ENS", date = 2021)
# Éecriture des données sous format Parquet
write_parquet(
x = donnees_BPE,
sink = "Data_parquet/BPE_ENS.parquet"
)
```
### Données volumineuses: écrire un fichier Parquet partitionné
Le package `arrow` présente une fonctionnalité supplémentaire qui consiste à créer et lire un fichier **Parquet partitionné**. Le partitionnement des fichiers Parquet présente des avantages pratiques qui sont expliqués dans la suite de cette fiche (voir partie [Lire et exploiter un fichier Parquet avec `R`]{#readparquet}).
Partitionner un fichier revient à le "découper" selon une clé de partitionnement, qui prend la forme d'une ou de plusieurs variables. Cela signifie en pratique que l'ensemble des données sera stockée sous forme d'un grand nombre de fichiers Parquet (un fichier par valeur des variable de partitionnement). Par exemple, il est possible de partitionner un fichier national par département: on obtient alors un fichier Parquet par département.
::: {.callout-tip}
- **Il est important de bien choisir les variables de partitionnement** d'un fichier **Parquet**. Il faut choisir des variables faciles à comprendre et qui soient cohérentes avec l'usage des données (année, département, secteur...). En effet, un partitionnement bien construit induit par la suite des gains d'efficacité sur les traitements et facilite la maintenance du fichier sur le long terme.
- **Il est inutile de partitionner des données de petite taille**. Si les données dépassent quelques millions d'observations et/ou si leur taille en CSV dépasse quelques giga-octets, il est utile de partitionner.
- **Il ne faut pas partitionner les données en trop de fichiers**. En pratique, il est rare d'avoir besoin de plus d'une ou deux variables de partitionnement.
- **Si vous souhaitez être compatible avec tous les outils lisant du parquet**, il est recommandé de ne pas partitionner sur une variable pouvant être `NA` ou une chaîne vide.
:::
Pour créer des fichiers **Parquet** partitionnés, il faut utiliser la fonction [`write_dataset()`](https://arrow.apache.org/docs/r/reference/write_dataset.html) du _package_ `arrow`. Voici ce que ça donne sur le fichier de la BPE :
```{r, eval = FALSE}
write_dataset(
dataset = donnees_BPE,
path = "Data/",
partitioning = c("REG"), # la variable de partitionnement
format="parquet"
)
```
Avec cette instruction, on a créé autant de répertoires que de modalités différentes de la variable `REG`. Vous pouvez noter la structure des dossiers nommés `REG==[valeur]`.
```{r, echo = FALSE, fig.cap = "Arborescence d'un fichier Parquet partitionné"}
knitr::include_graphics("../pics/parquet/fichier_partition.png")
```
### Données volumineuses: optimiser en triant
Il n'est pas toujours possible ou souhaitable de partitionner un fichier si la variable de partitionnement possède de trop nombreuses modalités (si celle-ci est non discrète ou possède des milliers de modalités...). Dans ces cas là, vous pouvez trier le fichier par la variable à utiliser, cela va permettre une recherche efficace à partir des métadonnées des fichiers et des groupes de lignes.
```{r, eval = FALSE}
donnees_BPE |>
arrange(EPCI) |>
write_parquet(
sink = "Data_parquet/BPE_ENS.parquet"
)
```
Vous pouvez bien sûr cumuler les partitions avec des tris :
```{r, eval = FALSE}
donnees_BPE |>
arrange(EPCI) |>
write_dataset(
path = "Data/",
partitioning = c("REG"), # la variable de partitionnement
format="parquet"
)
```
Cette méthode est quasiment aussi efficace que le partitionnement.
### Vérifier qu'un fichier parquet est correctement optimisé
Dans certains cas pathologiques quand vous manipulez de très gros volumes de données (par exemple quand vous partitionnez avec `arrow::open_dataset` un volume de données de plusieurs de dizaine de millions de lignes), le package `arrow` peut générer des fichiers parquet avec un nombre de lignes par row group très petite (inférieur à 1000 voire à 100). Cela rendra toutes les requêtes sur vos fichiers extrèmement lent.
Pour vérifier que vos fichiers ont une taille de row group correcte, vous pouvez utiliser la fonction suivante :
```{r, eval = FALSE}
library(arrow)
mean_row_group_size <- function(path) {
a <- arrow::ParquetFileReader$create(path)
(a$num_rows / a$num_row_groups)
}
mean_row_group_size('bpe2018/REG=11/part-0.parquet')
```
Vous devez obtenir une valeur au moins égale à 10 000. Si vous obtenez une valeur inférieure, vous aurez intérêt à regénérer votre fichier en passant par d'autres méthodes (par exemple générer chaque partition en faisant une boucle et un filtre).
## Lire et exploiter un fichier Parquet avec `R` {#readparquet}
### Cas des données peu volumineuses: importer les données en mémoire
**La méthode présentée dans cette section est valable uniquement pour les fichiers peu volumineux.** Elle implique en effet d'importer l'intégralité d'un fichier Parquet dans la mémoire vive de votre espace de travail avant de pouvoir travailler dessus. Il est possible d'effectuer des requêtes plus efficacement sur des fichiers Parquet. Pour cette raison, **il est conseillé d'utiliser la fonction `open_dataset` (présentée plus bas) pour accéder à des données stockées en format Parquet, plutôt que la fonction `read_parquet`.**
La fonction [`read_parquet()`](https://arrow.apache.org/docs/r/reference/read_parquet.html) du _package_ `arrow` permet d'importer des fichiers Parquet dans `R`. Elle possède un argument très utile `col_select` qui permet de sélectionner les variables à importer (par défaut toutes). Cet argument accepte soit une liste de noms de variables, soit [une expression dite de `tidy selection` issue du *tidyverse*](https://dplyr.tidyverse.org/reference/dplyr_tidy_select.html).
Pour utiliser `read_parquet()`, il faut charger le *package* `arrow` :
```{r, eval=FALSE}
library(arrow)
donnees <- arrow::read_parquet("Data/BPE_ENS.parquet")
```
- Exemple en ne sélectionnant que quelques variables à l'aide d'un vecteur de caractères :
```{r, eval = FALSE}
donnees <- arrow::read_parquet(
"Data/BPE_ENS.parquet",
col_select = c('AN','REG','DEP','SDOM','TYPEQU','NB_EQUIP')
)
```
- Exemple en ne sélectionnant que quelques variables à l'aide d'une `tidy selection` :
```{r, eval = FALSE}
donnees <- arrow::read_parquet(
"Data/BPE_ENS.parquet",
col_select = starts_with("DEP")
)
```
Dans les trois cas, le résultat obtenu est un objet directement utilisable dans `R`.
### Cas des données volumineuses: utiliser des requêtes `dplyr`
Il arrive fréquemment que la méthode proposée dans la section précédente ne puisse pas être appliquée, car les données que l'on souhaite exploiter sont trop volumineuses pour être importées dans la mémoire vive dont on dispose. Par exemple, le fichier des données du [recensement de la population 1968-2019](https://www.insee.fr/fr/statistiques/6671801) fait 3,2 Go et contient plus de 51,5 millions de lignes et 18 colonnes, ce qui est difficile à importer sur un ordinateur standard.
**Les _packages_ `arrow` et `dplyr` proposent une approche qui permet de traiter ces données très volumineuses sans les charger dans la mémoire vive**. Cette approche nécessite de charger les _packages_ `arrow` et `dplyr` et comprend trois étapes:
- On crée une connexion au fichier Parquet avec la fonction `open_dataset()`: comme la fonction `read_parquet()`, elle ouvre le fichier Parquet, mais elle n'importe pas les données contenues dans le fichier;
- On définit une chaîne de traitement (ou __requête__) avec la syntaxe du _tidyverse_ (voir la fiche [Manipuler des données avec le `tidyverse`](##tidyverse)). Consultez [cette page](https://arrow.apache.org/docs/dev/r/reference/acero.html) pour accéder à la liste des verbes issus du _tidyverse_ connus par `arrow`;
- On termine la requête avec la fonction `collect()`, qui indique à `R` que l'on souhaite récupérer le résultat de la requête sous forme d'un `data.frame`.
Voici un exemple avec une table peu volumineuse :
```{r, eval=FALSE}
library(dplyr)
library(arrow)
# Établir la connexion aux données
donnees_BPE <- open_dataset("Data/BPE_ENS.parquet")
# Définir la requête
requete <- donnees_BPE |>
filter(REG == "76") |>
group_by(DEP) |>
summarise(nb_equipements_total = SUM(NB_EQUIP))
# Récupérer le résultat sous forme d'un data.frame
resultat <- requete |> collect()
```
Avec cette syntaxe, la requête va automatiquement utiliser les variables du fichier **Parquet** dont elle a besoin (en l'occurence `REG`, `DEP` et `NB_EQUIP`) et minimiser l'occupation de la mémoire vive.
- Exemple avec une table volumineuse (Recensements 1968-2019, suivre ce [lien](https://gist.github.com/ddotta/acf6add0f2328f077791461ef4f37b84) pour obtenir le code qui permet de générer "Ficdep19.parquet" de façon reproductible) :
```{r, eval=FALSE}
# Attention ce morceau de code n'est pas reproductible,
# Il faut suivre le lien dans le texte pour reconstruire le fichier de données
library(dplyr)
# Établir la connexion aux données
donnees_Ficdep19 <- open_dataset("Data/Ficdep19.parquet")
# Définir la requête
requete2 < - donnees_Ficdep19 |>
filter(DEP_RES_21 == "11") |>
group_by(SEXE) |>
summarise(total = sum(pond)) |>
collect()
# Récupérer le résultat sous forme d'un data.frame
resultat2 <- requete2 |> collect()
```
Cette instruction s'exécute sur un ordinateur standard en quelques secondes.
::: {.callout-note}
Les _packages_ `arrow` et `duckdb` présentent une grande différence avec les _packages_ standard de manipulation de données comme `dplyr` ou `data.table`: lorsqu'on exécute une requête sur une table de données, ces _packages_ ne se contentent pas d'exécuter les commandes une à une, dans l'ordre du code, mais analysent le code pour **optimiser le plan d'exécution de la requête**. En pratique, cela signifie qu'`arrow` et `duckdb` essaient de n'importer que les observations nécessaires à la requête, de ne conserver que les colonnes nécessaires au calcul, etc. C'est cette optimisation du plan d'exécution (appelée _predicate push-down_) qui permet d'accélérer les traitements et de réduire la consommation de ressources informatiques.
:::
<!-- ## Exploiter un fichier Parquet avec le _package_ `duckdb` -->
<!-- Dans le cas de fichiers volumineux, il est également possible de les requêter avec le langage `SQL` grâce au _package_ [`duckdb`](https://duckdb.org/docs/api/r.html). Cette méthode est basée sur le moteur portable `DuckDB` qui permet à n'importe quel ordinateur d'accéder à des performances d'un moteur de base de données classique qui utilise un serveur. Pour plus d'informations sur la façon d'exécuter des requêtes sur des bases de données, consultez [cette fiche](https://book.utilitr.org/03_fiches_thematiques/fiche_connexion_bdd#ex%C3%A9cuter-des-requ%C3%AAtes). En fonction des cas d'usage, la méthode présentée ici peut être encore plus efficace que celle avec `arrow` et `dplyr`, mais elle implique de savoir exprimer les requêtes en langage SQL. -->
<!-- L'approche avec `duckdb` comprend trois étapes similaires à celle de l'approche avec `arrow` et `dplyr`: -->
<!-- - On crée une connexion au moteur `DuckDB` avec la fonction `DBI::dbConnect()`; cela nécessite de charger les _package_ `DBI` et `duckdb`; -->
<!-- - On définit une requête avec le langage SQL; -->
<!-- - On exécute la requête avec la fonction `DBI::dbGetQuery()`, qui indique à `R` que l'on souhaite récupérer le résultat de la requête sous forme d'un `data.frame`. -->
<!-- Voici un exemple avec une table peu volumineuse : -->
<!-- ```{r, eval = FALSE} -->
<!-- library(DBI) -->
<!-- library(duckdb) -->
<!-- # Établir la connexion au moteur duckdb -->
<!-- con <- dbConnect(duckdb::duckdb()) -->
<!-- donnees_Ficdep19 <- open_dataset("Data/Ficdep19.parquet") -->
<!-- # Définir la requête (en SQL) -->
<!-- requete3 < - "SELECT SUM(NB_EQUIP) FROM 'Data/BPE_ENS.parquet' -->
<!-- WHERE REG='76' -->
<!-- GROUP BY DEP" -->
<!-- # Récupérer le résultat sous forme d'un data.frame -->
<!-- resultat3 <- dbGetQuery(con, requete3) -->
<!-- ``` -->
<!-- Voici un exemple avec une table volumineuse (RP 1968-2019) : -->
<!-- ```{r, eval = FALSE} -->
<!-- # Établir la connexion au moteur duckdb -->
<!-- con <- dbConnect(duckdb::duckdb()) -->
<!-- # Définir la requête (en SQL) -->
<!-- requete4 < - "SELECT SUM(POND) FROM 'Data/Ficdep19.parquet' -->
<!-- WHERE DEP_RES_21='11' -->
<!-- GROUP BY SEXE" -->
<!-- # Récupérer le résultat sous forme d'un data.frame -->
<!-- resultat4 <- dbGetQuery(con, requete4) -->
<!-- ``` -->
<!-- Cette instruction s'exécute également en quelques secondes sur un ordinateur standard. -->
## Lire et exploiter un fichier Parquet partitionné
### Quel est l'intérêt d'utiliser des fichiers Parquet partitionnés?
Comme indiqué précédemment, les _packages_ `arrow` et `duckdb` ne se contentent pas d'exécuter les instructions de la requête une à une, dans l'ordre du code, mais analysent la requête dans son ensemble pour **optimiser le plan d'exécution de la requête**. Toutefois, il n'est pas possible de charger seulement quelques lignes d'un fichier Parquet: on importe nécessairement des colonnes entières. C'est principalement sur ce point qu'**utiliser un fichier Parquet partitionné facilite ce travail d'optimisation du plan d'exécution.** En effet, lorsque le fichier Parquet est partitionné, `arrow` est capable de filtrer les lignes à importer à l'aide des clés de partitionnement, ce qui permet d'accélérer l'importation des données.
**Exemple :** imaginons que la Base Permanente des Équipements soit stockée sous la forme d'un fichier Parquet partitionné par région (`REG`), et qu'on veuille compter le nombre d'équipements de chaque type dans chaque département de la région Hauts-de-France (`REG == "32"`). On utilisera le code suivant:
```{r, eval = FALSE}
# Établir la connexion au fichier Parquet partitionné
donnees_BPE_part <- open_dataset(
"Data/",
partitioning = arrow::schema(REG = arrow::utf8())
)
# Définir la requête
requete_BPE <- donnees_BPE_part |>
filter(REG == "32") %>% # Ici, on filtre selon la clé de partitionnement
select(DEP, TYPEQU, NB_EQUIP) %>%
group_by(DEP, TYPEQU) %>%
summarise(nb_equipements = sum(NB_EQUIP))
# Récupérer le résultat sous forme d'un data.frame
resultat_BPE <- requete_BPE |> collect()
```
Au moment d'exécuter cette requête, `arrow` va utiliser la variable de partitionnement pour ne lire que la partie `REG == "32"` du fichier partitionné (donc seulement une partie des observations). Autrement dit, le fait que le fichier Parquet soit partitionné accélère la lecture des données.
En conclusion, l'utilisation des fichiers Parquet partitionné présente deux avantages :
- Elle permet de travailler sur des fichiers **Parquet** de plus petite taille et de consommer moins de mémoire vive;
- Elle fait gagner du temps dans l'exécution des requêtes sur les fichiers volumineux (par rapport à un fichier **Parquet** unique).
### Comment bien utiliser les fichiers Parquet partitionnés?
#### Avec le _package_ `arrow`
La fonction [`open_dataset()`](https://arrow.apache.org/docs/r/reference/open_dataset.html) 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 deux différences:
- Le chemin indiqué n'est pas celui d'un fichier `.parquet`, mais le chemin d'un répertoire, dans lequel se trouve le fichier Parquet partitionné;
- Il est préférable d'indiquer le nom et le type de la ou des variable(s) de partitionnement.
Une fois que la connexion est établie avec le fichier partitionné, il est possible de l'utiliser exactement comme s'il s'agissait d'un seul fichier Parquet. Voici un exemple de code:
```{r, eval = FALSE}
# Établir la connexion au fichier Parquet partitionné
donnees_part <- open_dataset(
"Data/", # Ici, on met le chemin d'un répertoire
hive_style = TRUE,
partitioning = arrow::schema(REG = arrow::utf8()) # La variable de partitionnement
)
# Définir la requête
requete5 <- donnees_part |>
filter(REG == "32") |> # Ici, on filtre selon la clé de partitionnement
select(DEP, TYPEQU, NB_EQUIP) %>%
group_by(DEP) |>
summarise(nb_total = sum(NB_EQUIP))
# Récupérer le résultat sous forme d'un data.frame
resultat5 <- requete5 |> collect()
```
Pour bien utiliser un fichier Parquet partitionné, il est recommandé de suivre les deux conseils suivants:
- 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, la région);
- 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:
```{r, eval = FALSE}
donnees_part <- open_dataset(
"Data/",
partitioning = arrow::schema(variable1 = arrow::utf8(), variable2 = arrow::int16())
)
```
Les types les plus fréquents sont: nombre entier (`int8()`, `int16()`, `int32()`, `int64()`), nombre réel (`float()`, `float32()`, `float64()`), et chaîne de caractère (`utf8()`, `large_utf8()`). Il existe beaucoup d'autres types, vous pouvez en consulter la liste en exécutant `?arrow::float` ou en consultant [cette page](https://arrow.apache.org/docs/r/reference/data-type.html).
- 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:
```{r, eval = FALSE}
# Autoriser arrow à utiliser plusieurs processeurs en même temps
options(arrow.use_threads = TRUE)
# Définir le nombre de processeurs utilisés par arrow
# 10 processeurs sont suffisants dans la plupart des cas
arrow:::set_cpu_count(10)
```
<!-- #### Avec le _package_ `duckdb` -->
<!-- Il est tout à fait possible d'exploiter un fichier Parquet partitionné avec `duckdb`: il suffit d'utiliser la fonction `duckdb_register_arrow` pour indiquer au moteur `duckdb` qu'il existe une connexion au fichier Parquet partitionné. Voici un exemple: -->
<!-- ```{r, eval = FALSE} -->
<!-- library(arrow) -->
<!-- library(DBI) -->
<!-- library(duckdb) -->
<!-- # Établir la connexion au moteur duckdb -->
<!-- con <- dbConnect(duckdb::duckdb()) -->
<!-- # Établir la connexion au fichier Parquet partitionné avec arrow -->
<!-- donnees_part <- open_dataset( -->
<!-- "Data/", # Ici, on met le chemin d'un répertoire -->
<!-- hive_style = TRUE, -->
<!-- partitioning = arrow::schema(REG = arrow::utf8()) # La variable de partitionnement -->
<!-- ) -->
<!-- # Indiquer au moteur duckdb qu'il existe une connexion au fichier Parquet -->
<!-- duckdb::duckdb_register_arrow(con_ddb, "donnees_part", donnees_part) -->
<!-- # Exemple de requête: récupérer le nombre de lignes du fichier -->
<!-- dbGetQuery(con_ddb, "SELECT count(1) as nb_lignes FROM donnees_part") -->
<!-- ``` -->
## Pour en savoir plus
* [Page officielle de duckdb](https://duckdb.org/)
* [Apache Parquet pour le stockage de données volumineuses](https://www.cetic.be/Apache-Parquet-pour-le-stockage-de-donnees-volumineuses)