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 10.271106  9.283465
## 2  9.441269  9.068983
## 3 11.743186 11.590120
## 4 10.847266  8.186097
## 5  9.506233 10.220493
## 6 11.218890  8.110779

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  10.271106  9.283465 0.98764114    small
## 2   9.441269  9.068983 0.37228634    small
## 3  11.743186 11.590120 0.15306636    small
## 4  10.847266  8.186097 2.66116876    small
## 5   9.506233 10.220493 0.71425989    small
## 6  11.218890  8.110779 3.10811139      big
## 7  11.407552  8.171812 3.23573991      big
## 8  11.836197 11.543849 0.29234876    small
## 9   9.008792 11.342575 2.33378232    small
## 10  8.761947 10.650643 1.88869612    small
## 11 11.602918 10.066298 1.53662038    small
## 12  9.625425  9.672264 0.04683895    small
## 13  9.917394 10.234279 0.31688507    small
## 14 10.996360 10.642404 0.35395642    small
## 15 11.469860  9.834348 1.63551205    small
## 16  9.627214  8.794491 0.83272246    small
## 17  9.092141  9.445593 0.35345159    small
## 18  8.961830  9.667593 0.70576279    small
## 19  9.025131  9.513004 0.48787299    small
## 20  8.404514 10.030606 1.62609151    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  10.271106  9.283465 0.98764114    small
## 2   9.441269  9.068983 0.37228634    small
## 3  11.743186 11.590120 0.15306636    small
## 4  10.847266  8.186097 2.66116876    small
## 5   9.506233 10.220493 0.71425989    small
## 6  11.218890  8.110779 3.10811139      big
## 7  11.407552  8.171812 3.23573991      big
## 8  11.836197 11.543849 0.29234876    small
## 9   9.008792 11.342575 2.33378232    small
## 10  8.761947 10.650643 1.88869612    small
## 11 11.602918 10.066298 1.53662038    small
## 12  9.625425  9.672264 0.04683895    small
## 13  9.917394 10.234279 0.31688507    small
## 14 10.996360 10.642404 0.35395642    small
## 15 11.469860  9.834348 1.63551205    small
## 16  9.627214  8.794491 0.83272246    small
## 17  9.092141  9.445593 0.35345159    small
## 18  8.961830  9.667593 0.70576279    small
## 19  9.025131  9.513004 0.48787299    small
## 20  8.404514 10.030606 1.62609151    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] 11
# [2] VECTOR
sum(x %% 2 == 0)
## [1] 11
# 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
## [18] 324 361 400
# [2] VECTOR
(1:20)^2
##  [1]   1   4   9  16  25  36  49  64  81 100 121 144 169 196 225 256 289
## [18] 324 361 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.477
# [2] VECTOR
mean(sample(1:6, ntrials, replace = TRUE))
## [1] 3.505

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] 507 324 519 423 454 615 650 376 454 482 695 585 518 518 508 561 400
##  [18] 588 564 605 554 450 464 582 506 579 440 384 363 453 531 584 367 541
##  [35] 508 474 599 621 419 479 618 365 464 496 531 575 408 548 510 485 627
##  [52] 635 650 407 443 432 502 529 561 475 555 302 482 669 422 691 438 667
##  [69] 503 546 538 506 548 540 566 604 533 463 496 471 505 648 589 510 521
##  [86] 499 444 328 362 394 515 359 571 683 436 604 519 435 410 528
# FOR
sumRow <- rep(NA, times = nrow(myMat))
for(j in 1:nrow(myMat)){
  sumRow[j] <- sum(myMat[j, ])
}
print(sumRow)
##   [1] 507 324 519 423 454 615 650 376 454 482 695 585 518 518 508 561 400
##  [18] 588 564 605 554 450 464 582 506 579 440 384 363 453 531 584 367 541
##  [35] 508 474 599 621 419 479 618 365 464 496 531 575 408 548 510 485 627
##  [52] 635 650 407 443 432 502 529 561 475 555 302 482 669 422 691 438 667
##  [69] 503 546 538 506 548 540 566 604 533 463 496 471 505 648 589 510 521
##  [86] 499 444 328 362 394 515 359 571 683 436 604 519 435 410 528

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.43 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.355
## [1] 0.129
## [1] 1.586
## [1] 1.361
## [1] 0.995

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"
## [18] "w" "x" "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] 546 486 622 310 480 482 599 557 496 694 692 569 374 552 596 426 524
##  [18] 544 481 608 485 624 599 563 558 627 610 479 414 556 516 299 549 485
##  [35] 451 461 446 519 540 513 591 561 473 509 749 562 512 363 513 382 426
##  [52] 490 404 418 493 541 613 620 401 544 702 513 679 559 688 710 299 499
##  [69] 583 390 446 603 453 488 497 496 510 607 512 669 441 501 523 619 406
##  [86] 367 537 655 530 532 461 528 478 431 548 426 562 465 478 379

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

apply(X = myMat, MARGIN = 2, FUN = median)
##  [1] 46.5 48.5 61.0 47.5 48.5 50.0 55.0 61.0 50.5 50.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] 52.87 46.62 45.09 44.45 51.77 48.44 48.78 52.62 55.88 49.87
(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] 52.87 46.62 45.09 44.45 51.77 48.44 48.78 52.62 55.88 49.87
apply(X = myArr, MARGIN = 2, FUN = mean)
##  [1] 42.72 47.94 54.14 51.44 53.24 49.16 53.86 54.92 41.56 48.48 51.50
## [12] 45.50 60.00 39.68 47.08 48.76 51.62 46.94 51.58 52.66
(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] 42.72 47.94 54.14 51.44 53.24 49.16 53.86 54.92 41.56 48.48 51.50
## [12] 45.50 60.00 39.68 47.08 48.76 51.62 46.94 51.58 52.66
apply(X = myArr, MARGIN = 3, FUN = mean)
## [1] 48.450 51.645 48.855 54.020 45.225
c(mean(myArr[,,1]), mean(myArr[,,2]), mean(myArr[,,3]), 
  mean(myArr[,,4]), mean(myArr[,,5]))
## [1] 48.450 51.645 48.855 54.020 45.225

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,] 55.0 47.8 71.2 54.4 44.2 60.6 56.6 67.2 29.4  42.8  46.8  35.6  61.8
##  [2,] 39.0 75.8 57.6 46.8 44.8 54.2 66.2 67.4 42.6  29.6  71.4  44.2  41.0
##  [3,] 57.6 62.2 37.0 57.6 46.0 31.4 37.6 46.0 29.0  40.6  26.2  42.0  66.6
##  [4,] 37.0 26.6 47.8 56.0 30.8 36.4 57.4 44.2 29.6  70.2  50.0  34.6  29.2
##  [5,] 52.8 43.4 60.6 60.8 63.8 56.8 61.8 53.4 25.0  55.2  42.0  43.4  57.6
##  [6,] 53.4 60.4 31.4 28.0 52.2 62.8 43.2 56.2 50.8  39.8  37.8  52.6  79.8
##  [7,] 42.0 22.6 49.4 43.0 65.0 51.2 25.8 52.4 37.6  38.4  53.0  61.8  64.0
##  [8,] 21.0 49.0 76.0 41.8 58.0 49.4 58.6 56.0 51.2  63.8  70.4  59.6  52.6
##  [9,] 24.0 58.0 56.0 75.2 75.2 42.2 60.8 60.2 80.4  63.2  46.6  46.0  87.6
## [10,] 45.4 33.6 54.4 50.8 52.4 46.6 70.6 46.2 40.0  41.2  70.8  35.2  59.8
##       [,14] [,15] [,16] [,17] [,18] [,19] [,20]
##  [1,]  48.4  50.0  64.2  51.8  77.4  54.2  38.0
##  [2,]  19.8  32.8  21.2  37.2  23.0  52.2  65.6
##  [3,]  21.2  29.4  69.8  62.8  39.2  45.2  54.4
##  [4,]  39.2  52.6  54.6  36.2  42.6  54.6  59.4
##  [5,]  56.8  39.8  47.4  76.2  58.2  48.4  32.0
##  [6,]  34.0  52.8  44.4  53.2  40.8  57.4  37.8
##  [7,]  65.8  61.0  37.8  47.8  47.8  44.2  65.0
##  [8,]  32.4  44.0  60.8  58.2  37.8  47.0  64.8
##  [9,]  38.4  50.4  58.4  39.8  55.4  57.4  42.4
## [10,]  40.8  58.0  29.0  53.0  47.2  55.2  67.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] 44  4 12 22 98 70 46 35 39 97
## 
## $b
##  [1]  93  80  28  51  79  17  89  71 100  12
## 
## $c
##  [1] 77 20 14 67 41 89 46 91 75 97
## 
## $d
##  [1] 85 68 39 98 58 80 88 96 46 45
## 
## $e
##  [1] 86 68 53 76 32 83  4 90  6 77
lapply(myList, FUN = mean)
## $a
## [1] 46.7
## 
## $b
## [1] 62
## 
## $c
## [1] 61.7
## 
## $d
## [1] 70.3
## 
## $e
## [1] 57.5

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]  4  5  5  5 NA NA  5  5  2  5
## 
## $b
##  [1] 1 4 4 2 3 2 2 2 1 4
## 
## $c
##  [1]  2  2  3  5  2  5  4  4 NA  2
## 
## $d
##  [1] NA  5  2  3  5  5  3 NA  3  4
## 
## $e
##  [1] NA  5 NA NA  2  2  4  4  1  4
lapply(myList, FUN = mean)
## $a
## [1] NA
## 
## $b
## [1] 2.5
## 
## $c
## [1] NA
## 
## $d
## [1] NA
## 
## $e
## [1] NA
lapply(myList, FUN = mean, na.rm = TRUE)
## $a
## [1] 4.5
## 
## $b
## [1] 2.5
## 
## $c
## [1] 3.222222
## 
## $d
## [1] 3.75
## 
## $e
## [1] 3.142857

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] 4.5
## 
## $b
## [1] 2.5
## 
## $c
## [1] 3.222222
## 
## $d
## [1] 3.75
## 
## $e
## [1] 3.142857

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] 16 25 25 25 NA NA 25 25  4 25
## 
## $b
##  [1]  1 64 64  8 27  8  8  8  1 64
## 
## $c
##  [1]  4  4  9 25  4 25 16 16 NA  4
## 
## $d
##  [1] NA 25  4  9 25 25  9 NA  9 16
## 
## $e
##  [1] NA 25 NA NA  4  4 16 16  1 16

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] 4.5
## 
## $b
## [1] 2.5
## 
## $c
## [1] 3.222222
## 
## $d
## [1] 3.75
## 
## $e
## [1] 3.142857
sapply(myList, FUN = function(i){
  mean(i, na.rm = TRUE)
})
##        a        b        c        d        e 
## 4.500000 2.500000 3.222222 3.750000 3.142857

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 4 2 5 5

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        J  9.849620 15945.02 33.114214
## 2        F 10.441636 10768.43 19.398212
## 3        C 10.075986 13798.89 22.797092
## 4        D  9.972499 25502.94 28.845845
## 5        C 10.040076 10891.90 18.680606
## 6        E  9.924403 40233.85 37.582730
## 7        C 10.081690 11808.47 25.097151
## 8        D 10.154393 13113.78 21.082909
## 9        H 10.219178 30962.03 17.873006
## 10       A  9.706414 18548.85  9.455655

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. 
##   8.621   9.621   9.954   9.953  10.344  11.307 
## 
## $B
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.552   9.682  10.060  10.036  10.402  11.101 
## 
## $C
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.556   9.682  10.003  10.022  10.347  10.954 
## 
## $D
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.766   9.641   9.956   9.983  10.367  11.196 
## 
## $E
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.967   9.659  10.013   9.997  10.380  11.150 
## 
## $F
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.622   9.610   9.992   9.977  10.377  10.985 
## 
## $G
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.723   9.606   9.982   9.935  10.296  11.204 
## 
## $H
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.422   9.638  10.009   9.993  10.413  11.232 
## 
## $I
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.594   9.662   9.982   9.986  10.345  11.329 
## 
## $J
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.370   9.536  10.012   9.955  10.412  11.181

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  9.953261 23130.69 21.26433
## B 10.036156 24359.22 19.35617
## C 10.021846 22504.90 19.49788
## D  9.982547 25052.12 18.86833
## E  9.997101 24261.43 19.35158
## F  9.976600 25344.65 20.44562
## G  9.935284 24766.60 20.70463
## H  9.993497 26446.32 19.71905
## I  9.985877 24902.36 19.80869
## J  9.954635 25395.53 20.27956

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.