2.4 Eficiencia en R: preasignación, vectorización y desfases

R permite escribir código muy expresivo, pero no todas las formas de hacer lo mismo son igual de eficientes. A continuación se ilustran tres buenas prácticas habituales, empleando el paquete microbenchmark para comparar tiempos de ejecución (más fiable que una única medición con proc.time(), ya que repite cada alternativa varias veces).

Preasignar en lugar de crecer un vector dentro de un bucle. Cada vez que se amplía un vector elemento a elemento (a[i] <- i sin haber fijado antes su longitud), R puede tener que reservar memoria nueva y copiar todo el vector; si se preasigna con numeric() (o vector()), el tamaño ya es el definitivo y no hay redimensionamientos:

library(microbenchmark)
n <- 3e4
microbenchmark(
  crecer      = { a <- numeric(); for (i in seq_len(n)) a[i] <- i },
  preasignado = { d <- numeric(n); for (i in seq_len(n)) d[i] <- i },
  times = 10
)
## Unit: milliseconds
##         expr     min      lq     mean   median      uq
##       crecer 21.8519 23.8182 27.75937 25.98785 30.9824
##  preasignado  7.8367 11.0907 16.39377 16.95415 20.2057
##      max neval cld
##  39.1828    10  a 
##  26.0591    10   b

Con un n mucho mayor la diferencia se dispara (no se evalúa en el libro por el tiempo de cómputo que implicaría repetirlo varias veces):

n <- 1e8
a <- numeric()
system.time(for (i in seq_len(n)) a[i] <- i)
d <- numeric(n)
system.time(for (i in seq_len(n)) d[i] <- i)

Usar funciones vectorizadas en lugar de apply(). Funciones como colMeans() o rowSums() están implementadas internamente en C y evitan el bucle (implícito) de apply():

m <- matrix(rnorm(1e6), nrow = 1000)
microbenchmark(
  apply_mean = apply(m, 2, mean),
  colMeans   = colMeans(m),
  apply_sum  = apply(m, 1, sum),
  rowSums    = rowSums(m),
  times = 20
)
## Unit: milliseconds
##        expr     min       lq      mean   median       uq
##  apply_mean 27.2368 30.70620 43.623030 35.08620 42.34490
##    colMeans  1.5414  1.64550  1.971330  1.72520  2.08605
##   apply_sum 16.3958 17.21525 21.302610 19.80010 23.61280
##     rowSums  1.2935  1.37060  1.581515  1.44975  1.67865
##       max neval cld
##  169.3949    20 a  
##    3.9990    20  b 
##   37.2324    20   c
##    2.5217    20  b

Calcular desfases (lag/lead) sin bucles. Para comparar cada valor con el anterior (o el siguiente) de un vector, en R base basta con desplazar el vector y alinearlo con cbind(), en lugar de recorrerlo con un bucle:

a <- 1:11
diff(a)                 # diferencias consecutivas
##  [1] 1 1 1 1 1 1 1 1 1 1
y <- c(NA, a[-length(a)])  # "lag": a desplazado una posición hacia atrás
x <- c(a[-1], NA)          # "lead": a desplazado una posición hacia adelante
cbind(original = a, lag = y, lead = x)
##       original lag lead
##  [1,]        1  NA    2
##  [2,]        2   1    3
##  [3,]        3   2    4
##  [4,]        4   3    5
##  [5,]        5   4    6
##  [6,]        6   5    7
##  [7,]        7   6    8
##  [8,]        8   7    9
##  [9,]        9   8   10
## [10,]       10   9   11
## [11,]       11  10   NA

En el Capítulo 5 se retoma este mismo desfase con dplyr::lag()/lead() (Sección 5.6), comprobando que su sintaxis más legible no penaliza el rendimiento.