Verificado con la versión final
############################################################
# TESIS DE ESTADÍSTICA - SCRIPT INTEGRADO Y DEPURADO
# Predicción del riesgo de deserción universitaria
# Área de Matemáticas - FACEN-UNA
############################################################
rm(list = ls())
gc()
## used (Mb) gc trigger (Mb) max used (Mb)
## Ncells 625501 33.5 1330855 71.1 957620 51.2
## Vcells 1202296 9.2 8388608 64.0 1927711 14.8
options(scipen = 999)
set.seed(123)
############################################################
# 1. LIBRERÍAS
############################################################
paquetes <- c(
"readxl", "dplyr", "ggplot2", "caret", "pROC", "rpart", "rpart.plot",
"randomForest", "e1071", "nnet", "naivebayes", "openxlsx", "tidyr",
"car", "ResourceSelection"
)
instalar_si_falta <- function(pkg) {
if (!require(pkg, character.only = TRUE)) {
install.packages(pkg, dependencies = TRUE)
library(pkg, character.only = TRUE)
}
}
invisible(lapply(paquetes, instalar_si_falta))
## Cargando paquete requerido: readxl
## Cargando paquete requerido: dplyr
##
## Adjuntando el paquete: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
## Cargando paquete requerido: ggplot2
## Cargando paquete requerido: caret
## Cargando paquete requerido: lattice
## Cargando paquete requerido: pROC
## Type 'citation("pROC")' for a citation.
##
## Adjuntando el paquete: 'pROC'
## The following objects are masked from 'package:stats':
##
## cov, smooth, var
## Cargando paquete requerido: rpart
## Cargando paquete requerido: rpart.plot
## Cargando paquete requerido: randomForest
## randomForest 4.7-1.2
## Type rfNews() to see new features/changes/bug fixes.
##
## Adjuntando el paquete: 'randomForest'
## The following object is masked from 'package:ggplot2':
##
## margin
## The following object is masked from 'package:dplyr':
##
## combine
## Cargando paquete requerido: e1071
##
## Adjuntando el paquete: 'e1071'
## The following object is masked from 'package:ggplot2':
##
## element
## Cargando paquete requerido: nnet
## Cargando paquete requerido: naivebayes
## naivebayes 1.0.0 loaded
## For more information please visit:
## https://majkamichal.github.io/naivebayes/
## Cargando paquete requerido: openxlsx
## Cargando paquete requerido: tidyr
## Cargando paquete requerido: car
## Cargando paquete requerido: carData
##
## Adjuntando el paquete: 'car'
## The following object is masked from 'package:dplyr':
##
## recode
## Cargando paquete requerido: ResourceSelection
## ResourceSelection 0.3-6 2023-06-27
############################################################
# 2. CARGA DE DATOS
############################################################
# Ajustar esta ruta según tu equipo
ruta_datos <- "desercion.xlsx"
tesis <- read_excel(ruta_datos)
tesis <- as.data.frame(tesis, stringsAsFactors = FALSE)
# Carpeta de salidas
dir.create("salidas_tesis", showWarnings = FALSE, recursive = TRUE)
############################################################
# 3. LIMPIEZA BÁSICA Y NORMALIZACIÓN DE TEXTO
############################################################
cols_char <- names(tesis)[sapply(tesis, is.character)]
tesis[cols_char] <- lapply(tesis[cols_char], function(y) {
y <- as.character(y)
y <- enc2utf8(y)
y <- iconv(y, from = "", to = "UTF-8", sub = "")
y <- trimws(y)
y[is.na(y)] <- ""
y
})
############################################################
# 4. RECODIFICACIÓN DE VARIABLES
############################################################
# Variable respuesta: 1 = Desertor ; 0 = No_Desertor
tesis$ESTADO_ACADEMICO <- factor(
tesis$ESTADO_ACADEMICO,
levels = c(1, 0),
labels = c("Desertor", "No_Desertor")
)
tesis$SEXO <- factor(
tesis$SEXO,
levels = c("F", "M"),
labels = c("FEMENINO", "MASCULINO")
)
tesis$CARRERA <- factor(
tesis$CARRERA,
levels = c("2009E", "09MED", "2009P", "010EM", "010EMD"),
labels = c(
"ESTADISTICA-PRES",
"ESTADISTICA-SEMI",
"MATEMATICA",
"EDUCACION MATEMATICA-PRES",
"EDUCACION MATEMATICA-SEMI"
)
)
tesis$TIPO_INGRESO <- factor(
tesis$TIPO_INGRESO,
levels = c("INGRESO", "TRASLADO", "ADMISION DIRECTA"),
labels = c("INGRESO", "TRASLADO", "ADMISION DIRECTA")
)
tesis$MODALIDAD <- factor(
tesis$MODALIDAD,
levels = c("Presencial", "Semipresencial"),
labels = c("Presencial", "Semipresencial")
)
tesis$RENDIMIENTO <- factor(
tesis$RENDIMIENTO,
levels = c("APROBADO", "NO APROBADO"),
labels = c("APROBADO", "NO APROBADO")
)
tesis$RESIDENCIA <- factor(
tesis$RESIDENCIA,
levels = c("ASUNCIÓN", "CENTRAL", "RESTO"),
labels = c("ASUNCIÓN", "CENTRAL", "RESTO DEL PAÍS")
)
tesis$EST_CIVIL <- factor(
tesis$EST_CIVIL,
levels = c(1, 2, 3, 4),
labels = c("Soltero/a", "Casado/a", "Divorciado/a", "Otro")
)
tesis$TIPO_COL <- factor(
tesis$TIPO_COL,
levels = c(1, 2, 3),
labels = c("Público", "Subvencionado", "Privado")
)
tesis$TRABAJA <- factor(
tesis$TRABAJA,
levels = c(1, 2),
labels = c("SI", "NO")
)
tesis$SOLVENTAR <- factor(
tesis$SOLVENTAR,
levels = c(1, 2, 3, 4),
labels = c(
"Beca/exoneración total",
"Beca/exoneración parcial",
"Trabajo Personal",
"Ayuda Familiar"
)
)
tesis$ESTUDIOS_PADRES <- factor(
tesis$ESTUDIOS_PADRES,
levels = c(1, 2, 3, 4, 5),
labels = c("hasta 13 años", "14-23 años", "24-29 años", "30-34 años", "Más de 34 años")
)
tesis$INGRESO <- factor(
tesis$INGRESO,
levels = c(1, 2, 3, 4, 5),
labels = c(
"Hasta dos salarios mínimos",
"Más de dos y hasta cinco salarios mínimos",
"Más de cinco y hasta diez salarios mínimos",
"Más de diez y hasta quince salarios mínimos",
"Más de quince salarios mínimos"
)
)
tesis$NIVEL_SOCIO <- factor(
tesis$NIVEL_SOCIO,
levels = sort(unique(tesis$NIVEL_SOCIO))
)
tesis$EDAD <- as.numeric(tesis$EDAD)
############################################################
# 5. CHEQUEOS INICIALES
############################################################
cat("\n=============================\n")
##
## =============================
cat("ESTRUCTURA DE LA BASE\n")
## ESTRUCTURA DE LA BASE
cat("=============================\n")
## =============================
str(tesis)
## 'data.frame': 682 obs. of 15 variables:
## $ SEXO : Factor w/ 2 levels "FEMENINO","MASCULINO": 1 1 1 2 2 1 1 1 1 1 ...
## $ CARRERA : Factor w/ 5 levels "ESTADISTICA-PRES",..: 3 1 1 1 1 4 1 3 4 3 ...
## $ TIPO_INGRESO : Factor w/ 3 levels "INGRESO","TRASLADO",..: 1 1 1 1 1 1 1 1 1 1 ...
## $ MODALIDAD : Factor w/ 2 levels "Presencial","Semipresencial": 1 1 1 1 1 1 1 1 1 1 ...
## $ ESTADO_ACADEMICO: Factor w/ 2 levels "Desertor","No_Desertor": 1 1 2 1 1 2 1 1 1 1 ...
## $ RENDIMIENTO : Factor w/ 2 levels "APROBADO","NO APROBADO": 2 1 1 1 2 1 1 1 1 1 ...
## $ RESIDENCIA : Factor w/ 3 levels "ASUNCIÓN","CENTRAL",..: 2 2 3 2 2 3 1 1 3 1 ...
## $ EDAD : num 33 19 19 20 19 21 24 20 19 21 ...
## $ EST_CIVIL : Factor w/ 4 levels "Soltero/a","Casado/a",..: 1 1 1 1 1 1 1 1 1 1 ...
## $ TIPO_COL : Factor w/ 3 levels "Público","Subvencionado",..: 1 3 2 3 1 1 1 1 1 1 ...
## $ TRABAJA : Factor w/ 2 levels "SI","NO": 2 2 2 2 2 2 1 2 2 2 ...
## $ SOLVENTAR : Factor w/ 4 levels "Beca/exoneración total",..: 4 4 4 4 3 4 3 3 4 4 ...
## $ ESTUDIOS_PADRES : Factor w/ 5 levels "hasta 13 años",..: 1 2 2 3 1 2 3 2 2 2 ...
## $ INGRESO : Factor w/ 5 levels "Hasta dos salarios mínimos",..: 1 1 1 1 1 1 2 1 1 1 ...
## $ NIVEL_SOCIO : Factor w/ 2 levels "BAJO","MEDIO": 2 2 2 2 2 2 2 2 2 2 ...
cat("\n=============================\n")
##
## =============================
cat("TABLA DE LA VARIABLE RESPUESTA\n")
## TABLA DE LA VARIABLE RESPUESTA
cat("=============================\n")
## =============================
print(table(tesis$ESTADO_ACADEMICO))
##
## Desertor No_Desertor
## 459 223
cat("\n=============================\n")
##
## =============================
cat("RESUMEN GENERAL\n")
## RESUMEN GENERAL
cat("=============================\n")
## =============================
print(summary(tesis))
## SEXO CARRERA TIPO_INGRESO
## FEMENINO :395 ESTADISTICA-PRES :182 INGRESO :642
## MASCULINO:287 ESTADISTICA-SEMI :100 TRASLADO : 0
## MATEMATICA :150 ADMISION DIRECTA: 40
## EDUCACION MATEMATICA-PRES: 70
## EDUCACION MATEMATICA-SEMI:180
##
## MODALIDAD ESTADO_ACADEMICO RENDIMIENTO
## Presencial :402 Desertor :459 APROBADO :409
## Semipresencial:280 No_Desertor:223 NO APROBADO:273
##
##
##
##
## RESIDENCIA EDAD EST_CIVIL TIPO_COL
## ASUNCIÓN :229 Min. :18.00 Soltero/a :544 Público :465
## CENTRAL :336 1st Qu.:20.00 Casado/a :117 Subvencionado: 49
## RESTO DEL PAÍS:117 Median :23.00 Divorciado/a: 14 Privado :168
## Mean :27.11 Otro : 7
## 3rd Qu.:32.00
## Max. :70.00
## TRABAJA SOLVENTAR ESTUDIOS_PADRES
## SI:372 Beca/exoneración total : 45 hasta 13 años :157
## NO:310 Beca/exoneración parcial: 49 14-23 años :215
## Trabajo Personal :346 24-29 años :164
## Ayuda Familiar :242 30-34 años :101
## Más de 34 años: 45
##
## INGRESO NIVEL_SOCIO
## Hasta dos salarios mínimos :430 BAJO : 65
## Más de dos y hasta cinco salarios mínimos :190 MEDIO:617
## Más de cinco y hasta diez salarios mínimos : 45
## Más de diez y hasta quince salarios mínimos: 12
## Más de quince salarios mínimos : 5
##
############################################################
# 6. ANÁLISIS UNIVARIANTE
############################################################
tabla_edad <- data.frame(
Variable = "Edad",
Casos = sum(!is.na(tesis$EDAD)),
Minimo = min(tesis$EDAD, na.rm = TRUE),
Q1 = quantile(tesis$EDAD, 0.25, na.rm = TRUE),
Mediana = median(tesis$EDAD, na.rm = TRUE),
Media = mean(tesis$EDAD, na.rm = TRUE),
Q3 = quantile(tesis$EDAD, 0.75, na.rm = TRUE),
Maximo = max(tesis$EDAD, na.rm = TRUE)
)
tabla_edad[, -1] <- round(tabla_edad[, -1], 2)
row.names(tabla_edad) <- NULL
print(tabla_edad)
## Variable Casos Minimo Q1 Mediana Media Q3 Maximo
## 1 Edad 682 18 20 23 27.11 32 70
graf_estado <- ggplot(tesis, aes(x = ESTADO_ACADEMICO, fill = ESTADO_ACADEMICO)) +
geom_bar() +
geom_text(stat = "count", aes(label = after_stat(count)), vjust = -0.5) +
labs(
title = "Distribución del estado académico",
x = "Estado académico",
y = "Frecuencia"
) +
theme_minimal() +
theme(legend.position = "none")
print(graf_estado)
############################################################
# 6.1 TABLAS DESCRIPTIVAS DEL CAPÍTULO 4
############################################################
############################################################
# FUNCIONES AUXILIARES
############################################################
tabla_frecuencias <- function(data, var, nombre_variable = var) {
x <- data[[var]]
tab <- table(x, useNA = "ifany")
prop <- prop.table(tab) * 100
data.frame(
Variable = nombre_variable,
Categoria = names(tab),
Frecuencia = as.numeric(tab),
Porcentaje = round(as.numeric(prop), 2),
row.names = NULL
)
}
tabla_frecuencias_por_estado <- function(data, var, nombre_variable = var) {
tab <- table(data[[var]], data$ESTADO_ACADEMICO, useNA = "ifany")
prop_fila <- prop.table(tab, margin = 1) * 100
salida <- data.frame(
Variable = nombre_variable,
Categoria = rownames(tab),
Desertor = as.numeric(tab[, "Desertor"]),
No_Desertor = as.numeric(tab[, "No_Desertor"]),
Total = rowSums(tab),
Porc_Desertor = round(as.numeric(prop_fila[, "Desertor"]), 2),
Porc_No_Desertor = round(as.numeric(prop_fila[, "No_Desertor"]), 2),
row.names = NULL
)
salida
}
############################################################
# TABLA 4.5 COBERTURA DE LA POBLACIÓN CON INFORMACIÓN DISPONIBLE
############################################################
tabla_4_5_cobertura <- data.frame(
Poblacion_Inicial = 982,
Poblacion_Analizada = nrow(tesis),
Cobertura_Porcentaje = round((nrow(tesis) / 982) * 100, 2)
)
print(tabla_4_5_cobertura)
## Poblacion_Inicial Poblacion_Analizada Cobertura_Porcentaje
## 1 982 682 69.45
############################################################
# TABLA 4.7 SITUACIÓN ACADÉMICA DE LOS INGRESANTES
############################################################
tabla_estado <- table(tesis$ESTADO_ACADEMICO)
tabla_4_7_situacion_academica <- data.frame(
Estado_Academico = names(tabla_estado),
Frecuencia = as.numeric(tabla_estado),
Porcentaje = round(as.numeric(prop.table(tabla_estado)) * 100, 2),
row.names = NULL
)
print(tabla_4_7_situacion_academica)
## Estado_Academico Frecuencia Porcentaje
## 1 Desertor 459 67.3
## 2 No_Desertor 223 32.7
############################################################
# TABLA 4.8 VARIABLES DEMOGRÁFICAS
############################################################
vars_demograficas <- c("SEXO", "RESIDENCIA", "EDAD", "EST_CIVIL")
tabla_4_8_demograficas_cat <- do.call(
rbind,
lapply(c("SEXO", "RESIDENCIA", "EST_CIVIL"), function(v) {
tabla_frecuencias(tesis, v, v)
})
)
tabla_4_8_demograficas_num <- data.frame(
Variable = "EDAD",
Casos = sum(!is.na(tesis$EDAD)),
Minimo = round(min(tesis$EDAD, na.rm = TRUE), 2),
Q1 = round(quantile(tesis$EDAD, 0.25, na.rm = TRUE), 2),
Mediana = round(median(tesis$EDAD, na.rm = TRUE), 2),
Media = round(mean(tesis$EDAD, na.rm = TRUE), 2),
Q3 = round(quantile(tesis$EDAD, 0.75, na.rm = TRUE), 2),
Maximo = round(max(tesis$EDAD, na.rm = TRUE), 2)
)
print(tabla_4_8_demograficas_cat)
## Variable Categoria Frecuencia Porcentaje
## 1 SEXO FEMENINO 395 57.92
## 2 SEXO MASCULINO 287 42.08
## 3 RESIDENCIA ASUNCIÓN 229 33.58
## 4 RESIDENCIA CENTRAL 336 49.27
## 5 RESIDENCIA RESTO DEL PAÍS 117 17.16
## 6 EST_CIVIL Soltero/a 544 79.77
## 7 EST_CIVIL Casado/a 117 17.16
## 8 EST_CIVIL Divorciado/a 14 2.05
## 9 EST_CIVIL Otro 7 1.03
row.names(tabla_4_8_demograficas_num) <- NULL
print(tabla_4_8_demograficas_num)
## Variable Casos Minimo Q1 Mediana Media Q3 Maximo
## 1 EDAD 682 18 20 23 27.11 32 70
############################################################
# TABLA 4.9 VARIABLES INTERNAS AL MEDIO EDUCATIVO
############################################################
vars_internas <- c("CARRERA", "TIPO_INGRESO", "MODALIDAD", "RENDIMIENTO")
tabla_4_9_internas <- do.call(
rbind,
lapply(vars_internas, function(v) {
tabla_frecuencias(tesis, v, v)
})
)
print(tabla_4_9_internas)
## Variable Categoria Frecuencia Porcentaje
## 1 CARRERA ESTADISTICA-PRES 182 26.69
## 2 CARRERA ESTADISTICA-SEMI 100 14.66
## 3 CARRERA MATEMATICA 150 21.99
## 4 CARRERA EDUCACION MATEMATICA-PRES 70 10.26
## 5 CARRERA EDUCACION MATEMATICA-SEMI 180 26.39
## 6 TIPO_INGRESO INGRESO 642 94.13
## 7 TIPO_INGRESO TRASLADO 0 0.00
## 8 TIPO_INGRESO ADMISION DIRECTA 40 5.87
## 9 MODALIDAD Presencial 402 58.94
## 10 MODALIDAD Semipresencial 280 41.06
## 11 RENDIMIENTO APROBADO 409 59.97
## 12 RENDIMIENTO NO APROBADO 273 40.03
############################################################
# TABLA 4.10 VARIABLES EXTERNAS AL MEDIO EDUCATIVO
############################################################
vars_externas <- c("TRABAJA", "SOLVENTAR", "ESTUDIOS_PADRES", "TIPO_COL", "INGRESO", "NIVEL_SOCIO")
tabla_4_10_externas <- do.call(
rbind,
lapply(vars_externas, function(v) {
tabla_frecuencias(tesis, v, v)
})
)
print(tabla_4_10_externas)
## Variable Categoria Frecuencia
## 1 TRABAJA SI 372
## 2 TRABAJA NO 310
## 3 SOLVENTAR Beca/exoneración total 45
## 4 SOLVENTAR Beca/exoneración parcial 49
## 5 SOLVENTAR Trabajo Personal 346
## 6 SOLVENTAR Ayuda Familiar 242
## 7 ESTUDIOS_PADRES hasta 13 años 157
## 8 ESTUDIOS_PADRES 14-23 años 215
## 9 ESTUDIOS_PADRES 24-29 años 164
## 10 ESTUDIOS_PADRES 30-34 años 101
## 11 ESTUDIOS_PADRES Más de 34 años 45
## 12 TIPO_COL Público 465
## 13 TIPO_COL Subvencionado 49
## 14 TIPO_COL Privado 168
## 15 INGRESO Hasta dos salarios mínimos 430
## 16 INGRESO Más de dos y hasta cinco salarios mínimos 190
## 17 INGRESO Más de cinco y hasta diez salarios mínimos 45
## 18 INGRESO Más de diez y hasta quince salarios mínimos 12
## 19 INGRESO Más de quince salarios mínimos 5
## 20 NIVEL_SOCIO BAJO 65
## 21 NIVEL_SOCIO MEDIO 617
## Porcentaje
## 1 54.55
## 2 45.45
## 3 6.60
## 4 7.18
## 5 50.73
## 6 35.48
## 7 23.02
## 8 31.52
## 9 24.05
## 10 14.81
## 11 6.60
## 12 68.18
## 13 7.18
## 14 24.63
## 15 63.05
## 16 27.86
## 17 6.60
## 18 1.76
## 19 0.73
## 20 9.53
## 21 90.47
############################################################
# TABLA 4.11 RESUMEN DESCRIPTIVO DE VARIABLES NUMÉRICAS
############################################################
# En el dataset depurado la variable numérica disponible es EDAD.
# Si luego reincorporas ASIG_APROB, aquí puede agregarse del mismo modo.
tabla_4_11_numericas <- data.frame(
Variable = "EDAD",
Casos = sum(!is.na(tesis$EDAD)),
Minimo = round(min(tesis$EDAD, na.rm = TRUE), 2),
Q1 = round(quantile(tesis$EDAD, 0.25, na.rm = TRUE), 2),
Mediana = round(median(tesis$EDAD, na.rm = TRUE), 2),
Media = round(mean(tesis$EDAD, na.rm = TRUE), 2),
Q3 = round(quantile(tesis$EDAD, 0.75, na.rm = TRUE), 2),
Maximo = round(max(tesis$EDAD, na.rm = TRUE), 2),
DE = round(sd(tesis$EDAD, na.rm = TRUE), 2)
)
row.names(tabla_4_11_numericas) <- NULL
print(tabla_4_11_numericas)
## Variable Casos Minimo Q1 Mediana Media Q3 Maximo DE
## 1 EDAD 682 18 20 23 27.11 32 70 9.17
############################################################
# TABLAS OPCIONALES POR ESTADO ACADÉMICO
# ÚTILES SI QUIERES DESCRIPTIVAS MÁS RICAS EN CAPÍTULO 4
############################################################
tabla_4_8_demograficas_estado <- do.call(
rbind,
lapply(c("SEXO", "RESIDENCIA", "EST_CIVIL"), function(v) {
tabla_frecuencias_por_estado(tesis, v, v)
})
)
tabla_4_9_internas_estado <- do.call(
rbind,
lapply(vars_internas, function(v) {
tabla_frecuencias_por_estado(tesis, v, v)
})
)
tabla_4_10_externas_estado <- do.call(
rbind,
lapply(vars_externas, function(v) {
tabla_frecuencias_por_estado(tesis, v, v)
})
)
############################################################
# EXPORTACIÓN DE ESTAS TABLAS DESCRIPTIVAS
############################################################
write.csv(tabla_4_5_cobertura,
"salidas_tesis/tabla_4_5_cobertura.csv",
row.names = FALSE)
write.csv(tabla_4_7_situacion_academica,
"salidas_tesis/tabla_4_7_situacion_academica.csv",
row.names = FALSE)
write.csv(tabla_4_8_demograficas_cat,
"salidas_tesis/tabla_4_8_demograficas_categoricas.csv",
row.names = FALSE)
write.csv(tabla_4_8_demograficas_num,
"salidas_tesis/tabla_4_8_demograficas_numericas.csv",
row.names = FALSE)
write.csv(tabla_4_9_internas,
"salidas_tesis/tabla_4_9_internas.csv",
row.names = FALSE)
write.csv(tabla_4_10_externas,
"salidas_tesis/tabla_4_10_externas.csv",
row.names = FALSE)
write.csv(tabla_4_11_numericas,
"salidas_tesis/tabla_4_11_numericas.csv",
row.names = FALSE)
############################################################
# 7. ANÁLISIS BIVARIANTE
############################################################
clasificar_significancia <- function(p) {
ifelse(p < 0.01, "*** (p < 0.01)",
ifelse(p < 0.05, "** (p < 0.05)",
ifelse(p < 0.10, "* (p < 0.10)", "No significativo")))
}
# 7.1 Chi-cuadrado / Fisher para variables categóricas
covariables_cat <- c(
"RESIDENCIA", "CARRERA", "TIPO_INGRESO", "MODALIDAD", "RENDIMIENTO",
"TRABAJA", "SOLVENTAR", "ESTUDIOS_PADRES", "SEXO", "TIPO_COL",
"INGRESO", "EST_CIVIL", "NIVEL_SOCIO"
)
resultados_chi <- data.frame()
for (var in covariables_cat) {
tabla <- table(tesis[[var]], tesis$ESTADO_ACADEMICO)
chi_aux <- suppressWarnings(chisq.test(tabla, correct = FALSE))
if (any(chi_aux$expected < 5)) {
prueba <- fisher.test(tabla)
estadistico <- NA
metodo <- "Fisher"
} else {
prueba <- chi_aux
estadistico <- unname(prueba$statistic)
metodo <- "Chi-cuadrado"
}
resultados_chi <- rbind(
resultados_chi,
data.frame(
Variable = var,
Metodo = metodo,
Estadistico = estadistico,
P_Valor = prueba$p.value,
Significancia = clasificar_significancia(prueba$p.value)
)
)
}
resultados_chi <- resultados_chi[order(resultados_chi$P_Valor), ]
row.names(resultados_chi) <- NULL
print(resultados_chi)
## Variable Metodo Estadistico
## 1 RENDIMIENTO Chi-cuadrado 144.9473877
## 2 CARRERA Chi-cuadrado 73.1012024
## 3 MODALIDAD Chi-cuadrado 36.7897891
## 4 TRABAJA Chi-cuadrado 26.8970134
## 5 SOLVENTAR Chi-cuadrado 24.5391103
## 6 NIVEL_SOCIO Chi-cuadrado 2.5516367
## 7 EST_CIVIL Fisher NA
## 8 INGRESO Fisher NA
## 9 TIPO_INGRESO Fisher NA
## 10 RESIDENCIA Chi-cuadrado 2.4853373
## 11 ESTUDIOS_PADRES Chi-cuadrado 3.9439428
## 12 SEXO Chi-cuadrado 0.4037587
## 13 TIPO_COL Chi-cuadrado 0.9341439
## P_Valor Significancia
## 1 0.000000000000000000000000000000002205278 *** (p < 0.01)
## 2 0.000000000000005022182473894643603418153 *** (p < 0.01)
## 3 0.000000001315775866091108370413247097286 *** (p < 0.01)
## 4 0.000000214590233249842945095417490186662 *** (p < 0.01)
## 5 0.000019274982894101492435397215974290930 *** (p < 0.01)
## 6 0.110180066209064966842312571770889917389 No significativo
## 7 0.113516025141927309749512176040298072621 No significativo
## 8 0.125957268782453585265201922993583139032 No significativo
## 9 0.223085248122429330441818251529184635729 No significativo
## 10 0.288612981066232343607680377317592501640 No significativo
## 11 0.413645526632915150333502651847084052861 No significativo
## 12 0.525154472564691721103713462071027606726 No significativo
## 13 0.626834985394710275663499032816616818309 No significativo
# 7.2 Boxplot de EDAD
graf_edad <- ggplot(tesis, aes(x = ESTADO_ACADEMICO, y = EDAD)) +
geom_boxplot(fill = "skyblue", color = "darkblue") +
labs(
title = "Edad según estado académico",
x = "Estado académico",
y = "Edad"
) +
theme_minimal()
print(graf_edad)
hacer_prueba_asociacion <- function(data, var1, var2){
datos_tmp <- data[, c(var1, var2)]
datos_tmp <- na.omit(datos_tmp)
tab <- table(datos_tmp[[var1]], datos_tmp[[var2]])
if (nrow(tab) < 2 || ncol(tab) < 2) {
return(data.frame(
Variable1 = var1,
Variable2 = var2,
Metodo = NA_character_,
Estadistico = NA_real_,
P_Valor = NA_real_,
Significancia = NA_character_,
Celdas_esperadas_menor_5 = NA_integer_
))
}
chi <- suppressWarnings(chisq.test(tab, correct = FALSE))
n_celdas_menor_5 <- sum(chi$expected < 5)
if (n_celdas_menor_5 > 0) {
prueba <- tryCatch(
fisher.test(tab, workspace = 2e8),
error = function(e) {
fisher.test(tab, simulate.p.value = TRUE, B = 10000)
}
)
metodo <- ifelse(isTRUE(prueba$simulate.p.value),
"Fisher simulado",
"Fisher exacto")
estadistico <- NA
pvalor <- prueba$p.value
} else {
prueba <- chi
metodo <- "Chi-cuadrado"
estadistico <- unname(prueba$statistic)
pvalor <- prueba$p.value
}
data.frame(
Variable1 = var1,
Variable2 = var2,
Metodo = metodo,
Estadistico = estadistico,
P_Valor = pvalor,
Significancia = clasificar_significancia(pvalor),
Celdas_esperadas_menor_5 = n_celdas_menor_5
)
}
############################################################
# 8. PARTICIÓN TRAIN / TEST
############################################################
set.seed(123)
train_index <- createDataPartition(
tesis$ESTADO_ACADEMICO,
p = 0.7,
list = FALSE
)
train_data <- tesis[train_index, ]
test_data <- tesis[-train_index, ]
train_data$ESTADO_ACADEMICO <- factor(train_data$ESTADO_ACADEMICO,
levels = c("Desertor", "No_Desertor"))
test_data$ESTADO_ACADEMICO <- factor(test_data$ESTADO_ACADEMICO,
levels = c("Desertor", "No_Desertor"))
# Alineación de niveles entre train y test
for (col in names(train_data)) {
if (is.factor(train_data[[col]])) {
test_data[[col]] <- factor(test_data[[col]], levels = levels(train_data[[col]]))
}
}
cat("\n=============================\n")
##
## =============================
cat("PROPORCIÓN DE CLASES EN TRAIN\n")
## PROPORCIÓN DE CLASES EN TRAIN
cat("=============================\n")
## =============================
print(prop.table(table(train_data$ESTADO_ACADEMICO)))
##
## Desertor No_Desertor
## 0.6722338 0.3277662
cat("\n=============================\n")
##
## =============================
cat("PROPORCIÓN DE CLASES EN TEST\n")
## PROPORCIÓN DE CLASES EN TEST
cat("=============================\n")
## =============================
print(prop.table(table(test_data$ESTADO_ACADEMICO)))
##
## Desertor No_Desertor
## 0.6748768 0.3251232
############################################################
# 9. REGRESIÓN LOGÍSTICA BIVARIADA EN TRAIN
############################################################
# Para las variables numéricas se ajusta regresión logística simple.
# En la base depurada, la variable numérica considerada es EDAD.
variables_biv <- c("EDAD")
resultados_bivariados <- data.frame()
variables_omitidas <- data.frame()
for (var in variables_biv) {
base_modelo <- train_data[, c("ESTADO_ACADEMICO", var)]
base_modelo <- na.omit(base_modelo)
if (length(unique(base_modelo$ESTADO_ACADEMICO)) < 2) {
variables_omitidas <- rbind(
variables_omitidas,
data.frame(Variable = var, Motivo = "La respuesta tiene menos de 2 niveles")
)
next
}
if (is.numeric(base_modelo[[var]]) && length(unique(base_modelo[[var]])) < 2) {
variables_omitidas <- rbind(
variables_omitidas,
data.frame(Variable = var, Motivo = "Variable numérica sin variación")
)
next
}
formula_biv <- as.formula(paste("ESTADO_ACADEMICO ~", var))
modelo <- glm(formula_biv, family = binomial, data = base_modelo)
resumen <- summary(modelo)
coefs <- resumen$coefficients[-1, , drop = FALSE]
for (i in 1:nrow(coefs)) {
beta <- coefs[i, 1]
se <- coefs[i, 2]
z <- coefs[i, 3]
pvalor <- coefs[i, 4]
or <- exp(beta)
li <- exp(beta - 1.96 * se)
ls <- exp(beta + 1.96 * se)
resultados_bivariados <- rbind(
resultados_bivariados,
data.frame(
Variable = var,
Nivel = rownames(coefs)[i],
Coeficiente = round(beta, 4),
OR = round(or, 4),
IC95_LI = round(li, 4),
IC95_LS = round(ls, 4),
Estadistico_z = round(z, 4),
P_Valor = round(pvalor, 4),
Significancia = clasificar_significancia(pvalor)
)
)
}
}
print(resultados_bivariados)
## Variable Nivel Coeficiente OR IC95_LI IC95_LS Estadistico_z P_Valor
## 1 EDAD EDAD -0.0292 0.9713 0.9492 0.9939 -2.4841 0.013
## Significancia
## 1 ** (p < 0.05)
print(variables_omitidas)
## data frame with 0 columns and 0 rows
############################################################
# MATRIZ GRÁFICA DE ASOCIACIONES BIVARIADAS
# 14 VARIABLES EXPLICATIVAS
############################################################
# Paquetes
paquetes <- c("DescTools", "corrplot")
instalar <- paquetes[!paquetes %in% installed.packages()[, "Package"]]
if (length(instalar) > 0) install.packages(instalar)
library(DescTools)
##
## Adjuntando el paquete: 'DescTools'
## The following object is masked from 'package:car':
##
## Recode
## The following objects are masked from 'package:caret':
##
## MAE, RMSE
library(corrplot)
## corrplot 0.95 loaded
############################################################
# 1. DEFINIR VARIABLES EXPLICATIVAS
############################################################
variables_exp <- c(
"RESIDENCIA", "CARRERA", "TIPO_INGRESO", "MODALIDAD", "RENDIMIENTO",
"TRABAJA", "SOLVENTAR", "ESTUDIOS_PADRES", "SEXO", "TIPO_COL",
"INGRESO", "EST_CIVIL", "NIVEL_SOCIO", "EDAD"
)
############################################################
# 2. SELECCIONAR BASE DE DATOS
# Usa train_data si ya existe; si no, usa tesis
############################################################
if (exists("train_data")) {
base_asoc <- train_data
} else if (exists("tesis")) {
base_asoc <- tesis
} else {
stop("No se encontró ni 'train_data' ni 'tesis' en el entorno.")
}
faltantes <- setdiff(variables_exp, names(base_asoc))
if (length(faltantes) > 0) {
stop("Faltan estas variables en la base: ", paste(faltantes, collapse = ", "))
}
datos_asoc <- base_asoc[, variables_exp, drop = FALSE]
# Convertir character a factor
datos_asoc[] <- lapply(datos_asoc, function(x) {
if (is.character(x)) factor(x) else x
})
############################################################
# 3. FUNCIONES AUXILIARES
############################################################
# Eta (η): intensidad de asociación entre una variable numérica
# y una categórica. Devuelve valor entre 0 y 1.
eta_coef <- function(y, grupo) {
completos <- complete.cases(y, grupo)
y <- y[completos]
grupo <- droplevels(as.factor(grupo[completos]))
if (length(y) < 3 || nlevels(grupo) < 2) {
return(list(stat = NA_real_, p = NA_real_))
}
media_total <- mean(y)
medias_grupo <- tapply(y, grupo, mean)
n_grupo <- tapply(y, grupo, length)
ss_between <- sum(n_grupo * (medias_grupo - media_total)^2)
ss_total <- sum((y - media_total)^2)
eta <- ifelse(ss_total == 0, NA_real_, sqrt(ss_between / ss_total))
p_val <- tryCatch({
summary(aov(y ~ grupo))[[1]][["Pr(>F)"]][1]
}, error = function(e) NA_real_)
list(stat = eta, p = p_val)
}
# Asociación entre dos variables
calc_asociacion <- function(x, y) {
completos <- complete.cases(x, y)
x <- x[completos]
y <- y[completos]
if (length(x) < 3) {
return(list(stat = NA_real_, p = NA_real_))
}
x_num <- is.numeric(x)
y_num <- is.numeric(y)
# Numérica vs numérica -> Spearman
if (x_num && y_num) {
prueba <- suppressWarnings(cor.test(x, y, method = "spearman"))
return(list(stat = abs(unname(prueba$estimate)), p = prueba$p.value))
}
# Categórica vs categórica -> Cramér's V
if (!x_num && !y_num) {
x <- droplevels(as.factor(x))
y <- droplevels(as.factor(y))
if (nlevels(x) < 2 || nlevels(y) < 2) {
return(list(stat = NA_real_, p = NA_real_))
}
tabla <- table(x, y)
if (nrow(tabla) < 2 || ncol(tabla) < 2) {
return(list(stat = NA_real_, p = NA_real_))
}
p_val <- suppressWarnings(chisq.test(tabla, correct = FALSE)$p.value)
v <- suppressWarnings(CramerV(tabla, bias.correct = TRUE))
return(list(stat = as.numeric(v), p = p_val))
}
# Numérica vs categórica -> Eta
if (x_num && !y_num) return(eta_coef(x, y))
if (!x_num && y_num) return(eta_coef(y, x))
}
############################################################
# 4. CONSTRUIR MATRIZ DE ASOCIACIONES Y MATRIZ DE P-VALORES
############################################################
n <- length(variables_exp)
matriz_asoc <- matrix(NA_real_, nrow = n, ncol = n,
dimnames = list(variables_exp, variables_exp))
matriz_p <- matrix(NA_real_, nrow = n, ncol = n,
dimnames = list(variables_exp, variables_exp))
for (i in seq_len(n)) {
for (j in seq_len(n)) {
if (i == j) {
matriz_asoc[i, j] <- 1
matriz_p[i, j] <- 0
} else if (i < j) {
res <- calc_asociacion(datos_asoc[[i]], datos_asoc[[j]])
matriz_asoc[i, j] <- res$stat
matriz_asoc[j, i] <- res$stat
matriz_p[i, j] <- res$p
matriz_p[j, i] <- res$p
}
}
}
############################################################
# 5. REVISIÓN DE VARIABLES PROBLEMÁTICAS
############################################################
variables_constantes <- names(datos_asoc)[sapply(datos_asoc, function(x) {
length(unique(na.omit(x))) <= 1
})]
if (length(variables_constantes) > 0) {
message("Variables con un solo nivel o sin variación: ",
paste(variables_constantes, collapse = ", "))
}
############################################################
# 6. MATRIZ GRÁFICA
# Se muestran solo asociaciones significativas (p < 0.05)
############################################################
png("matriz_asociaciones_bivariadas_14_variables.png",
width = 1800, height = 1600, res = 220)
corrplot(
matriz_asoc,
method = "color",
type = "upper",
order = "hclust",
diag = FALSE,
is.corr = FALSE,
cl.lim = c(0, 1),
tl.col = "black",
tl.srt = 45,
addCoef.col = "black",
number.cex = 0.65,
p.mat = matriz_p,
sig.level = 0.05,
insig = "blank",
mar = c(0, 0, 2, 0),
title = "Matriz gráfica de asociaciones bivariadas entre variables explicativas"
)
## Warning in text.default(pos.xlabel[, 1], pos.xlabel[, 2], newcolnames, srt =
## tl.srt, : "cl.lim" es un parámetro gráfico inválido
## Warning in text.default(pos.ylabel[, 1], pos.ylabel[, 2], newrownames, col =
## tl.col, : "cl.lim" es un parámetro gráfico inválido
## Warning in title(title, ...): "cl.lim" es un parámetro gráfico inválido
dev.off()
## png
## 2
############################################################
# 7. EXPORTAR MATRICES
############################################################
write.csv(matriz_asoc,
"matriz_asociaciones_bivariadas_14_variables.csv",
row.names = TRUE)
write.csv(matriz_p,
"matriz_pvalores_asociaciones_14_variables.csv",
row.names = TRUE)
############################################################
# 8. MOSTRAR EN CONSOLA
############################################################
print(round(matriz_asoc, 3))
## RESIDENCIA CARRERA TIPO_INGRESO MODALIDAD RENDIMIENTO TRABAJA
## RESIDENCIA 1.000 0.135 0.073 0.168 0.030 0.074
## CARRERA 0.135 1.000 0.372 1.000 0.310 0.545
## TIPO_INGRESO 0.073 0.372 1.000 0.188 0.055 0.158
## MODALIDAD 0.168 1.000 0.188 1.000 0.208 0.535
## RENDIMIENTO 0.030 0.310 0.055 0.208 1.000 0.225
## TRABAJA 0.074 0.545 0.158 0.535 0.225 1.000
## SOLVENTAR 0.078 0.293 0.122 0.466 0.168 0.755
## ESTUDIOS_PADRES 0.183 0.099 0.086 0.106 0.104 0.085
## SEXO 0.066 0.200 0.070 0.098 0.137 0.120
## TIPO_COL 0.102 0.050 0.055 0.023 0.073 0.060
## INGRESO 0.167 0.186 0.054 0.333 0.141 0.305
## EST_CIVIL 0.054 0.270 0.106 0.438 0.061 0.336
## NIVEL_SOCIO 0.090 0.162 0.062 0.071 0.021 0.040
## EDAD 0.154 0.552 0.144 0.543 0.139 0.481
## SOLVENTAR ESTUDIOS_PADRES SEXO TIPO_COL INGRESO EST_CIVIL
## RESIDENCIA 0.078 0.183 0.066 0.102 0.167 0.054
## CARRERA 0.293 0.099 0.200 0.050 0.186 0.270
## TIPO_INGRESO 0.122 0.086 0.070 0.055 0.054 0.106
## MODALIDAD 0.466 0.106 0.098 0.023 0.333 0.438
## RENDIMIENTO 0.168 0.104 0.137 0.073 0.141 0.061
## TRABAJA 0.755 0.085 0.120 0.060 0.305 0.336
## SOLVENTAR 1.000 0.096 0.177 0.087 0.197 0.173
## ESTUDIOS_PADRES 0.096 1.000 0.146 0.201 0.233 0.099
## SEXO 0.177 0.146 1.000 0.015 0.215 0.082
## TIPO_COL 0.087 0.201 0.015 1.000 0.197 0.073
## INGRESO 0.197 0.233 0.215 0.197 1.000 0.151
## EST_CIVIL 0.173 0.099 0.082 0.073 0.151 1.000
## NIVEL_SOCIO 0.225 0.133 0.031 0.060 0.177 0.033
## EDAD 0.433 0.089 0.060 0.033 0.292 0.524
## NIVEL_SOCIO EDAD
## RESIDENCIA 0.090 0.154
## CARRERA 0.162 0.552
## TIPO_INGRESO 0.062 0.144
## MODALIDAD 0.071 0.543
## RENDIMIENTO 0.021 0.139
## TRABAJA 0.040 0.481
## SOLVENTAR 0.225 0.433
## ESTUDIOS_PADRES 0.133 0.089
## SEXO 0.031 0.060
## TIPO_COL 0.060 0.033
## INGRESO 0.177 0.292
## EST_CIVIL 0.033 0.524
## NIVEL_SOCIO 1.000 0.057
## EDAD 0.057 1.000
print(round(matriz_p, 4))
## RESIDENCIA CARRERA TIPO_INGRESO MODALIDAD RENDIMIENTO TRABAJA
## RESIDENCIA 0.0000 0.0267 0.2774 0.0012 0.8032 0.2714
## CARRERA 0.0267 0.0000 0.0000 0.0000 0.0000 0.0000
## TIPO_INGRESO 0.2774 0.0000 0.0000 0.0000 0.2290 0.0005
## MODALIDAD 0.0012 0.0000 0.0000 0.0000 0.0000 0.0000
## RENDIMIENTO 0.8032 0.0000 0.2290 0.0000 0.0000 0.0000
## TRABAJA 0.2714 0.0000 0.0005 0.0000 0.0000 0.0000
## SOLVENTAR 0.4407 0.0000 0.0672 0.0000 0.0037 0.0000
## ESTUDIOS_PADRES 0.0001 0.2721 0.4673 0.2539 0.2730 0.4855
## SEXO 0.3571 0.0007 0.1262 0.0314 0.0028 0.0085
## TIPO_COL 0.0417 0.9674 0.4840 0.8772 0.2761 0.4209
## INGRESO 0.0008 0.0000 0.8470 0.0000 0.0486 0.0000
## EST_CIVIL 0.8376 0.0000 0.1472 0.0000 0.6138 0.0000
## NIVEL_SOCIO 0.1419 0.0138 0.1720 0.1189 0.6427 0.3831
## EDAD 0.0034 0.0000 0.0016 0.0000 0.0022 0.0000
## SOLVENTAR ESTUDIOS_PADRES SEXO TIPO_COL INGRESO EST_CIVIL
## RESIDENCIA 0.4407 0.0001 0.3571 0.0417 0.0008 0.8376
## CARRERA 0.0000 0.2721 0.0007 0.9674 0.0000 0.0000
## TIPO_INGRESO 0.0672 0.4673 0.1262 0.4840 0.8470 0.1472
## MODALIDAD 0.0000 0.2539 0.0314 0.8772 0.0000 0.0000
## RENDIMIENTO 0.0037 0.2730 0.0028 0.2761 0.0486 0.6138
## TRABAJA 0.0000 0.4855 0.0085 0.4209 0.0000 0.0000
## SOLVENTAR 0.0000 0.3432 0.0018 0.2949 0.0000 0.0000
## ESTUDIOS_PADRES 0.3432 0.0000 0.0371 0.0000 0.0000 0.2968
## SEXO 0.0018 0.0371 0.0000 0.9474 0.0002 0.3573
## TIPO_COL 0.2949 0.0000 0.9474 0.0000 0.0000 0.5359
## INGRESO 0.0000 0.0000 0.0002 0.0000 0.0000 0.0011
## EST_CIVIL 0.0000 0.2968 0.3573 0.5359 0.0011 0.0000
## NIVEL_SOCIO 0.0000 0.0747 0.4961 0.4280 0.0048 0.9170
## EDAD 0.0000 0.4310 0.1915 0.7684 0.0000 0.0000
## NIVEL_SOCIO EDAD
## RESIDENCIA 0.1419 0.0034
## CARRERA 0.0138 0.0000
## TIPO_INGRESO 0.1720 0.0016
## MODALIDAD 0.1189 0.0000
## RENDIMIENTO 0.6427 0.0022
## TRABAJA 0.3831 0.0000
## SOLVENTAR 0.0000 0.0000
## ESTUDIOS_PADRES 0.0747 0.4310
## SEXO 0.4961 0.1915
## TIPO_COL 0.4280 0.7684
## INGRESO 0.0048 0.0000
## EST_CIVIL 0.9170 0.0000
## NIVEL_SOCIO 0.0000 0.2170
## EDAD 0.2170 0.0000
############################################################
# 9. DIRECTORIO DE SALIDA
############################################################
dir_salida <- "salida_tesis"
if (!dir.exists(dir_salida)) {
dir.create(dir_salida, recursive = TRUE)
}
############################################################
# 9.1 PARTICIÓN TRAIN / TEST
############################################################
library(caret)
set.seed(123)
# Variables completas para modelado
vars_modelo_total <- c(
"ESTADO_ACADEMICO",
"RESIDENCIA", "CARRERA", "TIPO_INGRESO",
"MODALIDAD", "RENDIMIENTO", "TRABAJA",
"SOLVENTAR", "ESTUDIOS_PADRES", "SEXO",
"TIPO_COL", "INGRESO", "EST_CIVIL",
"NIVEL_SOCIO", "EDAD"
)
# Base limpia para modelado
datos_modelo <- tesis[, vars_modelo_total]
# Eliminar NA
datos_modelo <- datos_modelo[complete.cases(datos_modelo), ]
# Partición estratificada
indice_train <- createDataPartition(
datos_modelo$ESTADO_ACADEMICO,
p = 0.70,
list = FALSE
)
train_data <- datos_modelo[indice_train, ]
test_data <- datos_modelo[-indice_train, ]
cat("\n=============================\n")
##
## =============================
cat("PARTICIÓN TRAIN / TEST\n")
## PARTICIÓN TRAIN / TEST
cat("=============================\n")
## =============================
cat("Train:", nrow(train_data), "observaciones\n")
## Train: 479 observaciones
cat("Test :", nrow(test_data), "observaciones\n")
## Test : 203 observaciones
############################################################
# 10. MODELADO PREDICTIVO - CONFIGURACIÓN COMÚN
############################################################
# Conjunto común de 14 variables explicativas
variables_modelado <- c(
"RESIDENCIA", "CARRERA", "TIPO_INGRESO", "MODALIDAD", "RENDIMIENTO",
"TRABAJA", "SOLVENTAR", "ESTUDIOS_PADRES", "SEXO", "TIPO_COL",
"INGRESO", "EST_CIVIL", "NIVEL_SOCIO", "EDAD"
)
# Fórmula común para TODOS los modelos comparados
form_general <- as.formula(
paste("ESTADO_ACADEMICO ~", paste(variables_modelado, collapse = " + "))
)
# Fórmula saturada para el modelo logístico final por stepwise
form_stepwise <- form_general
############################################################
# 10.1 BASES CONSISTENTES PARA MODELADO
############################################################
vars_modelo_total <- c("ESTADO_ACADEMICO", variables_modelado)
train_model <- train_data[, vars_modelo_total]
test_model <- test_data[, vars_modelo_total]
train_model <- train_model[complete.cases(train_model), ]
test_model <- test_model[complete.cases(test_model), ]
for (col in names(train_model)) {
if (is.factor(train_model[[col]])) {
train_model[[col]] <- droplevels(train_model[[col]])
}
}
for (col in names(train_model)) {
if (is.factor(train_model[[col]])) {
test_model[[col]] <- factor(test_model[[col]], levels = levels(train_model[[col]]))
}
}
cat("\n=============================\n")
##
## =============================
cat("BASE FINAL DE TRAIN PARA MODELADO\n")
## BASE FINAL DE TRAIN PARA MODELADO
cat("=============================\n")
## =============================
cat("Filas:", nrow(train_model), "\n")
## Filas: 479
cat("Columnas:", ncol(train_model), "\n")
## Columnas: 15
cat("\n=============================\n")
##
## =============================
cat("BASE FINAL DE TEST PARA MODELADO\n")
## BASE FINAL DE TEST PARA MODELADO
cat("=============================\n")
## =============================
cat("Filas:", nrow(test_model), "\n")
## Filas: 203
cat("Columnas:", ncol(test_model), "\n")
## Columnas: 15
############################################################
# 10.2 VALIDACIÓN CRUZADA COMPARTIDA
############################################################
set.seed(123)
folds_cv <- createMultiFolds(
y = train_model$ESTADO_ACADEMICO,
k = 10,
times = 3
)
ctrl <- trainControl(
method = "repeatedcv",
number = 10,
repeats = 3,
index = folds_cv,
classProbs = TRUE,
summaryFunction = twoClassSummary,
savePredictions = "final"
)
############################################################
# 11. ENTRENAMIENTO DE LOS 7 MODELOS
############################################################
# 11.1 Regresión logística
set.seed(123)
modelo_logit_cv <- train(
form_general,
data = train_model,
method = "glm",
family = binomial,
metric = "ROC",
trControl = ctrl
)
# 11.2 Árbol de decisión
set.seed(123)
modelo_tree_cv <- train(
form_general,
data = train_model,
method = "rpart",
metric = "ROC",
tuneLength = 10,
trControl = ctrl
)
# 11.3 Random Forest
set.seed(123)
modelo_rf_cv <- train(
form_general,
data = train_model,
method = "rf",
metric = "ROC",
tuneLength = 5,
ntree = 500,
trControl = ctrl
)
# 11.4 SVM radial
set.seed(123)
modelo_svm_cv <- train(
form_general,
data = train_model,
method = "svmRadial",
metric = "ROC",
preProcess = c("center", "scale"),
tuneLength = 10,
trControl = ctrl
)
# 11.5 KNN
set.seed(123)
modelo_knn_cv <- train(
form_general,
data = train_model,
method = "knn",
metric = "ROC",
preProcess = c("center", "scale"),
tuneLength = 10,
trControl = ctrl
)
# 11.6 Red neuronal
grid_nn <- expand.grid(
size = c(3, 5, 7),
decay = c(0.01, 0.10, 0.50)
)
set.seed(123)
modelo_nn_cv <- train(
form_general,
data = train_model,
method = "nnet",
metric = "ROC",
preProcess = c("center", "scale"),
tuneGrid = grid_nn,
trControl = ctrl,
trace = FALSE,
MaxNWts = 5000
)
# 11.7 Naive Bayes
grid_nb <- expand.grid(
laplace = c(0, 1),
usekernel = c(TRUE, FALSE),
adjust = c(1)
)
set.seed(123)
modelo_nb_cv <- train(
form_general,
data = train_model,
method = "naive_bayes",
metric = "ROC",
tuneGrid = grid_nb,
trControl = ctrl
)
############################################################
# 12. COMPARACIÓN EN VALIDACIÓN CRUZADA
############################################################
resultados_cv <- resamples(list(
Logit = modelo_logit_cv,
Tree = modelo_tree_cv,
RF = modelo_rf_cv,
SVM = modelo_svm_cv,
KNN = modelo_knn_cv,
NN = modelo_nn_cv,
NB = modelo_nb_cv
))
print(summary(resultados_cv))
##
## Call:
## summary.resamples(object = resultados_cv)
##
## Models: Logit, Tree, RF, SVM, KNN, NN, NB
## Number of resamples: 30
##
## ROC
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## Logit 0.6541667 0.7437500 0.7836174 0.7803046 0.8199219 0.8996212 0
## Tree 0.6845703 0.7714962 0.7973633 0.7940493 0.8305827 0.8750000 0
## RF 0.6458333 0.7341856 0.7910156 0.7854376 0.8286458 0.9091797 0
## SVM 0.6416667 0.7183268 0.7613636 0.7602734 0.8041992 0.8867188 0
## KNN 0.6318359 0.6966072 0.7434186 0.7483594 0.7955211 0.9062500 0
## NN 0.6208333 0.7228634 0.7753906 0.7677115 0.8043176 0.8916667 0
## NB 0.6041667 0.7292480 0.7802734 0.7714621 0.8144383 0.9104167 0
##
## Sens
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## Logit 0.68750 0.7500000 0.8125000 0.8071023 0.8473011 0.9696970 0
## Tree 0.65625 0.7500000 0.7812500 0.7886995 0.8167614 0.9375000 0
## RF 0.90625 0.9375000 0.9687500 0.9616477 0.9924242 1.0000000 0
## SVM 0.68750 0.8437500 0.8787879 0.8777462 0.9375000 1.0000000 0
## KNN 0.81250 0.9062500 0.9375000 0.9304924 0.9614110 1.0000000 0
## NN 0.65625 0.7500000 0.8125000 0.7967487 0.8437500 0.9090909 0
## NB 0.37500 0.6328125 0.7187500 0.6799874 0.7812500 0.8181818 0
##
## Spec
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## Logit 0.3333333 0.5000000 0.5625000 0.5548611 0.62500 0.7500 0
## Tree 0.3125000 0.4666667 0.6125000 0.5850000 0.68750 0.8750 0
## RF 0.0625000 0.1468750 0.1875000 0.2031944 0.25000 0.3750 0
## SVM 0.0000000 0.2000000 0.3229167 0.3322222 0.43750 0.6875 0
## KNN 0.0000000 0.1468750 0.2000000 0.2259722 0.31250 0.4375 0
## NN 0.2500000 0.3750000 0.5000000 0.4906944 0.61875 0.7500 0
## NB 0.4375000 0.6250000 0.7500000 0.7298611 0.81250 1.0000 0
# Gráficos en objeto
grafico_cv_roc <- bwplot(
resultados_cv,
metric = "ROC",
main = "Comparación del ROC en validación cruzada"
)
grafico_cv_sens <- bwplot(
resultados_cv,
metric = "Sens",
main = "Comparación de la sensibilidad en validación cruzada"
)
grafico_cv_spec <- bwplot(
resultados_cv,
metric = "Spec",
main = "Comparación de la especificidad en validación cruzada"
)
# Mostrar en pantalla
print(grafico_cv_roc)
print(grafico_cv_sens)
print(grafico_cv_spec)
############################################################
# 12.1 GUARDAR GRÁFICOS EN LA CARPETA salida_tesis
############################################################
# ROC
png(
filename = file.path(dir_salida, "grafico_cv_roc.png"),
width = 2000, height = 1400, res = 200
)
print(grafico_cv_roc)
dev.off()
## png
## 2
pdf(
file = file.path(dir_salida, "grafico_cv_roc.pdf"),
width = 10, height = 7
)
print(grafico_cv_roc)
dev.off()
## png
## 2
# Sensibilidad
png(
filename = file.path(dir_salida, "grafico_cv_sensibilidad.png"),
width = 2000, height = 1400, res = 200
)
print(grafico_cv_sens)
dev.off()
## png
## 2
pdf(
file = file.path(dir_salida, "grafico_cv_sensibilidad.pdf"),
width = 10, height = 7
)
print(grafico_cv_sens)
dev.off()
## png
## 2
# Especificidad
png(
filename = file.path(dir_salida, "grafico_cv_especificidad.png"),
width = 2000, height = 1400, res = 200
)
print(grafico_cv_spec)
dev.off()
## png
## 2
pdf(
file = file.path(dir_salida, "grafico_cv_especificidad.pdf"),
width = 10, height = 7
)
print(grafico_cv_spec)
dev.off()
## png
## 2
cat("\n=============================\n")
##
## =============================
cat("GRÁFICOS GUARDADOS EN:\n")
## GRÁFICOS GUARDADOS EN:
cat(normalizePath(dir_salida), "\n")
## C:\Users\DELL\Desktop\Tesis-Estadistica\salida_tesis
cat("=============================\n")
## =============================
############################################################
# 13. EVALUACIÓN EN CONJUNTO DE PRUEBA
############################################################
evaluar_modelo_caret <- function(modelo, nombre_modelo, test_data, variable_respuesta = "ESTADO_ACADEMICO") {
pred_clase <- predict(modelo, newdata = test_data, type = "raw")
pred_clase <- factor(pred_clase, levels = c("Desertor", "No_Desertor"))
pred_prob <- predict(modelo, newdata = test_data, type = "prob")[, "Desertor"]
real <- factor(test_data[[variable_respuesta]], levels = c("Desertor", "No_Desertor"))
mc <- confusionMatrix(pred_clase, real, positive = "Desertor")
roc_obj <- roc(
response = real,
predictor = pred_prob,
levels = c("No_Desertor", "Desertor"),
direction = "<"
)
auc_val <- as.numeric(auc(roc_obj))
resumen <- data.frame(
Modelo = nombre_modelo,
Accuracy = as.numeric(mc$overall["Accuracy"]),
Kappa = as.numeric(mc$overall["Kappa"]),
Sensitivity = as.numeric(mc$byClass["Sensitivity"]),
Specificity = as.numeric(mc$byClass["Specificity"]),
Precision = as.numeric(mc$byClass["Pos Pred Value"]),
F1 = ifelse(
(as.numeric(mc$byClass["Pos Pred Value"]) + as.numeric(mc$byClass["Sensitivity"])) == 0,
NA,
2 * as.numeric(mc$byClass["Pos Pred Value"]) * as.numeric(mc$byClass["Sensitivity"]) /
(as.numeric(mc$byClass["Pos Pred Value"]) + as.numeric(mc$byClass["Sensitivity"]))
),
AUC = auc_val,
row.names = NULL
)
list(
nombre = nombre_modelo,
confusion = mc,
roc = roc_obj,
resumen = resumen
)
}
modelos_finales <- list(
Logit = modelo_logit_cv,
Tree = modelo_tree_cv,
RF = modelo_rf_cv,
SVM = modelo_svm_cv,
KNN = modelo_knn_cv,
NN = modelo_nn_cv,
NB = modelo_nb_cv
)
resultados_test_lista <- lapply(names(modelos_finales), function(nombre) {
evaluar_modelo_caret(modelos_finales[[nombre]], nombre, test_model)
})
names(resultados_test_lista) <- names(modelos_finales)
tabla_metricas_test <- do.call(
rbind,
lapply(resultados_test_lista, function(x) x$resumen)
)
tabla_metricas_test <- tabla_metricas_test %>%
mutate(across(where(is.numeric), ~ round(.x, 4))) %>%
as.data.frame()
print(tabla_metricas_test)
## Modelo Accuracy Kappa Sensitivity Specificity Precision F1 AUC
## Logit Logit 0.7192 0.3627 0.7883 0.5758 0.7941 0.7912 0.7796
## Tree Tree 0.7340 0.4167 0.7664 0.6667 0.8268 0.7955 0.7853
## RF RF 0.7192 0.2154 0.9635 0.2121 0.7174 0.8224 0.7920
## SVM SVM 0.7192 0.2514 0.9270 0.2879 0.7299 0.8167 0.7773
## KNN KNN 0.6798 0.1541 0.8905 0.2424 0.7093 0.7896 0.7131
## NN NN 0.6946 0.2872 0.7956 0.4848 0.7622 0.7786 0.7460
## NB NB 0.7291 0.4169 0.7445 0.6970 0.8361 0.7876 0.7890
############################################################
# 14. CURVAS ROC COMPARATIVAS
############################################################
plot(resultados_test_lista$Logit$roc,
main = "Curvas ROC de los modelos en el conjunto de prueba",
lwd = 2)
plot(resultados_test_lista$Tree$roc, add = TRUE, lty = 2, lwd = 2)
plot(resultados_test_lista$RF$roc, add = TRUE, lty = 3, lwd = 2)
plot(resultados_test_lista$SVM$roc, add = TRUE, lty = 4, lwd = 2)
plot(resultados_test_lista$KNN$roc, add = TRUE, lty = 5, lwd = 2)
plot(resultados_test_lista$NN$roc, add = TRUE, lty = 6, lwd = 2)
plot(resultados_test_lista$NB$roc, add = TRUE, lty = 7, lwd = 2)
legend(
"bottomright",
legend = c(
paste0("Logit (AUC=", round(resultados_test_lista$Logit$resumen$AUC, 3), ")"),
paste0("Tree (AUC=", round(resultados_test_lista$Tree$resumen$AUC, 3), ")"),
paste0("RF (AUC=", round(resultados_test_lista$RF$resumen$AUC, 3), ")"),
paste0("SVM (AUC=", round(resultados_test_lista$SVM$resumen$AUC, 3), ")"),
paste0("KNN (AUC=", round(resultados_test_lista$KNN$resumen$AUC, 3), ")"),
paste0("NN (AUC=", round(resultados_test_lista$NN$resumen$AUC, 3), ")"),
paste0("NB (AUC=", round(resultados_test_lista$NB$resumen$AUC, 3), ")")
),
lty = 1:7,
lwd = 2,
cex = 0.8
)
############################################################
# 15. GRÁFICO COMPARATIVO DE 6 MÉTRICAS DE DESEMPEÑO
############################################################
metricas_graf <- tabla_metricas_test[, c(
"Modelo", "Accuracy", "Sensitivity", "Specificity", "Precision", "F1", "AUC"
)]
metricas_long <- tidyr::pivot_longer(
metricas_graf,
cols = c("Accuracy", "Sensitivity", "Specificity", "Precision", "F1", "AUC"),
names_to = "Metrica",
values_to = "Valor"
)
orden_modelos <- tabla_metricas_test$Modelo[order(tabla_metricas_test$AUC, decreasing = TRUE)]
metricas_long$Modelo <- factor(metricas_long$Modelo, levels = orden_modelos)
metricas_long$Metrica <- factor(
metricas_long$Metrica,
levels = c("Accuracy", "Sensitivity", "Specificity", "Precision", "F1", "AUC")
)
graf_6_metricas <- ggplot(metricas_long, aes(x = Modelo, y = Valor, fill = Metrica)) +
geom_col(position = position_dodge(width = 0.8), width = 0.7) +
geom_text(
aes(label = round(Valor, 3)),
position = position_dodge(width = 0.8),
vjust = -0.25,
size = 3
) +
labs(
title = "Comparación de 6 métricas de desempeño en el conjunto de prueba",
subtitle = "Accuracy, Sensitivity, Specificity, Precision, F1 y AUC",
x = "Modelo",
y = "Valor de la métrica",
fill = "Métrica"
) +
ylim(0, 1.08) +
theme_minimal(base_size = 13)
print(graf_6_metricas)
############################################################
# 13. EVALUACIÓN EN CONJUNTO DE PRUEBA
############################################################
library(caret)
library(pROC)
library(dplyr)
library(tidyr)
library(ggplot2)
library(forcats)
library(scales)
evaluar_modelo_caret <- function(modelo, nombre_modelo, test_data,
variable_respuesta = "ESTADO_ACADEMICO") {
# Clases predichas
pred_clase <- predict(modelo, newdata = test_data, type = "raw")
pred_clase <- factor(pred_clase, levels = c("Desertor", "No_Desertor"))
# Probabilidades
pred_prob <- predict(modelo, newdata = test_data, type = "prob")[, "Desertor"]
# Valores reales
real <- factor(test_data[[variable_respuesta]], levels = c("Desertor", "No_Desertor"))
# Matriz de confusión
mc <- confusionMatrix(pred_clase, real, positive = "Desertor")
# ROC
roc_obj <- roc(
response = real,
predictor = pred_prob,
levels = c("No_Desertor", "Desertor"),
direction = "<",
quiet = TRUE
)
auc_val <- as.numeric(auc(roc_obj))
precision_val <- as.numeric(mc$byClass["Pos Pred Value"])
sensitivity_val <- as.numeric(mc$byClass["Sensitivity"])
f1_val <- ifelse(
(precision_val + sensitivity_val) == 0,
NA,
2 * precision_val * sensitivity_val / (precision_val + sensitivity_val)
)
resumen <- data.frame(
Modelo = nombre_modelo,
Accuracy = as.numeric(mc$overall["Accuracy"]),
Kappa = as.numeric(mc$overall["Kappa"]),
Sensitivity = sensitivity_val,
Specificity = as.numeric(mc$byClass["Specificity"]),
Precision = precision_val,
F1 = f1_val,
AUC = auc_val,
row.names = NULL
)
list(
nombre = nombre_modelo,
confusion = mc,
roc = roc_obj,
resumen = resumen
)
}
############################################################
# MODELOS FINALES
############################################################
modelos_finales <- list(
Logit = modelo_logit_cv,
Tree = modelo_tree_cv,
RF = modelo_rf_cv,
SVM = modelo_svm_cv,
KNN = modelo_knn_cv,
NN = modelo_nn_cv,
NB = modelo_nb_cv
)
# CORRECCIÓN: usar test_data, no test_model
resultados_test_lista <- lapply(names(modelos_finales), function(nombre) {
evaluar_modelo_caret(modelos_finales[[nombre]], nombre, test_data)
})
names(resultados_test_lista) <- names(modelos_finales)
tabla_metricas_test <- do.call(
rbind,
lapply(resultados_test_lista, function(x) x$resumen)
)
tabla_metricas_test <- tabla_metricas_test %>%
mutate(across(where(is.numeric), ~ round(.x, 4))) %>%
as.data.frame()
print(tabla_metricas_test)
## Modelo Accuracy Kappa Sensitivity Specificity Precision F1 AUC
## Logit Logit 0.7192 0.3627 0.7883 0.5758 0.7941 0.7912 0.7796
## Tree Tree 0.7340 0.4167 0.7664 0.6667 0.8268 0.7955 0.7853
## RF RF 0.7192 0.2154 0.9635 0.2121 0.7174 0.8224 0.7920
## SVM SVM 0.7192 0.2514 0.9270 0.2879 0.7299 0.8167 0.7773
## KNN KNN 0.6798 0.1541 0.8905 0.2424 0.7093 0.7896 0.7131
## NN NN 0.6946 0.2872 0.7956 0.4848 0.7622 0.7786 0.7460
## NB NB 0.7291 0.4169 0.7445 0.6970 0.8361 0.7876 0.7890
############################################################
# 14. CURVAS ROC COMPARATIVAS CON GGPLOT2
############################################################
# Construir data frame para ROC
roc_df <- bind_rows(lapply(resultados_test_lista, function(res) {
data.frame(
Modelo = res$nombre,
FPR = 1 - res$roc$specificities,
TPR = res$roc$sensitivities
)
}))
# Etiquetas con AUC
auc_labels <- tabla_metricas_test %>%
arrange(desc(AUC)) %>%
mutate(
etiqueta = paste0(Modelo, " (AUC = ", sprintf("%.3f", AUC), ")")
)
roc_df$Modelo <- factor(roc_df$Modelo, levels = auc_labels$Modelo)
graf_roc <- ggplot(roc_df, aes(x = FPR, y = TPR, color = Modelo)) +
geom_line(linewidth = 1.15, alpha = 0.95) +
geom_abline(intercept = 0, slope = 1, linetype = "dashed", color = "grey55") +
coord_equal() +
scale_x_continuous(labels = label_number(accuracy = 0.01)) +
scale_y_continuous(labels = label_number(accuracy = 0.01)) +
labs(
title = "Curvas ROC de los modelos en el conjunto de prueba",
subtitle = "Comparación del desempeño discriminante en datos no observados",
x = "1 - Especificidad (FPR)",
y = "Sensibilidad (TPR)",
color = "Modelo"
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold", size = 15),
plot.subtitle = element_text(size = 11),
legend.position = "bottom",
legend.title = element_text(face = "bold"),
panel.grid.minor = element_blank()
) +
scale_color_discrete(labels = auc_labels$etiqueta)
print(graf_roc)
ggsave(
filename = "Curvas_ROC_modelos_test.png",
plot = graf_roc,
width = 10,
height = 7,
dpi = 320
)
############################################################
# 15. HEATMAP DE MÉTRICAS DE DESEMPEÑO
############################################################
metricas_graf <- tabla_metricas_test %>%
select(Modelo, Accuracy, Sensitivity, Specificity, Precision, F1, AUC)
# Ordenar modelos por AUC
orden_modelos <- metricas_graf %>%
arrange(desc(AUC)) %>%
pull(Modelo)
metricas_long <- metricas_graf %>%
pivot_longer(
cols = c(Accuracy, Sensitivity, Specificity, Precision, F1, AUC),
names_to = "Metrica",
values_to = "Valor"
) %>%
mutate(
Modelo = factor(Modelo, levels = orden_modelos),
Metrica = factor(Metrica,
levels = c("Accuracy", "Sensitivity", "Specificity",
"Precision", "F1", "AUC"))
)
graf_heatmap_metricas <- ggplot(metricas_long, aes(x = Metrica, y = Modelo, fill = Valor)) +
geom_tile(color = "white", linewidth = 0.7) +
geom_text(aes(label = sprintf("%.3f", Valor)), size = 3.7, fontface = "bold") +
scale_fill_gradient(
low = "#F2F2F2",
high = "#1F78B4",
limits = c(0, 1),
labels = label_number(accuracy = 0.01)
) +
labs(
title = "Matriz de métricas de desempeño en el conjunto de prueba",
subtitle = "Valores más altos indican mejor rendimiento",
x = "Métrica",
y = "Modelo",
fill = "Valor"
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold", size = 15),
plot.subtitle = element_text(size = 11),
axis.text.x = element_text(angle = 20, hjust = 1, face = "bold"),
axis.text.y = element_text(face = "bold"),
legend.position = "right",
panel.grid = element_blank()
)
print(graf_heatmap_metricas)
ggsave(
filename = "Heatmap_metricas_modelos_test.png",
plot = graf_heatmap_metricas,
width = 9,
height = 5.8,
dpi = 320
)
############################################################
# 16. OPCIONAL: GRÁFICO DE BARRAS EN FACETAS
# Más limpio que 6 barras juntas por modelo
############################################################
graf_metricas_facetas <- ggplot(metricas_long, aes(x = fct_reorder(Modelo, Valor), y = Valor)) +
geom_col(width = 0.68, fill = "#2C7FB8") +
geom_text(aes(label = sprintf("%.3f", Valor)), vjust = -0.25, size = 3.3) +
facet_wrap(~ Metrica, ncol = 3) +
coord_cartesian(ylim = c(0, 1.05)) +
labs(
title = "Comparación de métricas por modelo",
subtitle = "Visualización separada por métrica",
x = "Modelo",
y = "Valor"
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold", size = 15),
plot.subtitle = element_text(size = 11),
strip.text = element_text(face = "bold"),
axis.text.x = element_text(angle = 30, hjust = 1)
)
print(graf_metricas_facetas)
ggsave(
filename = "Metricas_facetas_modelos_test.png",
plot = graf_metricas_facetas,
width = 11,
height = 7,
dpi = 320
)
############################################################
# 16. MODELO LOGÍSTICO FINAL (STEPWISE) + VERIFICACIÓN
# DE SUPUESTOS BÁSICOS + ANÁLISIS DEL UMBRAL
############################################################
############################################################
# 16.1 VARIABLES CANDIDATAS Y MODELO INICIAL
############################################################
# Variables candidatas: las 14 covariables consideradas en el entrenamiento comparativo
variables_stepwise <- c(
"RESIDENCIA", "CARRERA", "TIPO_INGRESO", "MODALIDAD", "RENDIMIENTO",
"TRABAJA", "SOLVENTAR", "ESTUDIOS_PADRES", "SEXO", "TIPO_COL",
"INGRESO", "EST_CIVIL", "NIVEL_SOCIO", "EDAD"
)
form_stepwise <- as.formula(
paste("ESTADO_ACADEMICO ~", paste(variables_stepwise, collapse = " + "))
)
modelo_logit_inicial <- glm(
form_stepwise,
data = train_model,
family = binomial
)
############################################################
# 16.2 SELECCIÓN STEPWISE
############################################################
modelo_logit_final <- step(
modelo_logit_inicial,
direction = "both",
trace = FALSE
)
cat("\n=============================\n")
##
## =============================
cat("MODELO LOGÍSTICO FINAL SELECCIONADO POR STEPWISE\n")
## MODELO LOGÍSTICO FINAL SELECCIONADO POR STEPWISE
cat("=============================\n")
## =============================
print(formula(modelo_logit_final))
## ESTADO_ACADEMICO ~ RESIDENCIA + CARRERA + TIPO_INGRESO + RENDIMIENTO +
## NIVEL_SOCIO
print(summary(modelo_logit_final))
##
## Call:
## glm(formula = ESTADO_ACADEMICO ~ RESIDENCIA + CARRERA + TIPO_INGRESO +
## RENDIMIENTO + NIVEL_SOCIO, family = binomial, data = train_model)
##
## Coefficients:
## Estimate Std. Error z value
## (Intercept) 0.4701 0.4726 0.995
## RESIDENCIACENTRAL 0.3479 0.2626 1.325
## RESIDENCIARESTO DEL PAÍS -0.4734 0.3671 -1.290
## CARRERAESTADISTICA-SEMI -0.7379 0.4970 -1.485
## CARRERAMATEMATICA 0.4434 0.3177 1.396
## CARRERAEDUCACION MATEMATICA-PRES 1.2431 0.4056 3.065
## CARRERAEDUCACION MATEMATICA-SEMI -0.2644 0.3352 -0.789
## TIPO_INGRESOADMISION DIRECTA 1.7911 0.6210 2.884
## RENDIMIENTONO APROBADO -2.7763 0.3526 -7.874
## NIVEL_SOCIOMEDIO -0.8353 0.4259 -1.961
## Pr(>|z|)
## (Intercept) 0.31987
## RESIDENCIACENTRAL 0.18518
## RESIDENCIARESTO DEL PAÍS 0.19719
## CARRERAESTADISTICA-SEMI 0.13758
## CARRERAMATEMATICA 0.16275
## CARRERAEDUCACION MATEMATICA-PRES 0.00218 **
## CARRERAEDUCACION MATEMATICA-SEMI 0.43029
## TIPO_INGRESOADMISION DIRECTA 0.00392 **
## RENDIMIENTONO APROBADO 0.00000000000000344 ***
## NIVEL_SOCIOMEDIO 0.04984 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 606.02 on 478 degrees of freedom
## Residual deviance: 441.98 on 469 degrees of freedom
## AIC: 461.98
##
## Number of Fisher Scoring iterations: 5
############################################################
# 16.3 TABLA DE COEFICIENTES, OR E IC95%
############################################################
coefs <- summary(modelo_logit_final)$coefficients
tabla_or <- data.frame(
Variable = rownames(coefs),
Coeficiente = coefs[, "Estimate"],
Error_Estandar = coefs[, "Std. Error"],
Estadistico_z = coefs[, "z value"],
P_Valor = coefs[, "Pr(>|z|)"],
OR = exp(coefs[, "Estimate"]),
IC95_LI = exp(coefs[, "Estimate"] - 1.96 * coefs[, "Std. Error"]),
IC95_LS = exp(coefs[, "Estimate"] + 1.96 * coefs[, "Std. Error"])
)
tabla_or <- tabla_or %>%
mutate(across(where(is.numeric), ~ round(.x, 4))) %>%
as.data.frame()
cat("\n=============================\n")
##
## =============================
cat("ODDS RATIO E INTERVALOS DE CONFIANZA\n")
## ODDS RATIO E INTERVALOS DE CONFIANZA
cat("=============================\n")
## =============================
print(tabla_or)
## Variable Coeficiente
## (Intercept) (Intercept) 0.4701
## RESIDENCIACENTRAL RESIDENCIACENTRAL 0.3479
## RESIDENCIARESTO DEL PAÍS RESIDENCIARESTO DEL PAÍS -0.4734
## CARRERAESTADISTICA-SEMI CARRERAESTADISTICA-SEMI -0.7379
## CARRERAMATEMATICA CARRERAMATEMATICA 0.4434
## CARRERAEDUCACION MATEMATICA-PRES CARRERAEDUCACION MATEMATICA-PRES 1.2431
## CARRERAEDUCACION MATEMATICA-SEMI CARRERAEDUCACION MATEMATICA-SEMI -0.2644
## TIPO_INGRESOADMISION DIRECTA TIPO_INGRESOADMISION DIRECTA 1.7911
## RENDIMIENTONO APROBADO RENDIMIENTONO APROBADO -2.7763
## NIVEL_SOCIOMEDIO NIVEL_SOCIOMEDIO -0.8353
## Error_Estandar Estadistico_z P_Valor OR
## (Intercept) 0.4726 0.9947 0.3199 1.6002
## RESIDENCIACENTRAL 0.2626 1.3250 0.1852 1.4161
## RESIDENCIARESTO DEL PAÍS 0.3671 -1.2896 0.1972 0.6229
## CARRERAESTADISTICA-SEMI 0.4970 -1.4849 0.1376 0.4781
## CARRERAMATEMATICA 0.3177 1.3959 0.1628 1.5581
## CARRERAEDUCACION MATEMATICA-PRES 0.4056 3.0647 0.0022 3.4662
## CARRERAEDUCACION MATEMATICA-SEMI 0.3352 -0.7887 0.4303 0.7677
## TIPO_INGRESOADMISION DIRECTA 0.6210 2.8842 0.0039 5.9958
## RENDIMIENTONO APROBADO 0.3526 -7.8738 0.0000 0.0623
## NIVEL_SOCIOMEDIO 0.4259 -1.9613 0.0498 0.4337
## IC95_LI IC95_LS
## (Intercept) 0.6337 4.0411
## RESIDENCIACENTRAL 0.8464 2.3690
## RESIDENCIARESTO DEL PAÍS 0.3034 1.2790
## CARRERAESTADISTICA-SEMI 0.1805 1.2663
## CARRERAMATEMATICA 0.8359 2.9040
## CARRERAEDUCACION MATEMATICA-PRES 1.5653 7.6755
## CARRERAEDUCACION MATEMATICA-SEMI 0.3979 1.4809
## TIPO_INGRESOADMISION DIRECTA 1.7752 20.2511
## RENDIMIENTONO APROBADO 0.0312 0.1243
## NIVEL_SOCIOMEDIO 0.1882 0.9994
############################################################
# 16.4 VERIFICACIÓN BÁSICA 1: MULTICOLINEALIDAD
############################################################
# car::vif() puede devolver vector, matriz o fallar; se maneja de forma robusta
vif_raw <- tryCatch(
car::vif(modelo_logit_final),
error = function(e) NULL
)
cat("\n=============================\n")
##
## =============================
cat("VERIFICACIÓN DE MULTICOLINEALIDAD (VIF)\n")
## VERIFICACIÓN DE MULTICOLINEALIDAD (VIF)
cat("=============================\n")
## =============================
if (is.null(vif_raw)) {
cat("No fue posible calcular VIF con la estructura actual del modelo.\n")
vif_modelo <- data.frame(
Variable = "No disponible",
VIF = NA
)
} else {
if (is.matrix(vif_raw)) {
vif_modelo <- data.frame(
Variable = rownames(vif_raw),
GVIF = vif_raw[, 1],
Df = vif_raw[, 2],
GVIF_ajustado = vif_raw[, 3]
)
} else {
vif_modelo <- data.frame(
Variable = names(vif_raw),
VIF = as.numeric(vif_raw)
)
}
print(vif_modelo)
}
## Variable GVIF Df GVIF_ajustado
## RESIDENCIA RESIDENCIA 1.055059 2 1.013489
## CARRERA CARRERA 1.462960 4 1.048707
## TIPO_INGRESO TIPO_INGRESO 1.392938 1 1.180228
## RENDIMIENTO RENDIMIENTO 1.062937 1 1.030988
## NIVEL_SOCIO NIVEL_SOCIO 1.057698 1 1.028445
############################################################
# 16.5 VERIFICACIÓN BÁSICA 2: LINEALIDAD EN EL LOGIT
# (para variable numérica EDAD)
############################################################
train_bt <- train_model
train_bt$Y_BT <- ifelse(train_bt$ESTADO_ACADEMICO == "Desertor", 1, 0)
if ("EDAD" %in% names(train_bt) && all(train_bt$EDAD > 0, na.rm = TRUE)) {
modelo_bt <- glm(
Y_BT ~ EDAD + I(EDAD * log(EDAD)),
data = train_bt,
family = binomial
)
cat("\n=============================\n")
cat("VERIFICACIÓN DE LINEALIDAD EN EL LOGIT (BOX-TIDWELL PARA EDAD)\n")
cat("=============================\n")
print(summary(modelo_bt))
cat("\nInterpretación:\n")
cat("- Si el término I(EDAD * log(EDAD)) NO es significativo, la linealidad en el logit es razonable.\n")
cat("- Si es significativo, la relación puede no ser lineal en el logit.\n")
} else {
cat("\n=============================\n")
cat("VERIFICACIÓN DE LINEALIDAD EN EL LOGIT\n")
cat("=============================\n")
cat("No se aplicó Box-Tidwell porque EDAD no cumple las condiciones requeridas.\n")
}
##
## =============================
## VERIFICACIÓN DE LINEALIDAD EN EL LOGIT (BOX-TIDWELL PARA EDAD)
## =============================
##
## Call:
## glm(formula = Y_BT ~ EDAD + I(EDAD * log(EDAD)), family = binomial,
## data = train_bt)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -3.57673 1.88423 -1.898 0.0577 .
## EDAD 0.56372 0.28034 2.011 0.0443 *
## I(EDAD * log(EDAD)) -0.12062 0.06303 -1.914 0.0557 .
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 606.02 on 478 degrees of freedom
## Residual deviance: 595.94 on 476 degrees of freedom
## AIC: 601.94
##
## Number of Fisher Scoring iterations: 4
##
##
## Interpretación:
## - Si el término I(EDAD * log(EDAD)) NO es significativo, la linealidad en el logit es razonable.
## - Si es significativo, la relación puede no ser lineal en el logit.
############################################################
# 16.6 VERIFICACIÓN BÁSICA 3: BONDAD DE AJUSTE
# PRUEBA DE HOSMER-LEMESHOW
############################################################
prob_train_modelo <- predict(modelo_logit_final, newdata = train_model, type = "response")
medias_prob_train <- tapply(prob_train_modelo, train_model$ESTADO_ACADEMICO, mean)
if (medias_prob_train["No_Desertor"] > medias_prob_train["Desertor"]) {
prob_train_desertor <- 1 - prob_train_modelo
} else {
prob_train_desertor <- prob_train_modelo
}
y_train_num <- ifelse(train_model$ESTADO_ACADEMICO == "Desertor", 1, 0)
hl <- ResourceSelection::hoslem.test(y_train_num, prob_train_desertor, g = 10)
cat("\n=============================\n")
##
## =============================
cat("PRUEBA DE HOSMER-LEMESHOW\n")
## PRUEBA DE HOSMER-LEMESHOW
cat("=============================\n")
## =============================
print(hl)
##
## Hosmer and Lemeshow goodness of fit (GOF) test
##
## data: y_train_num, prob_train_desertor
## X-squared = 2.433, df = 8, p-value = 0.9648
cat("\nInterpretación:\n")
##
## Interpretación:
cat("- p > 0.05: no hay evidencia de mal ajuste.\n")
## - p > 0.05: no hay evidencia de mal ajuste.
cat("- p <= 0.05: podría haber problemas de ajuste/calibración.\n")
## - p <= 0.05: podría haber problemas de ajuste/calibración.
############################################################
# 16.7 VERIFICACIÓN BÁSICA 4: OBSERVACIONES INFLUYENTES
# RESIDUOS DE PEARSON Y DISTANCIA DE COOK
############################################################
residuos_pearson <- residuals(modelo_logit_final, type = "pearson")
cook <- cooks.distance(modelo_logit_final)
cat("\n=============================\n")
##
## =============================
cat("RESUMEN DE RESIDUOS DE PEARSON\n")
## RESUMEN DE RESIDUOS DE PEARSON
cat("=============================\n")
## =============================
print(summary(residuos_pearson))
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## -1.878984 -0.576109 -0.207892 -0.003554 0.644711 5.846053
cat("\n=============================\n")
##
## =============================
cat("RESUMEN DE DISTANCIAS DE COOK\n")
## RESUMEN DE DISTANCIAS DE COOK
cat("=============================\n")
## =============================
print(summary(cook))
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.000006225 0.000051886 0.001077027 0.002127516 0.002230838 0.029366789
umbral_cook <- 4 / nrow(train_model)
cat("\nUmbral orientativo para Cook =", round(umbral_cook, 4), "\n")
##
## Umbral orientativo para Cook = 0.0084
cat("Número de observaciones con Cook > umbral:", sum(cook > umbral_cook), "\n")
## Número de observaciones con Cook > umbral: 27
############################################################
# 16.8 GRÁFICOS DE DIAGNÓSTICO
############################################################
par(mfrow = c(1, 2))
plot(
residuos_pearson,
pch = 20,
main = "Residuos de Pearson",
ylab = "Residuo",
xlab = "Índice"
)
abline(h = c(-2, 2), lty = 2, col = "red")
plot(
cook,
pch = 20,
main = "Distancia de Cook",
ylab = "Cook",
xlab = "Índice"
)
abline(h = umbral_cook, lty = 2, col = "red")
par(mfrow = c(1, 1))
############################################################
# 16.9 CAPACIDAD DISCRIMINANTE EN TRAIN Y TEST
############################################################
# TRAIN
prob_train_modelo <- predict(modelo_logit_final, newdata = train_model, type = "response")
medias_prob_train <- tapply(prob_train_modelo, train_model$ESTADO_ACADEMICO, mean)
if (medias_prob_train["No_Desertor"] > medias_prob_train["Desertor"]) {
prob_train_desertor <- 1 - prob_train_modelo
} else {
prob_train_desertor <- prob_train_modelo
}
roc_train_logit <- roc(
response = train_model$ESTADO_ACADEMICO,
predictor = prob_train_desertor,
levels = c("No_Desertor", "Desertor"),
direction = "<"
)
auc_train_logit <- as.numeric(auc(roc_train_logit))
# TEST
prob_test_modelo <- predict(modelo_logit_final, newdata = test_model, type = "response")
medias_prob_test <- tapply(prob_test_modelo, test_model$ESTADO_ACADEMICO, mean)
if (medias_prob_test["No_Desertor"] > medias_prob_test["Desertor"]) {
prob_test_desertor <- 1 - prob_test_modelo
} else {
prob_test_desertor <- prob_test_modelo
}
roc_test_logit <- roc(
response = test_model$ESTADO_ACADEMICO,
predictor = prob_test_desertor,
levels = c("No_Desertor", "Desertor"),
direction = "<"
)
auc_test_logit <- as.numeric(auc(roc_test_logit))
cat("\n=============================\n")
##
## =============================
cat("AUC EN TRAIN Y TEST\n")
## AUC EN TRAIN Y TEST
cat("=============================\n")
## =============================
cat("AUC TRAIN:", round(auc_train_logit, 4), "\n")
## AUC TRAIN: 0.8298
cat("AUC TEST :", round(auc_test_logit, 4), "\n")
## AUC TEST : 0.7635
cat("Diferencia absoluta:", round(abs(auc_train_logit - auc_test_logit), 4), "\n")
## Diferencia absoluta: 0.0663
############################################################
# 16.10 MATRIZ DE CONFUSIÓN FINAL EN TEST (UMBRAL 0.50)
############################################################
umbral_final <- 0.50
pred_test_final <- ifelse(prob_test_desertor >= umbral_final, "Desertor", "No_Desertor")
pred_test_final <- factor(pred_test_final, levels = c("Desertor", "No_Desertor"))
cm_logit_final <- confusionMatrix(
data = pred_test_final,
reference = test_model$ESTADO_ACADEMICO,
positive = "Desertor"
)
tabla_logit_final <- data.frame(
Modelo = "Regresión logística final",
Formula_Final = paste(deparse(formula(modelo_logit_final)), collapse = " "),
Umbral = umbral_final,
AUC_Train = auc_train_logit,
AUC_Test = auc_test_logit,
Accuracy = as.numeric(cm_logit_final$overall["Accuracy"]),
Kappa = as.numeric(cm_logit_final$overall["Kappa"]),
Sensibilidad = as.numeric(cm_logit_final$byClass["Sensitivity"]),
Especificidad = as.numeric(cm_logit_final$byClass["Specificity"]),
Precision = as.numeric(cm_logit_final$byClass["Pos Pred Value"]),
VPN = as.numeric(cm_logit_final$byClass["Neg Pred Value"]),
Balanced_Accuracy = as.numeric(cm_logit_final$byClass["Balanced Accuracy"])
)
tabla_logit_final <- tabla_logit_final %>%
mutate(across(-c(1, 2), ~ if(is.numeric(.x)) round(.x, 4) else .x)) %>%
as.data.frame()
cat("\n=============================\n")
##
## =============================
cat("MATRIZ DE CONFUSIÓN FINAL EN TEST\n")
## MATRIZ DE CONFUSIÓN FINAL EN TEST
cat("=============================\n")
## =============================
print(cm_logit_final)
## Confusion Matrix and Statistics
##
## Reference
## Prediction Desertor No_Desertor
## Desertor 112 35
## No_Desertor 25 31
##
## Accuracy : 0.7044
## 95% CI : (0.6365, 0.7663)
## No Information Rate : 0.6749
## P-Value [Acc > NIR] : 0.2058
##
## Kappa : 0.299
##
## Mcnemar's Test P-Value : 0.2453
##
## Sensitivity : 0.8175
## Specificity : 0.4697
## Pos Pred Value : 0.7619
## Neg Pred Value : 0.5536
## Prevalence : 0.6749
## Detection Rate : 0.5517
## Detection Prevalence : 0.7241
## Balanced Accuracy : 0.6436
##
## 'Positive' Class : Desertor
##
cat("\n=============================\n")
##
## =============================
cat("TABLA RESUMEN DEL MODELO FINAL\n")
## TABLA RESUMEN DEL MODELO FINAL
cat("=============================\n")
## =============================
print(tabla_logit_final)
## Modelo
## 1 Regresión logística final
## Formula_Final
## 1 ESTADO_ACADEMICO ~ RESIDENCIA + CARRERA + TIPO_INGRESO + RENDIMIENTO + NIVEL_SOCIO
## Umbral AUC_Train AUC_Test Accuracy Kappa Sensibilidad Especificidad Precision
## 1 0.5 0.8298 0.7635 0.7044 0.299 0.8175 0.4697 0.7619
## VPN Balanced_Accuracy
## 1 0.5536 0.6436
############################################################
# 16.11 CURVA ROC FINAL EN TEST
############################################################
plot(
roc_test_logit,
main = paste0("Curva ROC - Modelo logístico final (AUC = ", round(auc_test_logit, 3), ")"),
lwd = 2
)
abline(a = 0, b = 1, lty = 2)
############################################################
# 16.12 ANÁLISIS DEL TRADE-OFF SEGÚN EL UMBRAL
############################################################
umbrales <- seq(0.01, 0.99, by = 0.01)
metricas_umbral <- data.frame()
for (t in umbrales) {
pred_t <- ifelse(prob_test_desertor >= t, "Desertor", "No_Desertor")
pred_t <- factor(pred_t, levels = c("Desertor", "No_Desertor"))
cm_t <- confusionMatrix(
data = pred_t,
reference = test_model$ESTADO_ACADEMICO,
positive = "Desertor"
)
metricas_umbral <- rbind(metricas_umbral, data.frame(
Umbral = t,
Sensitivity = as.numeric(cm_t$byClass["Sensitivity"]),
Specificity = as.numeric(cm_t$byClass["Specificity"]),
Accuracy = as.numeric(cm_t$overall["Accuracy"]),
Precision = as.numeric(cm_t$byClass["Pos Pred Value"]),
VPN = as.numeric(cm_t$byClass["Neg Pred Value"]),
Balanced_Accuracy = as.numeric(cm_t$byClass["Balanced Accuracy"])
))
}
metricas_umbral$Youden <- metricas_umbral$Sensitivity + metricas_umbral$Specificity - 1
fila_youden <- which.max(metricas_umbral$Youden)
umbral_youden <- metricas_umbral$Umbral[fila_youden]
fila_050 <- which.min(abs(metricas_umbral$Umbral - 0.50))
cat("\n=============================\n")
##
## =============================
cat("ANÁLISIS DE UMBRALES - MODELO LOGÍSTICO FINAL\n")
## ANÁLISIS DE UMBRALES - MODELO LOGÍSTICO FINAL
cat("=============================\n")
## =============================
cat("Umbral fijo utilizado en el documento:", 0.50, "\n")
## Umbral fijo utilizado en el documento: 0.5
cat("Umbral óptimo según Youden:", round(umbral_youden, 3), "\n")
## Umbral óptimo según Youden: 0.7
print(metricas_umbral[c(fila_050, fila_youden), ])
## Umbral Sensitivity Specificity Accuracy Precision VPN
## 50 0.5 0.8175182 0.4696970 0.7044335 0.7619048 0.5535714
## 70 0.7 0.6058394 0.8636364 0.6896552 0.9021739 0.5135135
## Balanced_Accuracy Youden
## 50 0.6436076 0.2872152
## 70 0.7347379 0.4694758
############################################################
# 16.13 TABLA RESUMEN DE UMBRALES CLAVE
############################################################
tabla_umbrales_clave <- rbind(
data.frame(
Criterio = "Umbral fijo 0.50",
Umbral = metricas_umbral$Umbral[fila_050],
Sensibilidad = metricas_umbral$Sensitivity[fila_050],
Especificidad = metricas_umbral$Specificity[fila_050],
Accuracy = metricas_umbral$Accuracy[fila_050],
Youden = metricas_umbral$Youden[fila_050],
Balanced_Accuracy = metricas_umbral$Balanced_Accuracy[fila_050]
),
data.frame(
Criterio = "Óptimo por Youden",
Umbral = metricas_umbral$Umbral[fila_youden],
Sensibilidad = metricas_umbral$Sensitivity[fila_youden],
Especificidad = metricas_umbral$Specificity[fila_youden],
Accuracy = metricas_umbral$Accuracy[fila_youden],
Youden = metricas_umbral$Youden[fila_youden],
Balanced_Accuracy = metricas_umbral$Balanced_Accuracy[fila_youden]
)
)
tabla_umbrales_clave <- tabla_umbrales_clave %>%
mutate(across(where(is.numeric), ~ round(.x, 4))) %>%
as.data.frame()
cat("\n=============================\n")
##
## =============================
cat("TABLA DE UMBRALES CLAVE\n")
## TABLA DE UMBRALES CLAVE
cat("=============================\n")
## =============================
print(tabla_umbrales_clave)
## Criterio Umbral Sensibilidad Especificidad Accuracy Youden
## 1 Umbral fijo 0.50 0.5 0.8175 0.4697 0.7044 0.2872
## 2 Óptimo por Youden 0.7 0.6058 0.8636 0.6897 0.4695
## Balanced_Accuracy
## 1 0.6436
## 2 0.7347
############################################################
# 16.14 GRÁFICO 1: SCATTER DEL TRADE-OFF
############################################################
graf_tradeoff_scatter <- ggplot(
metricas_umbral,
aes(x = Specificity, y = Sensitivity)
) +
geom_point(aes(size = Accuracy, color = Umbral), alpha = 0.80) +
scale_color_gradient(low = "#132B43", high = "#56B1F7") +
geom_point(
data = metricas_umbral[fila_050, , drop = FALSE],
shape = 21, fill = "white", color = "black", size = 6, stroke = 1.5
) +
labs(
title = "Trade-off entre sensibilidad y especificidad según el umbral",
subtitle = "Cada punto representa un punto de corte distinto",
x = "Especificidad",
y = "Sensibilidad",
color = "Umbral",
size = "Accuracy"
) +
theme_minimal(base_size = 13)
print(graf_tradeoff_scatter)
############################################################
# 16.15 GRÁFICO 2: CURVAS DE SENSIBILIDAD Y ESPECIFICIDAD
############################################################
metricas_long_umbral <- tidyr::pivot_longer(
metricas_umbral,
cols = c("Sensitivity", "Specificity"),
names_to = "Metrica",
values_to = "Valor"
)
metricas_long_umbral$Metrica <- factor(
metricas_long_umbral$Metrica,
levels = c("Sensitivity", "Specificity"),
labels = c("Sensitivity", "Specificity")
)
graf_tradeoff_lineas <- ggplot(
metricas_long_umbral,
aes(x = Umbral, y = Valor, color = Metrica)
) +
geom_line(linewidth = 1.4) +
geom_vline(xintercept = metricas_umbral$Umbral[fila_050],
linetype = "dashed", linewidth = 0.8) +
geom_vline(xintercept = umbral_youden,
linetype = "dotted", linewidth = 0.8) +
annotate("text",
x = metricas_umbral$Umbral[fila_050] - 0.01,
y = 0.08,
label = "0.50",
angle = 90,
vjust = -0.4,
size = 5) +
annotate("text",
x = umbral_youden - 0.01,
y = 0.08,
label = "Youden",
angle = 90,
vjust = -0.4,
size = 5) +
labs(
title = "Trade-off entre sensibilidad y especificidad",
subtitle = "Evaluación del efecto del punto de corte sobre el desempeño del modelo logístico",
x = "Umbral de clasificación",
y = "Valor de la métrica",
color = "Métrica"
) +
theme_minimal(base_size = 13)
print(graf_tradeoff_lineas)
############################################################
# 16.16 DECISIÓN OPERATIVA DEL UMBRAL
############################################################
decision_umbral <- data.frame(
Criterio = c("Modelo final reportado", "Referencia alternativa"),
Umbral = c(0.50, umbral_youden),
Justificacion = c(
"Se mantiene 0.50 por consistencia interpretativa y por priorizar sensibilidad en detección de desertores",
"Maximiza el índice de Youden y representa el mejor equilibrio global Sensibilidad-Especificidad"
)
)
cat("\n=============================\n")
##
## =============================
cat("DECISIÓN OPERATIVA DEL UMBRAL\n")
## DECISIÓN OPERATIVA DEL UMBRAL
cat("=============================\n")
## =============================
print(decision_umbral)
## Criterio Umbral
## 1 Modelo final reportado 0.5
## 2 Referencia alternativa 0.7
## Justificacion
## 1 Se mantiene 0.50 por consistencia interpretativa y por priorizar sensibilidad en detección de desertores
## 2 Maximiza el índice de Youden y representa el mejor equilibrio global Sensibilidad-Especificidad
############################################################
# 17. IMPORTANCIA DE VARIABLES - RANDOM FOREST
############################################################
imp_rf <- varImp(modelo_rf_cv)
print(imp_rf)
## rf variable importance
##
## only 20 most important variables shown (out of 29)
##
## Overall
## RENDIMIENTONO APROBADO 100.000
## EDAD 48.632
## CARRERAEDUCACION MATEMATICA-PRES 30.402
## TRABAJANO 18.016
## MODALIDADSemipresencial 17.647
## SOLVENTARAyuda Familiar 13.319
## RESIDENCIACENTRAL 12.720
## CARRERAMATEMATICA 12.529
## SOLVENTARTrabajo Personal 11.634
## SEXOMASCULINO 11.448
## ESTUDIOS_PADRES30-34 años 10.814
## ESTUDIOS_PADRES14-23 años 10.383
## NIVEL_SOCIOMEDIO 10.221
## CARRERAEDUCACION MATEMATICA-SEMI 10.175
## RESIDENCIARESTO DEL PAÍS 9.945
## ESTUDIOS_PADRES24-29 años 9.900
## TIPO_COLPrivado 9.302
## INGRESOMás de dos y hasta cinco salarios mínimos 9.205
## CARRERAESTADISTICA-SEMI 9.043
## TIPO_INGRESOADMISION DIRECTA 7.978
plot(imp_rf, top = 20, main = "Importancia de variables - Random Forest")
############################################################
# 18. EXPORTACIÓN DE RESULTADOS
############################################################
dir.create("salidas_tesis", showWarnings = FALSE)
############################################################
# 18.1 EXPORTACIÓN DE TABLAS EN CSV
############################################################
write.csv(resultados_chi,
"salidas_tesis/tabla_chi_fisher.csv",
row.names = FALSE)
write.csv(resultados_bivariados,
"salidas_tesis/tabla_logistica_bivariada.csv",
row.names = FALSE)
write.csv(tabla_metricas_test,
"salidas_tesis/tabla_metricas_test_modelos.csv",
row.names = FALSE)
write.csv(metricas_graf,
"salidas_tesis/tabla_6_metricas_modelos_test.csv",
row.names = FALSE)
write.csv(tabla_or,
"salidas_tesis/tabla_or_modelo_logistico_final.csv",
row.names = FALSE)
# Exportación directa del objeto VIF ya estructurado
write.csv(vif_modelo,
"salidas_tesis/tabla_vif_modelo_logistico_final.csv",
row.names = FALSE)
write.csv(tabla_logit_final,
"salidas_tesis/tabla_modelo_logistico_final.csv",
row.names = FALSE)
write.csv(metricas_umbral,
"salidas_tesis/tabla_metricas_por_umbral_logit.csv",
row.names = FALSE)
write.csv(tabla_umbrales_clave,
"salidas_tesis/tabla_umbrales_clave_logit.csv",
row.names = FALSE)
write.csv(decision_umbral,
"salidas_tesis/tabla_decision_umbral_logit.csv",
row.names = FALSE)
############################################################
# 18.2 EXPORTACIÓN A EXCEL CON FORMATO TIPO TESIS
############################################################
wb <- createWorkbook(creator = "ChatGPT - Tesis de Estadística")
# -----------------------------
# COLORES Y ESTILOS
# -----------------------------
color_azul_oscuro <- "#1F4E78"
color_gris_claro <- "#F4F6F8"
color_gris_borde <- "#B8C2CC"
color_blanco <- "#FFFFFF"
color_seccion <- "#44546A"
estilo_titulo <- createStyle(
fontSize = 14,
textDecoration = "bold",
fontColour = color_blanco,
fgFill = color_azul_oscuro,
halign = "left",
valign = "center"
)
estilo_seccion <- createStyle(
fontSize = 11,
textDecoration = "bold",
fontColour = color_blanco,
fgFill = color_seccion,
halign = "left",
valign = "center",
border = "TopBottomLeftRight",
borderColour = color_seccion
)
estilo_nota <- createStyle(
fontSize = 10,
fontColour = "#5B6570",
textDecoration = "italic",
halign = "left"
)
estilo_header <- createStyle(
fontSize = 11,
textDecoration = "bold",
fontColour = color_blanco,
fgFill = color_azul_oscuro,
halign = "center",
valign = "center",
border = "TopBottomLeftRight",
borderColour = color_gris_borde
)
estilo_texto_par <- createStyle(
fgFill = color_blanco,
border = "TopBottomLeftRight",
borderColour = color_gris_borde,
valign = "center"
)
estilo_texto_impar <- createStyle(
fgFill = color_gris_claro,
border = "TopBottomLeftRight",
borderColour = color_gris_borde,
valign = "center"
)
estilo_num_par <- createStyle(
fgFill = color_blanco,
border = "TopBottomLeftRight",
borderColour = color_gris_borde,
halign = "center",
valign = "center",
numFmt = "0.0000"
)
estilo_num_impar <- createStyle(
fgFill = color_gris_claro,
border = "TopBottomLeftRight",
borderColour = color_gris_borde,
halign = "center",
valign = "center",
numFmt = "0.0000"
)
estilo_entero_par <- createStyle(
fgFill = color_blanco,
border = "TopBottomLeftRight",
borderColour = color_gris_borde,
halign = "center",
valign = "center",
numFmt = "0"
)
estilo_entero_impar <- createStyle(
fgFill = color_gris_claro,
border = "TopBottomLeftRight",
borderColour = color_gris_borde,
halign = "center",
valign = "center",
numFmt = "0"
)
estilo_porcentaje_par <- createStyle(
fgFill = color_blanco,
border = "TopBottomLeftRight",
borderColour = color_gris_borde,
halign = "center",
valign = "center",
numFmt = "0.00"
)
estilo_porcentaje_impar <- createStyle(
fgFill = color_gris_claro,
border = "TopBottomLeftRight",
borderColour = color_gris_borde,
halign = "center",
valign = "center",
numFmt = "0.00"
)
estilo_texto_centrado_par <- createStyle(
fgFill = color_blanco,
border = "TopBottomLeftRight",
borderColour = color_gris_borde,
halign = "center",
valign = "center"
)
estilo_texto_centrado_impar <- createStyle(
fgFill = color_gris_claro,
border = "TopBottomLeftRight",
borderColour = color_gris_borde,
halign = "center",
valign = "center"
)
es_numerica <- function(x) is.numeric(x) || is.integer(x)
aplicar_estilos_fila <- function(wb, hoja, datos, fila_inicio = 4) {
if (nrow(datos) == 0) return(invisible(NULL))
for (i in seq_len(nrow(datos))) {
fila_excel <- fila_inicio + i
impar <- i %% 2 == 1
for (j in seq_along(datos)) {
valor <- datos[[j]][i]
nombre_col <- names(datos)[j]
estilo <- NULL
if (es_numerica(valor)) {
if (grepl("porcentaje|sens|spec|auc|accuracy|kappa|precision|f1|vpn|youden|or|ic95|coef|estadistico|umbral", tolower(nombre_col))) {
estilo <- if (impar) estilo_num_impar else estilo_num_par
} else if (abs(valor - round(valor)) < .Machine$double.eps^0.5) {
estilo <- if (impar) estilo_entero_impar else estilo_entero_par
} else {
estilo <- if (impar) estilo_num_impar else estilo_num_par
}
} else {
if (grepl("modelo|variable|categoria|criterio|metodo|significancia|formula|nivel|justificacion|estado", tolower(nombre_col))) {
estilo <- if (impar) estilo_texto_impar else estilo_texto_par
} else {
estilo <- if (impar) estilo_texto_centrado_impar else estilo_texto_centrado_par
}
}
addStyle(wb, hoja, style = estilo, rows = fila_excel, cols = j, gridExpand = FALSE, stack = TRUE)
}
}
}
exportar_hoja_tesis <- function(wb, nombre_hoja, datos, titulo, seccion = "Resultados exportados", nota = "Fuente: Elaboración propia") {
addWorksheet(wb, nombre_hoja)
writeData(wb, nombre_hoja, titulo, startRow = 1, startCol = 1)
mergeCells(wb, nombre_hoja, cols = 1:max(1, ncol(datos)), rows = 1)
addStyle(wb, nombre_hoja, estilo_titulo, rows = 1, cols = 1, gridExpand = TRUE)
setRowHeights(wb, nombre_hoja, rows = 1, heights = 24)
writeData(wb, nombre_hoja, seccion, startRow = 2, startCol = 1)
mergeCells(wb, nombre_hoja, cols = 1:max(1, ncol(datos)), rows = 2)
addStyle(wb, nombre_hoja, estilo_seccion, rows = 2, cols = 1, gridExpand = TRUE)
writeData(wb, nombre_hoja, nota, startRow = 3, startCol = 1)
mergeCells(wb, nombre_hoja, cols = 1:max(1, ncol(datos)), rows = 3)
addStyle(wb, nombre_hoja, estilo_nota, rows = 3, cols = 1, gridExpand = TRUE)
writeData(wb, nombre_hoja, datos, startRow = 4, startCol = 1, headerStyle = estilo_header, withFilter = TRUE)
aplicar_estilos_fila(wb, nombre_hoja, datos, fila_inicio = 4)
freezePane(wb, nombre_hoja, firstActiveRow = 5, firstActiveCol = 1)
setColWidths(wb, nombre_hoja, cols = 1:max(1, ncol(datos)), widths = "auto")
}
# Hoja índice
indice <- data.frame(
Hoja = c(
"01_Chi_Fisher", "02_Logistica_Bivariada", "03_Metricas_Test_Modelos",
"04_Metricas_6_Modelos", "05_OR_Modelo_Final", "06_VIF_Modelo_Final",
"07_Modelo_Logistico_Final", "08_Metricas_por_Umbral",
"09_Umbrales_Clave", "10_Decision_Umbral"
),
Descripcion = c(
"Resultados de pruebas Chi-cuadrado y Fisher",
"Regresión logística simple para variables numéricas",
"Métricas finales en el conjunto de prueba",
"Tabla de apoyo para gráfico comparativo de 6 métricas",
"Coeficientes, OR e IC95% del modelo logístico final",
"Diagnóstico de multicolinealidad del modelo final",
"Resumen del modelo logístico final en test",
"Desempeño del modelo logístico según el umbral",
"Comparación entre umbral 0.50 y umbral óptimo de Youden",
"Criterio final adoptado para el punto de corte"
),
stringsAsFactors = FALSE
)
exportar_hoja_tesis(
wb, "00_Indice", indice,
titulo = "Índice del libro de resultados",
seccion = "Resumen de hojas del archivo Excel"
)
exportar_hoja_tesis(wb, "01_Chi_Fisher", resultados_chi,
titulo = "Pruebas de asociación bivariante",
seccion = "Resultados Chi-cuadrado y Prueba Exacta de Fisher")
exportar_hoja_tesis(wb, "02_Logistica_Bivariada", resultados_bivariados,
titulo = "Regresión logística bivariada",
seccion = "Resultados para covariables numéricas")
exportar_hoja_tesis(wb, "03_Metricas_Test_Modelos", tabla_metricas_test,
titulo = "Métricas de desempeño en el conjunto de prueba",
seccion = "Comparación de modelos")
exportar_hoja_tesis(wb, "04_Metricas_6_Modelos", metricas_graf,
titulo = "Tabla base del gráfico de 6 métricas",
seccion = "Accuracy, Sensitivity, Specificity, Precision, F1 y AUC")
exportar_hoja_tesis(wb, "05_OR_Modelo_Final", tabla_or,
titulo = "Odds ratio del modelo logístico final",
seccion = "Coeficientes e intervalos de confianza")
exportar_hoja_tesis(wb, "06_VIF_Modelo_Final", vif_modelo,
titulo = "Diagnóstico de multicolinealidad",
seccion = "Factores de inflación de la varianza")
exportar_hoja_tesis(wb, "07_Modelo_Logistico_Final", tabla_logit_final,
titulo = "Resumen del modelo logístico final",
seccion = "Desempeño final en el conjunto de prueba")
exportar_hoja_tesis(wb, "08_Metricas_por_Umbral", metricas_umbral,
titulo = "Métricas del modelo logístico según el umbral",
seccion = "Evaluación del trade-off entre sensibilidad y especificidad")
exportar_hoja_tesis(wb, "09_Umbrales_Clave", tabla_umbrales_clave,
titulo = "Comparación de umbrales clave",
seccion = "Umbral fijo 0.50 y óptimo por Youden")
exportar_hoja_tesis(wb, "10_Decision_Umbral", decision_umbral,
titulo = "Decisión operativa del umbral",
seccion = "Criterio adoptado para la clasificación final")
saveWorkbook(wb, "salidas_tesis/resumen_resultados_tesis.xlsx", overwrite = TRUE)
############################################################
# 18.3 EXPORTACIÓN DE GRÁFICOS (ggplot)
############################################################
# Análisis descriptivo
ggsave(
filename = "salidas_tesis/grafico_estado_academico.png",
plot = graf_estado,
width = 8,
height = 6,
dpi = 300
)
ggsave(
filename = "salidas_tesis/boxplot_edad_estado_academico.png",
plot = graf_edad,
width = 8,
height = 6,
dpi = 300
)
# Comparación de modelos
ggsave(
filename = "salidas_tesis/grafico_6_metricas_modelos_test.png",
plot = graf_6_metricas,
width = 14,
height = 8,
dpi = 300
)
# Trade-off del modelo logístico final
ggsave(
filename = "salidas_tesis/scatter_tradeoff_umbral_modelo_final.png",
plot = graf_tradeoff_scatter,
width = 12,
height = 7,
dpi = 300
)
ggsave(
filename = "salidas_tesis/tradeoff_sens_specificity_modelo_final.png",
plot = graf_tradeoff_lineas,
width = 12,
height = 7,
dpi = 300
)
############################################################
# 18.4 EXPORTACIÓN DE GRÁFICOS BASE R
############################################################
# ROC comparativa de modelos
png("salidas_tesis/curvas_roc_modelos_test.png", width = 1400, height = 1000, res = 150)
plot(resultados_test_lista$Logit$roc,
main = "Curvas ROC de los modelos en el conjunto de prueba",
lwd = 2)
plot(resultados_test_lista$Tree$roc, add = TRUE, lty = 2, lwd = 2)
plot(resultados_test_lista$RF$roc, add = TRUE, lty = 3, lwd = 2)
plot(resultados_test_lista$SVM$roc, add = TRUE, lty = 4, lwd = 2)
plot(resultados_test_lista$KNN$roc, add = TRUE, lty = 5, lwd = 2)
plot(resultados_test_lista$NN$roc, add = TRUE, lty = 6, lwd = 2)
plot(resultados_test_lista$NB$roc, add = TRUE, lty = 7, lwd = 2)
legend(
"bottomright",
legend = c(
paste0("Logit (AUC=", round(resultados_test_lista$Logit$resumen$AUC, 3), ")"),
paste0("Tree (AUC=", round(resultados_test_lista$Tree$resumen$AUC, 3), ")"),
paste0("RF (AUC=", round(resultados_test_lista$RF$resumen$AUC, 3), ")"),
paste0("SVM (AUC=", round(resultados_test_lista$SVM$resumen$AUC, 3), ")"),
paste0("KNN (AUC=", round(resultados_test_lista$KNN$resumen$AUC, 3), ")"),
paste0("NN (AUC=", round(resultados_test_lista$NN$resumen$AUC, 3), ")"),
paste0("NB (AUC=", round(resultados_test_lista$NB$resumen$AUC, 3), ")")
),
lty = 1:7,
lwd = 2,
cex = 0.8
)
dev.off()
## png
## 2
# ROC del modelo logístico final
png("salidas_tesis/roc_modelo_logistico_final.png", width = 1200, height = 900, res = 150)
plot(
roc_test_logit,
main = paste0("Curva ROC - Modelo logístico final (AUC = ", round(auc_test_logit, 3), ")"),
lwd = 2
)
abline(a = 0, b = 1, lty = 2)
dev.off()
## png
## 2
# Importancia de variables en Random Forest
png("salidas_tesis/importancia_variables_random_forest.png", width = 1200, height = 900, res = 150)
plot(imp_rf, top = 20, main = "Importancia de variables - Random Forest")
dev.off()
## png
## 2
# Diagnóstico del modelo logístico final
png("salidas_tesis/diagnostico_modelo_logistico_final.png", width = 1400, height = 700, res = 150)
par(mfrow = c(1, 2))
plot(
residuos_pearson,
pch = 20,
main = "Residuos de Pearson",
ylab = "Residuo",
xlab = "Índice"
)
abline(h = c(-2, 2), lty = 2, col = "red")
plot(
cook,
pch = 20,
main = "Distancia de Cook",
ylab = "Cook",
xlab = "Índice"
)
abline(h = umbral_cook, lty = 2, col = "red")
par(mfrow = c(1, 1))
dev.off()
## png
## 2
############################################################
# 18.5 MENSAJE FINAL
############################################################
cat("\n=============================\n")
##
## =============================
cat("RESULTADOS EXPORTADOS CORRECTAMENTE\n")
## RESULTADOS EXPORTADOS CORRECTAMENTE
cat("=============================\n")
## =============================
cat("Archivos generados en la carpeta: salidas_tesis\n")
## Archivos generados en la carpeta: salidas_tesis
cat("- Tablas CSV\n")
## - Tablas CSV
cat("- Archivo Excel consolidado\n")
## - Archivo Excel consolidado
cat("- Gráficos PNG\n")
## - Gráficos PNG
cat("=============================\n")
## =============================
############################################################
# GRAFICO PANEL MEJORADO: COMPARACION DE METRICAS POR MODELO
############################################################
library(ggplot2)
library(dplyr)
library(tidyr)
library(grid)
metricas_plot <- tabla_metricas_test %>%
dplyr::select(Modelo, Accuracy, Precision, Sensitivity, Specificity, F1, AUC) %>%
tidyr::pivot_longer(
cols = -Modelo,
names_to = "Metrica",
values_to = "Valor"
) %>%
mutate(
Metrica = dplyr::recode(Metrica, "Sensitivity" = "Recall"),
Metrica = factor(
Metrica,
levels = c("Accuracy", "Precision", "Recall", "Specificity", "F1", "AUC")
),
Modelo = factor(
Modelo,
levels = c("Tree", "KNN", "NB", "RF", "NN", "Logit", "SVM"),
labels = c("Árbol", "KNN", "NB", "RF", "RN", "Logit", "SVM")
)
)
colores_modelos <- c(
"Árbol" = "#C75D00",
"KNN" = "#6EA61A",
"NB" = "#A8771A",
"RF" = "#6D6AAE",
"RN" = "#D9A400",
"Logit" = "#1F9D7A",
"SVM" = "#D92B8A"
)
grafico_metricas_modelos <- ggplot(
metricas_plot,
aes(x = Modelo, y = Valor, fill = Modelo)
) +
geom_col(width = 0.68, show.legend = FALSE) +
geom_text(
aes(label = sprintf("%.3f", Valor)),
vjust = -0.22,
size = 4.4,
fontface = "bold"
) +
facet_wrap(~ Metrica, ncol = 3) +
scale_fill_manual(values = colores_modelos) +
scale_y_continuous(
limits = c(0, 1.08),
breaks = c(0, 0.25, 0.50, 0.75, 1.00),
labels = sprintf("%.2f", c(0, 0.25, 0.50, 0.75, 1.00)),
expand = expansion(mult = c(0, 0.04))
) +
labs(
title = "Comparación de métricas de desempeño por modelo",
x = "Modelo",
y = "Valor"
) +
theme_minimal(base_size = 15) +
theme(
plot.title = element_text(
face = "bold",
hjust = 0.5,
size = 22,
margin = ggplot2::margin(b = 10)
),
axis.text.x = element_text(
angle = 32,
hjust = 1,
size = 14,
face = "bold"
),
axis.text.y = element_text(size = 13),
axis.title.x = element_text(face = "bold", size = 20),
axis.title.y = element_text(face = "bold", size = 20),
strip.text = element_text(face = "bold", size = 18),
panel.grid.minor = element_blank(),
panel.grid.major.x = element_blank(),
panel.grid.major.y = element_line(
linewidth = 0.35,
colour = "#D9D9D9"
),
panel.spacing = grid::unit(1.4, "lines"),
plot.margin = ggplot2::margin(15, 20, 15, 20)
)
print(grafico_metricas_modelos)
dir.create("salidas_tesis", showWarnings = FALSE)
ggsave(
filename = "salidas_tesis/metricas_modelos_panel_presentacion_mejorada.png",
plot = grafico_metricas_modelos,
width = 16,
height = 9.5,
dpi = 300
)
############################################################
# 16. FIGURA SCATTER: COMPARACION INTEGRADA DEL DESEMPENO
# DE LOS MODELOS EN EL CONJUNTO DE PRUEBA
############################################################
library(dplyr)
library(ggrepel)
datos_scatter <- tabla_metricas_test %>%
mutate(
Modelo = factor(
Modelo,
levels = c("NB", "Tree", "KNN", "SVM", "NN", "Logit", "RF")
),
Tipo = case_when(
Modelo %in% c("NN", "Logit", "RF") ~ "Optimo",
Modelo %in% c("Tree", "KNN") ~ "Intermedio",
Modelo %in% c("NB", "SVM") ~ "Sesgado",
TRUE ~ "Intermedio"
),
Specificity_plot = ifelse(Specificity == 0, 0.03, Specificity)
)
colores_tipo <- c(
"Intermedio" = "#E3B505",
"Optimo" = "#2FBF71",
"Sesgado" = "#E74C3C"
)
grafico_scatter_modelos_mejorado <- ggplot(
datos_scatter,
aes(
x = Specificity_plot,
y = Sensitivity,
size = AUC,
fill = Tipo,
label = Modelo
)
) +
geom_point(
shape = 21,
colour = "white",
stroke = 0.9,
alpha = 0.95
) +
ggrepel::geom_text_repel(
aes(color = Tipo),
size = 6,
fontface = "bold",
show.legend = FALSE,
box.padding = 0.35,
point.padding = 0.25,
segment.color = "grey60",
segment.size = 0.4,
max.overlaps = Inf
) +
scale_fill_manual(values = colores_tipo, name = "Tipo de modelo") +
scale_color_manual(values = colores_tipo, guide = "none") +
scale_size_continuous(
name = "AUC",
range = c(8, 16),
breaks = c(0.76, 0.77, 0.78, 0.79),
labels = c("0.76", "0.77", "0.78", "0.79")
) +
scale_x_continuous(
breaks = c(0.00, 0.10, 0.20, 0.30, 0.40, 0.50),
labels = c("0.00", "0.10", "0.20", "0.30", "0.40", "0.50"),
expand = expansion(mult = c(0.02, 0.03))
) +
coord_cartesian(
xlim = c(0.00, 0.56),
ylim = c(0.83, 0.985),
clip = "off"
) +
labs(
title = "Comparación integrada del desempeño de modelos",
subtitle = "Relación entre sensibilidad, especificidad y AUC (conjunto de prueba)",
x = "Especificidad",
y = "Sensibilidad"
) +
theme_minimal(base_size = 16) +
theme(
plot.title = element_text(face = "bold", hjust = 0.5, size = 24),
plot.subtitle = element_text(
hjust = 0.5,
size = 17,
margin = ggplot2::margin(b = 12)
),
axis.title = element_text(face = "bold", size = 20),
axis.text = element_text(size = 14),
legend.title = element_text(face = "bold", size = 16),
legend.text = element_text(size = 14),
panel.grid.minor = element_blank(),
panel.grid.major = element_line(linewidth = 0.35, colour = "#D9D9D9"),
plot.margin = ggplot2::margin(15, 70, 15, 15)
) +
guides(
fill = guide_legend(
order = 1,
override.aes = list(
shape = 21,
size = 5,
colour = "white",
fill = c("#E3B505", "#2FBF71", "#E74C3C"),
alpha = 1
)
),
size = guide_legend(
order = 2,
override.aes = list(
shape = 21,
fill = "grey20",
colour = "grey20",
alpha = 1
)
)
)
print(grafico_scatter_modelos_mejorado)
ggsave(
filename = "salidas_tesis/Figura_scatter_modelos_color_mejorado.png",
plot = grafico_scatter_modelos_mejorado,
width = 14,
height = 8.5,
dpi = 300
)
############################################################
# TABLA 4.10 - VERSIÓN 2 CORREGIDA
# REGRESIÓN LOGÍSTICA SIMPLE CON 13 VARIABLES CATEGÓRICAS
# FORMATO TIPO TESIS (BLOQUES POR VARIABLE)
############################################################
# =========================
# 0. PAQUETES
# =========================
paquetes <- c("dplyr", "purrr", "stringr", "openxlsx", "flextable")
instalar <- paquetes[!paquetes %in% installed.packages()[, "Package"]]
if (length(instalar) > 0) install.packages(instalar)
library(dplyr)
library(purrr)
##
## Adjuntando el paquete: 'purrr'
## The following object is masked from 'package:scales':
##
## discard
## The following object is masked from 'package:car':
##
## some
## The following object is masked from 'package:caret':
##
## lift
library(stringr)
library(openxlsx)
library(flextable)
##
## Adjuntando el paquete: 'flextable'
## The following object is masked from 'package:purrr':
##
## compose
# =========================
# 1. BASE DE DATOS
# =========================
# Se asume que ya existe train_data
datos <- train_data
# =========================
# 2. VARIABLE RESPUESTA
# Desertor = 1 ; No_Desertor = 0
# =========================
datos <- datos %>%
mutate(
ESTADO_BIN = ifelse(ESTADO_ACADEMICO == "Desertor", 1, 0)
)
# =========================
# 3. VARIABLES CATEGÓRICAS
# =========================
variables_categoricas <- c(
"SOLVENTAR",
"RESIDENCIA",
"CARRERA",
"TIPO_INGRESO",
"RENDIMIENTO",
"MODALIDAD",
"TRABAJA",
"ESTUDIOS_PADRES",
"SEXO",
"TIPO_COL",
"INGRESO",
"EST_CIVIL",
"NIVEL_SOCIO"
)
# =========================
# 4. ETIQUETAS DE VARIABLES
# =========================
etiquetas_variables <- c(
SOLVENTAR = "Solventar",
RESIDENCIA = "Residencia",
CARRERA = "Carrera",
TIPO_INGRESO = "Tipo_Ingreso",
RENDIMIENTO = "Rendimiento",
MODALIDAD = "Modalidad",
TRABAJA = "Trabaja",
ESTUDIOS_PADRES = "Estudios_Padres",
SEXO = "Sexo",
TIPO_COL = "Tipo_Col",
INGRESO = "Ingreso",
EST_CIVIL = "Est_Civil",
NIVEL_SOCIO = "Nivel_Socio"
)
# =========================
# 5. CATEGORÍAS DE REFERENCIA
# EXTRAÍDAS Y AJUSTADAS SEGÚN TU RECODIFICACIÓN
# =========================
referencias <- list(
SOLVENTAR = "Beca/exoneración total",
RESIDENCIA = "ASUNCIÓN",
CARRERA = "ESTADISTICA-PRES",
TIPO_INGRESO = "INGRESO",
RENDIMIENTO = "NO APROBADO",
MODALIDAD = "Presencial",
TRABAJA = "NO",
ESTUDIOS_PADRES = "hasta 13 años",
SEXO = "FEMENINO",
TIPO_COL = "Público",
INGRESO = "Hasta dos salarios mínimos",
EST_CIVIL = "Soltero/a",
NIVEL_SOCIO = levels(datos$NIVEL_SOCIO)[1]
)
# =========================
# 6. FUNCIÓN DE SIGNIFICANCIA
# =========================
etiqueta_significancia <- function(p) {
if (is.na(p)) {
return(NA_character_)
} else if (p < 0.01) {
return("*** (p < 0.01)")
} else if (p < 0.05) {
return("** (p < 0.05)")
} else if (p < 0.10) {
return("* (p < 0.10)")
} else {
return("No sig.")
}
}
# =========================
# 7. FUNCIÓN PARA AJUSTAR UN MODELO
# =========================
ajustar_logit_simple_cat_v2 <- function(var, indice_modelo, datos, refs, etiquetas_vars) {
datos_modelo <- datos %>%
select(ESTADO_BIN, all_of(var)) %>%
filter(!is.na(.data[[var]]))
datos_modelo[[var]] <- as.factor(datos_modelo[[var]])
# Fijar categoría de referencia
ref_actual <- refs[[var]]
niveles_actuales <- levels(datos_modelo[[var]])
if (!is.null(ref_actual) && ref_actual %in% niveles_actuales) {
datos_modelo[[var]] <- relevel(datos_modelo[[var]], ref = ref_actual)
} else {
warning(paste(
"No se encontró la referencia de", var,
"en los niveles de la base. Se usará la primera categoría disponible."
))
ref_actual <- niveles_actuales[1]
datos_modelo[[var]] <- relevel(datos_modelo[[var]], ref = ref_actual)
}
# Ajuste del modelo
formula_modelo <- as.formula(paste("ESTADO_BIN ~", var))
modelo <- glm(formula_modelo, data = datos_modelo, family = binomial(link = "logit"))
# Resumen del modelo
sm <- summary(modelo)$coefficients
sm <- as.data.frame(sm)
sm$term <- rownames(sm)
rownames(sm) <- NULL
# Excluir intercepto
sm <- sm %>%
filter(term != "(Intercept)")
# Intervalos de confianza Wald
sm <- sm %>%
mutate(
IC_beta_LI = Estimate - 1.96 * `Std. Error`,
IC_beta_LS = Estimate + 1.96 * `Std. Error`,
OR = exp(Estimate),
IC95_LI = exp(IC_beta_LI),
IC95_LS = exp(IC_beta_LS)
)
# Extraer nivel comparado
sm <- sm %>%
mutate(
Nivel = str_remove(term, paste0("^", var)),
`N°` = paste0("M", indice_modelo),
Variable = etiquetas_vars[[var]],
`Categoría de Referencia/Nivel` = ref_actual,
`Coefi-cientes` = Estimate,
`Exp (OR)` = OR,
`Estadístico_Z` = `z value`,
P_Valor = `Pr(>|z|)`,
Significancia = sapply(P_Valor, etiqueta_significancia)
) %>%
select(
`N°`,
Variable,
`Categoría de Referencia/Nivel`,
Nivel,
`Coefi-cientes`,
OR,
IC95_LI,
IC95_LS,
`Exp (OR)`,
`Estadístico_Z`,
P_Valor,
Significancia
)
sm
}
# =========================
# 8. AJUSTAR LOS 13 MODELOS
# =========================
tabla_4_10_v2 <- map2_dfr(
variables_categoricas,
seq_along(variables_categoricas),
~ ajustar_logit_simple_cat_v2(.x, .y, datos, referencias, etiquetas_variables)
)
# =========================
# 9. REDONDEAR
# =========================
tabla_4_10_v2 <- tabla_4_10_v2 %>%
mutate(
`Coefi-cientes` = round(`Coefi-cientes`, 4),
OR = round(OR, 4),
IC95_LI = round(IC95_LI, 4),
IC95_LS = round(IC95_LS, 4),
`Exp (OR)` = round(`Exp (OR)`, 4),
`Estadístico_Z` = round(`Estadístico_Z`, 4),
P_Valor = round(P_Valor, 4)
)
# =========================
# 10. FORMATO TIPO TESIS
# Mostrar una sola vez N°, Variable y Referencia por bloque
# =========================
tabla_4_10_v2_formato <- tabla_4_10_v2 %>%
group_by(`N°`, Variable) %>%
mutate(
`N°` = ifelse(row_number() == 1, `N°`, ""),
Variable = ifelse(row_number() == 1, Variable, ""),
`Categoría de Referencia/Nivel` = ifelse(
row_number() == 1,
`Categoría de Referencia/Nivel`,
""
)
) %>%
ungroup()
# =========================
# 11. RENOMBRAR COLUMNAS FINALES
# =========================
tabla_4_10_v2_formato <- tabla_4_10_v2_formato %>%
rename(
`IC.95 Inferior` = IC95_LI,
`IC.95 Superior` = IC95_LS
)
print(tabla_4_10_v2_formato)
## # A tibble: 28 × 12
## `N°` Variable Categoría de Referencia/Ni…¹ Nivel `Coefi-cientes` OR
## <chr> <chr> <chr> <chr> <dbl> <dbl>
## 1 "M1" "Solventar" "Beca/exoneración total" Beca… -0.182 0.833
## 2 "" "" "" Trab… 0.500 1.65
## 3 "" "" "" Ayud… -0.240 0.787
## 4 "M2" "Residencia" "ASUNCIÓN" CENT… -0.345 0.708
## 5 "" "" "" REST… 0.398 1.49
## 6 "M3" "Carrera" "ESTADISTICA-PRES" ESTA… 0.718 2.05
## 7 "" "" "" MATE… -0.507 0.602
## 8 "" "" "" EDUC… -1.60 0.202
## 9 "" "" "" EDUC… 0.495 1.64
## 10 "M4" "Tipo_Ingreso" "INGRESO" ADMI… -0.505 0.603
## # ℹ 18 more rows
## # ℹ abbreviated name: ¹`Categoría de Referencia/Nivel`
## # ℹ 6 more variables: `IC.95 Inferior` <dbl>, `IC.95 Superior` <dbl>,
## # `Exp (OR)` <dbl>, Estadístico_Z <dbl>, P_Valor <dbl>, Significancia <chr>
# =========================
# 12. EXPORTAR A EXCEL
# =========================
write.xlsx(
tabla_4_10_v2_formato,
file = "Tabla_4_10_logistica_simple_categoricas_v2.xlsx",
rowNames = FALSE
)
# =========================
# 13. TABLA BONITA PARA WORD / REPORTE
# =========================
ft_tabla_4_10 <- flextable(tabla_4_10_v2_formato)
ft_tabla_4_10 <- ft_tabla_4_10 |>
theme_booktabs() |>
autofit() |>
align(align = "center", part = "all") |>
valign(valign = "center", part = "all") |>
fontsize(size = 9, part = "all") |>
bold(part = "header") |>
set_caption("Tabla 4.10: Modelos de regresión con una sola variable explicativa con sus respectivos resúmenes")
ft_tabla_4_10
N° | Variable | Categoría de Referencia/Nivel | Nivel | Coefi-cientes | OR | IC.95 Inferior | IC.95 Superior | Exp (OR) | Estadístico_Z | P_Valor | Significancia |
|---|---|---|---|---|---|---|---|---|---|---|---|
M1 | Solventar | Beca/exoneración total | Beca/exoneración parcial | -0.1823 | 0.8333 | 0.2984 | 2.3274 | 0.8333 | -0.3479 | 0.7279 | No sig. |
Trabajo Personal | 0.4998 | 1.6485 | 0.7219 | 3.7642 | 1.6485 | 1.1865 | 0.2354 | No sig. | |||
Ayuda Familiar | -0.2395 | 0.7870 | 0.3432 | 1.8046 | 0.7870 | -0.5656 | 0.5716 | No sig. | |||
M2 | Residencia | ASUNCIÓN | CENTRAL | -0.3452 | 0.7080 | 0.4618 | 1.0856 | 0.7080 | -1.5834 | 0.1133 | No sig. |
RESTO DEL PAÍS | 0.3984 | 1.4895 | 0.7976 | 2.7814 | 1.4895 | 1.2504 | 0.2111 | No sig. | |||
M3 | Carrera | ESTADISTICA-PRES | ESTADISTICA-SEMI | 0.7185 | 2.0513 | 1.0127 | 4.1551 | 2.0513 | 1.9950 | 0.0460 | ** (p < 0.05) |
MATEMATICA | -0.5067 | 0.6025 | 0.3526 | 1.0295 | 0.6025 | -1.8537 | 0.0638 | * (p < 0.10) | |||
EDUCACION MATEMATICA-PRES | -1.5994 | 0.2020 | 0.0988 | 0.4130 | 0.2020 | -4.3843 | 0.0000 | *** (p < 0.01) | |||
EDUCACION MATEMATICA-SEMI | 0.4953 | 1.6410 | 0.9267 | 2.9058 | 1.6410 | 1.6990 | 0.0893 | * (p < 0.10) | |||
M4 | Tipo_Ingreso | INGRESO | ADMISION DIRECTA | -0.5053 | 0.6033 | 0.2673 | 1.3615 | 0.6033 | -1.2169 | 0.2236 | No sig. |
M5 | Rendimiento | NO APROBADO | APROBADO | -2.8607 | 0.0572 | 0.0298 | 0.1098 | 0.0572 | -8.6092 | 0.0000 | *** (p < 0.01) |
M6 | Modalidad | Presencial | Semipresencial | 1.0450 | 2.8434 | 1.8629 | 4.3400 | 2.8434 | 4.8434 | 0.0000 | *** (p < 0.01) |
M7 | Trabaja | NO | SI | 0.8681 | 2.3825 | 1.6128 | 3.5195 | 2.3825 | 4.3611 | 0.0000 | *** (p < 0.01) |
M8 | Estudios_Padres | hasta 13 años | 14-23 años | 0.0544 | 1.0559 | 0.6199 | 1.7986 | 1.0559 | 0.2002 | 0.8413 | No sig. |
24-29 años | -0.0146 | 0.9855 | 0.5576 | 1.7419 | 0.9855 | -0.0502 | 0.9599 | No sig. | |||
30-34 años | 0.1715 | 1.1871 | 0.6229 | 2.2621 | 1.1871 | 0.5213 | 0.6021 | No sig. | |||
Más de 34 años | -0.3713 | 0.6899 | 0.3164 | 1.5042 | 0.6899 | -0.9335 | 0.3506 | No sig. | |||
M9 | Sexo | FEMENINO | MASCULINO | 0.1531 | 1.1654 | 0.7894 | 1.7207 | 1.1654 | 0.7702 | 0.4412 | No sig. |
M10 | Tipo_Col | Público | Subvencionado | -0.2752 | 0.7594 | 0.3823 | 1.5087 | 0.7594 | -0.7857 | 0.4320 | No sig. |
Privado | -0.0143 | 0.9858 | 0.6297 | 1.5434 | 0.9858 | -0.0624 | 0.9502 | No sig. | |||
M11 | Ingreso | Hasta dos salarios mínimos | Más de dos y hasta cinco salarios mínimos | 0.0958 | 1.1006 | 0.7156 | 1.6925 | 1.1006 | 0.4363 | 0.6626 | No sig. |
Más de cinco y hasta diez salarios mínimos | 0.3018 | 1.3523 | 0.5788 | 3.1596 | 1.3523 | 0.6970 | 0.4858 | No sig. | |||
Más de diez y hasta quince salarios mínimos | 1.2826 | 3.6061 | 0.4377 | 29.7108 | 3.6061 | 1.1921 | 0.2332 | No sig. | |||
Más de quince salarios mínimos | -0.6633 | 0.5152 | 0.0715 | 3.7107 | 0.5152 | -0.6584 | 0.5103 | No sig. | |||
M12 | Est_Civil | Soltero/a | Casado/a | 0.3768 | 1.4577 | 0.8481 | 2.5053 | 1.4577 | 1.3638 | 0.1726 | No sig. |
Divorciado/a | 0.8650 | 2.3750 | 0.5058 | 11.1510 | 2.3750 | 1.0963 | 0.2730 | No sig. | |||
Otro | 0.4595 | 1.5833 | 0.1631 | 15.3708 | 1.5833 | 0.3963 | 0.6919 | No sig. | |||
M13 | Nivel_Socio | BAJO | MEDIO | 0.5208 | 1.6835 | 0.8799 | 3.2208 | 1.6835 | 1.5735 | 0.1156 | No sig. |
# =========================
# 14. OPCIONAL: AGREGAR EDAD AL FINAL
# =========================
# Si quieres anexar Edad ya calculada manualmente:
#
# fila_edad <- data.frame(
# `N°` = "M14",
# Variable = "Edad",
# `Categoría de Referencia/Nivel` = "",
# Nivel = "",
# `Coefi-cientes` = -0.0268,
# OR = 0.9735,
# `IC.95 Inferior` = 0.9526,
# `IC.95 Superior` = 0.9949,
# `Exp (OR)` = 0.9736,
# `Estadístico_Z` = -2.4231,
# P_Valor = 0.0154,
# Significancia = "** (p < 0.05)"
# )
#
# tabla_4_10_v2_completa <- bind_rows(tabla_4_10_v2_formato, fila_edad)
# print(tabla_4_10_v2_completa)
############################################################
# TABLA COMPARATIVA ENTRE train_data Y test_data
# Estructura agrupada con las 14 variables de la tesis
# Versión corregida: sin error en bind_rows()
############################################################
# =========================
# 0. PAQUETES
# =========================
paquetes <- c("dplyr", "openxlsx")
instalar <- paquetes[!paquetes %in% installed.packages()[, "Package"]]
if (length(instalar) > 0) install.packages(instalar)
library(dplyr)
library(openxlsx)
# =========================
# 1. VALIDACIONES
# =========================
if (!exists("train_data")) stop("No existe el objeto 'train_data'.")
if (!exists("test_data")) stop("No existe el objeto 'test_data'.")
vars_necesarias <- c(
"ESTADO_ACADEMICO",
"SEXO", "RESIDENCIA", "EST_CIVIL", "EDAD",
"CARRERA", "TIPO_INGRESO", "MODALIDAD", "RENDIMIENTO",
"TIPO_COL", "TRABAJA", "SOLVENTAR", "ESTUDIOS_PADRES",
"INGRESO", "NIVEL_SOCIO"
)
faltan_train <- setdiff(vars_necesarias, names(train_data))
faltan_test <- setdiff(vars_necesarias, names(test_data))
if (length(faltan_train) > 0) {
stop("Faltan en train_data las variables: ", paste(faltan_train, collapse = ", "))
}
if (length(faltan_test) > 0) {
stop("Faltan en test_data las variables: ", paste(faltan_test, collapse = ", "))
}
# =========================
# 2. ASEGURAR CODIFICACIÓN
# =========================
train_data$ESTADO_ACADEMICO <- as.factor(train_data$ESTADO_ACADEMICO)
test_data$ESTADO_ACADEMICO <- as.factor(test_data$ESTADO_ACADEMICO)
# =========================
# 3. FUNCIONES AUXILIARES
# =========================
fmt_n_pct <- function(n, total) {
paste0(n, " (", round(100 * n / total, 2), "%)")
}
fmt_pct <- function(x) {
paste0(round(x, 2), "%")
}
obs_prop <- function(p1, p2) {
dif <- abs(p1 - p2)
if (is.na(dif)) {
""
} else if (dif < 1) {
"Distribución prácticamente idéntica"
} else if (dif < 3) {
"Proporción consistente"
} else if (dif < 5) {
"Diferencia leve"
} else {
"Diferencia moderada"
}
}
obs_num <- function(x1, x2, umbral1 = 1, umbral2 = 2) {
dif <- abs(x1 - x2)
if (is.na(dif)) {
""
} else if (dif < umbral1) {
"Valores muy similares"
} else if (dif < umbral2) {
"Valores similares"
} else {
"Diferencia moderada"
}
}
crear_seccion <- function(nombre) {
data.frame(
Característica = as.character(nombre),
`Base de Entrenamiento` = as.character(""),
`Base de Prueba` = as.character(""),
Observación = as.character(""),
Tipo = as.character("SECCION"),
check.names = FALSE,
stringsAsFactors = FALSE
)
}
fila_texto <- function(caracteristica, entrena, prueba, obs = "") {
data.frame(
Característica = as.character(caracteristica),
`Base de Entrenamiento` = as.character(entrena),
`Base de Prueba` = as.character(prueba),
Observación = as.character(obs),
Tipo = as.character("DATO"),
check.names = FALSE,
stringsAsFactors = FALSE
)
}
resumen_categoria <- function(df, var, categoria) {
sum(df[[var]] == categoria, na.rm = TRUE)
}
fila_categoria <- function(var, categoria, etiqueta) {
n_train <- resumen_categoria(train_data, var, categoria)
n_test <- resumen_categoria(test_data, var, categoria)
total_train <- sum(!is.na(train_data[[var]]))
total_test <- sum(!is.na(test_data[[var]]))
p_train <- 100 * n_train / total_train
p_test <- 100 * n_test / total_test
fila_texto(
etiqueta,
fmt_n_pct(n_train, total_train),
fmt_n_pct(n_test, total_test),
obs_prop(p_train, p_test)
)
}
# =========================
# 4. RESUMEN GENERAL
# =========================
n_train <- nrow(train_data)
n_test <- nrow(test_data)
n_total <- n_train + n_test
prop_train <- 100 * n_train / n_total
prop_test <- 100 * n_test / n_total
prop_des_train <- mean(train_data$ESTADO_ACADEMICO == "Desertor", na.rm = TRUE) * 100
prop_des_test <- mean(test_data$ESTADO_ACADEMICO == "Desertor", na.rm = TRUE) * 100
prop_no_train <- mean(train_data$ESTADO_ACADEMICO == "No_Desertor", na.rm = TRUE) * 100
prop_no_test <- mean(test_data$ESTADO_ACADEMICO == "No_Desertor", na.rm = TRUE) * 100
tabla <- bind_rows(
crear_seccion("Resumen general"),
fila_texto(
"Tamaño de muestra",
paste0(round(prop_train, 0), "% (", n_train, " obs.)"),
paste0(round(prop_test, 0), "% (", n_test, " obs.)"),
"Proporción definida en la partición"
),
fila_texto(
"Desertores",
fmt_pct(prop_des_train),
fmt_pct(prop_des_test),
obs_prop(prop_des_train, prop_des_test)
),
fila_texto(
"No desertores",
fmt_pct(prop_no_train),
fmt_pct(prop_no_test),
obs_prop(prop_no_train, prop_no_test)
)
)
# =========================
# 5. VARIABLES DEMOGRÁFICAS
# =========================
edad_media_train <- mean(train_data$EDAD, na.rm = TRUE)
edad_media_test <- mean(test_data$EDAD, na.rm = TRUE)
edad_mediana_train <- median(train_data$EDAD, na.rm = TRUE)
edad_mediana_test <- median(test_data$EDAD, na.rm = TRUE)
edad_sd_train <- sd(train_data$EDAD, na.rm = TRUE)
edad_sd_test <- sd(test_data$EDAD, na.rm = TRUE)
edad_min_train <- min(train_data$EDAD, na.rm = TRUE)
edad_min_test <- min(test_data$EDAD, na.rm = TRUE)
edad_max_train <- max(train_data$EDAD, na.rm = TRUE)
edad_max_test <- max(test_data$EDAD, na.rm = TRUE)
tabla <- bind_rows(
tabla,
crear_seccion("Variables demográficas"),
fila_texto(
"Edad (media)",
round(edad_media_train, 2),
round(edad_media_test, 2),
obs_num(edad_media_train, edad_media_test, 1, 2)
),
fila_texto(
"Edad (mediana)",
round(edad_mediana_train, 2),
round(edad_mediana_test, 2),
ifelse(edad_mediana_train == edad_mediana_test,
"Consistencia en tendencia central",
"Diferencia en mediana")
),
fila_texto(
"Edad (DE)",
round(edad_sd_train, 2),
round(edad_sd_test, 2),
obs_num(edad_sd_train, edad_sd_test, 1, 2)
),
fila_texto("Edad (mínimo)", round(edad_min_train, 2), round(edad_min_test, 2), ""),
fila_texto("Edad (máximo)", round(edad_max_train, 2), round(edad_max_test, 2), ""),
fila_categoria("SEXO", "FEMENINO", "Sexo (FEMENINO)"),
fila_categoria("SEXO", "MASCULINO", "Sexo (MASCULINO)"),
fila_categoria("RESIDENCIA", "ASUNCIÓN", "Residencia (ASUNCIÓN)"),
fila_categoria("RESIDENCIA", "CENTRAL", "Residencia (CENTRAL)"),
fila_categoria("RESIDENCIA", "RESTO DEL PAÍS", "Residencia (RESTO DEL PAÍS)"),
fila_categoria("EST_CIVIL", "Soltero/a", "Estado civil (Soltero/a)"),
fila_categoria("EST_CIVIL", "Casado/a", "Estado civil (Casado/a)"),
fila_categoria("EST_CIVIL", "Divorciado/a", "Estado civil (Divorciado/a)"),
fila_categoria("EST_CIVIL", "Otro", "Estado civil (Otro)")
)
# =========================
# 6. VARIABLES INTERNAS AL MEDIO EDUCATIVO
# =========================
tabla <- bind_rows(
tabla,
crear_seccion("Variables internas al medio educativo"),
fila_categoria("CARRERA", "ESTADISTICA-PRES", "Carrera (ESTADISTICA-PRES)"),
fila_categoria("CARRERA", "ESTADISTICA-SEMI", "Carrera (ESTADISTICA-SEMI)"),
fila_categoria("CARRERA", "MATEMATICA", "Carrera (MATEMATICA)"),
fila_categoria("CARRERA", "EDUCACION MATEMATICA-PRES", "Carrera (EDUCACION MATEMATICA-PRES)"),
fila_categoria("CARRERA", "EDUCACION MATEMATICA-SEMI", "Carrera (EDUCACION MATEMATICA-SEMI)"),
fila_categoria("TIPO_INGRESO", "INGRESO", "Tipo de ingreso (INGRESO)"),
fila_categoria("TIPO_INGRESO", "TRASLADO", "Tipo de ingreso (TRASLADO)"),
fila_categoria("TIPO_INGRESO", "ADMISION DIRECTA", "Tipo de ingreso (ADMISION DIRECTA)"),
fila_categoria("MODALIDAD", "Presencial", "Modalidad (Presencial)"),
fila_categoria("MODALIDAD", "Semipresencial", "Modalidad (Semipresencial)"),
fila_categoria("RENDIMIENTO", "APROBADO", "Rendimiento (APROBADO)"),
fila_categoria("RENDIMIENTO", "NO APROBADO", "Rendimiento (NO APROBADO)")
)
# =========================
# 7. VARIABLES EXTERNAS AL MEDIO EDUCATIVO
# =========================
tabla <- bind_rows(
tabla,
crear_seccion("Variables externas al medio educativo"),
fila_categoria("TIPO_COL", "Público", "Tipo de colegio (Público)"),
fila_categoria("TIPO_COL", "Subvencionado", "Tipo de colegio (Subvencionado)"),
fila_categoria("TIPO_COL", "Privado", "Tipo de colegio (Privado)"),
fila_categoria("TRABAJA", "SI", "Trabaja (SI)"),
fila_categoria("TRABAJA", "NO", "Trabaja (NO)"),
fila_categoria("SOLVENTAR", "Beca/exoneración total", "Solventar (Beca/exoneración total)"),
fila_categoria("SOLVENTAR", "Beca/exoneración parcial", "Solventar (Beca/exoneración parcial)"),
fila_categoria("SOLVENTAR", "Trabajo Personal", "Solventar (Trabajo Personal)"),
fila_categoria("SOLVENTAR", "Ayuda Familiar", "Solventar (Ayuda Familiar)"),
fila_categoria("ESTUDIOS_PADRES", "hasta 13 años", "Estudios de los padres (hasta 13 años)"),
fila_categoria("ESTUDIOS_PADRES", "14-23 años", "Estudios de los padres (14-23 años)"),
fila_categoria("ESTUDIOS_PADRES", "24-29 años", "Estudios de los padres (24-29 años)"),
fila_categoria("ESTUDIOS_PADRES", "30-34 años", "Estudios de los padres (30-34 años)"),
fila_categoria("ESTUDIOS_PADRES", "Más de 34 años", "Estudios de los padres (Más de 34 años)"),
fila_categoria("INGRESO", "Hasta dos salarios mínimos", "Ingreso familiar (Hasta dos salarios mínimos)"),
fila_categoria("INGRESO", "Más de dos y hasta cinco salarios mínimos", "Ingreso familiar (Más de dos y hasta cinco salarios mínimos)"),
fila_categoria("INGRESO", "Más de cinco y hasta diez salarios mínimos", "Ingreso familiar (Más de cinco y hasta diez salarios mínimos)"),
fila_categoria("INGRESO", "Más de diez y hasta quince salarios mínimos", "Ingreso familiar (Más de diez y hasta quince salarios mínimos)"),
fila_categoria("INGRESO", "Más de quince salarios mínimos", "Ingreso familiar (Más de quince salarios mínimos)")
)
# NIVEL_SOCIO dinámico según niveles existentes
niveles_nivel_socio <- union(
levels(as.factor(train_data$NIVEL_SOCIO)),
levels(as.factor(test_data$NIVEL_SOCIO))
)
for (niv in niveles_nivel_socio) {
tabla <- bind_rows(
tabla,
fila_categoria("NIVEL_SOCIO", niv, paste0("Nivel socioeconómico (", niv, ")"))
)
}
# =========================
# 8. LIMPIEZA FINAL
# =========================
tabla_exportar <- tabla %>%
select(-Tipo)
print(tabla_exportar)
## Característica
## 1 Resumen general
## 2 Tamaño de muestra
## 3 Desertores
## 4 No desertores
## 5 Variables demográficas
## 6 Edad (media)
## 7 Edad (mediana)
## 8 Edad (DE)
## 9 Edad (mínimo)
## 10 Edad (máximo)
## 11 Sexo (FEMENINO)
## 12 Sexo (MASCULINO)
## 13 Residencia (ASUNCIÓN)
## 14 Residencia (CENTRAL)
## 15 Residencia (RESTO DEL PAÍS)
## 16 Estado civil (Soltero/a)
## 17 Estado civil (Casado/a)
## 18 Estado civil (Divorciado/a)
## 19 Estado civil (Otro)
## 20 Variables internas al medio educativo
## 21 Carrera (ESTADISTICA-PRES)
## 22 Carrera (ESTADISTICA-SEMI)
## 23 Carrera (MATEMATICA)
## 24 Carrera (EDUCACION MATEMATICA-PRES)
## 25 Carrera (EDUCACION MATEMATICA-SEMI)
## 26 Tipo de ingreso (INGRESO)
## 27 Tipo de ingreso (TRASLADO)
## 28 Tipo de ingreso (ADMISION DIRECTA)
## 29 Modalidad (Presencial)
## 30 Modalidad (Semipresencial)
## 31 Rendimiento (APROBADO)
## 32 Rendimiento (NO APROBADO)
## 33 Variables externas al medio educativo
## 34 Tipo de colegio (Público)
## 35 Tipo de colegio (Subvencionado)
## 36 Tipo de colegio (Privado)
## 37 Trabaja (SI)
## 38 Trabaja (NO)
## 39 Solventar (Beca/exoneración total)
## 40 Solventar (Beca/exoneración parcial)
## 41 Solventar (Trabajo Personal)
## 42 Solventar (Ayuda Familiar)
## 43 Estudios de los padres (hasta 13 años)
## 44 Estudios de los padres (14-23 años)
## 45 Estudios de los padres (24-29 años)
## 46 Estudios de los padres (30-34 años)
## 47 Estudios de los padres (Más de 34 años)
## 48 Ingreso familiar (Hasta dos salarios mínimos)
## 49 Ingreso familiar (Más de dos y hasta cinco salarios mínimos)
## 50 Ingreso familiar (Más de cinco y hasta diez salarios mínimos)
## 51 Ingreso familiar (Más de diez y hasta quince salarios mínimos)
## 52 Ingreso familiar (Más de quince salarios mínimos)
## 53 Nivel socioeconómico (BAJO)
## 54 Nivel socioeconómico (MEDIO)
## Base de Entrenamiento Base de Prueba Observación
## 1
## 2 70% (479 obs.) 30% (203 obs.) Proporción definida en la partición
## 3 67.22% 67.49% Distribución prácticamente idéntica
## 4 32.78% 32.51% Distribución prácticamente idéntica
## 5
## 6 26.84 27.74 Valores muy similares
## 7 23 24 Diferencia en mediana
## 8 9.03 9.47 Valores muy similares
## 9 18 18
## 10 70 61
## 11 281 (58.66%) 114 (56.16%) Proporción consistente
## 12 198 (41.34%) 89 (43.84%) Proporción consistente
## 13 159 (33.19%) 70 (34.48%) Proporción consistente
## 14 240 (50.1%) 96 (47.29%) Proporción consistente
## 15 80 (16.7%) 37 (18.23%) Proporción consistente
## 16 385 (80.38%) 159 (78.33%) Proporción consistente
## 17 79 (16.49%) 38 (18.72%) Proporción consistente
## 18 11 (2.3%) 3 (1.48%) Distribución prácticamente idéntica
## 19 4 (0.84%) 3 (1.48%) Distribución prácticamente idéntica
## 20
## 21 130 (27.14%) 52 (25.62%) Proporción consistente
## 22 73 (15.24%) 27 (13.3%) Proporción consistente
## 23 106 (22.13%) 44 (21.67%) Distribución prácticamente idéntica
## 24 48 (10.02%) 22 (10.84%) Distribución prácticamente idéntica
## 25 122 (25.47%) 58 (28.57%) Diferencia leve
## 26 454 (94.78%) 188 (92.61%) Proporción consistente
## 27 0 (0%) 0 (0%) Distribución prácticamente idéntica
## 28 25 (5.22%) 15 (7.39%) Proporción consistente
## 29 284 (59.29%) 118 (58.13%) Proporción consistente
## 30 195 (40.71%) 85 (41.87%) Proporción consistente
## 31 285 (59.5%) 124 (61.08%) Proporción consistente
## 32 194 (40.5%) 79 (38.92%) Proporción consistente
## 33
## 34 320 (66.81%) 145 (71.43%) Diferencia leve
## 35 39 (8.14%) 10 (4.93%) Diferencia leve
## 36 120 (25.05%) 48 (23.65%) Proporción consistente
## 37 261 (54.49%) 111 (54.68%) Distribución prácticamente idéntica
## 38 218 (45.51%) 92 (45.32%) Distribución prácticamente idéntica
## 39 28 (5.85%) 17 (8.37%) Proporción consistente
## 40 35 (7.31%) 14 (6.9%) Distribución prácticamente idéntica
## 41 242 (50.52%) 104 (51.23%) Distribución prácticamente idéntica
## 42 174 (36.33%) 68 (33.5%) Proporción consistente
## 43 103 (21.5%) 54 (26.6%) Diferencia moderada
## 44 154 (32.15%) 61 (30.05%) Proporción consistente
## 45 111 (23.17%) 53 (26.11%) Proporción consistente
## 46 75 (15.66%) 26 (12.81%) Proporción consistente
## 47 36 (7.52%) 9 (4.43%) Diferencia leve
## 48 300 (62.63%) 130 (64.04%) Proporción consistente
## 49 138 (28.81%) 52 (25.62%) Diferencia leve
## 50 29 (6.05%) 16 (7.88%) Proporción consistente
## 51 8 (1.67%) 4 (1.97%) Distribución prácticamente idéntica
## 52 4 (0.84%) 1 (0.49%) Distribución prácticamente idéntica
## 53 41 (8.56%) 24 (11.82%) Diferencia leve
## 54 438 (91.44%) 179 (88.18%) Diferencia leve
# =========================
# 9. EXPORTAR A EXCEL
# =========================
wb <- createWorkbook()
addWorksheet(wb, "Tabla comparativa")
writeData(wb, "Tabla comparativa", tabla_exportar, startRow = 1, startCol = 1)
# Estilos
estilo_header <- createStyle(
textDecoration = "bold",
fgFill = "#5B9BD5",
fontColour = "#FFFFFF",
halign = "center",
valign = "center",
wrapText = TRUE
)
estilo_seccion <- createStyle(
textDecoration = "bold",
fgFill = "#D9EAF7",
wrapText = TRUE
)
estilo_general <- createStyle(
valign = "top",
wrapText = TRUE
)
# Aplicar estilos
addStyle(wb, "Tabla comparativa", estilo_header, rows = 1, cols = 1:4, gridExpand = TRUE)
filas_seccion <- which(tabla$Tipo == "SECCION") + 1
if (length(filas_seccion) > 0) {
for (f in filas_seccion) {
addStyle(wb, "Tabla comparativa", estilo_seccion, rows = f, cols = 1:4, gridExpand = TRUE)
}
}
addStyle(
wb, "Tabla comparativa", estilo_general,
rows = 2:(nrow(tabla_exportar) + 1), cols = 1:4, gridExpand = TRUE, stack = TRUE
)
# Anchos de columna
setColWidths(wb, "Tabla comparativa", cols = 1, widths = 48)
setColWidths(wb, "Tabla comparativa", cols = 2:3, widths = 24)
setColWidths(wb, "Tabla comparativa", cols = 4, widths = 32)
freezePane(wb, "Tabla comparativa", firstRow = TRUE)
saveWorkbook(
wb,
"Tabla_comparacion_train_test_14_variables.xlsx",
overwrite = TRUE
)
cat("\nArchivo generado: Tabla_comparacion_train_test_14_variables.xlsx\n")
##
## Archivo generado: Tabla_comparacion_train_test_14_variables.xlsx
############################################################
# TABLA 4.19
# RESULTADOS DE LA VALIDACIÓN CRUZADA
# ROC, SENSIBILIDAD Y ESPECIFICIDAD
############################################################
# Se asume que ya existe:
# resultados_cv <- resamples(list(
# Logit = modelo_logit_cv,
# Tree = modelo_tree_cv,
# RF = modelo_rf_cv,
# SVM = modelo_svm_cv,
# KNN = modelo_knn_cv,
# NN = modelo_nn_cv,
# NB = modelo_nb_cv
# ))
library(dplyr)
library(openxlsx)
############################################################
# 1. RESUMEN DE VALIDACIÓN CRUZADA
############################################################
resumen_cv <- summary(resultados_cv)
############################################################
# 2. FUNCIÓN PARA ARMAR CADA TABLA DE MÉTRICA
############################################################
armar_tabla_metrica <- function(matriz_metric) {
tabla <- as.data.frame(matriz_metric, stringsAsFactors = FALSE)
# Eliminar columna de NA's si existe
if ("NA's" %in% colnames(tabla)) {
tabla <- tabla[, !colnames(tabla) %in% "NA's", drop = FALSE]
}
tabla$Modelos <- rownames(tabla)
rownames(tabla) <- NULL
# Reordenar columnas
tabla <- tabla[, c("Modelos", "Min.", "1st Qu.", "Median", "Mean", "3rd Qu.", "Max.")]
# Redondear columnas numéricas
tabla <- tabla %>%
mutate(across(where(is.numeric), ~ round(.x, 7))) %>%
as.data.frame()
tabla
}
############################################################
# 3. TABLAS POR MÉTRICA
############################################################
tabla_roc_419 <- armar_tabla_metrica(resumen_cv$statistics$ROC)
tabla_sens_419 <- armar_tabla_metrica(resumen_cv$statistics$Sens)
tabla_spec_419 <- armar_tabla_metrica(resumen_cv$statistics$Spec)
cat("\n=============================\n")
##
## =============================
cat("TABLA 4.19 - ROC\n")
## TABLA 4.19 - ROC
cat("=============================\n")
## =============================
print(tabla_roc_419)
## Modelos Min. 1st Qu. Median Mean 3rd Qu. Max.
## 1 Logit 0.6541667 0.7437500 0.7836174 0.7803046 0.8199219 0.8996212
## 2 Tree 0.6845703 0.7714962 0.7973633 0.7940493 0.8305827 0.8750000
## 3 RF 0.6458333 0.7341856 0.7910156 0.7854376 0.8286458 0.9091797
## 4 SVM 0.6416667 0.7183268 0.7613636 0.7602734 0.8041992 0.8867188
## 5 KNN 0.6318359 0.6966072 0.7434186 0.7483594 0.7955211 0.9062500
## 6 NN 0.6208333 0.7228634 0.7753906 0.7677115 0.8043176 0.8916667
## 7 NB 0.6041667 0.7292480 0.7802734 0.7714621 0.8144383 0.9104167
cat("\n=============================\n")
##
## =============================
cat("TABLA 4.19 - SENSIBILIDAD\n")
## TABLA 4.19 - SENSIBILIDAD
cat("=============================\n")
## =============================
print(tabla_sens_419)
## Modelos Min. 1st Qu. Median Mean 3rd Qu. Max.
## 1 Logit 0.68750 0.7500000 0.8125000 0.8071023 0.8473011 0.9696970
## 2 Tree 0.65625 0.7500000 0.7812500 0.7886995 0.8167614 0.9375000
## 3 RF 0.90625 0.9375000 0.9687500 0.9616477 0.9924242 1.0000000
## 4 SVM 0.68750 0.8437500 0.8787879 0.8777462 0.9375000 1.0000000
## 5 KNN 0.81250 0.9062500 0.9375000 0.9304924 0.9614110 1.0000000
## 6 NN 0.65625 0.7500000 0.8125000 0.7967487 0.8437500 0.9090909
## 7 NB 0.37500 0.6328125 0.7187500 0.6799874 0.7812500 0.8181818
cat("\n=============================\n")
##
## =============================
cat("TABLA 4.19 - ESPECIFICIDAD\n")
## TABLA 4.19 - ESPECIFICIDAD
cat("=============================\n")
## =============================
print(tabla_spec_419)
## Modelos Min. 1st Qu. Median Mean 3rd Qu. Max.
## 1 Logit 0.3333333 0.5000000 0.5625000 0.5548611 0.62500 0.7500
## 2 Tree 0.3125000 0.4666667 0.6125000 0.5850000 0.68750 0.8750
## 3 RF 0.0625000 0.1468750 0.1875000 0.2031944 0.25000 0.3750
## 4 SVM 0.0000000 0.2000000 0.3229167 0.3322222 0.43750 0.6875
## 5 KNN 0.0000000 0.1468750 0.2000000 0.2259722 0.31250 0.4375
## 6 NN 0.2500000 0.3750000 0.5000000 0.4906944 0.61875 0.7500
## 7 NB 0.4375000 0.6250000 0.7500000 0.7298611 0.81250 1.0000
############################################################
# 4. TABLA CONSOLIDADA EN FORMATO TIPO TESIS
############################################################
fila_titulo <- function(texto) {
data.frame(
Modelos = texto,
`Min.` = "",
`1st Qu.` = "",
Median = "",
Mean = "",
`3rd Qu.` = "",
`Max.` = "",
stringsAsFactors = FALSE,
check.names = FALSE
)
}
fila_blanca <- data.frame(
Modelos = "",
`Min.` = "",
`1st Qu.` = "",
Median = "",
Mean = "",
`3rd Qu.` = "",
`Max.` = "",
stringsAsFactors = FALSE,
check.names = FALSE
)
# Convertir a character para poder unir títulos y números sin errores
tabla_roc_chr <- tabla_roc_419 %>% mutate(across(everything(), as.character))
tabla_sens_chr <- tabla_sens_419 %>% mutate(across(everything(), as.character))
tabla_spec_chr <- tabla_spec_419 %>% mutate(across(everything(), as.character))
tabla_419_final <- bind_rows(
fila_titulo("ROC (AUC)"),
tabla_roc_chr,
fila_blanca,
fila_titulo("Sens (Sensibilidad)"),
tabla_sens_chr,
fila_blanca,
fila_titulo("Spec (Especificidad)"),
tabla_spec_chr,
fila_blanca,
data.frame(
Modelos = "Models: Logit, Tree, RF, SVM, KNN, NN, NB",
`Min.` = "",
`1st Qu.` = "",
Median = "",
Mean = "",
`3rd Qu.` = "",
`Max.` = "",
stringsAsFactors = FALSE,
check.names = FALSE
),
data.frame(
Modelos = paste0("Number of resamples: ", nrow(resultados_cv$values)),
`Min.` = "",
`1st Qu.` = "",
Median = "",
Mean = "",
`3rd Qu.` = "",
`Max.` = "",
stringsAsFactors = FALSE,
check.names = FALSE
)
)
cat("\n=============================\n")
##
## =============================
cat("TABLA 4.19 CONSOLIDADA\n")
## TABLA 4.19 CONSOLIDADA
cat("=============================\n")
## =============================
print(tabla_419_final)
## Modelos Min. 1st Qu. Median
## 1 ROC (AUC)
## 2 Logit 0.6541667 0.74375 0.7836174
## 3 Tree 0.6845703 0.7714962 0.7973633
## 4 RF 0.6458333 0.7341856 0.7910156
## 5 SVM 0.6416667 0.7183268 0.7613636
## 6 KNN 0.6318359 0.6966072 0.7434186
## 7 NN 0.6208333 0.7228634 0.7753906
## 8 NB 0.6041667 0.729248 0.7802734
## 9
## 10 Sens (Sensibilidad)
## 11 Logit 0.6875 0.75 0.8125
## 12 Tree 0.65625 0.75 0.78125
## 13 RF 0.90625 0.9375 0.96875
## 14 SVM 0.6875 0.84375 0.8787879
## 15 KNN 0.8125 0.90625 0.9375
## 16 NN 0.65625 0.75 0.8125
## 17 NB 0.375 0.6328125 0.71875
## 18
## 19 Spec (Especificidad)
## 20 Logit 0.3333333 0.5 0.5625
## 21 Tree 0.3125 0.4666667 0.6125
## 22 RF 0.0625 0.146875 0.1875
## 23 SVM 0 0.2 0.3229167
## 24 KNN 0 0.146875 0.2
## 25 NN 0.25 0.375 0.5
## 26 NB 0.4375 0.625 0.75
## 27
## 28 Models: Logit, Tree, RF, SVM, KNN, NN, NB
## 29 Number of resamples: 30
## Mean 3rd Qu. Max.
## 1
## 2 0.7803046 0.8199219 0.8996212
## 3 0.7940493 0.8305827 0.875
## 4 0.7854376 0.8286458 0.9091797
## 5 0.7602734 0.8041992 0.8867188
## 6 0.7483594 0.7955211 0.90625
## 7 0.7677115 0.8043176 0.8916667
## 8 0.7714621 0.8144383 0.9104167
## 9
## 10
## 11 0.8071023 0.8473011 0.969697
## 12 0.7886995 0.8167614 0.9375
## 13 0.9616477 0.9924242 1
## 14 0.8777462 0.9375 1
## 15 0.9304924 0.961411 1
## 16 0.7967487 0.84375 0.9090909
## 17 0.6799874 0.78125 0.8181818
## 18
## 19
## 20 0.5548611 0.625 0.75
## 21 0.585 0.6875 0.875
## 22 0.2031944 0.25 0.375
## 23 0.3322222 0.4375 0.6875
## 24 0.2259722 0.3125 0.4375
## 25 0.4906944 0.61875 0.75
## 26 0.7298611 0.8125 1
## 27
## 28
## 29
############################################################
# 5. EXPORTACIÓN
############################################################
dir.create("salidas_tesis", showWarnings = FALSE)
# CSV separados
write.csv(tabla_roc_419,
"salidas_tesis/tabla_4_19_roc_cv.csv",
row.names = FALSE)
write.csv(tabla_sens_419,
"salidas_tesis/tabla_4_19_sens_cv.csv",
row.names = FALSE)
write.csv(tabla_spec_419,
"salidas_tesis/tabla_4_19_spec_cv.csv",
row.names = FALSE)
write.csv(tabla_419_final,
"salidas_tesis/tabla_4_19_validacion_cruzada_consolidada.csv",
row.names = FALSE)
# Excel
wb <- createWorkbook()
addWorksheet(wb, "Tabla 4.19")
addWorksheet(wb, "ROC")
addWorksheet(wb, "Sens")
addWorksheet(wb, "Spec")
writeData(wb, "Tabla 4.19", tabla_419_final, startRow = 1, startCol = 1)
writeData(wb, "ROC", tabla_roc_419, startRow = 1, startCol = 1)
writeData(wb, "Sens", tabla_sens_419, startRow = 1, startCol = 1)
writeData(wb, "Spec", tabla_spec_419, startRow = 1, startCol = 1)
# Estilos básicos
estilo_header <- createStyle(
textDecoration = "bold",
fgFill = "#1F4E78",
fontColour = "#FFFFFF",
halign = "center"
)
estilo_titulo <- createStyle(
textDecoration = "bold",
fgFill = "#D9EAF7"
)
# Aplicar encabezados a hojas métricas
addStyle(wb, "ROC", estilo_header, rows = 1, cols = 1:ncol(tabla_roc_419), gridExpand = TRUE)
addStyle(wb, "Sens", estilo_header, rows = 1, cols = 1:ncol(tabla_sens_419), gridExpand = TRUE)
addStyle(wb, "Spec", estilo_header, rows = 1, cols = 1:ncol(tabla_spec_419), gridExpand = TRUE)
# Detectar títulos en hoja consolidada
filas_titulo <- which(tabla_419_final$Modelos %in% c("ROC (AUC)", "Sens (Sensibilidad)", "Spec (Especificidad)")) + 1
if (length(filas_titulo) > 0) {
for (f in filas_titulo) {
addStyle(wb, "Tabla 4.19", estilo_titulo, rows = f, cols = 1:ncol(tabla_419_final), gridExpand = TRUE)
}
}
setColWidths(wb, "Tabla 4.19", cols = 1:7, widths = "auto")
setColWidths(wb, "ROC", cols = 1:7, widths = "auto")
setColWidths(wb, "Sens", cols = 1:7, widths = "auto")
setColWidths(wb, "Spec", cols = 1:7, widths = "auto")
freezePane(wb, "ROC", firstRow = TRUE)
freezePane(wb, "Sens", firstRow = TRUE)
freezePane(wb, "Spec", firstRow = TRUE)
saveWorkbook(
wb,
"salidas_tesis/tabla_4_19_validacion_cruzada.xlsx",
overwrite = TRUE
)
cat("\n=============================\n")
##
## =============================
cat("TABLA 4.19 EXPORTADA CORRECTAMENTE\n")
## TABLA 4.19 EXPORTADA CORRECTAMENTE
cat("=============================\n")
## =============================
cat("Archivos generados en salidas_tesis:\n")
## Archivos generados en salidas_tesis:
cat("- tabla_4_19_roc_cv.csv\n")
## - tabla_4_19_roc_cv.csv
cat("- tabla_4_19_sens_cv.csv\n")
## - tabla_4_19_sens_cv.csv
cat("- tabla_4_19_spec_cv.csv\n")
## - tabla_4_19_spec_cv.csv
cat("- tabla_4_19_validacion_cruzada_consolidada.csv\n")
## - tabla_4_19_validacion_cruzada_consolidada.csv
cat("- tabla_4_19_validacion_cruzada.xlsx\n")
## - tabla_4_19_validacion_cruzada.xlsx
############################################################
# TABLA DE LAS 6 MÉTRICAS DE DESEMPEÑO EN TEST
# Accuracy, Sensitivity, Specificity, Precision, F1 y AUC
############################################################
# Se asume que ya existe:
# tabla_metricas_test
library(dplyr)
library(openxlsx)
############################################################
# 1. TABLA BASE
############################################################
tabla_6_metricas <- tabla_metricas_test %>%
dplyr::select(
Modelo,
Accuracy,
Sensitivity,
Specificity,
Precision,
F1,
AUC
) %>%
mutate(
across(-Modelo, ~ round(.x, 4))
)
print(tabla_6_metricas)
## Modelo Accuracy Sensitivity Specificity Precision F1 AUC
## Logit Logit 0.7192 0.7883 0.5758 0.7941 0.7912 0.7796
## Tree Tree 0.7340 0.7664 0.6667 0.8268 0.7955 0.7853
## RF RF 0.7192 0.9635 0.2121 0.7174 0.8224 0.7920
## SVM SVM 0.7192 0.9270 0.2879 0.7299 0.8167 0.7773
## KNN KNN 0.6798 0.8905 0.2424 0.7093 0.7896 0.7131
## NN NN 0.6946 0.7956 0.4848 0.7622 0.7786 0.7460
## NB NB 0.7291 0.7445 0.6970 0.8361 0.7876 0.7890
############################################################
# 2. ORDENAR POR AUC (OPCIONAL)
############################################################
tabla_6_metricas_ordenada <- tabla_6_metricas %>%
arrange(desc(AUC))
print(tabla_6_metricas_ordenada)
## Modelo Accuracy Sensitivity Specificity Precision F1 AUC
## RF RF 0.7192 0.9635 0.2121 0.7174 0.8224 0.7920
## NB NB 0.7291 0.7445 0.6970 0.8361 0.7876 0.7890
## Tree Tree 0.7340 0.7664 0.6667 0.8268 0.7955 0.7853
## Logit Logit 0.7192 0.7883 0.5758 0.7941 0.7912 0.7796
## SVM SVM 0.7192 0.9270 0.2879 0.7299 0.8167 0.7773
## NN NN 0.6946 0.7956 0.4848 0.7622 0.7786 0.7460
## KNN KNN 0.6798 0.8905 0.2424 0.7093 0.7896 0.7131
############################################################
# 3. EXPORTAR CSV
############################################################
write.csv(
tabla_6_metricas_ordenada,
"salidas_tesis/tabla_6_metricas_desempeno_test.csv",
row.names = FALSE
)
############################################################
# 4. EXPORTAR EXCEL
############################################################
wb <- createWorkbook()
addWorksheet(wb, "Tabla 6 métricas")
writeData(
wb,
sheet = "Tabla 6 métricas",
x = tabla_6_metricas_ordenada,
startRow = 1,
startCol = 1
)
# Estilo encabezado
estilo_header <- createStyle(
textDecoration = "bold",
fgFill = "#1F4E78",
fontColour = "#FFFFFF",
halign = "center",
valign = "center"
)
# Estilo cuerpo
estilo_cuerpo <- createStyle(
halign = "center",
valign = "center",
border = "TopBottomLeftRight",
borderColour = "#B8C2CC"
)
addStyle(
wb, "Tabla 6 métricas",
style = estilo_header,
rows = 1, cols = 1:ncol(tabla_6_metricas_ordenada),
gridExpand = TRUE
)
addStyle(
wb, "Tabla 6 métricas",
style = estilo_cuerpo,
rows = 2:(nrow(tabla_6_metricas_ordenada) + 1),
cols = 1:ncol(tabla_6_metricas_ordenada),
gridExpand = TRUE,
stack = TRUE
)
setColWidths(wb, "Tabla 6 métricas", cols = 1:ncol(tabla_6_metricas_ordenada), widths = "auto")
freezePane(wb, "Tabla 6 métricas", firstRow = TRUE)
saveWorkbook(
wb,
"salidas_tesis/tabla_6_metricas_desempeno_test.xlsx",
overwrite = TRUE
)
cat("\nArchivos exportados:\n")
##
## Archivos exportados:
cat("- salidas_tesis/tabla_6_metricas_desempeno_test.csv\n")
## - salidas_tesis/tabla_6_metricas_desempeno_test.csv
cat("- salidas_tesis/tabla_6_metricas_desempeno_test.xlsx\n")
## - salidas_tesis/tabla_6_metricas_desempeno_test.xlsx
############################################################
# MATRICES DE CONFUSIÓN GRÁFICAS PARA LOS 7 MODELOS
# Estilo similar al gráfico adjunto
############################################################
#############################
# 1. PAQUETES
#############################
paquetes <- c("ggplot2", "dplyr", "pROC", "gridExtra", "grid", "patchwork")
instalar <- paquetes[!paquetes %in% installed.packages()[, "Package"]]
if (length(instalar) > 0) install.packages(instalar)
library(ggplot2)
library(dplyr)
library(pROC)
library(gridExtra)
##
## Adjuntando el paquete: 'gridExtra'
## The following object is masked from 'package:randomForest':
##
## combine
## The following object is masked from 'package:dplyr':
##
## combine
library(grid)
library(patchwork)
#############################
# 2. DIRECTORIO DE SALIDA
#############################
dir_salida <- "salida_tesis"
if (!dir.exists(dir_salida)) {
dir.create(dir_salida, recursive = TRUE)
}
#############################
# 3. VALIDACIONES
#############################
if (!exists("test_model")) {
stop("No existe el objeto 'test_model'.")
}
if (!exists("modelo_logit_cv") ||
!exists("modelo_tree_cv") ||
!exists("modelo_rf_cv") ||
!exists("modelo_svm_cv") ||
!exists("modelo_knn_cv") ||
!exists("modelo_nn_cv") ||
!exists("modelo_nb_cv")) {
stop("Faltan uno o más modelos entrenados.")
}
# Asegurar codificación consistente
test_model$ESTADO_ACADEMICO <- factor(
test_model$ESTADO_ACADEMICO,
levels = c("Desertor", "No_Desertor")
)
#############################
# 4. LISTA DE MODELOS
#############################
modelos_finales <- list(
Logit = modelo_logit_cv,
Tree = modelo_tree_cv,
RF = modelo_rf_cv,
SVM = modelo_svm_cv,
KNN = modelo_knn_cv,
NN = modelo_nn_cv,
NB = modelo_nb_cv
)
#############################
# 5. FUNCIONES AUXILIARES
#############################
# Extraer probabilidades de la clase positiva
obtener_prob_desertor <- function(modelo, data_test) {
probs <- predict(modelo, newdata = data_test, type = "prob")
if (!"Desertor" %in% colnames(probs)) {
stop("La columna 'Desertor' no está presente en las probabilidades del modelo.")
}
probs[, "Desertor"]
}
# Calcular métricas por clase y métricas globales
calcular_reporte_clasificacion <- function(real, pred, prob_desertor) {
real <- factor(real, levels = c("Desertor", "No_Desertor"))
pred <- factor(pred, levels = c("Desertor", "No_Desertor"))
clases <- levels(real)
# AUC
roc_obj <- pROC::roc(
response = real,
predictor = prob_desertor,
levels = c("No_Desertor", "Desertor"),
direction = "<",
quiet = TRUE
)
auc_val <- as.numeric(pROC::auc(roc_obj))
# Métricas por clase
metricas_por_clase <- lapply(clases, function(cl) {
tp <- sum(real == cl & pred == cl, na.rm = TRUE)
fp <- sum(real != cl & pred == cl, na.rm = TRUE)
fn <- sum(real == cl & pred != cl, na.rm = TRUE)
support <- sum(real == cl, na.rm = TRUE)
precision <- ifelse((tp + fp) == 0, 0, tp / (tp + fp))
recall <- ifelse((tp + fn) == 0, 0, tp / (tp + fn))
f1 <- ifelse((precision + recall) == 0, 0,
2 * precision * recall / (precision + recall))
data.frame(
Clase = cl,
Precision = precision,
Recall = recall,
F1_Score = f1,
Support = support,
stringsAsFactors = FALSE
)
}) %>% bind_rows()
# Accuracy global
accuracy <- mean(real == pred, na.rm = TRUE)
# Métricas tomando Desertor como clase positiva
tp_pos <- sum(real == "Desertor" & pred == "Desertor", na.rm = TRUE)
fp_pos <- sum(real == "No_Desertor" & pred == "Desertor", na.rm = TRUE)
fn_pos <- sum(real == "Desertor" & pred == "No_Desertor", na.rm = TRUE)
tn_pos <- sum(real == "No_Desertor" & pred == "No_Desertor", na.rm = TRUE)
precision_pos <- ifelse((tp_pos + fp_pos) == 0, 0, tp_pos / (tp_pos + fp_pos))
recall_pos <- ifelse((tp_pos + fn_pos) == 0, 0, tp_pos / (tp_pos + fn_pos))
f1_pos <- ifelse((precision_pos + recall_pos) == 0, 0,
2 * precision_pos * recall_pos / (precision_pos + recall_pos))
specificity <- ifelse((tn_pos + fp_pos) == 0, 0, tn_pos / (tn_pos + fp_pos))
# Macro avg
macro_avg <- data.frame(
Clase = "Macro avg",
Precision = mean(metricas_por_clase$Precision, na.rm = TRUE),
Recall = mean(metricas_por_clase$Recall, na.rm = TRUE),
F1_Score = mean(metricas_por_clase$F1_Score, na.rm = TRUE),
Support = sum(metricas_por_clase$Support, na.rm = TRUE),
stringsAsFactors = FALSE
)
# Weighted avg
pesos <- metricas_por_clase$Support / sum(metricas_por_clase$Support)
weighted_avg <- data.frame(
Clase = "Weighted avg",
Precision = sum(metricas_por_clase$Precision * pesos, na.rm = TRUE),
Recall = sum(metricas_por_clase$Recall * pesos, na.rm = TRUE),
F1_Score = sum(metricas_por_clase$F1_Score * pesos, na.rm = TRUE),
Support = sum(metricas_por_clase$Support, na.rm = TRUE),
stringsAsFactors = FALSE
)
# Fila Accuracy estilo classification report
fila_accuracy <- data.frame(
Clase = "Accuracy",
Precision = NA,
Recall = NA,
F1_Score = accuracy,
Support = length(real),
stringsAsFactors = FALSE
)
tabla_reporte <- bind_rows(
metricas_por_clase %>% arrange(match(Clase, c("No_Desertor", "Desertor"))),
fila_accuracy,
macro_avg,
weighted_avg
)
list(
accuracy = accuracy,
precision = precision_pos,
recall = recall_pos,
f1 = f1_pos,
specificity = specificity,
auc = auc_val,
tabla_reporte = tabla_reporte,
roc = roc_obj
)
}
# Crear el gráfico compuesto
crear_grafico_matriz_confusion <- function(modelo, nombre_modelo, data_test) {
real <- factor(data_test$ESTADO_ACADEMICO, levels = c("Desertor", "No_Desertor"))
pred <- factor(
predict(modelo, newdata = data_test, type = "raw"),
levels = c("Desertor", "No_Desertor")
)
prob_desertor <- obtener_prob_desertor(modelo, data_test)
reporte <- calcular_reporte_clasificacion(real, pred, prob_desertor)
# Matriz para heatmap
tabla_mc <- table(
Real = factor(real, levels = c("Desertor", "No_Desertor")),
Prediccion = factor(pred, levels = c("No_Desertor", "Desertor"))
)
df_mc <- as.data.frame(tabla_mc)
colnames(df_mc) <- c("Real", "Prediccion", "Frecuencia")
# Heatmap
p_heat <- ggplot(df_mc, aes(x = Prediccion, y = Real, fill = Frecuencia)) +
geom_tile(color = "white", linewidth = 1) +
geom_text(aes(label = Frecuencia), size = 7, fontface = "bold", color = "black") +
scale_fill_gradient(low = "#E9EEF5", high = "#2F6FB0") +
scale_y_discrete(limits = c("No_Desertor", "Desertor")) +
labs(
title = "Matriz de confusión de evaluación de predicciones y resultados reales",
subtitle = paste("Modelo evaluado:", nombre_modelo),
x = "Predicción",
y = "Real"
) +
theme_minimal(base_size = 14) +
theme(
legend.position = "none",
plot.title = element_text(face = "bold", hjust = 0.5, size = 16),
plot.subtitle = element_text(hjust = 0.5, size = 11),
axis.title = element_text(face = "bold"),
panel.grid = element_blank()
)
# Tabla tipo classification report
tabla_rep <- reporte$tabla_reporte %>%
mutate(
Precision = ifelse(is.na(Precision), "", sprintf("%.2f", Precision)),
Recall = ifelse(is.na(Recall), "", sprintf("%.2f", Recall)),
F1_Score = sprintf("%.2f", F1_Score),
Support = as.character(Support)
)
tg <- tableGrob(
tabla_rep,
rows = NULL,
theme = ttheme_minimal(
core = list(
fg_params = list(fontsize = 10),
bg_params = list(fill = rep("white", nrow(tabla_rep)), col = NA)
),
colhead = list(
fg_params = list(fontsize = 10, fontface = "bold"),
bg_params = list(fill = "grey95", col = NA)
)
)
)
p_tabla <- wrap_elements(full = tg)
# Cuadro de métricas globales
texto_metricas <- paste0(
"Accuracy: ", sprintf("%.4f", reporte$accuracy), "\n",
"Precision: ", sprintf("%.4f", reporte$precision), "\n",
"Recall: ", sprintf("%.4f", reporte$recall), "\n",
"F1 Score: ", sprintf("%.4f", reporte$f1), "\n",
"Specificity: ", sprintf("%.4f", reporte$specificity), "\n",
"AUC-ROC: ", sprintf("%.4f", reporte$auc)
)
p_metricas <- ggplot() +
annotate(
"label",
x = 0, y = 1,
label = texto_metricas,
hjust = 0, vjust = 1,
size = 4,
label.size = 0.25,
fill = "white"
) +
xlim(-0.1, 1) +
ylim(0, 1.05) +
theme_void()
grafico_final <- p_heat / (p_tabla | p_metricas) +
plot_layout(heights = c(3, 1.3), widths = c(1.6, 1))
list(
grafico = grafico_final,
reporte = reporte,
pred = pred,
real = real
)
}
#############################
# 6. GENERAR Y GUARDAR LOS 7 GRÁFICOS
#############################
resultados_graficos <- list()
for (nombre in names(modelos_finales)) {
cat("\nGenerando gráfico para:", nombre, "\n")
salida <- crear_grafico_matriz_confusion(
modelo = modelos_finales[[nombre]],
nombre_modelo = nombre,
data_test = test_model
)
resultados_graficos[[nombre]] <- salida
# Mostrar en pantalla
print(salida$grafico)
# Guardar PNG
ggsave(
filename = file.path(dir_salida, paste0("Matriz_confusion_", nombre, ".png")),
plot = salida$grafico,
width = 12,
height = 9,
dpi = 300
)
# Guardar PDF
ggsave(
filename = file.path(dir_salida, paste0("Matriz_confusion_", nombre, ".pdf")),
plot = salida$grafico,
width = 12,
height = 9
)
}
##
## Generando gráfico para: Logit
## Warning in annotate("label", x = 0, y = 1, label = texto_metricas, hjust = 0, :
## Ignoring unknown parameters: `label.size`
##
## Generando gráfico para: Tree
## Warning in annotate("label", x = 0, y = 1, label = texto_metricas, hjust = 0, :
## Ignoring unknown parameters: `label.size`
##
## Generando gráfico para: RF
## Warning in annotate("label", x = 0, y = 1, label = texto_metricas, hjust = 0, :
## Ignoring unknown parameters: `label.size`
##
## Generando gráfico para: SVM
## Warning in annotate("label", x = 0, y = 1, label = texto_metricas, hjust = 0, :
## Ignoring unknown parameters: `label.size`
##
## Generando gráfico para: KNN
## Warning in annotate("label", x = 0, y = 1, label = texto_metricas, hjust = 0, :
## Ignoring unknown parameters: `label.size`
##
## Generando gráfico para: NN
## Warning in annotate("label", x = 0, y = 1, label = texto_metricas, hjust = 0, :
## Ignoring unknown parameters: `label.size`
##
## Generando gráfico para: NB
## Warning in annotate("label", x = 0, y = 1, label = texto_metricas, hjust = 0, :
## Ignoring unknown parameters: `label.size`
#############################
# 7. RESUMEN GLOBAL DE MÉTRICAS
#############################
tabla_metricas_resumen <- lapply(names(resultados_graficos), function(nombre) {
rep <- resultados_graficos[[nombre]]$reporte
data.frame(
Modelo = nombre,
Accuracy = rep$accuracy,
Precision = rep$precision,
Recall = rep$recall,
F1_Score = rep$f1,
Specificity = rep$specificity,
AUC = rep$auc,
stringsAsFactors = FALSE
)
}) %>% bind_rows() %>% as.data.frame()
tabla_metricas_resumen <- tabla_metricas_resumen %>%
mutate(across(where(is.numeric), ~ round(.x, 4))) %>%
as.data.frame()
print(tabla_metricas_resumen)
## Modelo Accuracy Precision Recall F1_Score Specificity AUC
## 1 Logit 0.7192 0.7941 0.7883 0.7912 0.5758 0.7796
## 2 Tree 0.7340 0.8268 0.7664 0.7955 0.6667 0.7853
## 3 RF 0.7192 0.7174 0.9635 0.8224 0.2121 0.7920
## 4 SVM 0.7192 0.7299 0.9270 0.8167 0.2879 0.7773
## 5 KNN 0.6798 0.7093 0.8905 0.7896 0.2424 0.7131
## 6 NN 0.6946 0.7622 0.7956 0.7786 0.4848 0.7460
## 7 NB 0.7291 0.8361 0.7445 0.7876 0.6970 0.7890
write.csv(
tabla_metricas_resumen,
file = file.path(dir_salida, "Resumen_metricas_matrices_confusion.csv"),
row.names = FALSE
)
cat("\n========================================\n")
##
## ========================================
cat("ARCHIVOS GENERADOS EN:", normalizePath(dir_salida), "\n")
## ARCHIVOS GENERADOS EN: C:\Users\DELL\Desktop\Tesis-Estadistica\salida_tesis
cat("Se generaron matrices de confusión gráficas para:\n")
## Se generaron matrices de confusión gráficas para:
cat(paste(names(modelos_finales), collapse = ", "), "\n")
## Logit, Tree, RF, SVM, KNN, NN, NB
cat("========================================\n")
## ========================================
############################################################
# 18. SALIDAS FINALES INTEGRADAS _finalisisma
# Gráficos y tablas finales para el Capítulo 4
############################################################
#############################
# 18.1 PAQUETES
#############################
paquetes_finales <- c("ggplot2", "dplyr", "tidyr", "stringr", "forcats", "pROC", "openxlsx", "caret", "rpart.plot")
invisible(lapply(paquetes_finales, instalar_si_falta))
#############################
# 18.2 CARPETAS DE SALIDA
#############################
dir_graficos_finalisisma <- "graficos_finalisisma"
dir_resultados_finalisisma <- "resultados_finalisisma"
dir.create(dir_graficos_finalisisma, showWarnings = FALSE, recursive = TRUE)
dir.create(dir_resultados_finalisisma, showWarnings = FALSE, recursive = TRUE)
#############################
# 18.3 OBJETOS BASE
#############################
# Modelos finales
if (!exists("modelos_finales")) {
modelos_finales <- list(
Logit = modelo_logit_cv,
Tree = modelo_tree_cv,
RF = modelo_rf_cv,
SVM = modelo_svm_cv,
KNN = modelo_knn_cv,
NN = modelo_nn_cv,
NB = modelo_nb_cv
)
}
# Base de prueba
if (!exists("test_model")) {
stop("No existe 'test_model'. Este bloque debe ejecutarse después del entrenamiento y la creación de la base de prueba.")
}
test_model$ESTADO_ACADEMICO <- factor(test_model$ESTADO_ACADEMICO, levels = c("Desertor", "No_Desertor"))
#############################
# 18.4 FUNCIÓN DE EVALUACIÓN SI NO EXISTE
#############################
if (!exists("evaluar_modelo_caret")) {
evaluar_modelo_caret <- function(modelo, nombre_modelo, test_data, variable_respuesta = "ESTADO_ACADEMICO") {
pred_clase <- predict(modelo, newdata = test_data, type = "raw")
pred_clase <- factor(pred_clase, levels = c("Desertor", "No_Desertor"))
pred_prob <- predict(modelo, newdata = test_data, type = "prob")[, "Desertor"]
real <- factor(test_data[[variable_respuesta]], levels = c("Desertor", "No_Desertor"))
mc <- confusionMatrix(pred_clase, real, positive = "Desertor")
roc_obj <- roc(response = real, predictor = pred_prob, levels = c("No_Desertor", "Desertor"), direction = "<")
auc_val <- as.numeric(auc(roc_obj))
precision_val <- as.numeric(mc$byClass["Pos Pred Value"])
recall_val <- as.numeric(mc$byClass["Sensitivity"])
f1_val <- ifelse((precision_val + recall_val) == 0, NA,
2 * precision_val * recall_val / (precision_val + recall_val))
resumen <- data.frame(
Modelo = nombre_modelo,
Accuracy = as.numeric(mc$overall["Accuracy"]),
Kappa = as.numeric(mc$overall["Kappa"]),
Sensitivity = recall_val,
Specificity = as.numeric(mc$byClass["Specificity"]),
Precision = precision_val,
F1 = f1_val,
AUC = auc_val,
row.names = NULL
)
list(nombre = nombre_modelo, confusion = mc, roc = roc_obj, resumen = resumen)
}
}
#############################
# 18.5 RESULTADOS EN TEST
#############################
if (!exists("resultados_test_lista")) {
resultados_test_lista <- lapply(names(modelos_finales), function(nombre) {
evaluar_modelo_caret(modelos_finales[[nombre]], nombre, test_model)
})
names(resultados_test_lista) <- names(modelos_finales)
}
tabla_metricas_final <- do.call(rbind, lapply(resultados_test_lista, function(x) x$resumen))
tabla_metricas_final <- as.data.frame(tabla_metricas_final)
rownames(tabla_metricas_final) <- NULL
cols_num <- intersect(c("Accuracy", "Kappa", "Sensitivity", "Specificity", "Precision", "F1", "AUC"), names(tabla_metricas_final))
tabla_metricas_final <- tabla_metricas_final %>%
mutate(across(any_of(cols_num), ~ suppressWarnings(as.numeric(as.character(.x))))) %>%
as.data.frame()
tabla_metricas_final <- tabla_metricas_final %>% arrange(desc(AUC), desc(Accuracy))
#############################
# 18.5A BLINDAJE DE OBJETOS
#############################
tabla_metricas_final <- as.data.frame(tabla_metricas_final, stringsAsFactors = FALSE)
if (!"Modelo" %in% names(tabla_metricas_final)) {
stop("tabla_metricas_final no contiene la columna 'Modelo'.")
}
#############################
# 18.6 RESULTADOS DE VALIDACIÓN CRUZADA
#############################
if (!exists("resultados_cv")) {
resultados_cv <- resamples(modelos_finales)
}
resumen_cv_final <- summary(resultados_cv)
#------------------------------------------
# Función robusta para extraer tablas CV
#------------------------------------------
extraer_tabla_cv <- function(obj_stats, prefijo) {
stat_df <- as.data.frame(obj_stats, stringsAsFactors = FALSE, check.names = FALSE)
tb <- data.frame(
Modelo = rownames(stat_df),
stat_df,
row.names = NULL,
check.names = FALSE,
stringsAsFactors = FALSE
)
# Renombrar columnas según existan realmente
nombres_originales <- names(tb)
nombres_nuevos <- sapply(nombres_originales, function(nm) {
if (nm == "Modelo") return("Modelo")
if (nm %in% c("Min.", "Min")) return(paste0(prefijo, "_Min"))
if (nm %in% c("1st Qu.", "1st Qu")) return(paste0(prefijo, "_Q1"))
if (nm == "Median") return(paste0(prefijo, "_Median"))
if (nm == "Mean") return(paste0(prefijo, "_Mean"))
if (nm %in% c("3rd Qu.", "3rd Qu")) return(paste0(prefijo, "_Q3"))
if (nm %in% c("Max.", "Max")) return(paste0(prefijo, "_Max"))
if (nm %in% c("NA's", "NAs", "NA")) return(paste0(prefijo, "_NAs"))
return(paste0(prefijo, "_", make.names(nm)))
})
names(tb) <- nombres_nuevos
as.data.frame(tb, stringsAsFactors = FALSE)
}
#------------------------------------------
# Extraer tablas para ROC, Sens y Spec
#------------------------------------------
tabla_cv_roc <- extraer_tabla_cv(resumen_cv_final$statistics$ROC, "ROC")
tabla_cv_sens <- extraer_tabla_cv(resumen_cv_final$statistics$Sens, "Sens")
tabla_cv_spec <- extraer_tabla_cv(resumen_cv_final$statistics$Spec, "Spec")
#------------------------------------------
# Unir tablas resumen CV
#------------------------------------------
tabla_cv_resumen_final <- tabla_cv_roc %>%
left_join(tabla_cv_sens, by = "Modelo") %>%
left_join(tabla_cv_spec, by = "Modelo") %>%
arrange(desc(ROC_Mean)) %>%
as.data.frame(stringsAsFactors = FALSE)
#------------------------------------------
# Tabla de generalización
#------------------------------------------
columnas_req <- c("Modelo", "AUC", "Sensitivity", "Specificity", "Accuracy")
columnas_opt <- c("Precision", "F1", "Kappa")
if (all(columnas_req %in% names(tabla_metricas_final))) {
columnas_existentes <- c(
columnas_req,
columnas_opt[columnas_opt %in% names(tabla_metricas_final)]
)
columnas_cv_req <- c("Modelo", "ROC_Mean", "Sens_Mean", "Spec_Mean")
if (all(columnas_cv_req %in% names(tabla_cv_resumen_final))) {
tabla_generalizacion_final <- tabla_metricas_final %>%
select(all_of(columnas_existentes)) %>%
rename(AUC_Test = AUC) %>%
left_join(
tabla_cv_resumen_final %>%
select(Modelo, AUC_CV = ROC_Mean, Sens_CV = Sens_Mean, Spec_CV = Spec_Mean),
by = "Modelo"
) %>%
mutate(
Brecha_AUC = AUC_Test - AUC_CV,
Brecha_Sens = Sensitivity - Sens_CV,
Brecha_Spec = Specificity - Spec_CV
) %>%
as.data.frame(stringsAsFactors = FALSE)
} else {
warning("No se encontraron todas las columnas esperadas en tabla_cv_resumen_final. No se generó tabla_generalizacion_final.")
tabla_generalizacion_final <- NULL
}
} else {
warning("No se encontraron todas las columnas requeridas en tabla_metricas_final. No se generó tabla_generalizacion_final.")
tabla_generalizacion_final <- NULL
}
#------------------------------------------
# Mostrar resultados
#------------------------------------------
print(tabla_cv_roc)
## Modelo ROC_Min ROC_Q1 ROC_Median ROC_Mean ROC_Q3 ROC_Max ROC_NAs
## 1 Logit 0.6541667 0.7437500 0.7836174 0.7803046 0.8199219 0.8996212 0
## 2 Tree 0.6845703 0.7714962 0.7973633 0.7940493 0.8305827 0.8750000 0
## 3 RF 0.6458333 0.7341856 0.7910156 0.7854376 0.8286458 0.9091797 0
## 4 SVM 0.6416667 0.7183268 0.7613636 0.7602734 0.8041992 0.8867188 0
## 5 KNN 0.6318359 0.6966072 0.7434186 0.7483594 0.7955211 0.9062500 0
## 6 NN 0.6208333 0.7228634 0.7753906 0.7677115 0.8043176 0.8916667 0
## 7 NB 0.6041667 0.7292480 0.7802734 0.7714621 0.8144383 0.9104167 0
print(tabla_cv_sens)
## Modelo Sens_Min Sens_Q1 Sens_Median Sens_Mean Sens_Q3 Sens_Max Sens_NAs
## 1 Logit 0.68750 0.7500000 0.8125000 0.8071023 0.8473011 0.9696970 0
## 2 Tree 0.65625 0.7500000 0.7812500 0.7886995 0.8167614 0.9375000 0
## 3 RF 0.90625 0.9375000 0.9687500 0.9616477 0.9924242 1.0000000 0
## 4 SVM 0.68750 0.8437500 0.8787879 0.8777462 0.9375000 1.0000000 0
## 5 KNN 0.81250 0.9062500 0.9375000 0.9304924 0.9614110 1.0000000 0
## 6 NN 0.65625 0.7500000 0.8125000 0.7967487 0.8437500 0.9090909 0
## 7 NB 0.37500 0.6328125 0.7187500 0.6799874 0.7812500 0.8181818 0
print(tabla_cv_spec)
## Modelo Spec_Min Spec_Q1 Spec_Median Spec_Mean Spec_Q3 Spec_Max Spec_NAs
## 1 Logit 0.3333333 0.5000000 0.5625000 0.5548611 0.62500 0.7500 0
## 2 Tree 0.3125000 0.4666667 0.6125000 0.5850000 0.68750 0.8750 0
## 3 RF 0.0625000 0.1468750 0.1875000 0.2031944 0.25000 0.3750 0
## 4 SVM 0.0000000 0.2000000 0.3229167 0.3322222 0.43750 0.6875 0
## 5 KNN 0.0000000 0.1468750 0.2000000 0.2259722 0.31250 0.4375 0
## 6 NN 0.2500000 0.3750000 0.5000000 0.4906944 0.61875 0.7500 0
## 7 NB 0.4375000 0.6250000 0.7500000 0.7298611 0.81250 1.0000 0
print(tabla_cv_resumen_final)
## Modelo ROC_Min ROC_Q1 ROC_Median ROC_Mean ROC_Q3 ROC_Max ROC_NAs
## 1 Tree 0.6845703 0.7714962 0.7973633 0.7940493 0.8305827 0.8750000 0
## 2 RF 0.6458333 0.7341856 0.7910156 0.7854376 0.8286458 0.9091797 0
## 3 Logit 0.6541667 0.7437500 0.7836174 0.7803046 0.8199219 0.8996212 0
## 4 NB 0.6041667 0.7292480 0.7802734 0.7714621 0.8144383 0.9104167 0
## 5 NN 0.6208333 0.7228634 0.7753906 0.7677115 0.8043176 0.8916667 0
## 6 SVM 0.6416667 0.7183268 0.7613636 0.7602734 0.8041992 0.8867188 0
## 7 KNN 0.6318359 0.6966072 0.7434186 0.7483594 0.7955211 0.9062500 0
## Sens_Min Sens_Q1 Sens_Median Sens_Mean Sens_Q3 Sens_Max Sens_NAs
## 1 0.65625 0.7500000 0.7812500 0.7886995 0.8167614 0.9375000 0
## 2 0.90625 0.9375000 0.9687500 0.9616477 0.9924242 1.0000000 0
## 3 0.68750 0.7500000 0.8125000 0.8071023 0.8473011 0.9696970 0
## 4 0.37500 0.6328125 0.7187500 0.6799874 0.7812500 0.8181818 0
## 5 0.65625 0.7500000 0.8125000 0.7967487 0.8437500 0.9090909 0
## 6 0.68750 0.8437500 0.8787879 0.8777462 0.9375000 1.0000000 0
## 7 0.81250 0.9062500 0.9375000 0.9304924 0.9614110 1.0000000 0
## Spec_Min Spec_Q1 Spec_Median Spec_Mean Spec_Q3 Spec_Max Spec_NAs
## 1 0.3125000 0.4666667 0.6125000 0.5850000 0.68750 0.8750 0
## 2 0.0625000 0.1468750 0.1875000 0.2031944 0.25000 0.3750 0
## 3 0.3333333 0.5000000 0.5625000 0.5548611 0.62500 0.7500 0
## 4 0.4375000 0.6250000 0.7500000 0.7298611 0.81250 1.0000 0
## 5 0.2500000 0.3750000 0.5000000 0.4906944 0.61875 0.7500 0
## 6 0.0000000 0.2000000 0.3229167 0.3322222 0.43750 0.6875 0
## 7 0.0000000 0.1468750 0.2000000 0.2259722 0.31250 0.4375 0
if (!is.null(tabla_generalizacion_final)) {
print(tabla_generalizacion_final)
}
## Modelo AUC_Test Sensitivity Specificity Accuracy Precision F1
## 1 RF 0.7919708 0.9635036 0.2121212 0.7192118 0.7173913 0.8224299
## 2 NB 0.7890400 0.7445255 0.6969697 0.7290640 0.8360656 0.7876448
## 3 Tree 0.7853351 0.7664234 0.6666667 0.7339901 0.8267717 0.7954545
## 4 Logit 0.7796395 0.7883212 0.5757576 0.7192118 0.7941176 0.7912088
## 5 SVM 0.7773170 0.9270073 0.2878788 0.7192118 0.7298851 0.8167203
## 6 NN 0.7460186 0.7956204 0.4848485 0.6945813 0.7622378 0.7785714
## 7 KNN 0.7131166 0.8905109 0.2424242 0.6798030 0.7093023 0.7896440
## Kappa AUC_CV Sens_CV Spec_CV Brecha_AUC Brecha_Sens
## 1 0.2153658 0.7854376 0.9616477 0.2031944 0.0065332240 0.001855922
## 2 0.4169408 0.7714621 0.6799874 0.7298611 0.0175779536 0.064538174
## 3 0.4167287 0.7940493 0.7886995 0.5850000 -0.0087141790 -0.022276137
## 4 0.3626549 0.7803046 0.8071023 0.5548611 -0.0006651483 -0.018781105
## 5 0.2514071 0.7602734 0.8777462 0.3322222 0.0170435278 0.049261087
## 6 0.2872352 0.7677115 0.7967487 0.4906944 -0.0216929099 -0.001128299
## 7 0.1541124 0.7483594 0.9304924 0.2259722 -0.0352428079 -0.039981475
## Brecha_Spec
## 1 0.008926768
## 2 -0.032891414
## 3 0.081666667
## 4 0.020896465
## 5 -0.044343434
## 6 -0.005845960
## 7 0.016452020
#############################
# 18.7 FUNCIÓN PARA GUARDAR PLOTS
#############################
guardar_plot_finalisisma <- function(plot_obj, nombre_base, width = 12, height = 8, dpi = 300) {
ggsave(filename = file.path(dir_graficos_finalisisma, paste0(nombre_base, ".png")),
plot = plot_obj, width = width, height = height, dpi = dpi)
ggsave(filename = file.path(dir_graficos_finalisisma, paste0(nombre_base, ".pdf")),
plot = plot_obj, width = width, height = height)
}
etiqueta_coma <- function(x, digits = 3) {
format(round(x, digits), nsmall = digits, decimal.mark = ",")
}
#############################
# 18.8 HEATMAP DE MÉTRICAS EN TEST
#############################
metricas_heat <- c("Accuracy", "Sensitivity", "Specificity", "Precision", "F1", "AUC")
metricas_heat <- metricas_heat[metricas_heat %in% names(tabla_metricas_final)]
orden_heat <- tabla_metricas_final$Modelo
heat_data <- tabla_metricas_final %>%
mutate(Modelo = factor(Modelo, levels = rev(orden_heat))) %>%
select(Modelo, all_of(metricas_heat)) %>%
pivot_longer(cols = -Modelo, names_to = "Metrica", values_to = "Valor") %>%
mutate(Metrica = factor(Metrica, levels = metricas_heat))
g_heatmap_finalisisma <- ggplot(heat_data, aes(x = Metrica, y = Modelo, fill = Valor)) +
geom_tile(color = "white", linewidth = 0.8) +
geom_text(aes(label = etiqueta_coma(Valor, 3)), size = 4.2) +
scale_fill_gradient(low = "#f7fbff", high = "#2171b5", limits = c(0, 1), name = "Valor") +
labs(
title = "Mapa de calor de las métricas de desempeño",
subtitle = "Conjunto de prueba independiente",
x = "Métrica",
y = "Modelo"
) +
theme_minimal(base_size = 15) +
theme(panel.grid = element_blank(),
axis.text.x = element_text(angle = 25, hjust = 1),
plot.title = element_text(face = "bold"))
print(g_heatmap_finalisisma)
guardar_plot_finalisisma(g_heatmap_finalisisma, "heatmap_metricas_modelos_test_finalisisma", width = 13, height = 8)
#############################
# 18.9 SCATTER INTEGRADO
#############################
scatter_data <- tabla_metricas_final %>%
mutate(
Tipo_comportamiento = case_when(
Sensitivity >= 0.90 & Specificity < 0.40 ~ "Sesgado",
Sensitivity >= 0.85 & Specificity >= 0.45 ~ "Óptimo",
TRUE ~ "Intermedio"
),
Tipo_comportamiento = factor(Tipo_comportamiento, levels = c("Intermedio", "Óptimo", "Sesgado"))
)
g_scatter_finalisisma <- ggplot(
scatter_data,
aes(x = Specificity, y = Sensitivity, size = AUC, color = Tipo_comportamiento, label = Modelo)
) +
geom_point(alpha = 0.9) +
geom_text(size = 5, fontface = "bold", nudge_y = 0.012, show.legend = FALSE) +
scale_size_continuous(range = c(8, 16)) +
scale_color_manual(
values = c(
"Óptimo" = "#2ca25f",
"Intermedio" = "#fec44f",
"Sesgado" = "#de2d26"
)
) +
coord_cartesian(
xlim = c(0, max(0.60, max(scatter_data$Specificity, na.rm = TRUE) + 0.03)),
ylim = c(min(0.65, min(scatter_data$Sensitivity, na.rm = TRUE) - 0.03), 1.00)
) +
labs(
title = "Comparación integrada del desempeño de modelos",
subtitle = "Sensibilidad, especificidad y AUC en el conjunto de prueba",
x = "Especificidad",
y = "Sensibilidad",
size = "AUC",
color = "Tipo de comportamiento"
) +
theme_minimal(base_size = 15) +
theme(
plot.title = element_text(face = "bold"),
legend.position = "right"
)
#############################
# 18.10 CURVAS ROC COMPARATIVAS
#############################
roc_df_final <- bind_rows(lapply(names(resultados_test_lista), function(nombre) {
roc_obj <- resultados_test_lista[[nombre]]$roc
data.frame(
Especificidad = rev(roc_obj$specificities),
Sensibilidad = rev(roc_obj$sensitivities),
Modelo = nombre,
stringsAsFactors = FALSE
)
}))
tabla_auc_test_final <- do.call(rbind, lapply(names(resultados_test_lista), function(nombre) {
data.frame(Modelo = nombre, AUC = as.numeric(resultados_test_lista[[nombre]]$resumen$AUC))
}))
g_roc_finalisisma <- ggplot(roc_df_final, aes(x = 1 - Especificidad, y = Sensibilidad, color = Modelo)) +
geom_line(linewidth = 1.1) +
geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "grey50") +
labs(
title = "Curvas ROC de los modelos en el conjunto de prueba",
x = "1 - Especificidad",
y = "Sensibilidad",
color = "Modelo"
) +
theme_minimal(base_size = 15) +
theme(plot.title = element_text(face = "bold"))
print(g_roc_finalisisma)
guardar_plot_finalisisma(g_roc_finalisisma, "curvas_ROC_modelos_finalisisma", width = 12, height = 8)
#############################
# 18.11 ÁRBOL DE DECISIÓN FINAL
#############################
if (exists("modelo_tree_cv")) {
png(filename = file.path(dir_graficos_finalisisma, "arbol_decision_final_finalisisma.png"),
width = 2200, height = 1500, res = 220)
rpart.plot(
modelo_tree_cv$finalModel,
main = "Árbol de decisión final",
type = 4,
extra = 104,
under = TRUE,
fallen.leaves = TRUE,
yesno = 2,
faclen = 0,
varlen = 0,
tweak = 1.10,
branch = 1,
compress = TRUE,
shadow.col = 0,
nn = FALSE
)
dev.off()
pdf(file = file.path(dir_graficos_finalisisma, "arbol_decision_final_finalisisma.pdf"), width = 14, height = 9)
rpart.plot(
modelo_tree_cv$finalModel,
main = "Árbol de decisión final",
type = 4,
extra = 104,
under = TRUE,
fallen.leaves = TRUE,
yesno = 2,
faclen = 0,
varlen = 0,
tweak = 1.10,
branch = 1,
compress = TRUE,
shadow.col = 0,
nn = FALSE
)
dev.off()
}
## png
## 2
#############################
# 18.12 IMPORTANCIA DE CATEGORÍAS PREDICTORAS - RANDOM FOREST
#############################
if (exists("modelo_rf_cv")) {
imp_rf_raw <- varImp(modelo_rf_cv)$importance
imp_rf <- as.data.frame(imp_rf_raw, stringsAsFactors = FALSE, check.names = FALSE)
imp_rf <- data.frame(
Variable = rownames(imp_rf),
imp_rf,
row.names = NULL,
check.names = FALSE,
stringsAsFactors = FALSE
)
cols_imp <- setdiff(names(imp_rf), "Variable")
if (length(cols_imp) == 0) {
stop("No se encontró ninguna columna de importancia en varImp(modelo_rf_cv).")
}
col_importancia <- if ("Overall" %in% cols_imp) "Overall" else cols_imp[1]
imp_rf_plot <- imp_rf %>%
arrange(.data[[col_importancia]]) %>%
tail(min(20, nrow(.))) %>%
mutate(Variable = factor(Variable, levels = Variable)) %>%
as.data.frame(stringsAsFactors = FALSE)
g_imp_rf_finalisisma <- ggplot(imp_rf_plot, aes(x = Variable, y = .data[[col_importancia]])) +
geom_col(fill = "#2b8cbe") +
coord_flip() +
labs(
title = "Importancia de categorías predictoras",
subtitle = "Modelo Random Forest",
x = NULL,
y = "Importancia"
) +
theme_minimal(base_size = 15) +
theme(plot.title = element_text(face = "bold"))
print(g_imp_rf_finalisisma)
guardar_plot_finalisisma(
g_imp_rf_finalisisma,
"importancia_variables_random_forest_finalisisma",
width = 11,
height = 8
)
}
#############################
# 18.13 BOXPLOTS PANELADOS DE VALIDACIÓN CRUZADA
#############################
cv_values_final <- as.data.frame(resultados_cv$values, stringsAsFactors = FALSE, check.names = FALSE)
if (!"Resample" %in% names(cv_values_final)) {
stop("El objeto resultados_cv$values no contiene la columna Resample.")
}
cv_largo_final <- cv_values_final %>%
tidyr::pivot_longer(cols = -Resample, names_to = "Modelo_Metrica", values_to = "Valor") %>%
dplyr::filter(stringr::str_detect(Modelo_Metrica, "~(ROC|Sens|Spec)$")) %>%
tidyr::separate(col = Modelo_Metrica, into = c("Modelo", "Metrica"), sep = "~") %>%
as.data.frame(stringsAsFactors = FALSE)
cv_largo_final$Metrica <- dplyr::recode(
cv_largo_final$Metrica,
"Spec" = "Especificidad",
"ROC" = "ROC",
"Sens" = "Sensibilidad"
)
orden_modelos_cv <- intersect(
c("KNN", "Logit", "NB", "NN", "RF", "SVM", "Tree"),
unique(cv_largo_final$Modelo)
)
cv_largo_final$Modelo <- factor(cv_largo_final$Modelo, levels = orden_modelos_cv)
cv_largo_final$Metrica <- factor(
cv_largo_final$Metrica,
levels = c("Especificidad", "ROC", "Sensibilidad")
)
g_cv_paneles_finalisisma <- ggplot(cv_largo_final, aes(x = Modelo, y = Valor)) +
geom_boxplot(width = 0.6, fill = "white", color = "black", outlier.shape = 16, outlier.size = 2) +
facet_wrap(~ Metrica, ncol = 1, scales = "free_y") +
labs(
title = "Resultados de validación cruzada por métrica",
x = "Modelo",
y = "Valor"
) +
theme_minimal(base_size = 16) +
theme(
strip.text = element_text(size = 14),
plot.title = element_text(size = 19, face = "bold", hjust = 0.5),
axis.text.x = element_text(angle = 25, hjust = 1)
)
print(g_cv_paneles_finalisisma)
guardar_plot_finalisisma(
g_cv_paneles_finalisisma,
"Boxplots_CV_paneles_finalisisma",
width = 12,
height = 10
)
tabla_cv_resumen_paneles_final <- cv_largo_final %>%
dplyr::group_by(Modelo, Metrica) %>%
dplyr::summarise(
Media = mean(Valor, na.rm = TRUE),
Mediana = median(Valor, na.rm = TRUE),
DE = sd(Valor, na.rm = TRUE),
Min = min(Valor, na.rm = TRUE),
Max = max(Valor, na.rm = TRUE),
.groups = "drop"
) %>%
as.data.frame(stringsAsFactors = FALSE)
#############################
# 18.14 TABLA ENTRE ENFOQUES
#############################
tabla_enfoques_final <- data.frame(
Enfoque = c("Modelización de datos", "Modelización algorítmica"),
Evidencia_principal = c(
"La regresión logística mostró desempeño competitivo, estabilidad en validación cruzada e interpretabilidad de coeficientes.",
"Los modelos algorítmicos evidenciaron patrones diferenciados: mayor sensibilidad en NB, SVM y KNN; mayor especificidad en RF; mejor equilibrio en NN."
),
stringsAsFactors = FALSE
)
#############################
# 18.15 EXPORTACIÓN INTEGRADA A EXCEL Y CSV
#############################
wb_final <- createWorkbook()
addWorksheet(wb_final, "Resultados_Test")
writeData(wb_final, "Resultados_Test", tabla_metricas_final)
addWorksheet(wb_final, "Ranking_Modelos")
writeData(wb_final, "Ranking_Modelos", tabla_metricas_final %>% arrange(desc(AUC), desc(Accuracy)))
addWorksheet(wb_final, "Resumen_CV")
writeData(wb_final, "Resumen_CV", tabla_cv_resumen_final)
addWorksheet(wb_final, "CV_Paneles")
writeData(wb_final, "CV_Paneles", tabla_cv_resumen_paneles_final)
addWorksheet(wb_final, "AUC_Test")
writeData(wb_final, "AUC_Test", tabla_auc_test_final)
addWorksheet(wb_final, "Enfoques")
writeData(wb_final, "Enfoques", tabla_enfoques_final)
if (!is.null(tabla_generalizacion_final)) {
addWorksheet(wb_final, "Generalizacion")
writeData(wb_final, "Generalizacion", tabla_generalizacion_final)
}
saveWorkbook(
wb_final,
file = file.path(dir_resultados_finalisisma, "Resumen_y_metricas_modelos_finalisisma.xlsx"),
overwrite = TRUE
)
write.csv(tabla_metricas_final, file.path(dir_resultados_finalisisma, "tabla_metricas_test_finalisisma.csv"), row.names = FALSE)
write.csv(tabla_cv_resumen_final, file.path(dir_resultados_finalisisma, "tabla_resumen_cv_finalisisma.csv"), row.names = FALSE)
write.csv(tabla_cv_resumen_paneles_final, file.path(dir_resultados_finalisisma, "tabla_cv_paneles_finalisisma.csv"), row.names = FALSE)
write.csv(tabla_auc_test_final, file.path(dir_resultados_finalisisma, "tabla_auc_test_finalisisma.csv"), row.names = FALSE)
write.csv(tabla_enfoques_final, file.path(dir_resultados_finalisisma, "tabla_enfoques_finalisisma.csv"), row.names = FALSE)
if (!is.null(tabla_generalizacion_final)) {
write.csv(tabla_generalizacion_final, file.path(dir_resultados_finalisisma, "tabla_generalizacion_finalisisma.csv"), row.names = FALSE)
}
#############################
# 18.16 MENSAJE FINAL
#############################
cat("\n====================================================\n")
##
## ====================================================
cat("SALIDAS FINALES _finalisisma GENERADAS CORRECTAMENTE\n")
## SALIDAS FINALES _finalisisma GENERADAS CORRECTAMENTE
cat("Gráficos guardados en: ", normalizePath(dir_graficos_finalisisma), "\n")
## Gráficos guardados en: C:\Users\DELL\Desktop\Tesis-Estadistica\graficos_finalisisma
cat("Resultados guardados en: ", normalizePath(dir_resultados_finalisisma), "\n")
## Resultados guardados en: C:\Users\DELL\Desktop\Tesis-Estadistica\resultados_finalisisma
cat("Archivo Excel: Resumen_y_metricas_modelos_finalisisma.xlsx\n")
## Archivo Excel: Resumen_y_metricas_modelos_finalisisma.xlsx
cat("====================================================\n")
## ====================================================
############################################################
# MODELO LOGÍSTICO FINAL - TESIS DESERCIÓN FACEN
# BLOQUE COMPLETO PARA 4.6 Y 4.7
############################################################
#############################
# 0. PAQUETES
#############################
paquetes <- c(
"readxl", "dplyr", "ggplot2", "caret", "pROC",
"car", "ResourceSelection", "openxlsx"
)
instalar_si_falta <- function(pkg) {
if (!require(pkg, character.only = TRUE)) {
install.packages(pkg, dependencies = TRUE)
library(pkg, character.only = TRUE)
}
}
invisible(lapply(paquetes, instalar_si_falta))
#############################
# 1. CARPETAS DE SALIDA
#############################
dir_resultados <- "resultados_modelo_logistico_final"
dir_graficos <- "graficos_modelo_logistico_final"
dir.create(dir_resultados, showWarnings = FALSE, recursive = TRUE)
dir.create(dir_graficos, showWarnings = FALSE, recursive = TRUE)
#############################
# 2. CARGA DE DATOS
#############################
# OPCIÓN A:
# Si ya tienes la base cargada en un data.frame llamado 'tesis',
# deja comentada la siguiente línea.
# tesis <- readxl::read_excel("desercion.xlsx")
# Validación mínima
if (!exists("tesis")) {
stop("No existe el objeto 'tesis'. Carga primero la base o ajusta la ruta del archivo Excel.")
}
#############################
# 3. PREPARACIÓN DE LA BASE
#############################
variables_candidatas <- c(
"RESIDENCIA", "CARRERA", "TIPO_INGRESO", "MODALIDAD", "RENDIMIENTO",
"TRABAJA", "SOLVENTAR", "ESTUDIOS_PADRES", "SEXO", "TIPO_COL",
"INGRESO", "EST_CIVIL", "NIVEL_SOCIO", "EDAD"
)
vars_necesarias <- c("ESTADO_ACADEMICO", variables_candidatas)
faltantes <- setdiff(vars_necesarias, names(tesis))
if (length(faltantes) > 0) {
stop(paste("Faltan estas variables en la base:", paste(faltantes, collapse = ", ")))
}
tesis_modelo <- tesis[, vars_necesarias]
tesis_modelo <- tesis_modelo[complete.cases(tesis_modelo), ]
# Asegurar tipos
vars_factor <- setdiff(vars_necesarias, c("EDAD"))
tesis_modelo[vars_factor] <- lapply(tesis_modelo[vars_factor], factor)
tesis_modelo$EDAD <- as.numeric(tesis_modelo$EDAD)
# Variable respuesta factor y binaria explícita
# IMPORTANTE: aquí se define sin ambigüedad que el evento de interés es DESERTOR = 1
tesis_modelo$ESTADO_ACADEMICO <- factor(
tesis_modelo$ESTADO_ACADEMICO,
levels = c("No_Desertor", "Desertor")
)
# Si en tu base original los niveles vienen al revés, este bloque corrige:
if (!all(c("No_Desertor", "Desertor") %in% levels(tesis_modelo$ESTADO_ACADEMICO))) {
niveles_actuales <- levels(tesis_modelo$ESTADO_ACADEMICO)
message("Niveles actuales de ESTADO_ACADEMICO: ", paste(niveles_actuales, collapse = ", "))
stop("Ajusta manualmente la codificación de ESTADO_ACADEMICO a 'No_Desertor' y 'Desertor'.")
}
tesis_modelo$Y_LOGIT <- ifelse(tesis_modelo$ESTADO_ACADEMICO == "Desertor", 1, 0)
# Función auxiliar para fijar referencias si existen
relevel_if_exists <- function(x, refs) {
x <- factor(x)
for (r in refs) {
if (r %in% levels(x)) {
return(stats::relevel(x, ref = r))
}
}
x
}
# Referencias sugeridas para alinear con la tesis
tesis_modelo$RESIDENCIA <- relevel_if_exists(tesis_modelo$RESIDENCIA, c("ASUNCIÓN", "Asunción"))
tesis_modelo$CARRERA <- relevel_if_exists(tesis_modelo$CARRERA, c("ESTADISTICA-PRES", "Estadística presencial", "ESTADISTICA PRESENCIAL"))
tesis_modelo$TIPO_INGRESO <- relevel_if_exists(tesis_modelo$TIPO_INGRESO, c("INGRESO", "Ingreso por examen", "EXAMEN DE INGRESO"))
tesis_modelo$RENDIMIENTO <- relevel_if_exists(tesis_modelo$RENDIMIENTO, c("APROBADO", "Aprobado"))
tesis_modelo$TRABAJA <- relevel_if_exists(tesis_modelo$TRABAJA, c("SI", "Sí", "TRABAJA"))
#############################
# 4. PARTICIÓN TRAIN / TEST
#############################
set.seed(123)
idx_train <- caret::createDataPartition(
y = tesis_modelo$ESTADO_ACADEMICO,
p = 0.70,
list = FALSE
)
train_model <- tesis_modelo[idx_train, ]
test_model <- tesis_modelo[-idx_train, ]
#############################
# 5. MODELO LOGÍSTICO INICIAL Y STEPWISE
#############################
form_stepwise <- as.formula(
paste("Y_LOGIT ~", paste(variables_candidatas, collapse = " + "))
)
modelo_nulo <- glm(
Y_LOGIT ~ 1,
data = train_model,
family = binomial(link = "logit")
)
modelo_logit_inicial <- glm(
form_stepwise,
data = train_model,
family = binomial(link = "logit")
)
modelo_logit_final <- step(
modelo_logit_inicial,
direction = "both",
trace = FALSE
)
cat("\n=============================\n")
##
## =============================
cat("MODELO LOGÍSTICO FINAL SELECCIONADO POR STEPWISE\n")
## MODELO LOGÍSTICO FINAL SELECCIONADO POR STEPWISE
cat("=============================\n")
## =============================
print(formula(modelo_logit_final))
## Y_LOGIT ~ CARRERA + TIPO_INGRESO + RENDIMIENTO + TRABAJA
print(summary(modelo_logit_final))
##
## Call:
## glm(formula = Y_LOGIT ~ CARRERA + TIPO_INGRESO + RENDIMIENTO +
## TRABAJA, family = binomial(link = "logit"), data = train_model)
##
## Coefficients:
## Estimate Std. Error z value
## (Intercept) 0.4598 0.2976 1.545
## CARRERAESTADISTICA-SEMI 0.4591 0.5026 0.914
## CARRERAMATEMATICA -0.3829 0.3071 -1.247
## CARRERAEDUCACION MATEMATICA-PRES -1.2584 0.4134 -3.044
## CARRERAEDUCACION MATEMATICA-SEMI 0.3080 0.3533 0.872
## TIPO_INGRESOADMISION DIRECTA -1.6173 0.5699 -2.838
## RENDIMIENTONO APROBADO 2.6019 0.3361 7.742
## TRABAJANO -0.4758 0.2756 -1.726
## Pr(>|z|)
## (Intercept) 0.12227
## CARRERAESTADISTICA-SEMI 0.36096
## CARRERAMATEMATICA 0.21248
## CARRERAEDUCACION MATEMATICA-PRES 0.00233 **
## CARRERAEDUCACION MATEMATICA-SEMI 0.38332
## TIPO_INGRESOADMISION DIRECTA 0.00454 **
## RENDIMIENTONO APROBADO 0.00000000000000978 ***
## TRABAJANO 0.08427 .
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 606.02 on 478 degrees of freedom
## Residual deviance: 449.20 on 471 degrees of freedom
## AIC: 465.2
##
## Number of Fisher Scoring iterations: 5
# Función auxiliar para convertir fórmulas largas en un solo texto
formula_a_texto <- function(f) {
paste(deparse(f), collapse = " ")
}
# Resumen del stepwise para el cuerpo de la tesis
formula_inicial_txt <- formula_a_texto(form_stepwise)
formula_final_txt <- formula_a_texto(formula(modelo_logit_final))
tabla_stepwise_resumen <- data.frame(
Item = c(
"Modelo inicial",
"Criterio de selección",
"Dirección del stepwise",
"Modelo final"
),
Valor = c(
formula_inicial_txt,
"AIC",
"both",
formula_final_txt
),
stringsAsFactors = FALSE
)
print(tabla_stepwise_resumen)
## Item
## 1 Modelo inicial
## 2 Criterio de selección
## 3 Dirección del stepwise
## 4 Modelo final
## Valor
## 1 Y_LOGIT ~ RESIDENCIA + CARRERA + TIPO_INGRESO + MODALIDAD + RENDIMIENTO + TRABAJA + SOLVENTAR + ESTUDIOS_PADRES + SEXO + TIPO_COL + INGRESO + EST_CIVIL + NIVEL_SOCIO + EDAD
## 2 AIC
## 3 both
## 4 Y_LOGIT ~ CARRERA + TIPO_INGRESO + RENDIMIENTO + TRABAJA
#############################
# 6. TABLA DE WALD E IC95%
#############################
coef_mat <- summary(modelo_logit_final)$coefficients
ic_wald <- confint.default(modelo_logit_final)
tabla_wald <- data.frame(
Variable = rownames(coef_mat),
Estimate = coef_mat[, "Estimate"],
Std_Error = coef_mat[, "Std. Error"],
z_value = coef_mat[, "z value"],
p_value = coef_mat[, "Pr(>|z|)"],
IC95_LI = ic_wald[, 1],
IC95_LS = ic_wald[, 2],
row.names = NULL,
stringsAsFactors = FALSE
)
#############################
# 7. TABLA DE ODDS RATIO
#############################
tabla_or <- tabla_wald
tabla_or$OR <- exp(tabla_or$Estimate)
tabla_or$OR_IC95_LI <- exp(tabla_or$IC95_LI)
tabla_or$OR_IC95_LS <- exp(tabla_or$IC95_LS)
tabla_or <- tabla_or[, c(
"Variable", "Estimate", "Std_Error", "z_value", "p_value",
"OR", "OR_IC95_LI", "OR_IC95_LS"
)]
#############################
# 8. PREDICCIONES TRAIN / TEST
#############################
prob_train <- predict(modelo_logit_final, newdata = train_model, type = "response")
prob_test <- predict(modelo_logit_final, newdata = test_model, type = "response")
#############################
# 9. MÉTRICAS DE CLASIFICACIÓN
#############################
metricas_clasificacion <- function(y_true, prob, umbral = 0.50) {
pred_bin <- ifelse(prob >= umbral, 1, 0)
TP <- sum(pred_bin == 1 & y_true == 1)
TN <- sum(pred_bin == 0 & y_true == 0)
FP <- sum(pred_bin == 1 & y_true == 0)
FN <- sum(pred_bin == 0 & y_true == 1)
sensitivity <- ifelse((TP + FN) == 0, NA, TP / (TP + FN))
specificity <- ifelse((TN + FP) == 0, NA, TN / (TN + FP))
accuracy <- (TP + TN) / (TP + TN + FP + FN)
precision <- ifelse((TP + FP) == 0, NA, TP / (TP + FP))
f1 <- ifelse(is.na(precision) | is.na(sensitivity) | (precision + sensitivity) == 0,
NA, 2 * precision * sensitivity / (precision + sensitivity))
balanced <- mean(c(sensitivity, specificity), na.rm = TRUE)
youden <- sensitivity + specificity - 1
# Kappa
total <- TP + TN + FP + FN
po <- accuracy
pe <- (((TP + FP) * (TP + FN)) + ((FN + TN) * (FP + TN))) / (total^2)
kappa <- ifelse((1 - pe) == 0, NA, (po - pe) / (1 - pe))
data.frame(
Umbral = umbral,
Accuracy = accuracy,
Sensitivity = sensitivity,
Specificity = specificity,
Precision = precision,
F1 = f1,
Balanced_Accuracy = balanced,
Youden = youden,
Kappa = kappa,
TP = TP,
TN = TN,
FP = FP,
FN = FN
)
}
tabla_metricas_test_050 <- metricas_clasificacion(test_model$Y_LOGIT, prob_test, umbral = 0.50)
#############################
# 10. MATRIZ DE CONFUSIÓN
#############################
pred_factor_050 <- factor(
ifelse(prob_test >= 0.50, "Desertor", "No_Desertor"),
levels = c("Desertor", "No_Desertor")
)
ref_factor_test <- factor(
ifelse(test_model$Y_LOGIT == 1, "Desertor", "No_Desertor"),
levels = c("Desertor", "No_Desertor")
)
matriz_confusion_050 <- caret::confusionMatrix(
data = pred_factor_050,
reference = ref_factor_test,
positive = "Desertor"
)
#############################
# 11. CURVA ROC Y AUC
#############################
roc_train <- pROC::roc(
response = train_model$Y_LOGIT,
predictor = prob_train,
levels = c(0, 1),
direction = "<"
)
roc_test <- pROC::roc(
response = test_model$Y_LOGIT,
predictor = prob_test,
levels = c(0, 1),
direction = "<"
)
auc_train <- as.numeric(pROC::auc(roc_train))
auc_test <- as.numeric(pROC::auc(roc_test))
tabla_auc <- data.frame(
Escenario = c("Train", "Test"),
AUC = c(auc_train, auc_test),
stringsAsFactors = FALSE
)
tabla_auc$Diferencia_Absoluta <- c(NA, abs(auc_test - auc_train))
#############################
# 12. UMBRAL ÓPTIMO Y YOUDEN
#############################
umbrales <- seq(0.01, 0.99, by = 0.01)
tabla_umbrales <- do.call(
rbind,
lapply(umbrales, function(u) metricas_clasificacion(test_model$Y_LOGIT, prob_test, umbral = u))
)
mejor_youden <- tabla_umbrales[which.max(tabla_umbrales$Youden), ]
# Métricas comparativas: 0.50 vs Youden
tabla_decision_umbral <- dplyr::bind_rows(
dplyr::mutate(tabla_metricas_test_050, Criterio = "Umbral fijo 0.50"),
dplyr::mutate(mejor_youden, Criterio = "Umbral óptimo Youden")
) %>%
dplyr::select(Criterio, everything())
#############################
# 13. RAZÓN DE VEROSIMILITUD
#############################
tabla_lr <- anova(modelo_nulo, modelo_logit_final, test = "Chisq")
tabla_lr <- as.data.frame(tabla_lr)
tabla_lr$Modelo <- c("Nulo", "Final")
#############################
# 14. HOSMER-LEMESHOW
#############################
hl <- ResourceSelection::hoslem.test(
x = train_model$Y_LOGIT,
y = fitted(modelo_logit_final),
g = 10
)
tabla_hl <- data.frame(
Chi_square = as.numeric(hl$statistic),
gl = as.numeric(hl$parameter),
p_value = hl$p.value
)
#############################
# 15. MULTICOLINEALIDAD (VIF / GVIF)
#############################
vif_raw <- tryCatch(
car::vif(modelo_logit_final),
error = function(e) NULL
)
if (is.null(vif_raw)) {
tabla_vif <- data.frame(
Variable = "No disponible",
VIF = NA
)
} else if (is.matrix(vif_raw)) {
tabla_vif <- data.frame(
Variable = rownames(vif_raw),
GVIF = vif_raw[, 1],
Df = vif_raw[, 2],
GVIF_ajustado = vif_raw[, 3],
row.names = NULL
)
} else {
tabla_vif <- data.frame(
Variable = names(vif_raw),
VIF = as.numeric(vif_raw),
row.names = NULL
)
}
#############################
# 16. LINEALIDAD EN EL LOGIT PARA EDAD
#############################
tabla_box_tidwell <- NULL
datos_linealidad <- NULL
if ("EDAD" %in% names(train_model) && all(train_model$EDAD > 0, na.rm = TRUE)) {
modelo_bt <- glm(
Y_LOGIT ~ EDAD + I(EDAD * log(EDAD)),
data = train_model,
family = binomial(link = "logit")
)
bt_coef <- summary(modelo_bt)$coefficients
tabla_box_tidwell <- data.frame(
Variable = rownames(bt_coef),
Estimate = bt_coef[, "Estimate"],
Std_Error = bt_coef[, "Std. Error"],
z_value = bt_coef[, "z value"],
p_value = bt_coef[, "Pr(>|z|)"],
row.names = NULL
)
eps <- 1e-4
datos_linealidad <- train_model %>%
dplyr::filter(!is.na(EDAD), EDAD > 0) %>%
dplyr::mutate(decil = dplyr::ntile(EDAD, 10)) %>%
dplyr::group_by(decil) %>%
dplyr::summarise(
Edad_media = mean(EDAD, na.rm = TRUE),
Prob_media = mean(Y_LOGIT, na.rm = TRUE),
.groups = "drop"
) %>%
dplyr::mutate(
Prob_media = pmin(pmax(Prob_media, eps), 1 - eps),
Logit_empirico = log(Prob_media / (1 - Prob_media))
)
graf_linealidad <- ggplot(datos_linealidad, aes(x = Edad_media, y = Logit_empirico)) +
geom_point(size = 2) +
geom_smooth(method = "lm", se = FALSE, color = "blue") +
labs(
title = "Evaluación exploratoria de linealidad del logit para EDAD",
x = "Edad media por decil",
y = "Logit empírico de la probabilidad de deserción"
) +
theme_minimal()
ggsave(
filename = file.path(dir_graficos, "linealidad_logit_edad.png"),
plot = graf_linealidad,
width = 10,
height = 6,
dpi = 300
)
}
## `geom_smooth()` using formula = 'y ~ x'
#############################
# 17. RESIDUOS DE PEARSON, COOK Y LEVERAGE
#############################
res_pearson <- residuals(modelo_logit_final, type = "pearson")
res_deviance <- residuals(modelo_logit_final, type = "deviance")
cook_d <- cooks.distance(modelo_logit_final)
leverage <- hatvalues(modelo_logit_final)
tabla_residuos_pearson <- data.frame(
Min = min(res_pearson, na.rm = TRUE),
Q1 = as.numeric(quantile(res_pearson, 0.25, na.rm = TRUE)),
Mediana = median(res_pearson, na.rm = TRUE),
Media = mean(res_pearson, na.rm = TRUE),
Q3 = as.numeric(quantile(res_pearson, 0.75, na.rm = TRUE)),
Max = max(res_pearson, na.rm = TRUE)
)
tabla_cook <- data.frame(
Min = min(cook_d, na.rm = TRUE),
Q1 = as.numeric(quantile(cook_d, 0.25, na.rm = TRUE)),
Mediana = median(cook_d, na.rm = TRUE),
Media = mean(cook_d, na.rm = TRUE),
Q3 = as.numeric(quantile(cook_d, 0.75, na.rm = TRUE)),
Max = max(cook_d, na.rm = TRUE)
)
umbral_cook <- 4 / nrow(train_model)
casos_influyentes <- data.frame(
Observacion = seq_along(cook_d),
Cook = as.numeric(cook_d),
Leverage = as.numeric(leverage),
Pearson = as.numeric(res_pearson),
Deviance = as.numeric(res_deviance),
Prob_Ajustada = fitted(modelo_logit_final)
) %>%
dplyr::filter(Cook > umbral_cook | abs(Pearson) > 2)
#############################
# 18. GRÁFICOS DE DIAGNÓSTICO
#############################
# 18.1 Panel Pearson + Cook
png(
filename = file.path(dir_graficos, "diagnostico_modelo_logistico_final.png"),
width = 1400,
height = 700,
res = 140
)
par(mfrow = c(1, 2))
plot(
res_pearson,
pch = 16,
main = "Residuos de Pearson",
xlab = "Índice",
ylab = "Residuo"
)
abline(h = c(-2, 2), col = "red", lty = 2)
plot(
cook_d,
pch = 16,
main = "Distancia de Cook",
xlab = "Índice",
ylab = "Cook"
)
abline(h = umbral_cook, col = "red", lty = 2)
dev.off()
## png
## 2
# 18.2 Distancia de Cook
png(
filename = file.path(dir_graficos, "distancia_cook.png"),
width = 1200,
height = 800,
res = 140
)
barplot(
cook_d,
border = NA,
main = "Distancia de Cook",
xlab = "Observación",
ylab = "Cook's distance",
col = "gray40"
)
abline(h = umbral_cook, col = "red", lty = 2)
dev.off()
## png
## 2
# 18.3 Leverage vs residuos de deviance
graf_leverage <- ggplot(
data.frame(
Leverage = leverage,
Residuo_Deviance = res_deviance,
Cook = cook_d
),
aes(x = Leverage, y = Residuo_Deviance, size = Cook)
) +
geom_point(shape = 21, fill = "gray40", color = "black", alpha = 0.8) +
geom_hline(yintercept = c(-2, 2), color = "red", linetype = 2, linewidth = 0.4) +
geom_vline(xintercept = 2 * mean(leverage), color = "blue", linetype = 2, linewidth = 0.4) +
labs(
title = "Leverage vs residuos de deviance",
x = "Leverage",
y = "Residuo de deviance",
size = "Cook"
) +
theme_minimal()
ggsave(
filename = file.path(dir_graficos, "leverage_residuos.png"),
plot = graf_leverage,
width = 10,
height = 7,
dpi = 300
)
#############################
# 19. CURVA ROC DEL MODELO FINAL
#############################
roc_df <- data.frame(
Specificity = rev(roc_test$specificities),
Sensitivity = rev(roc_test$sensitivities)
)
graf_roc <- ggplot(roc_df, aes(x = 1 - Specificity, y = Sensitivity)) +
geom_line(linewidth = 1, color = "black") +
geom_abline(slope = 1, intercept = 0, linetype = 2, color = "gray60") +
labs(
title = paste0("Curva ROC - Modelo logístico final (AUC = ", round(auc_test, 3), ")"),
x = "1 - Especificidad",
y = "Sensibilidad"
) +
theme_minimal()
ggsave(
filename = file.path(dir_graficos, "roc_modelo_logistico_final.png"),
plot = graf_roc,
width = 10,
height = 7,
dpi = 300
)
#############################
# 20. TRADE-OFF SENSIBILIDAD / ESPECIFICIDAD SEGÚN UMBRAL
#############################
tabla_umbrales_larga <- tabla_umbrales %>%
dplyr::select(Umbral, Sensitivity, Specificity) %>%
tidyr::pivot_longer(
cols = c(Sensitivity, Specificity),
names_to = "Metrica",
values_to = "Valor"
)
umbral_youden <- mejor_youden$Umbral[1]
graf_tradeoff_lineas <- ggplot(tabla_umbrales_larga, aes(x = Umbral, y = Valor, color = Metrica)) +
geom_line(linewidth = 1.3) +
geom_vline(xintercept = 0.50, linetype = 2, linewidth = 0.7) +
geom_vline(xintercept = umbral_youden, linetype = 3, linewidth = 0.7) +
annotate("text", x = 0.50, y = 0.05, label = "0.50", angle = 90, vjust = -0.3) +
annotate("text", x = umbral_youden, y = 0.05, label = "Youden", angle = 90, vjust = -0.3) +
labs(
title = "Trade-off entre sensibilidad y especificidad",
subtitle = "Evaluación del efecto del punto de corte sobre el desempeño del modelo logístico",
x = "Umbral de clasificación",
y = "Valor de la métrica",
color = "Métrica"
) +
theme_minimal()
ggsave(
filename = file.path(dir_graficos, "tradeoff_sens_specificity_modelo_final.png"),
plot = graf_tradeoff_lineas,
width = 12,
height = 7,
dpi = 300
)
# Scatter trade-off por umbral
graf_tradeoff_scatter <- ggplot(
tabla_umbrales,
aes(
x = Specificity,
y = Sensitivity,
color = Umbral,
size = Accuracy
)
) +
geom_point(alpha = 0.9) +
geom_point(
data = subset(tabla_umbrales, abs(Umbral - 0.50) < 1e-9),
shape = 21,
fill = "white",
color = "black",
stroke = 1.2,
size = 5
) +
labs(
title = "Trade-off entre sensibilidad y especificidad según el umbral",
subtitle = "Cada punto representa un punto de corte distinto",
x = "Especificidad",
y = "Sensibilidad",
color = "Umbral",
size = "Accuracy"
) +
theme_minimal()
ggsave(
filename = file.path(dir_graficos, "scatter_tradeoff_umbral_modelo_final.png"),
plot = graf_tradeoff_scatter,
width = 12,
height = 7,
dpi = 300
)
#############################
# 21. PERFILES DE PREDICCIÓN
#############################
# Esta sección genera perfiles de bajo y alto riesgo de forma automática
# basándose en el signo de los coeficientes del modelo final.
# Si deseas perfiles institucionales más específicos, puedes editar manualmente
# los valores del data.frame perfil_bajo_manual y perfil_alto_manual.
obtener_perfiles_riesgo <- function(modelo, data_train) {
vars_finales <- all.vars(formula(modelo))[-1]
coefs <- coef(modelo)
resumen_perfiles <- list()
perfil_bajo <- list()
perfil_alto <- list()
for (v in vars_finales) {
if (is.factor(data_train[[v]])) {
niveles <- levels(data_train[[v]])
ref <- niveles[1]
coef_names <- names(coefs)[grepl(paste0("^", v), names(coefs))]
coef_vals <- coefs[coef_names]
niveles_no_ref <- sub(paste0("^", v), "", coef_names)
nivel_bajo <- ref
nivel_alto <- ref
if (length(coef_vals) > 0) {
if (min(coef_vals, na.rm = TRUE) < 0) {
nivel_bajo <- niveles_no_ref[which.min(coef_vals)]
}
if (max(coef_vals, na.rm = TRUE) > 0) {
nivel_alto <- niveles_no_ref[which.max(coef_vals)]
}
}
perfil_bajo[[v]] <- factor(nivel_bajo, levels = niveles)
perfil_alto[[v]] <- factor(nivel_alto, levels = niveles)
resumen_perfiles[[v]] <- data.frame(
Variable = v,
Referencia = ref,
Perfil_Bajo = nivel_bajo,
Perfil_Alto = nivel_alto,
stringsAsFactors = FALSE
)
} else if (is.numeric(data_train[[v]])) {
q1 <- as.numeric(quantile(data_train[[v]], 0.25, na.rm = TRUE))
q3 <- as.numeric(quantile(data_train[[v]], 0.75, na.rm = TRUE))
beta <- ifelse(v %in% names(coefs), coefs[v], 0)
if (beta >= 0) {
perfil_bajo[[v]] <- q1
perfil_alto[[v]] <- q3
} else {
perfil_bajo[[v]] <- q3
perfil_alto[[v]] <- q1
}
resumen_perfiles[[v]] <- data.frame(
Variable = v,
Referencia = "No aplica",
Perfil_Bajo = perfil_bajo[[v]],
Perfil_Alto = perfil_alto[[v]],
stringsAsFactors = FALSE
)
}
}
list(
tabla = do.call(rbind, resumen_perfiles),
perfil_bajo = as.data.frame(perfil_bajo, stringsAsFactors = FALSE),
perfil_alto = as.data.frame(perfil_alto, stringsAsFactors = FALSE)
)
}
perfiles_auto <- obtener_perfiles_riesgo(modelo_logit_final, train_model)
prob_bajo <- predict(modelo_logit_final, newdata = perfiles_auto$perfil_bajo, type = "response")
prob_alto <- predict(modelo_logit_final, newdata = perfiles_auto$perfil_alto, type = "response")
tabla_predicciones <- perfiles_auto$tabla
tabla_predicciones$Probabilidad_Desercion_Perfil_Bajo <- c(rep(NA, nrow(tabla_predicciones) - 1), prob_bajo)
tabla_predicciones$Probabilidad_Desercion_Perfil_Alto <- c(rep(NA, nrow(tabla_predicciones) - 1), prob_alto)
#############################
# 22. TABLAS RESUMEN PARA LA TESIS
#############################
tabla_resumen_modelo_final <- data.frame(
Umbral = 0.50,
AUC_Train = auc_train,
AUC_Test = auc_test,
Accuracy = tabla_metricas_test_050$Accuracy,
Kappa = tabla_metricas_test_050$Kappa,
Sensitivity = tabla_metricas_test_050$Sensitivity,
Specificity = tabla_metricas_test_050$Specificity,
Precision = tabla_metricas_test_050$Precision,
F1 = tabla_metricas_test_050$F1,
Balanced_Accuracy = tabla_metricas_test_050$Balanced_Accuracy
)
tabla_supuestos_modelo_logit <- data.frame(
Criterio = c(
"Multicolinealidad (GVIF/VIF)",
"Linealidad en el logit para EDAD",
"Hosmer-Lemeshow",
"Residuos de Pearson",
"Distancias de Cook"
),
Resultado = c(
"Sin evidencia de multicolinealidad severa",
ifelse(is.null(tabla_box_tidwell), "No evaluado",
ifelse(any(grepl("I\\(EDAD \\* log\\(EDAD\\)\\)", tabla_box_tidwell$Variable)) &&
tabla_box_tidwell$p_value[grepl("I\\(EDAD \\* log\\(EDAD\\)\\)", tabla_box_tidwell$Variable)][1] > 0.05,
"Linealidad razonable", "Revisar posible no linealidad")),
paste0("Chi-cuadrado = ", round(tabla_hl$Chi_square, 4),
"; gl = ", tabla_hl$gl,
"; p = ", round(tabla_hl$p_value, 4)),
paste0("Máx = ", round(tabla_residuos_pearson$Max, 4)),
paste0("Máx = ", round(tabla_cook$Max, 4),
"; 4/n = ", round(umbral_cook, 4),
"; casos > 4/n = ", nrow(casos_influyentes %>% dplyr::filter(Cook > umbral_cook)))
),
Conclusion = c(
"Cumple",
ifelse(is.null(tabla_box_tidwell), "No evaluado",
ifelse(any(grepl("I\\(EDAD \\* log\\(EDAD\\)\\)", tabla_box_tidwell$Variable)) &&
tabla_box_tidwell$p_value[grepl("I\\(EDAD \\* log\\(EDAD\\)\\)", tabla_box_tidwell$Variable)][1] > 0.05,
"Cumple", "Revisar")),
ifelse(tabla_hl$p_value > 0.05, "Buen ajuste", "Revisar"),
"Sin patrón general de mal ajuste",
"Sin influencia extrema global"
),
stringsAsFactors = FALSE
)
#############################
# 23. EXPORTAR A EXCEL
#############################
wb <- openxlsx::createWorkbook()
openxlsx::addWorksheet(wb, "Stepwise")
openxlsx::writeData(wb, "Stepwise", tabla_stepwise_resumen)
openxlsx::addWorksheet(wb, "Wald_IC95")
openxlsx::writeData(wb, "Wald_IC95", tabla_wald)
openxlsx::addWorksheet(wb, "Odds_Ratio")
openxlsx::writeData(wb, "Odds_Ratio", tabla_or)
openxlsx::addWorksheet(wb, "Resumen_Modelo_Final")
openxlsx::writeData(wb, "Resumen_Modelo_Final", tabla_resumen_modelo_final)
openxlsx::addWorksheet(wb, "Matriz_Confusion_050")
openxlsx::writeData(wb, "Matriz_Confusion_050", as.data.frame(matriz_confusion_050$table))
openxlsx::addWorksheet(wb, "LR_Test")
openxlsx::writeData(wb, "LR_Test", tabla_lr)
openxlsx::addWorksheet(wb, "Hosmer_Lemeshow")
openxlsx::writeData(wb, "Hosmer_Lemeshow", tabla_hl)
openxlsx::addWorksheet(wb, "AUC_Train_Test")
openxlsx::writeData(wb, "AUC_Train_Test", tabla_auc)
openxlsx::addWorksheet(wb, "Decision_Umbral")
openxlsx::writeData(wb, "Decision_Umbral", tabla_decision_umbral)
openxlsx::addWorksheet(wb, "VIF_GVIF")
openxlsx::writeData(wb, "VIF_GVIF", tabla_vif)
if (!is.null(tabla_box_tidwell)) {
openxlsx::addWorksheet(wb, "Box_Tidwell")
openxlsx::writeData(wb, "Box_Tidwell", tabla_box_tidwell)
}
openxlsx::addWorksheet(wb, "Pearson")
openxlsx::writeData(wb, "Pearson", tabla_residuos_pearson)
openxlsx::addWorksheet(wb, "Cook")
openxlsx::writeData(wb, "Cook", tabla_cook)
openxlsx::addWorksheet(wb, "Casos_Influyentes")
openxlsx::writeData(wb, "Casos_Influyentes", casos_influyentes)
openxlsx::addWorksheet(wb, "Predicciones")
openxlsx::writeData(wb, "Predicciones", tabla_predicciones)
openxlsx::addWorksheet(wb, "Supuestos")
openxlsx::writeData(wb, "Supuestos", tabla_supuestos_modelo_logit)
openxlsx::saveWorkbook(
wb,
file = file.path(dir_resultados, "modelo_logistico_final_resultados.xlsx"),
overwrite = TRUE
)
#############################
# 24. EXPORTAR CSV
#############################
write.csv(tabla_stepwise_resumen, file.path(dir_resultados, "tabla_stepwise_resumen.csv"), row.names = FALSE)
write.csv(tabla_wald, file.path(dir_resultados, "tabla_wald_modelo_final.csv"), row.names = FALSE)
write.csv(tabla_or, file.path(dir_resultados, "tabla_or_modelo_final.csv"), row.names = FALSE)
write.csv(tabla_resumen_modelo_final, file.path(dir_resultados, "tabla_resumen_modelo_final.csv"), row.names = FALSE)
write.csv(tabla_lr, file.path(dir_resultados, "tabla_lr_modelo_final.csv"), row.names = FALSE)
write.csv(tabla_hl, file.path(dir_resultados, "tabla_hosmer_lemeshow_modelo_final.csv"), row.names = FALSE)
write.csv(tabla_auc, file.path(dir_resultados, "tabla_auc_train_test_logit.csv"), row.names = FALSE)
write.csv(tabla_decision_umbral, file.path(dir_resultados, "tabla_decision_umbral_logit.csv"), row.names = FALSE)
write.csv(tabla_vif, file.path(dir_resultados, "tabla_vif_modelo_logistico_final.csv"), row.names = FALSE)
if (!is.null(tabla_box_tidwell)) {
write.csv(tabla_box_tidwell, file.path(dir_resultados, "tabla_box_tidwell_edad.csv"), row.names = FALSE)
}
write.csv(tabla_residuos_pearson, file.path(dir_resultados, "tabla_residuos_pearson.csv"), row.names = FALSE)
write.csv(tabla_cook, file.path(dir_resultados, "tabla_distancia_cook.csv"), row.names = FALSE)
write.csv(casos_influyentes, file.path(dir_resultados, "casos_influyentes_modelo_logit.csv"), row.names = FALSE)
write.csv(tabla_predicciones, file.path(dir_resultados, "tabla_predicciones_perfiles.csv"), row.names = FALSE)
write.csv(tabla_supuestos_modelo_logit, file.path(dir_resultados, "tabla_supuestos_modelo_logit.csv"), row.names = FALSE)
#############################
# 25. MENSAJES FINALES
#############################
cat("\n=========================================\n")
##
## =========================================
cat("PROCESO FINALIZADO CORRECTAMENTE\n")
## PROCESO FINALIZADO CORRECTAMENTE
cat("=========================================\n")
## =========================================
cat("Modelo final:\n")
## Modelo final:
print(formula(modelo_logit_final))
## Y_LOGIT ~ CARRERA + TIPO_INGRESO + RENDIMIENTO + TRABAJA
cat("\nAUC train:", round(auc_train, 4), "\n")
##
## AUC train: 0.827
cat("AUC test :", round(auc_test, 4), "\n")
## AUC test : 0.7888
cat("Hosmer-Lemeshow p-value:", round(tabla_hl$p_value, 4), "\n")
## Hosmer-Lemeshow p-value: 0.5419
cat("Umbral óptimo Youden:", round(mejor_youden$Umbral[1], 2), "\n")
## Umbral óptimo Youden: 0.75
cat("Resultados guardados en:", dir_resultados, "\n")
## Resultados guardados en: resultados_modelo_logistico_final
cat("Gráficos guardados en:", dir_graficos, "\n")
## Gráficos guardados en: graficos_modelo_logistico_final
cat("=========================================\n")
## =========================================
############################################################
# 16. FIGURA SCATTER CORREGIDA: COMPARACIÓN INTEGRADA
# Sensibilidad, especificidad y AUC en conjunto de prueba
############################################################
library(dplyr)
library(ggplot2)
library(ggrepel)
library(scales)
# 16.1 Preparar datos del gráfico
datos_scatter <- tabla_metricas_test %>%
mutate(
Modelo = factor(
Modelo,
levels = c("NB", "Tree", "KNN", "SVM", "NN", "Logit", "RF")
),
Tipo = case_when(
Modelo %in% c("NB", "SVM") ~ "Sesgado",
Modelo %in% c("Tree", "KNN") ~ "Intermedio",
Modelo %in% c("NN", "Logit", "RF") ~ "Óptimo",
TRUE ~ "Intermedio"
),
Tipo = factor(Tipo, levels = c("Sesgado", "Intermedio", "Óptimo"))
)
# 16.2 Diagnóstico de valores faltantes
cat("\n=============================\n")
##
## =============================
cat("DIAGNÓSTICO PARA FIGURA SCATTER\n")
## DIAGNÓSTICO PARA FIGURA SCATTER
cat("=============================\n")
## =============================
print(datos_scatter %>% select(Modelo, Specificity, Sensitivity, AUC, Tipo))
## Modelo Specificity Sensitivity AUC Tipo
## Logit Logit 0.5758 0.7883 0.7796 Óptimo
## Tree Tree 0.6667 0.7664 0.7853 Intermedio
## RF RF 0.2121 0.9635 0.7920 Óptimo
## SVM SVM 0.2879 0.9270 0.7773 Sesgado
## KNN KNN 0.2424 0.8905 0.7131 Intermedio
## NN NN 0.4848 0.7956 0.7460 Óptimo
## NB NB 0.6970 0.7445 0.7890 Sesgado
modelos_con_na <- datos_scatter %>%
filter(is.na(Specificity) | is.na(Sensitivity) | is.na(AUC))
if (nrow(modelos_con_na) > 0) {
cat("\nATENCIÓN: modelos con métricas NA:\n")
print(modelos_con_na)
}
# 16.3 Usar solo modelos con métricas completas para graficar
datos_scatter_plot <- datos_scatter %>%
filter(
!is.na(Specificity),
!is.na(Sensitivity),
!is.na(AUC)
)
# 16.4 Colores solicitados
colores_tipo <- c(
"Sesgado" = "#D7191C",
"Intermedio" = "#FFD700",
"Óptimo" = "#1A9641"
)
# 16.5 Límites dinámicos para evitar pérdida de nodos
x_min <- max(0, min(datos_scatter_plot$Specificity, na.rm = TRUE) - 0.08)
x_max <- min(1, max(datos_scatter_plot$Specificity, na.rm = TRUE) + 0.08)
y_min <- max(0, min(datos_scatter_plot$Sensitivity, na.rm = TRUE) - 0.06)
y_max <- min(1, max(datos_scatter_plot$Sensitivity, na.rm = TRUE) + 0.06)
# 16.6 Gráfico corregido
Figura_scatter_modelo_color <- ggplot(
datos_scatter_plot,
aes(
x = Specificity,
y = Sensitivity
)
) +
geom_point(
aes(size = AUC, fill = Tipo),
shape = 21,
color = "black",
stroke = 0.7,
alpha = 0.92
) +
ggrepel::geom_text_repel(
aes(label = Modelo, color = Tipo),
size = 5.4,
fontface = "bold",
box.padding = 0.50,
point.padding = 0.35,
segment.color = "grey55",
segment.linewidth = 0.35,
max.overlaps = Inf,
show.legend = FALSE
) +
scale_fill_manual(
values = colores_tipo,
name = "Tipo de comportamiento"
) +
scale_color_manual(
values = colores_tipo,
guide = "none"
) +
scale_size_continuous(
name = "AUC",
range = c(7, 15),
labels = number_format(accuracy = 0.01)
) +
scale_x_continuous(
breaks = seq(0, 1, by = 0.10),
labels = number_format(accuracy = 0.01)
) +
scale_y_continuous(
breaks = seq(0, 1, by = 0.05),
labels = number_format(accuracy = 0.01)
) +
coord_cartesian(
xlim = c(x_min, x_max),
ylim = c(y_min, y_max),
clip = "off"
) +
labs(
title = "Comparación integrada del desempeño de los modelos",
subtitle = "Sensibilidad, especificidad y AUC en el conjunto de prueba",
x = "Especificidad",
y = "Sensibilidad"
) +
theme_minimal(base_size = 15) +
theme(
plot.title = element_text(face = "bold", hjust = 0.5, size = 22),
plot.subtitle = element_text(hjust = 0.5, size = 15),
axis.title = element_text(face = "bold", size = 17),
axis.text = element_text(size = 13),
legend.position = "right",
legend.title = element_text(face = "bold", size = 13),
legend.text = element_text(size = 12),
panel.grid.minor = element_blank(),
panel.grid.major = element_line(linewidth = 0.30, colour = "grey85"),
plot.margin = ggplot2::margin(15, 35, 15, 15)
) +
guides(
fill = guide_legend(
order = 1,
override.aes = list(
shape = 21,
size = 5,
colour = "black",
alpha = 1
)
),
size = guide_legend(order = 2)
)
## Warning in ggrepel::geom_text_repel(aes(label = Modelo, color = Tipo), size =
## 5.4, : Ignoring unknown parameters: `segment.linewidth`
print(Figura_scatter_modelo_color)
ggsave(
filename = "salidas_tesis/Figura_scatter_modelo_color_corregida.png",
plot = Figura_scatter_modelo_color,
width = 12.5,
height = 7.5,
dpi = 320
)
############################################################
# RESUMEN DESCRIPTIVO DE LA VARIABLE EDAD
############################################################
# Librería necesaria
library(readxl)
# Ajustar esta ruta según tu equipo
ruta_datos <- "desercion.xlsx"
# Importar base de datos
tesis <- read_excel(ruta_datos)
tesis <- as.data.frame(tesis, stringsAsFactors = FALSE)
# Seleccionar variable EDAD
edad <- tesis$EDAD
# Construcción de tabla descriptiva
tabla_edad <- data.frame(
Variables = "Edad",
`Número de Casos` = sum(!is.na(edad)),
Minimo = min(edad, na.rm = TRUE),
Q1 = quantile(edad, 0.25, na.rm = TRUE),
Mediana = median(edad, na.rm = TRUE),
Media = mean(edad, na.rm = TRUE),
Q3 = quantile(edad, 0.75, na.rm = TRUE),
Maximo = max(edad, na.rm = TRUE)
)
# Redondear resultados
tabla_edad[, -1] <- round(tabla_edad[, -1], 2)
# Mostrar tabla
print(tabla_edad)
## Variables Número.de.Casos Minimo Q1 Mediana Media Q3 Maximo
## 25% Edad 682 18 20 23 27.11 32 70
############################################################
# TABLA 4.10
# Comparación Train vs Test
############################################################
library(dplyr)
library(tidyr)
library(openxlsx)
############################################################
# FUNCIÓN AUXILIAR
############################################################
comparar_variable <- function(var, train_data, test_data){
# Variable numérica
if(is.numeric(train_data[[var]])){
salida <- data.frame(
Caracteristica = c(
paste0(var, " (media)"),
paste0(var, " (mediana)"),
paste0(var, " (DE)"),
paste0(var, " (mínimo)"),
paste0(var, " (máximo)")
),
Base_Entrenamiento = c(
round(mean(train_data[[var]], na.rm = TRUE), 2),
round(median(train_data[[var]], na.rm = TRUE), 2),
round(sd(train_data[[var]], na.rm = TRUE), 2),
round(min(train_data[[var]], na.rm = TRUE), 2),
round(max(train_data[[var]], na.rm = TRUE), 2)
),
Base_Prueba = c(
round(mean(test_data[[var]], na.rm = TRUE), 2),
round(median(test_data[[var]], na.rm = TRUE), 2),
round(sd(test_data[[var]], na.rm = TRUE), 2),
round(min(test_data[[var]], na.rm = TRUE), 2),
round(max(test_data[[var]], na.rm = TRUE), 2)
),
Observacion = c(
"Valores similares",
"Consistencia en tendencia central",
"Valores similares",
"",
""
)
)
return(salida)
}
# Variable categórica
else{
train_tab <- prop.table(table(train_data[[var]]))*100
test_tab <- prop.table(table(test_data[[var]]))*100
niveles <- union(names(train_tab), names(test_tab))
salida <- data.frame()
for(niv in niveles){
n_train <- sum(train_data[[var]] == niv, na.rm = TRUE)
p_train <- round(train_tab[niv], 2)
n_test <- sum(test_data[[var]] == niv, na.rm = TRUE)
p_test <- round(test_tab[niv], 2)
diferencia <- abs(p_train - p_test)
observacion <- case_when(
diferencia < 2 ~ "Distribución prácticamente idéntica",
diferencia < 5 ~ "Proporción consistente",
diferencia < 10 ~ "Diferencia leve",
TRUE ~ "Diferencia moderada"
)
fila <- data.frame(
Caracteristica = paste0(var, " (", niv, ")"),
Base_Entrenamiento =
paste0(n_train, " (", p_train, "%)"),
Base_Prueba =
paste0(n_test, " (", p_test, "%)"),
Observacion = observacion
)
salida <- rbind(salida, fila)
}
return(salida)
}
}
############################################################
# VARIABLES A ANALIZAR
############################################################
variables_modelo <- c(
"ESTADO_ACADEMICO",
"EDAD",
"SEXO",
"RESIDENCIA",
"EST_CIVIL",
"CARRERA",
"TIPO_INGRESO",
"MODALIDAD",
"RENDIMIENTO",
"TIPO_COL",
"TRABAJA",
"SOLVENTAR",
"ESTUDIOS_PADRES",
"INGRESO",
"NIVEL_SOCIO"
)
############################################################
# TABLA GENERAL
############################################################
tabla_410 <- data.frame()
############################################################
# RESUMEN GENERAL
############################################################
resumen_general <- data.frame(
Caracteristica = c(
"Tamaño de muestra",
"Desertores",
"No desertores"
),
Base_Entrenamiento = c(
paste0(
round(nrow(train_data)/(nrow(train_data)+nrow(test_data))*100,2),
"% (",
nrow(train_data),
" obs.)"
),
paste0(
round(mean(train_data$ESTADO_ACADEMICO=="DESERTOR")*100,2),
"%"
),
paste0(
round(mean(train_data$ESTADO_ACADEMICO=="NO_DESERTOR")*100,2),
"%"
)
),
Base_Prueba = c(
paste0(
round(nrow(test_data)/(nrow(train_data)+nrow(test_data))*100,2),
"% (",
nrow(test_data),
" obs.)"
),
paste0(
round(mean(test_data$ESTADO_ACADEMICO=="DESERTOR")*100,2),
"%"
),
paste0(
round(mean(test_data$ESTADO_ACADEMICO=="NO_DESERTOR")*100,2),
"%"
)
),
Observacion = c(
"Proporción definida en la partición",
"Distribución prácticamente idéntica",
"Distribución prácticamente idéntica"
)
)
tabla_410 <- rbind(tabla_410, resumen_general)
############################################################
# VARIABLES RESTANTES
############################################################
for(v in variables_modelo[-1]){
temp <- comparar_variable(v, train_data, test_data)
tabla_410 <- rbind(tabla_410, temp)
}
############################################################
# VISUALIZAR
############################################################
print(tabla_410)
## Caracteristica Base_Entrenamiento
## 1 Tamaño de muestra 70.23% (479 obs.)
## 2 Desertores 0%
## 3 No desertores 0%
## 4 EDAD (media) 26.84
## 5 EDAD (mediana) 23
## 6 EDAD (DE) 9.03
## 7 EDAD (mínimo) 18
## 8 EDAD (máximo) 70
## 9 SEXO (FEMENINO) 281 (58.66%)
## 10 SEXO (MASCULINO) 198 (41.34%)
## 11 RESIDENCIA (ASUNCIÓN) 159 (33.19%)
## 12 RESIDENCIA (CENTRAL) 240 (50.1%)
## 13 RESIDENCIA (RESTO DEL PAÍS) 80 (16.7%)
## 14 EST_CIVIL (Soltero/a) 385 (80.38%)
## 15 EST_CIVIL (Casado/a) 79 (16.49%)
## 16 EST_CIVIL (Divorciado/a) 11 (2.3%)
## 17 EST_CIVIL (Otro) 4 (0.84%)
## 18 CARRERA (ESTADISTICA-PRES) 130 (27.14%)
## 19 CARRERA (ESTADISTICA-SEMI) 73 (15.24%)
## 20 CARRERA (MATEMATICA) 106 (22.13%)
## 21 CARRERA (EDUCACION MATEMATICA-PRES) 48 (10.02%)
## 22 CARRERA (EDUCACION MATEMATICA-SEMI) 122 (25.47%)
## 23 TIPO_INGRESO (INGRESO) 454 (94.78%)
## 24 TIPO_INGRESO (TRASLADO) 0 (0%)
## 25 TIPO_INGRESO (ADMISION DIRECTA) 25 (5.22%)
## 26 MODALIDAD (Presencial) 284 (59.29%)
## 27 MODALIDAD (Semipresencial) 195 (40.71%)
## 28 RENDIMIENTO (APROBADO) 285 (59.5%)
## 29 RENDIMIENTO (NO APROBADO) 194 (40.5%)
## 30 TIPO_COL (Público) 320 (66.81%)
## 31 TIPO_COL (Subvencionado) 39 (8.14%)
## 32 TIPO_COL (Privado) 120 (25.05%)
## 33 TRABAJA (SI) 261 (54.49%)
## 34 TRABAJA (NO) 218 (45.51%)
## 35 SOLVENTAR (Beca/exoneración total) 28 (5.85%)
## 36 SOLVENTAR (Beca/exoneración parcial) 35 (7.31%)
## 37 SOLVENTAR (Trabajo Personal) 242 (50.52%)
## 38 SOLVENTAR (Ayuda Familiar) 174 (36.33%)
## 39 ESTUDIOS_PADRES (hasta 13 años) 103 (21.5%)
## 40 ESTUDIOS_PADRES (14-23 años) 154 (32.15%)
## 41 ESTUDIOS_PADRES (24-29 años) 111 (23.17%)
## 42 ESTUDIOS_PADRES (30-34 años) 75 (15.66%)
## 43 ESTUDIOS_PADRES (Más de 34 años) 36 (7.52%)
## 44 INGRESO (Hasta dos salarios mínimos) 300 (62.63%)
## 45 INGRESO (Más de dos y hasta cinco salarios mínimos) 138 (28.81%)
## 46 INGRESO (Más de cinco y hasta diez salarios mínimos) 29 (6.05%)
## 47 INGRESO (Más de diez y hasta quince salarios mínimos) 8 (1.67%)
## 48 INGRESO (Más de quince salarios mínimos) 4 (0.84%)
## 49 NIVEL_SOCIO (BAJO) 41 (8.56%)
## 50 NIVEL_SOCIO (MEDIO) 438 (91.44%)
## Base_Prueba Observacion
## 1 29.77% (203 obs.) Proporción definida en la partición
## 2 0% Distribución prácticamente idéntica
## 3 0% Distribución prácticamente idéntica
## 4 27.74 Valores similares
## 5 24 Consistencia en tendencia central
## 6 9.47 Valores similares
## 7 18
## 8 61
## 9 114 (56.16%) Proporción consistente
## 10 89 (43.84%) Proporción consistente
## 11 70 (34.48%) Distribución prácticamente idéntica
## 12 96 (47.29%) Proporción consistente
## 13 37 (18.23%) Distribución prácticamente idéntica
## 14 159 (78.33%) Proporción consistente
## 15 38 (18.72%) Proporción consistente
## 16 3 (1.48%) Distribución prácticamente idéntica
## 17 3 (1.48%) Distribución prácticamente idéntica
## 18 52 (25.62%) Distribución prácticamente idéntica
## 19 27 (13.3%) Distribución prácticamente idéntica
## 20 44 (21.67%) Distribución prácticamente idéntica
## 21 22 (10.84%) Distribución prácticamente idéntica
## 22 58 (28.57%) Proporción consistente
## 23 188 (92.61%) Proporción consistente
## 24 0 (0%) Distribución prácticamente idéntica
## 25 15 (7.39%) Proporción consistente
## 26 118 (58.13%) Distribución prácticamente idéntica
## 27 85 (41.87%) Distribución prácticamente idéntica
## 28 124 (61.08%) Distribución prácticamente idéntica
## 29 79 (38.92%) Distribución prácticamente idéntica
## 30 145 (71.43%) Proporción consistente
## 31 10 (4.93%) Proporción consistente
## 32 48 (23.65%) Distribución prácticamente idéntica
## 33 111 (54.68%) Distribución prácticamente idéntica
## 34 92 (45.32%) Distribución prácticamente idéntica
## 35 17 (8.37%) Proporción consistente
## 36 14 (6.9%) Distribución prácticamente idéntica
## 37 104 (51.23%) Distribución prácticamente idéntica
## 38 68 (33.5%) Proporción consistente
## 39 54 (26.6%) Diferencia leve
## 40 61 (30.05%) Proporción consistente
## 41 53 (26.11%) Proporción consistente
## 42 26 (12.81%) Proporción consistente
## 43 9 (4.43%) Proporción consistente
## 44 130 (64.04%) Distribución prácticamente idéntica
## 45 52 (25.62%) Proporción consistente
## 46 16 (7.88%) Distribución prácticamente idéntica
## 47 4 (1.97%) Distribución prácticamente idéntica
## 48 1 (0.49%) Distribución prácticamente idéntica
## 49 24 (11.82%) Proporción consistente
## 50 179 (88.18%) Proporción consistente
############################################################
# EXPORTAR
############################################################
if(!dir.exists("salidas_tesis")){
dir.create("salidas_tesis")
}
write.csv(
tabla_410,
"salidas_tesis/Tabla_4_10_Train_vs_Test.csv",
row.names = FALSE
)
############################################################
# EXPORTAR A EXCEL
############################################################
wb <- createWorkbook()
addWorksheet(wb, "Tabla_4_10")
writeData(
wb,
sheet = "Tabla_4_10",
x = tabla_410
)
saveWorkbook(
wb,
"salidas_tesis/Tabla_4_10_Train_vs_Test.xlsx",
overwrite = TRUE
)
cat("\n====================================\n")
##
## ====================================
cat("TABLA 4.10 GENERADA CORRECTAMENTE\n")
## TABLA 4.10 GENERADA CORRECTAMENTE
cat("====================================\n")
## ====================================
############################################################
# INSTALAR PAQUETES NECESARIOS
############################################################
paquetes <- c(
"partykit",
"gridExtra",
"RColorBrewer",
"rpart.plot"
)
instalar <- paquetes[!(paquetes %in% installed.packages()[, "Package"])]
if(length(instalar) > 0){
install.packages(instalar)
}
############################################################
# CARGAR LIBRERÍAS
############################################################
library(rpart)
library(partykit)
## Cargando paquete requerido: libcoin
## Cargando paquete requerido: mvtnorm
##
## Adjuntando el paquete: 'partykit'
## The following object is masked from 'package:flextable':
##
## width
library(grid)
library(gridExtra)
library(RColorBrewer)
library(rpart.plot)
############################################################
# ÁRBOL DE DECISIÓN FINAL - FORMATO ACADÉMICO MEJORADO
############################################################
############################################################
# 1. LIBRERÍAS
############################################################
paquetes <- c(
"rpart",
"rpart.plot"
)
instalar <- paquetes[!(paquetes %in% installed.packages()[, "Package"])]
if(length(instalar) > 0){
install.packages(instalar)
}
library(rpart)
library(rpart.plot)
############################################################
# 2. AJUSTE DEL MODELO
############################################################
set.seed(123)
modelo_tree_final <- rpart(
ESTADO_ACADEMICO ~ .,
data = train_data,
method = "class",
parms = list(split = "gini"),
control = rpart.control(
cp = 0.01,
minsplit = 20,
maxdepth = 4
)
)
############################################################
# 3. CREAR DIRECTORIO DE SALIDA
############################################################
if(!dir.exists("salidas_tesis")){
dir.create("salidas_tesis")
}
############################################################
# 4. EXPORTAR FIGURA EN ALTA CALIDAD
############################################################
png(
filename = "salidas_tesis/Arbol_decision_final_mejorado.png",
width = 3400,
height = 2200,
res = 320
)
rpart.plot(
modelo_tree_final,
##########################################################
# ESTRUCTURA GENERAL
##########################################################
type = 2,
extra = 104,
under = TRUE,
fallen.leaves = TRUE,
compress = TRUE,
uniform = TRUE,
##########################################################
# COLORES
##########################################################
box.palette = c("#8EC5F8", "#CDE8B6"),
shadow.col = "gray85",
border.col = "gray40",
branch.col = "gray45",
branch.lwd = 1.8,
##########################################################
# TEXTO
##########################################################
faclen = 0,
varlen = 14,
cex = 0.78,
split.cex = 0.72,
##########################################################
# FORMATO DE NODOS
##########################################################
nn = FALSE,
roundint = FALSE,
##########################################################
# TÍTULOS
##########################################################
main = "Árbol de decisión final",
sub = "Clasificación de estudiantes desertores y no desertores"
)
dev.off()
## png
## 2
############################################################
# 5. MOSTRAR EN PANTALLA
############################################################
rpart.plot(
modelo_tree_final,
##########################################################
# ESTRUCTURA GENERAL
##########################################################
type = 2,
extra = 104,
under = TRUE,
fallen.leaves = TRUE,
compress = TRUE,
uniform = TRUE,
##########################################################
# COLORES
##########################################################
box.palette = c("#8EC5F8", "#CDE8B6"),
shadow.col = "gray85",
border.col = "gray40",
branch.col = "gray45",
branch.lwd = 1.8,
##########################################################
# TEXTO
##########################################################
faclen = 0,
varlen = 14,
cex = 0.78,
split.cex = 0.72,
##########################################################
# FORMATO DE NODOS
##########################################################
nn = FALSE,
roundint = FALSE,
##########################################################
# TÍTULOS
##########################################################
main = "Árbol de decisión final",
sub = "Clasificación de estudiantes desertores y no desertores"
)
############################################################
# 6. MENSAJE FINAL
############################################################
cat("\n====================================\n")
##
## ====================================
cat("ÁRBOL EXPORTADO CORRECTAMENTE\n")
## ÁRBOL EXPORTADO CORRECTAMENTE
cat("====================================\n")
## ====================================
cat("Ubicación:\n")
## Ubicación:
cat("salidas_tesis/Arbol_decision_final_mejorado.png\n")
## salidas_tesis/Arbol_decision_final_mejorado.png
cat("====================================\n")
## ====================================
############################################################
# 4.6.3 OBSERVACIONES INFLUYENTES
# DISTANCIA DE COOK
############################################################
library(ggplot2)
library(dplyr)
############################################################
# 1. DISTANCIA DE COOK
############################################################
cook <- cooks.distance(modelo_logit_final)
datos_cook <- data.frame(
Observacion = 1:length(cook),
Cook = cook
)
############################################################
# 2. UMBRAL DE REFERENCIA
############################################################
umbral_cook <- 4 / nrow(train_model)
############################################################
# 3. IDENTIFICAR OBSERVACIONES INFLUYENTES
############################################################
obs_influyentes <- datos_cook %>%
filter(Cook > umbral_cook)
cat("\n====================================\n")
##
## ====================================
cat("OBSERVACIONES POTENCIALMENTE INFLUYENTES\n")
## OBSERVACIONES POTENCIALMENTE INFLUYENTES
cat("====================================\n")
## ====================================
print(obs_influyentes)
## Observacion Cook
## 9 5 0.01023796
## 30 17 0.01023796
## 32 19 0.01023796
## 41 24 0.01023796
## 144 93 0.02632527
## 147 94 0.01023796
## 190 127 0.01023796
## 221 151 0.01023796
## 231 158 0.01023796
## 263 175 0.01023796
## 300 207 0.01923223
## 323 224 0.03298445
## 332 230 0.01447493
## 352 245 0.01447493
## 477 334 0.01403820
## 493 346 0.03126254
## 496 349 0.01403820
## 501 353 0.01953395
## 507 357 0.01933387
## 527 370 0.01447493
## 567 393 0.01271334
## 635 442 0.01536198
## 641 448 0.01953395
## 648 454 0.01271334
## 653 457 0.01953395
## 659 462 0.01953395
## 671 471 0.01447493
## 682 479 0.01403820
############################################################
# 4. GRÁFICO DISTANCIA DE COOK
############################################################
grafico_cook <- ggplot(datos_cook,
aes(x = Observacion,
y = Cook)) +
geom_col(fill = "#2c7fb8",
alpha = 0.85) +
geom_hline(yintercept = umbral_cook,
color = "red",
linewidth = 1,
linetype = "dashed") +
labs(
title = "Distancia de Cook del modelo logístico final",
subtitle = paste0(
"Línea roja: umbral de referencia = ",
round(umbral_cook, 4)
),
x = "Observaciones",
y = "Distancia de Cook"
) +
theme_minimal(base_size = 15) +
theme(
plot.title = element_text(
face = "bold",
hjust = 0.5,
size = 20
),
plot.subtitle = element_text(
hjust = 0.5,
size = 13
),
axis.title = element_text(
face = "bold",
size = 15
)
)
print(grafico_cook)
############################################################
# 5. EXPORTAR GRÁFICO
############################################################
ggsave(
filename = "salidas_tesis/grafico_distancia_cook.png",
plot = grafico_cook,
width = 11,
height = 6,
dpi = 320
)
############################################################
# 6. EXPORTAR TABLA
############################################################
write.csv(
obs_influyentes,
"salidas_tesis/observaciones_influyentes_cook.csv",
row.names = FALSE
)
############################################################
# 4.6.4 CURVA DE CALIBRACIÓN
############################################################
library(ggplot2)
library(dplyr)
############################################################
# 1. PROBABILIDADES PREDICHAS
############################################################
prob_pred <- predict(
modelo_logit_final,
newdata = test_data,
type = "response"
)
############################################################
# 2. OBTENER RESPUESTA REAL DESDE EL MODELO
############################################################
respuesta_real <- model.response(
model.frame(modelo_logit_final)
)
############################################################
# 3. AJUSTAR LONGITUD
############################################################
# SOLO TOMAR OBSERVACIONES TEST
############################################################
n_test <- length(prob_pred)
respuesta_test <- tail(
respuesta_real,
n_test
)
############################################################
# 4. CONVERTIR A BINARIA
############################################################
observado_binario <- ifelse(
as.character(respuesta_test) %in%
c("Desertor", "1"),
1,
0
)
############################################################
# 5. CREAR DATAFRAME
############################################################
datos_calibracion <- data.frame(
Observado = observado_binario,
Probabilidad = prob_pred
)
############################################################
# 6. CREAR DECILES
############################################################
datos_calibracion$Decil <- cut(
datos_calibracion$Probabilidad,
breaks = quantile(
datos_calibracion$Probabilidad,
probs = seq(0, 1, 0.10),
na.rm = TRUE
),
include.lowest = TRUE
)
############################################################
# 7. TABLA CALIBRACIÓN
############################################################
tabla_calibracion <- datos_calibracion %>%
group_by(Decil) %>%
summarise(
Probabilidad_Predicha = mean(Probabilidad),
Probabilidad_Observada = mean(Observado),
n = n(),
.groups = "drop"
)
print(tabla_calibracion)
## # A tibble: 10 × 4
## Decil Probabilidad_Predicha Probabilidad_Observada n
## <fct> <dbl> <dbl> <int>
## 1 [0.0526,0.31] 0.237 0.8 25
## 2 (0.31,0.402] 0.388 0.619 21
## 3 (0.402,0.496] 0.496 0.64 25
## 4 (0.496,0.602] 0.535 0.7 10
## 5 (0.602,0.683] 0.666 0.541 37
## 6 (0.683,0.715] 0.715 0.333 6
## 7 (0.715,0.901] 0.879 0.5 20
## 8 (0.901,0.948] 0.935 0.75 20
## 9 (0.948,0.967] 0.964 0.724 29
## 10 (0.967,0.971] 0.971 0.6 10
############################################################
# 8. GRÁFICO
############################################################
grafico_calibracion <- ggplot(
tabla_calibracion,
aes(
x = Probabilidad_Predicha,
y = Probabilidad_Observada
)
) +
geom_point(
size = 4,
color = "#2166ac"
) +
geom_line(
color = "#2166ac",
linewidth = 1
) +
geom_abline(
intercept = 0,
slope = 1,
color = "red",
linetype = "dashed",
linewidth = 1
) +
labs(
title = "Curva de calibración del modelo logístico final",
subtitle = "Probabilidades observadas vs predichas",
x = "Probabilidad predicha",
y = "Probabilidad observada"
) +
coord_equal() +
theme_minimal(base_size = 15)
print(grafico_calibracion)
############################################################
# EXPORTAR GRÁFICO DE CALIBRACIÓN
############################################################
ggsave(
filename = "salidas_tesis/curva_calibracion_modelo_logistico_final.png",
plot = grafico_calibracion,
width = 10,
height = 7,
dpi = 320
)
############################################################
# EXPORTAR EN PDF (OPCIONAL)
############################################################
ggsave(
filename = "salidas_tesis/curva_calibracion_modelo_logistico_final.pdf",
plot = grafico_calibracion,
width = 10,
height = 7
)
cat("\n====================================\n")
##
## ====================================
cat("GRÁFICO DE CALIBRACIÓN EXPORTADO\n")
## GRÁFICO DE CALIBRACIÓN EXPORTADO
cat("====================================\n")
## ====================================
cat("PNG : curva_calibracion_modelo_logistico_final.png\n")
## PNG : curva_calibracion_modelo_logistico_final.png
cat("PDF : curva_calibracion_modelo_logistico_final.pdf\n")
## PDF : curva_calibracion_modelo_logistico_final.pdf
# ==========================================
# MODELO LOGÍSTICO FINAL
# ==========================================
modelo_final <- glm(
ESTADO_ACADEMICO~ CARRERA + TIPO_INGRESO + RENDIMIENTO + TRABAJA,
data = train_data,
family = binomial(link = "logit")
)
summary(modelo_final)
##
## Call:
## glm(formula = ESTADO_ACADEMICO ~ CARRERA + TIPO_INGRESO + RENDIMIENTO +
## TRABAJA, family = binomial(link = "logit"), data = train_data)
##
## Coefficients:
## Estimate Std. Error z value
## (Intercept) -0.2346 0.2838 -0.827
## CARRERAESTADISTICA-SEMI -0.8195 0.4998 -1.640
## CARRERAMATEMATICA 0.3153 0.3108 1.014
## CARRERAEDUCACION MATEMATICA-PRES 1.0093 0.3927 2.570
## CARRERAEDUCACION MATEMATICA-SEMI -0.3861 0.3485 -1.108
## TIPO_INGRESOADMISION DIRECTA 1.8464 0.6157 2.999
## RENDIMIENTONO APROBADO -2.7083 0.3480 -7.782
## TRABAJANO 0.2546 0.2681 0.950
## Pr(>|z|)
## (Intercept) 0.40843
## CARRERAESTADISTICA-SEMI 0.10105
## CARRERAMATEMATICA 0.31036
## CARRERAEDUCACION MATEMATICA-PRES 0.01016 *
## CARRERAEDUCACION MATEMATICA-SEMI 0.26787
## TIPO_INGRESOADMISION DIRECTA 0.00271 **
## RENDIMIENTONO APROBADO 0.00000000000000714 ***
## TRABAJANO 0.34227
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 606.02 on 478 degrees of freedom
## Residual deviance: 451.52 on 471 degrees of freedom
## AIC: 467.52
##
## Number of Fisher Scoring iterations: 5
# ==========================================
# ECUACIÓN DEL MODELO
# ==========================================
coeficientes <- coef(modelo_final)
coeficientes
## (Intercept) CARRERAESTADISTICA-SEMI
## -0.2346206 -0.8194958
## CARRERAMATEMATICA CARRERAEDUCACION MATEMATICA-PRES
## 0.3153176 1.0093170
## CARRERAEDUCACION MATEMATICA-SEMI TIPO_INGRESOADMISION DIRECTA
## -0.3860986 1.8464468
## RENDIMIENTONO APROBADO TRABAJANO
## -2.7082810 0.2546231
# Mostrar ecuación logística
cat("\nECUACIÓN LOGÍSTICA:\n")
##
## ECUACIÓN LOGÍSTICA:
cat("log(p/(1-p)) = ",
round(coeficientes[1],4),
ifelse(coeficientes[2] >=0, " + ", " "),
round(coeficientes[2],4),"*Carrera_EstadisticaSemi",
ifelse(coeficientes[3] >=0, " + ", " "),
round(coeficientes[3],4),"*Carrera_Matematica",
ifelse(coeficientes[4] >=0, " + ", " "),
round(coeficientes[4],4),"*Carrera_EducMatPres",
ifelse(coeficientes[5] >=0, " + ", " "),
round(coeficientes[5],4),"*Carrera_EducMatSemi",
ifelse(coeficientes[6] >=0, " + ", " "),
round(coeficientes[6],4),"*AdmisionDirecta",
ifelse(coeficientes[7] >=0, " + ", " "),
round(coeficientes[7],4),"*NoAprobado",
ifelse(coeficientes[8] >=0, " + ", " "),
round(coeficientes[8],4),"*NoTrabaja"
)
## log(p/(1-p)) = -0.2346 -0.8195 *Carrera_EstadisticaSemi + 0.3153 *Carrera_Matematica + 1.0093 *Carrera_EducMatPres -0.3861 *Carrera_EducMatSemi + 1.8464 *AdmisionDirecta -2.7083 *NoAprobado + 0.2546 *NoTrabaja
# ==========================================
# PROBABILIDADES PREDICHAS
# ==========================================
train_data$prob_predicha <- predict(
modelo_final,
type = "response"
)
head(train_data$prob_predicha)
## [1] 0.50500045 0.06366857 0.73678393 0.44161244 0.73678393 0.06738413
library(ggplot2)
ggplot(train_data,
aes(x = prob_predicha,
fill = factor(ESTADO_ACADEMICO))) +
geom_density(alpha = 0.4) +
labs(
title = "Distribución de probabilidades predichas",
subtitle = "Modelo de regresión logística final",
x = "Probabilidad predicha de deserción",
y = "Densidad",
fill = "Estado Académico"
) +
theme_minimal()