3  Bases de R

3.1 El lenguaje de programación R

En la sesión anterior hablamos de RStudio como una interfaz gráfica a R, pero no fuimos más allá de decir que R es un lenguaje de programación que se ejecuta en consola. Pues bien, R es, y cito textualmente, “un lenguaje y ambiente para la computación estadística y la creación de gráficos(R Core Team, 2022). ¿En castellano? Es un lenguaje creado especialmente para procedimientos estadísticos y el graficado de datos. Es un software libre, por lo que además de ser gratuito es auditable (i.e., cualquiera puede revisar el código fuente), “cualquiera” puede contribuir (ojo a las comillas). En su código fuente base (R base) incluye diversos algoritmos, modelos y distribuciones de probabilidad, aunque también es fácilmente extensible por medio de paquetes o librerías.

En este punto me dirás “Ok, Arturo, de acuerdo con lo que dices, pero ¿por qué se ha vuelto tan popular en análisis bioestadísticos?” Pues tiene que ver con varios factores. El primero es que es un lenguaje de alto nivel; es decir, está hecho para ser leído por humanos y no por computadoras. El segundo es que es interactivo, por lo que podemos ir modificando el código y ver su salida en tiempo real sin necesidad de compilar primero el código. El tercero tiene que ver su extensibilidad. Hay una gran variedad de librerías que agregan funcionalidades particulares, que van desde funcionalidades muy generales hasta la implementaciones muy particulares dependientes del área de investigación. ¿Algunos ejemplos? En tidyverse tenemos un dialecto enfocado a la ciencia de datos y la programación funcional. En ggplot2 tenemos un potente graficador. En vegan tenemos una gran cantidad de funciones para problemas de ecología de comunidades. En SIBER tenemos una forma de estimar tamaños de nichos isotópicos. En STAN (rstan, para ser más preciso) tenemos un ambiente general para inferencia Bayesiana. En fin, hay tantas librerías como problemas a resolver, y continuamente se añaden nuevas, pero antes de saltar a utilizarlas necesitamos entender las bases de R base (je).

3.2 Objetos

R es un lenguaje en el cual domina el paradigma de la programación orientada a objetos; i.e., en R todo es un objeto. El método de creación es el mismo para todos los casos: utilizar el símbolo de asignación <- (atajo de teclado alt -). Como ejemplo, creemos un objeto que contenga el texto “¡Hola Mundo!”:

texto <- "¡Hola mundo!"

Notarás que no hubo salida ni en la libreta ni en la consola. Esto quiere decir que el objeto fue creado satisfactoriamente, y ahora podemos acceder o utilizar ese texto llamando al objeto texto:

texto
[1] "¡Hola mundo!"

3.2.1 Consideraciones

Aunque podemos poner virtualmente cualquier nombre a nuestros objetos, es necesario que tengamos algunas cosas en cuenta:

  1. No empezar nombres de objetos con números.
  2. No utilizar nombres de funciones u otros objetos creados anteriormente: enmascaramiento (funciones) o sobre-escritura (variables)
  3. Evitar empezar con punto (.), pues el objeto queda oculto del ambiente de trabajo
  4. Utilizar nombres cortos, pero lo más descriptivos posibles (amundsen_plot >> plot)
  5. Se pueden utilizar _ o . dentro del nombre (como separadores, por ejemplo). La guía de estilo de R sugiere el uso de _, aunque la guía de estilo de Google para R sugiere el uso de CamelCase (AmundsenPlot). Lo más importante para uso personal/interno es ser consistente y evitar mezclar estilos.

3.3 Librerías y funciones

Aunque en R predomina el paradigma de la programación orientada a objetos, también podemos hacer uso del paradigma funcional de la programación; es decir, podemos construir nuestros programas mediante la aplicación y construcción de funciones. Aunque en este momento te suene poco intuitivo o como si estuviéramos comenzando por el final, vamos a comenzar con las funciones, para después hablar de los tipos de objetos que tenemos, pues resulta que para crear cierto tipo de objetos necesitamos utilizar funciones.

3.3.1 Funciones

Las funciones representan una serie de métodos para obtener un resultado, para utilizarlas emplearemos la estructura fun(arg1, arg2, ..., argn), donde arg*representa un argumento; es decir, un elemento “pasado” a la función para regular sus procesos. El ejemplo más simple, y con el cuál ya hemos estado en contacto, es la función print():

print("¡Hola Mundo!")
[1] "¡Hola Mundo!"

Esta función “imprime” un resultado en pantalla, pero puede ser utilizada para mucho más que para imprimir texto. A final de cuentas, este mismo resultado lo podemos obtener simplemente poniendo el texto en la consola. ¿Qué puede hacer la función? Eso lo podemos ver consultando la ayuda de la función, utilizando el operador ? antes del nombre, tal que:

?print

Te darás cuenta que no tenemos una salida en la libreta, lo cuál es normal, pues la ayuda tiene su propio espacio en el ambiente gráfico. Si estuviéramos trabajando solo con la línea de comandos en el terminal, ahí sí veríamos la salida en la consola. Una de las partes más importantes de la documentación de ayuda es que tenemos los argumentos de la función; es decir, qué necesita la función para realizar su trabajo, así como el papel de cada uno de estos elementos.

Por otra parte, cuando declaramos una función los llamaremos parámetros. Para declarar una función generaremos una variable cuyo nombre será el nombre “llamable” de la función, a la cual asignaremos el cuerpo de la función utilizando function(parámetros){cuerpo}. Para ejemplificar, creemos una función para calcular la media aritmética de un conjunto de números x:

media.arit <- function(x){
  # Mejora: Trabajo con NAs
  suma <- sum(x)
  n <- length(x)
  return(suma/n)
}

media.arit(1:5)
[1] 3

¿Cuándo declarar una función? Cuando tengamos un flujo de trabajo que consista de exactamente los mismos pasos con posibles pequeñas variaciones, el cuál aplicaremos de manera continua. De hecho, uno de los productos que obtendremos de estas reuniones será un script con las funciones relacionadas con los análisis tróficos IIR, PSIRI, gráficos de Amundsen, etc. para que puedan ser utilizados de manera sencilla, repetitiva, y consistente.

Por último, debido a que un gran número de funciones son altamente regulables (cuentan con un gran número de parámetros), te recomiendo hacer uso extensivo (excesivo) de la ayuda (?fun) para que obtengas de primera mano el conocimiento sobre su objetivo, sus entradas, su salida y, en consecuencia, ayudarte a prevenir o solucionar errores.

3.3.2 Librerías

Afortunadamente para nostros, muchas de las técnicas o procedimientos que realizamos ya fueron programados por alguien con más experiencia, y usualmente compilados en una librería de R. Hay una gran cantidad de librerías disponibles, algunas instalables directamente desde CRAN (R), mientras que otras son instalables desde repositorios públicos como GitHub. Por lo general, la gran mayoría las instalaremos desde R, utilizando la función install.packages("package", dependencies = T). Esta función buscará la versión más reciente del paquete solicitado y la descargará e instalará para que pueda ser utilizada por nosotros. Un ejemplo:

install.packages("tidyverse", dependencies = T)

Esta línea descargará el paquete tidyverse, que es un “súper” paquete formado por muchos otros paquetes que forman un “dialecto” dentro de R. Por el momento no te preocupes por eso, solo lo instalamos para evitarnos muchas descargas independientes de paquetes que pueden llegar a serte extremadamente útiles, tal como ggplot2. Es importante mencionar que una cosa es instalar la librería y otra cosa es utilizar la librería, para lo cual necesitamos de la función library(package), tal que:

library(ggplot2)

Una vez que hicimos esto ya podemos utilizar TODAS las funciones que forman parte de la librería. ¿Y si solo quiero utilizar una función particular? En ese caso puedes utilizar el operador ::. Probemos viendo la ayuda de la función str_extract de la librería stringr que forma parte del tidyverse:

?stringr::str_extract

Ahora que sabemos qué es una función, qué es una librería y cómo utilizarlos, vayamos a explorar el otro tipo de objetos: las variables.

Nota

Si te interesa ver la referencia de una librería puedes hacerlo con la función citation("librería").

3.4 Variables

A diferencia de una función en la cual almacenamos series de pasos para obtener un resultado, una variable nos permite almacenar todo lo demás: resultados, números, texto, tablas, e incluso otras variables. Su declaración la vimos arriba: con el símbolo de asignación (<-). Para imprimir el resultado en pantalla podemos llamar a la variable o utilizar la función print(var):

var <- 1:5
print(var)
[1] 1 2 3 4 5

3.5 Tipos de variables

Existen dos tipos de variables, los cuales a su vez se subdividen en otros tipos. Para conocer el tipo de una variable utilizamos la función typeof(var), mientras que las funciones is.*() nos permiten probar si una variable es de un tipo en específico (e.g. is.character(var)).

3.5.1 Datos

Las variables que contienen un solo elemento se conocen como datos:

  1. Character: Cadena de caracteres, indicadas por comillas dobles o sencillas:
char <- "a"
typeof(char)
[1] "character"
  1. Integer: Números enteros, indicados por la letra “L” después del número:
integer <- 5L
typeof(integer)
[1] "integer"
  1. Double: Fracciones, también conocidos como floating points:
dbl <- 7/5
typeof(dbl)
[1] "double"
  1. Logical: Valor lógico o booleano. Solo puede tomar dos valores: TRUE o FALSE o sus abreviaturas T o F
bool <- is.double(dbl)
print(paste('valor: ', bool))
[1] "valor:  TRUE"
print(paste('tipo: ', typeof(bool)))
[1] "tipo:  logical"
  1. Complex: Números complejos, con una parte real y una imaginaria:
comp.n <- 8+3i
typeof(comp.n)
[1] "complex"

3.5.2 Estructuras/arreglos

Las estructuras son colecciones de valores, cada una con sus propiedades y sus métodos de acceso a los valores que las conforman (indexación/indización):

3.5.2.1 Vector

La estructura más básica. Una colección unidimensional de elementos. Las funciones para crearlos son: c(), la cual combina una serie de elementos en un vector (mismo tipo) o una lista (diferentes tipos); vector(mode, length): genera un vector “vacío” con longitud (número de elementos) length y tipo de datos 'mode'.

vect_1 <- c(1:5)
vect_2 <- vector(mode = 'double', length = 5)
print(vect_1)
[1] 1 2 3 4 5
print(vect_2)
[1] 0 0 0 0 0

Para indexar un vector utilizamos: var[i], donde i representa la posición del (los) elemento(s) de interés:

print(vect_1[4])
[1] 4
print(vect_1[2:3])
[1] 2 3

Ejercicio: Genera un vector donde cada elemento sea una letra de tu primer nombre, luego extrae los elementos último, primero y segundo.

3.5.2.2 Factor

Representan variables categoricas. Contienen los valores de la variable así como los valores posibles que puede tomar (niveles). Se crean con la función factor(x, levels, labels), donde x representa los valores de la variable, levels representa los posibles niveles y labels (opcional) representan etiquetas de cada nivel:

fact_1 <- factor(x = c('a', 'b', 'c'), 
                 levels = c('a', 'b', 'c', 'd', 'e'))
fact_1
[1] a b c
Levels: a b c d e

Ejercicio: 1. Genera un factor con 5 categorías de acuerdo, con 4 observaciones que representen 2 de esas 5 categorías. Las categorías tienen orden. 2. Extrae los elementos 2 y 4 de ese factor.

3.5.2.3 Matrix

Una estructura bidimensional (columnas/renglones). Se generan utilizando la función matrix(data, nrow, ncol, byrow), donde data representa la colección de objetos que formarán la matriz, nrow y ncol el número de renglones y columnas, respectivamente, y byrow si se llenará por renglones (FALSE) o por columnas (TRUE, por defecto)

mat_1 <- matrix(c(T, F, F, T), nrow = 2, ncol = 2)
mat_1
      [,1]  [,2]
[1,]  TRUE FALSE
[2,] FALSE  TRUE

Para indexar una matriz utilizaremos también corchetes; sin embargo, indicaremos el par renglón,columna donde se ubica el elemento:

print(mat_1[1,1])
[1] TRUE
print(mat_1[2,1])
[1] FALSE

Si quisieramos indexar toda una dimensión (renglón o columna), utilizaríamos el mismo método, dejando en blanco la dimensión contraria; es decir, si nos interesa una columna, dejaremos en blanco el número de renglón y si nos interesa un renglón dejaremos en blanco el número de columna:

print(mat_1[,2])
[1] FALSE  TRUE
print(mat_1[1,])
[1]  TRUE FALSE

OJO: print(mat_1[c(1,1)]) NO da la diagonal de la matriz, esa la obtenemos con diag(mat1), sino que repite 2 veces el primer elemento de la matriz

Ejercicio:

  1. Extrae los elementos F de mat_1:
  1. Extrae los elementos de la diagonal de mat_1:

3.5.2.4 DataFrame

El DataFrame es la estructura con la que más comúnmente estaremos en contacto. Es una tabla completa que, a diferencia de la matriz, contiene nombres de columnas. Tiene dos particularidades que hay que considerar: 1) todos los elementos que forman a cada columna deberán ser del mismo tipo y 2) El número de renglones de todas las columnas debe de ser el mismo. Se crean utilizando la función data.frame(col_name = data, ...):

df_1 <- data.frame(col_a = c(0:5), 
                   col_b = c(20:25), 
                   col_c = c(15:20))
# Nota: Si no se indica el nombre de las columnas este será asignado automáticamente
df_1

Existen distintos modos de indexar un DataFrame. El primero de ellos var$col_name:

df_1$col_a
[1] 0 1 2 3 4 5

Como vemos, este modo de indexación extrae la columna completa en forma de un vector, por lo que si queremos accesar un valor en particular solo habrá que utilizar ese método de indexación:

df_1$col_b[4]
[1] 23

Finalmente, también podemos utilizar el método de indexación de matrices, recordando que se especifica el par renglón, columna:

df_1[4,2]
[1] 23

Ejercicio:

  1. Extrae los elementos 1, 3 y 5 de la columna c:
  1. Extrae los elementos 1, 3 y 5 de las columnas a y c:

Esta es la estructura con la que más debemos de familiarizarnos, pues la mayor parte de nuestros datos los representamos en ella. ¿Siempre debemos de ingresar los datos manualmente? Para nada, tenemos todo un abanico de funciones que nos permiten cargar datos directamente de archivos, pero eso lo veremos más adelante.

3.5.2.5 List

Las listas son una colección de cualquier combinación de datos o estructuras, incluyendo otras listas:

l_1 <- list(df_1, mat_1, vect_1)
print(l_1)
[[1]]
  col_a col_b col_c
1     0    20    15
2     1    21    16
3     2    22    17
4     3    23    18
5     4    24    19
6     5    25    20

[[2]]
      [,1]  [,2]
[1,]  TRUE FALSE
[2,] FALSE  TRUE

[[3]]
[1] 1 2 3 4 5

En la salida de arriba vemos el método de indexación: var[[i]][j,k], donde i representa el número de objeto en la lista y j,k el par renglón,columna (de aplicar). En el caso de DataFrames podemos seguir utilizando el operador $ para utilizar los noombres de columnas:

l_1[[1]]$col_a[6]
[1] 5

Ejercicios: Desde la lista:

  1. Extrae el segundo elemento de la segunda columna de mat_1:
  1. Extrae el quinto elemento de vect_1:
  1. Extrae los elementos 3 y 4 de la columna b de df_1:
  1. Extrae los elementos 6 y 1 de las columnas a y c de df_1:

Ahora que hemos hablado de todos los tipos de estructuras, y antes de encaminarnos hacia los procesos de automatización, hablemos de cómo cargar nuestros datos en R.

3.6 Carga de datos

El cómo carguemos nuestros datos depende de varios factores: a) el formato del archivo en el que estén archivados, b) el cómo esté acomodada la información, c) qué necesitemos para hacer los análisis posteriores. El ejemplo más simple es cargar un archivo de texto separado por comas, en el cuál las comas separan las columnas y los saltos de línea los renglones. Tomemos como ejemplo el archivo "datos1.csv":

datos1 <- read.table("datos/datos1.csv", sep = ",", header = T)

Podemos verificar la información obteniendo el encabezado del data.frame:

head(datos1)

Los archivos separados por comas son uno de los formatos más comunes, por lo que R cuenta con una función dedicada (la función read.table() con valores predefinidos):

datos1 <- read.csv("datos/datos1.csv")
head(datos1)

Aquí todo se cargó sin ningún problema porque el archivo estaba listo para ser leído, pero esto no siempre es el caso. Por ejemplo, los datos pueden estar en la segunda hoja de un archivo Excel, la cuál tiene 5 renglones de encabezado dando una descripción de los datos y en el renglón 6 están dispuestos los nombres de las variables. Además, sabemos que vamos a realizar un análisis de agrupamientos jerárquicos (clúster), el cuál requiere que los nombres de los individuos estén marcados en los nombres de los renglones (Ver archivo datos2.xlsx):

datos2 <- read.table("datos/datos2.xlsx")

Esto, evidentemente, da un error, pues le dimos a la función read.table() un archivo que no es de texto simple, sino un Excel.

Vamos entonces por partes:

  1. Formato: es un archivo Excel, por lo que hay que utilizar una función que permita leer ese tipo de archivo. En nuestro caso utilizaremos la función readxl::read_xlsx(). Aquí no obtendremos ningún error, pues el tipo de archivo es el correcto. Lo único que obtenemos es un mensaje (New names:) que nos diría a qué columnas se les asignó nombres nuevos (y cuáles).
datos2 <- readxl::read_xlsx("datos/datos2.xlsx")
New names:
• `Lp14-C` -> `Lp14-C...94`
• `Lp14-C` -> `Lp14-C...109`

Pero, ¿qué pasa si leemos el encabezado? Resulta que la función cargó la primera hoja del excel, cuando en realidad nosotros queríamos la segunda

head(datos2)

Necesitamos entonces indicar explícitamente que queremos se cargue la segunda hoja:

datos2 <- readxl::read_xlsx("datos/datos2.xlsx", sheet = 2)
New names:
• `` -> `...2`
• `` -> `...3`
• `` -> `...4`
• `` -> `...5`
• `` -> `...6`
• `` -> `...7`
• `` -> `...8`
• `` -> `...9`
• `` -> `...10`
• `` -> `...11`
• `` -> `...12`
• `` -> `...13`
• `` -> `...14`
• `` -> `...15`
• `` -> `...16`
• `` -> `...17`
• `` -> `...18`
• `` -> `...19`
• `` -> `...20`
• `` -> `...21`
• `` -> `...22`
• `` -> `...23`
• `` -> `...24`
• `` -> `...25`
• `` -> `...26`
• `` -> `...27`
• `` -> `...28`
• `` -> `...29`
• `` -> `...30`
• `` -> `...31`
• `` -> `...32`
• `` -> `...33`
• `` -> `...34`
• `` -> `...35`
• `` -> `...36`
• `` -> `...37`
• `` -> `...38`
• `` -> `...39`
• `` -> `...40`
• `` -> `...41`
• `` -> `...42`
• `` -> `...43`
• `` -> `...44`
• `` -> `...45`
• `` -> `...46`
• `` -> `...47`
• `` -> `...48`
• `` -> `...49`
• `` -> `...50`
head(datos2)
  1. Saltar renglones: Ya tenemos la hoja que nos interesa, el problema es que cargó el encabezado como renglones con observaciones, por lo que hay que saltarlos:
datos2 <- readxl::read_xlsx("datos/datos2.xlsx",
                            sheet = 2,
                            skip = 5)
head(datos2)

3.7 Operaciones comunes

Como ya vimos, no siempre vamos a obtener la información en el formato que necesitamos. Aunque podemos solventar algunas de estas carencias durante la carga de los archivos, a veces necesitamos “masajear” los datos o “manipularlos” para llevarlos a lo que las funciones que nos interesan nos piden. Tomemos como ejemplo los datos de la hoja número 1 del archivo datos2.xlsx:

datos3 <- readxl::read_xlsx("datos/datos2.xlsx", sheet = 1)
head(datos3)

3.7.1 Transposición

En estos datos las presas están en los renglones, y los individuos de los depredadores en las columnas. Aunque esta disposición no tiene fundamentalmente nada de malo, normalmente las instancias (observaciones individuales/réplicas) están en los renglones, y las variables (presas) en las columnas. Es necesario entonces transponer los datos. Esto lo podemos hacer de manera sencilla con la función t():

head(t(datos3))[,3]
    Prey    Cu1-C    Cu2-C    Cu3-C    Cz1-C    Ar1-C 
"Prey_3"      "0"      "0"      "0"     " 0"      "0" 

Esto logró nuestro objetivo, aunque con un pequeño gran problema: toda la información es texto. ¿Por qué? Resulta que las columnas solo pueden contener datos de un solo tipo, por lo que al tener el texto de las especies presa todas las columnas son transformadas a cadenas de caracter. ¿Qué podemos hacer? Transponer los datos en tres pasos.

3.7.2 “Rebanadas” (slices)

El primer paso es separar los nombres de las presas de los datos de los depredadores:

prey <- datos3$Prey
head(prey)
[1] "Prey_1" "Prey_2" "Prey_3" "Prey_4" "Prey_5" "Prey_6"
counts <- datos3[,2:ncol(datos3)]
head(counts)

Transponer la matriz de conteos:

countst <- as.data.frame(t(counts))
head(countst)

3.7.3 Cambiar nombres de columnas

Para acceder o asignar los nombres de las columnas o renglones de arreglos bidimensionales podemos utilizar los atributos colnames(data) y rownames(data):

colnames(countst) <- prey
head(countst)

3.7.4 Transformaciones

El resultado de las operaciones anteriores es una matriz; sin embargo, podemos pasarlo a un data.frame:

countst <- as.data.frame(countst)
head(countst)

3.7.5 Añadir vectores como columnas

Ahora tenemos un data.frame; sin embargo, tenemos las claves como nombres de los renglones y, según qué querramos realizar, podemos necesitar que estas formen su propia columna. Una forma de hacerlo es: 1) extraer los nombres de los renglones y 2) añadirlos como una columna adicional:

keys <- rownames(countst)
countst <- cbind(keys, countst)
head(countst)

3.8 Operadores lógicos

Los operadores lógicos nos sirven para hacer comparaciones y obtener un resultado booleano (T o F). Los más comunes son:

  1. cond1|cond2: Condicional “O”. T si se cumple alguna de las dos condiciones
c <- 5L
is.integer(c)|is.double(c)
[1] TRUE
  1. cond1&cond2: Condicional “Y”. T si se cumplen ambas condiciones
is.integer(c)&(c>3)
[1] TRUE
  1. <, >: Comparaciones, menor qué o mayor qué
print(c<10)
[1] TRUE
print(c>5)
[1] FALSE
  1. <=, >=: Comparaciones, menor o igual qué; mayor o igual qué.
  2. a!=b: Desigualdad, T si a es diferente de b
c!=5
[1] FALSE

3.9 Automatización

El primer paso de la automatización es generar funciones que te permitan realizar la misma acción múltiples veces y no cometer algún error en dichas repeticiones. Esta práctica se deriva de una de las máximas más importantes en programación: “Don’t repeat yourself” (DRY, no te repitas a ti mismo); es decir, dejar que la computadora haga las repeticiones por sí mismas. En la práctica, esto implica no estar copiando y pegando el mismo bloque de código una y otra vez y luego modificarlo manualmente, sino que escribirlo solo una vez y luego decirle a la computadora que repita esa acción n veces, modificando algún(os) argumento(s), o que cambie el comportamiento en función de si se cumple o no una condición en nuestros datos. En ese caso, las estructuras de control son nuestras mejores aliadas.

3.9.1 Ciclos for

Hay distintas formas de realizar la repetición de acciones, pero hoy introduciremos únicamente los ciclos for por ser los más probables a ser requeridos. Un ciclo for consta de cuatro elementos:

for (variable in vector) {action}

  1. La estructura de control for, evidentemente
  2. Una variable que hace las veces de un marcador de posición; es decir, la utilizaremos para indicar en dónde se van a sustituir los valores que queremos ciclar
  3. Los valores que ciclaremos, contenidos en un vector
  4. La acción a realizar

Como muchas otras cosas, es más fácil entenderlo utilizando algunos ejemplos. El primero de ellos es simplemente imprimir la secuencia de números del 1 al 10. Aunque podemos escribir 10 veces la función print e ir cambiando el número, es mucho más sencillo:

for (i in 1:10) {
  print(i)
}
[1] 1
[1] 2
[1] 3
[1] 4
[1] 5
[1] 6
[1] 7
[1] 8
[1] 9
[1] 10

¿Qué fue lo que hizo la computadora? Utilizó la función print() para mostrarnos el contenido de i, el cuál es el iésimo elemento de la secuencia 1:10. En otras palabras, si es la segunda vuelta que da, imprimirá el número 2, si es la séptima imprimirá 7, y así hasta que termine con todos los elementos en la secuencia. Ahora, sumemos 2 a cada número de la secuencia:

for (i in 1:10) {
  print(i+2)
}
[1] 3
[1] 4
[1] 5
[1] 6
[1] 7
[1] 8
[1] 9
[1] 10
[1] 11
[1] 12

Al igual que en el caso anterior, tomó uno por uno los valores (de forma secuencial), le añadió 2 y luego mostró el resultado en pantalla. Este tipo de estructuras son sumamente útiles, pues no solo nos ahorran errores, sino también tiempo de ejecución. Sobra decir que no es la única manera de hacer este tipo de ciclos, ni tampoco es la más rápida. Tenemos algo que se conocen como funciones vectorizadas.

3.9.2 Familia de funciones apply()

Podemos entender la vectorización como la aplicación de una función a cada elemento de un vector (igual que el ciclo for), aunque procesando todo el vector “al mismo tiempo”. Esta última parte no es estrictamente verdad, aunque lo cierto es que son más rápidas que los ciclos for tradicionales. En R tenemos toda una gama de funciones que hacen justo eso, la familia apply y sus relacionadas, compuesta por las funciones:

  • apply
  • lapply
  • sapply
  • tapply
  • mapply
  • by
  • aggregate

Todas estas funciones manipulan porciones de datos como matrices, arreglos, listas o data frames de forma repetitiva. Básicamente nos permiten evitar el uso explícito de un ciclo for. Toman como argumento una lista, matriz o data frame y le aplican una función con uno o más argumentos adicionales. Esta función puede ser:

  • Una función de agregación, por ejemplo la media o la suma
  • Funciones de transformaciones o para extraer sub-conjuntos
  • Otras funciones vectorizadas, que dan como resultado estructuras más complejas como listas, vectores, matrices o arreglos. Pero basta de cháchara, ¿cómo y cuándo debemos de utilizarlas? La respuesta depende totalmente de la estructura de los datos y el formato de salida que se necesite. Veamos algunos casos de uso:

3.9.2.1 apply()

Esta es la función “madre” de las demás, la cual opera sobre arreglos. Para simplicidad, vamos a limitarnos a arreglos bi-dimensionales como las matrices o data.frames. La sintaxis para su uso es apply(X, MARGIN, FUN, ...), donde X es el arreglo, MARGIN es el márgen sobre el cuál va a actuar la función; es decir, si queremos que la aplique a cada renglón (MARGIN = 1) o a cada columna (MARGIN = 2), y FUN es el nombre de la función a aplicar (puede ser cualquiera, incluso una función definida por nosotros).

Podemos pensar en obtener la suma o el promedio de cada columna de nuestro data.frame con las presas utilizando un ciclo, o podemos utilizar apply, tal que:

sums <- apply(X = datos2[,2:ncol(datos2)], MARGIN = 2, FUN = sum)
head(sums)
Prey_1 Prey_2 Prey_3 Prey_4 Prey_5 Prey_6 
     4     12      9     44      5      5 

3.9.2.2 aggregate()

Otra función extremadamente útil es la función aggregate(). Esta función nos permite aplicar una función a distintos grupos. Un escenario clásico es obtener el promedio de una variable para cada grupo, por ejemplo el promedio de conteos de quetognatos. La forma “tradicional” es: aggregate(x, by, FUN), donde x es el objeto R a agrupar, by es una lista con los grupos y FUN es la función a aplicar; sin embargo, podemos utilizar una notación más compacta utilizando una formula: aggregate(forumla, data, FUN). Las fórmulas en R son objetos sumamente útiles y que se utilizan para una gran diversidad de cosas. Su estructura es: Y ~ X, y se lee “Y con respecto a X”. En nuestro caso particular para obtener los conteos promedio de la presa 28:

aggregate(Prey_28~sp, data = datos1, FUN = sum)

3.9.3 Condicionales

Ok, ahora conocemos una manera de aplicar una función a una serie de elementos, pero que pasa si queremos aplicarla de manera condicionada; es decir, si queremos solo imprimir los números mayores a 5, por ejemplo. Eso es justo de lo que se tratan los condicionales, particularmente if, else, e ifelse. La lógica detrás de ellos es sumanmente simple: si se cumple una condición, realiza una acción, si no se cumple, realiza otra (o no realices nada). Comencemos con if. Su estructura es: if(condition){action T}, que notarás es básicamente la descripción que dimos, solo que sin una acción en caso de que no se cumpla la condición. Un ejemplo sería proporcionar un número y que nos diga si es mayor a 5:

x <- 6
if (x>5) {print("x es mayor a 5")}
[1] "x es mayor a 5"

¿Y si no se cumple la condición? Veamos qué pasa:

x <- 1
if (x>5) {print("x es mayor a 5")}

R no nos da ninguna salida, pues no sabe qué hacer. Una forma de decirle es utilizando el complemento de if, else, que nos permite establecer una acción secundaria:

if (x>5) {print("x es mayor a 5")
  }else{print("x no es mayor a 5")}
[1] "x no es mayor a 5"

Una notación mucho más compacta para este tipo de casos es utilizar la función ifelse(condition, action T, action F):

x <- 4
ifelse(x > 5,
       "x es mayor a 5",
       "x no es mayor a 5")
[1] "x no es mayor a 5"

El resultado fue el mismo que el anterior, pero qué pasa si tenemos más de dos escenarios, que, por ejemplo, nos interesara decir si el número es mayor, menor o igual a 5. Veamos primero lo que sucede si establecemos que x sea 5:

x <- 5
ifelse(x > 5,
       "x es mayor a 5",
       "x no es mayor a 5")
[1] "x no es mayor a 5"

La computadora no hizo nada mal, 5 no es mayor a 5. En otros lenguajes de programación añadiríamos una estructura llamada elif, pero en R solo hay que añadir otro(s) if. Mientras que else aplica para todos los casos donde la condición no se cumpla, estos if secundarios nos permiten establecer condiciones adicionales para cuando la condición principal no se cumpla. Volviendo a nuestro problema con x = 5:

x <- 4
if (x > 5) {
  print("x es mayor a 5")
}
if (x == 5) {
  print("x es 5")
}
if (x < 5){
  print("x es menor a 5")
  }
[1] "x es menor a 5"

Ahora sí cubrimos todas nuestras bases para este problema de comparación. Como te imaginarás, el siguiente paso lógico es mezclar for e if o, mejor dicho, anidarlos:

x <- 1:10

for (i in x) {
  if (i > 5) {
    print("x es mayor a 5")
  }
  if (i == 5) {
    print("x es 5")
  }else if (i < 5) {print("x es menor a 5")}
}
[1] "x es menor a 5"
[1] "x es menor a 5"
[1] "x es menor a 5"
[1] "x es menor a 5"
[1] "x es 5"
[1] "x es mayor a 5"
[1] "x es mayor a 5"
[1] "x es mayor a 5"
[1] "x es mayor a 5"
[1] "x es mayor a 5"

Con esto también quiero decir que podemos anidar ciclos for, tal que:

x <- 1:5
y <- 100:105

for (i in x) {
  for (j in y) {
    print(c(i,j))
  }
}
[1]   1 100
[1]   1 101
[1]   1 102
[1]   1 103
[1]   1 104
[1]   1 105
[1]   2 100
[1]   2 101
[1]   2 102
[1]   2 103
[1]   2 104
[1]   2 105
[1]   3 100
[1]   3 101
[1]   3 102
[1]   3 103
[1]   3 104
[1]   3 105
[1]   4 100
[1]   4 101
[1]   4 102
[1]   4 103
[1]   4 104
[1]   4 105
[1]   5 100
[1]   5 101
[1]   5 102
[1]   5 103
[1]   5 104
[1]   5 105

3.10 Ejercicios

  1. ¿Qué es un objeto?
  2. ¿Cuál es la diferencia entre una variable y una función?
  3. ¿Qué es una librería? ¿Qué pasa si utilizas la función ggplot() sin haber cargado la librería ggplot2?
  4. ¿Cuál es la diferencia entre un dato y una estructura?
  5. Carga los datos1.csv y añade un renglón adicional con los totales para cada columna.
  6. Carga los datos1.csv y obten una tabla con los conteos promedio de cada presa para cada especie (aggregate/apply o ciclos).
  7. Combina el ciclo for anidado del último ejemplo con un condicional, en el cuál se imprima si la suma de ambos números (i, j) es mayor, menor o igual a 105.
  8. Ejecuta el siguiente código y responde ¿qué hace cada línea? Recuerda que ante la duda siempre puedes revisar la ayuda de las funciones.
```{r}
pkgs <- c("tidyverse", "ggtext", "Rmisc", "rcompanion",
          "gap", "brms", "stats4", "Metrics",
          "performance", "ggdendro", "dendextend",
          "factoextra", "cluster", "NbClust",
          "FactoMineR", "MASS", "klaR", "vegan",
          "tidymodels", "MVN", "Hotelling",
          "gridExtra", "GGally", "car", "FSA",
          "randomcoloR", "gganimate", "reshape2",
          "DescTools", "PerformanceAnalytics",
          "corrplot", "ggrepel", "pROC", "RCurl",
          "glmnet", "pcsl", "MuMIn", "nlraa")

install.packages(pkgs, dependencies = T)
```
Importante

Si tienes algún problema con la ejecución por favor házmelo saber. Esto cubre la instalación de las librerías que utilizaremos durante el curso, por lo que es sumamente importante que se ejecuten correctamente.

Si en algún momento de la instalación R te pregunta si quieres instalar desde la fuente dile que NO. Aunque las versiones fuente están más actualizadas pueden llegar a romper la compatibilidad con otras librerías.