Generales 2026, todo bayesiano (I): el voto

2026
encuestas electorales
análisis bayesiano
stan
Bayesiano a tope. Intentando capturar la incertidumbre
Autor/a

José Luis Cañadas Reche

Fecha de publicación

20 de octubre de 2026

Fecha de última modificación

26 de octubre de 2026

Ver el código
library(tidyverse)

dir_fb <- here::here("2026/10/full-bayesian")
leer <- \(f) read_csv(file.path(dir_fb, f), show_col_types = FALSE)
num_es <- function(x, d = 1) {
  format(round(x, d), decimal.mark = ",", trim = TRUE)
}
pct_es <- function(p, d = 0) paste0(num_es(100 * p, d), " %")

# resultado del modelo conjunto (12_ajustar_conjunto.R), del pipeline de
# octubre (05_comparar.R, versión "actual"), de la ablación del error
# (16_ablacion_error.R) y de la sensibilidad de la Omega
# (17_sensibilidad_omega.R), los cuatro con el escenario mapa
may_cj <- leer("conjunto_mayorias.csv")
esc_cj <- leer("conjunto_escanos.csv")
may_oct <- leer("comparar_mayorias.csv") |> filter(version == "actual")
esc_oct <- leer("comparar_escanos.csv") |> filter(version == "actual")
esc_sin <- leer("conjunto_sin_error.csv")
may_om <- leer("sensibilidad_omega_mayorias.csv")

p_cj <- may_cj$prob[may_cj$escenario == "PP + VOX >= 176"]
p_oct <- may_oct$p_pp_vox_176
pp_cj <- esc_cj |> filter(partido == "PP")
pp_oct <- esc_oct |> filter(partido == "PP")
pp_sin <- esc_sin |> filter(partido == "PP")
p_om_vox <- may_om$p_pp_vox_176[may_om$variante == "vox_pp_05"]

Por qué otro modelo

En el post del metaanálisis hice una aproximación bayesiana a como agregar encuestas. En ese post se hacía en dos pasos, un modelo para estimar el % de voto a los partidos principales y otro modelo complementario para los partidos regionales. Pero se quedaba un poco cojo, había decisiones tomadas a ojo, como el sesgo histórico del PSOE.

Pero el espiritu de un modelo bayesiano es incluir las distintas fuentes de incertidumbre que pensemos que afectan al voto. Uno podría pensar en la volatilidad de las encuestas antes de las elecciones, en el error histórico de las casas, en el error conjunto que hayan tenido, en si cuando fallan a la vez en un partido aciertan en otro. En fin, multitud de cosas. En bayesiano uno también piensa en aprovechar toda la info disponible, como ya mismo en un post lujuria e intervención y se puede condicionar incluso por colliders, a pesar de lo que diga el bueno de Judea.
Así que, por qué no aprovechar la info que tenemos de elecciones generales anteriores e incluso de autonómicas, y meter también incluso los resultados reales que se dieron en el pasado..

Pongo aquí alguna de las diferencias respecto al primer post.

Pieza post inicial de meta análisis Aquí
Cuánto se mueve el voto hasta el día E Tendencia lineal, extrapolada Paseo aleatorio, con la volatilidad aprendida de seis campañas
Cuánto suelen fallar las encuestas Desviaciones medidas en cinco generales, tratadas como conocidas Estimado, con su propia incertidumbre
Sesgo del PSOE +1,32 puntos, encogido a la mitad “a ojo” Estimado con pooling
Correlación del error PP–Vox +0,92 encogido a +0,5 “a ojo” Aprendida de 44 elecciones
Error del bloque de partidos regionales 1,5 puntos, un supuesto Sin error propio: absorbe en proporción el ajuste de los grandes (supuesto declarado)

Así que lo ponemos todo junto en un único modelo bayesiano:

  • Las encuestas de las seis campañas generales desde 2015

  • Los resultados de las cinco que ya se votaron

  • Lo que han fallado las encuestas en 39 elecciones autonómicas.

Todo se estima a la vez, y cada trozo de la incertidumbre final sale de un dato o de un prior que se puede discutir.

Y como no podía ser de otra forma, sale lo mismo que en el primer post, jajaj, aquí no hay magia.

La media apenas se mueve (el PP en 128 escaños frente a 129), pero la horquilla del PP pasa de 114–143 a 104–152 y la probabilidad de mayoría de PP y Vox baja al 95 %.

Ver el código
nombres <- c(
  PP = "PP",
  PSOE = "PSOE",
  VOX = "Vox",
  SUMAR = "Frente Amplio (Sumar)",
  ERC = "ERC",
  AA = "Adelante Andalucía",
  PODEMOS = "Podemos"
)
bind_rows(
  esc_cj |> mutate(version = "Todo bayesiano"),
  esc_oct |> mutate(version = "Pipeline de octubre")
) |>
  filter(partido %in% names(nombres)) |>
  mutate(
    partido = factor(nombres[partido], rev(nombres)),
    version = factor(version, c("Pipeline de octubre", "Todo bayesiano"))
  ) |>
  ggplot(aes(x = mediana, y = partido, colour = version)) +
  geom_pointrange(
    aes(xmin = q05, xmax = q95),
    position = position_dodge(width = 0.6)
  ) +
  scale_colour_manual(values = c("grey60", "#b8742a")) +
  labs(x = "Escaños", y = NULL, colour = NULL) +
  theme_minimal(base_size = 12) +
  theme(legend.position = "top")
Figura 1: Escaños el 29-N con el modelo conjunto y con el pipeline de octubre, los dos con el mapa de listas a 26 de octubre (mediana e intervalo del 90 %).

Lo ancho de las horquillas sale del modelo. En cada draw de la posterior tenemos tanto el voto latente (como la agregación de las encuestas) en el día E de la elección, pero gracias a que sabemos el resultado real en elecciones anteriores hemos podido estimar una distribución de cuánto se equivocan las encuestas para cada partido. Si, ya sé que unas veces se habrán equivocado más a favor de uno o de otro, pero eso es lo que hacemos en bayesiano, sabemos que eso ha pasado en el pasado, cuantificamos cuánto y lo añadimos a nuestras estimaciones. Básicamente es reconocer los límites de hasta donde puede llegar el modelo, y en vez de esconder esa incertidumbre bajo una apariencia de exactitud, la modelamos expresamente.

Y luego para llevar a escaños podemos llevar solo el voto latente (sin ese error que sabemos que ha existido) o también añadir el error y aplicarle luego toda la capa de asignación de escaños que ya hemos contado en post anteriores.

Ver el código
knitr::kable(
  bind_rows(
    pp_sin |> mutate(Versión = "Todo bayesiano, solo el voto latente"),
    pp_cj |> mutate(Versión = "Todo bayesiano, con el error estimado"),
    pp_oct |> mutate(Versión = "Pipeline de primer post (error fijado a mano)")
  ) |>
    transmute(
      Versión,
      Mediana = mediana,
      `IC90` = paste0(round(q05), "–", round(q95)),
      `Ancho del IC90` = round(q95 - q05)
    ),
  align = "lrrr"
)
Tabla 1
Versión Mediana IC90 Ancho del IC90
Todo bayesiano, solo el voto latente 131 121–139 18
Todo bayesiano, con el error estimado 128 104–152 48
Pipeline de primer post (error fijado a mano) 129 114–143 29

La horquilla más estrecha corresponde a la que sale de proyectar escaños usando solo el voto latente, luego la que usamos en los posts anteriores, donde añadimos un error a ojo, y el intervalo más ancho es el del modelo bayesiano completo, donde se estima también la distribución del error histórico

Cuando uno ve un intervalo tan ancho, lo primero que uno piensa, es “este tipo no se quiere mojar”, y en muchos casos es así. Aquí sin embargo va más por poner sobre la mesa toda la incertidumbre que hay, no trabajo para un periódico, no tengo la presión de acertar o no, ni de dar horquillas mega estrechas jugandome el prestigio. Pongo lo que sale tras modelar, como he podido, las fuentes de incertidumbre. ¿Mejorable? Fijo.

Con un IC al 90% uno pensaría que estaría bien que, al repetir 100 veces las elecciones, el verdadero valor del voto estuviera en alrededor de 90 de los 100 intervalos que construimos. Esa es la idea frecuentista de cobertura, la del procedimiento repetido muchas veces. El intervalo que da el modelo es otra cosa: un 90% predictivo bayesiano dice que, dados el modelo y los datos, la probabilidad de que el resultado del 29-N caiga dentro es del 90%. No se puede repetir la elección 100 veces, pero sí que puedo hacer un viaje al pasado y comprobar la cobertura empírica: comparar como habrían quedado los intervalos si hubiera hecho el mismo procedimiento para elecciones pasadas. Eso lo pongo al final del post.

Que todo lo que va de las encuestas al voto nacional es un solo posterior: no hay trozos ajustados por separado ni capas puestas a mano entre medias. Lo que se queda fuera del full bayesian.

  • El paso de votos a escaños (reparto provincial, umbral y D’Hondt), es una “maquinaria” que toma como input la posterior del voto latente o la del voto latente con error, pero a nivel nacional. La proyección a escaños a nivel de provincia es una metodología ad hoc que ya he explicado, hay otras, por supuesto. Lo bueno, que la incertidumbre de la posterior del voto se propaga en cada simulación. Es lo que se hace en lo que se conoce como bayesian decision theory

  • Algunos supuestos explícitos que no se me ha ocurrido como meterlo todo en el bayesiano. Como la transferencia del voto podemos en Cataluña, la renuncia del Frente Amplio en Teruel (su voto se reparte entre el PSOE y fuera), el punto de partida de Adelante Andalucía o el tamaño de las encuestas antiguas que no lo publicaban. En esos casos, al menos utilizo una priori plausible pero sin verosimilitud.

Esto va en dos posts. Este es el del voto: cómo se estima y cuánto puede fallar. El siguiente, el de cómo ese voto se convierte en escaños.

Ver el código
# tamaño de los datos del modelo conjunto (11_datos_conjunto.R)
source(here::here("2026/10/full-bayesian/11_datos_conjunto.R"))
dc <- datos_conjunto()
#> Fuera (sin n publicado): 1 encuestas — InvyMark 
#> Fuera (casas_fuera: CIS, ElectoPanel ): 18 encuestas
n_camp <- tibble(
  campana = dc$campanas,
  encuestas = as.integer(table(factor(dc$enc_campana, seq_len(dc$C))))
)
n_2026_grandes <- sum(str_detect(dc$encuestas$encuesta, "^2026-(g|ag)"))
n_2026_desglose <- sum(str_detect(dc$encuestas$encuesta, "^2026-d"))
n_pasadas <- sum(n_camp$encuestas[n_camp$campana != "2026"])

Mapa conceptual, por aclararnos un poco.

La idea es sencilla : todas las campañas generales funcionan igual. Hay un voto que no vemos y que va cambiando de semana en semana. Las encuestas lo intentan medir, con su ruido y con la metodología propia de cada casa, que no conocemos. Y el día de la votación, el resultado es el que es y las encuestas fallan o aciertan en mayor o menor medida. Para las elecciones del 29 de Noviembre no tenemos ni zorra de si fallarán mucho o poco, pero si que podemos aprovechar lo que pasó en elecciones pasadas para calibrar en cierta forma como lo hicieron en el pasado y con la esperanza o desesperanza de que este año sea parecido (una distribución de nuevo). Al fin y al cabo eso es lo que hacemos cuando hacemos inferencia.

En resumen de las cinco generales anteriores ya conozco el resultado. De la de 2026, todavía no. Y un solo modelo aprende de las seis a la vez:

Ver el código
flowchart TD
  E1["Encuestas 2026<br/>grandes + desglose regional"]:::datos
  E2["Encuestas de las campañas<br/>20D, 26J, 28A, 10N y 23J"]:::datos
  C["Efecto de cada casa"]:::modelo
  V["Voto latente de cada campaña<br/>paseo aleatorio semanal"]:::modelo
  R["Resultados de<br/>esas cinco generales"]:::datos
  A["Última semana de encuestas<br/>en 39 autonómicas"]:::datos
  ER["Error del consenso el día E<br/>sesgo, tamaño y correlación"]:::modelo
  F["Voto el 29-N<br/>voto latente + error"]:::salida
  S["Escaños<br/>(segunda parte)"]:::salida
  E1 --> V
  E2 --> V
  C --> V
  V --> ER
  R --> ER
  A --> ER
  ER --> F
  F -.-> S
  classDef datos fill:#e3eefb,stroke:#3b6ea8,color:#1a1a1a
  classDef modelo fill:#fdebd3,stroke:#b8742a,color:#1a1a1a
  classDef salida fill:#e2f2e3,stroke:#3f8a46,color:#1a1a1a
flowchart TD
  E1["Encuestas 2026<br/>grandes + desglose regional"]:::datos
  E2["Encuestas de las campañas<br/>20D, 26J, 28A, 10N y 23J"]:::datos
  C["Efecto de cada casa"]:::modelo
  V["Voto latente de cada campaña<br/>paseo aleatorio semanal"]:::modelo
  R["Resultados de<br/>esas cinco generales"]:::datos
  A["Última semana de encuestas<br/>en 39 autonómicas"]:::datos
  ER["Error del consenso el día E<br/>sesgo, tamaño y correlación"]:::modelo
  F["Voto el 29-N<br/>voto latente + error"]:::salida
  S["Escaños<br/>(segunda parte)"]:::salida
  E1 --> V
  E2 --> V
  C --> V
  V --> ER
  R --> ER
  A --> ER
  ER --> F
  F -.-> S
  classDef datos fill:#e3eefb,stroke:#3b6ea8,color:#1a1a1a
  classDef modelo fill:#fdebd3,stroke:#b8742a,color:#1a1a1a
  classDef salida fill:#e2f2e3,stroke:#3f8a46,color:#1a1a1a
Figura 2: El modelo conjunto: cuatro fuentes de datos, un solo posterior. En azul, los datos; en naranja, las piezas del modelo; en verde, lo que sale.

Las piezas de en medio son las que voy a ir contando una a una:

  1. El voto latente. Lo que diría la media de las casas cada semana (si hicieran encuestas cada semana). Se mueve poco a poco, y cuánto se mueve lo aprende el modelo, campaña a campaña.
  2. El efecto casa. Cada encuestadora tira para su lado. Dentro de una campaña, unas manías compensan a otras: el voto latente es la media de las casas, no lo que diga ninguna en concreto.
  3. El error del consenso. Aunque las casas se compensen entre sí, todas juntas pueden meter la pata a la vez. Y eso no se ve en las encuestas: solo se ve el día de la votación. Por eso hacen falta los resultados pasados y las autonómicas.

Con los datos en la mano, son 445 encuestas de las cinco campañas pasadas, 39 encuestas de 2026 con los grandes partidos, 100 con el desglose de los partidos regionales y 217 encuestas de la última semana en 39 autonómicas.

Las tablas se pueden ordenar y filtrar. En las campañas pasadas, “Otros” lleva dentro a Cs en 2015-2019; en 2026, a los partidos regionales y al resto. Las encuestas antiguas que no publicaban el tamaño de la muestra van con n = 1.000, que algo hay que poner. Y el CIS y ElectoPanel no entran con los grandes partidos.

Código de las tablas
nombre_campana <- c(
  `20d` = "20D-2015",
  `26j` = "26J-2016",
  `28a` = "28A-2019",
  `10n` = "10N-2019",
  `23j` = "23J-2023",
  `2026` = "29N-2026"
)
# % de cada celda dentro de su encuesta (en el desglose, % del BLOQUE)
a_pct <- function(d) {
  d |>
    group_by(encuesta) |>
    mutate(n = sum(y), pct = 100 * y / n) |>
    ungroup()
}
tabla_dt <- function(d, cols_pct) {
  DT::datatable(
    d,
    rownames = FALSE,
    filter = "top",
    options = list(pageLength = 10, scrollX = TRUE)
  ) |>
    DT::formatRound(cols_pct, digits = 1, mark = ".", dec.mark = ",")
}

grandes <- bind_rows(
  map_dfr(elecciones_hist, encuestas_pasadas),
  encuestas_2026_grandes()
) |>
  a_pct() |>
  mutate(
    celda = recode(
      celda,
      RESTO = "Otros",
      BLOQUE = "Otros",
      IZQ = "Sumar + Podemos",
      VOX = "Vox",
      SUMAR = "Sumar",
      PODEMOS = "Podemos"
    ),
    campana = nombre_campana[campana]
  ) |>
  select(encuesta, campana, casa, fecha, n, celda, pct) |>
  pivot_wider(names_from = celda, values_from = pct) |>
  select(-encuesta) |>
  relocate(
    any_of(c("PP", "PSOE", "Vox", "Sumar", "Podemos", "SALF", "Otros")),
    .after = n
  ) |>
  arrange(desc(fecha))
#> Fuera (sin n publicado): 1 encuestas — InvyMark 
#> Fuera (casas_fuera: CIS, ElectoPanel ): 18 encuestas

desglose <- encuestas_2026_desglose() |>
  a_pct() |>
  mutate(celda = sub("EH_Bildu", "EH Bildu", celda)) |>
  select(encuesta, casa, fecha, `n (BLOQUE)` = n, celda, pct) |>
  pivot_wider(names_from = celda, values_from = pct) |>
  select(-encuesta) |>
  arrange(desc(fecha))

autonomicas <- read_csv(
  file.path(dir_fb, "errores_autonomicas.csv"),
  show_col_types = FALSE
) |>
  transmute(
    eleccion,
    casa = norm_casa(fuente),
    fecha,
    `PP encuesta` = PP,
    `PP real` = real_PP,
    `PSOE encuesta` = PSOE,
    `PSOE real` = real_PSOE,
    `Vox encuesta` = VOX,
    `Vox real` = real_VOX
  ) |>
  arrange(desc(fecha))
Ver el código
tabla_dt(
  grandes,
  intersect(
    c("PP", "PSOE", "Vox", "Sumar", "Podemos", "SALF", "Otros"),
    names(grandes)
  )
)
Ver el código
tabla_dt(desglose, setdiff(names(desglose), c("casa", "fecha", "n (BLOQUE)")))
Ver el código
tabla_dt(autonomicas, names(autonomicas)[-(1:3)])

El modelo en una página

Para el que prefiera verlo todo de golpe. Parece muy complicado, pero no lo es tanto. Un modelo generativo, donde para llegar al final, que son las encuestas observadas, se modela de donde puede venir el número que dan.

Voto latente que cada encuesta tiene un sesgo y no lo ve tal cual, el como ese voto latente cambia semana a semana y las encuestas solo toman “datos” en ciertos períodos. Y luego se usa el resultado real de elecciones pasadas y comparar con el voto latente que daban las encuestas para estimar como se equivocan . Usamos las autonómicas y el resultado real en estas para estimar mejor ese error y también para ver si por ejemplo se equivocan al repartir , luego se verá que cuando sobreestiman al pp subestiman a vox y al revés.

\[ \begin{aligned} \text{Encuestas:}\quad & \mathbf{y}_i \sim \operatorname{DirMult}\big(n_i,\ \phi_c\, \mathbf{q}_i\big), \qquad \mathbf{q}_i = \operatorname{celdas}\!\big(\operatorname{softmax}(\boldsymbol{\theta}_{c,t_i} + \boldsymbol{\beta}_{c,h_i})\big) \\[4pt] \text{Voto latente:}\quad & \boldsymbol{\theta}_{c,t} = \boldsymbol{\theta}_{c,t-1} + \boldsymbol{\omega}_{c,t}, \qquad \omega_{c,t,k} \sim N\!\big(0,\ \sigma_{c,g(k)}\, e^{\kappa\, \mathbb{1}[t \in \text{campaña}]}\big) \\[2pt] & \log \sigma_{c,g} = \log \sigma_g + \tau\, z_{c,g}, \qquad z_{c,g} \sim N(0, 1) \\[4pt] \text{Casas:}\quad & \beta_{c,h,k} \sim N\big(0,\ \sigma^{\text{casa}}_{g(k)}\big), \qquad \textstyle\sum_h \beta_{c,h,k} = 0 \\[4pt] \text{Resultado:}\quad & \operatorname{logit}(\mathbf{r}_c) - \operatorname{logit}(\mathbf{v}_{c,T}) \sim N\big(\boldsymbol{\mu}^{\text{gen}},\ \operatorname{diag}(\boldsymbol{\sigma}^{\text{gen}})\,\Omega\,\operatorname{diag}(\boldsymbol{\sigma}^{\text{gen}})\big), \qquad \mathbf{v}_{c,T} = \operatorname{softmax}(\boldsymbol{\theta}_{c,T}) \\[4pt] \text{Autonómicas:}\quad & z_{jk} \sim N\big(\mu^{\text{aut}}_k + \delta_{e,k} + b_{h,k},\ s_k\big), \qquad \boldsymbol{\delta}_e \sim N\big(\mathbf{0},\ \operatorname{diag}(\boldsymbol{\sigma}^{\text{aut}})\,\Omega\,\operatorname{diag}(\boldsymbol{\sigma}^{\text{aut}})\big) \\[2pt] & \boldsymbol{\sigma}^{\text{gen}} = \boldsymbol{\sigma}^{\text{aut}}\, e^{\gamma} \end{aligned} \]

Notación: \(k\) es la familia de partido y cada campaña \(c\) tiene las suyas: en 2026 son 14 (PP, PSOE, VOX, Sumar, Podemos, SALF, más Junts, ERC, PNV, EH Bildu, BNG, CC, UPN y el resto); en las pasadas solo se sigue a los grandes, con la coalición de la izquierda en una sola familia (la «SUMAR» de cada época) y todo lo no regional en RESTO. \(g(k)\) es el grupo de volatilidad al que pertenece cada familia: PP, PSOE, VOX y SUMAR cada uno por su lado, Podemos y SALF juntos, los regionales juntos y RESTO. En la línea de las autonómicas, \(k\) recorre los cuatro grupos del error (PP, PSOE, VOX y SUMAR), \(e\) la elección autonómica y \(j\) su encuesta. El resto: \(c\) campaña, \(t\) semana, \(h\) casa, \(i\) encuesta.

Y en cristiano, línea a línea:

  • Encuestas. La encuesta \(i\), de la casa \(h\), en la semana \(t\) de la campaña \(c\), cuenta votos por celdas. Cada celda suma una o varias familias de partidos (en 2026, la celda “otros” suma los regionales). La Dirichlet-multinomial deja que las encuestas varíen más de lo que da el puro muestreo; cuánto, lo dice \(\phi_c\) (si es muy grande, nada más que el muestreo). Las encuestas con desglose regional entran condicionadas al total de “otros” de esa encuesta: solo cuentan cómo se reparte ese total. Muchas son las mismas encuestas que dan los grandes partidos, pero no se cuentan dos veces: la probabilidad de una encuesta multinomial se descompone exactamente en la de los grandes con el total de “otros” por un lado y la del reparto de “otros” por otro.
  • Voto latente. \(\boldsymbol{\theta}_{c,t}\) es lo que diría la media de las casas esa semana, en la escala de la softmax. Cambia cada semana un paso al azar. El tamaño del paso depende del grupo de partidos \(g\) y de la campaña: cada campaña tiene su volatilidad, parecida a la de las demás pero no igual (\(\tau\) mide cuánto difieren). \(\kappa\) dice si el voto se mueve más en las ocho últimas semanas.
  • Casas. Cada casa tira hacia su lado (\(\beta\)), y dentro de cada campaña los efectos suman cero: el voto latente es la media de las casas.
  • Resultado. En las generales pasadas conocemos el resultado \(\mathbf{r}_c\). Lo que se separa del voto latente del día E, en logit, es el error del consenso: un sesgo \(\boldsymbol{\mu}\), un tamaño \(\boldsymbol{\sigma}\) por partido y una correlación \(\Omega\) entre partidos. Para 2026 ese error no se observa: se sortea de la misma normal.
  • Autonómicas. Aportan el error de 217 encuestas de la última semana en 39 elecciones: su propio sesgo, un error común a cada elección (\(\boldsymbol{\delta}_e\)) y un sesgo por casa (\(b\)). Comparten con las generales la correlación \(\Omega\) y el patrón de los tamaños, con un nivel propio (\(\gamma\)).

Las celdas

Ninguna encuesta entra al modelo partido a partido: entra en celdas, que son agrupaciones de familias. Y la verosimilitud se escribe sobre lo que la fuente publicó, ni más ni menos.

Un ejemplo real, que es el que motivó esto. La última Sigma Dos (El Mundo, campo del 1 al 14 de octubre, n = 3.007) no separa a SALF ni a los regionales: publica PP, PSOE, VOX y “Frente Amplio + Podemos” juntos.

Pasa con más encuestas, las opciones que barajaba eran excluir esas encuestas, o asumir cierto reparto. Pero en el modelo bayesiano en Stan se puede hacer otra cosa, meter cada encuesta con la info que aporta.

Celda Lo publicado Familias del modelo que agrupa
PP 31,5 % PP
PSOE 25,3 % PSOE
VOX 19,5 % VOX
Izquierda fuera del PSOE 10,9 % Sumar + Podemos
Resto 12,8 % SALF + los 7 regionales + otros

El multinomial sobre esas celdas hace dos cosas a la vez: exige que la suma cuadre con lo publicado y es indiferente al reparto interior mientras la suma cuadre.

El reparto se decide en base a lo que aportan otras encuestas. Si varias encuestas tienen la info separada de Sumar y Podemos se aprovecha eso, si otras lo publican junto también se aprovecha esa info. En el primer caso sirve para ver como se reparte y el segundo aporta información de la verosimilitud de la suma de ambos partidos.

No hay ninguna línea de código que “rellene” el interior de la celda: es la definición misma de la distribución posterior. Lo mismo que ya pasaba con el desglose regional, que entra condicional al total del BLOQUE: la verosimilitud de una encuesta multinomial se factoriza en la de sus grupos y la condicional del interior, y el modelo la aprovecha entera.

Ver el código
# qué lleva el modelo en la semana de la encuesta de Sigma Dos, a los dos
# bloques que la encuesta publica
semana_ag <- floor(as.numeric(as.Date("2026-10-14") - inicio_2026) / 7) + 1
bloque_ag <- leer("conjunto_tendencia.csv") |>
  filter(semana == semana_ag) |>
  mutate(
    bloque = case_when(
      partido %in% c("SUMAR", "PODEMOS") ~ "izquierda fuera del PSOE",
      partido %in% c("SALF", fams_bloque_cj, "RESTO") ~ "resto"
    )
  ) |>
  group_by(bloque) |>
  summarise(mediana = sum(mediana), .groups = "drop") |>
  filter(!is.na(bloque))
ag_izq <- bloque_ag$mediana[bloque_ag$bloque == "izquierda fuera del PSOE"]
ag_resto <- bloque_ag$mediana[bloque_ag$bloque == "resto"]

Con números: en la semana de la encuesta, el modelo lleva a Sumar y Podemos un 9,6 % juntos (Sigma Dos publicó 10,9) y a SALF, los regionales y los otros un 12,3 % (Sigma Dos publicó 12,8). La encuesta informa sobre las dos sumas, y no dice nada del reparto interior, ese queda a cargo de las encuestas que sí publican cada pieza.

Hay un detalle que hace aún más natural el agregado: “Sumar + Podemos” es exactamente el nivel de detalle con el que el modelo de error razona sobre las elecciones anteriores, cuando fueron juntos. (las generales de 2015–2023 solo conocen a la izquierda de entonces junta).

El voto del 29-N, simulación a simulación, es el voto latente de la última semana de 2026 más un error sorteado, en logit, para PP, PSOE, Vox y Sumar+Podemos. El resto de partidos se reescala para que todo sume 100.

Y lo mismo dibujado como un DAG, con la notación de los modelos gráficos: cada flecha va de un nodo a lo que depende de él. Los círculos grises son datos, los blancos son parámetros y los dobles se calculan a partir de sus padres.

Figura 3: El modelo conjunto como DAG, en una versión simplificada. A la izquierda, las generales; a la derecha, las autonómicas; en medio, lo que comparten. La sub-placa punteada c = 6 (predicción) espeja la de c ≤ 5 (resultados conocidos). Las flechas punteadas son la cadena del paseo aleatorio, sin dibujar todas las semanas. Cada encuesta i usa el voto latente de su semana, t(i), y el efecto de su casa, h(i). Pulsa para verlo en grande.

Se lee de izquierda a derecha:

  • Generales. El voto latente \(\theta_{c,t}\) es el centro: cada semana depende de la anterior (dentro de campaña, la sd se multiplica por \(e^{\kappa \cdot \text{semana}}\)) y de la volatilidad \(\sigma_{c,g}\). Las encuestas \(y_i\) dependen del voto latente de su semana y del efecto de su casa \(\boldsymbol{\beta}_{c,h}\), un vector con un componente por familia (el ruido extra \(\phi_c\) va como nota en la placa de las encuestas). El día E, \(\mathbf{v}_{c,T}\) se compara con el resultado \(r_c\), que solo existe para las cinco generales pasadas (\(c \le 5\)). Para 2026 (\(c = 6\)) el error \(\varepsilon^{2026}\) no se observa: se sortea con el mismo sesgo \(\boldsymbol{\mu}^{\text{gen}}\) y la misma covarianza \(\Sigma^{\text{gen}}\).
  • Autonómicas. El error de cada encuesta \(z_j\) es la suma de un sesgo medio \(\boldsymbol{\mu}^{\text{aut}}\), el error de su elección \(\delta_e\), el sesgo de su casa \(b_h\) y su ruido \(\sigma^{\text{enc}}\) (el \(s_k\) de las ecuaciones de arriba).
  • Lo que comparten. Solo \(\Omega\) y las escalas \(\boldsymbol{\sigma}^{\text{aut}}\), que son independientes entre sí y con las que se montan dos covarianzas: \(\Sigma^{\text{gen}} = \operatorname{diag}(\boldsymbol{\sigma}\, e^{\gamma})\,\Omega\, \operatorname{diag}(\boldsymbol{\sigma}\, e^{\gamma})\) para las generales y \(\Sigma^{\text{aut}} = \operatorname{diag}(\boldsymbol{\sigma})\, \Omega\, \operatorname{diag}(\boldsymbol{\sigma})\) para las autonómicas. Ninguna de las dos es un parámetro nuevo: se calculan a partir de sus padres y por eso van en círculo doble, cada una alimentando su tipo de elección. Los sesgos medios no se comparten: \(\boldsymbol{\mu}^{\text{gen}}\) y \(\boldsymbol{\mu}^{\text{aut}}\) van cada uno en su panel.

Lo que une las encuestas con los resultados reales es \(\mathbf{v}_{c,T}\): el mismo voto latente que sale de las encuestas es el que se compara con lo que se votó. Y lo que une las generales con las autonómicas son \(\Omega\) y \(\boldsymbol{\sigma}^{\text{aut}}\). Por eso es un solo modelo y no varios encadenados.

  • Está dibujada la versión centrada de cada nivel. En el Stan, varios van en la parametrización no centrada (por ejemplo, \(\sigma_{c,g} = \sigma_g\, e^{\tau z_{c,g}}\)), que es el mismo modelo.
  • No dibujo las constantes de los priors (por ejemplo, el voto de la general anterior, que centra \(\theta_{c,1}\)) ni las restricciones de suma cero de los efectos casa.
  • Para que el grafo se pueda leer, algunos objetos van como notas y no como nodos: \(\phi_c\) (nota en la placa de encuestas), \(\theta_{c,1}\) y \(\kappa\) (a la izquierda de la cadena, junto al stub punteado \(\theta_{c,t-1} \to \theta_{c,t}\)) y \(s_0\) y \(\tau_s\) (nota sobre \(\sigma^{\text{aut}}\)). El \(\gamma\) de las generales queda dentro de la fórmula de \(\Sigma^{\text{gen}}\).
  • Es un DAG simplificado, no un traslado literal del Stan: los efectos casa se centran dentro de cada campaña (suman cero), así que los \(\boldsymbol{\beta}_{c,h}\) de una campaña no son marginalmente independientes entre sí, aunque aquí se dibujen como nodos independientes. Lo que el dibujo respeta exactamente son las dependencias: qué alimenta a qué.
  • \(\mathbf{v}^{\text{29-N}}\) se calcula fuera de Stan, simulación a simulación, con el voto latente de la última semana de 2026 y \(\varepsilon^{2026}\).
  • La figura sale de 15_dag.R, que escribe el SVG a mano.

Todos en escala logit, centrados en valores razonables y con margen de sobra para que manden los datos. Los priors pueden ser informativos o no, pero siempre han de ser razonables. En este caso los priors se eligen de forma que se descarten cosas imposibles pero que permita variar lo bastante para que los datos (a través de la verosimilitu actúe)

Parámetro Prior En palabras
\(\boldsymbol{\theta}_{c,1}\) \(N(\log \text{voto de la general anterior},\ 0{,}5)\) Al empezar la campaña, el voto se parece al de la elección anterior, con mucho margen
\(\sigma_g\) (volatilidad) \(N^+(0;\ 0{,}05)\) por semana El voto se mueve poco de una semana a otra
\(\tau\) \(N^+(0;\ 0{,}5)\) Una campaña puede tener entre 0,4 y 2,3 veces la volatilidad media
\(\kappa\) \(N(0;\ 0{,}5)\) Sin preferencia sobre si la campaña mueve más el voto
\(\sigma^{\text{casa}}\) \(N^+(0;\ 0{,}2)\) Las casas difieren, pero no muchísimo
\(\log \phi_c\) \(N(\log 2.000;\ 1{,}4)\) Dispersión de las encuestas del orden de una muestra de 2.000
\(\boldsymbol{\mu}\) \(N(0;\ 0{,}1)\) El sesgo sistemático es pequeño
\(\boldsymbol{\sigma}^{\text{aut}}\) escala común \(\sim\) log-normal centrada en 0,1 Errores típicos de unos 2 puntos
\(\gamma\) \(N(0;\ 0{,}5)\) Sin preferencia sobre si las generales fallan más o menos que las autonómicas
\(\Omega\) LKJ(2) Correlaciones sin extremos
\(\sigma^b\) (casas, autonómicas) \(N^+(0;\ 0{,}1)\) Sesgos de casa moderados

El modelo completo está en modelo_conjunto.stan. Lo pego entero, para el que quiera cotillear:

// Modelo conjunto: todas las campañas generales (2015-2023 y 2026), sus
// resultados y los errores de las autonómicas, en un único posterior.
//
// 1. Campañas. Para la campaña c, voto latente de la media de las casas en
//    la semana t, en la escala de softmax SIN categoría de referencia (un
//    componente por familia, centrado):
//      theta_c,t = theta_c,t-1 + centrado(omega),
//      omega_k ~ N(0, sigma_rw[g(k)] * exp(kappa * campana_t))
//    sigma_rw por grupo de partido; kappa: cuánto más se mueve el voto en
//    las 8 últimas semanas. Con sigma_rw_campana = 1, cada campaña tiene su
//    volatilidad, con pooling parcial:
//      log sigma_rw[c, g] = log sigma_rw[g] + tau_rw * z_rwc[c, g]
//    (las campañas de 2015-2019, con la irrupción de Podemos, Cs y Vox, no
//    imponen su volatilidad a 2026). Con 0, sigma_rw[g] común a todas. Sin referencia, un
//    movimiento de RESTO (Cs en 2019) se queda en RESTO: en la versión con
//    log-ratios frente a RESTO contagiaba a todos (fit_conjunto_v1.rds).
//    Encuesta i (casa h): p_i = softmax(theta_c,t + beta_c,h); cada celda
//    suma sus familias y se normaliza dentro de las celdas de la encuesta
//    (el desglose regional es condicional al BLOQUE).
//      y_i ~ Dirichlet-multinomial(phi_c * q_i)
//    phi_c recoge la dispersión que no es de muestreo (si es grande, las
//    encuestas son multinomiales puras); así esa dispersión no se cuela en
//    sigma_rw. beta centrado por familia dentro de cada encuesta y de cada
//    campaña.
// 2. Resultados. En las campañas pasadas, para los grupos PP, PSOE, VOX y
//    SUMAR (v = suma de sus familias en softmax(theta_c,T)):
//      logit(real_c) - logit(v_c,T) ~ MVN(mu_gen, diag(sigma_gen) Omega diag(sigma_gen))
//    En la campaña objetivo (la última) el error se predice de esa MVN.
// 3. Autonómicas (errores en logit de la última semana):
//      z ~ N(mu_aut + delta_e + b_h, sigma_enc_a),
//      delta_e ~ MVN(0, diag(sigma_aut) Omega diag(sigma_aut)).
//    Omega común; sigma_gen = sigma_aut * exp(gamma) (patrón de las
//    autonómicas con nivel propio). Se probó también una escala propia para
//    las generales (modelo_conjunto_con_modo2.stan.bak): no cambió el
//    backtest y se descartó.
data {
  int<lower=1> C;                       // campañas; la C es el objetivo
  array[C] int<lower=2> K;              // familias por campaña
  array[C] int<lower=2> T;              // semanas por campaña (T = día E)
  int<lower=1> L1;                      // sum(K)
  vector[L1] theta1_mu;                 // log del voto de la elección anterior
  real<lower=0> eta1_sd;
  array[L1] int<lower=1> rwgrp;         // grupo de sigma_rw de cada familia
  int<lower=1> R;                       // número de grupos
  array[sum(T)] int<lower=0, upper=1> campana_sem;

  int<lower=1> N;                       // encuestas
  int<lower=1> S;                       // casas (campaña x casa)
  array[N] int<lower=1, upper=C> enc_campana;
  array[N] int<lower=1> enc_semana;
  array[N] int<lower=1, upper=S> enc_slot;
  array[N] int<lower=1> enc_c_ini;      // primera celda de la encuesta
  array[N] int<lower=1> enc_c_n;        // número de celdas
  array[S] int<lower=1, upper=C> slot_campana;
  int<lower=1> J;                       // celdas
  array[J] int<lower=0> y;
  array[J] int<lower=1> cel_m_ini;      // primera pertenencia de la celda
  array[J] int<lower=1> cel_m_n;
  int<lower=1> M;
  array[M] int<lower=1> memb_fam;       // familia (dentro de su campaña)

  int<lower=0> P;                       // campañas pasadas con resultado (1..P)
  int<lower=1> G;                       // grupos del error (PP, PSOE, VOX, SUMAR)
  matrix[P, G] logit_real;
  array[P, G] int<lower=0, upper=1> obs_real;
  int<lower=1> GM;
  array[GM] int<lower=1, upper=C> gm_campana;
  array[GM] int<lower=1, upper=G> gm_grupo;
  array[GM] int<lower=1> gm_fam;

  int<lower=0> Ja;                      // autonómicas (0 en el backtest del 26J)
  matrix[Ja, G] za;
  array[Ja, G] int<lower=0, upper=1> obsa;
  int<lower=0> Ea;
  array[Ja] int<lower=1, upper=max(Ea, 1)> elec_a;
  int<lower=0> Ha;
  array[Ja] int<lower=1, upper=max(Ha, 1)> casa_a;

  // escalas de los priors
  real<lower=0> sd_rw;
  real<lower=0> sd_kappa;
  real<lower=0> sd_casa;
  real log_phi_mu;                      // log phi ~ N(log_phi_mu, sd_log_phi)
  real<lower=0> sd_log_phi;
  real<lower=0> sd_mu;
  real<lower=0> sd_log_s0;
  real<lower=0> sd_tau_s;
  real<lower=0> sd_gamma;
  real<lower=0> sd_enc_a;
  real<lower=0> sd_b;
  real<lower=0> eta_lkj;
  int<lower=0, upper=1> sigma_rw_campana;
  real<lower=0> sd_tau_rw;
  int<lower=0, upper=1> prior_only;
}
transformed data {
  array[C] int off_th;                  // inicio de la campaña en theta1 / rwgrp
  array[C] int off_z;                   // inicio en z_rw
  array[C] int off_t;                   // inicio en campana_sem
  int Lz = 0;
  {
    int a = 0;
    int t = 0;
    for (c in 1:C) {
      off_th[c] = a;
      off_z[c] = Lz;
      off_t[c] = t;
      a += K[c];
      Lz += K[c] * (T[c] - 1);
      t += T[c];
    }
  }
  array[S] int off_b;
  int Lb = 0;
  for (s in 1:S) {
    off_b[s] = Lb;
    Lb += K[slot_campana[s]];
  }
  int Kmax = max(K);
  int Tmax = max(T);
  // autonómicas en formato largo
  int Na = 0;
  for (j in 1:Ja) {
    Na += sum(obsa[j]);
  }
  vector[Na] za_obs;
  array[Na] int ia_k;
  array[Na] int ia_e;
  array[Na] int ia_h;
  {
    int n = 0;
    for (j in 1:Ja) {
      for (k in 1:G) {
        if (obsa[j, k]) {
          n += 1;
          za_obs[n] = za[j, k];
          ia_k[n] = k;
          ia_e[n] = k + G * (elec_a[j] - 1);
          ia_h[n] = k + G * (casa_a[j] - 1);
        }
      }
    }
  }
}
parameters {
  vector[L1] theta1;
  vector[Lz] z_rw;
  vector<lower=0>[R] sigma_rw;
  matrix[C, R] z_rwc;
  real<lower=0> tau_rw;
  real kappa;
  vector[Lb] z_casa;
  vector<lower=0>[R] sigma_casa;
  vector[C] log_phi;

  vector[G] mu_gen;
  vector[G] mu_aut;
  cholesky_factor_corr[G] L_Omega;
  real log_s0;
  real<lower=0> tau_s;
  vector[G] z_s;
  real gamma;
  matrix[G, Ea] z_delta_a;
  vector<lower=0>[G] sigma_enc_a;
  matrix[G, Ha] z_b;
  vector<lower=0>[G] sigma_b;
}
transformed parameters {
  vector[G] sigma_aut = exp(log_s0 + tau_s * z_s);
  vector[G] sigma_gen = sigma_aut * exp(gamma);
  matrix[G, G] Omega = multiply_lower_tri_self_transpose(L_Omega);
  matrix[G, G] L_gen = diag_pre_multiply(sigma_gen, L_Omega);
  matrix[G, Ea] delta_a = diag_pre_multiply(sigma_aut, L_Omega) * z_delta_a;
  matrix[G, Ha] b = diag_pre_multiply(sigma_b, z_b);

  // volatilidad de cada campaña y grupo
  matrix[C, R] sigma_rw_c;
  for (c in 1:C) {
    for (g in 1:R) {
      sigma_rw_c[c, g] = sigma_rw[g]
                         * (sigma_rw_campana == 1 ? exp(tau_rw * z_rwc[c, g]) : 1);
    }
  }

  // voto latente: theta[c] es K x T (relleno de ceros hasta Kmax x Tmax:
  // Stan no admite matrices de tamaño distinto en un array). Cada columna
  // está centrada (suma cero): softmax no cambia y no hay referencia.
  array[C] matrix[Kmax, Tmax] theta;
  for (c in 1:C) {
    int kc = K[c];
    theta[c] = rep_matrix(0, Kmax, Tmax);
    vector[kc] th = theta1[(off_th[c] + 1):(off_th[c] + kc)];
    theta[c][1:kc, 1] = th - mean(th);
    for (t in 2:T[c]) {
      vector[kc] om;
      real f = exp(kappa * campana_sem[off_t[c] + t]);
      for (k in 1:kc) {
        om[k] = sigma_rw_c[c, rwgrp[off_th[c] + k]] * f
                * z_rw[off_z[c] + (t - 2) * kc + k];
      }
      theta[c][1:kc, t] = theta[c][1:kc, t - 1] + om - mean(om);
    }
  }

  // efectos casa: centrados por familia dentro de cada casa y, después,
  // por familia dentro de cada campaña
  vector[Lb] beta;
  for (s in 1:S) {
    int c = slot_campana[s];
    int kc = K[c];
    vector[kc] bs;
    for (k in 1:kc) {
      bs[k] = sigma_casa[rwgrp[off_th[c] + k]] * z_casa[off_b[s] + k];
    }
    beta[(off_b[s] + 1):(off_b[s] + kc)] = bs - mean(bs);
  }
  for (c in 1:C) {
    int kc = K[c];
    vector[kc] media = rep_vector(0, kc);
    int ns = 0;
    for (s in 1:S) {
      if (slot_campana[s] == c) {
        media += beta[(off_b[s] + 1):(off_b[s] + kc)];
        ns += 1;
      }
    }
    media /= ns;
    for (s in 1:S) {
      if (slot_campana[s] == c) {
        beta[(off_b[s] + 1):(off_b[s] + kc)] -= media;
      }
    }
  }
}
model {
  theta1 ~ normal(theta1_mu, eta1_sd);
  z_rw ~ std_normal();
  sigma_rw ~ normal(0, sd_rw);
  to_vector(z_rwc) ~ std_normal();
  tau_rw ~ normal(0, sd_tau_rw);
  kappa ~ normal(0, sd_kappa);
  z_casa ~ std_normal();
  sigma_casa ~ normal(0, sd_casa);
  log_phi ~ normal(log_phi_mu, sd_log_phi);

  mu_gen ~ normal(0, sd_mu);
  mu_aut ~ normal(0, sd_mu);
  L_Omega ~ lkj_corr_cholesky(eta_lkj);
  log_s0 ~ normal(log(0.1), sd_log_s0);
  tau_s ~ normal(0, sd_tau_s);
  z_s ~ std_normal();
  gamma ~ normal(0, sd_gamma);
  to_vector(z_delta_a) ~ std_normal();
  sigma_enc_a ~ normal(0, sd_enc_a);
  to_vector(z_b) ~ std_normal();
  sigma_b ~ normal(0, sd_b);

  if (!prior_only) {
    // 1. encuestas de todas las campañas
    for (i in 1:N) {
      int c = enc_campana[i];
      int kc = K[c];
      vector[kc] p = softmax(
        theta[c][1:kc, enc_semana[i]]
        + beta[(off_b[enc_slot[i]] + 1):(off_b[enc_slot[i]] + kc)]);
      int nc = enc_c_n[i];
      vector[nc] q;
      array[nc] int yi;
      for (j in 1:nc) {
        int cj = enc_c_ini[i] + j - 1;
        q[j] = 0;
        for (m in cel_m_ini[cj]:(cel_m_ini[cj] + cel_m_n[cj] - 1)) {
          q[j] += p[memb_fam[m]];
        }
        yi[j] = y[cj];
      }
      yi ~ dirichlet_multinomial(exp(log_phi[c]) * q / sum(q));
    }

    // 2. resultados de las campañas pasadas, frente a theta_c,T
    for (c in 1:P) {
      int kc = K[c];
      vector[kc] v = softmax(theta[c][1:kc, T[c]]);
      vector[G] vg = rep_vector(0, G);
      for (m in 1:GM) {
        if (gm_campana[m] == c) {
          vg[gm_grupo[m]] += v[gm_fam[m]];
        }
      }
      int no = sum(obs_real[c]);
      array[no] int idx;
      {
        int n = 0;
        for (g in 1:G) {
          if (obs_real[c, g]) {
            n += 1;
            idx[n] = g;
          }
        }
      }
      vector[no] err = logit_real[c, idx]' - logit(vg[idx]);
      err ~ multi_normal(mu_gen[idx], quad_form_diag(Omega[idx, idx], sigma_gen[idx]));
    }

    // 3. autonómicas
    za_obs ~ normal(mu_aut[ia_k] + to_vector(delta_a)[ia_e] + to_vector(b)[ia_h],
                    sigma_enc_a[ia_k]);
  }
}
generated quantities {
  matrix[K[C], T[C]] v_obj;
  for (t in 1:T[C]) {
    v_obj[, t] = softmax(theta[C][1:K[C], t]);
  }
  vector[G] err_new = mu_gen + multi_normal_cholesky_rng(rep_vector(0, G), L_gen);
}

El voto latente: lo que diría la media de las casas

Cada semana hay un voto latente, que las “casas” encuestadoras medirían si se hicieran encuestas cada semana. Sería la “media” de las casas. Y he supuesto que de una semana a la siguiente cambia un poco, al azar:

\[ \boldsymbol{\theta}_{c,t} = \boldsymbol{\theta}_{c,t-1} + \boldsymbol{\omega}_{c,t}, \qquad \omega_{c,t,k} \sim N\big(0,\ \sigma_{c,g(k)}\big) \]

El supuesto que subyace es un paseo aleatorio, básicamente, es decir que para el voto latente de la semana que viene, sería como el de esta semana más un error aleatorio. La incertidumbre crece con cada semana que pasa sin encuestas. ¿Cuánto crece? Lo deciden los datos: \(\sigma\) es el tamaño típico del paso semanal, en la escala de la softmax.

Ver el código
colores <- c(
  PP = "#005999",
  PSOE = "#FF0126",
  VOX = "#51962A",
  SUMAR = "#A00B85"
)
tend <- leer("conjunto_tendencia.csv") |>
  filter(partido %in% names(colores), fecha >= as.Date("2026-06-01"))
puntos <- bind_rows(
  grandes |>
    filter(campana == "29N-2026") |>
    select(fecha, PP, PSOE, VOX = Vox, SUMAR = Sumar) |>
    pivot_longer(-fecha, names_to = "partido", values_to = "pct"),
  # las agregadas también informan al modelo y publican PP, PSOE y VOX
  # (sin Sumar ni SALF)
  encuestas_2026_agregadas() |>
    group_by(encuesta, fecha) |>
    mutate(n = sum(y)) |>
    ungroup() |>
    filter(celda %in% c("PP", "PSOE", "VOX")) |>
    transmute(fecha, partido = celda, pct = 100 * y / n)
)
ultima <- max(puntos$fecha)
sem_29n <- round(as.numeric(as.Date("2026-11-29") - ultima) / 7)
ggplot(tend, aes(fecha, mediana, colour = partido, fill = partido)) +
  geom_ribbon(aes(ymin = q05, ymax = q95), alpha = 0.2, colour = NA) +
  geom_line(linewidth = 0.8) +
  geom_point(data = puntos, aes(y = pct), size = 1.2, alpha = 0.7) +
  geom_vline(xintercept = ultima, linetype = "dotted") +
  geom_vline(xintercept = as.Date("2026-11-29"), linetype = "dashed") +
  annotate(
    "text",
    x = ultima,
    y = 36,
    label = "última encuesta",
    hjust = 1.05,
    size = 3
  ) +
  annotate(
    "text",
    x = as.Date("2026-11-29"),
    y = 36,
    label = "29-N",
    hjust = 1.1,
    size = 3
  ) +
  scale_colour_manual(values = colores, aesthetics = c("colour", "fill")) +
  labs(x = NULL, y = "% del voto", colour = NULL, fill = NULL) +
  theme_minimal(base_size = 12) +
  theme(legend.position = "top")
Figura 4: Voto latente de los cuatro grandes en 2026 (mediana e intervalo del 90 %) y las encuestas que lo informan. Desde la última encuesta (25 de octubre) hasta el 29-N, el modelo solo puede extrapolar: el intervalo se abre.
Ver el código
pp_2026 <- bind_rows(
  grandes |> filter(campana == "29N-2026") |> select(casa, fecha, PP),
  # las agregadas también cuentan en la media de las encuestas
  encuestas_2026_agregadas() |>
    group_by(encuesta, fecha) |>
    mutate(n = sum(y)) |>
    ungroup() |>
    filter(celda == "PP") |>
    transmute(casa, fecha, PP = 100 * y / n)
)
pp_media_enc <- mean(pp_2026$PP)
pp_media_casas <- pp_2026 |>
  group_by(casa) |>
  summarise(pp = mean(PP)) |>
  pull(pp) |>
  mean()

Si os fijáis, las encuestas del PP tienden a caer algo por encima de su línea. No es un fallo: el voto latente es la media de las casas, no la de las encuestas.

Ver el código
par_cj <- leer("conjunto_parametros.csv")
vol <- par_cj |>
  filter(parametro == "sigma_rw_c") |>
  separate(partido, c("campana", "grupo"), sep = " ", extra = "merge") |>
  filter(grupo %in% c("PP", "PSOE", "VOX", "SUMAR")) |>
  # sin Vox en 2015-2016: su volatilidad ahí solo es prior
  filter(!(grupo == "VOX" & campana %in% c("20d", "26j"))) |>
  mutate(campana = factor(nombre_campana[campana], levels = nombre_campana))
v <- \(c, g, col = "mediana") {
  vol[[col]][vol$campana == nombre_campana[[c]] & vol$grupo == g]
}
tau <- par_cj |> filter(parametro == "tau_rw")
kappa <- par_cj |> filter(parametro == "kappa")

No hace falta una vida media

Otra gente que hace este tipo de cosas tienen cosas como dar más peso a las encuestas recientes y que pierda su importancia conforme pasa el tiempo. Algo como “una encuesta pierde la mitad de su peso cada dos semanas”. En mi aproximación no hace falta, la información de una encuesta de hace 10 semanas para llegar al voto latente de hoy ha tenido que atravesar 10 pasos del paseo aleatorio, y en cada paso pierde precisión. Y esa pérdida de precisión se estima.

En 2026 se está viendo que se pierde muy poca. El paso semanal del PP en esta campaña es de 0,008 en logit, que con el PP por el 32 % son unas décimas de punto. La cosa apenas se ha movido desde el verano, así que el modelo trata las encuestas de julio casi igual que las de octubre, si empiezan a salir encuestas dónde varía más las estimaciones de las encuestas, entonces el modelo estimaría una mayor incertidumbre de semana a semana.

Cada campaña tiene su ritmo

Lo que sí cambia, y mucho, es lo que se mueve el voto de una campaña a otra. Así que cada campaña tiene su propia \(\sigma\), parecida a la de las demás pero no igual:

\[ \log \sigma_{c,g} = \log \sigma_g + \tau\, z_{c,g} \]

\(\tau\) mide cuánto se parecen o no las campañas entre sí. Sale 0,77 (IC90: 0,5–1,11): una campaña puede moverse el doble o la mitad que otra.

Ver el código
ggplot(vol, aes(mediana, campana, colour = grupo)) +
  geom_pointrange(
    aes(xmin = q05, xmax = q95),
    position = position_dodge(width = 0.6)
  ) +
  scale_colour_manual(values = colores) +
  labs(x = "Paso semanal típico (logit)", y = NULL, colour = NULL) +
  theme_minimal(base_size = 12) +
  theme(legend.position = "top")
Figura 5: Volatilidad semanal del voto latente en cada campaña (mediana e intervalo del 90 %, logit por semana). Sin Vox en 2015-2016, cuando no existía como partido relevante.

Dos campañas destacan, y tienen su explicación: el 26J-2016, cuando Podemos e Izquierda Unida se unieron a mitad de precampaña (la volatilidad de su voto, 0,19, es la mayor de todas), y el 10N-2019, cuando Vox pasó del 10 % al 15 % del voto en seis meses (0,074). La de 2026 es de las más tranquilas: Vox 0,024 y Sumar 0,025.

¿Y esto por qué importa? Porque entre la última encuesta y el 29-N quedan 5 semanas en las que el modelo solo puede extrapolar. Si les aplicara la volatilidad media de todas las campañas, la incertidumbre sería aún mayor.

Probé también si el voto se mueve más en las ocho últimas semanas que antes (un parámetro \(\kappa\) que multiplica la volatilidad en ese tramo). Con una volatilidad para cada campaña, sale -0,02 (IC90: -0,44–0,44): nada de nada. Lo que mueve el voto no es que se acerque el día de la votación, sino lo que pasa en cada campaña. En una versión anterior, con la misma volatilidad para todas, \(\kappa\) sí salía positivo: estaba recogiendo que algunas campañas fueron más movidas que otras.

Casas, ruido y rebaño

Tres cosas que se mezclan

Una encuesta no se separa del voto real solo por azar. Hay tres cosas mezcladas, y el modelo tiene que separarlas. Ejemplo tonto: tres casas, tres semanas, % del PP.

Semana 1 Semana 2 Semana 3
Casa A 33 33 34
Casa B 31 31 32
Casa C 32 35 33

Se ven tres cosas distintas:

  1. La casa A siempre da un punto más que las otras, y la B uno menos. Pase lo que pase. Eso no es el voto: es la “manía” de cada casa (su método, su universo, cómo pondera). Es el efecto casa.
  2. En la semana 3, las tres suben un punto a la vez. Algo que ven todas las casas a la vez no es manía de nadie: el voto se ha movido. Es el voto latente.
  3. En la semana 2, la casa C da un 35. Ni la C da eso otras semanas ni las demás lo ven esa semana. Es cosa de esa encuesta suelta. Es ruido.

Y esa es toda la lógica: cada cosa se distingue por lo que comparte. Lo que se repite en la misma casa es efecto casa; lo que ven todas la misma semana es voto; lo que no comparte nadie es ruido. El modelo hace lo mismo que haríamos nosotros a ojo con la tabla, solo que con probabilidades en vez de medias y teniendo en cuenta el tamaño de cada encuesta.

Para que esto funcione hace falta que varias casas publiquen en las mismas semanas y que cada casa publique varias veces. Una casa con una sola encuesta no deja ver su forma de hacer las cosas, y el modelo deja su efecto “casa” cerca de cero (es decir, al no saber nada de esa casa, la coloca como una casa media)

Pero hay una cosa que esta tabla no puede ver: que las tres casas den un punto de más a la vez, y siempre. Para la tabla, eso es el voto. Por eso el voto latente es la media de las casas, no la verdad. Ese error común solo se descubre el día de la votación, y para medirlo hay que mirar al pasado.

Las encuestas no son urnas perfectas

Si cada encuesta fuera un sorteo perfecto , sacar 1.000 bolas de una urna con un 32 % de bolas azules, diez encuestas de la misma semana saldrían todas a un punto y medio del valor real. Ese es el margen de error de toda la vida, el de la multinomial, y es lo que usaba en los modelos de octubre.

Pero las encuestas no son sorteos perfectos. Cada una tiene su cocina: a quién consigue llamar esa semana, cómo pondera, qué hace con los que no recuerdan a quién votaron…

Porque el modelo solo tiene dos formas de explicar que las encuestas no coincidan: el azar del muestreo, que en una encuesta de 1.000 personas no da para más de un punto y medio, o que el voto se haya movido de verdad. Si le digo que las encuestas son sorteos perfectos, todo lo que pase de ese punto y medio lo tiene que explicar como movimiento. Imagina tres semanas seguidas: una encuesta da al PP un 30 %, la siguiente un 35 % y la siguiente un 31 %. Con sorteos perfectos, la única explicación es que el PP subió cinco puntos en una semana y bajó cuatro en la siguiente. El modelo aprende que el voto es muy nervioso, y cuando luego tiene que proyectar 5 semanas sin encuestas, hasta el 29-N, aplica esos nervios y la horquilla se dispara.

La solución es la Dirichlet-multinomial, que es justo lo de en medio: cada encuesta saca sus bolas de su propia urna, una copia algo movida de la de verdad. Cuánto se mueven las copias lo dice un parámetro, \(\phi\): con \(\phi = 200\) se mueven bastante; con \(\phi\) muy grande, las copias son iguales que la urna buena y volvemos al sorteo perfecto.

Ver el código
phi <- par_cj |>
  filter(parametro == "log_phi") |>
  mutate(across(c(mediana, q05, q95), exp), campana = nombre_campana[partido])
phi_c <- \(c) phi$mediana[phi$partido == c]

\(\phi\) no lo pongo yo: lo estima el modelo. ¿Cómo? Mirando cuánto se separan las encuestas del voto latente y comparándolo con lo que daría el sorteo puro, que se sabe porque conocemos el tamaño de cada encuesta. Si se separan lo justo, \(\phi\) sale muy grande. Si se separan mucho más, sale pequeño. Y no lo confunde con los cambios del voto por lo de la tabla de juguete: un cambio de verdad lo ven todas las casas a la vez; el ruido es cosa de una encuesta suelta, que a la semana siguiente vuelve a su sitio.

Y sale esto: en 2015, 2016 y abril de 2019 las encuestas iban cada una por su lado (\(\phi\) entre 62 y 209). Desde noviembre de 2019, nada de ruido extra: \(\phi\) pasa de las decenas de miles y las encuestas se comportan como sorteos perfectos.

Vamos a simular un poco. Diez casas hacen una encuesta de 1.000 personas la misma semana, y el PP tiene, de verdad, un 32 %. ¿Cuánto deberían parecerse las diez encuestas?

Ver el código
set.seed(2026)
n_juguete <- 1000
verdad <- 0.32
una_encuesta <- function(phi) {
  # cada encuesta saca su propia urna (una Beta, la Dirichlet de dos
  # partidos) y de ella sus 1.000 entrevistas; con phi infinito, la urna
  # es la buena
  urna <- if (is.infinite(phi)) {
    verdad
  } else {
    rbeta(1, phi * verdad, phi * (1 - verdad))
  }
  100 * rbinom(1, n_juguete, urna) / n_juguete
}
niveles <- c(
  "Sorteo perfecto",
  "Con ruido extra (φ = 200)",
  "Encuestas reales (sep.-oct. 2026)"
)
juguete <- tibble(mundo = niveles[1:2], phi = c(Inf, 200)) |>
  mutate(pp = map(phi, \(f) replicate(10, una_encuesta(f)))) |>
  unnest(pp)
reales <- grandes |>
  filter(campana == "29N-2026", fecha > as.Date("2026-09-11")) |>
  transmute(mundo = niveles[3], pp = PP)
rango <- \(m) range(juguete$pp[juguete$mundo == m])
bind_rows(juguete, reales) |>
  mutate(mundo = factor(mundo, levels = niveles)) |>
  ggplot(aes(pp, mundo)) +
  geom_vline(
    xintercept = 100 * verdad,
    linetype = "dashed",
    colour = "grey50"
  ) +
  geom_point(
    size = 3,
    alpha = 0.7,
    colour = "#005999",
    position = position_jitter(height = 0.08, seed = 1)
  ) +
  labs(x = "% del PP en cada encuesta", y = NULL) +
  theme_minimal(base_size = 12)
Figura 6: Diez encuestas de 1.000 personas la misma semana, con un PP del 32 %: si fueran sorteos perfectos (abajo) y con ruido extra de cada encuesta (en medio). Arriba, las encuestas reales del PP en las cuatro semanas anteriores al 11 de octubre de 2026.

Si cada encuesta fuera un sorteo perfecto (sacar 1.000 bolas de una urna con un 32 % de bolas azules), saldría lo de abajo: todas entre el 30 y el 34,4 %.

Pero las encuestas no son sorteos perfectos. Cada una tiene su cocina: a quién consigue llamar esa semana, cómo pondera, qué hace con los que no recuerdan a quién votaron… Con ese ruido extra, las mismas diez encuestas pueden irse del 29,4 al 39,8 %, que es lo de en medio.

¿Y las de verdad? Arriba están las encuestas reales del PP de las cuatro semanas anteriores al 11 de octubre (la línea discontinua ya no es “la verdad” para ellas: esa no la sabemos). Van del 28,5 al 34,7 %, a medio camino. Pero no es una comparación limpia: mezclan casas distintas, con sus manías, y cuatro semanas. Descontado eso, se parecen más al sorteo perfecto que al ruido extra, y en algún partido incluso menos dispersas, como se ve más abajo.

El rebaño

Ver el código
# cociente entre la dispersión observada de las encuestas de 2026 alrededor
# del voto latente de su semana y la que daría el puro muestreo
tend_sem <- leer("conjunto_tendencia.csv") |>
  select(partido, fecha_sem = fecha, latente = mediana)
rebano <- grandes |>
  filter(campana == "29N-2026") |>
  select(fecha, n, PP, PSOE, VOX = Vox, SUMAR = Sumar) |>
  pivot_longer(
    c(PP, PSOE, VOX, SUMAR),
    names_to = "partido",
    values_to = "pct"
  ) |>
  mutate(
    fecha_sem = inicio_2026 + 7 * floor(as.numeric(fecha - inicio_2026) / 7)
  ) |>
  left_join(tend_sem, by = c("partido", "fecha_sem")) |>
  group_by(partido) |>
  summarise(
    cociente = mean((pct - latente)^2) / mean(latente * (100 - latente) / n)
  )
coc <- \(p) num_es(rebano$cociente[rebano$partido == p], 2)

Pero en 2026 pasa algo curioso. Si comparo cuánto se separan las encuestas de su línea con lo que daría el sorteo puro, en el PSOE y en Vox se separan menos de lo que permite el azar: el cociente entre las dos cosas es 0,44 para el PSOE y 0,56 para Vox (1 sería sorteo puro). Es decir, las encuestas se parecen entre sí más de lo que deberían. Es el rebaño: las casas se miran unas a otras y no se alejan mucho del consenso. Se sabe, se comenta en el gremio, y aquí se ve con números.

Y esto la Dirichlet-multinomial no lo puede recoger: solo sabe añadir ruido, no quitarlo. Con \(\phi\) muy grande, el modelo trata estas encuestas como sorteos perfectos, o sea, algo más ruidosas de lo que son. Para la predicción no es grave, porque ese parecido entre casas es justo el error que cometen todas a la vez, y de eso se encarga la pieza siguiente.

  • Efecto casa. Hay un efecto por casa y campaña, y dentro de cada campaña suman cero. Así el voto latente es la media de las casas, cada una con el mismo peso. Una casa con pocas encuestas tiene un efecto encogido hacia cero (partial pooling).
  • Voto latente. El paseo aleatorio prefiere pasos pequeños: un salto de tres puntos que se deshace a la semana siguiente le sale “caro” en el prior de la volatilidad, así que el modelo prefiere llamarlo ruido.
  • Ruido. La Dirichlet-multinomial tiene un \(\phi\) por campaña. En 2026 hay pocas encuestas por semana; la separación entre ruido y movimiento se apoya también en las campañas pasadas, que están en el mismo modelo.
  • Lo que no se identifica: un sesgo común a todas las casas. Es el error del consenso, y lo mide la sección siguiente con los resultados de las elecciones pasadas.

El error del consenso

Dónde entra la verdad

Las encuestas solas no pueden saber si se equivocan todas a la vez: si todas dan un punto de más al PP, para ellas eso es el voto. Para medirlo hace falta algo que no sea una encuesta, lo que se votó de verdad en las elecciones.

De cinco de las seis campañas sabemos cómo acabaron, y ese resultado entra en el modelo como un dato más. Es el único sitio por donde entra la verdad, aunque no es lo único que esas cinco campañas aportan: también ayudan a estimar la volatilidad del voto y el ruido de las encuestas, que en 2026 solo aprenderíamos con unas pocas encuestas. En cada una de esas cinco, el modelo hace lo mismo que en 2026 (estimar el voto latente semana a semana hasta el día de la votación), pero además lo compara con lo que salió de las urnas:

\[ \text{error}_c = \operatorname{logit}(\text{resultado}_c) - \operatorname{logit}(\text{voto latente}_{c,T}) \ \sim\ N\big(\boldsymbol{\mu},\ \operatorname{diag}(\boldsymbol{\sigma})\,\Omega\,\operatorname{diag}(\boldsymbol{\sigma})\big) \]

De esas comparaciones aprende cómo suele ser el error: si tira hacia un lado con algún partido (\(\boldsymbol{\mu}\)), cuánto suele fallar con cada uno (\(\boldsymbol{\sigma}\)) y si los fallos van juntos (\(\Omega\)). Y para 2026, ¿qué hacemos?, pues simplemente estimamos el voto latente y le añadimos un valor aleatorio extraído de la distribución que tenemos estimada de como fue el error en el pasado. Y eso lo hacemos en cada una de las 4000 posteriores del voto latente, uno distinto en cada “draw” de la posteriori

Por qué en logit

Ver el código
hist_pp <- read_csv(
  here::here("2026/10/errores_encuestas_historicas.csv"),
  show_col_types = FALSE
) |>
  group_by(eleccion) |>
  summarise(encuestas = mean(PP), real = mean(PP + e_PP)) |>
  mutate(
    puntos = real - encuestas,
    logit = qlogis(real / 100) - qlogis(encuestas / 100)
  )
h <- \(e, col) hist_pp[[col]][hist_pp$eleccion == e]

¿Y en qué escala se mide el error? En puntos sería lo natural, pero dos puntos no son lo mismo para un partido del 32 % que para uno del 3 %. En el 28A las encuestas daban al PP un 19,7 % y sacó un 16,7 %: 3 puntos menos. En el 23J le daban un 34,2 % y sacó un 33,1 %: 1,1 puntos menos. ¿Fallaron mucho más en 2019? En puntos, unas 3 veces más. Pero un PP del 17 % y uno del 34 % no son el mismo partido: fallar tres puntos con un PP pequeño es más grave. En logit (cada partido frente a todos los demás), el error de 2019 es -0,2 y el de 2023, -0,05: unas 4 veces más. El logit pone a los partidos grandes y pequeños en la misma escala, y deja comparar elecciones aunque los partidos cambien de tamaño.

Por qué contra el voto latente

En el primer post de metanálisis medía el error de las encuestas contra la media de las encuestas de la última semana, y luego lo sumaba a la tendencia del modelo. Parece lo mismo, pero no lo es: en la media de las encuestas, la casa que más publica pesa más; en el voto latente, todas las casas pesan igual.

Un ejemplo inventado: la casa A publica cinco encuestas la última semana y da al PP un 35 %; otras cuatro casas publican una cada una y le dan un 32 %. La media de las encuestas es un 33,7 %; la de las casas, un 32,6 %. El PP saca un 33 %. Contra la media de las encuestas, “lo sobrestimaron”; contra la de las casas, “lo infraestimaron”. Cambia hasta el signo. Si aprendes a corregir una cosa y luego corriges otra, la puedes liar.

Ahora el error se mide contra el voto latente de las campañas pasadas, hecho con las mismas reglas que el de 2026. Y como todo se estima a la vez, el resultado también ayuda a situar el voto latente de las campañas antiguas, que a veces tenían pocas encuestas, y la duda que queda entra en el error.

Lo que aprende

Ver el código
p_dia_e <- c(PP = 0.32, PSOE = 0.265, VOX = 0.185, SUMAR = 0.09)
err_tab <- par_cj |>
  filter(parametro %in% c("mu_gen", "sigma_gen")) |>
  select(parametro, partido, mediana) |>
  pivot_wider(names_from = parametro, values_from = mediana) |>
  mutate(
    a_puntos = 100 * p_dia_e[partido] * (1 - p_dia_e[partido]),
    sesgo = mu_gen * a_puntos,
    tamano = sigma_gen * a_puntos,
    partido = recode(partido, VOX = "Vox", SUMAR = "Sumar + Podemos")
  )
knitr::kable(
  err_tab |>
    transmute(
      Partido = partido,
      `Sesgo típico (puntos)` = num_es(sesgo, 1),
      `Tamaño típico del fallo (puntos)` = num_es(tamano, 1),
      `Tamaño (logit)` = num_es(sigma_gen, 2)
    ),
  align = "lrrr"
)
Partido Sesgo típico (puntos) Tamaño típico del fallo (puntos) Tamaño (logit)
PP -0,5 3,0 0,14
PSOE 0,6 2,4 0,12
Vox -0,1 2,7 0,18
Sumar + Podemos 0,4 1,2 0,15

Va en puntos, aproximados con el tamaño actual de cada partido. El sesgo positivo del PSOE quiere decir que las encuestas se suelen quedar algo cortas con él. Y el tamaño típico del fallo es lo que más ensancha las horquillas: unos tres puntos con el PP.

Falta la otra mitad de la matriz de covarianzas del error, \(\Sigma = \operatorname{diag}(\boldsymbol{\sigma}^{\text{gen}})\,\Omega\,\operatorname{diag}(\boldsymbol{\sigma}^{\text{gen}})\). El \(\boldsymbol{\sigma}^{\text{gen}}\) es lo que falla cada partido por su lado: la columna “Tamaño (logit)” de la tabla de arriba (la versión en puntos depende del tamaño del partido; el logit, no). Y \(\Omega\) es la correlación entre los fallos de cada par de partidos: la diagonal lleva unos, y las seis casillas que quedan dicen si dos partidos fallan a la vez (+1), al revés (-1) o cada uno por su lado (0). Casilla a casilla:

Ver el código
# la matriz Omega completa, casilla a casilla, con la contracción de cada una
om_v <- \(p) par_cj$mediana[par_cj$parametro == "Omega" & par_cj$partido == p]
om_tab <- par_cj |>
  filter(parametro == "Omega") |>
  left_join(
    leer("contraccion_conjunto.csv") |>
      filter(parametro == "Omega") |>
      select(partido, contraccion),
    by = "partido"
  ) |>
  arrange(match(
    partido,
    c(
      "PSOE-PP",
      "VOX-PP",
      "VOX-PSOE",
      "SUMAR-PP",
      "SUMAR-PSOE",
      "SUMAR-VOX"
    )
  )) |>
  transmute(
    `Fallo entre` = recode(
      partido,
      "PSOE-PP" = "PP y PSOE",
      "VOX-PP" = "PP y Vox",
      "VOX-PSOE" = "PSOE y Vox",
      "SUMAR-PP" = "PP y Sumar + Podemos",
      "SUMAR-PSOE" = "PSOE y Sumar + Podemos",
      "SUMAR-VOX" = "Vox y Sumar + Podemos"
    ),
    `Correlación (mediana)` = num_es(mediana, 2),
    IC90 = paste0(num_es(q05, 2), " a ", num_es(q95, 2)),
    Contracción = num_es(contraccion, 2)
  )
knitr::kable(om_tab, align = "lrrr")
Fallo entre Correlación (mediana) IC90 Contracción
PP y PSOE 0,00 -0,28 a 0,27 0,80
PP y Vox -0,51 -0,69 a -0,25 0,87
PSOE y Vox -0,36 -0,57 a -0,10 0,85
PP y Sumar + Podemos -0,08 -0,54 a 0,42 0,41
PSOE y Sumar + Podemos -0,29 -0,72 a 0,28 0,30
Vox y Sumar + Podemos 0,35 -0,28 a 0,80 0,22

Tres casillas las deciden los datos —contracción por encima de 0,8—: el fallo con el PP y el de Vox van en direcciones opuestas (-0,51), Vox y PSOE también (-0,36), y el fallo con el PP y el del PSOE no va junto (0, que también es aprender: sus fallos van cada uno por su lado).

Lo que enseñan las autonómicas

Cinco generales son muy poca cosa para estimar todo eso: cuatro tamaños, cuatro sesgos y seis correlaciones, y Vox solo sale en tres. Pero las encuestas de las autonómicas las hacen las mismas casas, con los mismos métodos. No es lo mismo votar en unas generales que en unas autonómicas, pero si una casa falla con el PP en las autonómicas, algo dice de cómo puede fallar en las generales.

Uso 217 encuestas de la última semana en 39 autonómicas desde 2018 (sin las de a pie de urna). Y no las mezclo a lo loco:

Pieza ¿Se comparte con las generales?
Correlación entre los fallos (\(\Omega\)) Sí, entera
Qué partidos se fallan más (\(\boldsymbol{\sigma}\)) El patrón sí; el nivel, cada tipo de elección el suyo (\(\gamma\))
Sesgo sistemático (\(\boldsymbol{\mu}\)) No
Ruido de cada encuesta No
Ver el código
gamma <- par_cj |> filter(parametro == "gamma")

Salen dos cosas claras. La primera: \(\gamma\) es 0,03, prácticamente cero. Las generales fallan más o menos lo mismo que las autonómicas. La segunda, es interesante: la correlación entre el fallo con el PP y el fallo con Vox, la -0,50 de la tabla de arriba. Cuando las encuestas se pasan con el PP, se quedan cortas con Vox, y al revés.

Ojo, no dice que PP y Vox se peleen por los mismos votantes. Dice que las encuestas se equivocan al repartir el voto entre los dos, aunque se acerquen más con la suma.

Las “manías” de las casas, frente al resultado

Ver el código
suppressMessages(library(posterior))
fit_cj <- readRDS(file.path(dir_fb, "fit_conjunto.rds"))
p_pp <- 0.32
# 1) efecto casa del PP en 2026 (cuánto se aparta del grupo); solo casas con
# encuestas de los grandes partidos
off_b <- cumsum(c(0, dc$K[match(dc$slots$campana, dc$campanas)]))[seq_len(nrow(
  dc$slots
))]
casas_grandes <- unique(dc$encuestas$casa[str_detect(
  dc$encuestas$encuesta,
  "^2026-g"
)])
slots26 <- which(dc$slots$campana == "2026" & dc$slots$casa %in% casas_grandes)
beta_pp <- sapply(slots26, \(s) {
  median(as.numeric(fit_cj$draws(
    sprintf("beta[%d]", off_b[s] + 1),
    format = "draws_matrix"
  )))
})
# 2) cuánto se pasa cada casa con el PP frente al resultado, en autonómicas
db_aut <- datos_error(
  antes_de = as.Date(fecha_elecciones),
  casas_obj = character(0)
)
aut <- db_aut$tipo[db_aut$eleccion] == 2
nombres_aut <- db_aut$casas[sort(unique(db_aut$casa[aut]))]
b_pp <- sapply(seq_along(nombres_aut), \(h) {
  median(as.numeric(fit_cj$draws(
    sprintf("b[1,%d]", h),
    format = "draws_matrix"
  )))
})
casas <- tibble(
  casa = dc$slots$casa[slots26],
  grupo = 100 * beta_pp * p_pp * (1 - p_pp)
) |>
  left_join(
    tibble(casa = nombres_aut, historia = -100 * b_pp * p_pp * (1 - p_pp)),
    by = "casa"
  ) |>
  arrange(grupo)
cor_casas <- cor(casas$grupo, casas$historia, use = "complete.obs")
media_hist <- mean(casas$historia, na.rm = TRUE)

¿Pero no está el efecto casa justo para esto? Sí y no. El efecto casa responde a una pregunta: ¿esta casa se aparta del grupo? Y para eso no hace falta saber el resultado de nada. Lo que no sabe es quién tiene razón: solo quién va por libre. Para saber quién acierta hay que comparar con lo que se votó, y eso es la segunda columna, que sale de las autonómicas.

Las dos columnas se parecen (correlación de 0,7): las manías de cada casa son de verdad y se mantienen. Entonces, ¿por qué no corrijo a cada casa por su historia? Porque el efecto casa ya devuelve a cada una al grupo, y lo único que añadiría la historia es si el grupo entero se pasa o se queda corto. Juntando a las casas que publican ahora, en conjunto se pasan con el PP unas 3 décimas de punto… cuando lo que fallan todas a la vez suele ser del orden de tres puntos. Lo probé, y corregir casa a casa no mejoraba nada. Lo que importa es lo que fallan juntas.

Ver el código
knitr::kable(
  casas |>
    transmute(
      Casa = casa,
      `Se aparta del grupo en 2026 (puntos)` = num_es(grupo, 1),
      `Se pasa con el PP frente al resultado (puntos)` = if_else(
        is.na(historia),
        "sin historia",
        num_es(historia, 1)
      )
    ),
  align = "lrr"
)
Casa Se aparta del grupo en 2026 (puntos) Se pasa con el PP frente al resultado (puntos)
gesop -2,6 -1,8
more in common -2,4 sin historia
cluster17 -1,9 sin historia
imop -1,4 -1,4
target point -0,8 0,3
gad3 -0,7 1,7
sociometrica -0,2 0,3
40db 0,0 -0,1
celeste-tel 0,3 1,2
nc report 0,5 1,3
hamalgama metrica 0,6 0,8
invymark 0,7 sin historia
dym 1,3 0,1
data10 1,9 1,1

Sumar, Podemos y el resto

Un par de cosillas más de cómo se monta el voto del 29-N.

El error del pasado solo lo conozco para la familia de Sumar entera (Podemos e IU, Unidas Podemos, Sumar con Podemos dentro). En 2026 van por separado, así que el error se aplica a la suma de los dos y luego se reparte con el peso que tenga cada uno. Es decir, si las encuestas se quedan cortas con el espacio a la izquierda del PSOE, se quedan cortas con los dos en proporción.

El voto del 29-N

Ver el código
sim_cj <- readRDS(file.path(dir_fb, "conjunto_sim.rds"))
voto <- as_tibble(sim_cj$shares) |>
  select(PP, PSOE, VOX, SUMAR, PODEMOS) |>
  pivot_longer(everything(), names_to = "partido", values_to = "pct") |>
  group_by(partido) |>
  summarise(
    mediana = median(pct),
    q05 = quantile(pct, 0.05),
    q95 = quantile(pct, 0.95)
  ) |>
  arrange(desc(mediana))
knitr::kable(
  voto |>
    transmute(
      Partido = recode(
        partido,
        VOX = "Vox",
        SUMAR = "Sumar",
        PODEMOS = "Podemos"
      ),
      `Mediana (%)` = num_es(mediana),
      `Intervalo del 90 % (%)` = paste0(num_es(q05), "–", num_es(q95))
    ),
  align = "lrr"
)
Partido Mediana (%) Intervalo del 90 % (%)
PP 31,2 26,1–36,3
PSOE 27,2 22,7–31,8
Vox 19,1 14,3–24,7
Sumar 6,7 5,1–8,9
Podemos 3,2 2,3–4,3
Ver el código
der <- sim_cj$draws$PP + sim_cj$draws$VOX
Ver el código
colores_voto <- c(
  "PP" = "#005999",
  "PSOE" = "#FF0126",
  "Vox" = "#51962A",
  "Sumar" = "#A00B85",
  "Podemos" = "#6B2E8C"
)
voto_draws <- as_tibble(sim_cj$shares) |>
  select(PP, PSOE, VOX, SUMAR, PODEMOS) |>
  pivot_longer(everything(), names_to = "partido", values_to = "pct") |>
  mutate(
    partido = recode(
      partido,
      VOX = "Vox",
      SUMAR = "Sumar",
      PODEMOS = "Podemos"
    ),
    partido = fct_reorder(partido, pct, .fun = median)
  )
ggplot(voto_draws, aes(x = pct, y = partido, fill = partido)) +
  # normalize = "groups": cada partido ocupa su fila; sin él, la densidad de
  # Podemos (la más estrecha) aplana las de los grandes
  ggdist::stat_halfeye(
    normalize = "groups",
    .width = c(0.5, 0.9),
    alpha = 0.8
  ) +
  geom_label(
    data = voto_draws |>
      summarise(pct = median(pct), .by = partido),
    aes(label = num_es(pct)),
    size = 3,
    fill = "white",
    nudge_y = -0.25
  ) +
  scale_fill_manual(values = colores_voto, guide = "none") +
  labs(x = "Voto (%)", y = NULL)
Figura 7: Distribución a posteriori del voto el 29-N (6.000 simulaciones del modelo conjunto). Barra gruesa: intervalo del 50 %; fina: del 90 %. Etiqueta: mediana.

Es el voto latente de la última semana de 2026 más el error del consenso, simulación a simulación. Teniendo el voto estimado (el latente más el error histórico), podemos meter toda la movida de como repartir los escaños, que ya he contado en post anteriores.

Ver el código
knitr::kable(
  esc_cj |>
    arrange(desc(etiqueta)) |>
    transmute(
      Partido = recode(
        partido,
        VOX = "Vox",
        SUMAR = "Frente Amplio (Sumar)",
        PODEMOS = "Podemos",
        AA = "Adelante Andalucía"
      ),
      `Escaños (mediana)` = mediana,
      `Intervalo del 90 %` = paste0(round(q05), "–", round(q95)),
      `Reparto que suma 350` = etiqueta
    ),
  align = "lrrr"
)
Partido Escaños (mediana) Intervalo del 90 % Reparto que suma 350
PP 128 104–152 128
PSOE 108 86–130 108
Vox 66 42–93 67
Frente Amplio (Sumar) 11 7–20 12
ERC 10 8–12 10
EH Bildu 7 6–8 7
Junts 5 3–6 5
PNV 5 3–6 5
Adelante Andalucía 3 2–6 3
BNG 2 2–3 2
CC 1 1–1 1
Podemos 1 0–2 1
UPN 1 0–1 1
SALF 0 0–0 0

Las medianas de cada partido no tienen por qué sumar 350. La última columna reparte los 350 escaños por resto mayor, para quien quiera un número por partido.

PP y Vox suman 195 escaños de mediana (intervalo del 90 %: 175–214), y alcanzan la mayoría absoluta en el 95 % de las simulaciones. Con el pipeline de octubre, el 99 %.

Las horquillas de cada partido son anchas, y la de Vox, la que más. Salen de dos sitios: lo que suelen fallar las encuestas todas a la vez, que en el pasado fue del orden de tres puntos con el PP, y las 5 semanas sin encuestas que quedan hasta el 29-N. La de la suma de PP y Vox es más estrecha en proporción, por la correlación negativa: cuando las encuestas fallan con uno, suelen fallar al revés con el otro.

¿Nos lo creemos?

Un intervalo del 90 % promete algo muy concreto: nueve de cada diez veces, el resultado cae dentro. Si cae siempre dentro, es demasiado ancho; si cae fuera a menudo, demasiado estrecho. ¿Lo cumple? Para saberlo hice lo único que se puede hacer: viajar al pasado.

Me puse en la víspera de cuatro generales (26J-2016, 28A-2019, 10N-2019 y 23J-2023), con solo lo que se sabía entonces: las encuestas de su campaña, las generales de antes y las autonómicas de antes. Ajusté el modelo, predije votos y escaños y miré dónde cayó el resultado de verdad.

Ver el código
bt <- leer("backtest_conjunto_resumen_principal_26j_28a_10n_23j.csv") |>
  filter(version %in% c("actual", "conjunto_principal_26j_28a_10n_23j")) |>
  mutate(
    version = if_else(
      version == "actual",
      "Pipeline de octubre",
      "Modelo nuevo"
    )
  )
bt_m <- leer("backtest_conjunto_mayorias_principal_26j_28a_10n_23j.csv")
bt_esc <- leer("backtest_conjunto_escanos_principal_26j_28a_10n_23j.csv")
fallo <- bt_esc |> filter(!dentro)
knitr::kable(
  bt |>
    arrange(tipo, version) |>
    transmute(
      Qué = tipo,
      Versión = version,
      `El real cae dentro del IC90` = paste(
        round(cobertura_ic90 * celdas),
        "de",
        celdas
      ),
      `Posición del real (calibrado ≈ 0,49)` = num_es(posicion_media, 2),
      `Ancho medio del IC90` = num_es(ancho_medio_ic90, 1)
    ),
  align = "llrrr"
)
Qué Versión El real cae dentro del IC90 Posición del real (calibrado ≈ 0,49) Ancho medio del IC90
escaños Modelo nuevo 14 de 15 0,43 34,5
escaños Pipeline de octubre 14 de 15 0,54 25,6
voto Modelo nuevo 14 de 15 0,50 7,3
voto Pipeline de octubre 12 de 15 0,55 5,5

Este modelo nuevo falla una vez, a cambio de horquillas más anchas. El fallo es el PP del 28A: sacó 66 escaños y el modelo daba entre 70 y 97.

La columna de la posición afina más: dice en qué parte del intervalo cae el resultado, 0 si cae en el centro y 1 si cae en el borde. En un intervalo bien calibrado ronda 0,49. En voto, el modelo nuevo está casi clavado; en escaños, un pelín prudente.

En 2023, cuando casi todo el mundo daba por hecha la mayoría de PP y Vox, el modelo, con lo que se sabía la víspera, le habría dado un 46 %. Una moneda al aire. Sumaron 170.

Con cuatro elecciones, esto dice que el modelo no está mal calibrado, no que sea perfecto. Y hay una cosa que el backtest no puede probar: en las elecciones pasadas, la última encuesta estaba a seis días de la votación, y en 2026 quedan 5 semanas. Esa parte de la horquilla se apoya en la volatilidad estimada, no en una validación.

Ver el código
diag_cj <- leer("conjunto_diagnostico.csv")
prior_pp <- leer("conjunto_prior_predictive.csv")
contr <- leer("contraccion_conjunto.csv")
pp_prior <- \(p, col) prior_pp[[col]][prior_pp$partido == p]
  • Diagnósticos de Stan. 0 divergencias, \(\hat{R}\) máximo de 1,011 y un tamaño efectivo mínimo de 659 muestras. En los ajustes del backtest de 2016 y 2019, con muy poca historia detrás, hubo unas pocas divergencias.
  • Prior predictive. Solo con los priors, antes de ver los datos, el modelo deja al PP entre el 8,1 y el 59,7 % y a Vox entre el 2,5 y el 28,3 %. No sabe casi nada, pero no admite locuras.
  • Qué aprende de los datos y qué pone el prior. La contracción compara la incertidumbre del prior con la del posterior: cerca de 1, lo deciden los datos; cerca de 0, el prior.
Ver el código
etiqueta_par <- c(
  sigma_gen = "Tamaño del error en generales",
  Omega = "Correlación del error",
  mu_gen = "Sesgo en generales",
  gamma = "Nivel generales frente a autonómicas",
  tau_rw = "Diferencia de volatilidad entre campañas",
  kappa = "Efecto de la campaña",
  sigma_rw = "Volatilidad semanal"
)
knitr::kable(
  contr |>
    filter(parametro %in% names(etiqueta_par)) |>
    filter(!(parametro == "sigma_rw" & !partido %in% c("PP", "VOX"))) |>
    arrange(desc(contraccion)) |>
    transmute(
      Parámetro = etiqueta_par[parametro],
      Partido = coalesce(partido, ""),
      Contracción = num_es(contraccion, 2)
    ),
  align = "llr"
)
Parámetro Partido Contracción
Tamaño del error en generales PSOE 0,93
Tamaño del error en generales PP 0,93
Volatilidad semanal PP 0,88
Correlación del error VOX-PP 0,87
Correlación del error VOX-PSOE 0,85
Tamaño del error en generales SUMAR 0,83
Tamaño del error en generales VOX 0,82
Correlación del error PSOE-PP 0,80
Nivel generales frente a autonómicas 0,79
Sesgo en generales PSOE 0,76
Sesgo en generales PP 0,72
Efecto de la campaña 0,67
Sesgo en generales SUMAR 0,67
Diferencia de volatilidad entre campañas 0,60
Sesgo en generales VOX 0,53
Volatilidad semanal VOX 0,49
Correlación del error SUMAR-PP 0,41
Correlación del error SUMAR-PSOE 0,30
Correlación del error SUMAR-VOX 0,22

Lo que más pesa en la predicción (el tamaño del error y la correlación PP–Vox) lo deciden los datos. Lo que pone sobre todo el prior son las correlaciones con Sumar y, en parte, el sesgo y la volatilidad de Vox. Es lo que hay con tan pocas generales con Vox.

Todos los supuestos principales

Supuesto De dónde sale ¿Puesto a prueba?
El voto latente es un paseo aleatorio, con volatilidad propia de cada campaña Modelo Sí: backtest
El consenso es la media de las casas, cada una con el mismo peso Decisión No
El error del consenso es normal en logit, con sesgo, tamaño y correlación Modelo Sí: backtest
Las autonómicas comparten con las generales la correlación y el patrón de los fallos Decisión En parte: contracción y backtest
Sin CIS ni ElectoPanel en los grandes partidos (ElectoPanel sí en el desglose regional) Decisión No
Encuestas que no separan la izquierda fuera del PSOE (Sigma Dos, Winston, Ateneo del Dato, Data10): entran agregadas, con celda Sumar + Podemos y SALF sin observar Regla No
Encuestas antiguas sin tamaño publicado: 1.000 personas A mano No

Todo está en 2026/10/full-bayesian/:

Los ajustes guardados (.rds) no están en el repositorio porque pesan mucho: están en la release full-bayesian-29n.

Como siempre, esto es un ejercicio para mi propia diversión, así que puede haber alguna burrada , se agradecen comentarios