27  Otras nociones importantes

A lo largo de esta unidad hemos explorado las principales estructuras de datos que ofrece R, como vectores, matrices y listas, y aprendimos a manipularlas mediante operaciones tanto elementales como vectorizadas. En este último capítulo, reunimos algunas herramientas y conceptos adicionales que complementan y potencian el trabajo con estructuras de datos. Veremos, por un lado, funciones de la familia apply, que permiten realizar operaciones repetitivas de forma eficiente. También abordaremos formas de generar secuencias numéricas y de combinar estructuras como vectores, matrices y listas, operaciones fundamentales para construir y transformar datos. Por último, introduciremos los arreglos multidimensionales, una extensión natural de las matrices que permite trabajar con datos en más de dos dimensiones.

27.1 Familia de funciones apply

Como ya sabemos, una de las fortalezas del lenguaje R es la existencia de funciones que permiten aplicar operaciones de manera vectorizada o sobre estructuras de datos completas, evitando escribir estructuras de repetición explícitas como los bucles for. En particular, existe un conjunto de funciones conocidas como la familia apply que permiten resumir muchas tareas repetitivas con una sintaxis concisa, adaptándose a diferentes estructuras de datos.

27.1.1 Función apply

Supongamos que queremos encontrar el máximo valor en cada fila de una matriz. Podemos lograrlo de la siguiente forma. Creamos un vector maximos con lugar para guardar el máximo de cada fila. Luego, iteramos para recorrer cada fila de la matriz, buscar el máximo y guardarlo en el vector maximos:

m <- matrix(c(4,-2, 1, 20, -7, 12, -8, 13, 17), nrow = 3)
m
     [,1] [,2] [,3]
[1,]    4   20   -8
[2,]   -2   -7   13
[3,]    1   12   17
maximos <- numeric(nrow(m))
for (i in 1:nrow(m)) {
    maximos[i] <- max(m[i, ])
}
maximos
[1] 20 13 17

En R existe una forma más práctica y eficiente de conseguir el mismo resultado:

apply(m, 1, max)
[1] 20 13 17

La función apply() sirve para aplicar una misma operación a cada fila o columna de una matriz. En el ejemplo anterior:

  • el primer argumento, m, es la matriz a analizar.
  • el segundo argumento, 1, indica que la operación se realizará fila por fila (para que se haga por columna, debemos indicar 2)
  • el tercer argumento, max, es el nombre de la función que se le aplica a cada fila.

De manera similar, podemos encontrar el mínimo valor de cada columna:

apply(m, 2, min)
[1] -2 -7 -8

27.1.2 Funciones lapply() y sapply()

lapply(X, FUN) aplica una función FUN a cada elemento de una lista o vector y devuelve siempre una lista como resultado:

lista1 <- list(a = 1:5, b = 6:10, c = 11:15)

sapply() funciona igual que lapply(), pero intenta simplificar la salida: si todos los resultados son del mismo tipo y longitud, devuelve un vector o matriz en lugar de una lista:

sapply(lista1, mean)
 a  b  c 
 3  8 13 

Si la simplificación no es posible, el resultado será una lista, igual que con lapply().

En el caso de que la lista tenga elementos de distinto tipo, la función a aplicar debe ser admisible para cada uno de ellos, si no se generará un error:

lista2 <- list(
    w = c(-4.5, 12, 2.71),
    x = c("hola", "chau"),
    y = matrix(c(8, 11, 13, 16), nrow = 2),
    z = TRUE
)

# Vemos el largo de cada elemento de la lista
lapply(lista2, length)
$w
[1] 3

$x
[1] 2

$y
[1] 4

$z
[1] 1
sapply(lista2, length)
w x y z 
3 2 4 1 
# Vemos la suma de cada elemento de la lista: no se puede con algunos
lapply(lista2, sum)
Error in FUN(X[[i]], ...): invalid 'type' (character) of argument
sapply(lista2, sum)
Error in FUN(X[[i]], ...): invalid 'type' (character) of argument

27.1.3 Otras funciones

Dentro de la familia apply también se encuentran las funciones mapply() (versión multivariada de sapply(), que aplica una función a varios vectores o listas en paralelo), rapply() (aplica una función de forma recursiva sobre listas anidadas), tapply() (aplica una función a subconjuntos de datos) y vapply() (requiere que especifiquemos el tipo de salida esperado). No nos ocuparemos de usarlas en este curso.

Además de la familia apply, R ofrece otras herramientas muy potentes para realizar operaciones repetitivas de forma clara y expresiva. En particular, el ecosistema tidyverse introduce funciones como map(), map_dbl(), map_df() y otras variantes, que permiten aplicar funciones sobre listas y otros objetos de manera muy cómoda. Estas funciones combinan la versatilidad de lapply() y sapply() con una sintaxis más moderna y consistente, y ofrecen un control más explícito sobre los tipos de salida. Además, el tidyverse ofrece muchas opciones para realizar tareas repetitivas sobre conjuntos de datos. Si bien no forman parte de la base de R, son ampliamente utilizadas en la práctica y constituyen una alternativa muy recomendable para quienes ya trabajan con herramientas de dicho sistema de paquetes.

27.2 Generación de vectores con secuencias numéricas

A continuación mostramos cómo generar algunos vectores numéricos en R (algunos casos ya los estuvimos usando):

# Enteros de 1 a 5
1:5
[1] 1 2 3 4 5
# Números de 1 a 10 cada 2
seq(1, 10, 2)
[1] 1 3 5 7 9
# Números de 0 a -1 cada -0.1
seq(0, -1, -0.1)
 [1]  0.0 -0.1 -0.2 -0.3 -0.4 -0.5 -0.6 -0.7 -0.8 -0.9 -1.0
# Siete números equiespaciados entre 0 y 1
seq(0, 1, length.out = 7)
[1] 0.0000000 0.1666667 0.3333333 0.5000000 0.6666667 0.8333333 1.0000000
# Repetir el 1 tres veces
rep(1, 3)
[1] 1 1 1
# Repetir (1, 2, 3) tres veces
rep(1:3, 3)
[1] 1 2 3 1 2 3 1 2 3
# Repetir cada número tres veces
rep(1:3, each = 3)
[1] 1 1 1 2 2 2 3 3 3

27.3 Concatenación de vectores, matrices y listas

Los vectores pueden combinarse entre sí para crear nuevos vectores con c():

x <- 1:5
y <- c(10, 90, 87)
z <- c(x, y, x)
z
 [1]  1  2  3  4  5 10 90 87  1  2  3  4  5

Matrices que tienen la misma cantidad de filas pueden concatenarse una al lado de la otra con cbind() (unir por columnas):

m1 <- matrix(c(5, 8, 2, 2, 3, 1), nrow = 3)
m1
     [,1] [,2]
[1,]    5    2
[2,]    8    3
[3,]    2    1
m2 <- matrix(c(0, -1, 3, 1, 2, 4), nrow = 3)
m2
     [,1] [,2]
[1,]    0    1
[2,]   -1    2
[3,]    3    4
m3 <- cbind(m1, m2)
m3
     [,1] [,2] [,3] [,4]
[1,]    5    2    0    1
[2,]    8    3   -1    2
[3,]    2    1    3    4

Matrices que tienen la misma cantidad de columnas pueden concatenarse una debajo de la otra con rbind() (unir por filas):

m4 <- rbind(m1, m2)
m4
     [,1] [,2]
[1,]    5    2
[2,]    8    3
[3,]    2    1
[4,]    0    1
[5,]   -1    2
[6,]    3    4

Estas funciones también permiten unir matrices con vectores:

v <- 1:6
cbind(m4, v)
          v
[1,]  5 2 1
[2,]  8 3 2
[3,]  2 1 3
[4,]  0 1 4
[5,] -1 2 5
[6,]  3 4 6

También es posible combinar o juntar listas para formar nuevas listas que contengan todos los elementos de las originales. Esto puede resultar útil cuando queremos unificar resultados parciales, construir estructuras complejas a partir de otras más simples, o agregar nuevos elementos a una lista existente. La forma más directa de combinar listas es usando también la función c():

lista3 <- list(a = 1, b = 2)
lista4 <- list(c = 3, d = 4)

lista_combinada <- c(lista3, lista4)
lista_combinada
$a
[1] 1

$b
[1] 2

$c
[1] 3

$d
[1] 4

También es posible anidar listas dentro de otras listas. En el siguiente caso, lista_anidada es una lista de longitud 2, donde cada elemento es a su vez una lista:

lista_anidada <- list(lista3, lista4)
length(lista_anidada)
[1] 2
lista_anidada
[[1]]
[[1]]$a
[1] 1

[[1]]$b
[1] 2


[[2]]
[[2]]$c
[1] 3

[[2]]$d
[1] 4
lista_anidada[[1]]
$a
[1] 1

$b
[1] 2

Por último, si queremos agregar un nuevo elemento a una lista ya existente, podemos hacerlo mediante asignación directa:

lista3$e <- 5
lista3
$a
[1] 1

$b
[1] 2

$e
[1] 5

o usando la función append():

append(lista4, list(f = 6))
$c
[1] 3

$d
[1] 4

$f
[1] 6

27.4 Arreglos multidimensionales (lectura opcional)

Un arreglo multidimensional contiene más de dos dimensiones, es decir, requiere más de dos índices para identificar a cada uno de sus elementos. La representación matemática o visual ya no es tan sencilla como la de los vectores o matrices. Para interpretarlos o saber cuándo usarlos, pensamos que cada una de las dimensiones representa una característica de los elementos.

Por ejemplo, imaginemos que en un local comercial se quiere registrar cuántos clientes se atendieron en cada una de las tres cajas disponibles (primer dimensión del arreglo: caja 1, caja 2 o caja 3), ya sea en el turno mañana o tarde (segunda dimensión: 1 para la mañana o 2 para la tarde) en cada día hábil de una semana (tercera dimensión: 1 lunes, 2 martes, 3 miércoles, 4 jueves o 5 viernes). Si queremos registrar, por ejemplo, que la caja 1 en el turno tarde del día jueves atendió 12 clientes, tenemos que guardar el valor 12 en la posición [1, 2, 4] del arreglo.

El arreglo de 3 dimensiones que permite acomodar toda la información del ejemplo en una sola estructura puede definirse en R así:

clientes <- array(
  data = 0, 
  dim = c(3, 2, 5), 
  dimnames = list(
    caja = paste0("caja", 1:3), 
    turno = c("mañana", "tarde"), 
    dia = c("lun", "mar", "mié", "jue", "vie")
    )
  )
clientes
, , dia = lun

       turno
caja    mañana tarde
  caja1      0     0
  caja2      0     0
  caja3      0     0

, , dia = mar

       turno
caja    mañana tarde
  caja1      0     0
  caja2      0     0
  caja3      0     0

, , dia = mié

       turno
caja    mañana tarde
  caja1      0     0
  caja2      0     0
  caja3      0     0

, , dia = jue

       turno
caja    mañana tarde
  caja1      0     0
  caja2      0     0
  caja3      0     0

, , dia = vie

       turno
caja    mañana tarde
  caja1      0     0
  caja2      0     0
  caja3      0     0

Luego, si queremos registrar que la caja 1 en el turno tarde del día jueves atendió 12 clientes, hacemos:

clientes[1, 2, 4] <- 12
clientes
, , dia = lun

       turno
caja    mañana tarde
  caja1      0     0
  caja2      0     0
  caja3      0     0

, , dia = mar

       turno
caja    mañana tarde
  caja1      0     0
  caja2      0     0
  caja3      0     0

, , dia = mié

       turno
caja    mañana tarde
  caja1      0     0
  caja2      0     0
  caja3      0     0

, , dia = jue

       turno
caja    mañana tarde
  caja1      0    12
  caja2      0     0
  caja3      0     0

, , dia = vie

       turno
caja    mañana tarde
  caja1      0     0
  caja2      0     0
  caja3      0     0

O usamos los nombres, cuyo uso es opcional al crear el arreglo, pero resultan muy útiles:

clientes["caja3", "mañana", "lun"] <- 15
clientes
, , dia = lun

       turno
caja    mañana tarde
  caja1      0     0
  caja2      0     0
  caja3     15     0

, , dia = mar

       turno
caja    mañana tarde
  caja1      0     0
  caja2      0     0
  caja3      0     0

, , dia = mié

       turno
caja    mañana tarde
  caja1      0     0
  caja2      0     0
  caja3      0     0

, , dia = jue

       turno
caja    mañana tarde
  caja1      0    12
  caja2      0     0
  caja3      0     0

, , dia = vie

       turno
caja    mañana tarde
  caja1      0     0
  caja2      0     0
  caja3      0     0