Recordatorio

Entrega de video para el miércoles 20 de octubre en el siguiente link

El formato en que se debe guardar el video en el link es el siguiente nombre del alumno - tema de la presentación.avi

También, deben revisar la pagina de intranet, donde podrán encontrar el enunciado del primer problem set, el cual tiene fecha de entrega el 3 de noviembre.

HTML de clase 7

Familia apply()

La familia apply() consta de funciones vectorizadas que minimizan la necesidad de crear explicitamente loops (o bucles). Estas funciones aplican a un objeto de datos una función especificada y su principal diferencia está en la clase de objeto en la que se aplica la función (lista, data.frame, matriz, etc.) y la clase de objeto que se devolverá de la función.

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.

apply()

La función apply() toma un array o (en el caso de un 2D array tenemos una matriz) como entrada y entrega un vector, lista o matriz. La sintaxis de la función es la siguiente:

apply(X, MARGIN, FUN)

Como vemos arriba 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 filas y 2 son colummnas.
  • FUN: La función que aplicaremos a la matriz X en su dimención MARGIN.
# Creemos una Matriz de 5x6
X <- matrix(1:30, nrow=5, ncol=6)

# Sumemos los valores de cada columna con apply()
apply(X, 2, FUN = sum)
## [1]  15  40  65  90 115 140
# Calculemos la media de cada fila con apply()
apply(X, 1,FUN = mean)
## [1] 13.5 14.5 15.5 16.5 17.5

¿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().

¿Qué pasa si deseamos utilizar los demás argumentos de una función con apply?

En los casos en los que una función acepta más de un argumento, asignamos los valores de estos en base a las instrucciones 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.

help("quantile")
apply(X, MARGIN = 2, FUN = quantile, probs = c(.33, .66))
##     [,1] [,2]  [,3]  [,4]  [,5]  [,6]
## 33% 2.32 7.32 12.32 17.32 22.32 27.32
## 66% 3.64 8.64 13.64 18.64 23.64 28.64

Como podrás ver, hemos obtenido los resultados esperados.

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 sea la función.

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:

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 unidimensionales, es decir, solo tienen largo.

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.

¿Se acuerdan que es una lista en R?

Una lista es una colección ordenada de cualquier objeto de R. A diferencia de los vectores y las matrices, donde los elementos deben ser del mismo tipo, en el caso de las listas los elementos pueden ser de un tipo diferente o almacenar distintas estructuras.

# Veamos los primeros cinco renglones de trees.
trees[1:5, ]

Aplicamos la función mean(), usando su nombre.

lapply(X = trees, FUN = mean)
## $Girth
## [1] 13.24839
## 
## $Height
## [1] 76
## 
## $Volume
## [1] 30.17097

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().

arboles <- lapply(X = trees, FUN = mean)

class(arboles)
## [1] "list"

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)). 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.

lapply(X = trees, FUN = quantile, probs = .8)
## $Girth
##  80% 
## 16.3 
## 
## $Height
## 80% 
##  81 
## 
## $Volume
##  80% 
## 42.6

Si usamos lapply con una matriz, la función se aplicará a cada celda de la matriz, no a cada columna.

Creamos una matriz.

matriz <- matrix(1:9, ncol = 3)

# Resultado
matriz
##      [,1] [,2] [,3]
## [1,]    1    4    7
## [2,]    2    5    8
## [3,]    3    6    9
#Llamamos a lapply().
lapply(matriz, quantile, probs = .8)
## [[1]]
## 80% 
##   1 
## 
## [[2]]
## 80% 
##   2 
## 
## [[3]]
## 80% 
##   3 
## 
## [[4]]
## 80% 
##   4 
## 
## [[5]]
## 80% 
##   5 
## 
## [[6]]
## 80% 
##   6 
## 
## [[7]]
## 80% 
##   7 
## 
## [[8]]
## 80% 
##   8 
## 
## [[9]]
## 80% 
##   9

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().

lapply(as.data.frame(matriz), quantile, probs = .8)
## $V1
## 80% 
## 2.6 
## 
## $V2
## 80% 
## 5.6 
## 
## $V3
## 80% 
## 8.6

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.

matriz_t <- t(matriz)

lapply(as.data.frame(matriz_t), quantile, probs = .8)
## $V1
## 80% 
## 5.8 
## 
## $V2
## 80% 
## 6.8 
## 
## $V3
## 80% 
## 7.8

Usando lapply() en lugar de un loop/bucle

En muchos casos es posible reemplazar un bucle for() por un lapply().

De hecho, lapply() está haciendo lo mismo que un for(), iterando una operación en todos los elementos de una estructura de datos.

Por lo tanto, podemos comparar el resultado de usar un una función en un loop, o usando la familia apply(). El siguiente código es con un for():

# install.packages("tictoc", dependencies = TRUE) Usaremos este paquete para comparar el performance del loop y de lapply
library(tictoc)
mi_vector <- 6:12
resultado <- NULL
posicion <- 1

tic(for(numero in mi_vector) {
  resultado[posicion] <- sqrt(numero)
  posicion <- posicion + 1
})

toc()
## 0.021 sec elapsed
resultado
## [1] 2.449490 2.645751 2.828427 3.000000 3.162278 3.316625 3.464102

Esto nos dará los mismos resultados que el siguiente código con lapply().

resultado <- NULL

tic()
resultado <- lapply(mi_vector, sqrt)

toc()
## 0.001 sec elapsed
resultado
## [[1]]
## [1] 2.44949
## 
## [[2]]
## [1] 2.645751
## 
## [[3]]
## [1] 2.828427
## 
## [[4]]
## [1] 3
## 
## [[5]]
## [1] 3.162278
## 
## [[6]]
## [1] 3.316625
## 
## [[7]]
## [1] 3.464102

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.

as.numeric(resultado)
## [1] 2.449490 2.645751 2.828427 3.000000 3.162278 3.316625 3.464102

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 estandar 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.

# 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
## $df1
##             a          b          c
## 1 -0.42298398 -0.2647112 -0.6430347
## 2 -1.54987816  2.0994707 -1.0300287
## 3 -0.06442932  0.8633512  0.7124813
## 4  0.27088135 -0.6105871 -0.4457721
## 5  1.73528367  0.6370556  0.2489796
## 
## $df2
##            d          e          f
## 1 -1.0741940  1.2638637 -0.2401222
## 2 -1.8272617  0.2501979 -1.0586618
## 3  0.0154919  0.2581954  0.4194091
## 4 -1.6843613  1.7855342 -0.2709566
## 5  0.2044675 -1.2197058 -0.6318248
## 
## $df3
##            g          h          i
## 1 -0.2284119 -0.4897908 -0.3594423
## 2  1.1786797  1.4105216 -1.2995363
## 3 -0.2662727 -1.0752636 -0.8698701
## 4  0.5281408  0.2923947  1.0543623
## 5 -1.7686592 -0.2066645 -0.1486396

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.

cor(iris[1:4])
##              Sepal.Length Sepal.Width Petal.Length Petal.Width
## Sepal.Length    1.0000000  -0.1175698    0.8717538   0.8179411
## Sepal.Width    -0.1175698   1.0000000   -0.4284401  -0.3661259
## Petal.Length    0.8717538  -0.4284401    1.0000000   0.9628654
## Petal.Width     0.8179411  -0.3661259    0.9628654   1.0000000

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.

lapply(X = tablas, FUN = cor)
## $df1
##            a          b          c
## a  1.0000000 -0.4427336  0.6355358
## b -0.4427336  1.0000000 -0.1057007
## c  0.6355358 -0.1057007  1.0000000
## 
## $df2
##            d          e         f
## d  1.0000000 -0.6960942 0.4709283
## e -0.6960942  1.0000000 0.2624429
## f  0.4709283  0.2624429 1.0000000
## 
## $df3
##            g          h          i
## g  1.0000000  0.6228793 -0.1472657
## h  0.6228793  1.0000000 -0.1211321
## i -0.1472657 -0.1211321  1.0000000

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.

correlaciones <- lapply(tablas, cor)

# Extraemos el primer elemento de la lista
correlaciones[[1]]
##            a          b          c
## a  1.0000000 -0.4427336  0.6355358
## b -0.4427336  1.0000000 -0.1057007
## c  0.6355358 -0.1057007  1.0000000

sapply()

Esta función tiene un uso similar a la función lapply() ya que puede ser usada con data.frames, vectores y listas. Se diferencia ya que esta imprime por consola un vector de valores y no una lista. La estructura está dada por

sapply(X, # Vector, list or expression object FUN, # Function to be applied ..., # Additional arguments to be passed to FUN simplify = TRUE, # If FALSE returns a list. If "array" returns an array if possible USE.NAMES = TRUE) # If TRUE and if X is a character vector, uses the names of }

Para usar la función sapply() en R, deberán especificar la lista o vector que desean iterar en el primer argumento y la función que se desea aplicar a cada elemento del vector en el segundo. Tengan en cuenta que se puede utilizar una función de cualquier paquete o una función personalizada:

sapply(1:4, sqrt)
## [1] 1.000000 1.414214 1.732051 2.000000

Como pueden ver, lo anterior es equivalente a:

my_fun <- function(i) {
    sqrt(i)
}

sapply(1:4, my_fun) 
## [1] 1.000000 1.414214 1.732051 2.000000

La diferencia entre las funciones lapply y sapply es que la función sapply es una envoltura (wrapper) de la función lapply y devuelve un vector o una matriz en lugar de una lista.

Considere que desea calcular el exponencial de tres números. En este caso, si usa la función sapply obtendrá un vector como salida:

x <- list(a = 1:10, b = c(1,10,100,1000), c = seq(5,50,5))
x
## $a
##  [1]  1  2  3  4  5  6  7  8  9 10
## 
## $b
## [1]    1   10  100 1000
## 
## $c
##  [1]  5 10 15 20 25 30 35 40 45 50
sapply(x, mean)
##      a      b      c 
##   5.50 277.75  27.50

Pero si usa la función lapply, obtendrá una lista donde cada elemento corresponde a los componentes del vector anterior.

lapply(x, mean)
## $a
## [1] 5.5
## 
## $b
## [1] 277.75
## 
## $c
## [1] 27.5
sapply(x, summary)
##             a       b     c
## Min.     1.00    1.00  5.00
## 1st Qu.  3.25    7.75 16.25
## Median   5.50   55.00 27.50
## Mean     5.50  277.75 27.50
## 3rd Qu.  7.75  325.00 38.75
## Max.    10.00 1000.00 50.00
lapply(x, summary)
## $a
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    1.00    3.25    5.50    5.50    7.75   10.00 
## 
## $b
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    1.00    7.75   55.00  277.75  325.00 1000.00 
## 
## $c
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    5.00   16.25   27.50   27.50   38.75   50.00

¿Qué pasa si usamos summary(x)?

summary( )
## Error in is.factor(object): argument "object" is missing, with no default

El resto de la familia apply

Existen más funciones dentro de la familia apply:

  • eapply()
  • mapply()
  • rapply()
  • tapply()
  • vapply()

Estas las veremos más adelante. Pero si quieren investigar, aqui hay un par de referencias:

link

link