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.719458  8.705486
## 2 10.615751  8.970232
## 3 11.489622  9.578997
## 4 10.969352  9.996233
## 5 10.763592  8.006116
## 6  8.043056 10.352328

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.719458  8.705486 1.01397192    small
## 2  10.615751  8.970232 1.64551906    small
## 3  11.489622  9.578997 1.91062475    small
## 4  10.969352  9.996233 0.97311922    small
## 5  10.763592  8.006116 2.75747609    small
## 6   8.043056 10.352328 2.30927228    small
## 7  10.308829 10.744864 0.43603505    small
## 8  10.328023 10.854178 0.52615479    small
## 9   9.618543  9.221003 0.39753939    small
## 10  8.594911  9.005062 0.41015150    small
## 11 10.257861 10.716142 0.45828025    small
## 12 10.027734 10.582536 0.55480117    small
## 13  9.139714  9.659108 0.51939478    small
## 14  8.493198 11.423924 2.93072531    small
## 15 11.508191 10.753088 0.75510242    small
## 16  9.493427  9.359546 0.13388117    small
## 17 10.795847 10.052996 0.74285027    small
## 18 10.667033 11.476606 0.80957242    small
## 19 10.955230  8.861647 2.09358311    small
## 20  9.279428  9.353000 0.07357146    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.719458  8.705486 1.01397192    small
## 2  10.615751  8.970232 1.64551906    small
## 3  11.489622  9.578997 1.91062475    small
## 4  10.969352  9.996233 0.97311922    small
## 5  10.763592  8.006116 2.75747609    small
## 6   8.043056 10.352328 2.30927228    small
## 7  10.308829 10.744864 0.43603505    small
## 8  10.328023 10.854178 0.52615479    small
## 9   9.618543  9.221003 0.39753939    small
## 10  8.594911  9.005062 0.41015150    small
## 11 10.257861 10.716142 0.45828025    small
## 12 10.027734 10.582536 0.55480117    small
## 13  9.139714  9.659108 0.51939478    small
## 14  8.493198 11.423924 2.93072531    small
## 15 11.508191 10.753088 0.75510242    small
## 16  9.493427  9.359546 0.13388117    small
## 17 10.795847 10.052996 0.74285027    small
## 18 10.667033 11.476606 0.80957242    small
## 19 10.955230  8.861647 2.09358311    small
## 20  9.279428  9.353000 0.07357146    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] 10
# [2] VECTOR
sum(x %% 2 == 0)
## [1] 10
# 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.507
# [2] VECTOR
mean(sample(1:6, ntrials, replace = TRUE))
## [1] 3.555

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] 481 565 544 373 650 396 641 595 663 361 456 554 308 591 468 567 606 462
##  [19] 515 619 534 593 592 660 442 373 309 564 398 498 472 507 496 484 481 550
##  [37] 319 334 463 683 472 516 576 655 409 526 524 549 507 521 467 408 566 450
##  [55] 374 677 622 595 529 591 456 468 578 491 471 698 666 387 433 434 361 684
##  [73] 579 470 574 492 540 496 465 458 598 447 502 415 517 461 418 341 318 361
##  [91] 571 637 425 443 540 420 522 490 319 525
# FOR
sumRow <- rep(NA, times = nrow(myMat))
for(j in 1:nrow(myMat)){
  sumRow[j] <- sum(myMat[j, ])
}
print(sumRow)
##   [1] 481 565 544 373 650 396 641 595 663 361 456 554 308 591 468 567 606 462
##  [19] 515 619 534 593 592 660 442 373 309 564 398 498 472 507 496 484 481 550
##  [37] 319 334 463 683 472 516 576 655 409 526 524 549 507 521 467 408 566 450
##  [55] 374 677 622 595 529 591 456 468 578 491 471 698 666 387 433 434 361 684
##  [73] 579 470 574 492 540 496 465 458 598 447 502 415 517 461 418 341 318 361
##  [91] 571 637 425 443 540 420 522 490 319 525

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.5 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] 1.815
## [1] 1.762
## [1] 0.514
## [1] 0.401
## [1] 1.178

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] 466 475 521 450 338 625 400 514 672 397 546 438 574 557 469 512 377 613
##  [19] 514 482 630 660 452 491 436 737 623 706 606 452 587 417 504 458 506 508
##  [37] 614 463 562 525 465 357 459 598 469 370 452 413 497 347 545 454 307 629
##  [55] 455 603 497 325 435 563 332 727 664 425 348 467 554 540 604 462 425 454
##  [73] 232 383 530 659 396 501 500 716 531 403 475 518 457 594 329 350 403 464
##  [91] 493 589 667 314 489 613 542 602 433 547

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

apply(X = myMat, MARGIN = 2, FUN = median)
##  [1] 41.0 47.5 55.0 56.5 47.0 39.5 56.0 58.0 50.0 50.0

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] 48.62 50.92 50.28 51.40 51.26 49.71 50.68 45.50 51.09 47.90
(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] 48.62 50.92 50.28 51.40 51.26 49.71 50.68 45.50 51.09 47.90
apply(X = myArr, MARGIN = 2, FUN = mean)
##  [1] 52.22 46.72 51.88 47.74 54.60 47.28 53.46 49.02 50.44 48.44 54.82 47.60
## [13] 50.30 43.50 40.76 55.16 51.50 50.90 47.88 50.50
(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] 52.22 46.72 51.88 47.74 54.60 47.28 53.46 49.02 50.44 48.44 54.82 47.60
## [13] 50.30 43.50 40.76 55.16 51.50 50.90 47.88 50.50
apply(X = myArr, MARGIN = 3, FUN = mean)
## [1] 49.735 47.330 49.255 51.605 50.755
c(mean(myArr[,,1]), mean(myArr[,,2]), mean(myArr[,,3]), 
  mean(myArr[,,4]), mean(myArr[,,5]))
## [1] 49.735 47.330 49.255 51.605 50.755

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,] 49.4 34.0 80.6 44.2 60.2 59.0 63.8 27.2 34.8  50.4  62.4  45.0  40.6
##  [2,] 48.6 43.8 33.2 47.0 76.6 50.4 67.2 70.0 51.2  33.0  42.2  69.6  40.8
##  [3,] 64.6 39.6 46.2 71.8 57.0 45.8 58.2 35.4 59.6  54.2  44.6  59.2  28.0
##  [4,] 31.2 57.6 56.8 36.2 56.0 59.8 59.6 54.8 42.6  43.0  42.4  64.0  46.0
##  [5,] 73.4 55.0 59.8 45.2 59.8 44.8 46.2 36.2 58.4  35.2  65.4  41.4  70.4
##  [6,] 43.4 49.4 30.4 65.6 59.6 31.2 53.4 77.2 41.0  54.6  66.8  28.8  70.2
##  [7,] 53.2 58.8 44.6 52.0 46.8 33.2 34.0 39.8 41.8  40.6  58.0  66.8  54.8
##  [8,] 43.8 17.4 58.0 16.8 42.0 71.8 32.8 34.0 60.8  63.2  53.0  39.2  45.6
##  [9,] 79.0 51.6 68.8 47.0 59.4 53.4 53.0 62.0 50.8  56.8  52.8  19.4  67.8
## [10,] 35.6 60.0 40.4 51.6 28.6 23.4 66.4 53.6 63.4  53.4  60.6  42.6  38.8
##       [,14] [,15] [,16] [,17] [,18] [,19] [,20]
##  [1,]  26.8  63.4  37.2  52.6  52.6  53.8  34.4
##  [2,]  36.8  52.6  65.8  45.8  40.6  53.4  49.8
##  [3,]  33.4  31.8  69.0  58.2  63.0  46.0  40.0
##  [4,]  45.4  30.2  60.8  53.0  47.0  65.6  76.0
##  [5,]  59.6  49.6  48.8  51.8  30.2  59.0  35.0
##  [6,]  59.4  26.8  60.2  52.2  48.0  26.8  49.2
##  [7,]  45.4  39.8  67.2  56.6  74.4  36.4  69.4
##  [8,]  41.0  35.6  45.8  45.2  54.4  44.4  65.2
##  [9,]  23.2  41.0  40.0  46.8  61.6  39.0  48.4
## [10,]  64.0  36.8  56.8  52.8  37.2  54.4  37.6

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] 66 80 28 87  5 20 36 16 74  3
## 
## $b
##  [1]  4 13 80 25 69 59 48 49 54 81
## 
## $c
##  [1] 30 34 73 44 71 74 42 54 16 91
## 
## $d
##  [1] 86 17 66 15 57  1 40 59 69  7
## 
## $e
##  [1] 64 25 89 66 88 17 77 43 29 42
lapply(myList, FUN = mean)
## $a
## [1] 41.5
## 
## $b
## [1] 48.2
## 
## $c
## [1] 52.9
## 
## $d
## [1] 41.7
## 
## $e
## [1] 54

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]  5  5 NA  1 NA  4  1  5  3  5
## 
## $b
##  [1] NA NA  3  1  5  2  2  5  5  4
## 
## $c
##  [1]  5  4  1  3  2  2  4  1 NA  5
## 
## $d
##  [1]  2  4  5 NA  1 NA  4  3  3  2
## 
## $e
##  [1]  3 NA NA  2  3  4  2  5  5  5
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.625
## 
## $b
## [1] 3.375
## 
## $c
## [1] 3
## 
## $d
## [1] 3
## 
## $e
## [1] 3.625

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.625
## 
## $b
## [1] 3.375
## 
## $c
## [1] 3
## 
## $d
## [1] 3
## 
## $e
## [1] 3.625

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

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.625
## 
## $b
## [1] 3.375
## 
## $c
## [1] 3
## 
## $d
## [1] 3
## 
## $e
## [1] 3.625
sapply(myList, FUN = function(i){
  mean(i, na.rm = TRUE)
})
##     a     b     c     d     e 
## 3.625 3.375 3.000 3.000 3.625

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 NA  4  4 NA

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        I 10.552482 37912.909 18.80485
## 2        J  9.751189 13189.195 18.80356
## 3        A 10.144902 39912.922 18.69480
## 4        H 10.148621  7754.008 21.72357
## 5        E 10.586563 27810.099 29.73283
## 6        C  9.700371 30946.839 18.74036
## 7        J 10.647680 11360.460 14.56589
## 8        F 10.292551 33135.372 19.02871
## 9        B 10.276133 23966.861 19.81957
## 10       J 10.425206 44413.271 12.48840

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.705   9.632  10.021   9.981  10.363  11.176 
## 
## $B
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.937   9.663  10.045  10.044  10.423  11.475 
## 
## $C
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   9.224   9.791  10.005  10.082  10.419  11.478 
## 
## $D
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.956   9.669   9.957   9.951  10.238  11.188 
## 
## $E
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   7.722   9.701  10.045   9.993  10.375  11.033 
## 
## $F
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.638   9.623   9.906   9.904  10.257  11.108 
## 
## $G
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   9.025   9.625  10.035  10.036  10.419  11.251 
## 
## $H
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.812   9.715  10.016  10.023  10.341  11.345 
## 
## $I
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.834   9.704  10.023  10.029  10.290  11.219 
## 
## $J
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   8.673   9.738  10.061  10.058  10.429  11.369

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.981495 25144.20 21.38752
## B 10.043625 26366.71 19.57132
## C 10.082289 24220.55 19.64893
## D  9.951089 26190.32 20.08122
## E  9.992532 24903.84 20.86645
## F  9.903630 24182.43 19.31994
## G 10.035897 23646.77 20.75494
## H 10.023432 24584.59 19.60410
## I 10.029309 24588.44 19.78836
## J 10.057717 24406.67 20.25933

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.