library(arrow)
library(dplyr)
# Autoriser arrow à utiliser plusieurs processeurs en parallèle
options(arrow.use_threads = TRUE)
# Définir le nombre de processeurs qu'arrow peut utiliser
arrow::set_cpu_count(parallel::detectCores() %/% 4)
20 Manipuler des données avec arrow
20.1 Tâches concernées et recommandations
L’utilisateur souhaite manipuler des données structurées sous forme de data.frame
par le biais de l’écosystème Arrow
(sélectionner des variables, sélectionner des observations, créer des variables, joindre des tables).
Pour des tables de données de taille petite et moyenne (inférieure à 1 Go ou moins d’un million d’observations), il est recommandé d’utiliser les packages
tibble
,dplyr
ettidyr
qui sont présentés dans la fiche Manipuler des données avec letidyverse
;Pour des tables de données de grande taille (plus de 1 Go en CSV, plus de 200 Mo en Parquet, ou plus d’un million d’observations), il est recommandé d’utiliser soit les packages
arrow
(qui fait l’objet de la présente fiche) et#duckdb
(voir la fiche Manipuler des données avecduckdb
), soit le packagedata.table
qui fait l’objet de la fiche Manipuler des données avecdata.table
.Il est essentiel de travailler avec la dernière version d’
arrow
, deduckdb
et deR
car les packagesarrow
etduckdb
sont en cours de développement. Par ailleurs, les recommandations d’utilitR
peuvent évoluer en fonction du développement de ces packages.Si les données traitées sont très volumineuses (plus de 5 Go en CSV, plus de 1 Go en Parquet ou plus de 5 millions d’observations), il est essentiel de manipuler uniquement des objets
Arrow Table
, plutôt que destibbles
. Cela implique notamment d’utiliser la fonctioncompute()
plutôt quecollect()
dans les traitements intermédiaires.Pour les personnes qui découvrent
arrow
, il est recommandé de partir de l’exemple de script de la Section 20.6 pour se familiariser avec l’usaged'arrow
.
Apprendre à utiliser arrow
n’est pas difficile, car la syntaxe utilisée est quasiment identique à celle du tidyverse
. Toutefois, une bonne compréhension du fonctionnement de R
et de arrow
est nécessaire pour bien utiliser arrow
sur des données volumineuses. Voici quelques conseils pour bien démarrer:
- Il est indispensable de lire les fiches Importer des fichiers Parquet et Manipuler des données avec le
tidyverse
avant de lire la présente fiche. - Il est complètement normal de rencontrer des erreurs difficiles à comprendre lorsqu’on commence à utiliser
arrow
, il ne faut donc pas se décourager. - Il ne faut pas hésiter à demander de l’aide à des collègues, ou à poser des questions sur les salons Tchap adaptés (le salon Langage
R
par exemple).
20.2 Présentation du package arrow
et du projet associé
20.2.1 Qu’est-ce qu’arrow
?
Apache Arrow
est un projet open-source qui propose deux choses:
- une représentation standardisée des données tabulaires en mémoire vive appelée Apache Arrow Columnar Format, qui est à la fois efficace (les traitements sont rapides), interopérable (différents langages de programmation peuvent accéder aux mêmes données, sans conversion des données dans un autre format) et indépendante du langage de programmation utilisé.
- une implémentation de ce standard en
C++
, qui prend la forme d’une librairieC++
nomméelibarrow
. Il existe d’autres implémentations dans d’autres langages; l’implémentation enRust
est par exemple utilisée par le projetPolars
, une librairie alternative àdplyr
(R
) ouPandas
(Python
) pour le traitement de données.
Un point important à retenir est donc que arrow
n’est pas un outil spécifique à R
, et il faut bien distinguer le projet arrow
(et la librairie C++ libarrow
) du package R
arrow
. Ce package propose simplement une interface qui permet d’utiliser la librairie libarrow
avec R
, et il existe d’autres interfaces pour se servir de libarrow
avec d’autres langages: en Python, en Java, en Javascript, en Julia, etc.
20.2.2 Spécificités de arrow
Le projet arrow
présente cinq spécificités:
-
Représentation des données en mémoire :
arrow
organise les données en colonnes plutôt qu’en lignes (on parle de columnar format). Concrètement, cela veut dire que dans la RAM toutes les valeurs de la première colonne sont stockées de façon contiguë, puis les valeurs de la deuxième colonne, etc. Cette structuration des données rend les traitements très efficaces: si l’on veut par exemple calculer la moyenne d’une variable, il est possible d’accéder directement au bloc de mémoire vive qui contient l’intégralité de cette colonne (indépendamment des autres colonnes de la table de données), d’où un traitement très rapide.
Illustration de ce principe
Utilisation avec Parquet:
arrow
est souvent utilisé pour manipuler des données stockées en format Parquet. Parquet est un format de stockage orienté colonne conçu pour être très rapide en lecture (voir la fiche Importer des fichiers Parquet pour plus de détails).arrow
est optimisé pour travailler sur des fichiers Parquet, notamment lorsqu’ils contiennent des données très volumineuses.Traitement de données volumineuses:
arrow
est conçu pour traiter des données sans avoir besoin de les charger complètement dans la mémoire vive. Cela signifie qu’arrow
est capable de traiter des données plus volumineuses que la mémoire vive (RAM) dont on dispose. C’est un avantage majeur en comparaison aux autres approches possibles enR
(data.table
etdplyr
par exemple).Interopérabilité:
arrow
est conçu pour être interopérable entre plusieurs langages de programmation tels queR
, Python, Java, C++, etc. Cela signifie que les données peuvent être échangées entre ces langages sans avoir besoin de convertir les données, d’où des gains importants de temps et de performance.Lazy Evaluation:
arrow
prend en charge la lazy evaluation (évaluation différée) dans certains contextes. Cela signifie que les traitements ne sont effectivement exécutés que lorsqu’ils sont nécessaires, ce qui peut améliorer les performances en évitant le calcul de résultats intermédiaires non utilisés. La Section 21.5.2 présente en détail cette notion.
20.2.3 A quoi sert le package arrow
?
Du point de vue d’un statisticien utilisant R
, le package arrow
permet de faire trois choses:
- Importer des données (exemples: fichiers CSV, fichiers Parquet, stockage S3) et les organiser en mémoire vive dans un objet
Arrow Table
; - Manipuler des données organisées dans un
Arrow Table
avec la syntaxedplyr
, ou avec le langage SQL (grâce àduckdb
); - Écrire des données en format Parquet.
20.2.4 Quels sont les avantages d’arrow
?
En pratique, le package arrow
présente trois avantages:
-
Performances élevées:
arrow
est très efficace et très rapide pour la manipulation de données tabulaires (nettement plus performant quedplyr
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 dedplyr
(select
,mutate
, etc.) et/ou avec le langage SQL grâce àduckdb
. Par conséquent, il n’est pas nécessaire d’apprendre une nouvelle syntaxe pour utiliserarrow
, on peut s’appuyer sur la ou les approches que l’on maîtrise déjà. En revanche, il est à noter que le packagedata.table
n’est pas directement compatible avecarrow
(il faut convertir les objetsArrow Table
endata.table
, opération longue lorsque les données sont volumineuses).
20.3 Que faut-il savoir pour utiliser arrow
?
Le package arrow
présente quatre caractéristiques importantes:
- une structure de données spécifique: le
Arrow Table
; - une utilisation via la syntaxe
dplyr
; - un moteur d’exécution spécifique:
acero
; - un mode de fonctionnement particulier: l’évaluation différée.
20.3.1 Charger et paramétrer le package arrow
Pour utiliser arrow
, il faut commencer par charger le package. Comme arrow
s’utilise presque toujours avec dplyr
en pratique, il est préférable de prendre l’habitude de charger les deux packages ensemble. Par ailleurs, il est utile de définir systématiquement deux réglages qui sont importants pour les performances d’arrow
: autoriser arrow
à utiliser plusieurs processeurs en parallèle, et définir le nombre de processeurs qu’arrow
peut utiliser.
20.3.2 Le data.frame
version arrow
: le Arrow Table
Le package arrow
structure les données non pas dans un data.frame
classique, mais dans un objet spécifique à arrow
: le Arrow Table
. Dans un objet Arrow Table
, les données sont organisées en colonnes plutôt qu’en lignes, conformément aux spécifications d’arrow
(voir la présentation d’arrow
). Pour convertir un data.frame
ou un tibble
en Arrow Table
, il suffit d’utiliser la fonction as_arrow_table()
.
Par rapport à un data.frame
standard ou à un tibble
, le Arrow Table
se distingue immédiatement sur trois points. Pour illustrer ces différences, on utilise la base permanente des équipements 2018 (table bpe_ens_2018
), transformée en tibble
d’une part, et en Arrow Table
d’autre part.
# Charger les données et les convertir en tibble
bpe_ens_2018_tbl <- doremifasolData::bpe_ens_2018 |> as_tibble()
# Charger les données et les convertir en Arrow Table
bpe_ens_2018_arrow <- doremifasolData::bpe_ens_2018 |> as_arrow_table()
Première différence: alors que les data.frames
et les tibbles
apparaissent dans la rubrique Data
de l’environnement RStudio
(cadre rouge dans la capture d’écran), les objets Arrow Table
apparaissent dans la rubrique Values
(cadre blanc).
Deuxième différence: alors que l’affichage dans la console d’un data.frame
ou tibble
permet de visualiser les premières lignes, la même opération sur un Arrow Table
affiche uniquement des métadonnées (nombre de lignes et de colonnes, nom et type des colonnes).
# Affichage d'un tibble
bpe_ens_2018_tbl
# A tibble: 1,035,564 × 7
REG DEP DEPCOM DCIRIS AN TYPEQU NB_EQUIP
<chr> <chr> <chr> <chr> <dbl> <chr> <dbl>
1 84 01 01001 01001 2018 A401 2
2 84 01 01001 01001 2018 A404 4
3 84 01 01001 01001 2018 A504 1
4 84 01 01001 01001 2018 A507 1
5 84 01 01001 01001 2018 B203 1
6 84 01 01001 01001 2018 C104 1
7 84 01 01001 01001 2018 D233 1
8 84 01 01001 01001 2018 F102 1
9 84 01 01001 01001 2018 F111 1
10 84 01 01001 01001 2018 F113 1
# ℹ 1,035,554 more rows
# Affichage d'un Arrow Table
bpe_ens_2018_arrow
Table
1035564 rows x 7 columns
$REG <string>
$DEP <string>
$DEPCOM <string>
$DCIRIS <string>
$AN <double>
$TYPEQU <string>
$NB_EQUIP <double>
See $metadata for additional Schema metadata
Troisième différence: alors qu’il est possible d’afficher le contenu d’un data.frame
ou d’un tibble
en cliquant sur son nom dans la rubrique Data
de l’environnement ou en utilisant la fonction View()
, il n’est pas possible d’afficher directement le contenu d’un Arrow Table
. Pour afficher le contenu d’un Arrow Table
, il faut d’abord convertir le Arrow Table
en tibble
avec la fonction collect()
.
Il arrive fréquemment que l’on souhaite jeter un coup d’oeil au contenu d’un Arrow Table
. Toutefois, convertir directement un Arrow Table
très volumineux en tibble
peut poser de sérieux problèmes: temps de conversion, consommation importante de RAM, voire plantage de R
si le Arrow Table
est vraiment très gros. Il est donc fortement conseillé de prendre un petit extrait du Arrow Table
concerné et de convertir uniquement cet extrait en tibble
.
Exemple de code qui visualise un échantillon d’une table Arrow
# Extraire les 1000 premières lignes du Arrow Table et les convertir en tibble
extrait_bpe <- bpe_ens_2018_arrow |> slice_head(n = 1000) |> collect()
View(extrait_bpe)
20.3.3 Manipuler des Arrow Table
avec la syntaxe dplyr
Le package R
arrow
a été écrit de façon à ce qu’un Arrow Table
puisse être manipulé avec les fonctions de dplyr
(select
, filter
, mutate
, left_join
, etc.), comme si cette table était un data.frame
ou un tibble
standard.
Il est également possible d’utiliser sur un Arrow Table
un certain nombre de fonctions des packages du tidyverse
(comme stringr
et lubridate
). Cela s’avère très commode en pratique, car lorsqu’on sait utiliser dplyr
et le tidyverse
, on peut commencer à utiliser arrow
sans avoir à apprendre une nouvelle syntaxe de manipulation de données. Il y a néanmoins des subtilités à connaître, détaillées dans la suite de cette fiche.
Dans l’exemple suivant, on calcule le nombre d’équipements par région, à partir d’un tibble
et à partir d’un Arrow table
. La seule différence apparente entre les deux traitement est la présence de la fonction collect()
à la fin des instructions; cette fonction indique que l’on souhaite que le résultat du traitement soit stocké sous la forme d’un tibble
. La raison d’être de ce collect()
est expliquée plus loin, dans le paragraphe sur l’évaluation différée.
20.3.4 Le moteur d’exécution d’arrow
: acero
Il y a une différence fondamentale entre manipuler un data.frame
ou un tibble
et manipuler un Arrow Table
. Pour bien la comprendre, il faut d’abord comprendre la distinction entre syntaxe de manipulation des données et moteur d’exécution:
- La syntaxe de manipulation des données sert à décrire les manipulations de données qu’on veut faire (calculer des moyennes, faire des jointures…), indépendamment de la façon dont ces calculs sont effectivement réalisés;
- le moteur d’exécution fait référence à la façon dont les opérations sur les données sont effectivement réalisées en mémoire, indépendamment de la façon dont elles ont été décrites par l’utilisateur.
La grande différence entre manipuler un tibble
et manipuler un Arrow Table
tient au moteur d’exécution: si on manipule un tibble
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. C’est justement parce que le moteur d’exécution d’arrow
est beaucoup plus efficace que celui de dplyr
qu’arrow
est beaucoup plus rapide.
Cette différence de moteurs d’exécution a une conséquence technique importante: une fois que l’utilisateur a défini des instructions avec la syntaxe dplyr
, il est nécessaire que celles-ci soient converties pour que le moteur acero
(écrit en C++ et non en R
) puisse les exécuter. De façon générale, arrow
fait cette conversion de façon automatique et invisible, car le package arrow
contient la traduction C++ de plusieurs centaines de fonctions du tidyverse
. Par exemple, le package arrow
contient la traduction C++ de la fonction filter()
de dplyr
, ce qui fait que les instructions filter()
écrites en syntaxe tidyverse
sont converties de façon automatique et invisible en des instructions C++ équivalentes. La liste des fonctions du tidyverse supportées par acero
est disponible sur cette page. Il arrive toutefois qu’on veuille utiliser une fonction non supportée par acero
. Cette situation est décrite dans le paragraphe “Comment utiliser une fonction non supportée par acero
”.
20.3.5 L’évaluation différée avec arrow
(lazy evaluation)
Une caractéristique importante d’arrow
est qu’il pratique l’évaluation différée (lazy evaluation): les calculs ne sont effectivement réalisés que lorsqu’ils sont nécessaires. En pratique, 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 deux fonctions pour déclencher l’évaluation d’un traitement arrow
: collect()
et compute()
. Il n’y a qu’une seule différence entre collect()
et compute()
, mais elle est importante: collect()
renvoie le résultat du traitement sous la forme d’un tibble
, tandis que compute()
le renvoie sous la forme d’un Arrow Table
.
L’évaluation différée permet d’améliorer les performances en évitant le calcul de résultats intermédiaires inutiles, et en optimisant les requêtes pour utiliser le minimum de données et le minimum de ressources. L’exemple suivant illustre l’intérêt de l’évaluation différée dans un cas simple.
Dans cet exemple, on procède à un traitement en deux temps: on compte les équipements par département, puis on filtre sur le département. Il est important de souligner que la première étape ne réalise aucun calcul par elle-même, car elle ne comprend ni collect()
ni compute()
. L’objet equipements_par_departement
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 bpe_ens_2018_arrow
.
On pourrait penser que, lorsqu’on exécute l’ensemble de ce traitement, arrow
se contente d’exécuter les instructions les unes après les autres: compter les équipements par département, puis conserver uniquement le département 59. Mais en réalité arrow
fait beaucoup mieux que cela: arrow
analyse la requête avant de l’exécuter, et optimise le traitement pour minimiser le travail. Dans le cas présent, arrow
repère que la requête ne porte en fait que sur le département 59, et commence donc par filtrer les données sur le département avant de compter les équipements, de façon à ne conserver que le minimum de données nécessaires et à ne réaliser que le minimum de calculs. Ce type d’optimisation s’avère très utile quand les données à traiter sont très volumineuses.
20.5 Notions avancées
20.5.1 Connaître les limites d’arrow
Le projet arrow
est relativement récent et en développement actif. Il n’est donc pas surprenant qu’il y ait parfois des bugs, et que certaines fonctions standards de R
ne soient pas encore disponibles en arrow
. Il est important de connaître les quelques limites d’arrow
pour savoir comment les contourner. Voici quatre limites d’arrow
à la date de rédaction de cette fiche (janvier 2024):
les jointures de tables volumineuses:
arrow
ne parvient pas à joindre des tables de données très volumineuses; il est préférable d’utiliserduckdb
pour ce type d’opération;les réorganisations de données (wide-to-long et long-to-wide): il n’existe pas à ce jour dans
arrow
de fonctions pour réorganiser une table de données (commepivot_wider
etpivot_longer
du packagetidyr
).-
les fonctions fenêtre (window functions):
arrow
ne permet pas d’ajouter directement à une table des informations issues d’une agrégation par groupe de la même table. Par exemple,arrow
ne peut pas ajouter directement à la base permanente des équipements une colonne égale au nombre total d’équipements du département: -
les empilements de tables: il est facile d’empiler plusieurs
tibbles
avecdplyr
grâce à la fonctionbind_rows()
:bind_rows(table1, table2, table3, table4)
. En revanche, il n’existe pas à ce jour de fonction similaire dansarrow
. Les fonctionsunion
etunion_all
permettent d’empiler seulement deuxArrow Table
, donc pour empiler plusieursArrow Tables
il faut appeler plusieurs fois ces fonctions. Par ailleurs, les deuxArrow Table
doivent être parfaitement compatibles pour être empilés (il faut le même nombre de colonnes avec le même nom et le même type, ce qui n’est pas toujours le cas en pratique).
20.5.2 Surmonter le problème des fonctions non supportées par acero
Lorsqu’on manipule des données avec arrow
, il arrive fréquemment qu’on écrive un traitement que le moteur d’exécution acero
n’arrive pas à exécuter. En ce cas, R
renonce à manipuler les données sous forme de Arrow Table
avec le moteur acero
, convertit les données en tibble
et poursuit le traitement avec le moteur d’exécution de dplyr
(comme un traitement dplyr
standard). R
signale systématiquement le recours à cette solution de repli par un message d’erreur qui se termine par pulling data into R
.
Le recours à cette solution de repli a pour conséquence de dégrader fortement les performances (car le moteur de dplyr
est moins efficace qu’acero
). Il est donc préférable d’essayer de réécrire la partie du traitement qui pose problème avec des fonctions supportées par acero
. Cela est particulièrement recommandé si les données manipulées sont volumineuses ou si le traitement concerné doit être exécuté fréquemment.
Il arrive qu’il soit impossible de trouver une solution entièrement supportée par acero
, ou que la solution soit vraiment trop complexe à écrire. Ce n’est pas une catastrophe: en dernier recours, on peut tout à fait convertir temporairement les données en tibble
(avec collect()
) et exécuter le traitement qui pose problème avec dplyr
. Le traitement sera simplement plus lent (voire beaucoup plus lent). En revanche, il est important de reconvertir ensuite les données en Arrow Table
le plus vite possible, en utilisant la fonction as_arrow_table()
.
20.5.2.1 Une solution simple existe-t-elle?
Dans la plupart des cas, il est possible de trouver une solution simple pour écrire un traitement que le moteur acero
peut exécuter. Voici quelques pistes:
- Vérifier qu’on utilise la dernière version d’
arrow
et mettre à jour le package si ce n’est pas le cas; - Étudier en détail le message d’erreur renvoyé par
R
pour bien comprendre d’où vient le problème; - Regarder la liste des fonctions du tidyverse supportées par
acero
pour voir s’il est possible d’utiliser une fonction supportée paracero
; - Faire des tests pour pour voir si une réécriture mineure du traitement peut régler le problème.
L’exemple qui suit montre que la solution peut être très simple, même lorsque l’erreur semble complexe. Dans cet exemple, on veut calculer le nombre de boulangeries (TYPEQU == "B203"
) et de poissonneries (TYPEQU == "B206"
) dans chaque département, en stockant les résultats dans un Arrow Table
(avec compute()
). Malheureusement, acero
ne parvient pas à réaliser ce traitement, et R
est contraint de convertir les données en tibble
.
Le message d’erreur renvoyé par R
est la suivante: ! NotImplemented: Function 'multiply_checked' has no kernel matching input types (double, bool); pulling data into R
. En lisant attentivement le message d’erreur et en le rapprochant du traitement, on finit par comprendre que 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
:
resultats <- bpe_ens_2018_arrow |>
group_by(DEP) |>
summarise(
nb_boulangeries = sum(NB_EQUIP * as.integer(TYPEQU == "B203")),
nb_poissonneries = sum(NB_EQUIP * as.integer(TYPEQU == "B206"))
) |>
compute()
20.5.2.2 Passer par duckdb
Il arrive qu’il ne soit pas possible de résoudre le problème en réécrivant légèrement le traitement. Une autre solution peut consister à passer par duckdb
, qui permet de manipuler directement des objets Arrow Table
de façon simple et transparente. Dans l’exemple suivant, on veut ajouter à la base permanente des équipements une colonne égale au nombre total d’équipements du département. Cette opération ne peut pas être exécutée par arrow
(voir le paragraphe Section 20.5.1), contrairement à duckdb
. Voici comment faire avec duckdb
:
Cet exemple appelle trois commentaires:
- La fonction
to_duckdb()
sert à ce queduckdb
puisse accéder à l’objetArrow Table
; - Symétriquement, la fonction
to_arrow()
sert à remettre les données dans un objetArrow Table
; - Les instructions figurant entre ces deux étapes (le
group_by()
puis lemutate()
) sont exécutées par le moteur d’exécution deduckdb
, de façon complètement transparente pour l’utilisateur.
20.5.2.3 Définir soi-même des fonctions arrow
(utilisation avancée)
Si les pistes mentionnées précédemment ne fournissent pas de solution simple, il est possible d’aller plus loin et d’écrire ses propres fonctions arrow
. Cette approche permet de faire beaucoup plus de choses mais elle nécessite de bien comprendre le fonctionnement d’arrow
et les fonctions internes de la librairie libarrow
. Il s’agit d’une utilisation avancée d’arrow
qui dépasse le cadre de la documentation utilitR
. Les lecteurs intéressés pourront consulter les deux ressources suivantes:
-
un post de blog qui décrit en détail les liens entre
libarrow
etR
(en anglais); - la partie du
Apache Arrow R Cookbook
qui porte sur lesArrow functions
.
20.6 Un exemple de traitement de données avec arrow
Le code ci-dessous propose un exemple de traitement de données avec arrow
qui suit les recommandations et conseils de la présente fiche. Vous pouvez le copier-coller et vous en inspirer pour construire vos propres traitements!
library(arrow)
library(dplyr)
# Autoriser arrow à utiliser plusieurs processeurs en parallèle
options(arrow.use_threads = TRUE)
# Définir le nombre de processeurs qu'arrow peut utiliser - ici on prend la partie entière du nombre de processeurs disponibles divisé par 4
arrow::set_cpu_count(parallel::detectCores() %/% 4)
##################
### Se connecter aux données
### Conseil: utiliser open_dataset() plutôt que read_parquet()
##################
# Cas 1: se connecter à un unique fichier Parquet
dataset1 <- open_dataset("mon/beau/dossier/dataset1.parquet")
# Cas 2: se connecter à un fichier Parquet partitionné par la variable DEP
dataset2 <- open_dataset(
"mon/beau/dossier/dataset2/",
partitioning = schema(
DEP = utf8()
)
)
##################
### Faire les traitements
### Conseils:
### - Faire des étapes de traitement de 30-40 lignes, suivies d'un compute()
### - Ne pas utiliser collect() dans les calculs intermédiaires sur des données volumineuses
### - Faire attention à suivre la consommation de RAM
##################
# Une première étape de traitement portant sur le dataset1
table_intermediaire1 <- dataset1 |>
select(...) |>
filter(...) |>
mutate(...) |>
compute()
# Une première étape de traitement portant sur le dataset2
table_intermediaire2 <- dataset2 |>
select(...) |>
filter(...) |>
mutate(...) |>
compute()
# Et encore beaucoup d'autres étapes de traitement
# avec beaucoup d'instructions...
# La dernière étape du traitement
resultat_final <- table_intermediaire8 |>
left_join(
table_intermediaire9,
by = "identifiant"
) |>
compute()
##################
### Visualiser un extrait d'une table intermédiaire
### Vous pouvez utiliser collect() sur un extrait des données
##################
extrait_table2 <- table_intermediaire2 |> slice_head(n = 1000) |> collect()
View(extrait_table2)
##################
### Visualiser les résultats finaux sous forme de tibble
### Vous pouvez utiliser collect() sur de petites données
##################
resultat_final_tbl <- resultat_final |> collect()
##################
### Exporter les résultats
### Conseil: partitionner les fichiers Parquet si les données sont volumineuses
##################
# Cas 1: exporter les résultats sous la forme d'un unique fichier Parquet
write_parquet(resultat_final, "mon/dossier/de/sortie/resultat_final.parquet")
# Cas 2: exporter les résultats sous la forme d'un fichier Parquet par les variables DEP et annee
write_dataset(
resultat_final,
"mon/dossier/de/sortie/resultat_final/",
format= "parquet",
hive_style = TRUE,
partitioning = c("DEP", "annee")
)
20.7 Pour en savoir plus
- la documentation officielle du package
arrow
(en anglais); -
un post de blog qui décrit en détail les liens entre
libarrow
etR
(en anglais); - la liste des fonctions du tidyverse supportées par
acero
.
20.4 Comment bien utiliser
arrow
?Au premier abord, on peut avoir l’impression qu’
arrow
s’utilise exactement commedplyr
(c’est d’ailleurs fait exprès!). Il y a toutefois quelques différences qui peuvent avoir un impact considérable sur les performances des traitements. Cette partie détaille quatre recommandations à suivre pour bien utiliserarrow
:compute()
plutôt quecollect()
;open_dataset()
plutôt queread_parquet()
;R
.20.4.1 Savoir bien utiliser l’évaluation différée
La Section 21.5.2 a présenté la notion d’évaluation différée et son intérêt pour optimiser les performances. Toutefois, l’évaluation différée n’est pas toujours facile à utiliser, et présente des limites qu’il faut bien comprendre. Cette section décrit plus en détail le fonctionnement de l’évaluation différée et ses limites. Pour illustrer ce fonctionnement, on commence par exporter la base permanente des équipements sous la forme d’un dataset Arrow partitionné. La fiche Importer des fichiers Parquet décrit en détail ce qu’est un fichier Parquet partitionné et comment le manipuler.
20.4.1.1 Comment fonctionne l’évaluation différée?
Ce paragraphe s’adresse aux lecteurs qui souhaitent comprendre plus en détail le fonctionnement de l’évaluation différée. Les lecteurs pressés peuvent passer directement au paragraphe suivant, sur les limites de l’évaluation différée.
Le traitement suivant est un exemple simple d’utilisation de l’évaluation différée. Ce traitement comprend trois étapes: se connecter aux données avec
open_dataset()
, puis calculer le nombre d’équipements par département, et enfin sélectionner le département 59.Voici quelques commentaires pour comprendre ce traitement:
collect()
nicompute()
. Il faut exécuterresultats |> collect()
ouresultats |> compute()
pour que les calculs soient effectivement réalisés.ds_bpe2018
,eq_dep
etresultats
ne sont pas des tablesR
standards contenant des données: ce sont des requêtes (de classearrow_dplyr_query
), qui décrivent des opérations à mener sur des données. C’est justement en utilisantcollect()
oucompute()
qu’on demande àarrow
d’exécuter ces requêtes avec le moteuracero
.show_exec_plan()
.La première requête est très courte: elle ne contient que la description des données contenues dans le fichier Parquet partitionné.
La deuxième requête est un peu plus longue, et si on regarde en détail, on constate deux choses. Premièrement, elle contient la première requête, mais elle n’a conservé que les variables utilisées dans le traitement (
NB_EQUIP
etDEP
). C’est un exemple d’optimisation faite pararrow
: le moteuracero
a compris automatiquement qu’il suffisait de charger seulement deux variables pour réaliser le traitement. Deuxièmement, on retrouve tous les éléments du traitement (notamment legroup_by
et la somme), mais le traitement décrit en syntaxetidyverse
a été traduit automatiquement en fonctions internes d’arrow
(la fonctionsum
est par exemple remplacée parhash_sum
).Enfin, la troisième requête est encore plus longue, et contient les deux premières. Autrement dit, elle contient l’intégralité du traitement, donc on réalise l’intégralité du traitement lorsqu’on exécute cette requête avec
resultats |> collect()
.20.4.1.2 Quelles sont les limites de l’évaluation différée?
L’évaluation différée optimise les performances en minimisant la quantité de données chargées en RAM et la quantité de calculs effectivement réalisés. Avec cette vision en tête, on pourrait penser que la meilleure façon d’utiliser
arrow
est d’écrire un traitement entier en mode lazy (autrement dit, sans aucuncompute()
ni aucuncollect()
dans les étapes intermédiaires), et faire un uniquecompute()
oucollect()
tout à la fin du traitement, pour que toutes les opérations soient optimisées en une seule étape. Un traitement idéal ressemblerait alors à ceci:La réalité n’est malheureusement pas si simple, car l’évaluation différée a des limites. En effet, au moment de produire le résultat final de l’exemple précédent, la fonction
compute()
donne l’instruction au moteuracero
d’analyser puis d’exécuter l’intégralité du traitement en une seule fois (le paragraphe précédent donne un exemple détaillé). Or, le moteuracero
est certes puissant, mais il a ses limites et ne peut pas exécuter en une seule fois des traitements vraiment trop complexes. Par exemple,acero
rencontre des difficultés lorsqu’on enchaîne de multiples jointures de tables volumineuses.Ces limites de l’évaluation différée peuvent provoquer des bugs violents. Lorsque le moteur
acero
échoue à exécuter une requête trop complexe, les conséquences sont brutales:R
n’imprime aucun message d’erreur, la sessionR
plante et il faut simplement redémarrerR
et tout recommencer. Il est donc nécessaire de bien structurer le traitement pour profiter des avantages de l’évaluation différée sans en toucher les limites.20.4.1.3 Décomposer le traitement en étapes cohérentes, puis le tester
La solution évidente pour ne pas toucher les limites de l’évaluation différée consiste à décomposer le traitement en étapes, et à exécuter chaque étape séparément, en mettant un
compute()
. De cette façon,acero
va réaliser séquentiellement plusieurs traitements un peu complexes, plutôt qu’échouer à réaliser un seul traitement très complexe en une seule fois.La vraie difficulté consiste à savoir quelle est la bonne longueur de ces étapes intermédiaires: s’il faut éviter de faire de très longues étapes (sinon l’évaluation différée plante), il faut également éviter d’exécuter une à une les étapes du traitement (sinon on perd les avantages de l’évaluation différée). Il n’y a pas de solution miracle, et seule la pratique permet de déterminer ce qui est raisonnable. Voici toutefois quelques conseils de bon sens:
compute()
: le retraitement de la première table, le retraitement de la seconde table, et la jointure.filter()
et desselect()
, il est possible d’en enchaîner un certain nombre en une seule étape de traitement sans aucun problème, car ces opérations sont simples. Inversement, une étape de traitement ne doit pas comprendre plus de trois ou quatre jointures (car les jointures sont des opérations complexes), en particulier si les tables sont volumineuses.20.4.2 Lire des fichiers Parquet avec
open_dataset()
plutôt queread_parquet()
Il est recommandé d’utiliser systématiquement la fonction
open_dataset()
plutôt que la fonctionread_parquet()
pour accéder à des données stockées en format Parquet. En effet, la fonctionopen_dataset()
présente deux avantages:open_dataset()
crée une connexion au fichier Parquet, mais elle n’importe pas les données contenues dans le fichier tant que l’utilisateur ne le demande pas aveccompute()
oucollect()
, et elle est optimisée pour importer uniquement les données nécessaires au traitement. Inversement, la fonctionread_parquet()
importe immédiatement dansR
toutes les données du fichier Parquet, y compris des données qui ne servent pas à la suite du traitement.open_dataset()
peut se connecter à un fichier Parquet unique, mais aussi à des fichiers Parquet partitionnés, tandis queread_parquet()
ne peut pas lire ces derniers.20.4.3 Utiliser des objets
Arrow Table
plutôt que desdata.frames
Lorsqu’on manipule des données volumineuses, il est essentiel de manipuler uniquement des objets
Arrow Table
, plutôt que desdata.frames
(ou destibbles
). Cela implique deux recommandations:Importer les données directement dans des
Arrow Table
, ou à défaut convertir enArrow Table
avec la fonctionas_arrow_table()
. Par exemple, lorsqu’on importe un fichier Parquet avec la fonctionread_parquet()
ou un fichier csv avec la fonctionread_csv_arrow()
, il est recommandé d’utiliser l’optionas_data_frame = FALSE
pour que les données soient importées sous forme deArrow Table
.Utiliser systématiquement
compute()
plutôt quecollect()
dans les étapes de calcul intermédiaires. Cette recommandation est particulièrement importante.L’exemple suivant explique pourquoi il est préférable d’utiliser
compute()
dans les étapes intermédiaires:Situation à éviter
La première étape de traitement est déclenchée par
collect()
, la table intermédiaireres_etape1
est donc untibble
. C’est le moteur d’exécution dedplyr
qui est utilisé pour manipulerres_etape1
lors de la seconde étape, ce qui dégrade fortement les performances sur données volumineuses.Usage recommandé
La première étape de traitement est déclenchée par
compute()
, la table intermédiaireres_etape1
est donc unArrow Table
. C’est le moteur d’exécutionacero
qui est utilisé pour manipulerres_etape1
lors de la seconde étape, ce qui assure de bonnes performances notamment sur données volumineuses.Si vous ne savez plus si une table de données est un
Arrow Table
ou untibble
, il suffit d’exécuterclass(le_nom_de_ma_table)
. Si la table est unArrow Table
, vous obtiendrez ceci:"Table" "ArrowTabular" "ArrowObject" "R6"
. Si elle est untibble
, vous obtiendrez"tbl_df" "tbl" "data.frame"
.20.4.4 Surveiller la consommation de RAM de
R
Comme expliqué plus haut, les
Arrow Table
ne sont pas des objetsR
standards, mais des objets C++ qui peuvent être manipulés avecR
viaarrow
. En pratique, cela signifie queR
n’a qu’un contrôle partiel sur la RAM occupé pararrow
, et ne parvient pas toujours à libérer la RAM qu’arrow
a utilisée temporairement pour réaliser un traitement. En particulier, la fonctiongc()
ne permet pas de libérer la RAM qu’arrow
a utilisée temporairement. Cette imperfection de la gestion de la RAM implique deux choses:R
) ;R
. En pratique, redémarrer la sessionR
ne fait pas perdre plus de quelques minutes, car grâce àarrow
et Parquet le chargement des données est très rapide.