3.2 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:

  1. Programación imperativa: ciclos for y ciclos while.

  2. 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 -0.8138618 0.7842802  3.32045733 3.537299
#> 2   2  0.4934474 2.7447675  4.29982251 3.993813
#> 3   3 -0.4683543 2.3341549  3.51770246 4.258450
#> 4   4  0.6093561 1.2658810 -0.02536319 4.710169
#> 5   5  1.3414755 1.1707251  2.65192822 5.449400
#> 6   6  0.6717275 1.5095553  3.33444766 4.941827
#> 7   7 -0.5024535 2.6617354  5.16641842 3.849903
#> 8   8  1.0853923 2.8980573  2.92306011 3.913334
#> 9   9 -0.6691080 0.6441956  0.86357026 4.500421
#> 10 10  2.0540984 1.0897679  2.42112451 4.076140

Podemos 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.380172
mean(df$b)
#> [1] 1.710312
mean(df$c)
#> [1] 2.847317
mean(df$d)
#> [1] 4.323075

Con un ciclo for sería:

salida <- vector("double", 4)  
for (i in 1:4) {            
  salida[[i]] <- mean(df[[i+1]])      
}
salida
#> [1] 0.380172 1.710312 2.847317 4.323075

Los ciclos for tienen 3 componentes:

  1. La salida: salida <- vector("double", 4). Es importante especificar el tamaño de la salida antes de iniciar el ciclo for, de lo contrario el código puede ser muy lento.

  2. La secuencia: determina sobre que será la iteración, la función seq_along puede 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
  1. 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 paths contenía la ruta a distintos archivos csv. Crea la tabla de datos final usando un ciclo for.
paths <- dir("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.380172 1.710312 2.847317 4.323075

Podemos 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.500000 0.380172 1.710312 2.847317 4.323075
col_media(select(iris, -Species))
#> [1] 5.843333 3.057333 3.758000 1.199333

Y 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.5000000 0.5514017 1.3877181 3.1217587 4.1672951
col_describe(df, mean)
#> [1] 5.500000 0.380172 1.710312 2.847317 4.323075

Ahora 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.500000 0.380172 1.710312 2.847317 4.323075
map_dbl(select(iris, -Species), median)
#> Sepal.Length  Sepal.Width Petal.Length  Petal.Width 
#>         5.80         3.00         4.35         1.30

Usaremos map para ajustar un modelo lineal a subconjuntos de los datos mtcars determinados por el cilindraje del motor.

models <- mtcars %>% 
  split(.$cyl) %>% 
  map(function(df) lm(mpg ~ wt, data = df))

Podemos usar la notación . para hacer código más corto:

models <- mtcars %>% 
  split(.$cyl) %>% 
  map(~lm(mpg ~ wt, data = .))

Usemos 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 x 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
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 de iris.

  • Usa la función map_** para extraer el coeficiete de la variable wt para cada modelo:

models[[1]]$coefficients[2]
#>        wt 
#> -5.647025
  • Utiliza map_*** para crear un vector con la media de nitrato de cada estación de monitoreo, itera sobre el vector specdata_us_vec.