Tablas cruzadas y chi-cuadrado

r
estadistica
Tests sobre datos categóricos. table() y prop.table() con márgenes, chisq.test() y la regla del 80%/5, fisher.test() para frecuencias bajas y McNemar para datos pareados.

¿Por qué chi-cuadrado?

Cuando los datos son categóricos (sexo, región, sí/no, recuperado/no-recuperado), las herramientas anteriores no aplican. No hay medias que comparar. La pregunta cambia a “¿están las dos variables asociadas?”. El test estándar es el chi-cuadrado de independencia.

Tablas cruzadas: table() y prop.table()

El primer paso es siempre la tabla cruzada. R lo hace con table():

# Dataset interno: supervivencia en el Titanic
tabla <- with(as.data.frame(Titanic), table(Class, Survived))
tabla
#>        Survived
#> Class    No  Yes
#>   1st   122  203
#>   2nd   167  118
#>   3rd   528  178
#>   Crew  673  212

Para ver porcentajes en lugar de conteos, prop.table():

prop.table(tabla, margin = 1)   # porcentajes por FILA (clase)
#>        Survived
#> Class          No       Yes
#>   1st   0.3753846 0.6246154
#>   2nd   0.5859649 0.4140351
#>   3rd   0.7478754 0.2521246
#>   Crew  0.7604520 0.2395480

margin = 1 normaliza por filas (lectura: “de la clase 1ª, el 62 % sobrevivió”). margin = 2 normaliza por columnas. Sin margin, normaliza por el total.

Para reportar con miles de filas y márgenes ya formateados, janitor::tabyl() es más legible:

library(janitor)
library(dplyr)

as.data.frame(Titanic) |>
  uncount(Freq) |>
  tabyl(Class, Survived) |>
  adorn_totals(c("row", "col")) |>
  adorn_percentages("row") |>
  adorn_pct_formatting(digits = 1) |>
  adorn_ns()

Una tabla pulida directamente para informe.

chisq.test(): el test canónico

chisq.test(tabla)
#>
#>  Pearson's Chi-squared test
#>
#> data:  tabla
#> X-squared = 190.4, df = 3, p-value < 2.2e-16

Cómo funciona conceptualmente: el test compara las frecuencias observadas con las esperadas si las dos variables fueran independientes. Si la discrepancia es grande, rechaza la independencia.

test <- chisq.test(tabla)
test$expected   # las frecuencias esperadas bajo independencia
test$residuals  # residuos de Pearson (cuánto se aleja cada celda)

Los residuos de Pearson ((observado - esperado) / sqrt(esperado)) son muy útiles para identificar qué celdas contribuyen más al rechazo. Valores > |2| señalan asociaciones notables.

La regla del 80 % / 5 y por qué importa

chisq.test() asume que la mayoría de las frecuencias esperadas son ≥ 5. La regla habitual:

  • ≥ 80 % de las celdas con frecuencia esperada ≥ 5.
  • Ninguna celda con frecuencia esperada < 1.

Cuando esto se viola, el test de chi-cuadrado da p-values poco fiables. R te avisa con un warning:

Warning message:
In chisq.test(tabla) : Chi-squared approximation may be incorrect

Léelo. No es decoración. Si lo ves, mira test$expected y verifica.

Fisher exact: cuando las frecuencias son bajas

Para tablas 2×2 con celdas pequeñas, fisher.test() es la alternativa exacta:

# Tabla 2x2 con frecuencias bajas
tabla_pequena <- matrix(c(8, 2, 1, 5), nrow = 2,
                        dimnames = list(c("A", "B"), c("Sí", "No")))
tabla_pequena

fisher.test(tabla_pequena)

Ventajas:

  • No depende de aproximación asintótica: calcula el p-value exacto.
  • Funciona con cualquier tamaño muestral, incluso con frecuencias muy bajas.
  • Devuelve odds ratio con IC en tablas 2×2, lo cual es directamente reportable.

Desventajas: computacionalmente caro en tablas grandes (> 2x2), aunque R simula cuando hace falta.

Regla práctica: si la tabla es 2×2 y hay alguna celda esperada < 5, usa Fisher. Para tablas más grandes con problemas de frecuencia, considera agrupar categorías o usar simulación: chisq.test(tabla, simulate.p.value = TRUE).

McNemar: para datos pareados binarios

Si tus datos categóricos son pareados (mismos individuos antes/después, casos vs controles emparejados), el chi-cuadrado normal no es válido. Necesitas McNemar:

# Mismo paciente medido antes y después del tratamiento (sí/no recuperado)
tabla_mcnemar <- matrix(c(30, 15, 5, 50), nrow = 2,
                        dimnames = list(Antes = c("No", "Sí"),
                                        Despues = c("No", "Sí")))
tabla_mcnemar

mcnemar.test(tabla_mcnemar)

McNemar mira solo las celdas off-diagonal, los individuos que cambiaron de estado. Si los que mejoraron son aproximadamente iguales a los que empeoraron, no hay efecto. Si hay desbalance claro, lo detecta.

Visualización: mosaicos

Para tablas grandes, una visualización de mosaico ayuda a ver patrones:

mosaicplot(tabla, color = TRUE, main = "Supervivencia por clase (Titanic)")

O con ggplot2 + el paquete ggmosaic:

# install.packages("ggmosaic")
library(ggmosaic)
library(ggplot2)

as.data.frame(Titanic) |>
  ggplot() +
  geom_mosaic(aes(weight = Freq, x = product(Class), fill = Survived)) +
  labs(title = "Supervivencia por clase")

Los anchos de las columnas reflejan tamaños de grupo. Las áreas de color reflejan proporciones.

Trampas habituales

  • Ignorar el warning de aproximación incorrecta. R te avisa cuando las frecuencias esperadas son demasiado bajas. No filtres los warnings, léelos. Usa Fisher si la advertencia aparece en tabla 2×2.
  • Aplicar chi-cuadrado a datos pareados. El test ignora la estructura pareada y reporta un p-value menos potente. Si el mismo individuo aparece dos veces (medidas repetidas), usa McNemar.
  • Reportar solo el p-value. Una tabla cruzada con porcentajes por fila (prop.table(margin = 1)) y el chi-cuadrado juntos es lo que comunica el hallazgo. El p-value sin la tabla es opaco.
  • chisq.test() con un vector en lugar de tabla. chisq.test(c(10, 20, 30)) ejecuta un test de bondad de ajuste (¿se ajustan estos conteos a la distribución uniforme?). Distinto del test de independencia. Si tu objetivo es el segundo, asegúrate de pasar una matriz o table().

En la siguiente entrega

Has aprendido a testar asociación entre variables categóricas. La siguiente entrega cierra el bloque 2 con la correlación: cómo medir la asociación entre dos variables numéricas, con Pearson, Spearman y Kendall. Es lo siguiente.