25  Operaciones vectorizadas

En R, muchas funciones y operadores están diseñados para trabajar directamente con vectores completos, sin necesidad de escribir bucles explícitos para recorrer elemento por elemento. Esta capacidad se conoce como vectorización y es una de las características más poderosas y distintivas del lenguaje. Usar operaciones vectorizadas permite escribir código más compacto, más rápido y más fácil de leer. En este capítulo exploraremos qué significa que una operación sea vectorizada, cómo se comportan estas operaciones en distintos contextos y qué ventajas ofrece esta forma de programación. Ya que ahora conocemos cómo se construyen vectores y matrices y cómo acceder a sus elementos, es el momento de aprender a operar sobre ellos de manera eficiente.

Con los conocimientos compartidos hasta aquí en esta unidad somos capaces de escribir interesantes algoritmos y programas para operar con vectores y matrices (por ejemplo: ordenar, buscar el mínimo, realizar cálculos algebraicos, etc.). No obstante, son tareas para las que generalmente los lenguajes ya ofrecen una respuesta, entre el conjunto de funciones que ofrecen. Es decir, en este proceso de aprendizaje, hemos trabajado de más, resolviendo problemas que ya tienen solución, ¡pero fue para poder aprender! Ahora vamos a mencionar algunas funcionalidades que evitan que tengamos que trabajar tanto. En R, el uso de vectores y matrices es muy sencillo gracias a la vectorización.

Decimos que una operación es vectorizada cuando se aplica a todos los elementos de un vector (o matriz) de manera automática, sin necesidad de usar estructuras como for o while. Esto significa que no es necesario escribir instrucciones para acceder y modificar cada elemento por separado: simplemente escribimos la operación y R se encarga de aplicarla a todos los valores del objeto,haciendo que el código sea más conciso, fácil de leer y con menos chances de cometer errores.

Por ejemplo, si tenemos un vector numérico y queremos sumar 1 a cada elemento, no necesitamos escribir una estructura for. Basta con:

v <- c(3, 1, 6)
v + 1
[1] 4 2 7

El resultado es un nuevo vector, también de tres elementos. Internamente, R realiza la operación 1 + 1, 2 + 1 y 3 + 1, pero nosotros no tenemos que preocuparnos por escribir esa lógica paso a paso. Esto nos ahorra tiempo, reduce la cantidad de código y disminuye las posibilidades de cometer errores. Comparemos este enfoque con una versión que usa un for:

v <- c(3, 1, 6)
for (i in seq_along(v)) {
  v[i] <- v[i] + 1
}
v
[1] 4 2 7

Lo mismo ocurre con las matrices. Supongamos que queremos sumar las matrices m1 y m2:

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

Podemos hacer la suma celda por celda con dos estructuras for anidadas:

suma <- matrix(NA, nrow(m1), ncol(m1))
for (i in 1:nrow(m1)) {
    for (j in 1:ncol(m1)) {
        suma[i, j] <- m1[i, j] + m2[i, j]
    }
}
suma
     [,1] [,2]
[1,]    5    3
[2,]    7    5
[3,]    5    5

No obstante, gracias a la vectorización, todo lo anterior puede ser reemplazado sencillamente por:

m1 + m2
     [,1] [,2]
[1,]    5    3
[2,]    7    5
[3,]    5    5

Las formas vectorizadas, además, suelen ser más eficientes porque internamente R delega estas operaciones al lenguaje C, lo que las hace mucho más rápidas. El concepto de vectorización es uno de los pilares del lenguaje R y se aplica a operaciones aritméticas, comparaciones, funciones matemáticas, transformaciones de datos, entre muchas otras tareas.

25.1 Operaciones aritméticas vectorizadas

Una de las formas más comunes de vectorización en R ocurre con las operaciones aritméticas básicas como suma (+), resta (-), multiplicación (*), división (/) y exponenciación (^), entro otras. Estas operaciones se aplican elemento a elemento cuando los operandos son vectores o matrices del mismo largo o dimensión (como en el caso visto recién).

Acá R realiza las siguientes sumas: 1+10, 2+20, 3+30:

v1 <- c(1, 2, 3)
v2 <- c(10, 20, 30)

v1 + v2
[1] 11 22 33

En este caso, R toma cada valor de m1 y lo divide por el valor de m2 que está en la misma posición:

m1 / m2
           [,1] [,2]
[1,]        Inf 2.00
[2,] -8.0000000 1.50
[3,]  0.6666667 0.25

25.2 Operaciones entre un arreglo y un escalar

También podemos realizar operaciones entre una matriz o vector y un vector con un único valor (un escalar), como en el primer ejemplo de este capítulo. En ese caso, R recicla automáticamente el escalar para que tenga la misma longitud que el vector:

a <- c(1, 2, 3)
a * 10
[1] 10 20 30

Es como si R hubiera hecho:

a * c(10, 10, 10)
[1] 10 20 30

o bien:

c(1 * 10, 2 * 10, 3 * 10)
[1] 10 20 30

Lo mismo ocurre con las matrices. En los siguientes caso, R toma cada valor de m2 y lo eleva al cuadrado:

m2^2
     [,1] [,2]
[1,]    0    1
[2,]    1    4
[3,]    9   16

25.3 Operaciones entre vectores de distinto largo

Si los vectores tienen diferente longitud, R intentará reciclar el más corto, repitiendo sus elementos hasta alcanzar la longitud del más largo:

v1 <- c(1, 2, 3, 4)
v2 <- c(10, 20)

v1 + v2
[1] 11 22 13 24

En este caso, R realiza:

1 + 10
2 + 20
3 + 10
4 + 20

Si la longitud del más largo no es múltiplo exacto de la del más corto, R emitirá una advertencia:

v1 <- c(1, 2, 3)
v2 <- c(10, 20)

v1 + v2
Warning in v1 + v2: longer object length is not a multiple of shorter object
length
[1] 11 22 13

Si bien este comportamiento parece práctico, puede provocar errores difíciles de detectar si no se controla con cuidado, en especial cuando se trabaja con matrices. Es preferible siempre chequear que las dimensiones de los elementos con los que operamos sean los esperados y no hacer uso de esta posibilidad.

25.4 Funciones vectorizadas

En R, muchas funciones están diseñadas para operar de manera vectorizada. Esto quiere decir que si pasamos como argumento un vector o matriz, R aplica la función matemática a cada uno de sus elementos. Por ejemplo, si tenemos un vector de números, podemos obtener la raíz cuadrada de cada uno con una sola línea:

v1 <- c(1, 4, 9, 16)
sqrt(v1)
[1] 1 2 3 4

Si tenemos un vector de cadenas de texto, podemos ver cuántos caracteres hay en cada uno:

v2 <- c("hola", "mundo")
nchar(v2)
[1] 4 5

Otros ejemplos de funciones matemáticas vectorizadas aplicadas a vectores numéricos:

v1 <- c(2.5, 3.7, -1.2)

round(v1)    # Redondeo al entero más cercano
[1]  2  4 -1
ceiling(v1)  # Techo (entero inmediato superior)
[1]  3  4 -1
floor(v1)    # Piso (entero inmediato inferior)
[1]  2  3 -2
abs(v1)      # Valor absoluto
[1] 2.5 3.7 1.2
sin(v1)      # Seno
[1]  0.5984721 -0.5298361 -0.9320391

Es análogo con matrices:

m1 <- matrix(c(5, 8, 2, 2, 3, 1), nrow = 3)
m1
     [,1] [,2]
[1,]    5    2
[2,]    8    3
[3,]    2    1
log(m1)
          [,1]      [,2]
[1,] 1.6094379 0.6931472
[2,] 2.0794415 1.0986123
[3,] 0.6931472 0.0000000
sqrt(m1)
         [,1]     [,2]
[1,] 2.236068 1.414214
[2,] 2.828427 1.732051
[3,] 1.414214 1.000000

Por supuesto, R proporciona muchas funciones estadísticas que trabajan de forma vectorizada, es decir, que procesan todos los elementos de un vector en una sola llamada. Estas funciones son muy eficientes y permiten escribir código compacto para obtener resúmenes estadísticos básicos. Si bien la exploración estadística de datos no es objeto de esta asignatura, mencionamos algunas de estas funciones:

  • sum(v1): suma todos los elementos del vector v1.
  • mean(v1): calcula el promedio (media aritmética).
  • median(v1): calcula la mediana.
  • min(v1), max(v1): devuelven el mínimo y el máximo.
  • range(v1): devuelve un vector con el mínimo y el máximo.
  • sd(v1), var(v1): calculan el desvío estándar y la variancia.
  • quantile(x, probs = c(0.25, 0.5, .75)): calcula los cuartiles.
  • IQR(v1): calcula el rango intercuartil.
  • prod(v1): multiplica todos los elementos.
v1 <- c(2, 4, 6, 8)
range(v1)
[1] 2 8
mean(v1)
[1] 5
median(v1)
[1] 5
sd(v1)
[1] 2.581989
quantile(v1, probs = c(0.25, 0.5, .75))
25% 50% 75% 
3.5 5.0 6.5 
prod(v1)
[1] 384

También pueden ser aplicadas a matrices, por ejemplo:

sum(m1)
[1] 21
sum(m1) / (nrow(m1) * ncol(m2))
[1] 3.5
mean(m1)
[1] 3.5
sd(m1)
[1] 2.588436

En lugar de aplicar estas funciones estadísticas sobre todos los valores de una matriz, es más común hacerlo sobre vectores, ya que los mismos suelen representar datos observados de alguna variable. En cambio, suele ser más común calcular estos resúmenes numéricos para cada una de las filas o columnas de una matriz:

  • Suma y media de los elementos de cada fila:

    rowSums(m1)
    [1]  7 11  3
    rowMeans(m1)
    [1] 3.5 5.5 1.5
    rowSums(m1) / ncol(m1)
    [1] 3.5 5.5 1.5
  • Suma y media de los elementos de cada columna:

    colSums(m1)
    [1] 15  6
    colMeans(m1)
    [1] 5 2
    colSums(m1) / nrow(m1)
    [1] 5 2

25.5 Búsqueda de mínimos y máximos en arreglos numéricos

De esta forma podemos encontrar el valor mínimo y su ubicación en un vector numérico:

x <- c(40, 70, 20, 90, 20)
min(x)
[1] 20
which.min(x)       # si el mínimo se repite, esta es la posición del primero
[1] 3
which(x == min(x)) # si el mínimo se repite, esto muestra todas sus posiciones
[1] 3 5

Y así, encontrar el valor máximo y su ubicación en el vector:

max(x)
[1] 90
which.max(x)       # si el máximo se repite, esta es la posición del primero
[1] 4
which(x == max(x)) # si el máximo se repite, esto muestra todas sus posiciones
[1] 4

Cuando se trata de una matriz, tenemos:

m1
     [,1] [,2]
[1,]    5    2
[2,]    8    3
[3,]    2    1
# Valor máximo
max(m1)
[1] 8
# Posición (arr.ind = TRUE para que nos indique fila y columna)
which(m1 == max(m1), arr.ind = TRUE)
     row col
[1,]   2   1
# Valor mínimo
min(m1)
[1] 1
# Posición
which(m1 == min(m1), arr.ind = TRUE)
     row col
[1,]   3   2

25.6 Álgebra matricial

Como aprenderán en Álgebra, las matrices numéricas son muy útiles en diversos campos y por eso existen distintas operaciones que se pueden realizar con las mismas. Veamos algunos ejemplos de la aplicación del álgebra matricial en R:

  • Transpuesta de una matriz:

    m1
         [,1] [,2]
    [1,]    5    2
    [2,]    8    3
    [3,]    2    1
    t(m1)
         [,1] [,2] [,3]
    [1,]    5    8    2
    [2,]    2    3    1
  • Producto entre dos matrices (la cantidad de columnas de la primera debe coincidir con la cantidad de filas de la segunda):

    m3 <- matrix(1:4, nrow = 2)
    m1
         [,1] [,2]
    [1,]    5    2
    [2,]    8    3
    [3,]    2    1
    m3
         [,1] [,2]
    [1,]    1    3
    [2,]    2    4
    m1 %*% m3
         [,1] [,2]
    [1,]    9   23
    [2,]   14   36
    [3,]    4   10
  • Inversa de la matriz:

    solve(m3)
         [,1] [,2]
    [1,]   -2  1.5
    [2,]    1 -0.5
  • Obtener los elementos de la diagonal principal:

    diag(m3)
    [1] 1 4
  • Crear una matriz identidad:

    diag(5)
         [,1] [,2] [,3] [,4] [,5]
    [1,]    1    0    0    0    0
    [2,]    0    1    0    0    0
    [3,]    0    0    1    0    0
    [4,]    0    0    0    1    0
    [5,]    0    0    0    0    1

25.7 Operaciones lógicas vectorizadas

Al igual que las funciones matemáticas, los operadores de comparación en R también están vectorizados. Esto significa que podemos comparar cada elemento de un vector o una matriz con un valor, o comparar dos arreglos entre sí, sin necesidad de usar bucles y obteniendo como resultado otro arreglo de valores lógicos.

25.7.1 Comparaciones lógicas entre arreglos

Cuando dos vectores o matrices se vinculan a través de una comparación, se opera elemento a elemento obteniendo un vector o matriz de valores lógicos:

v1 <- c(40, 70, 20, 90, 20)
v2 <- c(10, 70, 30, 15, 21)
v1 > v2
[1]  TRUE FALSE FALSE  TRUE FALSE
v1 < v2 * 5
[1]  TRUE  TRUE  TRUE FALSE  TRUE
m1 <- matrix(c(5, 8, 2, 2, 3, 1), nrow = 3)
m2 <- matrix(c(0, -1, 3, 1, 2, 4), nrow = 3)
m1
     [,1] [,2]
[1,]    5    2
[2,]    8    3
[3,]    2    1
m2
     [,1] [,2]
[1,]    0    1
[2,]   -1    2
[3,]    3    4
m1 != m2
     [,1] [,2]
[1,] TRUE TRUE
[2,] TRUE TRUE
[3,] TRUE TRUE
m1 > m2
      [,1]  [,2]
[1,]  TRUE  TRUE
[2,]  TRUE  TRUE
[3,] FALSE FALSE

Si tenemos un vector o matriz de valores lógicos y queremos saber si todos o al menos uno de los elementos es igual a TRUE, podemos usar las funciones all() y any(), respectivamente:

all(m1 != m2)
[1] TRUE
any(m1 != m2)
[1] TRUE
all(m1 > m2)
[1] FALSE
any(m1 > m2)
[1] TRUE

25.7.2 Comparaciones lógicas entre un arreglo y un valor

Las operaciones de comparación pueden hacerse entre cada elemento de un vector o matriz y un único valor. Recordemos los vectores y matrices que estamos usando:

v1
[1] 40 70 20 90 20
v2
[1] 10 70 30 15 21
m1
     [,1] [,2]
[1,]    5    2
[2,]    8    3
[3,]    2    1
m2
     [,1] [,2]
[1,]    0    1
[2,]   -1    2
[3,]    3    4

Ahora veamos ejemplos de comparaciones lógicas entre ellos y un número:

v1 < 50
[1]  TRUE FALSE  TRUE FALSE  TRUE
m1 == 3
      [,1]  [,2]
[1,] FALSE FALSE
[2,] FALSE  TRUE
[3,] FALSE FALSE
m2 > 0
      [,1] [,2]
[1,] FALSE TRUE
[2,] FALSE TRUE
[3,]  TRUE TRUE

Los operadores lógicos que se utilizan para realizar cálculos elemento a elemento con vectores y matrices son &, | y !. Ellos nos permiten crear expresiones aún más complejas:

v1 < 50 & v2 > 50
[1] FALSE FALSE FALSE FALSE FALSE
m1 < 0 | m2 > 0
      [,1] [,2]
[1,] FALSE TRUE
[2,] FALSE TRUE
[3,]  TRUE TRUE
!(v1 <= 50)
[1] FALSE  TRUE FALSE  TRUE FALSE

25.8 Formas de indexación múltiple

Como ya sabemos, indexar es hacer referencia a uno o más elementos particulares dentro de una estructura de datos. Una de las formas más potentes y expresivas de trabajar con vectores en R es a través de la indexación lógica. Esta técnica permite seleccionar elementos de un arreglo usando condiciones que generan vectores lógicos (TRUE o FALSE).

25.8.1 Indexación múltiple en base a posiciones

Conocemos que para indexar a un vector, hace falta sólo un índice:

v1 <- c(10.4, 5.6, 3.1, 6.4, 21.7)
v1[3]
[1] 3.1

Y que para indexar matrices, son necesarios dos índices:

m1 <- matrix(c(4,-2, 1, 20, -7, 12, -8, 13, 17), nrow = 3)
m1
     [,1] [,2] [,3]
[1,]    4   20   -8
[2,]   -2   -7   13
[3,]    1   12   17
m1[2, 3]
[1] 13

Pero también podemos indexar a múltiples elementos de un vector o una matriz a la vez. Los siguientes ejemplos presentan distintas formas de seleccionar varias posiciones de un vector en simultáneo:

# Vector v1
v1
[1] 10.4  5.6  3.1  6.4 21.7
# Mostrar los primeros tres elementos del vector v1
v1[1:3]
[1] 10.4  5.6  3.1
# Mostrar los elementos en las posiciones 2 y 4
v1[c(2, 4)]
[1] 5.6 6.4
# Mostrar el último elemento
v1[length(v1)]
[1] 21.7
# Bonus: Mostrar todos los elementos menos el cuarto
v1[-4]
[1] 10.4  5.6  3.1 21.7

Para las matrices:

# Matriz m1
m1
     [,1] [,2] [,3]
[1,]    4   20   -8
[2,]   -2   -7   13
[3,]    1   12   17
# Filas 2 y 3, columna 2
m1[2:3, 2]
[1] -7 12
# Filas 1 y 3, columna 1
m1[c(1, 3), 1]
[1] 4 1
# Toda la fila 3
m1[3, ]
[1]  1 12 17
# Toda la columna 2
m1[, 2]
[1] 20 -7 12
# Submatriz con las columnas 1 y 2
m1[, 1:2]
     [,1] [,2]
[1,]    4   20
[2,]   -2   -7
[3,]    1   12
# Submatriz con las columnas 1 y 3
m1[, c(1, 3)]
     [,1] [,2]
[1,]    4   -8
[2,]   -2   13
[3,]    1   17
# Asignar el mismo valor en toda la fila 3
m1[3, ] <- 10
m1
     [,1] [,2] [,3]
[1,]    4   20   -8
[2,]   -2   -7   13
[3,]   10   10   10

25.8.2 Indexación múltiple en base a comparaciones lógicas

Ya hemos visto que, además de utilizar enteros para señalar posiciones, también podemos indexar usando nombres si los arreglos tienen seteado su atributo names. No obstante, estas no son las únicas formas de indexar: también podemos hacerlo con valores lógicos TRUE o FALSE.

# Vector v1
v1
[1] 10.4  5.6  3.1  6.4 21.7
# Indexar con valores lógicos. Obtenemos sólo las posiciones indicadas con TRUE:
v1[c(FALSE, FALSE, TRUE, TRUE, FALSE)]
[1] 3.1 6.4
# Sabiendo que la siguiente operación devuelve TRUE o FALSE para cada posición 
# de v1...
v1 > 10
[1]  TRUE FALSE FALSE FALSE  TRUE
# ...podemos usarla para quedarnos con aquellos elementos de x mayores a 10:
v1[v1 > 10]
[1] 10.4 21.7

Este tipo de expresión es muy habitual en el análisis de datos, ya que permite extraer subconjuntos en forma directa y legible. También se puede combinar con asignación de valor:

v1[v1 < 10] <- 0
v1
[1] 10.4  0.0  0.0  0.0 21.7