3.7: Carácter y Datos Categóricos
- Page ID
- 55160
Los conjuntos de datos científicos, especialmente los que se van a analizar estadísticamente, suelen contener entradas “categóricas”. Si cada fila en un marco de datos representa una sola medición, entonces una columna podría representar si el valor medido era de un “hombre” o “mujer”, o del grupo “control” o grupo de “tratamiento”. A veces estas categorías, aunque no numéricas, tienen un orden intrínseco, como dosis “bajas”, “medias” y “altas”.
Lamentablemente, la mayoría de las veces, estas entradas no están codificadas para facilitar el análisis. Considere el archivo separado por tabuladores expr_long_coded.txt
, donde cada línea representa una lectura de expresión (normalizada) para un gen (especificado por la columna ID) en un grupo de muestra dado. Este experimento probó los efectos de un tratamiento químico en una especie de planta agrícola. El grupo de muestra codifica información sobre qué genotipo se probó (ya sea C6
o L4
), el tratamiento aplicado a las plantas (ya sea testigo
o químico
), el tipo de tejido medido (ya sea A
, B
o C
para hoja, tallo o root), y números para réplicas estadísticas (1
, 2
o 3
).
Inicialmente, leeremos la tabla en un marco de datos. Para este conjunto de datos, es probable que queramos trabajar con la información categórica de forma independiente, por ejemplo, extrayendo solo valores para el tratamiento químico. Esto sería mucho más fácil si el marco de datos tuviera columnas individuales para genotipo
, tratamiento
, tejido
y replicación
en lugar de una sola columna de muestra
que abarca todo.
Una instalación básica de R incluye una serie de funciones para trabajar con vectores de caracteres, pero el paquete stringr
(disponible a través de install.packes (“stringr”)
en la consola interactiva) recopila muchas de estas en un conjunto de funciones bien nombradas con opciones comunes. Para una visión general, consulte help (package = “stringr”)
, pero en este capítulo cubriremos algunas de las funciones más importantes de ese paquete.
Columnas de división y encuadernación
La función str_split_fixed ()
del paquete stringr
opera en cada elemento de un vector de caracteres, dividiéndolo en pedazos basados en un patrón. Con esta función, podemos dividir cada elemento del vector expr_long$sample
en tres pedazos basados en el patrón “_”
. El “patrón” podría ser una expresión regular, usando la misma sintaxis que la utilizada por Python (y similar a la utilizada por sed).
El valor devuelto por la función str_split_fixed ()
es una matriz: como vectores, las matrices solo pueden contener un único tipo de datos (de hecho, son vectores con atributos que especifican el número de filas y columnas), pero al igual que los marcos de datos se puede acceder con [<row_selector>, <column_ selector>]
sintaxis. También pueden tener nombres de filas y columnas.
De todos modos, es probable que queramos convertir la matriz en un marco de datos usando la función data.frame ()
y asignar algunos nombres de columna razonables al resultado.
En este punto, tenemos un marco de datos expr_long
así como sample_split_df
. Estos dos tienen el mismo número de filas en un orden correspondiente, pero con columnas diferentes. Para obtener estos en un solo marco de datos, podemos usar la función cbind ()
, que une dichos marcos de datos por sus columnas, y solo funciona si contienen el mismo número de filas.
Una impresión rápida (head (expr_long_split))
nos permite saber si vamos en la dirección correcta.
En este punto, el número de columnas en el marco de datos ha crecido, por lo que print ()
ha optado por envolver la columna final alrededor de la salida impresa.
Detectando y %en%
Todavía no tenemos columnas separadas para tejido
y replicar
, pero sí tenemos esta información codificada juntas en una columna
tissue. Debido a que estos valores se codifican sin un patrón para dividirlos obviamente, str_split_fixed ()
puede no ser la solución más sencilla.
Aunque cualquier solución que suponga un conocimiento a priori de grandes contenidos de conjuntos de datos es peligrosa (ya que los valores extraños tienen formas de introducirse en conjuntos de datos), una inspección rápida de los datos revela que los tipos de tejido están codificados como A
, B
o C
, con al parecer no hay otras posibilidades. De manera similar, los números replicados son 1
, 2
y 3
.
Una función útil en el paquete stringr
detecta la presencia de un patrón en cada entrada de un vector de caracteres, devolviendo un vector lógico. Para la columna tejierep
que contiene “A1", “A3", “B1", “B2", “B3", “C1",...
, por ejemplo, str_detect (expr_long_split$tissue, “A”)
devolvería el vector lógico VERDADERO, VERDADERO, FALSO, FALSO, FALSO,...
. Así podemos comenzar creando una nueva columna de tejido
, inicialmente llena de valores de NA
.
Luego usaremos el reemplazo selectivo para llenar esta columna con el valor “A”
donde la columna tissue
tiene una “A”
como lo identifica str_detect ()
. De manera similar para “B”
y “C”
.
En el capítulo 34, “Remodelación y unión de marcos de datos”, también consideraremos métodos más avanzados para este tipo de división de columnas basada en patrones. Además, aunque estamos trabajando con columnas de marcos de datos, es importante recordar que siguen siendo vectores (existentes como columnas), y que las funciones que estamos demostrando operan principalmente sobre y devuelven vectores.
Si nuestra suposición de que “A”
, “B”
y “C”
eran los únicos tipos de tejido posibles, era correcta, no deberían quedar valores de NA
en la columna de tejido
. Deberíamos verificar esta suposición intentando imprimir todas las filas donde la columna de tejido
es NA
(usando la función is.na ()
, que devuelve un vector lógico).
En este caso, se imprime un marco de datos con cero filas. Existe la posibilidad de que tipos de tejido como “AA”
hayan sido recodificados como valores simples de “A”
usando esta técnica; para evitar este resultado, podríamos usar una expresión regular más restrictiva en str_detect ()
, como “^A\ d$”
, que solo coincidirá con elementos que comenzar con una sola “A”
seguida de un solo dígito. Consulte el capítulo 11, “Patrones (expresiones regulares)” y el capítulo 21, “Trucos bioinformáticos y expresiones regulares”, para obtener más información sobre los patrones de expresión regular.
Se puede usar un conjunto similar de comandos para llenar una nueva columna de réplica.
Nuevamente buscamos los valores de NA
sobrantes, y encontramos que esta vez hay algunas filas donde la columna rep
se reporta como NA
, al parecer porque algunas entradas en la tabla tienen un número replicado de 0
.
Hay algunas formas en las que podríamos manejar esto. Podríamos determinar cuáles deberían ser los números replicados de estas cinco muestras; quizás de alguna manera fueron mal codificadas. Segundo, podríamos agregar “0"
como una posibilidad de réplica separada (por lo que algunos grupos estaban representados por cuatro réplicas, en lugar de tres). Alternativamente, podríamos eliminar estas entradas misteriosas.
Finalmente, podríamos eliminar todas las mediciones para estos ID de genes, incluyendo las otras réplicas. Para este conjunto de datos, optaremos por este último, ya que la existencia de estas mediciones “misteriosas” pone en duda la precisión de las otras mediciones, al menos para este conjunto de cinco IDs.
Para ello, primero extraeremos un vector de los ID de genes “malos”, usando selección lógica en la columna id
basada en is.na ()
en la columna rep
.
Ahora, para cada elemento de la columna id
, ¿cuáles son iguales a uno de los elementos en el vector bad_ids
? Afortunadamente, R proporciona un operador %en%
para este tipo de comparación de muchos contra muchos. Dados dos vectores, %en%
devuelve un vector lógico que indica qué elementos del vector izquierdo coinciden con uno de los elementos de la derecha. Por ejemplo, c (3, 2, 5, 1) %en% c (1, 2) devuelve el vector lógico FALSO,
VERDADERO, FALSO, VERDADERO
. Esta operación requiere comparar cada uno de los elementos del vector izquierdo con cada uno de los elementos del vector derecho, por lo que el número de comparaciones es aproximadamente la longitud de las primeras veces la longitud del segundo. Si ambos son muy grandes, tal operación podría llevar bastante tiempo terminar.
Sin embargo, podemos usar el operador %en%
junto con la selección lógica para eliminar todas las filas que contienen un ID de gen “malo”.
En este punto, podríamos volver a verificar los valores de NA
en la columna rep
para asegurarnos de que los datos se hayan limpiado adecuadamente. Si quisiéramos, también podríamos verificar la longitud (bad_rows [bad_rows])
para ver cuántas filas malas se identificaron y eliminaron. (¿Ves por qué?)
Pegar
Si bien anteriormente discutimos dividir el contenido de los vectores de caracteres en múltiples vectores, ocasionalmente queremos hacer lo contrario: unir el contenido de los vectores de caracteres en un solo vector de carácter, elemento por elemento. La función str_c ()
de la biblioteca stringr
realiza esta tarea.
La función str_c ()
también es útil para imprimir frases muy bien formateadas para la depuración.
La función Base-R equivalente a str_c ()
es paste ()
, pero mientras que el separador predeterminado para str_c ()
es una cadena vacía, “”
, el separador predeterminado para paste ()
es un solo espacio, ""
”. La función Base-R equivalente para str_detect ()
es grepl ()
, y el equivalente más cercano a str_split_fixed ()
en Base-R es strsplit ()
. Sin embargo, como se mencionó anteriormente, se recomienda el uso de estas y otras funciones stringr
para este tipo de manipulación carácter-vector.
Factores
Por ahora, los factores se han mencionado varias veces de pasada, principalmente en el contexto del uso de StringSasFactors = FALSE
, para evitar que los vectores de caracteres se conviertan en tipos de factores cuando se crean marcos de datos. Los factores son un tipo de datos relativamente exclusivo de R, y proporcionan una alternativa para almacenar datos categóricos comparados con vectores de caracteres simples.
Un buen método para comprender los factores podría ser comprender una de las razones históricas de su desarrollo, aunque la razón ya no sea relevante hoy en día. ¿Cuánto espacio requeriría la columna de tratamiento
del marco de datos experimental anterior para almacenar en la memoria, si el almacenamiento se hacía ingenuamente? Por lo general, un solo carácter como “c”
se puede almacenar en un solo byte (8 bits, dependiendo de la codificación), por lo que una entrada como “química”
requeriría 8 bytes, y “control”
requeriría 7. Dado que hay ~360.000 entradas en la tabla completa, el espacio total requerido sería de ~0.36 megabytes. Obviamente, la cantidad de espacio sería mayor para una tabla más grande, y hace décadas incluso unos pocos megabytes de datos podrían representar un desafío.
Pero eso es para una codificación ingenua de los datos. Una alternativa podría ser codificar “químico”
y “control”
como enteros simples 1
y 2
(4 bytes pueden codificar números enteros de -2.1 a 2.1 mil millones), así como una tabla de búsqueda separada que mapee el entero 1
a “químico”
y 2
a “controlar”
. Esto sería un ahorro de espacio de aproximadamente dos veces, o más si los plazos fueran más largos. Este tipo de mecanismo de almacenamiento y mapeo es exactamente lo que proporcionan los factores. [1]
Podemos convertir un vector (o factor) de caracteres en un factor usando la función factor ()
, y como de costumbre la función head ()
se puede utilizar para extraer los primeros elementos.
Cuando se imprimen, los factores muestran sus niveles
, así como los elementos de datos individuales codificados a niveles. Observe que no se muestran las comillas generalmente asociadas con vectores de caracteres.
Es ilustrativo intentar usar las funciones str ()
y class ()
y attr ()
para profundizar en cómo se almacenan los factores. ¿Son listas, como los resultados de la función t.test ()
, o algo más? Desafortunadamente, son relativamente inmunes a la función str ()
; str (factor_tratamiento)
informa:
Este resultado ilustra que los datos parecen estar codificados como enteros. Si fuéramos a ejecutar print (class (treatment_factor))
, descubriríamos que su clase es “factor”
.
Resulta que la clase de un tipo de datos se almacena como un atributo.
Arriba, aprendimos que podíamos eliminar un atributo configurándolo en NULL
. Vamos a establecer el atributo “class”
en NULL
, y luego ejecutar str ()
en él.
¡Ajá! Esta operación revela la verdadera naturaleza de un factor: un vector entero, con un atributo de “niveles”
que almacena un vector de caracteres de etiquetas, y un atributo para “clase”
que especifica la clase del vector como factor. Los datos en sí se almacenan como 1
o 2
, pero el atributo levels tiene como primer elemento “químico”
(y por lo tanto un entero de 1
codifica “químico”
) y “control”
como su segundo (entonces 2
codifica “control”
).
Este atributo especial de “clase”
controla cómo funcionan funciones como str ()
e print ()
en un objeto, y si queremos cambiarlo, esto se hace mejor usando la función de acceso class ()
en lugar de la función attr ()
como se indicó anteriormente. Cambiemos la clase de nuevo a factor.
Renombrar niveles de factor
Debido a que los niveles se almacenan como un atributo de los datos, podemos cambiar fácilmente los nombres de los niveles modificando el atributo. Podemos hacer esto con la función attr ()
, pero como de costumbre, se prefiere una función de acceso específica llamada levels ()
.
¿Por qué se prefiere la función levels ()
sobre usar attr ()
? Porque al usar attr ()
, no habría nada que nos impida hacer algo irresponsable, como fijar los niveles a valores idénticos, como en c (“Agua”, “Agua”)
. La función levels ()
comprobará esto y otros absurdos.
Lo que la función levels ()
no puede verificar, sin embargo, es el significado semántico de los propios niveles. No sería buena idea mezclar los nombres, para que “Química”
en realidad se estuviera refiriendo a plantas tratadas con agua, y viceversa:
La razón por la que esto es una mala idea es que usar levels ()
solo modifica el atributo “levels”
pero no hace nada a los datos enteros subyacentes, rompiendo el mapeo.
Niveles de factor de reordenamiento
Aunque motivamos factores sobre la base del ahorro de memoria, en las versiones modernas de R, incluso los vectores de caracteres se almacenan internamente usando una estrategia sofisticada, y las computadoras modernas generalmente tienen almacenes más grandes de RAM además. Aún así, hay otra motivación para los factores: el hecho de que los niveles puedan tener un orden significativo. Algunas pruebas estadísticas podrían querer comparar ciertos subconjuntos de un marco de datos definidos por un factor; por ejemplo, los valores numéricos asociados con niveles de factor “bajos”
podrían compararse con los etiquetados como “medio”
, y esos a su vez deberían compararse con valores etiquetados como “altos”
. Pero, dadas estas etiquetas, no tiene sentido comparar las lecturas “bajas”
directamente con las lecturas “altas”
. Los factores proporcionan una manera de especificar que los datos son categóricos, pero también que “bajo” < “medio” < “alto”
.
Así podríamos precisar o cambiar el orden de los niveles dentro de un factor, para decir, por ejemplo, que el tratamiento del “Agua”
es de alguna manera menor que el tratamiento “Químico”
. Pero no podemos hacer esto simplemente cambiando el nombre de los niveles.
La forma más sencilla de especificar el orden de un vector o factor de carácter es convertirlo en un factor con factor ()
(aunque ya sea un factor) y especificar el orden de los niveles con el parámetro opcional levels =
. Por lo general, si se está suministrando un pedido específico, también vamos a querer especificar ordenado = VERDADERO
. Los niveles especificados por el parámetro levels =
deben coincidir con las entradas existentes. Para renombrar simultáneamente los niveles, también se puede usar el parámetro labels =
.
Ahora, se usa “Agua”
en lugar de “control”
, y el factor sabe que “Agua” < “Químico”
. Si quisiéramos tener “Químico” < “Agua”
, habríamos necesitado usar niveles = c (“químico”, “control”)
y etiquetas = c (“Química”, “Agua”)
en la llamada al factor ()
.
Sin tener en cuenta el argumento labels =
(usado solo cuando queremos renombrar niveles mientras se reordena), debido a que el argumento levels =
toma un vector de caracteres de las entradas únicas en el vector de entrada, estas podrían precalcularse para mantener los niveles en un orden dado. Quizás nos gustaría ordenar los tipos de tejido en orden alfabético inverso, por ejemplo:
En lugar de asignar a una variable tejes_factor
separada, podríamos reemplazar la columna de marco de datos con el vector ordenado asignando a expr_long_split$tissue
.
A menudo deseamos ordenar los niveles de un factor de acuerdo con algunos otros datos. En nuestro ejemplo, podríamos querer que el “primer” tipo de tejido sea el que tenga la expresión media más pequeña, y el último sea el que tenga la expresión media más alta. Una función especializada, reorder ()
, hace que este tipo de pedidos sea rápido y relativamente indoloro. Se necesitan tres parámetros importantes (entre otros opcionales):
- El factor o vector de caracteres para convertir a un factor con niveles reordenados.
- Un vector (generalmente numérico) de la misma longitud para usar como reordenamiento de datos.
- Una función a utilizar para determinar qué hacer con el argumento 2.
He aquí un ejemplo canónico rápido. Supongamos que tenemos dos vectores (o columnas en un marco de datos), uno de especies de peces muestreados (“lubina”, “salmón” o “trucha”) y otro de pesos correspondientes. Observe que los salmones son generalmente pesados, las truchas son livianas y los bajos están en el medio.
Si tuviéramos que convertir el vector especie en un factor con factor (especie)
, obtendríamos el orden alfabético predeterminado: lubina, salmón, trucha. Si preferimos organizar los niveles de acuerdo con la media de los pesos de grupo, podemos usar reorder ()
:
Con esta asignación, species_factor
será un factor ordenado con trucha < lubina < salmón
. Esta pequeña línea de código hace bastante, en realidad. Ejecuta la función mean ()
en cada grupo de pesos definidos por las diferentes etiquetas de especies, ordena los resultados por esos medios y utiliza el orden de grupo correspondiente para establecer los niveles de factores. Lo que es aún más impresionante es que podríamos haber usado con la misma facilidad la mediana
en lugar de la media, o cualquier otra función que opere sobre un vector numérico para producir un resumen numérico. Esta idea de especificar funciones como parámetros en otras funciones es uno de los poderosos enfoques “funcionales” que toma R, y estaremos viendo más de ella.
Notas finales sobre los factores
En muchos sentidos, los factores funcionan de manera muy similar a los vectores de caracteres y viceversa; %en%
y ==
pueden usarse para comparar elementos de factores tal como pueden con vectores de caracteres (por ejemplo, tejidos_factor == “A”
devuelve el vector lógico esperado).
Los factores obligan a que todos los elementos sean tratados como uno de los niveles nombrados en el atributo levels
, o NA
en caso contrario. Un factor que codifica 1 y 2 como macho
y hembra
, por ejemplo, tratará a cualquier otro entero subyacente como NA
. Para obtener un factor que acepte niveles novedosos, el atributo levels primero debe modificarse con la función levels ()
.
Por último, debido a que los factores funcionan de manera muy parecida a los vectores de caracteres, pero no imprimen sus citas, puede ser difícil distinguirlas de otros tipos cuando se imprimen. Esto va para vectores de caracteres simples cuando forman parte de marcos de datos. Considere la siguiente impresión de un marco de datos:
Debido a que las comillas se dejan fuera al imprimir marcos de datos, es imposible decir a partir de esta salida simple que la columna id
es un vector de caracteres, la columna de tejido
es un factor, la columna de conteo
es un vector entero y la columna de grupo
es un factor. [2] Usar class ()
en columnas individuales de un marco de datos puede ser útil para determinar qué tipos son realmente.
Ejercicios
- En el archivo de anotación
pz.ANNOT.txt
, cada ID de secuencia (columna 1) puede asociarse con múltiples “números” de ontología génica (GO) (columna 2) y un número de “términos” diferentes (columna 3). Muchos ID están asociados con múltiples números GO, y no hay nada que impida que un número o término en particular se asocie con múltiples ID. Si bien la mayoría de los ID de secuencia tienen un sufijo de guión bajo, no todos lo tienen. Comience leyendo en este archivo (las columnas están separadas por tabulaciones) y luego extrayendo un marco de datossuffix_only
que contiene solo aquellas filas donde el ID de secuencia contiene un guion bajo. Del mismo modo, extraiga un marco de datosno_suffix
para filas donde los ID de secuencia no contengan un guion bajo.A continuación, agregue a las columnas del marco de datos
sufix_only
para base_id
ysufijo
, donde los ID base son las partes antes del subrayado y suficientes son las partes después del subrayado (por ejemplo,base_id
es“PZ7180000023260"
y elsufijo
es“APN”
para el ID“PZ7180000023260_APN”
).Por último, producir versiones de estos dos marcos de datos donde se haya eliminado el prefijo
GO:
de todas las entradas de la segunda columna. - La línea
s <- muestra (c (“0", “1", “2", “3", “4"), size = 100, replace = TRUE)
genera un vector de caracteres de 100 aleatorios“0"
s,“1"
s,“2"
s,“3"
s, y“4"
s.
Supongamos que“0"
significa “Totalmente en desacuerdo”,“1"
significa “No estoy de acuerdo”,“2"
significa “Neutral”,“3"
significa “De acuerdo” y“4"
significa “Muy de acuerdo”. Convertirs
en un factor ordenado con nivelesMuy en desacuerdo < En desacuerdo < Neutral < De acuerdo < Muy de acuerdo
. - Al igual que los vectores, los marcos de datos (tanto filas como columnas) se pueden seleccionar por número de índice (vector numérico), vector lógico o nombre (vector de caracteres). Supongamos que
grade_current
es un vector de caracteres generado porgrade_current <- sample (c (“A”, “B”, “C”, “D”, “E”), size = 100, replace = TRUE)
, ygpa
es un vector numérico, como engpa <- runif (100, min = 0.0, max = 4.0)
. Además, los agregamos como columnas a un marco de datos,grades <- data.frame (current_grade, gpa, stringsasFactors = FALSE)
.Nos interesa sacar todas las filas que tengan
“A”
,“B”
o“C”
en la columnacurrent_grade
. Describa, en detalle, qué hace cada una de las tres posibles soluciones: ¿Cómo interpreta R cada una (es decir, qué intentará hacer R por cada una) y cuál sería el resultado? ¿Cuál (s) es (son) correctos? ¿Cuál reportará errores? ¿Las siguientes tres líneas son diferentes de las tres anteriores en lo que R intenta hacer?
- Aunque los vectores de caracteres también se almacenan de manera eficiente en las versiones actuales de R, los factores aún tienen usos únicos.
- Los factores creados a partir de vectores enteros son un tipo especial de dolor de cabeza. Considera una línea como
h <- factor (c (4, 1, 5, 6, 4))
; debido a que los factores son tratados como tipos de caracteres, éste se convertiría de manera que se basa enc (“4", “1", “5", “6", “4")
, donde el mapeo subyacente y el almacenamiento tienen una relación casi arbitraria con los elementos. Pruebaprint (como.numeric (h))
para ver el problema en el que uno puede meterse al mezclar estos tipos, así comoclass (h) <- NULL
seguido destr (h)
para ver el mapeo subyacente y por qué ocurre esto.