class: center, middle, inverse, title-slide # Programmation fonctionnelle avec R ## R à Québec 2019 ### Philippe Massicotte ### 2019-05-14 --- <br> <center><img src="images/myname.png" alt="drawing" width="400"/></center> <p align="left"> <b>Assistant de recherche à Takuvik (télédétection, modélisation, data science)</b><br> - Utilise R depuis plus de 8 ans<br> - Auteur de 3 librairies sur CRAN<br> - Blog R: www.pmassicotte.com<br> <br> <i class="fab fa-github"></i> https://github.com/PMassicotte <br> <i class="far fa-envelope"></i> philippe.massicotte@takuvik.ulaval.ca <br> <i class="fab fa-twitter"></i> @philmassicotte </p> --- class: inverse, center, middle # Plan de la formation --- ## Plan de la formation 1. Mise en contexte * Les dangers de la duplication du code: une étude de cas 2. La programmation fonctionnelle * Opérations sur des vecteurs * Opérations sur des *data frame* 3. Présentation disponible à cette adresse: **http://www.pmassicotte.com/rquebec2019/** 4. Données pour les exercices: **https://bit.ly/2W3KJjw** * Théorie + exercices après chaque concept * **Votre participation est essentielle!** --- ## La manipulation de données Dans plusieurs domaines, les données sont produites très rapidement: - Environnement: capteurs automatisés (température, précipitation, etc.) - Finances / assurances - Transport: géolocalisation La manipulation de ces données demande beaucoup de temps et le cycle doit souvent être répété. <figure> <center> <img src="https://d33wubrfki0l68.cloudfront.net/86cc45e87bb755a3bcecce462a6524e68d13a466/90635/images/tidy1.png" height = "200"> <figcaption>Source: https://moderndive.com/</figcaption> </center> </figure> --- ## La manipulation de données Il est important d'apprendre et de mettre en pratique les outils essentiels pour **capturer**, **manipuler** et **partager** des données de manière efficace. - Pourquoi est-ce important? **Le 80%/20% du *data science*** > Most data scientists spend only 20 percent of their time on actual data analysis and 80 percent of their time finding, cleaning, and reorganizing huge amounts of data, which is an inefficient data strategy (https://bit.ly/2wjB8IB). <br> .content-box-blue[Autrement dit: la manipulation de données est une tâche répétitive et propice aux erreurs!] --- class: inverse, center, middle # Exemple de code répétitif ## Une étude de cas --- ## Répéter la même opération sur plusieurs colonnes On veut normaliser chaque colonne de `df` de manière à ce que les données varient entre 0 et 1. ```r set.seed(1234) # Générer des données, 4 colonnes avec 10 observations df <- data.frame( a = rnorm(10), b = rnorm(10), c = rnorm(10), d = rnorm(10) ) df ``` ``` ## a b c d ## 1 -1.2070657 -0.47719270 0.1340882 1.1022975 ## 2 0.2774292 -0.99838644 -0.4906859 -0.4755931 ## 3 1.0844412 -0.77625389 -0.4405479 -0.7094400 ## 4 -2.3456977 0.06445882 0.4595894 -0.5012581 ## 5 0.4291247 0.95949406 -0.6937202 -1.6290935 ## 6 0.5060559 -0.11028549 -1.4482049 -1.1676193 ## 7 -0.5747400 -0.51100951 0.5747557 -2.1800396 ## 8 -0.5466319 -0.91119542 -1.0236557 -1.3409932 ## 9 -0.5644520 -0.83717168 -0.0151383 -0.2942939 ## 10 -0.8900378 2.41583518 -0.9359486 -0.4658975 ``` --- ## Répéter la même opération sur plusieurs colonnes On veut normaliser chaque colonne de `df` de manière à ce que les données varient entre 0 et 1. $$ \frac{x - \text{min}(x)}{\text{max}(x) - \text{min}(x)} $$ On peut valider en générant un vecteur de 10 nombres et en appliquant l'opération mathématique: ```r # Générer un vecteur de 10 nombres set.seed(1234) x <- sample(10) x ``` ``` ## [1] 10 6 5 4 1 8 2 7 9 3 ``` ```r # Normaliser les données (x - min(x)) / (max(x) - min(x)) ``` ``` ## [1] 1.0000000 0.5555556 0.4444444 0.3333333 0.0000000 0.7777778 0.1111111 ## [8] 0.6666667 0.8888889 0.2222222 ``` --- ## Répéter la même opération sur plusieurs colonnes Lorsque vous avez commencé à programmer, il y a de fortes chances que vous ayez procédé de la manière suivante: **répèter la même opération sur toutes les colonnes de `df`**. ```r # Normaliser la colonne a df$a <- (df$a - min(df$a, na.rm = TRUE)) / (max(df$a, na.rm = TRUE) - min(df$a, na.rm = TRUE)) # Normaliser la colonne b df$b <- (df$b - min(df$b, na.rm = TRUE)) / (max(df$b, na.rm = TRUE) - min(df$b, na.rm = TRUE)) # Normaliser la colonne c df$c <- (df$c - min(df$c, na.rm = TRUE)) / (max(df$c, na.rm = TRUE) - min(df$b, na.rm = TRUE)) # Normaliser la colonne d df$d <- (df$d - min(df$d, na.rm = TRUE)) / (max(df$d, na.rm = TRUE) - min(df$d, na.rm = TRUE)) ``` --- ## Répéter la même opération sur plusieurs colonnes Chaque colonne (vecteur) est maintenant entre 0-1. ```r df ``` ``` ## a b c d ## 1 0.3319492 0.15265375 2.7529837 1.0000000 ## 2 0.7647291 0.00000000 1.6659582 0.5192783 ## 3 1.0000000 0.06506096 1.7531918 0.4480343 ## 4 0.0000000 0.31129943 3.3193134 0.5114592 ## 5 0.8089534 0.57344857 1.3127049 0.1678518 ## 6 0.8313814 0.26011813 0.0000000 0.3084450 ## 7 0.5162933 0.14274906 3.5196877 0.0000000 ## 8 0.5244878 0.02553760 0.7386602 0.2556247 ## 9 0.5192926 0.04721860 2.4933490 0.5745131 ## 10 0.4243735 1.00000000 0.8912592 0.5222322 ``` --- ## Répéter la même opération sur plusieurs colonnes Quel est le problème? ```r df$a <- (df$a - min(df$a, na.rm = TRUE)) / (max(df$a, na.rm = TRUE) - min(df$a, na.rm = TRUE)) df$b <- (df$b - min(df$b, na.rm = TRUE)) / (max(df$b, na.rm = TRUE) - min(df$b, na.rm = TRUE)) df$c <- (df$c - min(df$c, na.rm = TRUE)) / (max(df$c, na.rm = TRUE) - min(df$b, na.rm = TRUE)) df$d <- (df$d - min(df$d, na.rm = TRUE)) / (max(df$d, na.rm = TRUE) - min(df$d, na.rm = TRUE)) ``` -- Erreur de copié/collé!!! ```r df$a <- (df$a - min(df$a, na.rm = TRUE)) / (max(df$a, na.rm = TRUE) - min(df$a, na.rm = TRUE)) df$b <- (df$b - min(df$b, na.rm = TRUE)) / (max(df$b, na.rm = TRUE) - min(df$b, na.rm = TRUE)) *df$c <- (df$c - min(df$c, na.rm = TRUE)) / (max(df$c, na.rm = TRUE) - min(df$b, na.rm = TRUE)) df$d <- (df$d - min(df$d, na.rm = TRUE)) / (max(df$d, na.rm = TRUE) - min(df$d, na.rm = TRUE)) ``` --- ## Nombre raisonnable de répétitions - L'approche DRY: **D**on't **R**epeate **Y**ourself - Il est souvent dit que si vous avez 3 copies du même code, **il est temps d'écrire une fonction**. <br> <center> <a href="https://imgflip.com/i/2y8ds5"><img src="https://i.imgflip.com/2y8ds5.jpg" height = "300", title="made at imgflip.com"/></a> </center> --- ## Répéter la même opération sur plusieurs colonnes Une première amélioration: **utiliser une fonction**. -- Étudier la formule de base: ```r (`df$d` - min(`df$d`, na.rm = TRUE)) / (max(`df$d`, na.rm = TRUE) - min(`df$d`, na.rm = TRUE)) ``` -- Paramétrer le code -> remplacer les variables par des arguments: ```r (`x` - min(`x`, na.rm = TRUE)) / (max(`x`, na.rm = TRUE) - min(`x`, na.rm = TRUE)) ``` -- Écrire une fonction : ```r # Fonction qui prend un vecteur numerique "x" et qui scale les valeurs entre 0-1 scale_vector <- function(`x`) { res <- (`x` - min(`x`, na.rm = TRUE)) / (max(`x`, na.rm = TRUE) - min(`x`, na.rm = TRUE)) return(res) } ``` --- ## Tester notre fonction ```r # Vecteur numérique v <- c(0, 1, 45, 98, -5) v ``` ``` ## [1] 0 1 45 98 -5 ``` ```r scale_vector(v) ``` ``` ## [1] 0.04854369 0.05825243 0.48543689 1.00000000 0.00000000 ``` -- <center> <image src="https://media.giphy.com/media/8JW82ndaYfmNoYAekM/giphy.gif" height="300" frameBorder="0" class="giphy-embed" allowFullScreen></image> </center> --- ## Utiliser la fonction sur le *data frame* Maintenant que la fonction `scale_vector()` fonctionne, utilisons-là sur chacune des colonnes du *data frame*. ```r df$a <- scale_vector(df$a) df$b <- scale_vector(df$b) df$c <- scale_vector(df$b) df$d <- scale_vector(df$d) df ``` ``` ## a b c d ## 1 0.3319492 0.15265375 0.15265375 1.0000000 ## 2 0.7647291 0.00000000 0.00000000 0.5192783 ## 3 1.0000000 0.06506096 0.06506096 0.4480343 ## 4 0.0000000 0.31129943 0.31129943 0.5114592 ## 5 0.8089534 0.57344857 0.57344857 0.1678518 ## 6 0.8313814 0.26011813 0.26011813 0.3084450 ## 7 0.5162933 0.14274906 0.14274906 0.0000000 ## 8 0.5244878 0.02553760 0.02553760 0.2556247 ## 9 0.5192926 0.04721860 0.04721860 0.5745131 ## 10 0.4243735 1.00000000 1.00000000 0.5222322 ``` --- ## Déjà mieux! L'utilisation de la fonction `scale_vector()` rend le code beaucoup plus simple. ```r df$a <- scale_vector(df$a) df$b <- scale_vector(df$b) df$c <- scale_vector(df$b) df$d <- scale_vector(df$d) ``` VS ```r df$a <- (df$a - min(df$a, na.rm = TRUE)) / (max(df$a, na.rm = TRUE) - min(df$a, na.rm = TRUE)) df$b <- (df$b - min(df$b, na.rm = TRUE)) / (max(df$b, na.rm = TRUE) - min(df$b, na.rm = TRUE)) df$c <- (df$c - min(df$c, na.rm = TRUE)) / (max(df$c, na.rm = TRUE) - min(df$a, na.rm = TRUE)) df$d <- (df$d - min(df$d, na.rm = TRUE)) / (max(df$d, na.rm = TRUE) - min(df$d, na.rm = TRUE)) ``` --- ## Utiliser la fonction sur le *data frame* Fonctionne très bien, mais il y a 4 répétitions. ```r df$a <- scale_vector(df$a) df$b <- scale_vector(df$b) df$c <- scale_vector(df$b) df$d <- scale_vector(df$d) ``` -- Toujours la possibilité de se tromper! .pull-left[ ```r df$a <- scale_vector(df$a) df$b <- scale_vector(df$b) *df$c <- scale_vector(df$b) df$d <- scale_vector(df$d) ``` ] .pull-right[ <image src="https://media.giphy.com/media/l1KVaj5UcbHwrBMqI/giphy.gif" height="200"</image> ] --- ## Répéter la même opération sur plusieurs colonnes Une deuxième amélioration: pourquoi ne pas **utiliser une boucle** qui permettrait de répéter l'opération plusieurs fois? <center> <image src="https://media.giphy.com/media/7XuPYJXaF1CBAmbwQQ/giphy.gif" width="480" height="360" frameBorder="0" class="giphy-embed" allowFullScreen></image> </center> --- class: inverse ## Exercice .full-width[.content-box-gray[Écrivez une boucle qui permet de normaliser toutes les colonnes de `df`. Au lieu de `scale_vector()`, vous pouvez utiliser la fonction `scale(x, center = FALSE, scale = TRUE)` fournit avec R.]] ```r scale_vector <- function(x) { res <- (x - min(x, na.rm = TRUE)) / (max(x, na.rm = TRUE) - min(x, na.rm = TRUE)) return(res) } set.seed(1234) # Générer des données, 4 colonnes avec 10 observations df <- data.frame( a = rnorm(10), b = rnorm(10), c = rnorm(10), d = rnorm(10) ) ``` --- ## Problèmes potentiels des boucles -- Il faut bien gérer les index utilisées, ce qui est propice aux erreurs lorsque plusieurs boucles sont imbriquées. ```r for(i in seq_along(df1)) { for(j = seq_along(df2)) { * df2[j, i] <- df[i, j] } } ``` De plus, l'accent est mis sur *la mécanique de l'opération* plutôt que sur le résultat attendu. --- class: inverse, center, middle # Notes sur les boucles *They are not the evil you might think* --- ## Rapidité des boucles **Contrairement à l'idée reçue, les boucles en R ne sont pas nécessairement lentes.** La plupart du temps, la lenteur des boucles est liée au fait que la variable qui reçoit le résultat n'est pas initialisée. Dans l'exemple suivant, on veut créer une boucle qui créer un vecteur de 1000 éléments avec les valeurs de 1 à 1000. .pull-left[ **Boucle sans initialisation** ```r f_slow <- function(n) { * v <- NULL for (i in 1:n) v <- c(v, i) return(v) } ``` ] .pull-right[ **Boucle avec initialisation** ```r f_fast <- function(n) { * v <- vector(mode = "numeric", length = n) for (i in 1:n) v[i] <- i return(v) } ``` ] </br> </br> .content-box-blue[Il est important de **toujours** initialiser les vecteurs avant d'entrer dans une boucle.] --- ## Rapidité des boucles Les boucles ne sont pas foncièrement lentes! ```r # Calculer le temps d'exécution des deux fonctions/boucles n <- 1000 timing <- microbenchmark::microbenchmark(f_slow(n), f_fast(n)) autoplot(timing) ``` <img src="index_files/figure-html/unnamed-chunk-21-1.svg" style="display: block; margin: auto;" /> --- ## Rapidité des boucles Les boucles ne sont pas foncièrement lentes! Evidemment, on aurait du utiliser: ```r v <- 1:1000 # Méthode la plus rapide pour initialiser un vecteur ``` ```r # Calculer le temps d'exécution des deux fonctions/boucles n <- 1000 timing <- microbenchmark::microbenchmark(f_slow(n), f_fast(n), 1:n) autoplot(timing) ``` <img src="index_files/figure-html/unnamed-chunk-23-1.svg" style="display: block; margin: auto;" /> --- class: inverse, center, middle ## La programmation fonctionnelle #### Les boucles c'est bien, mais c'est encore mieux sans! <img src="https://adv-r.hadley.nz/cover.png" height="400"> Advanced R (Chapman & Hall/CRC The R Series) --- ## La programmation fonctionnelle > La programmation fonctionnelle est un paradigme de programmation de type déclaratif qui considère le calcul en tant qu'évaluation de fonctions mathématiques. (Wikipedia) - La principale caractéristique de la programmation fonctionnelle est **que des fonctions sont passées en paramètre** à d'autres fonctions. - Comparé à l'utilisation de l'approche *impérative*, l'accent est mis sur l'opération effectuée (*i.e. la fonction à appliquer*) et non sur la mécanique nécessaire pour naviguer au travers des éléments et sur comment enregistrer le résultat. - Il y a de fortes chances que vous ayez déjà utilisé la fonction `lapply()`. - La fonction `lapply()` est appelé **un fonctionnel**, car elle utilise une fonction comme paramètre. --- ## Avantages de la programmation fonctionnelle 1. Le code est plus compacte (plus facile à lire et comprendre). 2. Plus facile à maintenir: * Lorsque vous créez une fonction pour une tâche répétée, il est facile de modifier cette fonction. Chaque emplacement de votre code où la même tâche est effectuée est automatiquement mis à jour. 3. Modularité: si vous écrivez une fonction pour des tâches individuelles spécifiques, vous pouvez les utiliser plusieurs fois. Une fonction que vous écrivez pour un script peut même être réutilisée dans d'autres scripts! ```r scale_vector <- function(x) { res <- (x - min(x, na.rm = TRUE)) / (max(x, na.rm = TRUE) - min(x, na.rm = TRUE)) return(res) } ``` --- ## *purrr* - La librairie `purrr` offre plusieurs outils de programmation fonctionnelle. - `purrr` n'est pas installé par défaut. L'installation de `tidyverse` inclut la librairie `purrr`. ```r install.packages("tidyverse") library(tidyverse) ``` <center><img src="https://purrr.tidyverse.org/logo.png" width="200"/></center> --- class: inverse, center, middle # Tidyverse ## Une série d'outils pour la manipulation de données <center> <img src="https://tidyverse.tidyverse.org/logo.png" width="200"/> </center> <center> <img src="https://dplyr.tidyverse.org/logo.png" width="120"/> <img src="https://tidyr.tidyverse.org/logo.png" width="120"/> <img src="https://readr.tidyverse.org/logo.png" width="120"/> <img src="https://readxl.tidyverse.org/logo.png" width="120"/> <img src="https://ggplot2.tidyverse.org/logo.png" width="120"/><img src="https://stringr.tidyverse.org/logo.png" width="120"/><img src="https://lubridate.tidyverse.org/logo.png" width="120"/><img src="https://purrr.tidyverse.org/logo.png" width="120"/> </center> --- ## La fonction `map()` - La fonction `map()` de la librairie `purrr` est une version *pimpée* de `lapply()`. - Elle permet **d'appliquer** une fonction `f()` à une liste d'éléments. <br> <center> <img src="https://d33wubrfki0l68.cloudfront.net/f0494d020aa517ae7b1011cea4c4a9f21702df8b/2577b/diagrams/functionals/map.png" height="250"/> </center> <small>Figure: https://adv-r.hadley.nz/functionals.html#map</small> --- ## La fonction `map()` Dans ce premier exemple, on veux ajouter la valeur de 1 à tous les éléments d'un vecteur numérique. ```r # Créer une fonction "f" qui prend un nombre et lui ajoute la valeur de 1. f <- function(x) { x + 1 } # "Appliquer" la fonction "f" au vecteur 1, 2, 3 map(.x = c(1, 2, 3), .f = f) # 1 + 1, 2 + 1, 3 + 1 ``` ``` ## [[1]] ## [1] 2 ## ## [[2]] ## [1] 3 ## ## [[3]] ## [1] 4 ``` <br> .content-box-blue[Il est important de prendre note que la fonction `map()` retourne toujours une liste d'éléments.] --- ## La fonction `map()` On aurait pu obtenir le même résultat en utilisant la fonction `lapply()` qui est inclut avec R: ```r lapply(X = c(1, 2, 3), FUN = f) ``` ``` ## [[1]] ## [1] 2 ## ## [[2]] ## [1] 3 ## ## [[3]] ## [1] 4 ``` Notez que l’utilisation de `map()` ou de `lapply()` dans ce cas-ci n'est pas très utile. On pourrait évidement reproduire le résultat en utilisant les capacités de vectorisation de R: ```r x <- 1:3 x + 1 ``` ``` ## [1] 2 3 4 ``` --- ## La fonction `map()` Dans ce deuxième exemple, on veut connaître la classe de chaque colonne du *data frame* `mtcars`. ```r head(mtcars, 20) # Afficher les 20 premières lignes ``` ``` ## mpg cyl disp hp drat wt qsec vs am gear carb ## Mazda RX4 21.0 6 160.0 110 3.90 2.620 16.46 0 1 4 4 ## Mazda RX4 Wag 21.0 6 160.0 110 3.90 2.875 17.02 0 1 4 4 ## Datsun 710 22.8 4 108.0 93 3.85 2.320 18.61 1 1 4 1 ## Hornet 4 Drive 21.4 6 258.0 110 3.08 3.215 19.44 1 0 3 1 ## Hornet Sportabout 18.7 8 360.0 175 3.15 3.440 17.02 0 0 3 2 ## Valiant 18.1 6 225.0 105 2.76 3.460 20.22 1 0 3 1 ## Duster 360 14.3 8 360.0 245 3.21 3.570 15.84 0 0 3 4 ## Merc 240D 24.4 4 146.7 62 3.69 3.190 20.00 1 0 4 2 ## Merc 230 22.8 4 140.8 95 3.92 3.150 22.90 1 0 4 2 ## Merc 280 19.2 6 167.6 123 3.92 3.440 18.30 1 0 4 4 ## Merc 280C 17.8 6 167.6 123 3.92 3.440 18.90 1 0 4 4 ## Merc 450SE 16.4 8 275.8 180 3.07 4.070 17.40 0 0 3 3 ## Merc 450SL 17.3 8 275.8 180 3.07 3.730 17.60 0 0 3 3 ## Merc 450SLC 15.2 8 275.8 180 3.07 3.780 18.00 0 0 3 3 ## Cadillac Fleetwood 10.4 8 472.0 205 2.93 5.250 17.98 0 0 3 4 ## Lincoln Continental 10.4 8 460.0 215 3.00 5.424 17.82 0 0 3 4 ## Chrysler Imperial 14.7 8 440.0 230 3.23 5.345 17.42 0 0 3 4 ## Fiat 128 32.4 4 78.7 66 4.08 2.200 19.47 1 1 4 1 ## Honda Civic 30.4 4 75.7 52 4.93 1.615 18.52 1 1 4 2 ## Toyota Corolla 33.9 4 71.1 65 4.22 1.835 19.90 1 1 4 1 ``` --- ## La fonction `map()` Il est tentant de faire du copier/coller en utilisant la fonction `class()`: ```r class(mtcars$mpg) ``` ``` ## [1] "numeric" ``` ```r class(mtcars$cyl) ``` ``` ## [1] "numeric" ``` ```r class(mtcars$disp) ``` ``` ## [1] "numeric" ``` --- ## La fonction `map()` Ou bien de faire une boucle: ```r # Vecteur pour le résultat res <- vector(mode = "character", length = ncol(mtcars)) # Faire une boucle sur toutes les colonnes de mtcars for(i in seq_along(mtcars)) { res[i] <- class(mtcars[, i]) } res ``` ``` ## [1] "numeric" "numeric" "numeric" "numeric" "numeric" "numeric" "numeric" ## [8] "numeric" "numeric" "numeric" "numeric" ``` --- class: inverse, center, middle ## Exercice .full-width[.content-box-gray[Utilisez la fonction `map()` pour extraire la classe de chacune des colonnes de `mtcars`.]] --- ## Les fonctions anonymes Revenons à notre exemple qui ajoute la valeur 1 à un vecteur. ```r # Créer une fonction "f" qui prend un nombre et lui ajoute la valeur de 1. f <- function(x) { x + 1 } # "Appliquer" la fonction "f" au vecteur 1, 2, 3 map(.x = c(1, 2, 3), .f = f) ``` - La fonction `f()` est très simple, on pourrait éviter de créer explicitement une fonction. - Il est possible d'utiliser des **fonctions anonymes** quand ça ne vaut pas la peine de lui donner un nom. --- ## Les fonctions anonymes et formules L'exemple précédent peut se ré-écrire en utilisant une fonction anonyme. Avec cette approche, nous n'avons pas besoin de fournir un nom de fonction. ```r map(1:3, function(x) x + 1) ``` ``` ## [[1]] ## [1] 2 ## ## [[2]] ## [1] 3 ## ## [[3]] ## [1] 4 ``` --- ## Les fonctions anonymes et formules On peut également utiliser l'approche par formule conjointement avec `~`. Ici on fait référence au paramètre en utilisant la notation `~.x`. ```r map(1:3, ~.x + 1) # 1 + 1, 2 + 1, 3 + 1 ``` ``` ## [[1]] ## [1] 2 ## ## [[2]] ## [1] 3 ## ## [[3]] ## [1] 4 ``` ```r map(1:3, ~log(.x)) # log(1), log(2), log(3) ``` ``` ## [[1]] ## [1] 0 ## ## [[2]] ## [1] 0.6931472 ## ## [[3]] ## [1] 1.098612 ``` --- class: inverse ## Exercice .full-width[.content-box-gray[Calculez la moyenne de chacune des colonnes de ce jeu de données en utilisant une fonction anonyme.]] ```r df <- read_csv("data/clean/cars.csv") head(df, 3) ``` ``` ## # A tibble: 3 x 9 ## car mpg cylinders displacement horsepower weight acceleration model ## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> ## 1 Chev~ 18 8 307 130 3504 12 70 ## 2 Buic~ 15 8 350 165 3693 11.5 70 ## 3 Plym~ 18 8 318 150 3436 11 70 ## # ... with 1 more variable: origin <chr> ``` ```r # Sélectionner seulement les colonnes numériques df <- select_if(df, is.numeric) ``` <small>Données: https://perso.telecom-paristech.fr/eagan/class/igr204</small> --- ## Utilisation des paramètres Jusqu'à maintenant, nous avons invoqué l'appel de fonctions sans l'utilisation de paramètres. Il est possible de spécifier les valeurs de paramètres à fournir à la fonction en utilisant des **paramètres nommés** de la manière suivante: ```r map(list, function, param1 = xxx, param2 = yyy, param3 = zzz) ``` Exemple avec les données *cars*. On veut calculer la moyenne de chacune des colonnes numériques. ```r # library(tidyverse) df <- read_csv("data/clean/cars.csv") df <- select_if(df, is.numeric) ``` La fonction `mean()` a un paramètre `na.rm = FALSE` qui permet de spécifier si les `NA` doivent être incluent dans le calcul. --- ## Utilisation des paramètres .pull-left[ ```r map(df, mean) ``` ``` ## $mpg ## [1] NA ## ## $cylinders ## [1] 5.475369 ## ## $displacement ## [1] 194.7796 ## ## $horsepower ## [1] 103.5296 ## ## $weight ## [1] 2979.414 ## ## $acceleration ## [1] 15.5197 ## ## $model ## [1] 75.92118 ``` ] .pull-right[ ```r *map(df, mean, na.rm = TRUE) ``` ``` ## $mpg ## [1] 23.07111 ## ## $cylinders ## [1] 5.475369 ## ## $displacement ## [1] 194.7796 ## ## $horsepower ## [1] 103.5296 ## ## $weight ## [1] 2979.414 ## ## $acceleration ## [1] 15.5197 ## ## $model ## [1] 75.92118 ``` ] --- ## Les fonctions anonymes et formules: un résumé Il y a trois façons différentes d'utiliser des fonctions anonymes avec `purrr`: ```r y <- 1:3 map(y, function(x) log(x, base = 2)) # Avec fonction anonyme map(y, ~log(.x, base = 2)) # Avec formule map(y, log, base = 2) ``` --- ## Les variantes de `map()` Il existe plusieurs variantes de `map_*()` qui permettent **d'être explicite sur la classe retournée par la fonction**. <br><br> | Fonction | Description | Exemples | | ------------ | --------------------- | ------------ | | `map_dbl()` | Vecteur de doubles | 2.1, pi | | `map_int()` | Vecteur d'entiers | 1, 3 | | `map_char()` | Vecteur de caractères | "R à Québec" | | `map_lgl()` | Vecteur de booléens | TRUE, FALSE | | `map_df()` | Liste de *data frame* | | --- ## Les variantes de `map()` Par exemple, on sait que la fonction `mean()` retourne une valeur numérique de type **double**. Dans ce cas, on peut utiliser la fonction `map_dbl()` pour calculer la valeur moyenne de chacune des colonnes de `mtcars`. ```r # Appliquer la fonction "mean" à chacune des colonnes de mtcars map_dbl(mtcars, mean) ``` ``` ## mpg cyl disp hp drat wt ## 20.090625 6.187500 230.721875 146.687500 3.596563 3.217250 ## qsec vs am gear carb ## 17.848750 0.437500 0.406250 3.687500 2.812500 ``` Il est intéressant de pouvoir être explicite sur la classe des données attendues. Dans le prochain cas, une erreur est générée, car la fonction `mean()` retourne une valeur de type *double* alors que je spécifie que je devrais reçevoir un *integer*. ```r map_int(mtcars, mean) ``` ``` ## Error: Can't coerce element 1 from a double to a integer ``` --- ## Les variantes de `map()` On peut également avoir le résultat sous forme d'un *data frame*. ```r map_df(mtcars, mean) ``` ``` ## # A tibble: 1 x 11 ## mpg cyl disp hp drat wt qsec vs am gear carb ## <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> ## 1 20.1 6.19 231. 147. 3.60 3.22 17.8 0.438 0.406 3.69 2.81 ``` --- class: inverse ## Exercice (1/2) .full-width[.content-box-gray[Séparez le jeu de données `mtcars` en fonction du nombre de cylindres. Utilisez une variante de la fonction `map_*()` pour connaitre le nombre d'observation de chaque groupe.]] ```r res <- split(mtcars, mtcars$cyl) ``` ``` ## $`4` ## mpg cyl disp hp drat wt qsec vs am gear carb ## Datsun 710 22.8 4 108.0 93 3.85 2.32 18.61 1 1 4 1 ## Merc 240D 24.4 4 146.7 62 3.69 3.19 20.00 1 0 4 2 ## ## $`6` ## mpg cyl disp hp drat wt qsec vs am gear carb ## Mazda RX4 21 6 160 110 3.9 2.620 16.46 0 1 4 4 ## Mazda RX4 Wag 21 6 160 110 3.9 2.875 17.02 0 1 4 4 ## ## $`8` ## mpg cyl disp hp drat wt qsec vs am gear carb ## Hornet Sportabout 18.7 8 360 175 3.15 3.44 17.02 0 0 3 2 ## Duster 360 14.3 8 360 245 3.21 3.57 15.84 0 0 3 4 ``` --- class: inverse ## Exercice (2/2) .full-width[.content-box-gray[Séparez le jeu de données `mtcars` en fonction du nombre de cylindres. Pour chacun des groupes, faire un modèle linéaire `mpg` en fonction de `wt` (`lm(mpg ~ wt)`). Écrivez une fonction `fit()` pour faire le modèle linéaire.]] ```r res <- split(mtcars, mtcars$cyl) fit <- function(...) { ... } map(..., ...) ``` Interdiction de faire... ```r mod1 <- lm(mpg ~ wt, data = subset(mtcars, cyl == 4)) mod2 <- lm(mpg ~ wt, data = subset(mtcars, cyl == 6)) mod3 <- lm(mpg ~ wt, data = subset(mtcars, cyl == 8)) ``` --- ## Map sur plusieurs vecteurs/listes Jusqu'ici, nous avons *mappé* avec une seule entrée (un seul vecteur ou une seule liste). Si vous avez plusieurs entrées, vous devez **parcourir en parallèle**. Pour ce faire, nous utiliserons les fonctions `map2()` et `pmap()`. La fonction `map2()` permet de parcourir en parallèle deux **vecteurs/listes**. L'utilisation de base de `map2()` est similaire à `map()`. ```r map2(.x, .y, .f, ...) map2(INPUT_ONE, INPUT_TWO, FUNCTION_TO_APPLY, OPTIONAL_ARGUMENTS) ``` --- ## Map sur plusieurs vecteurs/listes Imaginons que nous voulions générer trois vecteurs de valeurs aléatoires à l'aide de la fonction `rnorm()`. Pour chaque génération, nous voulons spécifier une moyenne différente. .pull-left[ ```r # Vecteur de moyennes mu <- c(4, -1, 3) map(mu, rnorm, n = 4) ``` ``` ## [[1]] ## [1] 4.026914 3.241738 3.647475 2.415420 ## ## [[2]] ## [1] -1.5534406 -0.5081871 -2.8831839 -1.9671185 ## ## [[3]] ## [1] 1.945614 3.050837 2.370583 3.924317 ``` ] .pull-right[ <center> <img src="images/purrr_map.png" height="300"/> </center> ] --- ## Map sur plusieurs vecteurs/listes Disons maintenant que nous voulions en plus spécifier une valeur d'écart-type différente pour chaque génération. Il faut spécifier un deuxième vecteur contenant les écart-types. Il faut alors utiliser la fonction `map2()`. .pull-left[ ```r # Vecteur de moyennes mu <- c(4, -1, 3) # Vecteur d'écart-types sigma <- c(1, 4, 7) map2(.x = mu, .y = sigma, .f = rnorm, n = 4) ``` ``` ## [[1]] ## [1] 5.101436 5.133209 3.434006 4.175488 ## ## [[2]] ## [1] -1.6102518 -1.8041091 -4.4294302 0.7992169 ## ## [[3]] ## [1] 0.8043237 9.0238669 1.9688712 0.7424697 ``` ] .pull-right[ <center> <img src="images/purrr_map2.png" height="300"/> </center> ] --- ## Map sur plusieurs vecteurs/listes Comme avec la fonction `map()`, il y a plusieurs variantes de `map2_*()`. ```r map2(c(1, 2, 3), c(4, 5, 6), sum) # 1 + 4, 2 + 5, 3 + 6 ``` ``` ## [[1]] ## [1] 5 ## ## [[2]] ## [1] 7 ## ## [[3]] ## [1] 9 ``` ```r map2_dbl(c(1, 2, 3), c(4, 5, 6), sum) # Être explicite sur la classe des valeurs de retour ``` ``` ## [1] 5 7 9 ``` --- ## Map sur plusieurs vecteurs/listes Lorsqu'on utilise une fonction anonyme, on peut utiliser la notation `.x` et `.y` pour faire référence aux paramètres utilisés dans la fonction. Dans l'exemple suivant on veut concatener deux vecteurs de caractères. ```r map2_chr(.x = c("a", "b", "c"), .y = c("x", "y", "z"), .f = ~paste(.x, .y, sep = "---")) ``` ``` ## [1] "a---x" "b---y" "c---z" ``` Dans ce cas, il y a eux trois itérations: 1. `.x = "a"` et `.y = "x"` 2. `.x = "b"` et `.y = "y"` 3. `.x = "c"` et `.y = "z"` Ce qui est équivalent à: ```r paste("a", "x", sep = "---") paste("b", "y", sep = "---") paste("c", "z", sep = "---") ``` --- ## Map sur plusieurs vecteurs/listes Maintenant, comment faire pour *mapper* sur plus de deux vecteurs/listes? La fonction `pmap()` (*parallel map*) permet d'itérer simultanément sur plusieurs vecteurs/listes. ```r pmap(.l, .f, ...) map2(LIST_INPUT, FUNCTION_TO_APPLY, OPTIONAL_ARGUMENTS) ``` --- ## Map sur plusieurs vecteurs/listes Dans cet exemple, on veut générer trois vecteurs aléatoires avec des moyennes, des écart-types et ce de longueurs différentes. Pour ce faire, il faut créer **une liste nommée** pour les paramètres. ```r l <- list( * mean = c(1, -200, 3), # Vecteur avec les 3 moyennes * sd = c(1, 2, 3), # Vecteur avec les 3 écart-types * n = c(1, 3, 6) # Vecteur avec les 3 longueurs de vecteurs ) l ``` ``` ## $mean ## [1] 1 -200 3 ## ## $sd ## [1] 1 2 3 ## ## $n ## [1] 1 3 6 ``` --- ## Map sur plusieurs vecteurs/listes Maintenant on peut appeler la fonction `pmap()` avec la liste `l`. ```r l <- list( * mean = c(1, -200, 3), # Vecteur avec les 3 moyennes * sd = c(1, 2, 3), # Vecteur avec les 3 écart-types * n = c(1, 3, 6) # Vecteur avec les 3 longueurs de vecteurs ) ``` ```r pmap(.l = l, .f = rnorm) ``` ``` ## [[1]] ## [1] 0.7214487 ## ## [[2]] ## [1] -198.3900 -200.5131 -200.2121 ## ## [[3]] ## [1] 5.5597270 7.7831691 4.3649485 0.4928501 3.9441637 3.6428748 ``` --- ## Map sur plusieurs vecteurs/listes Mais pourquoi cela fonctionne t'il? Comment les paramètres sont placés aux bons endroits? ```r l <- list( * mean = c(1, -200, 3), # Vecteur avec les 3 moyennes * sd = c(1, 2, 3), # Vecteur avec les 3 écart-types * n = c(1, 3, 6) # Vecteur avec les 3 longueurs de vecteurs ) ``` Regardons la structure de la fonction `rnorm()`. ```r rnorm(n, mean = 0, sd = 1) ``` - Il faut comprendre que la fonction `rnorm()` s'attend à reçevoir dans l'ordre: `n`, `mean` et `sd`. - Cependant, l'ordre de la liste est plutôt: `mean`, `sd`, et `n`. - Puisque la liste `l` est nommée **avec les mêmes noms que les paramètres de `rnorm()`**, R va placer automatiquement les paramètres aux bons endroits. --- ## Toujours utiliser une liste nommée .pull-left[ ```r set.seed(1234) pmap( .l = list( * mean = c(1, 5, 3), * sd = c(1, 2, 3), * n = c(1, 3, 2) ), .f = rnorm ) ``` ``` ## [[1]] ## [1] -0.2070657 ## ## [[2]] ## [1] 5.5548585 7.1688824 0.3086046 ## ## [[3]] ## [1] 4.287374 4.518168 ``` Equivalent à: ```r rnorm(n = 1, mean = 1, sd = 1) rnorm(n = 3, mean = 5, sd = 2) rnorm(n = 2, mean = 3, sd = 3) ``` ] .pull-right[ ```r set.seed(1234) pmap( .l = list( * c(1, 5, 3), * c(1, 2, 3), * c(1, 3, 2) ), .f = rnorm ) ``` ``` ## [[1]] ## [1] -0.2070657 ## ## [[2]] ## [1] 2.832288 5.253324 -5.037093 3.287374 3.518168 ## ## [[3]] ## [1] 1.850520 1.906736 1.871096 ``` Equivalent à: ```r rnorm(n = 1, mean = 1, sd = 1) rnorm(n = 5, mean = 2, sd = 3) rnorm(n = 3, mean = 3, sd = 2) ``` ] --- ## Map sur plusieurs vecteurs/listes Avec les fonctions `map()` et `map2()` on pouvait utiliser l'approche par *formule* en faisant référence aux paramètres en utilisant `.x` et `.y`. Rappel: ```r # Avec 1 ou 2 entrées, on peut utiliser .x et .y map2_chr(c("a", "b", "c"), c("x", "y", "z"), ~paste(.x, .y, sep = "---")) ``` Comment pouvons nous faire lorsqu'il y a plus de deux paramètres? Il suffit d'utiliser la notation `..1`, `..2`, `...n`. ```r pmap_chr( list( c("a", "b", "c"), # ..1 c("x", "y", "z"), # ..2 c(1, 2, 3) # ..3 ), * ~ paste(..3, ..1, ..2, sep = "---") ) ``` ``` ## [1] "1---a---x" "2---b---y" "3---c---z" ``` --- class: inverse, center, middle ## Exercice pratique sur un cas réel --- ## Lire le contenu d'un répertoire Une tâche courante consiste à lire plusieurs fichiers (CSV) dans un répertoire et de les combiner dans un seul *data frame*. <center> <img src="images/purrr_map_read_csv.svg.png" height="350"/> </center> <small>Inspiré de: https://www.gerkelab.com/blog/2018/09/import-directory-csv-purrr-readr/ </small> --- ## Lire le contenu d'un répertoire En utilisant l'approche fonctionnelle, il est très facile de lire une liste de fichiers contenus dans un répertoire. </br> <center> <img src="images/purrr_map_read_csv2.svg.png" height="200"/> </center> --- ## Étude de cas: *Capital bikeshare* https://www.capitalbikeshare.com/ > Capital Bikeshare is metro DC's bikeshare service, with 4,300 bikes and 500+ stations across 6 jurisdictions: Washington, DC.; Arlington, VA; Alexandria, VA; Montgomery, MD; Prince George's County, MD; and Fairfax County, VA. Designed for quick trips with convenience in mind, it's a fun and affordable way to get around. Similaire au système Bixi à Montréal. <center> <img src="http://www.parcjeandrapeau.com/medias/images/header/bixi-parc-jean-drapeau-montreal.jpg" height="300"/> </center> --- ## Les données - Les statistiques sur l'utilisation des vélos pour l'année 2018 sont dans 12 fichiers différents (1 par mois). - Données: https://s3.amazonaws.com/capitalbikeshare-data/index.html - Plus de 3 500 000 lignes/observations (réduit de 90% pour les exercices suivants). - Exemple pour le mois de janvier 2018. ``` ## Observations: 16,859 ## Variables: 9 ## $ duration <dbl> 369, 293, 717, 407, 383, 327, 167, 214, 4... ## $ start_date <dttm> 2018-01-01 00:18:07, 2018-01-01 00:23:41... ## $ end_date <dttm> 2018-01-01 00:24:17, 2018-01-01 00:28:35... ## $ start_station_number <dbl> 31618, 31646, 31111, 31116, 31101, 31601,... ## $ start_station <chr> "4th & East Capitol St NE", "Maine Ave & ... ## $ end_station_number <dbl> 31619, 31108, 31285, 31203, 31114, 31630,... ## $ end_station <chr> "Lincoln Park / 13th & East Capitol St NE... ## $ bike_number <chr> "W21076", "W00800", "W22760", "W23304", "... ## $ member_type <chr> "Member", "Member", "Member", "Member", "... ``` --- <img src="index_files/figure-html/unnamed-chunk-74-1.png" style="display: block; margin: auto;" /> --- ## Les données <img src="index_files/figure-html/unnamed-chunk-75-1.svg" style="display: block; margin: auto;" /> --- ## Les données <img src="index_files/figure-html/unnamed-chunk-76-1.svg" style="display: block; margin: auto;" /> --- class: inverse ## Exercice .full-width[.content-box-gray[Utilisez la fonction `map_df()` pour lire et combiner tous les fichiers *captial bikeshare*.]] ```r # Utiliser la fonction "list.files" pour lister tous les fichiers d'un répertoire files <- list.files("data/clean/capitale_bikeshare/", full.names = TRUE) files ``` ``` ## [1] "data/clean/capitale_bikeshare/201801_capitalbikeshare_tripdata.csv" ## [2] "data/clean/capitale_bikeshare/201802-capitalbikeshare-tripdata.csv" ## [3] "data/clean/capitale_bikeshare/201803-capitalbikeshare-tripdata.csv" ## [4] "data/clean/capitale_bikeshare/201804-capitalbikeshare-tripdata.csv" ## [5] "data/clean/capitale_bikeshare/201805-capitalbikeshare-tripdata.csv" ## [6] "data/clean/capitale_bikeshare/201806-capitalbikeshare-tripdata.csv" ## [7] "data/clean/capitale_bikeshare/201807-capitalbikeshare-tripdata.csv" ## [8] "data/clean/capitale_bikeshare/201808-capitalbikeshare-tripdata.csv" ## [9] "data/clean/capitale_bikeshare/201809-capitalbikeshare-tripdata.csv" ## [10] "data/clean/capitale_bikeshare/201810-capitalbikeshare-tripdata.csv" ## [11] "data/clean/capitale_bikeshare/201811-capitalbikeshare-tripdata.csv" ## [12] "data/clean/capitale_bikeshare/201812-capitalbikeshare-tripdata.csv" ``` ```r # Completez avec votre code pour lire et combiner tous les fichiers ``` --- class: inverse ## Exercice .full-width[.content-box-gray[Créez une fonction qui **(1)** ouvre chaque fichier et **(2)** qui calcule la moyenne de la variable/colonne `duration`.]] ```r # Utiliser la fonction "list.files" pour lister tous les fichiers d'un répertoire files <- list.files("data/clean/capitale_bikeshare/", full.names = TRUE) # Completez avec votre code ``` --- ## Note sur la lecture de fichiers Je vous conseil fortement d'utiliser la fonction `read_csv()` de la librairie `readr` (inclut avec `tidyverse`) pour lire vos fichiers, car beaucoup plus rapide que `read.csv()`. ```r file <- "data/clean/capitale_bikeshare/201801_capitalbikeshare_tripdata.csv" res <- microbenchmark::microbenchmark( read.csv = read.csv(file), read_csv =read_csv(file, col_types = cols()), fread = data.table::fread(file), times = 50 ) autoplot(res) ``` <img src="index_files/figure-html/unnamed-chunk-79-1.svg" style="display: block; margin: auto;" /> --- class: inverse, center, middle # Peut-on aller plus loin? ## Programmation parallèle avec `furrr` --- ## Librairie `furrr` ```r # Installer la librairie install.packages("furrr") ``` - Le but de `furrr` est de simplifier la combinaison de la famille de fonctions de mappage de `purrr` et des capacités de traitement parallèle. - Les fonctions de `furrr` débutent toutes par `future_*()` et peuvent remplacer pratiquement les utilisations de `map_*()`. ``` ## [1] "future_imap" "future_imap_chr" ## [3] "future_imap_dbl" "future_imap_dfc" ## [5] "future_imap_dfr" "future_imap_int" ## [7] "future_imap_lgl" "future_invoke_map" ## [9] "future_invoke_map_chr" "future_invoke_map_dbl" ## [11] "future_invoke_map_dfc" "future_invoke_map_dfr" ## [13] "future_invoke_map_int" "future_invoke_map_lgl" ## [15] "future_map" "future_map_at" ## [17] "future_map_chr" "future_map_dbl" ## [19] "future_map_dfc" "future_map_dfr" ``` --- ## Parallélisation ```r library(furrr) *availableCores() ``` ``` ## system ## 8 ``` ```r *plan(multicore(workers = availableCores() - 1)) # Utilise 7 coeurs files <- list.files("data/clean/capitale_bikeshare/", full.names = TRUE) df <- future_map_dfr(files, read_csv) glimpse(df) ``` ``` ## Observations: 354,270 ## Variables: 9 ## $ duration <dbl> 776, 477, 152, 281, 251, 746, 1005, 287, 98… ## $ start_date <dttm> 2018-01-19 08:26:27, 2018-01-18 20:24:13, … ## $ end_date <dttm> 2018-01-19 08:39:23, 2018-01-18 20:32:11, … ## $ start_station_number <dbl> 31031, 31604, 31267, 31286, 31203, 31213, 3… ## $ start_station <chr> "15th & N Scott St", "3rd & H St NW", "17th… ## $ end_station_number <dbl> 31225, 31109, 31213, 31519, 31254, 31110, 3… ## $ end_station <chr> "C & O Canal & Wisconsin Ave NW", "7th & T … ## $ bike_number <chr> "W01382", "W21574", "W00341", "W00273", "W2… ## $ member_type <chr> "Member", "Member", "Member", "Member", "Me… ``` --- class: inverse, center, middle # Programmation fonctionnelle sur les *data frame* --- ## Programmation fonctionnelle sur les *data frame* - Jusqu'à maintenant, à l'exception de `map_df()`, nous avons utilisé les fonctions `map_*()` qui retourne des vecteurs. - Il existe également plusieurs fonctions pour utiliser l'approche fonctionnelle sur des *data frame*. - La plupart des fonctions que nous verrons aujourd'hui proviennent de la librairie `dplyr`. <center> <img src="https://dplyr.tidyverse.org/logo.png" width="120"/> </center> - `dplyr` est inclut dans la librairie `tidyverse`. ```r library(tidyverse) ``` --- ## Retour sur l'exercice précédent Dans un exercice précédent, nous devions calculer la moyenne de la colonne `duration` pour chaque mois de 2018. Disons maintenant que nous voulons calculer la moyenne de toutes les colonnes numériques. ```r files <- list.files("data/clean/capitale_bikeshare/", full.names = TRUE) calculate_mean <- function(file) { df <- read_csv(file) # Ouvrir le fichier l <- list( mean_duration = mean(df$duration), mean_start_station_number = mean(df$start_station_number), mean_end_station_number = mean(df$end_station_number) ) return(l) } ``` --- ## Retour sur l'exercice précédent ```r res <- map(files, calculate_mean) head(res, 1) ``` ``` ## [[1]] ## [[1]]$mean_duration ## [1] 801.5229 ## ## [[1]]$mean_start_station_number ## [1] 31325.86 ## ## [[1]]$mean_end_station_number ## [1] 31328.16 ``` **Cette approche fonctionne bien, mais:** 1. Il est impossible de savoir à quel mois correspond chacune des valeurs du vecteur. 2. Le code devra être modifié si une nouvelle colonne numérique est ajoutée dans les fichiers csv. --- ## La fonction `summarise()` - La fonction `summarise()` permet de *compresser* un *data frame* en lui **appliquant une fonction** qui réduit le nombre d'observations. - Par exemple, on peut calculer la valeur moyenne de `duration` pour les données de janvier 2018. ```r df <- read_csv("data/clean/capitale_bikeshare/201801_capitalbikeshare_tripdata.csv") summarise(df, mean(duration)) ``` ``` ## # A tibble: 1 x 1 ## `mean(duration)` ## <dbl> ## 1 802. ``` Il est mieux de spéficier le nom de la nouvelle colonne à créer: ```r summarise(df, mean_duration = mean(duration)) ``` ``` ## # A tibble: 1 x 1 ## mean_duration ## <dbl> ## 1 802. ``` --- ## La fonction `summarise_if()` - La fonction `summarise_if()` est une variante de `summarise()` qui permet d'appliquer une fonction à un ensemble de colonnes qui répondent à une ou des condition(s). - La fonction `summarise_if()` s'utilise comme suit: ```r summarise_if(.tbl, .predicate, .funs, ...) ``` Où: - `.tbl` est un *data frame*. - `.predicate` une fonction qui retourne `TRUE/FALSE`. - `.funs` une liste de fonctions à appliquer. --- ## La fonction `summarise_if()` On peux maintenant utilise la fonction `summarise_if()` sur nos données. ```r df <- read_csv("data/clean/capitale_bikeshare/201801_capitalbikeshare_tripdata.csv") # On applique la fonction "mean" à toutes les colonnes de type "numeric" du data frame "df" summarise_if(.tbl = df, .predicate = is.numeric, .funs = mean) ``` ``` ## # A tibble: 1 x 3 ## duration start_station_number end_station_number ## <dbl> <dbl> <dbl> ## 1 802. 31326. 31328. ``` -- Ne célébrons pas trop vite! N'oubliez pas qu'on voulait faire ce calcule pour chacun des mois de l'année 2018. <center> <image src="https://media.giphy.com/media/OWJILXs8QpyiA/giphy.gif" height="200" frameBorder="0" class="giphy-embed" allowFullScreen></image> </center> --- ## La fonction `group_by()` - La fonction `group_by()` peut s'utiliser conjointement avec la fonction `summarise()` pour appliquer un calcule à un ensemble de groupes dans un *data frame*. - - La fonction `group_by()` **prépare** un *data frame* a être utilisé par la fonction `summarise()`. - La fonction `group_by()` s'utilise comme suit: ```r group_by(.data, ...) ``` Où: - `.data` est un *data frame*. - `...` est une liste de colonne à utiliser pour effectuer le regroupement. --- ## La fonction `group_by()` Par exemple, grouper `mtcars` sur la base de la colonne `cyl` (nombre de cylindres). Le résultat renvoyé nous indique qu'il y a trois groupes de `cyl` dans le *data frame*. ```r group_by(mtcars, cyl) ``` ``` ## # A tibble: 32 x 11 ## # Groups: cyl [3] ## mpg cyl disp hp drat wt qsec vs am gear carb ## * <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> ## 1 21 6 160 110 3.9 2.62 16.5 0 1 4 4 ## 2 21 6 160 110 3.9 2.88 17.0 0 1 4 4 ## 3 22.8 4 108 93 3.85 2.32 18.6 1 1 4 1 ## 4 21.4 6 258 110 3.08 3.22 19.4 1 0 3 1 ## 5 18.7 8 360 175 3.15 3.44 17.0 0 0 3 2 ## 6 18.1 6 225 105 2.76 3.46 20.2 1 0 3 1 ## 7 14.3 8 360 245 3.21 3.57 15.8 0 0 3 4 ## 8 24.4 4 147. 62 3.69 3.19 20 1 0 4 2 ## 9 22.8 4 141. 95 3.92 3.15 22.9 1 0 4 2 ## 10 19.2 6 168. 123 3.92 3.44 18.3 1 0 4 4 ## # ... with 22 more rows ``` --- ## Calculer les moyennes mensuelles Calculer **les moyennes mensuelles** de toutes les colonnes numériques, en utilisant toutes les données de *bikeshare*. On peut diviser ce problème en trois étapes: 1. Lire tous les fichiers. 2. Grouper les données par mois. 3. Calculer la moyenne de chacune des colonnes de type numérique. --- ## Calculer les moyennes mensuelles ```r # Étape 1: Lire tous les fichiers files <- list.files("data/clean/capitale_bikeshare/", full.names = TRUE) df <- map_df(.x = files, .f = read_csv) # Ajouter la colonne mois dans le data frame df$month <- format(x = df$start_date, "%b") # Étape 2: Grouper les données par mois. df <- group_by(.data = df, month) # Étape 3: Calculer la moyenne de chacune des colonnes de type numérique. df <- summarise_if(.tbl = df, .predicate = is.numeric, .funs = mean) head(df) # Afficher les 6 premières lignes ``` ``` ## # A tibble: 6 x 4 ## month duration start_station_number end_station_number ## <chr> <dbl> <dbl> <dbl> ## 1 Apr 1234. 31319. 31322. ## 2 Aug 1225. 31324. 31325. ## 3 Dec 883. 31334. 31332. ## 4 Feb 865. 31322. 31322. ## 5 Jan 802. 31326. 31328. ## 6 Jul 1352. 31323. 31321. ``` --- class: inverse, center, middle ## L'opérateur pipe ` %>% ` <center><img src="https://magrittr.tidyverse.org/logo.png" width="200"/></center> --- ## L'opérateur pipe ` %>% ` - L'opérateur pipe, `%>%`, permet d'enchaîner les opérations en passant le résultat d'un fonction comme premier argument dans la fonction suivante. Ce qui est très utile lorsqu'on a pas besoin des résultats intermédiaires. - Lorsque vous voyez `%>%`, vous devriez vous dire **ensuite**. - Implémenté dans la librairie `magrittr` (https://cran.r-project.org/web/packages/magrittr/vignettes/magrittr.html) et inclut avec `tidyverse`. <center><img src="https://magrittr.tidyverse.org/logo.png" width="200"/></center> --- ## L'opérateur pipe ` %>% ` Les deux commandes suivantes sont équivalentes: ```r 3 %>% log() ``` ``` ## [1] 1.098612 ``` ```r log(3) ``` ``` ## [1] 1.098612 ``` Par défaut, ` %>% ` passe le paramètre à la première position de la fonction suivante. On peut cependant contrôler ce comportement en utilisant le point `.` pour explicitement spécifier où doit aller le paramètre. Ces deux lignes sont équivalentes: ```r 2 %>% rnorm(n = 10, mean = .) rnorm(n = 10, mean = 2) ``` --- ## La version compacte .pull-left[ ```r files <- list.files( "data/clean/capitale_bikeshare/", full.names = TRUE ) files %>% map_df(read_csv) %>% group_by( month = format(start_date, "%b") ) %>% summarise_if(is.numeric, mean) ``` ``` ## # A tibble: 12 x 4 ## month duration start_station_number end_station_number ## <chr> <dbl> <dbl> <dbl> ## 1 Apr 1234. 31319. 31322. ## 2 Aug 1225. 31324. 31325. ## 3 Dec 883. 31334. 31332. ## 4 Feb 865. 31322. 31322. ## 5 Jan 802. 31326. 31328. ## 6 Jul 1352. 31323. 31321. ## 7 Jun 1235. 31325. 31327. ## 8 Mar 1092. 31321. 31322. ## 9 May 1276. 31321. 31322. ## 10 Nov 929. 31334. 31334. ## 11 Oct 1051. 31332. 31334. ## 12 Sep 1134. 31328. 31330. ``` ] -- .pull-right[ <center> <image src="https://media.giphy.com/media/iI6eeGjwScTCM/giphy.gif" height="300" frameBorder="0" class="giphy-embed" allowFullScreen></image> </center> ] --- ## Appliquer plusieurs fonctions Il est possible d'appliquer plus d'une fonction en utilisant le paramètre `.funs` avec une liste nommée de fonctions à utiliser. ```r files %>% map_df(read_csv) %>% group_by( month = lubridate::month(start_date, label = TRUE) ) %>% * summarise_if(is.numeric, .funs = list(mymean = mean, mysd = sd)) %>% glimpse() # La fonction glimpse() est une version pimpée de str() ``` ``` ## Observations: 12 ## Variables: 7 ## $ month <ord> Jan, Feb, Mar, Apr, May, Jun, Jul,... ## $ duration_mymean <dbl> 801.5229, 865.4528, 1092.4281, 123... ## $ start_station_number_mymean <dbl> 31325.86, 31322.42, 31320.56, 3131... ## $ end_station_number_mymean <dbl> 31328.16, 31322.27, 31321.56, 3132... ## $ duration_mysd <dbl> 1456.249, 1725.301, 2061.086, 2069... ## $ start_station_number_mysd <dbl> 227.3051, 223.2809, 217.6609, 216.... ## $ end_station_number_mysd <dbl> 225.8574, 220.9612, 213.6642, 215.... ``` --- class: inverse ## À vous de jouer! .full-width[.content-box-gray[Utiliser les concepts vu aujourd'hui pour reproduire le graphique suivant. Il s'agit de calculer la moyenne de la variable `duration` par heure et ce pour chaque mois de l'année.]] <img src="index_files/figure-html/unnamed-chunk-97-1.svg" style="display: block; margin: auto;" /> --- ## La fonction `nest()` La fonction `nest()` permet de créer une liste de *data frame* à l'intérieur d'un *data frame*. Cette fonction est généralement utilisée conjointement avec `group_by()`. ```r head(mtcars) ``` ``` ## mpg cyl disp hp drat wt qsec vs am gear carb ## Mazda RX4 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4 ## Mazda RX4 Wag 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4 ## Datsun 710 22.8 4 108 93 3.85 2.320 18.61 1 1 4 1 ## Hornet 4 Drive 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1 ## Hornet Sportabout 18.7 8 360 175 3.15 3.440 17.02 0 0 3 2 ## Valiant 18.1 6 225 105 2.76 3.460 20.22 1 0 3 1 ``` ```r mtcars %>% group_by(cyl) %>% # Grouper selon la variable "cyl" nest() # "Compresser" le data frame, par défault, la nouvelle colonne se nomme "data" ``` ``` ## # A tibble: 3 x 2 ## cyl data ## <dbl> <list> ## 1 6 <tibble [7 x 10]> ## 2 4 <tibble [11 x 10]> ## 3 8 <tibble [14 x 10]> ``` --- ## La fonction `unnest()` La fonction `unnest()` de décompresser un *data frame* qui a été compressé avec `nest()`. ```r mtcars %>% group_by(cyl) %>% # Grouper selon la variable "cyl" nest() %>% # "Compresser" le data frame, par défault, la nouvelle colonne se nomme "data" unnest() # "Décompresser" le data frame ``` ``` ## # A tibble: 32 x 11 ## cyl mpg disp hp drat wt qsec vs am gear carb ## <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> ## 1 6 21 160 110 3.9 2.62 16.5 0 1 4 4 ## 2 6 21 160 110 3.9 2.88 17.0 0 1 4 4 ## 3 6 21.4 258 110 3.08 3.22 19.4 1 0 3 1 ## 4 6 18.1 225 105 2.76 3.46 20.2 1 0 3 1 ## 5 6 19.2 168. 123 3.92 3.44 18.3 1 0 4 4 ## 6 6 17.8 168. 123 3.92 3.44 18.9 1 0 4 4 ## 7 6 19.7 145 175 3.62 2.77 15.5 0 1 5 6 ## 8 4 22.8 108 93 3.85 2.32 18.6 1 1 4 1 ## 9 4 24.4 147. 62 3.69 3.19 20 1 0 4 2 ## 10 4 22.8 141. 95 3.92 3.15 22.9 1 0 4 2 ## # ... with 22 more rows ``` --- ## La fonction `nest()` En combinant les fonctions `nest()` et `map()`, il est possible de rapidement créer un modèle linéaire pour chacun des groupes. ```r mtcars %>% group_by(cyl) %>% nest() %>% * mutate(mod = map(data, ~lm(mpg ~ wt, data = .))) ``` ``` ## # A tibble: 3 x 3 ## cyl data mod ## <dbl> <list> <list> ## 1 6 <tibble [7 x 10]> <lm> ## 2 4 <tibble [11 x 10]> <lm> ## 3 8 <tibble [14 x 10]> <lm> ``` La *data frame* contient maintenant: - La ou les variables de regroupement. - Les données compressées (colonne `data`). - Les modèles linéaires (colonne `mod`). **Les risques d'erreurs sont de beaucoup diminués, car tout est encapsulé dans un *data frame*.** --- ## La librairie `broom` La librairie `broom` permet de convertir des objets statistiques en *data frame*. ```r mtcars %>% group_by(cyl) %>% nest() %>% mutate(mod = map(data, ~lm(mpg ~ wt, data = .))) %>% * mutate(coef = map(mod, broom::tidy)) %>% unnest(coef) ``` ``` ## # A tibble: 6 x 6 ## cyl term estimate std.error statistic p.value ## <dbl> <chr> <dbl> <dbl> <dbl> <dbl> ## 1 6 (Intercept) 28.4 4.18 6.79 0.00105 ## 2 6 wt -2.78 1.33 -2.08 0.0918 ## 3 4 (Intercept) 39.6 4.35 9.10 0.00000777 ## 4 4 wt -5.65 1.85 -3.05 0.0137 ## 5 8 (Intercept) 23.9 3.01 7.94 0.00000405 ## 6 8 wt -2.19 0.739 -2.97 0.0118 ``` --- class: inverse ## À vous de jouer! .full-width[.content-box-gray[En utilisant le jeu de données `cars`, créer un modèle linéaire multiple (`mpg ~ horsepower + weight`) pour les `model` de chaque année. Par la suite, ajouter une colonne dans le *data frame* qui contient le R2 de la régression.]] ```r cars <- read_csv("data/clean/cars.csv") cars ``` ``` ## # A tibble: 406 x 9 ## car mpg cylinders displacement horsepower weight acceleration model ## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> ## 1 Chev~ 18 8 307 130 3504 12 70 ## 2 Buic~ 15 8 350 165 3693 11.5 70 ## 3 Plym~ 18 8 318 150 3436 11 70 ## 4 AMC ~ 16 8 304 150 3433 12 70 ## 5 Ford~ 17 8 302 140 3449 10.5 70 ## 6 Ford~ 15 8 429 198 4341 10 70 ## 7 Chev~ 14 8 454 220 4354 9 70 ## 8 Plym~ 14 8 440 215 4312 8.5 70 ## 9 Pont~ 14 8 455 225 4425 10 70 ## 10 AMC ~ NA 8 390 190 3850 8.5 70 ## # ... with 396 more rows, and 1 more variable: origin <chr> ``` --- ## Les données <img src="index_files/figure-html/unnamed-chunk-103-1.svg" style="display: block; margin: auto;" /> --- ## Conclusions - R est un langage de programmation fonctionnelle qui permet de faire beaucoup de chose rapidement et de manière élégante. - La programmation fonctionnelle permet de diminuer les chances d'erreurs dans le code. - Si erreur, plus facilement corrigeable, car on modifie à un seul endroit. - La librairie `purrr` offre un ensemble d'outils qui permet d'appliquer efficacement plusieurs concepts de la programmation fonctionnelle. - L'utilisation de `nest()` permet d'encapsuler les résultats dans un *data frame* ce qui permet de les faire suivre tout au long du processus d'analyse. --- ## Merci! Merci au comité organisateur de R à Québec 2019! <center> <image src="images/comite.bmp" height="400", align="middle"</image><image src="https://upload.wikimedia.org/wikipedia/commons/thumb/1/1b/R_logo.svg/724px-R_logo.svg.png" height="100", align="middle"</image><image src="images/logo_r_a_quebec_2019.png" height="100", align="middle"</image> </center> --- ## Références Beaucoup de matériel présenté dans cette classe provient de ressources en ligne. Merci à tout ceux et celles à qui j'ai piqué du code!!! - https://bit.ly/2wjB8IB - https://adv-r.hadley.nz/functionals.html#map - https://www.r-exercises.com/2018/01/12/functional-programming-with-purrr-exercises-part-1/ - https://jennybc.github.io/purrr-tutorial/ls03_map-function-syntax.html - https://sebastiansauer.github.io/multiple-lm-purrr2/ - https://nicercode.github.io/intro/repeating-things.html - https://www.earthdatascience.org/courses/earth-analytics/automate-science-workflows/write-efficient-code-for-science-r/