Introdução

purrr tidyverse rstats


Parte desse texto é uma tradução desse site: http://joshuamccrain.com/tutorials/purrr/purrr_introduction.html

Este tutorial fornece uma breve introdução ao pacote purrr, focando nas funções mais úteis e como elas se combinam com o dplyr para facilitar a nossa vida.

O pacote purrr é incrivelmente versátil e pode se tornar muito complexo dependendo da sua aplicação. Aqui, meu objetivo é construir a intuição em torno da família de funções chamadas map, mostrando aplicações.

Se você estiver familiarizado com a lógica por trás da família de pacotes apply do R-base, essa função deve ser familiar.

Introdução ao purrr::map

Vamos ver com exemplos como algumas das principais funções do purrr funcionam. A função mais usada é map:

x <- seq(5, 10)

map(x, ~ .* 5)
[[1]]
[1] 25

[[2]]
[1] 30

[[3]]
[1] 35

[[4]]
[1] 40

[[5]]
[1] 45

[[6]]
[1] 50



A função map precisa de dois argumentos: (1) um vetor (ou uma lista) e (2) uma função (ou uma fórmula).

Neste caso básico estou passando um vetor x, que é simplesmente uma sequência de números. Depois estou realizando uma operação básica para cada número nessa sequência, multiplicando-o por 5.

A função map retorna uma lista e é isso que obtemos. Também podemos usar variações de map_ que permitem retornar outros tipos de saídas de dados. Falarei mais sobre isso mais tarde, mas este é um exemplo básico usando map_dbl:

x <- seq(5, 10)

map_dbl(x, ~ .*5)
[1] 25 30 35 40 45 50

Outras variantes incluem map_lgl, map_int e map_chr. Observe que você também pode inserir seus dados via pipe %>%:

x <- seq(5, 10)

x %>% map_dbl(~ .*5)
[1] 25 30 35 40 45 50

Uma função adicional é map2. Ele é útil quando você tem dois vetores ou listas que deseja combinar em uma única fórmula. Uma observação importante aqui é que ambos devem ter o mesmo comprimento. Se não, as coisas ficam complicadas:

x <- seq(2000, 2010)
y <- seq(10, 20)

map2_dbl(x, y, ~ .x + .y)
 [1] 2010 2012 2014 2016 2018 2020 2022 2024 2026 2028 2030

Observe que as funções map funcionam tão bem com outros tipos de dados, como strings ou caracteres:

movies <- c("A New Hope",
            "The Empire Strikes Back",
            "Return of the Jedi",
            "Phantom Menace",
            "Attack of the Clones",
            "Revenge of the Sith",
            "The Force Awakens",
            "The Last Jedi",
            "Rise of Skywalker")

years <- c(1977, 1980, 1983, 1999, 2002, 2005, 2015, 2017, 2019)

map2_chr(movies, years, ~paste(.x, .y, sep=": "))
[1] "A New Hope: 1977"              "The Empire Strikes Back: 1980"
[3] "Return of the Jedi: 1983"      "Phantom Menace: 1999"         
[5] "Attack of the Clones: 2002"    "Revenge of the Sith: 2005"    
[7] "The Force Awakens: 2015"       "The Last Jedi: 2017"          
[9] "Rise of Skywalker: 2019"      

map_if: um exemplo aplicado

data("iris")
map_if(iris, is.numeric, shapiro.test)
$Sepal.Length

    Shapiro-Wilk normality test

data:  .x[[i]]
W = 0.97609, p-value = 0.01018


$Sepal.Width

    Shapiro-Wilk normality test

data:  .x[[i]]
W = 0.98492, p-value = 0.1012


$Petal.Length

    Shapiro-Wilk normality test

data:  .x[[i]]
W = 0.87627, p-value = 0.0000000007412


$Petal.Width

    Shapiro-Wilk normality test

data:  .x[[i]]
W = 0.90183, p-value = 0.0000000168


$Species
  [1] setosa     setosa     setosa     setosa     setosa     setosa    
  [7] setosa     setosa     setosa     setosa     setosa     setosa    
 [13] setosa     setosa     setosa     setosa     setosa     setosa    
 [19] setosa     setosa     setosa     setosa     setosa     setosa    
 [25] setosa     setosa     setosa     setosa     setosa     setosa    
 [31] setosa     setosa     setosa     setosa     setosa     setosa    
 [37] setosa     setosa     setosa     setosa     setosa     setosa    
 [43] setosa     setosa     setosa     setosa     setosa     setosa    
 [49] setosa     setosa     versicolor versicolor versicolor versicolor
 [55] versicolor versicolor versicolor versicolor versicolor versicolor
 [61] versicolor versicolor versicolor versicolor versicolor versicolor
 [67] versicolor versicolor versicolor versicolor versicolor versicolor
 [73] versicolor versicolor versicolor versicolor versicolor versicolor
 [79] versicolor versicolor versicolor versicolor versicolor versicolor
 [85] versicolor versicolor versicolor versicolor versicolor versicolor
 [91] versicolor versicolor versicolor versicolor versicolor versicolor
 [97] versicolor versicolor versicolor versicolor virginica  virginica 
[103] virginica  virginica  virginica  virginica  virginica  virginica 
[109] virginica  virginica  virginica  virginica  virginica  virginica 
[115] virginica  virginica  virginica  virginica  virginica  virginica 
[121] virginica  virginica  virginica  virginica  virginica  virginica 
[127] virginica  virginica  virginica  virginica  virginica  virginica 
[133] virginica  virginica  virginica  virginica  virginica  virginica 
[139] virginica  virginica  virginica  virginica  virginica  virginica 
[145] virginica  virginica  virginica  virginica  virginica  virginica 
Levels: setosa versicolor virginica

map_at: um exemplo aplicado

iris %>% map_at(c(4, 5), is.numeric)
$Sepal.Length
  [1] 5.1 4.9 4.7 4.6 5.0 5.4 4.6 5.0 4.4 4.9 5.4 4.8 4.8 4.3 5.8 5.7 5.4 5.1
 [19] 5.7 5.1 5.4 5.1 4.6 5.1 4.8 5.0 5.0 5.2 5.2 4.7 4.8 5.4 5.2 5.5 4.9 5.0
 [37] 5.5 4.9 4.4 5.1 5.0 4.5 4.4 5.0 5.1 4.8 5.1 4.6 5.3 5.0 7.0 6.4 6.9 5.5
 [55] 6.5 5.7 6.3 4.9 6.6 5.2 5.0 5.9 6.0 6.1 5.6 6.7 5.6 5.8 6.2 5.6 5.9 6.1
 [73] 6.3 6.1 6.4 6.6 6.8 6.7 6.0 5.7 5.5 5.5 5.8 6.0 5.4 6.0 6.7 6.3 5.6 5.5
 [91] 5.5 6.1 5.8 5.0 5.6 5.7 5.7 6.2 5.1 5.7 6.3 5.8 7.1 6.3 6.5 7.6 4.9 7.3
[109] 6.7 7.2 6.5 6.4 6.8 5.7 5.8 6.4 6.5 7.7 7.7 6.0 6.9 5.6 7.7 6.3 6.7 7.2
[127] 6.2 6.1 6.4 7.2 7.4 7.9 6.4 6.3 6.1 7.7 6.3 6.4 6.0 6.9 6.7 6.9 5.8 6.8
[145] 6.7 6.7 6.3 6.5 6.2 5.9

$Sepal.Width
  [1] 3.5 3.0 3.2 3.1 3.6 3.9 3.4 3.4 2.9 3.1 3.7 3.4 3.0 3.0 4.0 4.4 3.9 3.5
 [19] 3.8 3.8 3.4 3.7 3.6 3.3 3.4 3.0 3.4 3.5 3.4 3.2 3.1 3.4 4.1 4.2 3.1 3.2
 [37] 3.5 3.6 3.0 3.4 3.5 2.3 3.2 3.5 3.8 3.0 3.8 3.2 3.7 3.3 3.2 3.2 3.1 2.3
 [55] 2.8 2.8 3.3 2.4 2.9 2.7 2.0 3.0 2.2 2.9 2.9 3.1 3.0 2.7 2.2 2.5 3.2 2.8
 [73] 2.5 2.8 2.9 3.0 2.8 3.0 2.9 2.6 2.4 2.4 2.7 2.7 3.0 3.4 3.1 2.3 3.0 2.5
 [91] 2.6 3.0 2.6 2.3 2.7 3.0 2.9 2.9 2.5 2.8 3.3 2.7 3.0 2.9 3.0 3.0 2.5 2.9
[109] 2.5 3.6 3.2 2.7 3.0 2.5 2.8 3.2 3.0 3.8 2.6 2.2 3.2 2.8 2.8 2.7 3.3 3.2
[127] 2.8 3.0 2.8 3.0 2.8 3.8 2.8 2.8 2.6 3.0 3.4 3.1 3.0 3.1 3.1 3.1 2.7 3.2
[145] 3.3 3.0 2.5 3.0 3.4 3.0

$Petal.Length
  [1] 1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 1.5 1.6 1.4 1.1 1.2 1.5 1.3 1.4
 [19] 1.7 1.5 1.7 1.5 1.0 1.7 1.9 1.6 1.6 1.5 1.4 1.6 1.6 1.5 1.5 1.4 1.5 1.2
 [37] 1.3 1.4 1.3 1.5 1.3 1.3 1.3 1.6 1.9 1.4 1.6 1.4 1.5 1.4 4.7 4.5 4.9 4.0
 [55] 4.6 4.5 4.7 3.3 4.6 3.9 3.5 4.2 4.0 4.7 3.6 4.4 4.5 4.1 4.5 3.9 4.8 4.0
 [73] 4.9 4.7 4.3 4.4 4.8 5.0 4.5 3.5 3.8 3.7 3.9 5.1 4.5 4.5 4.7 4.4 4.1 4.0
 [91] 4.4 4.6 4.0 3.3 4.2 4.2 4.2 4.3 3.0 4.1 6.0 5.1 5.9 5.6 5.8 6.6 4.5 6.3
[109] 5.8 6.1 5.1 5.3 5.5 5.0 5.1 5.3 5.5 6.7 6.9 5.0 5.7 4.9 6.7 4.9 5.7 6.0
[127] 4.8 4.9 5.6 5.8 6.1 6.4 5.6 5.1 5.6 6.1 5.6 5.5 4.8 5.4 5.6 5.1 5.1 5.9
[145] 5.7 5.2 5.0 5.2 5.4 5.1

$Petal.Width
[1] TRUE

$Species
[1] FALSE

um exemplo aplicado com pipe

mtcars %>%
  split(.$cyl) %>% # from base R
  map(~ lm(mpg ~ wt, data = .)) %>%
  map(summary) %>%
  map_dbl("r.squared")
        4         6         8 
0.5086326 0.4645102 0.4229655 

pluck: um exemplo aplicado

Se você já trabalhou com listas no R, sabe que a sintaxe é um pouco contra-intuitiva. Em vez de ter que fazer coisas como list[[1]][2] para recuperar elementos específicos de listas, podemos usar a função de pluck muito útil:

Aqui está a funcionalidade básica:

example <- list(movies, years,preference = c(2, 1, 3, 7, 8, 9, 4, 6, 5))

example
[[1]]
[1] "A New Hope"              "The Empire Strikes Back"
[3] "Return of the Jedi"      "Phantom Menace"         
[5] "Attack of the Clones"    "Revenge of the Sith"    
[7] "The Force Awakens"       "The Last Jedi"          
[9] "Rise of Skywalker"      

[[2]]
[1] 1977 1980 1983 1999 2002 2005 2015 2017 2019

$preference
[1] 2 1 3 7 8 9 4 6 5
example[[1]][5]
[1] "Attack of the Clones"
example[[2]][5]
[1] 2002
example[[3]][5]
[1] 8

com o pluck:

pluck(example, 1)
[1] "A New Hope"              "The Empire Strikes Back"
[3] "Return of the Jedi"      "Phantom Menace"         
[5] "Attack of the Clones"    "Revenge of the Sith"    
[7] "The Force Awakens"       "The Last Jedi"          
[9] "Rise of Skywalker"      
pluck(example, 1, 5)
[1] "Attack of the Clones"
pluck(example, 2, 5)
[1] 2002
pluck(example, 3, 5)
[1] 8

Por que você pode querer usar isso além de uma sintaxe mais fácil? Talvez algo assim, onde queremos extrair a última palavra de cada título de filme:

example %>% 
  pluck(1) %>% 
  map_chr(~ word(., -1))
[1] "Hope"      "Back"      "Jedi"      "Menace"    "Clones"    "Sith"     
[7] "Awakens"   "Jedi"      "Skywalker"

O que está acontecendo aqui?

exemplo %>% pluck(1): pipe em nossa lista e pegue o primeiro elemento da lista, o título do filme; map_chr(~ word(., 1)): pegue cada título de filme e extraia a última palavra dele usando a função de palavra.

Veja como ainda podemos fazer isso com pipes, mas sem a funcionalidade purrr:
example %>% 
  .[[1]] %>% 
  word(-1)
[1] "Hope"      "Back"      "Jedi"      "Menace"    "Clones"    "Sith"     
[7] "Awakens"   "Jedi"      "Skywalker"

Não é tão ruim, mas você pode ver como isso pode ficar complexo e difícil de ler rapidamente com coisas como .[[1]].

Um exemplo de raspagem de dados

library(rvest)
library(tidyverse)
get_album_list <- function(url){
  read_html(url)  %>% 
    html_nodes(".col-md-12") %>%
    html_nodes("a") %>%
    html_attr("href")
    
}

url_album <- get_album_list("http://paroles2chansons.lemonde.fr/paroles-michel-sardou/discographie.html")

Ainda não tem {purrr} aqui, vamos pegar todas as informações de um album:

get_album_info <- function(url){
  page <- read_html(url) 
  date <- page %>% 
    html_nodes("small") %>%
    html_text() %>%
    stringr::str_replace_all("Date de Sortie : ", "") %>%
    lubridate::dmy()
  song_list <- page %>% 
    html_nodes(".font-small") %>%
    html_text() %>%
    discard(~ .x == "Plan de site" | .x == "Mention legale" | .x == "Chansons de mariage" | .x == "Chansons d'enterrement" )
  
  url_list <- page %>% 
    html_nodes(".font-small") %>%
    html_attr("href") %>%
    discard(~ .x == "/plan-du-site.html" | .x == "/mentions-legales.html" | .x == "/paroles-chansons-de-messe-d-enterrement/"| .x == "/paroles-chansons-de-messe-de-mariage/")
  
  album_name <- page %>%
    html_nodes(".breadcrumb") %>%
    html_text() %>%
    stringr::str_extract("\t.*$") %>%
    stringr::str_replace_all("\t", "")
  
  tibble(chanson = song_list, 
         url = url_list, 
         nom = album_name, 
         date = date)

}


albums_infos <- map_df(url_album, get_album_info) %>%
  filter(grepl("sardou", url))

reduce(), flatten(), invoke(), modify(), possibly(), walk(),cross(), every() e keep()

outras funções do purrr interessantes: reduce(), flatten(), invoke(), modify(), possibly() e keep().

Recursos adicionais

Este foi realmente um curso sobre a intuição do purrr e suas funções mais usadas. Há muita flexibilidade aqui e muitas outras aplicações. Aqui estão alguns bons recursos: