Familia de funciones apply - Parte 4

Las funciones eapply(), rapply() y mapply()

Función eapply

Uso de Environments

En pocas palabras, en R un Environment es un sitio para almacenar variables, o valores que asignamos a objetos. Cada vez que se inicia una nueva sesión de R, todos los objetos creados se añadirán al Environment global. Si creamos el objeto x, este quedará entonces en el Environment predeterminado de R, la función ls() sirve para listar los objetos creados y en este caso comprobar que x existe.

x <- 28022020
ls()
## [1] "x"

Es posible asignar el Environment global a un objeto, digamos environment_1. Una cosa curiosa es que tras hacer esto, environment_1 es un objeto que está en el Environmet global, pero al mismo tiempo es el Environmet global.

environment_1 <- globalenv()
class(environment_1)
## [1] "environment"
ls()
## [1] "environment_1" "x"

Este caso podría interpretarse como usar una lista, tema que ya fue discutido en este post. Como el objeto environment_1 es el ambiente global, contiene al objeto x, que fue asignado al Environment global; y también se contiene a sí mismo de una manera algo cíclica…

environment_1$x
## [1] 28022020
environment_1$environment_1
## <environment: R_GlobalEnv>
environment_1$environment_1$environment_1
## <environment: R_GlobalEnv>
ls(environment_1$environment_1$environment_1$environment_1)
## [1] "environment_1" "x"

El objeto environment_1 podría, en palabras del buen docto Emmett Brown, crear una paradoja que destruiría el universo, así que lo mejor es eliminarlo. La sección 2.1.10 de la definición del lenguaje R ofrece una descripción más formal de lo que es un Environment.

rm(environment_1)
ls()
## [1] "x"

Es posible crear un Environment nuevo que sea independiente del Environment global, la cual llamaremos environment_2. Al crearlo, podemos ver que este se encuentra vacío en comparación al Environment global, el cual contiene a x y a environment_2. Además, si generamos directamente una nueva variable, digamos z, esta se asignará al Environment global.

environment_2 <- new.env()
environment_2
## <environment: 0x000000001164e8d8>
ls(globalenv())
## [1] "environment_2" "x"
ls(environment_2)
## character(0)
z <- pi

ls(globalenv())
## [1] "environment_2" "x"             "z"
ls(environment_2)
## character(0)

Ahora crearemos una variable llamada y y se la asignaremos al environment_2. Tras hacer eso, la variable y estará contenida únicamente en el environment_2 y no en el Environment global, a pesar de que este último contiene al environment_2.

environment_2$y <- "Esto es muy abstracto"

ls(globalenv())
## [1] "environment_2" "x"             "z"
ls(environment_2)
## [1] "y"
environment(x)
## NULL

¿Los Environment sirven para algo más aparte de filosofar? La respuesta es que sí, y son fundamentales en algo muy utilizado en R: Las funciones. Por lo general, al construir una función se asume tiene únicamente dos componentes: los argumentos y el contenido de la función. Consideremos una sencilla función que invierte el signo de un número.

invertir_signo <- function(numero){
    numero*-1
}

En el caso anterior, la función invertir_signo() numero tiene un único argumento, mientras que su contenido es \(numero\cdot -1\). Sin embargo, las funciones cuentan con un tercer argumento: El Environment. Cuando la función invertir_signo() fue creada, quedó almacenada en el Environment global.

ls(globalenv())
## [1] "environment_2"  "invertir_signo" "x"              "z"
ls(environment_2)
## [1] "y"
environment(invertir_signo)
## <environment: R_GlobalEnv>

La consecuencia de esto es que la función invertir_signo() únicamente podrá ser evaluada en el Environment que tiene asignado. Por ejemplo, la función puede ser evaluada sobre el objeto x, que está en el environment global:

invertir_signo(x)
## [1] -28022020

Si creamos un objeto que también se llame x, pero que se encuentre en el environment_2, la función invertir_signo() seguirá siendo evaluada en el Environment global.

environment_2$x <- 123456
invertir_signo(x)
## [1] -28022020

La función eapply

Estos pequeños detalles que no suelen manejarse en la vida diaria pueden llegar a ser indispensables en ciertas situaciones, como cuando se quiere utilizar la función eapply(). Connociendo los aspectos más elementales de cómo trabajan los Environment, vamos a eliminar todos los objetos creados hasta ahora, esto para mantener el orden.

rm(list = ls())

Como también repasamos en este post, la función lapply() aplica una función a cada elemento de una lista. De forma similar, la función eapply() aplica una función a cada elemento nombrado en un Environment con la diferencia que en eaaply() el primer argumento es un Environment, y no una lista como en lapply(). vamos a crear entonces un nuevo Environment para aplicarle una función a sus elemntos que devuelva la raíz cuadrada de cada uno y sumarle 10:

environment_1 <- new.env()
environment_1$elemento_1 <- 4
environment_1$elemento_2 <- 9
environment_1$elemento_3 <- 25

eapply(environment_1, function(x){
    sqrt(x)+10
})
## $elemento_1
## [1] 12
## 
## $elemento_2
## [1] 13
## 
## $elemento_3
## [1] 15

El resultado puede solicitarse sin las etiquetas de los nombres:

eapply(environment_1, function(x){
    sqrt(x)+10
}, USE.NAMES = FALSE)
## [[1]]
## [1] 12
## 
## [[2]]
## [1] 13
## 
## [[3]]
## [1] 15

Las anteriores ejecuciones de la función eapply() evalúan nuestra función en todos los elemntos que contenga el Environment, pero hay excepciones si se tienen elementos ocultos. Los elementos ocultos son objetos que existen pero que no están a simple vista, podems crear el elemento_4 como oculto de la siguiente manera:

environment_1$.elemento_4 <- 48
ls(environment_1)
## [1] "elemento_1" "elemento_2" "elemento_3"

Si repetimos la evaluación anterior de la función eaaply(), se ejecutará solamente con los elementos visibles:

eapply(environment_1, function(x){
    sqrt(x)+10
})
## $elemento_1
## [1] 12
## 
## $elemento_2
## [1] 13
## 
## $elemento_3
## [1] 15

Pero podemos pedir que se ejecute sobre todos los elementos:

eapply(environment_1, function(x){
    sqrt(x)+10
}, all.names = TRUE)
## $.elemento_4
## [1] 16.9282
## 
## $elemento_1
## [1] 12
## 
## $elemento_2
## [1] 13
## 
## $elemento_3
## [1] 15

Función rapply

En esta función, la “r” hace referencia a “recursivo”. Podría decirse que tiene dos objetivos: Aplicar una función de manera recursiva a una lista o aplicar dicha función a solo los elementos de una lista con una clase específica. Esto de especial utilidad, pues como ya lo hemos discutido, las listas en R son quizás los objetos más útiles que hay, pues de manera muy sencilla almacenan bases de datos, números, cadenas de caracteres, gráficos, y más, por lo que resulta útil aplicar una función a, digamos, todos los elementos numéricos de una lista, sin la necesidad de conocer en qué posiciones se encuentran dichos elementos dentro de la lista. Un ejemplo sencillo se puede aplicar al famoso conjunto de datos iris:

rapply(iris, mean, class="numeric")
## Sepal.Length  Sepal.Width Petal.Length  Petal.Width 
##     5.843333     3.057333     3.758000     1.199333
rapply(iris, table, class="factor")
##     Species.setosa Species.versicolor  Species.virginica 
##                 50                 50                 50

O bien, si tenemos una lista con distintas clases de objetos, podemos pedir que nos multiplique por dos aquellos elementos de la lista que son numéricos:

rapply(list(2,5,7,"Esto no se puede multiplicar porque es una cadena de caracteres"), function(x){x*2}, class="numeric")
## [1]  4 10 14

Función mapply

La función mapply() puede verse como la versión multivariada de las funciones apply.Por ejemplo, con lapply() solo puede aplicarse la función a los elementos de una lista, pero si se tiene una lista cuyos elementos son un argumento de una función y otra lista a cuyos elementos son el otro argumento de la función, entonces se usa mapply(). La función que se va a aplicar debe tener un número de argumentos al menos igual al número de listas que se van a pasar a mapply. MoreArgs sirve en caso de que se tengan más argumentos que se necesiten pasar a la función. Resulta más sencillo mostrar su funcionamiento con un ejemplo que con palabras. Supongamos que queremos obtener el resultado de \(x*y+1\) variando los valores de \(x\) e \(y\) de la siguiente manera: \(1\cdot2+1,2\cdot3+1,3\cdot4+1,\cdots,10000\cdot10001+1\). La forma de obtener este cálculo mediante un ciclo for es la siguiente:

z <- NULL
k <- 1
x <- 1:10000
y <- 2:10001
for(i in 1:10000){
    z[k] <- x[i]*y[i]+1
    k <- k+1
}

Mientras que con la función mapply() se obtiene así:

mapply(function(x,y){x*y+1},
       x=1:10000,
       y=2:10001)

Ambas llegan al mismo resultado, sin embargo, mapply() es considerablemente más eficiente al realizar el cálculo. Compararemos los tiempos de ejecución de distintos procesos en el siguiente post, donde se mostrarán algunas versiones en paralelo de las funciones apply.

Relacionado

comments powered by Disqus
ORCID iD iconhttps://orcid.org/0000-0001-6733-4759