13  INDEXACIÓN

Todos los elementos dentro de una estructura de datos se encuentran indexados, lo que permite acceder a cualquiera de ellos mediante el correspondiente índice.

El usuario puede tener diferentes tipos de interacciones con los elementos de las estructuras de datos:

13.1 Extracción de elementos

La extracción se refiere a una operación realizada sobre un objeto contenedor de datos, cuyo resultado es el elemento o conjunto de elementos ubicados en una posición determinada de la estructura.

R cuenta con tres descriptores de acceso que pueden usarse para fines de extracción:

  • Los corchetes sencillos []
  • Los corchetes dobles [[]]
  • El símbolo dólar $

13.1.1 Descriptor de acceso []

El descriptor de acceso [] permite referenciar el elemento que ocupa una posición específica dentro de una estructura.

Es aplicable a cualquier contendedor de datos, ya sea atómico o recursivo, unidimensional o multidimensional.

13.1.1.1 Acceso a elementos dentro de estructuras unidimensionales

En estructuras unidimensionales, es decir, vectores y listas, cada elemento está indexado con un entero positivo correspondiente su posición. Luego, la referencia a un elemento se realiza mediante la instrucción genérica objeto[posición].

Así, a[2] se refiere al elemento que ocupa la segunda posición dentro del objeto a.

a <- c(3, 8, 21, 7, 14)
a[2]
[1] 8

Esto también aplica a la listas, que son vectores genéricos. Dado que los elementos de las listas pueden ser objetos de cualquier naturaleza, el índice hace referencia a tales objetos, que son los elementos o componentes de la lista.

Consideremos nuevamente la lista planilla, definida en la sección 12.3.2:

nombre <- c("Pablo", "Víctor", "Sandra")
rh <- c("O-", "AB+", "O-")
edad <- c(28, 53, 49)
planilla <- list(nombre, rh, edad)

La instrucción planilla[1] hace referencia al primer elemento de la lista planilla, es decir, al vector nombre:

planilla[1]
[[1]]
[1] "Pablo"  "Víctor" "Sandra"

13.1.1.2 Acceso a elementos dentro de estructuras multidimensionales

El acceso a una celda o elemento particular dentro de objetos multidimensionales, es decir, matrices, arreglos o data frames puede realizarse de varias formas.

La referenciación más intuitiva y directa de una celda consiste en usar un índice para cada una de las dimensiones, separándolos con comas.

Considérese el arreglo tridimensional letras presentado en la sección 8.3, el cual se esquematiza así:

Tiempo1
v1 v2 v3
unidad1 a x d
unidad2 c m b
unidad3 f j u
unidad4 d l y



Tiempo2
v1 v2 v3
unidad1 b h r
unidad2 j o s
unidad3 h w n
unidad4 l p q


La referencia al elemento de la unidad3, v1, tiempo2 se realiza usando los correspondientes índices para cada una de sus tres dimensiones, separados por comas, así:

letras[3, 1, 2]
[1] "h"
13.1.1.2.1 Acceso a todos los elementos de un componente dimensional

En estructuras multidimensionales, la primera dimensión se denomina genéricamente filas, y la segunda, columnas. Estos nombres —que son muy naturales en matrices y data frames— se mantienen incluso en arreglos de alta dimensionalidad en los que no pueda establecerse relación con una figura geométrica.

El usuario puede tener interés en acceder a todos los elementos de un componente dimensional determinado, es decir, a todos los elementos de una fila, de una columna, o de un componente de alguna otra dimensión diferente a la primera y la segunda.

Para referenciar todos los elementos de un componente dimensional determinado se usa el índice del componente en la posición correspondiente a la dimensión seleccionada, y se omiten los índices de las demás dimensiones, manteniendo, sin embargo, la(s) correspondiente(s) coma(s) de separación.

Considérese la matriz m:

m <- matrix(c(2, 0, 5, -1, 9, 1), nrow = 2, byrow = T)
print(m)
     [,1] [,2] [,3]
[1,]    2    0    5
[2,]   -1    9    1

Supóngase que se desean referenciar todos los elementos de la segunda fila.

Teniendo en cuenta que la matriz m tiene dos dimensiones (como todas las matrices), la referenciación a cualquier elemento o conjunto de elementos debe basarse en dos índices: el primero para filas, y el segundo para columnas, así: [índice_filas, índice_columnas].

Para referenciar todos los elementos de la segunda fila, mediante el descriptor de acceso [], se escribe el índice 2 en la primera posición (la correspondiente a las filas), seguido de coma (para indicar que el objeto tiene una segunda dimensión), pero se omite el índice de las columnas, lo que indica que no se elije una columna particular, sino todas, así:

m[2, ]
[1] -1  9  1
¡Ayuda nemotécnica!

Esta instrucción puede leerse como fila 2 con todas las columnas.

Análogamente, la referencia a todos los elementos de la tercera columna se realiza con la instrucción m[, 3], que puede leerse como todas las filas de la columna 3.

m[, 3]
[1] 5 1

En el arreglo letras, ¿cómo se realizaría la referencia a todas las lecturas del tiempo1?

Todas las lecturas de tiempo1 del arreglo letras
letras[, , 1]
     [,1] [,2] [,3]
[1,] "a"  "x"  "d" 
[2,] "c"  "m"  "b" 
[3,] "f"  "j"  "u" 
[4,] "d"  "l"  "y" 

Nótese que la salida de la anterior instrucción es un arreglo bidimensional, en el cual cada una de sus filas y columnas se encabeza con base en la nomenclatura expuesta.

En caso de que se hubieran asignado nombres a los diferentes componentes dimensionales, se mostrarían los nombres en lugar de la nomenclatura numérica.

Para ilustrarlo, asignémosles nombres a los diferentes componentes dimensionales del arreglo letras:

dimnames(letras) <- list(c("unidad1", "unidad2", "unidad3", "unidad4"),
                         c("v1", "v2", "v3"),
                         c("tiempo1", "tiempo2"))

A continuación extraemos todas las lecturas de tiempo2:

letras[, , 2]
        v1  v2  v3 
unidad1 "b" "h" "r"
unidad2 "j" "o" "s"
unidad3 "h" "w" "n"
unidad4 "i" "p" "q"

Cuando los componentes dimensionales tienen nombres, estos pueden usarse entrecomillados en lugar de los índices numéricos.

La referencia a todos los elementos de v2 puede escribirse así:

letras[, "v2", ]
        tiempo1 tiempo2
unidad1 "x"     "h"    
unidad2 "m"     "o"    
unidad3 "j"     "w"    
unidad4 "l"     "p"    

¿Y cómo se haría referencia a todas las lecturas de unidad3, en el tiempo2 del arreglo letras?

Todas las lecturas de unidad3, en el tiempo2 del arreglo letras
letras[3, , 2]  # Equivalentemente: letras["unidad3", , "tiempo2"]
 v1  v2  v3 
"h" "w" "n" 
13.1.1.2.2 Referencia a las celdas de arreglos usando un solo índice

Tal y como se indicó en la sección 13.1.1.2, la referenciación más intuitiva y directa de una celda consiste en usar un índice por dimensión, separándolos con comas. No obstante, también es posible referenciar cualquier elemento de una matriz o arreglo con base en un único índice.

En tales casos, el índice corresponde con la posición del elemento, contabilizando de manera continua y ordenada a través de las diferentes dimensiones, empezando con la primera.

Considérese nuevamente la matriz m definida anteriormente:

     [,1] [,2] [,3]
[1,]    2    0    5
[2,]   -1    9    1

Usando la nomenclatura convencional, el elemento de la primera fila y tercera columna se extrae así:

m[1, 3]
[1] 5

Y ahora, usando un único índice:

Posición m[1, 3] usando un solo índice
m[5]
[1] 5
¿¡No le atinó!?

El conteo se realiza haciendo variar inicialmente las filas (primera dimensión) y luego las columnas (segunda dimensión), así:

  • Fila 1, columna 1: m[1]
  • Fila 2, columna 1: m[2]
  • Fila 1, columna 2: m[3]
  • Fila 2, columna 2: m[4]
  • Fila 1, columna 3: m[5]
  • Fila 2, columna 3: m[6]

Consideremos a continuación el arreglo letras:

, , tiempo1

        v1  v2  v3 
unidad1 "a" "x" "d"
unidad2 "c" "m" "b"
unidad3 "f" "j" "u"
unidad4 "d" "l" "y"

, , tiempo2

        v1  v2  v3 
unidad1 "b" "h" "r"
unidad2 "j" "o" "s"
unidad3 "h" "w" "n"
unidad4 "i" "p" "q"

Usando nomenclatura numérica completa, ¿cómo se obtendría el valor correspondiente a v1, unidad3, tiempo2?

Valor correspondiente a v1, unidad3, tiempo2
letras[3, 1, 2]
[1] "h"


Debe tenerse presente que, sin importar el orden en el que se plantee la pregunta, las dimensiones tienen un orden intrínseco que es necesario respetar: filas, columnas, tercera dimensión, cuarta dimensión…

Es por esto que la instrucción letras[3, 1, 2] pareciera no corresponder con la pregunta planteada. Pero, de hecho, sí corresponde: inicialmente se usa el índice de las filas, donde están las unidades (3), luego, el de las columnas, donde están las variables (1), y finalmente, el de la tercera dimensión, donde están los tiempos (2).

¿Y cómo se referenciaría el mismo valor, usando un solo índice?

Referencia a la celda v1, unidad3, tiempo2, con un solo índice
letras[15]
[1] "h"
¿Complicado?

En este caso el conteo se realiza inicialmente a través de las unidades (primera dimensión), luego a través de las variables (segunda dimensión) y finalmente a través de los tiempos (tercera dimensión).

Realmente no es que sea demasiado complicado; prestando un poco de atención puede hacerse correctamente. Pero definitivamente es más directa la instrucción letras[3, 1, 2].

En aras de la claridad y de disminuir la probabilidad de cometer errores, se recomienda referenciar los elementos de los arreglos —incluyendo matrices— usando un índice para cada dimensión.

No obstante, es necesario conocer la equivalencia entre los dos sistemas a fin de interpretar correctamente el resultado que se genera al ubicar una posición en un objeto atómico multidimensional (cf. sección 13.4).

13.1.1.3 Referencia de varios elementos

Para seleccionar un grupo de elementos, se enumeran sus correspondientes posiciones.

Si las posiciones de los elementos son contiguas, podrá usarse el operador de secuencia (cf. sección 2.7); en caso contrario, deberá usarse un vector.

Consideremos nuevamente el arreglo letras:

, , tiempo1

        v1  v2  v3 
unidad1 "a" "x" "d"
unidad2 "c" "m" "b"
unidad3 "f" "j" "u"
unidad4 "d" "l" "y"

, , tiempo2

        v1  v2  v3 
unidad1 "b" "h" "r"
unidad2 "j" "o" "s"
unidad3 "h" "w" "n"
unidad4 "i" "p" "q"

¿Cómo se seleccionarían las unidades 2, 3 y 4, para todas las variables, en el primer tiempo?

Unidades 2, 3 y 4 del primer tiempo
letras[2:4, , 1]
        v1  v2  v3 
unidad2 "c" "m" "b"
unidad3 "f" "j" "u"
unidad4 "d" "l" "y"

¿Y cómo se elegirían las unidades 1 y 4 de las variables 1 y 3, en el tiempo 2?

Unidades 1 y 4 de las variables 1 y 3, en el tiempo 2
letras[c(1, 4), c(1, 3), 2]
        v1  v3 
unidad1 "b" "r"
unidad4 "i" "q"

13.1.1.4 Referencia en data frames

El uso del descriptor de acceso [] para extraer información de una celda o un conjunto de celdas dentro de data frames exige tener presente su naturaleza dual como objetos bidimensionales y como listas.

Para ilustrarlo, consideremos nuevamente el data frame df usado en la sección 8.4

id <- c("a23", "f31", "j33", "m54")
v1 <- c(2.4, 7.9, 1.1, 8.5)        
v2 <- c(4+3i, 2-0.8i, 1+1.1i, 3-5i)
df <- data.frame(id, v1, v2)
print(df)
   id  v1     v2
1 a23 2.4 4+3.0i
2 f31 7.9 2-0.8i
3 j33 1.1 1+1.1i
4 m54 8.5 3-5.0i

Al tratarse de objetos bidimensionales, es posible referenciar cualquier celda mediante el uso de dos índices: el primero para filas y el segundo para columnas, así:

df[3, 2]
[1] 1.1

Igualmente aplica la referencia a una fila o una columna completa, así:

df[4, ]
   id  v1   v2
4 m54 8.5 3-5i

O así:

df[, "id"]
[1] "a23" "f31" "j33" "m54"

Hasta aquí no hay ninguna novedad. La referencia mediante 2 índices funciona exactamente igual que en matrices y arreglos.

No obstante, cuando se usa un único índice en un data frame —a diferencia de lo que sucede en matrices y arreglos (cf. sección 13.1.1.2.2)— se extrae la correspondiente columna.

Así, para acceder a los elementos de la segunda columna del data frame df puede usarse simplemente:

df[2]
   v1
1 2.4
2 7.9
3 1.1
4 8.5

O también:

df[, 2]
[1] 2.4 7.9 1.1 8.5
¡Tiene sentido!

Teniendo en cuenta que los data frames también son listas (cf. nota 9.1), en las que las variables constituyen los elementos de la lista, no es de extrañar que el uso de un solo índice haga referencia al elemento de las listas, tal como en las listas genéricas (cf. sección 13.1.1.1).

¿Y por qué tienen diferente apariencia?

Aunque df[2] y df[, 2] extraen los mismos elementos del data frame df, lo hacen de distinta manera. En el primer caso, los elementos siguen estando dentro de un data frame:

class(df[2])
[1] "data.frame"

En el segundo caso, los elementos se extraen en forma de vector:

class(df[, 2])
[1] "numeric"

Este aspecto se ilustra con mayor profundidad en la advertencia 13.1.

13.1.2 Descriptor de acceso $

El descriptor de acceso $ se usa para referenciar —mediante sus nombres— los componentes de los objetos recursivos, esto es, las columnas de un data frame o los elementos con nombre de una lista.

13.1.2.1 Referencia de columnas en data frames usando $

Una de las maneras más usuales de extraer una columna específica de un data frame es usando el descriptor de acceso $ seguido del nombre de la columna1, .

El formato general es nombre.data.frame$nombre.columna.

¡Sin comillas!

Cuando se usa el descriptor de acceso $, los nombres no van entre comillas.

Esto contrasta con el uso de nombres cuando se usa el descriptor [] (cf. sección 13.1.1.4).

Considérese nuevamente el data frame df:

   id  v1     v2
1 a23 2.4 4+3.0i
2 f31 7.9 2-0.8i
3 j33 1.1 1+1.1i
4 m54 8.5 3-5.0i

Para hacer referencia a su primera columna, puede escribirse cualquiera de las siguientes instrucciones:

df$id
df[, "id"]
df[, 1]
df[1]
df["id"]
Advertencia 13.1: ¡Cuidado con la clase!

Aunque las cinco instrucciones anteriores hacen referencia a la misma información (la primera columna del data frame df), no todas dan lugar a objetos de la misma clase.

Las tres primeras instrucciones extraen el elemento de la estructura contenedora. El resultado es un vector atómico cuya clase se corresponde con su contenido (en este caso, character).

Las dos últimas instrucciones mantienen el vector dentro del data frame (un data frame conformado por un solo vector). Luego, el objeto resultante es de la clase data.frame.

Aunque en la mayoría de las situaciones, cualquiera de los dos tipos de objetos generados cumpliría el mismo rol, esta distinción podría ser relevante en casos en los que alguna función exigiera argumentos de una clase determinada.

13.1.2.2 Referencia en listas

El descriptor de acceso $ también puede usarse para referenciar los elementos de una lista, siempre que se les haya sido asignado un nombre.

Para ilustrarlo, partamos de la lista lis1:

nombre <- c("Pablo", "Víctor", "Sandra")
rh <- c("O-", "AB+", "O-")
edad <- c(28, 53, 49)
m <- matrix(c(2, -1, 0, 9, 5, 1), nrow = 2)
lis1 <- list(nombre, rh, edad, m)

A continuación construimos una segunda lista lis2, en la que lis1 es uno de sus componentes, así:

lis2 <- list(nombre, rh, edad, m, lis1)

A continuación les asignamos nombres los elementos de lis2. Aunque no es obligatorio asignarles nombres a los elementos de las listas, lo hacemos para ilustrar el uso del descriptor de acceso $.

names(lis2) <- c("nombre", "rh", "edad", "score", "lis1")

Ahora puede usarse el descriptor de acceso $ para invocar cualquiera de los elementos de lis2. Así, por ejemplo, la referencia al primer elemento de lis2 se realiza así:

lis2$nombre
[1] "Pablo"  "Víctor" "Sandra"

Es posible combinar el descriptor de acceso $ con el descriptor [] para acceder a los subelementos dentro de cualquiera de los elementos de lis2.

Así, para recuperar el tercer nombre del primer componente de lis2, se usa la siguiente instrucción:

lis2$nombre[3]
[1] "Sandra"

¿Cómo se hace referencia a todos los elementos ubicados en la primera fila de la matriz score, que constituye el cuarto elemento de lis2?

Todos los elementos de la fila 1 de la matriz score
lis2$score[1, ]
[1] 2 0 5

Cuando los subelementos de una lista también tienen nombre, pueden encadenarse varios descriptores de acceso $ para referenciar subelementos en un nivel más interno.

Considérense los siguientes nombres para los elementos de la lista lis1, que constituye el quinto elemento de la lista lis2.

names(lis2$lis1) <- c("nombre1", "rh1", "edad1", "score1")
¡Podrían ser los mismos nombres!

En el presente ejemplo se ha optado por usar nombres diferenciados para los elementos de lis1 y lis2, a fin de facilitar su visualización.

Sin embargo, en casos como este, en los que se tienen objetos anidados, los nombre de los elementos podrían coincidir.

¡Haga la prueba!

Para referenciar el subelemento de la fila 2, columna 2, de la matriz score1, que es uno de los componentes de la lista lis1, se usa la siguiente instrucción:

lis2$lis1$score1[2, 2]
[1] 9

13.1.3 Descriptor de acceso [[]]

El descriptor de acceso [[]] tiene un uso análogo al del descriptor de acceso $: permite elegir un componente2 de una estructura recursiva.

No obstante, a diferencia del descriptor $, que únicamente admite nombres, el descriptor [[]] permite seleccionar los componentes a partir de sus nombres o de sus índices de posición.

Cuando se usa el descriptor de acceso [[]] con nombres, estos deben ir entrecomillados (aunque al usar el descriptor de acceso $, los nombres también podrían entrecomillarse, al no ser necesario, no suele hacerse, en aras de la simplicidad).

Así, la misma referencia anterior, es decir, al subelemento de la fila 2, columna 2, de la matriz score1, que constituye el cuarto elemento de la lista lis1, que a su vez es el quinto elemento de lis2, puede realizarse así:

lis2[[5]][[4]][2, 2]
[1] 9

O así:

lis2[["lis1"]][["score1"]][2, 2]
¿Cómo se lee?

La anterior expresión puede leerse de derecha a izquierda o viceversa.

La lectura de derecha a izquierda da lugar a una descripción más compacta: “el elemento de la segunda fila, segunda columna del cuarto objeto (matriz score1) del quinto objeto (lista lis1) de la lista lis2”.

La lectura de izquierda a derecha, aunque no genera una expresión tan compacta, ayuda a entender mejor a cuál elemento se hace referencia: “Dentro del objeto lis2 se toma su quinto elemento, que es lis1; luego, dentro de lis1, se toma el cuarto elemento, que es score1; luego, dentro de score1, se toma el elemento de la fila 2, columna 2”.

Las anteriores guías de lectura no son más que ayudas nemotécnicas que el usuario podría utilizar para entender la lógica de la nomenclatura y a cuál elemento se está haciendo referencia. Desde luego, esto no tiene nada que ver con la forma en la que R decodifica la instrucción.

Precaución 13.2: ¿Y por qué no usar simplemente lis2[5][4][2, 2]?

El descriptor de acceso [] selecciona el elemento, pero lo mantiene dentro de la estructura contenedora (lista o data frame).

En contraste, el descriptor de acceso [[]] extrae el elemento de la estructura contenedora.

La extracción de los elementos de la estructura contenedora es la que posibilidad el uso encadenado del descriptor de acceso [[]] para referenciar cualquier subelemento dentro de una estructura con elementos anidados.

Para ilustrar lo señalado en el llamado de precacaución 13.2, se reproduce a continuación una explicación genial que nos presentan Wickham y Grolemund (2017)3.

Considérese un pimientero atípico que contiene sobres de pimienta. El pimientero puede pensarse como una lista (contenedor de objetos) que contiene otras listas (sobres), que a su vez contienen pimienta:

Si la lista x representa el pimientero, tenemos:

x


El primer sobre del pimientero está representado por x[1]. No obstante, acorde con lo indicado en el llamado de precacaución 13.2, el sobre se mantiene dentro del pimientero.

x[1]

El hecho de que el sobre se mantenga dentro del contenedor principal (el pimientero) impediría acceder a su contenido.

En contraste, x[[1]] también representa el primer sobre, pero extraído del pimientero.

x[[1]]

El hecho de que haber extraído el sobre de su contendedor principal permite acceder a su contenido, así:

x[[1]][[1]]

¿Podría evitarse el entrecomillado de los nombres?

Es posible eludir la restricción del entrecomillamiento de los nombres que van entre los corchetes dobles, usando la cadena de caracteres correspondiente al nombre, en lugar del nombre mismo.

Así para referenciar el segundo elemento lis2 puede usarse lis2[[2]] o lis2[["edad"]], pero también puede usarse la siguiente estrategia:

age <- "edad"
lis2[[age]]
[1] 28 53 49

Aunque esta solución normalmente resulta mucho más dispendiosa que el entrecomillamiento del nombre, eventualmente podría ser útil al interior de alguna función personalizada.

Resumen del uso de $, [[]] y [].


El descriptor de acceso $ se utiliza únicamente con nombres —nunca con posiciones— para seleccionar columnas de un data frame o elementos con nombre de una lista.

El descriptor de acceso [[]] se usa para seleccionar columnas de un data frame o elementos de una lista. Admite índices numéricos y nombres entrecomillados.

El descriptor de acceso [[]] siempre elige un único elemento; no es posible utilizarlo para seleccionar rangos de elementos.

El descriptor de acceso [] admite índices numéricos y nombres entrecomillados. Es el descriptor que debe usarse para seleccionar rangos de elementos.

El descriptor de acceso [] selecciona el elemento, pero lo mantiene dentro de la estructura contenedora (lista o data frame). En contraste, el descriptor de acceso [[]] extrae el elemento de la estructura contenedora.

13.1.3.1 Evaluación del tipo de componentes dentro de contenedores recursivos

En la sección 10.2, se indicó que existía la posibilidad de evaluar el tipo de objetos que conformaban un contenedor recursivo, mediante el uso de descriptores adecuados.

Así, para averiguar, por ejemplo, de qué tipo es el primer elemento de la lista lis2, se usa la siguiente instrucción.

typeof(lis2[[1]])
[1] "character"

Si se consulta qué tipo de objeto es el quinto elemento de lis2, se obtiene como resultado list, puesto que ese objeto es la lista lis1. No obstante, puede averiguarse el tipo de cualquiera de los componentes atómicos de lis1. Así, por ejemplo, para averiguar de qué tipo es el segundo elemento de lis1, se usa la siguiente instrucción:

typeof(lis2[[5]][[2]])
[1] "character"

En este caso, en el que los objetos de la lista tienen nombres, también podría haberse usado el descriptor $, así:

typeof(lis2$lis1$rh1)
[1] "character"

13.1.4 Referencia directa de variables

Hay varias circunstancias que permiten referenciar una variable de un data frame directamente por su nombre, sin usar comillas y sin necesidad de anteponer el nombre de la estructura contenedora ni el descriptor de acceso $.

  1. Cuando se utiliza alguna función como with, within o transform, (cf. capítulo 16), las cuales crean un ambiente de trabajo dentro del cual pueden referenciarse directamente las variables.

    Así, por ejemplo, la referencia a la variable ttos dentro del data frame dca puede realizarse así

   with(dca, aov(y ~ ttos))
  1. Cuando la referencia a la variable se hace dentro de una función que incluya el argumento data, mediante el cual se indique el nombre del data frame (dca en el presente ejemplo).
   aov(y ~ ttos, data = dca)
  1. Cuando se liga el data frame a la ruta de búsqueda de R, mediante la función attach.
    attach(dca)
    anova <- aov(y ~ ttos)
    detach(dca)
¡No lo amarre!

Esta alternativa, al modificar la ruta de búsqueda, puede generar confusión y errores de ejecución, máxime si se olvida desligar la base de datos después de haberla usado.

Aunque se desaconseja el uso de esta función, en caso de que el usuario decida utilizarla bajo su responsabilidad, se recomienda un desligado posterior, mediante la función detach.

El uso de la segunda alternativa, aunque es seguro, no siempre es viable. Únicamente puede usarse con funciones que consideren el argumento data.

La primera opción, además de ser segura, siempre es viable, pudiendo usarse conjuntamente con cualquier función.

13.2 Generación de subconjuntos

Existen dos estrategias para extraer un subconjunto de elementos de un objeto, de manera que tales elementos satisfagan una condición, bien sea de posición o de cualquier otra índole.

Una de tales estrategias se basa en el uso del descriptor de acceso [], presentado en la sección 13.1.1; la otra, en la función subset.

Para generar subconjuntos mediante el descriptor de acceso [], a partir de la posición de los elementos, se utilizan los correspondientes índices así:

  • Enteros positivos para mantener los elementos.
  • Enteros negativos para retirar los elementos.

Considérese la matriz m:

     [,1] [,2] [,3]
[1,]    2    0    5
[2,]   -1    9    1

Mediante la siguiente instrucción, basada en el descriptor de acceso [], se seleccionan las columnas 2 y 3 de la matriz m.

m[, 2:3]

Equivalentemente, la función subset permite seleccionar estas columnas:

subset(m, select = 2:3)
     [,1] [,2]
[1,]    0    5
[2,]    9    1

Nótese que la primera estrategia no hace uso de ninguna función (no hay paréntesis), sino que se referencian dentro de los corchetes los índices de las columnas que se mantienen en la matriz m. En el segundo caso, se usa la función subset, con dos argumentos: el primero corresponde al objeto (m), mientras que el segundo (select) indica las columnas que deben mantenerse.

Usando cualquiera de las siguientes instrucciones, mediante las cuales se retira la primera columna, se llega al mismo resultado:

m[, -1]
subset(m, select = -1)
     [,1] [,2]
[1,]    0    5
[2,]    9    1

En las instrucciones trasanteriores se usó el operador de secuencia :, aprovechando la vecindad de las dos columnas seleccionadas. Para seleccionar columnas no adyacentes, por ejemplo, la 1 y la 3, se usa el correspondiente vector de índices, en cualquiera de las siguientes instrucciones.

m[, c(1, 3)]

O equivalentemente:

subset(m, select = c(1, 3))
     [,1] [,2]
[1,]    2    5
[2,]   -1    1

Desde luego, la selección en cuestión también puede realizarse, retirando la segunda columna.

m[, -2]

O así:

subset(m, select = -2)
     [,1] [,2]
[1,]    2    5
[2,]   -1    1

Equivalentemente, podría utilizarse un vector conformado por elementos lógicos, haciendo corresponder etiquetas TRUE (o T) con las columnas que se mantienen, y etiquetas FALSE (o F) con las que se retiran.

m[, c(TRUE, FALSE, TRUE)]

O equivalentemente:

subset(m, select = c(T, F, T))
     [,1] [,2]
[1,]    2    5
[2,]   -1    1

Aunque todas las variantes anteriores ilustran la selección de un subconjunto de columnas de una matriz, bien podrían usarse sobre un data frame.

Considérese el data frame airquality, el cual forma parte de los datasets preinstalados en R, y cuya estructura se muestra a continuación.

'data.frame':   153 obs. of  6 variables:
 $ Ozone  : int  41 36 12 18 NA 28 23 19 8 NA ...
 $ Solar.R: int  190 118 149 313 NA NA 299 99 19 194 ...
 $ Wind   : num  7.4 8 12.6 11.5 14.3 14.9 8.6 13.8 20.1 8.6 ...
 $ Temp   : int  67 72 74 62 56 66 65 59 61 69 ...
 $ Month  : int  5 5 5 5 5 5 5 5 5 5 ...
 $ Day    : int  1 2 3 4 5 6 7 8 9 10 ...

Para generar otro data frame conformado únicamente por la primera y la tercera columna de airquality, puede usarse cualquiera de las instrucciones ilustradas anteriormente, incorporando los índices 1 y 3, especificando el retiro de las columnas 2, 4, 5 y 6 o usando etiquetas lógicas.

Adicionalmente, puesto que todo data frame tiene una serie de nombres asociados con sus columnas, estos pueden usarse, en lugar de los índices de posición, para la generación de los subconjuntos, usando cualquiera de las siguientes instrucciones:

airquality[, c('Ozone', "Wind")]
subset(airquality, select = c("Ozone", 'Wind'))
subset(airquality, select = c(Ozone, Wind))

A continuación, se resaltan varios aspectos relacionados con las instrucciones anteriores:

  1. La generación de subconjuntos, usando el operador de acceso [] (línea 1) exige que los nombres estén entrecomillados.

  2. Pueden usarse comillas dobles o sencillas para encerrar los nombres.

  3. La generación de subconjuntos, mediante la función subset (líneas 2 y 3) permite usar nombres entrecomillados o sin entrecomillar, pero no es posible mezclar unos y otros en un mismo vector.

  4. Cuando se usan nombres en lugar de índices numéricos, estos solo actúan para fines de inclusión de las correspondientes columnas; no existe un uso equivalente al de los índices negativos, para especificar la exclusión de columnas.

Para seleccionar un subconjunto de filas de una matriz o un data frame, se usa el operador de acceso [] con instrucciones análogas a las presentadas anteriormente, teniendo en cuenta que los índices de las filas van antes de la coma.

Es posible incluir simultáneamente restricciones por filas y por columnas. La siguiente instrucción genera el subconjunto de los cinco primeros registros de las variables Wind y Temp del data frame airquality.

airquality[1:5, c('Wind', 'Temp')]
  Wind Temp
1  7.4   67
2  8.0   72
3 12.6   74
4 11.5   62
5 14.3   56

13.2.1 Generación de subconjuntos con base en una condición

Aunque la selección de subconjuntos a partir de la posición es completamente válida, la selección de filas suele estar basada en la satisfacción de alguna condición, ya sea simple o compleja.

Para seleccionar los registros con lecturas superiores a 30 en la variable Ozone, se usa la siguiente instrucción.

airquality[airquality$Ozone > 30, ]
¡Incluya el nombre del data frame!

Al seleccionar mediante el operador de acceso [], no basta con escribir el nombre de la variable dentro de los corchetes (Ozone, en el presente ejemplo), sino que es necesario usar el operador de acceso $ para enlazarlo con el data frame en el que se encuentra contenido, tal y como se ilustra en la instrucción anterior.

Asimismo, es posible generar el subconjunto en cuestión, mediante la función subset, incluyendo la condición mediante el argumento homónimo subset.

subset(airquality, subset = Ozone > 30)

Aunque el argumento subset también puede utilizarse para imponer una restricción por posición, permitiendo elegir, por ejemplo, las 5 primeras filas, tal y como se ilustró mediante el uso de descriptor de acceso [], la especificación en este caso no se realiza usando índices, sino valores lógicos.

En tal sentido, la siguiente instrucción es incorrecta:

¡Instrucción incorrecta!
subset(airquality, subset = 1:5)

La instrucción correcta exige alimentar el argumento subset con un vector lógico que contenga valores verdaderos en las cinco primeras posiciones y falsos en las demás:

subset(airquality, subset = c(rep(T, 5), rep(F, 148)))

Cuando se impone una condición como la usada anteriormente (Ozone > 30), internamente se genera un vector de valores lógicos.

Pueden combinarse varias condiciones y criterios de selección en una misma instrucción. Así, por ejemplo, para generar un subconjunto con las lecturas de las variables Solar.R y Temp, que se hayan registrado durante los meses (Month) 5 o 6 y que correspondan a temperaturas (Temp) superiores a los 80 grados, puede utilizarse cualquiera de las siguientes instrucciones.

airquality[(airquality$Month == 5 | airquality$Month == 6) &
           airquality$Temp > 80, c("Solar.R", "Temp")]
subset(airquality, subset = (Month == 5 | Month == 6) & Temp > 
       80, select = c(Solar.R, Temp))
   Solar.R Temp
29     252   81
35     186   84
36     220   85
38     127   82
39     273   87
40     291   90
41     323   87
42     259   93
43     250   92
44     148   82
61     138   83
Cuando use subset y select

Cuando se usa la función subset, el argumento subset incorpora las condiciones para la selección de los registros (filas), mientras que el argumento select indica cuáles columnas quedan en el subconjunto.

¡Cuidado con los datos perdidos!

El comportamiento de las dos herramientas de selección presentadas (descriptor de acceso [] y función subset) diverge cuando la variable usada para establecer el criterio de selección tiene datos perdidos (NA).

Al usar el descriptor de acceso [], los registros NA se incorporan al subconjunto resultante, mientras que al usar la función subset, no.

Para ejemplificarlo, considérense los siguientes subconjuntos del data frame airquality, obtenidos mediante la condición de que la radiación solar (Solar.R) sea mayor que 330.

airquality[airquality$Solar.R > 330, ]
     Ozone Solar.R Wind Temp Month Day
NA      NA      NA   NA   NA    NA  NA
NA.1    NA      NA   NA   NA    NA  NA
NA.2    NA      NA   NA   NA    NA  NA
16      14     334 11.5   64     5  16
NA.3    NA      NA   NA   NA    NA  NA
45      NA     332 13.8   80     6  14
NA.4    NA      NA   NA   NA    NA  NA
NA.5    NA      NA   NA   NA    NA  NA
NA.6    NA      NA   NA   NA    NA  NA

La instrucción anterior genera un subconjunto en el que quedan incluidos, además de los registros que satisfacen la condición (filas 16 y 45), todos aquellos con información faltante para la variable criterio (7 registros). Estos últimos aparecen identificados en el objeto resultante como NA, NA.1, …, NA.6, y la información de todas las demás variables también aparece como NA, sin importar que pudiera haber contenido valores en la base de datos original.

subset(airquality, subset = Solar.R > 330)
   Ozone Solar.R Wind Temp Month Day
16    14     334 11.5   64     5  16
45    NA     332 13.8   80     6  14

En contraste, al usar la función subset, se obtiene, como es de esperarse, un subconjunto que contiene únicamente los registros que satisfacen la condición (filas 16 y 45).

Otro aspecto en el que pueden diferir los resultados generados mediante las dos herramientas expuestas tiene que ver con la simplificación del objeto resultante cuando hay reducción de dimensionalidad.

No obstante, este es un aspecto secundario, puesto que lo único en lo que realmente difieren las dos herramientas es en el comportamiento que manejan por defecto, pudiendo hacerlos coincidir, mediante el argumento drop.

Cuando al imponer una condición sobre una matriz o un data frame, se genera un objeto unidimensional (con una sola columna o una sola fila), bien puede suceder que tal objeto herede la clase del objeto generador (matriz o data frame) o que se extraiga de la estructura contenedora, simplificándose hacia un vector (clase integer, character, etc.).

Este comportamiento es regulado por el argumento drop (nemotécnicamente, simplificar), cuyo valor por defecto, cuando se usa el descriptor de acceso [] es TRUE (simplifica), y FALSE (no simplifica) cuando se usa la función subset.

Considérese, para el data frame airquality, la selección de los registros con vientos (Wind) mayores que 17, manteniendo únicamente los registros de dicha variable.

Usando el descriptor de acceso [], con su valor de drop por defecto (TRUE):

airquality[airquality$Wind > 17, 3]
[1] 20.1 18.4 20.7
class(airquality[airquality$Wind, 3])
[1] "numeric"

El objeto resultante no se mantiene dentro de la estructura original, sino que se extrae como un vector numérico.

Puede usarse el argumento drop para evitar la simplificación:

airquality[airquality$Wind > 17, 3, drop = F]
   Wind
9  20.1
18 18.4
48 20.7
class(airquality[airquality$Wind, 3, drop = F])
[1] "data.frame"

Ahora se ilustra el uso de la función subset, con su valor de drop por defecto (FALSE):

subset(airquality, subset = Wind > 17, select = 3)
   Wind
9  20.1
18 18.4
48 20.7
class(subset(airquality, subset = Wind > 17, select = 3))
[1] "data.frame"

Al usar la función subset, el objeto resultante se mantiene por defecto dentro de la estructura contenedora, heredando, por tanto, la clase del objeto generador, sin importar que se haya reducido la dimensionalidad.

No obstante, este comportamiento también puede controlarse mediante el argumento drop.

subset(airquality, subset = Wind > 17, select = 3, drop = T)
[1] 20.1 18.4 20.7
class(subset(airquality, subset = Wind > 17, select = 3, drop = T))
[1] "numeric"
En resumen…


El descriptor de acceso [] y la función subset permiten generar subconjuntos de datos.

Cuando se usa una condición basada en una variable con datos faltantes, la función subset genera resultados más adecuados, por cuanto no incluye los registros con información faltante.

Los objetos que se obtienen a partir de la función subset se mantienen por defecto dentro de la estructura original, heredando, por tanto, la clase del objeto generador; no obstante, dicho comportamiento podría modificarse a través del argumento drop.

La función subset, aunque permite elegir una serie de elementos con base en su posición, exige realizar la especificación mediante un vector de valores lógicos. Para dicho fin puede resultar más cómodo el uso del descriptor de acceso [].

13.3 Adición y sustitución de elementos

Para sustituir un elemento dentro de un objeto, basta con asignar otro valor a la correspondiente posición usando el descriptor de acceso [].

Considérese nuevamente la matriz m:

     [,1] [,2] [,3]
[1,]    2    0    5
[2,]   -1    9    1

Podría remplazarse el elemento de la segunda fila, tercera columna por 7, así:

m[2, 3] <- 7
m
     [,1] [,2] [,3]
[1,]    2    0    5
[2,]   -1    9    7

Puede remplazarse la primera fila de la matriz m, así:

m[1, ] <- c(1, 2, 3)
m
     [,1] [,2] [,3]
[1,]    1    2    3
[2,]   -1    9    7

Una asignación a una posición inexistente da lugar a la ampliación del objeto. Esta estrategia puede usarse con vectores, listas y data frames, mas no con matrices o arreglos.

a <- c(2, 0, -7)
a[4] <- 5
a
[1]  2  0 -7  5

Para el caso de vectores o listas no se requiere que los nuevos elementos estén contiguos a los existentes. Así, podría asignarse un nuevo elemento a la sexta posición del vector definido anteriormente, con lo cual se marca la quinta posición como vacía.

a[6] <- 4
a
[1]  2  0 -7  5 NA  4

Mediante un proceso análogo al anterior, es posible agregar nuevas filas o columnas a un data frame. Las nuevas filas no necesariamente tienen que ser contiguas a las existentes. Si no lo son, las celdas de las filas a las que no se les hubiera asignado información se marcarían como NA. No obstante, las nuevas columnas sí tendrían que quedar a continuación de las existentes, no siendo posible dejar columnas vacías en un data frame.

Un caso particular de sustitución se presenta cuando desea eliminar un elemento, pero sin afectar el tamaño de la estructura contenedora. Para ello, basta con remplazar el correspondiente valor con NA.

a[2] <- NA
a
[1]  2 NA -7  5 NA  4

13.4 Ubicación de una posición

La función which permite averiguar la posición ocupada por un elemento que satisfaga una condición específica.

Supóngase, por ejemplo, que se tiene un vector con ocho elementos y se desea conocer la posición ocupada por el máximo.

v <- c(7.9, 12.5, 19.0, 1.4, 77.7, 11.6, 12.4, 32.8)

En este caso, el máximo es 77.7; este valor ocupa la quinta posición del vector v.

El máximo puede obtenerse así:

max(v)
[1] 77.7

Si el vector es pequeño, como el del presente ejemplo, no hay ninguna dificultad en localizar el valor y determinar su posición. No obstante, si el vector es grande y/o se desea automatizar algún proceso, se recurre al comando which, el cual puede leerse nemotécnicamente como “la posición tal que…”.

En este caso se quiere averiguar la posición (del vector v) tal que su valor sea máximo. El argumento de which debe ser de tipo lógico; which devolverá la posición o posiciones para las cuales la condición sea verdadera (cf. sección 20.4).

which(v == max(v))
[1] 5

Para el caso particular de encontrar la posición en la cual el valor es máximo, puede usarse la función which.max:

which.max(v)
[1] 5

Asimismo, podría usarse la función which.min para determinar la posición del mínimo valor:

which.min(v)
[1] 4

La función which también puede dar por resultado varias posiciones que satisfagan una condición determinada.

which(v > 20)
[1] 5 8

La condición indicada (que v sea mayor que 20) es satisfecha por los elementos que ocupan la quinta y la octava posición.

Considérese ahora la matriz m:

     [,1] [,2] [,3]
[1,]    1    2    3
[2,]   -1    9    7
which (m < 2)
[1] 1 2

Un resultado como este puede ser engañoso. Podría creerse que se está haciendo referencia al elemento de la fila 1, columna 2 (que no satisface la condición especificada). Sin embargo, los resultados de which siempre se presentan mediante un único índice. Cuando aparecen varios índices, como en el presente caso, es porque varias posiciones satisfacen la condición.

En este caso, el resultado indica que la condición indicada (que m sea menor que 2) es satisfecha por los elementos que ocupan la primera y la segunda posición.

Recuérdese que cuando se usa un único índice para referenciar los elementos contenidos en objetos atómicos dimensionales (matrices y arreglos), el conteo se realiza haciendo variar inicialmente la primera dimensión (filas) y luego la segunda (columnas) (cf. sección 13.1.1.2.2).

Cuando se realizan modificaciones iterativas sobre un objeto, los resultados de la función which podrían generar confusión.

El siguiente vector, que contiene valores tanto positivos como negativos, podría representar los residuales de un modelo.

resid <- c(-2.5, 1.4, -4.3, 2.3, 0.2, 3.9, 0.4, -1.7)

Para averiguar cuál es la posición del vector resid tal que su valor absoluto sea máximo, se usa la siguiente instrucción.

which(abs(resid) == max(abs(resid)))
[1] 3

Nótese que en este caso es necesario incorporar el valor absoluto también en el término de la izquierda de la condición, pues de no hacerlo no se satisfaría la condición de igualdad si el máximo residual en valor absoluto correspondiera a un residual negativo, como en el presente caso.

Esta información podría resultar muy útil si se deseara, por ejemplo, retirar el valor correspondiente a dicha posición en otra base de datos. Suponiendo que el vector resid, contiene los residuales de un modelo ajustado con base en información contenida en un data frame llamado data, y que el usuario hubiera decidido retirar la observación correspondiente al mayor residual en valor absoluto, debería retirar la tercera observación del data frame data.

data <- data[-3, , drop = F]

Supóngase ahora que tras retirar la observación en cuestión y correr nuevamente el modelo se obtiene el siguiente vector de residuales.

resid <- c(-3.1, 1.1, 2.0, -0.8, 3.7, 0.3, -3.2)

Si de nuevo se realiza el proceso de búsqueda de la posición cuyo residual en valor absoluto sea máximo, se obtendría que este se ubica en la quinta posición.

Y si nuevamente se desea retirar la correspondiente observación del data frame data, se realizaría un proceso análogo al mostrado anteriormente, con lo cual se retiraría la observación que ocupa la quinta posición, es decir, y = 9.3.

Puede resultar desconcertante que al automatizar un proceso se elimine la observación y = 9.3, cuando aparentemente la observación que debería retirarse es y = 6.1. Nótese, sin embargo, que la observación que efectivamente ocupa la quinta posición es y = 9.3, a pesar de que el nombre de fila indique que esa es la observación 6. Esto se debe a que al realizar la creación o importación del data frame, el sistema automáticamente asigna a cada fila un nombre acorde con la posición ocupada, el cual no cambia cuando se modifica la posición de la observación.

Nótese que en el data frame original y = 9.3 ocupa la sexta posición, por lo cual se le asigna a esa fila el nombre 6. Posteriormente, al retirar la tercera fila, aunque todas las filas siguientes se desplazan una posición, mantienen sus nombres de fila originales, con lo cual la fila llamada 4 pasa a ocupar la posición 3, la 5 pasa a la posición 4 y así sucesivamente.

Aunque este hecho no tiene por qué afectar el desempeño de algún proceso basado en las posiciones, si ello resultara perturbador, podría solucionarse fácilmente renombrando las filas en una secuencia que vaya desde 1 hasta el número de observaciones, así:

rownames(data) <- 1:nrow(data)

Referencias Bibliográficas

Wickham, H., y G. Grolemund. 2017. R for Data Science: Import, Tidy, Transform, Visualize, and Model Data. Sebastopol, CA: O’Reilly Media, Inc. https://r4ds.had.co.nz/.

  1. Obligatoriamente, todas las columnas de los data frames tienen nombre.↩︎

  2. Los componentes en cuestión, para el caso de los data frames son sus columnas; para las listas son sus elementos.↩︎

  3. También hay disponible en línea una versión de este libro en español↩︎