1 Introducción

El acceso a la electricidad es una de las condiciones materiales más básicas para el desarrollo humano. Sin energía eléctrica no es posible refrigerar vacunas y alimentos, iluminar las aulas y los hogares en la noche, bombear agua potable, conectar a la población a internet ni sostener actividades productivas más allá de la agricultura de subsistencia. Por esta razón, la Agenda 2030 de las Naciones Unidas incluyó dentro del Objetivo de Desarrollo Sostenible 7 (ODS 7) la meta 7.1: “garantizar el acceso universal a servicios energéticos asequibles, fiables y modernos” para el año 2030.

A pesar de los avances de las últimas dos décadas, el más reciente reporte de seguimiento del ODS 7 (IEA, IRENA, UNSD, Banco Mundial y OMS, 2024) estima que todavía cerca de 685 millones de personas en el mundo no tienen electricidad en su vivienda, y que más del 80% de ellas se concentran en África subsahariana. La brecha no es homogénea: mientras que la mayoría de los países de Europa, Norteamérica y buena parte de Asia y Sudamérica alcanzaron coberturas cercanas al 100%, otros países conviven con coberturas inferiores al 50%, generalmente asociadas a bajos niveles de ingreso, población mayoritariamente rural y economías dependientes de la agricultura.

En este trabajo se construye una variable binaria que identifica si un país, en un año determinado, ha alcanzado el acceso (casi) universal a la electricidad, definido como una cobertura igual o superior al 95% de la población (1 = Sí, 0 = No). El umbral del 95% es un criterio usual en la literatura de seguimiento del ODS 7 para distinguir a los países que ya resolvieron el problema de cobertura (y cuyo reto se desplaza hacia la calidad y la transición energética) de aquellos en los que todavía una fracción relevante de la población permanece excluida.

Clasificar esta variable resulta interesante por varias razones. En primer lugar, permite identificar cuáles características estructurales de un país (ingreso, urbanización, densidad poblacional, estructura productiva, dinámica demográfica y geografía) están asociadas a haber alcanzado la cobertura universal, lo que aporta evidencia para la focalización de políticas públicas y de cooperación internacional. En segundo lugar, un modelo de clasificación bien calibrado puede usarse como herramienta de alerta: países que el modelo “espera” con acceso universal pero que aún no lo tienen revelan brechas de gestión o de infraestructura que no se explican por sus condiciones estructurales. Finalmente, el ejercicio permite comparar dos aproximaciones de aprendizaje supervisado: una paramétrica e interpretable, la regresión logística (Logit), y una no paramétrica basada en distancias, los K vecinos más cercanos (KNN).

El objetivo es Desarrollar y comparar modelos de clasificación supervisada (KNN y Logit) que permitan predecir si un país cuenta con acceso a la electricidad para al menos el 95% de su población, a partir de un conjunto de variables económicas, demográficas y geográficas, y evaluar cuál de los dos modelos resulta más adecuado para este problema.

2 Metodología

Planteamos el trabajo como un problema de clasificación binaria (Sí/No) y lo resolvimos con dos algoritmos: KNN y regresión logística. A continuación explicamos cómo armamos la base, cómo dividimos los datos, cómo preparamos las variables y cómo evaluamos los modelos.

2.1 Preparación y construcción de la base de datos

La base principal es Access to electricity vs. GDP per capita de Our World in Data (Ritchie, Rosado y Roser, 2024). Para cada país y año trae el porcentaje de la población con electricidad (indicador EG.ELC.ACCS.ZS del Banco Mundial, 2025), el PIB per cápita en dólares internacionales de 2021 ajustados por paridad de poder adquisitivo (PPA) y la región del mundo. Como esa base solo trae dos variables explicativas (PIB per cápita y región), la completamos con indicadores de los World Development Indicators del Banco Mundial y de Gapminder (Gapminder Foundation, 2025), que unimos por el código ISO del país y el año (archivo variables-adicionales-wdi.csv ):

  • Población urbana (% del total) — WDI: SP.URB.TOTL.IN.ZS.
  • Densidad poblacional (personas por km²) — WDI: EN.POP.DNST.
  • Valor agregado de la agricultura (% del PIB) — WDI: NV.AGR.TOTL.ZS.
  • Tasa de fecundidad (hijos por mujer) — Gapminder / WDI: SP.DYN.TFRT.IN.
  • Crecimiento poblacional anual (%) — WDI: SP.POP.GROW.
  • Condición de país sin litoral — clasificación geográfica de Gapminder.

Quitamos los agregados regionales de OWID (los códigos que empiezan por “OWID”), dejamos solo los años 2000 a 2022 y eliminamos las filas con algún dato faltante. Cada fila de la base es un país en un año (país-año)

2.2 Partición de datos

Cada país aparece hasta 23 veces en la base, una por año. Si repartimos las filas al azar, Colombia 2015 podría quedar en entrenamiento y Colombia 2016 en prueba, y como son casi iguales, el modelo parecería mejor de lo que realmente es (esto se conoce como fuga de información). Para evitarlo, hicimos la división 75/25 por países: con una semilla fija (set.seed(12345) ) escogimos al azar el 75% de los países para entrenar y dejamos el 25% restante para probar. Así, los modelos se evalúan con países que nunca vieron

2.3 Preprocesamiento de variables

El PIB per cápita y la densidad de población tienen distribuciones muy sesgadas (unos pocos países con valores enormes), así que las usamos en logaritmo natural. Como el KNN mide distancias, es muy sensible a la escala de las variables; por eso estandarizamos todas las variables numéricas (z-score) con la media y la desviación estándar del conjunto de entrenamiento, y le aplicamos esa misma transformación al de prueba. Las variables categóricas (región y litoral) se convirtieron en variables dummy.

2.4 Modelado y selección de hiperparámetros

2.4.1 Modelo K-Vecinos Más Cercanos (KNN)

El KNN clasifica cada observación nueva según la clase que más se repite entre sus k vecinos más cercanos, medidos con distancia euclidiana (James et al., 2021; Paredes, 2024). Lo implementamos de dos formas:

KNN básico (paquete class): se evaluaron valores de k entre 1 y 100 y se calculó la exactitud en el conjunto de prueba para cada uno, seleccionando el k que la maximiza. Esta implementación usa únicamente las seis variables numéricas estandarizadas.

KNN con validación cruzada (paquete caret): se utilizó la función train() con validación cruzada de 5 pliegues agrupada por país (groupKFold), explorando valores impares de k entre 1 y 101 y optimizando el área bajo la curva ROC (AUC). Esta variante incorpora tanto las variables numéricas como las categóricas.

2.4.2 Regresión Logística (Logit)

Estimamos la regresión logística con glm() (familia binomial, enlace logit). El modelo explica el logaritmo de los odds de tener acceso universal como una suma ponderada de los predictores:

\[ \ln\left(\frac{P(Y=1 \mid \mathbf{X})}{1-P(Y=1 \mid \mathbf{X})}\right) = \beta_0 + \sum_{j=1}^{p} \beta_j X_j \]

Para decidir a partir de qué probabilidad un país se clasifica como “Sí”, usamos el índice de Youden (J = sensibilidad + especificidad − 1), que busca el punto de la curva ROC donde mejor se equilibran las dos. Lo calculamos con los datos de entrenamiento, para que el conjunto de prueba no influyera en ninguna decisión del modelo.

2.4.3 Evaluación y comparación

Ambos modelos se evaluaron en el conjunto de prueba mediante la matriz de confusión y sus métricas derivadas (exactitud, Kappa, sensibilidad, especificidad, valores predictivos) y el AUC de la curva ROC.y se comparan los indicadores de los dos modelos.

2.5 Definición de las variables

La variable dependiente es el acceso (casi) universal a la electricidad (construimos a partir del porcentaje de la población con electricidad:Acceso_95 ), que Las variables independientes las escogimos pensando en qué hace más fácil o más difícil llevar electricidad a toda la población::

\[ Y_{it} = \begin{cases} 1 \; (\text{Si}) & \text{si el acceso a la electricidad del país } i \text{ en el año } t \geq 95\% \\ 0 \; (\text{No}) & \text{si el acceso a la electricidad del país } i \text{ en el año } t < 95\% \end{cases} \]

Las variables independientes se eligieron a partir de la literatura sobre los determinantes de la electrificación:

  • PIB per cápita (log): es el principal indicador de la capacidad económica de un país. Un mayor ingreso permite financiar la infraestructura de generación, transmisión y distribución, y aumenta la capacidad de pago de los hogares por el servicio. Se espera una relación positiva con el acceso.

  • Población urbana (%): llevar la red eléctrica a zonas urbanas es mucho más barato por usuario que electrificar zonas rurales dispersas. Se espera que una mayor urbanización aumente la probabilidad de acceso universal.

  • Densidad poblacional (log): a mayor número de personas por km², menor es la longitud de red necesaria por usuario, lo que reduce los costos de electrificación. Se espera un efecto positivo.

  • Agricultura (% del PIB): una alta participación de la agricultura en la economía es característica de países con estructuras productivas menos diversificadas y con más población rural. Se espera un efecto negativo.

  • Fecundidad (hijos por mujer): es un indicador sintético del nivel de desarrollo demográfico y social (educación, salud, rol de la mujer). Tasas altas suelen asociarse a hogares rurales y pobres; se espera un efecto negativo.

  • Crecimiento poblacional (%): un crecimiento acelerado de la población obliga a expandir la red más rápido solo para mantener la cobertura existente. Se espera un efecto negativo.

  • Región: recoge diferencias históricas, institucionales y geográficas entre continentes no capturadas por las demás variables (categoría base: África).

  • Litoral: los países sin salida al mar enfrentan mayores costos para importar combustibles y equipos, lo que puede dificultar la expansión de la red (categoría base: con litoral).

variables_modelo <- data.frame(
  Variable = c("Acceso_95", "PIB per cápita", "Población urbana", "Densidad poblacional",
               "Agricultura", "Fecundidad", "Crecimiento poblacional", "Región", "Litoral"),

  Descripción = c(
    "Variable dependiente: el país tiene acceso a la electricidad para al menos el 95% de su población.",
    "Producto interno bruto por habitante, ajustado por paridad de poder adquisitivo.",
    "Porcentaje de la población que vive en zonas urbanas.",
    "Número de personas por kilómetro cuadrado de superficie terrestre.",
    "Valor agregado de la agricultura, silvicultura y pesca como porcentaje del PIB.",
    "Número promedio de hijos por mujer (tasa global de fecundidad).",
    "Tasa de crecimiento anual de la población total.",
    "Región del mundo a la que pertenece el país.",
    "Indica si el país tiene o no salida al mar."
  ),

  Tipo_Variable = c(
    "Categórica (binaria)",
    "Cuantitativa continua",
    "Cuantitativa continua",
    "Cuantitativa continua",
    "Cuantitativa continua",
    "Cuantitativa continua",
    "Cuantitativa continua",
    "Categórica (6 niveles)",
    "Categórica (binaria)"
  ),

  Ejemplos_Notas = c(
    "1 = 'Si' (>= 95%), 0 = 'No' (< 95%).",
    "USD PPA de 2021. Se usa en logaritmo.",
    "Porcentaje (0 - 100).",
    "Personas/km². Se usa en logaritmo.",
    "Porcentaje del PIB.",
    "Ej: 1.5, 4.8.",
    "Porcentaje anual.",
    "África, Asia, Europa, Norteamérica, Oceanía, Sudamérica.",
    "'Con litoral', 'Sin litoral'."
  )
)

tabla_variables <- kable(
  variables_modelo,
  format = "html",
  col.names = c("Variable", "Descripción", "Tipo de Variable", "Ejemplos / Notas"),
  align = c('l', 'l', 'l', 'l'),
  caption = "Tabla 1. Variables utilizadas en el modelo de acceso universal a la electricidad"
) %>%
  kable_styling(
    bootstrap_options = c("hover", "condensed"),
    full_width = FALSE,
    font_size = 14,
    position = "center"
  ) %>%
  row_spec(1, background = PAL[["celeste"]]) %>%
  column_spec(1, bold = TRUE, color = PAL[["navy"]], width = "3cm") %>%
  column_spec(2, width = "6cm") %>%
  column_spec(3, width = "3cm") %>%
  column_spec(4, width = "3.5cm") %>%
  footnote(
    general = "Elaboración propia con base en Our World in Data, Banco Mundial (WDI).",
    general_title = "Fuente: ",
    footnote_as_chunk = TRUE
  )

tabla_variables
Tabla 1. Variables utilizadas en el modelo de acceso universal a la electricidad
Variable Descripción Tipo de Variable Ejemplos / Notas
Acceso_95 Variable dependiente: el país tiene acceso a la electricidad para al menos el 95% de su población. Categórica (binaria) 1 = ‘Si’ (>= 95%), 0 = ‘No’ (< 95%).
PIB per cápita Producto interno bruto por habitante, ajustado por paridad de poder adquisitivo. Cuantitativa continua USD PPA de 2021. Se usa en logaritmo.
Población urbana Porcentaje de la población que vive en zonas urbanas. Cuantitativa continua Porcentaje (0 - 100).
Densidad poblacional Número de personas por kilómetro cuadrado de superficie terrestre. Cuantitativa continua Personas/km². Se usa en logaritmo.
Agricultura Valor agregado de la agricultura, silvicultura y pesca como porcentaje del PIB. Cuantitativa continua Porcentaje del PIB.
Fecundidad Número promedio de hijos por mujer (tasa global de fecundidad). Cuantitativa continua Ej: 1.5, 4.8.
Crecimiento poblacional Tasa de crecimiento anual de la población total. Cuantitativa continua Porcentaje anual.
Región Región del mundo a la que pertenece el país. Categórica (6 niveles) África, Asia, Europa, Norteamérica, Oceanía, Sudamérica.
Litoral Indica si el país tiene o no salida al mar. Categórica (binaria) ‘Con litoral’, ‘Sin litoral’.
Fuente: Elaboración propia con base en Our World in Data, Banco Mundial (WDI).

2.6 Base de datos

electricidad_raw <- read_csv("access-to-electricity-vs-gdp-per-capita.csv", show_col_types = FALSE)
wdi_raw <- read_csv("variables-adicionales-wdi.csv", show_col_types = FALSE)

Base_datos <- electricidad_raw %>%
  rename(
    Pais = Entity,
    Codigo = Code,
    Año = Year,
    Acceso = `Share with access to electricity`,
    PIB_pc = `GDP per capita`,
    Region = `World region according to OWID`
  ) %>%
  filter(!str_starts(Codigo, "OWID"), between(Año, 2000, 2022)) %>%
  inner_join(wdi_raw, by = c("Codigo" = "Code", "Año" = "Year")) %>%
  drop_na() %>%
  mutate(
    Region = fct_recode(
      factor(Region),
      "África" = "Africa",
      "Europa" = "Europe",
      "Norteamérica" = "North America",
      "Oceanía" = "Oceania",
      "Sudamérica" = "South America"
    ),
    Region = fct_relevel(Region, "África"),
    Litoral = factor(Litoral, levels = c("Con litoral", "Sin litoral")),
    Acceso_95 = factor(if_else(Acceso >= 95, "Si", "No"), levels = c("No", "Si")),
    Y = as.integer(Acceso_95 == "Si"),
    log_PIB = log(PIB_pc),
    log_Densidad = log(Densidad)
  ) %>%
  arrange(Pais, Año)
tabla_interactiva <- Base_datos %>%
  select(Pais, Año, Region, Litoral, Acceso, Acceso_95, PIB_pc, Urbano, Densidad,
         Agricultura, Fertilidad, Crec_poblacion) %>%
  datatable(
    colnames = c(
      "País", "Año", "Región", "Litoral", "Acceso (%)", "Acceso >= 95%",
      "PIB per cápita (USD PPA)", "Población urbana (%)", "Densidad (hab/km²)",
      "Agricultura (% PIB)", "Fecundidad", "Crec. poblacional (%)"
    ),
    filter = 'top',
    extensions = c('Buttons', 'Scroller'),
    options = list(
      pageLength = 10,
      dom = 'Bfrtip',
      scrollX = TRUE,
      scrollY = "400px",
      scroller = TRUE,
      buttons = list('copy', 'csv', 'excel', 'pdf', 'print'),
      language = list(
        url = '//cdn.datatables.net/plug-ins/1.10.25/i18n/Spanish.json'
      )
    ),
    caption = htmltools::tags$caption(
      style = 'caption-side: top; text-align: left; color: #0B2545; font-family: "Libre Franklin", sans-serif; font-size: 16px; font-weight: 700; padding-bottom: 10px;',
      'Tabla 2. Base de datos de acceso a la electricidad por país y año (2000-2022)'
    ),
    class = 'row-border stripe hover order-column',
    rownames = FALSE,
    width = '100%'
  ) %>%
  formatCurrency(columns = 'PIB_pc', currency = '$', digits = 0, before = TRUE) %>%
  formatRound(columns = c('Acceso', 'Urbano', 'Densidad', 'Agricultura', 'Fertilidad', 'Crec_poblacion'), digits = 2) %>%
  formatStyle(
    'Acceso_95',
    color = styleEqual(c('No', 'Si'), c(PAL[["naranja"]], PAL[["azul"]])),
    fontWeight = 'bold'
  ) %>%
  formatStyle(
    'Acceso',
    background = styleColorBar(c(0, 100), '#D6E4F5'),
    backgroundSize = '95% 70%',
    backgroundRepeat = 'no-repeat',
    backgroundPosition = 'center'
  )

tabla_interactiva

La base final está compuesta por 4.151 observaciones país-año, correspondientes a 185 países observados entre 2000 y 2022. De ellas, el 56,4% corresponde a casos con acceso a la electricidad igual o superior al 95% (Y = 1), por lo que las clases se encuentran razonablemente balanceadas.

3 Resultados descriptivos

Antes de construir los modelos, miramos cómo se comportan las variables y cómo se relacionan con el acceso universal, tanto las cuantitativas como las categóricas

3.1 Estadísticas descriptivas de las variables cuantitativas

vars_cuant <- c(
  "Acceso (%)" = "Acceso",
  "PIB per cápita (USD)" = "PIB_pc",
  "Población urbana (%)" = "Urbano",
  "Densidad (hab/km²)" = "Densidad",
  "Agricultura (% PIB)" = "Agricultura",
  "Fecundidad (hijos/mujer)" = "Fertilidad",
  "Crec. poblacional (%)" = "Crec_poblacion"
)

resumen_general <- Base_datos %>%
  select(all_of(vars_cuant)) %>%
  pivot_longer(everything(), names_to = "Variable", values_to = "valor") %>%
  mutate(Variable = factor(Variable, levels = names(vars_cuant))) %>%
  group_by(Variable) %>%
  summarise(
    Media = mean(valor),
    Mediana = median(valor),
    `Desv. Estándar` = sd(valor),
    `Coef. Variación (%)` = 100 * sd(valor) / mean(valor),
    Mínimo = min(valor),
    Máximo = max(valor),
    .groups = "drop"
  ) %>%
  mutate(across(where(is.numeric), ~ round(., 2)))

tabla_estilo(
  resumen_general,
  "Tabla 3. Estadísticas descriptivas de las variables cuantitativas",
  align = c("l", rep("r", 6))
)
Tabla 3. Estadísticas descriptivas de las variables cuantitativas
Variable Media Mediana Desv. Estándar Coef. Variación (%) Mínimo Máximo
Acceso (%) 79.87 98.10 29.49 36.93 1.30 100.00
PIB per cápita (USD) 21935.06 13397.96 23567.77 107.44 711.98 149025.11
Población urbana (%) 56.90 58.30 22.72 39.92 8.04 100.00
Densidad (hab/km²) 222.05 79.36 746.81 336.32 1.58 8300.97
Agricultura (% PIB) 11.31 7.72 10.62 93.90 0.01 65.60
Fecundidad (hijos/mujer) 2.86 2.36 1.49 52.01 0.71 7.83
Crec. poblacional (%) 1.35 1.25 1.57 116.39 -11.36 21.70
Nota: Fuente: Elaboración propia con base en Our World in Data y Banco Mundial (WDI), 2000-2022

En promedio, las observaciones registran un acceso a la electricidad de 79,9%, con una mediana de 98,1%. La mediana superior a la media evidencia una distribución con asimetría negativa: la mayor parte de los países se concentra cerca del 100% de cobertura, mientras que un grupo más reducido presenta coberturas muy bajas (el mínimo es 1,3%), que arrastran el promedio hacia abajo.

El PIB per cápita presenta la mayor dispersión relativa de todas las variables: su media (USD 21.935) supera ampliamente a la mediana (USD 13.398), reflejando una fuerte asimetría positiva propia de la distribución mundial del ingreso, en la que unos pocos países muy ricos elevan el promedio. Algo similar ocurre con la densidad poblacional, cuyo máximo (8.301 hab/km², correspondiente a territorios urbanos como Singapur y Hong Kong) contrasta con una mediana de apenas 79,4 hab/km². Estas asimetrías justifican el uso de la transformación logarítmica en los modelos.

La población urbana promedio es de 56,9%, la agricultura representa en promedio el 11,3% del PIB, la fecundidad promedio es de 2,9 hijos por mujer y la población crece en promedio a una tasa de 1,3% anual, con valores que van desde tasas negativas (países que pierden población por conflictos o emigración, como Timor Oriental en 2000, Siria en 2014 o Ucrania en 2022) hasta tasas muy elevadas en los países del Golfo Pérsico (Catar, Omán y Emiratos Árabes Unidos), explicadas por la llegada masiva de trabajadores extranjeros.

3.2 Distribución de la variable dependiente

dist_y <- Base_datos %>%
  count(Acceso_95) %>%
  mutate(
    Proporcion = n / sum(n),
    Etiqueta = if_else(Acceso_95 == "Si", "Si (acceso >= 95%)", "No (acceso < 95%)")
  )

plot_ly(
  dist_y,
  x = ~Etiqueta,
  y = ~n,
  type = "bar",
  marker = list(color = unname(col_acceso)),
  text = ~paste0(num(n), " (", percent(Proporcion, accuracy = 0.1), ")"),
  textposition = "outside",
  textfont = list(family = fuente_titulos, size = 14, color = PAL[["tinta"]]),
  hovertemplate = "%{x}<br>Observaciones: %{y}<extra></extra>"
) %>%
  estilo_plotly(
    "Figura 1. Distribución de la variable dependiente",
    "",
    "Observaciones país-año",
    subtitulo = "Número de observaciones según si el país tiene acceso a la electricidad para al menos el 95% de su población"
  ) %>%
  layout(yaxis = list(range = c(0, max(dist_y$n) * 1.2)), bargap = 0.45, showlegend = FALSE)

La Figura 1 muestra que 2.342 observaciones (56.4%) corresponden a países con acceso universal, frente a 1.809 (43.6%) que no lo alcanzan. Las clases están relativamente balanceadas, lo que significa que un clasificador trivial que siempre prediga la clase mayoritaria acertaría el 56.4% de las veces: este valor (la tasa de no información) es la referencia mínima que deben superar los modelos.

3.3 Evolución temporal del acceso universal

evolucion <- Base_datos %>%
  group_by(Año, Region) %>%
  summarise(Porcentaje = mean(Y) * 100, Paises = n(), .groups = "drop")

evolucion_mundo <- Base_datos %>%
  group_by(Año) %>%
  summarise(Porcentaje = mean(Y) * 100, Paises = n(), .groups = "drop") %>%
  mutate(Region = "Total mundial")

colores_region <- col_region

plot_ly() %>%
  add_lines(
    data = evolucion, x = ~Año, y = ~Porcentaje, color = ~Region, colors = colores_region,
    line = list(width = 2.5),
    text = ~paste0("<b>", Region, "</b><br>Año: ", Año, "<br>Países con acceso >= 95%: ",
                   round(Porcentaje, 1), "%<br>Países observados: ", Paises),
    hoverinfo = "text"
  ) %>%
  add_lines(
    data = evolucion_mundo, x = ~Año, y = ~Porcentaje, name = "Total mundial",
    line = list(color = PAL[["tinta"]], width = 3.5, dash = "dot"),
    text = ~paste0("<b>Total mundial</b><br>Año: ", Año, "<br>Países con acceso >= 95%: ", round(Porcentaje, 1), "%"),
    hoverinfo = "text"
  ) %>%
  estilo_plotly(
    "Figura 2. Porcentaje de países con acceso universal a la electricidad, por región",
    "",
    "",
    subtitulo = "Proporción de países de cada región con acceso a la electricidad >= 95% de la población, 2000-2022"
  ) %>%
  layout(yaxis = list(ticksuffix = "%", range = c(0, 105)))

La Figura 2 evidencia el progreso sostenido en la electrificación durante las últimas dos décadas: el porcentaje de países con acceso igual o superior al 95% pasó de 48.5% en 2000 a 66.3% en 2022. Sin embargo, el avance ha sido muy desigual entre regiones. Europa se mantiene por encima del 90% durante todo el periodo y alcanza el 100% al final, mientras que Sudamérica, Norteamérica (que incluye a México, Centroamérica y el Caribe) y Asia muestran los mayores avances, con aumentos de entre 25 y 45 puntos porcentuales. Oceanía tiene un avance intermedio, limitado por pequeños estados insulares del Pacífico con poblaciones dispersas. África es la región más rezagada: solo el 7% de sus países tenía acceso universal en 2000, y en 2022 la cifra apenas llega al 12%; los únicos países africanos que superan el umbral en 2022 son Argelia, Egipto, Marruecos y Túnez, en el norte del continente, y los estados insulares de Cabo Verde y Mauricio.

3.4 Relación entre el ingreso y el acceso a la electricidad

Base_datos %>%
  plot_ly(
    x = ~PIB_pc,
    y = ~Acceso,
    color = ~Region,
    colors = colores_region,
    size = ~Urbano,
    sizes = c(5, 25),
    frame = ~Año,
    ids = ~Pais,
    type = "scatter",
    mode = "markers",
    marker = list(opacity = 0.8, line = list(color = "white", width = 0.6)),
    text = ~paste0("<b>", Pais, "</b><br>PIB pc: ", dollar(PIB_pc, accuracy = 1),
                   "<br>Acceso: ", round(Acceso, 1), "%<br>Población urbana: ", round(Urbano, 1), "%"),
    hoverinfo = "text"
  ) %>%
  estilo_plotly(
    "Figura 3. Acceso a la electricidad vs. PIB per cápita",
    "PIB per cápita (USD internacionales de 2021, escala logarítmica)",
    "",
    subtitulo = "Porcentaje de la población con acceso a la electricidad. El tamaño de la burbuja indica el % de población urbana. Presione Play."
  ) %>%
  layout(
    xaxis = list(type = "log"),
    yaxis = list(range = c(0, 105), ticksuffix = "%"),
    shapes = list(list(
      type = "line", xref = "paper", x0 = 0, x1 = 1, y0 = 95, y1 = 95,
      line = list(color = PAL[["ambar"]], dash = "dash", width = 2)
    )),
    legend = list(orientation = "v", x = 1.02, y = 1, xanchor = "left"),
    margin = list(b = 160, r = 140)
  ) %>%
  animation_opts(frame = 600, transition = 300, redraw = FALSE) %>%
  animation_slider(currentvalue = list(prefix = "Año: ", font = list(family = fuente_titulos, size = 16, color = PAL[["navy"]]))) %>%
  animation_button(bgcolor = PAL[["navy"]], font = list(color = "white"), bordercolor = PAL[["navy"]])

La Figura 3 es un gráfico dinámico que relaciona el PIB per cápita (en escala logarítmica) con el porcentaje de acceso a la electricidad, el tamaño de cada burbuja representa el porcentaje de población urbana y la línea amarilla discontinua marca el umbral del 95% que define la variable dependiente. Se aprecia una clara relación en forma de “S” (logística): por debajo de aproximadamente USD 5.000 per cápita el acceso es muy heterogéneo y en general bajo, mientras que a partir de unos USD 10.000-15.000 casi todos los países superan el umbral del 95%. Esta forma sigmoide es precisamente la que modela la regresión logística, lo cual anticipa un buen desempeño de este modelo. Con el paso de los años la nube de puntos se desplaza hacia arriba y a la derecha, reflejando el crecimiento económico y los avances en electrificación, aunque un grupo de países africanos permanece en la esquina inferior izquierda (bajo ingreso y baja cobertura).

3.5 Distribución de las variables cuantitativas según el acceso

por_clase <- Base_datos %>%
  select(Acceso_95, log_PIB, Urbano, log_Densidad, Agricultura, Fertilidad, Crec_poblacion) %>%
  pivot_longer(-Acceso_95, names_to = "Variable", values_to = "valor") %>%
  group_by(Variable) %>%
  summarise(
    `Media (No)` = mean(valor[Acceso_95 == "No"]),
    `Media (Si)` = mean(valor[Acceso_95 == "Si"]),
    `Mediana (No)` = median(valor[Acceso_95 == "No"]),
    `Mediana (Si)` = median(valor[Acceso_95 == "Si"]),
    `Valor p (Wilcoxon)` = wilcox.test(valor ~ Acceso_95)$p.value,
    .groups = "drop"
  ) %>%
  mutate(
    Variable = recode(
      Variable,
      log_PIB = "log(PIB per cápita)", Urbano = "Población urbana (%)",
      log_Densidad = "log(Densidad)", Agricultura = "Agricultura (% PIB)",
      Fertilidad = "Fecundidad", Crec_poblacion = "Crec. poblacional (%)"
    ),
    across(2:5, ~ round(., 2)),
    `Valor p (Wilcoxon)` = if_else(`Valor p (Wilcoxon)` < 0.001, "< 0.001", sprintf("%.3f", `Valor p (Wilcoxon)`))
  )

tabla_estilo(
  por_clase,
  "Tabla 4. Comparación de las variables cuantitativas según el acceso a la electricidad",
  nota = "Prueba de Wilcoxon-Mann-Whitney para diferencias en la distribución entre grupos. Elaboración propia con base en OWID y Banco Mundial.",
  align = c("l", rep("r", 5))
)
Tabla 4. Comparación de las variables cuantitativas según el acceso a la electricidad
Variable Media (No) Media (Si) Mediana (No) Mediana (Si) Valor p (Wilcoxon)
Agricultura (% PIB) 18.82 5.51 17.87 3.62 < 0.001
Crec. poblacional (%) 1.93 0.90 2.07 0.75 < 0.001
Fecundidad 4.01 1.96 3.95 1.77 < 0.001
Población urbana (%) 42.02 68.40 39.97 69.87 < 0.001
log(Densidad) 4.03 4.47 4.17 4.52 < 0.001
log(PIB per cápita) 8.47 10.14 8.39 10.19 < 0.001
Nota: Prueba de Wilcoxon-Mann-Whitney para diferencias en la distribución entre grupos. Elaboración propia con base en OWID y Banco Mundial.
etiquetas_vars <- c(
  log_PIB = "log(PIB per cápita)", Urbano = "Población urbana (%)",
  log_Densidad = "log(Densidad)", Agricultura = "Agricultura (% PIB)",
  Fertilidad = "Fecundidad (hijos/mujer)", Crec_poblacion = "Crec. poblacional (%)"
)

p_box <- Base_datos %>%
  select(Pais, Año, Acceso_95, all_of(names(etiquetas_vars))) %>%
  pivot_longer(all_of(names(etiquetas_vars)), names_to = "Variable", values_to = "Valor") %>%
  mutate(Variable = factor(etiquetas_vars[Variable], levels = etiquetas_vars)) %>%
  ggplot(aes(x = Acceso_95, y = Valor, fill = Acceso_95)) +
  geom_boxplot(alpha = 0.9, outlier.alpha = 0.25, outlier.size = 0.7, color = PAL[["tinta"]], linewidth = 0.4, width = 0.6) +
  facet_wrap(~Variable, scales = "free_y", ncol = 3) +
  scale_fill_manual(values = col_acceso, name = "Acceso >= 95%") +
  labs(
    title = "Figura 4. Variables cuantitativas según el acceso a la electricidad",
    x = "Acceso >= 95%",
    y = NULL
  ) +
  tema_datos() +
  theme(legend.position = "none")

ggplotly(p_box) %>%
  layout(margin = list(t = 80, b = 90)) %>%
  con_fuente(y = -0.12)

La Tabla 4 y la Figura 4 permiten identificar con claridad cuáles variables separan a los dos grupos. Lo que podemos interpletar como:

  • PIB per cápita: es la variable con la separación más marcada; las cajas de ambos grupos apenas se traslapan, lo que confirma que el ingreso es el principal determinante del acceso universal.
  • Población urbana: los países con acceso universal son, en promedio, mucho más urbanos.
  • Agricultura y fecundidad: muestran el patrón inverso; los países sin acceso universal dependen mucho más de la agricultura y tienen tasas de fecundidad considerablemente más altas.
  • Crecimiento poblacional: es mayor en los países sin acceso universal, lo que dificulta que la expansión de la red avance al ritmo del crecimiento de la población.
  • Densidad poblacional: es la variable con menor capacidad de separación; las distribuciones de ambos grupos se traslapan considerablemente, por lo que su aporte individual es menor.
p_dens <- Base_datos %>%
  ggplot(aes(x = PIB_pc, fill = Acceso_95)) +
  geom_density(alpha = 0.55, color = "white", linewidth = 0.6) +
  scale_x_log10(labels = dollar_format(accuracy = 1)) +
  scale_fill_manual(values = col_acceso, name = "Acceso >= 95%") +
  labs(
    title = "Figura 5. Densidad del PIB per cápita según el acceso",
    x = "PIB per cápita (USD PPA, escala log)",
    y = "Densidad"
  ) +
  tema_datos()

ggplotly(p_dens) %>%
  layout(legend = list(orientation = "h", x = 0, xanchor = "left", y = 1.02, yanchor = "bottom"), margin = list(t = 90, b = 120)) %>%
  con_fuente(y = -0.3)

La Figura 5 muestra que las distribuciones del PIB per cápita de ambos grupos tienen formas aproximadamente normales en escala logarítmica, con modas claramente separadas. La zona de traslape (aproximadamente entre USD 5.000 y USD 20.000) es donde se concentrarán los errores de clasificación, ya que allí conviven países de ingreso medio con y sin acceso universal.

3.6 Análisis de las variables categóricas

tabla_region <- Base_datos %>%
  count(Region, Acceso_95) %>%
  pivot_wider(names_from = Acceso_95, values_from = n, values_fill = 0) %>%
  mutate(
    Total = No + Si,
    `% con acceso >= 95%` = round(100 * Si / Total, 1),
    Paises = Base_datos %>% group_by(Region) %>% summarise(p = n_distinct(Codigo)) %>% pull(p)
  ) %>%
  select(Region, Paises, No, Si, Total, `% con acceso >= 95%`) %>%
  arrange(desc(`% con acceso >= 95%`))

chi_region <- chisq.test(table(Base_datos$Region, Base_datos$Acceso_95))
chi_litoral <- chisq.test(table(Base_datos$Litoral, Base_datos$Acceso_95))

tabla_estilo(
  tabla_region,
  "Tabla 5. Acceso a la electricidad según región",
  col.names = c("Región", "Países", "No", "Si", "Total", "% con acceso >= 95%"),
  align = c("l", rep("r", 5))
)
Tabla 5. Acceso a la electricidad según región
Región Países No Si Total % con acceso >= 95%
Europa 42 27 924 951 97.2
Asia 46 308 727 1035 70.2
Sudamérica 11 82 171 253 67.6
Norteamérica 22 222 276 498 55.4
Oceanía 13 174 117 291 40.2
África 51 996 127 1123 11.3
Nota: Fuente: Elaboración propia con base en Our World in Data y Banco Mundial (WDI), 2000-2022
cat_region <- Base_datos %>%
  count(Region, Acceso_95) %>%
  group_by(Region) %>%
  mutate(Porcentaje = 100 * n / sum(n)) %>%
  ungroup()

plot_ly(
  cat_region,
  x = ~Region,
  y = ~Porcentaje,
  color = ~Acceso_95,
  colors = col_acceso,
  type = "bar",
  text = ~paste0(round(Porcentaje, 1), "%"),
  textposition = "inside",
  insidetextfont = list(color = "white", family = fuente_titulos),
  hovertemplate = "%{x}<br>%{y:.1f}% de las observaciones<extra></extra>"
) %>%
  estilo_plotly(
    "Figura 6. Composición del acceso a la electricidad por región",
    "",
    "",
    subtitulo = "Porcentaje de observaciones país-año con y sin acceso >= 95%, 2000-2022"
  ) %>%
  layout(barmode = "stack", yaxis = list(ticksuffix = "%"))

La región es una variable con una fuerte asociación con el acceso universal. Europa presenta acceso universal en casi la totalidad de sus observaciones (las excepciones son Bielorrusia, Bulgaria y Rumania en los primeros años de la década de 2000, con coberturas ligeramente inferiores al 95%). En el extremo opuesto, África apenas registra acceso universal en una de cada diez observaciones. Asia y Sudamérica superan el 65%, mientras que Norteamérica (que incluye a México, Centroamérica y el Caribe, con casos como Haití, Honduras o Nicaragua) y Oceanía (con Papúa Nueva Guinea, Islas Salomón o Vanuatu) se ubican en posiciones intermedias.

tabla_litoral <- Base_datos %>%
  count(Litoral, Acceso_95) %>%
  group_by(Litoral) %>%
  mutate(Porcentaje = 100 * n / sum(n), Paises = n_distinct(Base_datos$Codigo[Base_datos$Litoral == first(Litoral)])) %>%
  ungroup()

plot_ly(
  tabla_litoral,
  x = ~Litoral,
  y = ~Porcentaje,
  color = ~Acceso_95,
  colors = col_acceso,
  type = "bar",
  text = ~paste0(round(Porcentaje, 1), "% (n = ", n, ")"),
  textposition = "auto",
  hovertemplate = "%{x}<br>%{y:.1f}%<extra></extra>"
) %>%
  estilo_plotly(
    "Figura 7. Acceso a la electricidad según la condición de litoral",
    "",
    "",
    subtitulo = "Porcentaje de observaciones país-año con y sin acceso >= 95%"
  ) %>%
  layout(barmode = "group", yaxis = list(ticksuffix = "%", range = c(0, 100)))

La Figura 7 indica que el 58.5% de las observaciones de países con litoral tienen acceso universal, frente al 48.8% de los países sin salida al mar. Debe tenerse en cuenta que el grupo sin litoral es muy heterogéneo: incluye países europeos de alto ingreso (Suiza, Austria, Hungría), las antiguas repúblicas soviéticas de Asia Central y el Cáucaso (Kazajistán, Uzbekistán, Kirguistán, Tayikistán, Turkmenistán, Armenia, Azerbaiyán), que tienen coberturas cercanas al 100%, y muchos de los países más pobres de África (Chad, Níger, Burundi, Malaui), con coberturas inferiores al 20%. Por ello, la menor cobertura de los países sin litoral podría reflejar en realidad su menor ingreso y su ubicación geográfica, y el efecto de esta variable podría cambiar una vez se controle por esas características en el modelo Logit.

3.7 Correlación entre las variables numéricas

matriz_cor <- Base_datos %>%
  select(Y, log_PIB, Urbano, log_Densidad, Agricultura, Fertilidad, Crec_poblacion) %>%
  rename(
    "Acceso >= 95%" = Y, "log(PIB pc)" = log_PIB, "Urbano" = Urbano,
    "log(Densidad)" = log_Densidad, "Agricultura" = Agricultura,
    "Fecundidad" = Fertilidad, "Crec. poblacional" = Crec_poblacion
  ) %>%
  cor()

p_cor <- ggcorrplot(
  matriz_cor,
  method = "square",
  type = "lower",
  lab = TRUE,
  lab_size = 3.5,
  colors = c(PAL[["naranja"]], "white", PAL[["azul"]]),
  outline.color = "white"
) +
  labs(title = "Figura 8. Matriz de correlaciones de Pearson") +
  tema_datos() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1), axis.title = element_blank(), panel.grid.major.y = element_blank())

ggplotly(p_cor) %>% layout(margin = list(b = 150)) %>% con_fuente(y = -0.33)

La matriz de correlaciones (Figura 8) confirma los patrones descriptivos. La variable dependiente tiene las correlaciones positivas más altas con el logaritmo del PIB per cápita (0.72) y con la población urbana (0.58), y las negativas más fuertes con la fecundidad (-0.69) y la participación de la agricultura (-0.62). La densidad poblacional tiene una correlación muy baja con el acceso (0.16).

Por otro lado, se observan correlaciones elevadas entre algunos predictores, como entre el PIB per cápita y la fecundidad (-0.78) o entre el PIB per cápita y la agricultura (-0.84). Esto es esperable, porque todas estas variables son manifestaciones del nivel de desarrollo de un país, pero implica cierta multicolinealidad que puede hacer que algunos coeficientes del modelo Logit resulten menos precisos o incluso cambien de signo respecto a su correlación simple.

4 Resultados de los modelos

Clasificar es asignar cada observación a una categoría a partir de sus características. Los modelos aprenden con datos donde ya conocemos la respuesta (países-año cuyo acceso sabemos) y luego predicen la clase de observaciones nuevas. Primero mostramos cómo quedaron divididos los datos y después los resultados de cada modelo

4.1 División de los datos

set.seed(12345)

paises <- unique(Base_datos$Codigo)
paises_train <- sample(paises, size = round(0.75 * length(paises)))

train <- Base_datos %>% filter(Codigo %in% paises_train)
test  <- Base_datos %>% filter(!Codigo %in% paises_train)

particion <- bind_rows(
  train %>% mutate(Conjunto = "Entrenamiento"),
  test %>% mutate(Conjunto = "Prueba")
) %>%
  group_by(Conjunto) %>%
  summarise(
    Países = n_distinct(Codigo),
    Observaciones = n(),
    `No (acceso < 95%)` = sum(Acceso_95 == "No"),
    `Si (acceso >= 95%)` = sum(Acceso_95 == "Si"),
    `% Si` = round(100 * mean(Y), 1),
    .groups = "drop"
  )

tabla_estilo(
  particion,
  "Tabla 6. Partición de los datos en entrenamiento y prueba (por países)",
  align = c("l", rep("r", 5))
)
Tabla 6. Partición de los datos en entrenamiento y prueba (por países)
Conjunto Países Observaciones No (acceso < 95%) Si (acceso >= 95%) % Si
Entrenamiento 139 3133 1348 1785 57.0
Prueba 46 1018 461 557 54.7
Nota: Fuente: Elaboración propia con base en Our World in Data y Banco Mundial (WDI), 2000-2022

Con una semilla fija (set.seed(12345)), se asignaron 139 países (3.133 observaciones) al conjunto de entrenamiento y 46 países (1.018 observaciones) al conjunto de prueba. La proporción de observaciones con acceso universal es similar en ambos conjuntos (57% y 54.7%), lo que garantiza que la evaluación sea representativa. Como la partición es por países, todos los años de un mismo país quedan en un único conjunto, de modo que el desempeño en la prueba mide la capacidad del modelo para clasificar países nuevos.

4.2 Modelo KNN

El KNN (K vecinos más cercanos) es un algoritmo no paramétrico: para clasificar un caso nuevo busca los k casos más parecidos del entrenamiento y le asigna la clase que más se repite entre ellos. No estima parámetros; guarda los datos y calcula las distancias cada vez que tiene que predecir. Su parámetro clave es k: con k pequeño el modelo se ajusta demasiado a casos puntuales (sobreajuste) y con k muy grande las fronteras se vuelven demasiado suaves (subajuste).

4.2.1 Modelo KNN (class)

En esta primera versión usamos la función knn() del paquete class, que solo acepta variables numéricas. Por eso trabajamos con las seis variables cuantitativas (PIB y densidad en logaritmo), estandarizadas con la media y la desviación estándar del entrenamiento.

vars_input <- c("log_PIB", "Urbano", "log_Densidad", "Agricultura", "Fertilidad", "Crec_poblacion")

train_input <- train[, vars_input]
test_input  <- test[, vars_input]
train_output <- train$Acceso_95
test_output  <- test$Acceso_95

scaler <- preProcess(train_input, method = c("center", "scale"))
train_input_scaled <- predict(scaler, train_input)
test_input_scaled  <- predict(scaler, test_input)

k_vals <- 1:100
resultado <- data.frame(k = k_vals, precision = NA_real_)

set.seed(0408)
for (n in k_vals) {
  pred_temp <- knn(
    train = train_input_scaled,
    test = test_input_scaled,
    cl = train_output,
    k = n
  )
  resultado$precision[n] <- mean(pred_temp == test_output)
}

k_optimo <- resultado$k[which.max(resultado$precision)]
prec_opt <- max(resultado$precision)

cat("K óptimo:", k_optimo, "| Exactitud óptima:", round(prec_opt, 4), "\n")
K óptimo: 85 | Exactitud óptima: 0.8507 
set.seed(0408)
pred_knn <- knn(
  train = train_input_scaled,
  test = test_input_scaled,
  cl = train_output,
  k = k_optimo,
  prob = TRUE
)

# Probabilidad estimada de la clase "Si" (proporción de vecinos con acceso universal)
prob_voto <- attr(pred_knn, "prob")
prob_knn_class <- ifelse(pred_knn == "Si", prob_voto, 1 - prob_voto)
plot_ly(resultado, x = ~k) %>%
  add_lines(
    y = ~precision,
    line = list(color = PAL[["teal"]], width = 2.5, shape = "spline"),
    name = "Exactitud",
    hovertemplate = "k = %{x}<br>Exactitud = %{y:.3f}<extra></extra>"
  ) %>%
  add_markers(
    y = ~precision,
    marker = list(size = 5, color = PAL[["teal"]], line = list(color = "white", width = 1)),
    showlegend = FALSE,
    hovertemplate = "k = %{x}<br>Exactitud = %{y:.3f}<extra></extra>"
  ) %>%
  add_segments(
    x = k_optimo, xend = k_optimo,
    y = min(resultado$precision), yend = max(resultado$precision),
    line = list(color = PAL[["ambar"]], width = 2.5, dash = "dash"),
    name = paste("k óptimo =", k_optimo)
  ) %>%
  estilo_plotly(
    "Figura 9. Exactitud del modelo KNN (class) según el número de vecinos",
    "Número de vecinos (k)",
    "",
    subtitulo = "Proporción de observaciones bien clasificadas en el conjunto de prueba para k = 1, ..., 100"
  ) %>%
  layout(yaxis = list(tickformat = ".1%"))

La Figura 9 muestra la exactitud para cada valor de k. El mejor resultado fue k = 85, con 85.1% de aciertos en el conjunto de prueba. Con k = 1, es decir, copiando la clase del vecino más parecido, la exactitud es 77.6%; a partir de k ≈ 30 se mantiene entre 82% y 85%, casi plana. Esto indica que los datos tienen una estructura bastante clara y que, pasado cierto número de vecinos, la elección de k no cambia mucho el resultado. Que el máximo esté casi al final del rango probado sugiere que aquí funciona mejor una frontera suave. Eso sí, como escogimos k mirando directamente el conjunto de prueba, este 85.1% es algo optimista; por eso en la siguiente versión escogemos k con validación cruzada.

cm_class <- confusionMatrix(pred_knn, test_output, positive = "Si")

tabla_confusion(cm_class, "Tabla 7. Matriz de confusión - Modelo KNN (class)")
Tabla 7. Matriz de confusión - Modelo KNN (class)
Valor real
Predicción No Si
No (acceso < 95%) 414 105
Si (acceso >= 95%) 47 452
Nota: En azul, la diagonal con las clasificaciones correctas; en naranja, los errores.
tabla_metricas(cm_class, "Tabla 8. Métricas de evaluación del modelo KNN (class)")
Tabla 8. Métricas de evaluación del modelo KNN (class)
Métrica Descripción Valor
Exactitud (Accuracy) Proporción total de predicciones correctas 0.8507
Intervalo de Confianza 95% Intervalo de confianza para la exactitud (0.8273, 0.8720)
Tasa de No Información Tasa al predecir siempre la clase mayoritaria 0.5472
Valor P [Exactitud > TNI] Significancia estadística vs tasa base 8.14e-95
Kappa de Cohen Acuerdo ajustado por azar entre observado y predicho 0.7019
Valor P Test de McNemar Test de simetría entre clasificaciones erróneas 0.0000
Sensibilidad (Recall) Capacidad de detectar países con acceso universal (VP / VP+FN) 0.8115
Especificidad Capacidad de detectar países sin acceso universal (VN / VN+FP) 0.8980
Valor Predictivo Positivo (Precision) Acierto cuando predice ‘Si’ (VP / VP+FP) 0.9058
Valor Predictivo Negativo Acierto cuando predice ‘No’ (VN / VN+FN) 0.7977
F1 Media armónica entre precisión y sensibilidad 0.8561
Prevalencia Proporción real de observaciones con acceso universal 0.5472
Tasa de Detección Verdaderos positivos sobre el total (VP / total) 0.4440
Prevalencia de Detección Proporción predicha como ‘Si’ (VP+FP / total) 0.4902
Exactitud Balanceada Promedio entre sensibilidad y especificidad 0.8548
Leyenda: VP = Verdadero Positivo, VN = Verdadero Negativo, FP = Falso Positivo, FN = Falso Negativo. Clase positiva: ‘Si’ (acceso >= 95%).

El KNN (class) acierta el 85,1% de las veces, muy por encima de la tasa de no información (54,7%), y la diferencia es significativa (p < 0,001). El Kappa de 0,702 indica un acuerdo sustancial entre lo que predice el modelo y lo que pasó en realidad, ya descontando lo que se acertaría por azar. La sensibilidad (81,1%) dice qué proporción de las observaciones con acceso universal reconoce bien el modelo, y la especificidad (89,8%), qué proporción de las que no lo tienen clasifica bien. Cuando el modelo dice que un país tiene acceso universal acierta el 90,6% de las veces, y cuando dice que no, acierta el 79,8%.

4.2.2 Modelo KNN (caret)

Con el paquete caret podemos usar también las variables categóricas (que se convierten en dummies automáticamente) y automatizar la validación cruzada y la elección de k. Para respetar que cada país aparece varias veces, armamos los 5 pliegues por país con groupKFold() : en cada vuelta, los países de validación no aparecen en el entrenamiento. Probamos los valores impares de k entre 1 y 101 y nos quedamos con el que daba el mayor AUC promedio en validación.

formula_modelo <- Acceso_95 ~ log_PIB + Urbano + log_Densidad + Agricultura +
  Fertilidad + Crec_poblacion + Region + Litoral

set.seed(0408)
folds <- groupKFold(train$Codigo, k = 5)

ctrl_knn <- trainControl(
  method = "cv",
  index = folds,
  classProbs = TRUE,
  summaryFunction = twoClassSummary
)

set.seed(0408)
modelo_knn <- train(
  formula_modelo,
  data = train,
  method = "knn",
  trControl = ctrl_knn,
  preProcess = c("center", "scale"),
  tuneGrid = data.frame(k = seq(1, 101, by = 2)),
  metric = "ROC"
)

pred_clase_knn <- predict(modelo_knn, newdata = test)
pred_prob_knn  <- predict(modelo_knn, newdata = test, type = "prob")
plot_ly(modelo_knn$results, x = ~k) %>%
  add_ribbons(
    ymin = ~ROC - ROCSD, ymax = ~ROC + ROCSD,
    fillcolor = "rgba(0,132,126,0.12)", line = list(color = "transparent"),
    name = "± 1 desv. estándar", hoverinfo = "none"
  ) %>%
  add_lines(
    y = ~ROC,
    line = list(color = PAL[["teal"]], width = 2.5, shape = "spline"),
    name = "AUC (validación cruzada)",
    hovertemplate = "k = %{x}<br>AUC = %{y:.4f}<extra></extra>"
  ) %>%
  add_markers(
    x = modelo_knn$bestTune$k,
    y = max(modelo_knn$results$ROC),
    marker = list(size = 13, color = PAL[["ambar"]], line = list(color = PAL[["navy"]], width = 2)),
    name = paste("k óptimo =", modelo_knn$bestTune$k),
    hovertemplate = "k óptimo = %{x}<br>AUC = %{y:.4f}<extra></extra>"
  ) %>%
  estilo_plotly(
    "Figura 10. AUC del modelo KNN (caret) según el número de vecinos",
    "Número de vecinos (k)",
    "",
    subtitulo = "Promedio del AUC en validación cruzada de 5 pliegues agrupada por país; la banda indica ± 1 desviación estándar"
  )

El mejor valor fue k = 85, con un AUC promedio en validación cruzada de 0,9316. En la Figura 10 se ve que el AUC sube rápido al pasar de k = 1 a valores intermedios (cuando el modelo deja de “memorizar” vecinos individuales) y luego se estabiliza. La banda sombreada muestra cuánto varía el resultado entre pliegues, que depende de qué países caen en cada uno.

cm_caret <- confusionMatrix(pred_clase_knn, test$Acceso_95, positive = "Si")

tabla_confusion(cm_caret, "Tabla 9. Matriz de confusión - Modelo KNN (caret)")
Tabla 9. Matriz de confusión - Modelo KNN (caret)
Valor real
Predicción No Si
No (acceso < 95%) 364 38
Si (acceso >= 95%) 97 519
Nota: En azul, la diagonal con las clasificaciones correctas; en naranja, los errores.
tabla_metricas(cm_caret, "Tabla 10. Métricas de evaluación del modelo KNN (caret)")
Tabla 10. Métricas de evaluación del modelo KNN (caret)
Métrica Descripción Valor
Exactitud (Accuracy) Proporción total de predicciones correctas 0.8674
Intervalo de Confianza 95% Intervalo de confianza para la exactitud (0.8450, 0.8876)
Tasa de No Información Tasa al predecir siempre la clase mayoritaria 0.5472
Valor P [Exactitud > TNI] Significancia estadística vs tasa base 9.25e-107
Kappa de Cohen Acuerdo ajustado por azar entre observado y predicho 0.7294
Valor P Test de McNemar Test de simetría entre clasificaciones erróneas 0.0000
Sensibilidad (Recall) Capacidad de detectar países con acceso universal (VP / VP+FN) 0.9318
Especificidad Capacidad de detectar países sin acceso universal (VN / VN+FP) 0.7896
Valor Predictivo Positivo (Precision) Acierto cuando predice ‘Si’ (VP / VP+FP) 0.8425
Valor Predictivo Negativo Acierto cuando predice ‘No’ (VN / VN+FN) 0.9055
F1 Media armónica entre precisión y sensibilidad 0.8849
Prevalencia Proporción real de observaciones con acceso universal 0.5472
Tasa de Detección Verdaderos positivos sobre el total (VP / total) 0.5098
Prevalencia de Detección Proporción predicha como ‘Si’ (VP+FP / total) 0.6051
Exactitud Balanceada Promedio entre sensibilidad y especificidad 0.8607
Leyenda: VP = Verdadero Positivo, VN = Verdadero Negativo, FP = Falso Positivo, FN = Falso Negativo. Clase positiva: ‘Si’ (acceso >= 95%).

En el conjunto de prueba, el KNN (caret) acierta el 86,7% (IC 95%: 84,5%–88,8%), con sensibilidad de 93,2%, especificidad de 79,0% y Kappa de 0,729. Le va un poco peor que al KNN (class), pero su resultado es más confiable: escoge k sin mirar el conjunto de prueba, así que mide mejor qué tan bien generaliza. Más adelante, en el análisis de errores, vemos que incluir región y litoral también le cuesta algo al KNN, porque esas dummies pesan mucho en la distancia.

4.3 Modelo Logit

La regresión logística estima la probabilidad de que una observación pertenezca a la clase positiva (acceso universal) en función de las variables explicativas. A diferencia del KNN, sí construye una fórmula explícita:

\[ P(\text{Acceso}_{95} = 1 \mid \mathbf{X}) = \frac{1}{1 + e^{-z}} \]

donde:

\[ \begin{aligned} z = \; & \beta_0 + \beta_1 \ln(\text{PIB pc}) + \beta_2 \, \text{Urbano} + \beta_3 \ln(\text{Densidad}) + \beta_4 \, \text{Agricultura} + \beta_5 \, \text{Fecundidad} \\ & + \beta_6 \, \text{Crec. poblacional} + \sum_{r} \gamma_r \, \text{Región}_r + \delta \, \text{Sin litoral} \end{aligned} \]

Lo entrenamos con glm() (familia binomial) sobre el conjunto de entrenamiento, con exactamente las mismas variables del KNN (caret).

fit_logit <- glm(
  Y ~ log_PIB + Urbano + log_Densidad + Agricultura + Fertilidad + Crec_poblacion + Region + Litoral,
  data = train,
  family = binomial()
)

# Umbral óptimo (índice de Youden) calculado en el conjunto de ENTRENAMIENTO
p_train <- predict(fit_logit, type = "response")
roc_train <- roc(response = train$Acceso_95, predictor = p_train, levels = c("No", "Si"), direction = "<")
umbral <- as.numeric(coords(roc_train, x = "best", best.method = "youden", ret = "threshold", transpose = FALSE)$threshold[1])

# Predicciones en el conjunto de prueba
p_hat <- predict(fit_logit, newdata = test, type = "response")
roc_logit <- roc(response = test$Acceso_95, predictor = p_hat, levels = c("No", "Si"), direction = "<")

pred_clase_logit <- factor(if_else(p_hat >= umbral, "Si", "No"), levels = c("No", "Si"))

# Indicadores de ajuste global
pseudo_r2 <- 1 - fit_logit$deviance / fit_logit$null.deviance
lr_stat <- fit_logit$null.deviance - fit_logit$deviance
lr_gl <- fit_logit$df.null - fit_logit$df.residual
lr_p <- pchisq(lr_stat, lr_gl, lower.tail = FALSE)
ic_or <- exp(confint.default(fit_logit))

nombres_coef <- c(
  "(Intercept)" = "Intercepto",
  "log_PIB" = "log(PIB per cápita)",
  "Urbano" = "Población urbana (%)",
  "log_Densidad" = "log(Densidad)",
  "Agricultura" = "Agricultura (% PIB)",
  "Fertilidad" = "Fecundidad",
  "Crec_poblacion" = "Crec. poblacional (%)",
  "RegionAsia" = "Región: Asia",
  "RegionEuropa" = "Región: Europa",
  "RegionNorteamérica" = "Región: Norteamérica",
  "RegionOceanía" = "Región: Oceanía",
  "RegionSudamérica" = "Región: Sudamérica",
  "LitoralSin litoral" = "Sin litoral"
)

tabla_logit_final <- tidy(fit_logit) %>%
  mutate(
    Variable = nombres_coef[term],
    OR = exp(estimate),
    IC_inf = ic_or[term, 1],
    IC_sup = ic_or[term, 2],
    Significancia = case_when(
      p.value < 0.001 ~ "***",
      p.value < 0.01  ~ "**",
      p.value < 0.05  ~ "*",
      p.value < 0.1   ~ ".",
      TRUE ~ ""
    ),
    p_txt = if_else(p.value < 0.001, "< 0.001", sprintf("%.4f", p.value))
  )

tabla_logit_final %>%
  transmute(
    Variable,
    Coeficiente = round(estimate, 4),
    `Error Estándar` = round(std.error, 4),
    `Estadístico z` = round(statistic, 3),
    `Valor p` = p_txt,
    Signif. = Significancia,
    `OR (e^β)` = round(OR, 3),
    `IC 95% OR` = paste0("[", round(IC_inf, 3), " ; ", round(IC_sup, 3), "]")
  ) %>%
  kbl(
    caption = "Tabla 11. Resultados del modelo de regresión logística (Logit)",
    align = c("l", "r", "r", "r", "r", "c", "r", "c")
  ) %>%
  kable_styling(
    bootstrap_options = c("hover", "condensed"),
    full_width = FALSE,
    font_size = 14,
    position = "center"
  ) %>%
  row_spec(which(tabla_logit_final$p.value >= 0.05), color = PAL[["gris_claro"]]) %>%
  column_spec(1, bold = TRUE, color = PAL[["navy"]], width = "4cm") %>%
  column_spec(7, bold = TRUE, color = ifelse(tabla_logit_final$OR > 1, PAL[["azul"]], PAL[["naranja"]])) %>%
  footnote(
    general = paste0(
      "Variable dependiente: Acceso >= 95% (1 = Si). Categorías base: África y Con litoral. ",
      "N = ", nobs(fit_logit), ". Deviance nula = ", round(fit_logit$null.deviance, 1),
      "; Deviance residual = ", round(fit_logit$deviance, 1),
      "; AIC = ", round(AIC(fit_logit), 1),
      "; Pseudo R² de McFadden = ", round(pseudo_r2, 3),
      ". Significancia: *** p<0.001, ** p<0.01, * p<0.05, . p<0.1. Las filas en gris no son significativas al 5%; los OR en azul aumentan la probabilidad y en naranja la disminuyen."
    ),
    general_title = "Nota: ",
    footnote_as_chunk = TRUE
  )
Tabla 11. Resultados del modelo de regresión logística (Logit)
Variable Coeficiente Error Estándar Estadístico z Valor p Signif. OR (e^β) IC 95% OR
Intercepto -28.4014 2.3168 -12.259 < 0.001 *** 0.000 [0 ; 0]
log(PIB per cápita) 2.6917 0.2100 12.817 < 0.001 *** 14.757 [9.778 ; 22.273]
Población urbana (%) 0.0254 0.0041 6.227 < 0.001 *** 1.026 [1.018 ; 1.034]
log(Densidad) 0.6529 0.0774 8.437 < 0.001 *** 1.921 [1.651 ; 2.236]
Agricultura (% PIB) -0.0069 0.0141 -0.488 0.6256 0.993 [0.966 ; 1.021]
Fecundidad -0.5456 0.1106 -4.934 < 0.001 *** 0.579 [0.467 ; 0.72]
Crec. poblacional (%) 0.1513 0.0827 1.829 0.0674 . 1.163 [0.989 ; 1.368]
Región: Asia 1.4806 0.2154 6.875 < 0.001 *** 4.395 [2.882 ; 6.704]
Región: Europa 0.8994 0.3226 2.788 0.0053 ** 2.458 [1.306 ; 4.626]
Región: Norteamérica -1.0066 0.2357 -4.271 < 0.001 *** 0.365 [0.23 ; 0.58]
Región: Oceanía 1.9025 0.3102 6.134 < 0.001 *** 6.703 [3.65 ; 12.311]
Región: Sudamérica 0.7794 0.2394 3.256 0.0011 ** 2.180 [1.364 ; 3.485]
Sin litoral 0.9264 0.2235 4.145 < 0.001 *** 2.525 [1.63 ; 3.913]
Nota: Variable dependiente: Acceso >= 95% (1 = Si). Categorías base: África y Con litoral. N = 3133. Deviance nula = 4282.1; Deviance residual = 1562.8; AIC = 1588.8; Pseudo R² de McFadden = 0.635. Significancia: *** p<0.001, ** p<0.01, * p<0.05, . p<0.1. Las filas en gris no son significativas al 5%; los OR en azul aumentan la probabilidad y en naranja la disminuyen.

La prueba de razón de verosimilitud compara el modelo estimado con un modelo que solo tiene intercepto: el estadístico es 2719.3 con 12 grados de libertad (p < 0,001), por lo que se rechaza la hipótesis de que todos los coeficientes son cero; en conjunto, las variables explicativas aportan información relevante para explicar el acceso universal. El pseudo R² de McFadden es 0.635; en la literatura se considera que valores entre 0,2 y 0,4 ya representan un ajuste muy bueno para este indicador (McFadden, 1979), por lo que el modelo tiene un poder explicativo alto.

Cada coeficiente es el cambio en el logaritmo de los odds de tener acceso universal cuando la variable sube una unidad y las demás se mantienen igual. Es más fácil leer su exponencial, el odds ratio (OR): si es mayor que 1, la variable aumenta la probabilidad de acceso universal, y si es menor que 1, la disminuye.

  • PIB per cápita: el coeficiente es 2.692 (estadísticamente significativo al 5%). Como la variable está en logaritmo, un aumento del 10% en el PIB per cápita multiplica los odds de acceso universal por 1.292, y duplicar el ingreso los multiplica por 6.46.
  • Población urbana: cada punto porcentual adicional de urbanización multiplica los odds por 1.026 (estadísticamente significativo al 5%).
  • Densidad poblacional: un aumento del 10% en la densidad multiplica los odds por 1.064 (estadísticamente significativo al 5%).
  • Agricultura: cada punto porcentual adicional de participación de la agricultura en el PIB multiplica los odds por 0.993 (no significativo al 5%).
  • Fecundidad: cada hijo adicional por mujer multiplica los odds por 0.579 (estadísticamente significativo al 5%).
  • Crecimiento poblacional: cada punto porcentual adicional de crecimiento anual de la población multiplica los odds por 1.163 (no significativo al 5%).
  • Región: frente a África (categoría base), los odds de acceso universal se multiplican por 4.4 en Asia, 2.46 en Europa, 0.37 en Norteamérica, 6.7 en Oceanía y 2.18 en Sudamérica, manteniendo constantes las demás características.
  • Sin litoral: frente a los países con salida al mar, los odds se multiplican por 2.525 (estadísticamente significativo al 5%).
p_forest <- tabla_logit_final %>%
  filter(term != "(Intercept)") %>%
  mutate(
    Variable = factor(Variable, levels = rev(Variable)),
    Efecto = if_else(OR > 1, "Aumenta la probabilidad", "Disminuye la probabilidad")
  ) %>%
  ggplot(aes(x = OR, y = Variable, color = Efecto,
             text = paste0(Variable, "<br>OR = ", round(OR, 3),
                           "<br>IC 95%: [", round(IC_inf, 3), " ; ", round(IC_sup, 3), "]"))) +
  geom_vline(xintercept = 1, linetype = "dashed", color = PAL[["gris"]]) +
  geom_errorbarh(aes(xmin = IC_inf, xmax = IC_sup), height = 0, linewidth = 1) +
  geom_point(size = 3.2) +
  scale_x_log10() +
  scale_color_manual(values = c("Aumenta la probabilidad" = PAL[["azul"]], "Disminuye la probabilidad" = PAL[["naranja"]])) +
  labs(
    title = "Figura 11. Odds ratios del modelo Logit (IC 95%)",
    x = "Odds ratio (escala logarítmica)",
    y = NULL,
    color = NULL
  ) +
  tema_datos() +
  theme(panel.grid.major.y = element_blank(), panel.grid.major.x = element_line(color = PAL[["grid"]], linetype = "dashed", linewidth = 0.4))

ggplotly(p_forest, tooltip = "text") %>%
  layout(legend = list(orientation = "h", x = 0, xanchor = "left", y = 1.02, yanchor = "bottom"), margin = list(t = 90, b = 120)) %>%
  con_fuente(y = -0.28)

El umbral es la probabilidad a partir de la cual una observación se clasifica como “Sí”. El valor por defecto, 0,5, no siempre es el mejor, así que lo escogimos con el índice de Youden sobre la curva ROC del entrenamiento, que busca el punto que maximiza la suma de sensibilidad y especificidad

roc_data_logit <- data.frame(
  FalsosPositivos = 1 - roc_logit$specificities,
  Sensibilidad = roc_logit$sensitivities
) %>%
  arrange(FalsosPositivos, Sensibilidad)

auc_logit <- round(as.numeric(auc(roc_logit)), 4)

punto_optimo_idx <- which.min(abs(roc_logit$thresholds - umbral))
punto_optimo_x <- 1 - roc_logit$specificities[punto_optimo_idx]
punto_optimo_y <- roc_logit$sensitivities[punto_optimo_idx]

plot_ly(roc_data_logit, type = "scatter", mode = "lines") %>%
  add_trace(
    x = ~FalsosPositivos, y = ~Sensibilidad,
    fill = "tozeroy", fillcolor = "rgba(44,107,184,0.10)",
    line = list(color = PAL[["azul"]], width = 3),
    name = "Curva ROC",
    hoverinfo = "text",
    text = ~paste("1-Especificidad:", round(FalsosPositivos, 3), "<br>Sensibilidad:", round(Sensibilidad, 3))
  ) %>%
  add_trace(
    x = c(0, 1), y = c(0, 1), type = "scatter", mode = "lines",
    line = list(color = PAL[["gris_claro"]], dash = "dash", width = 1.5),
    name = "Clasificador aleatorio", hoverinfo = "none"
  ) %>%
  add_trace(
    x = punto_optimo_x, y = punto_optimo_y, type = "scatter", mode = "markers",
    marker = list(color = PAL[["ambar"]], size = 13, line = list(color = PAL[["navy"]], width = 2)),
    name = "Umbral óptimo (Youden)",
    hoverinfo = "text",
    text = paste("Umbral:", round(umbral, 3), "<br>1-Especificidad:", round(punto_optimo_x, 3),
                 "<br>Sensibilidad:", round(punto_optimo_y, 3))
  ) %>%
  estilo_plotly(
    "Figura 12. Curva ROC del modelo Logit",
    "1 - Especificidad (tasa de falsos positivos)",
    "Sensibilidad",
    subtitulo = "Conjunto de prueba; el punto marca el umbral óptimo de Youden estimado en el entrenamiento"
  ) %>%
  layout(
    xaxis = list(range = c(0, 1)),
    yaxis = list(range = c(0, 1.02)),
    annotations = list(
      list(x = 0.97, y = 0.22, xanchor = "right", text = paste("<b>AUC =", auc_logit, "</b>"), showarrow = FALSE,
           font = list(family = fuente_titulos, size = 22, color = PAL[["azul"]])),
      list(x = 0.97, y = 0.1, xanchor = "right", text = paste("Umbral óptimo =", round(umbral, 3)), showarrow = FALSE,
           font = list(family = fuente_texto, size = 14, color = PAL[["gris"]])),
      list(x = 0, y = -0.34, text = fuente_datos, showarrow = FALSE, xref = "paper", yref = "paper",
           xanchor = "left", font = list(family = fuente_texto, size = 11, color = PAL[["gris"]]))
    )
  )

El umbral óptimo fue 0,565. Como es mayor que 0,5, el modelo necesita una probabilidad alta para decir que un país tiene acceso universal: es un modelo “conservador”, que gana especificidad (pocos falsos positivos) a cambio de perder algo de sensibilidad. El AUC en el conjunto de prueba es 0,952: si tomamos al azar una observación con acceso universal y otra sin él, el modelo le da mayor probabilidad a la primera el 95.2% de las veces. Un AUC de 0,5 equivale a lanzar una moneda y uno de 1 sería perfecto; según Hosmer, Lemeshow y Sturdivant (2013), un AUC de 0,9 o más indica una discriminación sobresaliente.

performa <- function(cutoff, prob, ref, postarget, negtarget) {
  predict <- factor(ifelse(prob >= cutoff, postarget, negtarget), levels = c(negtarget, postarget))
  conf <- confusionMatrix(predict, ref, positive = postarget)
  c(
    Sensibilidad = unname(conf$byClass["Sensitivity"]),
    Exactitud = unname(conf$overall["Accuracy"]),
    Precision = unname(conf$byClass["Precision"]),
    Especificidad = unname(conf$byClass["Specificity"])
  )
}

co <- seq(0.02, 0.98, length = 100)
result <- t(sapply(co, performa, prob = p_hat, ref = test$Acceso_95, postarget = "Si", negtarget = "No")) %>%
  as.data.frame() %>%
  mutate(Umbral = co) %>%
  pivot_longer(-Umbral, names_to = "Métrica", values_to = "Valor")

plot_ly(
  result, x = ~Umbral, y = ~Valor, color = ~Métrica,
  colors = c("Sensibilidad" = PAL[["azul"]], "Exactitud" = PAL[["teal"]], "Precision" = PAL[["morado"]], "Especificidad" = PAL[["naranja"]]),
  type = "scatter", mode = "lines", line = list(width = 3),
  hoverinfo = "text",
  text = ~paste("Métrica:", Métrica, "<br>Umbral:", round(Umbral, 3), "<br>Valor:", round(Valor, 3))
) %>%
  add_segments(
    x = umbral, xend = umbral, y = 0, yend = 1,
    line = list(color = PAL[["ambar"]], dash = "dash", width = 2.5),
    name = "Umbral óptimo", inherit = FALSE
  ) %>%
  estilo_plotly(
    "Figura 13. Desempeño del modelo Logit según el umbral de decisión",
    "Umbral de probabilidad",
    "",
    subtitulo = "Métricas en el conjunto de prueba para distintos puntos de corte"
  ) %>%
  layout(yaxis = list(range = c(0, 1.02)))

La Figura 13 muestra cómo cambian las métricas al mover el umbral: con umbrales bajos casi todo se clasifica como “Sí” (sensibilidad alta, especificidad baja) y con umbrales altos pasa lo contrario. La exactitud se mantiene por encima de 85% para umbrales entre 0,35 y 0,76, lo que indica que el modelo separa bien las clases y que la mayoría de las probabilidades estimadas quedan cerca de 0 o de 1

cm_logit <- confusionMatrix(pred_clase_logit, test$Acceso_95, positive = "Si")

tabla_confusion(cm_logit, "Tabla 13. Matriz de confusión - Modelo Logit")
Tabla 13. Matriz de confusión - Modelo Logit
Valor real
Predicción No Si
No (acceso < 95%) 407 90
Si (acceso >= 95%) 54 467
Nota: En azul, la diagonal con las clasificaciones correctas; en naranja, los errores.
tabla_metricas(cm_logit, "Tabla 14. Métricas de evaluación del modelo Logit")
Tabla 14. Métricas de evaluación del modelo Logit
Métrica Descripción Valor
Exactitud (Accuracy) Proporción total de predicciones correctas 0.8585
Intervalo de Confianza 95% Intervalo de confianza para la exactitud (0.8356, 0.8794)
Tasa de No Información Tasa al predecir siempre la clase mayoritaria 0.5472
Valor P [Exactitud > TNI] Significancia estadística vs tasa base 2.61e-100
Kappa de Cohen Acuerdo ajustado por azar entre observado y predicho 0.7165
Valor P Test de McNemar Test de simetría entre clasificaciones erróneas 0.0035
Sensibilidad (Recall) Capacidad de detectar países con acceso universal (VP / VP+FN) 0.8384
Especificidad Capacidad de detectar países sin acceso universal (VN / VN+FP) 0.8829
Valor Predictivo Positivo (Precision) Acierto cuando predice ‘Si’ (VP / VP+FP) 0.8964
Valor Predictivo Negativo Acierto cuando predice ‘No’ (VN / VN+FN) 0.8189
F1 Media armónica entre precisión y sensibilidad 0.8664
Prevalencia Proporción real de observaciones con acceso universal 0.5472
Tasa de Detección Verdaderos positivos sobre el total (VP / total) 0.4587
Prevalencia de Detección Proporción predicha como ‘Si’ (VP+FP / total) 0.5118
Exactitud Balanceada Promedio entre sensibilidad y especificidad 0.8606
Leyenda: VP = Verdadero Positivo, VN = Verdadero Negativo, FP = Falso Positivo, FN = Falso Negativo. Clase positiva: ‘Si’ (acceso >= 95%).

El Logit clasifica bien el 85,9% de las observaciones de prueba (IC 95%: 83,6%–87,9%), muy por encima de la tasa de no información (54,7%). El Kappa de 0,716 indica un acuerdo sustancial más allá del azar. La sensibilidad es 83,8% y la especificidad 88,3%: cuando el modelo dice que hay acceso universal acierta el 89,6% de las veces, y cuando dice que no, el 81,9%.

5 Comparación de modelos

Comparamos los tres clasificadores sobre el mismo conjunto de prueba (los 46 países que no se usaron para entrenar). La comparación principal es entre el KNN (caret), que usa las mismas variables que el Logit y escoge k con validación cruzada, y el Logit; el KNN (class) queda como referencia.

roc_knn <- roc(response = test$Acceso_95, predictor = pred_prob_knn$Si, levels = c("No", "Si"), direction = "<")
roc_class <- roc(response = test$Acceso_95, predictor = prob_knn_class, levels = c("No", "Si"), direction = "<")

brier <- function(prob) mean((prob - test$Y)^2)

fila_metricas <- function(cm, roc_obj, prob) {
  c(
    cm$overall["Accuracy"],
    cm$overall["Kappa"],
    cm$byClass["Sensitivity"],
    cm$byClass["Specificity"],
    cm$byClass["Pos Pred Value"],
    cm$byClass["Neg Pred Value"],
    cm$byClass["F1"],
    cm$byClass["Balanced Accuracy"],
    as.numeric(auc(roc_obj)),
    brier(prob),
    cm$table["Si", "No"],
    cm$table["No", "Si"]
  )
}

comparacion_integral <- data.frame(
  Metrica = c(
    "Exactitud (Accuracy)", "Kappa de Cohen", "Sensibilidad", "Especificidad",
    "Valor Predictivo Positivo", "Valor Predictivo Negativo", "F1",
    "Exactitud Balanceada", "AUC (área bajo la curva ROC)", "Puntaje de Brier (menor es mejor)",
    "Falsos positivos (predice Si, es No)", "Falsos negativos (predice No, es Si)"
  ),
  KNN_class = fila_metricas(cm_class, roc_class, prob_knn_class),
  KNN_caret = fila_metricas(cm_caret, roc_knn, pred_prob_knn$Si),
  Logit = fila_metricas(cm_logit, roc_logit, p_hat)
)

mejor <- c(rep("max", 9), rep("min", 3))
ganador <- sapply(seq_len(nrow(comparacion_integral)), function(i) {
  valores <- unlist(comparacion_integral[i, c("KNN_caret", "Logit")])
  if (abs(diff(valores)) < 1e-9) return("Empate")
  c("KNN (caret)", "Logit")[if (mejor[i] == "max") which.max(valores) else which.min(valores)]
})

comparacion_integral %>%
  mutate(
    across(KNN_class:Logit, ~ if_else(row_number() > 10, as.character(round(., 0)), sprintf("%.4f", .))),
    `Mejor (KNN caret vs Logit)` = ganador
  ) %>%
  kbl(
    caption = "Tabla 15. Comparación integral del desempeño de los modelos en el conjunto de prueba",
    align = c("l", "c", "c", "c", "c"),
    col.names = c("Métrica", "KNN (class)", "KNN (caret)", "Logit", "Mejor (KNN caret vs Logit)")
  ) %>%
  kable_styling(
    bootstrap_options = c("hover", "condensed"),
    full_width = FALSE,
    font_size = 14,
    position = "center"
  ) %>%
  row_spec(c(1, 3, 4, 9), background = PAL[["celeste"]]) %>%
  column_spec(1, bold = TRUE, color = PAL[["navy"]], width = "6cm") %>%
  column_spec(2:4, width = "2.5cm") %>%
  column_spec(5, bold = TRUE, color = ifelse(ganador == "Logit", PAL[["azul"]], ifelse(ganador == "Empate", PAL[["gris"]], PAL[["teal"]]))) %>%
  footnote(
    general = "Clase positiva: 'Si' (acceso >= 95%). Elaboración propia con base en OWID y Banco Mundial (2000-2022).",
    general_title = "Nota: ",
    footnote_as_chunk = TRUE
  )
Tabla 15. Comparación integral del desempeño de los modelos en el conjunto de prueba
Métrica KNN (class) KNN (caret) Logit Mejor (KNN caret vs Logit)
Exactitud (Accuracy) 0.8507 0.8674 0.8585 KNN (caret)
Kappa de Cohen 0.7019 0.7294 0.7165 KNN (caret)
Sensibilidad 0.8115 0.9318 0.8384 KNN (caret)
Especificidad 0.8980 0.7896 0.8829 Logit
Valor Predictivo Positivo 0.9058 0.8425 0.8964 Logit
Valor Predictivo Negativo 0.7977 0.9055 0.8189 KNN (caret)
F1 0.8561 0.8849 0.8664 KNN (caret)
Exactitud Balanceada 0.8548 0.8607 0.8606 KNN (caret)
AUC (área bajo la curva ROC) 0.9160 0.9527 0.9520 KNN (caret)
Puntaje de Brier (menor es mejor) 0.1151 0.0889 0.0913 KNN (caret)
Falsos positivos (predice Si, es No) 47 97 54 Logit
Falsos negativos (predice No, es Si) 105 38 90 KNN (caret)
Nota: Clase positiva: ‘Si’ (acceso >= 95%). Elaboración propia con base en OWID y Banco Mundial (2000-2022).
roc_comparada <- bind_rows(
  data.frame(FPR = 1 - roc_class$specificities, TPR = roc_class$sensitivities, Modelo = "KNN (class)"),
  data.frame(FPR = 1 - roc_knn$specificities, TPR = roc_knn$sensitivities, Modelo = "KNN (caret)"),
  data.frame(FPR = 1 - roc_logit$specificities, TPR = roc_logit$sensitivities, Modelo = "Logit")
) %>%
  arrange(Modelo, FPR, TPR)

auc_knn <- round(as.numeric(auc(roc_knn)), 4)
auc_class <- round(as.numeric(auc(roc_class)), 4)

plot_ly(colors = col_modelos) %>%
  add_trace(
    x = c(0, 1), y = c(0, 1), type = "scatter", mode = "lines",
    line = list(dash = "dash", color = PAL[["gris_claro"]], width = 1.2), name = "Clasificador aleatorio"
  ) %>%
  add_trace(
    data = roc_comparada, x = ~FPR, y = ~TPR, color = ~Modelo,
    type = "scatter", mode = "lines", line = list(width = 3),
    hoverinfo = "text",
    text = ~paste0(Modelo, "<br>FPR: ", round(FPR, 3), "<br>TPR: ", round(TPR, 3))
  ) %>%
  estilo_plotly(
    "Figura 14. Curvas ROC de los tres modelos",
    "Tasa de falsos positivos (1 - Especificidad)",
    "Sensibilidad",
    subtitulo = "Conjunto de prueba: 46 países que no participaron en el entrenamiento"
  ) %>%
  layout(
    xaxis = list(range = c(0, 1)),
    yaxis = list(range = c(0, 1.02)),
    annotations = list(
      list(x = 0.97, y = 0.34, xanchor = "right", text = paste("<b>KNN (class)</b>  AUC =", auc_class), showarrow = FALSE,
           font = list(family = fuente_texto, color = col_modelos[["KNN (class)"]], size = 14)),
      list(x = 0.97, y = 0.24, xanchor = "right", text = paste("<b>KNN (caret)</b>  AUC =", auc_knn), showarrow = FALSE,
           font = list(family = fuente_texto, color = col_modelos[["KNN (caret)"]], size = 14)),
      list(x = 0.97, y = 0.14, xanchor = "right", text = paste("<b>Logit</b>  AUC =", auc_logit), showarrow = FALSE,
           font = list(family = fuente_texto, color = col_modelos[["Logit"]], size = 14)),
      list(x = 0, y = -0.34, text = fuente_datos, showarrow = FALSE, xref = "paper", yref = "paper",
           xanchor = "left", font = list(family = fuente_texto, size = 11, color = PAL[["gris"]]))
    )
  )
metricas_comparacion <- comparacion_integral %>%
  slice(c(1, 2, 3, 4, 7, 8, 9)) %>%
  mutate(Metrica = c("Exactitud", "Kappa", "Sensibilidad", "Especificidad", "F1", "Exactitud balanceada", "AUC")) %>%
  pivot_longer(-Metrica, names_to = "Modelo", values_to = "Valor") %>%
  mutate(
    Modelo = recode(Modelo, KNN_class = "KNN (class)", KNN_caret = "KNN (caret)"),
    Metrica = factor(Metrica, levels = c("Exactitud", "Kappa", "Sensibilidad", "Especificidad", "F1", "Exactitud balanceada", "AUC"))
  )

plot_ly(
  metricas_comparacion, x = ~Metrica, y = ~Valor, color = ~Modelo,
  colors = col_modelos,
  type = "bar",
  text = ~sprintf("%.2f", Valor), textposition = "outside", textfont = list(size = 10, color = PAL[["gris"]]),
  hovertemplate = "%{x}<br>%{y:.4f}<extra></extra>"
) %>%
  estilo_plotly("Figura 15. Comparación de métricas entre modelos", "", "", subtitulo = "Valores en el conjunto de prueba (1 = desempeño perfecto)") %>%
  layout(barmode = "group", yaxis = list(range = c(0, 1.1)))

El KNN (caret) tiene mejor exactitud: 86,7% frente a 85,9% del Logit. Ambos superan con creces la tasa de no información (54,7%). La exactitud balanceada, que promedia el desempeño en las dos clases, queda prácticamente empatada: 86,1% para el KNN (caret) y 86,1% para el Logit.

En la detección de casos positivos, el KNN (caret) tiene mejor sensibilidad: 93,2% frente a 83,8% del Logit. Sin embargo, en especificidad —es decir, qué tan bien detecta a los países sin acceso universal, que para política pública son los que más importan— gana el Logit: 88,3% frente a 79,0%.

El KNN (caret) también tiene mejor AUC: 0,953 frente a 0,952 del Logit. Según la prueba de DeLong (Tabla 16), la diferencia no es significativa al 5% (p = 0,094). El KNN (class) tiene una exactitud de 85,1%, pero su k se escogió mirando el conjunto de prueba, así que ese número es optimista y no se puede comparar en igualdad de condiciones. Aun así, su AUC (0,916) queda por debajo de los otros dos modelos.

En otras comparaciones, el Kappa (0,729 para el KNN (caret) frente a 0,716 para el Logit) favorece al Logit, mientras que el F1 (0,885 frente a 0,866) favorece al KNN (caret). El puntaje de Brier, que mide qué tan buenas son las probabilidades —el error cuadrático medio entre la probabilidad predicha y lo que pasó—, es 0,089 para el KNN (caret) y 0,091 para el Logit, de nuevo mejor el KNN (caret). Por último, la prueba de McNemar (p = 0,113) indica que la diferencia en errores entre los dos modelos no es significativa.

Hay un matiz importante: el KNN clasifica con un umbral de 0,5 y el Logit con 0,565, así que exactitud, sensibilidad y especificidad dependen en parte de ese corte. El umbral por encima de 0,5 es lo que hace al Logit más específico y menos sensible. Por eso, la comparación más justa es el AUC, que no depende del umbral.

5.1 ¿Qué modelo se recomienda?

Teniendo en cuenta todos los criterios, se recomienda el KNN (caret) para este problema por tres razones. En cuanto al desempeño, tiene mejor exactitud (86,7% frente a 85,9%), mejor AUC (0,953 frente a 0,952), mejor F1 (0,885 frente a 0,866) y mejor puntaje de Brier (0,089 frente a 0,091), aunque las diferencias en exactitud y AUC no son estadísticamente significativas. Es un modelo que capta patrones locales y no impone una forma funcional a la relación entre ingreso y acceso, lo cual es útil cuando la forma exacta no está garantizada de antemano.

Hay dos contrapesos importantes. Primero, en especificidad —qué tan bien detecta a los países sin acceso universal, que para política pública son los que más importan— gana el Logit (88,3% frente a 79,0%). Segundo, el KNN funciona como una “caja negra” y no dice cuánto pesa cada variable, algo clave si el objetivo es orientar políticas públicas con magnitudes interpretables (odds ratios); el Logit, además, concuerda con la Figura 3, donde la relación entre ingreso y acceso tiene forma de “S”.

Respecto a las probabilidades, las del Logit son continuas y suaves, mientras que las del KNN son proporciones de votos entre k vecinos, así que van “en escalones”. En simplicidad, el Logit se resume en 13 coeficientes y predice un país nuevo al instante, mientras que el KNN tiene que guardar toda la base de entrenamiento y calcular la distancia a cada observación cada vez que predice.

El Logit sigue siendo una alternativa válida, especialmente cuando prima la interpretabilidad o cuando los falsos positivos (predecir acceso universal donde no lo hay) son costosos: es más específico, se explica con odds ratios y su forma funcional coincide con la relación observada entre ingreso y acceso. Si se quisiera mejorar el KNN, lo primero sería revisar cuánto pesan las variables dummy en la distancia.

6 Conclusiones

85,9%exactitud del Logit en países nunca vistos
0,952AUC del Logit (KNN caret: 0,953)
93,2%sensibilidad del KNN, el que mejor detecta el acceso universal
88,3%especificidad del Logit, el que mejor detecta a los rezagados

El presente estudio tuvo como objetivo desarrollar y comparar modelos de clasificación supervisada (KNN y regresión logística) para predecir si un país tiene acceso a la electricidad para al menos el 95% de su población, a partir de variables económicas, demográficas y geográficas, utilizando información de 185 países entre 2000 y 2022.

Sobre los determinantes del acceso universal. El análisis descriptivo y el modelo Logit confirman que el acceso universal a la electricidad es, ante todo, un reflejo del nivel de desarrollo económico y de la estructura territorial de la población. El PIB per cápita es la variable con mayor capacidad para separar las dos clases, seguida de la fecundidad, la participación de la agricultura en la economía y la urbanización. Los países que aún no alcanzan el acceso universal se caracterizan por ingresos bajos, población predominantemente rural, alta dependencia de la agricultura y un crecimiento poblacional acelerado que obliga a expandir la red solo para no perder cobertura.

En el modelo multivariado, el PIB per cápita, la urbanización y la densidad poblacional aumentan significativamente la probabilidad de acceso universal, mientras que la fecundidad la disminuye; la agricultura y el crecimiento poblacional pierden significancia porque su información ya está contenida en el ingreso y la fecundidad. La región aporta información relevante aun controlando por las demás variables, y la condición de no tener litoral cambia de signo respecto al análisis descriptivo, reflejando la herencia de la electrificación soviética en Asia Central. Estos resultados muestran la importancia de analizar las variables de forma conjunta y no solo de manera bivariada.

Sobre los modelos. Ambos modelos lograron un desempeño muy alto: exactitudes de 86,7% (KNN caret) y 85,9% (Logit) y AUC de 0,953 y 0,952, respectivamente, muy por encima del 54,7% que se obtendría prediciendo siempre la clase mayoritaria. Estos resultados son especialmente robustos porque la evaluación se hizo sobre países que no participaron en el entrenamiento y porque los hiperparámetros (k en KNN y el umbral en Logit) se seleccionaron sin usar el conjunto de prueba. Las diferencias entre los dos modelos son pequeñas y, en exactitud y AUC, no estadísticamente significativas: el KNN (caret) es mejor en exactitud, AUC, F1, sensibilidad y Brier, mientras que el Logit gana en especificidad y Kappa. Dado que ninguna diferencia es concluyente, ambos modelos son defendibles; el KNN (caret) tiene la ventaja empírica y el Logit la ventaja interpretativa, por lo que se recomienda reportarlos en conjunto.

¿El modelo logró responder al objetivo de la investigación?

Sí. El objetivo era construir y comparar modelos capaces de clasificar a los países según hayan alcanzado o no el acceso (casi) universal a la electricidad, e identificar las variables asociadas a esta condición. Los modelos clasifican correctamente cerca de 9 de cada 10 observaciones de países nunca vistos, con un AUC superior a 0,95, lo que demuestra que las variables seleccionadas contienen una señal muy fuerte sobre el acceso a la electricidad. Además, el modelo Logit permitió cuantificar la dirección y la magnitud del efecto de cada variable, cumpliendo también el propósito explicativo.

No obstante, deben reconocerse algunas limitaciones:

  • Los errores se concentran en los países de transición, cuya cobertura se encuentra cerca del 95%, y en países con trayectorias históricas particulares (Marruecos, Tayikistán) que las variables estructurales no capturan. Para estos casos, la clasificación binaria es sensible a pequeñas variaciones en la medición del acceso, que proviene de encuestas de hogares y modelos de estimación del Banco Mundial.
  • El modelo es de naturaleza asociativa y no causal: por ejemplo, la urbanización facilita la electrificación, pero la electrificación también atrae población hacia las ciudades.
  • Las observaciones de un mismo país en distintos años no son independientes; aunque la partición por países evita la fuga de información, los errores estándar del Logit podrían estar subestimados. Un modelo de panel con efectos aleatorios o errores agrupados por país permitiría hacer inferencias más rigurosas.
  • Variables como la calidad institucional, la inversión en infraestructura, la disponibilidad de recursos energéticos o los conflictos armados, que no se incluyeron, podrían explicar parte de los errores.

Recomendaciones. Con base en los hallazgos, se proponen las siguientes estrategias:

  1. Focalización: los países con baja probabilidad estimada de acceso universal (ingreso bajo, alta ruralidad, alta dependencia agrícola y alto crecimiento poblacional, en su mayoría de África subsahariana) deben ser prioritarios para la cooperación internacional y el financiamiento de la meta 7.1 del ODS 7.
  2. Soluciones para zonas rurales: como la ruralidad y la baja densidad elevan el costo de extender la red, es recomendable impulsar soluciones descentralizadas (mini-redes y sistemas solares domiciliarios), que reducen la dependencia de la infraestructura de transmisión.
  3. Aprender de los casos de éxito: los países que alcanzaron el acceso universal con condiciones estructurales desfavorables, como Marruecos con su programa de electrificación rural, ofrecen lecciones de política replicables en otros países de ingreso medio-bajo.
  4. Uso del modelo como herramienta de alerta: los países que el modelo clasifica con alta probabilidad de acceso universal, pero que en la realidad no lo han alcanzado, revelan brechas que no se explican por sus condiciones estructurales y que pueden deberse a problemas de gestión o de política energética; estos casos merecen un análisis particular.
  5. Mejoras metodológicas: incorporar variables institucionales y de infraestructura, estimar modelos de panel, evaluar el umbral de 95% frente a otros umbrales (por ejemplo, 80% o 99%) y comparar con algoritmos de ensamble como Random Forest o Gradient Boosting.

En síntesis, el aprendizaje supervisado demostró ser una herramienta eficaz para clasificar el acceso universal a la electricidad. El KNN (caret) ofrece el mejor desempeño empírico y el Logit aporta la interpretabilidad necesaria para entender qué variables explican el acceso; usar ambos de forma complementaria permite no solo predecir qué países han alcanzado la meta, sino también entender por qué otros siguen rezagados, aportando evidencia útil para el diseño de políticas orientadas a cumplir el ODS 7.

7 Bibliografía

  • Banco Mundial (2025). World Development Indicators: Access to electricity (% of population) [EG.ELC.ACCS.ZS]. https://data.worldbank.org/indicator/EG.ELC.ACCS.ZS

  • Gapminder Foundation (2025). Systema Globalis y Fast Track datasets. https://github.com/open-numbers

  • Hosmer, D. W., Lemeshow, S., & Sturdivant, R. X. (2013). Applied Logistic Regression (3ª ed.). John Wiley & Sons.

  • IEA, IRENA, UNSD, World Bank, WHO (2024). Tracking SDG 7: The Energy Progress Report 2024. World Bank, Washington DC. https://trackingsdg7.esmap.org/

  • James, G., Witten, D., Hastie, T., & Tibshirani, R. (2021). An Introduction to Statistical Learning with Applications in R (2ª ed.). Springer.

  • Kuhn, M. (2008). Building Predictive Models in R Using the caret Package. Journal of Statistical Software, 28(5), 1-26.

  • McFadden, D. (1979). Quantitative Methods for Analysing Travel Behaviour of Individuals: Some Recent Developments. En D. Hensher & P. Stopher (Eds.), Behavioural Travel Modelling (pp. 279-318). Croom Helm.

  • Naciones Unidas (2015). Transformar nuestro mundo: la Agenda 2030 para el Desarrollo Sostenible. Objetivo 7: Energía asequible y no contaminante.

  • Paredes, D. (2024). Aprendizaje supervisado. https://bookdown.org/dparedesi/data-science-con-r/aprendizaje-supervisado.html

  • Ritchie, H., Rosado, P., & Roser, M. (2024). Access to Energy. Our World in Data. https://ourworldindata.org/energy-access

  • Robin, X., Turck, N., Hainard, A., Tiberti, N., Lisacek, F., Sanchez, J.-C., & Müller, M. (2011). pROC: an open-source package for R and S+ to analyze and compare ROC curves. BMC Bioinformatics, 12, 77.

  • Venables, W. N., & Ripley, B. D. (2002). Modern Applied Statistics with S (4ª ed.). Springer. (Paquete class).

  • Wooldridge, J. M. (2012). Introductory Econometrics: A Modern Approach (5ª ed.). South-Western Cengage Learning.