From 214ed5d42139f5a6e7409717ed80bb619124c1f9 Mon Sep 17 00:00:00 2001 From: Damien Dotta Date: Mon, 27 Mar 2023 10:58:06 +0200 Subject: [PATCH 01/39] Initialisation (1er jet) pour la fiche sur Parquet (#475) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initialisation (1er jet) pour la fiche sur Parquet #448 * Prise en compte relecture de Romain L * Prise en compte relecture 1ère partie Lino * Ajout oubli sur une correction * Exemples désormais avec la BPE * suppression fichier json inutile * Prise en compte relecture 1ère partie Olivier * Suppression paragraphe en double * Ajout conseil sur variable de partitionnement --- .../Fiche_import_fichiers_parquet.qmd | 251 ++++++++++++++++++ 03_Fiches_thematiques/Fiche_targets.qmd | 4 +- pics/parquet/fichier_partition.png | Bin 0 -> 2926 bytes pics/parquet/stockage_colonne.png | Bin 0 -> 165285 bytes 4 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd create mode 100644 pics/parquet/fichier_partition.png create mode 100644 pics/parquet/stockage_colonne.png diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd new file mode 100644 index 00000000..f5a8d601 --- /dev/null +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -0,0 +1,251 @@ +# Importer des fichiers au format 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-recommandation .icon} + +- Pour les **non statisticiens**, il est recommandé d'utiliser **le format csv** ; +- Pour les **statisticiens**, il est recommandé d'utiliser **le format Parquet**. +- 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 et de manipuler simplement les fichiers au format **Parquet** avec `R` +- Lorsque les données peuvent être séparées en fonction de catégorie(s) qui font sens, partitionner un fichier **Parquet** peut être utile pour les fichiers volumineux. +- 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 logique (département, secteur, année...). + +::: + +## Qu'est-ce que Parquet et pourquoi s'en servir? + +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. + +**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, Java... +- il est conçu pour que les données puissent être chargées très rapidement en mémoire. + +Un point important à noter est que __Parquet encode 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. +C'est une des raisons pour lesquelles la recommandation suivante est faite à l'Insee :**Parquet présente quelques propriétés qui le distingue des formats de fichiers plus populaires :** + +## Caractéristiques du format Parquet + +- Parquet repose sur un **stockage orienté colonne**. Ainsi seront stockées dans un premier temps toutes les données du premier attribut, puis seulement dans un second temps les données du deuxième attribut 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") +``` + +Dans un contexte analytique, cette organisation des données génère plusieurs avantages dont les principaux sont : +- **Un gain de rapidité lors de la lecture des données pour un usage statistique**. Il n'est en effet pas nécessaire de scanner toutes les lignes pour ne lire que certaines colonnes comme ce serait le cas avec le `csv` ; +- **La possibilité d'avoir un haut niveau de compression**. Le taux de compression moyen par rapport au `csv` est entre 5 et 10. Pour des fichiers volumineux il est possible d'avoir des taux de compression bien supérieurs. + +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. + +**Un fichier Parquet contient à la fois les données et des métadonnées**. Ces métadonnées écrites à la fin du fichier enregistrent le schéma de ce fichier selon 3 niveaux : fichier, bloc et en-tête de page (voir [ici](https://parquet.apache.org/docs/file-format/metadata/) pour en savoir plus). 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. + +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/). +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). + +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.** + +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.** Cet exemple repose sur un fichier volumineux disponible sur le site de l'Insee. + +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://www.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`. Comme vous pouvez le voir ci-dessous, ces deux étapes sont réalisées en un seul temps, ce qui réduit l'utilisation de ressources informatiques. + +```{r, eval=FALSE} +library(data.table) +library(arrow) + +# Décompression du fichier zip +unzip("Data/dpt2021_csv.zip", exdir = "Data") +# Création du dossier "Data" +dir.create("Data") +# Conversion du fichier csv au format parquet +write_parquet( + x = fread("Data/dpt2021.csv"), # Utilisation de la fonction fread() + sink = "Data/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) + +# Création du dossier "Data" +dir.create("Data") + +# Téléchargement des données de la BPE +donnees_BPE <- telechargerDonnees("BPE_ENS", date = 2021) +# remotes::install_github("InseeFrLab/doremifasol", build_vignettes = TRUE) +library(doremifasol) +library(arrow) x = donnees_BPE, + sink = "Data/BPE_ENS.parquet" +) +``` + +## Lire un fichier Parquet avec `R` + +La fonction [`read_parquet()`](https://arrow.apache.org/docs/r/reference/read_parquet.html) 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) +``` + +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). + +```{r, eval = FALSE} +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. :tada: + +La méthode présentée dans cette section est valable 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, c'est ce que nous allons voir dans les sections suivantes. + +## Exploiter un fichier Parquet avec le package dplyr + +Si le statisticien souhaite travailler sur des fichiers plus volumineux (par exemple [celui des données du recensement de la population 1968-2019](https://www.insee.fr/fr/statistiques/6671801) de 3,2 Go et qui contient plus de 51,5 millions de lignes et 18 colonnes), il peut se heurter à **un manque de mémoire vive** s'il souhaite importer dans `R` l'intégralité de la table avant de pouvoir l'exploiter. + +## Exploiter un fichier Parquet avec le _package_ `dplyr` +- Exemple avec une table peu volumineuse : + +```{r, eval=FALSE} +library(dplyr) +library(arrow) + +open_dataset("Data/BPE_ENS.parquet") |> + filter(REG == "76") |> + group_by(DEP) |> + collect() +``` + +Avec cette syntaxe, la requête va utiliser seulement les variables du fichier **Parquet** dont elle a besoin (en l'occurence `REG`, `DEP` et `NB_EQUIP`). + +- 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} +library(dplyr) + +open_dataset("Data/Ficdep19.parquet") |> + filter(DEP_RES_21 == "11") |> + group_by(SEXE) |> + summarise(total = sum(pond)) |> + as.data.frame() |> + collect() +``` + +Cette instruction s'exécute sur mon espace de travail en un peu plus de 2 secondes. + +## 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://www.book.utilitr.org/03_fiches_thematiques/fiche_connexion_bdd#ex%C3%A9cuter-des-requ%C3%AAtes). Il faut noter que la méthode présentée ici est encore un peu plus efficace que celle présentée avec dans la section précédente avec les fonctions de `dplyr`. + +En `R`, il faut charger le package `duckdb` : + +```{r, eval = FALSE} +library(duckdb) +``` + +- Exemple avec une table peu volumineuse : + +```{r, eval = FALSE} +con <- dbConnect(duckdb::duckdb()) + +dbGetQuery(con, "SELECT SUM(NB_EQUIP) FROM 'Data/BPE_ENS.parquet' + WHERE REG='76' + GROUP BY DEP") +``` + +- Exemple avec une table volumineuse (RP 1968-2019) : + +```{r, eval = FALSE} +con <- dbConnect(duckdb::duckdb()) + +dbGetQuery(con, "SELECT SUM(POND) FROM 'Data/Ficdep19.parquet' + WHERE DEP_RES_21='11' + GROUP BY SEXE") +``` +Cette instruction s'exécute sur mon espace de travail en environ 0.5 secondes ! + +## Exploiter un fichier Parquet partitionné + +Le package `arrow` présente une fonctionnalité supplémentaire qui consiste à créer et lire un fichier **Parquet partitionné**. Partitionner un fichier revient à le "découper" selon une clé de partitionnement (qui peut prendre la forme par exemple d'une ou de plusieurs variables). Cela permet de pouvoir exécuter du code sur une table volumineuse qui dépasse la mémoire de son espace de travail dans la mesure où les requêtes seront alors exécutées selon **un plan d'exécution optimal**. + +::: {.callout-conseil .icon} +- Prendre le temps d'identifier les variables de partitionnement d'un fichier **Parquet** n'est pas du temps perdu dans la mesure où il permet par la suite des gains d'efficacité sur les traitements et facilite la maintenance du fichier sur le long terme. +::: + +Pour créer des fichiers **Parquet** partitionnés, il existe la fonction [`write_dataset()`](https://arrow.apache.org/docs/r/reference/write_dataset.html). Voici ce que ça donne sur le fichier de la BPE : + +```{r, eval = FALSE} +write_dataset( + dataset = read_parquet("Data/BPE_ENS.parquet"), + 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`. + +```{r, echo = FALSE, fig.cap = "Arborescence d'un fichier Parquet partitionné"} +knitr::include_graphics("../pics/parquet/fichier_partition.png") +``` + +Le statisticien peut désormais requêter les fichiers partitionnés à l'aide de la fonction [`open_dataset()](https://arrow.apache.org/docs/r/reference/open_dataset.html) qui permet d’ouvrir une connexion vers un ensemble partitionné de fichiers **Parquet** qui décrivent la même table de données. + +```{r, eval = FALSE} +open_dataset("Data",hive_style = FALSE) |> + filter(REG == "76") |> # Ici, on filtre selon la clé de partitionnement + group_by(DEP) |> + summarise(total = sum(NB_EQUIP)) |> + collect() +``` + +::: {.callout-conseil .icon} +- 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** définies (dans notre exemple, la région). +::: + +Cette méthode de partitionnement est très pratique car elle : +- Permet de travailler sur des fichiers **Parquet** de plus petite taille et de consommer moins de mémoire vive ; +- Facilite la maintenance des fichiers : seuls les fichiers concernés seront affectés si une mise à jour des données devaient avoir lieu (par exemple sur la région "76") +- Fait gagner du temps dans l'exécution des requêtes sur les fichiers volumineux (par rapport à un fichier **Parquet** unique).* [Page officielle du projet Arrow](https://arrow.apache.org/) + +Enfin, quelques précisions concernant le plan d'exécution d'`arrow`. Celui-ci fonctionne selon un `predicate push-down` ce qui signifie que les données sont lues uniquement aux endroits où elles sont utiles pour exécuter la requête. Le terme `predicate push-down` vient du fait que l'utilisateur indique à l'opérateur de balayage de la requête le prédicat qui sera ensuite utilisé pour filtrer les lignes d'intérêt. Ce mode de fonctionnement moderne se traduite par des **gains de temps importants** lors de l'exécution des requêtes. + +## 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) \ No newline at end of file diff --git a/03_Fiches_thematiques/Fiche_targets.qmd b/03_Fiches_thematiques/Fiche_targets.qmd index 551b6f7d..6eeda12f 100644 --- a/03_Fiches_thematiques/Fiche_targets.qmd +++ b/03_Fiches_thematiques/Fiche_targets.qmd @@ -331,7 +331,9 @@ de la science des données. Ce format présente plusieurs avantages, parmi lesquels le fait qu'il est très compressé, très rapide et qu'il conserve les métadonnées du fichier ce qui permet, à la différence -des formats type CSV, de conserver l'intégrité des typages des colonnes ; +des formats type CSV, de conserver l'intégrité des typages des colonnes +(voir la fiche [Importer des fichiers parquets](#importparquet) pour plus +de détails) ; - `fst_tbl` (utilisateurs du tidyverse) ou `fst_dt` (utilisateurs de data.table) : formats spécifiques à `R` présentant des avantages proches de ceux d'un fichier `parquet`. Ils préservent la nature d'un data.frame, ce qui permet de diff --git a/pics/parquet/fichier_partition.png b/pics/parquet/fichier_partition.png new file mode 100644 index 0000000000000000000000000000000000000000..7c164d35b4229bad1184a27b5037d308015741e4 GIT binary patch literal 2926 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 No&O%=)A?KD zSBr>ImD+TvtY~UcSwtVckYEyYS4F~6;{Tis0CQ2~UyvnBSjY2Wagwr+CfST-_O=V&aCx(?Gxd$& zVZHBx71yf3U_f8JO@jU^`e(~-#;fwl{%hQc&j$W8tyHTNwJsA^E3bXoRY|n{^=5^C zHFQqQf`iAXoIhd!YTCdmFfwHoB_mBOjRr4#fYcmh@m>4dopYK*()!z6Bca|Vv4vV9 zO_?`}W9*jaUQ`RU&Pym}|9QBa`t@H;!bcRKmxg4tbi+UsL2HGpe$Jc#-xjjbK0Gpn ztW@Vu*Cofl)=E<0x#iTUcHsQ#BDX{Ra%3O5`ogEw^j7unTcBu`_NMh%gI?sZHKA&WTf0}`f;7_^u-t<^ z>qXDNg6)Pe=V7nDK=*vM6tL@^zqwz|W+C%;9uvE3c78d4m%pIr1$2rka|i!^=4g7W zNu&W3QZ8WPzH}a!?8v$q0&f{(mzL?2RDV~kosdu|g0@cpnqU7l6dzaT;bW7fVTlxo zRvoG7fbb!m?v|7^8ljt!DDbn3l}E=XFIOJ@AJ*RbugU+5|0hQG5E)Vf2BYOs=gI|nZnU3u+2ykLedA9W$qV9SQWrPWvA6yxag^X)hzp)b2`4lq0vKLlk5{I%w zf`fnY7%>Aw+pgpg1CYQnPGJ`aS3g6FBDJ)(B|v@qld5}!l~WbK5&L%J%0>kAv^hjn zxk`XnB2m%i#i$l;$AQuT*c6nk{?3q+Ih%Xollxx%ig&UO9*gNfWh7kzV30k~z`zd+ zk+g$-&YUJ$B~Jy-WZf1>!uj?H*_Xa+TaPq9ZbBU9Z~1QJTb z0gD4Hy{w>vIi6McbtKz1H-Oqs}f8PH?CA{d(9 z()yWXA%JiMmvIwe&+n%#A;h!kWl9R%u^m=o8`MOf!D>`;uvH`um%sXl-UPLKHtv?^ zpMuqc#gAyAo>;zW4qp!^HlYmKD!AG*k3yu{JqJMbdH9Q5Y4pFRyU!+M4=AI18j}>% z*W^$!5c7X1Y&IbY!9sx7{jGFt-S~7-ePCSf*L6E&-F3z#L7nOa<96H)csexT31FB! zK^-h0c|y1VekIs543^ilO(q|DjXCEhL9$hn!vB`>*yWUXQW3t{pq zZqmFgkiY-6Pf#bfYnP8?aG5UPX*BHI`<+S707#l&k}2oSAbB|8xmQxP`Y!`N=Ymvy z{u@cTF~0)*Q9=L^_G3u&0q~0UaWN1s087O|qAQ)(v&iiIm-p)d?fFk1g?x0SQe z)vfpDsJOFx#-upk2~Jm1vJpS;7T%i-1eG48-u^DCH)P_PT|Rl>Nm=~qH}ACuLJ<%r z(~8#tiWF)qen?OUP7<;QvSBCS{4W4|rDOjNRBVFG#dNT)9DrX5>4NXEn*mDo(DI3l zW>7fze*5ih{_JkhmkvQ4lbX|qfXNb%J_C^xzq=d|m7LnJ@if*!bw53}u=vMIw@#&L zw_SXvTHx+`8kOf3w(iLK%QAyxgZ^Q?p73uLTm{leXlK|7s0b>`ZoA58Z{MgL^hX81i+=yJaj?n{=c+yKu zjRWIqUtuKp`tMs)Ny#SSUPrg}`CdoX*6V_fkEvEaym4B5ur13+4^kDK`a4DC%GEf52TQ5q|9mQSZ-y4t+8yJ7e)tQm#eI)QP_ID?MKz@9~BqtVP zZd-Q#B?`DdHR=3kV0zA+|2MYE2;w4=cF1tu(`#-yW5D6yKhS~yVJ+~|m;C}>{wIH2nky`TGDbr~$}aiwxgv_~D(Z3#Fcks(gy>UJGKrjO4!4$0JUDl%1tM5xX7w&G=C*f8 z=8cp2x%lLw*efnpC`BC|Sv$51iw3Zno%o7wz+uuBX(a)vtP3U3Px&9e;x9K?Gcdk@ zLF8+XnQ`{}qaK0>Yo<-UyJ*gO8@Iy|Q>p)Dmc!4w5?g*Vr_@XF0ccG;EM&T71tJPk z1<)Qhf=k(64{EsNKFo?Hp?Tr`serADOdWvb16HIg-G!7MZ#ZCRk4b5D`Kr4OkQ0$r z4)Q=QCOLx0Zhg%*`lG;E(-DQn2V;@h=?!mStOpnR=|~>6fa&@UWCqEExPPgN4lY8z z@Rc;-*vSKvN*u!PphYvTet~z}%)-6*W~^7kx@9c)^iD*q-=76ON{zvv6M>&h;nu)x z@N>1lsMjH3s(x(+n3l&AEV+-P#r!b0(kY9IQ!d`SZ( zs__BlRg8d=Fne4J{6Xq84LPiih*NP0YgoUlR6vqbOY{>AS>`4vC*{+PvNF3o@P1W1Vo&!d8<4`@9ftK|q2;wN z70~g$p7r1^#iD%eB+!9Hqd-|)@Bl}b0cTr_HlPkuMPgQ?4Ji0M`fBYKMdWL{W;Xze z*%Ar=pu_;+){h32N^|>%OPu$P#f#M5k1DzbU7XISjl}_+hnZ$<8k$9SlsltzVF!TL z%^Gf1%&{!d(i>O_=n&yLMs*ixRdn#AR|DLZ7rvbA3!;Xt9t`l z#aOo5al@t0(_=er?!6(qG^k|DBXOWhu5D-a#lCd371hGTiuvOVht!6riBp-#ARF;t zjz$>{DSXNhWA}5@D})W+<1)2{k#~w;Md2tuNvz2l01%As#{KbAPM+d<+7lB!0&LXM zqM}zlwiI0@0pk2Ti|-l-yfjk&->%=yIEqcit)DQ?WPztDKkoCGP%QnfhU0+*Nq6$# z_^)bBb%$0M#`V}feAD`VNkr$W-p+vgs{+Q9({a79t5d%#kl=fw4#Vu+pAF7C&2HB%c4d;VC$|PQOAM+=Y^HZN@H;My&R^|P+`|cN}SE^GKzskeW*C8Lz zHM+9SE6?8_$2>R>HawvuF9@y{g(HKu{bu~0b?9wtOTt~8*LbBnm&Ogi9~76=J+73f zEMNUZ!|nNHdKv&`XvL*j`%MaOjOPz3Z5fMNptwp@oU%7i&x&iL+_-?z>(}&je^_Ui zFA9%7#W9~w)4e8=r}rAGbUG5r>!M3GNH0M;I_c8^R#kl0(kbJ|+u}p!fx_ruRX-yNx67r86_7UryZxExP(Yb|$ghJIHQNejH}eR4 zx)(K-QU0K5x{Qnw5T>b>!4LbNh%r&6bkm)%xv=f!;|zchXEti^aG|zAWxkx)5Wwa z63H_bBgcC)#dK_Lbn?)0`)1iQBjwkgov!7Gi9C}B#w;rNTke)(;y}UJ-E==@<>C^q zFw{A0ORh6pJ4n3*i?W-~J{N)aO9AAQUT%GVrY9R&G!NWJ^9PB0S=YMMUl=s1@77Ee z-1YcDGa0$eBM9qqIw&^zhwhI0wdU9Hz8onTP7Uo)xfa-C;nV5k#t&ATHJvBb zTP9Y=#oRiJ0u?Q{;iId0jw6L7YvUg)52+}Rzl^?{m$Tqthxu2gH{*OfS}as{yaEV1 z@X;Ry4N^7DOmO*zRfB%w-Py`p`{Py((lC3%m%sqI zs|BT|-`DM>R13S$;2txx)ba{LYH2H}>$uNWqm_For!ppw*NlxSFHjOcA?$edS z()IB$!koUB3PV;;gIyT$^+duMUqJAOvR3C=+3?;sn~JFhyfJPmyPIhxHrs0j{4CE7 z1UB&-*`cosvJZ0`@PhU~9Re|G_hwXu1LXRK?Mng=$A*mz%PX=%V}AVMzgVbnZ|Srs z>T4%QO}Syu8Wnh8`qF|yOT^G(wVz%G`p@=C`YCn4lr%(2qU!LWM~#-M+$F%uDkQ+D zVU6D+?ph7EJ_+N3!dji7njAs`h~&V*l6|~Ec07RJolwqc9zFOi)`VQhD^`H?C;?&0qaj#MxVV07HHd#iOWIf%d;Ddj2UE>+%X!jx}QwH+9l~OhSpC!mPe7t zz#cyVm@gN%aJPTg{rM{yJ2`WH)OH%RHa=qhIdT8djMDUOf&gBq1kPxK%^#bzdEXE# z)#&}4J(-Pb&tLvt)A6}vC;ybRm8Ik>t>1KVQM%hqxP882QAbjJ zzd5|A{M2b5GW^6V0|VC8Q{6lDe}O@w4winTU+m9_3KANv5|^b4P)bX1X~ ztvEiMEdX;uQb#JaX5q1P^j|4(w52pHPZ?)VJpHq*8rlFc-spjZ4;49k6p||~kIXfW zHk}`~Imk|(^m*}NS+mNFW!XCcjYz?vm(xSXW-Gg-AlGh-wE*~oe&LK4^PQf6B-L|5On^>~5jU2%AxuMNt*Byg3TpF9IM z?T#Kc+PvmY9bO01#7Z-cfp}XAt3ow!ODr3f4926IYs(qenTdKdujyQTE%?`Irt8~M z7Y(g#XN*fqLka+-u?H&Cp%9g*GGVjL2k+uD{?pCe!L$~z>1eIg^SwA!X|nQZX86Di zjnrOqXbxB&DASI1`!Z$O2|F34GF5G>sr;2kII%%8aCL3$+rp_uJ$BUm`M3_VSnkh$ zDj_mlTsxX&ulmq1?K{FE3+zZ4R2t;1Rw>~Ay7Ljck95_3rri(y;kWiE>)<7^%)`Fe z;S5VS_Fb-D$aW-0gWipt|eRc{{u=Vov+tJbzV=Xzj;kZ z5dKfZgO0fD-?uEzhi|574am!UED{N5n+Mf-|BAJ`0n#C9B{h2E+xS)Q9x<9gFR=2B zZlA?kFk#35=~p&EXQM$4cv9&AcVriF2yZC5zBzvD^TOIqPuIVAx-X)Lf#z6&X{g-KW+^Ph$ZYT_Y~cOP!5d zXFgNwR`NC{BuQbd`PEE8nYZErYXyO8P%%YCe}!i2y~s=6&6Ir776lT=jra~JE8yFR zU96b`ybA6qyajtC5u4aV!so;*m@hVq9+^$V?j{T30pPmxBwzw3iP=yM$OhuWG-$RK zl2}Di+Td%m9*O8mw}!wIMn5XUtrUFYJVyxxsaaoRnuUBjfGA!KKKK6=O43N?qj(AV z#ny0VdVz7|6cBQjkCnRElj3h2J=fwV3<%8rhFdoO4z|hx;h5yf#mxcnV#RN?&a38i zMnQxjP9yJD_K50ay@`7V>uToB?)qK>rj`v)Cg{jeeQZ! z)I1MRAKW7YTfkiQJb*wN%w{K8$|C3Rn*-AxAk53cU2H45@ypC3e%G$PEDMg;MXiR~ z@FLnSukNv{?XpL{&tkrm)8AD|UU>nW)d0L?`9b zrGMGf(qrVF{3Ia`P$_J&vSp@JU5pVcHUsQO>`n)1s$3&5`@z4lyr#|oq_d^CTy3=O z&@$rNtQvqBurRW5U-)%cnBPDs-whlz8Iw6Mo_K*zWHI|PdQ3#nn#`QN0*V8z(72Ye1>@8E_-ive2lq*B5tsE`@R-q|RP8f@H#6R%VS9{&niJvxeSn%iK zCjwybm%AiQZ%Q4kDfZY_c8hRE!yrS!;7-Yi*K?1RsVIWDOFzrl@ZnTzDk}c6zY#0G zqnaAUW`AAvk9QyTS??FD;@7rVBy51R^9iSCOwG;49(_P! znqQ{pNn>70+s25Q-S$Dhi|PVbKZICxtG5Vy!%yMcK2T0)BcgY#ZAy8tqInwfsUJ9J!R@8P8;6xghL#rc zfI}4fEnGbXN!&37xxXk{CsBS}nbA|bOS4QRWA{oroq-^Hg0W`*)#=~UDc(rZtEU|# zZ?b}Y@AqGli#%1<04Fi@OgT^-ow?_n{t?iaBf=WMx4Q1dG0%e2{vmlV22M510zx&M znY8}d$Mwrh(QjeFdGDYMs$3zrV5St@h(zc@Y!f~sjTtq32?ghlkW5AJ2zwKy#YuqC*yw zl<8$hT4d;3aV3&ng<+g<@PlYDTDM%hCSOeBL!huvWEMds8FP=0Z1sq{X_q^){W$lj zY_fX47YXC>1Hd`pL&$ZbX8@tsQDzO^TG$rF{pkN}q6~#!ZOKOuwwMNtm@*yu5u{8FXH=8AUy<{oDT$b(EfX`LnzG(Z3x?7_*D6*zEs0&Gv1aX z*lO|p*=Ohv46@Z4ADR!x`vZ?yn_G3Jz=M~LC+026M_qiqw)-!L{ZgcZ`)q+E&@X!K z+nGcXL5#IjAJj+^0v||+Sy+$9Eb`h0udpWRORr=yRdn0@lmw*dM=KOtZp>3~eX`z2 zKWk1qqn#g*EfWuURc`l-N0QhZb~p0bdXnvo{3eE|PuM6) z!nke`P@K2DKI%%>vhVSWp<|QFTzUTFD*GyHazMSqpSLT`%d;*njAi6!&pL_WWq*A= zODrLlK7Ze8&BP0L)aYfs?r4JOn=Ce;cLr{BQlCbgvFG61TbNfhl@6S5_K~FV#RFF& z1D`|NRQKi7+@WHY?q(WGVfE<9A2tS@8yeh46CafD1F4HYz3q(&E!k5GN~b*gELa}y zPZgcYOEM19^q!GjHS5O`*~l&PgU(M@gj;%^VpObaSZ3>=*1NHj^iwo)s$5Gd47{0+ z?NPNOcx|)27fbi-hs~-x&Nt$(xtH&@adMktRbHKw0*vM}TNM$rKGMr?HWwCExjPlP zIt9_NfU1o2CSKQz5LH=4Pztz1%)dCfvvJE}H{$zQA&ce7#xiQqt={{)`)!=P{Pvg& z4MRUXy3$>3rv3YWG&Vve1AO2=$HXL`M70~7hdjn8D{kb%y5D5|)=(sH6{hmkRDRs4 z4o^9{Z5(HMM5=0+)k&CY?#Q-^56`ALBGS~^rmDo2^@^O#lYRwvfA8a}BEo0wQwj7T ztL7&J^xKh$MRv{EhXBH~v^R-CgWeIq}{4pD;ilgB|DtboKVDF?|cF zQ_N1M+WBtV5at?GMe&@(1Qhr%r>#M%OBdDumLy zd~8^2Mp`}lvzA?_4(uD)Il8eq${FU@sEfS{DD?W=x$0V;tCP#<&;j)CQ;J}?JHSH@=esUe; zKa%oZ?kTdXu|r%IxMkBGXmd@b^h>wGn@dBA_5Kkb+pXunS%T>5gbKG;jMUX>use=c z`~PV?Gb{CKX`1ErM7x&ct~9kjcwisbtiwMBeIl2g%oTGAI>;vDPDEDNMH;l-Q7Uz3 zni{JRvp3;(MY^~qDI{y4TMft6a@0);*&~;B&~!ZUK?`WfILieSm96_M+_xXM=&1?A zwzTwos=}lcsw@I*QMW$1w$=E||33Taf&vYq!t!7et%NN%LbA#dlXO)?_0EVk0y;Lr zb4xj6*RytmCg~@`gmVT6Yfhuve%XfoH|;>rP~8^W0+DVDJj6KRKy?2?N<5u?$hNFn zg`+CN{bu>yJbyXs*2K@I0oYq=t5HCCmk4`|UEXj%Ds;G`A#g=-f zA$ec9YW&?-f@(P7Vs&Z(jUwb5grYjTYkbLY0 zY;Rb%n@dD%)lE0DE(H4}%X)pAn3X>1s{+?sG7pwyYs5;oHzV06o3pG)TVtGa$vJR0 z^&>o5(bYMFlYOV6;qWDMWl?8)As^ovJ8+bTbDpW#Ib0L@As7=R z-gkHgdSy0*tV1qYWqwy7w@INMQPHnuNZ!9qp3%uT3aOdKfK zI4poTeFup6hXnuPY>9!kW1lNOA`*dVWzGswO!nknIBk;pCRiCcD763OX|C{W@(QGs z@qzls3yfC4UL~ORs7;=xp$jL^8#RUHMUm6xAz8X$pTV#%`uDC!g{EsucV&h>up>)# zIjwf#))SY7A}gh?9^SBMF!38Gv3tZqK`~CI%iQCE`B|WP_(iLkx|0M%anFp+?~NhvVt4>L+WM%p$XQ>$M@@|it_U4D4!SsI(J0oQfq~@l zW&CHNQZ!xgv5W%OjdfCd^()_e#!bifTa>)oZ~SR0Q`c9U1Atkv9`R#}(!S)Q8AabG zm0#?C?igytCB<%9m&4|8Abq+J?O@3ODK?Qh8}4aZo{%a6iHYSp5MPib9V&`CV3I6- zm336&Xv7Gz=!^PRQ8f#DbZR4%qUVzqrzFl%8?nc0XB#WXm0Y^S;2!Lg$}cabluw3mUyPpSa zuB|72+t2e6o=O3@Ar&`KYv4Y$3g82#PrLD3KLd2oGdaa{U;-Fn_NrOk_d;CiF|@RM zl?vC%4Mp&zg$@r`c7_h3e3)!wMg{%6Qzt;dMfZlRIfauvqWQM0*yqDrQNfy;F!fwB z@`NK{O^i?U?-JtA62_K@Qy?4Kj_SL@!cB}mhcW_q0&5kJ0@&yk^r|xXUKU6{=RnI^ z^7+s8-F)&=N~7;75WsE zlqt|)Q4SM;sZEH+hFniR1X%}BEk`A@`W$4;+6I7WWzajn`biXL{(}Vq>kAToNFG$R zaXZ)lmu&M>7hI)NJFRSRpD+cE1|g%2i$$WV-jW>{dab2^DwxP?!`c*Y^@TA%Sq2TS zJ=?aDkX0S-%uce`lm0Q7?WXhW$9)(Cj)>6?+cf_>KO%bO(B%3L_w>O9{K^cHJC`7rl1Z>98jplBGyEgpDaJr0qzs}d|nFaz30au15T;qb(fr@!0 zq&@ssP^BEHxvdG5(6`J|C^F+G*Db@JH zDQOw2UI*9tZ5)-pYOu>Chw*(EB3)~a%gq9TxvGGqUZX0%f!dysgHEq%UzL@{fJXN2 zq>~>5{tYJ^!IepnA0($DTX;<-Ksq!DWR6m*qZDqL@@ zA=HH@M#Yf07XdryBHhHaA zDJ(%pKlf&~Z=I*HUuiG;-^`Nw5~Gd8>T7Y^+lzXyl7XI5?MT=?HvEejFU+;* zH1*$Q6x<-R&1Dnt4NCnzr?)Iq%@fDjo=PGq>V~egVxvOFuxbi3x=hbMqAPiFU0>HT zj~_B>Ec{ULPYHHpLlOklJ=MzF*j3;#J>e3+@vtx}Rysd&SwDNq178cfFqIXl3=P>* z8FE1DBSjC0=SBol>ZCb_&6A2ftQk1{;AdKh<~y{UZ~jz-POj8RB^0W9ZeM$0V$`gK zd9T`nGT|z7A*7;c^T%&d1gi`rxM%AqW)?KQh96l`6>HddlK z3Ys&?y;WFVpSX`6cJ1EUc<6sNP#>ME&7A?SKU-sj1FFC9H)4gq9asy9@oTe8{Q6|N zZiC9az9G^c1i?eu*x+@?yrxRGHfmcOlx(Qypoz$!|#QdfM;*!zbS)?wjjw^uNL=m~N>I&6NUoObF3(ez7v(kIHxlHM|k1%D)!` zlmb4UcWHmO5h1l@nQICkQrePBP0k7&C;Di_rQw9HquuFppgQQy@02ik^kMbA(gHy+ zrt_2>HIZZ>L0yE;buK%9T?(_ zbmAbOkfL{I@fu=4K*0VISTnl5U^Kh*<&3ezW>UYS5V$7xU6&nNoPIQ@B~-JRuTN8* zinE<%P$yXQ>M*|!^Vp4{LwRyi%W;6I2glIK#(N zw|r7(bDO_~2%wG+P6Tmv!?z`U z3*Xt&wLh0y}Na1%@MjvwaB9`YLq;ibfdPt5L7(Y{*@Up<=Bph)>Dw91SJ}4?L|IcWt?={G4GZsY=r=oAe4#O) zXyxk{s4fYx%xV*}Us1}pSRcR!HPso0Xqq#L#k#q|e&x)I{Gmg?$)eo_Ly<*0n zY`np_t1tOV5RDUdvu9teYdjF&u#ka2uJx_dD1jeV2ikqzdMq<&RIJnsa`0L{9IYCvU2jRPOIwa00zQftEx{bjV5XL+3wkuvyy>^L*Z^XF{#yEME1(n_EP9F-+|&PBDFJ1D zNDETV8sW3M{Xi`JScw@-Z4^iawjxh5JNb1-BN~*hoL!AK+d^#rp-D-I#FsGpjO#nUKVOtdN!-;)9n6U=gp#=-wamCigUDOST zMOciq4r}xHE{FcB4rqiF3Z%YsS^0YLP{b_boIrX)fLj8_io9_4o@NZBEOo>8<84>b zTG2vLOKOFlOFuqRLM96%C4v6UentIEs`mAOJkjzyX|}B*xWy`V{Vdz%?Advx>QzJ| zjFrpvl3hEyMm~oi<(#0*yQK2;vu#UsS*e+zZxN3+mdZ#jd1P0wB7bJ zr*-amDjCg4EKNYEd!x1`F{vOXsxBa!&ErK%))3c)mMKv@y+|a4j<*dRAatt$1A&7D zqneidY;n|DKazn?kRi|E^YARUr_<8!KJ7S_%pA-8>F?kWT32WuP=Fix3C93v5>U5tc9TmLC7fkB6oKHJhT4)FlIZdh-|Ob0x!@6P7VDi4aY`amRj( zyym2a6)B3*m`e-U+Z$ZRWj<%|YR_f)i>z4bl-Q= zT&+%^PNhx_fe`#N4x0fBRsfrtf;MhA&B=dLHxwIVtelWbj=t+}x64QstXWg-QlvwQ z`fTlzo`YMrOAwXcZF{kSN*4f7Ut4QOIjY5wUOh^}%pw zRw0Z^A=5COi_t0$O6I};2Np$fQw@MvYY5qC2V}z2FSSP$xS);A3boD%i59yc#xKya zH>}0S{by*4_E?x1?*`fUoW~WT!viKob>^;{J* zS(Y)z&7p3yObu3zu)KT0U^PywI91pmSUJLeyX$7wolCG{6K?N-JJR1I#2jFtH5^N} zpx`XMTFyvTu4Mq2DE8W*WaEkm#;aP=CpZzRUbm+8+n-lm!ctR0TI19f zV=rQ~(R{3IsGDNGdn(++?{?R&sD*WRHd|1LATsD9#v~@+5TQEwTe1UEN|>G9+pp(1 zAl1>{>d=-Edf{`&91fugl)Qc1<^bjRp;m?-o0#!zq4j$B-Ng`tD?ts7d3+gSmz;nWK!)-E?NEf~dK63RpCi17l1Cm*((~?2u0={c*)Ye8dKx^l zf5nW#85Km@R=70Tq$sjV(%80H8A&YLB1<66RsQuSXtkd;TDO+G(8F0WD8qYr&>^JG zRQycR@8)m(1J(Nv?BCMWEpy{bkzWy_c@CLy2QOV{qyta=y_ zvse};Nz+V4Gk?v1_swNebvjpGr}M0x5l}7nK)-aE9e>AjyaF-`6yOWp$?;Do6{YKt zz)UHLKv!FdC{R8FoWujczaumT0l37=Ofs#>c+Sjh@AdkE{J@<7_P9d|XmqyrJ;v0X3S@n zZEc{Vof32Hj$zXetgzGG(XvuFHLclsYu1LCzxn9yJqme3vNXmhAd1>((@-K*pYAQx zkiSdt%R6<%WvRy^OpRI;v;>efM*0=4Gu3m~?w4dn+pMN~%&=&}?1FDvi>0mT>pP;0 zrMXmG5zoseH0DLpkELE0eS1zhd0?DHz%nN>m5#{M)t?@i&O6@@p&NclSKUmV56x0hDhhK0+Bl(svhI>$&Zy=tLTYHNHh;N0U2!_%$rg0WHXtwnn9 zYF+(;ih10&q{%-r|IJ1Ix=miT$;FrDnp2>4Ak4+SdxLH@O8jqUH3x8c8p2Yyngtu& zK2ZnwG^jCRf5gJV$&Gz)6sK)R(|=z0^Eg}k1c?lLyqEp2npZ(lwC70)c#x-o>8|~O zBUQ{%^7Tf!!0dy3QKrCWXA_|tArkw&w9nlkxvg59hfh`icAt)xe(E@`PM0Phmgfw7|l5UJYt1jW=} z0d*6Deiv`Gz}K!_zDtKn24$8)5wpeOKIEmk(Gcxd5!QA5UBEy-xO zGHAo+`r_&><@!59E9j~jnqd2_(MAGC$Xn1*3||hLj>$be3>y{Fs|h*m6Yh@@MHGoX zI3ROTz;LaSv9v%3W8~fiziKc3K&ee98$l`9O#r=kHN=Vd27QzR@|+MV;1GHpTbi4C z82l&;M+FaizJrW!V}}Xyz?LXnTFMr==5i%M;HNvG9%}`TooMqWgYxd6j~~2@)~WuY zf7fFd51CS^d4#ieRH|UvW1<-;HJB7bHh#1lJoHj2EYrzVuvjj^Mx1OQyAehZ@DTuf3<`FiNqj02I76s z)=^H6eKcusA_O<)FH4WL-@{ePqLSQb>?My&cjM+Y__#oanUhC*X z&cv&2TVea#vi)0QbTg5&H=R#deKQ1QamAM9zskxYXk(>rkL_5-IL`eX&0cS(C4Tc> zaAb2XMP^LWqvD+KK=XwT9h`VcX@P;6KJA3f{?#btm%a@fyODXLgZOT0JKZf19K$JQ17Ya!y>hz?b4iwVCo={ASkdNVd$r)n zeqKf(UfQTueC{0&!%y~My=E|EjU^8DU~xdo=$K7tk@XUSSmlV;cV$x)J~v_33lyH* z(E^(l3Dvwd3gj8~A@rmuj4FILjH5*s-D86Yx~x3d5e<xBe^NU8bN z_z>-eV`}K6qBbkR?&Pv%5WL9gF_k?dbTi3$?MC1*i(k{3X5%G7wdPN=l&er3(8RSJ zjXMyn4KSOnzhKz6C29Cr?u8y~mstkwFThC=oxS?#6kDV{wZ;ZRYe$|zKd!LUzIQ1e zQ44t5NqZR?>L<)+BI$ET6*BxTAd0QJGiQK-TcZ*pgmMZHN9c`R@f($b?6|8w`KVp2 z|86k`#Egg}OKEP7E2?Gw@UwH{PFzi7(IBs%J%gTshnb4QP5M2fv&E0Go_5`4K+j{q znlTM;+wU~_;%`>TrPn52vVW+~zC5$w|16TefqG_PcywByB#E7~J7wiQ+)wH)yrMD& zxj@Ble|HkIquQs>vGGrfRH-aYAVq9 zVGjkNW_u4!2gP5od(&CL!ohyamW6@QwFFpx<`2;d>QA?n$u+Mh-G3#K{FW#AE#=`G zy&RQVr__Uzj1RtDk*{m*PKHf`bv(~*&#v%0gu1Fv=j!M8Dls@>gPFGd%FuTS) z3;O)cViG$u=H4scTw&KCFOBzMnR8DPq-sa4^yAWAA6V{u2?GDUW~ZIQrKCdtN;n=2 z-}}0cd5Vsz4@u#GBDJi3v0DnZ9vE>)QM8ndLS~nv;h4L!E4)sx^wr11Ld33yCl~DE zSfX!I1OsO_ZovaDquJWu29GN)a{I7drcYlTD-^8z264;Lwp77^6t21+=R5}IrzASH zsB5B>O9g!21v@g(J)=S+$=L+Q-)+!Wk2RH1+0!YaBrQc~t(Hr{Z7I&4C@PAE+IIoJ zYM)LmYT)tNMNjDROV-jlK+3Q!^A;)XvC#zwGwqJQQ)VRhk)m7WI zE4W*c<;e6u8;?AVdx~H>OJ=dPM=~8FVpb=)xM#B77L8Wj+xWOU-$!3J1coIEna)VI7aPne(@}aJEmBMAiJLQx}E?c zC4aN^3QN$7EvCKC93J8@sCD#ip)L0)}MoxE}g&Om&P9Qw!Ba^ z3F=4MP4V;Oj~>|1{9B@NvHU-6#eI9R@W5bxMO>;=$x}i`Psma-P)!KoDsi>VaFy>r z5(jz;6uX>bH%PiKOCCpZ?R?!V1*5kE$pli$jj!};PcbO7S0`|>UfEXE5P2@pG%KJX zKQ4To0w@UVvb6gf9i89mK&R;lL-QlPzt?OK0pRa!>77ii< zvwwA^HJOHpw=7>sjHwe$I-86E0=i?h4oy*~?Sd*MH6v~bGh(!&zphisz`BVKe_K*to`xh|q>sdro*rTi{wrE(^6BR-H2)tFDEascKU^AUnc8!lgcqS^T zajN^b(}ckXds)Ge*>*TpTWR>mf<&YyZA)5%A4P&)`Twm2K=CthOafJ(0Q>T^J1~|? zgb@6YEx#>Ea_p*IH-1p6MKvSj8Y+&%*Wq*{!U-d_x00$_=0F$SSj;Ze=f{y?c}0%) z`cbY^K%Us)S1s-``RZrjcWIFWn3@`vS~SFVCHF`VWLu&|kmo2{_NG*Y$1Nm_A79H-LEk%u&k-gUm<9^JrI)es@ z1g+3cA|=o-Nf-ZxF+{F)whnC|LYHMueIa|J}p#32GFd162wOnwEXsu#I` z9bfF86}kMPKIwIj;>iDc654~!KKXK}ODwKFOzu%ldE1nrT*ljIDo(*^|NC&uII@-V zrT$Euz-F|5QU>v3buWubXJKH~xsBx#E-+K&6uOVtr@@N(J^lW=J<#`ZHE_CpBeyh_ zyEH@5)gi^-jnqcm&%e|Cze^&ujT4K;n!^TrM1m)hR3tc`oqfnn3Rk3y`*a>csnO9e zWAp(aRpQqtRI0cg#m21N97HOjF?uAdFG7!9)|I1DR4tG;B##C~@9M!|v?4l)sFr8; zl@w%MMKJSwNp!;w?bLSKrE`yGxXdVM0BEux`jQw7=4Hj9Zly;Lo5j#0Vvt(ug8vs= zcO4aF*ER}Vq;u#Txb_In3?>~% z4<5O1u)=-gDwd~)+-ls(?r>J?miKfq>}{s`%mj>`L6S081v26YUMX>b2GM_R`gu#`yf@_Sr7Cul zZ0n&25B@n^8~3V@`Vxqv*Gvv1q4JI)!UGAjxb^>FK3#rEA3~bf-k0$E@+A#a((D~( zQEDNzd5@ba(ZxeNzN=LU5wr0xP7&WGxLYI2;5|jNF<&S*5^c7}Oj>_S;qh3~{uRvq zVKY`&Ii|JT3WD^e9?V{i4?CS7K3isJmeTUwYq6{^*1M4cyd(o1ndV6Qnmy(X5u~}|f0dp^90)0KiRl+5O^Ult zRIZj8KgemE|JWE6hk*Lq4A(y$nGcbj;F_Vl9eW75ZEG3v0MhZ%wvFfkYdTZgqfqO< zGrAHab@btpC5=T*NTDqX`#2i+(!CNtOLy54$fJn$9clzlb|O*{KCMGdF$2(+FE9EC zIkXixYy!X*i%)10MHu(^3)*f#^e&yA<6av8+Zx_E{}4t0-VSm{U-WZ?hb$Z2;h=o=eMU5&4 zDJePDg;ytgMk0&)8!V|HdbJ-RnyckTSQzpuY(*rI(SMUmReL$g$mQ_k6w{IqDb<9( z@)t%U3w)n4qIT(^V2*250(*pH$`myKHQkp9%xo2(BB6uIG#J#fM9AZ){hGj6xNLYe zYQ@f-ft`{Kior7LXRcD!;;hJ(FP^i-PU2B}SW1dxY<(P}5wJ1I9mvi{Db_qSE(Nbq zG;Ck*Q-Y^Yvj7`}GI8<490!7@$aq+gGR9YVoXajuenE4KtFNP>Mb;JdUr6fKBz#Ys z$&gNQ!<;$z?bzT%^{VfW&hPQ!H({BKMzTD+6Bm-l!52E#lphM~7hpYCd?rz82G^QB zu1RJ>{0uNk@as9o&zGHpaxfXGXx#*0!)_$>oe3gl^zOs(alK-NnvNKaZ2EMy;X=Ni znNK%*C%!OynwZE?x|@cEUTrDnC9*`^-+N&9yo&Ern(BMPcjOd%qv0Wa`bs!&U*0#I zx0r&H2}(C9Xr zDsLF4zeBVkRYqR`xjNph1PiYWmmQhU*zp~~eZ%vV8cW3AE;l@@=`2(L>2KOWL%(LH zdxvV*@6k2E z44fz{7kl3>Nnkzx&C>4z#&p^d%kZI(BVt+-r#4vw%!gKx2B1?_1o~RWk4Td+?X|$# zuHgK)+rYtAAmJP`Tsvu~>w$OduG9^NUCcDF0kSHp?3zwNcBvjAsU&oWEk>@6?@CxD z$2*e}L92h?i$e0y?tZ4>NI%x2`#6C~9GVN+`Lb%qnu+#Kv7dUlSUJ-ZY)|T_&hTqaRaiW1 z)adb|Os$uj&*^ z#vi}~Sy<9q**9A>=)c z+vFiI)^YO)m`bT#VTWx~ynmPE)b?psYWppB5M1MPYj}$68<|?U@L9@s-hOdy&aJE; zKqzU2UfT|59=T>q`RgQ}hCzohXXdgybSIy!3SM%}t@q-WR-X&b?jvDhv?>OU>ph#w3_KmX%M#cn-1h@Ob5I z1+zsv0q0T%$7wLQR|JZP^yU;#B6ROx?1yV@GOf2-*b1^^GJY4ROOmefIZFsOt`+#b z8vops#W7fiOHe$`K!O zg$xXQsoncoy3$|`o?A``@B4sKKEY`-l@^g<6*2_^QbPpv+Wfu=Ks01}>HZ2YW$nb! zOxIut5_Mx=W8P8wcP;%cX(TBgpkct=Yq?_YJtFP*gv1Sv_g+JC68p9_PHLROJ>?8L zKRv1#*FGt*MX~%%rTxu(n*&-7pGXu^*E+_HuiG#1(^`Px@qL~ym=bl%yG=qBqA=^) z54ytkI)O{AAoQhH-STZq+g`r;ty&;OWEfyCju;j%Fkzota-V=3s?}N%o+7<(m>msK zW}V!F_9reV%-eCCw8h(dkbiEq7Kx%GzX52<4*uH~Z4?9uhyWQS`SnSbU2a8{h4Tq1 z8|I^6*_sg6ReKW*^L6}O}WE0dv6*Xsms{aTZ60dgY164PU@oe z5&o`T1p@7TABXlKPc!_)UWk8^@vJ+mp)DTwwfurx#cRot=Hm-FvJvogCH8F^fr@P! zoc{4~OH>cC(f)9OmpS#aEkR~7&9FoO8B!Y9G!oZZ|yVo9j z!_%SEGV50E*VJ2UsqRxzBvUn-xAWXE+%WRP^ZI4kd;zz)xn<(CO)}%*EnJC8YfP`z z%uUWZ6_RtM^#n%bXWXS?l_gj?NtH5gu`Q|H0JHBT_gNx?v86^}tgY$zucEuz+K!Ju zXDvhpV~KvmjdNp#dm9yKW{o&H#a0$OuvT*T;qhP}buDs)?zpi>nqo7bCH7c3cYSR! zHfp`darZ4U0`58s2K0Pfaut9EYq~ZOF77YXw!aG0mSFN|x%;%U8NWDDavN+J9^E?I z#glGd!VKAV*WP_MSE~ZmCTUfjNwsADvN*5s;kY`ws{$_7yl-Yv+QV;FEH)C(!VFtl z-OarGYC6)Iwp z!}I#eO>g`5M3V~pt`{<;2;uWd+b4NW%|qvKT;5ywS4IY?X);1#Df@(RVg2u`#kwUG zRB78b7p}ix=!VX={XrmM#!5YL^7)mobn-%7v<#xic$&W4jsmh=cCkFrdOzak}pXXkabSe!2~mF^GVgksp#qKRWM;HrfD!O zYd1JK0RQ>M1B08Elw38I$TYuFnrXaiD){3BkBnV%qI(HrJ`Y3rGUivzcV5e6(3P@i zjTW}$@*T{295cQU#&7s~fu-iIMNel+9^k9p%vybw(rC}L*!PUglXu|f)@V1bgJ*8wbA!U1s5_oykS4U~!5=dtHU>2LaBwb_TdDy!f!1RZ_a+3UGV zvAj6PoNj^EzjE0jBcN(ju8Zk*kuHPGyu`l21uhLA;|7k9Jc|AxBjf`t1KiN!Bj8u!2X2&}W`{-mkB03f7h zs67K$ET2_ntBKaYh0pmF-6F2tq;PZJv`!(PfJ(qWNDk?<^+5GhP3EF?h@m>$c<0<9 z!rm3B=_nws7&0^dVK`MLz*hV7V++CC0Os|P%};3y&AhdPGl%PIPuW7Lujr$0C^sAztnmmPWdcp zBgH73#G%L@7JYuLX@fyX1I(_9k?REsE{O4@ z_Bk|ytf4|vx>b};3^?T$o_AVg zRVa6%VE0n`4%}Hej{|fo_cPqABt}@T=Of_U2gs`sk+=tlw?mpP!-1?cB$=2+ob!I@ zAXLk;^afx7d_4m6({l5yMJE%6k!lox$GQD<`j_GjD*|4VX)iJ0)p8dKh)`hD1=P^@ zG4Y5&L+uD`U|%`r=)-@|I5e@?b5%4L*!v1?1p>FB+5N6)AxA0PZau2NlDa80+Ux0O zbIxZ6SWUp2&c_x5!2|zbmuU5J&3SXiU5(Rf+J%n1^S;CfS%cIi2{GW@s@a=@vgtDb z0y_PR(OH&Gl{`jMulVOD$Tp8>0D?)dbEb`cJLfC(r!mYP0{0GzcQSH}-M>t5OpbZ8>iKUnK-qXTz85FU&Jf`Hv3qH{~E(Q<9C zXT1s+25lvLQAQOl1fE{H{0jr3$zcHavv!Rc2MASFTOq5BB_K^Lp!kjai_$Lj`l4Y$ z?SFoq9T8#B^A^w?3Ftt9oEv9rCA>h8J}e;x`;38I?$2rNLVK@a4+wzM08L=6AR9o#+OV zj*M8yjt$40-MXPK7pGtzTmcPM0C>RC6MFL1L@cDsn><=#*lLXot z@N;PFWx&lc*LI&?y~UF&uJ)Pf`0}_<&=bGfYM}hI3>ZJ~jZL3Sk5>VelP>WK3;#~i zX%=lhu7YaDCCJ!bqT5=aM+I~`lI*VRqH)A`f3>#UcW>3bzIkC4upV3i>ejki?3Aw= zeJsk=3ZSaicG>8z!r&hK2{^*fEujbWXgo>?;XF{oT>Qg8fNk91Sq14iDqg&WnR=>i zCugdpn)PSZ?3bc8>ouUq)a{tAW!6#qR?gi2`_#qv6%T++5JO@}vzZj&#g>s!x$L=y`9bYxN52c)5*?`Vt@=t(U%>Z2Ns9iX6Y|5x+I`U`N@Rck+FfD_c1=7OKn zI1E_IBl19LBgpP5x6l}pDwp3Gno(K}6aBgJ%88s3NM%rcw#p7;Jw(^47y@s86WCl6 zQeKxuAbD&ZOsGv>1C`?ed3~4iE=DabMz=8T<{uv1oQ(bV>bkTGi$A*Qps#oI0@8IX z7<&5&AY^|-mt6v?(H!5*c_p;wsPdfj@7ui{G5A zBNSSdrl@^%HPZS+B##1JwB!TtT@-p`OB@8N0+AXqLjR5?HRUqz52SWQvH|3@qQ*98ZLXuwp~Fzo0>+ByRVcs4-EKS?c~ zoctq@#tGw61NnVl{RM)e^-KMM3Svy0bOLUx@XcyFCjbzs;(w`KbZARe+^?-QB*5}& ztW}=@LB7DElSns69))>OKMRRu0Vv+sBWV{?^m)iCS{bg2RIjL<4ZQ*W!a9Zvu(gHp2ONdv@`HizNDO`X zx!_-f_!^kOqDL(!Yye^)f1z~%7>>XP)wDal(r?|ku{35$x0{V1*6?k82CNr1^9fPF|_J@3h@P@)UyeKDeRVDnP35`*XQK zWZ>R=E_6Sh`yhYJlxpXY=J;21yNn8WP>z<^r%|!zuD0FmIhg#sXgOe9&z=JavgoXN zB-c2VzzEla7yi5Nc6wAUKLaoG=`Bg&|7Z=featl%d&e1?KoQ1O>572Ihr3<(J9Cnw z4#V>JA*GLON7&R7K+-z}foW$HKNq zbFrTFE6o7}x2XX9rfIu7U=s2wn=2RrH zVC}T^C5@lyI3c{WT^|k2)3Xc#^+il;%M762n3j7$+j=4^?Fh{09`g^QHGQBY|4bU) z?S>x;Re{^@%Bhs9*dh)Kt1g<~?lpg1T-!orVRPruuJDlxq@qX;#2%#qS*U=Vp&Ske zde;5JTxcBI$7zjW$&=-$_bhS7WO;k*h5x1)LpNS{VhXl`iueuw`S*hVD!Kh}gS%S} z{UkJ(4B9RJ{+x=%?%sD_@BP#k7cvj3|MdrW{+a1DqvsouU z#jz;o9)@sJ3Yj~C^DD9>5z8R&zo_#S*@G`9O$@-|?VvFTn0y2iKkUZa^%)5SMui?` zFyEi`db=d%y6#?wXCHV&|S@rY*M{1DX9}rdx=#KGzuRJ4}&U6MmHm9n}~9 z>U||##T}>xtAGl}`6=H3yMZk?9co<42u^?5JeR@1-@I8FH!m~4unp4-ncp96)VMDd z8}(cmU-iW`Xfs#o#;MSylvZuIi)_YQBD#O^?~P3yTO7$^OJ$%0_YELESpoNzuW_nx zz!)y~nXj^g9}DN!BnC7V4Rvu8{#FAPF~oLQ;iuk7z+$P7xjkps1qDF3x#eiSbu1$B z5_1g?GX3`1=7mx&z?D5JYuf(a!+7A?*R#fS(a;mjbo%LsLiSMK5-?MY`2xM;1vKMx zlE1kSW`ENqACkfZXk=pJ4Tgr+amnM|uo1GDi}RBV%$d(ZlOpfl0$Up>?5h_0T+J&o zJ4CjkImhii{+10{2#DtDAy>}!)$m<^p^10KxtSM&03b3hFEUnsods$jXDQ>!__(DOHTQ`*TPrfh0 zWs)B!@Nmf)XxoYTyX`1!%iIcz-S}5R-zN(!WZt`;4#};us(3B*`2!{(0XimJk35m< z4E;c%e+C}3j9cIc+|Qq$$iD@4U+TJk(^X4xn^w7eKGZf%0r=29=J!QK zn-Q_CNdvXL5%$}yH7m2gg7G--g*n!GB?=ncDG5#=1JO}-_lxAA21UC2*O>1vG12*| z@)DL;Ts3`fpVlPFE^R7{Zi^eMsqQx>bx*DP3Bw$X^Q^(8mc4hwxaJk2 zprl5Rr1!uIx>a9;PkED$fz%JKI7>c4~=KCwgJ;}O-`pyJF#1YebQ`>Ua%iz#HEBy=ghVN9!1 zK8#xQIz!kFK$~3JY(LiQh$9L?t=vU_vvvIi)uo2JJ`!6SFm4SzL$UggDGSC(Zima= zf7g&8(&LN##ncORkJH%4=dyfWph3w(soJ^rp14Wx0JrFFaAo z6UBdw0t2B*e(AWP_K8F5xu={y!dIn!YUZv~P?FhZQCfrw$%b!g;rnDJ?3xNz20EiP zi7aVlNEK3l?utm%?NXza(yd`iYG>(B=GyM05irCCo%WP%k)QABziDG{6H3yv&b{pB z&ZQ|Iqt&eou6W(IU9mQ`u6uVSwTJjcYuopAXTg?ljFH)rsBuc{L*K`y=bN*>bZtMh zb=quQTAaRr$nsg;nC~+A&Tjv1w+mjI;W9yGvOH_w<59*L?4^+9W#o2~ducwc+}gkX z<}5)+D#~_M-QuLoV^Ih-VQuVG_9NP`HC|)S@Y{aB26fNb?3DkdSX=8KCLhnE*u(=t z3HESXLv1ZLT+kWz#l#vo!*BzfF?&laFL2iomv<`8)3OAZ#*f7XJT<9&v6?;56krGliWc~eoF_NiFSu$ z^ZwLzb9bapY{ z9`>Nq;^emi957?FZF!4(`hzVwf3$N*94CmA_V?Q3{&Cy(%X0czmJ>9987LXCcuMA! zACHGUy}SAe{?t7^!9)Tn6xjB6;eg4KEo}(8)PY=$7BH4PuZ(BBY7e{S4n>z$!5mk> z*RzXZvBDFsZY*4}0P;a}QF}BF7=@Yg376cV(`c-wl*9g1Ohdp#Q3ea!uc863cCkxW z_h5d!+S~WQ#GrLkNr2wc>$HGb%5YAc`t0Y~4+lmrqKmP>wY1jLFTom)(^eD&b2KcO zuAA$AfP`6LML6pc3%Oy?yiC?J+80Hk{a-`LRkC>6-SA)wk$v6rkymX$_ua}Fa`BQG zP8{qSF1`ZT_Np|n$FPbPd`EFFcK)6KOS_d;bS9x_K-b*w^5{*7Ok)2MJ>7a<8*L@? zcP3bK;qLZ=`L<`nEZJ!{iOJ-8l}6wnQ}HTnPh~Paa;+N?? zxj?c?G9~<=&ztS{8y}kA!(e~-*+O23H!eLvPA7=mD5>jZBzv$}gf%HWd)SxNv9`8y zzrVvGEhSCETP{r=M%fZRD+RDI&(wzccidttjzQiNIuxnMiP9<3+a{2q_{%O-WTPmKqrOx;|Vi}T=j>L2hqZM&bCGzQ7f;nSl^mC<_KGGJ8 zKRA+T>}%YWquJ=Rc5rZT1HQGE%VfkxFb*B~Kwo+yQ!+g(Zfg87n|6tuyYxtTx5dtV zH7#0(ii|6L)GFm&!a~>2^?1$ouNoGGcSn;@SNFfA=U!f1ja=0n{C_}|#bHHE0}kd( z2_;BMw}`t)sf}*}#o@U<0pcCQ&foInBjqX|6~R;BGI1ABlT>DbMr4w38;sZQG6Inr z+|MZhXEcr$@hgnmKR8H}w6~WokWQRybm?-k!Szow0MZgk{Mh%NZZ4Zh_wJ{dVy_2B zF%-)kd#`pGIwxJ3T^jG_H=iYs5B?E5Gm7&A?9>;ola#8t34}|>sr4TlCyX0G`;{4u z%eXF3VH$O$vBJdn_uHky7S zBNMp`f89J>z3=H{<5%U-#XUVFa?zN5pXLbj!mU-zV;_>2k#@idse@XjS(pRN+n9rC z@;oQ={Cj$K@Ef)lhLjA2^3Fh35ME0n-UP}lw9x(KcfhwxzILxy0Tz@8oFbNgfYHg9 zXOqB%ycR2?@L@#2@Cv6r{(%`VhMg&O;;0?HdfcCBzA_`rL`jAG1B^T-Go$KBs?o^H zWn7tt1Zw!mvzjV3MfXygs+T0`D3!6G`1@uq-&xqhkZK(?4ROIdzLJ#KAzQZ>ZFdCu z8ejcmF;6_u|hF zc(e#2T&_VRcEv)@K&FZ21fr@+kWv31H%s%syyZWumhoK9<6{x&T6k;dOj=FAn)?-A z_Ofg4Dn7gWh6<8{@O9$yO<>`}Mc3qj%V^;4{Q*AX;R@Sf@^;t%u0yl5wUHhSj--gGlwsYIISFi+rSG|}X zOa`_h^m28e+6x8o>5#pC(7Q@#7HV}pg@PSa4e%T65oftcw>zwujhE*a9o={7YRohQ zc6e>|9Ds+$D?sxth~}GO1>J!HOWlH{*aEIAYlou6sCDtM8!y8zjC~SxVC*dD2OvS6 z`7Pk!8?gS^-32>+;7CoBY7Koc*3k${*1Q~@cPu;Wf9F>ic@XhPxSudd+Qb~RQ&r#C zBSF`bpc&urHCe1WSAN^j1xgd9$|)1rLF0c!VNt&{R>cC{aY){hNh&!We{-r&&ObGk_hqQb(dvNz@cq$N3un~XlOaW;w zGx#fM7!*6w^O~OcxHA2t_SRD-Cv}e_YnS}GQ6r|r6Umaq)zqcAXTN*s9g|LwPG{j`{Kj;6%mA<1k9{z`LWLqd~7)nQHs=2zT~RN0s+ZwWn5 z$^~x51RlAKf#-;vi{sYRyxOuBJoz7{E`MHkUW z*xVz^MGab|UR&A=eZ}X&r9B=+PZT-_Ogr?B=;qZm-pS~a7@vSN4nNLrOoP2Y$D<-} zOS@s_tBT2*T5J*q-t8P-e$I%uYj;XWV&%6X}yEi*w40 zA1b@_GX_`0cKySxp*}63==g8)@9t{Q?TP1%_hia3}GZTE{;6cr1KOKt-4G%K?dHO}*kxMduWM%$T z#Ix)vV%vw8u7Qbc)Zs;#uu@L5IWgN#fS5m}haNV2_VE}(ECnrXk-96DJar`Ua*Hj2 zV%`qy*Ff^JCQ_!IV;Oj0c>-&pp|ceO!i(5{!zPA>eIId&mNMm+5d=DDIbIJBF9=_z zTZ;@8nRglaJbQ-LjG)+5wC5M5aeqZwTP-P3HxUmD%>tDlExVuVleBf5F=an@#r6@B zA;Kya$>A>r`X`P=CMyP+?DQ)11h5q=zjdo$(29#TU6yv&_>nO(9n==U>YH1H^j%%Q z&FZ+IoHx1pV)C%{sozzEp1$Wh!VpGxdOKVN*1LtJn}s0kwwEEB3S3Pc7?A#ZU{bL3 zS{ZdKK5yOAV4`!^3#bw4`@$ogc<&di%hx$McBf?R#mvb^N&?PY5D#^qq!Z4yX&>Xb*}^E z;Ny2lM%!o%uRuP?yJJcj*1YsT2N1o}vB#?$!%C@=gRCT|BADrTyT_AiyKilKbFF(< z@~YHIvixgVKL`sO*ZLxh>|u^s7UrUtCy%PQX&hW`A$=9e7!0>#&WBLYNv41DE-aoA z6}--Nmwg3$#9-GQfI=N+Gf(v$Tr~}M>^1?xAXejR6*Di-XE#>J?KU~400DmEBk{K; z1swN8+X>sIU%ocElJ7(w;Y`BLI8ZviM7W6*NWw~<_~Vx@C4mqrbM2p3i3>>$xq62 zR+^VQ{(ct=cREdOYdw)@tNzKw9W&u=h!F;fL-xK#D8&`l&u3sbfZRXY{&~Tmbk=8; zh4Z14ISDlJfrbo$nd>aVq+n%hL%;m?I{0px2e`Mh?$e4#cx)q9ti9fw!gMtno~lgX zb35g5^QuFQq#_RdsWpB4NP4_*)bt|9ZCOpGQtbmc6l{gN*l{s9u1lsN;J4akk5Z(PM3gU3|eIDX3{RDLP?XuGToT?2kRk?L2K_J$how$rY|RAZ8o znNf>Qf_fV(`y#jMpji2%gcS*h&Iqb3?9;_{NbJ_b#bF{%POoku&&*8IJv8k7rBcai zCyLSIw&|0lmMwJO7QoI4isrc-l6mSPMpPE5M=H)fv7;c9?MBn=57i{p|#6Qoqt#KG#A&+bmiF8|w9&a0HJ%f@r7hY2SS z)cHxpZTWVA^drPFiqwb*qctp#J5m8{SMRR_XW4;5c3ta)-jDk`=N4m1L zPJKz1FkRk!nsTq<`glN{Y*fQM%}$c^9u^@gpXT*rj=^%LkZe{8SB2CWatud46APcZ zy8L(A4GA)ug5nivC-xC2${1AcePV6JaOTeRM^q9gHV;@IPlXcyL@0}_aQh!~4=m7> z3BDIgFat)f+@lz)kKs@%UC##z{SBx@5PX74keERzgr&0!O3=W|?Wih8g`317tU`Ul z8f(`>9rvf&4qm>6>mvpYGurAnK;gAHeThCuC}2X$EhlQ~Q89*VRIm#3_Vy$->mrnX z7ivc(jF5tNl7&tV@m)IRz;WR;<`E(78@PVUxW8U9?x6g{lejNqoBg{X3-(n=md5`1 zRmIj((lOIQ3KNxdvwoTGM7EIYBZ-D1mn)XGWB5#9LLwQ8%0k}Aa5(y`h*ARVy)$%P%*t#yYJ#0 z?QrV}hU~2CUwG}E0C$F6lk^t0Ohwpb;Fy`1&eh`TdQc4`vf$jFqBZ+({SQ;^6uU)Z z#wMtNE9*mXKg-UGSy!LmCRxl(fOkuka~RF_h~PKJpVOx!JaU`9`N6A?lNwP;^vn5> z0Xmz&H(J{BvSlfv6Ruu;3;4sekcG>P5qKffWH29)mPF>&an4Upm9MQxwwPL^exqiv zqH(oaA4RDdNtW4@XE{zpjgK$?aErAYEkfewi-crFhqe!{$_^ficd1JC3E2fXQDj-f zZY^C8E%A9boAVcSM)040Ode9wJD8bNz$7@&!dd9w7IXRLaf-wYBb4YpR*CfDLX`=yaYjZ?L9pMuXYzIVa{KbU)`bQkO>vRe>hpY@) zs`(dx(GYpJ2bmP>?aKMrB_DLt`^(T?;_|E0IRDK(fbG=#*=F>G3VAd~y>;(YLSS;a zoTcY7<7A!K(+ZEk|%N7m_zTGAza`*Tdb=LsW$INpenJO}0sFQ0z6P z$L(+DbiG_FZLhks2qShM^|nCbnVI_T{=?OvHVZ|ThYe-2i1$LDEV0|I8cMFN-)^dnu@nLtZ|N04;B@kI-dqF@z$0-nk zs7%Uc(fLWa*pjbis@0Q{ya}F5pCwYOh?uuv{)>1{1iM4?%b|m+r>fz~p;*z9KNyB} zCkA<-W5KORxna|_wpfiWc0|H!IhN`&3GmlgvdFoxaiK5?!E;>s5Uln-E?x7Anc%iw zj2uq`o7?_2N5=@d$Fco^LF9$~4srb}D6K)!tI0e+NIw%Zl`|oe~l`Sji9ppp{9vdN&bua^rEpuXo{BpP><6!$ zvW6n217g!ZK;;RB3g;20}z<&oTy`$82ba2G;O(pIb~5k!D2h`C6o8K6qXVe==BHG`c}(jLP4@f zi0$CD`W{rouqV~xkD{$jb_ug1f?>Ok5Wh8?6}B8h2)Et7r*XuhZyVR2Gc!1cm7Jz6 z(fbAxWidjTd zCiS2@qSzt^GpTKys({{V_*5V=019=F$ zj{>OhT$VCTsCAJHvah6vPIdTB`$ek3d*VHj+9F9F^{rub2YY^ z`qNNkt&>Y_``1Y;dV;twSy(?j)i;4r)QGO)bzQNMXumo&2u=fWEeIxVMwa}wA1@Vr z5D`~htc#$uY3h_0dPZ3dt$&@2NR$lKcVeYUicDuyigP1jq?A#L(;z68S0>TR=HL!d zSglVTG3=0yB4V&QX~+7ln0XlheXRaCmNW+K9X~p+$?g2(*_R$(ri%MoA%RMHhi9IE z#7p7Xj53?Dm3Gwq{!{X}_~$TBIOS(NhO6&q7j+vmXxs)frZ%<{8jOg-S&N=Va^2keLD zy^WAV0vt(6-eA!vUb4l5t*AdX3Id$U6uEW&7q zWz}R|cW7P7p4Kl4OP+^O;}r4z*84i1@=azoY+#kRaV659q+eh_jrBD(aGD31r82b5QYLu9pG7v=_WDS39MfBcRFJ|KCryrHiYOMYU$L`d1r!HQ*$gPZy6fBj+d!^4Did=gkH4)@fu2(i0XH%hAhL!D*ZS_B!>tTwc- z?MQZ+RA4Y4`amPtZ$aXuj6;k`V>ift0G}2MA*8p|wPjqBkIGh!uU7C)P{|QcS>?El zj2692=G~&lF01Qe_v#e6Ns=%v&s`CK>j_dyzzvXRIW8^lKr}Ok-G@+vWqNcDOp{Dj zJ3LIk0NnS|$YEL+CL+{pOx%q?iRQ!%4M7VFFm_%Lw{es$D!a_JGyM4Eq1*Ga4Rud! zug=;Do~ysZjd5!Xu61ACBri|vf;BIVk2y*&iTN(Y796n~@AFTs3%I7$PvUGy985Mt`bTSGWUX|18Xq)NtfW7W2S|{_w`Owpg z{LSZ7N}I<1*wA!E;+ozVhT+hU{d`Dlh<~Zs^sUsD;xG^O_{CIk@R}LBN)32-qocLC zzc!Z0azljay48*NaT}O)MyeiM%xg)b`)EUZ{&@V-Lw#mwU)fQg0EOs z)oj+2GA_?PaoPmh1*^PL8_g^WL*Q}s!};jgM9W5zb&5**WX~0WNZi*>oWTcnUyXWZ zCTKHU*feP^@4Zg!Uf7Q;RBgJiFWXM*#a=H@tm51dJH|CBq)HX^6(eZ3p*fT(Di}qz znV^JkVz&Ct(J4EqaFjOzkCNfx&!k8R2}+!v-U^)}?y8hIHMk{9R#f?F@L)U6k#p#X zRaDln1lva7JZe5O;!F2?-S2QEF3esWh9h1qMJMv^-T1-Z6f>M6K5Rprc;&pM!`g`+ zJ(0xl11p&_S+)Z^3>QU(>X>0(}EpOWFBREm0k)(Or~q zj*VkCF5Tdr}gNpQL|a|E7(?q zS7pHWICY+FSP}^ct9wb)JFICl?cafEb~w_@>3xKrThE9T@-eiBo<3LFqxG`3@&yg@ z)y@F+wsj__^T#*c{z&_u9rVX#;w7)(Z=a|nRwLcKrp33Dq91n1?me}aEED?NJNtNs zG*e~xmFSGRabX+o>oHo^Ji|y66iKVi$E&cNbd|E6X0VFMS)k^nLZ)H^VXhg8{_h;d z2_mntFde2)?u-d`@y^9%y{Spq1*6X|U3x+O^c7c6TrV&ULv_o+<$a%JCw{i~irjH4 z`vZalYSm%mb?ux3u2=7j-|2*!@DZ*XmE( zmk?0`Zv{;4Asq!17SdcKsQVJ{kqsrPcB9edYnKfIGAjz^VQqaErlt{YM=W8Njh;On zmlZhqKh5-YwT=QXq&rnS=$y?RVg}i97*GMAir?AVBYBMrMZ0#uNp5VS&^zZ;-1$Gp z-3yoBl3S7ox;g%)!o+?yEFaLqd+B-Z1;~di-<#=Gly_4yHrJIjY^flFbKy^547+HG z$BAHia>^?Gnsf*(RQ=+(0mB!}$|oh-aJ&b9!8S}P*-yi^L8~T=GZ|B1t@p`}nsH#1 z`~e%+(ZlD>G1eByDf zmJQ1>#FJ`kaBdJTX{aXDkR*NHt)rk49MeMjY)Hnhge(Y;1$HR+nNi(iCuxq#;JAX9 z++53+W$YRUMQSw@Fe$ulGrZ^+o_^_0RP{kh8iQ~IIX$1K2v;j(zFlGFSBwR_Kz{fZ z`u(_Pa~isvPV)|DhLk=eEw|qvqS$Idqj$5cpp3c~H|3GrtkWHS@?>O_Y9^bxlZU$c z-J>~c$xXF4Xn~O zR)6{C^4)UU%izTi9yJBH1!)v|7GDQXv&nm2bQ;h?1S+TiF|W7{G!vvYNGhXfj>>9| zXP!0&&=~j>r6jbxi;8xrPRy2PTvrSb?~`NZ%ck{8VE%ZDD7Cd3MGA}v+P=c{{KX#C zo2lPBn(4ck=ESXBuP3hQTH`p?feu6^Lb50MT@!$I6{=w8$~)pXtG-m1(nOl0pM91@ zM|tubzo<_1B-kk(ql6BQ+GP)TP6$ll-0KjK4G6-RsrGMu4QAZ%(Ji6dSh2XvqQ3@P zi65&5fAuF0wg%z`pr=E9mK|q#EY5IF&-)W!boU-TD?-kmB@OKyFiG7c;cS-jBw2DY zPP@h6VG~8QqJn>{H)UwpfOupoenUd}P84nt-X}GLv%|kjIQFZ1Ty&#Ll0n&9CCXy^ zOBCVN8(D<90e2#e|CdI*McSi3h4GoO$}1r2&Pj4^Zs4C~CH$J&_w@6hnP?O}q8hbKV|Ect#GrAw7&uqs%-WL@2ziT}gv1 z0!hdOlx-cC?|Q_H+TOJ+XN7+#mLeef%4No{wnQ*}#`4=QwZgvi&Vwy3{yni;!T0^H zkh@p(>QJ$7jvE_L%xhFb``PY=C5W5$v#JXC-2{8)7Z7(`>{#n>S<4XfqD|o>_3;UU^>GyiN*o5+`e+Sp zIG+q>2s1tSC+udAj|lFrGLl<43E1!uwEv}}u=;j5LLdRWw;SgDqmFO%gk{Fm!ZJv5 zcJWaZzL?vuAO67&*6pJH=Wo{DE0xMo12>I(`PCdO(r~60u0U^ocH^+xL+%JS?Q9`= zFcq0-J+Cj8RX&~xHF6FEq-R~5bpQL=+Ouf;Ia197owVUxQj#35k9g%on)6I8%+SVe z>=Od_{(}x&Pza0Q0?*aA{ZTz>uehM|ugvz2AX$~e&r{Vc?EH5i6v)vO+}ilk zvErQKXdrFDoTM}a8-~Mqcx{$IeQ|rLkc1e5ljp7tSqhBRUJZ#bVm0upeYBy${VnHBG}#;4J6+7pYlQ6quGZ zjn4O?5bXM6J!ot9n>j|KGYGMboh8mV_yZL=>78%k>;$|zs(|BQDVvTKa~vQj+7rv^GR7^ zVsf*IgGG9&HdE&Nt*|`uE-eU*SJlZOH_OfE=Clk%7?P0#8h(3wFh(6 zfDm$)Y%S{<-y5w2aXSka7-3fUz*{$klvErab|W{OKjO#IWk}qf4q4OqVxvXIF{7g3 zUoiN~Wl zfliCB-~FH%*K=|{F>fW7j4xsMH06S)N^`2iI~nPH_&Ia9BChNgGyV_Weq%&< zBcp4%FeyzDR{LSML)=T1$7hl$s_vGBOA+)wL84)$Zd9(Cs`u+&*W|6v``V=MRBhE{ zc$LP}I23ct#msekSE-dSA59gBega_T$h=cseQN&?MfCFfBW+1y?@8BU*W5}wJ6+Lp z9S1T6+9GTYouFYZ@3JMa*J`>x%4p;F71nEkm3xo!M@-$zrt01RF}OZaH%!d;b1bF3 z=FM?(5boc~%*Z#H{#RsWvXEXHLk%O6e39yV*sa@MpJnFBUDV(79*FZy`H_*BxAazr zg}x77It#MuNlIYzfF?PlImC_$yu?KlxMR3l!67HhOK}-DV-Z|0W0;gpUf|MiY7e#A zc1L-bypYnwGFTvKQjXF@Pa`8{VK!(TxKStws!- z;I~bqu?E%ilXDZJ{p=5`bU3=+JbjZ{TQec$P7PLiL>bBW>9Gw*(Zps2;~{}u{oTR| zN^g(c7ql2{v9=a&!rQVsB6Ot(AL;&W_2d&PPccG2Am+tXzQUcnYIU_guT_d(3HYNM zZ9J+c24coCG33GN7U+g$!%oP70W3-pu*k^;x{j9ZY70gP*Cd)khN|&Pcsz%A%Rlm* zWK;oO+4r;Bh>F`0W{9^ocA}3LIjK`+D8GK2LIdQC>yF|by8{w!?fdXXoOJ0e5;5Z9 z)d9U;*w%~^k`>`jOu63k_?op*CFm&#^z=IzI3$P513X7L*i3J8iwPsSFJOg?RbSR1Sy9<`% zau5~S1TqBV*i5~Tnvv~%%3mayL3pA?Fby-eGmUvKt+b)}w6~1RjKxvgd5fQA(T(64 zJ7uNPAD^1aSr6TZ;3>kWr&3}0$G$&!5=k#dYNiAH9?VaU?86>WI+z~yl4KML<*O*k zEb~EyHVQ_v%7!!6`YAKR|?-1ZR~DHP&^10nOHe% z;R!yNsu6jp-$sgSmBX;it}O!{VW1Su&Yyem~E4 zw|kbBp|ni1Re8*$ON)$5MLg&guJjE&40gP?KPp6tOsi>Tvp6@DVt-VYzu1a% z5y@omG&0=Lt3dG^303aBYW2dlhRNTcCRSGSfJGI*ky!WD*g;Bidd4I}=|LLPAwSCD z0^?-yc9KNiAN-h7>jw$j=~+$tl3WtBAIr_g$rlRJJl-VxSeQti&jj8Jh9*8jzS!_D z79?EsFxupC8R&P&!f>(2#mdV_rovTXs`x(qE%ri}?iSktr-|+R+!S$7gXf7fAhM>WCd~5RFPyA6P%C_D z3-%*uN}|99dg)3LaY;u0Fvi;HdCKdMHzhyf-FpXHlhop1?(jbwutay6Sxrsy^Q;?L ziIpQ6z(j`h2z_l47%oK2wGK!Y+>sbK;{nusG|al&LD2ZkE6l8t%!l}(t?&9`^lCY% zutb(+7F5rnFiC*O*FjQ>25w|~yX%cwuF@^P#a#RmQegF?R0;z< z>UDjo-+8XP_7S4dGFp}=S=lRY1hSLoGhx7zzQb+k&Uepo-Y#?mW>!jbS{o$bH`wYT zdPzVCa*gPOm9ou@vs*p$L%nV9&4X{(y?9zQeXEsmc9QW^G>o^#+?*BFC?lKJU}AlP{!s! zGB;SN+rViqvFptO0w>~K7jM_KmD5?itU|J2R1}5ZzD4e#l8oh2L^=AKWlS{O?wF;J zO&Lc4l{H^Z#+%byS2hAcSp0k9*|1w(xg(ZIXHeSCJN$JUrD|v{ex*Y3D3awSu^ua2 z3_7Nz2-I1$(yfdm((eoD=loZqwtHZ%6lv0(^mlW>aXXvT@q&nj@XUh^v{5RFwha_7 z6Yp(;IR8Q3n}NjJ9WnXG1?DOM$%(y(qdFl&JvYuI_UArT_Yv&LH25Fn#cPwOLG_9# z*l#m9WXLWwj4w1aR*ev&mQL`kvO5!->pkxMIX-3-^$nka3_*g?hn?kKzvWjKSS%u~ zL!#z|%>*htZA>!c`TtuV7J|hmmWZp`ynB(_n3aa2H_8#ZJv~9fqd8nD5+`m1q$dYs z(2)i0+P!2g0o2TTsXTN;Lc6Ig#ow8Df9U}hgP;1NpUvXWQFmv^P49V6xL#V$A@Vr) zBn`dQ?&EFlFXd1<9n4!YlO2nwne-0cS1v-9h!^??uSo~nIo<4jCeB&VOCYL8($)NR zMT?_dD`7LMO#(rxypgKHX>?3a5@L5VDy~6ZlA5mNKH2UXu&XmXDq6K4OqOu7B2-XQ zn0 zhZ?8qz6 z-8bbN-?MljjeG`s5g~f?J6+AKg&M@2-RM>Djl(a_Cx8KMYiCkTT0>pqoYbC3*a9`^ zEVGkJq^T>@94@*9N3sZY#rYIexHv~+?9>)CSS$Z-OW=$wKa@&xKxXP`q%uDF{-m;+ zjDc(YlP=9NXG1oLuO?~wn7gJrq8KgIDW2_Yx)4?o=#ezCgS}3Bd_d9+fy)mZznFS; zVGdaqhI8?+Tx;u^le#z^rYdaK-MTwI5s1|AD_=b6eCp^auIFJoR^>ULG8lt2-k)ZY z8Y;1>GTQ$I@u9u+k7;|xQlWpyo;lJd(qGG9{^j$YQaH`R^t}@!*Jpu}Dx*EmA^ld< zpn!-P|62aNRC#U>9^dEtlBTdBtrhEr)kX)|z3mrD#m`IX8YzWZNw2t*Po(TOBPz;g zoe~Z_p)NOmthWrttVwy{Sy@~4@-S1L7Mg5p86w|XYRklL5W-AH59q$tON<^8;qz*k)o(s&2r=AD=#6G_o(TUe`8shwPWKWu*5 z)0;9YG0UEPh}}_hu}S_8aheT%5zBsGGb@Ok|as zk*dpI(%L|yLIkNgz257+d{#djWS`z(b#F)@6{Jlqv5)f>TsW4q0$AN(z%068+&isB zN>j3!KFCj6VO`Kb5UX>Owqr2LHnmz2cbA?B?iLr>3fCH7aogQmfvIcMdA|EDyW2ge zO}a`D{Y`xJY#@+u`1{hYNAp+>Qt>wCxID~VfgEOv8*FwTeupELofLahEXF4iR*=j!p0mrF)2|-3taZ2U0I&u@p2MlpU z?85__og1Rh&?R*Bw+sRXT2ToFz28ettdX!u(}*$Rcb#BHuyv9Yi>~j(9xkAG5rnsM zJ9XSYk8_PEkKe6hFi%kwpSKN$@=*0T+zPA>mEdDBwH>1dLR&22Ise)I|l=9_s&Pn??0js+OV6Q!hodHM) zBpvWP(Xat&)?Pt((PwEp-e*1?EIUl7wNf-Zk-fIf71B8T^U*KJ>7ursdeR2Ss|A+V zV&LXgnf)Gy>fp?W4rw!UA2q=~^Ia5tpC-XQbt^3WW5=;5M9v9g7{_vWV$*e3QWSZsqQc| z|I}Hjk7L2Wb(LiNHUG}}lRsXLpYYRn#o z@VZx&6U+Z3oS5>^t@_HQv<{V`ZaSWP(jDl*mg9Fa!AkEerHG~ZJ>+PeB>F{gue(PJ zxAX0jTSG(Z*r%Bv5_%#EBXal*$g#Dw6o?P2elW@m`8d^(jFg#BNs`OF1N&oUcJd*y_v0~G#kSmFNLEs ze-pRNv-`1pOa6o*WD;*91pV>D4DA)I#mp^MERiI57Vi{D+@8#WPb=sxs9NssY$0k} zd)h)X-d)`yxu?!>sw|Q*AcFspn`Bi_d_?lrRmc?e`#6^w;f@;ifkCENaylitkjc9y z1&iK$;H5)e%-JBq=qqxXyKeF)kdcrI|1WcwM57`yCt%D5cA!mAFE-gJB_*TcKYOsX zvCTqqqHRO)QMF$nl|z-s=CQ3d94e(ruaLu_ruy|qUC*qO=>e}Ksv9OaLw5TmFD&+X zJ*MHd#t4j-D@%5$$LAK}Ex0>Q@+OJ}imcUHwl8k5RdE}-&yyQktt6j>&+(g;*W7fI z&@p&d@Wd&BgO)71gsAXi43{G0Q@0L^lx!iWI)_SaASrP<9xwPko7&ridV#Zu>64h? zIT=xSAy(i%TBR6>B)t$^jJcm%s#e>5u{&AI6&;@RNC<|1v|=pdYFp`~h@u!zWhsn) zQU|(l@#WNj3}4E!i`RQLu|0r9rAUIkKMN+(=-n*7ExR1(M2q|?R2jzy!@fLT>0*O6 zJ^yG~XTyHPp&*8u%BDbn4KuaJG1(5@(^gWzrc_kgMvZMgSi~k$JnTw8NZG^Ypu^8R zv__G!L0($P(6qA6R3X)wnK$a;Q`D-3JQAlKzdS>bhT8dJ6CWx%K8f4*6=iYg4=CfU zHD684tW_YS!y_T}f*trn_B(PcNF9O%S#G=#7f{w7D_hH{l11-el% zF>$<9O#-+45$rorZB!6-)Wg&C->4~{2+MUw45nkI815bql1+9WD3}>;*x5kwDTgRf z=2&i22eFCtXp&F+C6oznr?bdNkFK#O?Qzz`N=f}D9&rH)i3^nu2OSmZ6+op9*3Iff z&zuY==LVlt*V}xcLe*NNnAS)J|I)x&Bz>qs2%9e762o-pr7!SWMSmUn4Ax__htuJc zJz@jT%ghHo6KQ0J*$Fq|y^<}`?$DunUL#X3k0YYTN_~buzbxF7&43+s5QmS|Kv7n? zaaZSaHF5|B%Bb^^GswgBCrFp?WhxL3>EA7Vs;srGgLy1kA+2{Ej^CfzjPvbsI6Ci6 z{!wt&`@6JFSa1fR@EHgk`hIcztZ3_F`CS}~O9Mn$D(f$7@C*^{_mjUfQ(oAEO)6Q@ z^8KVtH)gp!z@E*;sknR&Mp*5iLSWJ~_J<0e+zyGpaJG2E@zT_CQh=NNA4TxU&^Cb` zgN}7Ix-YBXOThT&23Tm@y!EFDyo@iZyPK3I-*m|6xvkjMll4naF6CB~_h0~umnHn4$E$ry zxDnOzV2zYfWsee!RqJ@l6+FnDO6A-(-VsI7Ny%R?nzAuK9?;)9zkKxrFBlH)NBg<0VBa7lOsMgchJOvUN$ENQs1P_^? zqdbO4EmMYss#^*n5AO~%sM*-kY22_0lV?v^-YO6MX=z8mc?c<9tFdkgnsHCOD{+gM zl9t;l>dwO+5(VnpZ(YW_3AREhDoZ0Q>@{O@NA$^9To9H&>%J=rHmTgw z*OZZWcjvH&IJ*rUfEa&(n4XSObJq(UO%2A_kq=Cvho7kpWkm$tbx=>c^bpICms@RH zN9|nFfWYU>Z($9HL$Lqo4RkspA&2FjN{I!56WVN<-rpD7*A)k~&bVh|Wr~GV3=iMHgA$ zWoP3GzjYgBWuM}Ql?&(Rv$nB zj+rNmUTg|^6=)~Kh@x|QqH+)mq1r~kzElTt`mHl}tH%w{48JmNc=mU!Ta=8SBf+So zk97>_r_tC`p+ha173Sjst!YjMGvwVX!yIdbQqnU=M6|)t%NGYs?OJIGi=AsD-c2Hu zMpO^K=!p=&8E?pu+w_OMcYixJiy#)N?=}uXtbgtU}r&G<&gVZ)e0 zOe-jq*7X?|I5Ts8x6VvkN5;O0ynRtt>Q5C|J`@|&*#pj|nlZJ)g`dF^gy*vMyu)b% zEZ%gxuIM@*Zg;^>6G#rB!RzwDYgtv;zPYfCID(^P!-IK{%!lYklxb#+gwIp9?l$Z2 zh}Fa66}8n#xuZBn^aU*KpUr*c5pk5w#-`f`*~S;y0AJh# zDR|9ctgFwWF9inEhS+OeU+zQm%d@-7Dzk^hx8LzQ{NkJ4d+#vio!Ac^hW8&4w>Gg|L9FByf^De@l zMrwMRAQx8owjS2=RMrYg(iVi@7qcxU(zcgM4&`jd-Svxop2BS~%9Mvt6>))25vPul zeT|BP-c4sw-WW}7{vE>mdU(nG)m;`nklG^w3sUL?*SA5X{v6;qnmlTLU&GwHddgHr zH|~-i^}sL1inJgqrbw5|+FP_lBuGJrnFesZRyC6En}0Hk>>zsSP@5m}PneU$KF8qw@OhB-j2Ta_N%_jY^rJ#0+noby`?RUN?`rfle1K`qEud3U{RYhIdi zr!%xBtmqT3N-afr;lUB->CD0$h)g&E&f1wPFG4NpGSm0#cE}l_tj3E<5G^Ihl(%w6 zqRCLwrpIAMqZcM3vboYIrD(3}8GZ}4tZR@%2&~&M)eJ(v7|3Ho&m2>0RqM+oC8rSt z+XICQJ$}QR)psLPmE+lcR~xdW@nCViLBz%aB4(Kr#motNGDXfdvu>JiLkK-ZjjZMc zgJmFOYB@JN2cop&IW^&|tpa|Thl1j}3QJm)GF&m*RK~GHDO11H`~w|KaolyQfm9n@ zknun4F%K;v)JQHhNRO7toqPXPE#ZWNsDZ)%%=V=c3&?QRswuAncka#wFSq)@+Gw2g z@OblEyJCv-#)S9yfT{BUF!R}>3Ua@#|V{R#B zoUWdk&m^m0HT{Uos+xY<(7&;VKC1cY8^3t{ZoGk+SDfly?tf`|F)Jofdb0h3KY1+u zn}*h9iBfBW{w|}Kp&vy+_QDC=(W2j@6ZXc=xsTVeFJ;dwRr6h!+9&jIpM4?SACzNb zN~P3kOK2O3tAh>NhQb$a;!5txdy3%*@e0Ly@Gffpc}$RrNcCP86%3U9 z9H{xc_!ALP(xNcIzw8*{q9+xXluh4~FvygEjgy-&txRP9kpuJQAne$5Rk{opTGntD z#G_o4n|*Cw%VBThei@%+xYaf!QLrj*j*I?cQ5d`>0G7svWv*ytb9=1F+eZ-Qz^O9A zNyz&NORONMs5ZiqC9GH_#;F9=em!Yfn!nS6R@ZU z*oKz=yx|=MMr=VScIeU&dZ=AT6Y`qS^P^4AE10H;wL3#of2rBBzpsx1B{wm!^WrAM z>5oi0iBsA=#!puMObL?y%N3k{VyHXqdP&2Vp9KO(l~Et>B&1h{K}MTqoTLyURsjcr zU}xFUiHYkj+9GMqnIfh(zMAG^P{5;q4okfwQ49<)J9Sl0jRgn&@H8G*1$tGTH*$87xwe9>qz`XAHf}WH@wsiRl99mkSjWI9ZPR;$_u+uNX=;_w;p6VOwlNMbE}U*R z=WVjP0mIw!I)*o&CQv_I67)b9fKsca;G{?J->oR`4Kgo>aXkWkk(@`jOgT+t?g|Vb zIg&$tZ#}YAl9$wiML*(v=aKfLUNDlbwI)J2I!Tnxlo5M@7IIZEKFLlTf9W=Z;%+ z5jn7nEIhURCPy6Qq4FFnbQDQ+pxmjYlnLrFPC!24;M`qj9Ezu!nDL^#Na@+|_Np8t z{YN7<#<3B18z_7mguL@8og)H4WeTXK?*0hAy0hf7XhB(-ymTU^xULvXZ2F=J zsFwJ>$S4~p0KxLXR%f-6<#7D?1ggPmFVp`rsanCZVM^GGdGJH`Ra)X4AcE2{c5lvR-2&v@N|v-+ zLcF4|9YfK!@OkjNr&o@}n+Wm@EQO-=D*ufGo}4jqmQB?D*B6ARwvd<=GD`bLmV=8x{yj6>Su7Nfifk3H{66UseCJIeeJE&c8s!2O+i! zNFnzRkGFf(H%`d6oqzr*f7N80vYIHcGaM?LAp%i#KN*{V_Q$Wy4Jx3!)mRCITn>k_ z?Us`cYD)^%4;)z#w}?BXr7Q0UWxj(>rsN&6>~zDe8kE@x95>2`WQyfgf4R#I-K#BS zU|QLS*6}2ld*b8SV5W8xa#I9ampXpm;8O>$ih4s_9%$x{Xu!W0#f_0-4*wT`_ovxG@gq>-*rtkk8P(9gv;v4+I|BuIaeXD_uVRFz8P&AXF ztB4FR&ug|+mKCZeIdh#*-fLN8e3@*x``xi0qT7{Xj&eK|zV>T+q&4XcV^ve&>h$Kr zrbbVUE5|qmb6LS>wCSp=+jaJiHw?wIB;ruKJ%W)WYM%~vJ5G3HsEUWPtr>`DYeX8k z1`kpTqkG}DD~c~W_`4$A+|D^DoQ|gDP>np1j6f%;p*20P%Wv&%fgC^mf>9NcVlp;# zgjO&ait%mVXl@+0m#PTp)e*CIQN=#J-0-WgJC|s~_nYg1JKA!&(-v_hiEI|B3G{3` zKgJf~>Y{qS*^EoRLq-Zq$+T-epm`@tn~p;!bhV>Ki< z)JNPGsG)V!v{rG`lposCO?kPitr={uw~=lB2!tS01yWnYf9UOVcK(IE5JcGL%$tcn zgACmEvZjzR)PFx)GOQw-G5#B}pLoPRQT=N=>n`A*78 zD;FW4gzPn2eSvw*Js|5{KneYsEczClA+<$JQaogKNO3ga01KPw%Xi(le2TR}&i^2hW<9!_xwL$hU}B6BMwqIGf`6SAkp6SfPX6risx_Ge->IJ?oaSc?uL<^K0ADO%XS3-GpqjD=j{%(+Dc#N4{$|0|xlXq8{cu%B@ja$KHbE(?A{bnwz^fd~fU{iV?{usJ z5@<`MOhQ&E=59qUwmAtNPgrSkoD0`*ZL{`1`ZI|M#~(I=L_|<(0h1rR8tpB~9TbN~ z){i4ij>)aU?_Ok%a2J%Om)+T23p$R4Ic~I2FSVM zWE-{|SRaTuO!Ca95Wn0~y|u{W&gdLwXZcYK8=*<`5+|60F~USC%SO4UDa$!kw-|oP zUyf+${WfU*a`RhAr8@<iY@NYJ@62M z3C-We_(oRwD*O(6SRf1-9oQs0*zQFU=r*tADlxktUgu)(z9kKeBfL1mfd<&E@(2T_ z9LcTwc%5_vXgxzQ4?ZXz56{jD?z?N}GlUYv{t z^t|?=z>!QYiGojr5VMQ+)|E#N zxX+)L;EKot$NJ`eK3q2@wHRKLaZ|zis~b=O`WA4Po37nu6T4Z~z|;v7!Za%VheO|> zqfEiI>Pm1Du$1P^aO2+%`;7Q?f}`eyStq48c^g}qg9s9Nc1XNm(pnnWY$b4Ky9-TR zeM6ptvC$vI(l=J=xR>rL<=8^8aNQn@*E8uG04qgby_vSB3?^ilK)YKKW-RdZ<{3D{ zpMbbMYjk`YYBh5K2EA&lQaKL_e=PK0Apb>^5;#GZQw zEH2F*=6Hv&4_Rtoj(_O-RqJ-31I4gHn}P9YN`CiR=*nKV|BT(W1s}f6X3VywFNmfA z$PZ>o_^T{EVi)pVbnMUkhRy2UiycJYk>`k~lJm!0LXgtjPxoawtX7=@n4z`nOs7=d z*{j)SmuFSq7ERFRdZsfkx1k3{0F@#vr0vIbe7}VZ zyFo=5E{3yk=CjBHC1mqB=7^w+zX9jhxWjjBohFxR(Yl4T>{=5&BFM@>knG0Er?A#Q zoMy4@8z5VX1k069b&B`=6u9NWmAJnUJNw&3rH|ay$ozbeJa_afS_+!PA6hQqL+KcE zn_i2V-vOu66n)jd2M7uK4(~@_Mc_K>(x}0hP<<{}Kq!=_-CA-|>`(JCW3ckd21=I4 zM$K|Y0=t7lrEc*E}1!!}oe&hp;WUuP) zV+Sb6Yn>#G{#-V-g*1j1i>IZGCcvWL}xNdB?QRNhtS_AEm$GRP|x6!s+@n zM!z5G{(2<0FbSaMSJ`=~WR(kK_nS8by@{}vKYLdzCz`NzP%Yp64`Z0#X_$w5o=_SU zmTT9X3Vaf?t9y>u=u-ufQ8&K&yBKGxXK+n=aX)1s82D4P9`nPd&&HhjU2bhk0uPtQ zM_!=l%rM`?-prOT5Yt!U6UNiPwd|g-nIxL>U|Ii^m&`sp46pH|vB|~u`vS^c);d*h zl@|AipbW$Z^UocP#y`aT~?#{gAV$aO6#I;i#(<+HNtmt2+ zxB5F3IGR#r9*pQAW2j7z20q&T5pOQ99@tpV#p<~f7}!NV5MKL`3%H+$!+OZwFF>#0 z-+(RHw}fLGiYPyyFFr~!OOfnf z2VW_1G+4~kYo?@GPB9won2%3XZ&VOBjgypzp{Saj{z?zOhFumCRAOcrb?U7l@y%el z($%lR!H*>t&i0WFZYuC&ULYPR2~ z2+NI)wBPvm4zmGJg|1P8PFXm42VVM92DWHMImwOAMSdtng7jl`*1;bJV=$XePRwC#N6K zB7Nzw8$~mwtCf-T-$~-SX^X{h*0&3!&y4*Ny5*6P*sl-ZwaF8rDrCcZb^V&8;26N@ zkUn;j5_vzU?FaxxqOKa-KnYLtM%-zSw1e42oJ**0eMgA~y%3Ha2=tKtoVRRZB>wnLzeaagYTn>X% z92@7hNNcKZ{NP-Au#R~iZ8tJa4t^Njj zJ4QS12)<89zkhR%r%9v}CGFQ`M?|YdVvGcFYk2|%z$MbaUXAYS&zUGz2ukOqSQ?no z-|BeXA;Oq;Zt0#d{f4EJQW^uOXqMI0U#U6jEkLH2alc)c;weeSYCoX)z79?3p0|Gh z6m`Pc^L?Ip(2;?Ymxe%b)snc^_^x0lP;)LxUazy8m=XZtG4}i*r2ju3nsyH|xeshc zAk7FK0$$cWb#S!lmq3)#=hzZ6_c`Qo8gpRF(U#TvYWM$8`Y$P^uF9BjO5pYH86YHs zrlF9=xL$9yG4o0zJp*oUEzj>r1w08H>FKy42828qTksKATJ_#fKMqJ8s7~oPxrIHz z0WYHrh9|-|uHV`MS1ov~S;cy9>soiC7vIsJ0J zu2IwfW8*p&5NiNH%a&o?=l&M>m8Sp8E5R`%i&bfuCPdii+PeAjyFf<}S&Q06@qo^6CUQLh`w#v-K5-Tk8JCt0njqb~{yxDo?WgoRwSapTl4NRBr$P z1%(sMH#){|PJqXd!~iV~76s2<4QeM2Vx^8ky&WRj#s>Wr`p*4)lwPblzXbdr#Z%6A z06S~yseu)^Q^x0x=|-*{;=sQcKt1hzy>!Zxy)bY>yWYt`Jgnq;fxqLQu;#dabT zJOvz-Mixc^S|HzeakTmfATZl}QvgFeNlMqn2v3s!0T`cAeT;2LNz+lz>4VXTuL|puq1cVCP z?|1)FWAN{%X2p{DLn>~F^~8U0YEwjx8G}BdzF!m3bxV}Ow%_VkX-#K!N~>!Yp*$?J z>KSCUAAX%9@^pH`_2L_FFz#Oi>nr-?;IB zQAb_nDfozDFS^3nxS0*$znkg+!70Akh^J5e2tfXS(Rj_9#@t?5036D?GOOX&J(`Cd z@O{SC^X&>#(YHV1p92$q$DFU{=>luxt3*1Ee=;ZiaJhaLY3j~WZVmIByhX-Vw$r0R zK!W&B?*ox@O*=d@7;Kh(mYr24N_j0t_S@6?-rDiJNhq-SA0Jd-Gg8uoPuTliv&AUD z^4>}G?9}GG4e%5s`uYltx`0+w=ETDt$mInqx19y5-WvKzT66Jyrn&lh9CFp-OCAyMdBRM^6<-1t9)tkEO`ki3Ol^YVYr$2!tr=jI%6|1H+sMjPOzbiE>s2ALCZLao1XEu0a$DXON= zmKU?>OCAkY3kUto1^&(HhjYtoI-D+Jxn%g|^%@}Kf&aBT#GK#Yc@o*0e%jxR>57d;#z>Hk`oi-T2fui=p2G?pYeiK zzuunP_e)4{1rIPdG04X(0)|6kI&KMmC9|$OSM&ujXhy8U_@%RNVSHl`#*_rB5+aHoJ=0)&x~MTyc9 zAVTecYqnD;**`aMDv1HcgSnye+Rn@_|1v_+g$5!JV473)ZX%5UJL@&S6}4cA0BQd4 zKjf@=?E(Z<&sKQt!14_=U%f{QD5ZKM`i#3upK?%OL=WjoF1voRG*%cO?|SY4NI8t1 zSYl-6HnsHmwS5-652#K5A^GPFRd}ZD8}o5F>n>6(G&xo5gQ$1LxGfrF{a> zzQl9|0-R7L(k1C z4IOf04A1!-tSaojQ}hC$-5lwGW@*i&)igb>E{iXu$MV3}dub>_FwpEOu8FU^dJk+J zTb#oOLNL;|E)UtSJWn!0a2XZAM~GlzjKS5hfz=;C^o&2%%2y);Yyncq&ra{p|6J=^ zEs^V{c)k;W6|S`|s~bA)nui7|c%@@K_JLn524pZ_9~pI!wbcF2y`P=Et1CA@ij35<01!|B|W55h)=UiNV`RM%m{);_*}1E^4MLbuK~P5{Dq5W6h-zb9q^+k`o~2A5bM$qcY=Ogvx- zTpJ*zN&#iBbvP5hQ5) zSzb(7YgI!(-;V{aI+kzKSC5PIIoD7H=!I=m@8jKl z8*)D2%xz6Doz_dnmng!ABT~XpCdf9dyGfV?VqIV{L+j_Ys>cR%h#e#?i2m_Ys@6v- zRKAUmz1GF0D}|o|X>rVcb;v#|tqFS}t!V~Q9JVbFU9LrK81i%Sk2~HN2>hnA_C%++ zDv|OG63N)k=%8IzMVu!?&pNtvJ`->YiPr>@;lAK!M38#xrFqdcj7P!zzpr3xQv%@_ zbA~XvmzixoG#>+N#jj{vfLUS==D+~zjT{fBM7<+|1g!l|k0Z}Evhg)dsGipx-~lXf zQUzcBF!L235M5cEuij-WuzJWI^O^TZq3cl0&jo^o?LDxbO+O^_^Qz!Roxb@ijZUYs zZm_4Yc*tH>Qzd=1`$gP#S5iq?b%qyuC7^D3%Z6v)cEY`GcT8{m|C+7hY zwizA#rl*wm6t9^`TUr&sE}Jn9#9O*OR;{O){iGg?%rH77c4U5$j;+GLj#N&=gzIka zugQBj^#MbGk^-xB1|LVl*s&QH5BQCjkMZr|Haz8LL>&!%H-GnS@CzUJ@q0Q&g zJ(oRm6q7q6ezmyxDvN>7yO28v9^xi&-#*~#V#&&$n;+Ca4&4|H($u`l`>PD-EQZy` z&%dRP%(G*}*-cPu20HXfVV1U>ErGrwuxkU4eqkeqac)Z|SmuB(`1?lQzB?N>-x5dm zptR3%{G>w)%lR*FUtwc|(d#GxGxTb_C|bqfikpOd3mE*IMwbs+!)zh&Vv-LXYdwQ& zZ;Pz1tSRlSeK(uJmF$mgz#?`JmCv)>xw|AXl|iuIHGm#!>G5ba2w;hA{m~L5du0WXjn>kgI~S?wF5o$%Wec>l*!mJ zCv;B!$i_ecCmUVO)SCWz{(^0!UdBNHG+qt)B#6y-J5=U9G|g|j{i}{B<|qJ*xPVCn zOuU^^nVo)lxG#n4C%a6^b_w#yv`C7G&ovsNpPL)Zje?>e(9DV^lcSHVD6b*8q=_7p z#EigARVoU)jgVe`m}Z-^zB`_+|Np*fQ({w_P*m04L{+V#x{cZt zMeQy2h&|g?JLoj~Ex(|;fw!^X5n3%sIJ5*AR}eHY6?~Z8KT!fv zc663Gi7+!LHtLG;d%ZcX?-QT_jaK7c{hMBBFpn(y#RPpnMM}brA72IvY%Ol1_XE1K ztsgVjo<9PX^n<`6%1V%X2Ez^Vr(Re^&S;WJr2eJSTiC&p$$6}zT#B9ZM3 zSm*o?!GkSi{NC8I`BgO^5LbljqpeXE$c*P)(ITj>knQA;30=jgW<+r`xxz`3DO#H` z^-(?$W0HrCI(%^(nM8Xw7YcXFrZD7=kXvW0_s>Lt$tJ=WT7?`%zZ*JXvbT{u|J0aU z0zaM2fKj4CFB*z)PM&JB6F{IsZ*PBF;J4Z~1>a4FR}=8UVw6OFD3p1vzt?6SGSl9r zA9=gYscAr}28_~u9-lGd{F;D$#iNn5SKp)(M$BKBJSr6^Hn}~8s_qEr%wHSdRKpE< z=O`)^=0zv#cn(!fHZxye%uFWIPtd5)?ZFV>*x@2^rYL$S zzk0ajP`hi#m^rNWDEaG-8GiF{)rfei`wTKUSH{ry7QIfQ1rfqjh}6CCxo`rW;)Q=OAa zotqb5KURV;f>WcTw=ANzBO|N=+mYARwO%(J?nlqTGjY+{w0Zn3Ql!!? zAmST8s*3SF(baQtag3J~iw;o#^WmiT9{M_&9l-W~2x3CwP?7l8;881sZ74fVcB139YVa0WP%Ookp$8z z-bdW$(`#D2uA%GaD4+AH;<4&KUd}fG;CZ*m=T;a0eW|^v{>1j+jDs8ZgOkJ+tRj7z z8BF$j-ShBq=U>2TK9G6q^NVZXgxnb!j+TRmT!)&R?Z`PDUssQ6LpUS8T>o3L57+S% zh2~{3&!7FRJOBH5iP%CB;@P#qStq&NXN|Q}F~#(<7W+I1DmhPXw)w0wwur&j+#Xr< zZ*$X1wIRDdLhZ3PYpMwxy7MGwv!fvmEXJNRX#d9Sg-4Z!0j)++{uEB&-#P?*FGCd4 zhSWakWm@Y~ofy=C(}ZMS5ED-lX=nNmJXBU}Tj@Yv_xJNVk>Vd)zEqofsQetNOWsJ3ER<;e3I%q;pPgk5M10x7-aM4~NX4PRpdUP-oSK;ruF~-_(9d zzoDN~lzYz|Zp&Og?!-!p5NyF3zZ{pj3dY6%_VNyw<|g~0GK54Jg_1_8X-_b2gkHN_ z>yMo1=L08h6flEnvVe;Aa?mLx!#0r_MH7u#w}{ZINu!QRq|F=jQyXnwuzTSp^BwUZ zaJ>lyp*@4M3>|Lef#b|KYyy!BplS$?C^^34CfJQ#h(22oUv)$@ovvM+>zl6)M#(h+ za-5ZHh?J#~%Q3XJjgWwDa4(l2+Ec(!SEcnga6}3{YUtp`ynWz?e>hw0Ir7$b{Nll=OgQj4k@>Xu zx16fZ%bxG#z!>pu4V6=tFwMp{?5^kXwAw^VCl6FTd$fz4iUJ`59l1gOg*`8NL}>BF zB!I8+#W0E7ARB1%F(ZMxe3ehJC4a>s$vA70n%-WcWpOU4xYj$}GXIG`h+>pN{24R= zl%lW8au{dU=WIEOyA1sLOP*7?sZSovf93pSjPbeKRH@7`h=c&xYhz*ow-TLFuWN~Gj1C=;BYgN65%T$F z81Bd!?zhTffrF!+ar4`OW2kHMAPdPHy~JmtPvx=&8tyY|$O{wY! zgv-Q_3G5fxm=dIL1B0)wEK&NmuSr92_kfc(p@Dm2TdT<1Qw#Tg>K2cYq^ z%%I{tNA1>n?VrQ0Uc+{B-%1BiJe~*iiKK(9Xt2nwy08h)=XK$8UsBEBo8(*30;VRm zL~@BH=Y3g&W6iR2Xtj5AKVs^`7<^m`K{!4IDL67-#K{;$kZJ|r5xE!2;Yq(_T^}H* zsj;dB*3ifa@G&~M4Ct}A3}(L%OV&celF%2$+gff%zZbe&I-{321uZi5%O;pqc_d?8 zO88J%fv`X0!y^oCxE}JJS(5sO(zO9K(<-~e0H$r$WtBnOo*9Jr!HNQQK?9{>; zSJ^`InrPkhu#Apq))+ZzGML+cl5@nGfq5ej$VuX`Ogl`bhJL!jUM}P!=RR{ z4s5sz#y3H2=RX!{nUoGRHUDzq78`%rbQdK7I!Jlh#BN(G-J|`_YRX#RT841INCQGZ zMTw9!QDIhG?C$I^o5RRTC|EX&9yfB`y{-~+aly;DZ9i*md-i5m0h>8h2ZUEnOX%0 zW;gX~|0y5l|28Y~sA^kqe0+Am=6w3x6RI8Y98?uK@PetS>YszDO}n~t_d=aL_q>YF zsttgJzi>--*~&zQOBFO|xG}l!(WMc%GhJ@qOi_(VJLTt?&;_Vbo!mNk8>v)6Ne*#+ zS$O(qwpy7x;&^|}-uh>F3MW)Miqwi{07m?#c;oZ9@$ZulGF1PsTWn*j7SuE^)iZu@}NN#zm(R@ zPujY%oGLEx(p>N)6jSB6Dae0B#BKkiT%CR8l_b&f!=#{|hQARKIU`rdzX#((BY?76 zm8>52wW`$-@PQ#pTIOG1uMJt=MfK5hWcl%7QIKOs&)GPlw90R_{#_Cq(|t|NwRN4~ z8}oRlEH#!3eyz~@LPZitiLgQ`8C9L{Kl$T)WekL0g3t(LNG>P^W11qxdx(bKsXeF2 z8@9Wq6xAEF`vmUiCf@)wv*^2z!K zu3AJmemL?=54v{zRp}h5*(UQ&EpRwf;E0`TW3AosM>H}n49?(*F4&fn9mfLfCnM@t zq+3Ef*P*q8s$P)zjTyXazp6pDr2Gb^CN0yFMQzg^5X@ARk52eLhbX@TSG&9}*;z?hVV&g6R>g7PmA&83Tf%==(&`e>gClax~>EM9ww$f4kUhuo8 z!e<}H>y=d&n?K-ZhbuC)Vd}pAW1-zWC8z3Rn)xqB`qxK&nb=FTy~v(&-2?GTo4iSt}Ptgm!)^{?-vLF zoM58F!Oudf1+Cw)`7o(5ox-^Xqvd7Xs4=*H2=Vdo_-4xcod+yscvIQ{o}$LvE` zxhI0}{5yaj?OPF5s1wl3l=)p}{}oH~xOoN-pnx#0?H><%2%Ni5PDQdqLnzvRg3p6d zjp!_bcoRt|fUtN>^hm8#%vpWr}4_H^N z8=_hc?x9IPd-RY@X)fG_!zKC-i+T^v zT^a5BEdyzKW?fK86u;?(kT{0kHy7>?-0?&_$6m$Ccv@wN{6NeucA#Ib=hPs-b<|`+ z6`l=r0zSYnTc8l^vghRsulAJM{c4k_yI$4Los6o6#_M(Cv&XF{BzlE3ZF~fdr^!=u z?vtUWwK-;xUj}3VbC|Ob-tFdE$qvPCIlal?kzhz?U1si4i=14M|p_Oc8 zKL=ROnI3fL<$rkRH3ikFqdGJpa`=G)Vhqpsnk-}+ZX|SaJAa^+@Op^pbTERk z$F=F6+=h7 zP9skmn7VEEBL@^C;+)}4Ehxcsc$zl*XWu@O6}_^APy5^ z8Ou_o=tT6MxB^|!KF)jrnHYJ%S%d9S=LgYw-TiF`*B|fIjqc5?n93K@iK;2U&@OTC zhq+}A{Eom8IwCWe5oKCDgf83)SccWjf7_SWRTwK?@f@E}^*UeT`4f&>yC^SPj%Df=N|m0BXDSvK za=5=&t1`gpIu7vkRh$yJP$h@XtzlGem$Q;;_cU**t!HgP{`pl+zoJS{+ysqT+uJ1qu(Mf<#SJe-%(uP4dtrs#FVJF z?zlIaW0JSlS6z$e= zhc9PQMC`|J47V4w{7Uo+f0tk1BJaygvh4`w4A$m%0@~SmF9ffO$m4Y{X<_x$ddPBvLN4;0rP$S_6y z@`}^*3@_V|`v-#DiAs1G+?a3hQg1$w4?S#B3sKi76#C^x`;wQfd%W`1y^CovUZDTg zdH*upY9UQJOw49hT+sXT9Eoo1jJM!q50sJNP%4O@@I8om+0dxt*T_{|MN#HD>p72^ z5TZTg-%&Dfe~iZ@-bm4tJZG0~yR8D+r0kAyiy9R7f38Sbb!K}$tvX4&P;2|;MzOa0 zji2)0L~CKk+_!B$wZgumZk!?pyOFYych*MrbZN!`>_H&wd;4A;?~jN`Od)(>lAZ|6FgB49j9fTG~LNEi~!81jE@xZ6z^K zlI=qM+o4~`K4Qb*s-Nh!)Q*Me-j{N2Jzjt26U$Fs&&E4Ry_v|Fn&U}-fu$~|Q+Sly zd&LC|)6&5^0!j8dYNkJU^RI$BcS$A)- zvL>x-urPe1x@(Cta7Eo4@~Y>iLdX?YQ5|dwn|>fzzaP+}`oa`}D+`Yp$wS{t-kcBm&K$wtSphP(G}$&ff)NNiX4Yx9rMi9$}PG2(rk zJQ1P~YRow#Td>p*5EPSxX*$Wa1+mf#H@jCTAjyW~<046zukh3VDwbnyJz3}QZ~^oq z4hIIon%5;ysvnqTT-W*y@r;}+;t{(+$8-^8y?3;*Q>%(Eor0T&_CooQjDBp2-R9$* zue&$duO{vzi%*e2)?vHlJ7ur-+MqoSNhNx%*PEIz!kiodp>ZWRe)K!(OV}c=G^vd+ z()@QtRp_gVZ%ma2nhpOqr~ikCo(*Dz1e@?!sNLRIW04T6)l+uV#x)h4VbsVEYm;k( zo#4MHP$ye6FYvFh{9oxw=DdO3>1;iZLhHLMn~GrBY5|hcJ9--sZXlPrK z0k4!f+vkR07{v=j4Vrad|Cb^O{(S7gU}q7n zT$}&y-Cs;x`pLwBkQ2 zkmC0n3Gq_V?NA>!9nanjp8Bzva>CD`rgaW`GJLsQfcHV?A=8_*Ysm<%I+F+dXz0 zDCz=1o&`6mGl~3LbPk#J>3%KRf@i!aRFxB}cFf6q4E3w?S%xcW&R#ndmhs~n*77R% z8(@nP__+n|ia2Iis^1VPt%~`#!*co!6nVvPV<-x=()iM3aDjgku%{D&_ad%#6U}jh zk%e05DyT#JP&PNCnaT9pH+~Z4BR6#GrHtj!3xckm!^b~?QM1>6J!Lgl=}Ro=4xR>RiUFFO|rknI0f3Ln8`qGVjSF|3d*wKc+Zdk^Rhyks5{z+P<#yf7ZFOX>&EC zo5?9tdr1QsUpZFpQ`#tGXCyO&4!O5QhnEMYt4ehw-i0n&OYtkmJ^mupm z6y*yc9a}@Y0?azHcI_Iv#%O12?Z?&#NxK`5ephxaV%B&X^!aG->~B6yF#zAZpUO#* z8hV9EPFX*j-pNmB_`RF8^T4)U60s@ipEy+PX$?zxzd6bbGoIlUNTe6?Y~_yCq-vxU z5moqS^dR6qPl}$fh{Y(Iydl+fw%%fqtFeZyxwZ<@%v5z^0X_Ynk$Q*od?s4-6!l31 z`oBgVQ9LAZ8X`ZId6){TCceWw{zb1~AL4Aj05J-03-V04nGn4_S@QQjyC6*wy;9;2 z1b2$CrD*2LneLsB@^N&^@3dAHOm$C4{EhW)5jpFt5ir2x?EtD9t_)XmVnff^ITqA- zPI+5sICEaM+TW$+j$9%!LsBcwcoQ<6OnI;k`KKw|=3X3>xTQP^T`~yc1LLL;!j^|m z9&K<(jDx-cgA?lSdcQUoUo^>1xqd;oFL8nQ%N!u~v)|q*zJf!>M4siqPex1MJ};&N zG(1UOzk=r&;rKKz0^kUzW{u*ofiRc{ZQYNCKj8&p5bKn<`Iu4foJNZtiFq$RK7aOe ztsG!IU1+x)_Y(0pPFVB{$_i1lB47pxa>x+yqy04FGLl_`Z(} zX~_0GM^?8D=1oNN0ed3YXv1JMYk9kty{yriJTzHY~ zQ=y;psG9rJD49x^X3vSAqI;Nn^nl9LS?Z=Eo^kW4!pN`fhMj!OdVw^nI83aAofkmH zX8THIRT^JLOo(618dH!sBpU1}12zJCZcNiQrAIj@timD+8(NpN3Ofk%ute8sn+MMK zWSsISKlOhxhzTE1fyaA79MlqDJAr;ntUc|G4=rsyrh3QRI~UsDPorFH=8+R#ReDm* z`8QWRPqCFNglNk@r_$LU><8;&4rcDI72w@FR%rL^?X;FH%?oz;g_&u6STs0_;0oR; zfdGWw`S^Aggg&lOIWYEe5$aj(3~M^~?qP0p`Xgtm2=58yk!F(KFsnQO2x9KQL>7PJ zUqa6p(}EB*rOTVXbO9cmHAE>cG#4fOL)*As&pHqqM%0X6()ntT?>^K^qwOFA-&9md zCfBgXKD#-#O0jKcvobuvG9_C}J#y>ixegY-wmz+Hoh2Y`hC z6p*X%j!@IpgH=#!T!Sfe-7%cW2WJ8ZiSq|cQd)Gh;BvogN{@|O-nH?JT2bdDVqc~r zB=RRyy>Pw~*}}jRz2t_*Ejn$|78dXSqTuaUeSwtXt6_}x{}t|aj2YB$a`eGMzGClW zrjmo*V+{rr?J%%t-lx1>at;E8^bb}5V>CAIQ3nw1cIa29Uasw%Hk22Fb_eWC$dL0g zATk1L1LEdAt>m%Y|BrhQoDsrdNxOy-<}+d5m8Ol^-^6|(sBNiD&R za2t*dwrRoa_WzPgniu?iyo63(*+s3}pO+pxbXa(OpX{Vrs~4zVhecnxa^>P;-PynW z|MJ4nE7YxC7D2R_ymvRnjOwW(=y}Cjsp$Yrd%U;QIPUwsl46qKfN)9PhS9Sv@3Qj8 zQ8BV05eW}d=Ypa%iQ9YcBT%bt$9rL0;t|nnVTu7Lbo0WwtUX!Z5kcB_xi&|*WPf0v zFL^+osN<06?BjAO{c1|PzNPt+5_X(gEkM$o9X!hDyOR)bry zZ|k&*|JiKwPG4L~6tJmy!3eZ`e$*NDtsc^rEWju7-wVk1&MldKa;)hI;vM;=Z^=I! zw^mG|EcnMqiDKWr%U6z}OB7IW4A%^b9@>QsEE!^{5{L zIxI(AW~SBecb{^d$eqrA?|$ZP^XI+=uXkuaAG1a02B$f(v}T@fz9C81XQVfZthBrU z$85d49q=CE#NA;zBjFi&y0j)3McNd~R}X3J7f8}$R(EL~5WoGX{_}$WN&xh98F6`V zGjIC=I-Tf%DedE1`DYl2j@CZjL3I0UN$m(x zl)KF+=dE_T?mKA*FLvA|A7ba0W_#ddKq-#nw5i%TOFLK% zI|78%fw!c$3KgNmaeaGug0}W0gMwph$h&|6Y$M4lLS>n(ec*El!WFG`qM?Gds@?eJ zI;Q>bg^ncE9^%AHpG%S-QnZY`(I0iXt4dnkKU@73*k12EoQYua3Duifpaf|7lI8i{ zkLH~vn&P$Kho@v61fsbva@Tj+Z3<`csRCa9^|BN!|sPZLzhmkob@sU7XV zG;2awO@+^6j_RT#3E!hnzi&hkRQi^)R zLWh`ah?Yz1bmHx&wT1xMdP@_Z`OP4F#A&8%zjUOB`gE0QXwt4I$Yti}fcXg=BNOVA z!UQ77QmTA)7G4$Dsp#R_ho1bZPBn|#3xM+B*P<^F_5I9z%pk&g-x6G>zT9)i&}XQ2 z39TNLK51Ax)=P8Y(k`gNx1#qIhn6}Y4D-8Z8hVa{+Jnc4Q_<&$%#cp!1O1I%>-bV! z3EEPN#X^YCOUs-UiFP(AX3yIruK)lk)ADRcwYveykL))r-Q?ev4DEYo?j-G%1nB%d zSNj}G)lCUKpuG%NgjjU_eKu%;iw_~Mfut`sTQ@x$!hVGG5C&zF1lV;>(%rP234bkm zG)QEUOx81%$*CvwmR(xjMr}bK`4RU~7Oyv%73E%YnIg4-<7&4~E=kfvR34OsWV=AK z-o}&2N8LSe_6 z>xF}3@*`S)-#Z6ISlSawmHwe_2cQJo{s3-*AOb4(PS(WtsiyfYv<@jJ;mGnDaUqtA z^~3Yyo*oCuOd?3WrNxgl26Gs=m<4o>ogv*L_PatB^CBCl!*bzRGG;aHq~iSb+n=9G z6^u%Em2TZ~yS_6rW`B-FXzz!a&&bL{q6GW~SjDH2yFkQY3(srx=;z^WMjj9ptavCd9ub;BOzsxstc^LvtF*$dwX1 zpF^8Vz4ERjt=f&u>6tei_w^!$T61BZcDTX{i@-!CSWE`G7->(s-I37NIUx8a23#7s zx+!?fvm?&$7Fk|gqJ_AlgES9ZYWLT|x=!JT*G68VS#|I}viJ$wX;%1q+!y;Z55gGv z#3IO7>;zLOboPnBqhY>E82V!KVh=~g1oANDiCl|nAX@4GQahsg3*fcaFLaKqW#{y`{$1%1~TN9IPAD% zt!wA0EkOUlk#zUgfB26rt-aK@A0B3#^*w$qLR;PBG6?z7v@RGe6zDpW2unfea&sDQ@ zar%^jWapPk@+TOz{kOX7{dAf5gLPV+R<--173)ae#Ev`TBEBbax4>=!vew08YiBEK zZ>c6ohB_xRFY8{V_bQJT^;rlf?7k)oE-RMZfAzP&N+*%4ou1!cHVV(mln`tzG@$^O z+iC2fF1h^&QuQXTV9zLcmwqU>E`ki}ALV;TXW8MF3pN^0$Er{h5xRN(H(s0&$t$|C z22x!h6}F|-J!01=`Z<(bt?V$UY{XzMP%nZ#<`0f6iy*RcO|Q2_kDWu3gq2ldJz_2S2%Ywa`*gW-t%bAe7S1^T8Jd;JD6
O0 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 W?_5lKA(*)VzAVjbOv?|sMExHc$n-)0 literal 0 HcmV?d00001 From 66c9bdfb7044cc3258f3d8f3cf8ad471a414a8bd Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Mon, 27 Mar 2023 11:05:15 +0200 Subject: [PATCH 02/39] =?UTF-8?q?R=C3=A9=C3=A9criture=20recommandation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Fiche_import_fichiers_parquet.qmd | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index f5a8d601..59a51e11 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -7,12 +7,10 @@ ::: {.callout-recommandation .icon} -- Pour les **non statisticiens**, il est recommandé d'utiliser **le format csv** ; -- Pour les **statisticiens**, il est recommandé d'utiliser **le format Parquet**. -- 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 et de manipuler simplement les fichiers au format **Parquet** avec `R` -- Lorsque les données peuvent être séparées en fonction de catégorie(s) qui font sens, partitionner un fichier **Parquet** peut être utile pour les fichiers volumineux. -- 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 logique (département, secteur, année...). +- 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 et de manipuler simplement les fichiers au format **Parquet** avec `R`. +- Il est essentiel de travailler avec la dernière version d'`arrow` et de `R` car le _package_ `arrow` est en cours de développement. +- 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 logique (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. ::: From 086ecfb264181dc7f07b8372fe537005ff72fef1 Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Mon, 27 Mar 2023 13:34:49 +0200 Subject: [PATCH 03/39] Modifications de forme --- 03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index 59a51e11..48f24226 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -14,22 +14,24 @@ ::: +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? -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 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, Java... +- il est conçu pour être indépendant d'un logiciel: on peut lire des fichiers Parquet avec `R`, Python, C++, Java... - il est conçu pour que les données puissent être chargées très rapidement en mémoire. Un point important à noter est que __Parquet encode 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. C'est une des raisons pour lesquelles la recommandation suivante est faite à l'Insee :**Parquet présente quelques propriétés qui le distingue des formats de fichiers plus populaires :** -## Caractéristiques du format Parquet +### Caractéristiques du format Parquet - Parquet repose sur un **stockage orienté colonne**. Ainsi seront stockées dans un premier temps toutes les données du premier attribut, puis seulement dans un second temps les données du deuxième attribut 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 : From 97f1d4ed2740da6e8969218c1badbee40504c846 Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Mon, 27 Mar 2023 13:35:02 +0200 Subject: [PATCH 04/39] Supprimer un doublon --- 03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index 48f24226..496c3cae 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -56,7 +56,10 @@ Grâce aux travaux du projet Arrow, **les fichiers aux format Parquet sont inter 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/). -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). + +## Écrire des fichiers Parquet + +### Cas simple: é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.** From 60eaab1d93208180870a76e04acf79fd5cc69c35 Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Mon, 27 Mar 2023 13:41:48 +0200 Subject: [PATCH 05/39] =?UTF-8?q?D=C3=A9placer=20un=20paragraphe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Fiche_import_fichiers_parquet.qmd | 53 +++++++++++-------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index 496c3cae..cec95077 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -103,6 +103,36 @@ library(arrow) x = donnees_BPE, ) ``` +### Cas complexe: écrire un fichier Parquet partitionné + + +Le package `arrow` présente une fonctionnalité supplémentaire qui consiste à créer et lire un fichier **Parquet partitionné**. Partitionner un fichier revient à le "découper" selon une clé de partitionnement (qui peut prendre la forme par exemple d'une ou de plusieurs variables). Cela permet de pouvoir exécuter du code sur une table volumineuse qui dépasse la mémoire de son espace de travail dans la mesure où les requêtes seront alors exécutées selon **un plan d'exécution optimal**. + +::: {.callout-conseil .icon} +- Prendre le temps d'identifier les variables de partitionnement d'un fichier **Parquet** n'est pas du temps perdu dans la mesure où il permet par la suite des gains d'efficacité sur les traitements et facilite la maintenance du fichier sur le long terme. +::: + +Pour créer des fichiers **Parquet** partitionnés, il existe la fonction [`write_dataset()`](https://arrow.apache.org/docs/r/reference/write_dataset.html). Voici ce que ça donne sur le fichier de la BPE : + +```{r, eval = FALSE} +write_dataset( + dataset = read_parquet("Data/BPE_ENS.parquet"), + 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`. + +```{r, echo = FALSE, fig.cap = "Arborescence d'un fichier Parquet partitionné"} +knitr::include_graphics("../pics/parquet/fichier_partition.png") +``` + + + + + ## Lire un fichier Parquet avec `R` La fonction [`read_parquet()`](https://arrow.apache.org/docs/r/reference/read_parquet.html) 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). @@ -204,29 +234,6 @@ Cette instruction s'exécute sur mon espace de travail en environ 0.5 secondes ! ## Exploiter un fichier Parquet partitionné -Le package `arrow` présente une fonctionnalité supplémentaire qui consiste à créer et lire un fichier **Parquet partitionné**. Partitionner un fichier revient à le "découper" selon une clé de partitionnement (qui peut prendre la forme par exemple d'une ou de plusieurs variables). Cela permet de pouvoir exécuter du code sur une table volumineuse qui dépasse la mémoire de son espace de travail dans la mesure où les requêtes seront alors exécutées selon **un plan d'exécution optimal**. - -::: {.callout-conseil .icon} -- Prendre le temps d'identifier les variables de partitionnement d'un fichier **Parquet** n'est pas du temps perdu dans la mesure où il permet par la suite des gains d'efficacité sur les traitements et facilite la maintenance du fichier sur le long terme. -::: - -Pour créer des fichiers **Parquet** partitionnés, il existe la fonction [`write_dataset()`](https://arrow.apache.org/docs/r/reference/write_dataset.html). Voici ce que ça donne sur le fichier de la BPE : - -```{r, eval = FALSE} -write_dataset( - dataset = read_parquet("Data/BPE_ENS.parquet"), - 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`. - -```{r, echo = FALSE, fig.cap = "Arborescence d'un fichier Parquet partitionné"} -knitr::include_graphics("../pics/parquet/fichier_partition.png") -``` - Le statisticien peut désormais requêter les fichiers partitionnés à l'aide de la fonction [`open_dataset()](https://arrow.apache.org/docs/r/reference/open_dataset.html) qui permet d’ouvrir une connexion vers un ensemble partitionné de fichiers **Parquet** qui décrivent la même table de données. ```{r, eval = FALSE} From ef5a613e720f00eae77d99c15f1ebd89ec91e6ce Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Mon, 27 Mar 2023 13:48:40 +0200 Subject: [PATCH 06/39] =?UTF-8?q?Ajouts=20sur=20les=20parquets=20partition?= =?UTF-8?q?n=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Fiche_import_fichiers_parquet.qmd | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index cec95077..dab6df39 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -106,10 +106,12 @@ library(arrow) x = donnees_BPE, ### Cas complexe: écrire un fichier Parquet partitionné -Le package `arrow` présente une fonctionnalité supplémentaire qui consiste à créer et lire un fichier **Parquet partitionné**. Partitionner un fichier revient à le "découper" selon une clé de partitionnement (qui peut prendre la forme par exemple d'une ou de plusieurs variables). Cela permet de pouvoir exécuter du code sur une table volumineuse qui dépasse la mémoire de son espace de travail dans la mesure où les requêtes seront alors exécutées selon **un plan d'exécution optimal**. - +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 un fichier Parquet avec `R`]). + +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-conseil .icon} -- Prendre le temps d'identifier les variables de partitionnement d'un fichier **Parquet** n'est pas du temps perdu dans la mesure où il permet par la suite des gains d'efficacité sur les traitements et facilite la maintenance du fichier sur le long terme. +Il est important de bien choisir les variables de partitionnement d'un fichier **Parquet**. 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. ::: Pour créer des fichiers **Parquet** partitionnés, il existe la fonction [`write_dataset()`](https://arrow.apache.org/docs/r/reference/write_dataset.html). Voici ce que ça donne sur le fichier de la BPE : @@ -152,15 +154,19 @@ 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')) +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")) +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. :tada: @@ -234,6 +240,9 @@ Cette instruction s'exécute sur mon espace de travail en environ 0.5 secondes ! ## Exploiter un fichier Parquet partitionné +Cela permet de pouvoir exécuter du code sur une table volumineuse qui dépasse la mémoire de son espace de travail dans la mesure où les requêtes seront alors exécutées selon **un plan d'exécution optimal**. + + Le statisticien peut désormais requêter les fichiers partitionnés à l'aide de la fonction [`open_dataset()](https://arrow.apache.org/docs/r/reference/open_dataset.html) qui permet d’ouvrir une connexion vers un ensemble partitionné de fichiers **Parquet** qui décrivent la même table de données. ```{r, eval = FALSE} From 8cf16309524176d9ff29b970732a5446195b9a1a Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Mon, 27 Mar 2023 14:08:47 +0200 Subject: [PATCH 07/39] =?UTF-8?q?Compl=C3=A9ment=20sur=20les=20fichiers=20?= =?UTF-8?q?partitionn=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Fiche_import_fichiers_parquet.qmd | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index dab6df39..3135af0c 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -90,15 +90,16 @@ Vous pouvez télécharger ce fichier avec le package [`doremifasol`](https://ins ```{r, eval=FALSE} # remotes::install_github("InseeFrLab/doremifasol", build_vignettes = TRUE) library(doremifasol) +library(arrow) # Création du dossier "Data" dir.create("Data") # Téléchargement des données de la BPE donnees_BPE <- telechargerDonnees("BPE_ENS", date = 2021) -# remotes::install_github("InseeFrLab/doremifasol", build_vignettes = TRUE) -library(doremifasol) -library(arrow) x = donnees_BPE, + +write_parquet( + x = donnees_BPE, sink = "Data/BPE_ENS.parquet" ) ``` @@ -111,10 +112,12 @@ Le package `arrow` présente une fonctionnalité supplémentaire qui consiste à 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-conseil .icon} -Il est important de bien choisir les variables de partitionnement d'un fichier **Parquet**. 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 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 qu'une ou deux variables de partitionnement. ::: -Pour créer des fichiers **Parquet** partitionnés, il existe la fonction [`write_dataset()`](https://arrow.apache.org/docs/r/reference/write_dataset.html). Voici ce que ça donne sur le fichier de la BPE : +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( @@ -125,7 +128,7 @@ write_dataset( ) ``` -Avec cette instruction, on a créé autant de répertoires que de modalités différentes de la variable `REG`. +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") From c0649597cb321a47a4396d8ea22f7fdbf41a423f Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Mon, 27 Mar 2023 14:42:38 +0200 Subject: [PATCH 08/39] =?UTF-8?q?Am=C3=A9lioration=20pr=C3=A9sentation=20d?= =?UTF-8?q?u=20format=20Parquet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Fiche_import_fichiers_parquet.qmd | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index 3135af0c..204179fb 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -28,27 +28,30 @@ Le format Parquet présente plusieurs avantages cruciaux qui en font un concurre - il est conçu pour être indépendant d'un logiciel: on peut lire des fichiers Parquet avec `R`, Python, C++, Java... - il est conçu pour que les données puissent être chargées très rapidement en mémoire. -Un point important à noter est que __Parquet encode 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. -C'est une des raisons pour lesquelles la recommandation suivante est faite à l'Insee :**Parquet présente quelques propriétés qui le distingue des formats de fichiers plus populaires :** ### Caractéristiques du format Parquet -- Parquet repose sur un **stockage orienté colonne**. Ainsi seront stockées dans un premier temps toutes les données du premier attribut, puis seulement dans un second temps les données du deuxième attribut 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 : +Le format Parquet présente trois caractéristiques importantes du point de l'utilisateur: + +- __Parquet encode 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") ``` -Dans un contexte analytique, cette organisation des données génère plusieurs avantages dont les principaux sont : -- **Un gain de rapidité lors de la lecture des données pour un usage statistique**. Il n'est en effet pas nécessaire de scanner toutes les lignes pour ne lire que certaines colonnes comme ce serait le cas avec le `csv` ; -- **La possibilité d'avoir un haut niveau de compression**. Le taux de compression moyen par rapport au `csv` est entre 5 et 10. Pour des fichiers volumineux il est possible d'avoir des taux de compression bien supérieurs. +- **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). 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). -Dans un contexte analytique, cette organisation des données génère plusieurs avantages dont les principaux sont : +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. +- **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. -**Un fichier Parquet contient à la fois les données et des métadonnées**. Ces métadonnées écrites à la fin du fichier enregistrent le schéma de ce fichier selon 3 niveaux : fichier, bloc et en-tête de page (voir [ici](https://parquet.apache.org/docs/file-format/metadata/) pour en savoir plus). 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. +Inversement, le format Parquet présente deux contraintes inhabituelles pour les utilisateurs des autres formats (CSV, SAS, FST...): + +- Il n'est pas possible de charger seulement quelques lignes d'un fichier Parquet: on importe nécessairement des colonnes entières; +- 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"_. @@ -69,6 +72,7 @@ Dans un premier temps, on importe le fichier plat avec la fonction **fread()** d ```{r, eval=FALSE} library(data.table) +library(magrittr) library(arrow) # Décompression du fichier zip @@ -134,10 +138,6 @@ Avec cette instruction, on a créé autant de répertoires que de modalités dif knitr::include_graphics("../pics/parquet/fichier_partition.png") ``` - - - - ## Lire un fichier Parquet avec `R` La fonction [`read_parquet()`](https://arrow.apache.org/docs/r/reference/read_parquet.html) 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). From 61f6496f5563a32856680c4f97b9c9fd8608a788 Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Mon, 27 Mar 2023 16:05:44 +0200 Subject: [PATCH 09/39] Renommer une table --- 03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index 204179fb..d6fd6e73 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -125,7 +125,7 @@ Pour créer des fichiers **Parquet** partitionnés, il faut utiliser la fonction ```{r, eval = FALSE} write_dataset( - dataset = read_parquet("Data/BPE_ENS.parquet"), + dataset = donnees_BPE, path = "Data/", partitioning = c("REG"), # la variable de partitionnement format="parquet" From 183f6383a8280297b3a5a144f6864c39805fda5b Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Mon, 27 Mar 2023 16:06:02 +0200 Subject: [PATCH 10/39] =?UTF-8?q?R=C3=A9=C3=A9criture=20partie=20Parquet?= =?UTF-8?q?=20partitionn=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Fiche_import_fichiers_parquet.qmd | 77 ++++++++++++++++--- 1 file changed, 68 insertions(+), 9 deletions(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index d6fd6e73..1692d84d 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -243,30 +243,89 @@ Cette instruction s'exécute sur mon espace de travail en environ 0.5 secondes ! ## Exploiter un fichier Parquet partitionné -Cela permet de pouvoir exécuter du code sur une table volumineuse qui dépasse la mémoire de son espace de travail dans la mesure où les requêtes seront alors exécutées selon **un plan d'exécution optimal**. +### Quel est l'intérêt d'utiliser des fichiers Parquet partitionnés? +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 la requête 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. Cette optimisation du plan d'exécution (appelée _predicate push-down_) permet d'accélérer les traitements et de réduire la consommation de ressources informatiques. -Le statisticien peut désormais requêter les fichiers partitionnés à l'aide de la fonction [`open_dataset()](https://arrow.apache.org/docs/r/reference/open_dataset.html) qui permet d’ouvrir une connexion vers un ensemble partitionné de fichiers **Parquet** qui décrivent la même table de données. +**Utiliser un fichier Parquet partitionné facilite ce travail d'optimisation.** Comme mentionné plus haut, il n'est pas possible de charger seulement quelques lignes d'un fichier Parquet: on importe nécessairement des colonnes entières. Toutefois, 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} -open_dataset("Data",hive_style = FALSE) |> - filter(REG == "76") |> # Ici, on filtre selon la clé de partitionnement +open_dataset( + "Data/", + partitioning = arrow::schema(REG = arrow::utf8()) +) %>% + filter(REG == "32") %>% + select(DEP, TYPEQU, NB_EQUIP) %>% + group_by(DEP, TYPEQU) %>% + summarise(nb_equipements = sum(NB_EQUIP)) %>% + 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 particulier lorsque celles-ci sont volumineuses. + +### Comment bien utiliser les fichiers Parquet partitionnés? + +La fonction [`open_dataset()`](https://arrow.apache.org/docs/r/reference/open_dataset.html) permet d’ouvrir une connexion vers un fichier Parquet partitionné. Une fois que la connexion est établie avec le fichier partitionné, il est possible de l'utiliser exactement comme une table de données chargée en mémoire. Voici un exemple de code: + + +```{r, eval = FALSE} +open_dataset( + "Data/", + hive_style = TRUE, + partitioning = arrow::schema(REG = arrow::utf8()) +) |> + filter(REG == "32") |> # Ici, on filtre selon la clé de partitionnement + select(DEP, TYPEQU, NB_EQUIP) %>% group_by(DEP) |> - summarise(total = sum(NB_EQUIP)) |> + summarise(nb_total = sum(NB_EQUIP)) |> collect() ``` +Pour + + + + + + + +::: {.callout-conseil .icon} +Deux conseils importants: + +- 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) + ``` + +- 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... Cet argument s'utilise en construisant un schéma qui précise le type de chacune des variables de partitionnement: + + ```{r, eval = FALSE} + 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, ous pouvez en consulter la liste en exécutant `?arrow::float`. + +::: + ::: {.callout-conseil .icon} - 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** définies (dans notre exemple, la région). ::: Cette méthode de partitionnement est très pratique car elle : -- Permet de travailler sur des fichiers **Parquet** de plus petite taille et de consommer moins de mémoire vive ; -- Facilite la maintenance des fichiers : seuls les fichiers concernés seront affectés si une mise à jour des données devaient avoir lieu (par exemple sur la région "76") +- Permet de travailler sur des fichiers **Parquet** de plus petite taille et de consommer moins de mémoire vive; +- Facilite la maintenance des fichiers : seuls les fichiers concernés seront affectés si une mise à jour des données devaient avoir lieu (par exemple sur la région "76"); - Fait gagner du temps dans l'exécution des requêtes sur les fichiers volumineux (par rapport à un fichier **Parquet** unique).* [Page officielle du projet Arrow](https://arrow.apache.org/) -Enfin, quelques précisions concernant le plan d'exécution d'`arrow`. Celui-ci fonctionne selon un `predicate push-down` ce qui signifie que les données sont lues uniquement aux endroits où elles sont utiles pour exécuter la requête. Le terme `predicate push-down` vient du fait que l'utilisateur indique à l'opérateur de balayage de la requête le prédicat qui sera ensuite utilisé pour filtrer les lignes d'intérêt. Ce mode de fonctionnement moderne se traduite par des **gains de temps importants** lors de l'exécution des requêtes. - ## Pour en savoir plus * [Page officielle de duckdb](https://duckdb.org/) From ef1d79ad2f10b62bfee51752113c274ade54d6fc Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Mon, 27 Mar 2023 16:10:10 +0200 Subject: [PATCH 11/39] Finalisation partie partition --- .../Fiche_import_fichiers_parquet.qmd | 47 +++++++------------ 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index 1692d84d..36debb08 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -263,7 +263,12 @@ open_dataset( 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 particulier lorsque celles-ci sont volumineuses. +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 trois avantages : +- Elle permet de travailler sur des fichiers **Parquet** de plus petite taille et de consommer moins de mémoire vive; +- Elle facilite la maintenance des fichiers : seuls les fichiers concernés seront affectés si une mise à jour des données devaient avoir lieu (par exemple sur la région "76"); +- Elle fait gagner du temps dans l'exécution des requêtes sur les fichiers volumineux (par rapport à un fichier **Parquet** unique).* [Page officielle du projet Arrow](https://arrow.apache.org/) ### Comment bien utiliser les fichiers Parquet partitionnés? @@ -283,27 +288,9 @@ open_dataset( collect() ``` -Pour - - - - - - - -::: {.callout-conseil .icon} -Deux conseils importants: - -- 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) - ``` +Pour bien utiliser un fichier Parquet partitionné, il est recommandé de suivre les trois 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** définies (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... Cet argument s'utilise en construisant un schéma qui précise le type de chacune des variables de partitionnement: ```{r, eval = FALSE} @@ -314,17 +301,15 @@ Deux conseils importants: ``` 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, ous pouvez en consulter la liste en exécutant `?arrow::float`. +- 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: -::: - -::: {.callout-conseil .icon} -- 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** définies (dans notre exemple, la région). -::: - -Cette méthode de partitionnement est très pratique car elle : -- Permet de travailler sur des fichiers **Parquet** de plus petite taille et de consommer moins de mémoire vive; -- Facilite la maintenance des fichiers : seuls les fichiers concernés seront affectés si une mise à jour des données devaient avoir lieu (par exemple sur la région "76"); -- Fait gagner du temps dans l'exécution des requêtes sur les fichiers volumineux (par rapport à un fichier **Parquet** unique).* [Page officielle du projet Arrow](https://arrow.apache.org/) + ```{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) + ``` ## Pour en savoir plus From c9db49acc9f5296ca5122393470fb09ff8b8b044 Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Mon, 27 Mar 2023 16:13:25 +0200 Subject: [PATCH 12/39] Ajouter une ligne --- 03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd | 1 + 1 file changed, 1 insertion(+) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index 36debb08..ac5c9b56 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -263,6 +263,7 @@ open_dataset( 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 trois avantages : From 3461ad8c38feb1e5cf176a0cc898b719d5a9af71 Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Mon, 27 Mar 2023 18:03:26 +0200 Subject: [PATCH 13/39] Points-virgules --- 03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index ac5c9b56..70114bc1 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -7,9 +7,9 @@ ::: {.callout-recommandation .icon} -- 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 et de manipuler simplement les fichiers au format **Parquet** avec `R`. -- Il est essentiel de travailler avec la dernière version d'`arrow` et de `R` car le _package_ `arrow` est en cours de développement. -- 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 logique (département, secteur, année...). +- 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 et de manipuler simplement les fichiers au format **Parquet** avec `R`; +- Il est essentiel de travailler avec la dernière version d'`arrow` et de `R` car le _package_ `arrow` est en cours de développement; +- 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 logique (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. ::: From ac07bb9027cac249c2b868b9d2d733ba2b735a97 Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Mon, 27 Mar 2023 18:27:01 +0200 Subject: [PATCH 14/39] Ajouter la fiche parquet --- _quarto.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/_quarto.yml b/_quarto.yml index 75dec626..82919af4 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -57,6 +57,7 @@ book: - 03_Fiches_thematiques/Fiche_import_fichiers_plats.qmd - 03_Fiches_thematiques/Fiche_import_tables_sas.qmd - 03_Fiches_thematiques/Fiche_import_tableurs.qmd + - 03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd - 03_Fiches_thematiques/Fiche_api.qmd - 03_Fiches_thematiques/Fiche_connexion_bdd.qmd - part: "Manipuler des données avec R" From c5fb3ea8e2a421725304a056fb602cc2fd137878 Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Mon, 27 Mar 2023 18:35:02 +0200 Subject: [PATCH 15/39] =?UTF-8?q?D=C3=A9placer=20des=20=C3=A9l=C3=A9ments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Fiche_import_fichiers_parquet.qmd | 140 +++++++++--------- 1 file changed, 71 insertions(+), 69 deletions(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index 70114bc1..e2097217 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -140,7 +140,9 @@ knitr::include_graphics("../pics/parquet/fichier_partition.png") ## Lire un fichier Parquet avec `R` -La fonction [`read_parquet()`](https://arrow.apache.org/docs/r/reference/read_parquet.html) 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). +**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, c'est ce que nous allons voir dans les sections suivantes. + +La fonction [`read_parquet()`](https://arrow.apache.org/docs/r/reference/read_parquet.html) permet d'importer un fichier 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` : @@ -172,76 +174,10 @@ donnees <- arrow::read_parquet( ) ``` -Dans les trois cas, le résultat obtenu est un objet directement utilisable dans R. :tada: - -La méthode présentée dans cette section est valable 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, c'est ce que nous allons voir dans les sections suivantes. - -## Exploiter un fichier Parquet avec le package dplyr - -Si le statisticien souhaite travailler sur des fichiers plus volumineux (par exemple [celui des données du recensement de la population 1968-2019](https://www.insee.fr/fr/statistiques/6671801) de 3,2 Go et qui contient plus de 51,5 millions de lignes et 18 colonnes), il peut se heurter à **un manque de mémoire vive** s'il souhaite importer dans `R` l'intégralité de la table avant de pouvoir l'exploiter. - -## Exploiter un fichier Parquet avec le _package_ `dplyr` -- Exemple avec une table peu volumineuse : - -```{r, eval=FALSE} -library(dplyr) -library(arrow) - -open_dataset("Data/BPE_ENS.parquet") |> - filter(REG == "76") |> - group_by(DEP) |> - collect() -``` - -Avec cette syntaxe, la requête va utiliser seulement les variables du fichier **Parquet** dont elle a besoin (en l'occurence `REG`, `DEP` et `NB_EQUIP`). - -- 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} -library(dplyr) - -open_dataset("Data/Ficdep19.parquet") |> - filter(DEP_RES_21 == "11") |> - group_by(SEXE) |> - summarise(total = sum(pond)) |> - as.data.frame() |> - collect() -``` +Dans les trois cas, le résultat obtenu est un objet directement utilisable dans R. :tada: -Cette instruction s'exécute sur mon espace de travail en un peu plus de 2 secondes. - -## 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://www.book.utilitr.org/03_fiches_thematiques/fiche_connexion_bdd#ex%C3%A9cuter-des-requ%C3%AAtes). Il faut noter que la méthode présentée ici est encore un peu plus efficace que celle présentée avec dans la section précédente avec les fonctions de `dplyr`. - -En `R`, il faut charger le package `duckdb` : - -```{r, eval = FALSE} -library(duckdb) -``` - -- Exemple avec une table peu volumineuse : - -```{r, eval = FALSE} -con <- dbConnect(duckdb::duckdb()) - -dbGetQuery(con, "SELECT SUM(NB_EQUIP) FROM 'Data/BPE_ENS.parquet' - WHERE REG='76' - GROUP BY DEP") -``` - -- Exemple avec une table volumineuse (RP 1968-2019) : - -```{r, eval = FALSE} -con <- dbConnect(duckdb::duckdb()) - -dbGetQuery(con, "SELECT SUM(POND) FROM 'Data/Ficdep19.parquet' - WHERE DEP_RES_21='11' - GROUP BY SEXE") -``` -Cette instruction s'exécute sur mon espace de travail en environ 0.5 secondes ! -## Exploiter un fichier Parquet partitionné +## Lire un fichier Parquet partitionné ### Quel est l'intérêt d'utiliser des fichiers Parquet partitionnés? @@ -312,6 +248,72 @@ Pour bien utiliser un fichier Parquet partitionné, il est recommandé de suivre arrow:::set_cpu_count(10) ``` +## Exploiter un fichier Parquet avec le package `dplyr` + +Si le statisticien souhaite travailler sur des fichiers plus volumineux (par exemple [celui des données du recensement de la population 1968-2019](https://www.insee.fr/fr/statistiques/6671801) de 3,2 Go et qui contient plus de 51,5 millions de lignes et 18 colonnes), il peut se heurter à **un manque de mémoire vive** s'il souhaite importer dans `R` l'intégralité de la table avant de pouvoir l'exploiter. + +## Exploiter un fichier Parquet avec le _package_ `dplyr` +- Exemple avec une table peu volumineuse : + +```{r, eval=FALSE} +library(dplyr) +library(arrow) + +open_dataset("Data/BPE_ENS.parquet") |> + filter(REG == "76") |> + group_by(DEP) |> + collect() +``` + +Avec cette syntaxe, la requête va utiliser seulement les variables du fichier **Parquet** dont elle a besoin (en l'occurence `REG`, `DEP` et `NB_EQUIP`). + +- 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} +library(dplyr) + +open_dataset("Data/Ficdep19.parquet") |> + filter(DEP_RES_21 == "11") |> + group_by(SEXE) |> + summarise(total = sum(pond)) |> + as.data.frame() |> + collect() +``` + +Cette instruction s'exécute sur mon espace de travail en un peu plus de 2 secondes. + +## 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://www.book.utilitr.org/03_fiches_thematiques/fiche_connexion_bdd#ex%C3%A9cuter-des-requ%C3%AAtes). Il faut noter que la méthode présentée ici est encore un peu plus efficace que celle présentée avec dans la section précédente avec les fonctions de `dplyr`. + +En `R`, il faut charger le package `duckdb` : + +```{r, eval = FALSE} +library(duckdb) +``` + +- Exemple avec une table peu volumineuse : + +```{r, eval = FALSE} +con <- dbConnect(duckdb::duckdb()) + +dbGetQuery(con, "SELECT SUM(NB_EQUIP) FROM 'Data/BPE_ENS.parquet' + WHERE REG='76' + GROUP BY DEP") +``` + +- Exemple avec une table volumineuse (RP 1968-2019) : + +```{r, eval = FALSE} +con <- dbConnect(duckdb::duckdb()) + +dbGetQuery(con, "SELECT SUM(POND) FROM 'Data/Ficdep19.parquet' + WHERE DEP_RES_21='11' + GROUP BY SEXE") +``` +Cette instruction s'exécute sur mon espace de travail en environ 0.5 secondes ! + + ## Pour en savoir plus * [Page officielle de duckdb](https://duckdb.org/) From 35970146831216a433b3040953cd1f561151d9e7 Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Mon, 27 Mar 2023 18:49:56 +0200 Subject: [PATCH 16/39] Structure --- 03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index e2097217..131fbfd6 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -138,7 +138,9 @@ Avec cette instruction, on a créé autant de répertoires que de modalités dif knitr::include_graphics("../pics/parquet/fichier_partition.png") ``` -## Lire un fichier Parquet avec `R` +## Lire et exploiter un fichier Parquet avec `R` + +### 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, c'est ce que nous allons voir dans les sections suivantes. From ad626de9e60fec7883cd5c519987ed68a1cd0cb3 Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Mon, 27 Mar 2023 18:50:11 +0200 Subject: [PATCH 17/39] =?UTF-8?q?Compl=C3=A9ments=20sur=20dplyr?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Fiche_import_fichiers_parquet.qmd | 137 +++++++++--------- 1 file changed, 71 insertions(+), 66 deletions(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index 131fbfd6..316b9c69 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -178,6 +178,77 @@ donnees <- arrow::read_parquet( Dans les trois cas, le résultat obtenu est un objet directement utilisable dans R. :tada: +### 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 portable 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 comprend trois étapes: + +- On crée une connexion au fichier Parquet avec la fonction `open_dataset()`; +- 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)); +- On termine la equête avec la fonction `collect()`, qui indique à `R` que l'on souhaite récupérer le résultat du traitement sous forme d'un `data.frame`. + +Voici un exemple avec une table peu volumineuse : + +```{r, eval=FALSE} +library(dplyr) +library(arrow) + +open_dataset("Data/BPE_ENS.parquet") |> + filter(REG == "76") |> + group_by(DEP) |> + 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} +library(dplyr) + +open_dataset("Data/Ficdep19.parquet") |> + filter(DEP_RES_21 == "11") |> + group_by(SEXE) |> + summarise(total = sum(pond)) |> + as.data.frame() |> + collect() +``` + +Cette instruction s'exécute sur un ordinateur standard en quelques secondes. + +## 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://www.book.utilitr.org/03_fiches_thematiques/fiche_connexion_bdd#ex%C3%A9cuter-des-requ%C3%AAtes). Il faut noter que la méthode présentée ici est encore un peu plus efficace que celle présentée avec dans la section précédente avec les fonctions de `dplyr`. + +En `R`, il faut charger le package `duckdb` : + +```{r, eval = FALSE} +library(duckdb) +``` + +- Exemple avec une table peu volumineuse : + +```{r, eval = FALSE} +con <- dbConnect(duckdb::duckdb()) + +dbGetQuery(con, "SELECT SUM(NB_EQUIP) FROM 'Data/BPE_ENS.parquet' + WHERE REG='76' + GROUP BY DEP") +``` + +- Exemple avec une table volumineuse (RP 1968-2019) : + +```{r, eval = FALSE} +con <- dbConnect(duckdb::duckdb()) + +dbGetQuery(con, "SELECT SUM(POND) FROM 'Data/Ficdep19.parquet' + WHERE DEP_RES_21='11' + GROUP BY SEXE") +``` +Cette instruction s'exécute sur mon espace de travail en environ 0.5 secondes ! + + ## Lire un fichier Parquet partitionné @@ -250,72 +321,6 @@ Pour bien utiliser un fichier Parquet partitionné, il est recommandé de suivre arrow:::set_cpu_count(10) ``` -## Exploiter un fichier Parquet avec le package `dplyr` - -Si le statisticien souhaite travailler sur des fichiers plus volumineux (par exemple [celui des données du recensement de la population 1968-2019](https://www.insee.fr/fr/statistiques/6671801) de 3,2 Go et qui contient plus de 51,5 millions de lignes et 18 colonnes), il peut se heurter à **un manque de mémoire vive** s'il souhaite importer dans `R` l'intégralité de la table avant de pouvoir l'exploiter. - -## Exploiter un fichier Parquet avec le _package_ `dplyr` -- Exemple avec une table peu volumineuse : - -```{r, eval=FALSE} -library(dplyr) -library(arrow) - -open_dataset("Data/BPE_ENS.parquet") |> - filter(REG == "76") |> - group_by(DEP) |> - collect() -``` - -Avec cette syntaxe, la requête va utiliser seulement les variables du fichier **Parquet** dont elle a besoin (en l'occurence `REG`, `DEP` et `NB_EQUIP`). - -- 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} -library(dplyr) - -open_dataset("Data/Ficdep19.parquet") |> - filter(DEP_RES_21 == "11") |> - group_by(SEXE) |> - summarise(total = sum(pond)) |> - as.data.frame() |> - collect() -``` - -Cette instruction s'exécute sur mon espace de travail en un peu plus de 2 secondes. - -## 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://www.book.utilitr.org/03_fiches_thematiques/fiche_connexion_bdd#ex%C3%A9cuter-des-requ%C3%AAtes). Il faut noter que la méthode présentée ici est encore un peu plus efficace que celle présentée avec dans la section précédente avec les fonctions de `dplyr`. - -En `R`, il faut charger le package `duckdb` : - -```{r, eval = FALSE} -library(duckdb) -``` - -- Exemple avec une table peu volumineuse : - -```{r, eval = FALSE} -con <- dbConnect(duckdb::duckdb()) - -dbGetQuery(con, "SELECT SUM(NB_EQUIP) FROM 'Data/BPE_ENS.parquet' - WHERE REG='76' - GROUP BY DEP") -``` - -- Exemple avec une table volumineuse (RP 1968-2019) : - -```{r, eval = FALSE} -con <- dbConnect(duckdb::duckdb()) - -dbGetQuery(con, "SELECT SUM(POND) FROM 'Data/Ficdep19.parquet' - WHERE DEP_RES_21='11' - GROUP BY SEXE") -``` -Cette instruction s'exécute sur mon espace de travail en environ 0.5 secondes ! - - ## Pour en savoir plus * [Page officielle de duckdb](https://duckdb.org/) From 10d70806a00661ef23cbd2edcc96663eaa97d2d9 Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Mon, 27 Mar 2023 19:10:43 +0200 Subject: [PATCH 18/39] =?UTF-8?q?Am=C3=A9liorations=20de=20la=20partie=20d?= =?UTF-8?q?uckdb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Fiche_import_fichiers_parquet.qmd | 69 ++++++++++++++----- 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index 316b9c69..04443cbf 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -184,9 +184,9 @@ Il arrive fréquemment que la méthode proposée dans la section précédente ne **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 comprend trois étapes: -- On crée une connexion au fichier Parquet avec la fonction `open_dataset()`; +- On crée une connexion au fichier Parquet avec la fonction `open_dataset()`; cela nécessite de charger les _package_ `arrow` et `dplyr`; - 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)); -- On termine la equête avec la fonction `collect()`, qui indique à `R` que l'on souhaite récupérer le résultat du traitement sous forme d'un `data.frame`. +- 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 : @@ -194,10 +194,17 @@ Voici un exemple avec une table peu volumineuse : library(dplyr) library(arrow) -open_dataset("Data/BPE_ENS.parquet") |> +# É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) |> - collect() + 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. @@ -207,46 +214,72 @@ Avec cette syntaxe, la requête va automatiquement utiliser les variables du fic ```{r, eval=FALSE} library(dplyr) -open_dataset("Data/Ficdep19.parquet") |> +# É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)) |> as.data.frame() |> 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. ## 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://www.book.utilitr.org/03_fiches_thematiques/fiche_connexion_bdd#ex%C3%A9cuter-des-requ%C3%AAtes). Il faut noter que la méthode présentée ici est encore un peu plus efficace que celle présentée avec dans la section précédente avec les fonctions de `dplyr`. +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://www.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`. + -En `R`, il faut charger le package `duckdb` : +Voici un exemple avec une table peu volumineuse : ```{r, eval = FALSE} +library(DBI) library(duckdb) -``` -- Exemple avec une table peu volumineuse : -```{r, eval = FALSE} +# Établir la connexion au moteur duckdb con <- dbConnect(duckdb::duckdb()) -dbGetQuery(con, "SELECT SUM(NB_EQUIP) FROM 'Data/BPE_ENS.parquet' +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") + GROUP BY DEP" + +# Récupérer le résultat sous forme d'un data.frame +resultat3 <- dbGetQuery(con, requete3) ``` - -- Exemple avec une table volumineuse (RP 1968-2019) : + + +Voici un exemple avec une table volumineuse (RP 1968-2019) : ```{r, eval = FALSE} +# Établir la connexion au moteur duckdb con <- dbConnect(duckdb::duckdb()) - -dbGetQuery(con, "SELECT SUM(POND) FROM 'Data/Ficdep19.parquet' + +# Définir la requête (en SQL) +requete4 < - "SELECT SUM(POND) FROM 'Data/Ficdep19.parquet' WHERE DEP_RES_21='11' - GROUP BY SEXE") + GROUP BY SEXE" + +# Récupérer le résultat sous forme d'un data.frame +resultat4 <- dbGetQuery(con, requete4) ``` -Cette instruction s'exécute sur mon espace de travail en environ 0.5 secondes ! + +Cette instruction s'exécute également en quelques secondes sur un ordinateur standard. From e7ffae771fd6a3821ad556cd8421143041cae5f9 Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Mon, 27 Mar 2023 19:29:14 +0200 Subject: [PATCH 19/39] =?UTF-8?q?Compl=C3=A9ments=20divers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Fiche_import_fichiers_parquet.qmd | 57 ++++++++++++------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index 04443cbf..7c7c8279 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -182,12 +182,16 @@ Dans les trois cas, le résultat obtenu est un objet directement utilisable dans 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 portable 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 comprend trois étapes: +**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()`; cela nécessite de charger les _package_ `arrow` et `dplyr`; +- 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)); - 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`. +::: {.callout-remarque .icon} +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 la requête 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. +::: + Voici un exemple avec une table peu volumineuse : ```{r, eval=FALSE} @@ -283,26 +287,30 @@ Cette instruction s'exécute également en quelques secondes sur un ordinateur s -## Lire un fichier Parquet partitionné +## Lire et exploiter un fichier Parquet partitionné ### Quel est l'intérêt d'utiliser des fichiers Parquet partitionnés? -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 la requête 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. Cette optimisation du plan d'exécution (appelée _predicate push-down_) permet d'accélérer les traitements et de réduire la consommation de ressources informatiques. - -**Utiliser un fichier Parquet partitionné facilite ce travail d'optimisation.** Comme mentionné plus haut, il n'est pas possible de charger seulement quelques lignes d'un fichier Parquet: on importe nécessairement des colonnes entières. Toutefois, 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. +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} -open_dataset( +)# Établir la connexion au fichier Parquet partitionné +donnees_BPE_part <- open_dataset( "Data/", partitioning = arrow::schema(REG = arrow::utf8()) -) %>% - filter(REG == "32") %>% +) + +# 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)) %>% - collect() + summarise(nb_equipements = sum(NB_EQUIP)) + +# Récupérer le résultat sous forme d'un data.frame +resultat_BPE <- requete_BPE |> collect() ``` @@ -315,26 +323,37 @@ En conclusion, l'utilisation des fichiers Parquet partitionné présente trois a ### Comment bien utiliser les fichiers Parquet partitionnés? -La fonction [`open_dataset()`](https://arrow.apache.org/docs/r/reference/open_dataset.html) permet d’ouvrir une connexion vers un fichier Parquet partitionné. Une fois que la connexion est établie avec le fichier partitionné, il est possible de l'utiliser exactement comme une table de données chargée en mémoire. Voici un exemple de code: +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} -open_dataset( - "Data/", +# É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()) -) |> + 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)) |> - collect() + 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 trois 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** définies (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... Cet argument s'utilise en construisant un schéma qui précise le type de chacune des variables de partitionnement: +- 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} open_dataset( From 7ecef56c29dde3d3edbc42b2e931976c873d6685 Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Mon, 27 Mar 2023 19:30:42 +0200 Subject: [PATCH 20/39] Coquille de forme --- .../Fiche_import_fichiers_parquet.qmd | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index 7c7c8279..581a2b89 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -355,23 +355,23 @@ Pour bien utiliser un fichier Parquet partitionné, il est recommandé de suivre - 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** définies (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} - open_dataset( - "Data/", - partitioning = arrow::schema(variable1 = arrow::utf8(), variable2 = arrow::int16()) - ) - ``` +```{r, eval = FALSE} +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, ous pouvez en consulter la liste en exécutant `?arrow::float`. - 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) - ``` +```{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) +``` ## Pour en savoir plus From ffa125354b3289b87e1b1af696755f6597968380 Mon Sep 17 00:00:00 2001 From: Olivier Meslin <44379737+oliviermeslin@users.noreply.github.com> Date: Tue, 28 Mar 2023 09:32:19 +0200 Subject: [PATCH 21/39] Update 03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd Co-authored-by: Romain Lesur --- 03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index 581a2b89..82231077 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -33,7 +33,7 @@ Le format Parquet présente plusieurs avantages cruciaux qui en font un concurre Le format Parquet présente trois caractéristiques importantes du point de l'utilisateur: -- __Parquet encode 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 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 : From 8884e1d0be053b8cc1a1ddc128f39f0a4db350b1 Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Tue, 28 Mar 2023 09:35:45 +0200 Subject: [PATCH 22/39] =?UTF-8?q?Int=C3=A9grer=20les=20commentaires=20de?= =?UTF-8?q?=20Romain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Fiche_import_fichiers_parquet.qmd | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index 581a2b89..4732d1be 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -9,7 +9,7 @@ - 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 et de manipuler simplement les fichiers au format **Parquet** avec `R`; - Il est essentiel de travailler avec la dernière version d'`arrow` et de `R` car le _package_ `arrow` est en cours de développement; -- 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 logique (département, secteur, année...); +- 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érentes 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. ::: @@ -62,12 +62,10 @@ Pour cette utilisation, le statisticien privilégiera un système de gestion de ## Écrire des fichiers Parquet -### Cas simple: écrire un seul fichier 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.** -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.** Cet exemple repose sur un fichier volumineux disponible sur le site de l'Insee. - 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://www.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`. Comme vous pouvez le voir ci-dessous, ces deux étapes sont réalisées en un seul temps, ce qui réduit l'utilisation de ressources informatiques. ```{r, eval=FALSE} @@ -108,10 +106,10 @@ write_parquet( ) ``` -### Cas complexe: écrire un fichier Parquet partitionné +### 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 un fichier Parquet avec `R`]). +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. @@ -138,7 +136,7 @@ Avec cette instruction, on a créé autant de répertoires que de modalités dif knitr::include_graphics("../pics/parquet/fichier_partition.png") ``` -## Lire et exploiter un fichier Parquet avec `R` +## Lire et exploiter un fichier Parquet avec `R` {#readparquet} ### Cas des données peu volumineuses: importer les données en mémoire @@ -296,7 +294,7 @@ Comme indiqué précédemment, les _packages_ `arrow` et `duckdb` ne se contente 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é +# Établir la connexion au fichier Parquet partitionné donnees_BPE_part <- open_dataset( "Data/", partitioning = arrow::schema(REG = arrow::utf8()) @@ -316,10 +314,11 @@ 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 trois avantages : +En conclusion, l'utilisation des fichiers Parquet partitionné présente trois avantages : + - Elle permet de travailler sur des fichiers **Parquet** de plus petite taille et de consommer moins de mémoire vive; - Elle facilite la maintenance des fichiers : seuls les fichiers concernés seront affectés si une mise à jour des données devaient avoir lieu (par exemple sur la région "76"); -- Elle fait gagner du temps dans l'exécution des requêtes sur les fichiers volumineux (par rapport à un fichier **Parquet** unique).* [Page officielle du projet Arrow](https://arrow.apache.org/) +- 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? From eb55e6b26f23f6e61c3c34c7427614a100f556ed Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Tue, 28 Mar 2023 09:37:02 +0200 Subject: [PATCH 23/39] =?UTF-8?q?Compl=C3=A9ment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index d6fbd2c1..a8bf177b 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -25,7 +25,7 @@ Note: cette fiche n'a pas vocation à être exhaustive sur le format Parquet, ma 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++, Java... +- 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. From 1769bcf8f497db02095facc1cc45e23c72d4dd60 Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Tue, 28 Mar 2023 11:04:43 +0200 Subject: [PATCH 24/39] Parler de duckdb dans les recommandations --- 03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index a8bf177b..c8abf3bb 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -7,8 +7,13 @@ ::: {.callout-recommandation .icon} -- 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 et de manipuler simplement les fichiers au format **Parquet** avec `R`; -- Il est essentiel de travailler avec la dernière version d'`arrow` et de `R` car le _package_ `arrow` est en cours de développement; +- 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 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érentes 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. From 543f277c8602c32efe3405b1e543bea1f3a1dfcd Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Tue, 28 Mar 2023 11:07:32 +0200 Subject: [PATCH 25/39] =?UTF-8?q?Remarque=20de=20Jean-Fran=C3=A7ois?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Fiche_import_fichiers_parquet.qmd | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index c8abf3bb..65500dc9 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -71,21 +71,30 @@ Pour cette utilisation, le statisticien privilégiera un système de gestion de 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://www.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`. Comme vous pouvez le voir ci-dessous, ces deux étapes sont réalisées en un seul temps, ce qui réduit l'utilisation de ressources informatiques. +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://www.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/dpt2021_csv.zip", exdir = "Data") -# Création du dossier "Data" -dir.create("Data") -# Conversion du fichier csv au format parquet +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 = fread("Data/dpt2021.csv"), # Utilisation de la fonction fread() - sink = "Data/dpt2021.parquet" + x = dpt2021, + sink = "Data_parquet/dpt2021.parquet" ) ``` From aed18f63e44f29388ed56e0aef83091a1e57120d Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Tue, 28 Mar 2023 11:07:40 +0200 Subject: [PATCH 26/39] Idem --- 03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index 65500dc9..bc0b35f3 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -108,15 +108,13 @@ Vous pouvez télécharger ce fichier avec le package [`doremifasol`](https://ins library(doremifasol) library(arrow) -# Création du dossier "Data" -dir.create("Data") - # 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/BPE_ENS.parquet" + sink = "Data_parquet/BPE_ENS.parquet" ) ``` From 435f4f31c7deeadad0d35d475905f1db21dbd98e Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Tue, 28 Mar 2023 11:07:47 +0200 Subject: [PATCH 27/39] Idem --- 03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd | 1 - 1 file changed, 1 deletion(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index bc0b35f3..fc432cd1 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -236,7 +236,6 @@ requete2 < - donnees_Ficdep19 |> filter(DEP_RES_21 == "11") |> group_by(SEXE) |> summarise(total = sum(pond)) |> - as.data.frame() |> collect() # Récupérer le résultat sous forme d'un data.frame From e6b281f40fd20b6f48f3f673b49cc8dd72e90640 Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Tue, 28 Mar 2023 11:08:00 +0200 Subject: [PATCH 28/39] Itou --- 03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index fc432cd1..2433e8a2 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -325,10 +325,9 @@ 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 trois avantages : +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 facilite la maintenance des fichiers : seuls les fichiers concernés seront affectés si une mise à jour des données devaient avoir lieu (par exemple sur la région "76"); - 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? From b0b071a9c422194e12270a4142a1929266f1bf2b Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Tue, 28 Mar 2023 11:23:19 +0200 Subject: [PATCH 29/39] Ibid --- 03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index 2433e8a2..c182b043 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -55,7 +55,7 @@ Dans un contexte analytique, cette organisation des données génère plusieurs Inversement, le format Parquet présente deux contraintes inhabituelles pour les utilisateurs des autres formats (CSV, SAS, FST...): -- Il n'est pas possible de charger seulement quelques lignes d'un fichier Parquet: on importe nécessairement des colonnes entières; +- Il n'est pas possible de charger les 100 premières lignes d'un fichier Parquet (comme on peut facilement le faire pour un fichier CSV); - 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 From c526e6030b0817f51748e7c82c833db11fb32f58 Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Tue, 28 Mar 2023 11:24:20 +0200 Subject: [PATCH 30/39] =?UTF-8?q?Pr=C3=A9cision?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index c182b043..eebf2299 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -226,6 +226,8 @@ Avec cette syntaxe, la requête va automatiquement utiliser les variables du fic - 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 From 46cd826de5e27e42aa6c0651377879aba6974bf9 Mon Sep 17 00:00:00 2001 From: Damien Dotta Date: Tue, 28 Mar 2023 15:19:29 +0200 Subject: [PATCH 31/39] Corrections mineures (#477) --- .../Fiche_import_fichiers_parquet.qmd | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index eebf2299..be402d20 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -14,7 +14,7 @@ - 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 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érentes avec l’usage des données (département, secteur, année...); +- 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. ::: @@ -186,11 +186,11 @@ donnees <- arrow::read_parquet( ) ``` -Dans les trois cas, le résultat obtenu est un objet directement utilisable dans R. :tada: +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 portable standard. +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: @@ -304,7 +304,7 @@ Cette instruction s'exécute également en quelques secondes sur un ordinateur 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: +**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é @@ -361,7 +361,7 @@ requete5 <- donnees_part |> resultat5 <- requete5 |> collect() ``` -Pour bien utiliser un fichier Parquet partitionné, il est recommandé de suivre les trois conseils suivants: +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** définies (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: @@ -373,7 +373,8 @@ open_dataset( ) ``` - 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, ous pouvez en consulter la liste en exécutant `?arrow::float`. +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} From b72dda2f92868bee399c3f173e2b84b918f1da39 Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Mon, 10 Apr 2023 08:22:20 +0200 Subject: [PATCH 32/39] =?UTF-8?q?Compl=C3=A9ment=20sur=20duckdb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Fiche_import_fichiers_parquet.qmd | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index eebf2299..872debf3 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -334,6 +334,8 @@ En conclusion, l'utilisation des fichiers Parquet partitionné présente deux av ### 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é; @@ -363,7 +365,7 @@ resultat5 <- requete5 |> collect() Pour bien utiliser un fichier Parquet partitionné, il est recommandé de suivre les trois 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** définies (dans notre exemple, la région); +- 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} @@ -384,6 +386,34 @@ options(arrow.use_threads = TRUE) 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'établir une connexion entre le moteur `duckdb` et le fichier Parquet partitionné avec la fonction `duckdb_register_arrow`. 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 +) + +# Établir un lien logique entre le moteur duckdb +duckdb::duckdb_register_arrow(con_ddb, "donnees_part", donnees_part) + + +``` + + + ## Pour en savoir plus * [Page officielle de duckdb](https://duckdb.org/) From 2d6dcf0cf608b31cacd0f46385a48bf00acb93d9 Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Mon, 10 Apr 2023 08:29:18 +0200 Subject: [PATCH 33/39] =?UTF-8?q?Compl=C3=A9ments=20et=20reformulations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Fiche_import_fichiers_parquet.qmd | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index cc3c1bcb..202f0a54 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -369,7 +369,7 @@ Pour bien utiliser un fichier Parquet partitionné, il est recommandé de suivre - 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} -open_dataset( +donnees_part <- open_dataset( "Data/", partitioning = arrow::schema(variable1 = arrow::utf8(), variable2 = arrow::int16()) ) @@ -389,14 +389,13 @@ 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'établir une connexion entre le moteur `duckdb` et le fichier Parquet partitionné avec la fonction `duckdb_register_arrow`. Voici un exemple: +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()) @@ -407,10 +406,11 @@ donnees_part <- open_dataset( partitioning = arrow::schema(REG = arrow::utf8()) # La variable de partitionnement ) -# Établir un lien logique entre le moteur duckdb +# 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") ``` From 3a78cc4546653e429f79532035b670311b49efbf Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Tue, 11 Apr 2023 12:03:33 +0200 Subject: [PATCH 34/39] Ajout recommandation --- 03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd | 1 + 1 file changed, 1 insertion(+) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index 202f0a54..892fdf07 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -14,6 +14,7 @@ - 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. From b59e70dab929eebfafe37b13e33bfe7fac81fe71 Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Tue, 11 Apr 2023 12:03:49 +0200 Subject: [PATCH 35/39] =?UTF-8?q?R=C3=A9pondre=20=C3=A0=20une=20remarque?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index 892fdf07..22598187 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -56,7 +56,7 @@ Dans un contexte analytique, cette organisation des données génère plusieurs Inversement, le format Parquet présente deux contraintes inhabituelles pour les utilisateurs des autres formats (CSV, SAS, FST...): -- Il n'est pas possible de charger les 100 premières lignes d'un fichier Parquet (comme on peut facilement le faire pour un fichier CSV); +- 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 From 73a3b80006b5fa83fce7e642bfc0a6cbf532367a Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Tue, 11 Apr 2023 12:04:37 +0200 Subject: [PATCH 36/39] =?UTF-8?q?Pr=C3=A9ciser=20les=20choses=20sur=20read?= =?UTF-8?q?=5Fparquet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index 22598187..afed175c 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -153,19 +153,14 @@ knitr::include_graphics("../pics/parquet/fichier_partition.png") ### 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, c'est ce que nous allons voir dans les sections suivantes. +**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) permet d'importer un fichier 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). +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) -``` - -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). - -```{r, eval = FALSE} donnees <- arrow::read_parquet("Data/BPE_ENS.parquet") ``` From a9c12f324c6624f706f5b514f5b1c06893276794 Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Tue, 11 Apr 2023 15:27:31 +0200 Subject: [PATCH 37/39] =?UTF-8?q?D=C3=A9placer=20une=20remarque?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index afed175c..483f8922 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -194,10 +194,6 @@ Il arrive fréquemment que la méthode proposée dans la section précédente ne - 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)); - 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`. -::: {.callout-remarque .icon} -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 la requête 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. -::: - Voici un exemple avec une table peu volumineuse : ```{r, eval=FALSE} @@ -242,6 +238,11 @@ resultat2 <- requete2 |> collect() Cette instruction s'exécute sur un ordinateur standard en quelques secondes. + +::: {.callout-remarque .icon} +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://www.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. From 2a2e486307b68e9fff401058a43d49c094e291b8 Mon Sep 17 00:00:00 2001 From: Damien Dotta Date: Tue, 11 Apr 2023 15:27:49 +0200 Subject: [PATCH 38/39] Ajout liste verbes tidyveres compatibles avec arrow (#482) Closes Dans la fiche parquet, ajouter le lien vers la liste des verbes issus du `Tidyverse` connus par `arrow` #481 --- 03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index afed175c..f6a5003e 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -191,7 +191,7 @@ Il arrive fréquemment que la méthode proposée dans la section précédente ne **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)); +- 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`. ::: {.callout-remarque .icon} From 6811622b1c2d6c3d81c1c29b69f5ce985ca58828 Mon Sep 17 00:00:00 2001 From: Meslin Olivier Date: Tue, 11 Apr 2023 16:29:16 +0200 Subject: [PATCH 39/39] Commenter toutes les parties sur duckdb --- .../Fiche_import_fichiers_parquet.qmd | 114 +++++++++--------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd index 5615320a..592fb7a1 100644 --- a/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd +++ b/03_Fiches_thematiques/Fiche_import_fichiers_parquet.qmd @@ -243,55 +243,55 @@ Cette instruction s'exécute sur un ordinateur standard en quelques secondes. 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://www.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. + + + + + + + + + + @@ -384,31 +384,31 @@ options(arrow.use_threads = TRUE) 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") -``` + + + + + +