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: 0x0000024c4dffea30>
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.
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.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.
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.
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.
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.
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 |
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.
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._
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.
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.
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 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.
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().
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.
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..
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.
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.
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%.
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.