Modelos para conteos: GLM Poisson y binomial negativa

Dunn & Smyth (2018), Capítulo 10 — con R

Estadística Avanzada · Maestría en Ciencias de la Ingeniería

Universidad Iberoamericana CDMX

Antes de empezar

Ruta de la sesión

  1. Repaso: GLM Poisson, deviance, interpretación multiplicativa (§10.2)
  2. Tasas y offsets — cáncer de pulmón en ciudades danesas (§10.3)
  3. Tablas de contingencia y modelos log-lineales (§10.4)
    • 2 × 2, esquemas de muestreo, equivalencia Poisson ↔︎ multinomial ↔︎ binomial
    • 3 dimensiones: independencia, paradoja de Simpson
    • tablas de orden superior, ceros estructurales
  4. Sobredispersión: cuasi-Poisson vs. binomial negativa (§10.5)
  5. Estudio de caso: cangrejos herradura (§10.6)
  6. Resumen y ejercicios

Nota

El libro usa gráficas base y options(contrasts = ...). Aquí usamos ggplot2, broom y ajustes explícitos (factores sin ordenar, para que R use codificación de tratamiento y no polinomios ortogonales). Los datos vienen del paquete GLMsData.

Paquetes: copia y pega esta diapositiva

# --- Instalar (solo los que falten) ------------------------------------
paquetes <- c("tidyverse",  # ggplot2, dplyr, tidyr, purrr, tibble
              "GLMsData",   # datos del libro: danishlc, kstones, pock, hcrabs ...
              "statmod",    # qresid(): residuos cuantílicos aleatorizados
              "broom",      # tidy(): tablas de coeficientes
              "patchwork",  # combinar gráficas
              "MASS",       # glm.nb()  (se usa siempre como MASS::)
              "sandwich", "lmtest")   # errores estándar robustos (opcional)
faltan <- setdiff(paquetes, rownames(installed.packages()))
if (length(faltan) > 0) install.packages(faltan)

# --- Cargar -------------------------------------------------------------
library(tidyverse)
library(GLMsData)
library(statmod)
library(broom)
library(patchwork)

theme_set(theme_minimal(base_size = 14))
col1 <- "#2BA8E0"; col2 <- "#C45BC4"; col3 <- "#E8A33D"
set.seed(2026)

Tip

MASS no se carga con library() porque enmascara a dplyr::select(); usamos MASS::glm.nb().

Funciones auxiliares (copiar y pegar después de los paquetes)

# Bondad de ajuste: deviance y Pearson contra chi-cuadrada
gof <- function(m) {
  D  <- deviance(m); X2 <- sum(residuals(m, type = "pearson")^2); gl <- df.residual(m)
  tibble(desviación = D, X2_Pearson = X2, gl = gl,
         p_desviación = pchisq(D,  gl, lower.tail = FALSE),
         p_Pearson  = pchisq(X2, gl, lower.tail = FALSE),
         dispersion_Pearson = X2 / gl)
}

# Diagnósticos: residuos vs. ajustados, Q-Q y distancia de Cook
# cuantil = TRUE  -> residuos cuantílicos (modelos con distribución completa)
# cuantil = FALSE -> desviación estandarizada (p. ej. cuasi-Poisson, sin verosimilitud)
diag_glm <- function(m, titulo = NULL, cuantil = TRUE) {
  d <- tibble(i = seq_along(fitted(m)),
              ajustado = sqrt(fitted(m)),
              est  = rstandard(m),
              res  = if (cuantil) qresid(m) else rstandard(m),
              cook = cooks.distance(m))
  p1 <- ggplot(d, aes(ajustado, est)) + geom_point(alpha = .7) +
    geom_smooth(formula = y ~ x, method = "loess", se = FALSE, colour = col1) +
    labs(x = "Raíz de valores ajustados", y = "Residuo estandarizado")
  p2 <- ggplot(d, aes(sample = res)) + stat_qq(alpha = .7) + stat_qq_line(colour = col1) +
    labs(title = if (cuantil) "Q-Q (cuantílicos)" else "Q-Q (desviación est.)",
         x = "Cuantiles teóricos", y = "Cuantiles muestrales")
  p3 <- ggplot(d, aes(i, cook)) + geom_col(width = .4, fill = col2) +
    labs(x = "Índice", y = "Distancia de Cook")
  (p1 + p2 + p3) + plot_annotation(title = titulo)
}

Repaso: GLM Poisson (§10.2)

El modelo

\[ y\sim\text{Pois}(\mu),\qquad P(y;\mu)=\frac{e^{-\mu}\mu^y}{y!},\qquad \log\mu=\beta_0+\beta_1x_1+\dots+\beta_px_p \]

  • Familia exponencial de dispersión con \(\phi=1\) y función de varianza \(V(\mu)=\mu\).
  • Liga logarítmica (canónica): \(\mu=e^{\beta_0}\,(e^{\beta_1})^{x_1}\cdots(e^{\beta_p})^{x_p}\). Efectos multiplicativos: aumentar \(x_j\) en 1 multiplica \(\mu\) por \(e^{\beta_j}\).
  • Desviación unitaria: \(d(y,\mu)=2\bigl[y\log(y/\mu)-(y-\mu)\bigr]\) (con \(0\log0=0\)).
  • Con intercepto y liga log: \(\sum(y_i-\widehat{\mu_i})=0\), y la desviación se simplifica a \(D=2\sum w_iy_i\log(y_i/\hat\mu_i)\).
  • \(D\approx\chi^2_{n-p}\) (aprox. saddlepoint) razonable si \(y_i\ge3\) para todo \(i\).
  • Con factores únicamente \(\Rightarrow\) modelo log-lineal; con covariables \(\Rightarrow\) regresión de Poisson.

Verificación numérica de la desviación

set.seed(10)
n <- 60; x <- runif(n)
y <- rpois(n, exp(0.5 + 1.2 * x))
fit <- glm(y ~ x, family = poisson)
mu  <- fitted(fit)

d_unit <- 2 * (ifelse(y == 0, 0, y * log(y / mu)) - (y - mu))     # desviación unitaria
c(a_mano       = sum(d_unit),
  glm          = deviance(fit),
  simplificada = 2 * sum(ifelse(y == 0, 0, y * log(y / mu))))     # válida con intercepto
      a_mano          glm simplificada 
    62.12457     62.12457     62.12457 
sum(y - mu)                                                       # ≈ 0 con intercepto
[1] -1.421085e-14
tidy(fit, exponentiate = TRUE, conf.int = TRUE)                   # razones multiplicativas
# A tibble: 2 × 7
  term        estimate std.error statistic  p.value conf.low conf.high
  <chr>          <dbl>     <dbl>     <dbl>    <dbl>    <dbl>     <dbl>
1 (Intercept)     1.67     0.179      2.86 0.00425      1.16      2.34
2 x               2.80     0.306      3.36 0.000767     1.54      5.12

Tasas y offsets (§10.3)

¿Conteos o tasas?

Si cada conteo \(y_i\) proviene de una población de tamaño \(T_i\) (conocido y grande) y los eventos son raros, modelamos la tasa \(\mu_i/T_i\):

\[ \log\!\left(\frac{\mu_i}{T_i}\right)=\eta_i \quad\Longleftrightarrow\quad \log\mu_i=\underbrace{\log T_i}_{\text{offset}}+\beta_0+\sum_j\beta_jx_{ij} \]

  • \(\log T_i\) es un offset: entra al predictor lineal con coeficiente fijo = 1, no se estima.
  • En R: glm(y ~ x + offset(log(T)), family = poisson).
  • Si los eventos no fueran raros, serían proporciones y correspondería un GLM binomial (Cap. 9).

Datos: cáncer de pulmón en cuatro ciudades danesas

data(danishlc)
dlc <- as_tibble(danishlc) |>
  mutate(Age    = factor(Age, levels = unique(as.character(Age))),     # orden natural de la tabla (40-54, ..., >74)
         Tasa   = Cases / Pop * 1000,                                   # casos por 1000
         AgeNum = c(40, 55, 60, 65, 70, 75)[as.integer(Age)])           # límite inferior de la clase
dlc
# A tibble: 24 × 6
   Cases   Pop Age   City        Tasa AgeNum
   <int> <int> <fct> <fct>      <dbl>  <dbl>
 1    11  3059 40-54 Fredericia  3.60     40
 2    11   800 55-59 Fredericia 13.8      55
 3    11   710 60-64 Fredericia 15.5      60
 4    10   581 65-69 Fredericia 17.2      65
 5    11   509 70-74 Fredericia 21.6      70
 6    10   605 >74   Fredericia 16.5      75
 7    13  2879 40-54 Horsens     4.52     40
 8     6  1083 55-59 Horsens     5.54     55
 9    15   923 60-64 Horsens    16.3      60
10    10   834 65-69 Horsens    12.0      65
# ℹ 14 more rows
ggplot(dlc, aes(Age, Tasa, colour = City, group = City, shape = City)) +
  geom_line(linewidth = 1) + geom_point(size = 3) +
  labs(x = "Grupo de edad", y = "Casos por 1000", colour = NULL, shape = NULL)

La tasa crece con la edad y cae en el grupo \(>74\); no hay patrón claro por ciudad.

Ajuste: interacción, factor edad, edad numérica

m1 <- glm(Cases ~ offset(log(Pop)) + City * Age, family = poisson, data = dlc)
anova(m1, test = "Chisq")                       # la interacción City:Age no es necesaria (p ≈ 0.075)
Analysis of Deviance Table

Model: poisson, link: log

Response: Cases

Terms added sequentially (first to last)

         Df Deviance Resid. Df Resid. Dev Pr(>Chi)    
NULL                        23    129.908             
City      3    3.393        20    126.515  0.33495    
Age       5  103.068        15     23.447  < 2e-16 ***
City:Age 15   23.447         0      0.000  0.07509 .  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
m2 <- update(m1, . ~ offset(log(Pop)) + Age)                    # solo edad (factor)
m3 <- update(m1, . ~ offset(log(Pop)) + AgeNum)                 # edad lineal
m4 <- update(m1, . ~ offset(log(Pop)) + poly(AgeNum, 2))        # edad cuadrática
anova(m3, m4, test = "Chisq")
Analysis of Deviance Table

Model 1: Cases ~ AgeNum + offset(log(Pop))
Model 2: Cases ~ poly(AgeNum, 2) + offset(log(Pop))
  Resid. Df Resid. Dev Df Deviance  Pr(>Chi)    
1        22     48.968                          
2        21     32.500  1   16.468 4.948e-05 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Comparación de modelos

modelos <- list("Con interacción" = m1, "Solo edad (factor)" = m2,
                "Edad lineal" = m3, "Edad cuadrática" = m4)

tibble(modelo = names(modelos), AIC = map_dbl(modelos, AIC)) |> arrange(AIC)
# A tibble: 4 × 2
  modelo               AIC
  <chr>              <dbl>
1 Edad cuadrática     135.
2 Solo edad (factor)  137.
3 Con interacción     144.
4 Edad lineal         149.
map(modelos, gof) |> bind_rows(.id = "modelo") |> select(-dispersion_Pearson)
# A tibble: 4 × 6
  modelo             desviación X2_Pearson    gl p_desviación p_Pearson
  <chr>                   <dbl>      <dbl> <int>        <dbl>     <dbl>
1 Con interacción      3.55e-15   1.11e-25     0     0          0      
2 Solo edad (factor)   2.83e+ 1   2.60e+ 1    18     0.0575     0.0987 
3 Edad lineal          4.90e+ 1   4.29e+ 1    22     0.000807   0.00490
4 Edad cuadrática      3.25e+ 1   2.88e+ 1    21     0.0521     0.118  
sort(dlc$Cases)[1:3]          # mínimo = 2 < 3: la aproximación chi-cuadrada es dudosa, interpretar con cautela
[1] 2 4 5

Los modelos m2 y m4 tienen AIC muy parecido. Los diagnósticos decidirán.

Diagnósticos: factor edad vs. cuadrático

diag_glm(m2, "Edad como factor (m2)") / diag_glm(m4, "Edad cuadrática (m4)")

m4 muestra observaciones más influyentes y es menos simple: preferimos m2.

Interpretación del modelo m2

# Razones de tasas respecto al grupo 40-54 (liga log + offset => coeficientes = log razones de tasas)
tidy(m2, exponentiate = TRUE, conf.int = TRUE) |>
  mutate(term = str_remove(term, "^Age"), across(where(is.numeric), \(v) round(v, 3)))
# A tibble: 6 × 7
  term        estimate std.error statistic p.value conf.low conf.high
  <chr>          <dbl>     <dbl>     <dbl>   <dbl>    <dbl>     <dbl>
1 (Intercept)    0.003     0.174    -33.7        0    0.002     0.004
2 55-59          2.95      0.248      4.36       0    1.81      4.81 
3 60-64          4.49      0.231      6.49       0    2.86      7.11 
4 65-69          5.76      0.229      7.64       0    3.68      9.09 
5 70-74          6.34      0.235      7.86       0    4.00     10.1  
6 >74            4.09      0.25       5.63       0    2.50      6.68 
# Tasa esperada por 1000 habitantes: predecir con Pop = 1000
tibble(Age = factor(levels(dlc$Age), levels = levels(dlc$Age)), Pop = 1000) |>
  mutate(tasa_por_1000 = predict(m2, newdata = pick(everything()), type = "response"))
# A tibble: 6 × 3
  Age     Pop tasa_por_1000
  <fct> <dbl>         <dbl>
1 40-54  1000          2.84
2 55-59  1000          8.40
3 60-64  1000         12.8 
4 65-69  1000         16.4 
5 70-74  1000         18.0 
6 >74    1000         11.6 

¿Y si estimamos el coeficiente de \(\log T\)?

El offset impone que el coeficiente de \(\log T\) vale 1. Podemos probar esa hipótesis:

m_libre <- glm(Cases ~ log(Pop) + Age, family = poisson, data = dlc)
b  <- coef(summary(m_libre))["log(Pop)", ]
z  <- (b["Estimate"] - 1) / b["Std. Error"]
c(estimado = unname(b["Estimate"]), z = unname(z), p = 2 * pnorm(-abs(z)))
   estimado           z  p.Estimate 
-0.57188171 -2.51864536  0.01178072 

Si no se rechaza \(H_0:\beta_{\log T}=1\), el offset es razonable (¡aunque la tabla tiene solo 24 celdas!).

Tablas de contingencia: modelos log-lineales (§10.4)

Tablas de dos vías: GM y nivel de ingreso

Actitud de australianos hacia alimentos genéticamente modificados, según ingreso (1996–97):

gm <- tibble(Counts = c(263, 258, 151, 222),
             Att = gl(2, 2, 4, labels = c("For", "Against")),
             Inc = gl(2, 1, 4, labels = c("High", "Low")))
gm_tab <- xtabs(Counts ~ Att + Inc, data = gm)
gm_tab
         Inc
Att       High Low
  For      263 258
  Against  151 222
round(prop.table(gm_tab, margin = 2) * 100, 1)       # % por columna (ingreso)
         Inc
Att       High  Low
  For     63.5 53.8
  Against 36.5 46.2

Si \(A\) y \(B\) son independientes, \(\mu_{ij}=m\,\pi_{i\bullet}\pi_{\bullet j}\), es decir

\[ \log\mu_{ij}=\log m+\log\pi_{i\bullet}+\log\pi_{\bullet j}\qquad\text{(modelo sin interacción)} \]

Tres esquemas de muestreo, el mismo análisis

Esquema Distribución ¿Qué debe llevar el predictor?
Nada fijo (llegadas aleatorias) Poisson para cada celda nada especial
Total general fijo (\(m\)) Multinomial el intercepto (\(\Rightarrow\sum\hat\mu_{ij}=m\))
Totales de fila (o columna) fijos Multinomial en cada fila los efectos principales de ese factor (y el intercepto)

La log-verosimilitud multinomial coincide (salvo constantes) con la de Poisson, así que la desviación es la misma y basta ajustar un GLM Poisson, siempre que los términos de los márgenes fijos estén en el modelo.

Consecuencia práctica: los efectos principales solo reproducen los márgenes (rara vez interesan). Lo que importa son las interacciones.

Ajuste Poisson: independencia vs. interacción

gm_1   <- glm(Counts ~ Att + Inc, family = poisson, data = gm)    # independencia
gm_int <- glm(Counts ~ Att * Inc, family = poisson, data = gm)    # saturado (4 parámetros, 4 celdas)
anova(gm_int, test = "Chisq")
Analysis of Deviance Table

Model: poisson, link: log

Response: Counts

Terms added sequentially (first to last)

        Df Deviance Resid. Df Resid. Dev  Pr(>Chi)    
NULL                        3     38.260              
Att      1  24.6143         2     13.646 7.003e-07 ***
Inc      1   4.8769         1      8.769  0.027218 *  
Att:Inc  1   8.7686         0      0.000  0.003065 ** 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
coef(gm_1)
(Intercept)  AttAgainst      IncLow 
  5.4859102  -0.3341716   0.1479201 
c(exp_intercepto = exp(coef(gm_1)[[1]]), exp_AttAgainst = exp(coef(gm_1)[[2]]), exp_IncLow = exp(coef(gm_1)[[3]]))
exp_intercepto exp_AttAgainst     exp_IncLow 
   241.2684564      0.7159309      1.1594203 
sum(gm$Counts[gm$Inc == "Low"]) / sum(gm$Counts[gm$Inc == "High"])    # = exp(IncLow): ¡solo ajusta márgenes!
[1] 1.15942
# Con el intercepto, las sumas ajustadas coinciden con los totales (condicionamos en m = 894)
c(suma_ajustada = sum(fitted(gm_1)), total = sum(gm$Counts))
suma_ajustada         total 
          894           894 

La desviación de interacción es la prueba de independencia

dev_indep <- deviance(gm_1)                                     # = G² de razón de verosimilitudes
E  <- outer(rowSums(gm_tab), colSums(gm_tab)) / sum(gm_tab)     # valores esperados bajo independencia
G2 <- 2 * sum(gm_tab * log(gm_tab / E))
X2 <- chisq.test(gm_tab, correct = FALSE)

c(desviación_glm = dev_indep, G2_a_mano = G2, Pearson_X2 = unname(X2$statistic))
desviación_glm      G2_a_mano     Pearson_X2 
       8.76863        8.76863        8.73787 
pchisq(dev_indep, 1, lower.tail = FALSE)
[1] 0.003064555

desviación y \(G^2\) coinciden; Pearson es una aproximación asintóticamente equivalente.

Equivalencia con el GLM binomial

Si el interés es explicar la actitud con el ingreso (respuesta binaria), la regresión logística es equivalente al log-lineal:

gm_bin <- glm(I(Att == "Against") ~ Inc, family = binomial, weights = Counts, data = gm)
anova(gm_bin, test = "Chisq")                        # misma desviación (8.7686) y mismo valor p
Analysis of Deviance Table

Model: binomial, link: logit

Response: I(Att == "Against")

Terms added sequentially (first to last)

     Df Deviance Resid. Df Resid. Dev Pr(>Chi)   
NULL                     3     1214.7            
Inc   1   8.7686         2     1206.0 0.003065 **
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
c(OR_logistica  = exp(coef(gm_bin)[["IncLow"]]),
  OR_loglineal  = exp(coef(gm_int)[["AttAgainst:IncLow"]]),
  OR_tabla      = (263 * 222) / (258 * 151))         # razón de momios cruzados
OR_logistica OR_loglineal     OR_tabla 
    1.498691     1.498691     1.498691 

El coeficiente de interacción del log-lineal es el logaritmo de la razón de momios de la tabla. La razón: para \(m\) grande y \(\pi\) pequeña, binomial \(\to\) Poisson (\(m\pi(1-\pi)\to m\pi\)).

Tablas de tres dimensiones

Riñón: métodos para cálculos renales

Éxito de dos métodos (A, B) según el tamaño de los cálculos (Table 10.5). 350 pacientes por método (margen fijo \(\Rightarrow\) Method debe estar en el modelo).

data(kstones)
ks <- as_tibble(kstones)
ks_tab <- xtabs(Counts ~ Method + Outcome + Size, data = ks)
ks_tab
, , Size = Large

      Outcome
Method Failure Success
     A      71     192
     B      25      55

, , Size = Small

      Outcome
Method Failure Success
     A       6      81
     B      36     234

Jerarquía de modelos log-lineales

Modelo Términos Interpretación
Independencia mutua \(A+B+C\) cada variable por separado
Independencia parcial \(A\!\cdot\!B+C\) \(C\) independiente del par \((A,B)\)
Independencia condicional \(A\!\cdot\!C+B\!\cdot\!C\) \(A\perp B\mid C\)
Asociación uniforme todas las dobles sin triple interacción; difícil de interpretar
Saturado \(A\!\cdot\!B\!\cdot\!C\) ajuste perfecto, sin simplificación

Siempre se incluyen los efectos principales (principio de marginalidad).

Ajustamos los nueve modelos de golpe

formulas <- list(
  "Independencia mutua"      = Counts ~ Size + Method + Outcome,
  "Parcial: Size·Method"     = Counts ~ Size * Method + Outcome,
  "Parcial: Size·Outcome"    = Counts ~ Size * Outcome + Method,
  "Parcial: Outcome·Method"  = Counts ~ Outcome * Method + Size,
  "Condicional: sin M·O"     = Counts ~ Size * (Method + Outcome),
  "Condicional: sin O·S"     = Counts ~ Method * (Outcome + Size),
  "Condicional: sin M·S"     = Counts ~ Outcome * (Method + Size),
  "Asociación uniforme"      = Counts ~ Size * Method * Outcome - Size:Method:Outcome,
  "Saturado"                 = Counts ~ Size * Method * Outcome)

ks_fits <- map(formulas, \(f) glm(f, family = poisson, data = ks))

ks_tabla <- tibble(modelo = names(ks_fits),
                   desviación = map_dbl(ks_fits, deviance),
                   gl  = map_dbl(ks_fits, df.residual),
                   p   = if_else(gl > 0, pchisq(desviación, gl, lower.tail = FALSE), NA_real_),
                   AIC = map_dbl(ks_fits, AIC)) |>
  mutate(desviación = pmax(desviación, 0))
ks_tabla |> mutate(across(c(desviación, p, AIC), \(v) round(v, 3)))
# A tibble: 9 × 5
  modelo                  desviación    gl      p   AIC
  <chr>                        <dbl> <dbl>  <dbl> <dbl>
1 Independencia mutua         234.       4  0     289. 
2 Parcial: Size·Method         33.1      3  0      89.8
3 Parcial: Size·Outcome       205.       3  0     261. 
4 Parcial: Outcome·Method     232.       3  0     289. 
5 Condicional: sin M·O          3.45     2  0.178  62.2
6 Condicional: sin O·S         30.8      2  0      89.5
7 Condicional: sin M·S        202.       2  0     261. 
8 Asociación uniforme           1.01     1  0.315  61.7
9 Saturado                      0        0 NA      62.7

El modelo más simple adecuado (\(p>0.05\)): Size * (Method + Outcome), es decir, \(\text{Method}\perp\text{Outcome}\mid\text{Size}\). Los datos hay que estudiarlos separados por tamaño.

Paradoja de Simpson

tasas <- function(d, por) d |>
  summarise(n = sum(Counts), .by = all_of(c("Method", "Outcome", por))) |>
  mutate(prop = n / sum(n), .by = all_of(c("Method", por))) |>
  filter(Outcome == "Success")

bind_rows(tasas(ks, character(0)) |> mutate(Size = "Todos (colapsado)"), tasas(ks, "Size")) |>
  mutate(Size = factor(Size, levels = c("Small", "Large", "Todos (colapsado)"))) |>
  ggplot(aes(Method, prop, fill = Method)) +
  geom_col(width = .6) + geom_text(aes(label = scales::percent(prop, accuracy = .1)), vjust = -.4) +
  facet_wrap(~Size) + scale_fill_manual(values = c(col1, col3), guide = "none") +
  scale_y_continuous(labels = scales::percent, limits = c(0, 1)) +
  labs(x = "Método", y = "Tasa de éxito")

Colapsado, el método B parece mejor (83 % vs. 78 %); por tamaño, el método A gana en ambos estratos. A se usa más con cálculos grandes (más difíciles).

Razones de momios: colapsada vs. condicionales

or <- \(t) (t["A", "Success"] * t["B", "Failure"]) / (t["A", "Failure"] * t["B", "Success"])  # A vs. B

c(colapsada   = or(apply(ks_tab, c(1, 2), sum)),
  Small       = or(ks_tab[, , "Small"]),
  Large       = or(ks_tab[, , "Large"]))
colapsada     Small     Large 
0.7483485 2.0769231 1.2291933 
mantelhaen.test(ks_tab)          # razón de momios común (Mantel–Haenszel), condicionada en Size

    Mantel-Haenszel chi-squared test with continuity correction

data:  ks_tab
Mantel-Haenszel X-squared = 2.0913, df = 1, p-value = 0.1481
alternative hypothesis: true common odds ratio is not equal to 1
95 percent confidence interval:
 0.4374743 1.0919493
sample estimates:
common odds ratio 
        0.6911583 

La razón de momios colapsada es < 1 (favorece a B), las condicionales > 1. Colapsar sobre un factor es válido solo si el modelo lo permite.

Si solo nos interesa el resultado: el GLM binomial equivalente

ks_ancho <- ks |> pivot_wider(names_from = Outcome, values_from = Counts)

ks_bin <- glm(cbind(Success, Failure) ~ Size * Method, family = binomial, data = ks_ancho)
anova(ks_bin, test = "Chisq")
Analysis of Deviance Table

Model: binomial, link: logit

Response: cbind(Success, Failure)

Terms added sequentially (first to last)

            Df Deviance Resid. Df Resid. Dev  Pr(>Chi)    
NULL                            3     33.124              
Size         1  29.6736         2      3.450 5.113e-08 ***
Method       1   2.4421         1      1.008    0.1181    
Size:Method  1   1.0082         0      0.000    0.3153    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Éxito depende fuertemente del tamaño; no hay evidencia de diferencia entre métodos (ni global ni por tamaño), igual que el log-lineal. El log-lineal además detecta que Method está asociado con Size, información que aquí viene del diseño.

Tablas de orden superior y ceros

Depresión en jóvenes: cuatro factores

Edad, grupo (LD o SED), género y nivel de depresión (L/H). Nada fijo \(\Rightarrow\) no hay términos obligatorios.

data(dyouth)
dy_m1 <- glm(Obs ~ Age * Depression * Gender + Age * Group, data = dyouth, family = poisson)
anova(dy_m1, test = "Chisq")
Analysis of Deviance Table

Model: poisson, link: log

Response: Obs

Terms added sequentially (first to last)

                      Df Deviance Resid. Df Resid. Dev  Pr(>Chi)    
NULL                                     23     368.05              
Age                    2   11.963        21     356.09  0.002525 ** 
Depression             1  168.375        20     187.71 < 2.2e-16 ***
Gender                 1   58.369        19     129.34 2.172e-14 ***
Group                  1   69.104        18      60.24 < 2.2e-16 ***
Age:Depression         2    3.616        16      56.62  0.163964    
Age:Gender             2    3.631        14      52.99  0.162718    
Depression:Gender      1    7.229        13      45.76  0.007175 ** 
Age:Group              2   27.090        11      18.67 1.311e-06 ***
Age:Depression:Gender  2    8.325         9      10.35  0.015571 *  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
drop1(dy_m1, test = "Chisq")          # ¿qué términos de mayor orden sobreviven?
Single term deletions

Model:
Obs ~ Age * Depression * Gender + Age * Group
                      Df Deviance    AIC     LRT  Pr(>Chi)    
<none>                     10.347 145.02                      
Age:Group              2   37.437 168.11 27.0896 1.311e-06 ***
Age:Depression:Gender  2   18.672 149.34  8.3246   0.01557 *  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

¿Qué significa la triple interacción?

dyouth |>
  summarise(n = sum(Obs), .by = c(Age, Gender, Depression)) |>
  mutate(prop = n / sum(n), .by = c(Age, Gender)) |>
  filter(Depression == "H") |>
  ggplot(aes(Age, prop, colour = Gender, group = Gender)) +
  geom_line(linewidth = 1.1) + geom_point(size = 3) +
  scale_colour_manual(values = c(F = col2, M = col1)) +
  scale_y_continuous(labels = scales::percent) +
  labs(x = "Edad", y = "% con depresión alta", colour = "Género")

El patrón por edad es distinto en mujeres y varones. La interacción Age:Group solo refleja distinta composición por edad entre grupos; se conserva para que las pruebas con Depression queden ajustadas por esa demografía.

Ceros muestrales vs. ceros estructurales

  • Cero muestral: pudo haber ocurrido, simplemente no se observó. Se modela normalmente (aunque la aproximación chi-cuadrada se deteriora; a veces conviene combinar categorías).
  • Cero estructural: es imposible (cáncer de próstata en mujeres). Hay que eliminar esas celdas del análisis.
data(wacancer)
wc_mal <- glm(Counts ~ Cancer * Gender, data = wacancer, family = poisson)   # ignora el problema

wc <- wacancer |> as_tibble() |>
  filter(Cancer != "Breast",
         !(Cancer == "Cervix"   & Gender == "M"),
         !(Cancer == "Prostate" & Gender == "F"))
wc_m1 <- glm(Counts ~ Cancer * Gender, data = wc, family = poisson)

list(sin_corregir = anova(wc_mal, test = "Chisq"), corregido = anova(wc_m1, test = "Chisq"))
$sin_corregir
Analysis of Deviance Table

Model: poisson, link: log

Response: Counts

Terms added sequentially (first to last)

              Df Deviance Resid. Df Resid. Dev  Pr(>Chi)    
NULL                             13     6063.7              
Cancer         6   3281.5         7     2782.2 < 2.2e-16 ***
Gender         1     95.9         6     2686.2 < 2.2e-16 ***
Cancer:Gender  6   2686.2         0        0.0 < 2.2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

$corregido
Analysis of Deviance Table

Model: poisson, link: log

Response: Counts

Terms added sequentially (first to last)

              Df Deviance Resid. Df Resid. Dev  Pr(>Chi)    
NULL                              9    2774.32              
Cancer         5  2591.47         4     182.85 < 2.2e-16 ***
Gender         1   144.74         3      38.11 < 2.2e-16 ***
Cancer:Gender  3    38.11         0       0.00  2.68e-08 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Alternativa: dejar la tabla completa y poner peso 0 a las celdas imposibles (weights = 0), que glm() ignora; cambian los grados de libertad, no la conclusión.

Sobredispersión (§10.5)

¿Qué es y cómo se detecta?

Para Poisson, \(\text{var}(y)=\mu\). En la práctica a menudo \(\text{var}(y)>\mu\): sobredispersión (eventos agrupados, positivamente correlacionados, o heterogeneidad no modelada en \(\mu\)).

  • Los \(\hat\beta_j\) pueden no cambiar mucho, pero los errores estándar se subestiman \(\Rightarrow\) pruebas “demasiado significativas” e IC demasiado angostos.
  • Detección: si desviación y \(X^2\) de Pearson son mucho mayores que los grados de libertad (con el modelo más completo posible y sin atípicos).
  • Estimador de la dispersión: \(\hat\phi=X^2/(n-p)\).

Dos soluciones: binomial negativa (modelo con verosimilitud, \(V(\mu)=\mu+\mu^2/k\)) y cuasi-Poisson (solo momentos, \(V(\mu)=\phi\mu\)).

Binomial negativa como mezcla Poisson–gamma

\[ y_i\mid\lambda_i\sim\text{Pois}(\lambda_i),\quad\lambda_i\sim\text{Gamma}(\text{media }\mu_i,\ \text{CV}^2=\psi) \ \Longrightarrow\ E[y_i]=\mu_i,\ \ \text{var}[y_i]=\mu_i+\psi\mu_i^2=\mu_i+\frac{\mu_i^2}{k},\ \ k=1/\psi \]

Para \(k\) fijo es una familia exponencial (desviación unitaria \(2[y\log\frac y\mu-(y+k)\log\frac{y+k}{\mu+k}]\), \(\phi=1\)). En R \(k\) se estima junto con \(\beta\) con MASS::glm.nb() (ahí se llama theta).

mu <- 10; k <- 2
lambda <- rgamma(1e5, shape = k, rate = k / mu)      # media mu, CV² = 1/k
y_mezcla <- rpois(1e5, lambda)
c(media = mean(y_mezcla), varianza = var(y_mezcla), var_teorica = mu + mu^2 / k)
      media    varianza var_teorica 
    9.96351    59.63891    60.00000 
tibble(y = y_mezcla) |> count(y) |> mutate(frec = n / sum(n)) |>
  ggplot(aes(y, frec)) + geom_col(fill = col1, alpha = .6) +
  geom_line(data = tibble(y = 0:50), aes(y, dnbinom(y, mu = mu, size = k)), colour = col2, linewidth = 1) +
  geom_line(data = tibble(y = 0:50), aes(y, dpois(y, mu)), colour = col3, linewidth = 1, linetype = 2) +
  labs(x = "y", y = "Frecuencia", subtitle = "Barras: Poisson–gamma simulada · morado: NB teórica · naranja: Poisson")

Cuasi-Poisson

Mantiene \(V(\mu)=\mu\) pero deja la dispersión libre: \(\text{var}(y)=\phi\mu\). Motivación: población grande \(N\) con individuos correlacionados (\(\rho\)): \(\phi=1+(N-1)\rho\).

  • Mismos \(\hat\beta\) que Poisson; los errores estándar se inflan por \(\sqrt{\hat\phi}\).
  • Se usan pruebas \(F\) (porque \(\phi\) se estima).
  • No hay verosimilitud \(\Rightarrow\) no hay AIC ni residuos cuantílicos.
Cuasi-Poisson Binomial negativa
Varianza \(\phi\mu\) (lineal) \(\mu+\mu^2/k\) (cuadrática)
Verosimilitud / AIC no sí
Pondera igual que Poisson menos a \(\mu\) grandes, más a \(\mu\) pequeñas

Datos pock: marcas en membranas

Marcas (pocks) contadas a distintas diluciones de un medio viral. Usamos \(\log_2(\text{Dilución})\).

data(pock)
pk <- as_tibble(pock) |> mutate(ld = log2(Dilution))

resumen <- pk |> summarise(media = mean(Count), varianza = var(Count), .by = ld) |>
  mutate(razon = varianza / media) |> arrange(ld)
resumen
# A tibble: 5 × 4
     ld media varianza razon
  <dbl> <dbl>    <dbl> <dbl>
1     0 187.    1781.   9.54
2     1 105.     667.   6.37
3     2  50.8    360.   7.09
4     3  27.1    195.   7.20
5     4   9.1     17.7  1.94
p1 <- ggplot(pk, aes(ld, Count)) + geom_jitter(width = .08, height = 0, alpha = .7) +
  labs(x = "log2(dilución)", y = "Conteo de marcas")
p2 <- ggplot(resumen, aes(log(media), log(varianza))) + geom_point(size = 3) +
  geom_smooth(formula = y ~ x, method = "lm", se = FALSE, colour = col1) +
  labs(x = "log(media del grupo)", y = "log(varianza del grupo)")
p1 + p2

coef(lm(log(varianza) ~ log(media), data = resumen))     # pendiente ≈ 1.4: V(μ) ≈ μ^1.5
(Intercept)  log(media) 
 0.02861162  1.44318666 

La varianza es mucho mayor que la media y la razón crece con la media: entre Poisson (\(V=\mu\)) y gamma (\(V=\mu^2\)).

Tres modelos para pock

pk_pois <- glm(Count ~ ld, data = pk, family = poisson)
gof(pk_pois)                                          # desviación ≈ Pearson ≈ 290 con 46 gl: sobredispersión fuerte
# A tibble: 1 × 6
  desviación X2_Pearson    gl p_desviación p_Pearson dispersion_Pearson
       <dbl>      <dbl> <int>        <dbl>     <dbl>              <dbl>
1       290.       292.    46     3.29e-37  2.01e-37               6.34
pk_qp   <- glm(Count ~ ld, data = pk, family = quasipoisson)
pk_nb0  <- MASS::glm.nb(Count ~ ld, data = pk)
pk_nb0$theta                                          # k estimado
[1] 9.892894
pk_nb   <- MASS::glm.convert(pk_nb0)                  # como glm(); usar dispersion = 1 en summary/predict/anova

# (broom::tidy() no pasa `dispersion`, así que extraemos la tabla de summary() directamente)
tab_coef <- \(m, ...) as_tibble(coef(summary(m, ...)), rownames = "term") |>
  select(term, estimate = Estimate, std.error = `Std. Error`)

bind_rows(Poisson         = tab_coef(pk_pois),
          `Cuasi-Poisson` = tab_coef(pk_qp),
          `Bin. negativa` = tab_coef(pk_nb, dispersion = 1),
          .id = "modelo") |>
  pivot_wider(names_from = modelo, values_from = c(estimate, std.error)) |>
  mutate(across(where(is.numeric), \(v) round(v, 4)))
# A tibble: 2 × 7
  term        estimate_Poisson `estimate_Cuasi-Poisson` `estimate_Bin. negativa`
  <chr>                  <dbl>                    <dbl>                    <dbl>
1 (Intercept)            5.27                     5.27                     5.33 
2 ld                    -0.681                   -0.681                   -0.725
# ℹ 3 more variables: std.error_Poisson <dbl>, `std.error_Cuasi-Poisson` <dbl>,
#   `std.error_Bin. negativa` <dbl>
c(razon_SE_QP_Pois = unname(coef(summary(pk_qp))[2, 2] / coef(summary(pk_pois))[2, 2]),
  raiz_phi         = sqrt(summary(pk_qp)$dispersion))     # idénticos: SE se inflan por sqrt(phi)
razon_SE_QP_Pois         raiz_phi 
        2.517733         2.517733 

Interpretación y ajuste

nuevo <- tibble(ld = seq(0, 4, length.out = 100))

pred <- \(m, nm, crit, ...) {
  p <- predict(m, newdata = nuevo, se.fit = TRUE, ...)
  tibble(modelo = nm, ld = nuevo$ld, mu = exp(p$fit),
         lo = exp(p$fit - crit * p$se.fit), hi = exp(p$fit + crit * p$se.fit))
}
curvas <- bind_rows(
  pred(pk_pois, "Poisson",       qnorm(.975)),
  pred(pk_qp,   "Cuasi-Poisson", qt(.975, df.residual(pk_qp))),
  pred(pk_nb,   "Bin. negativa", qnorm(.975), dispersion = 1)) |>
  mutate(modelo = factor(modelo, levels = c("Poisson", "Cuasi-Poisson", "Bin. negativa")))

ggplot() +
  geom_ribbon(data = curvas, aes(ld, ymin = lo, ymax = hi), alpha = .25, fill = col1) +
  geom_line(data = curvas, aes(ld, mu), colour = col1, linewidth = 1) +
  geom_point(data = pk, aes(ld, Count), alpha = .6) +
  facet_wrap(~modelo) + labs(x = "log2(dilución)", y = "Conteo de marcas", subtitle = "IC del 95 % para μ")

Cada dilución al doble multiplica el conteo esperado por \(e^{-0.7}\approx0.5\): el número de marcas es proporcional a la concentración del virus. Poisson da bandas engañosamente estrechas.

Diagnósticos de los modelos para pock

diag_glm(pk_nb, "Binomial negativa", cuantil = TRUE) /
  diag_glm(pk_qp, "Cuasi-Poisson (desviación estandarizada)", cuantil = FALSE)

La NB subestima un poco la varianza de conteos pequeños; la cuasi-Poisson, la de conteos grandes (distinta función de varianza).

Alternativa moderna: errores estándar robustos (sandwich)

Otra forma de corregir la inferencia sin asumir la forma de la varianza: Poisson con matriz de covarianzas robusta (consistente si la media está bien especificada).

rob <- lmtest::coeftest(pk_pois, vcov. = sandwich::vcovHC(pk_pois, type = "HC0"))
tibble(termino = rownames(rob), estimado = rob[, 1],
       SE_Poisson = coef(summary(pk_pois))[, 2], SE_QP = coef(summary(pk_qp))[, 2], SE_robusto = rob[, 2])
# A tibble: 2 × 5
  termino     estimado SE_Poisson  SE_QP SE_robusto
  <chr>          <dbl>      <dbl>  <dbl>      <dbl>
1 (Intercept)    5.27      0.0226 0.0568     0.0629
2 ld            -0.681     0.0154 0.0389     0.0381

(Opcional) Prueba de sobredispersión con DHARMa

sim <- DHARMa::simulateResiduals(pk_pois, n = 500)
DHARMa::testDispersion(sim)


    DHARMa nonparametric dispersion test via sd of residuals fitted vs.
    simulated

data:  simulationOutput
dispersion = 7.4117, p-value < 2.2e-16
alternative hypothesis: two.sided

Estudio de caso: cangrejos herradura (§10.6)

Pregunta y datos

173 hembras anidando, cada una con un macho adjunto; se cuentan los machos satélite cercanos (Sat). Covariables: color, estado de la espina, ancho del caparazón y peso. ¿Qué atrae a los satélites?

data(hcrabs)
hc <- as_tibble(hcrabs) |>
  mutate(Col   = factor(Col,   levels = c("LM", "M", "DM", "D")),             # claro -> oscuro
         Spine = factor(Spine, levels = c("NoneOK", "OneOK", "BothOK")))      # sin daño -> daño
glimpse(hc)
Rows: 173
Columns: 5
$ Col   <fct> M, DM, LM, DM, DM, M, LM, DM, M, DM, DM, M, M, D, M, LM, M, M, D…
$ Spine <fct> NoneOK, NoneOK, BothOK, NoneOK, NoneOK, NoneOK, BothOK, OneOK, B…
$ Width <dbl> 28.3, 22.5, 26.0, 24.8, 26.0, 23.8, 26.5, 24.7, 23.7, 25.6, 24.3…
$ Sat   <int> 8, 0, 9, 0, 4, 0, 0, 0, 0, 0, 0, 0, 11, 0, 14, 8, 1, 1, 0, 5, 4,…
$ Wt    <int> 3050, 1550, 2300, 2100, 2600, 2100, 2350, 1900, 1950, 2150, 2150…
table(hc$Sat == 0)                                                              # ¡muchos ceros!

FALSE  TRUE 
  111    62 

(Usamos factores sin ordenar: así R codifica por tratamiento y no con polinomios ortogonales.)

Exploración

hc <- hc |> mutate(logSat = log(Sat + 1))
g1 <- ggplot(hc, aes(Wt, Sat)) + geom_jitter(height = .2, alpha = .6)
g2 <- ggplot(hc, aes(log(Wt), logSat)) + geom_jitter(height = .05, alpha = .6) + labs(y = "log(Sat+1)")
g3 <- ggplot(hc, aes(Col, logSat)) + geom_boxplot(fill = col1, alpha = .5) + labs(y = "log(Sat+1)")
g4 <- ggplot(hc, aes(Width, Sat)) + geom_jitter(height = .2, alpha = .6)
g5 <- ggplot(hc, aes(log(Width), logSat)) + geom_jitter(height = .05, alpha = .6) + labs(y = "log(Sat+1)")
g6 <- ggplot(hc, aes(Spine, logSat)) + geom_boxplot(fill = col2, alpha = .5) + labs(y = "log(Sat+1)")
(g1 + g2 + g3) / (g4 + g5 + g6)

Más satélites con hembras más grandes, más claras y sin daño en la espina.

Las covariables están relacionadas

coef(lm(log(Wt) ~ log(Width), data = hc))        # pendiente ≈ 2.6 (≈ 3 si fueran todas de la misma forma)
(Intercept)  log(Width) 
 -0.6004161   2.5629298 
ggplot(hc, aes(log(Width), log(Wt), colour = Col)) + geom_point(alpha = .7) +
  scale_colour_manual(values = c("#F4E3A1", col3, "#8C5A2B", "#2D1B0E")) +
  labs(colour = "Color")

El peso resume el tamaño: las hembras más claras y sin daño tienden a pesar más. Color y espina podrían ser efectos aparentes vía el peso.

Modelo completo y selección (cuasi-Poisson)

cr_m1 <- glm(Sat ~ log(Wt) + log(Width) + Spine + Col, family = quasipoisson, data = hc)
anova(cr_m1, test = "F")
Analysis of Deviance Table

Model: quasipoisson, link: log

Response: Sat

Terms added sequentially (first to last)

           Df Deviance Resid. Df Resid. Dev       F    Pr(>F)    
NULL                         172     632.79                      
log(Wt)     1   83.084       171     549.71 25.9645 9.429e-07 ***
log(Width)  1    0.007       170     549.70  0.0023    0.9621    
Spine       2    1.125       168     548.58  0.1758    0.8389    
Col         3    7.630       165     540.95  0.7948    0.4984    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
c(desviación = deviance(cr_m1), gl = df.residual(cr_m1))          # desviación > 3 veces los gl
desviación         gl 
   540.946    165.000 
cr_m2 <- glm(Sat ~ log(Wt), family = quasipoisson, data = hc)
tidy(cr_m2) |> mutate(across(where(is.numeric), \(v) signif(v, 3)))
# A tibble: 2 × 5
  term        estimate std.error statistic     p.value
  <chr>          <dbl>     <dbl>     <dbl>       <dbl>
1 (Intercept)   -12.6      2.66      -4.72 0.00000494 
2 log(Wt)         1.74     0.339      5.15 0.000000702
gof(cr_m2)
# A tibble: 1 × 6
  desviación X2_Pearson    gl p_desviación p_Pearson dispersion_Pearson
       <dbl>      <dbl> <int>        <dbl>     <dbl>              <dbl>
1       550.       531.   171     2.57e-41  1.45e-38               3.11

Solo log(Wt) importa una vez ajustado el peso. Como el modelo es \(\mu=e^{\beta_0}W^{\beta_1}\) con \(\hat\beta_1\approx1.74\), el número esperado de satélites crece algo más rápido que el peso.

¿Es válida la aproximación chi-cuadrada con tantos ceros?

Casi la mitad de Sat es 0 o 1. Verificamos por simulación (desviación bajo Poisson con las medias ajustadas):

mu_hat <- fitted(cr_m2); x <- log(hc$Wt)
dev_sim <- replicate(300, {
  y_s <- rpois(length(mu_hat), mu_hat)
  glm(y_s ~ x, family = quasipoisson)$deviance
})
c(media_sim = mean(dev_sim), sd_sim = sd(dev_sim),
  teorica_media = df.residual(cr_m2), teorica_sd = sqrt(2 * df.residual(cr_m2)))
    media_sim        sd_sim teorica_media    teorica_sd 
    188.80817      19.02693     171.00000      18.49324 

La aproximación es excelente, así que la desviación observada (≈ 550 con 171 gl) es evidencia de sobredispersión.

Binomial negativa para los cangrejos

cr_pois <- glm(Sat ~ log(Wt), family = poisson, data = hc)
cr_nb0  <- MASS::glm.nb(Sat ~ log(Wt), data = hc)
cr_nb   <- MASS::glm.convert(cr_nb0)

cr_nb0$theta                                          # k ≈ 0.96
[1] 0.9580286
anova(cr_nb, dispersion = 1, test = "Chisq")
Analysis of Deviance Table

Model: Negative Binomial(0.958), link: log

Response: Sat

Terms added sequentially (first to last)

        Df Deviance Resid. Df Resid. Dev  Pr(>Chi)    
NULL                      172     219.81              
log(Wt)  1   23.339       171     196.47 1.358e-06 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
coef(summary(cr_nb, dispersion = 1)) |> round(4)
            Estimate Std. Error z value Pr(>|z|)
(Intercept) -14.5558     3.1091 -4.6817        0
log(Wt)       1.9986     0.3984  5.0168        0
AIC(cr_pois, cr_nb0)                                  # comparación con verosimilitud (cuasi-Poisson no tiene AIC)
        df      AIC
cr_pois  2 909.0056
cr_nb0   3 752.0169

Cuasi-Poisson vs. binomial negativa: ajuste e incertidumbre

nuevoW <- tibble(Wt = seq(min(hc$Wt), max(hc$Wt), length.out = 100))
pr <- \(m, nm, crit, ...) {
  p <- predict(m, newdata = nuevoW, se.fit = TRUE, ...)
  tibble(modelo = nm, lWt = log(nuevoW$Wt), mu = exp(p$fit),
         lo = exp(p$fit - crit * p$se.fit), hi = exp(p$fit + crit * p$se.fit))
}
curv <- bind_rows(pr(cr_m2, "Cuasi-Poisson", qt(.975, df.residual(cr_m2))),
                  pr(cr_nb, "Bin. negativa", qnorm(.975), dispersion = 1))

ggplot() +
  geom_point(data = hc, aes(log(Wt), Sat), alpha = .5) +
  geom_ribbon(data = curv, aes(lWt, ymin = lo, ymax = hi, fill = modelo), alpha = .25) +
  geom_line(data = curv, aes(lWt, mu, colour = modelo, linetype = modelo), linewidth = 1) +
  scale_colour_manual(values = c(col1, col2)) + scale_fill_manual(values = c(col1, col2)) +
  labs(x = "log(peso)", y = "Número de satélites", colour = NULL, fill = NULL, linetype = NULL)

Las diferencias aparecen en los pesos grandes (sistemático) y en la forma de las bandas (aleatorio).

¿Cuántos ceros esperaría cada modelo?

esp0_pois <- sum(dpois(0, fitted(cr_pois)))
esp0_nb   <- sum(dnbinom(0, mu = fitted(cr_nb0), size = cr_nb0$theta))
c(observados = sum(hc$Sat == 0), Poisson = esp0_pois, Bin_negativa = esp0_nb)
  observados      Poisson Bin_negativa 
    62.00000     15.76238     50.06323 

Compara los tres números: una Poisson suele subestimar los ceros cuando hay sobredispersión, y la binomial negativa los acomoda mejor. Si aún quedara exceso de ceros, el paso siguiente serían modelos con inflación de ceros o de barrera (pscl, glmmTMB), que van más allá de este capítulo.

Cierre

Resumen

  • Poisson (liga log): efectos multiplicativos; desviación \(\approx\chi^2_{n-p}\) si \(y_i\ge3\); usar residuos cuantílicos.
  • Tasas \(\Rightarrow\) offset \(\log T\) (coeficiente fijo en 1).
  • Tablas de contingencia: el GLM Poisson es válido bajo cualquier esquema de muestreo si se incluyen los términos de los márgenes fijos. Lo importante son las interacciones; colapsar tablas incorrectamente produce paradojas de Simpson. Con respuesta binaria, el modelo binomial es equivalente.
  • Ceros estructurales: se eliminan de la tabla (o peso 0).
  • Sobredispersión (desviación o Pearson \(\gg\) gl): cuasi-Poisson (\(\phi\mu\), sin verosimilitud) o binomial negativa (\(\mu+\mu^2/k\), con verosimilitud y AIC); alternativa: errores robustos.
  • Los \(\hat\beta\) cambian poco, pero los errores estándar y las conclusiones sí.

Ejercicios propuestos

  1. Problema 10.1 del libro: demuestra que la binomial negativa con \(k\) fijo es una EDM y obtén su desviación unitaria.
  2. Problema 10.2: prueba que con intercepto y liga log la desviación de Poisson se reduce a \(2\sum y\log(y/\hat\mu)\) y que \(\sum\hat\mu_i=m\).
  3. Danish lung cancer: ajusta una binomial negativa o cuasi-Poisson a m2. ¿Cambia algo? ¿Qué dice el estadístico de Pearson?
  4. Kidney stones: reproduce la tabla de fitted values de los 9 modelos y verifica que Size*(Method+Outcome) produce razones de momios Method–Outcome iguales a 1 en cada estrato.
  5. Problema 10.8: simplifica el modelo de dyouth por pasos (step()) y compara con dy_m1.
  6. Horseshoe crabs: ajusta de nuevo la binomial negativa con Col y Spine. ¿Cambia la conclusión? Prueba además un modelo con inflación de ceros (pscl::zeroinfl) y compara con AIC.

Referencias

  • Dunn, P. K., Smyth, G. K. (2018). Generalized Linear Models With Examples in R. Springer. Cap. 10.
  • Dunn, P. K., Smyth, G. K. (1996). Randomized quantile residuals. J. Comput. Graph. Statist., 5, 236–244.
  • Venables, W. N., Ripley, B. D. (2002). Modern Applied Statistics with S. Springer (MASS::glm.nb).
  • Paquete GLMsData: datos del libro. Paquete statmod: qresid().