1. Cargar datos:
# Cargar datos
Heart_Failure_Records <- read.csv("heart_failure_clinical_records_dataset.csv")
2. Explorar datos:
# Visualizar los datos
head(Heart_Failure_Records)
## age anaemia creatinine_phosphokinase diabetes ejection_fraction
## 1 75 0 582 0 20
## 2 55 0 7861 0 38
## 3 65 0 146 0 20
## 4 50 1 111 0 20
## 5 65 1 160 1 20
## 6 90 1 47 0 40
## high_blood_pressure platelets serum_creatinine serum_sodium sex smoking time
## 1 1 265000 1.9 130 1 0 4
## 2 0 263358 1.1 136 1 0 6
## 3 0 162000 1.3 129 1 1 7
## 4 0 210000 1.9 137 1 0 7
## 5 0 327000 2.7 116 0 0 8
## 6 1 204000 2.1 132 1 1 8
## DEATH_EVENT
## 1 1
## 2 1
## 3 1
## 4 1
## 5 1
## 6 1
# Observar caracteristicas de variables
library(dplyr)
glimpse(Heart_Failure_Records)
## Rows: 299
## Columns: 13
## $ age <dbl> 75, 55, 65, 50, 65, 90, 75, 60, 65, 80, 75, 6…
## $ anaemia <int> 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, …
## $ creatinine_phosphokinase <int> 582, 7861, 146, 111, 160, 47, 246, 315, 157, …
## $ diabetes <int> 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, …
## $ ejection_fraction <int> 20, 38, 20, 20, 20, 40, 15, 60, 65, 35, 38, 2…
## $ high_blood_pressure <int> 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, …
## $ platelets <dbl> 265000, 263358, 162000, 210000, 327000, 20400…
## $ serum_creatinine <dbl> 1.90, 1.10, 1.30, 1.90, 2.70, 2.10, 1.20, 1.1…
## $ serum_sodium <int> 130, 136, 129, 137, 116, 132, 137, 131, 138, …
## $ sex <int> 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, …
## $ smoking <int> 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, …
## $ time <int> 4, 6, 7, 7, 8, 8, 10, 10, 10, 10, 10, 10, 11,…
## $ DEATH_EVENT <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, …
# Revisar los datos detalladamente
library(skimr)
skimr::skim(Heart_Failure_Records)
Name | Heart_Failure_Records |
Number of rows | 299 |
Number of columns | 13 |
_______________________ | |
Column type frequency: | |
numeric | 13 |
________________________ | |
Group variables | None |
Variable type: numeric
skim_variable | n_missing | complete_rate | mean | sd | p0 | p25 | p50 | p75 | p100 | hist |
---|---|---|---|---|---|---|---|---|---|---|
age | 0 | 1 | 60.83 | 11.89 | 40.0 | 51.0 | 60.0 | 70.0 | 95.0 | ▆▇▇▂▁ |
anaemia | 0 | 1 | 0.43 | 0.50 | 0.0 | 0.0 | 0.0 | 1.0 | 1.0 | ▇▁▁▁▆ |
creatinine_phosphokinase | 0 | 1 | 581.84 | 970.29 | 23.0 | 116.5 | 250.0 | 582.0 | 7861.0 | ▇▁▁▁▁ |
diabetes | 0 | 1 | 0.42 | 0.49 | 0.0 | 0.0 | 0.0 | 1.0 | 1.0 | ▇▁▁▁▆ |
ejection_fraction | 0 | 1 | 38.08 | 11.83 | 14.0 | 30.0 | 38.0 | 45.0 | 80.0 | ▃▇▂▂▁ |
high_blood_pressure | 0 | 1 | 0.35 | 0.48 | 0.0 | 0.0 | 0.0 | 1.0 | 1.0 | ▇▁▁▁▅ |
platelets | 0 | 1 | 263358.03 | 97804.24 | 25100.0 | 212500.0 | 262000.0 | 303500.0 | 850000.0 | ▂▇▂▁▁ |
serum_creatinine | 0 | 1 | 1.39 | 1.03 | 0.5 | 0.9 | 1.1 | 1.4 | 9.4 | ▇▁▁▁▁ |
serum_sodium | 0 | 1 | 136.63 | 4.41 | 113.0 | 134.0 | 137.0 | 140.0 | 148.0 | ▁▁▃▇▁ |
sex | 0 | 1 | 0.65 | 0.48 | 0.0 | 0.0 | 1.0 | 1.0 | 1.0 | ▅▁▁▁▇ |
smoking | 0 | 1 | 0.32 | 0.47 | 0.0 | 0.0 | 0.0 | 1.0 | 1.0 | ▇▁▁▁▃ |
time | 0 | 1 | 130.26 | 77.61 | 4.0 | 73.0 | 115.0 | 203.0 | 285.0 | ▆▇▃▆▃ |
DEATH_EVENT | 0 | 1 | 0.32 | 0.47 | 0.0 | 0.0 | 0.0 | 1.0 | 1.0 | ▇▁▁▁▃ |
Al inspeccionar los datos, podemos identificar que la variable de
respuesta es la variable factor death_event
. Los pacientes
que sobrevivieron se clasifican como death_event = 0, mientras los
pacientes que fallecieron como death_event = 1.
Por otro lado, podemos observar que la base de datos no contiene
valores faltantes ni datos duplicados. Por lo tanto, solo renombraremos
la columna DEATH_EVENT
para que siga el mismo formato que
las demás variables.
# Renombrar columnas
library(janitor)
Heart_Failure_Records <- Heart_Failure_Records %>%
janitor::clean_names()
names(Heart_Failure_Records)
## [1] "age" "anaemia"
## [3] "creatinine_phosphokinase" "diabetes"
## [5] "ejection_fraction" "high_blood_pressure"
## [7] "platelets" "serum_creatinine"
## [9] "serum_sodium" "sex"
## [11] "smoking" "time"
## [13] "death_event"
A diferencia del método de clasificación por KNN, los árboles de decisión no necesitan que los datos esten normalizados. Por lo tanto, en el siguiente paso solo vamos asegurarnos que la variable de clasificación sea tipo factor.
# Convertir en factor la variable de clasificación
Heart_Failure_Records$death_event <- as.factor(Heart_Failure_Records$death_event)
En este paso dividimos los datos en dos conjuntos aleatorios de entrenamiento y prueba. Para lograrlo, utilizamos la función createFolds() de la librería caret para garantizar un balance entre ambos conjuntos. Separamos los datos en 5 grupos, 4 de entrenamiento y 1 de prueba. De esta manera, nos aseguramos que un 80% de los datos esten en el conjunto de entrenamiento y el 20% restante en el conjunto de prueba.
# Dividir conjunto en entrenamiento y prueba
library(caret)
set.seed(2025)
folds <- createFolds(Heart_Failure_Records$death_event, k = 5)
entrenamiento <- Heart_Failure_Records[-folds[[5]],]
prueba <- Heart_Failure_Records[folds[[5]],]
# Crear etiquetas
entrenamiento_labels <- Heart_Failure_Records$death_event[-folds[[5]]]
prueba_labels <- Heart_Failure_Records$death_event[folds[[5]]]
Cantidad de observaciones en cada conjunto:
dim(entrenamiento)[1]
## [1] 239
dim(prueba)[1]
## [1] 60
En el siguiente paso, construimos el árbol de decisión por medio de dos distintos algortimos.
1. Árbol Cart
El primer modelo ajustado con los datos de entrenamiento fue construido utilizando la librería rpart y rpart.plot.
library(rpart)
library(rpart.plot)
arbol_1 <- rpart(death_event ~ ., data = entrenamiento)
rpart.plot(arbol_1)
Interpretación arbol_1:
Nodo Raíz:
time
. Esta variable muestra el período de
seguimiento que se le dió al paciente en días. El modelo eligió 74 como
el valor que mejor discrimina y logró llevarse el 80% de los fallecidos
(1) al lado derecho.time
es mayor o igual a 74, seguimos la rama de la
izquierda.time
es menor a 74, seguimos la rama de la
derecha.Rama Izquierda time >= 74:
serum_creatinine
, si el valor
es menor que 1.5, los valores pasan a la izquierda. Esto significa que
solo el 9% de los pacientes con serum_creatinine
menor que
1.5 fallecieron.platelets
en el valor de 236e+3 hacia la derecha.
platelets
es mayor o igual que 236e+3, la rama sigue
por el lado izquierdo. En este caso, el 26% de los pacientes
fallecieron.platelets
es menor que 236e+3, la rama sigue por la
derecha. En este caso, el 65% de los pacientes fallecieron. Al ser la
categoría de la mayor tamaño, el nodo muestra que death_event = 1.Rama derecha time < 74:
serum_sodium
, si el valor
es menor que 137, seguimos el lado derecho de la rama donde un 94% de
los pacientes fallecieron. Por lo tanto, el nodo final muestra el
death_event = 1.serum_sodium
es mayor o igual que 137
seguimos al lado izquierdo donde se subdivide nuevamente en la variable
de time
.
time
es mayor o igual que 49 días, solo el 30% de
los pacientes fallecen.time
es menor que 49 días, el 84% de los pacientes
fallecen.Conclusión arbol_1:
2. Árbol C50
El segundo modelo ajustado con los datos de entrenamiento fue construido con la librería C50.
library(C50)
arbol_2 <- C5.0(death_event ~ ., data = entrenamiento)
arbol_2
##
## Call:
## C5.0.formula(formula = death_event ~ ., data = entrenamiento)
##
## Classification Tree
## Number of samples: 239
## Number of predictors: 12
##
## Tree size: 19
##
## Non-standard options: attempt to group attributes
Visualizar el modelo:
# Visualización
plot(arbol_2)
A simple vista, podemos ver que el modelo tiene un tamaño demasiado
grande para ser interpretado. A diferencia de la paqueteria de rpart,
C50 no poda las ramas consideradas como no esenciales. Inncluso, repite
variables que fueron utilizadas para dividir los datos anteriormente.
Pudieramos especificar en el código las variables predictoras que
consideramos más importantes, para de esa forma utilizar un conjunto de
datos reducido. Otra forma de eliminar ramas sería utilizando el comando
de trails =
. Por lo tanto, no generamos una conclusión
general del modelo, pero si se discutieron algunas sugerencias para
mejorarlo.
1. Árbol Cart
En el siguiente paso, aplicamos la validación cruzada automática para el arbol_1 creado con rpart.
library(caret)
set.seed(2025)
train_control <- trainControl(method="cv",number=21,savePredictions = TRUE)
#Arbol 1 Rpart
arbol_cv1 <- train(death_event ~ ., data=cbind(prueba, death_event = prueba_labels),
method = "rpart", trControl = train_control,
tuneLength = 21)
# Matriz de confusión usando predicciones guardadas por caret
confusionMatrix(arbol_cv1$pred$pred, arbol_cv1$pred$obs)
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 836 98
## 1 25 301
##
## Accuracy : 0.9024
## 95% CI : (0.8846, 0.9182)
## No Information Rate : 0.6833
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 0.7628
##
## Mcnemar's Test P-Value : 8.469e-11
##
## Sensitivity : 0.9710
## Specificity : 0.7544
## Pos Pred Value : 0.8951
## Neg Pred Value : 0.9233
## Prevalence : 0.6833
## Detection Rate : 0.6635
## Detection Prevalence : 0.7413
## Balanced Accuracy : 0.8627
##
## 'Positive' Class : 0
##
Interpretación:
2. Árbol C5.0
Ahora realizamos el mismo proceso para el arbol_2 creado con la librería de C50 para más adelante comparar el rendimiento de ambos modelos.
# Árbol 2 C5.0
set.seed(2025)
arbol_cv2 <- train(death_event ~ ., data=cbind(prueba, death_event = prueba_labels),
method = "C5.0", trControl = train_control,
tuneLength = 21)
# Matriz de confusión usando predicciones guardadas por caret
confusionMatrix(arbol_cv2$pred$pred, arbol_cv2$pred$obs)
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 1645 275
## 1 159 561
##
## Accuracy : 0.8356
## 95% CI : (0.8209, 0.8496)
## No Information Rate : 0.6833
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 0.6055
##
## Mcnemar's Test P-Value : 3.387e-08
##
## Sensitivity : 0.9119
## Specificity : 0.6711
## Pos Pred Value : 0.8568
## Neg Pred Value : 0.7792
## Prevalence : 0.6833
## Detection Rate : 0.6231
## Detection Prevalence : 0.7273
## Balanced Accuracy : 0.7915
##
## 'Positive' Class : 0
##
Interpretación
# Comparar modelos
comparacion <- resamples(list(CART = arbol_cv1, C5.0 = arbol_cv2))
# Resumen de métricas
summary(comparacion)
##
## Call:
## summary.resamples(object = comparacion)
##
## Models: CART, C5.0
## Number of resamples: 21
##
## Accuracy
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## CART 0.6666667 1 1 0.9206349 1 1 0
## C5.0 0.6666667 1 1 0.9206349 1 1 0
##
## Kappa
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## CART 0 0.7 1 0.7578947 1 1 2
## C5.0 0 0.7 1 0.7578947 1 1 2
Interpretación:
Conclusión final:
El análisis anterior se dedicó a la comparación de dos modelos de
árboles de decisión, CART y C50 para poder predecir la variable
death_event
. El informe muestra las visualizaciones de
ambos modelos, junto con una interpretación y validación mediante
validación cruzada para determinar su precisión y estabilidad.
Como fue señalado anteriormente,CART supera a C5.0 tanto en precisión como en el Kappa, lo que lo hace un mejor modelo para este problema de clasificación. Igualmente, CART demuestra ser un modelo más robusto y con menos variabilidad que C5.0, que mostró tener una estructura más compleja y difícil de interpetar debido a la falta de poda de ramas.