Los niveles pequeños también mandan

estadística
modelos mixtos
R
2026
Por qué incluir o ignorar un nivel granular en un modelo mixto cambia las estimaciones de los niveles superiores
Published

July 31, 2026

Note

Cambiar ## Listening

El otro día en el curro me pasó algo que me hizo reflexionar.

Tienes un modelo mixto con estructura jerárquica: provincias, municipios, observaciones dentro de esos municipios Ajustas el modelo solo con el nivel provincia. Una provincia te sale con una estimación muy extrema que no cuadra con lo que esperas. Ahora haces un modelo incluyendo la variable municipio como nivel aleatorio y resulta que ahora la estimación a nivel provincia se modera drásticamente para esa provincia.

¿Por qué?

La respuesta tiene que ver con cómo los modelos mixtos reparten la varianza entre niveles. Lo interesante es que al meter el municipio , no sólo explica su propia varianza, sino que que cambia las estimaciones de los niveles superiores. Cuando uno empieza a estudiar modelos jerárquicos lo normal es pensar en que la estimación a nivel más pequeño se ve influenciada por las del nivel superior, pero no es así. En realidad, los niveles superiores también se ven influenciados por los niveles inferiores.

El ejemplo

A ver si soy capaz de contarlo de forma que se entienda.

Simulamos datos con tres niveles: provincia → municipio → observación. 20 provincias, 20 observaciones por municipio. Media nacional: −3.

Dos provincias son de especial interés:

  • Provincia heterogénea (prov_het): su efecto verdadero de provincia es 0 — es una provincia normal. Pero tiene solo 3 municipios muy dispares entre sí, con media global de −10, lo que hace que la media bruta de la provincia sea −13. (-3 dela media nacional más -10 derivada de lo que ha salido en sus municipios) La extremidad viene de los municipios, no de la provincia. El número reducido de municipios (3 frente a 8 del resto) amplifica el problema: menos datos para estimar la media de la provincia y más varianza interna.

  • Provincia homogénea (prov_hom): su efecto verdadero de provincia es −5 — genuinamente por debajo de la media. Tiene 8 municipios compactos y homogéneos. Media bruta: −4.

El resto de provincias tienen 8 municipios, efectos moderados y municipios homogéneos, con medias alrededor de −3.

Mostrar código
library(tidyverse)
library(lme4)

set.seed(42)

grand_mean  <- -3   # efecto global de -3, común a todas las provincias
n_prov      <- 20
n_mun_het   <-  3   # prov_het: pocos municipios
n_mun_resto <-  8   # resto: más municipios
n_obs       <- 20

(ef_prov <- c(
   0,                           # prov_het: efecto real = 0
  -5,                           # prov_hom: efecto real = -5
  rnorm(n_prov - 2, 0, 0.6)    # resto: moderadas
))
#>  [1]  0.00000000 -5.00000000  0.82257507 -0.33881890  0.21787705  0.37971756
#>  [7]  0.24256099 -0.06367471  0.90691320 -0.05679542  1.21105423 -0.03762846
#> [13]  0.78292179  1.37198724 -0.83331642 -0.16727326 -0.07999280  0.38157024
#> [19] -0.17055175 -1.59387325

# prov_het: 3 municipios muy dispares, media = -10  →  media provincia ≈ -13
(ef_mun_het <- c(-35, -10, 15))   # media = -10, sd ≈ 20.2
#> [1] -35 -10  15

# prov_hom: 8 municipios compactos  →  media provincia ≈ -4
# ponemos 1 en rnorm para quue sea que los municipios se desvián  en +1 del 
# efecto real de provincia, por lo que su media raw será mezcla de grand_mean (-3), 
# provincia (-5) y municipio +1 total de -7 más o menos
(ef_mun_hom <- rnorm(n_mun_resto, 1, 0.4))
#> [1] 0.02381323 1.52804534 0.87734456 0.28747663 0.93123306 1.48586988 1.75807738
#> [8] 0.82781235

Ahora juntamos todo

Mostrar código
datos <- bind_rows(
  # prov_het
  map_dfr(seq_len(n_mun_het), function(m) {
    tibble(
      provincia = "prov_01",
      municipio = paste0("prov_01_mun_", m),
      tipo      = "het (efecto real = 0, n_mun = 3, media ≈ -13)",
      y         = grand_mean + ef_prov[1] + ef_mun_het[m] + rnorm(n_obs, 0, 0.5)
    )
  }),
  # prov_hom y resto
  map_dfr(2:n_prov, function(p) {
    n_m  <- n_mun_resto
    tipo <- if (p == 2) "hom (efecto real = -5, n_mun = 8, media ≈ -4)" else "resto"
    ef_m <- if (p == 2) ef_mun_hom else rnorm(n_m, 0, 0.3)
    map_dfr(seq_len(n_m), function(m) {
      tibble(
        provincia = paste0("prov_", sprintf("%02d", p)),
        municipio = paste0("prov_", sprintf("%02d", p), "_mun_", m),
        tipo      = tipo,
        y         = grand_mean + ef_prov[p] + ef_m[m] + rnorm(n_obs, 0, 0.5)
      )
    })
  })
)
Mostrar código

DT::datatable(datos)

Así se ven los municipios dentro de cada provincia de interés:

Mostrar código
datos |>
  filter(provincia %in% c("prov_01", "prov_02")) |>
  ggplot(aes(x = municipio, y = y)) +
  geom_jitter(width = 0.1, alpha = 0.3, color = "steelblue") +
  stat_summary(fun = mean, geom = "point", size = 3, color = "firebrick") +
  stat_summary(
    fun = mean, geom = "hline",
    aes(yintercept = after_stat(y), group = provincia),
    linetype = "dashed", color = "firebrick"
  ) +
  facet_wrap(~tipo, scales = "free_x") +
  labs(
    x = NULL, y = "y",
    title = "Municipios dentro de cada provincia de interés",
    subtitle = "Puntos rojos = media del municipio  |  línea = media de la provincia"
  ) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 7))

prov_het tiene solo 3 municipios: uno en torno a −38, otro en −13 y otro en +12. Su media es −13 (le añade -10 al -3 de la media global) pero la dispersión interna es enorme y hay poca información para estimar la media de la provincia. prov_hom tiene 8 municipios, todos apretados alrededor de −7.

Los dos modelos

Para cada municipio hemos simulado 20 observaciones. Y ahora ajustamos los dos modelos. En el primer modelo, aunque usamos todos los datos no tenemos en cuenta la estructura de municipios, por lo que todas las observaciones de la misma provincia, el modelo considera que son observaciones a nivel de provincia, no sabe que la dispersión es debida a los municipios. En esta provincia heterogénea con 3 muncipios el modelo solo sabe que tiene 60 observaciones (3 x 20) y que son de la misma provincia.

Mostrar código
mod_sin_mun <- lmer(y ~ 1 + (1 | provincia),                    data = datos)
mod_con_mun <- lmer(y ~ 1 + (1 | provincia) + (1 | municipio),  data = datos)
Mostrar código
arm::display(mod_sin_mun)
#> lmer(formula = y ~ 1 + (1 | provincia), data = datos)
#> coef.est  coef.se 
#>    -3.61     0.55 
#> 
#> Error terms:
#>  Groups    Name        Std.Dev.
#>  provincia (Intercept) 2.45    
#>  Residual              2.92    
#> ---
#> number of obs: 3100, groups: provincia, 20
#> AIC = 15541.1, DIC = 15536.3
#> deviance = 15535.7
Mostrar código
arm::display(mod_con_mun)
#> lmer(formula = y ~ 1 + (1 | provincia) + (1 | municipio), data = datos)
#> coef.est  coef.se 
#>    -3.43     0.41 
#> 
#> Error terms:
#>  Groups    Name        Std.Dev.
#>  municipio (Intercept) 3.15    
#>  provincia (Intercept) 1.43    
#>  Residual              0.50    
#> ---
#> number of obs: 3100, groups: municipio, 155; provincia, 20
#> AIC = 5566.1, DIC = 5558.1
#> deviance = 5558.1

Varianzas estimadas: el mecanismo

Mostrar código
get_var <- function(mod, nivel) {
  as.data.frame(VarCorr(mod)) |> filter(grp == nivel) |> pull(vcov)
}

data.frame(
  componente = c(
    "sigma2_provincia  (sin municipios)",
    "sigma2_provincia  (con municipios)",
    "sigma2_municipio  (con municipios)",
    "sigma2_residual   (sin municipios)",
    "sigma2_residual   (con municipios)"
  ),
  varianza = c(
    get_var(mod_sin_mun, "provincia"),
    get_var(mod_con_mun, "provincia"),
    get_var(mod_con_mun, "municipio"),
    attr(VarCorr(mod_sin_mun), "sc")^2,
    attr(VarCorr(mod_con_mun), "sc")^2
  )
)
#>                           componente  varianza
#> 1 sigma2_provincia  (sin municipios) 6.0171773
#> 2 sigma2_provincia  (con municipios) 2.0488075
#> 3 sigma2_municipio  (con municipios) 9.9044626
#> 4 sigma2_residual   (sin municipios) 8.5311928
#> 5 sigma2_residual   (con municipios) 0.2504554
  • Sin municipios: prov_het aparece con media −13, muy lejos de la media nacional. El modelo interpreta eso como señal real del nivel provincia y estima \(\sigma^2_{\text{prov}}\) grande. Con \(\sigma^2_{\text{prov}}\) grande, el factor de shrinkage es bajo y los BLUPs se contraen poco — prov_het se queda cerca de −13.

  • Con municipios: la heterogeneidad interna de prov_het (municipios de −43 a +17) infla \(\sigma^2_{\text{mun}}\). El modelo reconoce que gran parte de lo que parecía señal de provincia era en realidad varianza entre municipios. \(\sigma^2_{\text{prov}}\) cae. Con \(\sigma^2_{\text{prov}}\) más pequeña, el shrinkage aumenta y el BLUP de prov_het se contrae mucho más hacia la media nacional.

En palabras llanas, si no se tiene en cuenta el municipio el modelo para la provincia heterogénea , no sabe que esa variabilidad es debida a que cada municipio es de su padre y de su madre, y aunque ve datos dispersos piensa que esa variabilidad es sólo a nivel de provincia, cuando tiene en cuenta que hay municipios es capaz de explicar más parte de la varianza total (se reduce la residual).

Esto va a afectar a las predicciones que hagamos a nivel de provincia.

Predicciones a nivel de provincia

Mostrar código
datos_prov <- datos |> distinct(provincia, tipo) |> arrange(provincia)

preds <- bind_rows(
  datos_prov |> mutate(
    pred   = predict(mod_sin_mun, newdata = datos_prov,
                     re.form = ~(1 | provincia)),
    modelo = "Sin municipios"
  ),
  datos_prov |> mutate(
    pred   = predict(mod_con_mun, newdata = datos_prov,
                     re.form = ~(1 | provincia)),
    modelo = "Con municipios"
  )
)

preds |>
  mutate(
    provincia = fct_reorder(provincia, pred),
    forma     = tipo
  ) |>
  ggplot(aes(x = pred, y = provincia,
             color = modelo, shape = forma)) +
  geom_vline(xintercept = grand_mean, linetype = "dashed", color = "grey50") +
  geom_point(size = 3, alpha = 0.85,
             position = position_dodge(width = 0.2)) +
  scale_color_manual(values = c(
    "Sin municipios" = "firebrick",
    "Con municipios" = "steelblue"
  )) +
  scale_shape_manual(values = c(
    "het (efecto real = 0, n_mun = 3, media ≈ -13)" = 17,
    "hom (efecto real = -5, n_mun = 8, media ≈ -4)" = 15,
    "resto"                                          = 16
  )) +
  labs(
    x     = "Predicción a nivel provincia  (re.form = ~(1|provincia))",
    y     = NULL, color = NULL, shape = NULL,
    title = "Predicciones de provincia: sin vs con municipios",
    subtitle = "Línea = media nacional (−3)  |  triángulo = het  |  cuadrado = hom"
  ) +
  theme_minimal() +
  theme(legend.position = "bottom", legend.direction = "vertical")

El triángulo (prov_het) se desplaza notablemente entre los dos modelos: sin municipios (azul) está cerca de su media bruta (−13); con municipios se acerca bastante a la media nacional porque el modelo ahora sabe que esa extremidad era ruido de municipio, no señal de provincia.

El cuadrado (prov_hom) apenas se mueve: su extremidad es genuina, los municipios son consistentes, y añadir el nivel municipio no cambia mucho su estimación.

El resto de provincias (círculos) tienen pequeñas variaciones porque su variabilidad interna es baja.

Mostrar código
preds |>
  pivot_wider(names_from = modelo, values_from = pred) |>
  mutate(
    media_bruta = map_dbl(provincia, ~ mean(datos$y[datos$provincia == .x])),
    cambio      = `Con municipios` - `Sin municipios`
  ) |>
  arrange(`Sin municipios`) |>
  mutate(across(where(is.numeric), \(x) round(x, 2))) |>
  select(provincia, tipo, media_bruta,
         `Sin municipios`, `Con municipios`, cambio) |>
  knitr::kable(
    col.names = c("Provincia", "Tipo", "Media bruta",
                  "Pred sin mun", "Pred con mun", "Cambio"),
    caption   = "Predicciones a nivel provincia (re.form = ~(1|provincia))"
  )
Predicciones a nivel provincia (re.form = ~(1|provincia))
Provincia Tipo Media bruta Pred sin mun Pred con mun Cambio
prov_01 het (efecto real = 0, n_mun = 3, media ≈ -13) -13.01 -12.80 -7.09 5.70
prov_02 hom (efecto real = -5, n_mun = 8, media ≈ -4) -7.06 -7.03 -5.69 1.34
prov_20 resto -4.51 -4.51 -4.10 0.40
prov_15 resto -3.89 -3.89 -3.71 0.17
prov_19 resto -3.48 -3.48 -3.46 0.02
prov_04 resto -3.46 -3.46 -3.45 0.01
prov_08 resto -3.20 -3.20 -3.28 -0.08
prov_10 resto -3.19 -3.19 -3.28 -0.09
prov_16 resto -3.13 -3.14 -3.24 -0.11
prov_17 resto -3.00 -3.01 -3.16 -0.15
prov_05 resto -3.00 -3.00 -3.16 -0.16
prov_12 resto -2.85 -2.85 -3.07 -0.21
prov_06 resto -2.77 -2.78 -3.02 -0.24
prov_07 resto -2.75 -2.75 -3.00 -0.25
prov_18 resto -2.64 -2.65 -2.94 -0.29
prov_13 resto -2.34 -2.35 -2.75 -0.40
prov_03 resto -2.23 -2.24 -2.68 -0.44
prov_09 resto -2.13 -2.14 -2.62 -0.48
prov_14 resto -1.83 -1.84 -2.43 -0.59
prov_11 resto -1.79 -1.80 -2.41 -0.60

Por qué no basta con que ambas tengan media −13

Podría parecer que la comparación más limpia sería poner las dos provincias de interés exactamente en −13. Sin embargo, si prov_hom también tiene media −13 con efecto genuino de −10, \(\sigma^2_{\text{prov}}\) en el modelo con municipios sigue siendo grande (prov_hom la mantiene inflada), y el shrinkage adicional de prov_het se diluye.

El efecto es máximo cuando prov_het es la principal responsable de que \(\sigma^2_{\text{prov}}\) sea grande en el modelo sin municipios. Al añadir municipios, esa varianza se reabsorbe, \(\sigma^2_{\text{prov}}\) cae, y prov_het recibe el impacto completo del shrinkage aumentado.

Resumen

Situación \(\sigma^2_{\text{prov}}\) estimada Shrinkage de la provincia extrema
Sin municipios, prov_het heterogénea Grande (inflada por prov_het) Poco — BLUP cerca de media bruta
Con municipios, prov_het heterogénea Pequeña (heterogeneidad reabsorbida) Mucho — BLUP se acerca a media nacional
Con o sin municipios, prov_hom homogénea Similar en ambos modelos Cambio pequeño

La moraleja: omitir un nivel intermedio no solo ignora ese nivel — distorsiona las estimaciones del superior. Y la distorsión es mayor cuanto más heterogéneo sea el nivel omitido, porque esa heterogeneidad infla \(\sigma^2_{\text{prov}}\) y reduce artificialmente el shrinkage.