1 INTRODUCCIÓN

La prueba ICFES es un examen de Estado estandarizado en Colombia que evalúa las competencias y conocimientos de los estudiantes que están terminando el grado undécimo de bachillerato. Su objetivo es comprobar el grado de desarrollo de las competencias de los estudiantes que están por finalizar el grado undécimo de la educación media, monitoreando la calidad de la educación de los establecimientos educativos del país, con fundamento en los estándares básicos de competencias y los referentes de calidad emitidos por el Ministerio de Educación Nacional (Instituto Colombiano para la Evaluación de la Educación, 2025).

En el área de matemáticas se evalúan las competencias para enfrentarse a situaciones que requieren el uso de herramientas matemáticas (álgebra y cálculo, geometría y estadística). El rendimiento en esta área está influenciado por condiciones socioeconómicas, demográficas e institucionales del estudiante (Instituto Colombiano para la Evaluación de la Educación, 2025).

Este análisis tiene como objetivo principal construir un modelo de clasificación binaria que permita predecir la probabilidad de que un estudiante del Valle del Cauca alcance un puntaje alto en el módulo de Matemáticas del ICFES. A través de la comparación entre un modelo paramétrico Regresión Logística (logit) y uno no paramétrico (KNN), se evaluará cuál de los dos algoritmos ofrece mayor precisión, sensibilidad y capacidad predictiva para el contexto regional.

Identificar las variables que influyen en el rendimiento escolar permite predecir si un estudiante obtendrá un desempeño alto o bajo en matemáticas, lo que aporta evidencia clave para comprender las brechas de aprendizaje en los sectores con menores puntajes.

El presente informe se encuentra estructurado de la siguiente manera: en primer lugar, se expone la Metodología, donde se especifica la binarización del desempeño en Matemáticas y la selección de las variables independientes. A continuación, en el Resultados Descriptivos, se explora el comportamiento de las variables mediante tablas e indicadores estadísticos apoyados en gráficos de ggplot2. Posteriormente, en la sección de Resultados de los Modelos, se evalúan y comparan los modelos de Regresión Logística y KNN a través de sus métricas de desempeño (exactitud, sensibilidad, especificidad y AUC). Finalmente, se presentan las Conclusiones sobre los hallazgos y el cumplimiento del objetivo del modelo.

2 METODOLOGÍA

El presente análisis adoptó un enfoque de aprendizaje supervisado orientado a la clasificación binaria del desempeño academico en las pruebas ICFES en el area de matemáticas (Alto rendimiento/Bajo rendimiento), implementado mediante dos familias de algoritmos: K-Vecinos Más Cercanos (KNN) y regresión logística (Logit). La metodología se estructuró en cinco etapas fundamentales que garantizaron la reproducibilidad, validez y comparabilidad de los resultados.

2.1 SELECCIÓN DE LA BASE DE DATOS

Se utilizaron los resultados individuales anonimizados del examen de Estado Saber 11, publicados por el Instituto Colombiano para la Evaluación de la Educación (ICFES) en el Portal Nacional de Datos Abiertos de Colombia (datos.gov.co), administrado por el Ministerio de Tecnologías de la Información y las Comunicaciones (MinTIC). El conjunto de datos “Resultados únicos Saber 11” contiene 7.109.704 registros correspondientes a los periodos comprendidos entre 2010 y 2022. Sus datos fueron actualizados por última vez el 23 de agosto de 2023 y sus metadatos el 18 de mayo de 2026; la base se consultó el 9 de octubre de 2026. La base contiene 51 variables, que incluyen los puntajes por área (matemáticas, lectura crítica, ciencias naturales, sociales e inglés), el puntaje global y la caracterización socioeconómica, demográfica e institucional de los evaluados a nivel nacional. De ellas se seleccionaron siete, las pertinentes para el estudio, como se detalla en las secciones siguientes.

2.2 PREPROCESAMIENTO Y LIMPIEZA DE DATOS

De la base nacional, que contiene 7.109.704 registros, se conservaron únicamente las variables relevantes para el estudio y se filtraron los estudiantes cuyo colegio se ubica en el Valle del Cauca, lo que dejó 642.592 registros. Posteriormente se eliminaron los registros con datos faltantes en alguna de las variables seleccionadas, quedando 609.750 casos completos.

Para el muestreo se clasificó como alto rendimiento a los estudiantes con puntaje superior a 70 en matemáticas (criterio que se justifica en la sección de definición de variables). Solo el 4,1% de los estudiantes del Valle del Cauca alcanza este nivel, de modo que una muestra aleatoria simple habría generado una clase de alto rendimiento muy reducida y dificultado el entrenamiento de los modelos. Por ello se extrajo una muestra aleatoria estratificada por nivel de rendimiento, con 2.500 estudiantes de alto rendimiento y 2.500 de bajo rendimiento (5.000 en total), y dentro de cada estrato se mantuvo la proporción de colegios oficiales y no oficiales existente en la población. La muestra se extrajo de manera conjunta de los periodos 2010 a 2022, sin estratificar por año de aplicación.

Finalmente, la muestra se dividió, de forma estratificada por rendimiento, en un conjunto de entrenamiento (75%) y uno de prueba (25%), utilizados por igual en ambos modelos.

A continuación se presenta la base de datos final, ya filtrada, depurada y muestreada, que se utilizó para el análisis. Contiene 5.000 observaciones y 8 variables: la variable binaria de rendimiento, el puntaje de matemáticas a partir del cual se construyó, el departamento de ubicación del colegio y las cinco variables explicativas (naturaleza del colegio, género, estrato de la vivienda, y tenencia de computador e internet en el hogar).

datos_vista <- datos |>
  select(Rendimiento = rendimiento,
         Departamento = COLE_DEPTO_UBICACION,
         Naturaleza = COLE_NATURALEZA,
         Género = ESTU_GENERO,
         Estrato = FAMI_ESTRATOVIVIENDA,
         Computador = FAMI_TIENECOMPUTADOR,
         Internet = FAMI_TIENEINTERNET,
         `Puntaje Mat.` = PUNT_MATEMATICAS)

datatable(
  datos_vista,
  rownames = FALSE,
  class = "stripe hover compact",
  caption = tags$caption(
    style = "caption-side: top; text-align: center; font-size: 18px; font-weight: bold; color: #2E86AB; padding-bottom: 8px;",
    "Base de datos final: muestra de 5.000 estudiantes del Valle del Cauca"
  ),
  options = list(
    dom = "t", paging = FALSE, scrollY = "400px", scrollCollapse = TRUE, scrollX = TRUE,
    columnDefs = list(list(className = "dt-center", targets = "_all")),
    initComplete = JS(
      "function(settings, json) {",
      "  $(this.api().table().header()).css({'background-color': '#2E86AB', 'color': 'white'});",
      "}"
    )
  )
)

2.3 DEFINICIÓN DE VARIABLES

De las 51 variables disponibles en la base original se seleccionaron siete: el puntaje en matemáticas, el departamento de ubicación del colegio (utilizado para delimitar el análisis al Valle del Cauca) y cinco variables institucionales, demográficas, socioeconómicas y de acceso a tecnología. A partir del puntaje se construyó la variable binaria de rendimiento, de modo que la base final cuenta con ocho variables.

2.3.1 Variable dependiente

A partir del puntaje de matemáticas se construyó la variable binaria rendimiento, que toma el valor “Alto” cuando el puntaje es superior a 70 y “Bajo” en caso contrario. Este umbral corresponde al inicio del nivel de desempeño 4 definido por el ICFES para el área de matemáticas, que agrupa a los estudiantes con un desempeño alto (Instituto Colombiano para la Evaluación de la Educación, 2025). Clasificar a los estudiantes según este criterio permite identificar qué condiciones del hogar y de la institución se asocian con alcanzar un desempeño sobresaliente. La muestra contiene 2500 estudiantes de alto rendimiento y 2500 de bajo rendimiento. En la base de datos la variable se codificó como 1 (alto rendimiento) y 0 (bajo rendimiento); en las tablas y en los modelos se presenta con las etiquetas “Alto” y “Bajo”.

Variable Categorías Tipo de variable Justificación
Rendimiento Alto (puntaje > 70), Bajo (puntaje ≤ 70) Cualitativa binaria Variable dependiente. Se construyó a partir del puntaje de matemáticas.

2.3.2 Variables explicativas

Se seleccionaron cinco variables explicativas, todas categóricas, que describen características institucionales, demográficas, socioeconómicas y de acceso a tecnología:

Variable Categorías Tipo de variable Justificacion
Naturaleza Oficial, No oficial Cualitativa binaria Recoge diferencias institucionales en recursos, infraestructura y composición del estudiantado asociadas al desempeño (Genuine School, 2026).
Género Femenino, Masculino Cualitativa binaria Permite evaluar si la brecha de género en matemáticas, reportada en las pruebas Saber, se mantiene al considerar las demás condiciones (Acosta y Quintero Rueda, 2023).
Estrato Estratos 1 a 6 y Sin estrato Cualitativa Ordinal Es el indicador socioeconómico disponible y se relaciona con el acceso a recursos educativos (Valencia, 2026).
Computador Sí, No Cualitativa binaria Mide el acceso a una herramienta de estudio y de consulta de información (Junca Rodríguez, 2019).
Internet Sí, No Cualitativa binaria Mide el acceso a información y recursos educativos digitales (Cerquera Losada et al., 2025).

La categoría “Sin estrato” cuenta con solo 32 estudiantes en la muestra, por lo que sus resultados deben interpretarse con cautela.

2.3.3 Variables excluidas del modelo

Las variables Puntaje Mat. y Binaria no se incluyeron como explicativas, porque la variable dependiente se construye a partir del puntaje y su inclusión permitiría al modelo reproducirla directamente. Tampoco se incluyó el departamento de ubicación del colegio, dado que todos los registros pertenecen al Valle del Cauca y la variable no presenta variación.

2.4 MODELO DE REGRESIÓN LOGÍSTICA (LOGIT)

La regresión logística es un modelo estadístico para variables dependientes binarias, que estima la probabilidad de que una observación pertenezca a una de las dos categorías a partir de un conjunto de variables explicativas. En este análisis se modela la probabilidad de que un estudiante alcance un alto rendimiento en matemáticas a partir de variables sociodemográficas y de acceso a tecnología, mediante la siguiente función logística:

\[ P(Y=1)=\frac{1}{1+e^{-(\beta_0+\beta_1X_1+\beta_2X_2+\dots+\beta_kX_k)}} \]

donde \(P(Y=1)\) es la probabilidad de que el estudiante tenga alto rendimiento, \(X_1,\dots,X_k\) son las variables explicativas (naturaleza del colegio, género, estrato de la vivienda y tenencia de computador e internet en el hogar) y \(\beta_0,\dots,\beta_k\) son los coeficientes que el modelo estima. Como todas las variables son categóricas, el modelo las incluye como variables indicadoras, tomando como categoría de referencia colegio no oficial, género femenino, estrato 1 y ausencia de computador y de internet.

El modelo se entrenó con el conjunto de entrenamiento (75% de la muestra) y posteriormente se evaluó con el conjunto de prueba (25%), que el modelo no había visto. Los coeficientes se interpretan según su signo y su significancia estadística, con un nivel de 5%: un coeficiente positivo indica que esa categoría aumenta la probabilidad de alto rendimiento respecto a la categoría de referencia, y uno negativo indica que la disminuye. Dado que la muestra se seleccionó según el nivel de rendimiento, estos resultados sirven para comparar el efecto de las variables, pero las probabilidades estimadas no reflejan las de la población.

Además, se verificó que las variables explicativas no estuvieran demasiado relacionadas entre sí (multicolinealidad), mediante el factor de inflación de la varianza (VIF).

2.5 MODELO K-NEAREST NEIGHBORS (KNN)

KNN es un algoritmo de clasificación que no estima coeficientes. Para clasificar a un estudiante, busca en el conjunto de entrenamiento a los k estudiantes más parecidos a él, sus vecinos más cercanos, y le asigna la clase que predomina entre ellos. Se utilizaron las mismas cinco variables explicativas del modelo logit, convertidas en variables de valores 0 y 1 para poder medir qué tan parecidos son dos estudiantes.

Como el resultado depende de cuántos vecinos se consideren (el valor de k), este número se eligió probando varias opciones. Para ello se dividió el conjunto de entrenamiento en 10 partes: el modelo aprendió con 9 y se evaluó con la restante, y el proceso se repitió hasta que cada parte sirvió de evaluación (validación cruzada). Se probaron únicamente valores impares de k entre 1 y 49, ya que con un k par puede ocurrir un empate entre las dos clases, y en ese caso el modelo tendría que decidir al azar. Se escogió el valor de k que obtuvo el mayor porcentaje de aciertos en promedio. Así, el conjunto de prueba no interviene en esta decisión.

Finalmente, con el k elegido, el modelo se evaluó con el conjunto de prueba, que no intervino en el entrenamiento, y con el mismo conjunto se evaluó el modelo logit, de modo que ambos pueden compararse en igualdad de condiciones. Para ello se utilizó la matriz de confusión, que cuenta cuántos estudiantes fueron clasificados correcta e incorrectamente, y las siguientes métricas:

  • Exactitud (accuracy): porcentaje de estudiantes que el modelo clasificó correctamente, tanto de alto como de bajo rendimiento.
  • Intervalo de confianza del 95 %: rango en el que probablemente se encuentra la exactitud real del modelo.
  • Tasa de no información (NIR): exactitud que se lograría si el modelo predijera siempre la clase más frecuente; es la referencia mínima que el modelo debe superar.
  • Valor p [Exactitud > NIR]: evalúa si la exactitud del modelo es significativamente mayor que la tasa de no información.
  • Índice Kappa: mide cuánto acierta el modelo por encima de lo que se esperaría por azar.
  • Prueba de McNemar: evalúa si el modelo se equivoca más con una clase que con la otra.
  • Sensibilidad: capacidad del modelo para detectar a los estudiantes de alto rendimiento. Es el porcentaje de ellos que el modelo clasificó como tales.
  • Especificidad: capacidad del modelo para detectar a los estudiantes de bajo rendimiento. Es el porcentaje de ellos que el modelo clasificó como tales.
  • Valor predictivo positivo (precisión): confiabilidad de las predicciones de alto rendimiento. Es el porcentaje de estudiantes clasificados como de alto rendimiento que realmente lo son.
  • Valor predictivo negativo: confiabilidad de las predicciones de bajo rendimiento. Es el porcentaje de estudiantes clasificados como de bajo rendimiento que realmente lo son.
  • Prevalencia: proporción de estudiantes de alto rendimiento en el conjunto de prueba.
  • Tasa de detección: porcentaje del conjunto de prueba que es de alto rendimiento y que el modelo identificó correctamente.
  • Prevalencia de detección: porcentaje del conjunto de prueba que el modelo clasificó como de alto rendimiento.
  • Exactitud balanceada: promedio entre la sensibilidad y la especificidad.
  • AUC: mide qué tan bien el modelo distingue entre estudiantes de alto y bajo rendimiento; un valor de 0,5 equivale a clasificar al azar y uno cercano a 1 indica una buena distinción.
  • Odds ratio (OR): cuántas veces cambian las odds de alto rendimiento de una categoría frente a su categoría de referencia.
  • Índice de Youden: sensibilidad + especificidad − 1; el umbral que lo maximiza es el que se usó para clasificar a los estudiantes como de alto o bajo rendimiento.

3 RESULTADOS DESCRIPTIVOS

3.1 VARIABLE CUANTITATIVA: PUNTAJE EN MATEMÁTICAS

resumen <- datos |>
  summarise(n       = n(),
            media   = mean(PUNT_MATEMATICAS),
            desv    = sd(PUNT_MATEMATICAS),
            minimo  = min(PUNT_MATEMATICAS),
            Q1      = quantile(PUNT_MATEMATICAS, 0.25),
            mediana = median(PUNT_MATEMATICAS),
            Q3      = quantile(PUNT_MATEMATICAS, 0.75),
            maximo  = max(PUNT_MATEMATICAS))
 
datatable(
  resumen,
  colnames = c("n", "Media", "Desv. estándar", "Mínimo", "Q1", "Mediana", "Q3", "Máximo"),
  rownames = FALSE,
  class = "stripe hover compact",
  caption = tags$caption(
    style = "caption-side: top; text-align: center; font-size: 18px; font-weight: bold; color: #2E86AB; padding-bottom: 8px;",
    "Estadísticas descriptivas del puntaje de matemáticas"
  ),
  options = list(
    dom = "t", paging = FALSE, ordering = FALSE,
    columnDefs = list(list(className = "dt-center", targets = "_all")),
    initComplete = JS(
      "function(settings, json) {",
      "  $(this.api().table().header()).css({'background-color': '#2E86AB', 'color': 'white'});",
      "}"
    )
  )
) |>
  formatRound(columns = 1, digits = 0, mark = ".") |>
  formatRound(columns = 2:8, digits = 2, mark = ".", dec.mark = ",")

El puntaje promedio en matemáticas de los 5.000 estudiantes de la muestra es de 62,4 puntos, con una desviación estándar de 16,85. La mediana (70,5) es mayor que la media, lo que indica que la distribución se alarga hacia los puntajes bajos. El 50 % central de los estudiantes obtuvo entre 48 y 75 puntos, y los puntajes van de un mínimo de 1 a un máximo de 125. Es importante tener en cuenta que la muestra se construyó con 2.500 estudiantes de alto rendimiento y 2.500 de bajo rendimiento, por lo que estos indicadores describen la muestra y no a toda la población del Valle del Cauca.

g1 <- ggplot(datos, aes(x = PUNT_MATEMATICAS)) +
    geom_histogram(binwidth = 5, boundary = 70, fill = "steelblue", color = "white") +
  geom_vline(xintercept = 70, linetype = "dashed", color = "red") +
  labs(title = "Distribución del puntaje de matemáticas",
       x = "Puntaje", y = "Número de estudiantes") +
  theme_minimal()
ggplotly(g1)

El histograma evidencia dos concentraciones separadas por el umbral de 70 puntos. A su izquierda, los estudiantes de bajo rendimiento forman una distribución amplia, con mayor concentración entre 40 y 55 puntos. A su derecha, los de alto rendimiento se agrupan de forma pronunciada justo por encima del umbral: más de la mitad (1.387 de 2.500) obtuvo entre 70 y 75 puntos, y y la frecuencia disminuye hacia los puntajes más altos, salvo un pequeño repunte entre 95 y 100 puntos. Esta forma bimodal es consecuencia del muestreo estratificado por nivel de rendimiento y no refleja la distribución de los puntajes en la población.

g2 <- ggplot(datos, aes(y = PUNT_MATEMATICAS)) +
  geom_boxplot(fill = "steelblue") +
  labs(title = "Diagrama de caja del puntaje de matemáticas",
       y = "Puntaje") +
  theme_minimal() +
  theme(axis.text.x = element_blank())
ggplotly(g2)

El diagrama de caja confirma que el 50 % central de los estudiantes se ubica entre 48 y 75 puntos, con una mediana de 70,5 cercana al tercer cuartil, lo que indica mayor dispersión en la mitad inferior de los puntajes. Se identifican únicamente dos valores atípicos, uno de 1 punto y otro de 125, que corresponden a un estudiante cada uno. Dado que el diagrama resume los datos en una sola caja, no permite apreciar las dos concentraciones que muestra el histograma, producto del muestreo estratificado.

3.2 VARIABLES CATEGÓRICAS

Para cada variable categórica se presenta la distribución de los estudiantes y el porcentaje de alto rendimiento dentro de cada categoría. Dado que la muestra se construyó con la mitad de estudiantes de alto rendimiento y la mitad de bajo rendimiento, esos porcentajes sirven para comparar categorías entre sí, pero no son las probabilidades reales de la población.

tabla_cat <- function(var) {
  datos |>
    count(Categoría = .data[[var]], rendimiento) |>
    pivot_wider(names_from = rendimiento, values_from = n, values_fill = 0) |>
    mutate(Total = Alto + Bajo,
           `% del total` = round(100 * Total / sum(Total), 1),
           `% Alto en la categoría` = round(100 * Alto / Total, 1))
}

graf_cat <- function(var, titulo) {
  d <- datos |>
    count(Categoria = .data[[var]], rendimiento) |>
    group_by(Categoria) |>
    mutate(pct = round(100 * n / sum(n), 1)) |>
    ungroup()
  g <- ggplot(d, aes(x = Categoria, y = pct, fill = rendimiento,
                     text = paste0(rendimiento, ": ", pct, "% (", n, " estudiantes)"))) +
    geom_col() +
    scale_fill_manual(values = c(Bajo = "grey60", Alto = "steelblue")) +
    labs(title = titulo, x = NULL, y = "% de estudiantes", fill = "Rendimiento") +
    theme_minimal()
  ggplotly(g, tooltip = "text")
}

3.2.1 Naturaleza del colegio

kable(tabla_cat("COLE_NATURALEZA"), caption = "Rendimiento según la naturaleza del colegio")
Rendimiento según la naturaleza del colegio
Categoría Bajo Alto Total % del total % Alto en la categoría
NO OFICIAL 938 1721 2659 53.2 64.7
OFICIAL 1562 779 2341 46.8 33.3
graf_cat("COLE_NATURALEZA", "Rendimiento según la naturaleza del colegio")

En cuanto a la naturaleza del colegio, la muestra está compuesta por 2.659 estudiantes de colegios no oficiales (53,2 %) y 2.341 de colegios oficiales (46,8 %). El porcentaje de estudiantes con alto rendimiento es de 64,7 % en los colegios no oficiales y de 33,3 % en los oficiales, es decir, una diferencia de 31,4 puntos porcentuales a favor de los colegios no oficiales. Esto sugiere que la naturaleza del colegio se asocia con el rendimiento en matemáticas (Genuine School, 2026). Como la muestra se construyó con igual número de estudiantes de alto y bajo rendimiento, estos porcentajes sirven para comparar categorías entre sí, pero no corresponden a las proporciones reales del Valle del Cauca.

3.2.2 Género

kable(tabla_cat("ESTU_GENERO"), caption = "Rendimiento según el género")
Rendimiento según el género
Categoría Bajo Alto Total % del total % Alto en la categoría
F 1371 985 2356 47.1 41.8
M 1129 1515 2644 52.9 57.3
graf_cat("ESTU_GENERO", "Rendimiento según el género")

Respecto al género la muestra incluye 2.356 mujeres (47,1 %) y 2.644 hombres (52,9 %). El porcentaje de estudiantes con alto rendimiento es de 57,3 % entre los hombres y de 41,8 % entre las mujeres, una diferencia de 15,5 puntos porcentuales a favor de los hombres. Este resultado es coherente con la brecha de género en matemáticas reportada en las pruebas Saber (Acosta y Quintero Rueda, 2023). Como la muestra se construyó con igual número de estudiantes de alto y bajo rendimiento, estos porcentajes permiten comparar categorías, pero no corresponden a las proporciones reales del Valle del Cauca.

3.2.3 Estrato

kable(tabla_cat("FAMI_ESTRATOVIVIENDA"), caption = "Rendimiento según el estrato de la vivienda")
Rendimiento según el estrato de la vivienda
Categoría Bajo Alto Total % del total % Alto en la categoría
Estrato 1 739 215 954 19.1 22.5
Estrato 2 970 604 1574 31.5 38.4
Estrato 3 545 677 1222 24.4 55.4
Estrato 4 125 390 515 10.3 75.7
Estrato 5 73 365 438 8.8 83.3
Estrato 6 26 239 265 5.3 90.2
Sin Estrato 22 10 32 0.6 31.2
graf_cat("FAMI_ESTRATOVIVIENDA", "Rendimiento según el estrato de la vivienda")

En cuanto al estrato de la vivienda, la mayor parte de los estudiantes se concentra en los estratos 2 (31,5 %), 3 (24,4 %) y 1 (19,1 %), mientras que los estratos 4, 5 y 6 representan el 10,3 %, el 8,8 % y el 5,3 % de la muestra, respectivamente. El porcentaje de estudiantes con alto rendimiento aumenta de forma sostenida con el estrato: pasa de 22,5 % en el estrato 1 a 38,4 % en el 2, 55,4 % en el 3, 75,7 % en el 4, 83,3 % en el 5 y 90,2 % en el 6, una diferencia de 67,7 puntos porcentuales entre los extremos. Los 32 estudiantes sin estrato registran un 31,2 % de alto rendimiento, pero al ser tan pocos este valor debe interpretarse con cautela. Estos resultados son coherentes con la relación entre el nivel socioeconómico y el acceso a recursos educativos (Valencia, 2026). Como la muestra se construyó con igual número de estudiantes de alto y bajo rendimiento, estos porcentajes permiten comparar categorías, pero no corresponden a las proporciones reales del Valle del Cauca.

3.2.4 Computador en el hogar

kable(tabla_cat("FAMI_TIENECOMPUTADOR"), caption = "Rendimiento según la tenencia de computador en el hogar")
Rendimiento según la tenencia de computador en el hogar
Categoría Bajo Alto Total % del total % Alto en la categoría
No 862 273 1135 22.7 24.1
Si 1638 2227 3865 77.3 57.6
graf_cat("FAMI_TIENECOMPUTADOR", "Rendimiento según la tenencia de computador en el hogar")

En cuanto a la tenencia de computador en el hogar, 3.865 estudiantes (77,3 %) cuentan con uno y 1.135 (22,7 %) no. El porcentaje de estudiantes con alto rendimiento es de 57,6 % entre quienes tienen computador y de 24,1 % entre quienes no lo tienen, una diferencia de 33,5 puntos porcentuales. Esto sugiere que el acceso a esta herramienta de estudio se asocia con un mejor desempeño en matemáticas (Junca Rodríguez, 2019). Como la muestra se construyó con igual número de estudiantes de alto y bajo rendimiento, estos porcentajes permiten comparar categorías, pero no corresponden a las proporciones reales del Valle del Cauca.

3.2.5 Internet en el hogar

kable(tabla_cat("FAMI_TIENEINTERNET"), caption = "Rendimiento según la tenencia de internet en el hogar")
Rendimiento según la tenencia de internet en el hogar
Categoría Bajo Alto Total % del total % Alto en la categoría
No 1003 272 1275 25.5 21.3
Si 1497 2228 3725 74.5 59.8
graf_cat("FAMI_TIENEINTERNET", "Rendimiento según la tenencia de internet en el hogar")

Respecto al acceso a internet en el hogar, 3.725 estudiantes (74,5 %) cuentan con el servicio y 1.275 (25,5 %) no. El porcentaje de estudiantes con alto rendimiento es de 59,8 % entre quienes tienen internet y de 21,3 % entre quienes no lo tienen, una diferencia de 38,5 puntos porcentuales. Esto sugiere que el acceso a información y recursos educativos digitales se asocia con un mejor desempeño en matemáticas (Cerquera Losada et al., 2025). Como la muestra se construyó con igual número de estudiantes de alto y bajo rendimiento, estos porcentajes permiten comparar categorías, pero no corresponden a las proporciones reales del Valle del Cauca.

3.2.5.1 Relación entre computador e internet

tb_ci <- table(Computador = datos$FAMI_TIENECOMPUTADOR,
               Internet   = datos$FAMI_TIENEINTERNET)
 
pct_ci <- as.data.frame.matrix(round(100 * prop.table(tb_ci, 1), 1))
 
tabla_ci <- data.frame(
  Computador         = c("Sin computador", "Con computador"),
  `Sin internet (%)` = pct_ci[["No"]],
  `Con internet (%)` = pct_ci[["Si"]],
  check.names = FALSE
)
 
datatable(
  tabla_ci,
  rownames = FALSE,
  class = "stripe hover compact",
  caption = tags$caption(
    style = "caption-side: top; text-align: center; font-size: 18px; font-weight: bold; color: #2E86AB; padding-bottom: 8px;",
    "Porcentaje de estudiantes según tenencia de internet, por tenencia de computador"
  ),
  options = list(
    dom = "t", paging = FALSE, ordering = FALSE,
    columnDefs = list(list(className = "dt-center", targets = "_all")),
    initComplete = JS(
      "function(settings, json) {",
      "  $(this.api().table().header()).css({'background-color': '#2E86AB', 'color': 'white'});",
      "}"
    )
  )
)

Entre los estudiantes con computador, el 88,8 % también tiene internet, mientras que entre quienes no tienen computador solo el 25,8 % lo tiene. Esta relación entre las dos variables es la razón por la que se revisó la multicolinealidad mediante el VIF en el modelo logit.

4 Modelos de Clasificación

4.1 Modelo Logit

El modelo Logit (o regresión logística) es un método estadístico de clasificación que calcula la probabilidad de que un evento ocurra entre dos opciones (como tener rendimiento “Alto” o “Bajo”), transformando los datos a una escala de 0% a 100% mediante una función sigmoide.

A diferencia del KNN, el Logit construye un modelo matemático explícito: el logaritmo del odds (razón de probabilidades) se modela linealmente respecto a los predictores.

4.1.1 Coeficientes del modelo

library(knitr)
library(kableExtra)

coefs <- as.data.frame(summary(fit_logit)$coefficients)
colnames(coefs) <- c("Coeficiente", "EE", "z", "p")

etiquetas <- c(
  "(Intercept)"                     = "Intercepto",
  "COLE_NATURALEZAOFICIAL"          = "Colegio oficial (ref.: no oficial)",
  "ESTU_GENEROM"                    = "Género masculino (ref.: femenino)",
  "FAMI_ESTRATOVIVIENDAEstrato 2"   = "Estrato 2 (ref.: estrato 1)",
  "FAMI_ESTRATOVIVIENDAEstrato 3"   = "Estrato 3 (ref.: estrato 1)",
  "FAMI_ESTRATOVIVIENDAEstrato 4"   = "Estrato 4 (ref.: estrato 1)",
  "FAMI_ESTRATOVIVIENDAEstrato 5"   = "Estrato 5 (ref.: estrato 1)",
  "FAMI_ESTRATOVIVIENDAEstrato 6"   = "Estrato 6 (ref.: estrato 1)",
  "FAMI_ESTRATOVIVIENDASin Estrato" = "Sin estrato (ref.: estrato 1)",
  "FAMI_TIENECOMPUTADORSi"          = "Con computador (ref.: sin computador)",
  "FAMI_TIENEINTERNETSi"            = "Con internet (ref.: sin internet)"
)
nombres <- rownames(coefs)
nombre_vista <- ifelse(is.na(etiquetas[nombres]), nombres, unname(etiquetas[nombres]))

coefs_vista <- data.frame(
  Variable = nombre_vista,
  Coeficiente = cell_spec(fmt(coefs$Coeficiente, 3), format = "html", bold = TRUE,
                          color = ifelse(coefs$Coeficiente > 0, "#0A00D1", "#B03A2E")),
  `Odds ratio (OR)` = fmt(exp(coefs$Coeficiente), 3),
  `Error estándar` = fmt(coefs$EE, 3),
  z = fmt(coefs$z, 2),
  `Valor p` = ifelse(coefs$p < 0.001, "&lt; 0,001", fmt(coefs$p, 3)),
  `Significativo (5 %)` = cell_spec(
    ifelse(coefs$p < 0.05, "Sí", "No"), format = "html", bold = TRUE,
    color      = ifelse(coefs$p < 0.05, "#155724", "#6c757d"),
    background = ifelse(coefs$p < 0.05, "#d4edda", "#e9ecef"),
    extra_css  = "padding: 3px 12px; border-radius: 12px;"),
  check.names = FALSE
)

coefs_vista %>%
  kbl(
    caption = "<b>Coeficientes y odds ratio del modelo logit (variable respuesta: alto rendimiento)</b>",
    align = c("l", "c", "c", "c", "c", "c", "c"),          # <- 7 columnas
    escape = FALSE, row.names = FALSE
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, font_size = 13, position = "center") %>%
  row_spec(0, background = "#080344", color = "white", bold = TRUE) %>%
  column_spec(1, bold = TRUE, width = "6.5cm") %>%
  column_spec(2:7, width = "2.8cm", extra_css = "white-space: nowrap;") %>%   # <- 2:7
  footnote(
    general = "Coeficientes en escala logit. Un coeficiente positivo (azul) aumenta la probabilidad de alto rendimiento respecto a la categoría de referencia; uno negativo (rojo) la disminuye. El odds ratio (OR) es e^coeficiente: un OR > 1 aumenta las odds de alto rendimiento y un OR < 1 las disminuye, manteniendo constantes las demás variables.",
    general_title = "Nota:", footnote_as_chunk = TRUE)
Coeficientes y odds ratio del modelo logit (variable respuesta: alto rendimiento)
Variable Coeficiente Odds ratio (OR) Error estándar z Valor p Significativo (5 %)
Intercepto -1,897 0,150 0,141 -13,47 < 0,001 Sí
Colegio oficial (ref.: no oficial) -0,439 0,645 0,083 -5,27 < 0,001 Sí
Género masculino (ref.: femenino) 0,678 1,970 0,076 8,97 < 0,001 Sí
Estrato 2 (ref.: estrato 1) 0,570 1,768 0,115 4,97 < 0,001 Sí
Estrato 3 (ref.: estrato 1) 1,013 2,754 0,122 8,28 < 0,001 Sí
Estrato 4 (ref.: estrato 1) 1,714 5,551 0,165 10,42 < 0,001 Sí
Estrato 5 (ref.: estrato 1) 2,033 7,635 0,188 10,84 < 0,001 Sí
Estrato 6 (ref.: estrato 1) 2,801 16,469 0,268 10,45 < 0,001 Sí
Sin estrato (ref.: estrato 1) 0,350 1,419 0,462 0,76 0,449 No
Con computador (ref.: sin computador) 0,232 1,261 0,114 2,03 0,042 Sí
Con internet (ref.: sin internet) 0,867 2,380 0,110 7,85 < 0,001 Sí
Nota: Coeficientes en escala logit. Un coeficiente positivo (azul) aumenta la probabilidad de alto rendimiento respecto a la categoría de referencia; uno negativo (rojo) la disminuye. El odds ratio (OR) es e^coeficiente: un OR > 1 aumenta las odds de alto rendimiento y un OR < 1 las disminuye, manteniendo constantes las demás variables.
n_sig  <- sum(coefs$p[-1] < 0.05)
n_coef <- nrow(coefs) - 1

De los 10 coeficientes asociados a las variables explicativas, 9 son estadísticamente significativos al 5 %. Un coeficiente positivo indica que esa categoría aumenta la probabilidad de alto rendimiento respecto a la categoría de referencia, y uno negativo indica que la disminuye.

Con un nivel de significancia del 5 %, todas las variables explicativas se asocian con la probabilidad de alto rendimiento en matemáticas, excepto la categoría “sin estrato”. Los estudiantes de colegios oficiales tienen 35,5 % menos odds de alto rendimiento que los de colegios no oficiales (OR = 0,645), y los hombres tienen casi el doble de odds que las mujeres (OR = 1,970). El estrato es la variable de mayor efecto: las odds aumentan de forma sostenida, desde 1,77 veces en el estrato 2 hasta 16,46 veces en el estrato 6, respecto al estrato 1. Contar con internet multiplica las odds por 2,38 y contar con computador por 1,26, aunque este último efecto es más débil (p = 0,042). Todos los efectos se interpretan manteniendo constantes las demás variables. La categoría “sin estrato” no resultó significativa (p = 0,449), y cuenta con solo 32 estudiantes.

4.1.2 Multicolinealidad

library(knitr)
library(kableExtra)

v <- car::vif(fit_logit)

etq_vars <- c(COLE_NATURALEZA      = "Naturaleza del colegio",
              ESTU_GENERO          = "Género",
              FAMI_ESTRATOVIVIENDA = "Estrato",
              FAMI_TIENECOMPUTADOR = "Computador",
              FAMI_TIENEINTERNET   = "Internet")

gvif_aj    <- v[, "GVIF^(1/(2*Df))"]
umbral_vif <- sqrt(5)   # 2,24: equivale a un VIF de 5
ok         <- gvif_aj < umbral_vif

vif_tabla <- data.frame(
  Variable = unname(etq_vars[rownames(v)]),
  GVIF     = fmt(v[, "GVIF"], 2),
  `Grados de libertad` = fmt(v[, "Df"], 0),
  # GVIF ajustado en verde si está por debajo del umbral y en rojo si lo supera
  `GVIF ajustado` = cell_spec(fmt(gvif_aj, 2), format = "html", bold = TRUE,
                              color = ifelse(ok, "#155724", "#B03A2E")),
  Diagnóstico = cell_spec(
    ifelse(ok, "Sin problema", "Revisar"), format = "html", bold = TRUE,
    color      = ifelse(ok, "#155724", "#721c24"),
    background = ifelse(ok, "#d4edda", "#f8d7da"),
    extra_css  = "padding: 3px 12px; border-radius: 12px;"),
  check.names = FALSE, row.names = NULL
)

vif_tabla %>%
  kbl(
    caption = "<b>Factor de inflación de la varianza generalizado (GVIF)</b>",
    align = c("l", "c", "c", "c", "c"),
    escape = FALSE, row.names = FALSE
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, font_size = 13, position = "center") %>%
  row_spec(0, background = "#080344", color = "white", bold = TRUE) %>%
  column_spec(1, bold = TRUE, width = "5cm") %>%
  column_spec(2:5, width = "3cm", extra_css = "white-space: nowrap;") %>%
  footnote(
    general = "GVIF ajustado = GVIF^(1/(2·gl)). Se considera problemático un valor superior a 2,24, equivalente a un VIF superior a 5.",
    general_title = "Nota:", footnote_as_chunk = TRUE)
Factor de inflación de la varianza generalizado (GVIF)
Variable GVIF Grados de libertad GVIF ajustado Diagnóstico
Naturaleza del colegio 1,23 1 1,11 Sin problema
Género 1,01 1 1,01 Sin problema
Estrato 1,32 6 1,02 Sin problema
Computador 1,47 1 1,21 Sin problema
Internet 1,47 1 1,21 Sin problema
Nota: GVIF ajustado = GVIF^(1/(2·gl)). Se considera problemático un valor superior a 2,24, equivalente a un VIF superior a 5.

Se verificó la multicolinealidad entre las variables explicativas mediante el factor de inflación de la varianza generalizado (GVIF), que se usa cuando hay variables categóricas con más de dos categorías. Los valores ajustados por grados de libertad oscilaron entre 1,01 y 1,21, muy por debajo del umbral de 2,24 (equivalente a un VIF de 5), por lo que no se detectaron problemas de multicolinealidad. Los valores más altos correspondieron a la tenencia de computador y de internet (1,21), coherente con la relación observada entre ambas variables en el análisis descriptivo, aunque sin alcanzar niveles preocupantes. En consecuencia, las cinco variables se mantuvieron en el modelo y sus coeficientes pueden interpretarse de forma confiable.

4.1.3 Curva ROC

library(plotly)

SC <- c(primary_blue = "#080344", dark_blue = "#030118", light_blue = "#6057FF",
        secondary_blue = "#0A00D1", gold = "#D4AF37", parchment = "#FDFBF7")

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

punto_idx <- which.min(abs(roc_logit$thresholds - umbral_logit))
punto_x   <- 1 - roc_logit$specificities[punto_idx]
punto_y   <- roc_logit$sensitivities[punto_idx]

Curva_ROC_Logit <- plot_ly(roc_data_logit, type = "scatter", mode = "lines") %>%
  add_trace(
    x = ~FalsosPositivos, y = ~Sensibilidad,
    fill = "tozeroy", fillcolor = "rgba(96, 87, 255, 0.15)",
    line = list(color = SC[["primary_blue"]], width = 3),
    name = "Curva ROC", hoverinfo = "text",
    text = ~paste0("<b>1 - Especificidad:</b> ", round(FalsosPositivos, 3),
                   "<br><b>Sensibilidad:</b> ", round(Sensibilidad, 3))
  ) %>%
  add_trace(
    x = c(0, 1), y = c(0, 1), type = "scatter", mode = "lines",
    line = list(color = "#888888", dash = "dash", width = 1.5),
    name = "Línea de azar (AUC = 0,5)", hoverinfo = "none"
  ) %>%
  add_trace(
    x = punto_x, y = punto_y, type = "scatter", mode = "markers",
    marker = list(color = SC[["gold"]], size = 10,
                  line = list(color = SC[["dark_blue"]], width = 2)),
    name = "Umbral óptimo (Youden)", hoverinfo = "text",
    text = paste0("<b>Umbral óptimo:</b> ", round(umbral_logit, 3),
                  "<br><b>1 - Especificidad:</b> ", round(punto_x, 3),
                  "<br><b>Sensibilidad:</b> ", round(punto_y, 3))
  ) %>%
  layout(
    title = list(text = "<b>Curva ROC del modelo de regresión logística (logit)</b>",
                 x = 0, xanchor = "left",
                 font = list(size = 17, color = SC[["primary_blue"]])),
    xaxis = list(title = "1 - Especificidad (tasa de falsos positivos)",
                 range = c(0, 1), zeroline = FALSE, gridcolor = "#F0F0F0"),
    yaxis = list(title = "Sensibilidad (tasa de verdaderos positivos)",
                 range = c(0, 1), zeroline = FALSE, gridcolor = "#F0F0F0"),
    showlegend = TRUE,
    legend = list(x = 0.52, y = 0.12, bgcolor = "rgba(255, 255, 255, 0.9)",
                  bordercolor = SC[["light_blue"]], borderwidth = 1),
    plot_bgcolor = SC[["parchment"]], paper_bgcolor = "white",
    margin = list(l = 80, r = 50, t = 80, b = 110),
    annotations = list(
      list(x = 0.65, y = 0.32, xref = "x", yref = "y",
           text = paste0("<b>Modelo logit:</b><br>• AUC: ", fmt(auc_logit, 3),
                         "<br>• Umbral óptimo (Youden): ", fmt(umbral_logit, 3)),
           showarrow = FALSE, align = "left",
           font = list(size = 11, color = SC[["dark_blue"]]),
           bgcolor = "rgba(255, 255, 255, 0.95)", bordercolor = SC[["gold"]],
           borderwidth = 1.5, borderpad = 6),
      list(x = 1, y = -0.28, xref = "paper", yref = "paper", xanchor = "right", yanchor = "top",
           text = "Fuente: Elaboración propia con base en el dataset del examen Saber 11",
           showarrow = FALSE, font = list(size = 10, color = SC[["secondary_blue"]]))
    )
  ) %>%
  config(displayModeBar = TRUE, displaylogo = FALSE, responsive = TRUE)

Curva_ROC_Logit

El umbral de clasificación que maximiza al mismo tiempo la sensibilidad y la especificidad (índice de Youden) fue de 0,448: los estudiantes con una probabilidad estimada igual o superior a este valor se clasifican como de alto rendimiento. El área bajo la curva (AUC) del modelo es de 0,768. Un AUC de 0,5 equivale a clasificar al azar y uno cercano a 1 indica una distinción casi perfecta, por lo que el valor obtenido refleja una capacidad moderada del modelo para distinguir entre estudiantes de alto y bajo rendimiento a partir de las condiciones institucionales, socioeconómicas y de acceso a tecnología.

4.1.4 Métricas de evaluación

library(knitr)
library(kableExtra)
library(scales)

SC2 <- c(primary_blue = "#080344")

pc <- function(x) scales::percent(x, accuracy = 0.1, decimal.mark = ",")
fp <- function(p) ifelse(is.na(p), "N/A", ifelse(p < 0.001, "< 0,001", fmt(p, 3)))

acc    <- cm_logit$overall[["Accuracy"]]
acc_lo <- cm_logit$overall[["AccuracyLower"]]
acc_hi <- cm_logit$overall[["AccuracyUpper"]]
nir    <- cm_logit$overall[["AccuracyNull"]]
kappa  <- cm_logit$overall[["Kappa"]]
sens   <- cm_logit$byClass[["Sensitivity"]]
spec   <- cm_logit$byClass[["Specificity"]]
vpp    <- cm_logit$byClass[["Pos Pred Value"]]
vpn    <- cm_logit$byClass[["Neg Pred Value"]]
prev   <- cm_logit$byClass[["Prevalence"]]
tdet   <- cm_logit$byClass[["Detection Rate"]]
pdet   <- cm_logit$byClass[["Detection Prevalence"]]
bacc   <- cm_logit$byClass[["Balanced Accuracy"]]

tabla_logit <- data.frame(
  Métrica = c(
    "Exactitud",
    "Intervalo de confianza (95 %)",
    "Tasa de no información (NIR)",
    "Valor p [Exactitud > NIR]",
    "Índice Kappa",
    "Valor p de la prueba de McNemar",
    "Sensibilidad",
    "Especificidad",
    "Valor predictivo positivo (precisión)",
    "Valor predictivo negativo",
    "Prevalencia",
    "Tasa de detección",
    "Prevalencia de detección",
    "Exactitud balanceada",
    "AUC"
  ),
  Valor = c(
    fmt(acc, 3),
    paste0("(", fmt(acc_lo, 3), "; ", fmt(acc_hi, 3), ")"),
    fmt(nir, 3),
    fp(cm_logit$overall[["AccuracyPValue"]]),
    fmt(kappa, 3),
    fp(cm_logit$overall[["McnemarPValue"]]),
    fmt(sens, 3),
    fmt(spec, 3),
    fmt(vpp, 3),
    fmt(vpn, 3),
    fmt(prev, 3),
    fmt(tdet, 3),
    fmt(pdet, 3),
    fmt(bacc, 3),
    fmt(auc_logit, 3)
  ),
  Interpretación = c(
    paste0("El modelo clasifica correctamente al ", pc(acc), " de los estudiantes del conjunto de prueba."),
    paste0("La exactitud real del modelo se ubica, con un 95 % de confianza, entre ", pc(acc_lo), " y ", pc(acc_hi), "."),
    paste0("Exactitud que se lograría prediciendo siempre la clase más frecuente (", pc(nir), ")."),
    "Evalúa si la exactitud del modelo es significativamente mayor que la tasa de no información.",
    paste0("Acierto por encima del esperado por azar (Kappa = ", fmt(kappa, 3), ")."),
    "Evalúa si el modelo se equivoca más con una clase que con la otra.",
    paste0("El modelo identifica al ", pc(sens), " de los estudiantes de alto rendimiento."),
    paste0("El modelo identifica al ", pc(spec), " de los estudiantes de bajo rendimiento."),
    paste0("Cuando el modelo predice alto rendimiento, acierta el ", pc(vpp), " de las veces."),
    paste0("Cuando el modelo predice bajo rendimiento, acierta el ", pc(vpn), " de las veces."),
    paste0("Proporción de estudiantes de alto rendimiento en el conjunto de prueba (", pc(prev), ")."),
    paste0("Porcentaje del conjunto de prueba que es de alto rendimiento y fue detectado como tal (", pc(tdet), ")."),
    paste0("Porcentaje del conjunto de prueba que el modelo clasifica como de alto rendimiento (", pc(pdet), ")."),
    paste0("Promedio entre la sensibilidad y la especificidad (", pc(bacc), ")."),
    paste0("Probabilidad de que el modelo asigne mayor probabilidad de alto rendimiento a un estudiante de alto rendimiento que a uno de bajo rendimiento (AUC = ", fmt(auc_logit, 3), ").")
  )
)

tabla_logit %>%
  kbl(
    caption = "<b>Métricas de evaluación del modelo logit en el conjunto de prueba</b>",
    align = c("l", "c", "l"),
    col.names = c("Métrica", "Valor", "Interpretación"),
    escape = FALSE
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, font_size = 13, position = "center") %>%
  row_spec(0, background = SC2[["primary_blue"]], color = "white", bold = TRUE) %>%
  column_spec(1, bold = TRUE, width = "4cm") %>%
  column_spec(2, width = "3cm") %>%
  column_spec(3, width = "10cm") %>%
  footnote(general = "Elaboración propia con base en el 25 % de prueba del examen Saber 11.",
           general_title = "Nota:", footnote_as_chunk = TRUE)
Métricas de evaluación del modelo logit en el conjunto de prueba
Métrica Valor Interpretación
Exactitud 0,701 El modelo clasifica correctamente al 70,1% de los estudiantes del conjunto de prueba.
Intervalo de confianza (95 %) (0,675; 0,726) La exactitud real del modelo se ubica, con un 95 % de confianza, entre 67,5% y 72,6%.
Tasa de no información (NIR) 0,500 Exactitud que se lograría prediciendo siempre la clase más frecuente (50,0%).
Valor p [Exactitud > NIR] < 0,001 Evalúa si la exactitud del modelo es significativamente mayor que la tasa de no información.
Índice Kappa 0,402 Acierto por encima del esperado por azar (Kappa = 0,402).
Valor p de la prueba de McNemar 0,008 Evalúa si el modelo se equivoca más con una clase que con la otra.
Sensibilidad 0,742 El modelo identifica al 74,2% de los estudiantes de alto rendimiento.
Especificidad 0,659 El modelo identifica al 65,9% de los estudiantes de bajo rendimiento.
Valor predictivo positivo (precisión) 0,685 Cuando el modelo predice alto rendimiento, acierta el 68,5% de las veces.
Valor predictivo negativo 0,719 Cuando el modelo predice bajo rendimiento, acierta el 71,9% de las veces.
Prevalencia 0,500 Proporción de estudiantes de alto rendimiento en el conjunto de prueba (50,0%).
Tasa de detección 0,371 Porcentaje del conjunto de prueba que es de alto rendimiento y fue detectado como tal (37,1%).
Prevalencia de detección 0,542 Porcentaje del conjunto de prueba que el modelo clasifica como de alto rendimiento (54,2%).
Exactitud balanceada 0,701 Promedio entre la sensibilidad y la especificidad (70,1%).
AUC 0,768 Probabilidad de que el modelo asigne mayor probabilidad de alto rendimiento a un estudiante de alto rendimiento que a uno de bajo rendimiento (AUC = 0,768).
Nota: Elaboración propia con base en el 25 % de prueba del examen Saber 11.

Con el conjunto de prueba, el modelo logit alcanzó una exactitud de 70,1 %, con una sensibilidad de 74,2 %, una especificidad de 65,9 % y un AUC de 0,768. Estos valores indican una capacidad moderada para distinguir entre estudiantes de alto y bajo rendimiento, por encima del 50 % que se obtendría al clasificar al azar en una muestra balanceada.

4.2 Modelo KNN

El modelo K-Nearest Neighbors (KNN) o tambien llamado el modelo K vecinos más cercanos es un algoritmo de clasificacion basico y esencial en Machine Learning. Pertenece al dominio del aprendizaje supervisado y encuentra una aplicación intensa en el reconocimiento de patrones, la minería de datos y la detección de intrusos.

4.2.1 Modelo KNN (caret)

# Formato en español (por si no se ha definido antes)
if (!exists("fmt")) fmt <- function(x, d = 1) formatC(x, format = "f", digits = d, decimal.mark = ",", big.mark = ".")

# Datos de entrenamiento (75 %) y prueba (25 %) definidos en el chunk cargar-base
train_clean <- na.omit(train)
test_clean  <- na.omit(test)
train_clean$rendimiento <- factor(train_clean$rendimiento, levels = c("Bajo", "Alto"))
test_clean$rendimiento  <- factor(test_clean$rendimiento,  levels = c("Bajo", "Alto"))

# Validación cruzada de 10 pliegues dentro del conjunto de entrenamiento.
# Sin indicar métrica, caret elige el k con la mayor exactitud promedio.
set.seed(2026)
ctrl_cv <- trainControl(method = "cv", number = 10)
k_grid  <- expand.grid(k = seq(1, 49, by = 2))   # solo valores impares

fit_knn <- train(
  rendimiento ~ COLE_NATURALEZA + ESTU_GENERO + FAMI_ESTRATOVIVIENDA +
                FAMI_TIENECOMPUTADOR + FAMI_TIENEINTERNET,
  data      = train_clean,
  method    = "knn",
  trControl = ctrl_cv,
  tuneGrid  = k_grid
)

k_optimo     <- fit_knn$bestTune$k
exactitud_cv <- max(fit_knn$results$Accuracy)

# Evaluación final con el conjunto de prueba
p_hat_knn <- predict(fit_knn, newdata = test_clean, type = "prob")[["Alto"]]

roc_knn <- pROC::roc(response  = test_clean$rendimiento,
                     predictor = p_hat_knn,
                     levels    = c("Bajo", "Alto"),
                     direction = "<")

# Umbral de clasificación que maximiza sensibilidad y especificidad (índice de Youden)
umbral_knn <- as.numeric(unlist(
  pROC::coords(roc_knn, x = "best", best.method = "youden", ret = "threshold")))[1]

pred_clase_knn <- factor(ifelse(p_hat_knn >= umbral_knn, "Alto", "Bajo"),
                         levels = c("Bajo", "Alto"))

cm_knn  <- confusionMatrix(pred_clase_knn, test_clean$rendimiento, positive = "Alto")
auc_knn <- as.numeric(pROC::auc(roc_knn))

cat("k óptimo (validación cruzada):", k_optimo, "\n")
## k óptimo (validación cruzada): 39
cat("Exactitud en validación cruzada:", fmt(100 * exactitud_cv, 2), "%\n")
## Exactitud en validación cruzada: 71,52 %
cat("Exactitud en el conjunto de prueba:", fmt(100 * cm_knn$overall[["Accuracy"]], 2), "%\n")
## Exactitud en el conjunto de prueba: 70,00 %
library(plotly)
library(scales)

resultado <- data.frame(k = fit_knn$results$k, exactitud = fit_knn$results$Accuracy)

y_min <- max(0.50, min(resultado$exactitud) - 0.01)
y_max <- min(1.00, max(resultado$exactitud) + 0.01)

Grafico_KNN <- plot_ly(resultado, x = ~k) %>%
  add_lines(
    y = ~exactitud,
    line = list(color = "#080344", width = 3, shape = "spline"),
    hovertemplate = "k = %{x}<br>Exactitud = %{y:.3f}<extra></extra>"
  ) %>%
  add_markers(
    y = ~exactitud,
    marker = list(size = 8, color = "#080344",
                  line = list(color = "white", width = 1.5), opacity = 0.9),
    hoverinfo = "text",
    text = ~paste0("<b>k = ", k, "</b><br>Exactitud: ",
                   scales::percent(exactitud, accuracy = 0.1, decimal.mark = ",")),
    showlegend = FALSE
  ) %>%
  add_segments(
    x = k_optimo, xend = k_optimo,
    y = y_min, yend = max(resultado$exactitud),
    line = list(color = "#D4AF37", width = 2.5, dash = "dash"),
    showlegend = FALSE, hoverinfo = "none"
  ) %>%
  layout(
    title = list(text = "<b>Exactitud del modelo KNN según el número de vecinos (k)</b>",
                 font = list(size = 17, color = "#080344"), x = 0, xanchor = "left"),
    xaxis = list(title = "Número de vecinos (k)", gridcolor = "#F0F0F0", dtick = 4),
    yaxis = list(title = "Exactitud (validación cruzada)", gridcolor = "#F0F0F0",
                 range = c(y_min, y_max), tickformat = ".1%"),
    plot_bgcolor = "#FDFBF7", paper_bgcolor = "white",
    showlegend = FALSE,
    margin = list(l = 80, r = 50, t = 80, b = 110),
    annotations = list(
      list(x = k_optimo, y = max(resultado$exactitud) + 0.002,
           text = paste("<b>k óptimo =", k_optimo, "</b>"), showarrow = FALSE,
           font = list(size = 11, color = "#030118"), bgcolor = "#D4AF37",
           bordercolor = "#030118", borderwidth = 1.5, borderpad = 4),
      list(x = 0.98, y = 0.08, xref = "paper", yref = "paper",
           xanchor = "right", yanchor = "bottom",
           text = paste0("<b>Resumen:</b><br>• Máxima exactitud: ",
                         scales::percent(max(resultado$exactitud), accuracy = 0.1, decimal.mark = ","),
                         "<br>• Mejor k: ", k_optimo,
                         "<br>• Rango evaluado: ", min(resultado$k), " a ", max(resultado$k)),
           showarrow = FALSE, align = "left", font = list(size = 11, color = "#333333"),
           bgcolor = "rgba(255,255,255,0.9)", bordercolor = "#968FFA", borderwidth = 1, borderpad = 6),
      list(x = 1, y = -0.28, xref = "paper", yref = "paper", xanchor = "right", yanchor = "top",
           text = "Fuente: Elaboración propia con base en el dataset del examen Saber 11",
           showarrow = FALSE, font = list(size = 10, color = "#0A00D1"))
    )
  ) %>%
  config(displayModeBar = TRUE, displaylogo = FALSE, responsive = TRUE)

Grafico_KNN

Mediante validación cruzada de 10 pliegues se seleccionó k = 39, con una exactitud promedio de 71,5 % en el conjunto de entrenamiento. La curva es muy plana: la exactitud varía entre 70,5 % y 71,5 % a lo largo de todos los valores de k evaluados, por lo que el desempeño del modelo es poco sensible al número de vecinos.

4.2.2 Métricas de evaluación

library(knitr)
library(kableExtra)
library(scales)

SC <- c(primary_blue = "#080344", parchment = "#FDFBF7")

pc <- function(x) scales::percent(x, accuracy = 0.1, decimal.mark = ",")
fp <- function(p) ifelse(is.na(p), "N/A", ifelse(p < 0.001, "< 0,001", fmt(p, 3)))

acc    <- cm_knn$overall[["Accuracy"]]
acc_lo <- cm_knn$overall[["AccuracyLower"]]
acc_hi <- cm_knn$overall[["AccuracyUpper"]]
nir    <- cm_knn$overall[["AccuracyNull"]]
kappa  <- cm_knn$overall[["Kappa"]]
sens   <- cm_knn$byClass[["Sensitivity"]]
spec   <- cm_knn$byClass[["Specificity"]]
vpp    <- cm_knn$byClass[["Pos Pred Value"]]
vpn    <- cm_knn$byClass[["Neg Pred Value"]]
prev   <- cm_knn$byClass[["Prevalence"]]
tdet   <- cm_knn$byClass[["Detection Rate"]]
pdet   <- cm_knn$byClass[["Detection Prevalence"]]
bacc   <- cm_knn$byClass[["Balanced Accuracy"]]

tabla_knn <- data.frame(
  Métrica = c(
    "Exactitud",
    "Intervalo de confianza (95 %)",
    "Tasa de no información (NIR)",
    "Valor p [Exactitud > NIR]",
    "Índice Kappa",
    "Valor p de la prueba de McNemar",
    "Sensibilidad",
    "Especificidad",
    "Valor predictivo positivo (precisión)",
    "Valor predictivo negativo",
    "Prevalencia",
    "Tasa de detección",
    "Prevalencia de detección",
    "Exactitud balanceada",
    "AUC"
  ),
  Valor = c(
    fmt(acc, 3),
    paste0("(", fmt(acc_lo, 3), "; ", fmt(acc_hi, 3), ")"),
    fmt(nir, 3),
    fp(cm_knn$overall[["AccuracyPValue"]]),
    fmt(kappa, 3),
    fp(cm_knn$overall[["McnemarPValue"]]),
    fmt(sens, 3),
    fmt(spec, 3),
    fmt(vpp, 3),
    fmt(vpn, 3),
    fmt(prev, 3),
    fmt(tdet, 3),
    fmt(pdet, 3),
    fmt(bacc, 3),
    fmt(auc_knn, 3)
  ),
  Interpretación = c(
    paste0("El modelo clasifica correctamente al ", pc(acc), " de los estudiantes del conjunto de prueba."),
    paste0("La exactitud real del modelo se ubica, con un 95 % de confianza, entre ", pc(acc_lo), " y ", pc(acc_hi), "."),
    paste0("Exactitud que se lograría prediciendo siempre la clase más frecuente (", pc(nir), ")."),
    "Evalúa si la exactitud del modelo es significativamente mayor que la tasa de no información.",
    paste0("Acierto por encima del esperado por azar (Kappa = ", fmt(kappa, 3), ")."),
    "Evalúa si el modelo se equivoca más con una clase que con la otra.",
    paste0("El modelo identifica al ", pc(sens), " de los estudiantes de alto rendimiento."),
    paste0("El modelo identifica al ", pc(spec), " de los estudiantes de bajo rendimiento."),
    paste0("Cuando el modelo predice alto rendimiento, acierta el ", pc(vpp), " de las veces."),
    paste0("Cuando el modelo predice bajo rendimiento, acierta el ", pc(vpn), " de las veces."),
    paste0("Proporción de estudiantes de alto rendimiento en el conjunto de prueba (", pc(prev), ")."),
    paste0("Porcentaje del conjunto de prueba que es de alto rendimiento y fue detectado como tal (", pc(tdet), ")."),
    paste0("Porcentaje del conjunto de prueba que el modelo clasifica como de alto rendimiento (", pc(pdet), ")."),
    paste0("Promedio entre la sensibilidad y la especificidad (", pc(bacc), ")."),
    paste0("Probabilidad de que el modelo asigne mayor probabilidad de alto rendimiento a un estudiante de alto rendimiento que a uno de bajo rendimiento (AUC = ", fmt(auc_knn, 3), ").")
  )
)

tabla_knn %>%
  kbl(
    caption = "<b>Métricas de evaluación del modelo KNN en el conjunto de prueba</b>",
    align = c("l", "c", "l"),
    col.names = c("Métrica", "Valor", "Interpretación"),
    escape = FALSE
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, font_size = 13, position = "center") %>%
  row_spec(0, background = SC[["primary_blue"]], color = "white", bold = TRUE) %>%
  column_spec(1, bold = TRUE, width = "4cm") %>%
  column_spec(2, width = "3cm") %>%
  column_spec(3, width = "10cm") %>%
  footnote(general = "Elaboración propia con base en el 25 % de prueba del examen Saber 11.",
           general_title = "Nota:", footnote_as_chunk = TRUE)
Métricas de evaluación del modelo KNN en el conjunto de prueba
Métrica Valor Interpretación
Exactitud 0,700 El modelo clasifica correctamente al 70,0% de los estudiantes del conjunto de prueba.
Intervalo de confianza (95 %) (0,674; 0,725) La exactitud real del modelo se ubica, con un 95 % de confianza, entre 67,4% y 72,5%.
Tasa de no información (NIR) 0,500 Exactitud que se lograría prediciendo siempre la clase más frecuente (50,0%).
Valor p [Exactitud > NIR] < 0,001 Evalúa si la exactitud del modelo es significativamente mayor que la tasa de no información.
Índice Kappa 0,400 Acierto por encima del esperado por azar (Kappa = 0,400).
Valor p de la prueba de McNemar 0,013 Evalúa si el modelo se equivoca más con una clase que con la otra.
Sensibilidad 0,739 El modelo identifica al 73,9% de los estudiantes de alto rendimiento.
Especificidad 0,661 El modelo identifica al 66,1% de los estudiantes de bajo rendimiento.
Valor predictivo positivo (precisión) 0,685 Cuando el modelo predice alto rendimiento, acierta el 68,5% de las veces.
Valor predictivo negativo 0,717 Cuando el modelo predice bajo rendimiento, acierta el 71,7% de las veces.
Prevalencia 0,500 Proporción de estudiantes de alto rendimiento en el conjunto de prueba (50,0%).
Tasa de detección 0,370 Porcentaje del conjunto de prueba que es de alto rendimiento y fue detectado como tal (37,0%).
Prevalencia de detección 0,539 Porcentaje del conjunto de prueba que el modelo clasifica como de alto rendimiento (53,9%).
Exactitud balanceada 0,700 Promedio entre la sensibilidad y la especificidad (70,0%).
AUC 0,765 Probabilidad de que el modelo asigne mayor probabilidad de alto rendimiento a un estudiante de alto rendimiento que a uno de bajo rendimiento (AUC = 0,765).
Nota: Elaboración propia con base en el 25 % de prueba del examen Saber 11.

Con el conjunto de prueba, el modelo KNN alcanzó una exactitud de 70,0 %, con una sensibilidad de 73,9 %, una especificidad de 66,1 % y un AUC de 0,765. Estos valores indican una capacidad moderada para distinguir entre estudiantes de alto y bajo rendimiento a partir de variables categóricas, por encima del 50 % que se obtendría al clasificar al azar en una muestra balanceada.

4.3 COMPARACIÓN DE LOS MODELOS

4.3.1 Métricas de evaluación

library(knitr)
library(kableExtra)

# El mejor valor de cada fila se resalta en verde
resalta <- function(x, mejor) {
  cell_spec(fmt(x, 3), format = "html", bold = mejor,
            color      = ifelse(mejor, "#155724", "#333333"),
            background = ifelse(mejor, "#d4edda", "transparent"),
            extra_css  = "padding: 3px 12px; border-radius: 12px;")
}

tabla_comp <- data.frame(
  Métrica = nombres_met,
  Logit   = resalta(m_lg, mejor_lg),
  KNN     = resalta(m_kn, mejor_kn),
  `Mejor modelo` = ifelse(mejor_lg, "Logit", ifelse(mejor_kn, "KNN", "Empate")),
  check.names = FALSE
)

tabla_comp %>%
  kbl(
    caption = "<b>Comparación de métricas de los modelos logit y KNN en el conjunto de prueba</b>",
    align = c("l", "c", "c", "c"),
    escape = FALSE, row.names = FALSE
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, font_size = 13, position = "center") %>%
  row_spec(0, background = "#080344", color = "white", bold = TRUE) %>%
  column_spec(1, bold = TRUE, width = "7cm") %>%
  column_spec(2:4, width = "3cm", extra_css = "white-space: nowrap;") %>%
  footnote(general = "El mejor valor de cada métrica está resaltado en verde. Ambos modelos se evaluaron con el mismo 25 % de prueba del examen Saber 11.",
           general_title = "Nota:", footnote_as_chunk = TRUE)
Comparación de métricas de los modelos logit y KNN en el conjunto de prueba
Métrica Logit KNN Mejor modelo
Exactitud 0,701 0,700 Logit
Índice Kappa 0,402 0,400 Logit
Sensibilidad 0,742 0,739 Logit
Especificidad 0,659 0,661 KNN
Valor predictivo positivo (precisión) 0,685 0,685 Empate
Valor predictivo negativo 0,719 0,717 Logit
Exactitud balanceada 0,701 0,700 Logit
AUC 0,768 0,765 Logit
Nota: El mejor valor de cada métrica está resaltado en verde. Ambos modelos se evaluaron con el mismo 25 % de prueba del examen Saber 11.

Los dos modelos tuvieron un desempeño prácticamente idéntico en el conjunto de prueba. El logit alcanzó una exactitud de 70,1 % y un AUC de 0,768, frente a 70,0 % y 0,765 del KNN (k = 39). La diferencia en exactitud, de 0,08 puntos porcentuales, equivale a un solo estudiante de los 1.250 del conjunto de prueba. Ambos modelos identifican mejor a los estudiantes de alto rendimiento (sensibilidad cercana al 74 %) que a los de bajo rendimiento (especificidad cercana al 66 %), y sus índices Kappa (0,40) indican un acuerdo moderado-bajo por encima del azar. ### Matrices de confusión

t_lg <- unclass(cm_logit$table)
t_kn <- unclass(cm_knn$table)

mc_tabla <- data.frame(
  c("Bajo", "Alto"),
  unname(t_lg[c("Bajo", "Alto"), "Bajo"]), unname(t_lg[c("Bajo", "Alto"), "Alto"]),
  unname(t_kn[c("Bajo", "Alto"), "Bajo"]), unname(t_kn[c("Bajo", "Alto"), "Alto"])
)

mc_tabla %>%
  kbl(
    caption = "<b>Matrices de confusión en el conjunto de prueba</b>",
    col.names = c("Predicción", "Real: Bajo", "Real: Alto", "Real: Bajo", "Real: Alto"),
    align = c("l", "c", "c", "c", "c"), row.names = FALSE
  ) %>%
  add_header_above(c(" " = 1, "Logit" = 2, "KNN" = 2), background = "#080344", color = "white", bold = TRUE) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, font_size = 13, position = "center") %>%
  row_spec(0, background = "#080344", color = "white", bold = TRUE) %>%
  column_spec(1, bold = TRUE) %>%
  footnote(general = "Las filas son la clase predicha por el modelo y las columnas la clase real. La clase positiva es el alto rendimiento.",
           general_title = "Nota:", footnote_as_chunk = TRUE)
<b>Matrices de confusión en el conjunto de prueba</b>
Logit
KNN
Predicción Real: Bajo Real: Alto Real: Bajo Real: Alto
Bajo 412 161 413 163
Alto 213 464 212 462
Nota: Las filas son la clase predicha por el modelo y las columnas la clase real. La clase positiva es el alto rendimiento.

Ambos modelos clasificaron correctamente a casi el mismo número de estudiantes: 876 el logit y 875 el KNN, de los 1.250 del conjunto de prueba. En los dos casos los errores se concentran más en los falsos positivos (213 y 212 estudiantes de bajo rendimiento clasificados como de alto) que en los falsos negativos (161 y 163 estudiantes de alto rendimiento clasificados como de bajo). Esto significa que los modelos tienden a predecir alto rendimiento con más frecuencia de la real (677 y 674 predicciones, frente a los 625 estudiantes de alto rendimiento que hay en la prueba), lo que explica su mayor sensibilidad que especificidad. Esa tendencia es coherente con los umbrales de Youden utilizados (0,448 en el logit y 0,498 en el KNN), que priorizan detectar a los estudiantes de alto rendimiento. En la práctica, las dos matrices son casi idénticas, lo que confirma que ambos modelos tienen un desempeño equivalente.

4.3.2 Curvas ROC

library(plotly)

col_lg <- "#080344"
col_kn <- "#E07A5F"

curva <- function(r) {
  data.frame(FP = 1 - r$specificities, Sens = r$sensitivities) %>% arrange(FP, Sens)
}
d_lg <- curva(roc_logit)
d_kn <- curva(roc_knn)

# Punto de cada curva que corresponde al umbral de Youden
punto <- function(r, u) {
  i <- which.min(abs(r$thresholds - u))
  c(x = 1 - r$specificities[i], y = r$sensitivities[i])
}
p_lg <- punto(roc_logit, umbral_logit)
p_kn <- punto(roc_knn,   umbral_knn)

ROC_comparacion <- plot_ly() %>%
  add_trace(data = d_lg, x = ~FP, y = ~Sens, type = "scatter", mode = "lines",
            line = list(color = col_lg, width = 3),
            name = paste0("Logit (AUC = ", fmt(auc_logit, 3), ")"),
            hoverinfo = "text",
            text = ~paste0("<b>Logit</b><br>1 - Especificidad: ", round(FP, 3),
                           "<br>Sensibilidad: ", round(Sens, 3))) %>%
  add_trace(data = d_kn, x = ~FP, y = ~Sens, type = "scatter", mode = "lines",
            line = list(color = col_kn, width = 3),
            name = paste0("KNN (AUC = ", fmt(auc_knn, 3), ")"),
            hoverinfo = "text",
            text = ~paste0("<b>KNN</b><br>1 - Especificidad: ", round(FP, 3),
                           "<br>Sensibilidad: ", round(Sens, 3))) %>%
  add_trace(x = c(0, 1), y = c(0, 1), type = "scatter", mode = "lines",
            line = list(color = "#888888", dash = "dash", width = 1.5),
            name = "Línea de azar (AUC = 0,5)", hoverinfo = "none") %>%
  add_trace(x = p_lg[["x"]], y = p_lg[["y"]], type = "scatter", mode = "markers",
            marker = list(symbol = "diamond", size = 12, color = col_lg,
                          line = list(color = "#D4AF37", width = 2)),
            name = "Umbral Youden (logit)", hoverinfo = "text",
            text = paste0("<b>Logit - umbral óptimo:</b> ", round(umbral_logit, 3))) %>%
  add_trace(x = p_kn[["x"]], y = p_kn[["y"]], type = "scatter", mode = "markers",
            marker = list(symbol = "diamond", size = 12, color = col_kn,
                          line = list(color = "#D4AF37", width = 2)),
            name = "Umbral Youden (KNN)", hoverinfo = "text",
            text = paste0("<b>KNN - umbral óptimo:</b> ", round(umbral_knn, 3))) %>%
  layout(
    title = list(text = "<b>Curvas ROC: modelo logit vs. modelo KNN</b>",
                 x = 0, xanchor = "left", font = list(size = 17, color = "#080344")),
    xaxis = list(title = "1 - Especificidad (tasa de falsos positivos)",
                 range = c(0, 1), zeroline = FALSE, gridcolor = "#F0F0F0"),
    yaxis = list(title = "Sensibilidad (tasa de verdaderos positivos)",
                 range = c(0, 1), zeroline = FALSE, gridcolor = "#F0F0F0"),
    legend = list(x = 0.52, y = 0.18, bgcolor = "rgba(255, 255, 255, 0.9)",
                  bordercolor = "#6057FF", borderwidth = 1),
    plot_bgcolor = "#FDFBF7", paper_bgcolor = "white",
    margin = list(l = 80, r = 50, t = 80, b = 110),
    annotations = list(
      list(x = 1, y = -0.28, xref = "paper", yref = "paper", xanchor = "right", yanchor = "top",
           text = "Fuente: Elaboración propia con base en el dataset del examen Saber 11",
           showarrow = FALSE, font = list(size = 10, color = "#0A00D1"))
    )
  ) %>%
  config(displayModeBar = TRUE, displaylogo = FALSE, responsive = TRUE)

ROC_comparacion

Cuanto más cerca esté una curva de la esquina superior izquierda, mejor distingue el modelo entre estudiantes de alto y bajo rendimiento. Los rombos marcan el punto de cada curva que corresponde al umbral de clasificación utilizado.

Las curvas ROC del logit y del KNN prácticamente se superponen en todo el recorrido, con AUC de 0,768 y 0,765 respectivamente, lo que indica una capacidad equivalente y moderada para distinguir entre estudiantes de alto y bajo rendimiento: ambas se ubican claramente por encima de la línea de azar (AUC = 0,5), pero lejos de una clasificación perfecta (AUC = 1). El logit se mantiene apenas por encima del KNN en el tramo intermedio de la curva, aunque la diferencia es mínima. Los umbrales óptimos de Youden de ambos modelos coinciden en el mismo punto de la curva, con una sensibilidad cercana a 74 % y una especificidad cercana a 66 %, por lo que ambos modelos hacen el mismo balance entre detectar a los estudiantes de alto rendimiento y no clasificar de más a los de bajo rendimiento.

library(knitr)
library(kableExtra)

youden <- function(cm, umbral) {
  s  <- cm$byClass[["Sensitivity"]]
  e  <- cm$byClass[["Specificity"]]
  c(Umbral = umbral, Sensibilidad = s, Especificidad = e, Youden = s + e - 1)
}
tb <- rbind(Logit = youden(cm_logit, umbral_logit),
            KNN   = youden(cm_knn,   umbral_knn))

data.frame(Modelo = rownames(tb),
           `Umbral óptimo` = fmt(tb[, "Umbral"], 3),
           Sensibilidad    = fmt(tb[, "Sensibilidad"], 3),
           Especificidad   = fmt(tb[, "Especificidad"], 3),
           `Índice de Youden (J)` = fmt(tb[, "Youden"], 3),
           check.names = FALSE) %>%
  kbl(caption = "<b>Umbral óptimo (índice de Youden) de cada modelo</b>",
      align = c("l", "c", "c", "c", "c"), escape = FALSE, row.names = FALSE) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, font_size = 13, position = "center") %>%
  row_spec(0, background = "#080344", color = "white", bold = TRUE) %>%
  column_spec(1, bold = TRUE) %>%
  footnote(general = "J = sensibilidad + especificidad − 1. El umbral óptimo es el que maximiza J en la curva ROC; las probabilidades iguales o superiores a él se clasifican como alto rendimiento.",
           general_title = "Nota:", footnote_as_chunk = TRUE)
Umbral óptimo (índice de Youden) de cada modelo
Modelo Umbral óptimo Sensibilidad Especificidad Índice de Youden (J)
Logit 0,448 0,742 0,659 0,402
KNN 0,498 0,739 0,661 0,400
Nota: J = sensibilidad + especificidad − 1. El umbral óptimo es el que maximiza J en la curva ROC; las probabilidades iguales o superiores a él se clasifican como alto rendimiento.

Ambos modelos alcanzan un índice de Youden de aproximadamente 0,40, lo que indica un equilibrio moderado entre sensibilidad y especificidad. Los umbrales óptimos (0,448 en el logit y 0,498 en el KNN) quedan por debajo o cerca de 0,5, lo que explica que los modelos detecten mejor el alto rendimiento (sensibilidad ≈ 74 %) que el bajo (especificidad ≈ 66 %).

4.3.3 ¿Es significativa la diferencia entre los modelos?

Con un conjunto de prueba de tamaño limitado, una diferencia de uno o dos puntos entre modelos puede deberse al azar. Por eso se aplicaron dos pruebas que comparan los modelos sobre los mismos estudiantes: la prueba de DeLong, que compara los AUC, y la prueba de McNemar, que compara los aciertos de ambos.

tabla_pruebas <- data.frame(
  Prueba = c("DeLong (comparación de AUC)", "McNemar (comparación de aciertos)"),
  Estadístico = c(fmt(z_delong, 2), fmt(unname(mc$statistic), 2)),
  `Valor p` = c(fp(p_delong), fp(p_mc)),
  Conclusión = cell_spec(
    ifelse(c(p_delong, p_mc) < 0.05, "Diferencia significativa", "Sin diferencia significativa"),
    format = "html", bold = TRUE,
    color      = ifelse(c(p_delong, p_mc) < 0.05, "#721c24", "#155724"),
    background = ifelse(c(p_delong, p_mc) < 0.05, "#f8d7da", "#d4edda"),
    extra_css  = "padding: 3px 12px; border-radius: 12px;"),
  check.names = FALSE
)

tabla_pruebas %>%
  kbl(
    caption = "<b>Pruebas de comparación entre el modelo logit y el KNN</b>",
    align = c("l", "c", "c", "c"), escape = FALSE, row.names = FALSE
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, font_size = 13, position = "center") %>%
  row_spec(0, background = "#080344", color = "white", bold = TRUE) %>%
  column_spec(1, bold = TRUE, width = "7cm") %>%
  footnote(general = "Nivel de significancia de 5 %.",
           general_title = "Nota:", footnote_as_chunk = TRUE)
Pruebas de comparación entre el modelo logit y el KNN
Prueba Estadístico Valor p Conclusión
DeLong (comparación de AUC) 1,34 0,182 Sin diferencia significativa
McNemar (comparación de aciertos) 0,00 1,000 Sin diferencia significativa
Nota: Nivel de significancia de 5 %.

La prueba de DeLong no detecta una diferencia estadísticamente significativa entre los AUC de los dos modelos (p = 0,182). Por su parte, la prueba de McNemar no encuentra diferencias significativas entre los aciertos de los dos modelos (p = 1,000): ambos modelos coinciden, acertando o fallando juntos, en el 99,8% de los estudiantes del conjunto de prueba. Por tanto, aunque el modelo logit obtuvo valores ligeramente superiores, la diferencia no es estadísticamente significativa y puede deberse al azar del conjunto de prueba: ambos modelos tienen un desempeño equivalente.

4.3.4 Interpretabilidad de los modelos

Además del desempeño predictivo, los modelos difieren en lo que permiten explicar. El logit estima un coeficiente y un odds ratio para cada categoría, lo que permite saber qué variables se asocian con el alto rendimiento, en qué sentido y con qué magnitud (por ejemplo, el efecto creciente del estrato). El KNN, en cambio, solo entrega una clasificación y no indica qué variables influyen en ella.

Como las cinco variables explicativas son categóricas, existen a lo sumo 112 perfiles distintos de estudiantes (2 × 2 × 7 × 2 × 2 combinaciones). Por ello, el KNN termina comparando a cada estudiante con otros de su mismo perfil o de perfiles muy cercanos, mientras que el logit asume que los efectos de las variables se suman. Esto ayuda a explicar que ambos modelos obtengan resultados parecidos.

5 CONCLUSIONES

  1. Se construyó y comparó un modelo de clasificación binaria para predecir si un estudiante del Valle del Cauca alcanza un alto rendimiento (más de 70 puntos) en Matemáticas del Saber 11. Se usó una muestra estratificada de 5.000 estudiantes (2.500 de alto y 2.500 de bajo rendimiento), tomada de 609.750 casos completos. Con el 25 % de prueba, ambos modelos superaron con claridad el azar (NIR = 50 %, p < 0,001). El logit obtuvo 70,1 % de exactitud y el KNN 70,0 %. Esto confirma que las condiciones institucionales, sociodemográficas y de acceso a tecnología contienen información útil para predecir el desempeño.

  2. Con el estrato 1 como referencia, las odds de alto rendimiento se multiplican por 1,77 en el estrato 2, 2,75 en el 3, 5,55 en el 4, 7,64 en el 5 y 16,46 en el 6. El efecto crece de forma sostenida y todos los coeficientes son significativos (p < 0,001). Los datos descriptivos lo respaldan: el porcentaje de alto rendimiento sube de 22,5 % (estrato 1) a 90,2 % (estrato 6), una brecha de 67,7 puntos porcentuales. El nivel socioeconómico del hogar es el principal predictor de quién llega a los puntajes más altos. La categoría “sin estrato” no fue significativa (p = 0,449), pero solo tiene 32 estudiantes, así que no permite concluir nada.

  3. El tipo de colegio y el género también pesan. Estudiar en un colegio oficial reduce las odds en 35,5 % frente a un colegio no oficial (OR = 0,645). En los datos, esto equivale a 33,3 % de alto rendimiento en oficiales contra 64,7 % en no oficiales. Los hombres tienen casi el doble de odds que las mujeres (OR = 1,970), lo que coincide con la brecha de género en Matemáticas reportada en las pruebas Saber. Ambos efectos se mantienen al controlar por estrato y acceso a tecnología.

  4. Tener internet multiplica las odds por 2,38 (p < 0,001). Tener computador las multiplica por 1,26, un efecto más débil y apenas significativo (p = 0,042). El acceso a información y recursos digitales parece importar más que poseer el equipo.

  5. El modelo logit es sólido. Nueve de los diez coeficientes son significativos al 5 %. El GVIF ajustado fue de 1,01 a 1,21, muy por debajo del umbral de 2,24, de modo que no hay multicolinealidad. Esto incluye el par computador–internet, que sí están relacionados (88,8 % de quienes tienen computador tienen internet). Por eso los coeficientes se interpretan con confianza, siempre con las demás variables constantes.

  6. Los dos modelos rinden prácticamente igual. El KNN se optimizó con validación cruzada de 10 pliegues (k = 39, exactitud de 71,5 %), con una curva muy plana entre 70,5 % y 71,5 %. Logit y KNN tuvieron resultados casi idénticos. La diferencia de exactitud equivale a un solo estudiante de 1.250 (876 contra 875 aciertos). La prueba de DeLong (p = 0,182) y la de McNemar (p = 1,000) confirman que no hay diferencia significativa. Ambos modelos coinciden en el 99,8 % de los estudiantes.

  7. Capacidad predictiva moderada, un AUC de 0,77 y un Kappa de 0,40 indican una capacidad moderada, lejos de una clasificación casi perfecta. Los modelos detectan mejor el alto rendimiento (sensibilidad ≈ 74 %) que el bajo (especificidad ≈ 66 %). Los falsos positivos (213 y 212) superan a los falsos negativos (161 y 163), así que predicen “alto” más de lo real: 677 y 674 predicciones contra 625 casos reales. Esto es coherente con los umbrales de Youden (0,448 y 0,498). Además, la muestra está balanceada 50/50 y en el Valle solo el 4,1 % alcanza el nivel alto. Con esa prevalencia, la precisión de una predicción de “alto” caería de 68,5 % a cerca de 8,5 % (estimación propia con el teorema de Bayes). Por eso el modelo sirve para identificar factores asociados y comparar perfiles, y no para predecir casos individuales en la población general.

6 BIBLIOGRAFÍA

  1. Instituto Colombiano para la Evaluación de la Educación [ICFES]. (2023). Resultados únicos Saber 11 [Conjunto de datos]. Portal Nacional de Datos Abiertos. https://www.datos.gov.co/Educaci-n/Resultados-nicos-Saber-11/kgxf-xxbe/about_data

  2. Instituto Colombiano para la Evaluación de la Educación. (2025, 22 de septiembre). Saber 11°: Niveles de desempeño prueba matemáticas [Documento PDF]. https://www.icfes.gov.co/wp-content/uploads/2025/09/22-septiembre-nd-prueba-matematicas-saber-11.pdf

  3. Genuine School. (2026, 20 de enero). Colegios privados vs colegios públicos: La decisión que marcará el futuro de tus hijos. https://studyatgenuine.com/blog/colegios-privados-vs-colegios-publicos/

  4. Acosta, J., & Quintero Rueda, C. M. (2023, 16 de marzo). Saber 11 reflejan la brecha de género en las matemáticas. Portafolio. https://www.portafolio.co/economia/saber-11-reflejan-la-brecha-de-genero-en-las-matematicas-580051

  5. Valencia, G. (2026, 22 de mayo). Las pruebas Saber y la desigualdad que Colombia no logra romper. Pares. https://www.pares.com.co/las-pruebas-saber-y-la-desigualdad-que-colombia-no-logra-romper/

  6. Junca Rodríguez, G. A. (2019). Desempeño académico en las pruebas Saber 11. Panorama Económico, 27(1), 8–38. https://share.google/LYcubyEYFqbiHGqCt

  7. Cerquera Losada, O. H., Conde Córdoba, K., & Amaya González, M. D. (2025). Brecha digital y rendimiento académico en Colombia: acceso a TIC durante la pandemia. Revista Interamericana de Investigación, Educación y Pedagogía, 19(1), 311–340. https://doi.org/10.15332/vd7jb409