Sesión 7 Funciones e iteracion
“To understand computations in R, two slogans are helpful:
Everything that exists is an object.
Everything that happens is a function call.”
— John Chambers
Funciones
En R todas las operaciones son producto de la llamada a una función, esto
incluye operaciones como +, operadores que controlan flujo como for, if y
while, e incluso operadores para obtener subconjuntos como [ ] y $.
a <- 3
b <- 4
a + b
#> [1] 7
`+`(a, b)
#> [1] 7
for (i in 1:2) print(i)
#> [1] 1
#> [1] 2
`for`(i, 1:2, print(i))
#> [1] 1
#> [1] 2Para escribir código eficiente y fácil de leer es importante saber escribir funciones, se dice que si hiciste copy-paste de una sección de tu código 3 o más veces es momento de escribir una función.
Escribimos una función para calcular un promedio ponderado:
wtd_mean <- function(x, wt = rep(1, length(x))) {
sum(x * wt) / sum(wt)
}Notemos que esta función recibe hasta dos argumentos:
x: el vector a partir del cual calcularemos el promedio ywt: un vector de ponderadores para cada componente del vectorx.
Notemos además que al segundo argumento le asignamos un valor predeterminado, esto implica que si no especificamos los ponderadores la función usará el valor predeterminado y promediara con mismo peso a todas las componentes.
wtd_mean(c(1:10))
#> [1] 5.5
wtd_mean(1:10, 10:1)
#> [1] 4Veamos como escribir una función que reciba un vector y devuelva el mismo vector centrado en cero.
- Comenzamos escribiendo el código para un caso particular, por ejemplo, reescalando el vector .
vec <- c(0, 5, 10)
vec - mean(vec)
#> [1] -5 0 5Una vez que lo probamos lo convertimos en función:
center_vector <- function(vec) {
vec - mean(vec)
}
center_vector(c(0, 5, 10))
#> [1] -5 0 5Ejercicio
Escribe una función que reciba un vector y devuelva el mismo vector reescalado al rango 0 a 1.
- Comienza escribiendo el código para un caso particular, por ejemplo,
empieza reescalando el vector . Tip: la función
range()devuelve el rango de un vector.
Estructura de una función
Las funciones de R tienen tres partes:
- El cuerpo: el código dentro de la función
body(wtd_mean)
#> {
#> sum(x * wt)/sum(wt)
#> }- Los formales: la lista de argumentos que controlan como puedes llamar a la función,
formals(wtd_mean)
#> $x
#>
#>
#> $wt
#> rep(1, length(x))- El ambiente: el mapeo de la ubicación de las variables de la función.
library(ggplot2)
environment(wtd_mean)
#> <environment: R_GlobalEnv>
environment(ggplot)
#> <environment: namespace:ggplot2>Veamos mas ejemplos, ¿qué regresan las siguientes funciones?
# 1
x <- 5
f <- function(){
y <- 10
c(x = x, y = y)
}
rm(x, f)
# 2
x <- 5
g <- function(){
x <- 20
y <- 10
c(x = x, y = y)
}
rm(x, g)
# 3
x <- 5
h <- function(){
y <- 10
i <- function(){
z <- 20
c(x = x, y = y, z = z)
}
i()
}
# 4 ¿qué ocurre si la corremos por segunda vez?
j <- function(){
if (!exists("a")){
a <- 5
} else{
a <- a + 1
}
print(a)
}
x <- 0
y <- 10
# 5 ¿qué regresa k()? ¿y k()()?
k <- function(){
x <- 1
function(){
y <- 2
x + y
}
}Las reglas de búsqueda determinan como se busca el valor de una variable libre en una función. A nivel lenguaje R usa lexical scoping, esto implica que en R los valores de los símbolos se basan en como se anidan las funciones cuando fueron creadas y no en como son llamadas.
f <- function(x) {
x + y
}f(2)
#> Error in f(2): object 'y' not foundSi creamos el objeto y.
y <- 1
f(2)
#> [1] 3Como consecuencia de las reglas de búsqueda de R, todos los objetos deben ser guardados en memoria y, si uno no es cuidadoso se pueden cometer errores fácilmente.
y <- 100
f(2)
#> [1] 102Observaciones del uso de funciones
- Cuando llamamos a una función podemos especificar los argumentos en base a posición, nombre completo o nombre parcial:
f <- function(abcdef, bcde1, bcde2) {
c(a = abcdef, b1 = bcde1, b2 = bcde2)
}
# Posición
f(1, 2, 3)
#> a b1 b2
#> 1 2 3
f(2, 3, abcdef = 1)
#> a b1 b2
#> 1 2 3
# Podemos abreviar el nombre de los argumentos
f(2, 3, a = 1)
#> a b1 b2
#> 1 2 3
# Siempre y cuando la abreviación no sea ambigua
f(1, 3, b = 1)
#> Error in f(1, 3, b = 1): argument 3 matches multiple formal arguments- Los argumentos de las funciones en R se evalúan conforme se necesitan (lazy evaluation),
f <- function(a, b){
a ^ 2
}
f(2)
#> [1] 4La función anterior nunca utiliza el argumento b, de tal manera que f(2) no produce ningún error.
- Funciones con el mismo nombre en distintos paquetes:
La función filter() (incluida en R base) aplica un filtro lineal a una serie
de tiempo de una variable.
x <- 1:100
filter(x, rep(1, 3))
#> Error in UseMethod("filter"): no applicable method for 'filter' applied to an object of class "c('integer', 'numeric')"Ahora cargamos dplyr.
library(dplyr)
filter(x, rep(1, 3))
#> Error in UseMethod("filter"): no applicable method for 'filter' applied to an object of class "c('integer', 'numeric')"R tiene un conflicto en la función a llamar, nosotros requerimos usar
filter de stats y no la función filter de dplyr. R utiliza por default
la función que pertenece al último paquete que se cargó.
search()
#> [1] ".GlobalEnv" "package:forcats" "package:stringr"
#> [4] "package:dplyr" "package:purrr" "package:readr"
#> [7] "package:tidyr" "package:tibble" "package:ggplot2"
#> [10] "package:tidyverse" "package:stats" "package:graphics"
#> [13] "package:grDevices" "package:utils" "package:datasets"
#> [16] "package:devtools" "package:usethis" "package:methods"
#> [19] "Autoloads" "package:base"Una opción es especificar el paquete en la llamada de la función:
stats::filter(x, rep(1, 3))
#> Time Series:
#> Start = 1
#> End = 100
#> Frequency = 1
#> [1] NA 6 9 12 15 18 21 24 27 30 33 36 39 42 45 48 51 54
#> [19] 57 60 63 66 69 72 75 78 81 84 87 90 93 96 99 102 105 108
#> [37] 111 114 117 120 123 126 129 132 135 138 141 144 147 150 153 156 159 162
#> [55] 165 168 171 174 177 180 183 186 189 192 195 198 201 204 207 210 213 216
#> [73] 219 222 225 228 231 234 237 240 243 246 249 252 255 258 261 264 267 270
#> [91] 273 276 279 282 285 288 291 294 297 NAComo alternativa surge el paquete conflicted que alerta cuando hay conflictos y tiene funciones para especificar a que paquete se desea dar preferencia en una sesión de R.
Iteración
En análisis de datos es común implementar rutinas iteraivas, esto es, cuando debemos aplicar los mismos pasos a distintas entradas. Veremos que hay dos paradigmas de iteración:
Programación imperativa: ciclos
fory cicloswhile.Programación funcional: los ciclos se implmentan mediante funciones,
La ventaja de la programación imperativa es que hacen la iteración de manera clara, sin embargo, veremos que una vez que nos familiarizamos con el paradigma de programación funcional, resulta en código más fácil de mantener y menos propenso a errores.
Ciclos for
Supongamos que tenemos una base de datos y queremos calcular la media de sus columnas numéricas.
df <- data.frame(id = 1:10, a = rnorm(10), b = rnorm(10, 2), c = rnorm(10, 3),
d = rnorm(10, 4))
df
#> id a b c d
#> 1 1 2.10139129 -0.7080430 1.946773 3.443728
#> 2 2 0.20651252 2.0375054 4.142905 4.202773
#> 3 3 0.86459014 2.0110593 2.217115 3.916508
#> 4 4 0.24726256 2.9644628 2.697344 3.507722
#> 5 5 -0.82649354 1.6438195 2.767733 4.119950
#> 6 6 -0.74763335 2.7516632 3.056634 2.377507
#> 7 7 -0.04359498 3.4777091 3.058397 4.717200
#> 8 8 0.35599948 0.3240221 3.288053 5.205653
#> 9 9 2.33756513 2.8398525 4.243360 3.654584
#> 10 10 0.83996573 2.8578998 2.581073 4.608858Podemos crear el código para cada columna pero esto involucra copy-paste y no será muy práctico si aumenta el número de columnas:
mean(df$a)
#> [1] 0.5335565
mean(df$b)
#> [1] 2.019995
mean(df$c)
#> [1] 2.999939
mean(df$d)
#> [1] 3.975448Con un ciclo for sería:
salida <- vector("double", 4)
for (i in 1:4) {
salida[[i]] <- mean(df[[i+1]])
}
salida
#> [1] 0.5335565 2.0199951 2.9999386 3.9754484Los ciclos for tienen 3 componentes:
La salida:
salida <- vector("double", 4). Es importante especificar el tamaño de la salida antes de iniciar el ciclofor, de lo contrario el código puede ser muy lento.La secuencia: determina sobre que será la iteración, la función
seq_alongpuede resultar útil.
salida <- vector("double", 5)
for (i in seq_along(df)) {
salida[[i]] <- mean(df[[i]])
}
seq_along(df)
#> [1] 1 2 3 4 5- El cuerpo:
salida[[i]] <- mean(df[[i]]), el código que calcula lo que nos interesa sobre cada objeto en la iteración.
Ejercicio
- Calcula el número de valores únicos en cada columna de los datos
iris.
head(iris)
#> Sepal.Length Sepal.Width Petal.Length Petal.Width Species
#> 1 5.1 3.5 1.4 0.2 setosa
#> 2 4.9 3.0 1.4 0.2 setosa
#> 3 4.7 3.2 1.3 0.2 setosa
#> 4 4.6 3.1 1.5 0.2 setosa
#> 5 5.0 3.6 1.4 0.2 setosa
#> 6 5.4 3.9 1.7 0.4 setosa- Recordando la limpieza de datos de la sección anterior en uno de los
últimos ejercicios leíamos archivos de manera iteativa, el vector
pathscontenía la ruta a distintos archivos csv. Crea la tabla de datos final usando un ciclofor.
paths <- list.files("data/specdata", pattern = ".csv", full.names = TRUE) Programación funcional
Ahora veremos como abordar iteración usando programación funcional.
Regresando al ejemplo inicial de calcular la media de las columnas de una tabla de datos:
salida <- vector("double", 4)
for (i in 1:4) {
salida[[i]] <- mean(df[[i+1]])
}
salida
#> [1] 0.5335565 2.0199951 2.9999386 3.9754484Podemos crear una función que calcula la media de las columnas de un
data.frame:
col_media <- function(df) {
salida <- vector("double", length(df))
for (i in seq_along(df)) {
salida[i] <- mean(df[[i]])
}
salida
}
col_media(df)
#> [1] 5.5000000 0.5335565 2.0199951 2.9999386 3.9754484
col_media(select(iris, -Species))
#> [1] 5.843333 3.057333 3.758000 1.199333Y podemos extender a crear más funciones que describan los datos:
col_mediana <- function(df) {
salida <- vector("double", length(df))
for (i in seq_along(df)) {
salida[i] <- median(df[[i]])
}
salida
}
col_sd <- function(df) {
salida <- vector("double", length(df))
for (i in seq_along(df)) {
salida[i] <- sd(df[[i]])
}
salida
}Podemos hacer nuestro código más general y compacto escribiendo una función que reciba los datos sobre los que queremos iterar y la función que queremos aplicar:
col_describe <- function(df, fun) {
salida <- vector("double", length(df))
for (i in seq_along(df)) {
salida[i] <- fun(df[[i]])
}
salida
}
col_describe(df, median)
#> [1] 5.500000 0.301631 2.394584 2.912184 4.018229
col_describe(df, mean)
#> [1] 5.5000000 0.5335565 2.0199951 2.9999386 3.9754484Ahora utilizaremos esta idea de pasar funciones a funciones para eliminar los
ciclos for.
La iteración a través de funciones es muy común en R, hay funciones para hacer
esto en R base (lapply(), apply(), sapply()). Nosotros utilizaremos las
funciones del paquete purrr,
La familia de funciones del paquete iteran siempre sobre un vector (vector atómico o lista), aplican una función a cada parte y regresan un nuevo vector de la misma longitud que el vector entrada. Cada función especifica en su nombre el tipo de salida:
map()devuelve una lista.map_lgl()devuelve un vector lógico.map_int()devuelve un vector entero.map_dbl()devuelve un vector double.map_chr()devuelve un vector caracter.
En el ejemplo de las medias, map puede recibir un data.frame (lista de
vectores) y aplicará las funciones a las columnas del mismo.
library(purrr)
map_dbl(df, mean)
#> id a b c d
#> 5.5000000 0.5335565 2.0199951 2.9999386 3.9754484
map_dbl(select(iris, -Species), median)
#> Sepal.Length Sepal.Width Petal.Length Petal.Width
#> 5.80 3.00 4.35 1.30Podemos crear un vector de tipo caracter que nos almacene el tipo de dato almacenado en cada columna de la tabla iris.
map_chr(iris, class)
#> Sepal.Length Sepal.Width Petal.Length Petal.Width Species
#> "numeric" "numeric" "numeric" "numeric" "factor"Si queremos calcular el número de faltantes en cada columna podemos hacer:
sum(is.na(airquality$Ozone))
#> [1] 37
map_int(airquality, function(x) sum(is.na(x)))
#> Ozone Solar.R Wind Temp Month Day
#> 37 7 0 0 0 0O utilizar la notación .:
map_int(airquality, ~ sum(is.na(.)))
#> Ozone Solar.R Wind Temp Month Day
#> 37 7 0 0 0 0Usaremos map para ajustar un modelo lineal a subconjuntos de los datos
mtcars determinados por el cilindraje del motor.
models <- mtcars |>
group_split(cyl) |>
map(function(df) lm(mpg ~ wt, data = df))Podemos usar la notación . para hacer código más corto:
models <- mtcars |>
group_split(cyl) |>
map(~lm(mpg ~ wt, data = .))
models
#> [[1]]
#>
#> Call:
#> lm(formula = mpg ~ wt, data = .)
#>
#> Coefficients:
#> (Intercept) wt
#> 39.571 -5.647
#>
#>
#> [[2]]
#>
#> Call:
#> lm(formula = mpg ~ wt, data = .)
#>
#> Coefficients:
#> (Intercept) wt
#> 28.41 -2.78
#>
#>
#> [[3]]
#>
#> Call:
#> lm(formula = mpg ~ wt, data = .)
#>
#> Coefficients:
#> (Intercept) wt
#> 23.868 -2.192Usemos map_** para unir tablas de datos que están almacenadas en múltiples
archivos csv.
names(paths) <- basename(paths)
specdata_us_vec <- map(paths, ~readr::read_csv(., col_types = "Tddi"),
.id = "filename")
specdata_us_vec[[2]]
#> # A tibble: 3,652 × 4
#> Date sulfate nitrate ID
#> <dttm> <dbl> <dbl> <int>
#> 1 2001-01-01 00:00:00 NA NA 2
#> 2 2001-01-02 00:00:00 NA NA 2
#> 3 2001-01-03 00:00:00 NA NA 2
#> 4 2001-01-04 00:00:00 NA NA 2
#> 5 2001-01-05 00:00:00 NA NA 2
#> 6 2001-01-06 00:00:00 NA NA 2
#> 7 2001-01-07 00:00:00 NA NA 2
#> 8 2001-01-08 00:00:00 NA NA 2
#> 9 2001-01-09 00:00:00 NA NA 2
#> 10 2001-01-10 00:00:00 NA NA 2
#> # … with 3,642 more rows
#> # ℹ Use `print(n = ...)` to see more rows
class(specdata_us_vec)
#> [1] "list"En este caso es más apropiado usar map_df
specdata_us <- map_df(paths, ~readr::read_csv(., col_types = "Tddi"),
.id = "filename")Ejercicio
Usa la función
map_**para calcular el número de valores únicos en las columnas deiris.Usa la función
map_**para extraer el coeficiete de la variablewtpara cada modelo:
models[[1]]$coefficients[2]
#> wt
#> -5.647025Utiliza map_*** para crear un vector con el valor mínimo de cada columna de los datos
airquality. ¿Cómo eliminas los faltantes?Con el siguiente código agregamos una variable categórica de velocidad del aire a los datos
airqualuty, para cada categoría ajusta un modelo lineal (con la funciónlm) para explorar la relación entre ozono y radiación solar.
airquality$Wind.cat <- cut(airquality$Wind, breaks = c(1.5, 9, 12, 21)) Puedes descargar un acordeón de purrr aquí.