La deserción universitaria es uno de los problemas más relevantes en la educación superior, pues afecta tanto al estudiante, como a la institución. Identificar de forma temprana a los estudiantes en riesgo de abandonar sus estudios permitiría diseñar intervenciones oportunas. En este proyecto abordamos la predicción de la deserción universitaria como un problema de clasificación supervisada, donde el objetivo es clasificar a cada estudiante en una de dos categorías: deserta o no deserta.
Para resolver el problema comparamos tres modelos de clasificación cubiertos en el curso:árbol de decisión, máquina de vector soporte (SVM) y clasificador bayesiano (Naive Bayes), con el fin de determinar cuál predice mejor la deserción. El clasificador bayesiano se incluye como modelo de referencia (baseline), mientras que el árbol y el SVM son los modelos principales: el primero por su interpretabilidad y el segundo por su capacidad predictiva.
Los datos provienen del conjunto “Predict Students’ Dropout and
Academic Success” del UCI Machine Learning Repository,
recopilados en una institución de educación superior de Portugal. El
conjunto contiene información de 4,424 estudiantes y 36 variables
predictoras de tipo demográfico, socioeconómico, macroeconómico y
académico (rendimiento al final del primer y segundo semestre), además
de la variable objetivo Target.
Target, con tres categorías
(Dropout, Enrolled, Graduate).El diccionario completo de las 37 variables (nombre, rol, tipo,
codificación y unidades) se encuentra en el archivo
diccionario_de_variables.csv que acompaña este documento.
La fuente reporta que el conjunto no contiene valores faltantes, ya que
los autores realizaron un preprocesamiento previo para tratar anomalías
y datos ausentes.
# install.packages("pacman") # ejecutar una sola vez si no está instalado
pacman::p_load(
tidyverse, # manipulación y visualización de datos (dplyr, tidyr, ggplot2, ...)
janitor, # limpieza de nombres de columnas
naniar, # diagnóstico de datos faltantes
plotly, # gráficos interactivos
caret, # partición de datos, validación cruzada y métricas
rpart, # árbol de decisión (CART)
rpart.plot, # visualización del árbol
e1071, # SVM y Naive Bayes
themis, # balanceo de clases (SMOTE)
recipes
)
El archivo data.csv usa punto y coma (;)
como separador de columnas, pero los decimales vienen con punto (por
ejemplo, 122.0). Usamos read.csv() indicando
explícitamente sep = ";" y dec = ".", y
fileEncoding = "UTF-8-BOM" para manejar el marcador BOM del
inicio del archivo.
Data <- read.csv("data.csv",
header = TRUE,
sep = ";",
dec = ".",
fileEncoding = "UTF-8-BOM",
check.names = FALSE)
# Dimensión de la base de datos
dim(Data)
## [1] 4424 37
# Vistazo a la estructura de las variables
glimpse(Data)
## Rows: 4,424
## Columns: 37
## $ `Marital status` <int> 1, 1, 1, 1, 2, 2, 1, …
## $ `Application mode` <int> 17, 15, 1, 17, 39, 39…
## $ `Application order` <int> 5, 1, 5, 2, 1, 1, 1, …
## $ Course <int> 171, 9254, 9070, 9773…
## $ `Daytime/evening attendance\t` <int> 1, 1, 1, 1, 0, 0, 1, …
## $ `Previous qualification` <int> 1, 1, 1, 1, 1, 19, 1,…
## $ `Previous qualification (grade)` <dbl> 122.0, 160.0, 122.0, …
## $ Nacionality <int> 1, 1, 1, 1, 1, 1, 1, …
## $ `Mother's qualification` <int> 19, 1, 37, 38, 37, 37…
## $ `Father's qualification` <int> 12, 3, 37, 37, 38, 37…
## $ `Mother's occupation` <int> 5, 3, 9, 5, 9, 9, 7, …
## $ `Father's occupation` <int> 9, 3, 9, 3, 9, 7, 10,…
## $ `Admission grade` <dbl> 127.3, 142.5, 124.8, …
## $ Displaced <int> 1, 1, 1, 1, 0, 0, 1, …
## $ `Educational special needs` <int> 0, 0, 0, 0, 0, 0, 0, …
## $ Debtor <int> 0, 0, 0, 0, 0, 1, 0, …
## $ `Tuition fees up to date` <int> 1, 0, 0, 1, 1, 1, 1, …
## $ Gender <int> 1, 1, 1, 0, 0, 1, 0, …
## $ `Scholarship holder` <int> 0, 0, 0, 0, 0, 0, 1, …
## $ `Age at enrollment` <int> 20, 19, 19, 20, 45, 5…
## $ International <int> 0, 0, 0, 0, 0, 0, 0, …
## $ `Curricular units 1st sem (credited)` <int> 0, 0, 0, 0, 0, 0, 0, …
## $ `Curricular units 1st sem (enrolled)` <int> 0, 6, 6, 6, 6, 5, 7, …
## $ `Curricular units 1st sem (evaluations)` <int> 0, 6, 0, 8, 9, 10, 9,…
## $ `Curricular units 1st sem (approved)` <int> 0, 6, 0, 6, 5, 5, 7, …
## $ `Curricular units 1st sem (grade)` <dbl> 0.00000, 14.00000, 0.…
## $ `Curricular units 1st sem (without evaluations)` <int> 0, 0, 0, 0, 0, 0, 0, …
## $ `Curricular units 2nd sem (credited)` <int> 0, 0, 0, 0, 0, 0, 0, …
## $ `Curricular units 2nd sem (enrolled)` <int> 0, 6, 6, 6, 6, 5, 8, …
## $ `Curricular units 2nd sem (evaluations)` <int> 0, 6, 0, 10, 6, 17, 8…
## $ `Curricular units 2nd sem (approved)` <int> 0, 6, 0, 5, 6, 5, 8, …
## $ `Curricular units 2nd sem (grade)` <dbl> 0.00000, 13.66667, 0.…
## $ `Curricular units 2nd sem (without evaluations)` <int> 0, 0, 0, 0, 0, 5, 0, …
## $ `Unemployment rate` <dbl> 10.8, 13.9, 10.8, 9.4…
## $ `Inflation rate` <dbl> 1.4, -0.3, 1.4, -0.8,…
## $ GDP <dbl> 1.74, 0.79, 1.74, -3.…
## $ Target <chr> "Dropout", "Graduate"…
En esta sección aplicamos las tareas de limpieza identificadas en el conjunto de datos. Aunque la fuente indica que no hay valores faltantes, realizamos el diagnóstico completo para verificarlo y documentarlo, además de corregir los problemas reales que sí presenta el archivo (nombres de columnas y tipos de variables).
Los nombres originales contienen espacios, paréntesis, barras,
apóstrofes e incluso un tabulador al final de la columna de asistencia
(Daytime/evening attendance\t). Usamos
janitor::clean_names() para estandarizarlos a un formato
limpio (minúsculas, guiones bajos, sin caracteres especiales).
# Nombres originales (problemáticos)
names(Data)
## [1] "Marital status"
## [2] "Application mode"
## [3] "Application order"
## [4] "Course"
## [5] "Daytime/evening attendance\t"
## [6] "Previous qualification"
## [7] "Previous qualification (grade)"
## [8] "Nacionality"
## [9] "Mother's qualification"
## [10] "Father's qualification"
## [11] "Mother's occupation"
## [12] "Father's occupation"
## [13] "Admission grade"
## [14] "Displaced"
## [15] "Educational special needs"
## [16] "Debtor"
## [17] "Tuition fees up to date"
## [18] "Gender"
## [19] "Scholarship holder"
## [20] "Age at enrollment"
## [21] "International"
## [22] "Curricular units 1st sem (credited)"
## [23] "Curricular units 1st sem (enrolled)"
## [24] "Curricular units 1st sem (evaluations)"
## [25] "Curricular units 1st sem (approved)"
## [26] "Curricular units 1st sem (grade)"
## [27] "Curricular units 1st sem (without evaluations)"
## [28] "Curricular units 2nd sem (credited)"
## [29] "Curricular units 2nd sem (enrolled)"
## [30] "Curricular units 2nd sem (evaluations)"
## [31] "Curricular units 2nd sem (approved)"
## [32] "Curricular units 2nd sem (grade)"
## [33] "Curricular units 2nd sem (without evaluations)"
## [34] "Unemployment rate"
## [35] "Inflation rate"
## [36] "GDP"
## [37] "Target"
# Estandarización automática de nombres
Data <- Data %>% janitor::clean_names()
# Nombres limpios
names(Data)
## [1] "marital_status"
## [2] "application_mode"
## [3] "application_order"
## [4] "course"
## [5] "daytime_evening_attendance"
## [6] "previous_qualification"
## [7] "previous_qualification_grade"
## [8] "nacionality"
## [9] "mothers_qualification"
## [10] "fathers_qualification"
## [11] "mothers_occupation"
## [12] "fathers_occupation"
## [13] "admission_grade"
## [14] "displaced"
## [15] "educational_special_needs"
## [16] "debtor"
## [17] "tuition_fees_up_to_date"
## [18] "gender"
## [19] "scholarship_holder"
## [20] "age_at_enrollment"
## [21] "international"
## [22] "curricular_units_1st_sem_credited"
## [23] "curricular_units_1st_sem_enrolled"
## [24] "curricular_units_1st_sem_evaluations"
## [25] "curricular_units_1st_sem_approved"
## [26] "curricular_units_1st_sem_grade"
## [27] "curricular_units_1st_sem_without_evaluations"
## [28] "curricular_units_2nd_sem_credited"
## [29] "curricular_units_2nd_sem_enrolled"
## [30] "curricular_units_2nd_sem_evaluations"
## [31] "curricular_units_2nd_sem_approved"
## [32] "curricular_units_2nd_sem_grade"
## [33] "curricular_units_2nd_sem_without_evaluations"
## [34] "unemployment_rate"
## [35] "inflation_rate"
## [36] "gdp"
## [37] "target"
Análisis: Al comparar los nombres antes y después, se observa el
efecto de la estandarización: los espacios se reemplazaron por guiones
bajos, se eliminaron los paréntesis y apóstrofes, y el tabulador que
tenía pegado la columna de asistencia desapareció. Por ejemplo,
Daytime/evening attendance\t quedó como
daytime_evening_attendance. Esto es importante porque
nombres con caracteres especiales pueden causar errores al referirnos a
las columnas en el código más adelante.
Confirmamos lo que indica la fuente: que no existen valores faltantes. Este paso demuestra que la decisión de no imputar es una conclusión informada y no un descuido. Siguiendo el criterio del curso, si no hay datos faltantes (o si fueran menos del 5%), no procede la imputación.
# Porcentaje total de valores faltantes
pct_miss(Data)
## [1] 0
# Datos faltantes por variable (visualización)
gg_miss_var(Data)
# Conteo total de NA en toda la base
sum(is.na(Data))
## [1] 0
Como se observa, el conjunto no presenta valores faltantes, por lo que no se requiere ninguna técnica de imputación.
# Dimensión antes de eliminar duplicados
dim(Data)
## [1] 4424 37
# Eliminar filas 100% duplicadas
Data <- Data %>% distinct()
# Dimensión después
dim(Data)
## [1] 4424 37
Análisis: Revisamos y, de existir, eliminamos filas completamente
duplicadas con distinct(). Comparando la dimensión antes y
después de aplicar distinct(),verificamos si se eliminaron
filas. Si el número de filas se mantiene igual, significa que no había
registros completamente duplicados; si disminuye, indica cuántos se
removieron. Eliminar duplicados es importante porque registros repetidos
pueden sesgar el entrenamiento del modelo al dar más peso a ciertas
observaciones.
Este es uno de los pasos más importantes del preprocesamiento. Muchas
variables están almacenadas como enteros, pero en realidad representan
categorías, no cantidades. Por ejemplo, en course el código
171 (Animación) no es “mayor” que el 33 (Biocombustibles); son
simplemente etiquetas. Si se dejaran como números, los modelos basados
en distancias (KNN, SVM) interpretarían magnitudes inexistentes.
Distinguimos tres grupos de variables:
# Variables categóricas nominales y binarias -> factor
vars_categoricas <- c(
"marital_status", "application_mode", "application_order", "course",
"daytime_evening_attendance", "previous_qualification", "nacionality",
"mothers_qualification", "fathers_qualification",
"mothers_occupation", "fathers_occupation",
"displaced", "educational_special_needs", "debtor",
"tuition_fees_up_to_date", "gender", "scholarship_holder", "international"
)
# Variables numéricas reales (calificaciones, edad, unidades curriculares y macroeconómicas)
vars_numericas_todas <- c(
"previous_qualification_grade", "admission_grade", "age_at_enrollment",
"curricular_units_1st_sem_credited", "curricular_units_1st_sem_enrolled",
"curricular_units_1st_sem_evaluations", "curricular_units_1st_sem_approved",
"curricular_units_1st_sem_grade", "curricular_units_1st_sem_without_evaluations",
"curricular_units_2nd_sem_credited", "curricular_units_2nd_sem_enrolled",
"curricular_units_2nd_sem_evaluations", "curricular_units_2nd_sem_approved",
"curricular_units_2nd_sem_grade", "curricular_units_2nd_sem_without_evaluations",
"unemployment_rate", "inflation_rate", "gdp"
)
Data <- Data %>%
mutate(across(all_of(vars_categoricas), as.factor)) %>%
# Aseguramos que las numéricas sean efectivamente numéricas (por seguridad)
mutate(across(all_of(vars_numericas_todas), as.numeric))
# Verificamos los tipos resultantes
glimpse(Data)
## Rows: 4,424
## Columns: 37
## $ marital_status <fct> 1, 1, 1, 1, 2, 2, 1, 1, 1…
## $ application_mode <fct> 17, 15, 1, 17, 39, 39, 1,…
## $ application_order <fct> 5, 1, 5, 2, 1, 1, 1, 4, 3…
## $ course <fct> 171, 9254, 9070, 9773, 80…
## $ daytime_evening_attendance <fct> 1, 1, 1, 1, 0, 0, 1, 1, 1…
## $ previous_qualification <fct> 1, 1, 1, 1, 1, 19, 1, 1, …
## $ previous_qualification_grade <dbl> 122.0, 160.0, 122.0, 122.…
## $ nacionality <fct> 1, 1, 1, 1, 1, 1, 1, 1, 6…
## $ mothers_qualification <fct> 19, 1, 37, 38, 37, 37, 19…
## $ fathers_qualification <fct> 12, 3, 37, 37, 38, 37, 38…
## $ mothers_occupation <fct> 5, 3, 9, 5, 9, 9, 7, 9, 9…
## $ fathers_occupation <fct> 9, 3, 9, 3, 9, 7, 10, 9, …
## $ admission_grade <dbl> 127.3, 142.5, 124.8, 119.…
## $ displaced <fct> 1, 1, 1, 1, 0, 0, 1, 1, 0…
## $ educational_special_needs <fct> 0, 0, 0, 0, 0, 0, 0, 0, 0…
## $ debtor <fct> 0, 0, 0, 0, 0, 1, 0, 0, 0…
## $ tuition_fees_up_to_date <fct> 1, 0, 0, 1, 1, 1, 1, 0, 1…
## $ gender <fct> 1, 1, 1, 0, 0, 1, 0, 1, 0…
## $ scholarship_holder <fct> 0, 0, 0, 0, 0, 0, 1, 0, 1…
## $ age_at_enrollment <dbl> 20, 19, 19, 20, 45, 50, 1…
## $ international <fct> 0, 0, 0, 0, 0, 0, 0, 0, 1…
## $ curricular_units_1st_sem_credited <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0…
## $ curricular_units_1st_sem_enrolled <dbl> 0, 6, 6, 6, 6, 5, 7, 5, 6…
## $ curricular_units_1st_sem_evaluations <dbl> 0, 6, 0, 8, 9, 10, 9, 5, …
## $ curricular_units_1st_sem_approved <dbl> 0, 6, 0, 6, 5, 5, 7, 0, 6…
## $ curricular_units_1st_sem_grade <dbl> 0.00000, 14.00000, 0.0000…
## $ curricular_units_1st_sem_without_evaluations <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0…
## $ curricular_units_2nd_sem_credited <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0…
## $ curricular_units_2nd_sem_enrolled <dbl> 0, 6, 6, 6, 6, 5, 8, 5, 6…
## $ curricular_units_2nd_sem_evaluations <dbl> 0, 6, 0, 10, 6, 17, 8, 5,…
## $ curricular_units_2nd_sem_approved <dbl> 0, 6, 0, 5, 6, 5, 8, 0, 6…
## $ curricular_units_2nd_sem_grade <dbl> 0.00000, 13.66667, 0.0000…
## $ curricular_units_2nd_sem_without_evaluations <dbl> 0, 0, 0, 0, 0, 5, 0, 0, 0…
## $ unemployment_rate <dbl> 10.8, 13.9, 10.8, 9.4, 13…
## $ inflation_rate <dbl> 1.4, -0.3, 1.4, -0.8, -0.…
## $ gdp <dbl> 1.74, 0.79, 1.74, -3.12, …
## $ target <chr> "Dropout", "Graduate", "D…
Análisis: Tras la conversión, glimpse() confirma que las
variables quedaron con el tipo correcto: las categóricas nominales y
binarias ahora aparecen como <fct> (factor) y las
numéricas reales como <dbl> o
<int>. Este paso es clave para los modelos: si
hubiéramos dejado variables como course o
marital_status en formato numérico, el SVM habría
interpretado, por ejemplo, que el curso 171 está “más lejos” del curso
33 que del 100, cuando en realidad son solo etiquetas sin orden ni
distancia.
Nuestro tema es la predicción de la deserción, que es inherentemente binario: interesa separar a quien abandona de quien no abandona. Por ello recodificamos la variable objetivo de tres clases (Dropout, Enrolled, Graduate) a dos clases, combinando Enrolled (sigue matriculado) y Graduate (se graduó) en la categoría “No deserta”, frente a Dropout como “Deserta”.
Esta es una decisión de modelado (transformación de la variable respuesta), no de limpieza. Se eligió el enfoque binario sobre el de tres clases porque (1) se alinea con la pregunta de investigación, (2) produce comparaciones más limpias e interpretables entre los modelos y (3) la categoría Enrolled pertenece de forma natural al grupo de quienes aún no han desertado.
Data <- Data %>%
mutate(target = case_when(
target == "Dropout" ~ "Deserta",
target == "Enrolled" ~ "No_deserta",
target == "Graduate" ~ "No_deserta"
)) %>%
mutate(target = factor(target, levels = c("No_deserta", "Deserta")))
# Distribución de la nueva variable binaria
table(Data$target)
##
## No_deserta Deserta
## 3003 1421
prop.table(table(Data$target))
##
## No_deserta Deserta
## 0.6787975 0.3212025
# Visualización del balance de clases
Data %>%
count(target) %>%
ggplot(aes(x = target, y = n, fill = target)) +
geom_col() +
labs(title = "Distribución de la variable objetivo (binaria)",
x = "Clase", y = "Frecuencia") +
theme_minimal() +
theme(legend.position = "none")
La clase “No deserta” representa aproximadamente el 68% y “Deserta” el 32%. Se trata de un desbalance moderado que debemos tener en cuenta al evaluar los modelos: una alta exactitud podría deberse simplemente a predecir siempre la clase mayoritaria. Por eso, más adelante priorizamos métricas como la sensibilidad sobre la clase “Deserta” y el índice Kappa.
Las variables categóricas con muchísimos niveles (ocupaciones y cualificaciones de los padres tienen entre 29 y 46 categorías cada una) generan decenas de columnas al codificarse, lo que complica los modelos basados en distancias sin aportar mucho poder predictivo. Siguiendo un criterio intermedio, descartamos esas variables de alta cardinalidad y conservamos las predictoras más informativas: las académicas (rendimiento de los dos semestres y calificaciones), las demográficas y socioeconómicas claras, las binarias y las macroeconómicas.
# Variables de alta cardinalidad que se descartan (poco aporte, mucha complejidad)
vars_descartar <- c("mothers_occupation", "fathers_occupation",
"mothers_qualification", "fathers_qualification",
"nacionality", "course", "application_mode")
Data_sel <- Data %>% select(-all_of(vars_descartar))
# Dimensión tras la selección
dim(Data_sel)
## [1] 4424 30
names(Data_sel)
## [1] "marital_status"
## [2] "application_order"
## [3] "daytime_evening_attendance"
## [4] "previous_qualification"
## [5] "previous_qualification_grade"
## [6] "admission_grade"
## [7] "displaced"
## [8] "educational_special_needs"
## [9] "debtor"
## [10] "tuition_fees_up_to_date"
## [11] "gender"
## [12] "scholarship_holder"
## [13] "age_at_enrollment"
## [14] "international"
## [15] "curricular_units_1st_sem_credited"
## [16] "curricular_units_1st_sem_enrolled"
## [17] "curricular_units_1st_sem_evaluations"
## [18] "curricular_units_1st_sem_approved"
## [19] "curricular_units_1st_sem_grade"
## [20] "curricular_units_1st_sem_without_evaluations"
## [21] "curricular_units_2nd_sem_credited"
## [22] "curricular_units_2nd_sem_enrolled"
## [23] "curricular_units_2nd_sem_evaluations"
## [24] "curricular_units_2nd_sem_approved"
## [25] "curricular_units_2nd_sem_grade"
## [26] "curricular_units_2nd_sem_without_evaluations"
## [27] "unemployment_rate"
## [28] "inflation_rate"
## [29] "gdp"
## [30] "target"
Análisis: La base pasó de 37 a 30 columnas tras descartar las siete variables de alta cardinalidad. Esta reducción no busca eliminar información valiosa, sino quitar variables que, al tener decenas de categorías, generarían demasiadas columnas al codificarlas (one-hot) y harían los modelos más lentos y difíciles de interpretar sin mejorar la predicción. Conservamos las variables que la literatura y el sentido común señalan como más relacionadas con la deserción: sobre todo el rendimiento académico de los dos primeros semestres.
Exploramos los valores atípicos en las principales variables numéricas mediante diagramas de caja. Los árboles de decisión y Naive Bayes son robustos a outliers, pero su detección es parte del análisis exploratorio y ayuda a entender la escala de cada variable de cara al escalado que requieren KNN y SVM.
vars_numericas <- c("previous_qualification_grade", "admission_grade",
"age_at_enrollment",
"curricular_units_1st_sem_grade",
"curricular_units_2nd_sem_grade")
Data_sel %>%
select(all_of(vars_numericas)) %>%
pivot_longer(everything(), names_to = "Variable", values_to = "Valor") %>%
ggplot(aes(x = Variable, y = Valor)) +
geom_boxplot() +
coord_flip() +
labs(title = "Diagramas de caja de variables numéricas",
x = "", y = "Valor") +
theme_minimal()
Análisis: Los diagramas de caja revelan un patrón esperable en las calificaciones de las unidades curriculares: aparecen muchos valores en cero, que corresponden a estudiantes que no aprobaron ninguna unidad ese semestre. Estos ceros no son errores ni outliers que debamos eliminar, sino información real y muy relevante para predecir la deserción (un estudiante con cero unidades aprobadas tiene alto riesgo de abandonar). Por eso no los tratamos como valores atípicos a corregir. Las calificaciones de admisión y previa se mueven en su rango esperado (0–200).
Revisamos la correlación entre las variables numéricas para detectar redundancias. Variables muy correlacionadas (por ejemplo, unidades inscritas vs. evaluadas) aportan información similar.
Data_sel %>%
select(where(is.numeric)) %>%
cor() %>%
round(2)
## previous_qualification_grade
## previous_qualification_grade 1.00
## admission_grade 0.58
## age_at_enrollment -0.11
## curricular_units_1st_sem_credited -0.01
## curricular_units_1st_sem_enrolled -0.03
## curricular_units_1st_sem_evaluations -0.07
## curricular_units_1st_sem_approved 0.05
## curricular_units_1st_sem_grade 0.06
## curricular_units_1st_sem_without_evaluations 0.00
## curricular_units_2nd_sem_credited -0.02
## curricular_units_2nd_sem_enrolled -0.03
## curricular_units_2nd_sem_evaluations -0.06
## curricular_units_2nd_sem_approved 0.05
## curricular_units_2nd_sem_grade 0.05
## curricular_units_2nd_sem_without_evaluations -0.02
## unemployment_rate 0.05
## inflation_rate 0.02
## gdp -0.05
## admission_grade age_at_enrollment
## previous_qualification_grade 0.58 -0.11
## admission_grade 1.00 -0.03
## age_at_enrollment -0.03 1.00
## curricular_units_1st_sem_credited 0.04 0.23
## curricular_units_1st_sem_enrolled -0.03 0.14
## curricular_units_1st_sem_evaluations -0.07 0.14
## curricular_units_1st_sem_approved 0.07 -0.05
## curricular_units_1st_sem_grade 0.07 -0.16
## curricular_units_1st_sem_without_evaluations 0.01 0.06
## curricular_units_2nd_sem_credited 0.04 0.21
## curricular_units_2nd_sem_enrolled -0.04 0.09
## curricular_units_2nd_sem_evaluations -0.06 0.06
## curricular_units_2nd_sem_approved 0.08 -0.11
## curricular_units_2nd_sem_grade 0.07 -0.17
## curricular_units_2nd_sem_without_evaluations -0.01 0.06
## unemployment_rate 0.04 0.03
## inflation_rate -0.02 0.03
## gdp -0.02 -0.06
## curricular_units_1st_sem_credited
## previous_qualification_grade -0.01
## admission_grade 0.04
## age_at_enrollment 0.23
## curricular_units_1st_sem_credited 1.00
## curricular_units_1st_sem_enrolled 0.77
## curricular_units_1st_sem_evaluations 0.54
## curricular_units_1st_sem_approved 0.63
## curricular_units_1st_sem_grade 0.12
## curricular_units_1st_sem_without_evaluations 0.12
## curricular_units_2nd_sem_credited 0.94
## curricular_units_2nd_sem_enrolled 0.64
## curricular_units_2nd_sem_evaluations 0.43
## curricular_units_2nd_sem_approved 0.49
## curricular_units_2nd_sem_grade 0.13
## curricular_units_2nd_sem_without_evaluations 0.06
## unemployment_rate 0.01
## inflation_rate 0.02
## gdp -0.03
## curricular_units_1st_sem_enrolled
## previous_qualification_grade -0.03
## admission_grade -0.03
## age_at_enrollment 0.14
## curricular_units_1st_sem_credited 0.77
## curricular_units_1st_sem_enrolled 1.00
## curricular_units_1st_sem_evaluations 0.68
## curricular_units_1st_sem_approved 0.77
## curricular_units_1st_sem_grade 0.38
## curricular_units_1st_sem_without_evaluations 0.13
## curricular_units_2nd_sem_credited 0.75
## curricular_units_2nd_sem_enrolled 0.94
## curricular_units_2nd_sem_evaluations 0.60
## curricular_units_2nd_sem_approved 0.67
## curricular_units_2nd_sem_grade 0.36
## curricular_units_2nd_sem_without_evaluations 0.07
## unemployment_rate 0.04
## inflation_rate 0.04
## gdp -0.03
## curricular_units_1st_sem_evaluations
## previous_qualification_grade -0.07
## admission_grade -0.07
## age_at_enrollment 0.14
## curricular_units_1st_sem_credited 0.54
## curricular_units_1st_sem_enrolled 0.68
## curricular_units_1st_sem_evaluations 1.00
## curricular_units_1st_sem_approved 0.52
## curricular_units_1st_sem_grade 0.42
## curricular_units_1st_sem_without_evaluations 0.24
## curricular_units_2nd_sem_credited 0.52
## curricular_units_2nd_sem_enrolled 0.61
## curricular_units_2nd_sem_evaluations 0.78
## curricular_units_2nd_sem_approved 0.44
## curricular_units_2nd_sem_grade 0.36
## curricular_units_2nd_sem_without_evaluations 0.13
## unemployment_rate 0.06
## inflation_rate -0.01
## gdp -0.10
## curricular_units_1st_sem_approved
## previous_qualification_grade 0.05
## admission_grade 0.07
## age_at_enrollment -0.05
## curricular_units_1st_sem_credited 0.63
## curricular_units_1st_sem_enrolled 0.77
## curricular_units_1st_sem_evaluations 0.52
## curricular_units_1st_sem_approved 1.00
## curricular_units_1st_sem_grade 0.70
## curricular_units_1st_sem_without_evaluations -0.01
## curricular_units_2nd_sem_credited 0.61
## curricular_units_2nd_sem_enrolled 0.73
## curricular_units_2nd_sem_evaluations 0.54
## curricular_units_2nd_sem_approved 0.90
## curricular_units_2nd_sem_grade 0.69
## curricular_units_2nd_sem_without_evaluations -0.05
## unemployment_rate 0.05
## inflation_rate -0.01
## gdp 0.02
## curricular_units_1st_sem_grade
## previous_qualification_grade 0.06
## admission_grade 0.07
## age_at_enrollment -0.16
## curricular_units_1st_sem_credited 0.12
## curricular_units_1st_sem_enrolled 0.38
## curricular_units_1st_sem_evaluations 0.42
## curricular_units_1st_sem_approved 0.70
## curricular_units_1st_sem_grade 1.00
## curricular_units_1st_sem_without_evaluations -0.07
## curricular_units_2nd_sem_credited 0.11
## curricular_units_2nd_sem_enrolled 0.41
## curricular_units_2nd_sem_evaluations 0.49
## curricular_units_2nd_sem_approved 0.67
## curricular_units_2nd_sem_grade 0.84
## curricular_units_2nd_sem_without_evaluations -0.07
## unemployment_rate 0.01
## inflation_rate -0.03
## gdp 0.05
## curricular_units_1st_sem_without_evaluations
## previous_qualification_grade 0.00
## admission_grade 0.01
## age_at_enrollment 0.06
## curricular_units_1st_sem_credited 0.12
## curricular_units_1st_sem_enrolled 0.13
## curricular_units_1st_sem_evaluations 0.24
## curricular_units_1st_sem_approved -0.01
## curricular_units_1st_sem_grade -0.07
## curricular_units_1st_sem_without_evaluations 1.00
## curricular_units_2nd_sem_credited 0.12
## curricular_units_2nd_sem_enrolled 0.11
## curricular_units_2nd_sem_evaluations 0.14
## curricular_units_2nd_sem_approved -0.01
## curricular_units_2nd_sem_grade -0.06
## curricular_units_2nd_sem_without_evaluations 0.58
## unemployment_rate -0.05
## inflation_rate -0.05
## gdp -0.14
## curricular_units_2nd_sem_credited
## previous_qualification_grade -0.02
## admission_grade 0.04
## age_at_enrollment 0.21
## curricular_units_1st_sem_credited 0.94
## curricular_units_1st_sem_enrolled 0.75
## curricular_units_1st_sem_evaluations 0.52
## curricular_units_1st_sem_approved 0.61
## curricular_units_1st_sem_grade 0.11
## curricular_units_1st_sem_without_evaluations 0.12
## curricular_units_2nd_sem_credited 1.00
## curricular_units_2nd_sem_enrolled 0.68
## curricular_units_2nd_sem_evaluations 0.43
## curricular_units_2nd_sem_approved 0.52
## curricular_units_2nd_sem_grade 0.13
## curricular_units_2nd_sem_without_evaluations 0.07
## unemployment_rate 0.01
## inflation_rate 0.01
## gdp -0.02
## curricular_units_2nd_sem_enrolled
## previous_qualification_grade -0.03
## admission_grade -0.04
## age_at_enrollment 0.09
## curricular_units_1st_sem_credited 0.64
## curricular_units_1st_sem_enrolled 0.94
## curricular_units_1st_sem_evaluations 0.61
## curricular_units_1st_sem_approved 0.73
## curricular_units_1st_sem_grade 0.41
## curricular_units_1st_sem_without_evaluations 0.11
## curricular_units_2nd_sem_credited 0.68
## curricular_units_2nd_sem_enrolled 1.00
## curricular_units_2nd_sem_evaluations 0.60
## curricular_units_2nd_sem_approved 0.70
## curricular_units_2nd_sem_grade 0.40
## curricular_units_2nd_sem_without_evaluations 0.07
## unemployment_rate 0.06
## inflation_rate 0.02
## gdp -0.01
## curricular_units_2nd_sem_evaluations
## previous_qualification_grade -0.06
## admission_grade -0.06
## age_at_enrollment 0.06
## curricular_units_1st_sem_credited 0.43
## curricular_units_1st_sem_enrolled 0.60
## curricular_units_1st_sem_evaluations 0.78
## curricular_units_1st_sem_approved 0.54
## curricular_units_1st_sem_grade 0.49
## curricular_units_1st_sem_without_evaluations 0.14
## curricular_units_2nd_sem_credited 0.43
## curricular_units_2nd_sem_enrolled 0.60
## curricular_units_2nd_sem_evaluations 1.00
## curricular_units_2nd_sem_approved 0.46
## curricular_units_2nd_sem_grade 0.45
## curricular_units_2nd_sem_without_evaluations 0.14
## unemployment_rate 0.05
## inflation_rate -0.01
## gdp 0.00
## curricular_units_2nd_sem_approved
## previous_qualification_grade 0.05
## admission_grade 0.08
## age_at_enrollment -0.11
## curricular_units_1st_sem_credited 0.49
## curricular_units_1st_sem_enrolled 0.67
## curricular_units_1st_sem_evaluations 0.44
## curricular_units_1st_sem_approved 0.90
## curricular_units_1st_sem_grade 0.67
## curricular_units_1st_sem_without_evaluations -0.01
## curricular_units_2nd_sem_credited 0.52
## curricular_units_2nd_sem_enrolled 0.70
## curricular_units_2nd_sem_evaluations 0.46
## curricular_units_2nd_sem_approved 1.00
## curricular_units_2nd_sem_grade 0.76
## curricular_units_2nd_sem_without_evaluations -0.06
## unemployment_rate 0.05
## inflation_rate -0.02
## gdp 0.02
## curricular_units_2nd_sem_grade
## previous_qualification_grade 0.05
## admission_grade 0.07
## age_at_enrollment -0.17
## curricular_units_1st_sem_credited 0.13
## curricular_units_1st_sem_enrolled 0.36
## curricular_units_1st_sem_evaluations 0.36
## curricular_units_1st_sem_approved 0.69
## curricular_units_1st_sem_grade 0.84
## curricular_units_1st_sem_without_evaluations -0.06
## curricular_units_2nd_sem_credited 0.13
## curricular_units_2nd_sem_enrolled 0.40
## curricular_units_2nd_sem_evaluations 0.45
## curricular_units_2nd_sem_approved 0.76
## curricular_units_2nd_sem_grade 1.00
## curricular_units_2nd_sem_without_evaluations -0.08
## unemployment_rate 0.00
## inflation_rate -0.04
## gdp 0.07
## curricular_units_2nd_sem_without_evaluations
## previous_qualification_grade -0.02
## admission_grade -0.01
## age_at_enrollment 0.06
## curricular_units_1st_sem_credited 0.06
## curricular_units_1st_sem_enrolled 0.07
## curricular_units_1st_sem_evaluations 0.13
## curricular_units_1st_sem_approved -0.05
## curricular_units_1st_sem_grade -0.07
## curricular_units_1st_sem_without_evaluations 0.58
## curricular_units_2nd_sem_credited 0.07
## curricular_units_2nd_sem_enrolled 0.07
## curricular_units_2nd_sem_evaluations 0.14
## curricular_units_2nd_sem_approved -0.06
## curricular_units_2nd_sem_grade -0.08
## curricular_units_2nd_sem_without_evaluations 1.00
## unemployment_rate -0.01
## inflation_rate -0.03
## gdp -0.08
## unemployment_rate inflation_rate
## previous_qualification_grade 0.05 0.02
## admission_grade 0.04 -0.02
## age_at_enrollment 0.03 0.03
## curricular_units_1st_sem_credited 0.01 0.02
## curricular_units_1st_sem_enrolled 0.04 0.04
## curricular_units_1st_sem_evaluations 0.06 -0.01
## curricular_units_1st_sem_approved 0.05 -0.01
## curricular_units_1st_sem_grade 0.01 -0.03
## curricular_units_1st_sem_without_evaluations -0.05 -0.05
## curricular_units_2nd_sem_credited 0.01 0.01
## curricular_units_2nd_sem_enrolled 0.06 0.02
## curricular_units_2nd_sem_evaluations 0.05 -0.01
## curricular_units_2nd_sem_approved 0.05 -0.02
## curricular_units_2nd_sem_grade 0.00 -0.04
## curricular_units_2nd_sem_without_evaluations -0.01 -0.03
## unemployment_rate 1.00 -0.03
## inflation_rate -0.03 1.00
## gdp -0.34 -0.11
## gdp
## previous_qualification_grade -0.05
## admission_grade -0.02
## age_at_enrollment -0.06
## curricular_units_1st_sem_credited -0.03
## curricular_units_1st_sem_enrolled -0.03
## curricular_units_1st_sem_evaluations -0.10
## curricular_units_1st_sem_approved 0.02
## curricular_units_1st_sem_grade 0.05
## curricular_units_1st_sem_without_evaluations -0.14
## curricular_units_2nd_sem_credited -0.02
## curricular_units_2nd_sem_enrolled -0.01
## curricular_units_2nd_sem_evaluations 0.00
## curricular_units_2nd_sem_approved 0.02
## curricular_units_2nd_sem_grade 0.07
## curricular_units_2nd_sem_without_evaluations -0.08
## unemployment_rate -0.34
## inflation_rate -0.11
## gdp 1.00
Análisis: La matriz de correlación permite identificar variables que aportan información similar. Se observan correlaciones altas, sobre todo entre las variables de unidades curriculares de un mismo semestre (por ejemplo, las unidades inscritas, evaluadas y aprobadas tienden a moverse juntas) y entre el primer y segundo semestre. Esto tiene sentido: un estudiante con buen desempeño en el primer semestre suele mantenerlo en el segundo. Lo tenemos presente porque la redundancia entre predictores puede afectar a algunos modelos, aunque árboles y SVM la manejan razonablemente bien.
El escalado es obligatorio para KNN y SVM, ya que se basan en distancias: si una variable está en una escala de 0–200 (calificaciones) y otra entre 0 y 20 (promedios), la primera dominaría artificialmente el cálculo. Tal como indica el material del curso, cuando las escalas difieren sí hay que estandarizar. Estandarizamos (media 0, desviación 1) únicamente las variables numéricas; los factores no se escalan.
Para evitar fuga de información (data leakage), el escalado se ajustará más adelante usando solo el conjunto de entrenamiento. Aquí dejamos preparada una versión estandarizada de referencia para la exploración.
# Identificar columnas numéricas
num_cols <- Data_sel %>% select(where(is.numeric)) %>% names()
num_cols
## [1] "previous_qualification_grade"
## [2] "admission_grade"
## [3] "age_at_enrollment"
## [4] "curricular_units_1st_sem_credited"
## [5] "curricular_units_1st_sem_enrolled"
## [6] "curricular_units_1st_sem_evaluations"
## [7] "curricular_units_1st_sem_approved"
## [8] "curricular_units_1st_sem_grade"
## [9] "curricular_units_1st_sem_without_evaluations"
## [10] "curricular_units_2nd_sem_credited"
## [11] "curricular_units_2nd_sem_enrolled"
## [12] "curricular_units_2nd_sem_evaluations"
## [13] "curricular_units_2nd_sem_approved"
## [14] "curricular_units_2nd_sem_grade"
## [15] "curricular_units_2nd_sem_without_evaluations"
## [16] "unemployment_rate"
## [17] "inflation_rate"
## [18] "gdp"
Siguiendo la práctica del curso, dividimos los datos de forma
aleatoria en un conjunto de entrenamiento (80%) para ajustar los modelos
y uno de prueba (20%) para evaluar su desempeño. Usamos
set.seed() para que los resultados sean reproducibles y
createDataPartition() de caret, que mantiene
la proporción de clases en ambos conjuntos (partición
estratificada).
set.seed(2025)
indice <- createDataPartition(Data_sel$target, p = 0.80, list = FALSE)
entrenamiento <- Data_sel[indice, ]
prueba <- Data_sel[-indice, ]
# Verificamos que se mantiene la proporción de clases
prop.table(table(entrenamiento$target))
##
## No_deserta Deserta
## 0.6788136 0.3211864
prop.table(table(prueba$target))
##
## No_deserta Deserta
## 0.678733 0.321267
Análisis: Las proporciones de clases en entrenamiento y prueba son prácticamente idénticas (alrededor de 68% “No deserta” y 32% “Deserta” en ambos), lo que confirma que la partición estratificada funcionó. Esto es importante porque garantiza que el conjunto de prueba sea representativo de la distribución real y que la evaluación del modelo no se vea distorsionada por una división desbalanceada.
Estandarizamos las variables numéricas aprendiendo la media y la desviación solo del conjunto de entrenamiento, y aplicamos esa misma transformación a la prueba.
# Parámetros de estandarización aprendidos del entrenamiento
preproc <- preProcess(entrenamiento[, num_cols], method = c("center", "scale"))
entrenamiento_esc <- entrenamiento
prueba_esc <- prueba
entrenamiento_esc[, num_cols] <- predict(preproc, entrenamiento[, num_cols])
prueba_esc[, num_cols] <- predict(preproc, prueba[, num_cols])
# Comprobación: las numéricas del entrenamiento ahora tienen media ~0
round(colMeans(entrenamiento_esc[, num_cols]), 3)
## previous_qualification_grade
## 0
## admission_grade
## 0
## age_at_enrollment
## 0
## curricular_units_1st_sem_credited
## 0
## curricular_units_1st_sem_enrolled
## 0
## curricular_units_1st_sem_evaluations
## 0
## curricular_units_1st_sem_approved
## 0
## curricular_units_1st_sem_grade
## 0
## curricular_units_1st_sem_without_evaluations
## 0
## curricular_units_2nd_sem_credited
## 0
## curricular_units_2nd_sem_enrolled
## 0
## curricular_units_2nd_sem_evaluations
## 0
## curricular_units_2nd_sem_approved
## 0
## curricular_units_2nd_sem_grade
## 0
## curricular_units_2nd_sem_without_evaluations
## 0
## unemployment_rate
## 0
## inflation_rate
## 0
## gdp
## 0
Análisis: La comprobación muestra que todas las variables numéricas del entrenamiento tienen ahora media aproximadamente cero (y desviación uno), lo que confirma que la estandarización se aplicó correctamente. Es clave haber aprendido la media y la desviación únicamente del entrenamiento y luego aplicarlas a la prueba: si hubiéramos usado toda la base para calcular esos parámetros, habríamos filtrado información del conjunto de prueba al de entrenamiento (data leakage), inflando artificialmente el desempeño aparente del modelo.
Para tratar el desbalance moderado (≈68/32) aplicamos SMOTE, que genera ejemplos sintéticos de la clase minoritaria (“Deserta”) en el conjunto de entrenamiento. Es importante balancear solo el entrenamiento, nunca la prueba, que debe reflejar la distribución real.
SMOTE opera sobre variables numéricas (calcula vecinos por
distancia), así que primero codificamos las variables categóricas
restantes con one-hot (step_dummy). Esta
codificación es además el paso de preprocesamiento que requieren SVM y
KNN para manejar factores. Construimos así una versión escalada,
codificada y balanceada del entrenamiento que usarán los modelos basados
en distancias (SVM).
set.seed(2025)
# Receta: one-hot de las categóricas + SMOTE sobre la clase objetivo
receta <- recipes::recipe(target ~ ., data = entrenamiento_esc) %>%
recipes::step_dummy(recipes::all_nominal_predictors()) %>%
themis::step_smote(target)
entrenamiento_bal <- receta %>%
recipes::prep() %>%
recipes::bake(new_data = NULL)
# Aplicamos la MISMA receta (sin SMOTE) para codificar la prueba de forma consistente
prueba_cod <- receta %>%
recipes::prep() %>%
recipes::bake(new_data = prueba_esc)
# Distribución balanceada
table(entrenamiento_bal$target)
##
## No_deserta Deserta
## 2403 2403
Análisis: Tras aplicar SMOTE, las dos clases del conjunto de entrenamiento quedaron igualadas en número. SMOTE no simplemente duplica observaciones de la clase minoritaria, sino que genera ejemplos sintéticos nuevos interpolando entre estudiantes reales que desertaron y sus vecinos más cercanos. Así el modelo aprende con suficientes ejemplos de ambas clases y no se sesga hacia “No deserta”. Recalcamos que esto se hace solo en el entrenamiento: el conjunto de prueba conserva su proporción real (68/32) para que la evaluación refleje lo que pasaría con datos nuevos.
Seguimos la estructura de cinco pasos del curso. El árbol de decisión
es nuestro modelo interpretable: además de predecir, nos muestra qué
variables separan mejor a los estudiantes que desertan. Usamos el
algoritmo CART mediante rpart(). Como los árboles manejan
variables mixtas y no requieren escalado, los entrenamos sobre los datos
sin estandarizar (pero sí balanceados).
# El árbol y Naive Bayes manejan factores de forma nativa y no requieren escalado,
# pero SMOTE sí necesita predictores numéricos. Por ello, para estos modelos usamos
# una versión balanceada con one-hot pero SIN escalar las numéricas.
set.seed(2025)
receta_ne <- recipes::recipe(target ~ ., data = entrenamiento) %>%
recipes::step_dummy(recipes::all_nominal_predictors()) %>%
themis::step_smote(target)
entrenamiento_bal_ne <- receta_ne %>% recipes::prep() %>% recipes::bake(new_data = NULL)
prueba_ne <- receta_ne %>% recipes::prep() %>% recipes::bake(new_data = prueba)
modelo_arbol <- rpart(target ~ ., data = entrenamiento_bal_ne, method = "class")
modelo_arbol
## n= 4806
##
## node), split, n, loss, yval, (yprob)
## * denotes terminal node
##
## 1) root 4806 2403 No_deserta (0.5000000 0.5000000)
## 2) curricular_units_2nd_sem_approved>=3.996111 2635 559 No_deserta (0.7878558 0.2121442)
## 4) tuition_fees_up_to_date_X1>=0.9951792 2394 364 No_deserta (0.8479532 0.1520468) *
## 5) tuition_fees_up_to_date_X1< 0.9951792 241 46 Deserta (0.1908714 0.8091286) *
## 3) curricular_units_2nd_sem_approved< 3.996111 2171 327 Deserta (0.1506218 0.8493782) *
# Visualización del árbol
rpart.plot(modelo_arbol)
Análisis: El árbol muestra cómo se van separando los estudiantes. Cada nodo es una regla y cada hoja indica la clase predicha con la proporción de casos. La variable de la raíz —el primer corte y la más importante— es curricular_units_2nd_sem_approved, con un punto de corte en 4 unidades aprobadas. El árbol quedó bien simple: los estudiantes que aprobaron menos de 4 unidades en el segundo semestre (45% de los casos) caen directo en “Deserta” con probabilidad 0.85. Los que aprobaron 4 o más (55%) se refinan con una segunda regla sobre tuition_fees_up_to_date: si tienen la matrícula al día, son “No_deserta” (prob 0.15 de desertar); si no la tienen al día, aunque hayan aprobado suficientes unidades, pasan a “Deserta” con prob 0.81. Lo interesante es que con solo dos variables el árbol ya captura lo esencial: lo que primero anuncia la deserción es lo que el estudiante aprueba, no la nota, y después pesa la situación financiera. Como ambas se observan al cierre del segundo semestre, el modelo serviría para marcar tempranamente a los estudiantes en riesgo.
set.seed(2025)
control_cv <- trainControl(method = "cv", number = 10)
arbol_cv <- train(target ~ ., data = entrenamiento_bal_ne,
method = "rpart",
trControl = control_cv)
arbol_cv
## CART
##
## 4806 samples
## 54 predictor
## 2 classes: 'No_deserta', 'Deserta'
##
## No pre-processing
## Resampling: Cross-Validated (10 fold)
## Summary of sample sizes: 4326, 4326, 4325, 4325, 4326, 4324, ...
## Resampling results across tuning parameters:
##
## cp Accuracy Kappa
## 0.008114856 0.8481122 0.6962230
## 0.062005826 0.8252249 0.6504416
## 0.631294216 0.6529338 0.3060674
##
## Accuracy was used to select the optimal model using the largest value.
## The final value used for the model was cp = 0.008114856.
Análisis: La validación cruzada de 10 particiones entrena y evalúa el
árbol diez veces con distintos subconjuntos, lo que da una estimación
más estable de su desempeño que una sola partición. La salida reporta la
exactitud promedio a lo largo de los pliegues para distintos valores del
parámetro de complejidad (cp); caret elige
automáticamente el que mejor resultado da. Que la exactitud sea
consistente entre pliegues indica que el modelo es estable y no depende
de una división particular de los datos.
# Predicción sobre el conjunto de prueba (codificado igual que el entrenamiento)
pred_arbol <- predict(modelo_arbol, prueba_ne, type = "class")
# Matriz de confusión y métricas (clase positiva = "Deserta")
mc_arbol <- confusionMatrix(pred_arbol, prueba_ne$target, positive = "Deserta")
mc_arbol
## Confusion Matrix and Statistics
##
## Reference
## Prediction No_deserta Deserta
## No_deserta 506 52
## Deserta 94 232
##
## Accuracy : 0.8348
## 95% CI : (0.8087, 0.8587)
## No Information Rate : 0.6787
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 0.6355
##
## Mcnemar's Test P-Value : 0.0006909
##
## Sensitivity : 0.8169
## Specificity : 0.8433
## Pos Pred Value : 0.7117
## Neg Pred Value : 0.9068
## Prevalence : 0.3213
## Detection Rate : 0.2624
## Detection Prevalence : 0.3688
## Balanced Accuracy : 0.8301
##
## 'Positive' Class : Deserta
##
Análisis: La matriz de confusión compara las predicciones contra las clases reales del conjunto de prueba. Los resultados del árbol son: exactitud de 0.835, sensibilidad de 0.817 sobre “Deserta” (de los estudiantes que realmente desertaron, el modelo detectó al 82%), especificidad de 0.843 y Kappa de 0.636 (corrige el azar). Son métricas sólidas y, lo más importante, la sensibilidad, que es nuestro objetivo principal, quedó alta. El árbol, además de predecir, nos dice qué variables señalan el riesgo de abandono, el número de unidades aprobadas en el segundo semestre y el estado de la matrícula al día, lo que conecta directamente con el valor práctico del proyecto: identificar tempranamente a los estudiantes en riesgo.
El SVM es nuestro modelo de alto rendimiento. Busca el hiperplano que
mejor separa las clases maximizando el margen. Requiere datos escalados,
por lo que usamos el conjunto estandarizado y balanceado. Probamos el
kernel radial (gaussiano), que es flexible y recomendado, ajustando los
hiperparámetros cost y gamma por validación
cruzada con tune().
set.seed(2025)
# Búsqueda de los mejores hiperparámetros por validación cruzada
svm_tune <- tune(svm, target ~ ., data = entrenamiento_bal,
kernel = "radial",
ranges = list(cost = c(0.1, 1, 10),
gamma = c(0.01, 0.05, 0.1)))
summary(svm_tune)
##
## Parameter tuning of 'svm':
##
## - sampling method: 10-fold cross validation
##
## - best parameters:
## cost gamma
## 10 0.1
##
## - best performance: 0.1069473
##
## - Detailed performance results:
## cost gamma error dispersion
## 1 0.1 0.01 0.1685343 0.02164289
## 2 1.0 0.01 0.1421063 0.02010642
## 3 10.0 0.01 0.1248337 0.02409649
## 4 0.1 0.05 0.1960001 0.02858461
## 5 1.0 0.05 0.1281666 0.02214884
## 6 10.0 0.05 0.1133948 0.01556360
## 7 0.1 0.10 0.2374116 0.02774403
## 8 1.0 0.10 0.1327430 0.02197539
## 9 10.0 0.10 0.1069473 0.01698043
# Mejor modelo encontrado
modelo_svm <- svm_tune$best.model
modelo_svm
##
## Call:
## best.tune(METHOD = svm, train.x = target ~ ., data = entrenamiento_bal,
## ranges = list(cost = c(0.1, 1, 10), gamma = c(0.01, 0.05, 0.1)),
## kernel = "radial")
##
##
## Parameters:
## SVM-Type: C-classification
## SVM-Kernel: radial
## cost: 10
##
## Number of Support Vectors: 2646
Análisis: La función tune() probó las 9 combinaciones de cost y gamma con validación cruzada de 10 pliegues y eligió la que produjo menor error. El parámetro cost controla cuánto penaliza el modelo los errores de clasificación (un valor alto ajusta más a los datos de entrenamiento, con riesgo de sobreajuste), y gamma controla la flexibilidad de la frontera del kernel radial. Los valores óptimos fueron cost = 10 y gamma = 0.1, con un error de CV de 0.107 (≈89.3% de exactitud). El modelo final usa 2,646 vectores de soporte, que es bastante (más de la mitad del entrenamiento balanceado), señal de que la frontera entre desertores y no desertores no es sencilla. Las tres mejores combinaciones del grid usaron todas cost = 10, así que el modelo tiende a preferir el extremo más flexible del rango probado.
# Predicción sobre el conjunto de prueba (codificado y escalado igual que el entrenamiento)
pred_svm <- predict(modelo_svm, prueba_cod)
mc_svm <- confusionMatrix(pred_svm, prueba_cod$target, positive = "Deserta")
mc_svm
## Confusion Matrix and Statistics
##
## Reference
## Prediction No_deserta Deserta
## No_deserta 537 84
## Deserta 63 200
##
## Accuracy : 0.8337
## 95% CI : (0.8075, 0.8577)
## No Information Rate : 0.6787
## P-Value [Acc > NIR] : < 2e-16
##
## Kappa : 0.6111
##
## Mcnemar's Test P-Value : 0.09903
##
## Sensitivity : 0.7042
## Specificity : 0.8950
## Pos Pred Value : 0.7605
## Neg Pred Value : 0.8647
## Prevalence : 0.3213
## Detection Rate : 0.2262
## Detection Prevalence : 0.2975
## Balanced Accuracy : 0.7996
##
## 'Positive' Class : Deserta
##
Análisis: Evaluamos el SVM sobre el mismo conjunto de prueba, leyendo las mismas métricas que en el árbol. El SVM quedó prácticamente empatado en exactitud con el árbol (0.834 vs 0.835) y le gana en especificidad (0.895 vs 0.843), es decir, se equivoca menos al clasificar a los que no desertan. Sin embargo, pierde claramente en lo que más nos importa: la sensibilidad sobre “Deserta” baja a 0.704 (vs 0.817 del árbol) y el Kappa también es menor (0.611 vs 0.636). En la práctica, el SVM deja escapar a más estudiantes en riesgo que el árbol. Su otra desventaja es que no es interpretable: nos dice qué tan bien clasifica, pero no qué variables pesan más.
Incluimos el clasificador bayesiano como modelo de referencia: es
simple y rápido, y nos da un “piso” contra el cual comparar si los
modelos principales realmente aportan. Usamos naiveBayes()
de e1071. Naive Bayes no requiere escalado, así que lo
entrenamos sobre los datos balanceados sin estandarizar.
modelo_nb <- naiveBayes(target ~ ., data = entrenamiento_bal_ne)
modelo_nb
##
## Naive Bayes Classifier for Discrete Predictors
##
## Call:
## naiveBayes.default(x = X, y = Y, laplace = laplace)
##
## A-priori probabilities:
## Y
## No_deserta Deserta
## 0.5 0.5
##
## Conditional probabilities:
## previous_qualification_grade
## Y [,1] [,2]
## No_deserta 133.3527 13.31667
## Deserta 131.0747 12.42348
##
## admission_grade
## Y [,1] [,2]
## No_deserta 128.1117 14.24938
## Deserta 124.7679 14.85939
##
## age_at_enrollment
## Y [,1] [,2]
## No_deserta 21.91178 6.556591
## Deserta 26.05978 8.555611
##
## curricular_units_1st_sem_credited
## Y [,1] [,2]
## No_deserta 0.7473991 2.463190
## Deserta 0.5683790 1.991848
##
## curricular_units_1st_sem_enrolled
## Y [,1] [,2]
## No_deserta 6.474407 2.52903
## Deserta 5.790891 2.22258
##
## curricular_units_1st_sem_evaluations
## Y [,1] [,2]
## No_deserta 8.507283 3.772718
## Deserta 7.592996 4.770764
##
## curricular_units_1st_sem_approved
## Y [,1] [,2]
## No_deserta 5.720766 2.637212
## Deserta 2.516183 2.777715
##
## curricular_units_1st_sem_grade
## Y [,1] [,2]
## No_deserta 12.229220 3.132708
## Deserta 7.160713 6.009635
##
## curricular_units_1st_sem_without_evaluations
## Y [,1] [,2]
## No_deserta 0.1215148 0.6872569
## Deserta 0.1866754 0.7445521
##
## curricular_units_2nd_sem_credited
## Y [,1] [,2]
## No_deserta 0.5867665 2.025921
## Deserta 0.4175101 1.583446
##
## curricular_units_2nd_sem_enrolled
## Y [,1] [,2]
## No_deserta 6.446109 2.241068
## Deserta 5.764967 2.026036
##
## curricular_units_2nd_sem_evaluations
## Y [,1] [,2]
## No_deserta 8.446109 3.436941
## Deserta 7.095341 4.646489
##
## curricular_units_2nd_sem_approved
## Y [,1] [,2]
## No_deserta 5.628797 2.441564
## Deserta 1.948073 2.547710
##
## curricular_units_2nd_sem_grade
## Y [,1] [,2]
## No_deserta 12.269340 3.109259
## Deserta 5.817888 6.064180
##
## curricular_units_2nd_sem_without_evaluations
## Y [,1] [,2]
## No_deserta 0.1152726 0.6292448
## Deserta 0.2260237 0.9482179
##
## unemployment_rate
## Y [,1] [,2]
## No_deserta 11.55339 2.612305
## Deserta 11.61329 2.604153
##
## inflation_rate
## Y [,1] [,2]
## No_deserta 1.181024 1.362764
## Deserta 1.271565 1.277339
##
## gdp
## Y [,1] [,2]
## No_deserta 0.07779442 2.269258
## Deserta -0.08526412 2.114802
##
## marital_status_X2
## Y [,1] [,2]
## No_deserta 0.06741573 0.2507928
## Deserta 0.12143127 0.3028574
##
## marital_status_X3
## Y [,1] [,2]
## No_deserta 0.0008322930 0.02884348
## Deserta 0.0006639305 0.02312068
##
## marital_status_X4
## Y [,1] [,2]
## No_deserta 0.01456513 0.1198289
## Deserta 0.02894496 0.1536568
##
## marital_status_X5
## Y [,1] [,2]
## No_deserta 0.005409904 0.07336809
## Deserta 0.007416647 0.07700707
##
## marital_status_X6
## Y [,1] [,2]
## No_deserta 0.000832293 0.02884348
## Deserta 0.004173146 0.05959651
##
## application_order_X1
## Y [,1] [,2]
## No_deserta 0.6562630 0.4750535
## Deserta 0.7458793 0.4072738
##
## application_order_X2
## Y [,1] [,2]
## No_deserta 0.1323346 0.3389247
## Deserta 0.1044371 0.2784938
##
## application_order_X3
## Y [,1] [,2]
## No_deserta 0.07490637 0.2632950
## Deserta 0.05607889 0.2104322
##
## application_order_X4
## Y [,1] [,2]
## No_deserta 0.06575114 0.2478982
## Deserta 0.03223103 0.1619929
##
## application_order_X5
## Y [,1] [,2]
## No_deserta 0.03412401 0.1815855
## Deserta 0.03907720 0.1770949
##
## application_order_X6
## Y [,1] [,2]
## No_deserta 0.03578860 0.1858013
## Deserta 0.02229644 0.1359753
##
## application_order_X9
## Y [,1] [,2]
## No_deserta 0.0004161465 0.02039967
## Deserta 0.0000000000 0.00000000
##
## daytime_evening_attendance_X1
## Y [,1] [,2]
## No_deserta 0.9146900 0.2794006
## Deserta 0.8482256 0.3294783
##
## previous_qualification_X2
## Y [,1] [,2]
## No_deserta 0.002496879 0.04991674
## Deserta 0.012236933 0.09868037
##
## previous_qualification_X3
## Y [,1] [,2]
## No_deserta 0.01955888 0.1385075
## Deserta 0.05492025 0.2103415
##
## previous_qualification_X4
## Y [,1] [,2]
## No_deserta 0.001664586 0.04077385
## Deserta 0.002834018 0.04979535
##
## previous_qualification_X5
## Y [,1] [,2]
## No_deserta 0.0000000000 0.00000000
## Deserta 0.0007153176 0.02511919
##
## previous_qualification_X6
## Y [,1] [,2]
## No_deserta 0.002496879 0.04991674
## Deserta 0.002248061 0.04474570
##
## previous_qualification_X9
## Y [,1] [,2]
## No_deserta 0.000000000 0.00000000
## Deserta 0.009752115 0.09098559
##
## previous_qualification_X10
## Y [,1] [,2]
## No_deserta 0.0004161465 0.02039967
## Deserta 0.0017233351 0.03651086
##
## previous_qualification_X12
## Y [,1] [,2]
## No_deserta 0.007490637 0.08624165
## Deserta 0.015156433 0.11194319
##
## previous_qualification_X14
## Y [,1] [,2]
## No_deserta 0.000000000 0.00000000
## Deserta 0.001048245 0.02879635
##
## previous_qualification_X15
## Y [,1] [,2]
## No_deserta 0.0004161465 0.02039967
## Deserta 0.0006029773 0.02235776
##
## previous_qualification_X19
## Y [,1] [,2]
## No_deserta 0.02039118 0.1413637
## Deserta 0.07689431 0.2452993
##
## previous_qualification_X38
## Y [,1] [,2]
## No_deserta 0.001248439 0.03531855
## Deserta 0.002167010 0.04361842
##
## previous_qualification_X39
## Y [,1] [,2]
## No_deserta 0.04993758 0.2178614
## Deserta 0.05156583 0.2050654
##
## previous_qualification_X40
## Y [,1] [,2]
## No_deserta 0.009155223 0.09526375
## Deserta 0.009373331 0.08889045
##
## previous_qualification_X42
## Y [,1] [,2]
## No_deserta 0.009155223 0.09526375
## Deserta 0.003192263 0.05079044
##
## previous_qualification_X43
## Y [,1] [,2]
## No_deserta 0.001664586 0.04077385
## Deserta 0.002107929 0.04255907
##
## displaced_X1
## Y [,1] [,2]
## No_deserta 0.5892634 0.4920699
## Deserta 0.4758085 0.4650160
##
## educational_special_needs_X1
## Y [,1] [,2]
## No_deserta 0.01123596 0.10542454
## Deserta 0.01168099 0.09859056
##
## debtor_X1
## Y [,1] [,2]
## No_deserta 0.06408656 0.2449580
## Deserta 0.21646612 0.3774315
##
## tuition_fees_up_to_date_X1
## Y [,1] [,2]
## No_deserta 0.9758635 0.1535048
## Deserta 0.6886040 0.4235222
##
## gender_X1
## Y [,1] [,2]
## No_deserta 0.2825635 0.4503396
## Deserta 0.4822578 0.4596588
##
## scholarship_holder_X1
## Y [,1] [,2]
## No_deserta 0.31793591 0.4657713
## Deserta 0.09608417 0.2702242
##
## international_X1
## Y [,1] [,2]
## No_deserta 0.02580108 0.1585744
## Deserta 0.02167166 0.1330797
Análisis: El output muestra las probabilidades a-priori (0.5 y 0.5, porque los datos están balanceados por SMOTE) y las probabilidades condicionales de cada variable dado el valor de target. Para las numéricas, R reporta media [,1] y desviación estándar [,2] por clase; para las categóricas, la probabilidad de cada categoría. Esto es lo que el modelo usa internamente, vía el teorema de Bayes, para clasificar nuevos estudiantes.
pred_nb <- predict(modelo_nb, prueba_ne)
mc_nb <- confusionMatrix(pred_nb, prueba_ne$target, positive = "Deserta")
mc_nb
## Confusion Matrix and Statistics
##
## Reference
## Prediction No_deserta Deserta
## No_deserta 556 127
## Deserta 44 157
##
## Accuracy : 0.8066
## 95% CI : (0.779, 0.8321)
## No Information Rate : 0.6787
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 0.5195
##
## Mcnemar's Test P-Value : 3.594e-10
##
## Sensitivity : 0.5528
## Specificity : 0.9267
## Pos Pred Value : 0.7811
## Neg Pred Value : 0.8141
## Prevalence : 0.3213
## Detection Rate : 0.1776
## Detection Prevalence : 0.2274
## Balanced Accuracy : 0.7397
##
## 'Positive' Class : Deserta
##
Análisis: Naive Bayes es nuestro modelo de referencia (baseline). Su supuesto de que todas las variables son independientes entre sí rara vez se cumple del todo (vimos en la matriz de correlación que algunas están relacionadas), por lo que esperábamos que no fuera el mejor clasificador. Eso se confirma: con exactitud de 0.807, Kappa de 0.519 y sensibilidad de apenas 0.553, queda por debajo del árbol y del SVM en casi todas las métricas. Solo gana en especificidad (0.927), pero porque se inclina hacia predecir “No_deserta”. Detecta poco más de la mitad de los estudiantes que sí desertan, mientras el árbol detecta cuatro de cada cinco. Esto valida que el árbol y el SVM sí aportan valor por encima del piso del baseline.
Reunimos las métricas clave de los tres modelos. Dado el desbalance, no nos quedamos solo con la exactitud: priorizamos la sensibilidad (capacidad de detectar a los estudiantes que sí desertan, que es el objetivo práctico), el índice Kappa y la exactitud balanceada. Como advierte el material del curso, un Kappa bajo con exactitud alta indicaría un modelo sesgado hacia la clase mayoritaria.
resultados <- data.frame(
Modelo = c("Árbol de decisión", "SVM (radial)", "Naive Bayes (baseline)"),
Exactitud = c(mc_arbol$overall["Accuracy"],
mc_svm$overall["Accuracy"],
mc_nb$overall["Accuracy"]),
Kappa = c(mc_arbol$overall["Kappa"],
mc_svm$overall["Kappa"],
mc_nb$overall["Kappa"]),
Sensibilidad = c(mc_arbol$byClass["Sensitivity"],
mc_svm$byClass["Sensitivity"],
mc_nb$byClass["Sensitivity"]),
Especificidad = c(mc_arbol$byClass["Specificity"],
mc_svm$byClass["Specificity"],
mc_nb$byClass["Specificity"]),
Exactitud_balanceada = c(mc_arbol$byClass["Balanced Accuracy"],
mc_svm$byClass["Balanced Accuracy"],
mc_nb$byClass["Balanced Accuracy"])
)
resultados %>% mutate(across(where(is.numeric), ~round(.x, 4)))
## Modelo Exactitud Kappa Sensibilidad Especificidad
## 1 Árbol de decisión 0.8348 0.6355 0.8169 0.8433
## 2 SVM (radial) 0.8337 0.6111 0.7042 0.8950
## 3 Naive Bayes (baseline) 0.8066 0.5195 0.5528 0.9267
## Exactitud_balanceada
## 1 0.8301
## 2 0.7996
## 3 0.7397
Análisis: Esta tabla reúne las métricas de los tres modelos en un solo lugar para compararlos directamente. Recordando el desbalance, no basta con mirar la exactitud: un modelo podría tener exactitud alta simplemente prediciendo siempre “No deserta”. Por eso miramos sobre todo la sensibilidad (detectar a los que sí desertan) y el Kappa. Mirando ambas, el árbol de decisión es el ganador: tiene la mejor sensibilidad (0.817), el mejor Kappa (0.636) y la mejor exactitud balanceada (0.830) de los tres modelos. El SVM queda muy cerca en exactitud global pero pierde 11 puntos en sensibilidad, y el Naive Bayes confirma su rol de baseline quedando claramente por debajo. El árbol será entonces el modelo seleccionado.
# Visualización comparativa
p_comp <- resultados %>%
pivot_longer(-Modelo, names_to = "Metrica", values_to = "Valor") %>%
ggplot(aes(x = Metrica, y = Valor, fill = Modelo,
text = paste(Modelo, "<br>", Metrica, ":", round(Valor, 3)))) +
geom_col(position = "dodge") +
coord_cartesian(ylim = c(0, 1)) +
labs(title = "Comparación de métricas entre modelos",
x = "", y = "Valor") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 30, hjust = 1))
ggplotly(p_comp, tooltip = "text")
A partir de la comparación, se selecciona como mejor modelo el árbol de decisión. Aunque el SVM logra una exactitud y una especificidad ligeramente mayores, el árbol detecta a un 11% más de los estudiantes que efectivamente desertan (sensibilidad de 0.817 vs 0.704 del SVM), métrica que priorizamos por su orientación práctica: el costo de no detectar a un estudiante en riesgo es mucho mayor que el de marcar a uno que finalmente no abandona. Además, el árbol tiene el mejor Kappa (0.636) y la mejor exactitud balanceada (0.830) de los tres modelos. A esa ventaja métrica se suma su interpretabilidad: el modelo se puede presentar al área académica como un conjunto de reglas simples y accionables.
Las variables más predictivas según el árbol son, en orden: el número de unidades curriculares aprobadas en el segundo semestre (variable de la raíz, que por sí sola separa al 45% de la muestra que va directo a “Deserta”) y el estado de la matrícula al día (tuition_fees_up_to_date), que entra como segundo corte para los estudiantes que sí aprobaron suficientes unidades. Es decir, el riesgo de deserción se anuncia mucho más por lo que el estudiante aprueba que por la nota con que aprueba, y la situación financiera con la institución es un segundo eje claro.
Implicaciones prácticas: El modelo permite construir un sistema de alerta temprana al cierre del segundo semestre: marcar a los estudiantes que aprobaron menos de 4 unidades para ingresarlos a un programa de acompañamiento académico (tutorías, ajuste de carga, orientación) antes de que la deserción se consume. La señal de matrícula impaga sugiere además que las intervenciones académicas son insuficientes si no se acompañan de rutas de apoyo financiero —becas, planes de pago, conexión con servicios estudiantiles—. Que las variables del segundo semestre dominen sobre las del primero es coherente con que la deserción sea típicamente una decisión que se cristaliza al cierre del primer año, lo cual da una ventana de intervención clara entre el segundo semestre y el inicio del segundo año. Finalmente, vale recordar que los datos provienen de una institución portuguesa, por lo que trasladar los umbrales específicos (las 4 unidades, por ejemplo) al contexto local requeriría reentrenar el modelo con datos institucionales propios; lo transferible no son los umbrales sino la estructura del problema y la jerarquía de variables.
Sobre la elección de modelos. Se eligieron tres de los cinco métodos vistos en clase porque el objetivo era resolver el problema con un conjunto representativo y diverso de enfoques: basado en reglas (árbol), basado en márgenes (SVM) y basado en probabilidad (Naive Bayes como baseline). Se descartaron KNN y redes neuronales: el primero por ser redundante con el SVM como referencia basada en distancias, y las redes neuronales porque en datos tabulares de unos pocos miles de observaciones rara vez superan a un árbol o un SVM bien ajustado, a un costo de entrenamiento e interpretabilidad mucho mayor.
Marco local (Universidad de Puerto Rico). Las tasas de retención y graduación por cohorte publicadas por la institución muestran que la mayor caída en la retención ocurre justamente entre el primer y el segundo año, lo cual es consistente con que las variables de rendimiento del primer y segundo semestre sean las más predictivas de la deserción en este modelo. Esto motiva el problema y le da relevancia local al análisis.