En este paso, cargaremos los datos, revisaremos si existen datos faltantes, codificaremos variables como catégoricas si es necesario, crearemos la variable predictora a función de G3 y seleccionaremos las variables predictoras a utilizarse en el modelo.
1. Cargar datos:
# Cargar los datos:
student_mat_data <- read.csv("Student-mat.csv")
2. Explorar datos:
# Visualizar los datos:
head(student_mat_data)
## school sex age address famsize Pstatus Medu Fedu Mjob Fjob reason
## 1 GP F 18 U GT3 A 4 4 at_home teacher course
## 2 GP F 17 U GT3 T 1 1 at_home other course
## 3 GP F 15 U LE3 T 1 1 at_home other other
## 4 GP F 15 U GT3 T 4 2 health services home
## 5 GP F 16 U GT3 T 3 3 other other home
## 6 GP M 16 U LE3 T 4 3 services other reputation
## guardian traveltime studytime failures schoolsup famsup paid activities
## 1 mother 2 2 0 yes no no no
## 2 father 1 2 0 no yes no no
## 3 mother 1 2 3 yes no yes no
## 4 mother 1 3 0 no yes yes yes
## 5 father 1 2 0 no yes yes no
## 6 mother 1 2 0 no yes yes yes
## nursery higher internet romantic famrel freetime goout Dalc Walc health
## 1 yes yes no no 4 3 4 1 1 3
## 2 no yes yes no 5 3 3 1 1 3
## 3 yes yes yes no 4 3 2 2 3 3
## 4 yes yes yes yes 3 2 2 1 1 5
## 5 yes yes no no 4 3 2 1 2 5
## 6 yes yes yes no 5 4 2 1 2 5
## absences G1 G2 G3
## 1 6 5 6 6
## 2 4 5 5 6
## 3 10 7 8 10
## 4 2 15 14 15
## 5 4 6 10 10
## 6 10 15 15 15
# Observar características de las variables
str(student_mat_data)
## 'data.frame': 395 obs. of 33 variables:
## $ school : chr "GP" "GP" "GP" "GP" ...
## $ sex : chr "F" "F" "F" "F" ...
## $ age : int 18 17 15 15 16 16 16 17 15 15 ...
## $ address : chr "U" "U" "U" "U" ...
## $ famsize : chr "GT3" "GT3" "LE3" "GT3" ...
## $ Pstatus : chr "A" "T" "T" "T" ...
## $ Medu : int 4 1 1 4 3 4 2 4 3 3 ...
## $ Fedu : int 4 1 1 2 3 3 2 4 2 4 ...
## $ Mjob : chr "at_home" "at_home" "at_home" "health" ...
## $ Fjob : chr "teacher" "other" "other" "services" ...
## $ reason : chr "course" "course" "other" "home" ...
## $ guardian : chr "mother" "father" "mother" "mother" ...
## $ traveltime: int 2 1 1 1 1 1 1 2 1 1 ...
## $ studytime : int 2 2 2 3 2 2 2 2 2 2 ...
## $ failures : int 0 0 3 0 0 0 0 0 0 0 ...
## $ schoolsup : chr "yes" "no" "yes" "no" ...
## $ famsup : chr "no" "yes" "no" "yes" ...
## $ paid : chr "no" "no" "yes" "yes" ...
## $ activities: chr "no" "no" "no" "yes" ...
## $ nursery : chr "yes" "no" "yes" "yes" ...
## $ higher : chr "yes" "yes" "yes" "yes" ...
## $ internet : chr "no" "yes" "yes" "yes" ...
## $ romantic : chr "no" "no" "no" "yes" ...
## $ famrel : int 4 5 4 3 4 5 4 4 4 5 ...
## $ freetime : int 3 3 3 2 3 4 4 1 2 5 ...
## $ goout : int 4 3 2 2 2 2 4 4 2 1 ...
## $ Dalc : int 1 1 2 1 1 1 1 1 1 1 ...
## $ Walc : int 1 1 3 1 2 2 1 1 1 1 ...
## $ health : int 3 3 3 5 5 5 3 1 1 5 ...
## $ absences : int 6 4 10 2 4 10 0 6 0 0 ...
## $ G1 : int 5 5 7 15 6 15 12 6 16 14 ...
## $ G2 : int 6 5 8 14 10 15 12 5 18 15 ...
## $ G3 : int 6 6 10 15 10 15 11 6 19 15 ...
# Revisar los datos detalladamente
library(skimr)
skimr::skim(student_mat_data)
Name | student_mat_data |
Number of rows | 395 |
Number of columns | 33 |
_______________________ | |
Column type frequency: | |
character | 17 |
numeric | 16 |
________________________ | |
Group variables | None |
Variable type: character
skim_variable | n_missing | complete_rate | min | max | empty | n_unique | whitespace |
---|---|---|---|---|---|---|---|
school | 0 | 1 | 2 | 2 | 0 | 2 | 0 |
sex | 0 | 1 | 1 | 1 | 0 | 2 | 0 |
address | 0 | 1 | 1 | 1 | 0 | 2 | 0 |
famsize | 0 | 1 | 3 | 3 | 0 | 2 | 0 |
Pstatus | 0 | 1 | 1 | 1 | 0 | 2 | 0 |
Mjob | 0 | 1 | 5 | 8 | 0 | 5 | 0 |
Fjob | 0 | 1 | 5 | 8 | 0 | 5 | 0 |
reason | 0 | 1 | 4 | 10 | 0 | 4 | 0 |
guardian | 0 | 1 | 5 | 6 | 0 | 3 | 0 |
schoolsup | 0 | 1 | 2 | 3 | 0 | 2 | 0 |
famsup | 0 | 1 | 2 | 3 | 0 | 2 | 0 |
paid | 0 | 1 | 2 | 3 | 0 | 2 | 0 |
activities | 0 | 1 | 2 | 3 | 0 | 2 | 0 |
nursery | 0 | 1 | 2 | 3 | 0 | 2 | 0 |
higher | 0 | 1 | 2 | 3 | 0 | 2 | 0 |
internet | 0 | 1 | 2 | 3 | 0 | 2 | 0 |
romantic | 0 | 1 | 2 | 3 | 0 | 2 | 0 |
Variable type: numeric
skim_variable | n_missing | complete_rate | mean | sd | p0 | p25 | p50 | p75 | p100 | hist |
---|---|---|---|---|---|---|---|---|---|---|
age | 0 | 1 | 16.70 | 1.28 | 15 | 16 | 17 | 18 | 22 | ▇▅▅▁▁ |
Medu | 0 | 1 | 2.75 | 1.09 | 0 | 2 | 3 | 4 | 4 | ▁▃▆▆▇ |
Fedu | 0 | 1 | 2.52 | 1.09 | 0 | 2 | 2 | 3 | 4 | ▁▆▇▇▇ |
traveltime | 0 | 1 | 1.45 | 0.70 | 1 | 1 | 1 | 2 | 4 | ▇▃▁▁▁ |
studytime | 0 | 1 | 2.04 | 0.84 | 1 | 1 | 2 | 2 | 4 | ▅▇▁▂▁ |
failures | 0 | 1 | 0.33 | 0.74 | 0 | 0 | 0 | 0 | 3 | ▇▁▁▁▁ |
famrel | 0 | 1 | 3.94 | 0.90 | 1 | 4 | 4 | 5 | 5 | ▁▁▃▇▅ |
freetime | 0 | 1 | 3.24 | 1.00 | 1 | 3 | 3 | 4 | 5 | ▁▃▇▆▂ |
goout | 0 | 1 | 3.11 | 1.11 | 1 | 2 | 3 | 4 | 5 | ▂▆▇▅▃ |
Dalc | 0 | 1 | 1.48 | 0.89 | 1 | 1 | 1 | 2 | 5 | ▇▂▁▁▁ |
Walc | 0 | 1 | 2.29 | 1.29 | 1 | 1 | 2 | 3 | 5 | ▇▅▅▃▂ |
health | 0 | 1 | 3.55 | 1.39 | 1 | 3 | 4 | 5 | 5 | ▂▂▅▃▇ |
absences | 0 | 1 | 5.71 | 8.00 | 0 | 0 | 4 | 8 | 75 | ▇▁▁▁▁ |
G1 | 0 | 1 | 10.91 | 3.32 | 3 | 8 | 11 | 13 | 19 | ▂▇▇▆▂ |
G2 | 0 | 1 | 10.71 | 3.76 | 0 | 9 | 11 | 13 | 19 | ▁▂▇▆▂ |
G3 | 0 | 1 | 10.42 | 4.58 | 0 | 8 | 11 | 14 | 20 | ▂▃▇▅▁ |
Al inspeccionar los datos, podemos concluir que no existen datos faltantes. En el siguiente paso, vamos a recodificar las variables que necesitan ser categóricas.
3. Recodificar a factor las variables binarias
# Codificar variables binarias como tipo factor con niveles determinados
library(dplyr)
student_mat_data <- student_mat_data %>%
mutate(across(c(school,sex,address,famsize,Pstatus,Mjob,Fjob,reason,guardian,schoolsup,famsup,paid,activities,nursery,higher,internet,romantic), as.factor))
str(student_mat_data)
## 'data.frame': 395 obs. of 33 variables:
## $ school : Factor w/ 2 levels "GP","MS": 1 1 1 1 1 1 1 1 1 1 ...
## $ sex : Factor w/ 2 levels "F","M": 1 1 1 1 1 2 2 1 2 2 ...
## $ age : int 18 17 15 15 16 16 16 17 15 15 ...
## $ address : Factor w/ 2 levels "R","U": 2 2 2 2 2 2 2 2 2 2 ...
## $ famsize : Factor w/ 2 levels "GT3","LE3": 1 1 2 1 1 2 2 1 2 1 ...
## $ Pstatus : Factor w/ 2 levels "A","T": 1 2 2 2 2 2 2 1 1 2 ...
## $ Medu : int 4 1 1 4 3 4 2 4 3 3 ...
## $ Fedu : int 4 1 1 2 3 3 2 4 2 4 ...
## $ Mjob : Factor w/ 5 levels "at_home","health",..: 1 1 1 2 3 4 3 3 4 3 ...
## $ Fjob : Factor w/ 5 levels "at_home","health",..: 5 3 3 4 3 3 3 5 3 3 ...
## $ reason : Factor w/ 4 levels "course","home",..: 1 1 3 2 2 4 2 2 2 2 ...
## $ guardian : Factor w/ 3 levels "father","mother",..: 2 1 2 2 1 2 2 2 2 2 ...
## $ traveltime: int 2 1 1 1 1 1 1 2 1 1 ...
## $ studytime : int 2 2 2 3 2 2 2 2 2 2 ...
## $ failures : int 0 0 3 0 0 0 0 0 0 0 ...
## $ schoolsup : Factor w/ 2 levels "no","yes": 2 1 2 1 1 1 1 2 1 1 ...
## $ famsup : Factor w/ 2 levels "no","yes": 1 2 1 2 2 2 1 2 2 2 ...
## $ paid : Factor w/ 2 levels "no","yes": 1 1 2 2 2 2 1 1 2 2 ...
## $ activities: Factor w/ 2 levels "no","yes": 1 1 1 2 1 2 1 1 1 2 ...
## $ nursery : Factor w/ 2 levels "no","yes": 2 1 2 2 2 2 2 2 2 2 ...
## $ higher : Factor w/ 2 levels "no","yes": 2 2 2 2 2 2 2 2 2 2 ...
## $ internet : Factor w/ 2 levels "no","yes": 1 2 2 2 1 2 2 1 2 2 ...
## $ romantic : Factor w/ 2 levels "no","yes": 1 1 1 2 1 1 1 1 1 1 ...
## $ famrel : int 4 5 4 3 4 5 4 4 4 5 ...
## $ freetime : int 3 3 3 2 3 4 4 1 2 5 ...
## $ goout : int 4 3 2 2 2 2 4 4 2 1 ...
## $ Dalc : int 1 1 2 1 1 1 1 1 1 1 ...
## $ Walc : int 1 1 3 1 2 2 1 1 1 1 ...
## $ health : int 3 3 3 5 5 5 3 1 1 5 ...
## $ absences : int 6 4 10 2 4 10 0 6 0 0 ...
## $ G1 : int 5 5 7 15 6 15 12 6 16 14 ...
## $ G2 : int 6 5 8 14 10 15 12 5 18 15 ...
## $ G3 : int 6 6 10 15 10 15 11 6 19 15 ...
4. Crear variable de clasificación
En el siguiente paso, creamos una variable de clasificacion en función de G3. Si G3 > 10, el estudiante aprobó, si G3 <= 10, entonces el estudiante no aprobó. Esta es la variable objetivo de nuestro clasificador, por lo tanto la convertiremos a factor.
# Crear variable objetivo
student_mat_data$calificacion <- ifelse(student_mat_data$G3 > 10, "Aprobado", "No Aprobado")
student_mat_data$calificacion <- as.factor(student_mat_data$calificacion)
5. Seleccionar variables predictoras importantes
Elegiremos las variables que a criterio consideramos son las más importantes para poder predecir la calificación del estudiante.
# Seleccionar variables
student_mat_data <- student_mat_data %>%
select(-sex,-address,-Pstatus,-guardian,-nursery,-Dalc,-Walc,-romantic, -internet, -health)
Dividimos los datos en conjuntos aleatorios de de entrenamiento y prueba. En este caso, separaremos los datos en 4 grupos (folds=4), 3 de entrenamiento y 1 de prueba. De esta forma, nos aseguramos que el conjunto de entrenamiento tenga alrededor de un 75% de los datos y el conjunto de prueba un 25%. Adicinonalmente, creamos las etiquetas de los grupos para aplicar validación cruzada más adelante.
# Crear conjuntos de entrenamiento y prueba
set.seed(2025)
library(caret)
folds <- createFolds(student_mat_data$calificacion, k = 4)
entrenamiento <- student_mat_data[-folds[[4]],]
prueba <- student_mat_data[folds[[4]],]
# Crear etiquetas
entrenamiento_etiquetas <- student_mat_data$calificacion[-folds[[4]]]
prueba_etiquetas <- student_mat_data$calificacion[folds[[4]]]
Ver cantidad de observaciones en cada conjunto:
dim(entrenamiento)[1]
## [1] 296
dim(prueba)[1]
## [1] 99
1. Entrenar modelo
# Construir el modelo de clasificacion
library(e1071)
# Entrenar el modelo
modelo_nb <- naiveBayes(calificacion ~ ., data = entrenamiento)
# Revisar el modelo
modelo_nb
##
## Naive Bayes Classifier for Discrete Predictors
##
## Call:
## naiveBayes.default(x = X, y = Y, laplace = laplace)
##
## A-priori probabilities:
## Y
## Aprobado No Aprobado
## 0.5304054 0.4695946
##
## Conditional probabilities:
## school
## Y GP MS
## Aprobado 0.91719745 0.08280255
## No Aprobado 0.87769784 0.12230216
##
## age
## Y [,1] [,2]
## Aprobado 16.53503 1.184995
## No Aprobado 16.85612 1.354334
##
## famsize
## Y GT3 LE3
## Aprobado 0.7006369 0.2993631
## No Aprobado 0.6978417 0.3021583
##
## Medu
## Y [,1] [,2]
## Aprobado 2.840764 1.083143
## No Aprobado 2.618705 1.052228
##
## Fedu
## Y [,1] [,2]
## Aprobado 2.643312 1.062247
## No Aprobado 2.338129 1.066988
##
## Mjob
## Y at_home health other services teacher
## Aprobado 0.11464968 0.11464968 0.36305732 0.28662420 0.12101911
## No Aprobado 0.19424460 0.04316547 0.37410072 0.23741007 0.15107914
##
## Fjob
## Y at_home health other services teacher
## Aprobado 0.05732484 0.05095541 0.53503185 0.28025478 0.07643312
## No Aprobado 0.05755396 0.04316547 0.58992806 0.25179856 0.05755396
##
## reason
## Y course home other reputation
## Aprobado 0.3566879 0.2802548 0.1019108 0.2611465
## No Aprobado 0.3812950 0.2733813 0.1079137 0.2374101
##
## traveltime
## Y [,1] [,2]
## Aprobado 1.388535 0.6268530
## No Aprobado 1.561151 0.7719269
##
## studytime
## Y [,1] [,2]
## Aprobado 2.146497 0.8757069
## No Aprobado 1.971223 0.7888943
##
## failures
## Y [,1] [,2]
## Aprobado 0.0955414 0.3158919
## No Aprobado 0.5827338 0.9471489
##
## schoolsup
## Y no yes
## Aprobado 0.91719745 0.08280255
## No Aprobado 0.82733813 0.17266187
##
## famsup
## Y no yes
## Aprobado 0.3821656 0.6178344
## No Aprobado 0.4028777 0.5971223
##
## paid
## Y no yes
## Aprobado 0.4713376 0.5286624
## No Aprobado 0.5683453 0.4316547
##
## activities
## Y no yes
## Aprobado 0.5031847 0.4968153
## No Aprobado 0.4892086 0.5107914
##
## higher
## Y no yes
## Aprobado 0.01910828 0.98089172
## No Aprobado 0.07913669 0.92086331
##
## famrel
## Y [,1] [,2]
## Aprobado 3.885350 0.9538596
## No Aprobado 3.935252 0.8271536
##
## freetime
## Y [,1] [,2]
## Aprobado 3.184713 1.0242049
## No Aprobado 3.266187 0.9292005
##
## goout
## Y [,1] [,2]
## Aprobado 2.949045 1.042655
## No Aprobado 3.309353 1.172443
##
## absences
## Y [,1] [,2]
## Aprobado 5.044586 6.833500
## No Aprobado 6.546763 9.880608
##
## G1
## Y [,1] [,2]
## Aprobado 13.235669 2.488796
## No Aprobado 8.330935 1.799309
##
## G2
## Y [,1] [,2]
## Aprobado 13.363057 2.157732
## No Aprobado 7.755396 2.672571
##
## G3
## Y [,1] [,2]
## Aprobado 13.560510 2.176028
## No Aprobado 6.899281 3.730592
Interpretación
Probabilidades A-priori Las probabilidades A-priori muestran la proporción de cada clase en los datos de entrenamiento: - 53% de los estudiantes aprobaron. - 47% de los estudiantes no aprobaron.
Probabilidades condicionales Por otro lado, las probabilidades condicionales muestran cómo se comportan las variables según cada clase. Evaluaremos las variables más significativas.
school
: Los estudiantes de GP tienen más probabilidad
de aprobar (91.7%) que los de MS.failures
: Los estudiantes que aprueban tienen un menor
promedio de fracasos de 0.0955 que los estudiantes que no aprueban que
tienen un promedio mayor de 0.5827.absences
: Los estudiantes que aprueban tienen un
promedio de ausencias menor de 5.04, a diferencia de los estudiantes que
no aprueban que tienen un promedio de 6.55.studytime
: Los estudiantes que aprueban tienen un
promedio de 2.15 horas de estudio en la semana, a diferencia de los que
no aprueban que tienen un promedio de 1.97 horas.higher
: Los estudiantes con más motivación de seguir
estudiando aprueban más (98%).goout
: Los estudiantes que aprueban tienen un promedio
menor de tiempo de salida de 2.95, a diferencia que los que no aprueban
que cuentan con un promedio de salida mayor de 3.31.G1
, G2
y G3
: Los estudiantes
que aprueban tienen calificaciones más altas en los tres trimestres que
los que no aprueban.Calcular la predicción del modelo
# Hacer preddiciones en el conjunto de prueba
pred_nb <- predict(modelo_nb, prueba[,-24])
table(pred_nb, prueba_etiquetas)
## prueba_etiquetas
## pred_nb Aprobado No Aprobado
## Aprobado 47 4
## No Aprobado 5 43
Interpretación:
Matriz de confusión - 47 casos fueron clasificados como “Aprobado” correctamente, mientras 4 casos fueron clasificados erróneamente bajo la clase de “No Aprobado”. - 43 casos fueron clasificados correctamente como “No Aprobado”, mientras 5 casos fueron clasificados erróneamente como “Aprobado”.
Precisión El modelo clasificó correctamente el 90.91% de los casos en el conjunto de prueba.
Kappa El modelo tiene un índice de 0.8170, lo cual es excelente ya que es mayor de 0.80 e indica una buena concordancia con las predicciones y las etiquetas.
Evaluar rendimiento con matriz de confusión
# Matriz de confusion
confusionMatrix(pred_nb, prueba_etiquetas)
## Confusion Matrix and Statistics
##
## Reference
## Prediction Aprobado No Aprobado
## Aprobado 47 4
## No Aprobado 5 43
##
## Accuracy : 0.9091
## 95% CI : (0.8344, 0.9576)
## No Information Rate : 0.5253
## P-Value [Acc > NIR] : <2e-16
##
## Kappa : 0.8179
##
## Mcnemar's Test P-Value : 1
##
## Sensitivity : 0.9038
## Specificity : 0.9149
## Pos Pred Value : 0.9216
## Neg Pred Value : 0.8958
## Prevalence : 0.5253
## Detection Rate : 0.4747
## Detection Prevalence : 0.5152
## Balanced Accuracy : 0.9094
##
## 'Positive' Class : Aprobado
##
Precisión El modelo clasificó correctamente el 90.91% de los casos en el conjunto de prueba.
Kappa El modelo tiene un índice de 0.8170, lo cual es excelente ya que es mayor de 0.80 e indica una buena concordancia con las predicciones y las etiquetas del modelo.
En conclusión, el modelo es bastante fuerte y certer.
Aplicaremos la validación cruzada para validar la estabilidad del modelo y asegurarnos que la tasa de aciertos no dependa de la partición de los datos de entrenamiento. En este caso, utilizamos 20 particiones porque fue el número que mejor certeza nos dió.
# Validación cruzada automática
library(naivebayes)
set.seed(2025)
train_control <- trainControl(method="cv", number=20, savePredictions = TRUE)
NBC_cv <- train(calificacion ~ ., data=cbind(prueba, calificacion=prueba_etiquetas),
method = "naive_bayes", trControl = train_control)
# Ver resultados
NBC_cv
## Naive Bayes
##
## 99 samples
## 23 predictors
## 2 classes: 'Aprobado', 'No Aprobado'
##
## No pre-processing
## Resampling: Cross-Validated (20 fold)
## Summary of sample sizes: 93, 95, 95, 94, 93, 94, ...
## Resampling results across tuning parameters:
##
## usekernel Accuracy Kappa
## FALSE 0.8716667 0.7353147
## TRUE 0.9266667 0.8462121
##
## Tuning parameter 'laplace' was held constant at a value of 0
## Tuning
## parameter 'adjust' was held constant at a value of 1
## Accuracy was used to select the optimal model using the largest value.
## The final values used for the model were laplace = 0, usekernel = TRUE
## and adjust = 1.
Buscaremos la matriz de confusión nuevamente, solo que esta vez utilizaremos las predicciones guardadas anteriormente por la libreria de caret.
# Matriz de confusion utilizando predicciones guardadas
confusionMatrix(NBC_cv$pred$pred, NBC_cv$pred$obs)
## Confusion Matrix and Statistics
##
## Reference
## Prediction Aprobado No Aprobado
## Aprobado 92 8
## No Aprobado 12 86
##
## Accuracy : 0.899
## 95% CI : (0.8483, 0.9372)
## No Information Rate : 0.5253
## P-Value [Acc > NIR] : <2e-16
##
## Kappa : 0.7979
##
## Mcnemar's Test P-Value : 0.5023
##
## Sensitivity : 0.8846
## Specificity : 0.9149
## Pos Pred Value : 0.9200
## Neg Pred Value : 0.8776
## Prevalence : 0.5253
## Detection Rate : 0.4646
## Detection Prevalence : 0.5051
## Balanced Accuracy : 0.8998
##
## 'Positive' Class : Aprobado
##
Para construir este modelo, eliminamos 10 variables a criterio propio que entendemos que no son importantes para la clasificación final. También utilizamos k = 20 en la validación cruzada, ya que fue el número de particiones que mejor precisión nos dió.
En la evaluación en el conjunto de prueba del paso 4, obtuvimos los siguientes resultados clave:
Resultados de la validación cruzada con Naive Bayes
El modelo con mejor precisión fue con el uso de usekernel = TRUE, con una precisión de 92.67% y un Kappa de 0.8462.
Matriz de confusión - 92 casos fueron clasificados como “Aprobado” correctamente, mientras 8 casos fueron clasificados erróneamente bajo “No Aprobado”. - 86 casos fueron clasificados como “No Aprobado” correctamente, mientras 12 casos fueron classificados erróneamente bajo “Aprobado”.
Precisión El modelo predice correctamente el 89.9% de los casos.
Kappa El modelo tiene un índice Kappa del 0.7979, lo cual indica tener un buena concordancia entre las predicciones del modelo y las etiquetas reales.
Conclusión general El modelo muestra ser bastante fuerte y robusto para clasificar a los estudiantes en las dos clases de “Aprobado” y “No Aprobado”. Estos resultados de validación cruzada con 20 particiones muestran que el modelo es bastante estable.