-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path10-apply.Rmd
440 lines (304 loc) · 18.8 KB
/
10-apply.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
430
431
432
433
434
435
436
437
438
439
440
# La familia apply
La familia de funciones `apply` es usada para aplicar una función a cada elemento de una estructura de datos. En particular, es usada para aplicar funciones en matrices, data frames, arrays y listas.
Con esta familia de funciones podemos automatizar tareas complejas usando poca líneas de código y es una de las características distintivas de R como lenguaje de programación.
La familia de funciones `apply` es una expresión de los rasgos del paradigma funcional de programación presentes en R. Sobre esto no profundizaremos demasiado, pero se refiere saber que en R las funciones son "ciudadanos de primera", con la misma importancia que los objetos, y por lo tanto, operamos en ellas.
La familia de funciones apply no sólo recibe datos como argumentos, también recibe funciones.
### Un recordatorio sobre vectorización
Para entender más fácilmente el uso de la familia 0, recordemos la [vectorización de operaciones](###vectorización-de-operaciones).
Hay operaciones que, si las aplicamos a un vector, son aplicadas a todos sus elementos.
```{r recordar vectorizacion, echo=TRUE}
mi_vector <- 1:10
mi_vector
mi_vector ^ 2
```
Lo anterior es, generalmente, preferible a escribir una operación para cada elemento o a usar un bucle **for**, como se describió en el capítulo sobre [estructuras de control](#estructuras-de-control).
Como todo lo que ocurre en R es una función, podemos decir que **al vectorizar estamos aplicando una función a cada elemento de un vector**. La familia de funciones **apply** nos permite implementar esto en estructuras de datos distintas a los vectores.
### Las funciones de la familia apply
La familia apply esta formada por las siguientes funciones:
* `apply()`
* `eapply()`
* `lapply()`
* `mapply()`
* `rapply()`
* `sapply()`
* `tapply()`
* `vapply()`
Es una familia numerosa y esta variedad de funciones se debe a que varias de ellas tienen aplicaciones sumamente específicas.
Todas las funciones de esta familia tienen una característica en común: **reciben como argumentos a un objeto y al menos una función**.
Hasta ahora, todas las funciones que hemos usado han recibido como argumentos estructuras de datos, sean vectores, data frames o de otro tipo. Las funciones de la familia apply tienen la particularidad que pueden recibir a otra función como un argumento. Lo anterior puede sonar confuso, pero es más bien intuitivo al verlo implementado.
Nosotros trabajaremos con las funciones más generales y de uso común de esta familia:
* `apply()`
* `lapply()`
Estas dos funciones nos permitirán solucionar casi todos los problemas a los que nos encontremos. Además, conociendo su uso, las demás funciones de la familia **apply** serán relativamente fáciles de entender.
## apply
`apply` aplica una función a todos los elementos de una **matriz**.
La estructura de esta función es la siguiente.
```{r, eval = F}
apply(X, MARGIN, FUN)
```
`apply` tiene tres argumentos:
* `X`: Una matriz o un objeto que pueda coercionarse a una matriz, generalmente, un data frame.
* `MARGIN`: La dimensión (margen) que agrupará los elementos de la matriz `X`, para aplicarles una función. Son identificadas con números, **1** son renglones y **2** son columnas.
* `FUN`: La función que aplicaremos a la matriz `X` en su dimensión `MARGIN`.
### ¿Qué es X
`X` es una matriz o cualquier otro objeto que sea posible coercionar a una matriz. Esto es, principalmente, vectores y data frames.
Recuerda que puedes coercionar objetos a matriz usando `as.matrix()` y puedes comprobar si un objeto es de esta clase con `is.matrix()`.
```{r, echo = TRUE}
# Creamos un data frame
mi_df <- data.frame(v1 = 1:3, v2 = 4:6)
mi_df
# Coerción a matriz
mi_matriz <- as.matrix(mi_df)
# Verificamos que sea matriz
is.matrix(mi_matriz)
# Resultado
mi_matriz
```
Aunque también podemos coercionar listas y arrays a matrices, los resultados que obtenemos no siempre son apropiados para `apply()`, por lo que no es recomendable usar estos objetos como argumentos.
### ¿Qué es MARGIN?
Recuerda que las matrices y los data frames están formadas por vectores y que estas estructuras tienen dos dimensiones, ordenadas en renglones y columnas. Esto lo vimos en en [Matrices y arrays](##matrices-y-arrays) y [Data frames](##data-frames).
Para `MARGIN`:
* 1 es renglones.
* 2 es columnas.
Por ejemplo, podemos usar `apply()` para obtener la sumatoria de los elementos de una matriz, por renglón.
Creamos una matriz de cuatro renglones.
```{r, echo=TRUE}
matriz <- matrix(1:14, nrow = 4)
```
Aplicamos `apply()`, dando la función `sum()` el argumento `FUN`, nota que sólo necesitamos el nombre de la función, sin paréntesis.
Por último, damos el argumento `MARGIN = 1`, para aplicar la función por renglón.
```{r, echo=TRUE}
apply(X = matriz, MARGIN = 1, FUN = sum)
```
Esto es equivalente a hacer lo siguiente.
```{r, echo = TRUE}
sum(matriz[1, ])
sum(matriz[2, ])
sum(matriz[3, ])
sum(matriz[4, ])
```
Y naturalmente, es equivalente a hacer lo siguiente.
```{r eval=FALSE, include=FALSE}
sum(vector_1)
sum(vector_2)
sum(vector_3)
sum(vector_4)
```
**Estamos aplicando una función a cada elemento de nuestra matriz. Los elementos son los renglones. Cada renglón es un vector. Cada vector es usado como argumento de la función.**
Si cambiamos el argumento MARGIN de `MARGIN = 1` a `MARGIN = 2`, entonces la función se aplicará por columna.
```{r, echo=TRUE}
apply(X = matriz, MARGIN = 2, FUN = sum)
```
En este caso, la función `sum()` ha sido aplicado a cada elementos de nuestra matriz, los elementos son las columnas, y cada columna es un vector.
### ¿Qué es FUN?
FUN es un argumento que nos pide el **nombre de una función que se se aplicarla a todos los elementos de nuestra matriz**.
El ejemplo de la sección anterior aplicamos las funciones `mean()` y `sum()` usando sus nombres, sin paréntesis, esto es, sin especificar argumentos.
Podemos dar como argumento cualquier nombre de función, siempre y cuando ésta acepte vectores como argumentos.
Probemos cambiando el argumento `FUN`. Usaremos la función `mean()` para obtener la media de cada renglón y de cada columna.
Aplicado a los renglones.
```{r, echo=TRUE}
apply(matriz, 1, mean)
```
Aplicado a las columnas
```{r, echo=TRUE}
apply(matriz, 2, mean)
```
Las siguientes llamadas a `sd()`, `max()` y `quantile()` se ejecutan sin necesidad de especificar argumentos.
```{r, echo=TRUE}
# Desviación estándar
apply(matriz, 1, FUN = sd)
# Máximo
apply(matriz, 1, FUN = max)
# Cuantiles
apply(matriz, 1, FUN = quantile)
```
### ¿Cómo sabe FUN cuáles son sus argumentos?
Recuerda que podemos llamar una función y proporcionar sus argumentos en orden, tal como fueron establecidos en su definición.
Por lo tanto, **el primer argumento que espera la función, será la `X` del `apply()`**.
Para ilustrar esto, usaremos la función `quantile()`. Llama `?quantile` en la consola para ver su documentación.
```{r, echo=TRUE, eval=FALSE}
?quantile
```
`quantile()` espera siempre un argumento `x`, que debe ser un vector numérico, además tener varios argumentos adicionales.
* `probs` es un vector numérico con las probabilidades de las que queremos extraer cuantiles.
* `na.rm`, si le asignamos `TRUE` quitará de x los `NA` y `NaN` antes de realizar operaciones.
* `names`, si le asignamos `TRUE`, hará que el objeto resultado de la función tenga nombres.
* `type` espera un valor entre 1 y 9, para determinar el algoritmo usado para el cálculo de los cuantiles.
En orden, el primer argumento es `x`, el segundo `probs`, y así sucesivamente.
Cuando usamos `quantile()` en un `apply()`, el argumento `x` de la función será cada elemento de nuestra matriz. Es decir, los vectores como renglones o columnas de los que está constituida la matriz.
Esto funcionará siempre y cuando los argumentos sean apropiados para la función. Si proporcionamos un argumento inválido, la función no se ejecutará y **apply** fallará.
Por ejemplo, intentamos obtener cuantiles de las columnas de una matriz, en la que una de ellas es de tipo carácter.
Creamos una matriz.
```{r, echo=TRUE}
matriz2 <- matrix(c(1:2, "a", "b"), nrow = 2)
# Resultado
```
Aplicamos la función y obtenemos un error.
```{r, echo=TRUE, error=TRUE}
apply(matriz2, 2, quantile)
```
Por lo tanto, **apply sólo puede ser usado con funciones que esperan vectores como argumentos**.
### ¿Qué pasa si deseamos utilizar los demás argumentos de una función con apply?
En los casos en los que una función tiene recibe más de un argumento, asignamos los valores de estos del nombre de la función, separados por comas, usando sus propios nombres (a este procedimiento es al que se refiere el argumento `...` descrito en la documentación de `apply`).
Supongamos que deseamos encontrar los cuantiles de un vector, correspondientes a las probabilidades **.33** y **.66**. Esto es definido con el argumento `probs` de esta función.
Para ello, usamos `quantile()` y después de haber escrito el nombre de la función, escribimos el nombre del argumento probs y los valores que deseamos para este.
```{r, echo = TRUE}
apply(X = matriz, MARGIN = 2, FUN = quantile, probs = c(.33, .66))
```
Como podrás ver, hemos obtenido los resultados esperados.
Si además deseamos que el resultado aparezca sin nombres, entonces definimos el valor del argumento `names` de la misma manera.
```{r, echo=TRUE}
apply(matriz, 2, quantile, probs = c(.33, .66), names = FALSE)
```
De este modo es posible aplicar funciones complejas que aceptan múltiples argumentos, con la ventaja que usamos pocas líneas de código.
### ¿Qué tipo de resultados devuelve apply?
En los ejemplos anteriores, el resultado de `apply()` en algunas ocasiones fue un vector y en otros fue una matriz.
Si aplicamos `mean()`, obtenemos como resultado un vector.
```{r, echo=TRUE}
mat_media <- apply(matriz, 1, mean)
class(mat_media)
```
Pero si aplicamos `quantile()`, obtenemos una matriz.
```{r, echo=TRUE}
mat_cuant <- apply(matriz, 1, quantile)
class(mat_cuant)
```
Este comportamiento se debe a que **`apply()` nos devolverá objetos del mismo tipo que la función aplicada devuelve**. Dependiendo de la función, será el tipo de objeto que obtengamos.
Sin embargo, este comportamiento puede causarte algunos problemas. En primer lugar, anterior te obliga a conocer de antemano el tipo del resultado que obtendrás, lo cual no siempre es fácil de determinar, en particular si las funciones que estás utilizando son poco comunes o tienen comportamientos poco convencionales.
Cuando estás trabajando en proyectos en los que el resultado de una operación será usado en operaciones posteriores, corres el riesgo de que en alguna parte del proceso, un `apply()` te devuelva un resultado que te impida continuar adelante.
Con algo de práctica es más o menos sencillo identificar problemas posibles con los resultados de `apply()`, pero es algo que debes tener en cuenta, pues puede explicar por qué tu código no funciona como esperabas.
En este sentido, `lapply()` tiene la ventaja de que siempre devuelve una lista.
## lapply
`lapply()` es un caso especial de `apply()`, diseñado para **aplicar funciones a todos los elementos de una lista**. La **l** de su nombre se refiere, precisamente, a **lista**.
`lapply()` intentará coercionar a una lista el objeto que demos como argumento y después aplicará una función a todos sus elementos.
`lapply` siempre nos devolverá una lista como resultado. A diferencia de `apply`, sabemos que siempre obtendremos un objeto de tipo lista después de aplicar una función, sin importar cuál función sea.
Dado que en R todas las estructuras de datos pueden coercionarse a una lista, `lapply()` puede usarse en un número más amplio de casos que `apply()`, además de que esto nos permite utilizar funciones que aceptan argumentos distintos a vectores.
La estructura de esta función es:
```{r, eval=FALSE}
lapply(X, FUN)
```
En donde:
* `X` es una lista o un objeto coercionable a una lista.
* `FUN` es la función a aplicar.
Estos argumentos son idéntico a los de `apply()`, pero a diferencia aquí no especificamos `MARGIN`, pues las listas son estructuras con una unidimensionales, que sólo tienen largo.
### Usando lapply()
Probemos `lapply()` aplicando una función a un data frame. Usaremos el conjunto de datos `trees`, incluido por defecto en R *base*.
`trees` contiene datos sobre el grueso, alto y volumen de distinto árboles de cerezo negro. Cada una de estas variables está almacenada en una columna del data frame.
Veamos los primeros cinco renglones de `trees`.
```{r, echo=TRUE}
trees[1:5, ]
```
Aplicamos la función `mean()`, usando su nombre.
```{r, echo=TRUE}
lapply(X = trees, FUN = mean)
```
Dado que un data frame está formado por columnas y cada columna es un vector atómico, cuando usamos `lapply()` , la función es aplicada a cada columna. `lapply()`, a diferencia de `apply()` no puede aplicarse a renglones.
En este ejemplo, obtuvimos la media de grueso (Girth), alto (Height) y volumen (Volume), como una lista.
Verificamos que la clase de nuestro resultado es una lista con `class()`.
```{r, echo=TRUE}
arboles <- lapply(X = trees, FUN = mean)
class(arboles)
```
Esto es muy conveniente, pues la recomendación para almacenar datos en un data frame es que cada columna represente una variable y cada renglón un caso (por ejemplo, el enfoque **tidy** de [Wickham (2014)](https://www.jstatsoft.org/article/view/v059i10/v59i10.pdf)). Por lo tanto, con `lapply()` podemos manipular y transformar datos, por variable.
Al igual que con `apply()`, podemos definir argumentos adicionales a las funciones que usemos, usando sus nombres, después del nombre de la función.
```{r, echo=TRUE}
lapply(X = trees, FUN = quantile, probs = .8)
```
Si usamos `lapply` con una matriz, la función se aplicará a cada **celda** de la matriz, no a cada columna.
Creamos una matriz.
```{r, echo=TRUE}
matriz <- matrix(1:9, ncol = 3)
# Resultado
matriz
```
Llamamos a `lapply()`.
```{r}
lapply(matriz, quantile, probs = .8)
```
Para usar una matriz con `lapply()` y que la función se aplique a cada columna, primero la coercionamos a un data frame con la función `as.data.frame()`
```{r, echo=TRUE}
lapply(as.data.frame(matriz), quantile, probs = .8)
```
Si deseamos aplicar una función a los renglones de una matriz, una manera de lograr es transponer la matriz con `t()` y después coercionar a un data frame.
```{r, echo=TRUE}
matriz_t <- t(matriz)
lapply(as.data.frame(matriz_t), quantile, probs = .8)
```
Con vectores como argumento, `lapply()` aplicará la función a cada elementos del vector, de manera similar a una vectorización de operaciones.
Por ejemplo, usamos `lapply()` para obtener la raíz cuadrada de un vector numérico del 1 al 4, con la función `sqrt()`.
```{r, echo=TRUE}
mi_vector <- 1:4
lapply(mi_vector, sqrt)
```
### Usando lapply() en lugar de un bucle for
En muchos casos es posible reemplazar un bucle `for()` por un `lapply()`.
De hecho, `lapply()` está haciendo lo mismo que un `for()`, está iterando una operación en todos los elementos de una estructura de datos.
Por lo tanto, el siguiente código con un `for()`...
```{r, echo=TRUE}
mi_vector <- 6:12
resultado <- NULL
posicion <- 1
for(numero in mi_vector) {
resultado[posicion] <- sqrt(numero)
posicion <- posicion + 1
}
resultado
```
... nos dará los mismos resultados que el siguiente código con `lapply()`.
```{r, echo=TRUE}
resultado <- NULL
resultado <- lapply(mi_vector, sqrt)
resultado
```
El código con `lapply()` es mucho más breve y más sencillo de entender, al menos para otros usuarios de R.
El inconveniente es que obtenemos una lista como resultado en lugar de un vector, pero eso es fácil de resolver usando la función `as.numeric()` para hacer coerción a tipo numérico.
```{r, echo=TRUE}
as.numeric(resultado)
```
El siguiente código es la manera en la que usamos `for()` si deseamos aplicar una función a todas sus columnas, tiene algunas partes que no hemos discutido, pero es sólo para ilustrar la diferencia simplemente usar `trees_max <- lapply(trees, max)`.
```{r, echo=TRUE}
trees_max <- NULL
i <- 1
columnas <- ncol(trees)
for(i in 1:columnas) {
trees_max[i] <- max(trees[, i])
i <- i +1
}
trees_max
```
### Usando lapply con listas
Hasta hora hemos hablado de usar `lapply()` con objetos que pueden coercionarse a una lista, pero ¿qué pasa si usamos esta función con una lista que contiene a otros objetos?
Pues la función se aplicará a cada uno de ellos. Por lo tanto, así podemos utilizar funciones que acepten todo tipo de objetos como argumento. Incluso podemos aplicar funciones a listas recursivas, es decir, listas de listas.
Por ejemplo, obtendremos el coeficiente de correlación de cuatro data frames contenidos en una sola lista. Esto no es posible con `apply()`, porque sólo podemos usar funciones que aceptan vectores como argumentos, pero con `lapply()` no es ningún problema.
Empezaremos creando una lista de data frames. Para esto, usaremos las función `rnorm()`, que genera números al azar y `set.seed()`, para que obtengas los mismos resultados aquí mostrados.
`rnorm()` creara `n` números al azar (pseudoaleatorios, en realidad), sacados de una distribución normal con media 0 y desviación estándar 1. `set.seed()` es una función que "fija" los resultados de una generación de valores al azar. Cada que ejecutas `rnorm()` obtienes resultados diferentes, pero si das un número como argumento `seed` a `set.seed()`, siempre obtendrás los mismos números.
```{r, echo = TRUE}
# Fijamos seed
set.seed(seed = 2018)
# Creamos una lista con tres data frames dentro
tablas <- list(
df1 = data.frame(a = rnorm(n = 5), b = rnorm(n = 5), c = rnorm(n = 5)),
df2 = data.frame(d = rnorm(n = 5), e = rnorm(n = 5), f = rnorm(n = 5)),
df3 = data.frame(g = rnorm(n = 5), h = rnorm(n = 5), i = rnorm(n = 5))
)
# Resultado
tablas
```
Para obtener el coeficiente de correlación usaremos la función `cor()`.
Esta función acepta como argumento una data frame o una matriz. Con este objeto, calculará el coeficiente de correlación **R de Pearson** existente entre cada una de sus columnas. Como resultado obtendremos una matriz de correlación.
Por ejemplo, este es el resultado de aplicar `cor()` a `iris`.
```{r, echo=TRUE}
cor(iris[1:4])
```
Con `lapply` aplicaremos `cor()` a cada uno de los data frames contenidos en nuestra lista. El resultado será una lista de matrices de correlaciones.
Esto lo logramos con una línea de código.
```{r, echo=TRUE}
lapply(X = tablas, FUN = cor)
```
De esta manera puedes manipular información de múltiples data frames, matrices o listas con muy pocas líneas de código y, en muchos casos, más rápidamente que con las alternativas existentes.
Finalmente, si asignamos los resultados de las última operación a un objeto, podemos usarlos y manipularlos de la misma manera que cualquier otra lista.
```{r, echo = TRUE}
correlaciones <- lapply(tablas, cor)
# Extraemos el primer elemento de la lista
correlaciones[[1]]
```