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
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.
Este documento recupera y organiza los resultados de las cuatro actividades realizadas durante el curso.
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:
ahd?Se construyeron dos modelos:
max_hr como variable
respuesta.ahd como variable
respuesta.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.
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
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)
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>
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.
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"
)
| 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 |
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"
)
| 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.
En la Actividad 1 se realizaron las siguientes acciones:
clean_names().id.ca y dos
en thal.slope se recodificó de 1, 2 y 3 a 0, 1 y 2.reversable se corrigió a
reversible.grupo_edad y ahd_binaria.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"
)
| 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 |
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"
)
| 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.
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")
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.
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.
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.
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"
)
| 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.
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.
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.
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%).
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.
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"
)
| 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.
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:
max_hr.oldpeak se asoció con una
disminución promedio de 3.574 latidos por minuto.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.
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
)
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.
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:
oldpeak: OR = 1.63 por cada unidad.slope = 1: OR = 3.55.thal = reversible: OR = 4.26.Los OR expresan asociaciones ajustadas. No representan probabilidades directas ni efectos causales.
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.
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.
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%.
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.