Tabla de Contenidos
Loop Functions
En los últimos dos post hemos visto las estructuras de control y las funciones en R. Comentamos que escribir bucles for
y while
es útil cuando programamos, pero no tanto cuando trabajamos en modo interactivo (consola). Para solucionar esto existen las loop functions, las cuales son funciones que permite aplicar una función a los elementos de una estructura de datos sin recurrir a bucles..A su vez, estas permiten realizar programación funcional en R y ayudan a mejorar la elegancia de nuestros códigos.
Las loop functions que veremos son:
lapply:
itera sobre una lista o vector y aplica una función a cada elemento de esta.sapply:
hace lo mismo quelapply
, pero al final simplifica el resultado de ser posible.apply:
aplica una función sobre columnas o filas de un array.tapply:
aplica una función sobre un subconjunto de un vector.mapply:
aplicar múltiples conjuntos de argumentos a una función.
lapply
La función lapply
aplica una función sobre los elementos de una lista o vector y retorna una lista con los elementos transformados:
(x1, x2, ..., xn) ⇒ f(x1), f(x1), ..., f(xn))
Veamos un ejemplo:
x <- list(a = floor(runif(5, 1, 10)), b = floor(runif(5, 1, 10)), c = floor(runif(5, 1, 10))) x $a [1] 4 4 5 1 7 $b [1] 2 9 7 8 8 $c [1] 8 3 8 3 3 lapply(X = x, FUN = max) $a [1] 7 $b [1] 9 $c [1] 8
En este caso tenemos una lista compuesta por tres elementos (a
, b
y c
) a los cuales mediante lapply
le aplicamos a cada uno la función max
(la cual devuelve el elemento de mayor valor de un vector). Observamos que el resultado es una nueva lista, en la que cada elemento resultante tiene el mismo nombre que el elemento original sobre el cual se aplicó la función.
Cuando utilizamos lapply
, esta toma los elementos de la lista y los pasa como primer argumento de la función que aplicará. ¿Qué hacemos entonces cuando necesitamos que a la función se le indiquen otros argumentos? Fácil, pasamos los restantes argumentos de la función luego del nombre de esta. Internamente, lapply
trabaja con el argumento ...
que habíamos visto en el post de funciones, de modo que captura todos los argumentos extra que pasemos pasándolos luego a la función que aplica.
x <- rep(5, 5) x [1] 5 5 5 5 5 lapply(x, rnorm, mean = 100, sd = 5) [[1]] [1] 101.42165 100.77961 96.19109 95.63834 99.82672 [[2]] [1] 100.1418 101.9446 103.6468 102.2501 104.1094 [[3]] [1] 105.19075 95.26370 100.25450 99.84487 108.98500 [[4]] [1] 99.50580 100.18585 95.31237 101.38840 105.75572 [[5]] [1] 99.96794 97.41872 109.72289 84.07879 96.06522
En este caso lo que buscamos es crear varias muestras de una distribución normal. Lo primero que hacemos es crear un vector x
con la cantidad de muestras que vayamos a generar y el tamaño de cada una. En este caso vamos a generar 5 grupos de muestras de tamaño 5, pero como no queremos que las muestras sea de una normal estándar (mean = 0
y sd = 1
), debemos pasar a lapply
los parámetros adecuados.
Ahora vemos otro caso.
x <- list(a = floor(runif(5, 1, 10)), b = floor(runif(5, 1, 10)), c = floor(runif(5, 1, 10))) x $a [1] 1 9 2 4 5 $b [1] 9 8 4 9 7 $c [1] 6 9 3 6 7 lapply(x, function(z){max(z) - min(z)}) $a [1] 8 $b [1] 5 $c [1] 6
Vemos que hemos utilizado una función que definimos dentro de lapply
, pero que no existe fuera de él, ni no posee nombre. A estas funciones se las llama funciones anónimas. El resultado de este ejemplo es el mismo que si hubiéramos definido la función fuera de lapply
.
# lapply(x, function(z){max(z) - min(z)}) f <- function(z) { max(z) - min(z) }lapply(x, f)
Definir la función o utilizarla en forma anónima depende del contexto, ya que el resultado obtenido es el mismo. En general, si la función es simple y la usaremos únicamente en una ocasión lo más elegante es usarla en forma anónima.
sapply
La siguiente función es sapply
, la cual trabaja de una manero muy similar a lapply
, pero cambia en la forma que presenta el resultado. Mientas que lapply
siempre devuelve una lista, sapply
trata de simplificar el resultado:
- Si el resultado es una lista donde cada elemento es de longitud 1, entonces esta se simplifica a un vector.
- Si el resultado es una lista donde cada elemento es un vector de igual longitud (mayor a 1), entonces esta se simplifica a una array (o matriz)
- Si lo anterior no se cumple, entonces se devuelve una lista.
Ejemplo:
x <- list(a = floor(runif(5, 1, 10)), b = floor(runif(5, 1, 10)), c = floor(runif(5, 1, 10))) x $a [1] 6 9 5 6 3 $b [1] 2 4 7 3 1 $c [1] 5 1 9 5 9 lapply(x, function(z){max(z) - min(z)}) $a [1] 6 $b [1] 6 $c [1] 8 sapply(x, function(z){max(z) - min(z)}) a b c 6 6 8
split
La función split
toma un vector y lo divide en grupos determinados por un factor o una lista de factores. Los argumentos principales de esta función son:
x:
es el vector, lista o data frame a ser divido.f:
es el factor (o lista de factores) en los que se dividiráx
.
Entonces:
x <- c(rnorm(5, mean = 0), rnorm(5, mean = 5), rnorm(5, mean = -5)) x [1] -0.7092309 1.6024699 0.2676539 -1.4502213 -0.2673208 4.8952157 4.7401708 5.8802186 6.6116415 [10] 4.0680763 -4.6925130 -6.0959817 -6.7935937 -5.8665151 -4.6716932 f <- gl(n = 3, k = 5) f [1] 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 Levels: 1 2 3 split(x, f) $`1` [1] -0.7092309 1.6024699 0.2676539 -1.4502213 -0.2673208 $`2` [1] 4.895216 4.740171 5.880219 6.611641 4.068076 $`3` [1] -4.692513 -6.095982 -6.793594 -5.866515 -4.671693
En este ejemplo primero creamos un vector x
compuesto de 3 grupos de 5 muestras cada uno. Luego queremos dividir cada grupo de muestras en una lista. Para esto primero creamos f
utilizando la función gl
(Generate Factor Levels) que nos permite crear una lista de factores (con n
indicamos la cantidad de factores, y con k
la cantidad de cada uno). Finalmente la función split
hace una asociación de valores del vector x
al factor que le corresponde (por posición) de f
. Los elementos que esten asociados a un mismo factor van a para a un mismo elemento de la lista.
La función split
se la suele utilizar en combinación con lapply
y sapply
. La idea es tomar una estructura de datos y dividirla en subconjuntos definidos por una variable categórica, y luego a cada uno de estos aplicarle una función. Finalmente se agrupan los resultados obtenidos en una nueva estructura de datos. Esta secuencia de operaciones se la suele conocer map-reduce.
Para ejemplificar, continuemos con el caso anterior. Mediante la función split
dividimos el vector en grupos, y ahora queremos calcular la media sobre cada uno de estos grupos.
lapply(X = split(x, f), FUN = mean) $`1` [1] -0.1113298 $`2` [1] 5.239065 $`3` [1] -5.624059
Veamos ahora un ejemplo utilizando el dataset mtcars
.
head(mtcars)
mpg cyl disp hp drat wt qsec vs am gear carb
Mazda RX4 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4
Mazda RX4 Wag 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4
Datsun 710 22.8 4 108 93 3.85 2.320 18.61 1 1 4 1
Hornet 4 Drive 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1
Hornet Sportabout 18.7 8 360 175 3.15 3.440 17.02 0 0 3 2
Valiant 18.1 6 225 105 2.76 3.460 20.22 1 0 3 1
Queremos ver cuanto es el valor medio de cada variable (columna) según la cantidad de cilindros de los vehículos (cyl
). Lo primero es agrupar los modelos de auto según su la cantidad de cilindros mediante split
, y luego utilizar lapply
para obtener los promedios para cada subconjunto.
split(mtcars, mtcars$cyl) $`4` mpg cyl disp hp drat wt qsec vs am gear carb Datsun 710 22.8 4 108.0 93 3.85 2.320 18.61 1 1 4 1 Merc 240D 24.4 4 146.7 62 3.69 3.190 20.00 1 0 4 2 Merc 230 22.8 4 140.8 95 3.92 3.150 22.90 1 0 4 2 Fiat 128 32.4 4 78.7 66 4.08 2.200 19.47 1 1 4 1 Honda Civic 30.4 4 75.7 52 4.93 1.615 18.52 1 1 4 2 Toyota Corolla 33.9 4 71.1 65 4.22 1.835 19.90 1 1 4 1 Toyota Corona 21.5 4 120.1 97 3.70 2.465 20.01 1 0 3 1 Fiat X1-9 27.3 4 79.0 66 4.08 1.935 18.90 1 1 4 1 Porsche 914-2 26.0 4 120.3 91 4.43 2.140 16.70 0 1 5 2 Lotus Europa 30.4 4 95.1 113 3.77 1.513 16.90 1 1 5 2 Volvo 142E 21.4 4 121.0 109 4.11 2.780 18.60 1 1 4 2 $`6` mpg cyl disp hp drat wt qsec vs am gear carb Mazda RX4 21.0 6 160.0 110 3.90 2.620 16.46 0 1 4 4 Mazda RX4 Wag 21.0 6 160.0 110 3.90 2.875 17.02 0 1 4 4 Hornet 4 Drive 21.4 6 258.0 110 3.08 3.215 19.44 1 0 3 1 Valiant 18.1 6 225.0 105 2.76 3.460 20.22 1 0 3 1 Merc 280 19.2 6 167.6 123 3.92 3.440 18.30 1 0 4 4 Merc 280C 17.8 6 167.6 123 3.92 3.440 18.90 1 0 4 4 Ferrari Dino 19.7 6 145.0 175 3.62 2.770 15.50 0 1 5 6 $`8` mpg cyl disp hp drat wt qsec vs am gear carb Hornet Sportabout 18.7 8 360.0 175 3.15 3.440 17.02 0 0 3 2 Duster 360 14.3 8 360.0 245 3.21 3.570 15.84 0 0 3 4 Merc 450SE 16.4 8 275.8 180 3.07 4.070 17.40 0 0 3 3 Merc 450SL 17.3 8 275.8 180 3.07 3.730 17.60 0 0 3 3 Merc 450SLC 15.2 8 275.8 180 3.07 3.780 18.00 0 0 3 3 Cadillac Fleetwood 10.4 8 472.0 205 2.93 5.250 17.98 0 0 3 4 Lincoln Continental 10.4 8 460.0 215 3.00 5.424 17.82 0 0 3 4 Chrysler Imperial 14.7 8 440.0 230 3.23 5.345 17.42 0 0 3 4 Dodge Challenger 15.5 8 318.0 150 2.76 3.520 16.87 0 0 3 2 AMC Javelin 15.2 8 304.0 150 3.15 3.435 17.30 0 0 3 2 Camaro Z28 13.3 8 350.0 245 3.73 3.840 15.41 0 0 3 4 Pontiac Firebird 19.2 8 400.0 175 3.08 3.845 17.05 0 0 3 2 Ford Pantera L 15.8 8 351.0 264 4.22 3.170 14.50 0 1 5 4 Maserati Bora 15.0 8 301.0 335 3.54 3.570 14.60 0 1 5 8 lapply(split(mtcars, mtcars$cyl), colMeans) $`4` mpg cyl disp hp drat wt qsec vs am gear carb 26.6636364 4.0000000 105.1363636 82.6363636 4.0709091 2.2857273 19.1372727 0.9090909 0.7272727 4.0909091 1.5454545 $`6` mpg cyl disp hp drat wt qsec vs am gear carb 19.7428571 6.0000000 183.3142857 122.2857143 3.5857143 3.1171429 17.9771429 0.5714286 0.4285714 3.8571429 3.4285714 $`8` mpg cyl disp hp drat wt qsec vs am gear carb 15.1000000 8.0000000 353.1000000 209.2142857 3.2292857 3.9992143 16.7721429 0.0000000 0.1428571 3.2857143 3.5000000 sapply(split(mtcars, mtcars$cyl), colMeans) 4 6 8 mpg 26.6636364 19.7428571 15.1000000 cyl 4.0000000 6.0000000 8.0000000 disp 105.1363636 183.3142857 353.1000000 hp 82.6363636 122.2857143 209.2142857 drat 4.0709091 3.5857143 3.2292857 wt 2.2857273 3.1171429 3.9992143 qsec 19.1372727 17.9771429 16.7721429 vs 0.9090909 0.5714286 0.0000000 am 0.7272727 0.4285714 0.1428571 gear 4.0909091 3.8571429 3.2857143 carb 1.5454545 3.4285714 3.5000000
tapply
La función tapply
es usada para aplicar una función sobre un subconjunto de un vector. Esto sería como una combinación de las funciones split
y lapply
o sapply
, pero únicamente para vectores.
Los argumentos de tapply
son:
x:
el vector.INDEX:
factor, o lista de factores.FUN:
función a ser aplicada....:
resto de argumentossimplify:
si simplificar el resultado.
Entonces:
x <- c(rnorm(5, mean = 0), rnorm(5, mean = 5), rnorm(5, mean = -5)) f <- gl(n = 3, k = 5) tapply(X = x, INDEX = f, FUN = mean, simplify = TRUE) 1 2 3 -0.3474965 5.2008844 -4.7861727 tapply(X = x, INDEX = f, FUN = mean, simplify = FALSE) $`1` [1] -0.3474965 $`2` [1] 5.200884 $`3` [1] -4.786173 tapply(X = x, INDEX = f, FUN = range, simplify = TRUE) $`1` [1] -1.6484844 0.6662139 $`2` [1] 3.438505 6.561279 $`3` [1] -5.983519 -3.851376
En caso de que el resultado no pueda ser simplificado, aunque le pongamos simplify = TRUE
, el resultado será una lista.
apply
La función apply
es usada para evaluar una función sobre las filas o columnas de una matriz. Realmente puede ser utilizada para realizar operaciones sobre arrays, pero lo usual es utilizarla para trabajar con matrices.
Los argumentos de la función son:
x:
array o matriz.MARGIN:
vector que indica con que margenes del array nos quedamos (filas o columnas en cada de una matriz).FUN:
función a ser aplicada....:
resto de argumentos.
Entonces:
x <- matrix(rnorm(10), 2, 5) x [,1] [,2] [,3] [,4] [,5] [1,] -1.105622 -0.1478308 -0.7974648 -0.91208208 0.1560445 [2,] 0.235895 -0.6406140 1.7697260 -0.02703201 0.3445469 apply(x, 1, mean) % sobre las filas [1] -0.5613911 0.3365044 apply(x, 2, mean) % sobre las columnas [1] -0.4348637 -0.3942224 0.4861306 -0.4695570 0.2502957
Una forma más compacta y elegante de realizar sumas o promedios sobre columnas o filas de una matriz es mediante las funciones:
rowSums = apply(x, 1, sum)
rowMeans = apply(x, 1, mean)
colSums = apply(x, 2, sum)
colMeans = apply(x, 2, mean)
Sin embargo hay operaciones más complicadas para las cuales no hay funciones predefinidas, como calcular el desvío estándar, la mediana o los cuartiles. Para estos casos es que debemos usar apply
.
mapply
La función mapply
es una versión de apply
, la cual permite realizar múltiples llamadas una función (en paralelo) sobre un conjunto de argumentos.
Los argumentos de la función son:
FUN
: función a ser aplicada....
: resto de argumentos.MoreArgs
: lista de otros argumento para pasar a la funciónFUN
.simplify
: si simplificar el resultado.
Hay que notas que para esta función invertimos el orden que veníamos utilizando en los argumentos. En este caso, lo primero es la función que vamos a llamar, y luego los parámetros con que realizaremos las llamadas.
mapply(FUN = rnorm, 3, c(1,10,100), 1, SIMPLIFY = F) [[1]] [1] 0.4558360 1.4742719 0.9190874 [[2]] [1] 10.776188 9.920658 12.546668 [[3]] [1] 100.05986 99.71401 100.81646
En este caso estamos pasando a la función rnorm
los argumentos (3, 1, 1)
, (3, 10, 1)
y (3, 100, 1)
en paralelo. Como indicamos que al final no se simplifique el resultado obtenemos una lista, donde cada elemento de la lista es el resultado de la llamada a la función con cada grupo de paramentos.
Vectorización de funciones
La función mapply
puede ser usada para realizar llamadas a una función en paralelo. A esto se lo conoce como vectorizar una función, lo cual significa que estamos creando una nueva función que en vez de tomar un solo grupo de parámetros, puede tomar un conjunto de estos. Mediante la función Vectorize
podemos realizar lo mismo de una manera más clara.
Por ejemplo:
vrnorm <- Vectorize(FUN = rnorm, vectorize.args = c('n', 'mean', 'sd'), SIMPLIFY = F) vrnorm(5, c(1, 10, 100), 0.1) [[1]] [1] 0.9788635 0.9264093 0.7952612 0.9666706 1.1013841 [[2]] [1] 9.979132 10.171233 9.890230 9.872685 10.031089 [[3]] [1] 99.69378 100.06064 99.97483 100.12716 100.08537
En este caso estamos creando la función vrnorm
, la cual es una versión vectorizada de rnorm
a la que le podemos pasar los parámetros que indicamos en vectorize.args
. De esta forma, hemos creado una función que nos permite de manera compacta crear grupos de muestras aleatorias a partir de una distribución normal con distintos parámetros.
Excelente explicación y excelentes tips. Muchas gracias por compartir tu conocimiento.