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: 0x0000023f526fea70>

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: 0x0000023f526fea70>
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.5 años y la mediana de 56 años. chol y max_hr mostraron la mayor dispersión en sus escalas originales.

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()

La edad se concentra principalmente en personas de mediana edad y mayores.

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

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 (53.9%) y 137 con enfermedad (46.1%).

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")

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 ahd = yes presentaron mayor edad promedio, menor frecuencia cardiaca máxima y mayor oldpeak.

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")

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")

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")

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
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 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'

Se observa una relación negativa: la frecuencia cardiaca máxima tiende a disminuir conforme aumenta la edad.

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

Se identificaron nueve posibles valores atípicos en rest_bp, cinco en chol, uno en max_hr y cinco en oldpeak. No se eliminaron porque pueden corresponder a valores reales.

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
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
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

En el grupo sin enfermedad, el intervalo fue de 51.15 a 54.14 años para age, de 155.61 a 161.55 para max_hr y de 0.48 a 0.72 para oldpeak. En el grupo con enfermedad, los intervalos fueron de 55.42 a 58.09, de 135.27 a 142.95 y de 1.37 a 1.81, respectivamente. Los intervalos muestran diferencias claras entre los grupos, especialmente en max_hr y oldpeak.

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

Debido a los posibles valores atípicos, se realizan pruebas de Wilcoxon como complemento.

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

Los resultados fueron:

  • age: diferencia entre grupos, p < 0.001.
  • rest_bp: diferencia de menor magnitud, p = 0.009.
  • chol: la prueba t no mostró diferencia, p = 0.165.
  • max_hr: diferencia clara, p < 0.001.
  • oldpeak: diferencia clara, p < 0.001.

En chol, Wilcoxon produjo p = 0.047. Por ello, el resultado debe interpretarse con cautela.

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

Se encontró asociación de ahd con sex, chest_pain, rest_ecg, ex_ang, slope, ca y thal. fbs no mostró asociación. La presencia de enfermedad fue de 55.7% entre los hombres y 26.0% entre las mujeres. También fue mayor en las categorías asymptomatic de dolor torácico (72.5%), presencia de angina por ejercicio (76.3%) y thal = reversible (76.5%).

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 selección consideró los resultados exploratorios, las pruebas estadísticas, las correlaciones y el propósito de cada modelo.

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

Los intervalos de confianza de los coeficientes se obtienen con confint().

confint(modelo_lineal)
##                      2.5 %       97.5 %
## (Intercept)  153.315198301 196.17521075
## age           -1.124409815  -0.59707005
## sexmasculino  -3.128420948   6.96983095
## rest_bp        0.026510486   0.28817925
## chol          -0.003396765   0.08467555
## oldpeak       -5.675284490  -1.47371154
## ahdyes       -19.236324936  -8.94437282

Los principales resultados fueron:

  • Cada año adicional de edad se asoció con una disminución promedio de 0.861 latidos por minuto en max_hr.
  • Cada unidad adicional de oldpeak se asoció con una disminución promedio de 3.574 latidos por minuto.
  • Las personas con ahd = yes presentaron, en promedio, 14.090 latidos por minuto menos que las personas con ahd = no.
  • rest_bp mostró una asociación positiva pequeña.
  • sex y chol no mostraron evidencia suficiente después del ajuste.

El modelo obtuvo R² = 0.319 y R² ajustada = 0.305. Por tanto, explicó aproximadamente 31.9% de la variación de max_hr.

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
)

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 a ambos lados de cero, aunque existe una dispersión considerable y algunos valores de mayor magnitud.

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

Las razones de momios se obtienen al aplicar exp() a los coeficientes.

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

Los intervalos de confianza se calculan de la siguiente manera.

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 asociaciones ajustadas más claras fueron:

  • Sexo masculino: OR = 3.81.
  • Dolor torácico asintomático: OR = 8.09.
  • oldpeak: OR = 1.63 por cada unidad.
  • slope = 1: OR = 3.55.
  • Uno, dos y tres vasos: OR = 7.99, 16.65 y 7.40.
  • thal = reversible: OR = 4.26.

Los OR expresan asociaciones ajustadas. No representan probabilidades directas 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 exactitud se obtiene con un comando directo.

mean(
  heart$ahd == heart$prediccion
)
## [1] 0.8754209

El modelo clasificó correctamente 260 de 297 observaciones, equivalente a 87.5%.

Las probabilidades estimadas se resumen por grupo observado.

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 fue 0.183 para las personas sin enfermedad y 0.786 para las personas con enfermedad. Las medianas fueron 0.097 y 0.918, respectivamente. Estos resultados se calcularon con la misma base utilizada para construir el modelo, por lo que pueden ser optimistas.

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()

Las personas con enfermedad recibieron, en general, probabilidades mayores. Sin embargo, existe traslape entre los grupos.

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 completo obtuvo un AIC de 2604.8, menor que el modelo reducido. El modelo logístico completo obtuvo un AIC de 225.5, también menor que el modelo reducido. Esto indica un mejor ajuste inicial de los modelos completos.

Resultados principales

El análisis mostró que 46.1% de las observaciones tenía enfermedad cardiaca. Las diferencias numéricas más claras se observaron en age, max_hr y oldpeak. Entre las variables categóricas destacaron chest_pain, ex_ang, ca y thal.

La regresión lineal explicó 31.9% de la variación de max_hr. La edad, oldpeak, rest_bp y ahd fueron las variables más relevantes en este modelo.

La regresión logística mostró asociaciones ajustadas con el sexo masculino, el dolor torácico asintomático, oldpeak, slope, ca y thal. La exactitud interna fue de 87.5%.

Conclusiones

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