-
Notifications
You must be signed in to change notification settings - Fork 7
/
10-funcionais.Rmd
430 lines (331 loc) · 15.5 KB
/
10-funcionais.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
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
# Programação funcional (purrr) {#funcionais}
Programação funcional (PF) é um paradigma de programação com o qual a maior parte das pessoas que estudam estatística não está familiarizada. Essa técnica costuma ser ignorada na
maioria dos tutoriais de R por não estar diretamente envolvida com manipulação e
visualização de dados, mas isso não quer dizer que ela não tenha suas vantagens.
Usando programação funcional podemos criar códigos mais concisos e *pipeáveis*, características que por tabela também tornam mais simples o processo de encontrar erros. Além disso, códigos funcionais geralmente são paralelizáveis, permitindo que tratemos problemas muito grandes com poucas modificações.
Apesar de o R `base` já ter funções que podem ser consideradas elementos de PF, a
implementação destas não é tão elegante e, portanto, este tutorial abordará
somente a implementação de PF realizada pelo pacote `purrr`.
Para instalar e carregar o `purrr`, rode o código a seguir. Nas próximas seções
deste tutorial, assumiremos que você têm esse pacote instalado e carregado.
```{r, eval=FALSE}
install.packages("purrr")
library(purrr)
```
## Iterações básicas
```{r, message=FALSE, warning=FALSE, include=FALSE}
library(purrr)
```
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 quase como substitutas para laços `for`,
elas 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
```
O que de fato estamos tentando fazer com o laço acima? Temos um vetor (`obj`) e
queremos aplicar uma função (`soma_um()`) em cada elemento dele. A função `map()`
remove a necessidade de declaramos um objeto iterador auxiliar (`i`) e
simplesmente aplica a função desejada em cada elemento do objeto dado.
```{r}
soma_um <- function(x) { x + 1 }
obj <- 10:15
obj <- map(obj, soma_um)
obj
```
Como você deve ter percebido, o resultado da execução acima não é exatamente
igual ao que tivemos com o laço. Isso acontece porque a `map()` tenta ser
extremamente genérica, retornando por padrão uma lista com um elemento para
cada saída.
Se quisermos "achatar" o resultado, devemos informar qual será o seu tipo. Isso
é super simples e pode ser feito com as irmãs da `map()`: `map_chr()` (para
strings), `map_dbl()` (para números reais), `map_int()` (para números inteiros) e
`map_lgl()` (para booleanos).
```{r}
obj <- 10:15
map_dbl(obj, soma_um)
```
> 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:
```{r}
soma_n <- function(x, n = 1) { x + n }
obj <- 10:15
map_dbl(obj, ~soma_n(.x, 2))
```
Como vemos no exemplo acima, para utilizar fórmulas precisamos colocar um til
(`~`) antes da função que será chamada. Feito isso, podemos utilizar o
placeholder `.x` para indicar onde deve ser colocado cada elemento de `obj`.
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 uma maior flexibilidade (podemos, por exemplo, declarar
funções anônimas como `~.x+2`), enquanto com as reticências temos maior
legibilidade.
## Iterações intermediárias
Agora que já exploramos os básicos da família `map()` podemos partir para
iterações um pouco mais complexas. Observe o laço a seguir:
```{r}
soma_ambos <- function(x, y) { x + y }
obj_1 <- 10:15
obj_2 <- 20:25
for (i in seq_along(obj_1)) {
obj_1[i] <- soma_ambos(obj_1[i], obj_2[i])
}
obj_1
```
Com a função `map2()` podemos reproduzir o laço acima em apenas uma linha. Ela
abstrai a iteração em paralelo, aplica a função em cada par de elementos das
entradas e, assim como sua prima `map()`, 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}
soma_ambos <- function(x, y) { x + y }
obj_1 <- 10:15
obj_2 <- 20:25
obj_1 <- map2_dbl(obj_1, obj_2, soma_ambos)
obj_1
```
Como o pacote `purrr` é extremamente consistente, a `map2()` também funciona com
reticências e fórmulas. Poderíamos, por exemplo, transformar `soma_ambos()` em
uma função anônima:
```{r}
obj_1 <- 10:15
obj_2 <- 20:25
map2_dbl(obj_1, obj_2, ~.x+.y)
```
Desta vez também temos acesso ao placeholder `.y` para indicar onde os elementos
de 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 essa função devemos passar uma lista em que cada
elemento é um dos objetos a ser iterado:
```{r}
soma_varios <- function(x, y, z) { x + y + z }
obj_1 <- 10:15
obj_2 <- 20:25
obj_3 <- 30:35
obj_1 <- pmap_dbl(list(obj_1, obj_2, obj_3), soma_varios)
obj_1
```
Com a `pmap()` infelizmente não podemos usar fórmulas. Se quisermos usar uma
função anônima com ela, precisamos declará-la a função no seu corpo:
```{r}
obj_1 <- 10:15
obj_2 <- 20:25
obj_3 <- 30:35
pmap_dbl(list(obj_1, obj_2, obj_3), function(x, y, z) { x + y + z })
```
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}
obj <- 10:15
imap_dbl(obj, ~.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á vimos como substituir iterações de nível básico e de nível
intermediário com a família `map()`, podemos passar para os tipos mais obscuros
de laços. Cada item desta seção será mais denso do que os das passadas, por isso
encorajamos todos os leitores para que também leiam a documentação de cada função
aqui abordada.
### Iterações com condicionais
Imagine que precisamos aplicar uma função somente em alguns elementos de um vetor.
Com um laço isso é uma tarefa fácil, mas 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 um predicado é
`TRUE`. Esse predicado pode ser uma função ou uma fórmula (que serão aplicadas
em cada elemento da entrada e devem retornar `TRUE` ou `FALSE`). Infelizmente
a `map_if()` não funciona com sufixos, então devemos achatar o resultado:
```{r}
eh_impar <- function(x) { x%%2 == 1 }
dobra <- function(x) { x*2 }
obj <- 10:15
map_if(obj, eh_impar, dobra) %>% flatten_dbl()
```
Com fórmulas poderíamos eliminar completamente a necessidade de funções
declaradas:
```{r}
obj <- 10:15
map_if(obj, ~.x%%2 == 1, ~.x*2) %>% flatten_dbl()
```
A segunda dessas funções é a irmã gêmea de `map_if()` e funciona de forma muito
semelhante. Para `map_at()` devemos passar um vetor de nomes ou índices onde a
função deve ser aplicada:
```{r}
obj <- 10:15
map_at(obj, c(2, 4, 6), ~.x*2) %>% flatten_dbl()
```
### Iterações com tabelas e funções
Duas funções menos utilizadas da família `map()` são `map_dfc()` e `map_dfr()`,
que equivalem a um `map()` seguido de um `dplyr::bind_cols()` ou de um
`dplyr::bind_rows()` respectivamente.
> A maior utilidade dessas funções é quando temos uma tabela espalhada 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)`
Outro membro obscuro da família `map()` é a `invoke_map()`. Na verdade essa
função pode ser considerada um membro da família `invoke()`, mas vamos ver
que as semelhanças são muitas. Primeiramente, vamos demonstrar o que faz a
`invoke()` sozinha:
```{r}
soma_ambos <- function(x, y) { x + y }
invoke(soma_ambos, list(x = 10, y = 15))
```
É fácil de ver que essa função recebe uma função e uma lista de argumentos para
usar em uma chamada desta. Agora generalizando esta lógica temos `invoke_map()`,
que chama uma mesma função com uma lista de listas de argumentos ou uma lista
de funções com uma lista de argumentos. A família `invoke()` também aceita os
sufixos como veremos a seguir:
```{r}
soma_ambos <- function(x, y) { x + y }
soma_um <- function(x) { x + 1 }
soma_dois <- function(x) { x + 2 }
invoke_map_dbl(soma_ambos, list(list(x = 10, y = 15), list(x = 20, y = 25)))
invoke_map_dbl(list(soma_um, soma_dois), list(x = 10))
```
### Redução e acúmulo
Outras funções simbólicas de programação funcional além da `map()` sã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
```
Essa soma cumulativa é bastante simples, mas não é difícil imaginar uma situação
em que um programador desavisado confunde um índice com o outro e o *bug* acaba
passando desapercebido. Para evitar esse tipo de situação, podemos utilizar
`accumulate()` (tanto com uma função quanto com uma fórmula):
```{r}
soma_ambos <- function(x, y) { x + y }
obj <- 10:15
accumulate(obj, soma_ambos)
accumulate(obj, ~.x+.y)
```
**Obs.:** 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, podemos usar
`reduce()`:
```{r}
obj <- 10:15
reduce(obj, ~.x+.y)
```
Para a nossa comodidade, essas duas funções também têm variedades paralelas
(`accumulate2()` e `reduce2()`), assim como variedades invertidas
`accumulate_right()` e `reduce_right()`).
## Miscelânea
Por fim, veremos algumas funções do `purrr` que não têm exatamente a ver com
laços, mas que acabam sendo bastante úteis quando usando as funções que vimos
até agora. Elas não serão apresentadas em nenhuma ordem específica, este é apenas
um apanhado de funções sortidas que achamos úteis enquanto programando com o
`purrr`.
### Manter e descartar
Se quisermos filtrar elementos de um vetor ou lista, podemos usar as funções
`keep()` e `discard()`. Elas funcionam com fórmulas e podem ser extremamente úteis
em situações que `dplyr::select()` e `magrittr::extract()` não conseguem cobrir:
```{r}
obj <- list(10:15, 20:25, c(30:34, NA))
keep(obj, ~any(is.na(.x)))
discard(obj, ~!any(is.na(.x)))
```
No exemplo acima descartamos todos os vetores da lista que não têm pelo menos um
elemento omisso (`NA`).
### A família `is`
Uma outra família do pacote `purrr` é a `is()`. Com essa série de funções podemos
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)
```
### 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; a segunda, não muda a
estrutura do objeto sendo iterado, ela substitui os próprios elementos da entrada.
> 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 predicado for verdade (geralmente de
queremos transformar as colunas de fator para caractere).
### Transposição e indexação profunda
Quando precisarmos lidar com listas complexas e profundas, o `purrr` nos fornece
duas funções extremamente úteis: `transpose()` e `pluck()`. A primeira transpõe
uma lista, enquanto a segunda é capaz de acessar elementos profundos de uma lista
sem a necessidade de colchetes.
```{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))
```
**Obs.:** 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 quisermos pré-preencher os argumentos de uma função (seja para usá-la em uma
pipeline ou com alguma função do próprio `purrr`), temos `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 = 1, y = 2)
nova_soma(3)
```
### Execução segura
Não é incomum executarmos uma função e recebermos um erro de volta. Isso pode ser
lidado com facilidade em um laço com um condicional, mas essa tarefa já é mais
complexa quando se trata de programação funcional. Para isso, no `purrr` temos
algumas funções que embrulham uma função e, quando esta retornar um erro, o
silenciam e retornam um valor padrão em seu lugar.
`quietly()` retorna uma lista com resultado, saída, mensagem e alertas, `safely()`
retorna uma lista com resultado e erro (um destes sempre é `NULL`), e `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.