Exploración, inferencia y modelado estadístico de la presencia de enfermedad cardiaca: un análisis del dataset Heart Disease con R

El presente documento es el informe integrador del proyecto de exploración, inferencia y modelado estadístico, utilizando el dataset Heart, que contiene información clínica y demográfica de 303 pacientes evaluados por posible enfermedad cardiaca.

Introducción

Este proyecto tiene como objetivo general construir e interpretar modelos estadísticos sencillos, uno de regresión lineal múltiple y uno de regresión logística múltiple, que permitan describir la relación entre variables clínicas de un grupo de pacientes. Nos enfocamos en estudiar la frecuencia cardiaca máxima alcanzada durante una prueba de esfuerzo, usando un modelo de regresión lineal, y la presencia o ausencia de enfermedad cardiaca, en un modelo de regresión lineal.

El análisis de datos puede ser relevante en problemas clínicos como éste, ya que permite identificar de manera sistemática y reproducible qué variables se asocian con una condición de salud, buscando evidencia estadística que respalde la existencia de esas asociaciones. Es importante especificar que en este tipo de análisis tienen limitaciones y que debe evitarse caer en afirmaciones diagnósticas o de causalidad.

Metodología

El desarrollo del proyecto se organizó en cuatro etapas. En la primera se trabajó con el conjunto de datos original, identificando inconsistencias en los tipos de variable, dando tratamiento a los valores faltantes encontrado y generando una versión limpia de la base. En la segunda se realizó un análisis exploratorio (univariado, bivariado y multivariado), utilizando estadísticas descriptivas y visualizaciones, con el objetivo de identificar qué variables podrían relacionarse entre sí y con la enfermedad cardiaca. En la tercera etapa buscamos obtener evidencia estadística que respaldara la asociación entre las variables, mediante intervalos de confianza, pruebas de hipótesis (t de Student, Wilcoxon y chi-cuadrada) y analizando la correlación. Esto permitió realizar una selección de variables candidatas para el modelado. En la cuarta etapa se plantearon y compararon distintos modelos de regresión lineal múltiple (con la frecuencia cardiaca máxima como variable respuesta) y de un modelo de regresión logística múltiple (con la presencia de enfermedad cardiaca como respuesta), interpretando sus coeficientes y evaluando su desempeño de forma básica.

El proyecto se organizó siguiendo, de manera adaptada, las etapas de la metodología CRISP-DM (Cross Industry Standard Process for Data Mining). La comprensión del negocio corresponde al planteamiento del problema descrito arriba: entender qué se busca explicar, en este caso la presencia de enfermedad cardiaca y, secundariamente, la capacidad funcional cardiovascular. La comprensión de los datos se llevó a cabo principalmente en la primera etapa.

La preparación de los datos también se realizó en esa etapa (limpieza inicial, tratamiento de valores faltantes y recodificación de tipos de variables). El modelado, por su parte, se concentró en la última etapa, pero se apoyó en el trabajo de comprensión y preparación de los datos y, sobre todo, en el análisis inferencial y de selección de variables realizado en la tercera etapa. La evaluación de los modelos se hizo de forma básica, comparando los valores de R² y AIC (en el caso lineal) o su AIC, devianza y exactitud de clasificación (en el caso logístico). El presente informe constituye la etapa de comunicación del proyecto.

Problema abordado

El dataset Heart Disease contiene 303 observaciones, correspondientes a pacientes evaluados por posible presencia de enfermedad cardiaca. La base incluye variables relacionadas con características personales, resultados de mediciones médicas, respuestas a pruebas de esfuerzo y algunos indicadores clínicos utilizados como apoyo en la evaluación cardiovascular.

Cada fila del conjunto de datos representa a un paciente y cada columna corresponde a una variable observada. Se cuenta con 14 variables predictoras de tipo clínico y demográfico: edad, sexo, tipo de dolor torácico, presión arterial en reposo, colesterol sérico, glucosa en ayunas, resultado electrocardiográfico en reposo, frecuencia cardiaca máxima alcanzada, presencia de angina inducida por ejercicio, depresión del segmento ST, pendiente del segmento ST, número de vasos principales observados por fluoroscopía y resultado de una prueba de talasemia, además de la variable ahd, la cual indica si el paciente presenta o no enfermedad cardiaca.

Los modelos generados consideran dos variables objetivo distintas y complementarias. La de interés inicial fue ahd, misma que motivó el trabajo de selección de variables en la segunda etapa, utilizada para plantear un problema de clasificación mediante regresión logística. La frecuencia cardiaca máxima, max_hr, se utilizó como variable respuesta de un modelo de regresión lineal múltiple, debido a que se identificó que se relaciona tanto con variables demográficas (la edad) como con otras variables clínicas (la depresión del segmento ST) y con la propia presencia de enfermedad cardiaca.

El propósito académico de los análisis realizados fue poner en práctica las fases iniciales de la metodología CRISP-DM; incluyendo limpieza, exploración, inferencia formal, modelado, interpretación de resultados y comunicación de los hallazgos.

Datos utilizados, limpieza y transformación

heart <- read_csv("a1_Ruiz_Perla_limpia.csv")

heart$sex <- factor(heart$sex, levels = 0:1)
heart$fbs <- factor(heart$fbs, levels = 0:1)
heart$rest_ecg <- factor(heart$rest_ecg, levels = 0:2)
heart$ex_ang <- factor(heart$ex_ang, levels = 0:1)
heart$slope <- factor(heart$slope, levels = 1:3)

heart <- heart |>
  mutate(
    chest_pain = factor(
      chest_pain,
      levels = c("typical", "nontypical", "nonanginal", "asymptomatic"),
      labels = c("Typical", "Nontypical", "Nonanginal", "Asymptomatic")
    ),
    ahd = factor(
      ahd,
      levels = c("no", "yes"),
      labels = c("Sin_enfermedad", "Con_enfermedad")
    ),
    thal = factor(
      thal,
      levels = c("normal", "fixed", "reversable", "sd"),
      labels = c("Normal", "Fixed", "Reversable", "Sin dato")
    )
  )

heart <- heart |>
  select(-x1)

glimpse(heart)
## Rows: 303
## Columns: 14
## $ age        <dbl> 63, 67, 67, 37, 41, 56, 62, 57, 63, 53, 57, 56, 56, 44, 52,~
## $ sex        <fct> 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1,~
## $ chest_pain <fct> Typical, Asymptomatic, Asymptomatic, Nonanginal, Nontypical~
## $ rest_bp    <dbl> 145, 160, 120, 130, 130, 120, 140, 120, 130, 140, 140, 140,~
## $ chol       <dbl> 233, 286, 229, 250, 204, 236, 268, 354, 254, 203, 192, 294,~
## $ fbs        <fct> 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0,~
## $ rest_ecg   <fct> 2, 2, 2, 0, 2, 0, 2, 0, 2, 2, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0,~
## $ max_hr     <dbl> 150, 108, 129, 187, 172, 178, 160, 163, 147, 155, 148, 153,~
## $ ex_ang     <fct> 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,~
## $ oldpeak    <dbl> 2.3, 1.5, 2.6, 3.5, 1.4, 0.8, 3.6, 0.6, 1.4, 3.1, 0.4, 1.3,~
## $ slope      <fct> 3, 2, 2, 3, 1, 1, 3, 1, 2, 3, 2, 2, 2, 1, 1, 1, 3, 1, 1, 1,~
## $ ca         <dbl> 0, 3, 2, 0, 0, 0, 2, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,~
## $ thal       <fct> Fixed, Normal, Reversable, Normal, Normal, Normal, Normal, ~
## $ ahd        <fct> Sin_enfermedad, Con_enfermedad, Con_enfermedad, Sin_enferme~
colSums(is.na(heart))
##        age        sex chest_pain    rest_bp       chol        fbs   rest_ecg 
##          0          0          0          0          0          0          0 
##     max_hr     ex_ang    oldpeak      slope         ca       thal        ahd 
##          0          0          0          0          0          0          0

El archivo Heart.csv contiene 303 observaciones y 15 columnas, incluyendo una columna de índice sin significado clínico (x1) que se eliminó. De las 14 variables restantes, seis son numéricas: la edad en años (age), la presión arterial en reposo en mmHg (rest_bp), el colesterol sérico en mg/dL (chol), la frecuencia cardiaca máxima alcanzada en la prueba de esfuerzo (max_hr), la depresión del segmento ST inducida por el ejercicio (oldpeak) y el número de vasos principales observados mediante fluoroscopía (ca, numérica discreta). Las ocho variables restantes son categóricas y tuvieron que codificarse como tal después de importar los datos: el sexo (sex), el tipo de dolor torácico (chest_pain, con cuatro categorías), la glucosa en ayunas mayor a 120 mg/dL (fbs), el resultado electrocardiográfico en reposo (rest_ecg, tres niveles), la angina inducida por ejercicio (ex_ang), la pendiente del segmento ST (slope), el resultado de la prueba de talasemia (thal, con cuatro categorías incluyendo un nivel para registros sin dato) y, por supuesto, la variable objetivo ahd.

En cuanto a valores faltantes, encontramos únicamente 4 valores faltantes en la variable ca y 2 valores faltantes en thal . Dado que eran muy pocos valores faltantes, no se utilizó una estrategia de imputación más elaborada. A los dos registros sin dato en thal se les asignó la categoría “sd” (sin dato), tratándola como un nivel adicional de la variable en vez de eliminarla, mientras que a los cuatro registros sin dato en ca, al tratarse de una variable numérica discreta, se les asignó la moda de la variable (el valor 0, el más frecuente con 176 de 303 observaciones). Adicionalmente, se limpiaron las variables de texto (chest_pain, thal, ahd) recortando espacios en blanco y convirtiéndolas a minúsculas.

Análisis exploratorio y visualización de datos

En el análisis exploratorio, se identificó que en las variables numéricas:

  • La variable objetivo ahd se distribuye de forma balanceada: 164 pacientes (54.1%) no presentan enfermedad cardiaca y 139 (45.9%) sí la presentan.
  • La edad promedio de los pacientes es de 54.4 años (desviación estándar de 9.0), con un rango de 29 a 77 años.
  • La presión arterial en reposo promedia 131.7 mmHg (desviación de 17.6).
  • El colesterol sérico promedia 246.7 mg/dL (desviación de 51.8, la mayor dispersión relativa de todas las variables numéricas)
  • La frecuencia cardiaca máxima promedia 149.6 latidos por minuto (desviación de 22.9)
  • La depresión del segmento ST promedia 1.04 (desviación de 1.16, con una distribución muy asimétrica hacia la derecha).
  • El número de vasos observados tiene una media de 0.66, con la mayoría de los pacientes (59.4%) sin vasos afectados.

Entre las variables categóricas, se observó que:

-El tipo de dolor torácico más frecuente es el asintomático (47.5% de los pacientes), seguido del no anginal (28.4%), el no típico (16.5%) y el típico (7.6%). - El 67.8% de los pacientes son hombres. - Sólo el 14.9% tiene glucosa en ayunas elevada. - El 32.7% presenta angina inducida por ejercicio. - En el resultado de talasemia, 55% de los pacientes tiene un resultado normal, 38% reversible y el resto fijo, con dos registros sin dato disponible. - El colesterol es la variable con mayor dispersión absoluta (rango de 438 unidades, desviación estándar de 51.8). - La depresión del segmento ST presenta la distribución más asimétrica: más de la mitad de los pacientes tiene valores por debajo de 1, con una cola de valores altos (hasta 6.2). - La edad y el colesterol tienen distribuciones aproximadamente simétricas. - La frecuencia cardiaca máxima presenta un ligero sesgo a la izquierda y la depresión del segmento ST un sesgo marcado a la derecha.

Los diagramas de caja individuales identificaron valores atípicos puntuales: colesterol por encima de 400 mg/dL, presión arterial en reposo por encima de 170 mmHg y un valor mínimo de frecuencia cardiaca máxima de 71 latidos por minuto, claramente separado del resto de la distribución.

Al agrupar las variables numéricas según la presencia de enfermedad cardiaca aparecieron las primeras diferencias relevantes entre grupos. La edad promedio es de 56.6 años en los pacientes con enfermedad frente a 52.6 años en los que no la tienen; la frecuencia cardiaca máxima promedia 158.4 en el grupo sin enfermedad frente a un valor notablemente menor en el grupo con enfermedad; y la depresión del segmento ST muestra la diferencia más marcada de las cuatro variables numéricas exploradas, con valores sistemáticamente mayores en los pacientes enfermos. El colesterol, en cambio, mostró diferencias más leves y menos consistentes entre grupos. Estas diferencias se visualizaron con diagramas de caja separados por diagnóstico, como el de la frecuencia cardiaca máxima y el de la depresión del segmento ST que se muestran a continuación.

ggplot(heart, aes(x = max_hr, y = ahd)) +
  geom_boxplot() +
  labs(
    title = "Distribución de la FCM por diagnóstico de enfermedad cardiaca",
    x = "Frecuencia cardiaca máxima",
    y = "Diagnóstico"
  ) +
  theme_minimal()

En esta gráfica se aprecia cómo los pacientes sin enfermedad alcanzan, en general, una mayor frecuencia cardiaca máxima durante la prueba de esfuerzo que los pacientes con enfermedad, aunque hay traslape entre los rangos intercuartílicos de ambos grupos; también se identifican algunos valores atípicos inferiores, principalmente en el grupo sin enfermedad.

ggplot(heart, aes(x = oldpeak, y = ahd)) +
  geom_boxplot() +
  labs(
    title = "Distribución de la depresión del segmento ST por diagnóstico",
    x = "Depresión del segmento ST",
    y = "Diagnóstico"
  ) +
  theme_minimal()

Aquí la diferencia entre grupos es más clara: la cuarta parte de los pacientes con enfermedad tiene valores por debajo de 0.5, mientras que en los pacientes sin enfermedad esa proporción es más del doble, lo que sugiere una asociación más fuerte entre esta variable y el diagnóstico que la observada para el colesterol.

En cuanto a las variables categóricas, las tablas de frecuencias mostraron que el dolor torácico asintomático es, con mucho, el más común entre los pacientes con enfermedad, mientras que los otros tres tipos de dolor predominan entre los pacientes sanos; algo similar ocurre con la angina inducida por ejercicio, presente en una proporción mucho mayor de pacientes enfermos que sanos, y con el resultado de la prueba de talasemia, donde la categoría reversible concentra la mayor proporción de casos con enfermedad. La matriz de correlación entre las variables numéricas mostró, en general, relaciones débiles a moderadas: la correlación más fuerte observada fue la negativa entre edad y frecuencia cardiaca máxima (-0.39), seguida de la positiva entre edad y número de vasos observados (0.37) y de la negativa entre frecuencia cardiaca máxima y depresión del segmento ST (-0.34); ninguna correlación fue lo suficientemente alta como para sugerir redundancia extrema entre variables, aunque sí se identificó a la edad y la frecuencia cardiaca máxima como un par a vigilar en modelos multivariados posteriores.

heart_num <- heart |>
  select(age, rest_bp, chol, max_hr, oldpeak, ca) |>
  drop_na()

correlaciones <- cor(heart_num)
round(correlaciones, 2)
##           age rest_bp chol max_hr oldpeak    ca
## age      1.00    0.28 0.21  -0.39    0.20  0.37
## rest_bp  0.28    1.00 0.13  -0.05    0.19  0.10
## chol     0.21    0.13 1.00   0.00    0.05  0.12
## max_hr  -0.39   -0.05 0.00   1.00   -0.34 -0.27
## oldpeak  0.20    0.19 0.05  -0.34    1.00  0.30
## ca       0.37    0.10 0.12  -0.27    0.30  1.00

En conjunto, el análisis exploratorio señaló que las variables con diferencias más marcadas entre pacientes con y sin enfermedad cardiaca fueron la edad, la frecuencia cardiaca máxima y la depresión del segmento ST entre las numéricas, y el tipo de dolor torácico, la angina inducida por ejercicio y el resultado de talasemia entre las categóricas; el colesterol mostró diferencias más leves y menos consistentes. La mayor dispersión relativa se observó en el colesterol y, en términos de forma de la distribución, en la depresión del segmento ST. Los valores atípicos más notorios se encontraron en el colesterol, la presión arterial en reposo y, de forma puntual, en la frecuencia cardiaca máxima. Con esta base, en la siguiente etapa del análisis se sometieron estas impresiones exploratorias a pruebas estadísticas formales.

Análisis inferencial y selección de variables

La etapa 3 comenzó calculando intervalos de confianza al 95% para la media de las variables numéricas principales. Para toda la muestra, la edad promedio se ubica entre 53.4 y 55.5 años, la frecuencia cardiaca máxima entre 147.0 y 152.2, la depresión del segmento ST entre 0.91 y 1.17, y el colesterol entre 240.8 y 252.5 mg/dL. Al calcular estos mismos intervalos por separado para los grupos con y sin enfermedad, se observó que los intervalos de edad, frecuencia cardiaca máxima y depresión del segmento ST no se traslapan entre grupos, lo que sugiere diferencias relevantes, mientras que los intervalos de colesterol sí se traslapan, indicando que la diferencia entre grupos es menos clara para esta variable; en todo caso, quedó explícito que la falta de traslape entre intervalos de confianza no equivale, por sí sola, a una prueba de hipótesis formal.

Las pruebas de hipótesis confirmaron en general estas impresiones. La prueba t para la edad, que se distribuye de forma razonablemente simétrica, arrojó una diferencia de medias estadísticamente significativa entre pacientes con y sin enfermedad (con un intervalo de confianza para la diferencia que no incluye el cero). Para la depresión del segmento ST y la frecuencia cardiaca máxima, cuyas distribuciones son asimétricas y con valores atípicos, se utilizó la prueba de Wilcoxon en lugar de la prueba t, ya que no depende del supuesto de normalidad y es más robusta ante ese tipo de distribuciones; ambas variables mostraron diferencias significativas entre grupos (p < 0.05). El colesterol, evaluado también con Wilcoxon por la presencia de un valor atípico importante, mostró igualmente una diferencia significativa, a pesar de que sus intervalos de confianza se traslapaban, lo que confirma que la prueba de hipótesis formal puede detectar diferencias que el simple traslape de intervalos no deja ver con claridad. En conjunto, de las variables numéricas evaluadas, oldpeak, max_hr y age fueron las que mostraron la evidencia estadística más sólida (los valores p y estadísticos más grandes), seguidas por chol.

t.test(age ~ ahd, data = heart)
## 
##  Welch Two Sample t-test
## 
## data:  age by ahd
## t = -4.0303, df = 300.93, p-value = 7.061e-05
## alternative hypothesis: true difference in means between group Sin_enfermedad and group Con_enfermedad is not equal to 0
## 95 percent confidence interval:
##  -6.013385 -2.067682
## sample estimates:
## mean in group Sin_enfermedad mean in group Con_enfermedad 
##                     52.58537                     56.62590
wilcox.test(oldpeak ~ ahd, data = heart)
## 
##  Wilcoxon rank sum test with continuity correction
## 
## data:  oldpeak by ahd
## W = 6037, p-value = 6.813e-13
## alternative hypothesis: true location shift is not equal to 0
wilcox.test(max_hr ~ ahd, data = heart)
## 
##  Wilcoxon rank sum test with continuity correction
## 
## data:  max_hr by ahd
## W = 16990, p-value = 1.861e-13
## alternative hypothesis: true location shift is not equal to 0

Para las variables categóricas se utilizó la prueba ji-cuadrada de independencia, que compara las frecuencias observadas en una tabla de contingencia contra las que se esperarían si la variable categórica y ahd fueran independientes. El tipo de dolor torácico mostró una asociación muy fuerte con la enfermedad (estadístico de 81.8, p < 2.2e-16), con el dolor asintomático concentrando el 73% de los pacientes enfermos que lo presentan; el sexo también mostró una asociación significativa (p ≈ 2.7e-06); la angina inducida por ejercicio mostró una asociación clara (p < 0.001), con el 76% de quienes la presentan diagnosticados con enfermedad; y el resultado de la prueba de talasemia mostró la asociación más fuerte de todas las variables categóricas evaluadas (p ≈ 2.2e-16), aunque con la advertencia de que dos de sus cuatro categorías (fija y “sin dato”) tienen pocos casos, lo que puede afectar la estabilidad de la prueba. La glucosa en ayunas, en cambio, no mostró asociación significativa con la enfermedad (p ≈ 0.78), por lo que se descartó como variable relevante, al igual que el resultado electrocardiográfico en reposo.

tabla_chest_ahd <- table(heart$chest_pain, heart$ahd)
chisq.test(tabla_chest_ahd)
## 
##  Pearson's Chi-squared test
## 
## data:  tabla_chest_ahd
## X-squared = 81.816, df = 3, p-value < 2.2e-16
tabla_thal <- table(heart$thal, heart$ahd)
chisq.test(tabla_thal)
## 
##  Pearson's Chi-squared test
## 
## data:  tabla_thal
## X-squared = 83.296, df = 3, p-value < 2.2e-16

Con toda esta evidencia (exploratoria, inferencial y de correlación) llegué a la selección final de variables para el modelado: ahd, oldpeak, max_hr y chest_pain. Las tres últimas mostraron, de manera consistente, la evidencia estadística más fuerte de asociación con la presencia de enfermedad cardiaca; la edad, aunque también mostró evidencia fuerte, se dejó fuera de esta selección principal por su correlación con max_hr (r = -0.39), que podría generar inestabilidad en los coeficientes de un modelo multivariado si ambas variables se incluyen juntas. La variable thal, a pesar de su fuerte asociación estadística, se descartó por los problemas de registros sin dato y categorías con pocos casos ya mencionados; chol y rest_bp se consideraron variables secundarias, con evidencia más débil o menos consistente; y fbs y rest_ecg se descartaron por no mostrar asociación clara con la enfermedad.

Modelado estadístico básico

En la etapa 4 se construyeron modelos de regresión lineal múltiple, con max_hr como variable respuesta, y de regresión logística múltiple, con ahd como variable respuesta, retomando en ambos casos la selección de variables de la etapa 3, aunque en el caso lineal fue necesario redistribuir esas variables, ya que max_hr deja de ser predictora para convertirse en respuesta.

El primer modelo lineal (modelo_lm_1) explica max_hr a partir de age, oldpeak y ahd. Las tres variables resultaron altamente significativas (p < 0.003 en los tres casos) y con coeficientes negativos: cada año adicional de edad se asocia con una disminución de 0.75 latidos por minuto en la frecuencia cardiaca máxima; cada unidad adicional de depresión del segmento ST se asocia con una disminución de 3.21 latidos por minuto; y los pacientes con enfermedad cardiaca alcanzan, en promedio, casi 13 latidos por minuto menos que los pacientes sin enfermedad, siendo éste el coeficiente de mayor magnitud del modelo. El modelo explica alrededor del 29.1% de la variabilidad de max_hr (R² ajustada ≈ 28.3%), una proporción moderada pero razonable considerando el número reducido de predictoras. Al revisar los residuales no se observaron patrones evidentes de curvatura ni heterocedasticidad marcada, aunque sí algunos residuales grandes (superiores a 50 latidos por minuto en valor absoluto, en su mayoría negativos), lo que indica que existen pacientes cuya frecuencia cardiaca real es notablemente menor a la predicha por el modelo, posiblemente por factores no incluidos (por ejemplo, el uso de medicamentos que reducen directamente la frecuencia cardiaca).

modelo_lm_1 <- lm(max_hr ~ age + oldpeak + ahd, data = heart)
tidy(modelo_lm_1, conf.int = TRUE)
## # A tibble: 4 x 7
##   term              estimate std.error statistic  p.value conf.low conf.high
##   <chr>                <dbl>     <dbl>     <dbl>    <dbl>    <dbl>     <dbl>
## 1 (Intercept)        200.        6.82      29.3  7.10e-90   186.     213.   
## 2 age                 -0.754     0.127     -5.91 9.12e- 9    -1.00    -0.503
## 3 oldpeak             -3.21      1.07      -3.00 2.89e- 3    -5.31    -1.11 
## 4 ahdCon_enfermedad  -12.9       2.50      -5.17 4.25e- 7   -17.8     -7.99
glance(modelo_lm_1) |> select(r.squared, adj.r.squared, AIC, BIC)
## # A tibble: 1 x 4
##   r.squared adj.r.squared   AIC   BIC
##       <dbl>         <dbl> <dbl> <dbl>
## 1     0.291         0.283 2662. 2680.

Como segunda versión ajusté un modelo más amplio (modelo_lm_2), agregando sex, rest_bp y chol a las tres variables anteriores, ya que en el análisis exploratorio se habían identificado como variables con cierto potencial explicativo para max_hr, aunque con evidencia más débil. Este segundo modelo mejora ligeramente el ajuste (R² ≈ 0.314, AIC de 2657.29 frente a 2661.65 del modelo más simple), pero dos de sus seis predictoras, sex y chol, no resultan estadísticamente significativas, y el comportamiento de los residuales no mejora de forma clara respecto al primer modelo. Ante una mejora tan modesta en el ajuste, y considerando el principio de parsimonia, decidí conservar el modelo más simple (modelo_lm_1) como primera aproximación para estimar la frecuencia cardiaca máxima promedio. De forma exploratoria, también utilicé la función step() para una selección automática de variables por AIC a partir de todas las variables disponibles; el resultado sugirió un modelo distinto al que yo había elegido manualmente (con variables adicionales como el tipo de dolor torácico, la presión arterial, el colesterol, la angina inducida, la pendiente del segmento ST y la propia condición de enfermedad), lo que ilustra que un criterio puramente estadístico de selección automática no siempre coincide con una selección de variables guiada por la evidencia inferencial previa y por consideraciones de interpretabilidad.

Para el modelo logístico, la variable respuesta se recodificó como ahd_bin (1 para pacientes con enfermedad, 0 en caso contrario). El primer modelo logístico (modelo_log_1) utiliza oldpeak, max_hr y chest_pain como predictoras, exactamente la selección de la etapa 3. Los momios (odds) de tener enfermedad cardiaca se multiplican, en promedio, por 2.13 por cada unidad adicional de depresión del segmento ST, y por 0.976 por cada latido adicional en la frecuencia cardiaca máxima (es decir, disminuyen ligeramente); en cuanto al dolor torácico, un paciente con dolor asintomático tiene, en promedio, siete veces más momios de enfermedad que uno con dolor típico, mientras que las categorías de dolor no típico y no anginal no mostraron diferencias significativas frente a la categoría de referencia. El modelo alcanzó una exactitud de clasificación del 77.2% sobre los mismos datos con los que fue ajustado (234 de 303 pacientes correctamente clasificados, con 41 falsos negativos y 28 falsos positivos), y un AIC de 294.4.

heart <- heart |>
  mutate(ahd_bin = if_else(ahd == "Con_enfermedad", 1, 0))

modelo_log_1 <- glm(
  ahd_bin ~ oldpeak + max_hr + chest_pain,
  data = heart,
  family = binomial
)
tidy(modelo_log_1, exponentiate = TRUE, conf.int = TRUE)
## # A tibble: 6 x 7
##   term                   estimate std.error statistic p.value conf.low conf.high
##   <chr>                     <dbl>     <dbl>     <dbl>   <dbl>    <dbl>     <dbl>
## 1 (Intercept)               5.81    1.29        1.37  1.71e-1    0.467    73.9  
## 2 oldpeak                   2.13    0.164       4.62  3.91e-6    1.56      2.98 
## 3 max_hr                    0.976   0.00731    -3.33  8.62e-4    0.962     0.990
## 4 chest_painNontypical      1.39    0.660       0.494 6.22e-1    0.385     5.26 
## 5 chest_painNonanginal      0.878   0.584      -0.223 8.23e-1    0.287     2.90 
## 6 chest_painAsymptomatic    7.10    0.564       3.48  5.06e-4    2.45     22.9
glance(modelo_log_1) |> select(AIC, BIC, deviance, null.deviance)
## # A tibble: 1 x 4
##     AIC   BIC deviance null.deviance
##   <dbl> <dbl>    <dbl>         <dbl>
## 1  294.  317.     282.          418.

Como segunda versión del modelo logístico probé una combinación distinta de predictoras: ex_ang, ca, rest_bp y sex (modelo_log_2). La justificación fue, por un lado, sustituir oldpeak por ex_ang, ya que ambas se refieren a condiciones medidas durante la prueba de esfuerzo y ex_ang guarda cierto paralelismo conceptual con chest_pain; por otro lado, incorporar rest_bp como contraste con lo observado en esfuerzo, ca por ser un indicador directo de afectación vascular, y sex por reflejar diferencias poblacionales relevantes en los rangos normales de algunas mediciones clínicas. Este segundo modelo obtuvo un AIC de 283.3 (menor que el de modelo_log_1) y una exactitud de clasificación ligeramente mayor, de 77.9%, con todos sus coeficientes estadísticamente significativos, a diferencia de modelo_log_1, donde dos de las categorías de chest_pain no lo eran. Aunque ambos modelos tienen un número similar de parámetros (modelo_log_1 incluye cinco coeficientes más el intercepto, por las cuatro categorías de chest_pain, y modelo_log_2 incluye cuatro, por tratarse de variables binarias o casi binarias), considero que modelo_log_2 ofrece un mejor balance entre ajuste, significancia estadística de sus coeficientes e interpretabilidad, por lo que lo conservaría como primera aproximación, sin dejar de reconocer que la selección de variables de la etapa 3 (oldpeak, max_hr, chest_pain) seguía siendo informativa y estadísticamente defendible, aunque no resultó ser, en este caso, la que mejor ajuste ofreció entre las combinaciones exploradas.

En ambos tipos de modelo, lineal y logístico, es importante señalar que se ajustaron y evaluaron sobre la misma muestra que se usó para entrenarlos, sin un conjunto de prueba independiente, por lo que las medidas de desempeño reportadas (R², exactitud, AIC) reflejan qué tan bien los modelos describen a estos 303 pacientes, no qué tan bien generalizarían a pacientes nuevos.

Resultados principales

El análisis exploratorio permitió identificar, desde una etapa temprana del proyecto, que la edad, la frecuencia cardiaca máxima y la depresión del segmento ST son las variables numéricas con diferencias más marcadas entre pacientes con y sin enfermedad cardiaca, y que el tipo de dolor torácico, la angina inducida por ejercicio y el resultado de la prueba de talasemia son las variables categóricas con asociaciones visuales más claras. El análisis inferencial confirmó estadísticamente todas estas impresiones (con pruebas t, Wilcoxon y ji-cuadrada, todas con p < 0.05 para las variables mencionadas) y permitió, además, descartar formalmente a la glucosa en ayunas y al resultado electrocardiográfico en reposo como variables relevantes para explicar la enfermedad, algo que en el EDA sólo se había intuido. Las variables finalmente seleccionadas para el modelado, ahd, oldpeak, max_hr y chest_pain, resultaron ser, en efecto, informativas: las tres funcionaron como predictoras estadísticamente significativas del modelo logístico principal, y oldpeak y ahd (junto con la edad, reincorporada en esta etapa) resultaron igualmente relevantes para explicar max_hr en el modelo lineal.

En cuanto al modelado, el mejor modelo lineal explorado (modelo_lm_2, con seis predictoras) explica alrededor del 31.4% de la variabilidad de la frecuencia cardiaca máxima, aunque el modelo más simple de tres predictoras (modelo_lm_1) ofrece un ajuste casi idéntico (29.1%) con menos variables y mayor interpretabilidad, por lo que se prefirió este último. En el caso logístico, el modelo con ex_ang, ca, rest_bp y sex (modelo_log_2) superó ligeramente, en AIC y exactitud, al modelo con las variables originalmente seleccionadas en la etapa 3 (modelo_log_1), alcanzando una exactitud de clasificación de 77.9% frente a 77.2%, lo que sugiere que, si bien la selección de variables basada en pruebas de hipótesis fue un buen punto de partida, no necesariamente identificó la combinación óptima de predictoras para el modelo final.

Problemas encontrados durante el desarrollo

El problema más relevante detectado durante el desarrollo del proyecto fue un error de codificación en la variable slope, cometido en la etapa 1. El documento de descripción del dataset sugería que esta variable tomaba valores de 0 a 2, y con base en ello la codifiqué como factor(slope, levels = 0:2); sin embargo, los datos crudos en realidad contienen los valores 1, 2 y 3, sin ningún caso con valor 0. Al restringir los niveles válidos de 0 a 2, los 21 registros con valor 3 (cerca del 7% de la muestra) se convirtieron silenciosamente en valores faltantes, y como ese archivo ya recodificado fue el que se exportó a .csv y el que alimentó todas las actividades posteriores, la pérdida de esos 21 valores se propagó a lo largo de todo el proyecto sin posibilidad de recuperación, más allá de volver a partir del archivo Heart.csv original. En actividades posteriores ajusté el rango de niveles utilizado al recodificar slope (primero a 0:3 y finalmente a 1:3, que es el rango realmente observado en los datos), pero esos ajustes sólo corrigieron la definición de las categorías válidas hacia adelante; no pudieron recuperar los 21 valores ya perdidos desde la etapa 1. Afortunadamente, slope no llegó a formar parte de las variables predictoras de ninguno de los modelos finales, por lo que este error no comprometió los resultados del modelado, pero sí constituye una lección clara sobre la importancia de revisar los conteos de una variable categórica inmediatamente después de recodificarla, y no sólo confiar en la documentación de origen del dataset.

Un segundo problema, de naturaleza más recurrente que grave, fue que el formato .csv no conserva la información de tipo factor de R: cada vez que se reimportó el archivo limpio en una nueva etapa fue necesario reaplicar manualmente la conversión a factor de las variables categóricas (sex, fbs, rest_ecg, ex_ang, slope, chest_pain, thal y ahd), repitiendo el mismo bloque de código en cada documento. Esto no generó errores en los resultados, pero sí fue una fuente de trabajo repetitivo que, en un proyecto de mayor escala, convendría resolver guardando la base limpia en un formato que sí preserve los tipos de dato, como .rds.

También surgieron dificultades de interpretación relacionadas con variables categóricas de varios niveles, en particular chest_pain y thal. Al tener más de dos categorías, cada modelo que las incluye requiere interpretar cada categoría por separado en comparación con una categoría de referencia, lo que resulta menos directo que interpretar una variable numérica o binaria; además, no todas las categorías resultaron informativas (por ejemplo, las categorías “no típico” y “no anginal” de chest_pain no se distinguieron claramente de la categoría de referencia en el modelo logístico), lo que obliga a matizar las conclusiones categoría por categoría en lugar de hacer afirmaciones generales sobre la variable completa. De forma relacionada, la advertencia identificada en la etapa 3 sobre la posible colinealidad entre age y max_hr (correlación de -0.39) requirió tomar decisiones explícitas sobre qué variable incluir en cada modelo, dependiendo de si max_hr era la variable respuesta o una posible predictora, decisiones que fueron revisadas más de una vez a lo largo del proyecto conforme se compararon distintas versiones de los modelos.

Conclusiones

De este dataset aprendí que, aunque se trata de un conjunto de datos relativamente pequeño y ya ampliamente utilizado en la literatura de aprendizaje estadístico, requiere un trabajo cuidadoso de limpieza antes de poder analizarse: varias variables categóricas están codificadas como números, algunos valores faltantes no son evidentes en un primer resumen numérico, y la documentación de origen no siempre coincide exactamente con los valores observados en los datos, como ocurrió con slope. Las variables que parecieron más relevantes a lo largo de todo el proyecto, de forma consistente entre el EDA, las pruebas inferenciales y el modelado, fueron la depresión del segmento ST (oldpeak), la frecuencia cardiaca máxima (max_hr), la edad (age) y el tipo de dolor torácico (chest_pain); en el modelado logístico final también resultaron relevantes la angina inducida por ejercicio (ex_ang), el número de vasos observados (ca) y el sexo (sex).

El análisis exploratorio aportó una primera aproximación, necesaria pero no suficiente, a las relaciones presentes en los datos: permitió formular hipótesis razonables sobre qué variables podrían asociarse con la enfermedad cardiaca y detectar valores atípicos y patrones de dispersión que orientaron las decisiones metodológicas posteriores, pero por sí solo no puede confirmar si esas relaciones son estadísticamente significativas. El análisis inferencial aportó precisamente esa confirmación formal, mediante intervalos de confianza y pruebas de hipótesis específicas para cada tipo de variable, y permitió descartar con mayor seguridad variables que en el EDA parecían poco informativas (como la glucosa en ayunas). El modelado estadístico, finalmente, aportó una forma de combinar varias variables a la vez y cuantificar su relación conjunta con los desenlaces de interés, mostrando que un modelo de regresión logística con apenas tres o cuatro predictoras puede alcanzar una exactitud de clasificación cercana al 78% sobre la propia muestra de entrenamiento, y que un modelo de regresión lineal con tres predictoras explica cerca de una tercera parte de la variabilidad de la frecuencia cardiaca máxima.

Lo que debe revisarse con cautela, antes de dar demasiado peso a estos resultados, es que ambos modelos se evaluaron sobre la misma muestra con la que fueron ajustados, sin un conjunto de prueba independiente ni validación cruzada, por lo que las medidas de desempeño reportadas son probablemente optimistas y no reflejan necesariamente qué tan bien generalizarían estos modelos a pacientes nuevos. Tampoco deben interpretarse los coeficientes de estos modelos como relaciones causales, ni mucho menos como criterios de diagnóstico individual: son asociaciones estadísticas ajustadas, obtenidas de una muestra particular de 303 pacientes, útiles para orientar hipótesis futuras pero insuficientes, por sí solas, para sustentar decisiones clínicas. Las siguientes etapas naturales de este proyecto serían precisamente abordar esa limitación mediante validación con datos independientes o validación cruzada, además de explorar con mayor formalidad la selección de variables (por ejemplo, contrastando de forma sistemática la selección basada en pruebas de hipótesis con métodos de selección automática como el usado con step()).

Referencias

James, G., Witten, D., Hastie, T., Tibshirani, R., & Taylor, J. (2023). An Introduction to Statistical Learning with Applications in R. https://www.statlearning.com/

Robinson, D., Hayes, A., & Couch, S. (s. f.). broom: Convert statistical objects into tidy tibbles. https://broom.tidymodels.org/

Waring, E., Quinn, M., McNamara, A., de la Rubia, E. A., Zhu, H., & Ellis, S. (s. f.). skimr: Compact and Flexible Summaries of Data. https://docs.ropensci.org/skimr/

Wickham, H. (s. f.). ggplot2: Create Elegant Data Visualisations Using the Grammar of Graphics. https://ggplot2.tidyverse.org/

Wickham, H., Çetinkaya-Rundel, M., & Grolemund, G. (s. f.). R for Data Science. https://r4ds.hadley.nz/

Detrano, R., Janosi, A., Steinbrunn, W., Pfisterer, M., Schmid, J., Sandhu, S., Guppy, K., Lee, S., & Froelicher, V. (1989). International application of a new probability algorithm for the diagnosis of coronary artery disease. American Journal of Cardiology, 64(5), 304-310. (Fuente original del conjunto de datos Heart Disease / Cleveland Clinic Foundation.)