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.
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.
La variable dependiente es DEATH_EVENT,
una variable binaria que indica si el paciente falleció durante el
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.
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.
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.
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.
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:
# 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:
##
## Fallecido Sobreviviente
## 96 203
##
## 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
## Tamaño conjunto de prueba: 74
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)| 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.
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)| 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.
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.
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:
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.
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.
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
##
| 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.
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
| 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)| 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) ===
## 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) ===
## 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
##
| 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.
# 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:
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%.
| 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.
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.
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.
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)