1. Introducción

La insuficiencia cardíaca (IC) es una de las enfermedades más graves a nivel mundial en términos de mortalidad. De acuerdo con la Organización Mundial de la Salud (OMS), las enfermedades cardiovasculares son responsables de aproximadamente 17.9 millones de muertes al año, y dentro de ese grupo, la IC se destaca por tener tasas altas de reingreso hospitalario y un pronóstico bastante complicado: alrededor del 50% de los pacientes muere en los cinco años siguientes al diagnóstico. Por eso, poder identificar de forma temprana a los pacientes con mayor riesgo de morir se ha vuelto algo muy importante tanto en la práctica clínica como en la investigación.

Para este estudio se usó el Heart Failure Clinical Records Dataset, que está disponible en el UCI Machine Learning Repository. Los datos fueron recolectados originalmente en el Hospital de Fuzhou, China, y luego fueron procesados y publicados por Davide Chicco y Giuseppe Jurman (2020) en la revista BMC Medical Informatics and Decision Making. El dataset tiene registros clínicos de 299 pacientes con insuficiencia cardíaca e incluye 13 variables clínicas como la edad, la fracción de eyección, la creatinina sérica y algunas comorbilidades como anemia, diabetes e hipertensión, entre otras. La variable que se quiere predecir es DEATH_EVENT, que es una variable binaria que indica si el paciente falleció durante el período de seguimiento. Una ventaja práctica de este dataset es que no tiene valores faltantes y su tamaño es manejable para trabajar con modelos de clasificación.

En el taller se implementaron y compararon dos métodos de aprendizaje supervisado: K-Vecinos Más Cercanos (KNN), que es un algoritmo no paramétrico que clasifica cada observación según la clase mayoritaria de sus vecinos más cercanos; y la Regresión Logística (Logit), que es un modelo paramétrico que estima probabilidades usando la función sigmoide y tiene la ventaja de producir coeficientes interpretables como odds ratios. El objetivo fue ver cuál de los dos modelos predice mejor y qué variables clínicas son más útiles para clasificar el riesgo de mortalidad en pacientes con insuficiencia cardíaca.


2. Metodología

2.1 Fuente de datos

Los datos corresponden al Heart Failure Clinical Records Dataset, disponible en el UCI Machine Learning Repository (Chicco & Jurman, 2020). La información clínica fue recolectada originalmente en el Hospital de Fuzhou, China, e incluye 299 pacientes con diagnóstico de insuficiencia cardíaca. El dataset no presenta valores faltantes y cuenta con 13 variables clínicas.

2.2 Variable dependiente

La variable dependiente es DEATH_EVENT, una variable binaria que indica si el paciente falleció durante el período de seguimiento:

  • 1 = Fallecido: el paciente murió por insuficiencia cardíaca
  • 0 = Sobreviviente: el paciente sobrevivió al período de seguimiento

Poder predecir a tiempo si un paciente cardíaco tiene alto riesgo de morir es fundamental para tomar mejores decisiones clínicas, saber a quién priorizar en cuidados intensivos y ajustar los tratamientos según cada caso. Además, identificar qué variables son las más importantes para hacer esa predicción les permite a los profesionales de la salud enfocar los recursos disponibles en los pacientes que más los necesitan.

2.3 Variables independientes

Se seleccionaron 5 variables independientes con base en su relevancia clínica y su relación esperada con la mortalidad:

Variable Descripción Unidad/Formato Relación esperada con mortalidad
age Edad del paciente Años Positiva
ejection_fraction Fracción de eyección cardíaca Porcentaje (%) Negativa
serum_creatinine Nivel de creatinina en sangre mg/dL Positiva
high_blood_pressure Presencia de hipertensión arterial 0 = No, 1 = Sí Positiva
serum_sodium Nivel de sodio en sangre mEq/L Negativa

Justificación teórica de las variables:

  • age: La edad es uno de los factores de riesgo más impoertantes en enfermedades cardiovasculares. A mayor edad, mayor deterioro del sistema cardiovascular y menor capacidad de recuperación ante episodios de insuficiencia cardíaca.

  • ejection_fraction: La fracción de eyección mide el porcentaje de sangre que el ventrículo izquierdo bombea en cada latido. Valores bajos (< 40%) indican disfunción sistólica severa y se asocian fuertemente con mayor mortalidad en pacientes con insuficiencia cardíaca.

  • serum_creatinine: Niveles elevados de creatinina sérica reflejan deterioro de la función renal (síndrome cardiorrenal), una comorbilidad frecuente y pronósticamente desfavorable en pacientes con insuficiencia cardíaca.

  • high_blood_pressure: La hipertensión arterial es una comorbilidad frecuente en pacientes con insuficiencia cardíaca y contribuye directamente al remodelado ventricular patológico. Aunque su efecto puede atenuarse en el análisis multivariado por confusión con la edad, su relevancia clínica está ampliamente documentada en las guías ESC de insuficiencia cardíaca.

  • serum_sodium: El sodio sérico es un marcador establecido de gravedad en insuficiencia cardíaca. La hiponatremia (sodio bajo) refleja activación neurohormonal intensa y retención de agua libre, asociándose con peor pronóstico y mayor mortalidad, razón por la cual se incluye pese a perder significancia estadística en el modelo multivariado.

2.4 Modelos de clasificación

K-Vecinos Más Cercanos (KNN)

El KNN es un algoritmo no paramétrico que para clasificar una nueva observación simplemente mira las k observaciones más parecidas dentro del conjunto de entrenamiento y le asigna la clase que más se repite entre esos vecinos. Una de sus ventajas principales es que no asume ninguna forma específica para los datos, lo que lo hace bastante flexible. Sin embargo, tiene dos puntos sensibles: la escala de las variables y el valor de k, ya que si las variables no están en la misma escala el algoritmo puede dar más peso a unas que a otras sin que tenga sentido, y si k no está bien elegido el modelo puede sobreajustarse o quedar demasiado simplificado. Por eso, antes de ajustar el modelo se estandarizaron las variables y el valor óptimo de k se buscó mediante validación cruzada.

Regresión Logística (Logit)

La regresión logística es un modelo paramétrico que estima la probabilidad de pertenecer a la clase positiva mediante la función sigmoide:

\[P(Y=1 | X) = \frac{1}{1 + e^{-(\beta_0 + \beta_1 X_1 + \cdots + \beta_p X_p)}}\]

A diferencia del KNN, el Logit produce coeficientes interpretables que permiten entender la dirección y magnitud del efecto de cada variable sobre la probabilidad de mortalidad del paciente. El umbral de clasificación se optimiza mediante el índice de Youden sobre la curva ROC.

2.5 Partición de los datos y evaluación

Los datos se dividen en 75% entrenamiento y 25% prueba usando createDataPartition del paquete caret, que garantiza que la proporción de clases se mantiene en ambos conjuntos (muestreo estratificado). El desempeño se evalúa con:

  • Accuracy: proporción de clasificaciones correctas
  • Sensibilidad: capacidad de detectar correctamente los pacientes fallecidos
  • Especificidad: capacidad de detectar correctamente los pacientes sobrevivientes
  • AUC-ROC: capacidad discriminante global del modelo
  • Kappa: acuerdo corregido por azar
# Definir variables del modelo globalmente
vars_modelo <- c("age", "ejection_fraction", "serum_creatinine", "high_blood_pressure", "serum_sodium")

# Cargar base de datos
data_hf <- read_csv("heart_failure_clinical_records_dataset.csv")

# Convertir la variable dependiente a factor
data_hf <- data_hf %>%
  mutate(
    DEATH_EVENT = factor(
      ifelse(DEATH_EVENT == 1, "Fallecido", "Sobreviviente"),
      levels = c("Fallecido", "Sobreviviente")
    ),
    high_blood_pressure = as.integer(high_blood_pressure)
  )

cat("Distribución de la variable dependiente:\n")
## Distribución de la variable dependiente:
print(table(data_hf$DEATH_EVENT))
## 
##     Fallecido Sobreviviente 
##            96           203
cat("\nObservaciones totales:", nrow(data_hf), "\n")
## 
## Observaciones totales: 299
# Partición entrenamiento / prueba
set.seed(42)

idx_train <- createDataPartition(
  data_hf$DEATH_EVENT,
  p    = 0.75,
  list = FALSE
)

train_data <- data_hf[idx_train, ]
test_data  <- data_hf[-idx_train, ]

cat("\nTamaño conjunto de entrenamiento:", nrow(train_data), "\n")
## 
## Tamaño conjunto de entrenamiento: 225
cat("Tamaño conjunto de prueba:", nrow(test_data), "\n")
## Tamaño conjunto de prueba: 74

3. Resultados Descriptivos

3.1 Estadísticas descriptivas generales

El análisis descriptivo que se hizo al inicio permite ver el tamaño y la variación de las variables que se usaron en los modelos. Esta parte es importante porque ayuda a detectar posibles valores atípicos, a estandarizar correctamente las variables antes de ajustar el KNN y a entender en qué rango clínico se mueve cada predictor.

vars_num <- c("age", "ejection_fraction", "serum_creatinine" , "serum_sodium")

data_hf %>%
  select(all_of(vars_num)) %>%
  psych::describe() %>%
  select(n, mean, sd, min, median, max) %>%
  round(2) %>%
  kable(
    caption   = "Estadísticas descriptivas de las variables continuas del modelo",
    align     = "rrrrrr",
    col.names = c("n", "Media", "Desv. Est.", "Mínimo", "Mediana", "Máximo")
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Estadísticas descriptivas de las variables continuas del modelo
n Media Desv. Est. Mínimo Mediana Máximo
age 299 60.83 11.89 40.0 60.0 95.0
ejection_fraction 299 38.08 11.83 14.0 38.0 80.0
serum_creatinine 299 1.39 1.03 0.5 1.1 9.4
serum_sodium 299 136.63 4.41 113.0 137.0 148.0
  • Edad (age). Los pacientes tienen una edad media de 60.8 años (DE = 11.9), con un rango que va desde 40 hasta 95 años. La mediana (60.0 años) es muy cercana a la media, lo que indica una distribución aproximadamente simétrica. La desviación estándar de 11.9 años refleja una heterogeneidad moderada en la edad de los pacientes con insuficiencia cardíaca, abarcando tanto adultos jóvenes como adultos mayores.

  • Fracción de eyección (ejection_fraction). El valor medio es del 38.1% (DE = 11.8), con una mediana de 38.0%. Este valor es clínicamente relevante porque se encuentra en el límite inferior de lo que se considera fracción de eyección normal (>50%). La mayoría de los pacientes de la muestra presentan insuficiencia cardíaca con fracción de eyección reducida (ICFER), que se define como FE < 40%. El rango va desde 14% (falla severa) hasta 80% (función conservada), lo que muestra la diversidad de la muestra.

  • Creatinina sérica (serum_creatinine). La media es de 1.39 mg/dL (DE = 1.03), pero lo más relevante es la presencia de valores extremos: el valor máximo alcanza 9.4 mg/dL, muy por encima del rango normal (0.6-1.2 mg/dL). Esta asimetría se refleja en la diferencia entre la media (1.39) y la mediana (1.1), lo que indica que unos pocos pacientes con falla renal severa están elevando el promedio. Estos casos atípicos (outliers) corresponden probablemente a pacientes con síndrome cardiorrenal avanzado.

  • Sodio sérico (serum_sodium). El promedio es de 136.6 mEq/L (DE = 4.4), dentro del rango normal de referencia (135-145 mEq/L). Sin embargo, el valor mínimo de 113 mEq/L revela la presencia de casos con hiponatremia severa (<125 mEq/L), un hallazgo clínicamente importante ya que la hiponatremia es un marcador bien documentado de mal pronóstico en pacientes con insuficiencia cardíaca. La mediana (137.0 mEq/L) y la media (136.6 mEq/L) son muy cercanas, lo que sugiere que la mayoría de los pacientes tienen valores normales y solo unos pocos presentan la alteración severa.

3.2 Descriptivos por grupo de desenlace

data_hf %>%
  group_by(DEATH_EVENT) %>%
  summarise(
    n                  = n(),
    age_media          = round(mean(age), 1),
    ejection_media     = round(mean(ejection_fraction), 1),
    creatinine_media   = round(mean(serum_creatinine), 2),
    pct_hbp            = round(mean(high_blood_pressure) * 100, 1),
    sodium_media       = round(mean(serum_sodium), 1)
  ) %>%
  kable(
    caption   = "Media de variables por grupo de desenlace (Fallecido vs. Sobreviviente)",
    align     = "lrrrrrr",
    col.names = c("Grupo", "n", "Edad (años)", "Frac. Eyección (%)",
                  "Creatinina (mg/dL)", "% Hipertensión", "Sodio sérico (mEq/L)")
  ) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Media de variables por grupo de desenlace (Fallecido vs. Sobreviviente)
Grupo n Edad (años) Frac. Eyección (%) Creatinina (mg/dL) % Hipertensión Sodio sérico (mEq/L)
Fallecido 96 65.2 33.5 1.84 40.6 135.4
Sobreviviente 203 58.8 40.3 1.18 32.5 137.2

Las diferencias entre grupos son clínicamente relevantes. Los pacientes fallecidos presentan una fracción de eyección promedio de 33.5% frente a 40.3% en los sobrevivientes — una diferencia considerable que anticipa el poder predictivo de esta variable. La creatinina sérica es también marcadamente más alta en el grupo fallecido (1.84 vs. 1.18 mg/dL). Adicionalmente, el sodio sérico tiende a ser más bajo en los pacientes fallecidos, consistente con la hiponatremia como marcador de gravedad.

3.3 Distribución de la variable dependiente

ggplot(data_hf, aes(x = DEATH_EVENT, fill = DEATH_EVENT)) +
  geom_bar(width = 0.5) +
  geom_text(stat = "count", aes(label = after_stat(count)), vjust = -0.5, size = 4.5) +
  labs(
    title    = "Distribución de la variable dependiente",
    subtitle = "Heart Failure Clinical Records Dataset",
    x        = "Desenlace del paciente",
    y        = "Número de pacientes"
  ) +
  scale_fill_manual(values = c("Fallecido" = "tomato", "Sobreviviente" = "steelblue")) +
  theme_minimal(base_size = 12) +
  theme(legend.position = "none")

Interpretación. La figura muestra que 96 pacientes fallecieron durante el período de seguimiento (32.1%) y 203 sobrevivieron (67.9%). Existe un desbalance moderado entre clases que debe tenerse en cuenta al interpretar las métricas de desempeño: la sensibilidad (detección de fallecidos) cobra especial importancia clínica.

3.4 Boxplots por grupo de desenlace

Los diagramas de caja permiten ver al mismo tiempo la mediana, qué tan dispersos están los datos y si hay valores atípicos en cada variable continua dependiendo del desenlace del paciente, lo que facilita identificar cuáles predictores muestran una diferencia más clara entre los dos grupos.

data_hf %>%
  select(DEATH_EVENT, age, ejection_fraction, serum_creatinine, serum_sodium) %>%
  pivot_longer(cols = -DEATH_EVENT,
               names_to  = "variable",
               values_to = "valor") %>%
  mutate(variable = recode(variable,
    "age"                = "Edad (años)",
    "ejection_fraction"  = "Fracción de eyección (%)",
    "serum_creatinine"   = "Creatinina sérica (mg/dL)",
    "serum_sodium"       = "Sodio sérico (mEq/L)"
  )) %>%
  ggplot(aes(x = DEATH_EVENT, y = valor, fill = DEATH_EVENT)) +
  geom_boxplot(alpha = 0.7) +
  facet_wrap(~variable, scales = "free_y", ncol = 4) +
  scale_fill_manual(values = c("Fallecido" = "tomato", "Sobreviviente" = "steelblue")) +
  labs(
    title    = "Distribución de variables continuas por desenlace del paciente",
    subtitle = "Heart Failure Clinical Records Dataset",
    x        = NULL, y = "Valor",
    fill     = "Desenlace"
  ) +
  theme_minimal(base_size = 11) +
  theme(legend.position = "bottom", axis.text.x = element_blank())

Interpretación. Los diagramas de caja revelan separaciones entre grupos en las variables continuas:

  • Fracción de eyección: Los pacientes fallecidos presentan medianas claramente más bajas que los sobrevivientes. Es el predictor con separación más consistente.
  • Creatinina sérica: Valores más altos se asocian con el grupo fallecido, aunque con mayor variabilidad y presencia de outliers extremos en ambos grupos.
  • Edad: Los pacientes fallecidos tienden a ser de mayor edad, aunque la superposición entre cajas es moderada.
  • Sodio sérico: Los pacientes fallecidos tienden a presentar niveles más bajos de sodio, coherente con la hiponatremia como marcador de activación neurohormonal y peor pronóstico.

3.5 Proporción de variables binarias por grupo

data_hf %>%
  select(DEATH_EVENT, high_blood_pressure) %>%
  pivot_longer(cols = -DEATH_EVENT, names_to = "variable", values_to = "valor") %>%
  mutate(
    variable = recode(variable,
      "high_blood_pressure" = "Hipertensión arterial"
    ),
    valor = ifelse(valor == 1, "Sí", "No")
  ) %>%
  group_by(DEATH_EVENT, variable, valor) %>%
  summarise(n = n(), .groups = "drop") %>%
  group_by(DEATH_EVENT, variable) %>%
  mutate(pct = n / sum(n) * 100) %>%
  filter(valor == "Sí") %>%
  ggplot(aes(x = variable, y = pct, fill = DEATH_EVENT)) +
  geom_col(position = "dodge", width = 0.5) +
  geom_text(aes(label = paste0(round(pct, 1), "%")),
            position = position_dodge(width = 0.5), vjust = -0.5, size = 3.5) +
  scale_fill_manual(values = c("Fallecido" = "tomato", "Sobreviviente" = "steelblue")) +
  labs(
    title    = "Prevalencia de hipertensión por grupo de desenlace",
    subtitle = "Porcentaje de pacientes con la condición presente",
    x        = "Condición clínica",
    y        = "% de pacientes",
    fill     = "Desenlace"
  ) +
  ylim(0, 70) +
  theme_minimal(base_size = 12) +
  theme(legend.position = "bottom")

Interpretación. La gráfica compara qué tan frecuente es la hipertensión arterial entre los pacientes fallecidos y los sobrevivientes. Si la diferencia entre los dos grupos es grande, significa que la variable tiene buen poder para discriminar entre ellos; si la diferencia es pequeña, quiere decir que su aporte es más bien clínico y puede perder peso en el modelo por estar relacionada con otras variables como la edad.

3.6 Matriz de correlaciones

datos_cor <- data_hf %>%
  select(all_of(vars_modelo)) %>%
  rename(
    "Edad"           = age,
    "Frac. Eyección" = ejection_fraction,
    "Creatinina"     = serum_creatinine,
    "Hipertensión"   = high_blood_pressure,
    "Sodio sérico"   = serum_sodium
  ) %>%
  cor(use = "complete.obs")

ggcorrplot(datos_cor,
           hc.order = TRUE,
           type     = "lower",
           lab      = TRUE,
           lab_size = 3.5,
           title    = "Matriz de correlaciones entre variables independientes",
           colors   = c("tomato", "white", "steelblue"))

Interpretación. La matriz muestra qué tan relacionadas linealmente están las variables predictoras entre sí. Si hay correlaciones muy altas entre ellas, eso podría ser un problema de multicolinealidad para el modelo Logit, por lo que se revisarán los VIF para verificarlo. El KNN no tiene ese problema ya que al ser no paramétrico no le afecta la multicolinealidad. En cuanto a las relaciones esperadas, se anticipa que la edad y la creatinina estén correlacionadas porque ambas reflejan un deterioro clínico progresivo, y posiblemente haya una correlación inversa entre el sodio sérico y la creatinina, ya que los dos están relacionados con disfunción renal y retención de líquidos.


4. Resultados del Modelo

4.1 Modelo KNN

El modelo KNN clasifica a cada paciente basándose en cómo son los pacientes más parecidos a él dentro del conjunto de entrenamiento. Como el algoritmo funciona calculando distancias entre observaciones, las variables se estandarizaron antes de ajustarlo para que ninguna tenga más influencia que las demás simplemente por estar en una escala más grande.

# Estandarizar variables
train_X <- train_data %>% select(all_of(vars_modelo)) %>% scale()
test_X  <- test_data  %>% select(all_of(vars_modelo)) %>%
  scale(center = attr(train_X, "scaled:center"),
        scale  = attr(train_X, "scaled:scale"))

train_Y <- train_data$DEATH_EVENT
test_Y  <- test_data$DEATH_EVENT

# Búsqueda del k óptimo mediante validación cruzada
set.seed(42)
ctrl <- trainControl(method = "cv", number = 5)

knn_entrenado <- train(
  x          = train_X,
  y          = train_Y,
  method     = "knn",
  tuneLength = 15,
  trControl  = ctrl
)

knn_entrenado
## k-Nearest Neighbors 
## 
## 225 samples
##   5 predictor
##   2 classes: 'Fallecido', 'Sobreviviente' 
## 
## No pre-processing
## Resampling: Cross-Validated (5 fold) 
## Summary of sample sizes: 180, 181, 180, 180, 179 
## Resampling results across tuning parameters:
## 
##   k   Accuracy   Kappa     
##    5  0.7068072  0.25776695
##    7  0.7111550  0.25189253
##    9  0.7156917  0.25863305
##   11  0.7112429  0.20954869
##   13  0.7024550  0.18705015
##   15  0.7026526  0.15805068
##   17  0.7114449  0.17613575
##   19  0.7070004  0.15028288
##   21  0.7027536  0.13287834
##   23  0.6892227  0.08209623
##   25  0.6936671  0.09161794
##   27  0.6936671  0.09161794
##   29  0.6979139  0.09909966
##   31  0.7024594  0.10863872
##   33  0.6934695  0.08154861
## 
## Accuracy was used to select the optimal model using the largest value.
## The final value used for the model was k = 9.
plot(knn_entrenado,
     main = "Accuracy del KNN según número de vecinos (k)",
     xlab = "Número de vecinos (k)",
     ylab = "Accuracy (Validación Cruzada)")

Interpretación. El gráfico muestra cómo varía la exactitud promedio del modelo KNN en función del número de vecinos k. El valor óptimo es k = 9, que maximiza la precisión en validación cruzada. Valores de k muy bajos tienden a sobreajustar (alta varianza), mientras que valores muy altos simplifican demasiado el modelo (alto sesgo).

# Predicción en test
knn_pred <- predict(knn_entrenado, newdata = as.data.frame(test_X))
knn_prob <- predict(knn_entrenado, newdata = as.data.frame(test_X), type = "prob")

# Matriz de confusión
cm_knn <- confusionMatrix(knn_pred, test_Y, positive = "Fallecido")
cm_knn
## Confusion Matrix and Statistics
## 
##                Reference
## Prediction      Fallecido Sobreviviente
##   Fallecido            11             1
##   Sobreviviente        13            49
##                                          
##                Accuracy : 0.8108         
##                  95% CI : (0.703, 0.8925)
##     No Information Rate : 0.6757         
##     P-Value [Acc > NIR] : 0.007166       
##                                          
##                   Kappa : 0.5038         
##                                          
##  Mcnemar's Test P-Value : 0.003283       
##                                          
##             Sensitivity : 0.4583         
##             Specificity : 0.9800         
##          Pos Pred Value : 0.9167         
##          Neg Pred Value : 0.7903         
##              Prevalence : 0.3243         
##          Detection Rate : 0.1486         
##    Detection Prevalence : 0.1622         
##       Balanced Accuracy : 0.7192         
##                                          
##        'Positive' Class : Fallecido      
## 

Interpretación de la matriz de confusión — KNN

Matriz de confusión — KNN
Prediction Fallecido Sobreviviente
Fallecido 11 1
Sobreviviente 13 49
  • Accuracy (81.1%): El modelo KNN clasifica correctamente el 81.1% de los pacientes en el conjunto de prueba. Esto supera la No Information Rate (67.6%).

  • Kappa (0.504): Indica un acuerdo moderado entre predicciones y valores reales, según la escala de Landis y Koch (1977).

  • Sensibilidad (45.8%): De todos los pacientes fallecidos, el modelo identificó correctamente el 45.8%. En contexto clínico, esta métrica es crítica para evitar clasificar erroneamente como “sobreviviente” a un paciente en riesgo.

  • Especificidad (98%): De todos los pacientes sobrevivientes, el modelo los identificó correctamente el 98% de las veces.

  • Balanced Accuracy (71.9%): Promedio entre sensibilidad y especificidad — medida más robusta ante el desbalance de clases presente en este dataset.

4.2 Modelo Logit

La regresión logística estima la probabilidad de que un paciente pertenezca al grupo de fallecidos usando las variables clínicas que se seleccionaron. A diferencia del KNN, este modelo tiene la ventaja de que se puede interpretar directamente qué tanto y en qué dirección afecta cada variable a esa probabilidad.

# Entrenar modelo logit
fit_logit <- glm(
  DEATH_EVENT ~ age + ejection_fraction + serum_creatinine + high_blood_pressure + serum_sodium,
  data   = train_data %>%
    mutate(DEATH_EVENT = ifelse(DEATH_EVENT == "Fallecido", 1, 0)),
  family = binomial()
)

summary(fit_logit)
## 
## Call:
## glm(formula = DEATH_EVENT ~ age + ejection_fraction + serum_creatinine + 
##     high_blood_pressure + serum_sodium, family = binomial(), 
##     data = train_data %>% mutate(DEATH_EVENT = ifelse(DEATH_EVENT == 
##         "Fallecido", 1, 0)))
## 
## Coefficients:
##                     Estimate Std. Error z value Pr(>|z|)    
## (Intercept)         -0.31424    5.00072  -0.063 0.949894    
## age                  0.05354    0.01423   3.762 0.000168 ***
## ejection_fraction   -0.05944    0.01642  -3.620 0.000294 ***
## serum_creatinine     0.55836    0.16175   3.452 0.000556 ***
## high_blood_pressure  0.43150    0.33203   1.300 0.193747    
## serum_sodium        -0.01890    0.03637  -0.520 0.603364    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 282.09  on 224  degrees of freedom
## Residual deviance: 234.04  on 219  degrees of freedom
## AIC: 246.04
## 
## Number of Fisher Scoring iterations: 4

Interpretación de coeficientes Logit

Coeficientes del modelo Logit
Variable Estimado Error Std. Valor z Valor p
(Intercept) (Intercept) -0.3142 5.0007 -0.063 0.9499
age age 0.0535 0.0142 3.762 0.0002
ejection_fraction ejection_fraction -0.0594 0.0164 -3.620 0.0003
serum_creatinine serum_creatinine 0.5584 0.1617 3.452 0.0006
high_blood_pressure high_blood_pressure 0.4315 0.3320 1.300 0.1937
serum_sodium serum_sodium -0.0189 0.0364 -0.520 0.6034

En la regresión logística los coeficientes representan el cambio en el logaritmo de las odds de fallecer por cada unidad adicional en la variable predictora. Los odds ratios (exponencial de los coeficientes) facilitan la interpretación:

data.frame(
  Variable   = names(coef(fit_logit)),
  Odds_Ratio = round(exp(coef(fit_logit)), 4)
) %>%
  kable(caption = "Odds Ratios del modelo Logit",
        col.names = c("Variable", "Odds Ratio")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Odds Ratios del modelo Logit
Variable Odds Ratio
(Intercept) (Intercept) 0.7303
age age 1.0550
ejection_fraction ejection_fraction 0.9423
serum_creatinine serum_creatinine 1.7478
high_blood_pressure high_blood_pressure 1.5396
serum_sodium serum_sodium 0.9813
  • age: Entre más edad tenga el paciente, mayor es la probabilidad de que fallezca. Cada año adicional de vida aumenta las odds de mortalidad, lo cual tiene sentido porque con el envejecimiento el sistema cardiovascular se va deteriorando progresivamente.

  • ejection_fraction: Entre más alta sea la fracción de eyección del paciente, menor es la probabilidad de que fallezca. El coeficiente negativo de esta variable confirma que tener una función sistólica conservada actúa como un factor protector en pacientes con insuficiencia cardíaca.

  • serum_creatinine: Niveles más altos de creatinina sérica incrementan fuertemente las posibilidades de mortalidad, reflejando el impacto del deterioro renal sobre el pronóstico cardiovascular (síndrome cardiorrenal).

  • high_blood_pressure: Se espera que tener hipertensión arterial aumente la probabilidad de mortalidad en el paciente. Sin embargo, en el análisis multivariado esta variable puede perder significancia estadística porque parte de su efecto se confunde con el de la edad, aunque su importancia clínica es suficiente razón para mantenerla en el modelo.

  • serum_sodium: Niveles más bajos de sodio sérico se asocian con mayor mortalidad. Un coeficiente negativo confirmaría que la hiponatremia es un marcador de gravedad y activación neurohormonal intensa en insuficiencia cardíaca.

# Predicción con umbral 0.5
p_hat    <- predict(fit_logit, newdata = test_data, type = "response")
pred_05  <- factor(ifelse(p_hat >= 0.5, "Fallecido", "Sobreviviente"),
                   levels = c("Fallecido", "Sobreviviente"))
cm_logit_05 <- confusionMatrix(pred_05, test_Y, positive = "Fallecido")

cat("=== Matriz de confusión Logit (umbral = 0.5) ===\n")
## === Matriz de confusión Logit (umbral = 0.5) ===
cm_logit_05
## Confusion Matrix and Statistics
## 
##                Reference
## Prediction      Fallecido Sobreviviente
##   Fallecido            11             2
##   Sobreviviente        13            48
##                                           
##                Accuracy : 0.7973          
##                  95% CI : (0.6878, 0.8819)
##     No Information Rate : 0.6757          
##     P-Value [Acc > NIR] : 0.014757        
##                                           
##                   Kappa : 0.4749          
##                                           
##  Mcnemar's Test P-Value : 0.009823        
##                                           
##             Sensitivity : 0.4583          
##             Specificity : 0.9600          
##          Pos Pred Value : 0.8462          
##          Neg Pred Value : 0.7869          
##              Prevalence : 0.3243          
##          Detection Rate : 0.1486          
##    Detection Prevalence : 0.1757          
##       Balanced Accuracy : 0.7092          
##                                           
##        'Positive' Class : Fallecido       
## 
# Umbral óptimo por índice de Youden
roc_logit <- roc(
  response  = factor(test_data$DEATH_EVENT, levels = c("Sobreviviente", "Fallecido")),
  predictor = p_hat,
  levels    = c("Sobreviviente", "Fallecido")
)

thr_opt <- coords(roc_logit, x = "best", best.method = "youden", ret = "threshold")
umbral  <- as.numeric(thr_opt[1])
cat("Umbral óptimo (Youden):", round(umbral, 3), "\n")
## Umbral óptimo (Youden): 0.401
pred_opt <- factor(ifelse(p_hat >= umbral, "Fallecido", "Sobreviviente"),
                   levels = c("Fallecido", "Sobreviviente"))
cm_logit <- confusionMatrix(pred_opt, test_Y, positive = "Fallecido")

cat("\n=== Matriz de confusión Logit (umbral óptimo) ===\n")
## 
## === Matriz de confusión Logit (umbral óptimo) ===
cm_logit
## Confusion Matrix and Statistics
## 
##                Reference
## Prediction      Fallecido Sobreviviente
##   Fallecido            16             4
##   Sobreviviente         8            46
##                                           
##                Accuracy : 0.8378          
##                  95% CI : (0.7339, 0.9133)
##     No Information Rate : 0.6757          
##     P-Value [Acc > NIR] : 0.001322        
##                                           
##                   Kappa : 0.6132          
##                                           
##  Mcnemar's Test P-Value : 0.386476        
##                                           
##             Sensitivity : 0.6667          
##             Specificity : 0.9200          
##          Pos Pred Value : 0.8000          
##          Neg Pred Value : 0.8519          
##              Prevalence : 0.3243          
##          Detection Rate : 0.2162          
##    Detection Prevalence : 0.2703          
##       Balanced Accuracy : 0.7933          
##                                           
##        'Positive' Class : Fallecido       
## 

Interpretación de la matriz de confusión — Logit (umbral óptimo)

Matriz de confusión — Logit (umbral óptimo)
Prediction Fallecido Sobreviviente
Fallecido 16 4
Sobreviviente 8 46
  • Accuracy (83.8%): Con el umbral optimizado, el modelo Logit clasifica correctamente el 83.8% de los pacientes.

  • Kappa (0.613): Indica un acuerdo sustancial entre predicciones y valores reales.

  • Sensibilidad (66.7%): El Logit identifica correctamente el 66.7% de los pacientes fallecidos. En contexto clínico, maximizar esta métrica reduce los falsos negativos (pacientes en riesgo no detectados).

  • Especificidad (92%): Identifica correctamente el 92% de los pacientes sobrevivientes.

4.3 Curvas ROC y AUC

# ROC para KNN
roc_knn <- roc(
  response  = test_Y,
  predictor = knn_prob[, "Fallecido"],
  levels    = c("Sobreviviente", "Fallecido")
)

auc_knn   <- auc(roc_knn)
auc_logit <- auc(roc_logit)

# Graficar ambas curvas
plot(roc_knn,
     col  = "steelblue",
     lwd  = 2,
     main = sprintf("Curvas ROC — KNN (AUC=%.3f) vs Logit (AUC=%.3f)",
                    auc_knn, auc_logit))
plot(roc_logit, col = "tomato", lwd = 2, add = TRUE)
abline(a = 0, b = 1, lty = 2, col = "gray50")
legend("bottomright",
       legend = c(sprintf("KNN   AUC = %.3f", auc_knn),
                  sprintf("Logit AUC = %.3f", auc_logit)),
       col = c("steelblue", "tomato"),
       lwd = 2,
       bty = "n")

Interpretación. La curva ROC grafica la relación entre la Tasa de Verdaderos Positivos (Sensibilidad) y la Tasa de Falsos Positivos (1 − Especificidad) para todos los posibles umbrales. Una curva más cercana a la esquina superior izquierda indica mejor desempeño; la diagonal representa un clasificador aleatorio.

El AUC (Área Bajo la Curva) resume el desempeño en un solo número:

  • KNN: AUC = 0.857 → capacidad discriminante buena
  • Logit: AUC = 0.83 → capacidad discriminante buena

En términos concretos: si tomamos al azar un paciente fallecido y uno sobreviviente, el modelo KNN tiene una probabilidad del 85.7% de asignarle una probabilidad de muerte más alta al que realmente falleció; el Logit, del 83%.

4.4 Comparación de modelos

Comparación de métricas entre modelos KNN y Logit
Métrica KNN Logit
Accuracy 81.1% 83.8%
Kappa 0.504 0.613
Sensibilidad 45.8% 66.7%
Especificidad 98% 92%
Balanced Accuracy 71.9% 79.3%
AUC-ROC 0.857 0.83
  • Sensibilidad (métrica prioritaria). El Logit (66.7%) le gana por bastante al KNN (45.8%) en esta métrica. Para entenderlo en términos más concretos: de cada 3 pacientes que fallecen, el Logit logra identificar correctamente a 2, mientras que el KNN solo identifica a 1. Esta diferencia es muy importante porque cada falso negativo, es decir, un paciente que en realidad va a fallecer pero el modelo clasifica como sobreviviente, significa una oportunidad de intervención médica que se pierde. En esta métrica el Logit es claramente el mejor.

  • Especificidad. Los dos modelos tienen un desempeño muy bueno en esta métrica: KNN (98%) y Logit (92%). Esto quiere decir que de cada 100 pacientes que sobreviven, el KNN solo genera 2 falsas alarmas y el Logit genera 8. Aunque el KNN sale un poco mejor en especificidad, esa diferencia es menos relevante clínicamente si se compara con la ventaja que tiene el Logit en sensibilidad.

  • Balanced Accuracy. Al promediar la sensibilidad y la especificidad, el Logit (79.3%) supera al KNN (71.9%). Esta métrica es más confiable que el accuracy simple cuando las clases están desbalanceadas, como en este caso, y confirma que el Logit logra un mejor balance entre detectar correctamente a los pacientes fallecidos y clasificar bien a los sobrevivientes.

  • AUC-ROC. El KNN (0.857) tiene una ventaja marginal sobre el Logit (0.830). Ambos valores se encuentran en el rango de “buena” capacidad discriminante (0.8-0.9). Esto indica que, al tomar un par de pacientes (uno fallecido y uno sobreviviente), el KNN tiene una probabilidad del 85.7% de asignar una probabilidad de muerte más alta al que realmente fallece, mientras que el Logit lo hace en el 83.0% de los casos. La diferencia es pequeña y no es suficiente para considerar al KNN como modelo superior, especialmente dado su bajo desempeño en sensibilidad.

  • Kappa. El Logit (0.613) alcanza un acuerdo “sustancial” según la escala de Landis y Koch (1977), mientras que el KNN (0.504) se ubica en el rango de acuerdo “moderado”. Esto indica que las predicciones del Logit son más consistentes con la realidad, incluso después de corregir por el azar.

Reflexion sobre ambos modelos

Con 299 pacientes y un desbalance moderado de clases (~32% de fallecidos), ambos modelos tienen condiciones razonables para el ajuste. Sin embargo, el modelo Logit presenta ventajas adicionales sobre el KNN en este contexto clínico:

  • Interpretabilidad clínica. El Logit produce odds ratios que permiten responder preguntas como: “¿en cuánto aumenta el riesgo de muerte por cada año adicional de edad?” o “¿cuánto reduce el riesgo una fracción de eyección más alta?”. El KNN, por su naturaleza no paramétrica, no ofrece este tipo de interpretaciones.

  • Sensibilidad prioritaria. En un contexto de tamizaje clínico, es mejor equivocarse alertando a un paciente que en realidad no tiene alto riesgo, que dejar de alertar a uno que sí va a morir. En ese sentido, el Logit prácticamente duplica la sensibilidad del KNN (66.7% vs 45.8%), lo que lo hace mucho más adecuado para este tipo de aplicación.

  • Estabilidad con muestras pequeñas. El KNN depende de que los vecinos más cercanos en el espacio de variables sean representativos, y con solo 224 pacientes en el conjunto de entrenamiento esa condición no se cumple del todo bien. El Logit, al ser un modelo paramétrico, maneja mejor este tipo de situaciones y es más estable cuando el tamaño de la muestra es moderado como en este caso.

  • Manejo de variables. El KNN es bastante sensible a variables que no aportan información útil o que tienen mucho ruido. Aunque en este taller se hizo una selección cuidadosa de las 5 variables del modelo, si se llegara a incluir alguna variable adicional irrelevante, el KNN se vería mucho más afectado que el Logit.


5. Conclusiones

En este taller se compararon dos modelos de aprendizaje supervisado — KNN y Regresión Logística — para predecir la mortalidad en pacientes con insuficiencia cardíaca, usando el Heart Failure Clinical Records Dataset con 299 pacientes y 5 variables clínicas como predictores.

En cuanto al problema, los resultados descriptivos confirmaron que existen diferencias clínicamente relevantes entre los pacientes fallecidos y los sobrevivientes. Los pacientes que murieron presentaron en promedio una fracción de eyección más baja (33.5% vs 40.3%), niveles más altos de creatinina sérica (1.84 vs 1.18 mg/dL) y niveles más bajos de sodio sérico, lo que es consistente con lo que la literatura clínica describe para pacientes con insuficiencia cardíaca severa. Esto indica que las variables seleccionadas tienen una base teórica sólida y mostraron poder discriminante desde el análisis exploratorio.

En cuanto a los modelos, el Logit resultó ser el mejor para este problema. Aunque el KNN obtuvo un AUC-ROC ligeramente mayor (0.857 vs 0.830), el Logit fue superior en prácticamente todas las métricas restantes: mayor accuracy (83.8% vs 81.1%), mayor Kappa (0.613 vs 0.504), mayor Balanced Accuracy (79.3% vs 71.9%) y, lo más importante, una sensibilidad mucho más alta (66.7% vs 45.8%). Esta última métrica es la más crítica en un contexto clínico, ya que un falso negativo — es decir, clasificar como sobreviviente a un paciente que en realidad va a fallecer — representa una oportunidad de intervención médica que se pierde. En términos concretos, de cada 3 pacientes que fallecen, el Logit detecta correctamente a 2, mientras que el KNN solo detecta a 1.

Además de su mejor desempeño predictivo, el Logit ofrece la ventaja de ser interpretable: los coeficientes del modelo mostraron que la edad, la fracción de eyección y la creatinina sérica son los predictores con mayor significancia estadística, mientras que la hipertensión y el sodio sérico, aunque relevantes clínicamente, no alcanzaron significancia en el modelo multivariado. Esta capacidad de interpretación es especialmente útil en el ámbito médico, donde no basta con que el modelo acierte, sino que también se necesita entender el porqué de la predicción.

Por lo tanto, se concluye que la Regresión Logística es el modelo más adecuado para este problema, tanto por su desempeño predictivo como por su utilidad clínica e interpretabilidad.


6. Bibliografía

Chicco, D., & Jurman, G. (2020). Machine learning can predict survival of patients with heart failure from serum creatinine and ejection fraction alone. BMC Medical Informatics and Decision Making, 20(16). https://doi.org/10.1186/s12911-020-1023-5

Dua, D., & Graff, C. (2019). UCI Machine Learning Repository. University of California, Irvine, School of Information and Computer Sciences. https://archive.ics.uci.edu/ml/index.php

Hilbert, M. (2011). The end justifies the definition: The manifold outlooks on the digital divide and their practical usefulness for policy-making. Telecommunications Policy, 35(8), 715–736. https://doi.org/10.1016/j.telpol.2011.06.012

Landis, J. R., & Koch, G. G. (1977). The measurement of observer agreement for categorical data. Biometrics, 33(1), 159–174. https://doi.org/10.2307/2529310

Organización Mundial de la Salud. (2021). Enfermedades cardiovasculares. https://www.who.int/es/news-room/fact-sheets/detail/cardiovascular-diseases-(cvds)