Curso de posgrado: Introducción al ambiente de R (módulo 3, parte I)

Uinersidad Federal de Pelotas. Departamento de Ecología Zoología y Genética

Modified

Thursday, June 20, 2024

Nota: este documento contiene el material del módulo 3 (parte I) del curso dictado a los estudiantes del programa de Posgrado en Biodiversidad Animal (junio de 2024). El curso fue dirigido por el Dr. Sebastián Sendoya y apoyado por mí (específicamente estuve a cargo del presente módulo).

1 Objetivos

  1. Conocer las principales estructuras de datos en R y la sintaxis del lenguaje para indexar sus elementos (Parte I).

  2. Realacionar el diseño del muestreo con las principales formatos para tabulación de datos y su exploración (Parte II).

Una vez finalizado el módulo usted estará en capacidad de: (i) reconocer y manipular los principales tipos de objetos que usa R para almacenar datos, (ii) tabular los datos de su trabajo dado el diseño de su experimento, y (iii) combinar i y ii para realizar una exploración de los mismos.

2 Estructuras de datos e indexación

Basado en mi propia experiencia y la de la mayoría de mis colegas —-biólogos/ecólogos con poco o nulo entrenamiento formal en programación, un entendimiento superficial de la manera en que se indexan vectores, matrices, arrays, data frames (tibbles) o listas, ralentiza la curva de aprendizaje y el uso de estructuras de programación más complejas. Es por eso que en este módulo decidí hacer énfasis en la indexación como una base fundamental para entender el lenguaje.

La indexación se refiere a las reglas (o sintaxis, si se quiere ser más técnico) que usa R para extraer o aislar información de cada estructura de datos. Estas reglas están basadas en los atributos del objeto, además de la posición y/o condiciones sobre los elementos que contiene. R usa tres símbolos fundamentales y combinaciones de ellos para indexar cualquier estructura de datos: [], [[]] y $. Pongamos lo anterior en contexto, comenzando con la estructura básica que emplea R almacenar datos, los vectores.

3 Vectores

Los vectores, y en realidad cualquier estructura de datos, poseen tres atributos: nombres, tamaño (i.e. una o n dimensiones) y clase (o tipo). R cuenta con funciones para explorar cada uno:

  • Nombres: names(), colnames(), rownames(), dimnames()

  • Tamaño: length(), ncol(), nrow(), dim()

  • Clase: class(), str() –realmente provee información sobre todos los atributos–, is.* (funciones como is.numeric(), is.logical(), is.array()…)

Creemos el vector v1 que contiene los números del 1 al 10 y veamos cómo se aplican algunas de estas funciones

set.seed(5)
v1 <- sample(1:10)
v1
 [1]  2  9  7  3  1  6  5 10  4  8

Sabemos que es un objeto con 10 elementos, nosotros lo creamos, pero podríamos confirmarlo:

length(v1)
[1] 10

¿y sus nombres?

names(v1)
NULL

R retorna NULL –puede entenderse como la nada, un conjunto vacío– dado que v1 aún no tiene nombres.

Asignemos nombres a cada elemento v1

names(v1) <- c('a', 'b', 'c', 'd', 'e', 
               'f', 'g', 'h', 'i', 'j')
names(v1)
 [1] "a" "b" "c" "d" "e" "f" "g" "h" "i" "j"

Ahora veamos los atributos de v1

str(v1)
 Named int [1:10] 2 9 7 3 1 6 5 10 4 8
 - attr(*, "names")= chr [1:10] "a" "b" "c" "d" ...

La primera línea indica que v1 es un vector de números enteros (int), con una longitud de 10 ([1:10]) y nombres (attr(*, "names")), los cuales son de clase caracter (chr).

veamos:

class(names(v1))
[1] "character"

Como mencioné anteriormente, podemos usar estos atributos para indexar los elmentos en v1.

Primero indexemos por posición

. En este caso nos interesa el quinto elemento en el vector v1

v1[5]
e 
1 

Note que usamos [] para referirnos a los elementos contenidos en v1, y 5 se refiere a la posición. Además, observe que v1[5] retorna no solo el elemento de la quinta posición, sino también su nombre. Es decir, cuando indexamos un objeto con [] el resultado es un subset que hereda atributos del objeto del cual fue extraído, en este caso el nombre del quinto elemento (e). Ahora indexemos el mismo elemento pero empleando [[]].

v1[[5]]
[1] 1

R retorna el elemento sin ningún otro atributo. Esto ocurre porque [[]] se emplea para enfatizar el objeto que se desea indexar, sin considerar sus atributos en el objeto (v1 en este caso). Parece una diferencia trivial entre ambas notaciones, sin embargo cuando estamos automatizando procesos o programando funciones, [[]] es una opción más segura para indexar un único elemento. Volveremos sobre este tema cuando veamos indexación de data frames (tibbles) y listas.

Ahora indexemos más de un elemento por su posición, veamos tres opciones:

elementos consecutivos:

v1[3:7]
c d e f g 
7 3 1 6 5 

elementos no consecutivos:

v1[c(1, 3:5, 8)]
 a  c  d  e  h 
 2  7  3  1 10 

En este caso debemos emplear la función concatenar (c()) para agrupar las posiciones de los elementos que precisamos indexar. Lo hacemos porque la “,” en notación de indexación es un carcater reservado para denotar más de una dimensión y, recuerde, los vectores solo tienen una. Veamos:

v1[1, 3:5, 8]
Error in v1[1, 3:5, 8]: incorrect number of dimensions

El mensaje Error in v1[1, 3:5, 8] : incorrect number of dimensions es autoexplicativo.

Ahora indexemos v1 empleando los nombres de sus elementos:

v1['a']
a 
2 
v1[c('a', 'f')]
a f 
2 6 

Note que, en esencia, estamos usando un vector de tipo caracter (los nombres de v1) para indexar los valores alojados en v1. Entonces, también podríamos guardar los nombres de v1 en un nuevo objeto y utilizarlo para indexar v1. Veamos:

nomes_v1 <- names(v1)

v1[nomes_v1[c(3:5, 9)]]
c d e i 
7 3 1 4 

Este ejemplo nos ayuda a ilustrar dos puntos claves para entender cómo opera el código de R:

  1. Podemos utilizar objetos para indexar otros objetos.

  2. R ejecuta el código de manera anidada. Es decir, en el código v1[nomes_v1[c(3:5, 9)]] el orden de ejecución fue: (i) la función c() agrupó los números del 3 al 5 y el 9 (i.e. c(3:5, 9)):

c(3:5, 9)
[1] 3 4 5 9
  1. R empleó esta secuencia de números para indexar el vector nomes_v1
nomes_v1[c(3:5, 9)]
[1] "c" "d" "e" "i"
  1. luego usó el vector ['c', 'd', 'e', 'i'] (o c('c', 'd', 'e', 'i') en sintaxis de R) para indexar v1:
v1[nomes_v1[c(3:5, 9)]]
c d e i 
7 3 1 4 

Esta es una de varías diferencias entre R y otros lenguajes de programación (e.g. Python, JavaScript), donde el código se lee y se ejecuta de izquierda a derecha. Ya tendremos oportunidad de resaltar otra diferencia fundamental.

El último método de indexación se basa en el uso de los operadores booleanos

para realizar “preguntas” con respuestas binarias. Esto es, usamos la sintaxis de R para consultar si los elementos contenidos en un objeto cumplen cierta condición o no.

Primero veamos los principales operadores booleanos son:

  • X == Y: igual
  • X > Y: mayor qué
  • X < Y: menor qué
  • X >= Y: mayor o igual
  • X <= Y: menor o igual
  • X | Y: X o Y
  • X & Y: X Y Y
  • !X: negación (i.e. NO X)

Podemos usarlos para hacer preguntas de tipo lógico (i.e. SI o NO, TRUE o FALSE), por ejemplo:

5 > v1
    a     b     c     d     e     f     g     h     i     j 
 TRUE FALSE FALSE  TRUE  TRUE FALSE FALSE FALSE  TRUE FALSE 

El resultado es un vector de tipo booleano donde cada elemento corresponde a las preguntas 5 > V1[1], …, 5 > V1[10]. Es decir, R itera sobre cada elemento de v1 para realizar esta o cualquier tipo de operación. Esto se conoce como vectorización y, de nuevo, diferencia a R de lenguajes como Python.

Empleemos este vector booleano para indexar v1, tenemos dos alternativas:

b_v1 <- 5 > v1
v1[b_v1]
a d e i 
2 3 1 4 

o

v1[5 > v1]
a d e i 
2 3 1 4 

El resultado es un subconjunto que contiene los elementos de v1 mayores a 5 (i.e. aquello que es TRUE). Podemos realizar operaciones más complejas, combinando distintos tipos de operadores

v1[!(v1 <= 10 & v1 > 4 | v1 >= 3)]
a e 
2 1 

Lo anterior es la base para entender la indexación de vectores y sienta el fundamento para la de objetos más complejos. Esto es ventajoso, ya que las demás estructuras son, en esencia, vectores con dos o más dimensiones (matrices y arrays) o colecciones de vectores agrupadas en un único objeto (data frames, tibbles y listas).

4 Matrices

Las matrices son vectores con dos dimensiones, filas y columnas. Convenientemente R posee la función matrix() para generar las matrices, sus argumentos están explicados en el siguiente código:

v2 <- rpois(6*5, 10)
m <- matrix(data = v2, # datos a incluir en la matriz
            ncol = 5, # número de columnas
            nrow = 6, # número de filas
            byrow = F) # cómo los elementos en v2 serán organizados en la matriz

Exploremos el vector v2 y veamos cómo fue organizado en formato de matriz

v2
 [1]  9 10  7 10 13  7  9 15 15 11 12 12 14 12 12  9 14 14  7  6 13 17 13 14 13
[26] 15  8 12 16  8
m
     [,1] [,2] [,3] [,4] [,5]
[1,]    9    9   14    7   13
[2,]   10   15   12    6   15
[3,]    7   15   12   13    8
[4,]   10   11    9   17   12
[5,]   13   12   14   13   16
[6,]    7   12   14   14    8

Bien, m contiene una matriz de \(6 \times 5\). Podemos explorar sus atributos con las funciones length(), ncol(), nrow(), dim(), colnames(), rownames(), dimnames() y str(). Antes, asignemos nombres a filas y columnas para luego continuar con la indexación.

rownames(m) <- paste('F', 1:nrow(m), sep = '')
colnames(m) <- paste('C', 1:ncol(m), sep = '')
m
   C1 C2 C3 C4 C5
F1  9  9 14  7 13
F2 10 15 12  6 15
F3  7 15 12 13  8
F4 10 11  9 17 12
F5 13 12 14 13 16
F6  7 12 14 14  8

Cómo supondrá, la asignación de nombres a filas y columnas en una matriz sigue el mismo procedimiento que el de los vectores. Las funciones para hacerlo difieren, pero tienen un nombre lo suficientemente explicativo para intuir cómo usarlas.

Ya mencioné que en notación de indexación R usa el caracter , para denotar distintas dimensiones. En el caso de objetos con estructura tabular la , se usa para separa los indices de filas y columnas: matriz[fila , columna]. Esta es la única diferencia con la indexación de vector, lo demás es básicamente igual a lo que presenté para los vectores.

Veamos algunos ejemplos:

# indexación por posición

m[1:4, ]
   C1 C2 C3 C4 C5
F1  9  9 14  7 13
F2 10 15 12  6 15
F3  7 15 12 13  8
F4 10 11  9 17 12
m[3:nrow(m), c(1:3, 5)]
   C1 C2 C3 C5
F3  7 15 12  8
F4 10 11  9 12
F5 13 12 14 16
F6  7 12 14  8
# indexación por nombres 

nom_fila <- rownames(m)
nom_col <- colnames(m)

m[nom_fila[c(1:3, nrow(m))], nom_col[ncol(m)]]
F1 F2 F3 F6 
13 15  8  8 
# por condición

m[m[, 1] > 10, 3:5]
C3 C4 C5 
14 13 16 
# combinando distintos métodos

m[nom_fila[1:3], m[1, ] < 10]
   C1 C2 C4
F1  9  9  7
F2 10 15  6
F3  7 15 13

Indexé m de una manera premeditadamente confusa, si no consigue comprenderla –suponiendo que mi explicación fue clara– le sugiero que de un paso atrás y relea la sección de vectores. Si se entendió, notemos varias cosas: (i) No indicar ningún índice en el espacio de filas o columnas significa, obviamente, que nada se indexa en esa dimensión. Por ejemplo, m[1:3, ] puede leerse como un subconjunto de m que conserva las filas de la 1 a la 3 y todas las columnas de m. Lo mismo aplicaría para un espacio en blanco a la izquierda de la ,. (ii) Cuando la indexación resulta en los elementos de una sola dimensión (fila o columna), R por defecto colapsa la matriz a un vector. Veamos:

Si indexemos la primera fila de la matriz tenemos:

is.vector(m[1, ])
[1] TRUE
is.matrix(m[1, ])
[1] FALSE

Para evitar esta reducción en la dimensionalidad de la matriz, usamos el argumento drop = FALSE en los corchetes de la indexación. Veamos y comparemos el resultado en la consola:

m[1, ] # vector (1 dimension)
C1 C2 C3 C4 C5 
 9  9 14  7 13 
m[1, , drop = FALSE] # matriz de 1 x 5
   C1 C2 C3 C4 C5
F1  9  9 14  7 13

drop = FALSE controla que el objeto indexado no sea reducido a una sola dimensión (i.e. a un vector), drop = TRUE indica lo contrario. El argumento drop opera de la misma manera en objetos tabulares como data frames y tibbles, y sus implicaciones no son tribales cuando programamos funciones y automatizamos procesos.

Retrocedamos un poco y volvamos sobre algo que mencioné en la primera parte de esta sección. Las matrices son vectores de dos dimensiones, no sorprende que su longitud (length(m)) sea igual al número de celdas en la matriz:

length(m)
[1] 30

o

m[1:length(m)]
 [1]  9 10  7 10 13  7  9 15 15 11 12 12 14 12 12  9 14 14  7  6 13 17 13 14 13
[26] 15  8 12 16  8

Continuemos con otro tipo de matrices.

5 Arrays

Los arrays son matrices de \(i \times j\) organizadas en N dimensiones. Las dimensiones básicamente implican una serie de matrices anidadas de manera jerárquica, donde R usa la , para denotar cada anidamiento en la indexación. El resto de la sintaxis es la misma que la de matrices: array[fila, columna, anidamiento 1, ... , anidamiento N]

Creemos el vector v3 y veamos como agruparlo con la función array().

set.seed(5)
v3 <- rnbinom(6*5*2*2*3, mu = 20, size = 3) 
array1 <- 
  array(data = v3, 
        dim = c(6, 5, 2, 2, 3), 
        dimnames = 
          list(
            paste('F', 1:6, sep = ''), 
            paste('C', 1:5, sep = ''), 
            c('ani_1a', 'ani_1b'),
            c('ani_2a', 'ani_2b'), 
            c('ani_3a', 'ani_3b', 'ani_3c')))

array1
, , ani_1a, ani_2a, ani_3a

   C1 C2 C3 C4 C5
F1  7 21 15 13 13
F2  8 32 26 16 15
F3 35 35 37 25 11
F4 14 29  9 14 18
F5 23 21  3 10  9
F6  9 10 17 10 16

, , ani_1b, ani_2a, ani_3a

   C1 C2 C3 C4 C5
F1 15 19 17 20  0
F2 24 12 23  4 24
F3 25 16  6  6  5
F4 10 25 16 32 15
F5  4 18 15 30 13
F6 32 13 15 17 21

, , ani_1a, ani_2b, ani_3a

   C1 C2 C3 C4 C5
F1 15 26 17 15 17
F2 18 28 19 15 21
F3 41 14  7 18 13
F4 33 16 12 14 35
F5 46  9 53  6 22
F6  7 18 27 15  8

, , ani_1b, ani_2b, ani_3a

   C1 C2 C3 C4 C5
F1 26 31 29  9  8
F2 26 34 25 14 22
F3  6  8 27 45  6
F4 25 14 11 15  5
F5 28 17 11  8 14
F6 43 11 45 17 28

, , ani_1a, ani_2a, ani_3b

   C1 C2 C3 C4 C5
F1 19 34 21 47 26
F2 32 22 14 13  8
F3 28  3 15 15 20
F4 59 45 39 43 42
F5 61  8 15 11 43
F6 18 20 21  9 32

, , ani_1b, ani_2a, ani_3b

   C1 C2 C3 C4 C5
F1 11  8  3 16 16
F2 15 16  9  7 33
F3 26  7 17 63 27
F4 17 16 23 32 11
F5  2 43 13  8 26
F6 28 13 25 46 14

, , ani_1a, ani_2b, ani_3b

   C1 C2 C3 C4 C5
F1 19  9 13 12 12
F2 18 11  7 34 20
F3 20 11 15 10 28
F4 10 27 16 10 21
F5 11 10 29 28 19
F6 28 37 23 13 25

, , ani_1b, ani_2b, ani_3b

   C1 C2 C3 C4 C5
F1 20 16 33 31 43
F2 13 29 13 33  1
F3  9 40 21 33 18
F4  7 25 21 30 14
F5  7 18  9 24 21
F6 22 39 18 48  6

, , ani_1a, ani_2a, ani_3c

   C1 C2 C3 C4 C5
F1  8  6 27 14 10
F2 27  9 14 25 12
F3 14  8 12  9  9
F4 28 11  6 30 22
F5  3 13 15 13  7
F6 29  7  3 20 33

, , ani_1b, ani_2a, ani_3c

   C1 C2 C3 C4 C5
F1 35 33 19  1 16
F2  8 39 45 11 18
F3  2 39 17 34 33
F4 17 42 34 19 28
F5  6  4 24 13 11
F6 14 28 37 25 22

, , ani_1a, ani_2b, ani_3c

   C1 C2 C3 C4 C5
F1 35 12 16 17  5
F2 16 17 70 19 26
F3  7 20  7 15 40
F4 35 21 53 13 15
F5 21 22  8  4 16
F6  9 38  8 20 10

, , ani_1b, ani_2b, ani_3c

   C1 C2 C3 C4 C5
F1 52 21  6  6 33
F2 22 24 10 23 10
F3 34 10 17 54 36
F4 11 11 22 15  7
F5 30 22  9  6 14
F6 10 35 34  6 33

La indexación de array1 es fundamentalmente la misma a la de matriz. Ahora solo debemos considerar las dimensiones (o anidamientos extra). Por ejemplo, indexemos las matrices contenidas en el anidamiento 1a (ani_1a):

array1[, , 'ani_1a', ,]
, , ani_2a, ani_3a

   C1 C2 C3 C4 C5
F1  7 21 15 13 13
F2  8 32 26 16 15
F3 35 35 37 25 11
F4 14 29  9 14 18
F5 23 21  3 10  9
F6  9 10 17 10 16

, , ani_2b, ani_3a

   C1 C2 C3 C4 C5
F1 15 26 17 15 17
F2 18 28 19 15 21
F3 41 14  7 18 13
F4 33 16 12 14 35
F5 46  9 53  6 22
F6  7 18 27 15  8

, , ani_2a, ani_3b

   C1 C2 C3 C4 C5
F1 19 34 21 47 26
F2 32 22 14 13  8
F3 28  3 15 15 20
F4 59 45 39 43 42
F5 61  8 15 11 43
F6 18 20 21  9 32

, , ani_2b, ani_3b

   C1 C2 C3 C4 C5
F1 19  9 13 12 12
F2 18 11  7 34 20
F3 20 11 15 10 28
F4 10 27 16 10 21
F5 11 10 29 28 19
F6 28 37 23 13 25

, , ani_2a, ani_3c

   C1 C2 C3 C4 C5
F1  8  6 27 14 10
F2 27  9 14 25 12
F3 14  8 12  9  9
F4 28 11  6 30 22
F5  3 13 15 13  7
F6 29  7  3 20 33

, , ani_2b, ani_3c

   C1 C2 C3 C4 C5
F1 35 12 16 17  5
F2 16 17 70 19 26
F3  7 20  7 15 40
F4 35 21 53 13 15
F5 21 22  8  4 16
F6  9 38  8 20 10

array1[, , 'ani_1a', ] implica una indexación de las matrices a y b alojadas en el anidamiento ani_2, además de las a, b y c alojadas en ani_3. Note que, dado que ani_3 es un anidamiento superior, ambas matrices en ani_2 se repiten en los dos niveles de ani_3 ¿Enredado? Probablemente, veamos un esquema del array completo y luego del indexado (array1[, , 'ani_1a', ]):

Array sin indexación

array1[, , 'ani_1a', ] (el sombreado amarillo denota la indexación)

Podemos emplear la sintaxis de R para continuar indexando diferentes elementos en el array. Por ejemplo:

array1[4:5, # filas 4 y 5 
       2:3, # columnas 2 y 3
       1, # anidamiento 1 
       2, # matriz b
       2:3] # matriz b y c
, , ani_3b

   C2 C3
F4 27 16
F5 10 29

, , ani_3c

   C2 C3
F4 21 53
F5 22  8

El esquema de array1[4:5, 2:3, 1, 2, 2:3]

En primera instancia la indexación de arrays siempre resulta confusa, lo fue para mí y para la mayoría de colegas con los que trabajé. La buena noticia es que no es una estructura de datos que se use con frecuencia en análisis más básicos. Usted seguramente la escuchará y por el momento no la usará en sus análisis, pero es buena idea conocerla y entender cómo manipularla. Yo, por ejemplo, solo la he usado como outputs de modelos bayesianos con decenas de parámetros; también sé de paquetes de R especializados en análisis morfométricos que usan este tipo de estructura de datos. Resulta útil cuando manejamos datos de un mismo tipo con una estructura anidada, por ejemplo: imagine un conjunto de datos en los que cada fila corresponde a un individuo, cada columna a un rasgo funcional, el primer anidamiento corresponde a parches de bosque, el segundo a paisajes y el tercero a ecorregiones.

Como mensaje principal, los arrays son estructuras para almacenar datos tabulares que se estructuran de forma anidada; se indexan igual que las matrices y se emplea la , para denotar cada dimensión.

6 Data frames y tibbles

Los data frames y tibbles son estructuras de datos tabulares, cuyas columnas pueden ser de distinta clase y, por ende, una de los objetos que más comúnmente usamos en nuestros análisis. En general, se emplean para almacenar conjuntos de datos relacionales donde las columnas corresponden a variables y las filas a casos u observaciones. En esencia, podemos emplear data frames y tibbles de forma intercambiable, sin embargo hay algunas diferencias que merecen ser mencionadas:

  • Los data frames vienen por defecto con la instalación de R. Los tibbles, por el contrario, hacen parte del paquete tibble.

  • Ejecutar un objeto de clase tibble en la consola retorna las diez primeras filas de la tabla y un encabezado con el nombre y la clase de la variable.

  • En términos de visualización en la consola, no sé si esto se extiende a la eficiencia, los tibbles son mejores para anidar listas en columnas. Esto es particularmente útil cuando precisamos iterar sobre secciones del conjunto de datos.

  • ¿Recuerda el argumento drop? En los tibbles por defecto está ajustado como FALSE. Así que mientras data_frame[, 1] resulta en un vector de la columna 1, tibble[, 1] genera un tibble (o tabla) de \(N~filas \times 1\).

Nota 1: en lo personal prefiero usar los tibbles, la visualización es más cómoda. Pero la selección de uno u otro resulta trivial para lo que generalmente hacemos en biología.

Nota 2: Los ejemplos a continuación son generalizables para ambos tipos de objetos, en el código presentaré ambas versiones.

La sintaxis para crear ambos tipos de objetos es la misma, solo varían en el nombre de la función a emplear. Recuerde, la única condición es que los vectores (i.e. columnas) tengan la misma longitud.

tibbles:

tibble(
  var1 = vector,
  var2 = vector,
  ...
  var_n = vector
)

data frames:

data.frame(
  var1 = vector,
  var2 = vector,
  ...
  var_n = vector
)

Bien, generemos nuestro primer data frame/tibble:

library(tibble)

set.seed(123)
df <- 
  data.frame(
    site = rep(c('bosque1', 'bosque2', 'bosque3'), 
            each = 10),
    trap = rep(c(1, 2, 3, 4, 5), each = 2, length.out = 30), 
    formigas = rnbinom(30, mu = 10, size = 3)
  )

set.seed(123)
tib <- 
  tibble(
    site = rep(c('bosque1', 'bosque2', 'bosque3'), 
            each = 10),
    trap = rep(c(1, 2, 3, 4, 5), each = 2, length.out = 30), 
    formigas = rnbinom(30, mu = 10, size = 3)
  )

Ahora veamos las diferencias de visualización entre ambos tipos de objeto:

data frame:

df
      site trap formigas
1  bosque1    1        9
2  bosque1    1       18
3  bosque1    2       14
4  bosque1    2        4
5  bosque1    3        9
6  bosque1    3       17
7  bosque1    4       10
8  bosque1    4        9
9  bosque1    5        4
10 bosque1    5        2
11 bosque2    1        4
12 bosque2    1        2
13 bosque2    2       14
14 bosque2    2        1
15 bosque2    3       11
16 bosque2    3        4
17 bosque2    4       10
18 bosque2    4        9
19 bosque2    5       22
20 bosque2    5       17
21 bosque3    1        1
22 bosque3    1       14
23 bosque3    2       11
24 bosque3    2        5
25 bosque3    3       17
26 bosque3    3       11
27 bosque3    4       11
28 bosque3    4       12
29 bosque3    5       10
30 bosque3    5        3

tibble:

tib
# A tibble: 30 × 3
   site     trap formigas
   <chr>   <dbl>    <dbl>
 1 bosque1     1        9
 2 bosque1     1       18
 3 bosque1     2       14
 4 bosque1     2        4
 5 bosque1     3        9
 6 bosque1     3       17
 7 bosque1     4       10
 8 bosque1     4        9
 9 bosque1     5        4
10 bosque1     5        2
# … with 20 more rows

En adelante continuaré con tibbles, todo lo que mencione aplica exactamente a data frames.

La indexación de tibbles es casi igual a la de las matrices, casi! La novedad radica en nuevas formas de indexar las columnas del conjunto de datos. Veamos seis formas de extraer una misma columna en del objeto tib:

tib[, 1]
# A tibble: 30 × 1
   site   
   <chr>  
 1 bosque1
 2 bosque1
 3 bosque1
 4 bosque1
 5 bosque1
 6 bosque1
 7 bosque1
 8 bosque1
 9 bosque1
10 bosque1
# … with 20 more rows
tib[, 'site']
# A tibble: 30 × 1
   site   
   <chr>  
 1 bosque1
 2 bosque1
 3 bosque1
 4 bosque1
 5 bosque1
 6 bosque1
 7 bosque1
 8 bosque1
 9 bosque1
10 bosque1
# … with 20 more rows
tib[['site']]
 [1] "bosque1" "bosque1" "bosque1" "bosque1" "bosque1" "bosque1" "bosque1"
 [8] "bosque1" "bosque1" "bosque1" "bosque2" "bosque2" "bosque2" "bosque2"
[15] "bosque2" "bosque2" "bosque2" "bosque2" "bosque2" "bosque2" "bosque3"
[22] "bosque3" "bosque3" "bosque3" "bosque3" "bosque3" "bosque3" "bosque3"
[29] "bosque3" "bosque3"
tib[[1]]
 [1] "bosque1" "bosque1" "bosque1" "bosque1" "bosque1" "bosque1" "bosque1"
 [8] "bosque1" "bosque1" "bosque1" "bosque2" "bosque2" "bosque2" "bosque2"
[15] "bosque2" "bosque2" "bosque2" "bosque2" "bosque2" "bosque2" "bosque3"
[22] "bosque3" "bosque3" "bosque3" "bosque3" "bosque3" "bosque3" "bosque3"
[29] "bosque3" "bosque3"
tib[, 1][[1]]
 [1] "bosque1" "bosque1" "bosque1" "bosque1" "bosque1" "bosque1" "bosque1"
 [8] "bosque1" "bosque1" "bosque1" "bosque2" "bosque2" "bosque2" "bosque2"
[15] "bosque2" "bosque2" "bosque2" "bosque2" "bosque2" "bosque2" "bosque3"
[22] "bosque3" "bosque3" "bosque3" "bosque3" "bosque3" "bosque3" "bosque3"
[29] "bosque3" "bosque3"
tib$site
 [1] "bosque1" "bosque1" "bosque1" "bosque1" "bosque1" "bosque1" "bosque1"
 [8] "bosque1" "bosque1" "bosque1" "bosque2" "bosque2" "bosque2" "bosque2"
[15] "bosque2" "bosque2" "bosque2" "bosque2" "bosque2" "bosque2" "bosque3"
[22] "bosque3" "bosque3" "bosque3" "bosque3" "bosque3" "bosque3" "bosque3"
[29] "bosque3" "bosque3"

Las formas 2 y 3 son exactas a lo que haríamos para extraer las columnas si tib fuera una matriz. En indexación de tibbles el símbolo [[]] se usa para extraer una columna dado su nombre (forma 3) o su posición (formas 4 y 5). Detengámonos un poco en tib[, 1][[1]]. En este caso el código se lee de izquierda a derecha: (i) indexamos la primera columna de tib (tib[, 1]), (ii) luego el extraemos la primer columna en formato de vector ([[1]]). Esta forma forma de indexación se asemeja a la de las listas porque, en esencia, los tibbles y data frames son listas de vectores. La sexta forma de indexar columnas usa el símbolo $ y el nombre de la columna.

El filtrado de data frames o tibbles y su exploración lo veremos en la segunda parte del módulo, así que dejamos momentaneamente su indexación en este punto.

7 Listas

Las listas son la estructura de datos más flexible en R. Puede imaginarlas como bolsas donde cualquier cosa puede ser almacenada: vectores, matrices, arrays, data frames, funciones, otras listas… Cualquier objeto. Creemos una lista y vamos desde lo básico para entender cómo indexarlas.

Creamos listas con la función list() -existen otras formas, pero por ahora mantengámoslo simple.

l1 <- list()

El objeto l1 es una lista vacía. Ahora vamos a almacenar diferentes objetos usando el símbolo $:

l1$vector <- v1
l1$matriz <- m
l1$array <- array1
l1$data_frame <- df
l1$tibble <- tib

l1
$vector
 a  b  c  d  e  f  g  h  i  j 
 2  9  7  3  1  6  5 10  4  8 

$matriz
   C1 C2 C3 C4 C5
F1  9  9 14  7 13
F2 10 15 12  6 15
F3  7 15 12 13  8
F4 10 11  9 17 12
F5 13 12 14 13 16
F6  7 12 14 14  8

$array
, , ani_1a, ani_2a, ani_3a

   C1 C2 C3 C4 C5
F1  7 21 15 13 13
F2  8 32 26 16 15
F3 35 35 37 25 11
F4 14 29  9 14 18
F5 23 21  3 10  9
F6  9 10 17 10 16

, , ani_1b, ani_2a, ani_3a

   C1 C2 C3 C4 C5
F1 15 19 17 20  0
F2 24 12 23  4 24
F3 25 16  6  6  5
F4 10 25 16 32 15
F5  4 18 15 30 13
F6 32 13 15 17 21

, , ani_1a, ani_2b, ani_3a

   C1 C2 C3 C4 C5
F1 15 26 17 15 17
F2 18 28 19 15 21
F3 41 14  7 18 13
F4 33 16 12 14 35
F5 46  9 53  6 22
F6  7 18 27 15  8

, , ani_1b, ani_2b, ani_3a

   C1 C2 C3 C4 C5
F1 26 31 29  9  8
F2 26 34 25 14 22
F3  6  8 27 45  6
F4 25 14 11 15  5
F5 28 17 11  8 14
F6 43 11 45 17 28

, , ani_1a, ani_2a, ani_3b

   C1 C2 C3 C4 C5
F1 19 34 21 47 26
F2 32 22 14 13  8
F3 28  3 15 15 20
F4 59 45 39 43 42
F5 61  8 15 11 43
F6 18 20 21  9 32

, , ani_1b, ani_2a, ani_3b

   C1 C2 C3 C4 C5
F1 11  8  3 16 16
F2 15 16  9  7 33
F3 26  7 17 63 27
F4 17 16 23 32 11
F5  2 43 13  8 26
F6 28 13 25 46 14

, , ani_1a, ani_2b, ani_3b

   C1 C2 C3 C4 C5
F1 19  9 13 12 12
F2 18 11  7 34 20
F3 20 11 15 10 28
F4 10 27 16 10 21
F5 11 10 29 28 19
F6 28 37 23 13 25

, , ani_1b, ani_2b, ani_3b

   C1 C2 C3 C4 C5
F1 20 16 33 31 43
F2 13 29 13 33  1
F3  9 40 21 33 18
F4  7 25 21 30 14
F5  7 18  9 24 21
F6 22 39 18 48  6

, , ani_1a, ani_2a, ani_3c

   C1 C2 C3 C4 C5
F1  8  6 27 14 10
F2 27  9 14 25 12
F3 14  8 12  9  9
F4 28 11  6 30 22
F5  3 13 15 13  7
F6 29  7  3 20 33

, , ani_1b, ani_2a, ani_3c

   C1 C2 C3 C4 C5
F1 35 33 19  1 16
F2  8 39 45 11 18
F3  2 39 17 34 33
F4 17 42 34 19 28
F5  6  4 24 13 11
F6 14 28 37 25 22

, , ani_1a, ani_2b, ani_3c

   C1 C2 C3 C4 C5
F1 35 12 16 17  5
F2 16 17 70 19 26
F3  7 20  7 15 40
F4 35 21 53 13 15
F5 21 22  8  4 16
F6  9 38  8 20 10

, , ani_1b, ani_2b, ani_3c

   C1 C2 C3 C4 C5
F1 52 21  6  6 33
F2 22 24 10 23 10
F3 34 10 17 54 36
F4 11 11 22 15  7
F5 30 22  9  6 14
F6 10 35 34  6 33


$data_frame
      site trap formigas
1  bosque1    1        9
2  bosque1    1       18
3  bosque1    2       14
4  bosque1    2        4
5  bosque1    3        9
6  bosque1    3       17
7  bosque1    4       10
8  bosque1    4        9
9  bosque1    5        4
10 bosque1    5        2
11 bosque2    1        4
12 bosque2    1        2
13 bosque2    2       14
14 bosque2    2        1
15 bosque2    3       11
16 bosque2    3        4
17 bosque2    4       10
18 bosque2    4        9
19 bosque2    5       22
20 bosque2    5       17
21 bosque3    1        1
22 bosque3    1       14
23 bosque3    2       11
24 bosque3    2        5
25 bosque3    3       17
26 bosque3    3       11
27 bosque3    4       11
28 bosque3    4       12
29 bosque3    5       10
30 bosque3    5        3

$tibble
# A tibble: 30 × 3
   site     trap formigas
   <chr>   <dbl>    <dbl>
 1 bosque1     1        9
 2 bosque1     1       18
 3 bosque1     2       14
 4 bosque1     2        4
 5 bosque1     3        9
 6 bosque1     3       17
 7 bosque1     4       10
 8 bosque1     4        9
 9 bosque1     5        4
10 bosque1     5        2
# … with 20 more rows

Note que con el uso de $ almacenamos objetos y sus nombres:

names(l1)
[1] "vector"     "matriz"     "array"      "data_frame" "tibble"    

Las listas son una estructura de datos con una sola dimensión, longitud:

length(l1)
[1] 5

Su indexación se da en dos pasos y sigue la misma sintaxis que empleamos para vector:

  1. Usamos nombres o posiciones para indexar cada elemento en la lista: e.g. lista[‘nombre 1’] - lista[1], lista[c(‘nombre 1’, ‘nombre 2’)] - lista[1:2]. En este punto debemos tener particular atención respecto al uso de [] o [[]], veamos:

indexando un objeto

l1['vector']
$vector
 a  b  c  d  e  f  g  h  i  j 
 2  9  7  3  1  6  5 10  4  8 
l1[1]
$vector
 a  b  c  d  e  f  g  h  i  j 
 2  9  7  3  1  6  5 10  4  8 

indexando dos o más objetos

l1[names(l1)[c(1:2, 4)]]
$vector
 a  b  c  d  e  f  g  h  i  j 
 2  9  7  3  1  6  5 10  4  8 

$matriz
   C1 C2 C3 C4 C5
F1  9  9 14  7 13
F2 10 15 12  6 15
F3  7 15 12 13  8
F4 10 11  9 17 12
F5 13 12 14 13 16
F6  7 12 14 14  8

$data_frame
      site trap formigas
1  bosque1    1        9
2  bosque1    1       18
3  bosque1    2       14
4  bosque1    2        4
5  bosque1    3        9
6  bosque1    3       17
7  bosque1    4       10
8  bosque1    4        9
9  bosque1    5        4
10 bosque1    5        2
11 bosque2    1        4
12 bosque2    1        2
13 bosque2    2       14
14 bosque2    2        1
15 bosque2    3       11
16 bosque2    3        4
17 bosque2    4       10
18 bosque2    4        9
19 bosque2    5       22
20 bosque2    5       17
21 bosque3    1        1
22 bosque3    1       14
23 bosque3    2       11
24 bosque3    2        5
25 bosque3    3       17
26 bosque3    3       11
27 bosque3    4       11
28 bosque3    4       12
29 bosque3    5       10
30 bosque3    5        3
l1[c(1:2, 4)]
$vector
 a  b  c  d  e  f  g  h  i  j 
 2  9  7  3  1  6  5 10  4  8 

$matriz
   C1 C2 C3 C4 C5
F1  9  9 14  7 13
F2 10 15 12  6 15
F3  7 15 12 13  8
F4 10 11  9 17 12
F5 13 12 14 13 16
F6  7 12 14 14  8

$data_frame
      site trap formigas
1  bosque1    1        9
2  bosque1    1       18
3  bosque1    2       14
4  bosque1    2        4
5  bosque1    3        9
6  bosque1    3       17
7  bosque1    4       10
8  bosque1    4        9
9  bosque1    5        4
10 bosque1    5        2
11 bosque2    1        4
12 bosque2    1        2
13 bosque2    2       14
14 bosque2    2        1
15 bosque2    3       11
16 bosque2    3        4
17 bosque2    4       10
18 bosque2    4        9
19 bosque2    5       22
20 bosque2    5       17
21 bosque3    1        1
22 bosque3    1       14
23 bosque3    2       11
24 bosque3    2        5
25 bosque3    3       17
26 bosque3    3       11
27 bosque3    4       11
28 bosque3    4       12
29 bosque3    5       10
30 bosque3    5        3

Sabemos que el segundo objeto de l1 es una matriz. Si la indexamos, deberíamos tener un objeto con la clase matrix, ¿cierto? Bueno, depende:

class(l1[2])
[1] "list"
class(l1[[2]])
[1] "matrix" "array" 

l1[2] resulta en una lista porque este tipo de indexación implica un “recorte”, no la extracción del objeto en cuestión. En cambio l1[[2]] hace énfasis en la extracción del objeto.

La matriz l1[[2]] podría asignarse a un nuevo objeto e indexarla de la manera que ya vimos. Pero, si planea indexar de manera conjunta la lista y el objeto que contiene, entonces entonces continuamos en el segundo paso.

  1. Indexamos el objeto de la lista (lista[[objeto]]), luego utilizamos la indexación correspondiente para extraer sus elementos (lista[[objeto]][indexacion objeto]):
Indexando el primer objeto (vector)

:

l1[['vector']][c(1, 3:5)]
a c d e 
2 7 3 1 
Indexando el segundo (matriz)

:

l1[['matriz']][4:5, , drop = T]
   C1 C2 C3 C4 C5
F4 10 11  9 17 12
F5 13 12 14 13 16
Indexando el tercero (array)

:

l1[['array']][c(1, 4:4), 3:5, 1, 1:2, ]
, , ani_2a, ani_3a

   C3 C4 C5
F1 15 13 13
F4  9 14 18

, , ani_2b, ani_3a

   C3 C4 C5
F1 17 15 17
F4 12 14 35

, , ani_2a, ani_3b

   C3 C4 C5
F1 21 47 26
F4 39 43 42

, , ani_2b, ani_3b

   C3 C4 C5
F1 13 12 12
F4 16 10 21

, , ani_2a, ani_3c

   C3 C4 C5
F1 27 14 10
F4  6 30 22

, , ani_2b, ani_3c

   C3 C4 C5
F1 16 17  5
F4 53 13 15
Indexando el cuarto (data frame)

:

l1[['data_frame']][1, 2:3, drop = F]
  trap formigas
1    1        9

Recuerdan que las listas pueden almacenar otras listas. En esos, y básicamente en cualquier caso, las reglas de indexación son las mismas. Solo debemos estar atentos a la secuencia de los anidamientos, veamos un ejemplo:

l1$sub_lista <- l1

l1[['sub_lista']][['array']][c(1, 3:4), 3:5, 1, 1:2, 2:3]
, , ani_2a, ani_3b

   C3 C4 C5
F1 21 47 26
F3 15 15 20
F4 39 43 42

, , ani_2b, ani_3b

   C3 C4 C5
F1 13 12 12
F3 15 10 28
F4 16 10 21

, , ani_2a, ani_3c

   C3 C4 C5
F1 27 14 10
F3 12  9  9
F4  6 30 22

, , ani_2b, ani_3c

   C3 C4 C5
F1 16 17  5
F3  7 15 40
F4 53 13 15

Lo anterior son los elementos básicos que usted precisa para manipular cualquier estructura de datos en R. Comprender estas reglas le darán una base razonablemente sólida para comprender los errores más comunes, programar funciones, realizar tareas con iteraciones y, en general, sacar el mayor provecho a las posibilidades del lenguaje.