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  9.301778 10.715599
## 2 10.545081 10.487634
## 3 10.522290 10.996690
## 4 10.710894 11.110614
## 5 10.554023  9.973743
## 6 10.317723  8.306726

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   9.301778 10.715599 1.41382070    small
## 2  10.545081 10.487634 0.05744742    small
## 3  10.522290 10.996690 0.47439950    small
## 4  10.710894 11.110614 0.39972001    small
## 5  10.554023  9.973743 0.58027980    small
## 6  10.317723  8.306726 2.01099645    small
## 7   9.555368  9.078112 0.47725614    small
## 8  10.572966  9.588702 0.98426340    small
## 9  11.624998  8.698841 2.92615711    small
## 10  7.793133 10.335574 2.54244117    small
## 11 11.086542  9.774731 1.31181180    small
## 12 10.185221  9.632241 0.55298012    small
## 13 10.575674  9.730773 0.84490047    small
## 14  9.591491  9.985848 0.39435735    small
## 15  9.904906 11.339704 1.43479789    small
## 16  9.157575  9.802442 0.64486734    small
## 17  9.679637 10.635254 0.95561701    small
## 18 11.163929 10.865614 0.29831478    small
## 19 10.707952 10.181741 0.52621042    small
## 20  8.762087  9.376552 0.61446446    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   9.301778 10.715599 1.41382070    small
## 2  10.545081 10.487634 0.05744742    small
## 3  10.522290 10.996690 0.47439950    small
## 4  10.710894 11.110614 0.39972001    small
## 5  10.554023  9.973743 0.58027980    small
## 6  10.317723  8.306726 2.01099645    small
## 7   9.555368  9.078112 0.47725614    small
## 8  10.572966  9.588702 0.98426340    small
## 9  11.624998  8.698841 2.92615711    small
## 10  7.793133 10.335574 2.54244117    small
## 11 11.086542  9.774731 1.31181180    small
## 12 10.185221  9.632241 0.55298012    small
## 13 10.575674  9.730773 0.84490047    small
## 14  9.591491  9.985848 0.39435735    small
## 15  9.904906 11.339704 1.43479789    small
## 16  9.157575  9.802442 0.64486734    small
## 17  9.679637 10.635254 0.95561701    small
## 18 11.163929 10.865614 0.29831478    small
## 19 10.707952 10.181741 0.52621042    small
## 20  8.762087  9.376552 0.61446446    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] 8
# [2] VECTOR
sum(x %% 2 == 0)
## [1] 8
# 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.463
# [2] VECTOR
mean(sample(1:6, ntrials, replace = TRUE))
## [1] 3.5

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] 440 557 521 513 609 599 640 618 314 513 437 553 386 467 518 404 518
##  [18] 693 430 632 469 469 531 527 509 445 537 553 574 393 404 557 401 672
##  [35] 456 351 616 472 333 330 455 467 615 503 497 466 437 492 541 410 427
##  [52] 320 566 512 596 503 534 536 439 478 454 510 527 550 424 554 640 563
##  [69] 465 449 461 563 492 551 563 507 374 531 368 434 457 594 463 577 353
##  [86] 661 514 529 505 433 499 593 525 432 569 503 474 467 433 494
# FOR
sumRow <- rep(NA, times = nrow(myMat))
for(j in 1:nrow(myMat)){
  sumRow[j] <- sum(myMat[j, ])
}
print(sumRow)
##   [1] 440 557 521 513 609 599 640 618 314 513 437 553 386 467 518 404 518
##  [18] 693 430 632 469 469 531 527 509 445 537 553 574 393 404 557 401 672
##  [35] 456 351 616 472 333 330 455 467 615 503 497 466 437 492 541 410 427
##  [52] 320 566 512 596 503 534 536 439 478 454 510 527 550 424 554 640 563
##  [69] 465 449 461 563 492 551 563 507 374 531 368 434 457 594 463 577 353
##  [86] 661 514 529 505 433 499 593 525 432 569 503 474 467 433 494

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.84 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.554
## [1] 0.605
## [1] 0.097
## [1] 0.069
## [1] 0.244

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] 700 570 458 669 660 502 476 421 465 667 463 554 482 416 610 443 451
##  [18] 426 606 429 536 584 595 541 431 465 563 531 547 542 566 628 604 409
##  [35] 436 496 542 480 395 689 452 445 591 640 380 524 369 499 603 608 638
##  [52] 563 401 455 295 586 548 351 492 646 535 489 418 452 569 442 475 618
##  [69] 500 579 668 346 558 368 247 504 369 628 537 614 412 426 533 589 500
##  [86] 541 541 430 552 399 437 567 362 483 515 528 566 757 522 459

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

apply(X = myMat, MARGIN = 2, FUN = median)
##  [1] 51.0 59.5 56.0 60.5 51.5 49.5 46.0 53.5 59.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 52.08 48.04 48.46 53.67 48.38 52.19 48.03 55.11 53.40
(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 52.08 48.04 48.46 53.67 48.38 52.19 48.03 55.11 53.40
apply(X = myArr, MARGIN = 2, FUN = mean)
##  [1] 51.16 56.46 49.94 44.72 45.24 49.04 55.40 53.58 54.32 49.58 46.78
## [12] 54.28 49.12 52.98 56.30 49.52 51.46 48.64 51.04 54.90
(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] 51.16 56.46 49.94 44.72 45.24 49.04 55.40 53.58 54.32 49.58 46.78
## [12] 54.28 49.12 52.98 56.30 49.52 51.46 48.64 51.04 54.90
apply(X = myArr, MARGIN = 3, FUN = mean)
## [1] 51.180 48.535 52.105 49.780 54.515
c(mean(myArr[,,1]), mean(myArr[,,2]), mean(myArr[,,3]), 
  mean(myArr[,,4]), mean(myArr[,,5]))
## [1] 51.180 48.535 52.105 49.780 54.515

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,] 52.6 70.2 39.2 41.8 40.2 44.4 63.2 54.4 63.2  32.2  31.4  52.0  43.6
##  [2,] 70.6 59.6 53.0 57.6 41.8 54.0 54.2 39.0 34.6  25.6  69.0  50.2  45.4
##  [3,] 41.2 39.6 53.2 52.4 41.6 56.0 22.8 60.4 47.2  49.6  37.2  71.2  60.6
##  [4,] 34.6 45.2 45.0 46.0 74.6 40.0 79.8 32.8 36.2  67.6  37.4  46.8  41.2
##  [5,] 46.0 54.2 45.2 50.8 39.4 39.0 63.4 70.8 47.6  32.0  66.8  69.4  27.8
##  [6,] 38.0 55.0 42.0 43.0 40.0 44.4 59.8 61.8 63.6  57.6  59.8  61.4  45.2
##  [7,] 47.6 80.2 46.2 46.4 42.8 47.0 58.2 66.2 57.6  60.4  38.6  47.6  62.0
##  [8,] 49.0 58.2 39.8 30.4 45.4 42.4 45.8 53.0 55.2  61.6  43.8  43.6  57.6
##  [9,] 65.4 46.6 64.0 44.8 55.4 58.0 56.8 60.6 74.6  49.2  42.0  62.4  53.8
## [10,] 66.6 55.8 71.8 34.0 31.2 65.2 50.0 36.8 63.4  60.0  41.8  38.2  54.0
##       [,14] [,15] [,16] [,17] [,18] [,19] [,20]
##  [1,]  33.2  86.0  47.4  73.2  63.4  42.0  83.8
##  [2,]  71.4  36.8  65.6  65.0  66.6  42.4  39.2
##  [3,]  52.4  45.6  55.6  52.2  41.4  38.4  42.2
##  [4,]  39.8  63.2  39.2  40.0  64.6  34.6  60.6
##  [5,]  64.0  72.8  70.2  51.6  17.2  73.2  72.0
##  [6,]  47.0  25.2  44.6  25.2  37.6  65.8  50.6
##  [7,]  58.0  49.8  43.6  41.6  34.8  51.2  64.0
##  [8,]  39.0  62.0  65.2  49.0  27.4  57.8  34.4
##  [9,]  61.2  47.0  22.4  54.2  75.4  39.2  69.2
## [10,]  63.8  74.6  41.4  62.6  58.0  65.8  33.0

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] 59 85  3 11 77 26 95 86 82 51
## 
## $b
##  [1] 24  4 97 26  1 88 77  2 12 33
## 
## $c
##  [1] 36 30 32 15 19 28 55 90 42 73
## 
## $d
##  [1] 14 97 27 35 11 95 98 62 66 86
## 
## $e
##  [1] 35 26 22 45  6 51 95 71 10 85
lapply(myList, FUN = mean)
## $a
## [1] 57.5
## 
## $b
## [1] 36.4
## 
## $c
## [1] 42
## 
## $d
## [1] 59.1
## 
## $e
## [1] 44.6

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] NA  5  2  4  2  2  4  5  5  4
## 
## $b
##  [1]  1  3 NA NA NA NA  3  5  5  4
## 
## $c
##  [1] 5 1 5 5 3 4 4 1 5 3
## 
## $d
##  [1]  2 NA  2  1  3  4  4  3 NA  5
## 
## $e
##  [1] 1 3 5 4 1 3 5 3 3 1
lapply(myList, FUN = mean)
## $a
## [1] NA
## 
## $b
## [1] NA
## 
## $c
## [1] 3.6
## 
## $d
## [1] NA
## 
## $e
## [1] 2.9
lapply(myList, FUN = mean, na.rm = TRUE)
## $a
## [1] 3.666667
## 
## $b
## [1] 3.5
## 
## $c
## [1] 3.6
## 
## $d
## [1] 3
## 
## $e
## [1] 2.9

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.666667
## 
## $b
## [1] 3.5
## 
## $c
## [1] 3.6
## 
## $d
## [1] 3
## 
## $e
## [1] 2.9

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

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.666667
## 
## $b
## [1] 3.5
## 
## $c
## [1] 3.6
## 
## $d
## [1] 3
## 
## $e
## [1] 2.9
sapply(myList, FUN = function(i){
  mean(i, na.rm = TRUE)
})
##        a        b        c        d        e 
## 3.666667 3.500000 3.600000 3.000000 2.900000

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  3  1 NA  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        G 10.394016 16286.94  9.881555
## 2        D  8.900278 40163.99 18.305014
## 3        D  9.865840 14574.48 41.462006
## 4        I  9.838521 17523.70 14.060432
## 5        C 10.035662 19437.21 36.113822
## 6        C  9.645201 27188.28 12.144912
## 7        A  9.944548 25469.25  7.538817
## 8        B 10.160569 20999.31 22.837892
## 9        B  9.629863 28729.89 15.276244
## 10       H  9.461601 53981.01 18.789801

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.917   9.667   9.989  10.006  10.329  11.029 
## 
## $B
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.687   9.684  10.032  10.016  10.355  11.155 
## 
## $C
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.525   9.638   9.997   9.928  10.261  10.725 
## 
## $D
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.311   9.567  10.028   9.995  10.418  11.218 
## 
## $E
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.793   9.761   9.983  10.029  10.313  11.499 
## 
## $F
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.821   9.647   9.973  10.018  10.462  11.428 
## 
## $G
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.510   9.669  10.043  10.004  10.351  11.100 
## 
## $H
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.924   9.634  10.038  10.029  10.346  11.412 
## 
## $I
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.188   9.724   9.984   9.979  10.264  11.085 
## 
## $J
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.714   9.712  10.065  10.052  10.385  11.055

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.005591 23598.36 19.88203
## B 10.016295 23200.91 19.41561
## C  9.928500 25172.28 19.50144
## D  9.995103 28179.31 20.20496
## E 10.028706 23700.84 20.33883
## F 10.018154 25005.12 20.44254
## G 10.003998 25081.15 18.73344
## H 10.028906 24695.09 19.46131
## I  9.979317 25689.27 21.13171
## J 10.052341 23682.53 19.95923

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.