Nombre del curso: Análisis de datos con lenguaje R (básico)
Profesores: Enrique Escalante Notario y Gabriela Patricia González Alarcón
Conjunto de datos: Heart Disease
Archivo utilizado: a1_garcia_chanes_rosa_estela_limpia.csv

Introducción

Este informe integra el proceso realizado en las cuatro actividades del curso Análisis de datos con lenguaje R (básico) con el conjunto de datos Heart Disease. El objetivo es describir la información, comparar a las personas con y sin enfermedad cardiaca y explorar qué variables se relacionan con la presencia de enfermedad.

El análisis de datos permite organizar información clínica, identificar patrones y estudiar asociaciones. Para realizarlo se utilizó R, porque permite integrar en un mismo documento el código, las tablas, las gráficas, los resultados y su interpretación.

El trabajo incluyó las siguientes etapas: importación de la base, limpieza, transformación de variables, análisis exploratorio, pruebas estadísticas, selección de variables y construcción de modelos de regresión.

Los resultados tienen un propósito académico. No deben interpretarse como diagnósticos clínicos ni como evidencia de causalidad.

Actividades integradas

Este documento recupera y organiza los resultados de las cuatro actividades realizadas durante el curso.

Planteamiento del problema

El conjunto de datos contiene información demográfica y clínica de personas evaluadas por posible enfermedad cardiaca. La variable principal es ahd, que indica ausencia (no) o presencia (yes) de enfermedad.

Las preguntas principales fueron:

  1. ¿Cómo se distribuyen las variables de la base?
  2. ¿Qué diferencias existen entre las personas con y sin enfermedad cardiaca?
  3. ¿Qué variables se asocian con ahd?
  4. ¿Qué variables se relacionan con la frecuencia cardiaca máxima?
  5. ¿Qué variables pueden incluirse en un modelo estadístico?

Se construyeron dos modelos:

Descripción del conjunto de datos

La base original tenía 303 observaciones y 15 variables. Después de eliminar seis registros con valores faltantes y crear dos variables nuevas, la base limpia quedó formada por 297 observaciones y 17 variables.

Cada fila representa un registro y cada columna contiene una característica demográfica, una medición clínica o un resultado de una prueba.

Paquetes utilizados

Carga de paquetes


library(tidyverse)  # Manipulación, importación y transformación de datos
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.2.1     ✔ readr     2.2.0
## ✔ forcats   1.0.1     ✔ stringr   1.6.0
## ✔ ggplot2   4.0.3     ✔ tibble    3.3.1
## ✔ lubridate 1.9.5     ✔ tidyr     1.3.2
## ✔ purrr     1.2.2     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(janitor)    # Limpieza de nombres de variables y tablas
## 
## Adjuntando el paquete: 'janitor'
## 
## The following objects are masked from 'package:stats':
## 
##     chisq.test, fisher.test
library(skimr)      # Inspección general de datos
library(stringr)    # Manejo de cadenas de texto
library(lubridate)  # Manejo de fechas
library(knitr)      # Presentación de tablas en reportes
library(broom)      # Organización de resultados estadísticos
library(yardstick) # Métricas básicas para clasificación
## 
## Adjuntando el paquete: 'yardstick'
## 
## The following object is masked from 'package:readr':
## 
##     spec

Importación

El archivo CSV debe guardarse en la misma carpeta que este documento.

heart <- read_csv(
  "a1_garcia_chanes_rosa_estela_limpia.csv",
  show_col_types = FALSE
)

heart <- clean_names(heart)

Se revisan las dimensiones, los nombres y la estructura.

dim(heart)
## [1] 297  17
names(heart)
##  [1] "id"          "age"         "sex"         "chest_pain"  "rest_bp"    
##  [6] "chol"        "fbs"         "rest_ecg"    "max_hr"      "ex_ang"     
## [11] "oldpeak"     "slope"       "ca"          "thal"        "ahd"        
## [16] "grupo_edad"  "ahd_binaria"
str(heart)
## spc_tbl_ [297 × 17] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
##  $ id         : num [1:297] 1 2 3 4 5 6 7 8 9 10 ...
##  $ age        : num [1:297] 63 67 67 37 41 56 62 57 63 53 ...
##  $ sex        : chr [1:297] "masculino" "masculino" "masculino" "masculino" ...
##  $ chest_pain : chr [1:297] "typical" "asymptomatic" "asymptomatic" "nonanginal" ...
##  $ rest_bp    : num [1:297] 145 160 120 130 130 120 140 120 130 140 ...
##  $ chol       : num [1:297] 233 286 229 250 204 236 268 354 254 203 ...
##  $ fbs        : chr [1:297] "sí" "no" "no" "no" ...
##  $ rest_ecg   : chr [1:297] "posible hipertrofia" "posible hipertrofia" "posible hipertrofia" "normal" ...
##  $ max_hr     : num [1:297] 150 108 129 187 172 178 160 163 147 155 ...
##  $ ex_ang     : chr [1:297] "no" "sí" "sí" "no" ...
##  $ oldpeak    : num [1:297] 2.3 1.5 2.6 3.5 1.4 0.8 3.6 0.6 1.4 3.1 ...
##  $ slope      : num [1:297] 2 1 1 2 0 0 2 0 1 2 ...
##  $ ca         : chr [1:297] "0 vasos" "3 vasos" "2 vasos" "0 vasos" ...
##  $ thal       : chr [1:297] "fixed" "normal" "reversible" "normal" ...
##  $ ahd        : chr [1:297] "no" "yes" "yes" "no" ...
##  $ grupo_edad : chr [1:297] "55 a 64 años" "65 años o más" "65 años o más" "Menor de 45 años" ...
##  $ ahd_binaria: num [1:297] 0 1 1 0 0 0 1 0 1 1 ...
##  - attr(*, "spec")=
##   .. cols(
##   ..   id = col_double(),
##   ..   age = col_double(),
##   ..   sex = col_character(),
##   ..   chest_pain = col_character(),
##   ..   rest_bp = col_double(),
##   ..   chol = col_double(),
##   ..   fbs = col_character(),
##   ..   rest_ecg = col_character(),
##   ..   max_hr = col_double(),
##   ..   ex_ang = col_character(),
##   ..   oldpeak = col_double(),
##   ..   slope = col_double(),
##   ..   ca = col_character(),
##   ..   thal = col_character(),
##   ..   ahd = col_character(),
##   ..   grupo_edad = col_character(),
##   ..   ahd_binaria = col_double()
##   .. )
##  - attr(*, "problems")=<pointer: 0x0000024c4dffea30>

Preparación de las variables

Al guardar una base en CSV, las variables categóricas pueden importarse como texto o números. Por esta razón se convierten nuevamente en factores.

heart$id <- as.character(heart$id)

heart$sex <- factor(
  heart$sex,
  levels = c("femenino", "masculino")
)

heart$chest_pain <- factor(
  heart$chest_pain,
  levels = c("typical", "nontypical", "nonanginal", "asymptomatic")
)

heart$fbs <- factor(
  heart$fbs,
  levels = c("no", "sí")
)

heart$rest_ecg <- factor(
  heart$rest_ecg,
  levels = c("normal", "anormal", "posible hipertrofia")
)

heart$ex_ang <- factor(
  heart$ex_ang,
  levels = c("no", "sí")
)

heart$slope <- factor(
  heart$slope,
  levels = c(0, 1, 2)
)

heart$ca <- factor(
  heart$ca,
  levels = c("0 vasos", "1 vaso", "2 vasos", "3 vasos")
)

heart$thal <- factor(
  heart$thal,
  levels = c("normal", "fixed", "reversible")
)

heart$ahd <- factor(
  heart$ahd,
  levels = c("no", "yes")
)

heart$grupo_edad <- factor(heart$grupo_edad)

heart$ahd_binaria <- ifelse(
  heart$ahd == "yes",
  1,
  0
)

Se verifica la estructura final.

str(heart)
## spc_tbl_ [297 × 17] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
##  $ id         : chr [1:297] "1" "2" "3" "4" ...
##  $ age        : num [1:297] 63 67 67 37 41 56 62 57 63 53 ...
##  $ sex        : Factor w/ 2 levels "femenino","masculino": 2 2 2 2 1 2 1 1 2 2 ...
##  $ chest_pain : Factor w/ 4 levels "typical","nontypical",..: 1 4 4 3 2 2 4 4 4 4 ...
##  $ rest_bp    : num [1:297] 145 160 120 130 130 120 140 120 130 140 ...
##  $ chol       : num [1:297] 233 286 229 250 204 236 268 354 254 203 ...
##  $ fbs        : Factor w/ 2 levels "no","sí": 2 1 1 1 1 1 1 1 1 2 ...
##  $ rest_ecg   : Factor w/ 3 levels "normal","anormal",..: 3 3 3 1 3 1 3 1 3 3 ...
##  $ max_hr     : num [1:297] 150 108 129 187 172 178 160 163 147 155 ...
##  $ ex_ang     : Factor w/ 2 levels "no","sí": 1 2 2 1 1 1 1 2 1 2 ...
##  $ oldpeak    : num [1:297] 2.3 1.5 2.6 3.5 1.4 0.8 3.6 0.6 1.4 3.1 ...
##  $ slope      : Factor w/ 3 levels "0","1","2": 3 2 2 3 1 1 3 1 2 3 ...
##  $ ca         : Factor w/ 4 levels "0 vasos","1 vaso",..: 1 4 3 1 1 1 3 1 2 1 ...
##  $ thal       : Factor w/ 3 levels "normal","fixed",..: 2 1 3 1 1 1 1 1 3 3 ...
##  $ ahd        : Factor w/ 2 levels "no","yes": 1 2 2 1 1 1 2 1 2 2 ...
##  $ grupo_edad : Factor w/ 4 levels "45 a 54 años",..: 2 3 3 4 4 2 2 2 2 1 ...
##  $ ahd_binaria: num [1:297] 0 1 1 0 0 0 1 0 1 1 ...
##  - attr(*, "spec")=
##   .. cols(
##   ..   id = col_double(),
##   ..   age = col_double(),
##   ..   sex = col_character(),
##   ..   chest_pain = col_character(),
##   ..   rest_bp = col_double(),
##   ..   chol = col_double(),
##   ..   fbs = col_character(),
##   ..   rest_ecg = col_character(),
##   ..   max_hr = col_double(),
##   ..   ex_ang = col_character(),
##   ..   oldpeak = col_double(),
##   ..   slope = col_double(),
##   ..   ca = col_character(),
##   ..   thal = col_character(),
##   ..   ahd = col_character(),
##   ..   grupo_edad = col_character(),
##   ..   ahd_binaria = col_double()
##   .. )
##  - attr(*, "problems")=<pointer: 0x0000024c4dffea30>
dim(heart)
## [1] 297  17
colSums(is.na(heart))
##          id         age         sex  chest_pain     rest_bp        chol 
##           0           0           0           0           0           0 
##         fbs    rest_ecg      max_hr      ex_ang     oldpeak       slope 
##           0           0           0           0           0           0 
##          ca        thal         ahd  grupo_edad ahd_binaria 
##           0           0           0           0           0

La base tiene 297 observaciones, 17 variables y no contiene valores faltantes.

Variables principales

diccionario <- data.frame(
  variable = c(
    "age", "sex", "chest_pain", "rest_bp", "chol",
    "fbs", "rest_ecg", "max_hr", "ex_ang", "oldpeak",
    "slope", "ca", "thal", "ahd"
  ),
  descripcion = c(
    "Edad en años",
    "Sexo registrado",
    "Tipo de dolor torácico",
    "Presión arterial en reposo",
    "Colesterol sérico",
    "Glucosa en ayuno mayor de 120 mg/dl",
    "Electrocardiograma en reposo",
    "Frecuencia cardiaca máxima",
    "Angina inducida por ejercicio",
    "Depresión del segmento ST",
    "Pendiente del segmento ST",
    "Número de vasos principales",
    "Resultado de la prueba thal",
    "Ausencia o presencia de enfermedad cardiaca"
  )
)

kable(
  diccionario,
  caption = "Descripción de las variables principales"
)
Descripción de las variables principales
variable descripcion
age Edad en años
sex Sexo registrado
chest_pain Tipo de dolor torácico
rest_bp Presión arterial en reposo
chol Colesterol sérico
fbs Glucosa en ayuno mayor de 120 mg/dl
rest_ecg Electrocardiograma en reposo
max_hr Frecuencia cardiaca máxima
ex_ang Angina inducida por ejercicio
oldpeak Depresión del segmento ST
slope Pendiente del segmento ST
ca Número de vasos principales
thal Resultado de la prueba thal
ahd Ausencia o presencia de enfermedad cardiaca

Metodología general

El análisis se organizó con base en la metodología CRISP-DM.

metodologia <- data.frame(
  etapa = c(
    "Comprensión del problema",
    "Comprensión de los datos",
    "Preparación de los datos",
    "Modelado",
    "Evaluación",
    "Comunicación"
  ),
  actividad = c(
    "Definir las preguntas y la variable principal ahd.",
    "Revisar dimensiones, variables, categorías y faltantes.",
    "Limpiar nombres, recodificar variables y eliminar faltantes.",
    "Construir una regresión lineal y una regresión logística.",
    "Revisar coeficientes, R cuadrada, AIC y clasificación.",
    "Integrar el análisis en un documento R Markdown."
  )
)

kable(
  metodologia,
  caption = "Etapas de CRISP-DM aplicadas en el proyecto"
)
Etapas de CRISP-DM aplicadas en el proyecto
etapa actividad
Comprensión del problema Definir las preguntas y la variable principal ahd.
Comprensión de los datos Revisar dimensiones, variables, categorías y faltantes.
Preparación de los datos Limpiar nombres, recodificar variables y eliminar faltantes.
Modelado Construir una regresión lineal y una regresión logística.
Evaluación Revisar coeficientes, R cuadrada, AIC y clasificación.
Comunicación Integrar el análisis en un documento R Markdown.

Las etapas se relacionan entre sí. La revisión inicial permitió identificar los problemas de la base; la limpieza hizo posible el análisis; el análisis exploratorio orientó la selección de variables; y las variables seleccionadas se utilizaron en los modelos.

Limpieza y transformación de datos

En la Actividad 1 se realizaron las siguientes acciones:

limpieza <- data.frame(
  problema = c(
    "Nombres de variables no estandarizados",
    "Valores faltantes en ca y thal",
    "Categorías registradas como números",
    "Slope codificada como 1, 2 y 3",
    "Categoría reversable",
    "Necesidad de variables para análisis"
  ),
  solucion = c(
    "Aplicar clean_names()",
    "Eliminar seis registros incompletos",
    "Convertir las variables en factores",
    "Recodificar como 0, 1 y 2",
    "Corregir a reversible",
    "Crear grupo_edad y ahd_binaria"
  )
)

kable(
  limpieza,
  caption = "Decisiones de limpieza y transformación"
)
Decisiones de limpieza y transformación
problema solucion
Nombres de variables no estandarizados Aplicar clean_names()
Valores faltantes en ca y thal Eliminar seis registros incompletos
Categorías registradas como números Convertir las variables en factores
Slope codificada como 1, 2 y 3 Recodificar como 0, 1 y 2
Categoría reversable Corregir a reversible
Necesidad de variables para análisis Crear grupo_edad y ahd_binaria

Análisis exploratorio y visualización

Variables numéricas

Se utilizan comandos directos para resumir las variables numéricas.

summary(
  heart[c("age", "rest_bp", "chol", "max_hr", "oldpeak")]
)
##       age           rest_bp           chol           max_hr     
##  Min.   :29.00   Min.   : 94.0   Min.   :126.0   Min.   : 71.0  
##  1st Qu.:48.00   1st Qu.:120.0   1st Qu.:211.0   1st Qu.:133.0  
##  Median :56.00   Median :130.0   Median :243.0   Median :153.0  
##  Mean   :54.54   Mean   :131.7   Mean   :247.4   Mean   :149.6  
##  3rd Qu.:61.00   3rd Qu.:140.0   3rd Qu.:276.0   3rd Qu.:166.0  
##  Max.   :77.00   Max.   :200.0   Max.   :564.0   Max.   :202.0  
##     oldpeak     
##  Min.   :0.000  
##  1st Qu.:0.000  
##  Median :0.800  
##  Mean   :1.056  
##  3rd Qu.:1.600  
##  Max.   :6.200

También se calculan la media y la desviación estándar.

resumen_numericas <- data.frame(
  variable = c("age", "rest_bp", "chol", "max_hr", "oldpeak"),
  media = c(
    mean(heart$age),
    mean(heart$rest_bp),
    mean(heart$chol),
    mean(heart$max_hr),
    mean(heart$oldpeak)
  ),
  mediana = c(
    median(heart$age),
    median(heart$rest_bp),
    median(heart$chol),
    median(heart$max_hr),
    median(heart$oldpeak)
  ),
  desviacion_estandar = c(
    sd(heart$age),
    sd(heart$rest_bp),
    sd(heart$chol),
    sd(heart$max_hr),
    sd(heart$oldpeak)
  )
)

kable(
  resumen_numericas,
  digits = 2,
  caption = "Resumen de las variables numéricas"
)
Resumen de las variables numéricas
variable media mediana desviacion_estandar
age 54.54 56.0 9.05
rest_bp 131.69 130.0 17.76
chol 247.35 243.0 52.00
max_hr 149.60 153.0 22.94
oldpeak 1.06 0.8 1.17

La edad promedio fue de 54.54 años, con una mediana de 56 años y una desviación estándar de 9.05. La presión arterial en reposo presentó una media de 131.69 y el colesterol una media de 247.35. La frecuencia cardiaca máxima tuvo una media de 149.60 latidos por minuto, mientras que oldpeak presentó una media de 1.06. Las medias y medianas de age, rest_bp, chol y max_hr fueron relativamente cercanas. En cambio, la media de oldpeak fue mayor que su mediana, lo que sugiere una distribución desplazada hacia valores altos. El colesterol y la frecuencia cardiaca máxima presentaron las mayores desviaciones estándar en sus respectivas escalas de medición. Estas estadísticas describen al conjunto completo; posteriormente se comparan según la presencia o ausencia de enfermedad cardiaca..

ggplot(heart, aes(x = age)) +
  geom_histogram(
    bins = 20,
    fill = "steelblue",
    color = "white"
  ) +
  labs(
    title = "Distribución de la edad",
    x = "Edad",
    y = "Frecuencia"
  ) +
  theme_minimal()

El histograma muestra que la edad se concentra principalmente entre los 45 y 65 años, con una mayor frecuencia alrededor de los 55 a 60 años. La distribución es aproximadamente unimodal y no presenta separaciones marcadas entre grupos. Se observan pocos casos en las edades más jóvenes y en las edades más avanzadas. La edad mínima fue de 29 años y la máxima de 77 años. Esta gráfica describe la distribución general de la edad, pero todavía no permite determinar si existen diferencias entre las personas con y sin enfermedad cardiaca; esa comparación se realiza posteriormente mediante boxplots y pruebas estadísticas.

Variables categóricas

Las frecuencias se obtienen con table().

table(heart$sex)
## 
##  femenino masculino 
##        96       201
table(heart$chest_pain)
## 
##      typical   nontypical   nonanginal asymptomatic 
##           23           49           83          142
table(heart$fbs)
## 
##  no  sí 
## 254  43
table(heart$rest_ecg)
## 
##              normal             anormal posible hipertrofia 
##                 147                   4                 146
table(heart$ex_ang)
## 
##  no  sí 
## 200  97
table(heart$slope)
## 
##   0   1   2 
## 139 137  21
table(heart$ca)
## 
## 0 vasos  1 vaso 2 vasos 3 vasos 
##     174      65      38      20
table(heart$thal)
## 
##     normal      fixed reversible 
##        164         18        115
table(heart$ahd)
## 
##  no yes 
## 160 137

Las frecuencias muestran que la base contiene 201 hombres y 96 mujeres. El dolor torácico asintomático fue la categoría más frecuente, con 142 casos, seguido de nonanginal con 83, nontypical con 49 y typical con 23. La mayoría no presentó glucosa elevada en ayuno (fbs = no, 254 casos) ni angina inducida por ejercicio (ex_ang = no, 200 casos). En rest_ecg, las categorías normal y posible hipertrofia tuvieron frecuencias similares, con 147 y 146 casos, mientras que anormal tuvo solamente cuatro. También se observaron pocos registros en slope = 2, tres vasos y thal = fixed. Estas categorías con pocos casos deben interpretarse con cautela en las pruebas estadísticas y en el modelo.

Los porcentajes de la variable principal se calculan con prop.table().

tabla_ahd <- table(heart$ahd)
tabla_ahd
## 
##  no yes 
## 160 137
prop.table(tabla_ahd) * 100
## 
##       no      yes 
## 53.87205 46.12795

La base contiene 160 observaciones sin enfermedad cardiaca (53.9%) y 137 con enfermedad (46.1%). Los tamaños de ambos grupos son relativamente próximos, lo que permite realizar comparaciones sin que una categoría sea claramente predominante.

ggplot(heart, aes(x = ahd, fill = ahd)) +
  geom_bar() +
  geom_text(
    stat = "count",
    aes(label = after_stat(count)),
    vjust = -0.4
  ) +
  labs(
    title = "Presencia de enfermedad cardiaca",
    x = "AHD",
    y = "Frecuencia"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

La gráfica confirma que el grupo sin enfermedad cardiaca es ligeramente más numeroso que el grupo con enfermedad. Sin embargo, la diferencia entre las dos barras no es amplia: aproximadamente 54 de cada 100 observaciones corresponden a ausencia de enfermedad y 46 de cada 100 a presencia de enfermedad. Esta distribución describe la frecuencia de ahd, pero no permite identificar por sí misma qué variables se relacionan con la enfermedad. ## Comparación de variables numéricas por AHD

heart |>
  group_by(ahd) |>
  summarise(
    edad_media = mean(age),
    presion_media = mean(rest_bp),
    colesterol_medio = mean(chol),
    max_hr_media = mean(max_hr),
    oldpeak_media = mean(oldpeak)
  )
## # A tibble: 2 × 6
##   ahd   edad_media presion_media colesterol_medio max_hr_media oldpeak_media
##   <fct>      <dbl>         <dbl>            <dbl>        <dbl>         <dbl>
## 1 no          52.6          129.             243.         159.         0.599
## 2 yes         56.8          135.             252.         139.         1.59

Las personas con enfermedad cardiaca presentaron una edad promedio de 56.76 años, mientras que en el grupo sin enfermedad fue de 52.64 años, lo que representa una diferencia aproximada de 4.12 años. La presión arterial promedio también fue mayor en el grupo con enfermedad: 134.64 frente a 129.18. El colesterol promedio fue de 251.85 entre las personas con enfermedad y de 243.49 entre quienes no la presentaron. Las diferencias más marcadas se observaron en max_hr y oldpeak: la frecuencia cardiaca máxima promedio fue menor en el grupo con enfermedad, con 139.11 frente a 158.58 latidos por minuto, mientras que oldpeak fue mayor, con 1.59 frente a 0.60. Estos resultados son descriptivos; posteriormente se utilizan pruebas estadísticas para determinar si las diferencias presentan evidencia estadística.

ggplot(heart, aes(x = ahd, y = age, fill = ahd)) +
  geom_boxplot() +
  labs(
    title = "Edad según AHD",
    x = "AHD",
    y = "Edad"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

El boxplot muestra que las personas con enfermedad cardiaca tienden a tener mayor edad. La mediana fue de 58 años en el grupo con enfermedad y de 52 años en el grupo sin enfermedad. Aunque las cajas y los rangos se traslapan, la distribución del grupo con enfermedad se encuentra desplazada hacia edades mayores. Esto sugiere una diferencia de edad entre los grupos, pero la gráfica por sí sola no permite determinar si es estadísticamente significativa; esa diferencia se evalúa posteriormente con la prueba t.

ggplot(heart, aes(x = ahd, y = max_hr, fill = ahd)) +
  geom_boxplot() +
  labs(
    title = "Frecuencia cardiaca máxima según AHD",
    x = "AHD",
    y = "Frecuencia cardiaca máxima"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

El boxplot muestra que la frecuencia cardiaca máxima (max_hr) fue menor entre las personas con enfermedad cardiaca. La mediana fue de 142 latidos por minuto en el grupo con enfermedad y de 161 en el grupo sin enfermedad. Las cajas muestran una separación visible entre los valores centrales, aunque existe traslape entre ambos grupos y se observa un posible valor atípico. Esto indica que max_hr puede ser una variable relevante para diferenciar los grupos, pero no permite clasificar por sí sola a todas las personas. La diferencia se evalúa posteriormente mediante las pruebas t y de Wilcoxon.

ggplot(heart, aes(x = ahd, y = oldpeak, fill = ahd)) +
  geom_boxplot() +
  labs(
    title = "Oldpeak según AHD",
    x = "AHD",
    y = "Oldpeak"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

El boxplot muestra que oldpeak presenta valores más altos entre las personas con enfermedad cardiaca. La mediana fue de 1.4 en el grupo con enfermedad y de 0.2 en el grupo sin enfermedad. También se observa mayor dispersión y algunos valores extremos, especialmente en el grupo con enfermedad. Aunque existe traslape entre las distribuciones, la diferencia sugiere que una mayor depresión del segmento ST podría estar asociada con la presencia de enfermedad. Debido a la asimetría y a los posibles valores atípicos, esta relación se evalúa posteriormente mediante la prueba t y, de manera complementaria, con la prueba de Wilcoxon. Las gráficas muestran diferencias claras en las tres variables, aunque existe traslape entre los grupos.

Variables categóricas y AHD

tabla_dolor <- table(
  heart$chest_pain,
  heart$ahd
)

tabla_dolor
##               
##                 no yes
##   typical       16   7
##   nontypical    40   9
##   nonanginal    65  18
##   asymptomatic  39 103
prop.table(tabla_dolor, margin = 1) * 100
##               
##                      no      yes
##   typical      69.56522 30.43478
##   nontypical   81.63265 18.36735
##   nonanginal   78.31325 21.68675
##   asymptomatic 27.46479 72.53521

La tabla muestra diferencias importantes en la presencia de enfermedad según el tipo de dolor torácico. En la categoría asymptomatic, 103 de 142 personas presentaron enfermedad, equivalente a 72.5%. En cambio, la proporción fue de 30.4% en typical, 21.7% en nonanginal y 18.4% en nontypical. Estos resultados sugieren una asociación entre el tipo de dolor torácico y ahd, que posteriormente se evalúa mediante la prueba de chi-cuadrada.

ggplot(heart, aes(x = chest_pain, fill = ahd)) +
  geom_bar(position = "fill") +
  coord_flip() +
  labs(
    title = "Proporción de AHD según dolor torácico",
    x = "Dolor torácico",
    y = "Proporción",
    fill = "AHD"
  ) +
  theme_minimal()

La gráfica confirma que la categoría asymptomatic presentó la mayor proporción de personas con enfermedad cardiaca. En las categorías typical, nonanginal y nontypical predominó el grupo sin enfermedad. Esta visualización muestra un patrón diferente entre las categorías, pero no permite concluir que el tipo de dolor cause la enfermedad ni considera el efecto simultáneo de otras variables. La categoría asymptomatic presentó la mayor proporción de personas con ahd = yes.

Relación entre edad y frecuencia cardiaca máxima

ggplot(
  heart,
  aes(x = age, y = max_hr, color = ahd)
) +
  geom_point() +
  geom_smooth(
    method = "lm",
    se = FALSE
  ) +
  facet_wrap(~ sex) +
  labs(
    title = "Edad y frecuencia cardiaca máxima según sexo",
    x = "Edad",
    y = "Frecuencia cardiaca máxima",
    color = "AHD"
  ) +
  theme_minimal()
## `geom_smooth()` using formula = 'y ~ x'

La gráfica muestra una relación negativa entre la edad y la frecuencia cardiaca máxima: conforme aumenta la edad, max_hr tiende a disminuir. Este patrón se observa tanto en hombres como en mujeres. Además, las personas con enfermedad cardiaca se concentran con mayor frecuencia en valores bajos de max_hr, aunque existe una dispersión amplia y traslape entre los grupos. Por ello, ni la edad ni la frecuencia cardiaca máxima permiten diferenciar por sí solas a todas las personas con y sin enfermedad. La relación entre ambas variables se evalúa posteriormente mediante la correlación y los modelos estadísticos.

Posibles valores atípicos

El número de posibles valores atípicos se obtiene con boxplot.stats().

atipicos <- data.frame(
  variable = c("age", "rest_bp", "chol", "max_hr", "oldpeak"),
  numero = c(
    length(boxplot.stats(heart$age)$out),
    length(boxplot.stats(heart$rest_bp)$out),
    length(boxplot.stats(heart$chol)$out),
    length(boxplot.stats(heart$max_hr)$out),
    length(boxplot.stats(heart$oldpeak)$out)
  )
)

kable(
  atipicos,
  caption = "Posibles valores atípicos"
)
Posibles valores atípicos
variable numero
age 0
rest_bp 9
chol 5
max_hr 1
oldpeak 5

De acuerdo con el criterio utilizado por boxplot.stats(), no se identificaron valores atípicos en age. Se detectaron nueve posibles valores atípicos en rest_bp, cinco en chol, uno en max_hr y cinco en oldpeak. Estos valores no se eliminaron porque se encuentran dentro de rangos clínicamente posibles y podrían representar diferencias reales entre las personas, en lugar de errores de captura. La función identifica observaciones alejadas mediante el criterio del rango intercuartílico, pero no demuestra que sean datos incorrectos. Debido a su presencia, las comparaciones numéricas se complementaron con la prueba de Wilcoxon y se recomienda revisar su posible influencia en los modelos.

Análisis inferencial y selección de variables

Intervalos de confianza

Se calculan intervalos de confianza del 95% para las variables más relevantes.

t.test(
  heart$age[heart$ahd == "no"]
)$conf.int
## [1] 51.15246 54.13504
## attr(,"conf.level")
## [1] 0.95
t.test(
  heart$age[heart$ahd == "yes"]
)$conf.int
## [1] 55.42444 58.09381
## attr(,"conf.level")
## [1] 0.95

En el grupo sin enfermedad cardiaca, el intervalo de confianza del 95% para la edad media fue de 51.15 a 54.14 años. En el grupo con enfermedad, el intervalo fue de 55.42 a 58.09 años. Los intervalos no se traslapan, lo que sugiere que la edad promedio es mayor entre las personas con enfermedad. Esta diferencia se confirma posteriormente mediante la prueba t.

t.test(
  heart$max_hr[heart$ahd == "no"]
)$conf.int
## [1] 155.6079 161.5546
## attr(,"conf.level")
## [1] 0.95
t.test(
  heart$max_hr[heart$ahd == "yes"]
)$conf.int
## [1] 135.2724 142.9466
## attr(,"conf.level")
## [1] 0.95

El intervalo de confianza del 95% para la frecuencia cardiaca máxima media fue de 155.61 a 161.55 latidos por minuto en el grupo sin enfermedad y de 135.27 a 142.95 en el grupo con enfermedad. La separación entre los intervalos muestra que las personas con enfermedad presentan, en promedio, una frecuencia cardiaca máxima considerablemente menor.

t.test(
  heart$oldpeak[heart$ahd == "no"]
)$conf.int
## [1] 0.4758451 0.7216549
## attr(,"conf.level")
## [1] 0.95
t.test(
  heart$oldpeak[heart$ahd == "yes"]
)$conf.int
## [1] 1.368565 1.809538
## attr(,"conf.level")
## [1] 0.95

El intervalo de confianza del 95% para la media de oldpeak fue de 0.48 a 0.72 en el grupo sin enfermedad y de 1.37 a 1.81 en el grupo con enfermedad. Los intervalos no se traslapan y muestran que oldpeak tiende a ser mayor entre las personas con enfermedad cardiaca.

En conjunto, los intervalos de confianza muestran diferencias entre los grupos en age, max_hr y oldpeak. Estos intervalos corresponden a las medias de cada grupo y no representan los valores posibles de todas las personas. Las pruebas de hipótesis permiten evaluar formalmente si las diferencias observadas presentan evidencia estadística._

Pruebas para variables numéricas

La prueba t de Welch compara las medias entre las personas con y sin enfermedad.

t.test(age ~ ahd, data = heart)
## 
##  Welch Two Sample t-test
## 
## data:  age by ahd
## t = -4.0636, df = 294.66, p-value = 6.204e-05
## alternative hypothesis: true difference in means between group no and group yes is not equal to 0
## 95 percent confidence interval:
##  -6.108514 -2.122234
## sample estimates:
##  mean in group no mean in group yes 
##          52.64375          56.75912
t.test(rest_bp ~ ahd, data = heart)
## 
##  Welch Two Sample t-test
## 
## data:  rest_bp by ahd
## t = -2.6385, df = 271.2, p-value = 0.008808
## alternative hypothesis: true difference in means between group no and group yes is not equal to 0
## 95 percent confidence interval:
##  -9.534037 -1.386036
## sample estimates:
##  mean in group no mean in group yes 
##           129.175           134.635
t.test(chol ~ ahd, data = heart)
## 
##  Welch Two Sample t-test
## 
## data:  chol by ahd
## t = -1.3919, df = 293.27, p-value = 0.165
## alternative hypothesis: true difference in means between group no and group yes is not equal to 0
## 95 percent confidence interval:
##  -20.181405   3.460876
## sample estimates:
##  mean in group no mean in group yes 
##          243.4938          251.8540
t.test(max_hr ~ ahd, data = heart)
## 
##  Welch Two Sample t-test
## 
## data:  max_hr by ahd
## t = 7.9286, df = 266.44, p-value = 6.108e-14
## alternative hypothesis: true difference in means between group no and group yes is not equal to 0
## 95 percent confidence interval:
##  14.63637 24.30715
## sample estimates:
##  mean in group no mean in group yes 
##          158.5813          139.1095
t.test(oldpeak ~ ahd, data = heart)
## 
##  Welch Two Sample t-test
## 
## data:  oldpeak by ahd
## t = -7.7558, df = 216, p-value = 3.429e-13
## alternative hypothesis: true difference in means between group no and group yes is not equal to 0
## 95 percent confidence interval:
##  -1.241970 -0.738632
## sample estimates:
##  mean in group no mean in group yes 
##          0.598750          1.589051

La prueba t de Welch evaluó si las medias de las variables numéricas eran diferentes entre las personas con y sin enfermedad cardiaca. Se utilizó un nivel de significancia de 0.05. Los resultados fueron los siguientes:

Edad: se encontró una diferencia estadísticamente significativa (p < 0.001). La edad promedio fue mayor en el grupo con enfermedad: 56.76 frente a 52.64 años. Presión arterial en reposo: se encontró una diferencia estadísticamente significativa (p = 0.009). El promedio fue de 134.64 en el grupo con enfermedad y de 129.18 en el grupo sin enfermedad. Aunque existe evidencia estadística, la diferencia fue de menor magnitud que la observada en otras variables. Colesterol: no se encontró evidencia estadística suficiente de una diferencia entre los grupos (p = 0.165). Aunque el promedio fue mayor en las personas con enfermedad —251.85 frente a 243.49—, la diferencia podría deberse a la variabilidad de la muestra. Frecuencia cardiaca máxima: se encontró una diferencia estadísticamente significativa (p < 0.001). Las personas con enfermedad presentaron una frecuencia cardiaca máxima promedio menor: 139.11 frente a 158.58 latidos por minuto. Oldpeak: se encontró una diferencia estadísticamente significativa (p < 0.001). El promedio fue mayor en el grupo con enfermedad: 1.59 frente a 0.60.

Para cerrar el apartado, agrega:

En conjunto, las pruebas t mostraron diferencias entre los grupos en age, rest_bp, max_hr y oldpeak. El colesterol fue la única variable numérica evaluada que no mostró una diferencia estadísticamente significativa mediante esta prueba.

wilcox.test(
  rest_bp ~ ahd,
  data = heart,
  exact = FALSE
)
## 
##  Wilcoxon rank sum test with continuity correction
## 
## data:  rest_bp by ahd
## W = 9292.5, p-value = 0.02346
## alternative hypothesis: true location shift is not equal to 0
wilcox.test(
  chol ~ ahd,
  data = heart,
  exact = FALSE
)
## 
##  Wilcoxon rank sum test with continuity correction
## 
## data:  chol by ahd
## W = 9492, p-value = 0.04669
## alternative hypothesis: true location shift is not equal to 0
wilcox.test(
  max_hr ~ ahd,
  data = heart,
  exact = FALSE
)
## 
##  Wilcoxon rank sum test with continuity correction
## 
## data:  max_hr by ahd
## W = 16399, p-value = 1.674e-13
## alternative hypothesis: true location shift is not equal to 0
wilcox.test(
  oldpeak ~ ahd,
  data = heart,
  exact = FALSE
)
## 
##  Wilcoxon rank sum test with continuity correction
## 
## data:  oldpeak by ahd
## W = 5833.5, p-value = 1.539e-12
## alternative hypothesis: true location shift is not equal to 0

La prueba de Wilcoxon se utilizó como complemento de la prueba t porque algunas variables presentaron asimetría o posibles valores atípicos. Esta prueba compara la posición o distribución de los valores entre los grupos mediante rangos y no se basa directamente en la comparación de las medias.

Presión arterial en reposo: se encontró evidencia de una diferencia entre los grupos (p = 0.023). Aunque ambos grupos tuvieron una mediana de 130, la distribución y el orden de los valores no fueron iguales. Colesterol: la prueba produjo un valor p = 0.047, ligeramente inferior a 0.05. Este resultado difiere de la prueba t, que obtuvo p = 0.165. Por ello, la evidencia de una diferencia en el colesterol es débil y sensible al método utilizado, por lo que debe interpretarse con cautela. Frecuencia cardiaca máxima: se encontró una diferencia clara entre los grupos (p < 0.001). Este resultado coincide con la prueba t y respalda que max_hr tiende a ser menor entre las personas con enfermedad. Oldpeak: se encontró una diferencia clara entre los grupos (p < 0.001). El resultado coincide con la prueba t y respalda que los valores de oldpeak tienden a ser mayores en las personas con enfermedad.

En conjunto, las pruebas t y de Wilcoxon produjeron resultados coincidentes para rest_bp, max_hr y oldpeak. En el caso del colesterol, las conclusiones fueron diferentes, por lo que no puede afirmarse que exista una diferencia clara y consistente entre los grupos.

Pruebas para variables categóricas

Se crean tablas de contingencia y se aplica la prueba de chi-cuadrada.

tabla_sexo <- table(heart$sex, heart$ahd)
tabla_sexo
##            
##              no yes
##   femenino   71  25
##   masculino  89 112
chisq.test(tabla_sexo)
## 
##  Pearson's Chi-squared test with Yates' continuity correction
## 
## data:  tabla_sexo
## X-squared = 21.852, df = 1, p-value = 2.946e-06
tabla_dolor <- table(heart$chest_pain, heart$ahd)
tabla_dolor
##               
##                 no yes
##   typical       16   7
##   nontypical    40   9
##   nonanginal    65  18
##   asymptomatic  39 103
chisq.test(tabla_dolor)
## 
##  Pearson's Chi-squared test
## 
## data:  tabla_dolor
## X-squared = 77.276, df = 3, p-value < 2.2e-16
tabla_fbs <- table(heart$fbs, heart$ahd)
tabla_fbs
##     
##       no yes
##   no 137 117
##   sí  23  20
chisq.test(tabla_fbs)
## 
##  Pearson's Chi-squared test with Yates' continuity correction
## 
## data:  tabla_fbs
## X-squared = 1.9997e-31, df = 1, p-value = 1
tabla_rest_ecg <- table(heart$rest_ecg, heart$ahd)
tabla_rest_ecg
##                      
##                       no yes
##   normal              92  55
##   anormal              1   3
##   posible hipertrofia 67  79
chisq.test(tabla_rest_ecg)
## Warning in stats::chisq.test(x, y, ...): Chi-squared approximation may be
## incorrect
## 
##  Pearson's Chi-squared test
## 
## data:  tabla_rest_ecg
## X-squared = 9.5755, df = 2, p-value = 0.008331
fisher.test(tabla_rest_ecg)
## 
##  Fisher's Exact Test for Count Data
## 
## data:  tabla_rest_ecg
## p-value = 0.004834
## alternative hypothesis: two.sided
tabla_ex_ang <- table(heart$ex_ang, heart$ahd)
tabla_ex_ang
##     
##       no yes
##   no 137  63
##   sí  23  74
chisq.test(tabla_ex_ang)
## 
##  Pearson's Chi-squared test with Yates' continuity correction
## 
## data:  tabla_ex_ang
## X-squared = 50.943, df = 1, p-value = 9.511e-13
tabla_slope <- table(heart$slope, heart$ahd)
tabla_slope
##    
##      no yes
##   0 103  36
##   1  48  89
##   2   9  12
chisq.test(tabla_slope)
## 
##  Pearson's Chi-squared test
## 
## data:  tabla_slope
## X-squared = 43.473, df = 2, p-value = 3.63e-10
tabla_ca <- table(heart$ca, heart$ahd)
tabla_ca
##          
##            no yes
##   0 vasos 129  45
##   1 vaso   21  44
##   2 vasos   7  31
##   3 vasos   3  17
chisq.test(tabla_ca)
## 
##  Pearson's Chi-squared test
## 
## data:  tabla_ca
## X-squared = 72.301, df = 3, p-value = 1.373e-15
tabla_thal <- table(heart$thal, heart$ahd)
tabla_thal
##             
##               no yes
##   normal     127  37
##   fixed        6  12
##   reversible  27  88
chisq.test(tabla_thal)
## 
##  Pearson's Chi-squared test
## 
## data:  tabla_thal
## X-squared = 82.46, df = 2, p-value < 2.2e-16

Las pruebas de asociación mostraron que sex, chest_pain, ex_ang, slope, ca y thal se relacionaron estadísticamente con la presencia de enfermedad cardiaca (p < 0.001). rest_ecg también mostró asociación mediante la prueba de chi-cuadrada (p = 0.008); debido a que la categoría anormal tuvo solamente cuatro observaciones, se aplicó además la prueba exacta de Fisher, que respaldó el resultado (p = 0.0047). En cambio, fbs no mostró asociación con ahd (p = 1.000), ya que la proporción de enfermedad fue prácticamente igual entre sus categorías.

Los patrones más marcados se observaron en el sexo, el dolor torácico, la angina inducida por ejercicio, el número de vasos y thal. La enfermedad estuvo presente en 55.7% de los hombres y en 26.0% de las mujeres; en 72.5% de las personas con dolor torácico asintomático; en 76.3% de quienes presentaron angina inducida por ejercicio; y en 76.5% de la categoría thal = reversible. Asimismo, la proporción de enfermedad aumentó de 25.9% entre las personas con cero vasos a 67.7%, 81.6% y 85.0% entre quienes tenían uno, dos y tres vasos, respectivamente.

Correlaciones

variables_correlacion <- heart[
  c("age", "rest_bp", "chol", "max_hr", "oldpeak")
]

cor(
  variables_correlacion,
  use = "complete.obs"
)
##                age     rest_bp          chol        max_hr     oldpeak
## age      1.0000000  0.29047626  2.026435e-01 -3.945629e-01  0.19712262
## rest_bp  0.2904763  1.00000000  1.315357e-01 -4.910766e-02  0.19124314
## chol     0.2026435  0.13153571  1.000000e+00 -7.456799e-05  0.03859579
## max_hr  -0.3945629 -0.04910766 -7.456799e-05  1.000000e+00 -0.34763997
## oldpeak  0.1971226  0.19124314  3.859579e-02 -3.476400e-01  1.00000000

La correlación entre age y max_hr fue aproximadamente -0.39. La correlación entre oldpeak y max_hr fue aproximadamente -0.35. No se observaron correlaciones muy altas.

Variables seleccionadas

seleccion <- data.frame(
  modelo = c(
    "Regresión lineal",
    "Regresión logística"
  ),
  respuesta = c(
    "max_hr",
    "ahd"
  ),
  predictoras = c(
    "age, sex, rest_bp, chol, oldpeak y ahd",
    "age, sex, chest_pain, max_hr, ex_ang, oldpeak, slope, ca y thal"
  )
)

kable(
  seleccion,
  caption = "Variables seleccionadas para los modelos"
)
Variables seleccionadas para los modelos
modelo respuesta predictoras
Regresión lineal max_hr age, sex, rest_bp, chol, oldpeak y ahd
Regresión logística ahd age, sex, chest_pain, max_hr, ex_ang, oldpeak, slope, ca y thal

La correlación negativa más importante se observó entre age y max_hr (r = −0.395), lo que indica que la frecuencia cardiaca máxima tiende a disminuir conforme aumenta la edad. También se encontró una correlación negativa entre oldpeak y max_hr (r = −0.348), por lo que los valores altos de oldpeak tienden a relacionarse con una menor frecuencia cardiaca máxima.

Entre las correlaciones positivas, la mayor fue la relación entre age y rest_bp (r = 0.290), seguida de age con chol (r = 0.203) y de age con oldpeak (r = 0.197). Estas relaciones fueron de magnitud baja. La correlación entre chol y max_hr fue prácticamente cero, lo que indica ausencia de una relación lineal apreciable entre ambas variables.

Ninguna correlación tuvo un valor absoluto igual o superior a 0.40, por lo que no se identificaron relaciones lineales fuertes entre las variables numéricas seleccionadas. Esto disminuye la preocupación inicial por incluir variables numéricas altamente redundantes en los modelos. Sin embargo, la correlación no demuestra causalidad y solamente describe relaciones lineales entre pares de variables.

Modelado estadístico

Regresión lineal múltiple

La variable respuesta es max_hr.

modelo_lineal <- lm(
  max_hr ~ age + sex + rest_bp + chol + oldpeak + ahd,
  data = heart
)

summary(modelo_lineal)
## 
## Call:
## lm(formula = max_hr ~ age + sex + rest_bp + chol + oldpeak + 
##     ahd, data = heart)
## 
## Residuals:
##    Min     1Q Median     3Q    Max 
## -58.84 -11.71   1.87  12.35  41.91 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  174.74520   10.88825  16.049  < 2e-16 ***
## age           -0.86074    0.13397  -6.425 5.39e-10 ***
## sexmasculino   1.92071    2.56538   0.749 0.454644    
## rest_bp        0.15734    0.06647   2.367 0.018591 *  
## chol           0.04064    0.02237   1.816 0.070347 .  
## oldpeak       -3.57450    1.06738  -3.349 0.000919 ***
## ahdyes       -14.09035    2.61459  -5.389 1.47e-07 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 19.13 on 290 degrees of freedom
## Multiple R-squared:  0.3187, Adjusted R-squared:  0.3047 
## F-statistic: 22.61 on 6 and 290 DF,  p-value: < 2.2e-16

La regresión lineal múltiple analizó la frecuencia cardiaca máxima (max_hr) considerando simultáneamente la edad, el sexo, la presión arterial en reposo, el colesterol, oldpeak y la presencia de enfermedad cardiaca. Los coeficientes se interpretan manteniendo constantes las demás variables del modelo.

La edad presentó una asociación negativa con max_hr: por cada año adicional, la frecuencia cardiaca máxima disminuyó en promedio 0.861 latidos por minuto (p < 0.001; IC95%: −1.124 a −0.597). Oldpeak también mostró una asociación negativa: cada unidad adicional se relacionó con una disminución promedio de 3.574 latidos por minuto (p < 0.001; IC95%: −5.675 a −1.474).

Las personas con enfermedad cardiaca presentaron, en promedio, 14.090 latidos por minuto menos que las personas sin enfermedad, después de ajustar por las demás variables (p < 0.001; IC95%: −19.236 a −8.944). La presión arterial en reposo mostró una asociación positiva de pequeña magnitud: por cada unidad adicional, max_hr aumentó en promedio 0.157 latidos por minuto (p = 0.019; IC95%: 0.027 a 0.288).

Los hombres presentaron, en promedio, 1.921 latidos por minuto más que las mujeres, pero la asociación no fue estadísticamente significativa (p = 0.455). El colesterol presentó un coeficiente positivo de 0.041, pero tampoco mostró evidencia estadística suficiente (p = 0.070). En ambos casos, los intervalos de confianza incluyeron cero.

El modelo obtuvo un R² de 0.319 y un R² ajustado de 0.305. Esto significa que las variables incluidas explicaron aproximadamente 31.9% de la variación observada en max_hr. El porcentaje restante se relaciona con variables no incluidas, variabilidad individual y error. El R² representa capacidad explicativa dentro de la muestra y no el porcentaje de personas clasificadas correctamente. Los intervalos de confianza de los coeficientes se obtienen con confint().

Valores observados y ajustados

plot(
  fitted(modelo_lineal),
  heart$max_hr,
  xlab = "Valores ajustados",
  ylab = "Valores observados",
  main = "Valores observados y ajustados"
)

abline(
  0,
  1,
  col = "red",
  lty = 2
)

Los valores observados tienden a aumentar conforme aumentan los valores estimados por el modelo, lo que indica que la regresión recupera una parte del patrón de max_hr. Los puntos próximos a la línea roja representan predicciones cercanas a los valores observados, mientras que los puntos alejados muestran errores de mayor magnitud. La dispersión alrededor de la línea es considerable, lo que coincide con el R² de 0.319 y confirma que el modelo explica solamente una parte de la variación de la frecuencia cardiaca máxima. ## Revisión de residuales

plot(
  fitted(modelo_lineal),
  residuals(modelo_lineal),
  xlab = "Valores ajustados",
  ylab = "Residuales",
  main = "Residuales del modelo lineal"
)

abline(
  h = 0,
  lty = 2,
  col = "red"
)

Los residuales se distribuyen tanto por encima como por debajo de cero y no muestran una curvatura claramente definida, lo que indica que el modelo recupera parcialmente una relación lineal. Sin embargo, existe una dispersión considerable y algunos residuales de mayor magnitud. Esta revisión es solamente visual; antes de utilizar el modelo con fines predictivos sería necesario evaluar con mayor detalle la normalidad y la varianza constante de los residuales, así como identificar posibles observaciones influyentes.

Regresión logística múltiple

La variable respuesta es ahd_binaria, donde 0 significa ausencia y 1 presencia de enfermedad.

modelo_logistico <- glm(
  ahd_binaria ~ age + sex + chest_pain + max_hr +
    ex_ang + oldpeak + slope + ca + thal,
  data = heart,
  family = binomial
)

summary(modelo_logistico)
## 
## Call:
## glm(formula = ahd_binaria ~ age + sex + chest_pain + max_hr + 
##     ex_ang + oldpeak + slope + ca + thal, family = binomial, 
##     data = heart)
## 
## Coefficients:
##                         Estimate Std. Error z value Pr(>|z|)    
## (Intercept)            -3.325603   2.599111  -1.280 0.200715    
## age                     0.001296   0.022982   0.056 0.955041    
## sexmasculino            1.338225   0.503988   2.655 0.007924 ** 
## chest_painnontypical    1.201221   0.761452   1.578 0.114671    
## chest_painnonanginal    0.043549   0.665758   0.065 0.947846    
## chest_painasymptomatic  2.090033   0.661347   3.160 0.001576 ** 
## max_hr                 -0.012197   0.010795  -1.130 0.258497    
## ex_angsí                0.676727   0.431672   1.568 0.116954    
## oldpeak                 0.488454   0.227831   2.144 0.032039 *  
## slope1                  1.265983   0.473538   2.673 0.007507 ** 
## slope2                  0.497388   0.881393   0.564 0.572537    
## ca1 vaso                2.078728   0.486671   4.271 1.94e-05 ***
## ca2 vasos               2.812203   0.739859   3.801 0.000144 ***
## ca3 vasos               2.001039   0.888650   2.252 0.024337 *  
## thalfixed              -0.191516   0.776263  -0.247 0.805128    
## thalreversible          1.450034   0.420053   3.452 0.000556 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 409.95  on 296  degrees of freedom
## Residual deviance: 193.48  on 281  degrees of freedom
## AIC: 225.48
## 
## Number of Fisher Scoring iterations: 6

La regresión logística múltiple evaluó la presencia de enfermedad cardiaca considerando simultáneamente las variables seleccionadas. Los coeficientes del resumen se encuentran expresados en logaritmos de los momios, por lo que su interpretación directa resulta poco intuitiva. Para facilitarla, en el siguiente apartado los coeficientes se transforman en razones de momios mediante exp(coef(modelo_logistico)).

De manera preliminar, el resumen mostró evidencia estadística para el sexo masculino, el dolor torácico asintomático, oldpeak, slope = 1, las categorías de uno a tres vasos y thal = reversible. La edad, max_hr, la angina inducida por ejercicio y algunas categorías de dolor torácico, slope y thal no mostraron evidencia suficiente después del ajuste conjunto. La magnitud exacta de estas asociaciones se interpreta mediante las razones de momios y sus intervalos de confianza.

exp(coef(modelo_logistico))
##            (Intercept)                    age           sexmasculino 
##             0.03595083             1.00129647             3.81227257 
##   chest_painnontypical   chest_painnonanginal chest_painasymptomatic 
##             3.32417241             1.04451089             8.08518421 
##                 max_hr               ex_angsí                oldpeak 
##             0.98787672             1.96742687             1.62979449 
##                 slope1                 slope2               ca1 vaso 
##             3.54657738             1.64441970             7.99429557 
##              ca2 vasos              ca3 vasos              thalfixed 
##            16.64655765             7.39673574             0.82570634 
##         thalreversible 
##             4.26325855
exp(
  confint.default(modelo_logistico)
)
##                               2.5 %    97.5 %
## (Intercept)            0.0002204662  5.862406
## age                    0.9571957943  1.047429
## sexmasculino           1.4196728514 10.237163
## chest_painnontypical   0.7473682448 14.785378
## chest_painnonanginal   0.2832820124  3.851296
## chest_painasymptomatic 2.2118213075 29.554921
## max_hr                 0.9671957193  1.009000
## ex_angsí               0.8442255123  4.584994
## oldpeak                1.0428032170  2.547202
## slope1                 1.4019510005  8.971933
## slope2                 0.2922598998  9.252436
## ca1 vaso               3.0798207536 20.750806
## ca2 vasos              3.9044098758 70.973051
## ca3 vasos              1.2960438846 42.214388
## thalfixed              0.1803306983  3.780781
## thalreversible         1.8715086601  9.711616

Las categorías de referencia fueron sexo femenino, dolor torácico typical, ausencia de angina inducida por ejercicio, slope = 0, cero vasos y thal = normal. Manteniendo constantes las demás variables, los hombres presentaron 3.81 veces los momios de enfermedad cardiaca de las mujeres (IC95%: 1.42 a 10.24; p = 0.008). El dolor torácico asintomático presentó 8.09 veces los momios de enfermedad respecto del dolor típico (IC95%: 2.21 a 29.56; p = 0.002).

Cada unidad adicional de oldpeak multiplicó los momios de enfermedad por 1.63, equivalente a un incremento aproximado de 63% (IC95%: 1.04 a 2.55; p = 0.032). La categoría slope = 1 presentó 3.55 veces los momios de enfermedad respecto de slope = 0 (IC95%: 1.40 a 8.97; p = 0.008).

En comparación con cero vasos, las categorías de uno, dos y tres vasos presentaron razones de momios de 7.99, 16.65 y 7.40, respectivamente. Las tres asociaciones fueron estadísticamente significativas, pero sus intervalos de confianza fueron amplios, especialmente en las categorías de dos y tres vasos, lo que indica menor precisión. No se observa un incremento progresivo porque el OR de tres vasos fue menor que el de dos vasos.

Thal = reversible presentó 4.26 veces los momios de enfermedad en comparación con thal = normal (IC95%: 1.87 a 9.71; p < 0.001). En cambio, age, max_hr, ex_ang, slope = 2, thal = fixed y algunas categorías de dolor torácico no mostraron evidencia estadística suficiente después del ajuste conjunto.

Las razones de momios representan asociaciones ajustadas. No son probabilidades directas, riesgos relativos ni efectos causales..

Probabilidades y clasificación

heart$probabilidad <- predict(
  modelo_logistico,
  type = "response"
)

heart$prediccion <- ifelse(
  heart$probabilidad >= 0.5,
  "yes",
  "no"
)
tabla_clasificacion <- table(
  Observado = heart$ahd,
  Predicho = heart$prediccion
)

tabla_clasificacion
##          Predicho
## Observado  no yes
##       no  146  14
##       yes  23 114

La matriz de clasificación muestra que el modelo identificó correctamente 146 de los 160 casos sin enfermedad y 114 de los 137 casos con enfermedad. También clasificó incorrectamente 14 casos sin enfermedad como positivos y 23 casos con enfermedad como negativos. En total, realizó 260 clasificaciones correctas y 37 incorrectas, para una exactitud de 87.5%.

El porcentaje de clasificación correcta fue de 91.3% entre los casos sin enfermedad y de 83.2% entre los casos con enfermedad. Sin embargo, estas cifras se calcularon utilizando la misma base con la que se construyó el modelo, por lo que describen su desempeño interno y pueden ser optimistas.

mean(
  heart$ahd == heart$prediccion
)
## [1] 0.8754209
heart |>
  group_by(ahd) |>
  summarise(
    numero = n(),
    probabilidad_media = mean(probabilidad),
    probabilidad_mediana = median(probabilidad)
  )
## # A tibble: 2 × 4
##   ahd   numero probabilidad_media probabilidad_mediana
##   <fct>  <int>              <dbl>                <dbl>
## 1 no       160              0.183               0.0973
## 2 yes      137              0.786               0.918

La probabilidad media estimada fue de 0.183 en las personas sin enfermedad y de 0.786 en aquellas con enfermedad. Las medianas fueron de 0.097 y 0.918, respectivamente. Esto indica que el modelo asignó, en general, probabilidades mayores a las personas que efectivamente presentaban enfermedad. Sin embargo, estas probabilidades fueron calculadas con los mismos datos utilizados para ajustar el modelo.

ggplot(
  heart,
  aes(x = probabilidad, fill = ahd)
) +
  geom_histogram(
    bins = 20,
    alpha = 0.6,
    position = "identity"
  ) +
  labs(
    title = "Probabilidades estimadas según AHD",
    x = "Probabilidad estimada",
    y = "Frecuencia",
    fill = "AHD"
  ) +
  theme_minimal()

El histograma muestra que las probabilidades de las personas sin enfermedad se concentran principalmente en valores bajos, mientras que las probabilidades de quienes presentan enfermedad se desplazan hacia valores altos. No obstante, existe traslape entre las distribuciones: algunos casos sin enfermedad recibieron probabilidades elevadas y algunos casos con enfermedad recibieron probabilidades bajas. Este traslape explica las 37 clasificaciones incorrectas observadas en la matriz de confusión.

Comparación de modelos

Se comparan los modelos completos con versiones más sencillas.

modelo_lineal_reducido <- lm(
  max_hr ~ age + oldpeak,
  data = heart
)

AIC(
  modelo_lineal_reducido,
  modelo_lineal
)
##                        df      AIC
## modelo_lineal_reducido  4 2632.635
## modelo_lineal           8 2604.825
modelo_logistico_reducido <- glm(
  ahd_binaria ~ age + max_hr + oldpeak,
  data = heart,
  family = binomial
)

AIC(
  modelo_logistico_reducido,
  modelo_logistico
)
##                           df      AIC
## modelo_logistico_reducido  4 328.5080
## modelo_logistico          16 225.4775

El modelo lineal reducido obtuvo un AIC de 2632.6, mientras que el modelo lineal completo obtuvo un AIC de 2604.8. El modelo logístico reducido obtuvo un AIC de 328.5 y el modelo logístico completo un AIC de 225.5. Dentro de cada comparación, el menor AIC favorece al modelo completo porque presenta un mejor equilibrio inicial entre ajuste y complejidad.

Estos resultados no demuestran que los modelos funcionen adecuadamente con datos nuevos. Tampoco debe compararse directamente el AIC del modelo lineal con el del modelo logístico, porque utilizan variables respuesta y distribuciones diferentes.

Resultados principales

El análisis exploratorio mostró que 46.1% de las observaciones presentó enfermedad cardiaca. Las diferencias numéricas más claras se observaron en age, max_hr y oldpeak. Las pruebas bivariadas también identificaron asociaciones de ahd con sex, chest_pain, rest_ecg, ex_ang, slope, ca y thal; fbs no mostró asociación.

La regresión lineal explicó 31.9% de la variación de max_hr. Después del ajuste, age, oldpeak, rest_bp y ahd mostraron asociaciones estadísticamente significativas, mientras que sex y chol no presentaron evidencia suficiente.

En la regresión logística, las asociaciones ajustadas más claras correspondieron al sexo masculino, el dolor torácico asintomático, oldpeak, slope = 1, las categorías de uno a tres vasos y thal = reversible. La edad, max_hr y ex_ang, aunque mostraron patrones bivariados, no conservaron evidencia suficiente después del ajuste. La exactitud interna fue de 87.5%.

Conclusiones

El análisis se realizó con 297 observaciones completas, después de eliminar seis registros con valores faltantes. Los resultados corresponden a esta muestra y deben interpretarse como asociaciones estadísticas, no como relaciones causales.

En la regresión lineal múltiple, la edad, oldpeak, la presión arterial en reposo y la presencia de enfermedad cardiaca mostraron asociaciones estadísticamente significativas con la frecuencia cardiaca máxima (max_hr), manteniendo constantes las demás variables incluidas en el modelo.

Por cada año adicional de edad, max_hr disminuyó en promedio 0.861 latidos por minuto. Cada unidad adicional de oldpeak se asoció con una disminución promedio de 3.574 latidos por minuto. Asimismo, las personas con enfermedad cardiaca presentaron, en promedio, 14.090 latidos por minuto menos que las personas sin enfermedad. La presión arterial en reposo mostró una asociación positiva de pequeña magnitud: por cada unidad adicional, max_hr aumentó en promedio 0.157 latidos por minuto.

El sexo y el colesterol no mostraron evidencia estadística suficiente de una asociación independiente con max_hr. En particular, aunque el coeficiente del colesterol fue positivo, su valor p fue de 0.070 y su intervalo de confianza incluyó cero, por lo que no puede concluirse que exista una asociación después del ajuste.

El modelo lineal obtuvo un R² de 0.319 y un R² ajustado de 0.305. Esto significa que las variables incluidas explicaron aproximadamente 31.9% de la variación observada en max_hr. Por tanto, el modelo permite explicar una parte de las diferencias en la frecuencia cardiaca máxima, pero la mayor parte de su variación continúa relacionada con otros factores, con la variabilidad individual y con el error. Este resultado no demuestra capacidad predictiva fuera de la muestra.

Para analizar la presencia de enfermedad cardiaca se utilizó una regresión logística múltiple. Después de ajustar simultáneamente por las demás variables, el sexo masculino, el dolor torácico asintomático, oldpeak, la categoría 1 de slope, el número de vasos y thal = reversible presentaron las asociaciones más claras.

Los hombres tuvieron 3.81 veces los momios de enfermedad cardiaca de las mujeres. Las personas con dolor torácico asintomático presentaron 8.09 veces los momios de enfermedad respecto de quienes tenían dolor típico. Por cada unidad adicional de oldpeak, los momios de enfermedad se multiplicaron por 1.63, lo que representa un incremento aproximado de 63%.

La categoría 1 de slope presentó 3.55 veces los momios de enfermedad respecto de la categoría 0. En comparación con las personas con cero vasos, las categorías de uno, dos y tres vasos presentaron razones de momios de 7.99, 16.65 y 7.40, respectivamente. Estos valores no muestran un incremento progresivo y sus intervalos de confianza fueron amplios, por lo que deben interpretarse con cautela. Por su parte, thal = reversible presentó 4.26 veces los momios de enfermedad en comparación con thal = normal.

La edad, la frecuencia cardiaca máxima y la angina inducida por ejercicio no mostraron evidencia estadística suficiente de una asociación independiente en el modelo logístico completo. Esto no significa que carezcan de relación con la enfermedad. Indica que las diferencias observadas inicialmente se redujeron al considerar simultáneamente las demás variables.

Con un punto de corte de 0.5, el modelo logístico clasificó correctamente 260 de las 297 observaciones, lo que equivale a una exactitud de 87.5%. Identificó correctamente 114 de los 137 casos con enfermedad y 146 de los 160 casos sin enfermedad. No obstante, esta exactitud es interna y puede ser optimista porque el modelo se construyó y evaluó utilizando la misma base.

El menor AIC de los modelos completos indica que presentaron un mejor equilibrio entre ajuste y complejidad que las versiones reducidas dentro de la muestra analizada. Sin embargo, el AIC no demuestra por sí mismo que los modelos tengan un buen desempeño con datos nuevos.

En conclusión, la regresión lineal permitió identificar variables relacionadas con la frecuencia cardiaca máxima, mientras que la regresión logística permitió reconocer un conjunto de características asociadas con la presencia de enfermedad cardiaca. El modelo logístico es el más adecuado para responder la pregunta principal del estudio, pero todavía debe considerarse exploratorio. Los resultados no constituyen una herramienta diagnóstica ni permiten afirmar que las variables identificadas causen enfermedad cardiaca.

Antes de utilizar el modelo con fines predictivos, será necesario validarlo con datos diferentes, revisar posibles observaciones influyentes, evaluar sensibilidad, especificidad, curva ROC, AUC y calibración, y analizar la estabilidad de las estimaciones en las categorías con pocos casos.

Recomendaciones

Referencias