Meta-análisis. Agregando encuestas III. Elecciones andaluzas 2026

2026
muestreo
encuestas electorales
análisis bayesiano
Agregación bayesiana de encuestas para las elecciones al Parlamento de Andalucía del 17 de mayo de 2026, usando un modelo multinomial con brms.
Published

May 10, 2026

Nota previa del “autor” humano

Este post se ha generado solito usando Claude code. Simplemente le dije que mirara como lo hice en las elecciones de 2023 (generales) y 2024(europeas) , que leyera mis posts, y que buscara datos en la web de las encuestas. Me ha creado el csv (que ni he revisado) y que hiciera la estimación siguiendo mi metodología.

El próximo sábado 16 de mayo de 2026 lo revisaré por si hay alguna cagada. O revisadlo vosotros y me decís que está mal.

Tiempo total que me ha llevado hacer el post, menos de 1 hora.

Introducción

Ya lo hice para las elecciones generales de julio de 2023 y para las europeas de junio de 2024. Toca repetir el ejercicio para las elecciones al Parlamento de Andalucía del 17 de mayo de 2026.

La metodología es la misma: modelo multinomial bayesiano con brms donde cada encuesta aporta un vector de votos estimados, la empresa encuestadora entra como efecto aleatorio y la variable temporal (time, días hasta las elecciones) recoge la tendencia.

Los datos los he sacado de la tabla de encuestas de Wikipedia (en). Solo he incluido encuestas con tamaño muestral publicado. La columna resto es el complemento a 100 de los cinco partidos principales: PP, PSOE-A, Vox, Por Andalucía y Adelante Andalucía.

Warning

Esto es solo por diversión. No soy experto en predicción electoral. Para algo serio habría que incluir más encuestas, datos provinciales y traducir los porcentajes a escaños con la ley D’Hondt por circunscripción.

Datos

Show the code
library(tidyverse)
library(DT)

df <- read_csv(here::here("2026/05/encuestas_andalucia_2026.csv")) |>
  select(empresa, fecha, n, everything())

datatable(df)

Calculamos time (días hasta las elecciones) y votos (estimación * n / 100) para cada partido.

Show the code
fecha_elecciones <- lubridate::ymd("2026-05-17")

df_long <- df |>
  pivot_longer(c(pp, psoe, vox, por_andalucia, adelante, resto),
               names_to = "partido",
               values_to = "estim") |>
  mutate(
    votos = round(n * estim / 100),
    time  = as.numeric(fecha - fecha_elecciones)
  )

datatable(df_long)

Pintamos la evolución temporal de la estimación por partido:

Show the code
colores <- c(
  "pp"            = "#005999",
  "psoe"          = "#FF0126",
  "vox"           = "#51962A",
  "por_andalucia" = "#E51C55",
  "adelante"      = "#8C66F1",
  "resto"         = "grey"
)

df_long |>
  ggplot(aes(x = time, y = estim, color = partido)) +
  geom_point() +
  scale_color_manual(values = colores) +
  geom_smooth(se = FALSE) +
  labs(
    title = "Encuestas elecciones andaluzas 2026",
    x = "Días hasta las elecciones (0 = 17 mayo)",
    y = "Estimación (%)"
  )

Formato ancho para brms

Show the code
df_wider <- df_long |>
  select(-estim) |>
  pivot_wider(
    id_cols  = c(empresa, n, time),
    names_from  = partido,
    values_from = votos
  ) |>
  mutate(
    n = pp + psoe + vox + por_andalucia + adelante + resto
  )

df_wider$cell_counts <- with(
  df_wider,
  cbind(pp, psoe, vox, por_andalucia, adelante, resto)
)

datatable(df_wider |> select(empresa, time, n, cell_counts))

Modelo meta-análisis

Show the code
library(cmdstanr)
library(brms)
library(tidybayes)

options(brms.backend = "cmdstanr")
Show the code
formula <- brmsformula(
  cell_counts | trials(n) ~ time + (time | empresa)
)

(priors <- get_prior(formula, df_wider, family = multinomial()))
#>                 prior     class      coef   group resp           dpar nlpar lb
#>                lkj(1)       cor                                               
#>                lkj(1)       cor           empresa                             
#>                (flat)         b                            muadelante         
#>                (flat)         b      time                  muadelante         
#>  student_t(3, 0, 2.5) Intercept                            muadelante         
#>  student_t(3, 0, 2.5)        sd                            muadelante        0
#>  student_t(3, 0, 2.5)        sd           empresa          muadelante        0
#>  student_t(3, 0, 2.5)        sd Intercept empresa          muadelante        0
#>  student_t(3, 0, 2.5)        sd      time empresa          muadelante        0
#>                (flat)         b                        muporandalucia         
#>                (flat)         b      time              muporandalucia         
#>  student_t(3, 0, 2.5) Intercept                        muporandalucia         
#>  student_t(3, 0, 2.5)        sd                        muporandalucia        0
#>  student_t(3, 0, 2.5)        sd           empresa      muporandalucia        0
#>  student_t(3, 0, 2.5)        sd Intercept empresa      muporandalucia        0
#>  student_t(3, 0, 2.5)        sd      time empresa      muporandalucia        0
#>                (flat)         b                                mupsoe         
#>                (flat)         b      time                      mupsoe         
#>  student_t(3, 0, 2.5) Intercept                                mupsoe         
#>  student_t(3, 0, 2.5)        sd                                mupsoe        0
#>  student_t(3, 0, 2.5)        sd           empresa              mupsoe        0
#>  student_t(3, 0, 2.5)        sd Intercept empresa              mupsoe        0
#>  student_t(3, 0, 2.5)        sd      time empresa              mupsoe        0
#>                (flat)         b                               muresto         
#>                (flat)         b      time                     muresto         
#>  student_t(3, 0, 2.5) Intercept                               muresto         
#>  student_t(3, 0, 2.5)        sd                               muresto        0
#>  student_t(3, 0, 2.5)        sd           empresa             muresto        0
#>  student_t(3, 0, 2.5)        sd Intercept empresa             muresto        0
#>  student_t(3, 0, 2.5)        sd      time empresa             muresto        0
#>                (flat)         b                                 muvox         
#>                (flat)         b      time                       muvox         
#>  student_t(3, 0, 2.5) Intercept                                 muvox         
#>  student_t(3, 0, 2.5)        sd                                 muvox        0
#>  student_t(3, 0, 2.5)        sd           empresa               muvox        0
#>  student_t(3, 0, 2.5)        sd Intercept empresa               muvox        0
#>  student_t(3, 0, 2.5)        sd      time empresa               muvox        0
#>  ub tag       source
#>              default
#>         (vectorized)
#>              default
#>         (vectorized)
#>              default
#>              default
#>         (vectorized)
#>         (vectorized)
#>         (vectorized)
#>              default
#>         (vectorized)
#>              default
#>              default
#>         (vectorized)
#>         (vectorized)
#>         (vectorized)
#>              default
#>         (vectorized)
#>              default
#>              default
#>         (vectorized)
#>         (vectorized)
#>         (vectorized)
#>              default
#>         (vectorized)
#>              default
#>              default
#>         (vectorized)
#>         (vectorized)
#>         (vectorized)
#>              default
#>         (vectorized)
#>              default
#>              default
#>         (vectorized)
#>         (vectorized)
#>         (vectorized)
Show the code
model_andalucia <- brm(
  formula,
  df_wider,
  multinomial(),
  prior   = priors,
  iter    = 4000,
  warmup  = 1000,
  cores   = 4,
  chains  = 4,
  seed    = 47,
  file    = here::here("2026/05/mod_meta_andalucia"),
  backend = "cmdstanr",
  control = list(adapt_delta = 0.95),
  refresh = 0
)

summary(model_andalucia)
#>  Family: multinomial 
#>   Links: mupsoe = logit; muvox = logit; muporandalucia = logit; muadelante = logit; muresto = logit 
#> Formula: cell_counts | trials(n) ~ time + (time | empresa) 
#>    Data: df_wider (Number of observations: 13) 
#>   Draws: 4 chains, each with iter = 4000; warmup = 1000; thin = 1;
#>          total post-warmup draws = 12000
#> 
#> Multilevel Hyperparameters:
#> ~empresa (Number of levels: 8) 
#>                                                   Estimate Est.Error l-95% CI
#> sd(mupsoe_Intercept)                                  0.09      0.05     0.01
#> sd(mupsoe_time)                                       0.00      0.00     0.00
#> sd(muvox_Intercept)                                   0.15      0.11     0.01
#> sd(muvox_time)                                        0.01      0.00     0.00
#> sd(muporandalucia_Intercept)                          0.10      0.08     0.00
#> sd(muporandalucia_time)                               0.00      0.00     0.00
#> sd(muadelante_Intercept)                              0.15      0.10     0.01
#> sd(muadelante_time)                                   0.00      0.00     0.00
#> sd(muresto_Intercept)                                 0.42      0.18     0.21
#> sd(muresto_time)                                      0.01      0.01     0.00
#> cor(mupsoe_Intercept,mupsoe_time)                     0.15      0.57    -0.93
#> cor(muvox_Intercept,muvox_time)                       0.02      0.59    -0.95
#> cor(muporandalucia_Intercept,muporandalucia_time)     0.19      0.58    -0.92
#> cor(muadelante_Intercept,muadelante_time)             0.21      0.58    -0.92
#> cor(muresto_Intercept,muresto_time)                   0.53      0.41    -0.52
#>                                                   u-95% CI Rhat Bulk_ESS
#> sd(mupsoe_Intercept)                                  0.20 1.00     5086
#> sd(mupsoe_time)                                       0.01 1.00     3774
#> sd(muvox_Intercept)                                   0.42 1.00     2924
#> sd(muvox_time)                                        0.01 1.00     2003
#> sd(muporandalucia_Intercept)                          0.31 1.00     6121
#> sd(muporandalucia_time)                               0.01 1.00     2745
#> sd(muadelante_Intercept)                              0.40 1.00     4202
#> sd(muadelante_time)                                   0.01 1.00     3170
#> sd(muresto_Intercept)                                 0.85 1.00     3520
#> sd(muresto_time)                                      0.02 1.00     2198
#> cor(mupsoe_Intercept,mupsoe_time)                     0.96 1.00    10050
#> cor(muvox_Intercept,muvox_time)                       0.96 1.00     5116
#> cor(muporandalucia_Intercept,muporandalucia_time)     0.98 1.00     3221
#> cor(muadelante_Intercept,muadelante_time)             0.98 1.00     7381
#> cor(muresto_Intercept,muresto_time)                   0.98 1.00     8481
#>                                                   Tail_ESS
#> sd(mupsoe_Intercept)                                  4290
#> sd(mupsoe_time)                                       5109
#> sd(muvox_Intercept)                                   4862
#> sd(muvox_time)                                        5069
#> sd(muporandalucia_Intercept)                          5735
#> sd(muporandalucia_time)                               4115
#> sd(muadelante_Intercept)                              4156
#> sd(muadelante_time)                                   4024
#> sd(muresto_Intercept)                                 3650
#> sd(muresto_time)                                      1821
#> cor(mupsoe_Intercept,mupsoe_time)                     8576
#> cor(muvox_Intercept,muvox_time)                       6628
#> cor(muporandalucia_Intercept,muporandalucia_time)     5184
#> cor(muadelante_Intercept,muadelante_time)             7699
#> cor(muresto_Intercept,muresto_time)                   4695
#> 
#> Regression Coefficients:
#>                          Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS
#> mupsoe_Intercept            -0.64      0.05    -0.73    -0.55 1.00     8429
#> muvox_Intercept             -1.11      0.08    -1.29    -0.95 1.00     8445
#> muporandalucia_Intercept    -1.58      0.08    -1.73    -1.40 1.00     5262
#> muadelante_Intercept        -1.82      0.09    -2.01    -1.66 1.00     7982
#> muresto_Intercept           -2.10      0.18    -2.46    -1.71 1.00     4029
#> mupsoe_time                 -0.00      0.00    -0.00     0.00 1.00     6394
#> muvox_time                  -0.00      0.00    -0.01     0.00 1.00     5302
#> muporandalucia_time          0.00      0.00    -0.00     0.01 1.00     4175
#> muadelante_time              0.00      0.00    -0.00     0.01 1.00     5209
#> muresto_time                 0.00      0.00    -0.01     0.01 1.00     3671
#>                          Tail_ESS
#> mupsoe_Intercept             6319
#> muvox_Intercept              8238
#> muporandalucia_Intercept     6794
#> muadelante_Intercept         5153
#> muresto_Intercept            2607
#> mupsoe_time                  4260
#> muvox_time                   5435
#> muporandalucia_time          5120
#> muadelante_time              3187
#> muresto_time                 1950
#> 
#> Draws were sampled using sample(hmc). For each parameter, Bulk_ESS
#> and Tail_ESS are effective sample size measures, and Rhat is the potential
#> scale reduction factor on split chains (at convergence, Rhat = 1).

Estimación para el día de las elecciones

Tratamos las elecciones como una “nueva encuesta” con time = 0 y empresa desconocida:

Show the code
newdata <- tibble(
  empresa = "votaciones_17mayo",
  time    = 0,
  n       = 1
)

estimaciones <- newdata |>
  add_epred_draws(model_andalucia, allow_new_levels = TRUE) |>
  mutate(partido = as_factor(.category)) |>
  select(-.category)

dim(estimaciones)
#> [1] 72000    10

Resumen con intervalo de credibilidad al 90%:

Show the code
estimaciones |>
  group_by(partido) |>
  summarise(
    media   = mean(.epred),
    mediana = median(.epred),
    low     = quantile(.epred, 0.05),
    high    = quantile(.epred, 0.95)
  ) |>
  mutate(across(media:high, \(x) 100 * round(x, 3)))
#> # A tibble: 6 × 5
#>   partido       media mediana   low  high
#>   <fct>         <dbl>   <dbl> <dbl> <dbl>
#> 1 pp             42.4    42.4  40.3  44.5
#> 2 psoe           22.5    22.6  19.7  25.1
#> 3 vox            14      14    10.8  17  
#> 4 por_andalucia   8.8     8.7   7.4  10.2
#> 5 adelante        6.9     6.9   5.5   8.4
#> 6 resto           5.4     5     3.2   9.2
Show the code
estimaciones |>
  ggplot(aes(x = .epred, fill = partido)) +
  geom_density(alpha = 0.5) +
  scale_x_continuous(labels = scales::percent, limits = c(0, 0.6)) +
  scale_fill_manual(values = colores) +
  labs(
    title = "Agregando encuestas andaluzas 2026. Estimación día de las elecciones",
    x     = "Porcentaje estimado",
    y     = "Densidad"
  )

¿Mayoría absoluta del PP?

El Parlamento andaluz tiene 109 escaños; la mayoría absoluta son 55. No traduzco aquí porcentajes a escaños (necesitaría hacerlo provincia a provincia con D’Hondt), pero sí puedo ver la distribución del voto estimado al PP y compararlo con lo que necesitaría grosso modo.

Show the code
estimaciones |>
  filter(partido == "pp") |>
  summarise(
    prob_pp_sobre_40pct = mean(.epred > 0.40),
    prob_pp_sobre_43pct = mean(.epred > 0.43)
  )
#> # A tibble: 1 × 7
#> # Groups:   empresa, time, n, .row [1]
#>   empresa     time     n  .row .category prob_pp_sobre_40pct prob_pp_sobre_43pct
#>   <chr>      <dbl> <dbl> <int> <fct>                   <dbl>               <dbl>
#> 1 votacione…     0     1     1 pp                      0.967               0.313

Coda

Datos de 13 encuestas (fuente: Wikipedia EN, tabla de sondeos). La metodología es idéntica a los posts anteriores: modelo multinomial bayesiano con efectos aleatorios por empresa y tendencia temporal. Actualización post-electoral pendiente una vez se conozcan los resultados del 17-M.