forked from cienciadedatos/r4ds
-
Notifications
You must be signed in to change notification settings - Fork 0
/
21-iteration.qmd
977 lines (715 loc) · 36.6 KB
/
21-iteration.qmd
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
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
# Iteración
## Introducción
En [funciones], hablamos sobre la importancia de reducir la duplicación en el código creando funciones, en lugar de copiar y pegar. Reducir la duplicación de código tiene tres beneficios principales:
1. Es más fácil ver el objetivo de tu código; lo diferente llama
más atención a la vista que aquello que permanece igual.
2. Es más sencillo responder a cambios en los requerimientos. A medida que tus
necesidades cambian, solo necesitarás realizar cambios en un lugar, en vez
de recordar cambiar en cada lugar donde copiaste y pegaste el código.
3. Es probable que tengas menos errores porque cada línea de código es
utilizada en más lugares.
Una herramienta para reducir la duplicación de código son las funciones, que reducen dicha duplicación al identificar patrones repetidos de código y extraerlos en piezas independientes que pueden reutilizarse y actualizarse fácilmente. Otra herramienta para reducir la duplicación es la __iteración__, que te ayuda cuando necesitas hacer la misma tarea con múltiples entradas: repetir la misma operación en diferentes columnas o en diferentes conjuntos de datos.
En este capítulo aprenderás sobre dos paradigmas de iteración importantes: la programación imperativa y la programación funcional. Por el lado imperativo, tienes herramientas como _for loops_ y _while loops_, que son un gran lugar para comenzar porque hacen que la iteración sea muy explícita, por lo que es obvio qué está pasando. Sin embargo, los bucles _for_ son bastante detallados y requieren bastante código que se duplica para cada bucle. La programación funcional (PF) ofrece herramientas para extraer este código duplicado, por lo que cada patrón común de bucle obtiene su propia función. Una vez que domines el vocabulario de PF, podrás resolver muchos problemas comunes de iteración con menos código, mayor facilidad y menos errores.
### Prerrequisitos
Una vez que hayas dominado los bucles _for_ proporcionados por R base, aprenderás algunas de las potentes herramientas de programación proporcionadas por __purrr__, uno de los paquetes principales de _tidyverse_.
```{r setup, message = FALSE}
library(tidyverse)
library(datos)
```
## Bucles _for_
Imagina que tenemos este simple _tibble_:
```{r}
df <- tibble(
a = rnorm(10),
b = rnorm(10),
c = rnorm(10),
d = rnorm(10)
)
```
Queremos calcular la mediana de cada columna. _Podrías hacerlo_ copiando y pegando el siguiente código:
```{r}
median(df$a)
median(df$b)
median(df$c)
median(df$d)
```
Pero eso rompe nuestra regla de oro: nunca copiar y pegar más de dos veces. En cambio, podríamos usar un bucle _for_:
```{r}
output <- vector("double", ncol(df)) # 1. output
for (i in seq_along(df)) { # 2. secuencia
output[[i]] <- median(df[[i]]) # 3. cuerpo
}
output
```
Cada bucle tiene tres componentes:
1. __output__: `output <- vector("double", length(x))`.
Antes de comenzar el bucle, siempre debes asignar suficiente espacio
para la salida. Esto es muy importante para la eficiencia: si aumentas
el bucle _for_ en cada iteración usando, por ejemplo, `c ()` , el bucle _for_
será muy lento.
Una forma general de crear un vector vacío de longitud dada es la función `vector()`.
Tiene dos argumentos: el tipo de vector (_"logical"_,
_"integer"_, _"double"_, _"character"_, etc) y su longitud.
2. La __secuencia__: `i in seq_along (df)`. Este código determina sobre qué iterar:
cada ejecución del bucle _for_ asignará a `i` un valor diferente de
`seq_along (df)`. Es útil pensar en `i` como un pronombre, como "eso".
Es posible que no hayas visto `seq_along ()` con anterioridad. Es una versión segura de la más
familiar `1:length(l)`, con una diferencia importante: si se tiene un
vector de longitud cero, `seq_along ()` hace lo correcto:
```{r}
y <- vector("double", 0)
seq_along(y)
1:length(y)
```
Probablemente no vas a crear un vector de longitud cero deliberadamente, pero
es fácil crearlos accidentalmente. Si usamos `1: length (x)` en lugar
de `seq_along (x)`, es posible que obtengamos un mensaje de error confuso.
3. El __cuerpo__: `output[[i]] <- median(df[[i]])`. Este es el código que hace
el trabajo. Se ejecuta repetidamente, con un valor diferente para `i` cada vez.
La primera iteración ejecutará `output[[1]] <- median(df[[1]])`,
la segunda ejecutará `output [[2]] <- median (df [[2]])`, y así sucesivamente.
¡Eso es todo lo que hay para el bucle _for_! Ahora es un buen momento para practicar creando algunos bucles _for_ básicos (y no tan básicos) usando los ejercicios que se encuentran a continuación. Luego avanzaremos en algunas variaciones de este bucle que te ayudarán a resolver otros problemas que surgirán en la práctica.
### Ejercicios
1. Escribe bucles _for_ para:
1. Calcular la media de cada columna en `datos::mtautos`.
2. Determinar el tipo de cada columna en `datos::vuelos`.
3. Calcular el número de valores únicos en cada columna de `datos::flores`.
4. Generar diez normales aleatorias de distribuciones con medias -10, 0, 10 y 100.
Piensa en el resultado, la secuencia y el cuerpo __antes__ de empezar a escribir
el bucle.
2. Elimina el bucle _for_ en cada uno de los siguientes ejemplos aprovechando alguna función existente que trabaje con vectores:
```{r, eval = FALSE}
out <- ""
for (x in letters) {
out <- stringr::str_c(out, x)
}
x <- sample(100)
sd <- 0
for (i in seq_along(x)) {
sd <- sd + (x[i] - mean(x)) ^ 2
}
sd <- sqrt(sd / (length(x) - 1))
x <- runif(100)
out <- vector("numeric", length(x))
out[1] <- x[1]
for (i in 2:length(x)) {
out[i] <- out[i - 1] + x[i]
}
```
3. Combina tus habilidades para escribir funciones y bucles _for_:
1. Escribe un bucle _for_ que imprima (_`prints()`_) la letra de la canción de niños
"Cinco ranitas verdes" (u otra).
2. Convierte la canción infantil "Cinco monitos saltaban en la cama" en una función. Generalizar
a cualquier cantidad de monitos en cualquier estructura para dormir.
3. Convierte la canción "99 botellas de cerveza en la pared" en una función.
Generalizar a cualquier cantidad, de cualquier tipo de recipiente que contenga
cualquier líquido sobre cualquier superficie.
4. Es común ver bucles _for_ que no preasignan el output y en su lugar
aumentan la longitud de un vector en cada paso:
```{r, eval = FALSE}
output <- vector("integer", 0)
for (i in seq_along(x)) {
output <- c(output, lengths(x[[i]]))
}
output
```
¿Cómo afecta esto el rendimiento? Diseña y ejecuta un experimento.
## Variaciones de bucles _for_
Una vez que tienes el bucle _for_ básico en tu haber, hay algunas variaciones que debes tener en cuenta. Estas variaciones son importantes independientemente de cómo hagas la iteración, así que no te olvides de ellas una vez que hayas dominado las técnicas de programación funcional (PF) que aprenderás en la próxima sección.
Hay cuatro variaciones del bucle _for_ básico:
1. Modificar un objeto existente, en lugar de crear un nuevo objeto.
2. Iterar sobre nombres o valores, en lugar de índices.
3. Manejar outputs de longitud desconocida.
4. Manejar secuencias de longitud desconocida.
### Modificar un objeto existente
Algunas veces querrás usar un bucle _for_ para modificar un objeto existente. Por ejemplo, recuerda el desafío que teníamos en el capítulo sobre [funciones]. Queríamos reescalar cada columna en un _data frame_:
```{r}
df <- tibble(
a = rnorm(10),
b = rnorm(10),
c = rnorm(10),
d = rnorm(10)
)
rescale01 <- function(x) {
rng <- range(x, na.rm = TRUE)
(x - rng[1]) / (rng[2] - rng[1])
}
df$a <- rescale01(df$a)
df$b <- rescale01(df$b)
df$c <- rescale01(df$c)
df$d <- rescale01(df$d)
```
Para resolver esto con un bucle _for_, volvamos a pensar en los tres componentes:
1. __Output__: ya tenemos el _output_ --- ¡es lo mismo que la entrada!
2. __Secuencia__: podemos pensar en un _data frame_ como una lista de columnas, por lo
que podemos iterar sobre cada columna con `seq_along (df)`.
3. __Cuerpo__: aplicar `rescale01 ()`.
Esto nos da:
```{r}
for (i in seq_along(df)) {
df[[i]] <- rescale01(df[[i]])
}
```
Por lo general, se modificará una lista o un _data frame_ con este tipo de bucle, así que recuerda utilizar `[[` y no `[`. Te habrás fijado que usamos `[[` en todos nuestros bucles _for_: creemos que es mejor usar `[[` incluso para vectores atómicos porque deja en claro que queremos trabajar con un solo elemento.
### Patrones de bucle
Hay tres formas básicas de hacer un bucle sobre un vector. Hasta ahora hemos visto la más general: iterar sobre los índices numéricos con `for (i in seq_along (xs))`, y extraer el valor con `x [[i]]`. Hay otras dos formas:
1. Iterar sobre los elementos: `for (x in xs)`. Esta forma es la más útil si solo
te preocupas por los efectos secundarios, como graficar o grabar un archivo, porque es
difícil almacenar el output de forma eficiente.
2. Iterar sobre los nombres: `for (nm in names(xs))`. Esto te entrega el nombre, que se puede usar para acceder al valor con `x [[nm]]`.
Esto es útil si queremos utilizar el nombre en el título de un gráfico o
en el nombre de un archivo. Si estás creando un output con nombre, asegúrate de nombrar el vector de resultados de esta manera:
```{r, eval = FALSE}
resultados <- vector("list", length(x))
names(resultados) <- names(x)
```
3. Iterar sobre los índices numéricos es la forma más general, porque dada la posición se puede extraer tanto el nombre como el valor:
```{r, eval = FALSE}
for (i in seq_along(x)) {
name <- names(x)[[i]]
value <- x[[i]]
}
```
### Longitud de _output_ desconocida
Es posible que algunas veces no sepas el tamaño que tendrá el output. Por ejemplo, imagina que quieres simular algunos vectores aleatorios de longitudes aleatorias. Podrías tener la tentación de resolver este problema haciendo crecer el vector progresivamente:
```{r}
medias <- c(0, 1, 2)
output <- double()
for (i in seq_along(medias)) {
n <- sample(100, 1)
output <- c(output, rnorm(n, medias[[i]]))
}
str(output)
```
Pero esto no es muy eficiente porque en cada iteración, R tiene que copiar todos los datos de las iteraciones anteriores. En términos técnicos, obtienes un comportamiento "cuadrático" ($O(n^2)$), lo que significa que un bucle que tiene tres veces más elementos tomaría nueve ($3^2$) veces más tiempo en ejecutarse.
Una mejor solución es guardar los resultados en una lista y luego combinarlos en un solo vector una vez que se complete el ciclo:
```{r}
out <- vector("list", length(medias))
for (i in seq_along(medias)) {
n <- sample(100, 1)
out[[i]] <- rnorm(n, medias[[i]])
}
str(out)
str(unlist(out))
```
Aquí usamos `unlist ()` (_deslistar_ en inglés) para aplanar una lista de vectores en un solo vector. Una opción más estricta es usar `purrr :: flatten_dbl ()` (_aplanar dobles_) --- arrojará un error si el input no es una lista de dobles.
Este patrón ocurre también en otros lugares:
1. Podrías estar generando una cadena larga. En lugar de pegar (`paste ()`) cada iteración con la anterior, guarda el output en un vector de caracteres y
luego combina ese vector en una cadena con
`paste(output, collapse = "")`.
2. Podrías estar generando un _data frame_ grande. En lugar de enlazar (`rbind ()`) secuencialmente
en cada iteración, guarda el resultado en una lista y luego utiliza
`dplyr::bind_rows(output)` para combinar el output en un solo _data frame_.
Cuidado con este patrón. Cuando lo veas, cambia a un objeto de resultado más complejo y luego combínalo en un solo paso al final.
### Longitud de secuencia desconocida
A veces ni siquiera sabemos cuánto tiene que durar la secuencia de entrada. Esto es común cuando se hacen simulaciones. Por ejemplo, es posible que se quiera realizar un bucle hasta que se obtengan tres caras seguidas. No podemos hacer ese tipo de iteración con un bucle _for_. En su lugar, podemos utilizar un bucle _while_ (_mientras_, en inglés). Un bucle _while_ es más simple que un bucle _for_ porque solo tiene dos componentes, una condición y un cuerpo:
```{r, eval = FALSE}
while (condición) {
# cuerpo
}
```
Un bucle _while_ también es más general que un bucle _for_, porque podemos reescribir este último como un bucle _while_, pero no podemos reescribir todos los bucles _while_ bucles _for_:
```{r, eval = FALSE}
for (i in seq_along(x)) {
# cuerpo
}
# Equivalente a
i <- 1
while (i <= length(x)) {
# cuerpo
i <- i + 1
}
```
Así es como podríamos usar un bucle _while_ para encontrar cuántos intentos se necesitan para obtener tres caras seguidas:
```{r}
lanzamiento <- function() sample(c("S", "C"), 1)
lanzamientos <- 0
ncaras <- 0
while (ncaras < 3) {
if (lanzamiento() == "C") {
ncaras <- ncaras + 1
} else {
ncaras <- 0
}
lanzamientos <- lanzamientos + 1
}
lanzamientos
```
Mencionamos los bucles _while_ brevemente, porque casi nunca los usamos. Se utilizan con mayor frecuencia para hacer simulaciones, un tema que está fuera del alcance de este libro. Sin embargo, es bueno saber que existen en caso que nos encontremos con problemas en los que el número de iteraciones no se conoce de antemano.
### Ejercicios
1. Imagina que tienes un directorio lleno de archivos CSV que quieres importar.
Tienes sus ubicaciones en un vector,
`files <- dir("data/", pattern = "\\.csv$", full.names = TRUE)`, y ahora
quieres leer cada uno con `read_csv()`. Escribe un bucle _for_ que los
cargue en un solo _data frame_.
1. ¿Qué pasa si utilizamos `for (nm in names(x))` y `x` no tiene nombres (_names_)?
¿Qué pasa si solo algunos elementos están nombrados?
¿Qué pasa si los nombres no son únicos?
1. Escribe una función que imprima el promedio de cada columna numérica en un
_data frame_, junto con su nombre. Por ejemplo, `mostrar_promedio(flores)` debe imprimir:
```{r, eval = FALSE}
mostrar_promedio(flores)
#> Largo.Sepalo: 5.84
#> Ancho.Sepalo: 3.06
#> Largo.Petalo: 3.76
#> Ancho.Petalo: 1.20
```
(Desafío adicional: ¿qué función utilizamos para asegurarnos que los números
queden alineados a pesar que los nombres de las variables tienen diferentes longitudes?)
1. ¿Qué hace este código? ¿Cómo funciona?
```{r, eval = FALSE}
trans <- list(
cilindrada = function(x) x * 0.0163871,
transmision = function(x) {
factor(x, labels = c("automática", "manual"))
}
)
for (var in names(trans)) {
mtautos[[var]] <- trans[[var]](mtautos[[var]])
}
```
## Bucles _for_ vs. funcionales
Los bucles _for_ no son tan importantes en R como en otros lenguajes porque R es un lenguaje de programación funcional. Esto significa que es posible envolver los bucles en una función y llamar a esa función en lugar de usar el bucle _for_ directamente.
Para ver por qué esto es importante, consideremos (nuevamente) este _data frame_ simple:
```{r}
df <- tibble(
a = rnorm(10),
b = rnorm(10),
c = rnorm(10),
d = rnorm(10)
)
```
Imagina que quieres calcular la media de cada columna. Podríamos hacer eso con un bucle _for_:
```{r}
output <- vector("double", length(df))
for (i in seq_along(df)) {
output[[i]] <- mean(df[[i]])
}
output
```
Como te das cuenta que vas querer calcular los promedios de cada columna con bastante frecuencia, extraer el bucle en una función:
```{r}
col_media <- function(df) {
output <- vector("double", length(df))
for (i in seq_along(df)) {
output[i] <- mean(df[[i]])
}
output
}
```
Pero entonces pensamos que también sería útil poder calcular la mediana y la desviación estándar, así que copiamos y pegamos la función `col_media ()` y reemplazamos `mean ()` con `median ()` y `sd ()`:
```{r}
col_mediana <- function(df) {
output <- vector("double", length(df))
for (i in seq_along(df)) {
output[i] <- median(df[[i]])
}
output
}
col_desvest <- function(df) {
output <- vector("double", length(df))
for (i in seq_along(df)) {
output[i] <- sd(df[[i]])
}
output
}
```
¡Oh oh! Copiaste y pegaste este código dos veces, por lo que es hora de pensar cómo generalizarlo. Ten en cuenta que la mayoría de este código corresponde al texto trillado del bucle _for_, lo que hace difícil ver la única cosa que es diferente entre las funciones (`mean ()`, `median ()`, `sd ()`).
¿Qué podŕias hacer si ves un conjunto de funciones como esta?:
```{r}
f1 <- function(x) abs(x - mean(x)) ^ 1
f2 <- function(x) abs(x - mean(x)) ^ 2
f3 <- function(x) abs(x - mean(x)) ^ 3
```
Por suerte, habrás notado que hay mucha duplicación que puedes extraer con un argumento adicional:
```{r}
f <- function(x, i) abs(x - mean(x)) ^ i
```
Redujiste la posibilidad de errores (porque ahora tienes 1/3 menos de código) y hiciste más fácil generalizar para situaciones nuevas.
Podemos hacer exactamente lo mismo con `col_media ()`, `col_mediana ()` y `col_desvest ()` agregando un argumento que proporciona la función a aplicar en cada columna:
```{r}
col_resumen <- function(df, fun) {
out <- vector("double", length(df))
for (i in seq_along(df)) {
out[i] <- fun(df[[i]])
}
out
}
col_resumen(df, median)
col_resumen(df, mean)
```
La idea de pasar una función a otra es extremadamente poderosa y es uno de los comportamientos que hacen de R un lenguaje de programación funcional. Puede que te tome un tiempo comprender la idea, pero vale la pena el esfuerzo. En el resto del capítulo aprenderás y usarás el paquete __purrr__, que proporciona funciones que eliminan la necesidad de muchos de los bucles comunes. La familia de funciones de R base _apply_ (_aplicar_: `apply ()`, `lapply ()`, `tapply ()`, etc.) resuelve un problema similar; sin embargo, _purrr_ es más consistente y, por lo tanto, es más fácil de aprender.
El objetivo de usar las funciones de _purrr_ en lugar de los bucles es que te permite dividir los desafíos comunes de manipulación de listas en partes independientes:
1. ¿Cómo puedes resolver el problema para un solo elemento de la lista?
Una vez que encuentres la solución, _purrr_ se encargará de generalizarla
a cada elemento de la lista.
2. Si estás resolviendo un problema complejo, ¿cómo puedes dividirlo
en pequeñas etapas que te permitan avanzar paso a paso hacia la solución?
Con __purrr__ obtienes muchas piezas pequeñas que puedes ensamblar utilizando el _pipe_ (`%>%`).
Esta estructura facilita la resolución de nuevos problemas. También hace que sea más fácil entender las soluciones a problemas antiguos cuando relees código que escribiste en el pasado.
### Ejercicios
1. Lee la documentación para `apply ()`. En el caso 2d, ¿qué dos bucles _for_ generaliza?
2. Adapta `col_resumen()` para que solo se aplique a las columnas numéricas.
Podrías querer comenzar con la función `is_numeric ()` que devuelve un vector lógico que tenga un _TRUE_ por cada columna numérica.
## Las funciones _map_
El patrón de iterar sobre un vector, hacer algo con cada elemento y guardar los resultados es tan común que el paquete __purrr__ proporciona una familia de funciones que lo hacen por ti. Hay una función para cada tipo de output:
* `map ()` crea una lista.
* `map_lgl ()` crea un vector lógico.
* `map_int ()` crea un vector de enteros.
* `map_dbl ()` crea un vector de dobles.
* `map_chr ()` crea un vector de caracteres.
Cada función `map` (_mapa_, en español) toma un vector como input, aplica una función a cada elemento y luego devuelve un nuevo vector que tiene la misma longitud (y los mismos nombres) que el input. El tipo de vector está determinado por el sufijo de la función _map_.
Una vez que domines estas funciones, descubrirás que lleva mucho menos tiempo resolver los problemas de iteración. Sin embargo, nunca debes sentirse mal por usar un bucle _for_ en lugar de una función _map_. Las funciones _map_ son un nivel superior de abstracción y puede llevar mucho tiempo entender cómo funcionan. Lo importante es que resuelvas el problema en el que estás trabajando, no que escribas el código más conciso y elegante (¡aunque eso es definitivamente algo a lo que aspirar!).
Algunas personas te dirán que evites los bucles _for_ porque son lentos. ¡Están equivocados! (Bueno, al menos están bastante desactualizados, ya que los bucles _for_ han dejado de ser lentos desde hace muchos años). Los principales beneficios de usar funciones como `map ()` no es la velocidad, sino la claridad: hacen que tu código sea más fácil de escribir y leer.
Podemos usar estas funciones para realizar los mismos cálculos que el último bucle _for_. Esas funciones de resumen devolvían valores decimales, por lo que necesitamos usar `map_dbl ()`:
```{r}
map_dbl(df, mean)
map_dbl(df, median)
map_dbl(df, sd)
```
Comparado con el uso de un bucle for, el foco está en la operación que se está realizando (es decir, `mean ()`, `median ()`, `sd ()`), y no en llevar la cuenta de las acciones requeridas para recorrer cada elemento y almacenar el output. Esto es aún más evidente si usamos el _pipe_:
```{r}
df %>% map_dbl(mean)
df %>% map_dbl(median)
df %>% map_dbl(sd)
```
Existen algunas diferencias entre `map _ * ()` y `col_resumen()`:
* Todas las funciones de purrr están implementadas en C. Esto las hace más
rápidas a expensas de la legibilidad.
* El segundo argumento, `.f`, la función a aplicar, puede ser una fórmula,
un vector de caracteres o un vector de enteros. Aprenderás acerca de estos
atajos útiles en la siguiente sección.
* `map_*()` usa ... ([dot dot dot] - punto punto punto) para pasar los argumentos adicionales a `.f`
cada vez que se llama:
```{r}
map_dbl(df, mean, trim = 0.5)
```
* Las funciones _map_ también preservan los nombres:
```{r}
z <- list(x = 1:3, y = 4:5)
map_int(z, length)
```
### Atajos
Existen algunos atajos que puedes usar con `.f` para ahorrar algo de tipeo Imagina que quieres ajustar un modelo lineal a cada grupo en un conjunto de datos. El siguiente ejemplo de juguete divide el dataset `mtautos` en tres partes (una para cada valor de cilindro) y ajusta el mismo modelo lineal a cada parte:
```{r}
modelos <- mtautos %>%
split(.$cilindros) %>%
map(function(df) lm(millas ~ peso, data = df))
```
La sintaxis para crear una función anónima en R es bastante verbosa, por lo que purrr provee un atajo conveniente: una fórmula unilateral.
```{r}
modelos <- mtautos %>%
split(.$cilindros) %>%
map(~lm(millas ~ peso, data = .x))
```
Hemos usado `.` como pronombre: se refiere al elemento actual de la lista (del mismo modo que `i` se refiere al índice actual en el loop for). `.x` corresponde al argumento de una función anónima.
Cuando examinas múltiples modelos, puedes querer extraer un estadístico resumen como lo es $R^2$. Para hacer eso primero necesitas correr `summary()` y luego extraer la componente `r.squared` (R-cuadrado). Podríamos hacerlo usando un atajo para las funciones anónimas:
```{r}
modelos %>%
map(summary) %>%
map_dbl(~.x$r.squared)
```
Sin embargo, extraer componentes con nombres es una operación común, por lo que purrr provee un atajo aún más corto: puedes usar una cadena de caracteres (o _string_).
```{r}
modelos %>%
map(summary) %>%
map_dbl("r.squared")
```
También puedes usar un entero para seleccionar elementos de acuerdo a su posición:
```{r}
x <- list(list(1, 2, 3), list(4, 5, 6), list(7, 8, 9))
x %>% map_dbl(2)
```
### R Base
Si la familia de funciones _apply_ en R base te son familiares, podrás haber notado algunas similitudes con las funciones de purrr:
* `lapply()` es básicamente idéntica a `map()`, excepto que `map()` es consistente
con todas las otras funciones de purrr y puedes usar atajos para `.f`.
* `sapply()` es un envoltorio (_wrapper_) de `lapply()` que automáticamente
simplifica el output. Esto es úti lpara el trabajo interactivo pero es
problemático en una función, ya que nunca sabrás qué tipo de output
vas a obtener:
```{r}
x1 <- list(
c(0.27, 0.37, 0.57, 0.91, 0.20),
c(0.90, 0.94, 0.66, 0.63, 0.06),
c(0.21, 0.18, 0.69, 0.38, 0.77)
)
x2 <- list(
c(0.50, 0.72, 0.99, 0.38, 0.78),
c(0.93, 0.21, 0.65, 0.13, 0.27),
c(0.39, 0.01, 0.38, 0.87, 0.34)
)
umbral <- function(x, cutoff = 0.8) x[x > cutoff]
x1 %>% sapply(umbral) %>% str()
x2 %>% sapply(umbral) %>% str()
```
* `vapply()` es una alternativa más segura a `sapply()` porque debes
ingresar un argumento adicional que define el tipo de output. El único problema con
`vapply()` es es que requiere mucha escritura:
`vapply(df, is.numeric, logical(1))` es equivalente a `map_lgl(df, is.numeric)`.
Una ventaja de `vapply()` sobre las funciones _map_ de purrr es que también
puede generar matrices --- las funciones _map_ solo generan vectores.
Aquí nos enfocamos en las funciones de purrr, ya que proveen nombres y argumentos consistentes, atajos útiles y en el futuro proveerán paralelización simple y barras de progreso.
### Ejercicios
1. Escribe un código que use una de las funciones de map para:
1. Calcular la media de cada columna en `datos::mautos`.
1. Obtener de qué tipo es cada columna en `datos::vuelos`.
1. Calcular la cantidad de valores únicos en cada columna de `datos::flores`.
1. Generar diez normales aleatorias de distribuciones con medias -10, 0, 10 y 100.
1. ¿Cómo puedes crear un vector tal que para cada columna en un data frame indique si
corresponde o no a un factor?
1. ¿Qué ocurre si usas las funciones _map_ en vectores que no son listas?
¿Qué hace `map(1:5, runif)`? ¿Por qué?
1. ¿Qué hace `map(-2:2, rnorm, n = 5)`? ¿Por qué?
¿Qué hace `map_dbl(-2:2, rnorm, n = 5)`? ¿Por qué?
1. Reescribe `map(x, function(df) lm(mpg ~ wt, data = df))` para eliminar
todas las funciones anónimas.
## Manejando los errores
Cuando usas las funciones _map_ para repetir muchas operaciones, la probabilidad de que una de estas falle es mucho más alta. Cuando esto ocurre, obtendrás un mensaje de error y no una salida. Esto es molesto: ¿por qué un error evita que accedas a todo lo que sí funcionó? ¿Cómo puedes asegurarte de que una manzana podrida no arruine todo el barril?
En esta sección aprenderás a manejar estas situaciones con una nueva función: `safely()` (_de forma segura_, en inglés). `safely()` es un adverbio: toma una función (un verbo) y entrega una versión modificada. En este caso, la función modificada nunca lanzará un error. En cambio, siempre devolverá una lista de dos elementos:
1. `result` es el resultado original. Si hubo un error, aparecerá como `NULL`,
1. `error` es un objeto de error. Si la operación fue exitosa, será `NULL`.
(Puede que estés familiarizado con la función `try()` (_intentar_) de R base. Es similar, pero dado que a veces entrega el resultado original y a veces un objeto de error, es más díficil para trabajar.)
Veamos esto con un ejemplo simple: `log()`:
```{r}
log_seguro <- safely(log)
str(log_seguro(10))
str(log_seguro("a"))
```
Cuando la función es exitosa, el elemento `result` contiene el resultado y el elemento `error` es `NULL`. Cuando la función falla, el elemento `result` es `NULL` y el elemento `error` contiene un objeto de error.
`safely()` está diseñada para funcionar con _map_:
```{r}
x <- list(1, 10, "a")
y <- x %>% map(safely(log))
str(y)
```
Esto sería más fácil de trabajar si tuvieramos dos listas: una con todos los errores y otra con todas las salidas, Esto es fácil de obtener con `purrr::transpose()` (_transponer_):
```{r}
y <- y %>% transpose()
str(y)
```
Queda a tu criterio cómo manejar los errores, pero típicamente puedes mirar los valores de `x` donde `y` es un error, o trabajar con los valores que `y` que están ok:
```{r}
estan_ok <- y$error %>% map_lgl(is_null)
x[!estan_ok]
y$result[estan_ok] %>% flatten_dbl()
```
Purrr provee otros dos adverbios útiles:
* Al igual que `safely()`, `possibly()` (_posiblemente_) siempre tendrá éxito. Es más simple que `safely()`,
ya que le das un valor por defecto para devolver cuando haya un error.
```{r}
x <- list(1, 10, "a")
x %>% map_dbl(possibly(log, NA_real_))
```
* `quietly()` (_silenciosamente_) tiene un rol similar a `safely()`, pero en lugar de capturar
los errores, captura el output impreso, los mensajes y las advertencias:
```{r}
x <- list(1, -1)
x %>% map(quietly(log)) %>% str()
```
## Usar _map_ sobre múltiples argumentos
Hasta ahora hemos "mapeado" sobre un único input. Pero a menudo tienes multiples inputs relacionados y necesitas iterar sobre ellos en paralelo. Ese es el trabajo de las funciones `map2()` y `pmap()`. Por ejemplo, imagina que quieres simular normales aleatorias con distintas medias. Ya sabes hacerlo con `map()`:
```{r}
mu <- list(5, 10, -3)
mu %>%
map(rnorm, n = 5) %>%
str()
```
¿Qué ocurre si también necesitas cambiar la desviación estándar? Una forma de hacerlo sería iterar sobre los índices e indexar en vectores de medias y desviaciones estándar:
```{r}
sigma <- list(1, 5, 10)
seq_along(mu) %>%
map(~rnorm(5, mu[[.x]], sigma[[.x]])) %>%
str()
```
Pero esto oscurece la intención del código. En su lugar podríamos usar `map2()`, que itera sobre dos vectores en paralelo:
```{r}
map2(mu, sigma, rnorm, n = 5) %>% str()
```
`map2()` genera esta serie de llamadas a funciones:
```{r, echo = FALSE}
knitr::include_graphics("diagrams_w_text_as_path/es/lists-map2.svg")
```
Observa que los argumentos que varían para cada llamada van _antes_ de la función; argumentos que son los mismos para cada llamada van _después_.
Al igual que `map()`, `map2()` es un envoltorio en torno a un bucle _for_:
```{r}
map2 <- function(x, y, f, ...) {
out <- vector("list", length(x))
for (i in seq_along(x)) {
out[[i]] <- f(x[[i]], y[[i]], ...)
}
out
}
```
También te podrás imaginar `map3()`, `map4()`, `map5()`, `map6()`, etc., pero eso se volvería tedioso rápidamente. En cambio, purrr provee `pmap()`, que toma una lista de argumentos. Puedes usar eso si quieres cambiar la media, desviación estándar y el número de muestras:
```{r}
n <- list(1, 3, 5)
args1 <- list(n, mu, sigma)
args1 %>%
pmap(rnorm) %>%
str()
```
Esto se ve así:
```{r, echo = FALSE}
knitr::include_graphics("diagrams_w_text_as_path/es/lists-pmap-unnamed.svg")
```
Si no nombras todos los elementos de la lista, `pmap()` usará una coincidencia posicional al llamar la función. Esto es un poco frágil y hace el código más difícil de leer, por lo que es mejor nombrar los argumentos:
```{r, eval = FALSE}
args2 <- list(mean = mu, sd = sigma, n = n)
args2 %>%
pmap(rnorm) %>%
str()
```
Esto genera llamadas más largas, pero más seguras:
```{r, echo = FALSE}
knitr::include_graphics("diagrams_w_text_as_path/es/lists-pmap-named.svg")
```
Dado que los argumentos son todos del mismo largo, tiene sentido almacenarlos en un data frame:
```{r}
params <- tribble(
~mean, ~sd, ~n,
5, 1, 1,
10, 5, 3,
-3, 10, 5
)
params %>%
pmap(rnorm)
```
Utilizar un data frame cuando tu código se vuelve complicado nos parece una buena aproximación, ya que asegura que cada columna tenga nombre y el mismo largo que las demás columnas.
### Invocando distintas funciones
Existe un paso adicional en términos de complejidad. Así como cambias los argumentos de la función también puedes cambiar la función misma:
```{r}
f <- c("runif", "rnorm", "rpois")
param <- list(
list(min = -1, max = 1),
list(sd = 5),
list(lambda = 10)
)
```
Para manejar este caso, puedes usar `invoke_map()`:
```{r}
invoke_map(f, param, n = 5) %>% str()
```
```{r, echo = FALSE, out.width = NULL}
knitr::include_graphics("diagrams_w_text_as_path/es/lists-invoke.svg")
```
El primer argumento es una lista de funciones o un vector de caracteres con nombres de funciones. El segundo argumento es una lista de listas que indica los argumentos que cambian en cada función. Los argumentos subsecuentes pasan a cada función.
Nuevamente, puedes usar `tribble()` para hacer la creación de tuplas coincidentes un poco más fácil:
```{r, eval = FALSE}
sim <- tribble(
~f, ~params,
"runif", list(min = -1, max = 1),
"rnorm", list(sd = 5),
"rpois", list(lambda = 10)
)
sim %>%
mutate(sim = invoke_map(f, params, n = 10))
```
## Walk {#walk}
_Walk_ es una alternativa a _map_ que puedes usar cuando quieras llamar a una función por sus efectos colaterales, más que por sobre el valor que devuelve. Típicamente hacemos esto cuando queremos mostrar la salida en pantalla o guardar archivos en el disco. Lo importante es la acción, no el valor resultante. Aquí hay un ejemplo simple:
```{r}
x <- list(1, "a", 3)
x %>%
walk(print)
```
Generalmente `walk()` no es tan útil si se compara con `walk2()` o `pwalk()`. Por ejemplo, si tienes una lista de gráficos y un vector con nombres de archivos, puedes usar `pwalk()` para guardar cada archivo en su ubicación correspondiente en el disco:
```{r, eval = FALSE}
library(ggplot2)
plots <- mtcars %>%
split(.$cyl) %>%
map(~ggplot(.x, aes(mpg, wt)) + geom_point())
paths <- stringr::str_c(names(plots), ".pdf")
pwalk(list(paths, plots), ggsave, path = tempdir())
```
`walk()`, `walk2()` y `pwalk()` devuelven de forma invisible `.`, el primer argumento. Esto las hace adecuadas para usar dentro de cadenas de pipes.
## Otros patrones para los bucles _for_
Purr entrega algunas funciones que resumen otros tipos de bucles _for_. Si bien los usarás de manera menos frecuente que las funciones map, es útil conocerlas. El objetivo aquí es ilustrar brevemente cada una, con la esperanza de que vengan a tu mente en el futuro cuando veas un problema similar. Luego puedes consultar la documentación para más detalles.
### Funciones predicativas
Algunas funciones trabajan con funciones _predicativas_ que entregan un único valor `TRUE` o `FALSE`.
`keep()` y `discard()` mantienen los elementos de la entrada donde el predicado es `TRUE` o `FALSE`, respectivamente:
```{r}
flores %>%
keep(is.factor) %>%
str()
flores %>%
discard(is.factor) %>%
str()
```
`some()` y `every()` determinan si el predicado es verdadero para todos o para algunos de los elementos.
```{r}
x <- list(1:5, letters, list(10))
x %>%
some(is_character)
x %>%
every(is_vector)
```
`detect()` encuentra el primer elemento donde el predicado es verdadero; `detect_index()` entrega su posición.
```{r}
x <- sample(10)
x
x %>%
detect(~ . > 5)
x %>%
detect_index(~ . > 5)
```
`head_while()` y `tail_while()` toman elementos al inicio y final de un vector cuando el predicado es verdadero:
```{r}
x %>%
head_while(~ .x > 5)
x %>%
tail_while(~ .x > 5)
```
### Reducir y acumular
A veces tendrás una lista compleja que quieres reducir a una lista simple aplicando repetidamente una función que reduce un par a un elemento único. Esto es útil si quieres aplicar un verbo de dos tablas de dplyr a múltiples tablas. Por ejemplo, si tienes una lista de data frames, y quieres reducirla a un unico data frame uniendo los elementos:
```{r}
dfs <- list(
age = tibble(name = "John", age = 30),
sex = tibble(name = c("John", "Mary"), sex = c("M", "F")),
trt = tibble(name = "Mary", treatment = "A")
)
dfs %>% reduce(full_join)
```
O puedes tener una lista de vectores y quieres encontrar la intersección:
```{r}
vs <- list(
c(1, 3, 5, 6, 10),
c(1, 2, 3, 7, 8, 10),
c(1, 2, 3, 4, 8, 9, 10)
)
vs %>% reduce(intersect)
```
La función `reduce()` (_reducir_) toma una función "binaria" (e.g. una función con dos inputs primarios) y la aplica repetidamente a una lista hasta que quede un solo elemento.
`accumulate()` (_acumular_) es similar, pero mantiene todos los resultados intermedios. Podría usarse para implementar una suma acumulativa:
```{r}
x <- sample(10)
x
x %>% accumulate(`+`)
```
### Ejercicios
1. Implementa tu propia versión de `every()` usando un bucle _for_. Compárala con
`purrr::every()`. ¿Qué hace la versión de purrr que la tuya no?
1. Crea una mejora de `col_resumen()` que aplique una función de resumen a cada
columna numérica en un data frame.
1. Un posible equivalente de `col_resumen()` es:
```{r}
col_resumen3 <- function(df, f) {
is_num <- sapply(df, is.numeric)
df_num <- df[, is_num]
sapply(df_num, f)
}
```
Pero tiene una cantidad de bugs que queda ilustrada con las siguientes entradas:
```{r, eval = FALSE}
df <- tibble(
x = 1:3,
y = 3:1,
z = c("a", "b", "c")
)
# OK
col_resumen3(df, mean)
# Tiene problemas: no siempre devuelve un vector numérico
col_resumen3(df[1:2], mean)
col_resumen3(df[1], mean)
col_resumen3(df[0], mean)
```
¿Qué causa los _bugs_?