1 Ressources à contrôler : temps d’exécution et mémoire
1.1 Introduction
Ressources à contrôler : temps d’exécution et mémoire
Formation données volumineuses
Les données volumineuses ont un impact sur l’utilisation des ressources. Deux principales ressources doivent être contrôlées lors de la manipulation de données volumineuses :
Le temps d’exécution du code
La mémoire utilisée
En pratique, comment évaluer les ressources utilisées ?
1.2 Initialisation
Ressources à contrôler : temps d’exécution et mémoire
Ressources à contrôler : temps d’exécution et mémoire
Formation données volumineuses
Un temps d’exécution important peut être pénalisant à plusieurs niveaux :
Lors de la production du code, il est particulièrement pénible et inefficace de devoir attendre plusieurs minutes avant de pouvoir vérifier le résultat du code produit
Même une fois le code produit, il est préférable d’avoir un code s’exécutant rapidement pour être plus efficace dans les tâches dépendant du code en question
1.4 Mesure avec Sys.time
Ressources à contrôler : temps d’exécution et mémoire
Formation données volumineuses
Pour mesurer le temps d’exécution, il est possible d’utiliser Sys.time (fonction native à R) :
# Récupération de l'heure de débutdebut<-Sys.time()# Algorithme à mesurerinvisible(sqrt(1:1e6))# invisible permet de ne pas afficher le résultat# Récupération de l'heure de finfin<-Sys.time()# Calcul du temps écoulétemps_ecoule<-fin-debuttemps_ecoule
Ressources à contrôler : temps d’exécution et mémoire
Formation données volumineuses
On préfèrera utiliser les fonctions de benchmark comme la fonction mark disponible à travers le package bench. La fonction mark permet de comparer précisément une ou plusieurs expressions sur plusieurs itérations avec différents indicateurs. Elle s’utilise comme suit :
Ecrire un code permettant d’améliorer le temps d’exécution de l’algorithme
Chiffrer ce gain de temps
1.7 Correction 1
Ressources à contrôler : temps d’exécution et mémoire
Formation données volumineuses
# Définir les deux expressions à comparerexpression_avec_boucle<-quote({code_com_rs<-c()for(xinnaissances1019[["code_com"]]){code_com_r<-substr(x, 1, 2)code_com_rs<-c(code_com_rs, code_com_r)}naissances1019$code_com_reduit<-code_com_rs})expression_avec_substr<-quote({naissances1019$code_com_reduit<-substr(naissances1019$code_com, 1, 2)})# Comparer les performances avec bench::markresultats<-bench::mark( boucle =eval(expression_avec_boucle), substr =eval(expression_avec_substr))# Afficher les noms des expressions dans le tibble de résultatsresultats$expression<-c("boucle", "substr")# Afficher les résultatsprint(resultats)
# A tibble: 2 × 13
expression min median `itr/sec` mem_alloc `gc/sec` n_itr n_gc total_time
<chr> <bch:tm> <bch:> <dbl> <bch:byt> <dbl> <int> <dbl> <bch:tm>
1 boucle 4.5s 4.5s 0.222 4.53GB 23.8 1 107 4.5s
2 substr 1.52ms 1.77ms 477. 272.48KB 2.00 239 1 500.5ms
# ℹ 4 more variables: result <list>, memory <list>, time <list>, gc <list>
1.8 Profiling
Ressources à contrôler : temps d’exécution et mémoire
Formation données volumineuses
La méthode dite du profiling permet d’identifier plus facilement les parties du code coûteuses en temps d’exécution.
Pour cela, on peut utiliser la fonction profvis afin d’identifier les parties lentes d’un code. Elle crée un widget qui va recenser chaque appel de fonction, et chaque étape du code. Cette fonction permet également de connaître la quantité de mémoire utilisée. L’analyse permet de décomposer les appels imbriqués lorsqu’une fonction en appelle d’autres ce qui permet d’avoir une vision macro du profiling et également d’aller plus dans le détail.
Afin de profiler un code, il suffit d’évaluer une expression à l’intérieur de profvis (comme pour mark, il est essentiel que l’évaluation se fasse au moment de l’appel de profvis).
On profile du point de vue de R : l’appel de programmes externes type DuckDB apparaîtra comme une boîte noire.
1.9 Question 2
Ressources à contrôler : temps d’exécution et mémoire
Formation données volumineuses
Profiler la fonction filtrer_moins_100 sur naissances1019 :
# Fonction rowSums à laquelle on ajoute une attente de 1 seconde pour allonger artificiellement l'exécutionlong_sum<-function(..., na.rm=TRUE){Sys.sleep(0.0001)return(sum(..., na.rm =na.rm))}# df désigne ici le dataframe contenant les données# cette fonction retourne le dataframe df privé des lignes pour lesquelles la somme des naissances est inférieure à 100# et trie le dataframe selon le nombre de naissancesfiltrer_moins_100<-function(df){df_moins_100<-df%>%group_by(code_com, an)%>%summarise(nb_naissances =long_sum(naiss))%>%filter(nb_naissances<100)%>%arrange(nb_naissances)return(df_moins_100)}
1.10 Correction 2
Ressources à contrôler : temps d’exécution et mémoire
Formation données volumineuses
profvis::profvis({naissances_moins_100_profilage<-naissances1019%>%filtrer_moins_100(.)})# On constate ici que l'appel de la fonction Sys.sleep correspond à la quasi totalité du temps d'exécution.
1.11 Mémoire 1/3
Ressources à contrôler : temps d’exécution et mémoire
Formation données volumineuses
La mémoire est une ressource essentielle en programmation. Dans ce contexte, la mémoire fait référence à la RAM (Random Access Memory), ou mémoire vive. On la distingue de la mémoire interne (ou disque dur ou disque SSD) dont le stockage est plus pérenne mais l’accès plus lent.
Pour mesurer la mémoire allouée à un objet, il est possible d’utiliser la fonction object.size de la librairie pryr, qui donne le nombre d’octets (de bytes) occupés par cet objet dans la mémoire :
## Espace occupé par une séquencepryr::object_size(c(1, 3, 4, 5, 6))
96 B
pryr::object_size(c(1, NA, 4 , NA , 3))# les NA prennent la même place qu'un nombre !!!
96 B
## Espace occupé par une fonction native de Rpryr::object_size(mean)
1.13 kB
## Espace occupé par une base de données# Cette base vient avec l'installation de RStudiopryr::object_size(mtcars)
7.21 kB
1.12 Mémoire 2/3
Ressources à contrôler : temps d’exécution et mémoire
Formation données volumineuses
Cette fonction est plus précise que la fonction native object.size car elle tient compte des éléments partagés au sein d’un objet, entre autres. Autrement dit, la mémoire occupée par plusieurs objets n’est pas forcément égale à la somme de la mémoire occupée par chacun des objets.
Voici une démonstration :
x1<-rep(1, 1e6)y1<-list(x1, x1, x1, x1)# mémoire occupée estimée par la fonction native `object.size()`object.size(x1)
8000048 bytes
object.size(y1)# La mémoire occupée estimée est 4 fois celle de x1
32000272 bytes
# mémoire occupée estimée par la fonction `pryr::object_size()`pryr::object_size(x1)
8.00 MB
pryr::object_size(y1)# L'objet `y` contient 4 fois une référence qui pointe vers le même objet `x`.
8.00 MB
1.13 Mémoire 3/3
Ressources à contrôler : temps d’exécution et mémoire
Formation données volumineuses
Cependant, si nous initialisons des objets de façon indépendante, object_size calculera une allocation de mémoire forcément supérieure :
Des fois le méchanisme de lazy evaluation conduit à des résultats surprenants :
x3<-1:1e6pryr::object_size(x3)# très léger car lazy evaluation
680 B
y3<-2*x3pryr::object_size(x3)# faire fois deux force l'évaluation
4.00 MB
2 Méthodes d’optimisation
2.1 Introduction
Méthodes d’optimisation
Formation données volumineuses
La partie précédente nous a permis de comprendre quelles sont les ressources à contrôler lors de la production d’un code (temps d’exécution et mémoire) et comment mesurer l’utilisation de ces ressources. Nous allons dans cette partie présenter plusieurs méthodes permettant d’optimiser l’utilisation de ces ressources : l’utilisation du garbage collector, la parallélisation et le calcul distribué.
2.2 Garbage collection 1/2
Méthodes d’optimisation
Formation données volumineuses
Supposons par exemple que nous ayons créé ces objets :
A<-0:1000B<-0:9999
Après l’exécution de ces lignes de code, les objets A et B deviennent accessibles dans l’environnement global. Cependant, nous observons une diminution de la performance de notre code après la création de ces objets. Cette dégradation est dû à leur utilisation importante de la mémoire.
On pourrait alors utiliser rm(list = ls())
Il se peut que la mémoire soit encore occupée, car la fonction rm a seulement supprimé tous les objets de données de l’espace de travail, mais elle n’a pas libéré la mémoire R.
2.3 Garbage collection 2/2
Méthodes d’optimisation
Formation données volumineuses
R s’occupe automatiquement d’allouer et de libérer la mémoire mais il se peut qu’il ne le fasse pas immédiatement. On peut utiliser la fonction gc (garbage collection) pour demander manuellement la libération de la mémoire inutilisée.
La fonction gc peut être appelée à tout moment pour déclencher une collecte des déchets.
Cela peut être utile dans des scénarios où vous souhaitez libérer la mémoire à un moment précis de l’exécution du programme.
2.4 Parallélisation 1/2
Méthodes d’optimisation
Formation données volumineuses
Le calcul parallèle est une forme de calcul qui permet d’effectuer plusieurs opérations simultanément en utilisant de nombreux processeurs.
R est, par défaut, monothread : il n’utilise qu’un processeur. Certains paquets lancent du calcul parallèle par eux-mêmes (data.table, duckdb…). On discute ici de comment paralléliser à la main.
Le calcul parallèle consiste en trois étapes :
une séparation d’un problème en plusieurs sous-problèmes à résoudre
la résolution simultanée des différents sous-problèmes par les processeurs
l’agrégation des résultats obtenus par les processeurs
2.5 Parallélisation 2/2
Méthodes d’optimisation
Formation données volumineuses
Cette méthode de calcul a été utilisée avec succès dans de nombreux domaines, tel que la génération de mots de passe. En effet, si l’utilisateur doit rechercher les combinaisons possibles de mots de passe commençant par les lettres A,B ou C, il peut créer trois sous-problèmes, un par lettre, afin de les répartir entre trois processeurs.
Systèmes d’exploitation
Certaines fonctions de ces packages ne sont pas disponibles pour tout les systèmes d’exploitations (notamment Windows). Ainsi, si vous souhaitez éxecuter du code en local puis sur une machine virtuelle du SSPCloud, il est possible que vous n’ayez pas les mêmes erreurs.
NB : les fonctions parallel::mclapply et doParallel::foreach fonctionnent sur les systèmes d’exploitations Linux et Windows, ce qui n’est pas le cas de la fonction parallel::parLapply.
2.6 Les packages future et future.apply
Méthodes d’optimisation
Formation données volumineuses
Le traitement parallèle peut être exécuté à l’aide des librairies future et future.apply. Après l’import des librairies, il suffit de préciser le mode de traitement souhaité à l’aide de la fonction plan :
# install.packages("future")# install.packages("future.apply")library(future.apply)future::plan(sequential)# Traitement séquentiel "classique" des donnéesfuture::plan(multisession, .cleanup =TRUE)# Traitement en parallèle dans d'autres sessions R en arrière-planfuture::availableCores()# Pour déterminer le nombre total de sessions R utilisées en arrière-plan.
Un fois le calcul parallèle effectué, il ne faut pas oublier de revenir en traitement séquentiel avec future::plan(sequential).
2.7 Exemple
# Définir la fonction de calculcalc_sum_of_squares<-function(n){return(sum((1:n)^2))}# Créer un vecteur de valeurs nn_values<-1:10000# Approche séquentielleresults_sequential<-quote({lapply(n_values, FUN =calc_sum_of_squares)})# Approche parallèle avec future.applyresults_parallel<-quote({future.apply::future_lapply(n_values, FUN =calc_sum_of_squares)})# Comparer les temps d'exécutionresultats<-bench::mark( sequentiel =eval(results_sequential), parrallel =eval(results_parallel), check =FALSE)# Afficher les noms des expressions dans le tibble de résultatsresultats$expression<-c("séquentiel", "parallèle")# Afficher les résultatsprint(resultats)
# A tibble: 2 × 13
expression min median `itr/sec` mem_alloc `gc/sec` n_itr n_gc total_time
<chr> <bch:tm> <bch:> <dbl> <bch:byt> <dbl> <int> <dbl> <bch:tm>
1 séquentiel 349ms 397ms 2.52 573MB 17.6 2 14 795ms
2 parallèle 273ms 358ms 2.79 589MB 12.6 2 9 717ms
# ℹ 4 more variables: result <list>, memory <list>, time <list>, gc <list>
2.8 Le package parallel
Méthodes d’optimisation
Formation données volumineuses
La fonction mclapply permet simplement de remplacer la fonction lapply par une version parallélisée :
Cependant, il arrive que les coûts de distribution de calculs sur plusieurs cœurs soient plus importants que le temps de calcul non parallélisé et qu’il ne soit pas pertinent d’y avoir recours
2.9 Le package doParallel 1/2
Le package foreach issu de doParallel propose également des fonctions pour l’implémentation simple de boucles parallélisées.
Notamment, la fonction foreach() a un comportement similaire à une boucle for classique, mais elle est parallélisée et retourne un résultat. On utilise %dopar% pour spécifier que l’on souhaite que la boucle soit parallélisée. Autrement, on peut le remplacer par l’itérateur %do%.
Il est également possible de créer un “cluster” pour paramétrer le backend parallèle et choisir le nombre de cœurs sur lequel on veut que la fonction soit parallélisée.
Il est possible que des problèmes complexes ne puissent pas être résolus en utilisant un seul ordinateur, même avec l’aide du calcul parallèle. Il est alors possible d’utiliser plusieurs machines regroupées au sein d’un même système : un cluster. Chaque ordinateur d’un système distribué est appelé nœud.
Les services de cloud computing permettent généralement de créer des clusters.
Enfin, si vous avez accès à plusieurs machines, vous pouvez essayer de mettre en place un cluster pour les utiliser, par exemple avec ce code :
sparklyr est une librairie open-source qui fournit une interface entre R et Apache Spark. L’utilisateur peut tirer parti des capacités de Spark dans un environnement R moderne, grâce à la capacité de Spark à interagir avec des données distribuées avec une faible latence. sparklyr constitue donc un outil efficace pour interagir avec de grands ensembles de données dans un environnement interactif.
sparklyr vs arrow et duckdb
La plupart du temps, il est recommandé d’utiliser arrow et duckdb, qui seront présentés en séquence 2 car plus simples à utiliser. Cependant, dans des cas de données très volumineuses où arrow et duckdb montrent leurs limites, on peut gagner beaucoup de temps à passer par sparklyr
2.15 La librairie sparklyr 2/3
Méthodes d’optimisation
Formation données volumineuses
Pour installer sparklyr sur l’ordinateur et se connecter à Spark, on utilise le code suivant :
# Installation de la librairie`sparkly\` install.packages("sparklyr")# Import de la librairielibrary(sparklyr)# Installation de spark# spark_install()# Connexion à un cluster Spark à un noeud en localsc<-spark_connect(master ="local")# Deconnexion de Sparkspark_disconnect(sc)
Une fois la connection établie, une icône Spark apparaîtra dans l’onglet ‘Connections’ de RStudio.
2.16 La librairie sparklyr 3/3
Méthodes d’optimisation
Formation données volumineuses
sparklyr permet d’exécuter du code R à l’échelle de votre cluster Spark grâce à la fonction spark_apply.
spark_apply applique une fonction R à un objet Spark (typiquement, un dataframe Spark). Les objets Spark sont partitionnés afin de pouvoir être distribués sur un cluster.
Note
Partitionner un fichier revient, comme son nom l’indique, à le “découper” selon une clé de partitionnement, qui prend la forme d’une ou de plusieurs variables.
Vous pouvez utiliser spark_apply avec les partitions par défaut ou vous pouvez définir vos propres partitions avec l’argument group_by. Votre fonction R doit retourner un autre Spark dataframe. spark_apply exécutera votre fonction R sur chaque partition et retournera un seul Spark dataframe.
3 Quiz
3.1 Question 1
Quiz
Formation données volumineuses
Qu’est-ce qu’un garbage collection ?
A) Le garbage collection est un processus utilisé pour libérer manuellement la mémoire occupée par des objets inutilisés.
B) Le garbage collection est un processus utilisé pour collecter automatiquement la mémoire occupée par des objets supprimés.
C) Le garbage collection est un processus utilisé pour libérer automatiquement la mémoire occupée par des objets inutilisés.
3.2 Correction 1
Quiz
Formation données volumineuses
La réponse correcte est la réponse :
A) Le garbage collection est un processus utilisé pour libérer manuellement la mémoire occupée par des objets inutilisés.
3.3 Question 2
Quiz
Formation données volumineuses
Quelle est la principale différence entre le calcul parallèle et le calcul distribué ?
A) Le calcul parallèle implique l’utilisation de plusieurs processeurs sur la même machine, tandis que le calcul distribué implique l’utilisation de plusieurs machines distinctes.
B) Le calcul distribué utilise des algorithmes parallèles, mais le calcul parallèle ne peut pas être distribué sur plusieurs machines.
C) Le calcul parallèle et le calcul distribué sont des termes interchangeables sans distinction significative.
D) Le calcul distribué est spécifique à la programmation orientée objet, tandis que le calcul parallèle concerne la programmation impérative.
3.4 Correction 2
Quiz
Formation données volumineuses
La réponse correcte est la réponse :
A) Le calcul parallèle implique l’utilisation de plusieurs processeurs sur la même machine, tandis que le calcul distribué implique l’utilisation de plusieurs machines distinctes.
3.5 Question 3
Quiz
Formation données volumineuses
Comment pouvez-vous paralléliser l’application d’une fonction à l’aide de future.apply ?
A) En utilisant la fonction parallel_apply du package future.apply.
B) En enveloppant la fonction à appliquer avec future_lapply.
C) En spécifiant l’option parallel = TRUE dans la fonction apply.
D) En utilisant uniquement les fonctionnalités intégrées de R sans nécessiter de packages supplémentaires.
3.6 Correction 3
Quiz
Formation données volumineuses
La réponse correcte est la réponse :
B) En enveloppant la fonction à appliquer avec future_lapply.
4 Exercices complémentaires
4.1 Question 1
Exercices complémentaires
Formation données volumineuses
Comparer l’efficacité des fonctions suivantes sur la base naissances1019. Quel est leur temps médian d’exécution et leur utilisation de la mémoire ?
length
ncol
4.2 Correction 1
Exercices complémentaires
Formation données volumineuses
On utilise la fonction bench::mark pour calculer le temps d’exécution médian et la mémoire utilisée :
Créer une fonction R qui effectue une analyse statistique simple et prenant en argument un dataframe et un nom de colonne. Par exemple, vous pouvez calculer la moyenne, l’écart-type ou tout autre indicateur pertinent.
4.4 Correction 2
Exercices complémentaires
Formation données volumineuses
# Créez une fonction pour calculer la moyenne d'une colonne spécifiquecalculate_mean<-function(data, column_name){column<-data[[column_name]]return(mean(column, na.rm =TRUE))}
4.5 Question 3
Exercices complémentaires
Formation données volumineuses
Utiliser la parallélisation avec future.apply pour calculer la moyenne (ou autre statistique calculée par la fonction précédente) de différentes colonnes en parallèle. Vous pouvez spécifier plusieurs colonnes à analyser.
4.6 Correction 3
# Spécifiez les noms des colonnes à analysercolumns_to_analyze<-c("an", "naiss")# Utilisez future_lapply pour appliquer la fonction en parallèleparallel_means<-future.apply::future_lapply(columns_to_analyze, FUN =calculate_mean, data =naissances1019)
4.7 Question 4
Exercices complémentaires
Formation données volumineuses
Enfin, afficher les résultats des moyennes calculées en parallèle.
4.8 Correction 4
Exercices complémentaires
Formation données volumineuses
# Récupérez les résultatsresults<-as.data.frame(parallel_means)colnames(results)<-columns_to_analyze# Affichez les moyennes calculéesprint(results)
an naiss
1 2024 18.91919
4.9 Question 5
Exercices complémentaires
Formation données volumineuses
Les deux codes suivants produisent le même résultat : ils retournent tous deux un data.frame contenant uniquement les lignes pour lesquels la somme des naissances est strictement inférieure à 100.
Selon vous, lequel de ces deux codes a le temps d’exécution le plus rapide ? Vérifier cette hypothèse.
filter_code_com_moins_100<-function(x){new_data<-x# Création d'une colonne contenant le nombre de naissances pour chaque code_com/abnew_data=new_data%>%group_by(code_com, an)%>%mutate(total =sum(naiss))return(filter(new_data, total<100)[, 1:ncol(x)])}naissances_moins_100_function<-naissances1019%>%filter(.$code_com%in%filter_code_com_moins_100(.)$code_com)
4.10 Correction 5
Exercices complémentaires
Formation données volumineuses
Le premier code effectue ce que l’on souhaite. Le second utilise une fonction qui va venir filtrer les données via la création d’une colonne total. On fait ensuite appel à la fonction filter pour sélectionner les lignes ayant un code_com appartenant au data.frame retourné par la fonction. Instinctivement, on pense que la plus rapide est la première. Vérifions-le en comparant les temps d’exécution ainsi que la quantité de ressources consommée. Pour cela, nous utilisons la fonction mark du package bench.
4.11 Correction 5
Exercices complémentaires
Formation données volumineuses
# A tibble: 2 × 13
expression min median `itr/sec` mem_alloc `gc/sec` n_itr n_gc total_time
<bch:expr> <bch> <bch:> <dbl> <bch:byt> <dbl> <int> <dbl> <bch:tm>
1 directly 360ms 378ms 2.64 9.11MB 11.9 2 9 756ms
2 with_function 306ms 321ms 3.12 11.7MB 9.35 2 6 642ms
# ℹ 4 more variables: result <list>, memory <list>, time <list>, gc <list>
La fonction bench::mark s’utilise pour comparer entre autres les temps d’exécution (minimum et médian) et l’utilisation des ressources machine pour plusieurs expressions.
Effectivement, le premier code est le plus rapide (il est deux fois plus rapide que le second) et consomme également moins de mémoire. Attention toutefois, ce n’est pas toujours le cas. Lorsqu’on a deux versions d’un même code et que l’on veut conserver la plus adaptée, comparer ces aspects-là est utile. Par exemple, un code qui consommerait moins de ressources mais en étant un peu moins rapide peut tout à fait être le meilleur choix selon le contexte (ressources disponibles, utilisation du code, etc.).
4.12 Question 6
Exercices complémentaires
Formation données volumineuses
Parmi les deux fonctions suivantes, laquelle est la plus rapide ? Pourquoi ? Pouvez-vous le vérifier à l’aide de la fonction bench::mark ?
Bien que le résultat soit surprenant, la fonction mean1 est plus lente que la fonction mean2.
En effet, la fonction mean effectue deux passages sur le vecteur x. Lors du premier passage, elle calcule la somme de tous les éléments de x, et lors du deuxième passage, elle divise cette somme par la longueur totale de x pour obtenir la moyenne. Cette approche a pour objectif d’améliorer la précision numérique.
En revanche, la méthode sum / length effectue le calcul en une seule passe.
5 Approfondissements
5.1 Utilisation de C++ avec R
Approfondissements
Formation données volumineuses
Le but de cette section n’est pas de dispenser un cours détaillé de C++, mais plutôt de :
Présenter les principales similarités et différences entre R et C++ ;
Reconnaître les cas où il est pertinent de recourir à du code C++ pour optimiser son code R ;
Être capable de réemployer des extraits de code RCPP ou C++ et les adapter si besoin.
5.2 Qu’est ce que Rcpp ?
Approfondissements
Formation données volumineuses
Rcpp est un package qui permet l’intégration de code C++ dans R. Il offre notamment une façon simple d’écrire des fonctions C++ qui peuvent être appelées comme des fonctions R.
Zoom : quels sont les différences entre C++ et R ?
C (et donc C++) est un langage compilé alors que R est un langage interprété.
La principale implication de cette différence est que le code C++, étant donné qu’il communique directement avec le CPU (via ce que l’on appelle du code machine), est beaucoup plus rapide que le code R qui passe par plusieurs étapes de vérifications. Cependant, le code C++ est beaucoup plus long à programmer, et il demande d’être beaucoup plus précis. Il est notamment beaucoup plus complexe à débugger. Ainsi, si l’on souhaite intégrer du code C++, il est important de faire très attention à l’objectif de la fonction, aux entrées et sorties.
5.3 Quand avoir recours à Rcpp ? 1/2
Approfondissements
Formation données volumineuses
La plupart des fonctions R base sont codées à partir de fichiers C ou Fortran et sont donc déjà très performantes. En conséquence, le recours à C++ ne doit pas se faire de façon systématique.
On va privilégier son recours uniquement quand on souhaite :
créer un package, qui implique une utilisation importante et fréquente des fonctions, et pour qui la performance est importante
créer un programme que l’on va avoir besoin de exécuter souvent et / ou sur des bases de données volumineuses
Si l’on se retrouve dans l’un de ces deux cas, il faut ensuite analyser si l’utilisation de C++ est pertinente.
5.4 Quand avoir recours à Rcpp ? 2/2
Approfondissements
Formation données volumineuses
Le code C++ va notamment permettre :
une performance plus importante : Le fait d’avoir du code machine permet d’être plus rapide
l’accès aux bibliothèques C++ :Rcpp permet d’utiliser des bibliothèques C++ existantes dans le code R
Les problématiques typiques que C++ permet de résoudre sont les suivantes :
Les boucles qui ne peuvent pas être facilement vectorisées parce que les itérations sont dépendantes entre elles
Les problèmes qui nécessitent des structures de données et des algorithmes avancés que R ne fournit pas :
Les fonctions récursives ou les problèmes qui impliquent d’appeler des fonctions un grand nombre de fois.
5.5 La fonction cppFunction
Approfondissements
Formation données volumineuses
La fonction cppFunction permet de coder directement dans le fichier .R une fonction en C++ qui est ensuite utilisable normalement sur R.
Ci-dessous la comparaison entre le calcul de la distance euclidienne, calculée avec R :
cppFunction("NumericVector pdistC(double x, NumericVector ys) { int n = ys.size(); NumericVector out(n); for(int i = 0; i < n; ++i) { out[i] = sqrt(pow(ys[i] - x, 2.0)); } return out;}")
5.6 Question 1
Approfondissements
Formation données volumineuses
A quelles fonctions R correspondent ces fonctions C++ ?
# Fonction 1cppFunction("double f1(NumericVector x) { int n = x.size(); double y = 0; for(int i = 0; i < n; ++i) { y += x[i] / n; } return y;}")# Fonction 2cppFunction("NumericVector f2(NumericVector x) { int n = x.size(); NumericVector out(n); out[0] = x[0]; for(int i = 1; i < n; ++i) { out[i] = out[i - 1] + x[i]; } return out;}")
5.7 Correction 1
Approfondissements
Formation données volumineuses
# Fonction 1cppFunction("double f1(NumericVector x) { int n = x.size(); double y = 0; for(int i = 0; i < n; ++i) { y += x[i] / n; } return y;}")res_f1C<-f1(c(15, 16, 18, 10, 12, 16, 18, 9))res_meanR<-mean(c(15, 16, 18, 10, 12, 16, 18, 9))print("Egalité des résultats de la fonction 1 en C++ et mean de R :")print(res_f1C==res_meanR)# Fonction 2cppFunction("NumericVector f2(NumericVector x) { int n = x.size(); NumericVector out(n); out[0] = x[0]; for(int i = 1; i < n; ++i) { out[i] = out[i - 1] + x[i]; } return out;}")res_f2C<-f2(c(15, 16, 18, 10, 12, 16, 18, 9))res_cumsumR<-cumsum(c(15, 16, 18, 10, 12, 16, 18, 9))print("Egalité des résultats de la fonction 2 en C++ et cumsum R :")print(res_f2C==res_cumsumR)
5.8 La fonction sourceCpp
Approfondissements
Formation données volumineuses
La fonction sourceCpp permet d’écrire un code C++ dans un fichier .cpp séparé et y faire appel afin d’utiliser les objets créés sur R directement.
Il faut toujours initier le fichier .cpp avec la ligne #include \<Rcpp.h\> using namespace Rcpp; et précéder chaque fonction que l’on veut pouvoir utiliser sur R de la ligne // \[\[Rcpp::export\]\], comme suit :
# include \<Rcpp.h\> using namespace Rcpp;// \[\[Rcpp::export\]\]NumericVector f2(NumericVector x) { int n =x.size(); NumericVector out(n); out[0] = x[0];for(int i =1; i < n; ++i) { out[i] = out[i -1] + x[i]; } return out;}
L’appel au fichier se fait ensuite dans un fichier .R classique :
sourceCpp("path/to/file.cpp")# Ajuster le chemin en fonction de l'emplacement du fichier.
5.9 La fonction evalCpp
Approfondissements
Formation données volumineuses
La fonction evalCpp permet d’évaluer le résultat de l’exécution d’une seule expression C++. Cela en fait un bon outil pour l’expérimentation interactive, notamment avant de se lancer dans la programmation d’une fonction en C++.
7 Les outils pour traiter des données volumineuses (2/2)
7.1 Objectifs principaux
Formation données volumineuses
La séquence précédente a permis de présenter les ressources à optimiser lors de la production de code et en particulier de la manipulation de données volumineuses (temps de calcul et mémoire) et des méthodes pour mesurer l’utilisation de ces ressouces. Elle a permis de présenter deux méthodes de calcul (calcul parallélisé et distribué) utiles dans des contextes où l’utilisation de ces ressources peut être importante, notamment lors de la manipulation de données volumineuses.
Cette séquence présente des structures de données répondant plus spécifiquement aux sujets de stockage et de lecture des données volumineuses : arrow et duckDB. Ces formats de données sont particulièrement adaptés aux données volumineuses, permettant ainsi d’enrichir la palette d’outils mobilisables par les stagiaires pour manipuler efficacement des données volumineuses. De plus, ils sont applicables dans différents langages de programmation, comme R mais aussi Python par exemple.
Le format arrow est un type d’objet manipulable dans R et optimisé pour traiter des données volumineuses. En effet, le format arrow permet de traiter les données hors mémoire : les données ne sont pas chargées dans la mémoire vive, permettant ainsi de résoudre la problématique de mémoire trop limitée face à des données volumineuses.
Le moteur de calcul de arrow a été précurseur mais est aujourd’hui moins complet que celui de duckdb. Cf la fiche UtilitR de comparaison.
8.2 Format Parquet
Le package arrow
Formation données volumineuses
Le format arrow est associé au format de fichier appelé Parquet pour le stockage. Les fichiers Parquet se révèlent très utiles pour stocker des données volumineuses, notamment pour les raisons suivantes :
La taille de ces fichiers est optimisée : ils occupent moins d’espace de stockage
Les fichiers Parquet sont adaptés au partitionnement, méthode permettant de découper un fichier volumineux en plusieurs fichiers moins volumineux
Les types des colonnes sont contenus dans les métadonnées. Ainsi on peut charger un fichier Parquet sans préciser le type de chaque variable.
Le format est opensource donc facilement utilisable par les logiciels.
Format parquet
Le format parquet devient la norme à l’INSEE. Il est donc impératif de maîtriser ce format. cf. Note sur Symphonie
8.3 Ecriture de fichiers Parquet
Le package arrow
Formation données volumineuses
L’exemple suivant illustre l’écriture d’un fichier Parquet avec la fonction write_dataset de la librairie arrow (le fichier utilisé est suffisamment léger pour qu’on puisse écrire les données dans un seul fichier Parquet) :
# Téléchargement du fichier zipdownload.file("https://www.insee.fr/fr/statistiques/fichier/7633685/dpt2022_csv.zip", destfile =file.path(PATH_INPUT, "dpt2022_csv.zip"))# Décompression du fichier zipunzip(file.path(PATH_INPUT, "dpt2022_csv.zip"), exdir =PATH_FINAL)# Lecture du fichier CSVdpt2022<-read.csv(file.path(PATH_FINAL, "dpt2022.csv"), sep =";")# Supprime le fichier s'il existe déjàfilename<-file.path(PATH_FINAL, "dpt2022.parquet")if(file.exists(filename))unlink(filename)# Écriture des données en format Parquetarrow::write_dataset(dpt2022, path =file.path(PATH_FINAL, "dpt2022.parquet"))
8.4 Questions
Le package arrow
Formation données volumineuses
Comparer dans l’explorateur de fichiers la taille de dpt2022.csv et dpt2022.parquet.
Quel autre avantage de ce format remarquez-vous ?
Il existe également le package parquetize qui permet d’écrire et lire des fichiers parquet.
8.5 Partitionnement 1/3
Le package arrow
Formation données volumineuses
Un autre façon d’optimiser les ressources de l’ordinateur s’appelle le partitionnement.
Partitionner un fichier revient, comme son nom l’indique, à le “découper” selon une clé de partitionnement, qui prend la forme d’une ou de plusieurs variables. Comme évoqué dans la fiche UtilitR sur le format Parquet, cela signifie en pratique que l’ensemble des données sera stocké sous forme d’un grand nombre de fichiers Parquet (un fichier par valeur des variables de partitionnement).
Par exemple, il est possible de partitionner un fichier national par département : on obtient alors un fichier Parquet pour chaque département. On peut également partitionner selon plusieurs variables, par exemple le département et le sexe : on obtient alors pour chaque département un fichier par sexe. L’ordre des variables de partitionnement est alors important car le découpage se fait dans l’ordre de ces variables (ici, on a un premier découpage par département et dans chaque partition par département, on partitionne par sexe)
8.6 Partitionnement 2/3
Le package arrow
Formation données volumineuses
Cela permet d’avoir un traitement plus efficace lorsque l’on sait qu’on peut se limiter à une partie des données correspondant à un sous-ensemble des valeurs de la clé de partitionnement, ou bien que l’on peut effectuer les traitements par groupe ce qui permet de distribuer les traitements selon la clé de partitionnement.
Dans d’autres cas, partitionner des données peut faire perdre en performance à cause de la lecture multiple de fichiers que cela implique
Le nom de la variable est ‘dpt’, ce sera donc notre clé de partitionnement. Pour créer des fichiers Parquet partitionnés, on utilise toujours la fonction write_dataset, en précisant cette fois la variable utilisée pour partitionner le fichier initial (dans l’argument partitioning)
8.7 Partitionnement 3/3
Le package arrow
Formation données volumineuses
# Création d'un sous-dossier pour contenir les donnéesdossier_partitions<-file.path(PATH_FINAL, "dpt2022 partitions")if(dir.exists(dossier_partitions)){unlink(dossier_partitions, recursive =TRUE)# Suprression du dossier}dir.create(dossier_partitions)arrow::write_dataset( dataset =dpt2022, path =dossier_partitions, partitioning =c("dpt"), # la variable de partitionnement format ="parquet")
Attention
Si vous enregistrez plusieurs fois dans le même dossier en changeant de partition, de nouvelles partitions seront écrites dans le dossier sans écraser les précédentes et les données se retrouvent alors dupliquées.
NB : Si les clés de partitionnement contiennent des NA, les valeurs manquantes seront considérées comme une valeur de partition en soi comme toute autre valeur
8.8 Requêtes via des fichiers Parquet 1/3
Le package arrow
Formation données volumineuses
Nous allons maintenant lire les fichiers Parquet nouvellement créés et effectuer des requêtes dont le résultat sera converti en dataframe. L’utilisation de cette approche par requêtes permet de traiter les données hors mémoire ce qui représente un avantage considérable face à des données volumineuses :
On commence par utiliser la fonction open_dataset utilisée qui permet de se connecter aux données au format Parquet sans les charger
open_dataset vs read_parquet
Il est important de bien différencier les fonctions qui lancent une utilisation des données immédiate et celles qui la diffèrent. Alors que read_parquet charge immédiatement la base entière en RAM, open_dataset se contente de préparer ce chargement sans l’éxécuter.
8.9 Requêtes via des fichiers Parquet 2/3
Le package arrow
Formation données volumineuses
On peut ensuite définir une requête sur les données en utilisant dplyr (NB : la requête ne renvoie pas de données)
Attention, tous les verbes dplyr ne sont pas reconnus en arrow. Cf. documentation pour la liste des verbes utilisables
On peut enfin récupérer les données sous forme d’un dataframe grâce à la fonction collect de dplyr
Comment ça marche ?
Une fonction dplyr comme filter est une fonction générique qui va renvoyer vers une autre fonction selon la classe de l’objet en entrée. Ainsi pour un data.frame, filter va renvoyer vers dplyr:::filter.data.frame alors que pour un objet issu de open_dataset, il renverra vers arrow:::filter.arrow_dplyr_query.
8.10 Requêtes via des fichiers Parquet 2/2
Le package arrow
Formation données volumineuses
# Connexion au fichier Parquet partitionnédonnees_dpt22_part<-open_dataset(dossier_partitions, partitioning =arrow::schema(dpt =arrow::utf8())# Il faut spécifier les clés de paritionnement utilisées et leur format (ici utf8 indique l'encodage de la chaîne de caractères))# Requêterequete<-donnees_dpt22_part%>%filter(dpt=="75")%>%# Les filtres sur la variable de partition sont particulièrement efficaces puisque les fichiers sont déjà séparésselect(sexe, preusuel, annais, nombre)%>%group_by(sexe, annais)%>%summarise(nb_prenoms =sum(nombre))# Transformation du résultat en dataframeresultat_dpt2022<-requete%>%collect()resultat_dpt2022
Le “schéma” décrit la structure des données (métadonnées) ce qui permet un stockage et un traitement efficace cf. UtiltR pour plus d’informations.`
Pour les exercices suivants, on s’appuiera sur les données communales ‘Base du comparateur de territoires’, disponibles à cet url en format xlsx. Ce fichier présente une trentaine d’indicateurs décrivant la population, les logements, les revenus, l’emploi et les établissements au niveau communal :
# install.packages("openxlsx")download.file("https://www.insee.fr/fr/statistiques/fichier/2521169/base_cc_comparateur_xlsx.zip", destfile =file.path(PATH_INPUT, "base_cc_comparateur_xlsx.zip"))unzip(file.path(PATH_INPUT, "base_cc_comparateur_xlsx.zip"), exdir =PATH_FINAL)# On utilise data.table pour l'exemple même si le fichier n'a pas une taille supérieure à 1 Gofilename<-paste(PATH_FINAL, "base_cc_comparateur.xlsx", sep ="/")indreg<-openxlsx::read.xlsx(filename, startRow =6)head(indreg)
8.11 Question 1
Le package arrow
Formation données volumineuses
Créer une fonction permettant de partitionner en fichiers Parquet un dataframe ou data.table.
Cette fonction devra inclure comme arguments : le dataframe à partitionner, le chemin vers le dossier contenant les fichiers Parquet partitionnés, ainsi que la clé de partitionnement
8.12 Correction 1
Le package arrow
Formation données volumineuses
parquet_partitionner<-function(dataset, chemin_partition, cle_part){dossier_partitions<-file.path(PATH_FINAL, chemin_partition)dir.create(dossier_partitions)write_dataset( dataset =dataset, path =dossier_partitions, partitioning =c(cle_part), # la variable de partitionnement format ="parquet")return(dossier_partitions)}
8.13 Question 2
Le package arrow
Formation données volumineuses
Partitionner le fichier “base_cc_comparateur.xlsx” en fichiers Parquet partitionnés à l’aide de la fonction créée en utilisant comme clé de partitionnement la région (nom de colonne : REG).
Créer une fonction permettant d’avoir le solde naturel (nombre de naissances moins le nombre de décès) au sein d’une région prédéfinie en 2024 à partir du fichier parquet. Les colonnes nécessaires à ce calcul sont : “NAISD24” et “DECES24”.
Utiliser cette fonction pour la région avec le code “75”, puis déterminer la région possédant le solde naturel le plus élevé.
8.18 Correction 4
Le package arrow
Formation données volumineuses
# On utilise la fonction. Ici, on filtre selon la clé de partitionnement, on veut la région avec le code "75"nnat_idf<-requeter_collecter_nnat("75")# On détermine la région possédant le solde naturel le plus élevéregions<-unique(indreg$REG)soldes_reg<-regions%>%lapply(requeter_collecter_nnat)%>%bind_rows()max_solde<-soldes_reg[which.max(soldes_reg$solde_naturel), ]max_region<-max_solde$REGcat("la région possédant le solde naturel le plus élevé est ", max_region, "\n")
la région possédant le solde naturel le plus élevé est 06
9 Utilisation de DuckDB dans R
9.1 Présentation
Utilisation de DuckDB dans R
Formation données volumineuses
DuckDB est un logiciel de gestion de base de données très récent qui a fait une percée spectaculaire dans le monde de la donnée grâce à des performances.
DuckDB s’interface avec R grâce aux paquets DBI (interface générale de R avec les bases de données)et duckdb (permet à DBI de traiter spécifiquement duckdb).
DuckDB attent une syntaxe SQL et le paquet dbplyr permet la traduction des verbes dplyr vers SQL.
La fonction arrow::to_duckdb() transforme un objet arrow par exemple issu d’un open_dataset vers duckdb.
DuckDB fournit un format de base de données, .duckdb qui peut contenir plusieurs tables et exécuter toutes les opérations standards de gestion de base de données.
En soi, il peut être vu comme un concurrent du format Parquet mais il est spécifique à DuckDB et est donc moins universel que le format Parquet.
De plus, il rassemble toutes les différentes tables dans le même fichier le rendant moins clair que des Parquet séparés.
Cependant il est plus compressé et plus performant pour le moteur DuckDB
9.3CREATE TABLE vs CREATE VIEW
Utilisation de DuckDB dans R
Formation données volumineuses
Dans DuckDB, on peut créer des tables dans la base de données .duckdb grâce à la commande SQL CREATE TABLE. Il s’agit d’écrire les données en “dur”. On peut aussi créer des vues grâce à la commande SQL CREATE VIEW. La table ne sera pas intégrée dans la base de données, sa création est différée pour une utilisation ultérieure. Le programme ne garde en mémoire que la requête créant la vue, sans cherchant à l’exécuter pour l’instant.
9.4 Les 3 façons de travailler avec des Parquet
Utilisation de DuckDB dans R
Formation données volumineuses
Il y a donc trois façons de travailler avec duckdb depuis des fichiers Parquet :
que des vues : Ne jamais créer de tables mais uniquement des vues (views) jusqu’au collect final qui ramènera le résultat en objet R standard en RAM
des tables en RAM : Créer une ou plusieurs tables intermédiaires dans un base de données DuckDB en RAM
des tables en dur : Créer une ou plusieurs tables intermédiaires dans un base de données DuckDB sur le disque
9.5 Travail avec DuckDB : que des vues 1/2
Utilisation de DuckDB dans R
Formation données volumineuses
On commence par ouvrir la connexion :
con=DBI::dbConnect(duckdb(), ":memory:")# ou equivalent con = DBI::dbConnect(duckdb())
Ici on choisit d’écrire la base DuckDB en RAM mais cela n’a pas d’importance car on ne la remplira jamais.
Puis on lit les fichiers parquet :
DBI::dbExecute(con, " CREATE VIEW dpt2022 AS SELECT * FROM read_parquet('../Data/Final/dpt2022.parquet/*.parquet') ")
[1] 0
ou si on veut partir des fichiers partitionnés utiliser :
Cela crée une table base_int dans DuckDB qu’on peut, de nouveau, récupérer dans R avec base_int = tbl(con, "base_int").
La base DuckDB prend de la place dans la RAM (mais moins que la table R équivalente car compressée). Néanmoins les calculs à partir de cette table seront très performants.
Cette méthode peut être intéressante, par exemple, si les données ne sont pas énormes et qu’on a des bases intermédiaires qu’on utilise beaucoup.
9.9 Travail avec DuckDB : des tables en dur
Utilisation de DuckDB dans R
Formation données volumineuses
La seule différence avec la méthode précédente est de préciser qu’on veut la base DuckDB en dur :
<SQL>
SELECT longueur, COUNT(*) AS n
FROM (
SELECT dpt2022.*, LENGTH(preusuel) AS longueur
FROM dpt2022
WHERE (annais = '1980')
) q01
GROUP BY longueur
ORDER BY n DESC
Beaucoup d’éléments ont été traduits : filter en WHERE, nchar en LENGTH, n() en COUNT, arrange en ORDER BY etc.
9.11 La traduction vers SQL par dbplyr 2/3
Utilisation de DuckDB dans R
Formation données volumineuses
Quand dbplyr ne connaît pas une fonction, il la laisse telle quelle
<SQL>
SELECT
dpt,
AVG(longueur) AS moy,
VARIANCE(longueur) AS var,
fonction_qui_n_existe_pas(longueur) AS autre
FROM (
SELECT dpt2022.*, LENGTH(preusuel) AS longueur
FROM dpt2022
WHERE (annais = '1980')
) q01
GROUP BY dpt
Cela permet de transmettre des fonctions DuckDB directement comme strptime au lieu de as.Date cf Utilitr
9.12 La traduction vers SQL par dbplyr 2/2
Utilisation de DuckDB dans R
Formation données volumineuses
dbplyr traduit les verbes dplyr en SQL et peut donc ne pas marcher avec des mélanges dplyr/R base :
<SQL>
SELECT dpt2022.*, LENGTH(preusuel) AS longueur
FROM dpt2022
WHERE (annais = '1980')
9.13 Configuration avancée de duckDB
Le package duckdb
Formation données volumineuses
On peut configurer finement le driver DuckDB :
# Configurer le driver duckdbdrv<-duckdb::duckdb( dbdir ="fichier.duckdb", # choix d'écriture sur le disque de la BDD DuckDB config =list( threads ="4", # nombre de thread pour la parallèlisation, par défaut DuckDB les prend tous, on peut vouloir limiter ici memory_limit ="40GB", # Limiter la RAM utilisée. temp_directory ="tmp_path/", # endroit où les fichiers temporaires sont conservés preserve_insertion_order ="true"# conserver l'ordre des lignes peut prendre du temps, on peut le forcer ici))# Initaliser la base de données duckdb avec la configurationconn_ddb<-DBI::dbConnect(drv =drv)
La règle à connaître est qu’il est recommandé de disposer de 5 à 10Go de mémoire par thread.
9.14 Question 5
Le package duckdb
Formation données volumineuses
On veut obtenir le vecteurs de tous les couples prénoms/département différents, or le R baseunique(dept2022 %>% select(dpt, preusuel)) conduit à un résultat non souhaité. Ecrire la même requête en dplyr et essayer.
Déterminer quels départements ont le plus de prénoms différents pour la génération 1980 en partant des parquets partitionnés avec les 3 méthodes : que des vues, une table en RAM, une table en dur.
Essayer d’anticiper les étapes qui seront longues.
Question bonus : Partir des parquets partitionnés est-il judicieux ?
con=DBI::dbConnect(duckdb(), ":memory:")DBI::dbExecute(con, " CREATE TABLE dpt2022 AS SELECT * FROM read_parquet('../Data/Final/dpt2022 partitions/**/*.parquet') ")# long car écriture en RAM de la table depuis les parquetsdpt2022=tbl(con, "dpt2022")# quasi immédiatdpt2022%>%filter(annais=="1980")%>%distinct(dpt, preusuel)%>%group_by(dpt)%>%summarise(n =n())%>%arrange(desc(n))%>%collect()# plutôt rapide car table DuckDB en RAMDBI::dbDisconnect(con)
9.19 Correction 6
Le package duckdb
Formation données volumineuses
Une table en dur :
con=DBI::dbConnect(duckdb(), "base.duckdb")DBI::dbExecute(con, " CREATE TABLE dpt2022 AS SELECT * FROM read_parquet('../Data/Final/dpt2022 partitions/**/*.parquet') ")# long, écriture sur disque de la table depuis les parquetsdpt2022=tbl(con, "dpt2022")# quasi immédiatdpt2022%>%filter(annais=="1980")%>%distinct(dpt, preusuel)%>%group_by(dpt)%>%summarise(n =n())%>%arrange(desc(n))%>%collect()# rapide car table en base DuckDB mais ralentit par lecture disqueDBI::dbDisconnect(con)
9.20 Correction 6 Bonus
Le package duckdb
Formation données volumineuses
La fonction explain() à utiliser comme show_query fournit le plan d’éxecution de DuckDB. L’ordre des opérations peut différer de l’ordre des instructions grâce à un moteur d’optimisation des requêtes propres à DuckDB. Si au tout début du plan, on part des départements (filtre sur les départements ou group_by) alors le partitionnement est intéressant. Si DuckDB commence par le filtre de la génération 1980, le partionnement perd de l’intérêt.
9.21 Question 7
Le package duckdb
Formation données volumineuses
La requête suivante donnera une erreur, déterminer pour quelle raison en regardant la requête traduite :
<SQL>
SELECT
annais,
dpt,
SUM(nombre) AS somme,
STDDEV(nombre) AS sd,
MEDIAN(nombre) AS mediane,
wtd.quantiles(nombre, poids, 0.5 AS probs) AS mediane_ponderee
FROM (
SELECT dpt2022.*, 10.0 + RANDOM() AS poids
FROM dpt2022
WHERE (sexe = 1.0)
) q01
GROUP BY annais, dpt
Alors que le runif a bien été traduit en RANDOM, le wtd.quantiles n’a pas été traduit et a été laissé tel quel. Il déclenchera une erreur. Malheureusement la fonction équivalente DuckDB n’existe pas encore.
9.23 Question 8
Le package duckdb
Formation données volumineuses
Créer une requête permettant d’obtenir les années et départements où la proportion du prénom ‘ABEL’ a été la plus élevée parmi les prénoms de sexe masculin.
9.24 Correction 8
Le package duckdb
Formation données volumineuses
query<-dpt2022%>%filter(sexe==1)%>%group_by(annais, dpt)%>%summarise( nb_prenoms_abel =sum(nombre[preusuel=="ABEL"]), # utilisation de R base <vecteur>[condition] dont la traduction passe bien nb_prenoms =sum(nombre))%>%mutate( part_abel =nb_prenoms_abel/nb_prenoms)%>%arrange(desc(part_abel))
B) Les noms des colonnes de partitionnement doivent être spécifiés sous forme de symboles.
C) Il manque une étape pour créer le répertoire de sortie avant le partitionnement.
D) La fonction arrow::write_dataset ne nécessite pas la spécification du répertoire de sortie.
10.2 Correction 1
Quiz
Formation données volumineuses
La réponse correcte est la réponse :
C) Il manque une étape pour créer le répertoire de sortie avant le partitionnement.
Explication : Avant de partitionner le fichier Parquet, il est nécessaire de créer le répertoire de sortie s’il n’existe pas déjà. Dans le code fourni, il manque une étape pour créer le répertoire output_parquet avant d’y écrire les partitions du fichier Parquet. La correction pourrait inclure l’ajout de la ligne suivante avant la fonction arrow::write_dataset :
10.3 Question 2
Quiz
Formation données volumineuses
Quelle est la fonction principale du package duckdb ?
A) Le package duckdb fournit des fonctionnalités pour créer des canards virtuels dans un environnement R.
B) Le package duckdb offre des outils pour la modélisation de données relationnelles dans R.
C) Le package duckdb permet la connexion à une base de données DuckDB et l’exécution de requêtes SQL depuis R.
D) Le package duckdb est utilisé pour l’analyse statistique avancée des données dans R.
10.4 Correction 2
Quiz
Formation données volumineuses
La réponse correcte est la réponse :
C) Le package duckdb permet la connexion à une base de données DuckDB et l’exécution de requêtes SQL depuis R.
10.5 Question 3
Quiz
Formation données volumineuses
Considérez une situation où vous avez des données stockées au format Arrow et vous souhaitez les intégrer à votre base de données DuckDB existante. Quelle fonction de la bibliothèque Arrow serait la plus appropriée pour accomplir cette tâche ?