-
Notifications
You must be signed in to change notification settings - Fork 0
/
05-functiona-programming-purrr.Rmd
392 lines (268 loc) · 18.5 KB
/
05-functiona-programming-purrr.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
# Programação funcional (purrr) {#funcionais}
Programação Funcional (PF) é um estilo de programar que alguns profissionais desenvolveram utilizando a ideia de tratar a programação como funções matemáticas. Esta forma de programar envolve um paradigma de programação com o qual a maior parte
dos estatísticos não está familiarizada e muitos tutoriais de R costuma ignorar esta técnica por ela não estar diretamente envolvida com manipulação e visualização de dados.
A forma como o código é organizado, utilizando programação funcional, nos permite criar códigos mais concisos e *pipeáveis*, que facilita o trabalho de depurar, estender e documentar o trabalho que está sendo desenvolvido. Além disso, códigos funcionais geralmente são paralelizáveis, permitindo que tratemos problemas muito grandes com poucas modificações.
R não é uma linguagem de programação funcional pura, mas é possível, no R `base`, escrever código usando o paradigma de programação funcional, entretanto é necessário algum esforço.
O pacote `purrr` foi desenvolvido com o objetido de fornecer recursos básicos de programação funcional no R com algumas funções muito interessantes.
Para instalar e carregar o `purrr` basta executar o código a seguir.
```{r, eval=FALSE}
install.packages(c("Rtools", "devtools", "purrr"))
devtools::install_github("jennybc/repurrrsive")
library(tidyverse) # Ou
library(purrr)
library(repurrrsive)
```
## Iterações básicas
```{r, message=FALSE, warning=FALSE, include=FALSE}
library(tidyverse)
```
Em vez de usar loops, as linguagens de programação puramente funcionais usam funções que alcançam o mesmo resultado. A primeira família de funções do `purrr` que veremos também é a mais útil e extensível. As funções `map()` são muito consistentes e, portanto, mais fáceis de usar. São quase como substitutas para laços `for` e abstraem a iteração em apenas uma linha. Veja esse exemplo de laço usando `for`:
>```{r}
soma_um <- function(x) { x + 1 }
obj <- 10:15
for (i in seq_along(obj)) {
obj[i] <- soma_um(obj[i])
}
obj
```
```{r}
sw_people[[1]]
sw_people[1]
map(sw_people, ~length(.x$starships))
```
O código acima mostra como é possível extrair informações de forma sequencial sem a necessidade de utilização de laços. A PF, por meio da função `map()`, permite aplicar uma função ou uma fórmula desejada em cada elemento do objeto dado, dispensando a necessidade de declararmos um iterador auxiliar (`i`).
Para utilizar fórmulas dentro da `map()`, basta colocar um til (`~`) antes da função que será chamada, conforme mostrado no exemplo anterior. Feito isso, podemos utilizar o placeholder `.x` para indicar onde deve ser colocado cada elemento do objeto.
>```{r}
soma_um <- function(x) { x + 1 }
obj <- 10:15
obj <- map(obj, soma_um)
obj
```
Você deve ter percebido que o resultado desta última execução não foi exatamente igual à quando utilizamos o loop, isto aconteceu porque a função `map()` tenta ser mais genérica, retornando por padrão uma lista com um elemento para cada saída.
Mas é possível "achatar" o resultado informando qual será o seu tipo. Isso pode ser feito por meio da utilização das funções pertencentes à família `map()`: `map_chr()` (para strings), `map_dbl()` (para números reais), `map_int()` (para números inteiros) e `map_lgl()` (para booleanos).
```{r}
map_int(sw_people, ~ length(.x[["starships"]]))
map_chr(sw_people, ~ .x[["hair_color"]])
map_lgl(sw_people, ~ .x[["gender"]] == "male")
map_dbl(sw_people, ~ .x[["mass"]])
map(sw_people, ~ .x[["mass"]])
map_dbl(sw_people, ~ as.numeric(.x[["mass"]]))
map_chr(sw_people, ~ .x[["mass"]]) %>%
readr::parse_number(na = "unknown")
```
> O `purrr` também nos fornece outra ferramenta interessante para
achatar listas: a família `flatten()`. No fundo, `map_chr()`
é quase um atalho para `map() %>% flatten_chr()`!
Algo bastante útil da família `map()` é a possibilidade de passar argumentos
fixos para a função que será aplicada. A primeira forma de fazer isso envolve
fórmulas:
> BRUNO, VAMOS MANTER ESSA PARTE?! A outra forma de passar argumentos para a função é através das reticências da
`map()`. Desta maneira precisamos apenas dar o nome do argumento e seu valor
logo após a função `soma_n()`.
```{r}
soma_n <- function(x, n = 1) { x + n }
obj <- 10:15
map_dbl(obj, soma_n, n = 2)
```
Usando fórmulas temos maior flexibilidade, pois podemos declarar, por exemplo, funções anônimas como `~.x+2`, ao invés de `soma_dois`, por exemplo), enquanto com as reticências temos maior legibilidade.
## Iterações intermediárias
Em algumas situações é necessário realizar operações com mais de um objeto. O objetivo desta seção é apresentar a função `map2()`, mas antes disso vamos mostrar duas funções (`walk()` e `modify()`) que irão nos ajudar a apresentar a `map2()` mais adiante.
### Andar e modificar
`walk()` e `modify()` são pequenas alterações da família `map()` que vêm a calhar em diversas situações. A primeira destas funciona exatamente igual à `map()` mas
não devolve resultado, apenas efeitos colaterais, quando não existe a necessidade de utilizar um valor de retorno. Normalmente utilizamos ela quando desejamos renderizar a saída na tela ou salvar arquivos no disco - o importante é a ação, não o valor de retorno. Aqui está um exemplo simples:
```{r}
x <- list(1, "a", 3)
x %>%
walk(print)
```
Já a a segunda, não muda a estrutura do objeto sendo iterado, ela substitui os próprios elementos da entrada, aplicando a função em cada elemento.
```{r}
x <- list(1, 2, 3)
modify(x, ~.+2)
```
> A maior utilidade de `walk` é quando precisamos salvar múltiplas tabelas. Para fazer isso, podemos usar algo como
`walk(tabelas, readr::write_csv)`. Um caso de uso interessante da `modify()` é ao lado do sufixo `_if()`, combinação que nos permite iterar nas colunas de uma tabela e aplicar transformações de tipo apenas quando um atributo for verdade (geralmente de queremos transformar as colunas de fator para caractere).
A função `map2()` é uma generalização da `map()` para mais de um argumento e nos permite reproduzir o laço acima em apenas uma linha. A `map2()` abstrai a iteração em paralelo, aplica a função em cada par de elementos das entradas e pode achatar o objeto retornado com os sufixos `_chr`, `_dbl`, `_int` e `_lgl`.
> O termo "paralelo" neste capítulos se refere a laços em mais de uma estrutura e não a paralelização de computações em mais de uma unidade de processamento.
```{r}
gap_split_small <- gap_split[1:10]
countries <- names(gap_split_small)
ggplot(gap_split_small[[1]], aes(year, lifeExp)) +
geom_line() +
labs(title = countries[[1]])
plots <- map2(gap_split_small, countries,
~ ggplot(.x, aes(year, lifeExp)) +
geom_line() +
labs(title = .y))
plots[[1]]
walk(plots, print)
walk2(.x = plots, .y = countries,
~ ggsave(filename = paste0(.y, ".pdf"), plot = .x))
file.remove(paste0(countries, ".pdf"))
```
Como o pacote `purrr` é extremamente consistente, a `map2()` também funciona com reticências, fórmulas e dá acesso ao placeholder `.y` para indicar onde os elementos do segundo vetor devem ir.
Para não precisar oferecer uma função para cada número de argumentos, o pacote `purrr` fornece a `pmap()`. Para esta função devemos passar uma lista em que cada
elemento é um dos objetos a ser iterado:
```{r}
x <- list(1, 1, 1)
y <- list(10, 20, 30)
z <- list(100, 200, 300)
pmap(list(x, y, z), sum)
```
Infelizmente a `pmap()` não nos permite utilizar fórmulas. Se quisermos usar uma função anônima com ela, precisamos declará-la no seu corpo:
```{r}
pmap(list(x, y, z), function(first, second, third) (first + third) * second)
l <- list(a = x, b = y, c = z)
pmap(l, function(c, b, a) (a + c) * b)
```
A última função que veremos nessa seção é a `imap()`. No fundo ela é um atalho para `map2(x, names(x), ...)` quando `x` tem nomes e para `map2(x, seq_along(x), ...)` caso contrário:
```{r}
objeto <- 10:20
imap_dbl(objeto, ~.x+.y)
```
Como podemos observar, agora `.y` é o placeholder para o índice atual (equivalente ao `i` no laço com `for`). Naturalmente, assim como toda a família `map()`, a
`imap()` também funciona com os sufixos de achatamento.
## Iterações avançadas
Agora que já tivemos uma noção da sintaxe da PF com a família `map()`, para substituição de laços, podemos passar para os tipos de laços que envolvem condicionais.
> Como o objetivo deste módulo é mostrar uma "nova forma" de manipular dados, recomendamos fortemente que vocês não fiquem restritos a este material e também leiam a documentação de cada função aqui abordada.
### Iterações com condicionais
Imagine que seja necessário aplicar uma função somente em alguns elementos de um vetor. Sabemos que com a utilização de um laço isso é uma tarefa fácil, no entanto com as funções da família `map()` apresentadas até agora isso seria extremamente difícil. Veja o trecho de código
a seguir por exemplo:
```{r}
dobra <- function(x) { x*2 }
obj <- 10:15
for (i in seq_along(obj)) {
if (obj[i] %% 2 == 1) { obj[i] <- dobra(obj[i]) }
else { obj[i] <- obj[i] }
}
obj
```
No exemplo acima, aplicamos a função `dobra()` apenas nos elementos ímpares do vetor `obj`. Com o pacote `purrr` temos duas maneiras de fazer isso: com `map_if()` ou `map_at()`.
A primeira dessas funções aplica a função dada apenas quando uma condição é satisfeita. Esta condição pode ser a saída de uma função ou uma fórmula (que serão aplicadas em cada elemento da entrada e devem retornar `TRUE` ou `FALSE`).
A função `map_if()`, diferente das apresentadas anteriormente, não funciona com sufixos, isto implica que devemos achatar o resultado:
```{r}
eh_par <- function(x) { x%%2 == 0}
raiz <- function(x) { sqrt(x) }
numeros <- 2:20
map_if(numeros, eh_par, raiz) %>% flatten_dbl()
```
A utilização de fórmulas permite eliminar completamente a necessidade de funções declaradas:
```{r}
map_if(numeros, ~.x%%2 == 0, ~sqrt(.x)) %>% flatten_dbl()
map_if(numeros, ~.x%%2 == 0, ~sqrt(.x), .else = ~.x/2) %>% flatten_dbl()
```
Também é possível especificar a condição `else`, como mostra o comando a seguir:
```{r}
map_if(numeros, ~.x%%2 == 0, ~sqrt(.x), .else = ~.x/2) %>%
flatten_dbl()
```
A segunda dessas funções é a `map_at()`, que funciona de forma muito semelhante à `map_if()`. Para `map_at()` devemos passar um vetor de nomes ou índices onde a função deve ser aplicada:
```{r}
map_at(numeros, c(2, 4, 6), ~sqrt(.x)) %>% flatten_dbl()
```
### Iterações com tabelas e funções
Ainda dentro da família `map()` existem duas funções, que são menos utilizadas, `map_dfc()` e `map_dfr()`, que retornam um dataframe criado por vinculação de linha e vinculação de coluna, respectivamente. Isto é equivalente a um `map()` seguido de um `dplyr::bind_cols()` ou de um
`dplyr::bind_rows()`, nesta ordem.
> A maior utilidade dessas funções é quando precisamos estruturar uma tabela com informações espalhadas em muitos
arquivos. Se elas estiverem divididas por grupos de colunas, podemos usar algo
como `map_dfc(arquivos, readr::read_csv)` e se elas estiverem
divididas por grupos de linhas, `map_dfr(arquivos, readr::read_csv)`.
Outra função do pacote `purrr` pouco utilizada é a `invoke_map()`. Antes de apresentar a funcionalidade da `invoke_map()`, vamos demonstrar o que faz a
`invoke()` sozinha:
```{r}
invoke(runif, list(n = 10))
invoke(runif, n = 10)
```
A `invoke` recebe uma função e uma lista de argumentos já a `invoke_map()` pode recerber tanto uma única função com uma lista de argumentos quanto uma lista de funções com uma lista de argumentos, sendo uma generalização da primeira. Assim como a família `map()`, a família `invoke()` também aceita os sufixos, como veremos a seguir:
```{r}
invoke_map(list(runif, rnorm), list(list(n = 10), list(n = 5)))
invoke_map(list(runif, rnorm), list(list(n = 5)))
invoke_map(list(runif, rnorm), n = 5)
invoke_map("runif", list(list(n = 5), list(n = 10)))
```
### Redução e acúmulo
Reduzir é outro conceito importante na programação funcional. Permite partir de uma lista com alguns elementos e obter um objeto com um único elemento, combinando os elementos de alguma forma. Sendo assim o pacote `purrr` possui duas funções que ajudam a realizar essa transformação: `reduce` e `accumulate`, que aplicam transformações em valores acumulados.
> Observe o laço a seguir:
>```{r}
soma_ambos <- function(x, y) { x + y }
obj <- 10:15
for (i in 2:length(obj)) {
obj[i] <- soma_ambos(obj[i-1], obj[i])
}
obj
```
A soma cumulativa utilizando um laço é bastante simples, mas também é muito fácil de fazer confusão entre os índices e o *bug* acabar passando desapercebido. Para evitar esse tipo de situação, podemos utilizar `accumulate()` ou `reduce` (tanto com uma função quanto com uma fórmula). A diferença entre elas é que a primeira mantém os resultados intermediários, enquanto a segunda retorna o resultado final. Primeiramente vamos mostrar a utilização da `accumulate()`:
```{r}
1:5 %>% accumulate(`+`)
1:5 %>% accumulate(`+`, .dir = "backward")
rerun(5, rnorm(100)) %>%
set_names(paste0("sim", 1:5)) %>%
map(~ accumulate(., ~ .05 + .x + .y)) %>%
map_dfr(~ tibble(value = .x, step = 1:100), .id = "simulation") %>%
ggplot(aes(x = step, y = value)) +
geom_line(aes(color = simulation)) +
ggtitle("Simulations of a random walk with drift")
```
**DICA:** Nesse caso, os placeholders têm significados ligeiramente diferentes. Aqui, `.x` é o valor acumulado e `.y` é o valor "atual" do objeto sendo iterado.
Se não quisermos o valor acumulado em cada passo da iteração:
```{r}
1:5 %>% reduce(`+`)
```
Essas duas funções também têm variedades paralelas (`accumulate2()` e `reduce2()`), assim como variedades invertidas `accumulate_right()` e `reduce_right()`).
## Miscelânea
A Programação Funcional não facilita apenas para evitar o uso de laços, o `purrr` possui algumas funções que não relacionadas com laços, mas que são bastante úteis quando utilizamos as funções apresentadas até o momento.
### Manter e descartar
Se tivermos o objetivo de filtrar elementos de um vetor ou lista, podemos usar as funções `keep()`, seleciona elementos que passam por um teste lógico, e `discard()`, seleciona elementos que não passam por um teste lógico. Elas funcionam com fórmulas e podem ser extremamente úteis em situações que `dplyr::select()` e `magrittr::extract()` não conseguem cobrir:
```{r}
rep(10, 10) %>%
map(sample, 5) %>%
keep(~ mean(.x) > 6)
```
No exemplo acima estamos mantendo todos os vetores da lista com média maior que 6.
```{r}
x <- rerun(5, a = rbernoulli(1), b = sample(10))
x
x %>% keep("a")
x %>% discard("a")
```
### Verificações
Quando há necessidade de verificar o tipo de um ou mais objetos, existe, no pacote `purrr`, uma outra família que ajuda a realizar essas verificações, que é a `is()`. Esta família possui uma série de funções que nos permite fazer verificações extremamente estritas em objetos dos mais variados tipos. Seguem alguns poucos exemplos:
```{r}
is_scalar_integer(10:15)
is_bare_integer(10:15)
is_atomic(10:15)
is_vector(10:15)
```
### Transposição e indexação profunda
Quando acontece de estarmos trabalhando com listas complexas e profundas, às vezes é necessário acessar algum elemento da mesma ou transpô-la para facilitar a manipulação ou análise. O `purrr` nos fornece duas funções extremamente úteis para facilitar o nosso trabalho: `pluck()` e `transpose()`, respectivamente. A primeira possibilita o acesso de elementos profundos de uma lista sem a necessidade de colchetes, enquanto a segunda transpõe a lista.
```{r}
obj <- list(list(a = 1, b = 2, c = 3), list(a = 4, b = 5, c = 6))
str(obj)
pluck(obj, 2, "b")
str(transpose(obj))
```
**DICA:** Se você estiver com muitos problemas com listas profundas, dê uma olhada nas funções relacionadas a `depth()` pois elas podem ser muito úteis.
### Aplicação parcial
Se o nosso objetivo for definir valor para os arhgumentos de uma função (seja para usá-la em uma pipeline ou com alguma função do próprio `purrr`), podemos utilizar a `partial()`. Ela funciona nos moldes da família `invoke()` e pode ser bastante útil para tornar suas pipelines mais enxutas:
```{r}
soma_varios <- function(x, y, z) { x + y + z }
nova_soma <- partial(soma_varios, x = 5, y = 10)
nova_soma(3)
```
No exemplo anterior, fixamos o valor de x e y da função `soma_varios`, sendo necessário definir posteriormente apenas o valor de `z`.
### Execução segura
É bem comum criarmos uma função e, quando a executamos, temos um erro como retorno. Isto pode ser contornado com facilidade em um laço com um condicional, mas se trata de uma tarefa mais complexa quando está trabalhando com programação funcional. Desta forma, o `purrr` possui algumas funções que encapsulam as saídas, e quando esta possui um erro, o silenciam e retornam um valor padrão em seu lugar.
A função `quietly()` retorna uma lista com resultado, saída, mensagem e alertas, já a `safely()` retorna uma lista com resultado e erro (um destes sempre é `NULL`), e a `possibly()` silencia o erro e retorna um valor dado pelo usuário.
```{r}
soma_um <- function(x) { x + 1 }
s_soma_um <- safely(soma_um, 0)
obj <- c(10, 11, "a", 13, 14, 15)
s_soma_um(obj)
```
## Exercícios
A base `imdb` nos exercícios abaixo pode ser baixada [clicando aqui](https://github.com/curso-r/livro-material/raw/master/assets/data/imdb.rds).
**1.** Utilize a função `map()` para calcular a média de cada coluna da base `mtcars`.
**2.** Use a função `map()` para testar se cada elemento do vetor `letters` é uma vogal ou não. Dica: você precisará criar uma função para testar se é uma letra é vogal. Faça o resultado ser (a) uma lista de `TRUE/FALSE` e (b) um vetor de `TRUE/FALSE`.
**3** Faça uma função que divida um número por 2 se ele for par ou multiplique ele por 2 caso seja ímpar. Utilize uma função `map` para aplicar essa função ao vetor `1:100`. O resultado do código deve ser um vetor numérico.
**4.** Use a função `map()` para criar gráficos de dispersão da receita vs orçamento para os filmes da base `imdb`. Os filmes de cada ano deverão compor um gráfico diferente. Faça o resultado ser (a) uma lista de gráficos e (b) uma nova coluna na base `imdb` (utilizando a função `tidyr::nest()`).
**5.** Utilize a função `walk` para salvar cada ano da base `imdb` em um arquivo `.rds` diferente, isto é, o arquivo `imdb_2001.rds`, por exemplo, deve conter apenas filmes do ano de 2001.