title: “Modelo Analítico: Enfermedades Cardíacas” author: “Catalina Aguilera” date: “2026-10-18” output: html_document: toc: true toc_float: true toc_depth: 3 number_sections: true code_folding: hide theme: flatly df_print: paged —
Las enfermedades cardíacas representan un reto para la gestión de los servicios de salud debido a la necesidad de identificar oportunamente a los pacientes con mayor riesgo de complicaciones y mortalidad. El análisis de datos clínicos permite reconocer patrones, explorar factores asociados con los desenlaces y apoyar la planeación de los recursos sanitarios.
Este proyecto utiliza una base de registros clínicos de pacientes con insuficiencia cardíaca para desarrollar un modelo analítico que estime la probabilidad de mortalidad a partir de variables demográficas y clínicas.
La ausencia de herramientas analíticas que permitan identificar patrones asociados con la mortalidad puede dificultar la priorización del seguimiento clínico y la planeación de recursos. Por ello, se propone construir un modelo de regresión logística que explore la relación entre las características de los pacientes y el evento de muerte.
Construir y evaluar un modelo analítico de regresión logística para estimar la probabilidad de mortalidad en pacientes con insuficiencia cardíaca, utilizando variables clínicas y demográficas.
El modelo se evaluará mediante el área bajo la curva ROC (AUC), la sensibilidad, la especificidad y la exactitud de clasificación. Como referencia inicial, se buscará un AUC igual o superior a 0,70; este valor es un criterio exploratorio y no garantiza por sí solo que el modelo sea clínicamente útil.
library(readxl)
library(dplyr)
library(janitor)
library(skimr)
library(rsample)
library(caret)
library(pROC)
library(broom)
library(ggplot2)
library(knitr)
library(tidyr)
# Seleccionar el archivo real desde el explorador
archivo <- file.choose()
# Leer la base según su formato
extension <- tolower(tools::file_ext(archivo))
if (extension == "csv") {
datos <- read.csv(archivo, header = TRUE)
} else if (extension %in% c("xlsx", "xls")) {
datos <- read_excel(archivo)
} else {
stop("Formato no reconocido. Selecciona un archivo CSV o Excel válido.")
}
# Estandarizar nombres de columnas
datos <- clean_names(datos)
# Revisar la estructura
cat("Número de filas:", nrow(datos), "\n")
## Número de filas: 299
cat("Número de columnas:", ncol(datos), "\n")
## Número de columnas: 13
glimpse(datos)
## 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, …
| Variable | Tipo | Valores_faltantes | |
|---|---|---|---|
| age | age | numeric | 0 |
| anaemia | anaemia | integer | 0 |
| creatinine_phosphokinase | creatinine_phosphokinase | integer | 0 |
| diabetes | diabetes | integer | 0 |
| ejection_fraction | ejection_fraction | integer | 0 |
| high_blood_pressure | high_blood_pressure | integer | 0 |
| platelets | platelets | numeric | 0 |
| serum_creatinine | serum_creatinine | numeric | 0 |
| serum_sodium | serum_sodium | integer | 0 |
| sex | sex | integer | 0 |
| smoking | smoking | integer | 0 |
| time | time | integer | 0 |
| death_event | death_event | integer | 0 |
| Name | datos |
| 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 | ▇▁▁▁▃ |
La variable objetivo es death_event, que representa si
ocurrió el evento de muerte durante el seguimiento registrado. Las demás
variables se consideran candidatas para explicar o predecir este
desenlace.
if (!"death_event" %in% names(datos)) {
stop("No se encuentra la columna death_event. Verifica los nombres de la base.")
}
candidatas <- c(
"age",
"anaemia",
"creatinine_phosphokinase",
"diabetes",
"ejection_fraction",
"high_blood_pressure",
"platelets",
"serum_creatinine",
"serum_sodium",
"sex"
)
variables <- intersect(candidatas, names(datos))
if (length(variables) == 0) {
stop("No se encontraron las variables clínicas esperadas.")
}
cat("Variables disponibles:", paste(variables, collapse = ", "))
## Variables disponibles: age, anaemia, creatinine_phosphokinase, diabetes, ejection_fraction, high_blood_pressure, platelets, serum_creatinine, serum_sodium, sex
base_modelo <- datos %>%
select(all_of(c(variables, "death_event"))) %>%
mutate(
death_event = as.character(death_event),
death_event = trimws(death_event),
death_event = case_when(
death_event %in% c("0", "No", "no", "FALSE", "False") ~ "No",
death_event %in% c("1", "Si", "Sí", "si", "sí",
"TRUE", "True") ~ "Si",
TRUE ~ NA_character_
),
across(
all_of(variables),
~ suppressWarnings(as.numeric(as.character(.x)))
)
) %>%
filter(!is.na(death_event)) %>%
drop_na() %>%
mutate(
death_event = factor(death_event, levels = c("No", "Si"))
)
if (nrow(base_modelo) < 30) {
stop("Hay muy pocos registros válidos para continuar con este análisis.")
}
if (n_distinct(base_modelo$death_event) < 2) {
stop("La variable death_event debe contener las dos clases: No y Si.")
}
cat("Registros incluidos:", nrow(base_modelo), "\n")
## Registros incluidos: 299
cat("Registros excluidos:", nrow(datos) - nrow(base_modelo), "\n")
## Registros excluidos: 0
knitr::kable(
as.data.frame(table(base_modelo$death_event)),
col.names = c("Evento de mortalidad", "Número de pacientes"),
caption = "Distribución de la variable objetivo"
)
| Evento de mortalidad | Número de pacientes |
|---|---|
| No | 203 |
| Si | 96 |
Se separará la base en 75 % para entrenamiento y 25 % para prueba. La base de prueba se reservará para evaluar el desempeño con registros que no participaron en el ajuste.
set.seed(123)
division <- initial_split(
base_modelo,
prop = 0.75,
strata = death_event
)
entrenamiento <- training(division)
prueba <- testing(division)
cat("Registros de entrenamiento:", nrow(entrenamiento), "\n")
## Registros de entrenamiento: 224
cat("Registros de prueba:", nrow(prueba), "\n")
## Registros de prueba: 75
control <- trainControl(
method = "cv",
number = 5,
classProbs = TRUE,
summaryFunction = twoClassSummary
)
modelo <- train(
death_event ~ .,
data = entrenamiento,
method = "glm",
family = binomial(),
metric = "ROC",
trControl = control
)
print(modelo)
## Generalized Linear Model
##
## 224 samples
## 10 predictor
## 2 classes: 'No', 'Si'
##
## No pre-processing
## Resampling: Cross-Validated (5 fold)
## Summary of sample sizes: 178, 180, 178, 180, 180
## Resampling results:
##
## ROC Sens Spec
## 0.7715515 0.8948387 0.4542857
Los odds ratios (OR) muestran cómo se relacionan las variables con las probabilidades relativas del evento de muerte, manteniendo constantes las demás variables del modelo. Un OR superior a 1 indica una asociación positiva con el desenlace; un OR inferior a 1 indica una asociación negativa. Estas asociaciones no demuestran causalidad.
| Variable | OR | IC_inferior | IC_superior | Valor_p |
|---|---|---|---|---|
| age | 1.060 | 1.030 | 1.093 | 1.27e-04 |
| anaemia | 1.065 | 0.531 | 2.129 | 8.58e-01 |
| creatinine_phosphokinase | 1.000 | 1.000 | 1.000 | 4.71e-01 |
| diabetes | 0.985 | 0.496 | 1.955 | 9.66e-01 |
| ejection_fraction | 0.923 | 0.889 | 0.955 | 8.80e-06 |
| high_blood_pressure | 2.003 | 0.993 | 4.078 | 5.30e-02 |
| platelets | 1.000 | 1.000 | 1.000 | 1.64e-01 |
| serum_creatinine | 1.878 | 1.330 | 2.888 | 9.32e-04 |
| serum_sodium | 0.958 | 0.877 | 1.045 | 3.29e-01 |
| sex | 0.741 | 0.354 | 1.549 | 4.24e-01 |
probabilidades <- predict(
modelo,
newdata = prueba,
type = "prob"
)
predicciones <- predict(
modelo,
newdata = prueba
)
matriz <- confusionMatrix(
data = predicciones,
reference = prueba$death_event,
positive = "Si"
)
print(matriz)
## Confusion Matrix and Statistics
##
## Reference
## Prediction No Si
## No 42 13
## Si 9 11
##
## Accuracy : 0.7067
## 95% CI : (0.5902, 0.8062)
## No Information Rate : 0.68
## P-Value [Acc > NIR] : 0.3602
##
## Kappa : 0.2949
##
## Mcnemar's Test P-Value : 0.5224
##
## Sensitivity : 0.4583
## Specificity : 0.8235
## Pos Pred Value : 0.5500
## Neg Pred Value : 0.7636
## Prevalence : 0.3200
## Detection Rate : 0.1467
## Detection Prevalence : 0.2667
## Balanced Accuracy : 0.6409
##
## 'Positive' Class : Si
##
| Indicador | Resultado |
|---|---|
| AUC | 0.735 |
| Exactitud | 0.707 |
| Sensibilidad | 0.458 |
| Especificidad | 0.824 |
El modelo de regresión logística permite explorar la relación entre las características clínicas y demográficas de los pacientes y la ocurrencia de mortalidad. El desempeño debe analizarse con las métricas obtenidas en la base de prueba, especialmente el AUC, la sensibilidad y la especificidad.
Desde la gestión sanitaria, los resultados pueden orientar la identificación de grupos que podrían requerir seguimiento prioritario, la planeación de controles clínicos y la asignación de recursos para la atención integral. Las recomendaciones deben basarse en los resultados reales del análisis y en la valoración de los profesionales de salud.
El modelo tiene carácter exploratorio. Antes de utilizarlo para apoyar decisiones clínicas, se requiere validación externa, revisión de la calidad de los datos y evaluación de su utilidad en la población objetivo. La predicción estadística no sustituye el criterio clínico.