Bucles for en R

Introducción a R Estructuras de control
Bucle for en R

El bucle for en R, también conocido como ciclo for, es una iteración repetitiva (en bucle) de cualquier código, donde en cada iteración se evalúa un mismo código a través de los elementos de un vector o lista.

Sintaxis del bucle for en R

La sintaxis del bucle for en R es muy simple:

for (i in lista) {
  # Código
}

También puedes escribir un bucle for en una sola línea de código sin corchetes. Sin embargo, no es recomendable escribir así los bucles for.

for (i in lista) # Código

Como primer ejemplo, podrías pensar en imprimir i + 1, siendo i = 1, ..., 5 en cada iteración del bucle. En este caso, el bucle for comenzará con i = 1 y finalizará con i = 5, por lo tanto la salida será la que se muestra a continuación:

for (i in 1:5) {
  print(i + 1)
}

# Equivalente a :
# for (i in 1:5) print (i + 1) 
2
3
4
5
6

Ten en cuenta que ‘i’ toma su valor correspondiente en cada iteración. Cabe destacar también que puedes usar cualquier letra o cadena de caracteres en lugar de ‘i’. Sin embargo, usar ‘i’ es la forma más común de representar la iteración actual en un único bucle.

Bucles for anidados en R

También puedes escribir sentencias for dentro de otras. A esto se le llama bucles anidados. La sintaxis se representa en el siguiente bloque de código:

for (i in lista) {
        # Código
    for(j in lista) {
        # Código
    }
}

Asegúrate de cambiar el nombre que le das a los índices cuando trabajes con bucles anidados. Nunca uses el mismo nombre para los índices dos veces.

Ejemplos del ciclo for en R

Bootstrap con el bucle for

Supón que quieres conocer la media muestral de n observaciones obtenidas independientemente de una distribución uniforme a lo largo del intervalo (0, 1). El problema mencionado se puede resolver teóricamente, pero vamos a realizar una simulación. Para ese propósito, debemos seguir estos simples pasos:

  1. Generar n observaciones con distribución uniforme en (0, 1).
  2. Calcular la media muestral de los datos.
  3. Repetir los pasos anteriores un elevado número de repeticiones.
  4. Aproximar la distribución de la media muestral con el histograma obtenido con las medias muestrales obtenidas en las repeticiones.

Si estás familiarizado con los métodos estadísticos, puede que te hayas dado cuenta de que estamos ejecutando un bootstrap uniforme.

set.seed(1)  # Fijamos semilla para reproductibilidad
rep <- 50000 # Número de repeticiones
n <- 2       # Número de puntos

Mean <- numeric(rep)

for (irep in 1:rep) {
    x <- runif(n)
    Mean[irep] <- mean(x)
}


hist(Mean, breaks = 40, main = paste("n = ", n))

Estudio de simulación con el bucle for en R

Creando un reloj con el bucle for

Ahora vamos a representar un minuto en segundos de reloj. Esta animación es recomendable que la hagas en R base en lugar de en RStudio, ya que la tasa de refresco de los gráficos en R GUI es menor.

angulo <- seq(0, 360, length = 60)
radianes <- angulo * pi / 180

x <- cos(radianes)
y <- sin(radianes)

for (i in 1:60) {
    plot(y, x, axes = F, xlab = "", ylab = "", type = "l", col = "grey")
    arrows(0, 0, y[i], x[i], col = "blue")
    Sys.sleep(1) # Espera un segundo
}

En cada iteración, el bucle anterior dibuja un reloj y después de un segundo dibuja el segundo siguiente y así sucesivamente. La representación de una iteración se muestra en la siguiente imagen:

Ejemplo de creación de un reloj con el bucle for en R

Las funciones break y next

A veces es necesario detener el ciclo en algún índice si se cumple alguna condición o evitar evaluar algún código para algún índice o condición. Para ese propósito puedes usar las funciones break y next.

En el siguiente ejemplo, el bucle se interrumpirá en la sexta iteración (que ya no será evaluada) a pesar de que el ciclo completo tiene 15 iteraciones, y además se saltará la tercera iteración.

for (iter in 1:15) {

  if (iter == 3) {
        next  
    }

    if (iter == 6) {
        break   
    }

  print(iter)
}
1
2
4
5

Preasignar espacio para ejecutar bucles

Los bucles son especialmente lentos en R. Si ejecutas o planeas ejecutar tareas computacionalmente costosas, debes preasignar memoria. Esta técnica consiste en reservar espacio para los objetos que estás creando o rellenando dentro de un bucle. Veamos un ejemplo.

Primero, puedes crear una variable llamada almacenar sin indicar el tamaño de la variable final una vez que se haya llenado dentro del bucle. La función Sys.time almacenará la hora exacta en el que se ejecuta la función en sí, así que asegúrate de llamar al siguiente código de una vez, no línea por línea.

inicio <- Sys.time()

almacenar <- numeric() # Sin preasignación

for (i in 1:1000000){
 almacenar[i] <- i ** 2
}

fin <- Sys.time()
fin - inicio  # El código tardó en ejecutarse 0.4400518 segundos en mi ordenador

Segundo, copia el código anterior y preasigna la variable almacenar con la longitud final que tendrá el vector.

inicio <- Sys.time()

almacenar <- numeric(1000000) # Con preasignación

for (i in 1:1000000){
 almacenar[i] <- i ** 2
}

fin <- Sys.time()
fin - inicio   # El código tardó en ejecutarse 0.126972 segundos

¡Casi 3.5 veces más rápido!

Ten en cuenta que los resultados pueden depender de la velocidad de tu ordenador y variarán si ejecutas el código varias veces. Sin embargo, cuantos más recursos consuman la tarea, mayor será la reducción de tiempo de ejecución si preasignas espacio para los objectos en la memoria. Si intentas ejecutar los códigos anteriores para solo 1000 o 10000 iteraciones, no verás prácticamente ninguna diferencia.

Bucle for vectorizado

La función foreach es una alternativa del bucle for del paquete foreach. Sin embargo, esta función es similar a la función apply. Ten en cuenta que también necesitarás usar el operador %do%. Esta función puede hacer que tus bucles sean más rápidos, pero la velocidad final podría depender del bucle que realices.

En el siguiente ejemplo creamos una función llamada for_each donde ejecutamos la raíz cuadrada del valor correspondiente de cada iteración. Como foreach devuelve una lista de forma predeterminada, puedes usar el argumento .combine y establecerlo como 'c' para que la salida se concatene. Otra opción es devolver el resultado envuelto por la función unlist.

# install.packages("foreach")
library(foreach)

for_each <- function(x) {

    res <- foreach(i = 1:x, .combine = 'c') %do% {
        sqrt(i)
    }

    return(res)
}

for_each(10)
1.000000 1.414214 1.732051 2.000000 2.236068 2.449490 2.645751 2.828427 3.000000 3.162278

Bucle for con proceso en paralelo

Cuando se trata de tareas computacionalmente muy intensivas, como estudios de simulación, puede que necesites hacer que tus bucles sean paralelos. Para eso necesitarás hacer uso de los paquetes parallel y doParallel. Sin embargo, el segundo paquete se carga cuando carga el primero, por lo que no necesitas cargar ambos.

En el siguiente ejemplo, configuramos nuestra ejecución paralela con todos los núcleos disponibles, pero puedes usar tantos como quieras. Luego registramos la paralelización y al acabar, debes recordar detener el clúster.

Nótese que necesitas usar %dopar% en lugar de %do%.

library(parallel)

par_for_each <- function(x) {

    cl <- parallel::makeCluster(detectCores())
    doParallel::registerDoParallel(cl) 

    res <- foreach(i = 1:x, .combine = 'c') %dopar% {
        sqrt(i)
    }

  parallel::stopCluster(cl)
  return(res)
}

par_for_each(10)
1.000000 1.414214 1.732051 2.000000 2.236068 2.449490 2.645751 2.828427 3.000000 3.162278

Los bucles en paralelo necesitan cierto tiempo para registrar el entorno en paralelo. Por tanto, solo son más eficientes con ciclos for muy intensivos desde el punto de vista computacional.