Solución paso a paso:

Paso 1: Limpieza y preparación inicial:

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)
Data summary
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)

Paso 2: Dividir los datos en conjunto de entrenamiento y prueba:

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

Paso 3: Construcción del árbol de decisión:

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:

    • El primer nodo muestra “0” porque es la clase de mayor tamaño.
    • Según el índice GINI, la variable que representa mayor relevancia de clasificación es 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.
    • Si time es mayor o igual a 74, seguimos la rama de la izquierda.
    • Si time es menor a 74, seguimos la rama de la derecha.
  • Rama Izquierda time >= 74:

    • Partiendo en la variable 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.
    • Si es mayor o igual que 1.5, se subdivide nuevamente en la variable platelets en el valor de 236e+3 hacia la derecha.
      • Si platelets es mayor o igual que 236e+3, la rama sigue por el lado izquierdo. En este caso, el 26% de los pacientes fallecieron.
      • Si 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:

    • Partiendo en la variable de 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.
    • Si el valor de serum_sodium es mayor o igual que 137 seguimos al lado izquierdo donde se subdivide nuevamente en la variable de time.
      • Si time es mayor o igual que 49 días, solo el 30% de los pacientes fallecen.
      • Si time es menor que 49 días, el 84% de los pacientes fallecen.

Conclusión arbol_1:

  • Podemos sacar las siguientes conclusiones del modelo anterior:
    • Los pacientes con mayor tiempo de seguimiento tienen más probabilidad de sobrevivir.
    • Los pacientes con creatinina mayor de 1.5 y sodio mayor o igual que 137 tienen más probabilidad de fallecer.

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.

Paso 4: Validar la estabilidad del modelo:

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:

  • Utilizamos tuneLength = 21 porque fue el número que más precisión nos dió.
  • El modelo tiene una precisión de 90.2% para acertar predicciones.
  • Comete más errores al clasificar death_event = 1 (muerte), ya que la matriz de confusión nos muestra 98 errores de predicción mal clasificados que los que clasifica como death_event = 0 (no muerte), que solo contiene 25 errores.

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

  • Utilizamos el mismo número en tuneLegnth = 21 para hacer una comparación justa con el modelo de rpart. No significa que 21 sea el número que mejor precisión obtuvo.
  • El modelo obtuvo una precisión de 83.5% para acertar predicciones.
  • Similar al modelo del Árbol Cart, la matriz de confusión muestra que es más dificil predecir las muertes que las personas que sobrevivieron. Podemos observar que hay 265 errores de clasificación en las muertes (1) y 159 errores de clasificación en las no muertes (0).
  • Este modelo aparenta tener más predicciones que el anterior. Esto puede deberse a que el árbol C5.0 terminó siendo mucho más complejo.

Paso 5: Comparar desempeño de CART vs. C5.0 e Interpetación Final

# 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:

  • Podemos observar que ambos modelos tienen un porcentaje alto de precisión. No onstante, el modelo de CART supera ligeramente a C5.0, ya que resultó tener una media equivalente a 91.6%, a diferencia de C5.0 que obtuvo una media de 89.9%. Esto indica que el modelo clasifica mejor los datos.
  • Igualmente, la mediana de CART de 92.9% es superior a la de C5.0, que obtuvo una mediana de 84.5%. Esto sugiere que CART tiene mejor estabilidad.
  • CART también tiene un mejor rendimiento en Kappa (73.9% vs 70.6%), lo que indica mejor concordancia entre pedricciones.
  • Igualmente, la mediana de CART en Kappa es mayor que la de C5.0 (83.3% vs 66.7%), lo que sugiere que CART es más consistente.

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.