-
Notifications
You must be signed in to change notification settings - Fork 1
/
Functionals.qmd
1073 lines (727 loc) · 47.9 KB
/
Functionals.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
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Funcionales {#sec-functionals}
```{r, include = FALSE}
source("common.R")
source("emoji.R")
cols <- function(x, n = 4) {
pal <- scales::gradient_n_pal(c("white", x))
col <- pal(seq(0, 1, length = n + 1)[-1])
scales::show_col(col)
col
}
print.rlang_lambda_function <- function(x, ...) {
print(unclass(x))
}
cols("#FF9300")
cols("#8DD3FB")
```
## Introducción
\index{functionals}
> Para volverse significativamente más confiable, el código debe volverse más transparente. En particular, las condiciones anidadas y los bucles deben verse con gran sospecha. Los flujos de control complicados confunden a los programadores. El código desordenado a menudo esconde errores.
>
> --- Bjarne Stroustrup
Un **funcional** es una función que toma una función como entrada y devuelve un vector como salida. Aquí hay un funcional simple: llama a la función proporcionada como entrada con 1000 números uniformes aleatorios.
```{r}
randomise <- function(f) f(runif(1e3))
randomise(mean)
randomise(mean)
randomise(sum)
```
Lo más probable es que ya hayas usado un funcional. Es posible que haya utilizado reemplazos de bucle for como `lapply()`, `apply()` y `tapply()` de base R; o `map()` de purrr; o quizás hayas usado un funcional matemático como `integrate()` u `optim()`.
\index{loops!replacing} Un uso común de los funcionales es como una alternativa a los bucles for. Los bucles for tienen mala reputación en R porque mucha gente cree que son lentos\[\^funcionals-1\], pero la verdadera desventaja de los bucles for es que son muy flexibles: un bucle transmite que estás iterando, pero no lo que deberías terminar con los resultados. Así como es mejor usar `while` que `repeat`, y es mejor usar `for` que `while` (@sec-for-family), es mejor usar un funcional que `for`. Cada funcional está diseñado para una tarea específica, por lo que cuando reconoce el funcional, inmediatamente sabe por qué se está utilizando.
Si es un usuario experimentado de bucles for, cambiar a funcionales suele ser un ejercicio de coincidencia de patrones. Miras el bucle for y encuentras un funcional que coincida con la forma básica. Si no existe uno, no intente torturar un funcional existente para que se ajuste a la forma que necesita. ¡En su lugar, déjalo como un bucle for! (O una vez que haya repetido el mismo bucle dos o más veces, tal vez piense en escribir su propio funcional).
### Outline {.unnumbered}
- La @sec-map introduce tu primer funcional: `purrr :: map ()`.
- La @sec-purrr-style demuestra cómo puede combinar múltiples funciones simples para resolver un problema más complejo y analiza cómo el estilo purrr difiere de otros enfoques.
- La @sec-map-variants te enseña alrededor de 18 (!!) variantes importantes de `purrr::map()`. Afortunadamente, su diseño ortogonal los hace fáciles de aprender, recordar y dominar.
- La @sec-reduce introduce un nuevo estilo de funcional: `purrr::reduce()`. `reduce()` reduce sistemáticamente un vector a un solo resultado mediante la aplicación de una función que toma dos entradas.
- La @sec-predicate-functionals te enseña acerca de los predicados: funciones que devuelven un solo `TRUE` o `FALSE`, y la familia de funciones que los usan para resolver problemas comunes.
- La @sec-base-functionals revisa algunos funcionales en base R que no son miembros de las familias map, reduce o predicate.
### Requsisitos previos {.unnumbered}
Este capítulo se centrará en las funciones proporcionadas por el [paquete purrr](https://purrr.tidyverse.org) [@purrr]. Estas funciones tienen una interfaz consistente que facilita la comprensión de las ideas clave que sus equivalentes básicos, que han crecido orgánicamente durante muchos años. Compararé y contrastaré las funciones básicas de R a medida que avanzamos, y luego terminaré el capítulo con una discusión de las funciones básicas que no tienen equivalentes purrr.
```{r setup}
library(purrr)
```
## My first functional: `map()` {#sec-map}
\index{map()}
El funcional más fundamental es `purrr::map()`\[\^funcionals-2\]. Toma un vector y una función, llama a la función una vez por cada elemento del vector y devuelve los resultados en una lista. En otras palabras, `map(1:3, f)` es equivalente a `list(f(1), f(2), f(3))`.
```{r}
triple <- function(x) x * 3
map(1:3, triple)
```
O, gráficamente:
```{r, echo = FALSE, out.width = NULL}
knitr::include_graphics("diagrams/functionals/map.png")
```
::: sidebarinplace
Quizás se pregunte por qué esta función se llama `map()`. ¿Qué tiene que ver con representar las características físicas de la tierra o el mar `r emoji ("world_map")`? De hecho, el significado proviene de las matemáticas donde *mapa* se refiere a "una operación que asocia cada elemento de un conjunto dado con uno o más elementos de un segundo conjunto". Esto tiene sentido aquí porque `map()` define un mapeo de un vector a otro. (*"Map"* también tiene la agradable propiedad de ser corto, lo cual es útil para un bloque de construcción tan fundamental).
:::
La implementación de `map()` es bastante simple. Asignamos una lista de la misma longitud que la entrada y luego completamos la lista con un bucle for. El corazón de la implementación es solo un puñado de líneas de código:
```{r}
simple_map <- function(x, f, ...) {
out <- vector("list", length(x))
for (i in seq_along(x)) {
out[[i]] <- f(x[[i]], ...)
}
out
}
```
La verdadera función `purrr::map()` tiene algunas diferencias: está escrita en C para aprovechar hasta el último ápice de rendimiento, conserva los nombres y admite algunos atajos que aprenderá en la @sec-purrr-shortcuts.
::: base
\index{lapply()}
El equivalente básico de `map()` es `lapply()`. La única diferencia es que `lapply()` no es compatible con los asistentes que aprenderá a continuación, por lo que si solo está usando `map()` de purrr, puede omitir la dependencia adicional y usar `lapply()` directamente.
:::
### Producción de vectores atómicos {#sec-map-atomic}
`map()` devuelve una lista, lo que la convierte en la más general de la familia de mapas porque puedes poner cualquier cosa en una lista. Pero es un inconveniente devolver una lista cuando lo haría una estructura de datos más simple, por lo que hay cuatro variantes más específicas: `map_lgl()`, `map_int()`, `map_dbl()` y `map_chr()`. Cada uno devuelve un vector atómico del tipo especificado:
```{r}
# map_chr() siempre devuelve un vector de caracteres
map_chr(mtcars, typeof)
# map_lgl() siempre devuelve un vector de valore boleanos
map_lgl(mtcars, is.double)
# map_int() siempre devuelve un vector de números enteros
n_unique <- function(x) length(unique(x))
map_int(mtcars, n_unique)
# map_dbl() siempre devuelve un vector doble
map_dbl(mtcars, mean)
```
purrr usa la convención de que los sufijos, como `_dbl()`, se refieren a la salida. Todas las funciones `map_*()` pueden tomar cualquier tipo de vector como entrada. Estos ejemplos se basan en dos hechos: `mtcars` es un data frame y los data frames son listas que contienen vectores de la misma longitud. Esto es más obvio si dibujamos un data frame con la misma orientación que el vector:
```{r, echo = FALSE, out.width = NULL}
knitr::include_graphics("diagrams/functionals/map-list.png")
```
Todas las funciones de mapa siempre devuelven un vector de salida de la misma longitud que la entrada, lo que implica que cada llamada a `.f` debe devolver un solo valor. Si no es así, obtendrá un error:
```{r, error = TRUE}
pair <- function(x) c(x, x)
map_dbl(1:2, pair)
```
Esto es similar al error que obtendrá si `.f` devuelve el tipo de resultado incorrecto:
```{r, error = TRUE}
map_dbl(1:2, as.character)
```
En cualquier caso, a menudo es útil volver a `map()`, porque `map()` puede aceptar cualquier tipo de salida. Eso le permite ver la salida problemática y averiguar qué hacer con ella.
```{r}
map(1:2, pair)
map(1:2, as.character)
```
::: base
```{=tex}
\index{sapply()}
\index{vapply()}
```
Base R tiene dos funciones de aplicación que pueden devolver vectores atómicos: `sapply()` y `vapply()`. Te recomiendo que evites `sapply()` porque intenta simplificar el resultado, por lo que puede devolver una lista, un vector o una matriz. Esto dificulta la programación y debe evitarse en entornos no interactivos. `vapply()` es más seguro porque le permite proporcionar una plantilla, `FUN.VALUE`, que describe la forma de salida. Si no quieres usar purrr, te recomiendo que siempre uses `vapply()` en tus funciones, no `sapply()`. La principal desventaja de `vapply()` es su verbosidad: por ejemplo, el equivalente a `map_dbl(x, mean, na.rm = TRUE)` es `vapply(x, mean, na.rm = TRUE, FUN.VALUE = doble(1))`.
:::
### Funciones y accesos directos anónimos {#sec-purrr-shortcuts}
\index{functions!anonymous} \index{\textasciitilde}
En lugar de usar `map()` con una función existente, puede crear una función anónima en línea (como se menciona en la @sec-first-class-functions):
```{r}
map_dbl(mtcars, function(x) length(unique(x)))
```
Las funciones anónimas son muy útiles, pero la sintaxis es detallada. Así que purrr admite un atajo especial:
```{r}
map_dbl(mtcars, ~ length(unique(.x)))
```
Esto funciona porque todas las funciones purrr traducen fórmulas, creadas por `~` (pronunciado "twiddle"), en funciones. Puedes ver lo que sucede detrás de escena llamando a `as_mapper()`:
```{r}
as_mapper(~ length(unique(.x)))
```
Los argumentos de la función parecen un poco extravagantes pero le permiten referirse a `.` para funciones de un argumento, `.x` y `.y` para funciones de dos argumentos, y `..1`, `..2`, `. .3`, etc., para funciones con un número arbitrario de argumentos. `.` permanece para la compatibilidad con versiones anteriores, pero no recomiendo usarlo porque se confunde fácilmente con el `.` utilizado por la canalización de magrittr.
Este atajo es particularmente útil para generar datos aleatorios:
```{r}
x <- map(1:3, ~ runif(2))
str(x)
```
Reserve esta sintaxis para funciones cortas y simples. Una buena regla general es que si su función abarca líneas o usa `{}`, es hora de darle un nombre.
\index{pluck()}
Las funciones del mapa también tienen atajos para extraer elementos de un vector, impulsados por `purrr::pluck()`. Puede utilizar un vector de caracteres para seleccionar elementos por nombre, un vector entero para seleccionar por posición o una lista para seleccionar tanto por nombre como por posición. Estos son muy útiles para trabajar con listas profundamente anidadas, que a menudo surgen cuando se trabaja con JSON.
```{r, error = TRUE}
x <- list(
list(-1, x = 1, y = c(2), z = "a"),
list(-2, x = 4, y = c(5, 6), z = "b"),
list(-3, x = 8, y = c(9, 10, 11))
)
# Selecciona por nombre
map_dbl(x, "x")
# O por posición
map_dbl(x, 1)
# Or por ambos
map_dbl(x, list("y", 1))
# Obtendrá un error si un componente no existe:
map_chr(x, "z")
# A menos que proporcione un valor .default
map_chr(x, "z", .default = NA)
```
::: base
En las funciones básicas de R, como `lapply()`, puede proporcionar el nombre de la función como una cadena. Esto no es tremendamente útil ya que `lapply(x, "f")` es casi siempre equivalente a `lapply(x, f)` y es más tipeo.
:::
### Pasar argumentos con `...` {#sec-passing-arguments}
\index{...}
\index{vectorisation}
A menudo es conveniente pasar argumentos adicionales a la función que está llamando. Por ejemplo, es posible que desee pasar `na.rm = TRUE` junto con `mean()`. Una forma de hacerlo es con una función anónima:
```{r}
x <- list(1:5, c(1:10, NA))
map_dbl(x, ~ mean(.x, na.rm = TRUE))
```
Pero debido a que las funciones del mapa pasan `...`, hay una forma más simple disponible:
```{r}
map_dbl(x, mean, na.rm = TRUE)
```
Esto es más fácil de entender con una imagen: cualquier argumento que viene después de `f` en la llamada a `map()` se inserta *después* de los datos en llamadas individuales a `f()`:
```{r, echo = FALSE, out.width = NULL}
knitr::include_graphics("diagrams/functionals/map-arg.png")
```
Es importante tener en cuenta que estos argumentos no están descompuestos; o dicho de otra manera, `map()` solo se vectoriza sobre su primer argumento. Si un argumento después de `f` es un vector, se pasará como está:
```{r, echo = FALSE, out.width = NULL}
knitr::include_graphics("diagrams/functionals/map-arg-recycle.png")
```
(Aprenderá acerca de las variantes de mapa que *están* vectorizadas sobre múltiples argumentos en las Secciones @sec-map2 y @sec-pmap.)
Tenga en cuenta que hay una sutil diferencia entre colocar argumentos adicionales dentro de una función anónima en comparación con pasarlos a `map()`. Ponerlos en una función anónima significa que serán evaluados cada vez que se ejecute `f()`, no solo una vez cuando llames a `map()`. Esto es más fácil de ver si hacemos que el argumento adicional sea aleatorio:
```{r}
plus <- function(x, y) x + y
x <- c(0, 0, 0, 0)
map_dbl(x, plus, runif(1))
map_dbl(x, ~ plus(.x, runif(1)))
```
### Nombres de argumentos {#sec-argument-names}
En los diagramas, he omitido los nombres de los argumentos para centrarme en la estructura general. Pero recomiendo escribir los nombres completos en su código, ya que lo hace más fácil de leer. `map(x, mean, 0.1)` es un código perfectamente válido, pero llamará `mean(x[[1]], 0.1)` por lo que depende de que el lector recuerde que el segundo argumento de `mean()` es `trim`. Para evitar una carga innecesaria en el cerebro del lector\[\^funcionals-3\], sea amable y escriba `map(x, mean, trim = 0.1)`.
Esta es la razón por la que los argumentos de `map()` son un poco extraños: en lugar de ser `x` y `f`, son `.x` y `.f`. Es más fácil ver el problema que conduce a estos nombres usando `simple_map()` definido anteriormente. `simple_map()` tiene argumentos `x` y `f`, por lo que tendrá problemas cada vez que la función a la que llama tenga argumentos `x` o `f`:
```{r, error = TRUE}
bootstrap_summary <- function(x, f) {
f(sample(x, replace = TRUE))
}
simple_map(mtcars, bootstrap_summary, f = mean)
```
<!-- GVW: Sería muy útil un diagrama aquí que muestre cómo las diversas f y x se combinan entre sí en el ejemplo anterior. -->
El error es un poco desconcertante hasta que recuerdas que la llamada a `simple_map()` es equivalente a `simple_map(x = mtcars, f = mean, bootstrap_summary)` porque la coincidencia con nombre supera a la coincidencia posicional.
Las funciones purrr reducen la probabilidad de que se produzca un conflicto de este tipo mediante el uso de `.f` y `.x` en lugar de las más comunes `f` y `x`. Por supuesto, esta técnica no es perfecta (porque la función a la que está llamando aún puede usar `.f` y `.x`), pero evita el 99% de los problemas. El 1% restante del tiempo, utilice una función anónima.
::: base
Las funciones base que transmiten `...` usan una variedad de convenciones de nomenclatura para evitar la coincidencia de argumentos no deseados:
- La familia apply utiliza principalmente letras mayúsculas (por ejemplo, `X` y `FUN`).
- `transform()` usa el prefijo más exótico `_`: esto hace que el nombre no sea sintáctico, por lo que siempre debe estar entre `` ` ``, como se describe en la @sec-non-syntactic. Esto hace que las coincidencias no deseadas sean extremadamente improbables.
- Otras funciones como `uniroot()` y `optim()` no hacen ningún esfuerzo por evitar conflictos, pero tienden a usarse con funciones especialmente creadas, por lo que es menos probable que se produzcan conflictos.
:::
### Variando otro argumento {#sec-change-argument}
Hasta ahora, el primer argumento de `map()` siempre se ha convertido en el primer argumento de la función. Pero, ¿qué sucede si el primer argumento debe ser constante y desea variar un argumento diferente? ¿Cómo se obtiene el resultado en esta imagen?
```{r, echo = FALSE, out.width = NULL}
knitr::include_graphics("diagrams/functionals/map-arg-flipped.png")
```
Resulta que no hay forma de hacerlo directamente, pero hay dos trucos que puedes usar en su lugar. Para ilustrarlos, imagine que tengo un vector que contiene algunos valores inusuales y quiero explorar el efecto de diferentes cantidades de recorte al calcular la media. En este caso, el primer argumento de `mean()` será constante, y quiero variar el segundo argumento, `trim`.
```{r}
trims <- c(0, 0.1, 0.2, 0.5)
x <- rcauchy(1000)
```
- La técnica más simple es usar una función anónima para reorganizar el orden de los argumentos:
```{r}
map_dbl(trims, ~ mean(x, trim = .x))
```
Esto todavía es un poco confuso porque estoy usando `x` y `.x`. Puedes hacerlo un poco más claro abandonando el ayudante `~`:
```{r}
map_dbl(trims, function(trim) mean(x, trim = trim))
```
- A veces, si quiere ser (demasiado) inteligente, puede aprovechar las reglas flexibles de coincidencia de argumentos de R (como se describe en la @sec-prefix-form). Por ejemplo, en este ejemplo puede reescribir `mean(x, trim = 0.1)` como `mean(0.1, x = x)`, por lo que podría escribir la llamada a `map_dbl()` como:
```{r}
map_dbl(trims, mean, x = x)
```
No recomiendo esta técnica ya que se basa en la familiaridad del lector con el orden de los argumentos en `.f` y las reglas de coincidencia de argumentos de R.
Verá una alternativa más en la @sec-pmap.
### Ejercicios
1. Utilice `as_mapper()` para explorar cómo purrr genera funciones anónimas para los ayudantes de enteros, caracteres y listas. ¿Qué ayudante te permite extraer atributos? Lea la documentación para averiguarlo.
2. `map(1:3, ~ runif(2))` es un patrón útil para generar números aleatorios, pero `map(1:3, runif(2))` no lo es. ¿Por qué no? ¿Puede explicar por qué devuelve el resultado que lo hace?
3. Use la función `map()` apropiada para:
a) Calcule la desviación estándar de cada columna en un data frame numéricos.
b) Calcule la desviación estándar de cada columna numérica en un data frame mixto. (Sugerencia: deberá hacerlo en dos pasos).
c) Calcule el número de niveles para cada factor en un data frame.
4. El siguiente código simula el rendimiento de una prueba t para datos no normales. Extraiga el valor p de cada prueba, luego visualice.
```{r}
trials <- map(1:100, ~ t.test(rpois(10, 10), rpois(7, 10)))
```
5. El siguiente código usa un mapa anidado dentro de otro mapa para aplicar una función a cada elemento de una lista anidada. ¿Por qué falla y qué debe hacer para que funcione?
```{r, error = TRUE}
x <- list(
list(1, c(3, 9)),
list(c(3, 6), 7, c(4, 7, 6))
)
triple <- function(x) x * 3
map(x, map, .f = triple)
```
6. Use `map()` para ajustar modelos lineales al conjunto de datos `mtcars` usando las fórmulas almacenadas en esta lista:
```{r}
formulas <- list(
mpg ~ disp,
mpg ~ I(1 / disp),
mpg ~ disp + wt,
mpg ~ I(1 / disp) + wt
)
```
7. Ajuste el modelo `mpg ~ disp` a cada una de las réplicas de arranque de `mtcars` en la lista a continuación, luego extraiga el $R^2$ del ajuste del modelo (Sugerencia: puede calcular el $R^2$ con `summary ()`.)
```{r}
bootstrap <- function(df) {
df[sample(nrow(df), replace = TRUE), , drop = FALSE]
}
bootstraps <- map(1:10, ~ bootstrap(mtcars))
```
## Estilo Purrr {#sec-purrr-style}
\index{\%>\%}
Antes de continuar explorando más variantes de mapas, echemos un vistazo rápido a cómo tiende a usar varias funciones purrr para resolver un problema moderadamente realista: ajustar un modelo a cada subgrupo y extraer un coeficiente del modelo. Para este ejemplo de juguete, voy a dividir el conjunto de datos `mtcars` en grupos definidos por el número de cilindros, utilizando la función base `split`:
```{r}
by_cyl <- split(mtcars, mtcars$cyl)
```
Esto crea una lista de tres data frames: los automóviles con 4, 6 y 8 cilindros respectivamente.
Ahora imagine que queremos ajustar un modelo lineal, luego extraiga el segundo coeficiente (es decir, la pendiente). El siguiente código muestra cómo puede hacer eso con purrr:
```{r}
by_cyl |>
map(~ lm(mpg ~ wt, data = .x)) |>
map(coef) |>
map_dbl(2)
```
(Si no ha visto `|>`, la canalización, antes, se describe en la @sec-function-composition.)
Creo que este código es fácil de leer porque cada línea encapsula un solo paso, puedes distinguir fácilmente lo funcional de lo que hace, y los ayudantes purrr nos permiten describir de manera muy concisa qué hacer en cada paso.
¿Cómo atacarías este problema con la base R? Ciertamente *podría* reemplazar cada función purrr con la función base equivalente:
```{r}
by_cyl |>
lapply(function(data) lm(mpg ~ wt, data = data)) |>
lapply(coef) |>
vapply(function(x) x[[2]], double(1))
```
O, por supuesto, podría usar un bucle for:
```{r}
slopes <- double(length(by_cyl))
for (i in seq_along(by_cyl)) {
model <- lm(mpg ~ wt, data = by_cyl[[i]])
slopes[[i]] <- coef(model)[[2]]
}
slopes
```
Es interesante notar que a medida que pasa de purrr a aplicar funciones básicas a bucles for, tiende a hacer más y más en cada iteración. En purrr iteramos 3 veces (`map()`, `map()`, `map_dbl()`), con funciones apply iteramos dos veces (`lapply()`, `vapply()`), y con un for loop iteramos una vez. Prefiero más pasos, pero más simples, porque creo que hace que el código sea más fácil de entender y luego modificar.
## Variantes de map {#sec-map-variants}
Hay 23 variantes principales de `map()`. Hasta ahora, ha aprendido acerca de cinco (`map()`, `map_lgl()`, `map_int()`, `map_dbl()` y `map_chr()`). Eso significa que tienes 18 (!!) más para aprender. Eso parece mucho, pero afortunadamente el diseño de purrr significa que solo necesitas aprender cinco nuevas ideas:
- Salida del mismo tipo que la entrada con `modify()`
- Iterar sobre dos entradas con `map2()`.
- Iterar con un índice usando `imap()`
- No devuelve nada con `walk()`.
- Iterar sobre cualquier número de entradas con `pmap()`.
La familia de funciones del mapa tiene entradas y salidas ortogonales, lo que significa que podemos organizar toda la familia en una matriz, con entradas en las filas y salidas en las columnas. Una vez que haya dominado la idea en una fila, puede combinarla con cualquier columna; una vez que haya dominado la idea en una columna, puede combinarla con cualquier fila. Esa relación se resume en el siguiente cuadro:
| | List | Atómico | El mismo tipo | Nada |
|-----------------|--------------|--------------|--------------|--------------|
| Un argumento | `map()` | `map_lgl()`, ... | `modify()` | `walk()` |
| Dos argumentos | `map2()` | `map2_lgl()`, ... | `modify2()` | `walk2()` |
| Un argumento + índice | `imap()` | `imap_lgl()`, ... | `imodify()` | `iwalk()` |
| N argumentos | `pmap()` | `pmap_lgl()`, ... | --- | `pwalk()` |
### Mismo tipo de salida que de entrada: `modify()` {#sec-modify}
\index{modify()}
Imagina que quisieras duplicar cada columna en un data frame. Primero puede intentar usar `map()`, pero `map()` siempre devuelve una lista:
```{r}
df <- data.frame(
x = 1:3,
y = 6:4
)
map(df, ~ .x * 2)
```
Si desea mantener la salida como un data frame, puede usar `modify()`, que siempre devuelve el mismo tipo de salida que la entrada:
```{r}
modify(df, ~ .x * 2)
```
A pesar del nombre, `modify()` no modifica en su lugar, devuelve una copia modificada, por lo que si desea modificar permanentemente `df`, debe asignarlo:
```{r}
df <- modify(df, ~ .x * 2)
```
Como de costumbre, la implementación básica de `modify()` es simple y, de hecho, es incluso más simple que `map()` porque no necesitamos crear un nuevo vector de salida; podemos simplemente reemplazar progresivamente la entrada. (El código real es un poco complejo para manejar casos extremos con más gracia).
```{r}
simple_modify <- function(x, f, ...) {
for (i in seq_along(x)) {
x[[i]] <- f(x[[i]], ...)
}
x
}
```
En la @sec-predicate-map aprenderá sobre una variante muy útil de `modify()`, llamada `modify_if()`. Esto le permite (p. ej.) solo duplicar columnas *numéricas* de un data frame con `modify_if(df, is.numeric, ~ .x * 2)`.
### Dos entradas: `map2()` y amigos {#sec-map2}
\index{map2()}
`map()` se vectoriza sobre un único argumento, `.x`. Esto significa que solo varía `.x` cuando se llama a `.f`, y todos los demás argumentos se pasan sin cambios, por lo que no es adecuado para algunos problemas. Por ejemplo, ¿cómo encontraría una media ponderada cuando tiene una lista de observaciones y una lista de pesos? Imagina que tenemos los siguientes datos:
```{r}
xs <- map(1:8, ~ runif(10))
xs[[1]][[1]] <- NA
ws <- map(1:8, ~ rpois(10, 5) + 1)
```
Puedes usar `map_dbl()` para calcular las medias no ponderadas:
```{r}
map_dbl(xs, mean)
```
Pero pasar `ws` como argumento adicional no funciona porque los argumentos después de `.f` no se transforman:
```{r, error = TRUE}
map_dbl(xs, weighted.mean, w = ws)
```
```{r, echo = FALSE, out.width = NULL}
knitr::include_graphics("diagrams/functionals/map-arg-recycle.png")
```
Necesitamos una nueva herramienta: un `map2()`, que se vectoriza sobre dos argumentos. Esto significa que tanto `.x` como `.y` varían en cada llamada a `.f`:
```{r}
map2_dbl(xs, ws, weighted.mean)
```
```{r, echo = FALSE, out.width = NULL}
knitr::include_graphics("diagrams/functionals/map2.png")
```
Los argumentos de `map2()` son ligeramente diferentes a los argumentos de `map()` ya que dos vectores vienen antes de la función, en lugar de uno. Los argumentos adicionales todavía van después:
```{r}
map2_dbl(xs, ws, weighted.mean, na.rm = TRUE)
```
```{r, echo = FALSE, out.width = NULL}
knitr::include_graphics("diagrams/functionals/map2-arg.png")
```
La implementación básica de `map2()` es simple y bastante similar a la de `map()`. En lugar de iterar sobre un vector, iteramos sobre dos en paralelo:
```{r}
simple_map2 <- function(x, y, f, ...) {
out <- vector("list", length(x))
for (i in seq_along(x)) {
out[[i]] <- f(x[[i]], y[[i]], ...)
}
out
}
```
Una de las grandes diferencias entre `map2()` y la función simple anterior es que `map2()` recicla sus entradas para asegurarse de que tengan la misma longitud:
```{r, echo = FALSE, out.width = NULL}
knitr::include_graphics("diagrams/functionals/map2-recycle.png")
```
En otras palabras, `map2(x, y, f)` automáticamente se comportará como `map(x, f, y)` cuando sea necesario. Esto es útil al escribir funciones; en las secuencias de comandos, generalmente solo usaría la forma más simple directamente.
::: base
La base equivalente más cercana a `map2()` es `Map()`, que se analiza en la @sec-pmap.
:::
### Sin salidas: `walk()` y amigos
```{=tex}
\index{walk()}
\index{walk2()}
\index{invisible()}
```
La mayoría de las funciones se llaman por el valor que devuelven, por lo que tiene sentido capturar y almacenar el valor con una función `map()`. Pero algunas funciones se llaman principalmente por sus efectos secundarios (por ejemplo, `cat()`, `write.csv()` o `ggsave()`) y no tiene sentido capturar sus resultados. Toma este ejemplo simple que muestra un mensaje de bienvenida usando `cat()`. `cat()` devuelve `NULL`, así que mientras `map()` funciona (en el sentido de que genera las bienvenidas deseadas), también devuelve `list(NULL, NULL)`.
```{r}
welcome <- function(x) {
cat("Welcome ", x, "!\n", sep = "")
}
names <- c("Hadley", "Jenny")
# Además de generar las bienvenidas, también muestra
# el valor de retorno de cat()
map(names, welcome)
```
Podrías evitar este problema asignando los resultados de `map()` a una variable que nunca usas, pero que enturbiaría la intención del código. En su lugar, purrr proporciona la familia de funciones walk que ignoran los valores de retorno de `.f` y en su lugar devuelven `.x` de forma invisible[^functionals-1].
[^functionals-1]: En resumen, los valores invisibles solo se imprimen si lo solicita explícitamente. Esto los hace muy adecuados para las funciones llamadas principalmente por sus efectos secundarios, ya que permite ignorar su salida de forma predeterminada, al tiempo que ofrece una opción para capturarla. Ver @sec-invisible para más detalles.
```{r}
walk(names, welcome)
```
Mi representación visual de caminar intenta capturar la importante diferencia con `map()`: las salidas son efímeras y la entrada se devuelve de forma invisible.
```{r, echo = FALSE, out.width = NULL}
knitr::include_graphics("diagrams/functionals/walk.png")
```
Una de las variantes de `walk()` más útiles es `walk2()` porque un efecto secundario muy común es guardar algo en el disco, y cuando guardas algo en el disco siempre tienes un par de valores: el objeto y la ruta que en el que desea guardarlo.
```{r, echo = FALSE, out.width = NULL}
knitr::include_graphics("diagrams/functionals/walk2.png")
```
Por ejemplo, imagina que tienes una lista de data frames (que he creado aquí usando `split()`) y te gustaría guardar cada uno en un archivo CSV separado. Eso es fácil con `walk2()`:
```{r}
temp <- tempfile()
dir.create(temp)
cyls <- split(mtcars, mtcars$cyl)
paths <- file.path(temp, paste0("cyl-", names(cyls), ".csv"))
walk2(cyls, paths, write.csv)
dir(temp)
```
Aquí el `walk2 ()` es equivalente a `write.csv(cyls[[1]], paths[[1]])`, `write.csv(cyls[[2]], paths[[2]])`, `write.csv(cyls[[3]], paths[[3]])`.
::: base
No existe una base equivalente a `walk()`; envuelva el resultado de `lapply()` en `invisible()` o guárdelo en una variable que nunca se use.
:::
### Iterando sobre valores e índices
\index{imap()}
\index{loops!common patterns}
Hay tres formas básicas de recorrer un vector con un bucle for:
- Bucle sobre los elementos: `for (x in xs)`
- Bucle sobre los índices numéricos: `for (i in seq_along(xs))`
- Bucle sobre los nombres: `for (nm in names(xs))`
La primera forma es análoga a la familia `map()`. Las formas segunda y tercera son equivalentes a la familia `imap()` que le permite iterar sobre los valores y los índices de un vector en paralelo.
`imap()` es como `map2()` en el sentido de que su `.f` se llama con dos argumentos, pero aquí ambos se derivan del vector. `imap(x, f)` es equivalente a `map2(x, nombres(x), f)` si x tiene nombres, y `map2(x, seq_along(x), f)` si no los tiene.
`imap()` suele ser útil para construir etiquetas:
```{r}
imap_chr(iris, ~ paste0("The first value of ", .y, " is ", .x[[1]]))
```
Si el vector no tiene nombre, el segundo argumento será el índice:
```{r}
x <- map(1:6, ~ sample(1000, 10))
imap_chr(x, ~ paste0("The highest value of ", .y, " is ", max(.x)))
```
`imap()` es una ayuda útil si desea trabajar con los valores de un vector junto con sus posiciones.
### Cualquier número de entradas: `pmap ()` y amigos {#sec-pmap}
\index{pmap()}
Ya que tenemos `map()` y `map2()`, podrías esperar `map3()`, `map4()`, `map5()`, ... Pero, ¿dónde te detendrías? En lugar de generalizar `map2()` a un número arbitrario de argumentos, purrr adopta un rumbo ligeramente diferente con `pmap()`: le proporciona una sola lista, que contiene cualquier número de argumentos. En la mayoría de los casos, será una lista de vectores de igual longitud, es decir, algo muy similar a un data frame. En los diagramas, enfatizaré esa relación dibujando la entrada de forma similar a un data frame.
```{r, echo = FALSE, out.width = NULL}
knitr::include_graphics("diagrams/functionals/pmap.png")
```
Hay una equivalencia simple entre `map2()` y `pmap()`: `map2(x, y, f)` es lo mismo que `pmap(list(x, y), f)`. El `pmap()` equivalente a `map2_dbl(xs, ws,weighted.mean)` utilizado anteriormente es:
```{r}
pmap_dbl(list(xs, ws), weighted.mean)
```
Como antes, los argumentos variables vienen antes de `.f` (aunque ahora deben estar envueltos en una lista), y los argumentos constantes vienen después.
```{r}
pmap_dbl(list(xs, ws), weighted.mean, na.rm = TRUE)
```
```{r, echo = FALSE, out.width = NULL}
knitr::include_graphics("diagrams/functionals/pmap-arg.png")
```
Una gran diferencia entre `pmap()` y las otras funciones de mapa es que `pmap()` te da un control mucho más preciso sobre la coincidencia de argumentos porque puedes nombrar los componentes de la lista. Volviendo a nuestro ejemplo de la @sec-change-argument, donde queríamos cambiar el argumento `trim` a `x`, podríamos usar `pmap()`:
```{r}
trims <- c(0, 0.1, 0.2, 0.5)
x <- rcauchy(1000)
pmap_dbl(list(trim = trims), mean, x = x)
```
Creo que es una buena práctica nombrar los componentes de la lista para dejar muy claro cómo se llamará a la función.
A menudo es conveniente llamar a `pmap()` con un data frame. Una forma práctica de crear ese data frame es con `tibble::tribble()`, que le permite describir un data frame fila por fila (en lugar de columna por columna, como de costumbre): pensando en los parámetros a una función como data frame es un patrón muy poderoso. El siguiente ejemplo muestra cómo puede dibujar números uniformes aleatorios con diferentes parámetros:
```{r}
params <- tibble::tribble(
~ n, ~ min, ~ max,
1L, 0, 1,
2L, 10, 100,
3L, 100, 1000
)
pmap(params, runif)
```
```{r, echo = FALSE, out.width = NULL}
knitr::include_graphics("diagrams/functionals/pmap-3.png")
```
Aquí, los nombres de las columnas son fundamentales: elegí cuidadosamente hacerlos coincidir con los argumentos de `runif()`, por lo que `pmap(params, runif)` es equivalente a `runif(n = 1L, min = 0, max = 1)`, `runif(n = 2, min = 10, max = 100)`, `runif(n = 3L, min = 100, max = 1000)`. (Si tiene un data frame en la mano y los nombres no coinciden, use `dplyr::rename()` o similar).
::: base
```{=tex}
\index{Map()}
\index{mapply()}
```
Hay dos equivalentes base para la familia `pmap()`: `Map()` y `mapply()`. Ambos tienen importantes inconvenientes:
- `Map()` vectoriza sobre todos los argumentos para que no pueda proporcionar argumentos que no varíen.
- `mapply()` es la versión multidimensional de `sapply()`; conceptualmente, toma la salida de `Map()` y la simplifica si es posible. Esto le da problemas similares a `sapply()`. No existe un equivalente de múltiples entradas de `vapply()`.
:::
### Ejercicios
1. Explique los resultados de `modify(mtcars, 1)`.
2. Reescribe el siguiente código para usar `iwalk()` en lugar de `walk2()`. ¿Cuáles son las ventajas y desventajas?
```{r, eval = FALSE}
cyls <- split(mtcars, mtcars$cyl)
paths <- file.path(temp, paste0("cyl-", names(cyls), ".csv"))
walk2(cyls, paths, write.csv)
```
3. Explique cómo el siguiente código transforma un data frame utilizando funciones almacenadas en una lista.
```{r}
trans <- list(
disp = function(x) x * 0.0163871,
am = function(x) factor(x, labels = c("auto", "manual"))
)
nm <- names(trans)
mtcars[nm] <- map2(trans, mtcars[nm], function(f, var) f(var))
```
Compare y contraste el enfoque `map2()` con este enfoque `map()`:
```{r, eval = FALSE}
mtcars[nm] <- map(nm, ~ trans[[.x]](mtcars[[.x]]))
```
4. ¿Qué devuelve `write.csv()`, es decir, qué sucede si lo usa con `map2()` en lugar de `walk2()`?
## Familia reduce {#sec-reduce}
Después de la familia map, la siguiente familia de funciones más importante es la familia reduce. Esta familia es mucho más pequeña, con solo dos variantes principales, y se usa con menos frecuencia, pero es una idea poderosa, nos brinda la oportunidad de analizar algo de álgebra útil y potencia el marco de reducción de mapas que se usa con frecuencia para procesar conjuntos de datos muy grandes.
### Lo esencial
\index{reduce()}
\index{fold|see {reduce}}
`reduce()` toma un vector de longitud *n* y produce un vector de longitud 1 llamando a una función con un par de valores a la vez: `reduce(1:4, f)` es equivalente a `f(f(f(1, 2), 3), 4)`.
```{r, echo = FALSE, out.width = NULL}
knitr::include_graphics("diagrams/functionals/reduce.png")
```
`reduce()` es una forma útil de generalizar una función que funciona con dos entradas (una función **binaria**) para que funcione con cualquier cantidad de entradas. Imagina que tienes una lista de vectores numéricos y quieres encontrar los valores que ocurren en cada elemento. Primero generamos algunos datos de muestra:
```{r}
l <- map(1:4, ~ sample(1:10, 15, replace = T))
str(l)
```
Para resolver este desafío necesitamos usar `intersect()` repetidamente:
```{r}
out <- l[[1]]
out <- intersect(out, l[[2]])
out <- intersect(out, l[[3]])
out <- intersect(out, l[[4]])
out
```
`reduce()` Automatiza esta solución para nosotros, para que podamos escribir:
```{r}
reduce(l, intersect)
```
Podríamos aplicar la misma idea si quisiéramos listar todos los elementos que aparecen en al menos una entrada. Todo lo que tenemos que hacer es cambiar de `intersect()` a `union()`:
```{r}
reduce(l, union)
```
Al igual que la familia de mapas, también puede pasar argumentos adicionales. `intersect()` y `union()` no aceptan argumentos adicionales, así que no puedo demostrarlos aquí, pero el principio es sencillo y le hice un dibujo.
```{r, echo = FALSE, out.width = NULL}
knitr::include_graphics("diagrams/functionals/reduce-arg.png")
```
Como de costumbre, la esencia de `reduce()` se puede reducir a un simple envoltorio alrededor de un bucle for:
```{r}
simple_reduce <- function(x, f) {
out <- x[[1]]
for (i in seq(2, length(x))) {
out <- f(out, x[[i]])
}
out
}
```
::: base
El equivalente básico es `Reduce()`. Tenga en cuenta que el orden de los argumentos es diferente: la función viene primero, seguida del vector, y no hay forma de proporcionar argumentos adicionales.
:::
### accumulate
\index{accumulate()}
La primera variante `reduce()`, `accumulate()`, es útil para comprender cómo funciona reduce, porque en lugar de devolver solo el resultado final, también devuelve todos los resultados intermedios:
```{r}
accumulate(l, intersect)
```
Otra forma útil de entender reduce es pensar en `sum()`: `sum(x)` es equivalente a `x[[1]] + x[[2]] + x[[3]] + ...`, es decir `` reduce(x, `+`) ``. Entonces `` accumulate(x, `+`) `` es la suma acumulada:
```{r}
x <- c(4, 3, 10)
reduce(x, `+`)
accumulate(x, `+`)
```
### Tipos de salida
En el ejemplo anterior usando `+`, ¿qué debería devolver `reduce()` cuando `x` es corto, es decir, longitud 1 o 0? Sin argumentos adicionales, `reduce()` solo devuelve la entrada cuando `x` tiene una longitud de 1:
```{r}
reduce(1, `+`)
```
Esto significa que `reduce()` no tiene forma de verificar que la entrada sea válida:
```{r}
reduce("a", `+`)
```
¿Qué pasa si es de longitud 0? Recibimos un error que sugiere que necesitamos usar el argumento `.init`:
```{r, error = TRUE}
reduce(integer(), `+`)
```
¿Qué debería ser `.init` aquí? Para averiguarlo, necesitamos ver qué sucede cuando se proporciona `.init`:
```{r, echo = FALSE, out.width = NULL}
knitr::include_graphics("diagrams/functionals/reduce-init.png")
```
Así que si llamamos a `` reduce(1, `+`, init) `` el resultado será \``1 + init`. Ahora sabemos que el resultado debería ser solo `1`, lo que sugiere que `.init` debería ser 0:
```{r}
reduce(integer(), `+`, .init = 0)
```
Esto también asegura que `reduce()` verifique que las entradas de longitud 1 sean válidas para la función que estás llamando:
```{r, error = TRUE}
reduce("a", `+`, .init = 0)
```
Si quieres ser algebraico al respecto, 0 se llama la **identidad** de los números reales en la operación de suma: si agregas un 0 a cualquier número, obtienes el mismo número. R aplica el mismo principio para determinar qué debe devolver una función de resumen con una entrada de longitud cero:
```{r, warning = FALSE}
sum(integer()) # x + 0 = x
prod(integer()) # x * 1 = x
min(integer()) # min(x, Inf) = x
max(integer()) # max(x, -Inf) = x
```
Si está utilizando `reduce()` en una función, siempre debe proporcionar `.init`. Piense detenidamente qué debe devolver su función cuando pasa un vector de longitud 0 o 1, y asegúrese de probar su implementación.
### Múltiples entradas
\index{reduce2()}
Muy ocasionalmente necesita pasar dos argumentos a la función que está reduciendo. Por ejemplo, puede tener una lista de data frames que desea unir y las variables que usa para unir variarán de un elemento a otro. Este es un escenario muy especializado, por lo que no quiero dedicarle mucho tiempo, pero sí quiero que sepas que `reduce2()` existe.
La longitud del segundo argumento varía en función de si se proporciona `.init` o no: si tiene cuatro elementos de `x`, `f` solo se llamará tres veces. Si proporciona init, `f` se llamará cuatro veces.
```{r, echo = FALSE, out.width = NULL}
knitr::include_graphics("diagrams/functionals/reduce2.png")
```
```{r, echo = FALSE, out.width = NULL}
knitr::include_graphics("diagrams/functionals/reduce2-init.png")
```
### Mapa reducido
\index{map-reduce}
Es posible que haya oído hablar de map-reduce, la idea que impulsa la tecnología como Hadoop. Ahora puedes ver cuán simple y poderosa es la idea subyacente: map-reduce es un mapa combinado con una reducción. La diferencia para los datos grandes es que los datos se distribuyen en varias computadoras. Cada computadora realiza el mapa en los datos que tiene, luego envía el resultado a un coordinador que *reduce* los resultados individuales a un solo resultado.
Como un ejemplo simple, imagine calcular la media de un vector muy grande, tan grande que tiene que dividirse entre varias computadoras. Puede pedirle a cada computadora que calcule la suma y la longitud, y luego devolverlos al coordinador que calcula la media general dividiendo la suma total por la longitud total.
## Funcionales de predicado {#sec-predicate-functionals}
\index{predicates} \index{functions!predicate|see {predicates}}
Un **predicado** es una función que devuelve un solo `TRUE` o `FALSE`, como `is.character()`, `is.null()` o `all()`, y decimos un predicado **coincide** con un vector si devuelve `TRUE`.
### Lo esencial
Un **predicado funcional** aplica un predicado a cada elemento de un vector. purrr proporciona siete funciones útiles que se dividen en tres grupos:
- `some(.x, .p)` devuelve `TRUE` si *algún* elemento coincide;\
`every(.x, .p)` devuelve `TRUE` si *todos* los elementos coinciden;\
`none(.x, .p)` devuelve `TRUE` si *ningún* elemento coincide.
Estos son similares a `any(map_lgl(.x, .p))`, `all(map_lgl(.x, .p))` y `all(map_lgl(.x, negate(.p)))` pero terminar antes de tiempo: `some()` devuelve `TRUE` cuando ve el primer `TRUE`, y `cada()` y `ninguno()` devuelve `FALSE` cuando ven el primer `FALSE` o `TRUE` respectivamente .
- `detect(.x, .p)` devuelve el *valor* de la primera coincidencia; `detect_index(.x, .p)` devuelve la *ubicación* de la primera coincidencia.
- `keep(.x, .p)` *mantiene* todos los elementos coincidentes; `discard(.x, .p)` *suelta* todos los elementos coincidentes.
El siguiente ejemplo muestra cómo puede usar estas funciones con un data frame:
```{r}
df <- data.frame(x = 1:3, y = c("a", "b", "c"))
detect(df, is.factor)
detect_index(df, is.factor)
str(keep(df, is.factor))
str(discard(df, is.factor))
```
### Variantes de map {#sec-predicate-map}
`map()` y `modify()` vienen en variantes que también toman funciones de predicado, transformando solo los elementos de `.x` donde `.p` es `TRUE`.
```{r}
df <- data.frame(
num1 = c(0, 10, 20),
num2 = c(5, 6, 7),
chr1 = c("a", "b", "c"),
stringsAsFactors = FALSE
)
str(map_if(df, is.numeric, mean))
str(modify_if(df, is.numeric, mean))
str(map(keep(df, is.numeric), mean))
```
### Ejercicios
1. ¿Por qué `is.na()` no es una función de predicado? ¿Qué función base de R está más cerca de ser una versión predicada de `is.na()`?
2. `simple_reduce()` tiene un problema cuando `x` tiene una longitud de 0 o de 1. Describa el origen del problema y cómo podría solucionarlo.
```{r}
simple_reduce <- function(x, f) {
out <- x[[1]]
for (i in seq(2, length(x))) {
out <- f(out, x[[i]])
}
out
}
```
3. Implemente la función `span()` de Haskell: dada una lista `x` y una función de predicado `f`, `span(x, f)` devuelve la ubicación de la ejecución secuencial más larga de elementos donde el predicado es verdadero. (Sugerencia: puede encontrar útil `rle()`).
4. Implementar `arg_max()`. Debe tomar una función y un vector de entradas, y devolver los elementos de la entrada donde la función devuelve el valor más alto. Por ejemplo, `arg_max(-10:5, function(x) x ^ 2)` debería devolver -10. `arg_max(-5:5, function(x) x ^ 2)` debería devolver `c(-5, 5)`. Implemente también la función coincidente `arg_min()`.
5. La siguiente función escala un vector para que caiga en el rango \[0, 1\]. ¿Cómo lo aplicaría a cada columna de un data frame? ¿Cómo lo aplicaría a cada columna numérica en un data frame?
```{r}
scale01 <- function(x) {
rng <- range(x, na.rm = TRUE)
(x - rng[1]) / (rng[2] - rng[1])
}
```
## Funcionales base {#sec-base-functionals}
Para terminar el capítulo, aquí ofrezco un resumen de importantes funciones base que no son miembros de las familias map, reduce o predicate y, por lo tanto, no tienen equivalente en purrr. Esto no quiere decir que no sean importantes, pero tienen un sabor más matemático o estadístico y, en general, son menos útiles en el análisis de datos.
### Matrices y arreglos
\index{apply()}
`map()` y amigos están especializados para trabajar con vectores unidimensionales. `base::apply()` está especializado para trabajar con vectores bidimensionales y superiores, es decir, matrices y arreglos. Puede pensar en `apply()` como una operación que resume una matriz o conjunto al colapsar cada fila o columna en un solo valor. Tiene cuatro argumentos: