rollercoaster-801833_640

 

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 que lapply, 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 argumentos
  • simplify: 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ón FUN.
  • 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.

 

 

 

 

 

Comentarios

1 Comment

  1. Joaquin A

    Excelente explicación y excelentes tips. Muchas gracias por compartir tu conocimiento.

    Reply

Submit a Comment

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

This site uses Akismet to reduce spam. Learn how your comment data is processed.