Manejo de datos categóricos con R

Parte 1:
Formatos de organización y Tablas de contigencia

Pablo Andrés Guzmán
Taller R y Rmarkdown

Marzo 2022


En este recurso revisaremos cuales son las diferentes formas de organización que pueden tener conjuntos de datos dominados por variables categóricas, como se visualizan en R y como podemos, a partir de dichos formatos, generar conteos o tablas de contingencia en R.


Datos de prueba

Tipos de Sangre

Carmona-Fonseca (2006) reporta datos de tipos sanguíneos para una muestra de 827 trabajadores afiliados al Seguro Social en el valle de Aburrá y en el cercano oriente antioqueño. Su objetivo fue aportar al conocimiento de la distribución de los tipos sanguíneos en la región. Los datos se encuentran el archivo carmonaABO.csv en el cual cada fila es un sujeto y cada columna es una variable y las columnas están separadas por una coma. Importe los datos con el siguiente código:

carmona <- read.csv(file = "carmonaABO.csv")   # se importan los datos
str(carmona)                                   # se imprime su estructura
'data.frame':   827 obs. of  4 variables:
 $ sujeto: int  1 2 3 4 5 6 7 8 9 10 ...
 $ region: chr  "aburra" "aburra" "aburra" "aburra" ...
 $ rh    : chr  "pos" "pos" "pos" "pos" ...
 $ abo   : chr  "O" "O" "A" "O" ...

Plantas raras

Un estudio citado por Gotelli & Ellison (2013) pretendió evaluar factores asociados con el estatus de conservación de 73 poblaciones de plantas raras en Nueva Inglaterra. A cada población se le identificó si estaba declinando en tamaño y si estaba legalmente protegida entre otras variables. Los datos se encuentran en el archivo rare_plants_agrup.txt donde cada columna es una variable y cada fila es una combinación de categorías de las variables observadas. Las columnas están separadas por un espacio sencillo.

plantas <- read.csv(file = "rare_plants_agrup.txt", sep = "")   # se importa el archivo
str(plantas)                                                    # se imprime su estructura
'data.frame':   40 obs. of  5 variables:
 $ decline  : chr  "yes" "yes" "yes" "yes" ...
 $ invaded  : chr  "no" "no" "no" "no" ...
 $ protected: chr  "no" "no" "no" "no" ...
 $ light    : int  0 1 2 3 4 0 1 2 3 4 ...
 $ frequency: int  2 0 2 2 0 1 0 0 7 4 ...

Formatos de organización

Un conjunto de datos con dominancia de variables categóricas puede presentarse bajo tres formatos de organización:

Datos crudos

Donde cada fila es un sujeto (o un registro). De esta manera se encuentran los datos descargados del archivo carmonaABO.csv.

head(carmona, 3)   # tres primeras filas del data.frame carmona
  sujeto region  rh abo
1      1 aburra pos   O
2      2 aburra pos   O
3      3 aburra pos   A

Observe que tenemos una columna que indica el sujeto. En este formato, la cantidad total de sujetos se corresponde con el número de filas de la tabla.

Datos agrupados en formato largo

Donde cada fila es una combinación de categorías de las variables observadas. Debe existir una columna que indica la frecuencia de cada combinación. Los datos del archivo rare_plants_agrup.txt están bajo este formato:

head(plantas, 3)   # tres primeras filas del data.frame plantas
  decline invaded protected light frequency
1     yes      no        no     0         2
2     yes      no        no     1         0
3     yes      no        no     2         2

Note que en este problema el “sujeto” es una población de plantas de modo que existen 73 “sujetos” . Sin embargo, cada fila en la tabla plantas no significa una población o “sujeto” si no una combinación de categorías de las variables observadas a cada población. La columna frequency representa la cantidad de “sujetos” o poblaciones con dicha combinación. Por ejemplo, de la 1era. fila concluimos que existen 2 poblaciones (de las 73 en total) que tienen decline = yes; invaded = no; protected = no y light = 0. Así, la cantidad total de “sujetos” en este formato corresponde la suma de la columna de frecuencia.

Datos agrupados en una tabla de contingencia

En este caso el conteo para cada combinación de categorías se organiza en una tabla de contingencia. Esta tabla puede ser plana como se muestra enseguida para los datos de carmona:

        abo   A      AB       B       O    
        rh  neg pos neg pos neg pos neg pos
region                                     
aburra       15 118   1   7   6  34  29 205
oriente      10 118   0   3   1  20  31 229

Pero también puede ser una tabla de mutiples dimensiones (una dimensión por cada variable). Los datos de carmona tienen tres variables que generarían una tabla de contingencia de tres dimensiones, por ejemplo de 2 (rh) x 4 (abo) x 2 (regiones), como se observa enseguida:

, , region = aburra

     abo
rh      A  AB   B   O
  neg  15   1   6  29
  pos 118   7  34 205

, , region = oriente

     abo
rh      A  AB   B   O
  neg  10   0   1  31
  pos 118   3  20 229

Generando conteos y tablas de contigencia

El análisis inicial con variables categóricas es generar conteos (frec. absolutas) de sus categorías. Estos conteos se organizan tipicamente en tablas de contigencia (planas o multidimensionales). Algunos comandos de R que permiten hacer esto son xtabs y ftable.

Usando xtabs

El comando xtabs permite generar tablas de conteos de una o más dimensiones usando la dupla de argumentos formula y data. Aquí algunos ejemplos desde los datos crudos del data.frame carmona:

xtabs(~ region, data = carmona)             # tabla de conteos de una variable
region
 aburra oriente 
    415     412 
xtabs(~ region + abo, data = carmona)       # tabla de conteos de dos variables
         abo
region      A  AB   B   O
  aburra  133   8  40 234
  oriente 128   3  21 260
xtabs(~ rh + abo + region, data = carmona)  # tabla de conteos de tres variables
, , region = aburra

     abo
rh      A  AB   B   O
  neg  15   1   6  29
  pos 118   7  34 205

, , region = oriente

     abo
rh      A  AB   B   O
  neg  10   0   1  31
  pos 118   3  20 229

Desde datos agrupados en formato largo también se puede usar xtabs. Por ejemplo, una tabla de contigencia entre las variables protected y decline del data.frame plantas se obtiene como:

xtabs(frequency ~ protected + decline, data = plantas)
         decline
protected no yes
      no  15  18
      yes 32   8

En el comando xtabs, el orden de las variables ubicadas en la fórmula a la derecha de la virgulilla (~) equivale al orden en el cual se acomodarán las variables en las dimensiones del arreglo generado por xtabs. La 1era. variable en la fórmula se ubicará en al 1era. dimensión (filas), la 2da. variable en la fórmula quedará en la 2da. dimensión (columnas), y así sucesivamente.

Usando xtabs y as.data.frame

El comando as.data.frame convierte el objeto que entrega xtabs en un data.frame con los datos agrupados en formato largo:

f <-  xtabs(frequency ~ protected + decline, data = plantas)
as.data.frame(f)
  protected decline Freq
1        no      no   15
2       yes      no   32
3        no     yes   18
4       yes     yes    8

Por defecto, el comando as.data.frame crea una columna llamada Freq con los conteos para cada combinación de categorías de las variables en la tabla de contigencia.

Agregando totales marginales

El comando addmargins recibe el objeto que entrega xtabs y adiciona totales en las margenes1 de la tabla:

f <- xtabs(~ region, data = carmona)      # tabla de conteos de una variable
addmargins(f)   # agregando totales en las margenes
region
 aburra oriente     Sum 
    415     412     827 
f <- xtabs(~ rh + abo, data = carmona)    # tabla de conteos de dos variables
addmargins(f)   # agregando totales en las margenes
     abo
rh      A  AB   B   O Sum
  neg  25   1   7  60  93
  pos 236  10  54 434 734
  Sum 261  11  61 494 827

Calculando proporciones

El comando prop.table recibe el objeto que entrega xtabs y convierte los conteos (frec. absolutas) en frecuencias relativas o proporciones:

f <- xtabs(~ rh + abo, data = carmona)       # tabla de conteos de una variable
prop.table(f)    # calculando proporciones (divide por el gran total)
     abo
rh              A          AB           B           O
  neg 0.030229746 0.001209190 0.008464329 0.072551391
  pos 0.285368803 0.012091898 0.065296252 0.524788392

Por defecto el comando prop.table divide por el gran total. Sin embargo le podemos indicar que genere las proporciones dividiendo por el total de filas:

prop.table(f, margin = 1)   # proporciones por el total de fila
     abo
rh             A         AB          B          O
  neg 0.26881720 0.01075269 0.07526882 0.64516129
  pos 0.32152589 0.01362398 0.07356948 0.59128065

O que produzca las proporciones dividiendo por el total de columna2:

prop.table(f, margin = 2)   # proporciones por el total de columna
     abo
rh             A         AB          B          O
  neg 0.09578544 0.09090909 0.11475410 0.12145749
  pos 0.90421456 0.90909091 0.88524590 0.87854251

De manera opcional, use el comando round para redonder las proporciones a la cantidad de decimales deseada y puede multiplicar por 100 el objeto que entrega prop.table para ver los resultados como porcentajes.

Usando ftable

El comando ftable produce tablas de contigencia planas desde un data.frame con datos crudos o desde objetos generados por el comando xtabs. En ftable también se utiliza la dupla de argumentos de formula y data, si embargo, la aplicación de la fórmula es diferente a xtabs. Considere el siguiente ejemplo:

# Tabla de contingencia plana desde datos crudos:
ftable(abo ~ region + rh, data = carmona)
            abo   A  AB   B   O
region  rh                     
aburra  neg      15   1   6  29
        pos     118   7  34 205
oriente neg      10   0   1  31
        pos     118   3  20 229

En la fórmula de ftable, las categorías de las variables ubicadas a la izquierda de la virgulilla (~) quedan en las columnas de la tabla de contingencia, mientras que las categorías de las variables a la derecha de la virgulilla (~) conformarán las filas de la tabla de contingencia.

El comando ftable también crea una tabla plana desde un objeto creado por xtabs, es decir, desde una tabla de múltiples dimensiones. Considere el siguiente ejemplo:

# Se guarda tabla de tres dimensiones
f <- xtabs(~ rh + abo + region, data = carmona)   

# Se genera tabla de contingencia plana desde tabla de tres dimensiones
ftable(abo ~ region + rh, data = f)
            abo   A  AB   B   O
region  rh                     
aburra  neg      15   1   6  29
        pos     118   7  34 205
oriente neg      10   0   1  31
        pos     118   3  20 229

Resumen de comandos

La siguiente tabla presenta una lista de los comandos revisados arriba.

Comando Paquete Requiere instalación Descripción
xtabs stats no Genera tablas de contingencia multidimensionales mediante la dupla de argumentos: formula y data. Requiere datos crudos o agrupados en formato largo. Entrega un objeto de clase table.
ftable stats no Genera tablas de contingencia planas mediante la dupla de argumentos: formula y data. Requiere datos crudos o agrupados en tablas multidimensionales. Entrega un objeto de clase ftable.
addmargins stats no Agrega totales en las margenes de una tabla multidimensional
as.data.frame base no Convierte una tabla multidimensional en un data.frame con los datos agrupados en formato largo.
prop.table base no Cálcula proporciones desde una tabla multidimensional. Puede usar como denominador el gran total, o el total de alguna de las dimensiones de la tabla.

Ejercicios

  1. Escriba código R que le permita, a partir de los datos del data.frame carmona:

    1. generar una tabla de conteos de la region (filas) contra el rh (columnas)
    2. convertir la tabla generada en (a) en una de proporciones donde el divisor sea el total por region.
  2. Escriba código R que le permita, a partir del data.frame plantas:

    1. Crear una arreglo de conteos de tres dimensiones con las siguientes variables: invaded, decline y protected en dicho orden.
    2. Convertir el arreglo obtenido en (a) en datos agrupados en formato largo.
    3. Exportar los datos generados en (b) a un archivo separado por comas.
  3. El archivo artritisFa.csv contiene los datos de un ensayo clínico donde participaron un total de 84 sujetos. En el ensayo se comparó un tratamiento contra la artritis con un placebo. La respuesta o desenlace es una variable categórica ordinal llamada Improved (“mejora”). Se cuenta, además, con la edad y el género. Los datos están agrupados en formato largo. Importe el archivo a R y realice las siguientes actividades:

    1. Realiza una tabla de contingencia de dos dimensiones que compare las categorías de la respuesta entre los dos tratamientos. ¿Cuántos sujetos mostraron ninguna mejora y estuvieron en el grupo placebo?
    2. Use la tabla generada en (a) para estimar la probabilidad de observar cada una de las categorías de la respuesta condicionado al tratamiento. ¿Existe mayor probabilidad de presentar una mejora marcada si se es tratado que si no? Explique.
  4. En un artículo reportan una tabla de contigencia que relaciona los tipos de transtornos del sueño y el género en 165 estudiantes de medicina. Vaya a la Tabla 2 del artículo y escriba código R que le permita digitar los datos en dicha tabla de manera agrupada en formato largo. Luego use el comando xtabs para crear la tabla mostrada en el artículo.

Referencias

Carmona-Fonseca, J. (2006). Frecuencia de los grupos sanguı́neos ABO y Rh en la población laboral del valle de Aburrá y del cercano oriente de Antioquia (Colombia). Acta médica Colombiana, 31(1), 20–30.
Gotelli, N., & Ellison, A. (2013). A primer of ecological statistics (2nd ed.). Sinauer Associates, Inc.
Zar, J. H. (2014). Biostatistical analysis (5th ed.). Pearson, Harlow.



Para saber más

Filtrar antes de contar con xtabs

El comando xtabs tiene un tercer argumento, subset, que permite aplicar un filtro lógico a las filas del data.frame antes de realizar el conteo. Por ejemplo, considere el siguiente ejemplo:

# Tabla de conteos entre 'protected' y 'decline' para cada nivel de 'invaded'.
# Resulta una tabla de tres dimensiones:
xtabs(frequency ~ protected + decline + invaded, data = plantas)
, , invaded = no

         decline
protected no yes
      no   6   6
      yes 20   3

, , invaded = yes

         decline
protected no yes
      no   9  12
      yes 12   5
# Tabla de conteos entre 'protected' y 'decline' solo para el 
# nivel 'yes' de 'invaded'. Resulta una tabla de dos dimensiones:
xtabs(frequency ~ protected + decline, data = plantas, subset = invaded == "yes")
         decline
protected no yes
      no   9  12
      yes 12   5

Desde datos agrupados a datos crudos

Para convertir datos agrupados en formato largo a datos crudos podemos usar el comando expand.dft o expand.table (paquete: vcdExtra). En el ejemplo siguiente pasamos la tabla plantas a un data.frame como datos crudos:

library(vcdExtra)   # se carga la libreria (debe verificar que la tenga instalada)
plantas0 <- expand.dft(x = plantas, freq = "frequency")   # se hace la conversion
str(plantas0)       # se observa la estructura de la nueva tabla para verificar
'data.frame':   73 obs. of  4 variables:
 $ decline  : Factor w/ 2 levels "no","yes": 2 2 2 2 2 2 2 2 2 2 ...
 $ invaded  : Factor w/ 2 levels "no","yes": 1 1 1 1 1 1 2 2 2 2 ...
 $ protected: Factor w/ 2 levels "no","yes": 1 1 1 1 1 1 1 1 1 1 ...
 $ light    : int  0 0 2 2 3 3 0 3 3 3 ...

En el comando expand.dft, el argumento freq es una cadena de texto que indica el nombre de la columna que contiene la frecuencia de cada fila en la tabla. Observe que en la nueva tabla plantas0, cada fila representa un “sujeto,” en este caso, una población de plantas. ¿Porqué lo podemos saber?

El comando expand.dft también recibe una tabla de contingencia generada, por ejemplo, desde xtabs. Considere el siguiente ejemplo:

f <- xtabs(~ rh + abo + region, data = carmona)  # tabla de contiengencia de tres dimensiones
carmona0 <- expand.dft(x = f)                    # se hace la conversion
str(carmona0)                                    # se revisa la estructura de la nueva tabla
'data.frame':   827 obs. of  3 variables:
 $ rh    : Factor w/ 2 levels "neg","pos": 1 1 1 1 1 1 1 1 1 1 ...
 $ abo   : Factor w/ 4 levels "A","AB","B","O": 1 1 1 1 1 1 1 1 1 1 ...
 $ region: Factor w/ 2 levels "aburra","oriente": 1 1 1 1 1 1 1 1 1 1 ...

Entrando desde teclado una tabla de contingencia

Para digitar una tabla de contingencia directamente, entramos los conteos en una matriz o arreglo y asignamos nombres a cada categoría y variable usando el comando matrix (si hay dos variables) o el comando array (si hay dos o más variables).

Ejemplo: Considere el siguiente ejemplo presentado por Zar (2014)

Suponga que usted quiere digitar la tabla de contingencia propuesta. El siguiente código permite realizar la entrada:

# Entrando tabla de contingencia de 2 x 2 x 2 desde teclado:
zar8 <- array(
  data = c(44,28,12,22, 38,20,10,18), 
  dim  = c(2,2,2), 
  dimnames = list(
    sp      = c("sp1", "sp2"),
    loc     = c("loc1", "loc2"),
    disease = c("present", "absent")
  )
)

# Se asigna la clase `table` al arreglo creado
zar8 <- as.table(zar8)

El último paso donde se actualiza la clase del arreglo creado es importante para garantizar que otros comandos que operan sobre objetos de clase table o xtabs pueden funcionar. Ahora imprimamos la tabla o arreglo para verificar la digitación:

addmargins(zar8, margin = 1:2)
, , disease = present

     loc
sp    loc1 loc2 Sum
  sp1   44   12  56
  sp2   28   22  50
  Sum   72   34 106

, , disease = absent

     loc
sp    loc1 loc2 Sum
  sp1   38   10  48
  sp2   20   18  38
  Sum   58   28  86

Reordenando o colapsando dimensiones

Considere la tabla (o arreglo) de contingencia zar8 de tres dimensiones, 2 x 2 x 2, creada en la sección anterior. Suponga que quisieramos colapsar la tabla zar8 para explorar sólo la relación entre la especie y la enfermedad. Para esto usamos el comando margin.table:

# Colapsando (sumando) categorias de la variable en la dimension 2
zar8.sp.d <- margin.table(x = zar8, margin = c(1,3))
zar8.sp.d
     disease
sp    present absent
  sp1      56     48
  sp2      50     38

El argumento margin del comando margin.table permite indicar cuales dimensiones se quiere que permanezcan. Aquellas dimensiones ausentes en este argumento serán colapsadas (sumadas) y desaparecerán en el arreglo resultante.

Al argumento margin también le podemos indicar todas las dimensiones, pero en un nuevo orden. En este caso, el arreglo resultante tendrá el nuevo orden en sus dimensiones:

# Cambiando el orden de las dimensiones del arreglo original
# La variable en la dimension 3 se pasa a la dimension 2 y viceversa
zar8r <- margin.table(x = zar8, margin = c(1,3,2))
zar8r
, , loc = loc1

     disease
sp    present absent
  sp1      44     38
  sp2      28     20

, , loc = loc2

     disease
sp    present absent
  sp1      12     10
  sp2      22     18

  1. En estadística, a estos totales en las margenes se les llama la distribución marginal de la variable en cada margen (o dimensión) de la tabla↩︎

  2. Cuando se divide por el total de fila o de columna estamos calculando una proporción condicional, es decir una proporción de la forma \(p(\text{A dado B}) = p(A | B)\)↩︎