9 Algorítmico

9.1 Pruebas lógicas con if

Si queremos realizar una operación diferente según una condición, podemos configurar una prueba lógica del tipo SI esto ENTONCES esto SINO esto. Con R esto dará como resultado la función if(cond) cons.express alt.expr como se muestra en la función help.

myVar <- 2
if(myVar < 3) print("myVar < 3")
## [1] "myVar < 3"
if(myVar < 3) print("myVar < 3") else print("myVar > 3")
## [1] "myVar < 3"

Cuando hay varias líneas de código para ejecutar basadas en la prueba lógica, o simplemente para hacer que el código sea más fácil de leer, utilizamos varias líneas con {} y con identacion.

myVar <- 2
myResult <- 0
if(myVar < 3){
  print("myVar < 3")
  myResult <- myVar + 10
} else {
  print("myVar > 3")
  myResult <- myVar - 10
}
## [1] "myVar < 3"
print(myResult)
## [1] 12

En este ejemplo definimos una variable myVar. Si esta variable es menor que 3, la variable myResult se establece en myVar + 10, y de lo contrario myResult se establece en myVar - 10.

Ya hemos visto el uso de la prueba lógica if en el capítulo sobre las funciones. Habiamos probado si la variable ingresada como argumento en nuestra función era de tipo character.

myVar <- "qwerty"
if(is.character(myVar)){
  print("ok")
} else {
  print("error")
}
## [1] "ok"

También podemos anidar pruebas lógicas entre sí.

myVar <- TRUE
if(is.character(myVar)){
  print("myVar: character")
} else {
  if(is.numeric(myVar)){
    print("myVar: numeric")
  } else {
    if(is.logical(myVar)){
      print("myVar: logical")
    } else {
      print("myVar: ...")
    }
  }
}
## [1] "myVar: logical"

También es posible estipular varias condiciones, como vimos en el capítulo sobre operadores de comparación.

myVar <- 2
if(myVar > 1 & myVar < 50){
  print("ok")
}
## [1] "ok"

En este ejemplo, myVar está en formato numeric, por lo que la primera condición (> 1) y la segunda condición (< 50) son verificables. Por otro lado, si asignamos una variable de tipo character a myVar entonces R transformará 0 y 10 en objetos de tipo character y probará si myVar> "1" y despues si myVar < "50" basandose en la clasificación alfabética. En el siguiente ejemplo, "azerty" no está ubicado segun el orden alfabético entre "1" y "50", pero para "2azerty" es el caso, lo que resulta problematico.

myVar <- "azerty"
limInit <- 1
limEnd <- 50
if(myVar > limInit & myVar < limEnd){
  print(paste0(myVar, " is between ", limInit, " and ", limEnd, "."))
} else {
  print(paste0(myVar, " not between ", limInit, " and ", limEnd, "."))
}
## [1] "azerty not between 1 and 50."
myVar <- "2azerty"
if(myVar > limInit & myVar < limEnd){
  print(paste0(myVar, " is between ", limInit, " and ", limEnd, "."))
} else {
  print(paste0(myVar, " not between ", limInit, " and ", limEnd, "."))
}
## [1] "2azerty is between 1 and 50."

Entonces, lo que nos gustaría hacer es probar si myVar está en formato numeric, y entonces solo si es el caso probar las siguientes condiciones.

myVar <- "2azerty"
if(is.numeric(myVar)){
  if(myVar > limInit & myVar < limEnd){
    print(paste0(myVar, " is between ", limInit, " and ", limEnd, "."))
  } else {
    print(paste0(myVar, " not between ", limInit, " and ", limEnd, "."))
  }
} else {
  print(paste0("Object ", myVar, " is not numeric"))
}
## [1] "Object 2azerty is not numeric"

A veces es posible que necesitemos probar una primera condición y luego una segunda condición solo si la primera es verdadera en la misma prueba. Por ejemplo, para un sitio nos gustaría saber si hay una sola especie y probar si su abundancia es mayor que 10. Imagine un conjunto de datos con abundancia de vectores. Probaremos el número de especies con la función length().

mySpecies <- c(15, 14, 20, 12)
if(length(mySpecies) == 1 & mySpecies > 10){
  print("ok!")
}
## Warning message:
## In if (length(mySpecies) == 1 & mySpecies > 10) { :
##   the condition has length > 1 and only the first element will be used

R devuelve un error porque no puede dentro de una prueba lógica con if() verificar la segunda condición. De hecho, mySpecies > 10 devuelve TRUE TRUE TRUE TRUE TRUE. Podemos separar el código en dos condiciones:

mySpecies <- c(15, 14, 20, 12)
if(length(mySpecies) == 1){
  if(mySpecies > 10){
    print("ok!")
  }
}

Una alternativa más elegante es decirle a R que verifique la segunda condición solo si la primera es verdadera. Para eso podemos usar && en lugar de &.

mySpecies <- c(15, 14, 20, 12)
if(length(mySpecies) == 1 && mySpecies > 10){
  print("ok!")
}
mySpecies <- 15
if(length(mySpecies) == 1 && mySpecies > 10){
  print("ok!")
}
## [1] "ok!"
mySpecies <- 5
if(length(mySpecies) == 1 && mySpecies > 10){
  print("ok!")
}

Con & R comprobará todas las condiciones, y con && R tomará cada condición una después de la otra y continuará solo si es verdadera. Esto puede parecer anecdótico, pero es bueno saber la diferencia entre & y && porque a menudo los encontramos en los códigos disponibles en Internet o en los paquetes.

9.2 Pruebas lógicas con switch

La función switch() es una variante de if() que es útil cuando tenemos muchas opciones posibles para la misma expresión. El siguiente ejemplo muestra cómo transformar el código usando if() a switch().

x <- "aa"
if(x == "a"){
  result <- 1
}
if(x == "aa"){
  result <- 2
}
if(x == "aaa"){
  result <- 3
}
if(x == "aaaa"){
  result <- 4
}
print(result)
## [1] 2
x <- "aa"
switch(x, 
  a = result <- 1,
  aa = result <- 2,
  aaa = result <- 3,
  aaaa = result <- 4)
print(result)
## [1] 2

9.3 El bucle for

En programación, cuando tenemos que repetir la misma línea de código varias veces, es un signo que indica que debemos usar un bucle. Un bucle es una forma de iterar sobre un conjunto de objetos (o los elementos de un objeto) y repetir una operación. Imaginamos un data.frame con mediciones de datos de campo en dos fechas.

bdd <- data.frame(date01 = rnorm(n = 100, mean = 10, sd = 1), 
                  date02 = rnorm(n = 100, mean = 10, sd = 1))
print(head(bdd))
##     date01    date02
## 1 11.08157 12.042192
## 2 10.96308 10.681778
## 3 10.25868 12.997187
## 4 11.05507  9.531929
## 5 11.32432  8.984654
## 6 11.50681  9.361067

Nos gustaría cuantificar la diferencia entre la primera y la segunda fecha, luego poner un indicador para saber si esta diferencia es pequeña o grande, por ejemplo, con un umbral arbitrario de 3. Entonces, para cada línea podríamos hacer:

bdd$dif <- NA
bdd$isDifBig <- NA

bdd$dif[1] <- sqrt((bdd$date01[1] - bdd$date02[1])^2)
bdd$dif[2] <- sqrt((bdd$date01[2] - bdd$date02[2])^2)
bdd$dif[3] <- sqrt((bdd$date01[3] - bdd$date02[3])^2)
# ...
bdd$dif[100] <- sqrt((bdd$date01[100] - bdd$date02[100])^2)

if(bdd$dif[1] > 3){
  bdd$isDifBig[1] <- "big"
}else{
  bdd$isDifBig[1] <- "small"
}
if(bdd$dif[2] > 3){
  bdd$isDifBig[2] <- "big"
}else{
  bdd$isDifBig[2] <- "small"
}
if(bdd$dif[3] > 3){
  bdd$isDifBig[3] <- "big"
}else{
  bdd$isDifBig[3] <- "small"
}
# ...
if(bdd$dif[100] > 3){
  bdd$isDifBig[100] <- "big"
}else{
  bdd$isDifBig[100] <- "small"
}

Esta forma de hacer las cosas sería extremadamente tediosa de lograr, y casi imposible de lograr si la tabla contuviera 1000 o 100000 líneas. Puede parecer lógico querer iterar sobre las líneas de nuestro data.frame para obtener las nuevas columnas. Es lo que vamos a hacer aun que no es la solución que retendremos más adelante.

Vamos a usar un bucle for(). El bucle for() recorrerá los elementos de un objeto que vamos a dar como argumento. Por ejemplo, aquí hay un bucle que para todos los números del 3 al 9 calculará su valor al cuadrado. El valor actual del número está simbolizado por un objeto que puede tomar el nombre que queramos (aquí será i).

for(i in c(3, 4, 5, 6, 7, 8, 9)){
  print(i^2)
}
## [1] 9
## [1] 16
## [1] 25
## [1] 36
## [1] 49
## [1] 64
## [1] 81

Eso podemos mejorar usando la función :.

for(i in 3:9){
  print(i^2)
}

El bucle for() puede iterar sobre todos los tipos de elementos.

nChar <- c("a", "z", "e", "r", "t", "y")
for(i in nChar){
  print(i)
}
## [1] "a"
## [1] "z"
## [1] "e"
## [1] "r"
## [1] "t"
## [1] "y"

Volvamos a nuestro caso. Vamos a iterar sobre el número de líneas de nuestro data.frame bdd. Antes de eso crearemos las columnas dif y isDifBig con los valores NA. Luego usaremos la función nrow() para encontrar el número de líneas.

bdd$dif <- NA
bdd$isDifBig <- NA
for(i in 1:nrow(bdd)){
  bdd$dif[i] <- sqrt((bdd$date01[i] - bdd$date02[i])^2)
  if(bdd$dif[i] > 3){
    bdd$isDifBig[i] <- "big"
  }else{
    bdd$isDifBig[i] <- "small"
  }
}
print(head(bdd, n = 20))
##       date01    date02         dif isDifBig
## 1  11.081575 12.042192 0.960617588    small
## 2  10.963078 10.681778 0.281300831    small
## 3  10.258683 12.997187 2.738503709    small
## 4  11.055069  9.531929 1.523140137    small
## 5  11.324320  8.984654 2.339666153    small
## 6  11.506812  9.361067 2.145744547    small
## 7  10.668047 10.659740 0.008306904    small
## 8  12.717266  7.914531 4.802734454      big
## 9  10.057564 11.533825 1.476261463    small
## 10 10.732572 10.578534 0.154037817    small
## 11  9.017811  8.364674 0.653136933    small
## 12  9.155670  9.541653 0.385983074    small
## 13  8.607951  9.849409 1.241457364    small
## 14  8.897687 11.000430 2.102743764    small
## 15 11.247456  9.521813 1.725643223    small
## 16  9.112583 11.818153 2.705569598    small
## 17 10.283146  9.615653 0.667492151    small
## 18 10.111092  9.180646 0.930446076    small
## 19 10.434803  9.666041 0.768762246    small
## 20  9.190073 10.802210 1.612136941    small

En la práctica, esta no es la mejor manera de realizar este ejercicio porque se trata de cálculos simples en vectores contenidos en un data.frame. R es particularmente potente para realizar operaciones en vectores. Donde sea posible, siempre tenemos que enfócarnos en operaciones vectoriales. Aquí nuestro código se convierte en:

bdd$dif <- sqrt((bdd$date01 - bdd$date02)^2)
bdd$isDifBig <- "small"
bdd$isDifBig[bdd$dif > 3] <- "big"
print(head(bdd, n = 20))
##       date01    date02         dif isDifBig
## 1  11.081575 12.042192 0.960617588    small
## 2  10.963078 10.681778 0.281300831    small
## 3  10.258683 12.997187 2.738503709    small
## 4  11.055069  9.531929 1.523140137    small
## 5  11.324320  8.984654 2.339666153    small
## 6  11.506812  9.361067 2.145744547    small
## 7  10.668047 10.659740 0.008306904    small
## 8  12.717266  7.914531 4.802734454      big
## 9  10.057564 11.533825 1.476261463    small
## 10 10.732572 10.578534 0.154037817    small
## 11  9.017811  8.364674 0.653136933    small
## 12  9.155670  9.541653 0.385983074    small
## 13  8.607951  9.849409 1.241457364    small
## 14  8.897687 11.000430 2.102743764    small
## 15 11.247456  9.521813 1.725643223    small
## 16  9.112583 11.818153 2.705569598    small
## 17 10.283146  9.615653 0.667492151    small
## 18 10.111092  9.180646 0.930446076    small
## 19 10.434803  9.666041 0.768762246    small
## 20  9.190073 10.802210 1.612136941    small

La mayoría de los ejemplos que se pueden encontrar en Internet sobre el bucle for() pueden reemplazarse por operaciones vectoriales. Aquí hay algunos ejemplos adaptados de varias fuentes:

# prueba si los números son pares
# [1] FOR
x <- sample(1:100, size = 20)
count <- 0
for (val in x) {
  if(val %% 2 == 0){
    count <- count + 1
  }
}
print(count)
## [1] 12
# [2] VECTOR
sum(x %% 2 == 0)
## [1] 12
# calcular cuadrados
# [1] FOR
x <- rep(0, 20)
for (j in 1:20){
  x[j] <- j^2
}
print(x)
##  [1]   1   4   9  16  25  36  49  64  81 100 121 144 169 196 225 256 289 324 361
## [20] 400
# [2] VECTOR
(1:20)^2
##  [1]   1   4   9  16  25  36  49  64  81 100 121 144 169 196 225 256 289 324 361
## [20] 400
# repetir una tirada de dados y promediar
# [1] FOR
ntrials = 1000
trials = rep(0, ntrials)
for (j in 1:ntrials){
  trials[j] = sample(1:6, size = 1)
}
mean(trials)
## [1] 3.57
# [2] VECTOR
mean(sample(1:6, ntrials, replace = TRUE))
## [1] 3.536

Es un buen ejercicio explorar los muchos ejemplos disponibles en Internet en el bucle for() e intentar convertirlos en operaciones vectoriales. Esto nos permite adquirir buenos reflejos de programación con R. El bucle for() es muy útil, por ejemplo, para leer varios archivos y tratar la información que contienen de la misma manera, hacer gráficos, o Cuando las operaciones vectoriales se vuelven tediosas. Imagina una matriz de 10 columnas y 100 líneas. Queremos la suma de cada línea (veremos cómo hacer con la función apply() mas adelante).

myMat <- matrix(sample(1:100, size = 1000, replace = TRUE), ncol = 10)
# VECTOR
sumRow <- myMat[, 1] + myMat[, 2] + myMat[, 3] + myMat[, 4] + 
  myMat[, 5] + myMat[, 6] + myMat[, 7] + myMat[, 8] + 
  myMat[, 9] + myMat[, 10]
print(sumRow)
##   [1] 566 476 292 496 480 539 573 568 552 531 346 437 454 530 389 570 610 500
##  [19] 537 556 525 583 392 399 575 468 546 595 535 501 526 476 351 315 482 648
##  [37] 406 505 371 456 500 355 408 517 424 526 404 365 687 470 669 477 563 520
##  [55] 508 470 664 510 471 511 556 419 629 494 603 556 396 380 294 623 358 524
##  [73] 617 460 468 602 455 329 510 403 363 621 419 524 495 459 558 511 395 606
##  [91] 533 524 444 386 563 460 330 384 491 629
# FOR
sumRow <- rep(NA, times = nrow(myMat))
for(j in 1:nrow(myMat)){
  sumRow[j] <- sum(myMat[j, ])
}
print(sumRow)
##   [1] 566 476 292 496 480 539 573 568 552 531 346 437 454 530 389 570 610 500
##  [19] 537 556 525 583 392 399 575 468 546 595 535 501 526 476 351 315 482 648
##  [37] 406 505 371 456 500 355 408 517 424 526 404 365 687 470 669 477 563 520
##  [55] 508 470 664 510 471 511 556 419 629 494 603 556 396 380 294 623 358 524
##  [73] 617 460 468 602 455 329 510 403 363 621 419 524 495 459 558 511 395 606
##  [91] 533 524 444 386 563 460 330 384 491 629

En conclusión, se recomienda no usar el bucle for() con R siempre que sea posible, y en este capítulo veremos alternativas como los bucles familiares apply().

9.4 El bucle while

El bucle while(), a diferencia del bucle for(), significa MIENTRAS. Mientras no se cumpla una condición, el bucle continuará ejecutándose. Atención porque en caso de error, podemos programar fácilmente bucles que nunca terminan. Este bucle es menos común que el bucle for(). Tomemos un ejemplo:

i <- 0
while(i < 10){
  print(i)
  i <- i + 1
}
## [1] 0
## [1] 1
## [1] 2
## [1] 3
## [1] 4
## [1] 5
## [1] 6
## [1] 7
## [1] 8
## [1] 9

En este ejemplo, la variable i tiene como valor inicial 0. MIENTRAS QUE i < 10, mostramos i con print(). Para que este bucle finalice, no olvidamos cambiar el valor de i, esto se hace con la línea i <- i + 1. Cuando la condición i < 10 ya no se cumple, el bucle se detiene.

El bucle while() es muy útil para crear scripts que realizarán cálculos en variables cuyo valor cambia con el tiempo. Por ejemplo, imaginamos un número entre 0 y 10000 y un generador aleatorio que intentará determinar el valor de este número. Si queremos limitar los intentos de R a 2 segundos, podemos escribir el siguiente script (que debería funcionar cada vez en una computadora de escritorio típica que pueda realizar fácilmente 35000 pruebas en 2 segundos):

myNumber <- sample(x = 10000, size = 1)
myGuess <- sample(x = 10000, size = 1)
startTime <- Sys.time()
numberGuess <- 0
while(Sys.time() - startTime < 2){
  if(myGuess == myNumber){
    numberGuess <- numberGuess + 1
    print("Number found !")
    print(paste0("And I have plenty of time left: ", 
      round(2 - as.numeric(Sys.time() - startTime), digits = 2), 
      " sec"))
    break
  }else{
    myGuess <- sample(x = 10000, size = 1)
    numberGuess <- numberGuess + 1
  }
}
## [1] "Number found !"
## [1] "And I have plenty of time left: 1.25 sec"

En este script generamos un número aleatorio para adivinar con la función sample(), y cada uno de los intentos con la misma función sample(). Luego usamos la función Sys.time() (con una S mayúscula a Sys), para saber la hora de inicio del bucle. Siempre que la diferencia entre cada iteración del bucle y la hora de inicio sea inferior a 2 segundos, el bucle while() verificará si el número correcto estaba adivinando en la prueba lógica con if() y luego si es el caso nos informa que se encontró el número, y nos indica el tiempo restante antes de los dos segundos. Luego para finalizar el bucle usamos la palabra clave “break” en la que volveremos. En resumen, break, permite salir de un bucle. Si no se ha adivinado el número, el bucle realiza otra prueba con la función sample().

Más concretamente, podríamos imaginar algoritmos para explorar un espacio de soluciones a un problema con un tiempo limitado para lograrlo. El bucle while() también puede ser útil para que un script se ejecute solo cuando un archivo de otro programa esté disponible … En la práctica, el bucle while() se usa poco con R.

9.5 El bucle repeat

El bucle repeat() permite repetir una operación sin condiciones para verificar. Para salir de este bucle debemos usar la palabra clave break.

i <- 1
repeat{
  print(i^2)
  i <- i + 1
  if(i == 5){
    break
  }
}
## [1] 1
## [1] 4
## [1] 9
## [1] 16

Si volvemos al ejemplo anterior, podemos usar un bucle repeat() para repetirlo cinco veces.

numTry <- 0
repeat{
  myNumber <- sample(x = 10000, size = 1)
  myGuess <- sample(x = 10000, size = 1)
  startTime <- Sys.time()
  numberGuess <- 0
  while(Sys.time() - startTime < 2){
    if(myGuess == myNumber){
      numberGuess <- numberGuess + 1
      print(round(as.numeric(Sys.time() - startTime), digits = 3))
      break
    }else{
      myGuess <- sample(x = 10000, size = 1)
      numberGuess <- numberGuess + 1
    }
  }
  numTry <- numTry + 1
  if(numTry == 5){break}
}
## [1] 0.133
## [1] 1.484
## [1] 0.294
## [1] 0.424
## [1] 1.014

Al igual que el bucle while(), el bucle repeat() no se usa mucho con R.

9.6 next y break

Ya hemos visto la palabra clave break que permite salir del bucle actual. Por ejemplo, si buscamos el primer dígito después de 111 que es divisible por 32:

myVars <- 111:1000
for(myVar in myVars){
  if(myVar %% 32 == 0){
    print(myVar)
    break
  }
}
## [1] 128

Aunque hemos visto que en la práctica podemos evitar el bucle for() con una operación vectorial:

(111:1000)[111:1000 %% 32 == 0][1]
## [1] 128

La palabra clave next permite pasar a la siguiente iteración de un bucle si se cumple una determinada condición. Por ejemplo, si queremos imprimir las letras del alfabeto sin las vocales:

for(myLetter in letters){
  if(myLetter %in% c("a", "e", "i", "o", "u", "y")){
    next
  }
  print(myLetter)
}
## [1] "b"
## [1] "c"
## [1] "d"
## [1] "f"
## [1] "g"
## [1] "h"
## [1] "j"
## [1] "k"
## [1] "l"
## [1] "m"
## [1] "n"
## [1] "p"
## [1] "q"
## [1] "r"
## [1] "s"
## [1] "t"
## [1] "v"
## [1] "w"
## [1] "x"
## [1] "z"

De nuevo podimos evitar el bucle for() con:

letters[! letters %in% c("a", "e", "i", "o", "u", "y")]
##  [1] "b" "c" "d" "f" "g" "h" "j" "k" "l" "m" "n" "p" "q" "r" "s" "t" "v" "w" "x"
## [20] "z"

En conclusión, si usamos bucles, las palabras clave next y break suelen ser muy útiles, pero siempre que sea posible es mejor usar operaciones vectoriales. Cuando no es posible trabajar con vectores, es mejor usar los bucles del tipo apply que son el tema de la siguiente sección.

9.7 Los bucles de la familia apply

9.7.1 apply

La función apply() permite aplicar una función a todos los elementos de un array o un matrix. Por ejemplo, si queremos saber la suma de cada fila de una matriz de 10 columnas y 100 líneas:

myMat <- matrix(sample(1:100, size = 1000, replace = TRUE), ncol = 10)
apply(X = myMat, MARGIN = 1, FUN = sum)
##   [1] 525 543 455 464 347 541 483 482 341 461 664 361 687 364 576 569 484 523
##  [19] 372 426 404 529 465 571 560 643 553 604 346 534 524 485 392 424 371 569
##  [37] 505 520 567 390 505 419 359 578 553 353 529 544 467 558 449 433 539 413
##  [55] 436 418 530 654 398 524 443 448 382 557 525 509 470 436 415 521 444 408
##  [73] 576 333 595 588 596 375 405 659 435 507 617 489 585 532 450 451 397 417
##  [91] 629 587 581 591 562 375 587 455 463 571

Si queremos saber la mediana de cada columna, la expresión se convierte en:

apply(X = myMat, MARGIN = 2, FUN = median)
##  [1] 55.0 45.0 41.0 42.5 58.0 45.5 51.0 47.5 52.0 51.5

El argumento X es el objeto en el que el bucle apply se repetirá. El argumento MARGEN corresponde a la dimensión a tener en cuenta (1 para las filas y 2 para las columnas). El argumento FUN es la función a aplicar. En un objeto array, el argumento MARGIN puede tomar tantos valores como dimensiones. En este ejemplo, MARGIN = 1 es el promedio de cada fila - dimensión 1 - (todas las dimensiones combinadas), MARGIN = 2 es el promedio de cada columna - dimensión 2 - (todas las dimensiones combinadas), y MARGEN = 3 es el promedio de cada dimensión 3. Debajo cada cálculo se realiza de dos maneras diferentes para explicar su operación.

myArr <- array(sample(1:100, size = 1000, replace = TRUE), dim = c(10, 20, 5))
apply(X = myArr, MARGIN = 1, FUN = mean)
##  [1] 50.29 49.92 49.01 52.29 51.04 48.66 53.35 49.15 51.65 49.85
(apply(myArr[,,1], 1, mean) + apply(myArr[,,2], 1, mean) + 
  apply(myArr[,,3], 1, mean) + apply(myArr[,,4], 1, mean) + 
  apply(myArr[,,5], 1, mean))/5
##  [1] 50.29 49.92 49.01 52.29 51.04 48.66 53.35 49.15 51.65 49.85
apply(X = myArr, MARGIN = 2, FUN = mean)
##  [1] 54.42 57.04 52.74 48.84 43.46 52.88 48.68 61.30 45.30 53.44 47.56 52.64
## [13] 46.58 47.98 47.82 49.94 54.38 47.96 45.90 51.56
(apply(myArr[,,1], 2, mean) + apply(myArr[,,2], 2, mean) + 
  apply(myArr[,,3], 2, mean) + apply(myArr[,,4], 2, mean) + 
  apply(myArr[,,5], 2, mean))/5
##  [1] 54.42 57.04 52.74 48.84 43.46 52.88 48.68 61.30 45.30 53.44 47.56 52.64
## [13] 46.58 47.98 47.82 49.94 54.38 47.96 45.90 51.56
apply(X = myArr, MARGIN = 3, FUN = mean)
## [1] 52.850 50.590 51.345 49.480 48.340
c(mean(myArr[,,1]), mean(myArr[,,2]), mean(myArr[,,3]), 
  mean(myArr[,,4]), mean(myArr[,,5]))
## [1] 52.850 50.590 51.345 49.480 48.340

También podemos calcular el promedio de cada fila y valor de columna (la función luego itera en la dimensión 3):

apply(X = myArr, MARGIN = c(1, 2), FUN = mean)
##       [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11] [,12] [,13]
##  [1,] 62.4 52.2 44.8 47.8 27.0 56.4 35.8 64.8 60.0  49.0  42.8  72.0  32.8
##  [2,] 55.0 65.6 41.8 50.0 36.0 64.6 35.2 54.2 50.6  51.0  36.6  49.0  73.4
##  [3,] 53.6 47.6 52.4 22.0 44.6 43.4 50.8 64.6 43.6  62.8  63.8  41.0  36.0
##  [4,] 49.0 52.6 54.6 33.4 60.2 44.2 64.4 61.8 41.2  73.2  33.4  64.2  37.6
##  [5,] 53.4 28.4 66.0 64.4 39.4 71.2 60.6 44.6 44.2  56.8  55.2  62.4  31.8
##  [6,] 50.8 49.2 45.2 50.4 45.2 62.2 26.8 65.8 58.6  51.0  58.2  26.8  43.2
##  [7,] 43.4 77.2 65.2 60.0 56.2 47.6 56.4 58.4 43.8  55.2  46.0  57.6  56.6
##  [8,] 52.2 77.2 39.0 55.0 44.4 64.2 63.8 67.4 18.8  45.0  43.8  61.0  61.6
##  [9,] 57.6 64.2 64.4 49.8 48.4 35.6 46.4 58.6 56.4  62.2  41.2  42.2  51.2
## [10,] 66.8 56.2 54.0 55.6 33.2 39.4 46.6 72.8 35.8  28.2  54.6  50.2  41.6
##       [,14] [,15] [,16] [,17] [,18] [,19] [,20]
##  [1,]  61.6  30.0  63.4  44.8  47.2  51.6  59.4
##  [2,]  45.0  39.2  40.0  66.0  44.6  53.8  46.8
##  [3,]  55.0  67.6  49.4  55.8  66.6  31.4  28.2
##  [4,]  54.6  50.0  49.2  55.2  49.4  56.6  61.0
##  [5,]  46.0  57.0  67.2  23.4  49.4  46.4  53.0
##  [6,]  41.2  37.0  39.4  59.2  59.4  55.4  48.2
##  [7,]  35.4  51.8  44.6  66.6  35.2  37.8  72.0
##  [8,]  33.4  48.0  31.6  43.6  43.4  20.6  69.0
##  [9,]  48.2  58.2  57.6  54.0  40.8  49.2  46.8
## [10,]  59.4  39.4  57.0  75.2  43.6  56.2  31.2

9.7.2 lapply

Como se indica en la documentación, lapply() devuelve una lista de la misma longitud que X, y cada elemento resulta de la aplicación FUN al elemento X correspondiente. Si X es una list que contiene vector y estamos tratando de obtener el promedio de cada elemento de list, podemos usar la función lapply():

myList <- list(
  a = sample(1:100, size = 10), 
  b = sample(1:100, size = 10), 
  c = sample(1:100, size = 10), 
  d = sample(1:100, size = 10), 
  e = sample(1:100, size = 10)
)
print(myList)
## $a
##  [1]  8 45  7 71 83 57 47  6 69 87
## 
## $b
##  [1] 82 19 20 94 68 88 36 66  3 43
## 
## $c
##  [1] 53 23  7 66 60 59 20 17 24 13
## 
## $d
##  [1] 89 99 30 67 61 48 81 64 59 40
## 
## $e
##  [1]  73  39   3 100   8  14  99   2  67  15
lapply(myList, FUN = mean)
## $a
## [1] 48
## 
## $b
## [1] 51.9
## 
## $c
## [1] 34.2
## 
## $d
## [1] 63.8
## 
## $e
## [1] 42

Al igual que con la función apply(), podemos pasar argumentos adicionales a la función lapply() agregándolos después de la función. Esto es útil, por ejemplo, si nuestra list contiene estos valores faltantes NA y queremos ignorarlos para calcular los promedios (con el argumento na.rm = TRUE).

myList <- list(
  a = sample(c(1:5, NA), size = 10, replace = TRUE), 
  b = sample(c(1:5, NA), size = 10, replace = TRUE), 
  c = sample(c(1:5, NA), size = 10, replace = TRUE), 
  d = sample(c(1:5, NA), size = 10, replace = TRUE), 
  e = sample(c(1:5, NA), size = 10, replace = TRUE)
)
print(myList)
## $a
##  [1]  1  5  5  5 NA  2 NA  2  5  3
## 
## $b
##  [1]  2  1  5  3 NA  2  4  3  1  1
## 
## $c
##  [1]  5  1  4  1 NA  3  1  3  3  4
## 
## $d
##  [1]  5  2 NA  4 NA  5 NA  1  3  5
## 
## $e
##  [1]  4  3  1  5  3  2 NA NA  1  4
lapply(myList, FUN = mean)
## $a
## [1] NA
## 
## $b
## [1] NA
## 
## $c
## [1] NA
## 
## $d
## [1] NA
## 
## $e
## [1] NA
lapply(myList, FUN = mean, na.rm = TRUE)
## $a
## [1] 3.5
## 
## $b
## [1] 2.444444
## 
## $c
## [1] 2.777778
## 
## $d
## [1] 3.571429
## 
## $e
## [1] 2.875

Para mayor legibilidad o si se debemos realizar varias operaciones dentro del argumento FUN, podemos usar el siguiente script:

lapply(myList, FUN = function(i){
  mean(i, na.rm = TRUE)
})
## $a
## [1] 3.5
## 
## $b
## [1] 2.444444
## 
## $c
## [1] 2.777778
## 
## $d
## [1] 3.571429
## 
## $e
## [1] 2.875

Por ejemplo, si queremos obtener i^2 si el promedio es mayor que 3, y i^3 de lo contrario:

lapply(myList, FUN = function(i){
  m <- mean(i, na.rm = TRUE)
  if(m > 3){
    return(i^2)  
  }else{
    return(i^3)
  }
})
## $a
##  [1]  1 25 25 25 NA  4 NA  4 25  9
## 
## $b
##  [1]   8   1 125  27  NA   8  64  27   1   1
## 
## $c
##  [1] 125   1  64   1  NA  27   1  27  27  64
## 
## $d
##  [1] 25  4 NA 16 NA 25 NA  1  9 25
## 
## $e
##  [1]  64  27   1 125  27   8  NA  NA   1  64

9.7.3 sapply

La función sapply() es una versión modificada de la función lapply() que realiza la misma operación pero devuelve el resultado en un formato simplificado siempre que sea posible.

lapply(myList, FUN = function(i){
  mean(i, na.rm = TRUE)
})
## $a
## [1] 3.5
## 
## $b
## [1] 2.444444
## 
## $c
## [1] 2.777778
## 
## $d
## [1] 3.571429
## 
## $e
## [1] 2.875
sapply(myList, FUN = function(i){
  mean(i, na.rm = TRUE)
})
##        a        b        c        d        e 
## 3.500000 2.444444 2.777778 3.571429 2.875000

La función sapply() es interesante para recuperar, por ejemplo, el elemento “n” de cada elemento de una list. La función que se llama para hacer esto es '[['.

sapply(myList, FUN = '[[', 2)
## a b c d e 
## 5 1 1 2 3

9.7.4 tapply

La función tapply() permite aplicar una función tomando como elemento para iterar una variable existente. Imaginamos información sobre especies representadas por letras mayúsculas (por ejemplo, A, B, C) y valores de mediciones biologicas en diferentes ubicaciones.

species <- sample(LETTERS[1:10], size = 1000, replace = TRUE)
perf1 <- rnorm(n = 1000, mean = 10, sd = 0.5)
perf2 <- rlnorm(n = 1000, meanlog = 10, sdlog = 0.5)
perf3 <- rgamma(n = 1000, shape = 10, rate = 0.5)
dfSpecies <- data.frame(species, perf1, perf2, perf3)
print(head(dfSpecies, n = 10))
##    species     perf1    perf2    perf3
## 1        E  9.886035 25322.41 32.96742
## 2        D  9.610285 36512.66 25.90880
## 3        I  9.799424 11766.75 18.75991
## 4        F  9.891675 27291.72 18.64142
## 5        F  9.980331 17935.19 19.80527
## 6        H  9.211741 32307.61 24.09375
## 7        C 10.034522 13761.65 21.88348
## 8        E 10.705197 35397.58 24.54154
## 9        D 10.715855 23887.16 23.12923
## 10       G  9.950672 42734.65 13.00897

Podemos obtener fácilmente un resumen de las mediciones para cada especie con la función tapply() y la función summary().

tapply(dfSpecies$perf1, INDEX = dfSpecies$species, FUN = summary)
## $A
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   9.137   9.699  10.030  10.052  10.363  11.091 
## 
## $B
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.857   9.683   9.948   9.951  10.229  11.076 
## 
## $C
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.977   9.608  10.010   9.985  10.329  11.289 
## 
## $D
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.882   9.775   9.989  10.038  10.338  11.224 
## 
## $E
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   9.008   9.672   9.898   9.974  10.275  11.310 
## 
## $F
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.799   9.593   9.980   9.946  10.214  11.639 
## 
## $G
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.887   9.536   9.848   9.881  10.196  11.292 
## 
## $H
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.877   9.659   9.957   9.931  10.202  10.941 
## 
## $I
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    9.06    9.83   10.02   10.05   10.34   11.16 
## 
## $J
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.747   9.688  10.030  10.012  10.403  11.203

También podemos obtener el valor promedio de cada mediciones combinando una función sapply() con la función tapply() y usando la función mean().

sapply(2:4, FUN = function(i){
  tapply(dfSpecies[,i], INDEX = dfSpecies$species, FUN = mean)
})
##        [,1]     [,2]     [,3]
## A 10.051571 24122.66 20.00674
## B  9.951009 25419.83 20.31136
## C  9.985461 25122.29 20.38010
## D 10.037936 25878.14 20.39121
## E  9.973991 23247.50 20.02727
## F  9.945782 24336.39 18.27318
## G  9.881427 24325.28 19.26992
## H  9.930504 24372.00 19.96906
## I 10.045332 26270.76 19.52240
## J 10.011758 23560.11 20.55293

9.7.5 mapply

La función mapply() es una versión de la función sapply() que usa múltiples argumentos. Por ejemplo, si tenemos una lista de dos elementos 1:5 y 5:1 y queremos agregar 10 al primer elemento y 100 al segundo elemento:

mapply(FUN = function(i, j){i+j}, i = list(1:5, 5:1), j = c(10, 100))
##      [,1] [,2]
## [1,]   11  105
## [2,]   12  104
## [3,]   13  103
## [4,]   14  102
## [5,]   15  101

9.8 Conclusión

Felicitaciones, hemos llegado al final de este capítulo sobre algoritmos. Recordemos este mensaje clave: cuando una operación debe realizarse más de dos veces en un script y repetir el código que ya se ha escrito, es un signo que nos debe llevar a utilizar un bucle. Sin embargo, siempre que sea posible, se recomienda no usar los bucles tradicionales for(), while(), y repeat(), sino preferir operaciones sobre vectores o bucles de la familia apply. Esto puede ser difícil de integrar al principio, pero veremos que nuestros scripts serán más fáciles de mantener y leer, y mucho más eficientes si seguimos estos hábitos.