Capítulo 12 Estructuras de control: bucles

Aunque la mayoría de veces son sustituibles por otras expresiones más legibles y eficientes, es importante que conozcamos otra archiconocida expresion de control: los bucles.

  • for: permite repetir el mismo código un número fijo (y conocido a priori) de veces (normalmente en función de un índice).
  • while: permite repetir el mismo código un número indeterminado de veces, hasta que una condición dada se deje de cumplir.

12.1 for

Aunque la mayoría de las veces los bucles pueden ser sustituidos por expresiones mucho más eficientes (fíjate que ya hemos filtrado columnas, filas y aplicado operaciones por filas y columnas, sin necesitarlo), a veces no nos quedará más remedio que usarlos por lo que nunca viene mal conocer su estructura.

Un bucle for{} es una estructura que nos permite repetir un conjunto de órdenes un número finito y conocido de veces: dado un conjunto de índices, el bucle irá recorriendo cada uno de ellos, y para cada uno ejecutará lo que tenga dentro de las llaves.

Veamos un ejemplo muy sencillo. Vamos a definir un vector de números de longitud 4, por ejemplo x <- c(0, -7, 1, 4). Si quisiéramos devolver el primer elemento al cuadrado escribiríamos x[1]^2 (accedemos con [1] al primer elemento y lo elevamos al cuadrado). Si quisiéramos devolver el segundo elemento al cuadrado escribiríamos x[2]^2. Si lo quisiéramos hacer en general, para el elemento i-ésimo (el que ocupa la posición i), escribiríamos x[i]^2. Lo que haremos dentro del for (...) {} es indicarle que valores irá tomando i: creamos un vector de índices, en este caso 1:4, para que i vaya tomando sus valores (primero i=1, luego i=2, etc)

x <- c(0, -7, 1, 4)
for (i in 1:4) {
  
  print(x[i]^2) # que lo imprima
  
}
## [1] 0
## [1] 49
## [1] 1
## [1] 16

Lo que tenemos dentro de los paréntesis, en la línea del for, no es más que la secuencia de números que hemos aprendido a construir.

1:4
## [1] 1 2 3 4

Si quisiéramos que haga lo mismo pero excluyendo por ejemplo el segundo elemento bastaría con definir los índices a recorrer como c(1, 3, 4) o c(1, 3:4)

for (i in c(1, 3, 4)) {
  
  print(x[i]^2) # que lo imprima
  
}
## [1] 0
## [1] 1
## [1] 16

Podemos definir también una variable y <- rep(0, 4) (un vector de la misma longitud pero, de momento, lleno de ceros), y hacer que elemento i-ésimo del vector y se defina como x[i]^2

y <- rep(0, 4)
for (i in 1:4) {
  
  y[i] <- x[i]^2
  
}

y
## [1]  0 49  1 16

Fíjate que, dado que las operaciones en R ya se hacen por defecto de forma vectorial, elemento a elemento, lo anterior es equivalente a esto

y <- x^2
y
## [1]  0 49  1 16

No solo es equivalente sino que además, haciendo uso del paquete microbenchmark podemos comprobar como el bucle, amén de no ser necesario, es menos eficiente (de ahí que la mayoría de veces los intentemos evitar si existe otra alternativa)

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

# Le pasamos dos conjuntos de códigos, y le indicamos con
# times el número de simulaciones a realizar para luego obtener
# un promedio de tiempos
microbenchmark(x^2, for (i in 1:4) { y[i] <- x[i]^2 },
               times = 1000)
## Unit: nanoseconds
##                                   expr     min        lq        mean  median
##                                    x^2      97     136.5     577.197     341
##  for (i in 1:4) {     y[i] <- x[i]^2 } 1347539 1413386.0 1598199.028 1475116
##         uq     max neval cld
##      783.5   17950  1000  a 
##  1584073.0 6403272  1000   b

 

Veamos otro ejemplo: vamos a definir de nuevo un vector de edades y nombres, y vamos a recorrer cada uno imprimiento un mensaje por pantalla.

library(glue)

nombres <- c("Javi", "Laura", "Carlos", "Lucía", "Mar")
edades <- c(32, 51, 18, 43, 29)

# Recorremos cada uno de los 5 elementos e imprimimos un
# mensaje que depende de ese índice i
for (i in 1:5) { 
  
  print(glue("{nombres[i]} tiene {edades[i]} años")) 
  
}
## Javi tiene 32 años
## Laura tiene 51 años
## Carlos tiene 18 años
## Lucía tiene 43 años
## Mar tiene 29 años

Fíjate que si no nos queremos preocupar de si añadimos otra persona, podemos hacer que el bucle empiece en 1 y termine en el último lugar (sea el que sea), usando length()

for (i in 1:length(nombres)) { 
  
  print(glue("{nombres[i]} tiene {edades[i]} años")) 
  
}
## Javi tiene 32 años
## Laura tiene 51 años
## Carlos tiene 18 años
## Lucía tiene 43 años
## Mar tiene 29 años

Aunque normalmente el conjunto que recorre el bucle suelen ser índices numéricos, podemos recorrer cualquier tipo de objeto, por ejemplo días e la semana

library(stringr)
dias_semana <- c("lunes", "martes", "miércoles", "jueves",
                 "viernes", "sábado", "domingo")

for (dias in dias_semana) { # dias recorre los días de la semana
  
  print(str_to_upper(dias)) # Imprimimos en mayúsculas el día
}
## [1] "LUNES"
## [1] "MARTES"
## [1] "MIÉRCOLES"
## [1] "JUEVES"
## [1] "VIERNES"
## [1] "SÁBADO"
## [1] "DOMINGO"

Como hemos visto, además de imprimir por pantalla podemos asignar valores a variables o cambiarlas. Por ejemplo, vamos a recorrer nuestro conjunto swiss de {datasets} y vamos a pasar a dato ausente todos los valores de fertilidad superiores a 80. Para ello recorreremos cada fila para después ejecutar un if.

for (i in 1:nrow(swiss)) {
  
  # si cumple la condición dicha fila, ponemos ausente.
  if (swiss$Fertility[i] > 80) { 
    
    swiss$Fertility[i] <- NA
    
  }
}
swiss
##              Fertility Agriculture Examination Education Catholic
## Courtelary          NA        17.0          15        12     9.96
## Delemont            NA        45.1           6         9    84.84
## Franches-Mnt        NA        39.7           5         5    93.40
## Moutier             NA        36.5          12         7    33.77
## Neuveville        76.9        43.5          17        15     5.16
## Porrentruy        76.1        35.3           9         7    90.57
## Broye               NA        70.2          16         7    92.85
## Glane               NA        67.8          14         8    97.16
## Gruyere             NA        53.3          12         7    97.67
## Sarine              NA        45.2          16        13    91.38
## Veveyse             NA        64.5          14         6    98.61
## Aigle             64.1        62.0          21        12     8.52
## Aubonne           66.9        67.5          14         7     2.27
## Avenches          68.9        60.7          19        12     4.43
## Cossonay          61.7        69.3          22         5     2.82
## Echallens         68.3        72.6          18         2    24.20
## Grandson          71.7        34.0          17         8     3.30
## Lausanne          55.7        19.4          26        28    12.11
## La Vallee         54.3        15.2          31        20     2.15
## Lavaux            65.1        73.0          19         9     2.84
## Morges            65.5        59.8          22        10     5.23
## Moudon            65.0        55.1          14         3     4.52
## Nyone             56.6        50.9          22        12    15.14
## Orbe              57.4        54.1          20         6     4.20
## Oron              72.5        71.2          12         1     2.40
## Payerne           74.2        58.1          14         8     5.23
## Paysd'enhaut      72.0        63.5           6         3     2.56
## Rolle             60.5        60.8          16        10     7.72
## Vevey             58.3        26.8          25        19    18.46
## Yverdon           65.4        49.5          15         8     6.10
## Conthey           75.5        85.9           3         2    99.71
## Entremont         69.3        84.9           7         6    99.68
## Herens            77.3        89.7           5         2   100.00
## Martigwy          70.5        78.2          12         6    98.96
## Monthey           79.4        64.9           7         3    98.22
## St Maurice        65.0        75.9           9         9    99.06
## Sierre              NA        84.6           3         3    99.46
## Sion              79.3        63.1          13        13    96.83
## Boudry            70.4        38.4          26        12     5.62
## La Chauxdfnd      65.7         7.7          29        11    13.79
## Le Locle          72.7        16.7          22        13    11.22
## Neuchatel         64.4        17.6          35        32    16.92
## Val de Ruz        77.6        37.6          15         7     4.97
## ValdeTravers      67.6        18.7          25         7     8.65
## V. De Geneve      35.0         1.2          37        53    42.34
## Rive Droite       44.7        46.6          16        29    50.43
## Rive Gauche       42.8        27.7          22        29    58.33
##              Infant.Mortality
## Courtelary               22.2
## Delemont                 22.2
## Franches-Mnt             20.2
## Moutier                  20.3
## Neuveville               20.6
## Porrentruy               26.6
## Broye                    23.6
## Glane                    24.9
## Gruyere                  21.0
## Sarine                   24.4
## Veveyse                  24.5
## Aigle                    16.5
## Aubonne                  19.1
## Avenches                 22.7
## Cossonay                 18.7
## Echallens                21.2
## Grandson                 20.0
## Lausanne                 20.2
## La Vallee                10.8
## Lavaux                   20.0
## Morges                   18.0
## Moudon                   22.4
## Nyone                    16.7
## Orbe                     15.3
## Oron                     21.0
## Payerne                  23.8
## Paysd'enhaut             18.0
## Rolle                    16.3
## Vevey                    20.9
## Yverdon                  22.5
## Conthey                  15.1
## Entremont                19.8
## Herens                   18.3
## Martigwy                 19.4
## Monthey                  20.2
## St Maurice               17.8
## Sierre                   16.3
## Sion                     18.1
## Boudry                   20.3
## La Chauxdfnd             20.5
## Le Locle                 18.9
## Neuchatel                23.0
## Val de Ruz               20.0
## ValdeTravers             19.5
## V. De Geneve             18.0
## Rive Droite              18.2
## Rive Gauche              19.3

Esto sería exactamente equivalente al ifelse vectorizado que vimos en el tema anterior

data("swiss") # lo cargamos de 0
swiss$Fertility <- ifelse(swiss$Fertility > 80, NA, swiss$Fertility)
swiss
##              Fertility Agriculture Examination Education Catholic
## Courtelary          NA        17.0          15        12     9.96
## Delemont            NA        45.1           6         9    84.84
## Franches-Mnt        NA        39.7           5         5    93.40
## Moutier             NA        36.5          12         7    33.77
## Neuveville        76.9        43.5          17        15     5.16
## Porrentruy        76.1        35.3           9         7    90.57
## Broye               NA        70.2          16         7    92.85
## Glane               NA        67.8          14         8    97.16
## Gruyere             NA        53.3          12         7    97.67
## Sarine              NA        45.2          16        13    91.38
## Veveyse             NA        64.5          14         6    98.61
## Aigle             64.1        62.0          21        12     8.52
## Aubonne           66.9        67.5          14         7     2.27
## Avenches          68.9        60.7          19        12     4.43
## Cossonay          61.7        69.3          22         5     2.82
## Echallens         68.3        72.6          18         2    24.20
## Grandson          71.7        34.0          17         8     3.30
## Lausanne          55.7        19.4          26        28    12.11
## La Vallee         54.3        15.2          31        20     2.15
## Lavaux            65.1        73.0          19         9     2.84
## Morges            65.5        59.8          22        10     5.23
## Moudon            65.0        55.1          14         3     4.52
## Nyone             56.6        50.9          22        12    15.14
## Orbe              57.4        54.1          20         6     4.20
## Oron              72.5        71.2          12         1     2.40
## Payerne           74.2        58.1          14         8     5.23
## Paysd'enhaut      72.0        63.5           6         3     2.56
## Rolle             60.5        60.8          16        10     7.72
## Vevey             58.3        26.8          25        19    18.46
## Yverdon           65.4        49.5          15         8     6.10
## Conthey           75.5        85.9           3         2    99.71
## Entremont         69.3        84.9           7         6    99.68
## Herens            77.3        89.7           5         2   100.00
## Martigwy          70.5        78.2          12         6    98.96
## Monthey           79.4        64.9           7         3    98.22
## St Maurice        65.0        75.9           9         9    99.06
## Sierre              NA        84.6           3         3    99.46
## Sion              79.3        63.1          13        13    96.83
## Boudry            70.4        38.4          26        12     5.62
## La Chauxdfnd      65.7         7.7          29        11    13.79
## Le Locle          72.7        16.7          22        13    11.22
## Neuchatel         64.4        17.6          35        32    16.92
## Val de Ruz        77.6        37.6          15         7     4.97
## ValdeTravers      67.6        18.7          25         7     8.65
## V. De Geneve      35.0         1.2          37        53    42.34
## Rive Droite       44.7        46.6          16        29    50.43
## Rive Gauche       42.8        27.7          22        29    58.33
##              Infant.Mortality
## Courtelary               22.2
## Delemont                 22.2
## Franches-Mnt             20.2
## Moutier                  20.3
## Neuveville               20.6
## Porrentruy               26.6
## Broye                    23.6
## Glane                    24.9
## Gruyere                  21.0
## Sarine                   24.4
## Veveyse                  24.5
## Aigle                    16.5
## Aubonne                  19.1
## Avenches                 22.7
## Cossonay                 18.7
## Echallens                21.2
## Grandson                 20.0
## Lausanne                 20.2
## La Vallee                10.8
## Lavaux                   20.0
## Morges                   18.0
## Moudon                   22.4
## Nyone                    16.7
## Orbe                     15.3
## Oron                     21.0
## Payerne                  23.8
## Paysd'enhaut             18.0
## Rolle                    16.3
## Vevey                    20.9
## Yverdon                  22.5
## Conthey                  15.1
## Entremont                19.8
## Herens                   18.3
## Martigwy                 19.4
## Monthey                  20.2
## St Maurice               17.8
## Sierre                   16.3
## Sion                     18.1
## Boudry                   20.3
## La Chauxdfnd             20.5
## Le Locle                 18.9
## Neuchatel                23.0
## Val de Ruz               20.0
## ValdeTravers             19.5
## V. De Geneve             18.0
## Rive Droite              18.2
## Rive Gauche              19.3

 

¿Te acuerdas de las operaciones por filas y columnas que hicimos con los apply()? Podemos hacer lo mismo (aunque menos eficiente) con un bucle: vamos a sumar las filas del conjunto swiss. Para ello antes definiremos una variable llena de ceros que luego rellenaremos, de tamaño igual al número de filas, con suma <- rep(0, nrow(swiss))

suma <- rep(0, nrow(swiss))
for (i in 1:nrow(swiss)) {
  
  suma[i] <- sum(swiss[i, ])
}
suma
##  [1]     NA     NA     NA     NA 178.16 244.57     NA     NA     NA     NA
## [11]     NA 184.12 176.77 187.73 179.52 206.30 154.00 161.41 133.45 188.94
## [21] 180.53 164.02 173.34 157.00 180.10 183.33 165.06 171.32 168.46 166.50
## [31] 281.21 286.68 292.30 285.06 272.72 275.76     NA 283.33 172.72 147.69
## [41] 154.52 188.92 162.17 146.45 186.54 204.93 199.13

 

12.2 while

Otra manera de diseñar un bucle es con la estructura while{}, que ejecutará el bucle un número de veces pero que a priori es desconocido, lo hará hasta que la condición impuesta deje de ser TRUE (en el for sabemos de antemano el número de iteraciones). Por ejemplo, vamos a inicializar una variable ciclos <- 1, y en cada paso aumentaremos una unidad ciclos, y no saldremos del bucle hasta que ciclos > 5

ciclos <- 1
# Mientras el número de ciclos sea inferior 5, imprime
while(ciclos <= 5) {
  
  print(paste("Todavía no, vamos por el ciclo ", ciclos)) # Pegamos la frase al número de ciclo por el que vayamos con paste
  ciclos <- ciclos + 1
  
}
## [1] "Todavía no, vamos por el ciclo  1"
## [1] "Todavía no, vamos por el ciclo  2"
## [1] "Todavía no, vamos por el ciclo  3"
## [1] "Todavía no, vamos por el ciclo  4"
## [1] "Todavía no, vamos por el ciclo  5"

¿Y qué sucede cuando la condición nunca llega a ser FALSE? Compruébalo tú mismo/a.

while (1 > 0) { # Nunca va a dejar de ser cierto
  
  print("Presiona ESC para salir del bucle")
  
}

12.3 break/next

En R tenemos dos comandos reservados para poder abortar un bucle o avanzar forzosamente un bucle: dichas palabras son break y next. La primera nos habilita para parar un bucle aunque no haya llegado al final de su conjunto de índices a recorrer (o se siga cumpliendo la condición del while{}).

Vamos a hacer un bucle de 1 a 10: cuando el índice i es igual a 7, el bucle se para.

for(i in 1:10) {
  if (i == 7) {
    
    break # si i es 7, el bucle frena aquí (nunca llegará a imprimir el 7 ni los sucesivos)
    
  }
  print(i)
}
## [1] 1
## [1] 2
## [1] 3
## [1] 4
## [1] 5
## [1] 6

Mientras que la segunda, el next obliga al bucle a avanzar a la siguiente interacción, abortando la iteración actual en la que se encuentra. Vamos ahora a hacer un bucle de 1 a 10: cuando el índice i es igual a 7, el bucle salta al i = 8.

for(i in 1:10) {
  if (i == 7) {
    
    next # si i es 7, la iteración frenará aquí y pasará a la siguiente por lo que imprimirá todos menos el 7
    
  }
  print(i)
}
## [1] 1
## [1] 2
## [1] 3
## [1] 4
## [1] 5
## [1] 6
## [1] 8
## [1] 9
## [1] 10

12.4 repeat

Aunque es una opción muy poco usada, existe una estructura de control llamada repeat{} que nos ejecuta un bucle de forma infinita hasta que le ordenemos parar con un break.

conteo <- 0
repeat { 
  
  conteo <- conteo + 1
  if (conteo >= 100) { break }
  
}
conteo
## [1] 100

 

WARNING: cuidado con los bucles infinitos

Las estucturas while{} y repeat{} son de las menos usadas por su peligrosidad, ya que si no incluimos un break o la condición nunca llega a ser TRUE, el bucle seguirá ejecutándose de forma infinita y solo podrá ser detenido abortando la ejecución con la tecla ESC.

 

12.5 Consejos

CONSEJOS

 

Evita bucles (si puedes)

Recuerda: por lo general, di no a los bucles en R (intenta evitarlos, en la mayoría de casos hay una alternativa mejor).

 

Evita bucles (parte II)

Evita al máximo los bucles en R: suele existir una forma más eficiente de programarlo. Veamos un ejemplo muy sencillo: dado un vector de índices idx, queremos calcular su cuadrado y guardarlo. Vamos a comparar como sería con un sencillo bucle y de forma vectorial, repitiéndolo 1000 veces para sacar tiempos medios, haciendo uso del paquete microbenchmark.

idx <- 1:10000
x <- y <- rep(0, length(idx))
microbenchmark::microbenchmark(x <- idx^2, 
                               for (i in idx) {  y[i] <- idx[i]^2},
                               times = 1e3)
## Unit: microseconds
##                                     expr      min       lq       mean    median
##                               x <- idx^2   18.579   40.807   50.32104   52.2565
##  for (i in idx) {     y[i] <- idx[i]^2 } 1852.620 1941.765 2161.00315 2006.0200
##         uq      max neval cld
##    58.2865  176.224  1000  a 
##  2179.2910 6427.473  1000   b

Una tarea tan sencilla, programada en un bucle (segunda fila), tarda 40 veces más que hacerlo de forma vectorial (primera fila) (elevando cada elemento al cuadrado, iterando internamente, sin necesidad de implementar un bucle).

12.6 📝 Ejercicios

(haz click en las flechas para ver soluciones)

📝Ejercicio 1: modifica el código inferior para diseñar un bucle for de 5 iteraciones que recorra los 5 primeros impares y les sume uno.

  • Solución:
# Una forma
for (i in seq(1, 9, by = 2)) {
  
  print(i + 1)
}
## [1] 2
## [1] 4
## [1] 6
## [1] 8
## [1] 10
# Otra
for (i in c(1, 3, 5, 7, 9)) {
  
  print(i + 1)
}
## [1] 2
## [1] 4
## [1] 6
## [1] 8
## [1] 10
for (i in 1:5) {
  
  print(i)
}
## [1] 1
## [1] 2
## [1] 3
## [1] 4
## [1] 5

 

📝Ejercicio 2: modifica el código inferior para diseñar un bucle while que parta con una variable conteo <- 1 y pare cuando llegue a 6.

  • Solución:
conteo <- 1
while (conteo < 6) {
  
  print(conteo)
  conteo <- conteo + 1
}
## [1] 1
## [1] 2
## [1] 3
## [1] 4
## [1] 5
conteo <- 1
while (conteo == 2) {
  
  print(conteo)
}

 

📝Ejercicio 3: construye un bucle que recorra las primeras 8 filas del conjunto de datos datasets::airquality y que imprima un mensaje con la temperatura

  • Solución:
for (i in 1:8) {
  
  print(glue("La temperatura es de {airquality$Temp[i]}ºF"))
}
## La temperatura es de 67ºF
## La temperatura es de 72ºF
## La temperatura es de 74ºF
## La temperatura es de 62ºF
## La temperatura es de 56ºF
## La temperatura es de 66ºF
## La temperatura es de 65ºF
## La temperatura es de 59ºF

 

📝Ejercicio 4: diseña un bucle for de 200 iteraciones que, empezando en un valor inicial de 100 (euros), te sume 3€ si el número actual es par y te reste 5€ si es impar. Un número par o impar: un número par será todo aquel número que al dividir entre 2, la división es exacta, es decir, que su resto es nulo. Por ejemplo, al dividir 5 entre 2, el resto es 1, pero al dividir 12 entre 2 el resto es 0. Para calcula ese resto usaremos la función %%.

  • Solución:
# pares e impares
5 %% 2
## [1] 1
12 %% 2
## [1] 0
23 %% 2
## [1] 1
46 %% 2
## [1] 0
# dinero inicial
dinero <- 100

# Bucle for
for (i in 1:200) {
  
  dinero <- ifelse(dinero %% 2 == 0, dinero + 3, dinero  - 5)
  
}
dinero
## [1] -100

 

📝Ejercicio 5: diseña el anterior bucle pero guardando el dinero de cada iteración.

  • Solución:
# vector de importes
dinero <- rep(0, 201)
dinero[1] <- 100 # dinero inicial

# Bucle for
for (i in 2:201) {
  
  # si dinero[i - 1] es par o  impar
  dinero[i] <- ifelse(dinero[i - 1] %% 2 == 0, dinero[i - 1] + 3,
                      dinero[i - 1]  - 5)
  
}
dinero
##   [1]  100  103   98  101   96   99   94   97   92   95   90   93   88   91   86
##  [16]   89   84   87   82   85   80   83   78   81   76   79   74   77   72   75
##  [31]   70   73   68   71   66   69   64   67   62   65   60   63   58   61   56
##  [46]   59   54   57   52   55   50   53   48   51   46   49   44   47   42   45
##  [61]   40   43   38   41   36   39   34   37   32   35   30   33   28   31   26
##  [76]   29   24   27   22   25   20   23   18   21   16   19   14   17   12   15
##  [91]   10   13    8   11    6    9    4    7    2    5    0    3   -2    1   -4
## [106]   -1   -6   -3   -8   -5  -10   -7  -12   -9  -14  -11  -16  -13  -18  -15
## [121]  -20  -17  -22  -19  -24  -21  -26  -23  -28  -25  -30  -27  -32  -29  -34
## [136]  -31  -36  -33  -38  -35  -40  -37  -42  -39  -44  -41  -46  -43  -48  -45
## [151]  -50  -47  -52  -49  -54  -51  -56  -53  -58  -55  -60  -57  -62  -59  -64
## [166]  -61  -66  -63  -68  -65  -70  -67  -72  -69  -74  -71  -76  -73  -78  -75
## [181]  -80  -77  -82  -79  -84  -81  -86  -83  -88  -85  -90  -87  -92  -89  -94
## [196]  -91  -96  -93  -98  -95 -100

 

📝Ejercicio 6: diseña el bucle del ejercicio 4 parando cuando no nos quede dinero.

  • Solución:
dinero <- 100 # dinero inicial

# Bucle while
while (dinero > 0) {
  
  dinero <- ifelse(dinero %% 2 == 0, dinero + 3, dinero - 5)
  
}
dinero
## [1] 0