☕ Café

evaluación
yardstick
pronósticos
elecciones

¿Qué tan acertadas han sido las encuestas y los pronósticos? Un repaso por 3 ciclos electorales.

Autor/a
Afiliación

Recetas Electorales

Análisis independiente

Fecha de publicación

25 de mayo de 2026

Fecha de última modificación

1 de junio de 2026

Las Recetas Electorales han producido pronósticos para las elecciones presidenciales colombianas de 2026, 2022 y 2018. Este artículo evalúa su precisión con métricas estadísticas del paquete yardstick.

En 2026, la primera vuelta registró el mayor error sistemático de las encuestadoras colombianas en tres ciclos. Abelardo de la Espriella (43.72%) superó a Cepeda (40.92%) y quedó más de 15 puntos por encima del promedio de encuestas. Paloma Valencia fue sobreestimada en más de 13 puntos. Los modelos bayesianos, alimentados con las mismas encuestas, cometieron el mismo error.

En 2022, todos fallamos. Rodolfo Hernández fue una gran sorpresa: ningún modelo lo proyectó por encima del 25%, y obtuvo el 27.8%. Las razones son múltiples: las encuestadoras lo subestimaron sistemáticamente durante toda la campaña; su campaña viral y de bajo presupuesto era invisible a los modelos basados en datos históricos; su momentum se aceleró en las últimas dos semanas, después de que los pronósticos ya se habían realizado; y los modelos de promedio ponderado de encuestas amortiguan exactamente los cambios bruscos que caracterizan una candidatura insurgente. Es un recordatorio de que los modelos bayesianos no son inmunes a errores de medición sistemáticos en los datos que los alimentan.

En 2018, los modelos bayesianos jerárquicos fueron buenos predictores. El campo era relativamente predecible: Duque y Petro como favoritos, Fajardo como incógnita moderada, así que los modelos Simple y Mixto registraron los MAE más bajos de ese ciclo. Validación positiva para el enfoque de pooling bayesiano con encuestas colombianas.

Cada pronóstico es un vector de porcentajes de votos por candidato. Lo comparamos con los resultados reales usando estas métricas:

Métrica Fórmula Interpretación
MAE \(\frac{1}{n}\sum\|y_i - \hat{y}_i\|\) Error promedio en puntos porcentuales
RMSE \(\sqrt{\frac{1}{n}\sum(y_i - \hat{y}_i)^2}\) Penaliza más los errores grandes
Sesgo \(\frac{1}{n}\sum(\hat{y}_i - y_i)\) Sobreestimación (+) o subestimación (−) sistemática
sMAPE \(\frac{2}{n}\sum\frac{\|y_i - \hat{y}_i\|}{\|y_i\| + \|\hat{y}_i\|}\) Error relativo simétrico (adimensional)
\(1 - \frac{\sum(y_i - \hat{y}_i)^2}{\sum(y_i - \bar{y})^2}\) Correlación entre pronóstico y resultado
Brier \(\frac{1}{K}\sum_{k=1}^{K}(p_k - o_k)^2\) Regla de puntuación propia; \(p_k, o_k\) son proporciones normalizadas
Winkler \((u-l) + \frac{2}{\alpha}\max(l-y,0) + \frac{2}{\alpha}\max(y-u,0)\) Puntaje de intervalo al 95%; combina amplitud y penalización por fallas

Para los pronósticos con intervalos también calculamos cobertura y el puntaje de Winkler, que penaliza simultáneamente los intervalos anchos y los que no capturan el resultado real.

R² es inestable con 4–5 observaciones (candidatos) por fuente. El Brier score normaliza los pronósticos a proporciones que sumen 1, lo que lo hace comparable entre fuentes aunque los totales pronosticados difieran. MAE y RMSE siguen siendo las métricas más directamente interpretables.

Elecciones 2026

Primera vuelta

Ver código
library(ggdist)

# Índice para junio 1 — igual que J_pred en el artículo Gaussiano
j_pred_fin     <- as.numeric(lubridate::ymd("2026-05-31") - min(encuestas_raw$fecha)) + 1L
cols_gauss_fin <- paste0("p_pred[", j_pred_fin, ",", 1:6, "]")

color_map_2026 <- tibble::tibble(
  cod        = cand_codes,
  color_cand = c(colores_2026[["ic"]], colores_2026[["adle"]],
                 colores_2026[["pv"]], colores_2026[["sf"]], colores_2026[["cl"]])
)

gauss_draws_fig <- arrow::read_parquet(
  gaussiano_path,
  col_select = dplyr::all_of(cols_gauss_fin)
) |>
  purrr::set_names(c(cand_codes, "ruido")) |>
  tidyr::pivot_longer(dplyr::everything(), names_to = "cod", values_to = "prop") |>
  dplyr::filter(cod != "ruido") |>
  dplyr::left_join(candidatos_2026_df, by = "cod") |>
  dplyr::left_join(color_map_2026,     by = "cod") |>
  dplyr::group_by(cod) |>
  dplyr::mutate(candidato_m = mean(prop)) |>
  dplyr::ungroup()

nombre_orden_2026 <- gauss_draws_fig |>
  dplyr::distinct(cod, candidato_m, nombre) |>
  dplyr::arrange(candidato_m) |>
  dplyr::pull(nombre)

gauss_draws_fig |>
  dplyr::mutate(nombre = factor(nombre, levels = nombre_orden_2026)) |>
  ggplot2::ggplot(ggplot2::aes(x = prop, y = nombre)) +
  ggdist::stat_dist_slabinterval(
    ggplot2::aes(
      fill      = color_cand,
      fill_ramp = ggplot2::after_stat(level)
    ),
    .width         = c(0.5, 0.8, 0.95, 0.99),
    interval_alpha = 0.95,
    show.legend    = c(fill = FALSE, fill_ramp = TRUE, size = FALSE)
  ) +
  ggdist::stat_pointinterval(
    ggplot2::aes(label = scales::percent(candidato_m, accuracy = 0.1)),
    geom   = "label",
    .width = 0,
    vjust  = -0.5,
    fill   = "white",
    color  = "black"
  ) +
  {
    res_fig <- resultados_2026 |>
      dplyr::rename(nombre = candidato) |>
      dplyr::left_join(
        dplyr::left_join(candidatos_2026_df, color_map_2026, by = "cod") |>
          dplyr::select(nombre, color_cand),
        by = "nombre"
      ) |>
      dplyr::mutate(
        nombre    = factor(nombre, levels = nombre_orden_2026),
        y_num     = as.numeric(factor(nombre, levels = nombre_orden_2026)),
        resultado = resultado / 100
      )
    list(
      ggplot2::geom_segment(
        data = res_fig,
        ggplot2::aes(
          x = resultado, xend = resultado,
          y = y_num - 0.4, yend = y_num + 0.4,
          color = color_cand
        ),
        linewidth = 2.5, inherit.aes = FALSE
      ),
      ggplot2::geom_label(
        data = res_fig,
        ggplot2::aes(
          x     = resultado,
          y     = y_num + 0.6,
          label = scales::percent(resultado, accuracy = 0.1),
          fill  = color_cand
        ),
        color         = "white",
        size          = 5,
        fontface      = "bold",
        label.size    = 0,
        label.padding = ggplot2::unit(0.25, "lines"),
        inherit.aes   = FALSE
      ),
      ggplot2::scale_color_identity(guide = "none")
    )
  } +
  ggplot2::scale_x_continuous(
    labels = scales::percent_format(accuracy = 1),
    expand = expansion(mult = c(0.05, 0.15))
  ) +
  ggplot2::scale_fill_identity(na.translate = FALSE) +
  ggdist::scale_fill_ramp_discrete(
    name   = "Intervalo creíble",
    range  = c(0.25, 1),
    breaks = c(0.5, 0.8, 0.95, 0.99),
    labels = c("50%", "80%", "95%", "99%"),
    na.translate = FALSE
  ) +
  ggplot2::labs(
    x       = "Distribución posterior, % votos primera vuelta 2026 (Gaussiano, 31 mayo)",
    y       = NULL,
    title   = "Modelo Gaussiano vs resultado — primera vuelta 2026",
    caption = "Intervalos creíbles: 50%, 80%, 95%, 99%. Línea vertical: resultado real 31 de mayo."
  ) +
  theme_recetas(base_size = 16) +
  ggplot2::theme(
    panel.grid.major.y = ggplot2::element_blank(),
    panel.grid.minor   = ggplot2::element_blank(),
    legend.position    = "bottom"
  )
Figura 1: Distribución posterior del modelo Gaussiano — primera vuelta 2026
Ver código
calcular_metricas(datos_2026) |>
  tabla_metricas(titulo = "Pronósticos 2026")
Tabla 1: Métricas de precisión — Primera vuelta 2026
Pronósticos 2026
Fuente MAE RMSE sMAPE Sesgo Brier Cobertura Amplitud Winkler
Gaussiano 7.08 9.91 53.7% 0.752 -1.81 0.00956 40% 9.4 142.4
Encuestas 7.44 10.33 55.8% 0.729 -2.07 0.01038 NA NA NA
Cazuela 7.72 11.15 61.1% 0.715 -3.78 0.01082 20% 4.6 222.2
MAE/RMSE: puntos porcentuales (p.p.), menor es mejor.  Sesgo: sobreestimación (+) o subestimación (−) promedio.  sMAPE: error porcentual simétrico, menor es mejor.  R²: 1 = pronóstico perfecto.  Brier: regla de puntuación propia sobre proporciones normalizadas, menor es mejor.  Winkler: puntaje de intervalo al 95%, menor es mejor.  Cobertura: % candidatos dentro del intervalo predicho.  Amplitud: ancho promedio del intervalo (p.p.).

Encuestadoras

El resultado de la primera vuelta de 2026 registró el mayor error sistemático de las encuestadoras colombianas en tres ciclos. Abelardo de la Espriella obtuvo 43.72% cuando ninguna casa lo proyectaba por encima del 35%. Paloma Valencia fue sobreestimada en más de 13 puntos: las encuestas le daban entre 16% y 22%, y obtuvo 6.92%. La tabla compara la última encuesta de cada casa con los modelos de Recetas Electorales.

Ver código
calcular_metricas(datos_2026_todos) |>
  tabla_metricas(titulo = "Encuestadoras y modelos — 2026")
Tabla 2: Ranking de precisión — encuestadoras y modelos, primera vuelta 2026
Encuestadoras y modelos — 2026
Fuente MAE RMSE sMAPE Sesgo Brier Cobertura Amplitud Winkler
AtlasIntel 3.65 4.78 24% 0.960 -0.79 0.00202 NA NA NA
Invamer 5.20 6.57 48.7% 0.882 -0.39 0.00453 NA NA NA
Gaussiano 7.08 9.91 53.7% 0.752 -1.81 0.00956 40% 9.4 142.4
Guarumo 7.29 9.98 46.4% 0.740 -1.15 0.01012 NA NA NA
CELAG 7.40 11.88 50.5% 0.637 -3.39 0.01454 NA NA NA
Encuestas 7.44 10.33 55.8% 0.729 -2.07 0.01038 NA NA NA
CNC 7.49 11.39 60.5% 0.720 -4.59 0.01074 NA NA NA
GAD3 7.61 10.83 64% 0.767 -4.35 0.00887 NA NA NA
Cazuela 7.72 11.15 61.1% 0.715 -3.78 0.01082 20% 4.6 222.2
Tempo 8.93 12.74 49% 0.547 -0.83 0.01737 NA NA NA
Genesis Crea 10.09 13.22 70.4% 0.519 -1.63 0.01827 NA NA NA
Corp. Miguel Maldonado Manjarrez 10.85 14.67 68.4% 0.409 -1.51 0.02329 NA NA NA
MAE/RMSE: puntos porcentuales (p.p.), menor es mejor.  Sesgo: sobreestimación (+) o subestimación (−) promedio.  sMAPE: error porcentual simétrico, menor es mejor.  R²: 1 = pronóstico perfecto.  Brier: regla de puntuación propia sobre proporciones normalizadas, menor es mejor.  Winkler: puntaje de intervalo al 95%, menor es mejor.  Cobertura: % candidatos dentro del intervalo predicho.  Amplitud: ancho promedio del intervalo (p.p.).
Ver código
encuestadoras_2026_largo |>
  dplyr::mutate(
    candidato = factor(candidato, levels = candidatos_2026_df$nombre),
    fuente    = forcats::fct_reorder(fuente, pronostico, .fun = mean)
  ) |>
  ggplot2::ggplot(ggplot2::aes(x = pronostico, y = fuente)) +
  ggplot2::geom_vline(
    data = resultados_2026 |>
      dplyr::rename(candidato = candidato) |>
      dplyr::mutate(candidato = factor(candidato, levels = candidatos_2026_df$nombre)),
    ggplot2::aes(xintercept = resultado),
    linetype = "dashed", color = "#333333", linewidth = 0.9,
    inherit.aes = FALSE
  ) +
  ggplot2::geom_point(
    ggplot2::aes(color = candidato),
    size = 3
  ) +
  ggplot2::facet_wrap(~ candidato, scales = "free_x", ncol = 1) +
  ggplot2::scale_color_manual(values = colores_2026_nombres, guide = "none") +
  ggplot2::scale_x_continuous(
    labels = scales::label_percent(scale = 1, suffix = "%"),
    expand = expansion(mult = c(0.08, 0.12))
  ) +
  ggplot2::labs(
    title    = "Última encuesta vs resultado — encuestadoras 2026",
    subtitle = "Línea punteada oscura: resultado real · Cada punto: última encuesta de la casa",
    x        = NULL,
    y        = NULL,
    caption  = "Encuestas desde el 12 de marzo de 2026. Última encuesta publicada por cada casa."
  ) +
  theme_recetas() +
  ggplot2::theme(
    strip.text    = ggplot2::element_text(face = "bold", size = 11),
    panel.spacing = ggplot2::unit(1, "lines"),
    axis.text.y   = ggplot2::element_text(size = 9)
  )
Figura 2: Última encuesta por casa vs resultado real — primera vuelta 2026

Elecciones 2022

Primera vuelta

Cuatro candidatos, ocho pronósticos. Rodolfo Hernández fue la gran sorpresa: ningún modelo lo proyectó por encima del 25% y obtuvo 27.8%.

Ver código
calcular_metricas(datos_2022) |>
  tabla_metricas(titulo = "Pronósticos 2022")
Tabla 3: Métricas de precisión — Primera vuelta 2022
Pronósticos 2022
Fuente MAE RMSE sMAPE Sesgo Brier Cobertura Amplitud Winkler
Encuestas 2.49 2.95 21.8% 0.960 0.26 0.00099 100% 21.2 21.2
La Silla Vacia 2.72 4.01 14.9% 0.918 -1.61 0.00154 NA NA NA
@calc_electoral 2.90 3.36 16.8% 0.936 0.14 0.00122 100% 14.2 14.2
@JorgeGalindo El Pais 3.78 5.17 23% 0.852 -1.17 0.00298 NA NA NA
@PoliticaConDato 5.01 5.96 35% 0.785 0.07 0.00390 50% 8.1 49.0
Ajiaco 5.47 7.43 35.4% 0.735 -3.36 0.00501 75% 13.2 81.4
@ColombiaRisk 6.99 8.52 41.9% 0.734 -5.31 0.00542 NA NA NA
Mixto 2022 7.01 9.35 46% 0.567 -3.78 0.00910 50% 7.0 185.4
Simple 2022 7.34 9.48 48.2% 0.541 -3.46 0.00952 50% 6.0 196.0
MAE/RMSE: puntos porcentuales (p.p.), menor es mejor.  Sesgo: sobreestimación (+) o subestimación (−) promedio.  sMAPE: error porcentual simétrico, menor es mejor.  R²: 1 = pronóstico perfecto.  Brier: regla de puntuación propia sobre proporciones normalizadas, menor es mejor.  Winkler: puntaje de intervalo al 95%, menor es mejor.  Cobertura: % candidatos dentro del intervalo predicho.  Amplitud: ancho promedio del intervalo (p.p.).
Ver código
plot_cafe(datos_2022, resultados_2022, candidatos_2022,
          "Pronósticos vs resultado — 2022", colores_2022)
Figura 3: Pronósticos vs resultado real — Primera vuelta 2022

Encuestadoras

El error de 2022 fue Rodolfo Hernández: obtuvo 27.82% cuando ninguna encuestadora lo proyectaba por encima del 18%. Petro y Gutierrez fueron estimados con mayor precisión. Fajardo fue sobreestimado por casi todas las casas.

Ver código
calcular_metricas(datos_2022_todos) |>
  tabla_metricas(titulo = "Encuestadoras y modelos — 2022")
Tabla 4: Ranking de precisión — encuestadoras y modelos, primera vuelta 2022
Encuestadoras y modelos — 2022
Fuente MAE RMSE sMAPE Sesgo Brier Cobertura Amplitud Winkler
La Silla Vacia 2.72 4.01 14.9% 0.918 -1.61 0.00154 NA NA NA
@calc_electoral 2.90 3.36 16.8% 0.936 0.14 0.00122 100% 14.2 14.2
AtlasIntel 3.58 4.25 27.7% 0.897 0.37 0.00201 NA NA NA
@JorgeGalindo El Pais 3.78 5.17 23% 0.852 -1.17 0.00298 NA NA NA
CNC 3.93 5.01 16% 0.951 -3.93 0.00098 NA NA NA
Guarumo 4.20 5.28 16.6% 0.837 -0.53 0.00310 NA NA NA
CELAG 4.25 4.90 19.1% 0.895 -1.23 0.00292 NA NA NA
@PoliticaConDato 5.01 5.96 35% 0.785 0.07 0.00390 50% 8.1 49.0
Mosqueteros 5.22 6.65 30.6% 0.785 -1.38 0.00521 NA NA NA
YanHaas 5.37 8.15 35.8% 0.707 -3.86 0.00725 NA NA NA
Ajiaco 5.47 7.43 35.4% 0.735 -3.36 0.00501 75% 13.2 81.4
Invamer 5.78 7.46 33.1% 0.729 -1.18 0.00652 NA NA NA
MassiveCaller 5.85 7.91 30.9% 0.698 -3.28 0.00670 NA NA NA
TYSE 6.07 8.74 35.9% 0.701 -5.11 0.00731 NA NA NA
@ColombiaRisk 6.99 8.52 41.9% 0.734 -5.31 0.00542 NA NA NA
Mixto 2022 7.01 9.35 46% 0.567 -3.78 0.00910 50% 7.0 185.4
Simple 2022 7.34 9.48 48.2% 0.541 -3.46 0.00952 50% 6.0 196.0
MAE/RMSE: puntos porcentuales (p.p.), menor es mejor.  Sesgo: sobreestimación (+) o subestimación (−) promedio.  sMAPE: error porcentual simétrico, menor es mejor.  R²: 1 = pronóstico perfecto.  Brier: regla de puntuación propia sobre proporciones normalizadas, menor es mejor.  Winkler: puntaje de intervalo al 95%, menor es mejor.  Cobertura: % candidatos dentro del intervalo predicho.  Amplitud: ancho promedio del intervalo (p.p.).
Ver código
encuestadoras_2022_largo |>
  dplyr::mutate(
    candidato = factor(candidato, levels = candidatos_2022_df$nombre),
    fuente    = forcats::fct_reorder(fuente, pronostico, .fun = mean, na.rm = TRUE)
  ) |>
  ggplot2::ggplot(ggplot2::aes(x = pronostico, y = fuente)) +
  ggplot2::geom_vline(
    data = resultados_2022 |>
      dplyr::mutate(candidato = factor(candidato, levels = candidatos_2022_df$nombre)),
    ggplot2::aes(xintercept = resultado),
    linetype = "dashed", color = "#333333", linewidth = 0.9,
    inherit.aes = FALSE
  ) +
  ggplot2::geom_point(
    ggplot2::aes(color = candidato),
    size = 3
  ) +
  ggplot2::facet_wrap(~ candidato, scales = "free_x", ncol = 1) +
  ggplot2::scale_color_manual(values = colores_2022, guide = "none") +
  ggplot2::scale_x_continuous(
    labels = scales::label_percent(scale = 1, suffix = "%"),
    expand = ggplot2::expansion(mult = c(0.08, 0.12))
  ) +
  ggplot2::labs(
    title    = "Última encuesta vs resultado — encuestadoras 2022",
    subtitle = "Línea punteada oscura: resultado real · Cada punto: última encuesta de la casa",
    x        = NULL,
    y        = NULL,
    caption  = "Última encuesta publicada antes del 29 de mayo de 2022."
  ) +
  theme_recetas() +
  ggplot2::theme(
    strip.text    = ggplot2::element_text(face = "bold", size = 11),
    panel.spacing = ggplot2::unit(1, "lines"),
    axis.text.y   = ggplot2::element_text(size = 9)
  )
Figura 4: Última encuesta por casa vs resultado real — primera vuelta 2022

Segunda vuelta

Dos candidatos, encuestas profundamente divididas: las telefónicas favorecían a Hernández, las presenciales a Petro. El promedio general no capturó bien la diferencia. Petro ganó con 50.44%.

Ver código
calcular_metricas_2v(datos_2022_2v) |>
  tabla_metricas_2v(titulo = "Segunda vuelta 2022")
Tabla 5: Métricas de precisión — Segunda vuelta 2022
Segunda vuelta 2022
Fuente MAE RMSE Sesgo Cobertura Amplitud Winkler
GAD3 1.96 2.40 -1.38 NA NA NA
Invamer 2.06 2.38 -1.17 NA NA NA
Guarumo 2.41 2.86 -1.52 NA NA NA
Media encuestas 2.80 3.15 -2.80 NA NA NA
AtlasIntel 2.91 2.92 -0.02 NA NA NA
MassiveCaller 3.96 4.12 1.12 NA NA NA
Mosqueteros 4.62 5.03 -4.62 NA NA NA
CNC 5.93 5.94 -5.93 NA NA NA
YanHaas 8.88 9.52 -8.88 NA NA NA
MAE/RMSE: puntos porcentuales (p.p.), menor es mejor.  Sesgo: sobreestimación (+) o subestimación (−) promedio.  R², sMAPE y Brier omitidos (solo 2 candidatos por fuente).  Winkler: puntaje de intervalo al 95%, menor es mejor.  Cobertura: % candidatos dentro del intervalo predicho.  Amplitud: ancho promedio del intervalo (p.p.).
Ver código
plot_cafe(
  datos_2022_2v, resultados_2022_2v,
  c("Gustavo Petro", "Rodolfo Hernández"),
  "Pronósticos vs resultado — Segunda vuelta 2022",
  colores_2022
)
Figura 5: Pronósticos vs resultado real — Segunda vuelta 2022

Elecciones 2018

Primera vuelta

Cinco candidatos, seis pronósticos. Las recetas bayesianas Simple y Mixto fueron las más precisas; todos fallaron en anticipar la votación de Sergio Fajardo.

Ver código
calcular_metricas(datos_2018) |>
  tabla_metricas(titulo = "Pronósticos 2018")
Tabla 6: Métricas de precisión — Primera vuelta 2018
Pronósticos 2018
Fuente MAE RMSE sMAPE Sesgo Brier Cobertura Amplitud Winkler
Simple 2.84 4.71 21.6% 0.895 -1.85 0.00229 60% 5.1 72.0
Mixto 2.87 4.78 21.8% 0.894 -2.01 0.00230 80% 6.2 65.8
El Pais 3.23 4.06 25.3% 0.913 -0.80 0.00163 NA NA NA
18 Encuestas 3.29 5.04 23.4% 0.886 -2.26 0.00227 NA NA NA
Cifras y Conceptos 6.44 7.49 47% 0.691 -0.40 0.00588 0% 2.6 209.0
ANIF 9.84 11.14 64.3% 0.311 -0.63 0.01315 NA NA NA
MAE/RMSE: puntos porcentuales (p.p.), menor es mejor.  Sesgo: sobreestimación (+) o subestimación (−) promedio.  sMAPE: error porcentual simétrico, menor es mejor.  R²: 1 = pronóstico perfecto.  Brier: regla de puntuación propia sobre proporciones normalizadas, menor es mejor.  Winkler: puntaje de intervalo al 95%, menor es mejor.  Cobertura: % candidatos dentro del intervalo predicho.  Amplitud: ancho promedio del intervalo (p.p.).
Ver código
plot_cafe(datos_2018, resultados_2018, candidatos_2018,
          "Pronósticos vs resultado — 2018", colores_2018)
Figura 6: Pronósticos vs resultado real — Primera vuelta 2018

Encuestadoras

El error más notable de 2018 fue la subestimación de Sergio Fajardo, que obtuvo 23.73% cuando la mayoría de casas lo proyectaba entre 10% y 17%. Duque y Petro fueron estimados con mayor precisión; Vargas Lleras fue sobreestimado por varias casas.

Ver código
calcular_metricas(datos_2018_todos) |>
  tabla_metricas(titulo = "Encuestadoras y modelos — 2018")
Tabla 7: Ranking de precisión — encuestadoras y modelos, primera vuelta 2018
Encuestadoras y modelos — 2018
Fuente MAE RMSE sMAPE Sesgo Brier Cobertura Amplitud Winkler
CNC 2.03 3.18 20.9% 0.954 -1.26 0.00086 NA NA NA
Guarumo 2.82 3.92 19.6% 0.926 -1.24 0.00139 NA NA NA
Simple 2.84 4.71 21.6% 0.895 -1.85 0.00229 60% 5.1 72.0
Mixto 2.87 4.78 21.8% 0.894 -2.01 0.00230 80% 6.2 65.8
Invamer 3.01 4.02 15.4% 0.928 -0.30 0.00179 NA NA NA
18 Encuestas 3.29 5.04 23.4% 0.886 -2.26 0.00227 NA NA NA
YanHaas 3.40 4.80 24.6% 0.912 -2.66 0.00181 NA NA NA
Datexco 3.66 4.77 17.5% 0.884 -0.96 0.00251 NA NA NA
CELAG 4.30 5.37 25% 0.862 -2.08 0.00291 NA NA NA
Cifras & Conceptos 4.90 5.79 34.6% 0.859 -2.16 0.00269 NA NA NA
MAE/RMSE: puntos porcentuales (p.p.), menor es mejor.  Sesgo: sobreestimación (+) o subestimación (−) promedio.  sMAPE: error porcentual simétrico, menor es mejor.  R²: 1 = pronóstico perfecto.  Brier: regla de puntuación propia sobre proporciones normalizadas, menor es mejor.  Winkler: puntaje de intervalo al 95%, menor es mejor.  Cobertura: % candidatos dentro del intervalo predicho.  Amplitud: ancho promedio del intervalo (p.p.).
Ver código
encuestadoras_2018_largo |>
  dplyr::mutate(
    candidato = factor(candidato, levels = candidatos_2018_df$nombre),
    fuente    = forcats::fct_reorder(fuente, pronostico, .fun = mean, na.rm = TRUE)
  ) |>
  ggplot2::ggplot(ggplot2::aes(x = pronostico, y = fuente)) +
  ggplot2::geom_vline(
    data = resultados_2018 |>
      dplyr::mutate(candidato = factor(candidato, levels = candidatos_2018_df$nombre)),
    ggplot2::aes(xintercept = resultado),
    linetype = "dashed", color = "#333333", linewidth = 0.9,
    inherit.aes = FALSE
  ) +
  ggplot2::geom_point(
    ggplot2::aes(color = candidato),
    size = 3
  ) +
  ggplot2::facet_wrap(~ candidato, scales = "free_x", ncol = 1) +
  ggplot2::scale_color_manual(values = colores_2018, guide = "none") +
  ggplot2::scale_x_continuous(
    labels = scales::label_percent(scale = 1, suffix = "%"),
    expand = ggplot2::expansion(mult = c(0.08, 0.12))
  ) +
  ggplot2::labs(
    title    = "Última encuesta vs resultado — encuestadoras 2018",
    subtitle = "Línea punteada oscura: resultado real · Cada punto: última encuesta de la casa",
    x        = NULL,
    y        = NULL,
    caption  = "Última encuesta publicada antes del 27 de mayo de 2018."
  ) +
  theme_recetas() +
  ggplot2::theme(
    strip.text    = ggplot2::element_text(face = "bold", size = 11),
    panel.spacing = ggplot2::unit(1, "lines"),
    axis.text.y   = ggplot2::element_text(size = 9)
  )
Figura 7: Última encuesta por casa vs resultado real — primera vuelta 2018

Segunda vuelta

Duque superó casi todas las encuestas; Petro fue subestimado por la mayoría. El Plato Mixto, con su intervalo demasiado grande de 46–57%, cubrió el resultado de Duque (53.98%) aunque el punto central quedó corto.

Ver código
calcular_metricas_2v(datos_2018_2v) |>
  tabla_metricas_2v(titulo = "Segunda vuelta 2018")
Tabla 8: Métricas de precisión — Segunda vuelta 2018
Segunda vuelta 2018
Fuente MAE RMSE Sesgo Cobertura Amplitud Winkler
CNC 3.39 3.42 -3.39 NA NA NA
Guarumo 3.64 4.24 -3.64 NA NA NA
Invamer 3.87 3.92 -0.64 NA NA NA
Plato Mixto 3.89 4.00 -3.89 50% 9 25.2
Media encuestas 4.05 4.13 -4.05 NA NA NA
Datexco 4.69 5.62 -4.69 NA NA NA
YanHaas 4.89 5.70 -4.89 NA NA NA
Cifras & Conceptos 7.05 7.23 -7.05 NA NA NA
MAE/RMSE: puntos porcentuales (p.p.), menor es mejor.  Sesgo: sobreestimación (+) o subestimación (−) promedio.  R², sMAPE y Brier omitidos (solo 2 candidatos por fuente).  Winkler: puntaje de intervalo al 95%, menor es mejor.  Cobertura: % candidatos dentro del intervalo predicho.  Amplitud: ancho promedio del intervalo (p.p.).
Ver código
plot_cafe(
  datos_2018_2v, resultados_2018_2v,
  c("Iván Duque", "Gustavo Petro"),
  "Pronósticos vs resultado — Segunda vuelta 2018",
  colores_2018
)
Figura 8: Pronósticos vs resultado real — Segunda vuelta 2018

Comparación entre elecciones

Ver código
tbl_data <- dplyr::bind_rows(
  datos_2018_todos, datos_2022_todos, datos_2026_todos
) |>
  dplyr::group_by(eleccion, fuente) |>
  yardstick::mae(truth = resultado, estimate = pronostico) |>
  dplyr::arrange(eleccion, .estimate) |>
  dplyr::group_by(eleccion) |>
  dplyr::mutate(
    rango   = dplyr::row_number(),
    mae_fmt = round(.estimate, 2)
  ) |>
  dplyr::ungroup()

grupos <- tbl_data |>
  dplyr::group_by(eleccion) |>
  dplyr::summarise(n = dplyr::n(), .groups = "drop") |>
  dplyr::mutate(end = cumsum(n), start = end - n + 1)

tbl_comp <- tbl_data |>
  dplyr::select(Elección = eleccion, Fuente = fuente, MAE = mae_fmt, `#` = rango) |>
  knitr::kable(format = "html", caption = "Ranking por MAE") |>
  kableExtra::kable_styling(full_width = TRUE, font_size = 13) |>
  kableExtra::row_spec(0, bold = TRUE, background = "#FF4900", color = "white")

for (i in seq_len(nrow(grupos))) {
  tbl_comp <- kableExtra::group_rows(tbl_comp, grupos$eleccion[i], grupos$start[i], grupos$end[i])
}

knitr::asis_output(paste0(
  as.character(tbl_comp),
  '<div style="font-size:0.82em;margin-top:4px;padding:4px 0;border-top:1px solid #e0e0e0;line-height:1.7;">',
  'MAE: error absoluto promedio en puntos porcentuales, menor es mejor.',
  '</div>'
))
Tabla 9: Mejores pronósticos por elección — MAE (p.p.)
Ranking por MAE
Elección Fuente MAE #
2018
2018 CNC 2.03 1
2018 Guarumo 2.82 2
2018 Simple 2.84 3
2018 Mixto 2.87 4
2018 Invamer 3.01 5
2018 18 Encuestas 3.29 6
2018 YanHaas 3.40 7
2018 Datexco 3.66 8
2018 CELAG 4.30 9
2018 Cifras & Conceptos 4.90 10
2022
2022 La Silla Vacia 2.72 1
2022 @calc_electoral 2.90 2
2022 AtlasIntel 3.58 3
2022 @JorgeGalindo El Pais 3.78 4
2022 CNC 3.93 5
2022 Guarumo 4.20 6
2022 CELAG 4.25 7
2022 @PoliticaConDato 5.01 8
2022 Mosqueteros 5.22 9
2022 YanHaas 5.37 10
2022 Ajiaco 5.47 11
2022 Invamer 5.78 12
2022 MassiveCaller 5.85 13
2022 TYSE 6.07 14
2022 @ColombiaRisk 6.99 15
2022 Mixto 2022 7.01 16
2022 Simple 2022 7.34 17
2026
2026 AtlasIntel 3.65 1
2026 Invamer 5.20 2
2026 Gaussiano 7.08 3
2026 Guarumo 7.29 4
2026 CELAG 7.40 5
2026 Encuestas 7.44 6
2026 CNC 7.49 7
2026 GAD3 7.61 8
2026 Cazuela 7.72 9
2026 Tempo 8.93 10
2026 Genesis Crea 10.09 11
2026 Corp. Miguel Maldonado Manjarrez 10.85 12
MAE: error absoluto promedio en puntos porcentuales, menor es mejor.
Ver código
dplyr::bind_rows(
  datos_2018_todos, datos_2022_todos, datos_2026_todos
) |>
  dplyr::mutate(error_abs = abs(resultado - pronostico)) |>
  dplyr::group_by(eleccion, fuente) |>
  dplyr::summarise(error_abs = mean(error_abs), .groups = "drop") |>
  ggplot2::ggplot(ggplot2::aes(
    x    = reorder(fuente, error_abs),
    y    = error_abs,
    fill = eleccion
  )) +
  ggplot2::geom_col(width = 0.6) +
  ggplot2::facet_wrap(~ eleccion, scales = "free_x") +
  ggplot2::labs(
    x       = NULL,
    y       = "Error absoluto promedio (p.p.)",
    title   = "Error por pronóstico — comparación entre elecciones",
    caption = "Fuente: www.recetas-electorales.com"
  ) +
  ggplot2::scale_fill_manual(
    values = c("2018" = recetas_naranja, "2022" = recetas_rojo, "2026" = "#1F4E79")
  ) +
  theme_recetas(base_size = 16) +
  ggplot2::theme(
    legend.position = "none",
    axis.text.x     = ggplot2::element_text(angle = 30, hjust = 1, size = 8)
  )
Figura 9: Distribución de error absoluto por pronóstico y elección

Cómo citar

BibTeX
@online{recetas_electorales2026,
  author = {{Recetas Electorales}},
  title = {☕ Café},
  date = {2026-05-25},
  url = {https://www.recetas-electorales.com/elecciones/2026-colombia/2026-05-30-cafe/2026-cafe.html},
  langid = {es}
}
Por favor, cita este trabajo como:
Recetas Electorales. 2026. “☕ Café.” May 25. https://www.recetas-electorales.com/elecciones/2026-colombia/2026-05-30-cafe/2026-cafe.html.