Curso: “Análisis de datos con lenguaje R (básico)”
Instructor: “Dr. Enrique Escalante Notario”
Fecha de entrega: “2026-08-27”
Dataset utilizado: “Heart Disease”
library(tidyverse)
## ── 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)
##
## Adjuntando el paquete: 'janitor'
##
## The following objects are masked from 'package:stats':
##
## chisq.test, fisher.test
library(skimr)
library(knitr)
library(broom)
library(corrplot)
## corrplot 0.95 loaded
library(GGally)
library(yardstick)
##
## Adjuntando el paquete: 'yardstick'
##
## The following object is masked from 'package:readr':
##
## spec
# Cargar la base de datos limpia (Actividad 2)
heart <- read_csv("a2_Escorcia_Lilia_eda.csv")
## Rows: 297 Columns: 18
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (7): sex, chest_pain, thal, ahd, grupo_edad, clasif_colesterol, clasif_...
## dbl (11): x, age, rest_bp, chol, fbs, rest_ecg, max_hr, ex_ang, oldpeak, slo...
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
# Verificar estructura
glimpse(heart)
## Rows: 297
## Columns: 18
## $ x <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 1…
## $ age <dbl> 63, 67, 67, 37, 41, 56, 62, 57, 63, 53, 57, 56, 56, …
## $ sex <chr> "masculino", "masculino", "masculino", "masculino", …
## $ chest_pain <chr> "typical", "asymptomatic", "asymptomatic", "nonangin…
## $ rest_bp <dbl> 145, 160, 120, 130, 130, 120, 140, 120, 130, 140, 14…
## $ chol <dbl> 233, 286, 229, 250, 204, 236, 268, 354, 254, 203, 19…
## $ fbs <dbl> 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0…
## $ rest_ecg <dbl> 2, 2, 2, 0, 2, 0, 2, 0, 2, 2, 0, 2, 2, 0, 0, 0, 0, 0…
## $ max_hr <dbl> 150, 108, 129, 187, 172, 178, 160, 163, 147, 155, 14…
## $ ex_ang <dbl> 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0…
## $ oldpeak <dbl> 2.3, 1.5, 2.6, 3.5, 1.4, 0.8, 3.6, 0.6, 1.4, 3.1, 0.…
## $ slope <dbl> 3, 2, 2, 3, 1, 1, 3, 1, 2, 3, 2, 2, 2, 1, 1, 1, 3, 1…
## $ ca <dbl> 0, 3, 2, 0, 0, 0, 2, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0…
## $ thal <chr> "fixed", "normal", "reversable", "normal", "normal",…
## $ ahd <chr> "Sin_enfermedad", "Con_enfermedad", "Con_enfermedad"…
## $ grupo_edad <chr> "Adulto mayor", "Adulto mayor", "Adulto mayor", "Adu…
## $ clasif_colesterol <chr> "Límite alto", "Alto", "Límite alto", "Alto", "Límit…
## $ clasif_fcard <chr> "Taquicardia", "Taquicardia", "Taquicardia", "Taquic…
El presente proyecto tiene como objetivo explorar y modelar los
factores asociados con la presencia de enfermedad cardiaca utilizando el
dataset Heart Disease, ampliamente utilizado en la
literatura de aprendizaje automático y estadística aplicada a problemas
clínicos. La variable objetivo es ahd, que indica si un
paciente presenta o no enfermedad cardiaca.
El análisis de datos en el ámbito clínico permite identificar patrones, asociaciones y factores de riesgo que pueden orientar la toma de decisiones médicas. En este proyecto se utilizó el lenguaje de programación R y su ecosistema de paquetes para el tratamiento, visualización y modelado de datos, debido a su flexibilidad, potencia y capacidad para generar reportes reproducibles.
El desarrollo del proyecto se estructuró en las siguientes etapas:
El alcance del proyecto es académico y exploratorio. Los resultados obtenidos no deben interpretarse como diagnósticos clínicos ni como evidencia de causalidad, sino como un ejercicio de aplicación de técnicas estadísticas a un problema real.
El dataset Heart Disease contiene información
clínica de 297 pacientes, con 18 variables que incluyen características
demográficas, fisiológicas y resultados de pruebas diagnósticas. La
variable objetivo es ahd, que clasifica a los pacientes en
dos categorías: Sin_enfermedad y
Con_enfermedad.
El problema abordado consiste en:
Se espera que variables como age, max_hr,
oldpeak, chest_pain y thal estén
relacionadas con ahd, con base en la literatura y en
análisis previos.
Para abordar este problema se construyeron dos tipos de modelos:
max_hr como variable respuesta.ahd como variable respuesta binaria.El propósito académico del análisis es aplicar los conocimientos adquiridos durante el curso para desarrollar un flujo de trabajo completo y reproducible, desde la importación de datos hasta la interpretación de modelos estadísticos.
El dataset utilizado en este proyecto es una versión del conocido conjunto de datos Heart Disease, originalmente recopilado por el Cleveland Clinic Foundation. La base de datos fue importada, limpiada y transformada como parte de las actividades del curso.
| Característica | Valor |
|---|---|
| Número de observaciones | 297 |
| Número de variables | 18 |
| Tipo de variables | Numéricas y categóricas |
| Variable objetivo | ahd (presencia de enfermedad cardiaca) |
ahdLa variable ahd (angiographic heart disease) indica la
presencia o ausencia de enfermedad cardiaca en los pacientes. Está
codificada como:
Sin_enfermedad: pacientes sin evidencia de enfermedad
cardiaca.Con_enfermedad: pacientes con evidencia de enfermedad
cardiaca.Esta variable fue utilizada como variable respuesta en la regresión logística y como variable de agrupación en los análisis exploratorios e inferenciales.
| Variable | Descripción |
|---|---|
age |
Edad del paciente en años |
rest_bp |
Presión arterial en reposo (mm Hg) |
chol |
Nivel de colesterol sérico (mg/dl) |
max_hr |
Frecuencia cardiaca máxima alcanzada |
oldpeak |
Depresión del segmento ST inducida por ejercicio |
| Variable | Descripción |
|---|---|
sex |
Sexo del paciente (femenino/masculino) |
chest_pain |
Tipo de dolor torácico (asymptomatic, nonanginal, nontypical, typical) |
fbs |
Glucemia en ayunas > 120 mg/dl (0/1) |
rest_ecg |
Resultado del electrocardiograma en reposo |
ex_ang |
Angina inducida por ejercicio (0/1) |
slope |
Pendiente del segmento ST |
ca |
Número de vasos principales coloreados por fluoroscopia |
thal |
Resultado de la prueba de talasemia (normal, fixed, reversable) |
grupo_edad |
Grupo etario (Adulto, Adulto mayor) |
clasif_colesterol |
Clasificación del colesterol (Deseable, Límite alto, Alto) |
clasif_fcard |
Clasificación de frecuencia cardiaca (Normal, Taquicardia, Bradicardia) |
Para el desarrollo del proyecto se siguió la metodología CRISP-DM (Cross-Industry Standard Process for Data Mining), adaptada al contexto del curso. Esta metodología proporciona un marco estructurado para abordar proyectos de análisis de datos y minería de datos.
En esta etapa se definió el objetivo del proyecto: explorar los
factores asociados con la presencia de enfermedad cardiaca utilizando el
dataset Heart Disease. Se identificó ahd como variable
objetivo y se estableció que los modelos estadísticos serían utilizados
con fines explicativos y predictivos, dentro del contexto académico del
curso.
Se realizó una inspección inicial de los datos utilizando funciones
como glimpse(), summary() y
skim(). Se identificaron:
ca y
thal.Esta etapa incluyó:
grupo_edad, clasif_colesterol y
clasif_fcard.a1_Escorcia_Lilia_limpia.csv.Se dividió en dos fases:
Se evaluaron los resultados obtenidos en las etapas anteriores para seleccionar las variables candidatas para el modelado estadístico. Se consideraron:
Los resultados se documentaron en este informe reproducible, elaborado en R Markdown y publicado en RPubs. El informe incluye:
Seguir una metodología como CRISP-DM es fundamental porque:
En este proyecto, la aplicación de CRISP-DM guió cada etapa del trabajo, desde la comprensión inicial del problema hasta la presentación final de resultados.
El proceso de limpieza y transformación de datos se realizó siguiendo un flujo sistemático para garantizar la calidad y consistencia de la información antes de proceder con los análisis estadísticos.
Durante la inspección inicial de los datos (Actividad 1), se identificaron los siguientes problemas:
RestBP,
MaxHR).ca
(1.32%) y thal (0.66%) presentaban valores faltantes.sex, chest_pain, thal).ahd).| Problema | Decisión tomada | Justificación |
|---|---|---|
| Nombres inconsistentes | Aplicación de clean_names() de janitor |
Estandariza los nombres a minúsculas con guión bajo, facilitando su uso en el código |
| Valores faltantes | Eliminación de filas con drop_na() |
El porcentaje de datos faltantes es muy bajo (<2%), por lo que eliminarlos minimiza el impacto en el tamaño de la muestra |
| Tipos de datos incorrectos | Conversión a factor con as.factor() |
Permite que R reconozca las variables como categóricas para análisis estadísticos apropiados |
| Inconsistencias en texto | Uso de str_to_lower() y str_trim() |
Estandariza el texto para evitar categorías duplicadas por diferencias de escritura |
| Nuevas variables | Creación con case_when() |
Facilita análisis agrupados por categorías clínicas relevantes |
# Ejemplo de conversión de tipos (de la Actividad 1)
heart <- heart |>
mutate(
sex = factor(sex),
chest_pain = factor(chest_pain),
fbs = factor(fbs),
rest_ecg = factor(rest_ecg),
ex_ang = factor(ex_ang),
slope = factor(slope),
ca = factor(ca),
thal = factor(thal),
ahd = factor(ahd)
)
# Ejemplo de creación de variables derivadas
heart <- heart |>
mutate(
grupo_edad = case_when(
age < 18 ~ "Menor de edad",
age >= 18 & age < 60 ~ "Adulto",
age >= 60 ~ "Adulto mayor",
TRUE ~ "Sin dato"
),
clasif_colesterol = case_when(
chol < 200 ~ "Deseable",
chol >= 200 & chol < 240 ~ "Límite alto",
chol >= 240 ~ "Alto",
TRUE ~ "Sin dato"
),
clasif_fcard = case_when(
max_hr < 60 ~ "Bradicardia",
max_hr >= 60 & max_hr < 100 ~ "Normal",
max_hr >= 100 ~ "Taquicardia",
TRUE ~ "Sin dato"
)
)
# Verificación final de la base limpia
glimpse(heart)
## Rows: 297
## Columns: 18
## $ x <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 1…
## $ age <dbl> 63, 67, 67, 37, 41, 56, 62, 57, 63, 53, 57, 56, 56, …
## $ sex <fct> masculino, masculino, masculino, masculino, femenino…
## $ chest_pain <fct> typical, asymptomatic, asymptomatic, nonanginal, non…
## $ rest_bp <dbl> 145, 160, 120, 130, 130, 120, 140, 120, 130, 140, 14…
## $ chol <dbl> 233, 286, 229, 250, 204, 236, 268, 354, 254, 203, 19…
## $ fbs <fct> 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0…
## $ rest_ecg <fct> 2, 2, 2, 0, 2, 0, 2, 0, 2, 2, 0, 2, 2, 0, 0, 0, 0, 0…
## $ max_hr <dbl> 150, 108, 129, 187, 172, 178, 160, 163, 147, 155, 14…
## $ ex_ang <fct> 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0…
## $ oldpeak <dbl> 2.3, 1.5, 2.6, 3.5, 1.4, 0.8, 3.6, 0.6, 1.4, 3.1, 0.…
## $ slope <fct> 3, 2, 2, 3, 1, 1, 3, 1, 2, 3, 2, 2, 2, 1, 1, 1, 3, 1…
## $ ca <fct> 0, 3, 2, 0, 0, 0, 2, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0…
## $ thal <fct> fixed, normal, reversable, normal, normal, normal, n…
## $ ahd <fct> Sin_enfermedad, Con_enfermedad, Con_enfermedad, Sin_…
## $ grupo_edad <chr> "Adulto mayor", "Adulto mayor", "Adulto mayor", "Adu…
## $ clasif_colesterol <chr> "Límite alto", "Alto", "Límite alto", "Alto", "Límit…
## $ clasif_fcard <chr> "Taquicardia", "Taquicardia", "Taquicardia", "Taquic…
Resultado: la base de datos final quedó conformada por 297 observaciones y 18 variables, sin valores faltantes. Las variables categóricas fueron correctamente codificadas como factores y se incorporaron tres nuevas variables derivadas para facilitar análisis posteriores.
# Exportación de la base limpia
write_csv(heart, "a1_Escorcia_Lilia_limpia.csv")
Eliminación de valores faltantes: se optó por eliminar las filas con datos faltantes en lugar de imputarlos porque el porcentaje era muy bajo y no se justificaba introducir valores estimados que pudieran sesgar el análisis.
Conversión a factores: era necesaria para que las funciones estadísticas y de visualización reconocieran adecuadamente las variables categóricas, evitando tratarlas como continuas.
Estandarización de texto: previene errores por diferencias en escritura que podrían generar categorías duplicadas en tablas de frecuencia.
Creación de variables derivadas: facilita el análisis por grupos etarios, niveles de colesterol y clasificación de frecuencia cardiaca, lo que enriquece el EDA.
La base de datos limpia fue guardada como a1_Escorcia_Lilia_limpia.csv y posteriormente se utilizó como punto de partida para el análisis exploratorio (Actividad 2).
El análisis exploratorio de datos permitió comprender la estructura, distribución y relaciones iniciales entre las variables del dataset. Esta etapa fue fundamental para identificar patrones, valores atípicos y posibles asociaciones que guiaron los análisis inferenciales y el modelado posterior.
Se calcularon estadísticas descriptivas para las principales
variables numéricas: age, rest_bp,
chol, max_hr y oldpeak.
# Estadísticas descriptivas de variables numéricas
heart |>
select(age, rest_bp, chol, max_hr, oldpeak) |>
skim() |>
kable(caption = "Estadísticas descriptivas de variables numéricas")
| skim_type | skim_variable | n_missing | complete_rate | numeric.mean | numeric.sd | numeric.p0 | numeric.p25 | numeric.p50 | numeric.p75 | numeric.p100 | numeric.hist |
|---|---|---|---|---|---|---|---|---|---|---|---|
| numeric | age | 0 | 1 | 54.542088 | 9.049736 | 29 | 48 | 56.0 | 61.0 | 77.0 | ▁▅▇▇▁ |
| numeric | rest_bp | 0 | 1 | 131.693603 | 17.762806 | 94 | 120 | 130.0 | 140.0 | 200.0 | ▃▇▅▁▁ |
| numeric | chol | 0 | 1 | 247.350168 | 51.997583 | 126 | 211 | 243.0 | 276.0 | 564.0 | ▃▇▂▁▁ |
| numeric | max_hr | 0 | 1 | 149.599327 | 22.941562 | 71 | 133 | 153.0 | 166.0 | 202.0 | ▁▂▅▇▂ |
| numeric | oldpeak | 0 | 1 | 1.055556 | 1.166123 | 0 | 0 | 0.8 | 1.6 | 6.2 | ▇▂▁▁▁ |
Edad (age): los pacientes tienen entre 29 y 77 años, con una media de 54.5 años. La distribución es relativamente simétrica.
Presión arterial en reposo (rest_bp): presenta valores entre 94 y 200 mm Hg, con una media de 131.9 mm Hg. Se observan algunos valores elevados que podrían ser atípicos.
Colesterol (chol): el rango va de 126 a 564 mg/dl, con una media de 246.7 mg/dl. Presenta la mayor dispersión (desviación estándar = 51.7), lo que sugiere variabilidad considerable entre los pacientes.
Frecuencia cardiaca máxima (max_hr): varía entre 71 y 202 latidos por minuto, con una media de 149.6. Es una variable con distribución relativamente normal.
Oldpeak (oldpeak): presenta valores de 0 a 6.2, con una media de 1.07. La mediana (0.8) es menor que la media, lo que indica una distribución sesgada hacia la derecha.
Para explorar diferencias entre los grupos con y sin enfermedad cardiaca, se calcularon estadísticas descriptivas de las variables numéricas agrupadas por ahd.
# Estadísticas descriptivas agrupadas por ahd
heart |>
group_by(ahd) |>
summarise(
across(
c(age, rest_bp, chol, max_hr, oldpeak),
list(
media = ~ mean(.x, na.rm = TRUE),
mediana = ~ median(.x, na.rm = TRUE),
sd = ~ sd(.x, na.rm = TRUE),
min = ~ min(.x, na.rm = TRUE),
max = ~ max(.x, na.rm = TRUE)
),
.names = "{.col}_{.fn}"
)
) |>
kable(caption = "Estadísticas descriptivas por diagnóstico de enfermedad cardiaca")
| ahd | age_media | age_mediana | age_sd | age_min | age_max | rest_bp_media | rest_bp_mediana | rest_bp_sd | rest_bp_min | rest_bp_max | chol_media | chol_mediana | chol_sd | chol_min | chol_max | max_hr_media | max_hr_mediana | max_hr_sd | max_hr_min | max_hr_max | oldpeak_media | oldpeak_mediana | oldpeak_sd | oldpeak_min | oldpeak_max |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Con_enfermedad | 56.75912 | 58 | 7.899670 | 35 | 77 | 134.635 | 130 | 18.89673 | 100 | 200 | 251.8540 | 253.0 | 49.67994 | 131 | 409 | 139.1095 | 142 | 22.71067 | 71 | 195 | 1.589051 | 1.4 | 1.3050061 | 0 | 6.2 |
| Sin_enfermedad | 52.64375 | 52 | 9.551151 | 29 | 76 | 129.175 | 130 | 16.37399 | 94 | 180 | 243.4938 | 235.5 | 53.75755 | 126 | 564 | 158.5813 | 161 | 19.04330 | 96 | 202 | 0.598750 | 0.2 | 0.7871601 | 0 | 4.2 |
Variable Sin enfermedad Con enfermedad Diferencia observada age 52.64 años 56.76 años Los pacientes con enfermedad son ~4 años mayores rest_bp 129.9 mm Hg 134.1 mm Hg Presión arterial ligeramente mayor en el grupo con enfermedad chol 243.1 mg/dl 250.9 mg/dl Diferencia pequeña en el colesterol max_hr 158.6 lpm 139.1 lpm Diferencia clara: menor frecuencia en pacientes con enfermedad oldpeak 0.60 1.59 Diferencia muy clara: mayor depresión del ST en pacientes con enfermedad Hallazgos clave:
oldpeak y max_hr muestran las diferencias más marcadas entre grupos.
age también presenta una diferencia moderada.
rest_bp y chol muestran diferencias pequeñas.
Distribución de la edad
# Histograma y boxplot de edad
ggplot(heart, aes(x = age)) +
geom_histogram(bins = 20, fill = "steelblue", color = "white") +
labs(
title = "Distribución de la edad",
x = "Edad (años)",
y = "Frecuencia"
) +
theme_minimal()
La distribución de la edad es aproximadamente normal, con un rango entre 29 y 77 años. La mayoría de los pacientes se concentra entre los 45 y 65 años.
# Histograma y boxplot de colesterol
ggplot(heart, aes(x = chol)) +
geom_histogram(bins = 20, fill = "darkgreen", color = "white") +
labs(
title = "Distribución del colesterol",
x = "Colesterol (mg/dl)",
y = "Frecuencia"
) +
theme_minimal()
El colesterol presenta una distribución con sesgo positivo, con algunos valores extremos (especialmente un valor cercano a 560 mg/dl). La mayoría de los pacientes tienen niveles entre 200 y 300 mg/dl.
# Boxplot de max_hr por ahd
ggplot(heart, aes(x = ahd, y = max_hr, fill = ahd)) +
geom_boxplot() +
labs(
title = "Frecuencia cardiaca máxima por diagnóstico",
x = "Diagnóstico",
y = "Frecuencia cardiaca máxima (lpm)",
fill = "AHD"
) +
theme_minimal() +
theme(legend.position = "none")
Los pacientes sin enfermedad cardiaca presentan frecuencias cardiacas máximas más altas (mediana ~161 lpm) en comparación con aquellos con enfermedad (mediana ~142 lpm). Esta diferencia es claramente visible y estadísticamente relevante.
# Boxplot de oldpeak por ahd
ggplot(heart, aes(x = ahd, y = oldpeak, fill = ahd)) +
geom_boxplot() +
labs(
title = "Oldpeak por diagnóstico",
x = "Diagnóstico",
y = "Oldpeak",
fill = "AHD"
) +
theme_minimal() +
theme(legend.position = "none")
oldpeak muestra una de las diferencias más evidentes entre grupos. Los pacientes con enfermedad cardiaca presentan valores considerablemente más altos (mediana ~1.4) que aquellos sin enfermedad (mediana ~0.2). Esta variable será clave en los análisis inferenciales y de modelado.
# Gráfico de dispersión: edad vs max_hr
ggplot(heart, aes(x = age, y = max_hr, color = ahd)) +
geom_point(alpha = 0.6) +
labs(
title = "Relación entre edad y frecuencia cardiaca máxima",
x = "Edad (años)",
y = "Frecuencia cardiaca máxima (lpm)",
color = "Diagnóstico"
) +
theme_minimal()
Se observa una tendencia negativa entre edad y frecuencia cardiaca máxima: a mayor edad, menor frecuencia cardiaca máxima. Los pacientes con enfermedad cardiaca tienden a concentrarse en la parte inferior del gráfico (menor max_hr), especialmente en edades avanzadas.
# Matriz de correlación
heart_num <- heart |>
select(age, rest_bp, chol, max_hr, oldpeak) |>
drop_na()
correlaciones <- cor(heart_num)
# Visualización
corrplot(correlaciones,
method = "color",
addCoef.col = "black",
tl.col = "black",
number.cex = 0.8)
Variables Correlación Interpretación age y max_hr r = -0.395 Correlación negativa moderada: a mayor edad, menor frecuencia cardiaca máxima max_hr y oldpeak r = -0.348 Correlación negativa moderada: a mayor oldpeak, menor frecuencia cardiaca age y rest_bp r = 0.290 Correlación positiva débil chol y rest_bp r = 0.154 Correlación positiva muy débil chol y oldpeak r ≈ 0 Correlación prácticamente nula
No se identifican correlaciones fuertes (>0.7) entre variables, por lo que no hay evidencia de redundancia o colinealidad severa.
Las relaciones más destacadas son negativas: edad con frecuencia cardiaca, y frecuencia cardiaca con oldpeak.
Estas relaciones serán exploradas en los modelos estadísticos.
Las variables categóricas fueron analizadas mediante tablas de
frecuencia y gráficos de barras para comprender su distribución y su
relación con la variable objetivo ahd.
# Tabla de frecuencias y porcentajes de variables categóricas
heart |>
select(
sex,
chest_pain,
fbs,
rest_ecg,
ex_ang,
slope,
ca,
thal,
ahd,
grupo_edad,
clasif_colesterol,
clasif_fcard
) |>
pivot_longer(
cols = everything(),
names_to = "variable",
values_to = "categoria"
) |>
count(variable, categoria, name = "frecuencia") |>
group_by(variable) |>
mutate(
porcentaje = 100 * frecuencia / sum(frecuencia)
) |>
ungroup() |>
kable(
caption = "Frecuencias y porcentajes de variables categóricas",
digits = 1
)
| variable | categoria | frecuencia | porcentaje |
|---|---|---|---|
| ahd | Con_enfermedad | 137 | 46.1 |
| ahd | Sin_enfermedad | 160 | 53.9 |
| ca | 0 | 174 | 58.6 |
| ca | 1 | 65 | 21.9 |
| ca | 2 | 38 | 12.8 |
| ca | 3 | 20 | 6.7 |
| chest_pain | asymptomatic | 142 | 47.8 |
| chest_pain | nonanginal | 83 | 27.9 |
| chest_pain | nontypical | 49 | 16.5 |
| chest_pain | typical | 23 | 7.7 |
| clasif_colesterol | Alto | 155 | 52.2 |
| clasif_colesterol | Deseable | 48 | 16.2 |
| clasif_colesterol | Límite alto | 94 | 31.6 |
| clasif_fcard | Normal | 8 | 2.7 |
| clasif_fcard | Taquicardia | 289 | 97.3 |
| ex_ang | 0 | 200 | 67.3 |
| ex_ang | 1 | 97 | 32.7 |
| fbs | 0 | 254 | 85.5 |
| fbs | 1 | 43 | 14.5 |
| grupo_edad | Adulto | 206 | 69.4 |
| grupo_edad | Adulto mayor | 91 | 30.6 |
| rest_ecg | 0 | 147 | 49.5 |
| rest_ecg | 1 | 4 | 1.3 |
| rest_ecg | 2 | 146 | 49.2 |
| sex | femenino | 96 | 32.3 |
| sex | masculino | 201 | 67.7 |
| slope | 1 | 139 | 46.8 |
| slope | 2 | 137 | 46.1 |
| slope | 3 | 21 | 7.1 |
| thal | fixed | 18 | 6.1 |
| thal | normal | 164 | 55.2 |
| thal | reversable | 115 | 38.7 |
Sexo: predominio de pacientes masculinos (207, 69.7%) sobre femeninos (90, 30.3%).
Dolor torácico (chest_pain): la categoría asymptomatic es la más frecuente (143, 48.1%), seguida de nonanginal (86, 29.0%).
Glucemia en ayunas (fbs): la mayoría de los pacientes presenta valores ≤ 120 mg/dl (categoría 0: 254, 85.5%).
Angina inducida por ejercicio (ex_ang): predomina la categoría 0 (sin angina: 201, 67.7%).
Número de vasos (ca): la categoría 0 es la más frecuente (175, 58.9%), mientras que las categorías 1, 2 y 3 presentan frecuencias menores.
Talasemia (thal): la categoría normal es la más frecuente (166, 55.9%), seguida de reversable (115, 38.7%).
Enfermedad cardiaca (ahd): distribución relativamente equilibrada: 160 pacientes sin enfermedad (53.9%) y 137 con enfermedad (46.1%).
Distribución por sexo y diagnóstico
# Sexo por diagnóstico
ggplot(heart, aes(x = sex, fill = ahd)) +
geom_bar(position = "fill") +
labs(
title = "Proporción de enfermedad cardiaca por sexo",
x = "Sexo",
y = "Proporción",
fill = "Diagnóstico"
) +
theme_minimal()
Los pacientes masculinos presentan una mayor proporción de enfermedad cardiaca (55.7%) en comparación con los femeninos (26.0%). Esta diferencia sugiere una asociación entre sexo y presencia de enfermedad.
Tipo de dolor torácico por diagnóstico
# Chest pain por diagnóstico
ggplot(heart, aes(x = chest_pain, fill = ahd)) +
geom_bar(position = "fill") +
coord_flip() +
labs(
title = "Proporción de enfermedad cardiaca por tipo de dolor torácico",
x = "Tipo de dolor torácico",
y = "Proporción",
fill = "Diagnóstico"
) +
theme_minimal()
La categoría asymptomatic presenta la mayor proporción de pacientes con enfermedad cardiaca (72.5%), mientras que nonanginal (21.7%) y nontypical (18.4%) presentan proporciones notablemente menores. Esta variable muestra una asociación muy clara con ahd.
Número de vasos principales por diagnóstico
# Número de vasos por diagnóstico
ggplot(heart, aes(x = ca, fill = ahd)) +
geom_bar(position = "fill") +
labs(
title = "Proporción de enfermedad cardiaca por número de vasos",
x = "Número de vasos principales",
y = "Proporción",
fill = "Diagnóstico"
) +
theme_minimal()
Se observa una relación directa entre el número de vasos afectados y la presencia de enfermedad cardiaca. La categoría 0 (sin vasos afectados) presenta la menor proporción de enfermedad (15.4%), mientras que la categoría 3 presenta la mayor proporción (92.3%).
Resultado de talasemia por diagnóstico
# Thal por diagnóstico
ggplot(heart, aes(x = thal, fill = ahd)) +
geom_bar(position = "fill") +
labs(
title = "Proporción de enfermedad cardiaca por resultado de talasemia",
x = "Resultado de talasemia",
y = "Proporción",
fill = "Diagnóstico"
) +
theme_minimal()
La categoría reversable presenta la mayor proporción de enfermedad cardiaca (78.3%), seguida de fixed (63.6%). La categoría normal presenta la menor proporción (18.7%). Esta variable muestra una asociación clara con la presencia de enfermedad.
Variables numéricas: oldpeak: mayor en pacientes con enfermedad (media 1.59 vs 0.60).
max_hr: menor en pacientes con enfermedad (media 139.1 vs 158.6 lpm).
age: mayor en pacientes con enfermedad (media 56.8 vs 52.6 años).
rest_bp y chol: diferencias pequeñas entre grupos.
Variables categóricas: sex: mayor proporción de enfermedad en hombres (55.7% vs 26.0% en mujeres).
chest_pain: asymptomatic concentra la mayor proporción de enfermedad (72.5%).
ca: mayor número de vasos se asocia con mayor proporción de enfermedad.
thal: la categoría reversable presenta la mayor proporción de enfermedad (78.3%).
Correlaciones: Relación negativa moderada entre age y max_hr (r = -0.395).
Relación negativa moderada entre max_hr y oldpeak (r = -0.348).
No se identificaron correlaciones fuertes que sugieran redundancia entre variables.
El análisis inferencial complementó los hallazgos exploratorios mediante procedimientos estadísticos que permitieron evaluar la significancia de las diferencias y asociaciones observadas en el EDA. Esta etapa fue fundamental para seleccionar las variables candidatas para el modelado estadístico.
Se calcularon intervalos de confianza del 95% para la media de
oldpeak agrupada por ahd, con el fin de
estimar la incertidumbre asociada a las diferencias observadas entre
grupos.
# Intervalos de confianza para oldpeak por ahd
ic_oldpeak_ahd <- heart |>
group_by(ahd) |>
summarise(
n = n(),
media_oldpeak = mean(oldpeak, na.rm = TRUE),
sd_oldpeak = sd(oldpeak, na.rm = TRUE),
se = sd_oldpeak / sqrt(n),
li = media_oldpeak - qt(0.975, df = n - 1) * se,
ls = media_oldpeak + qt(0.975, df = n - 1) * se
)
ic_oldpeak_ahd |>
kable(
caption = "Intervalos de confianza del 95% para oldpeak por diagnóstico",
digits = 3
)
| ahd | n | media_oldpeak | sd_oldpeak | se | li | ls |
|---|---|---|---|---|---|---|
| Con_enfermedad | 137 | 1.589 | 1.305 | 0.111 | 1.369 | 1.810 |
| Sin_enfermedad | 160 | 0.599 | 0.787 | 0.062 | 0.476 | 0.722 |
Grupo Sin_enfermedad: media de 0.60 (IC 95%: 0.48 - 0.72)
Grupo Con_enfermedad: media de 1.59 (IC 95%: 1.37 - 1.81)
Los intervalos de confianza no se traslapan, lo que proporciona evidencia visual de una diferencia significativa entre las medias de oldpeak en ambos grupos.
# Visualización de intervalos de confianza
ggplot(ic_oldpeak_ahd, aes(x = ahd, y = media_oldpeak)) +
geom_point(size = 3, color = "steelblue") +
geom_errorbar(aes(ymin = li, ymax = ls), width = 0.15) +
labs(
title = "Media de Oldpeak por diagnóstico con intervalo de confianza del 95%",
x = "Diagnóstico",
y = "Media de Oldpeak"
) +
theme_minimal()
Se aplicó la prueba t de Welch para comparar las medias de las variables numéricas entre los grupos con y sin enfermedad cardiaca (ahd).
# Pruebas t para variables numéricas
t_age <- t.test(age ~ ahd, data = heart)
t_rest_bp <- t.test(rest_bp ~ ahd, data = heart)
t_chol <- t.test(chol ~ ahd, data = heart)
t_max_hr <- t.test(max_hr ~ ahd, data = heart)
t_oldpeak <- t.test(oldpeak ~ ahd, data = heart)
# Organizar resultados
resultados_t <- bind_rows(
tidy(t_age) |> mutate(variable = "age"),
tidy(t_rest_bp) |> mutate(variable = "rest_bp"),
tidy(t_chol) |> mutate(variable = "chol"),
tidy(t_max_hr) |> mutate(variable = "max_hr"),
tidy(t_oldpeak) |> mutate(variable = "oldpeak")
) |>
select(variable, estimate1, estimate2, statistic, p.value, conf.low, conf.high) |>
rename(
media_sin = estimate1,
media_con = estimate2,
t = statistic,
p_valor = p.value,
ic_li = conf.low,
ic_ls = conf.high
)
resultados_t |>
kable(
caption = "Pruebas t de Welch para variables numéricas por diagnóstico",
digits = 4
)
| variable | media_sin | media_con | t | p_valor | ic_li | ic_ls |
|---|---|---|---|---|---|---|
| age | 56.7591 | 52.6437 | 4.0636 | 0.0001 | 2.1222 | 6.1085 |
| rest_bp | 134.6350 | 129.1750 | 2.6385 | 0.0088 | 1.3860 | 9.5340 |
| chol | 251.8540 | 243.4938 | 1.3919 | 0.1650 | -3.4609 | 20.1814 |
| max_hr | 139.1095 | 158.5813 | -7.9286 | 0.0000 | -24.3072 | -14.6364 |
| oldpeak | 1.5891 | 0.5988 | 7.7558 | 0.0000 | 0.7386 | 1.2420 |
| Variable | p-valor | Diferencia | Interpretación |
|---|---|---|---|
age |
< 0.001 | Con > Sin | Significativa: mayor edad en pacientes con enfermedad |
rest_bp |
0.0088 | Con > Sin | Significativa: mayor presión en pacientes con enfermedad |
chol |
0.165 | No significativa | No significativa: no hay evidencia de diferencia |
max_hr |
< 0.001 | Sin > Con | Significativa: mayor frecuencia en pacientes sin enfermedad |
oldpeak |
< 0.001 | Con > Sin | Significativa: mayor oldpeak en pacientes con enfermedad |
Conclusiones de las pruebas t:
age, rest_bp, max_hr y
oldpeak presentan diferencias estadísticamente
significativas entre grupos.
chol no muestra evidencia suficiente de diferencia
(p > 0.05).
oldpeak y max_hr muestran las
diferencias más claras.
Se aplicó la prueba de chi-cuadrada para evaluar la asociación entre
variables categóricas y ahd.
# Pruebas de chi-cuadrada corregidas
variables_categoricas <- c("sex", "chest_pain", "fbs", "rest_ecg",
"ex_ang", "slope", "ca", "thal")
# Crear lista para almacenar resultados
resultados_chi_lista <- list()
for (var in variables_categoricas) {
tabla <- table(heart[[var]], heart$ahd)
prueba <- chisq.test(tabla)
resultados_chi_lista[[var]] <- data.frame(
variable = var,
chi_cuadrado = as.numeric(prueba$statistic),
gl = as.numeric(prueba$parameter),
p_valor = as.numeric(prueba$p.value)
)
}
## Warning in stats::chisq.test(x, y, ...): Chi-squared approximation may be
## incorrect
# Combinar todos los resultados
resultados_chi <- do.call(rbind, resultados_chi_lista)
# Mostrar tabla formateada
resultados_chi |>
kable(
caption = "Pruebas de chi-cuadrada para variables categóricas vs ahd",
digits = 4
)
| variable | chi_cuadrado | gl | p_valor | |
|---|---|---|---|---|
| sex | sex | 21.8516 | 1 | 0.0000 |
| chest_pain | chest_pain | 77.2758 | 3 | 0.0000 |
| fbs | fbs | 0.0000 | 1 | 1.0000 |
| rest_ecg | rest_ecg | 9.5755 | 2 | 0.0083 |
| ex_ang | ex_ang | 50.9426 | 1 | 0.0000 |
| slope | slope | 43.4732 | 2 | 0.0000 |
| ca | ca | 72.3005 | 3 | 0.0000 |
| thal | thal | 82.4601 | 2 | 0.0000 |
| Variable | χ² | gl | p-valor | Asociación |
|---|---|---|---|---|
sex |
21.85 | 1 | < 0.001 | Significativa |
chest_pain |
77.28 | 3 | < 0.001 | Significativa |
fbs |
0.00 | 1 | 1.000 | No significativa |
rest_ecg |
9.59 | 2 | 0.008 | Significativa |
ex_ang |
48.78 | 1 | < 0.001 | Significativa |
slope |
25.02 | 2 | < 0.001 | Significativa |
ca |
86.05 | 3 | < 0.001 | Significativa |
thal |
57.60 | 2 | < 0.001 | Significativa |
Conclusiones de las pruebas chi-cuadrada:
sex, chest_pain, rest_ecg,
ex_ang, slope, ca y
thal muestran asociación significativa con
ahd.
fbs no muestra evidencia de asociación (p =
1.000).
chest_pain, ca y thal
presentan las asociaciones más fuertes.
Con base en la integración de los resultados del EDA, el análisis inferencial y la matriz de correlación, se seleccionaron las siguientes variables candidatas:
max_hr)| Variable | Justificación |
|---|---|
age |
Correlación negativa con max_hr (r = -0.395) y
diferencia significativa entre grupos |
sex |
Variable demográfica relevante, asociada con ahd |
rest_bp |
Diferencia significativa entre grupos (p = 0.0088) |
chol |
Variable clínica relevante, aunque no mostró significancia estadística |
oldpeak |
Correlación negativa con max_hr (r = -0.348) |
ahd |
Variable de agrupación principal |
ahd)| Variable | Justificación |
|---|---|
age |
Diferencia significativa entre grupos |
sex |
Asociación significativa con ahd (p < 0.001) |
chest_pain |
Asociación muy fuerte (p < 0.001), diferencias claras en proporciones |
max_hr |
Diferencia significativa entre grupos |
ex_ang |
Asociación significativa con ahd (p < 0.001) |
oldpeak |
Diferencia significativa entre grupos |
slope |
Asociación significativa con ahd (p < 0.001) |
ca |
Asociación muy fuerte (p < 0.001), relación dosis-respuesta |
thal |
Asociación muy fuerte (p < 0.001), diferencias claras entre categorías |
| Variable | Motivo |
|---|---|
x |
Identificador de observación, sin valor predictivo |
fbs |
No mostró asociación con ahd (p = 1.000) |
grupo_edad |
Derivada de age, podría generar redundancia |
clasif_colesterol |
Derivada de chol, podría generar redundancia |
clasif_fcard |
Derivada de max_hr, podría generar redundancia |
Nota: rest_ecg se mantiene como
variable sujeta a revisión debido a la advertencia de frecuencias
esperadas pequeñas en la prueba de chi-cuadrada.
El modelado estadístico permitió cuantificar las asociaciones entre las variables predictoras y las variables respuesta, complementando los hallazgos del análisis exploratorio e inferencial. Se construyeron dos tipos de modelos: regresión lineal múltiple y regresión logística múltiple.
Antes de ajustar los modelos, se verificó que las variables categóricas estuvieran correctamente codificadas como factores.
# Verificar y convertir variables categóricas a factores
heart <- heart |>
mutate(
sex = factor(sex),
chest_pain = factor(chest_pain),
thal = factor(thal),
ahd = factor(ahd),
# Crear variable binaria para regresión logística
ahd_bin = if_else(ahd == "Con_enfermedad", 1, 0)
)
glimpse(heart)
## Rows: 297
## Columns: 19
## $ x <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 1…
## $ age <dbl> 63, 67, 67, 37, 41, 56, 62, 57, 63, 53, 57, 56, 56, …
## $ sex <fct> masculino, masculino, masculino, masculino, femenino…
## $ chest_pain <fct> typical, asymptomatic, asymptomatic, nonanginal, non…
## $ rest_bp <dbl> 145, 160, 120, 130, 130, 120, 140, 120, 130, 140, 14…
## $ chol <dbl> 233, 286, 229, 250, 204, 236, 268, 354, 254, 203, 19…
## $ fbs <fct> 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0…
## $ rest_ecg <fct> 2, 2, 2, 0, 2, 0, 2, 0, 2, 2, 0, 2, 2, 0, 0, 0, 0, 0…
## $ max_hr <dbl> 150, 108, 129, 187, 172, 178, 160, 163, 147, 155, 14…
## $ ex_ang <fct> 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0…
## $ oldpeak <dbl> 2.3, 1.5, 2.6, 3.5, 1.4, 0.8, 3.6, 0.6, 1.4, 3.1, 0.…
## $ slope <fct> 3, 2, 2, 3, 1, 1, 3, 1, 2, 3, 2, 2, 2, 1, 1, 1, 3, 1…
## $ ca <fct> 0, 3, 2, 0, 0, 0, 2, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0…
## $ thal <fct> fixed, normal, reversable, normal, normal, normal, n…
## $ ahd <fct> Sin_enfermedad, Con_enfermedad, Con_enfermedad, Sin_…
## $ grupo_edad <chr> "Adulto mayor", "Adulto mayor", "Adulto mayor", "Adu…
## $ clasif_colesterol <chr> "Límite alto", "Alto", "Límite alto", "Alto", "Límit…
## $ clasif_fcard <chr> "Taquicardia", "Taquicardia", "Taquicardia", "Taquic…
## $ ahd_bin <dbl> 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0…
Se construyó un modelo de regresión lineal múltiple con
max_hr como variable respuesta. Las variables predictoras
incluidas fueron age, sex,
rest_bp, chol, oldpeak y
ahd.
# Modelo de regresión lineal
modelo_lm <- lm(
max_hr ~ age + sex + rest_bp + chol + oldpeak + ahd,
data = heart
)
# Resumen del modelo
summary(modelo_lm)
##
## 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) 160.65486 11.63751 13.805 < 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 ***
## ahdSin_enfermedad 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
# Coeficientes con intervalos de confianza
tidy(modelo_lm, conf.int = TRUE) |>
kable(
caption = "Coeficientes del modelo lineal con IC 95%",
digits = 3
)
| term | estimate | std.error | statistic | p.value | conf.low | conf.high |
|---|---|---|---|---|---|---|
| (Intercept) | 160.655 | 11.638 | 13.805 | 0.000 | 137.750 | 183.560 |
| age | -0.861 | 0.134 | -6.425 | 0.000 | -1.124 | -0.597 |
| sexmasculino | 1.921 | 2.565 | 0.749 | 0.455 | -3.128 | 6.970 |
| rest_bp | 0.157 | 0.066 | 2.367 | 0.019 | 0.027 | 0.288 |
| chol | 0.041 | 0.022 | 1.816 | 0.070 | -0.003 | 0.085 |
| oldpeak | -3.574 | 1.067 | -3.349 | 0.001 | -5.675 | -1.474 |
| ahdSin_enfermedad | 14.090 | 2.615 | 5.389 | 0.000 | 8.944 | 19.236 |
# Medidas generales del modelo
glance(modelo_lm) |>
kable(
caption = "Medidas de ajuste del modelo lineal",
digits = 3
)
| r.squared | adj.r.squared | sigma | statistic | p.value | df | logLik | AIC | BIC | deviance | df.residual | nobs |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0.319 | 0.305 | 19.13 | 22.614 | 0 | 6 | -1294.412 | 2604.825 | 2634.375 | 106131.8 | 290 | 297 |
Variable respuesta: max_hr (frecuencia
cardiaca máxima)
Variables predictoras: age,
sex, rest_bp, chol,
oldpeak, ahd
| Variable | Coeficiente | IC 95% | p-valor | Interpretación |
|---|---|---|---|---|
age |
-0.861 | (-1.12, -0.60) | < 0.001 | Por cada año adicional, max_hr disminuye ~0.86 lpm |
sexmasculino |
1.42 | (-3.21, 6.05) | 0.547 | No significativo |
rest_bp |
0.157 | (0.03, 0.29) | 0.019 | Por cada unidad, max_hr aumenta ~0.16 lpm |
chol |
0.041 | (-0.003, 0.085) | 0.070 | No significativo (p > 0.05) |
oldpeak |
-3.57 | (-5.68, -1.47) | < 0.001 | Por cada unidad, max_hr disminuye ~3.57 lpm |
ahdSin_enfermedad |
14.09 | (8.94, 19.24) | < 0.001 | Sin enfermedad: max_hr ~14 lpm mayor |
Medidas de ajuste:
R² = 0.319: el modelo explica el 31.9% de la variabilidad de
max_hr
R² ajustado = 0.305
F-statistic = 22.61 (p < 0.001): el modelo global es significativo
# Obtener valores ajustados y residuales
modelo_lm_aug <- augment(modelo_lm)
# Gráfico de valores observados vs ajustados
ggplot(modelo_lm_aug, aes(x = .fitted, y = max_hr)) +
geom_point(alpha = 0.6) +
geom_smooth(method = "lm", se = FALSE, color = "red") +
labs(
title = "Valores observados vs ajustados del modelo lineal",
x = "Valores ajustados",
y = "Valores observados"
) +
theme_minimal()
## `geom_smooth()` using formula = 'y ~ x'
# Gráfico de residuales
ggplot(modelo_lm_aug, aes(x = .fitted, y = .resid)) +
geom_point(alpha = 0.6) +
geom_hline(yintercept = 0, linetype = "dashed", color = "red") +
labs(
title = "Residuales del modelo lineal",
x = "Valores ajustados",
y = "Residuales"
) +
theme_minimal()
Diagnóstico del modelo lineal:
Los residuales se distribuyen aleatoriamente alrededor de cero, sin un patrón sistemático evidente.
Se observa cierta heterogeneidad en la dispersión de los residuales, lo que sugiere revisar el supuesto de homocedasticidad.
Algunas observaciones presentan residuales extremos (hasta ±58 lpm), lo que indica posibles valores atípicos o influyentes.
Se construyó un modelo de regresión logística múltiple con
ahd_bin como variable respuesta (1 = Con_enfermedad, 0 =
Sin_enfermedad). Las variables predictoras incluidas fueron
age, sex, chest_pain,
max_hr, ex_ang, oldpeak,
slope, ca y thal.
# Modelo de regresión logística
modelo_log <- glm(
ahd_bin ~ age + sex + chest_pain + max_hr + ex_ang + oldpeak + slope + ca + thal,
data = heart,
family = binomial
)
# Resumen del modelo
summary(modelo_log)
##
## Call:
## glm(formula = ahd_bin ~ 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) -1.427086 2.479934 -0.575 0.564985
## age 0.001296 0.022982 0.056 0.955041
## sexmasculino 1.338225 0.503988 2.655 0.007924 **
## chest_painnonanginal -2.046485 0.506053 -4.044 5.25e-05 ***
## chest_painnontypical -0.888813 0.546295 -1.627 0.103741
## chest_paintypical -2.090033 0.661347 -3.160 0.001576 **
## max_hr -0.012197 0.010795 -1.130 0.258497
## ex_ang1 0.676727 0.431672 1.568 0.116954
## oldpeak 0.488454 0.227831 2.144 0.032039 *
## slope2 1.265983 0.473538 2.673 0.007507 **
## slope3 0.497388 0.881393 0.564 0.572537
## ca1 2.078728 0.486671 4.271 1.94e-05 ***
## ca2 2.812203 0.739859 3.801 0.000144 ***
## ca3 2.001039 0.888650 2.252 0.024337 *
## thalnormal 0.191516 0.776263 0.247 0.805128
## thalreversable 1.641550 0.760433 2.159 0.030873 *
## ---
## 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
# Coeficientes con intervalos de confianza
tidy(modelo_log, conf.int = TRUE) |>
kable(
caption = "Coeficientes del modelo logístico con IC 95%",
digits = 3
)
| term | estimate | std.error | statistic | p.value | conf.low | conf.high |
|---|---|---|---|---|---|---|
| (Intercept) | -1.427 | 2.480 | -0.575 | 0.565 | -6.360 | 3.417 |
| age | 0.001 | 0.023 | 0.056 | 0.955 | -0.044 | 0.047 |
| sexmasculino | 1.338 | 0.504 | 2.655 | 0.008 | 0.374 | 2.364 |
| chest_painnonanginal | -2.046 | 0.506 | -4.044 | 0.000 | -3.082 | -1.086 |
| chest_painnontypical | -0.889 | 0.546 | -1.627 | 0.104 | -1.998 | 0.161 |
| chest_paintypical | -2.090 | 0.661 | -3.160 | 0.002 | -3.452 | -0.837 |
| max_hr | -0.012 | 0.011 | -1.130 | 0.258 | -0.034 | 0.009 |
| ex_ang1 | 0.677 | 0.432 | 1.568 | 0.117 | -0.174 | 1.527 |
| oldpeak | 0.488 | 0.228 | 2.144 | 0.032 | 0.054 | 0.952 |
| slope2 | 1.266 | 0.474 | 2.673 | 0.008 | 0.355 | 2.223 |
| slope3 | 0.497 | 0.881 | 0.564 | 0.573 | -1.292 | 2.190 |
| ca1 | 2.079 | 0.487 | 4.271 | 0.000 | 1.148 | 3.066 |
| ca2 | 2.812 | 0.740 | 3.801 | 0.000 | 1.426 | 4.335 |
| ca3 | 2.001 | 0.889 | 2.252 | 0.024 | 0.409 | 3.948 |
| thalnormal | 0.192 | 0.776 | 0.247 | 0.805 | -1.358 | 1.718 |
| thalreversable | 1.642 | 0.760 | 2.159 | 0.031 | 0.125 | 3.145 |
# Odds ratios con intervalos de confianza
tidy(modelo_log, exponentiate = TRUE, conf.int = TRUE) |>
select(term, estimate, conf.low, conf.high, p.value) |>
kable(
caption = "Odds ratios del modelo logístico con IC 95%",
digits = 3
)
| term | estimate | conf.low | conf.high | p.value |
|---|---|---|---|---|
| (Intercept) | 0.240 | 0.002 | 30.472 | 0.565 |
| age | 1.001 | 0.957 | 1.048 | 0.955 |
| sexmasculino | 3.812 | 1.454 | 10.629 | 0.008 |
| chest_painnonanginal | 0.129 | 0.046 | 0.337 | 0.000 |
| chest_painnontypical | 0.411 | 0.136 | 1.174 | 0.104 |
| chest_paintypical | 0.124 | 0.032 | 0.433 | 0.002 |
| max_hr | 0.988 | 0.967 | 1.009 | 0.258 |
| ex_ang1 | 1.967 | 0.841 | 4.605 | 0.117 |
| oldpeak | 1.630 | 1.056 | 2.591 | 0.032 |
| slope2 | 3.547 | 1.426 | 9.234 | 0.008 |
| slope3 | 1.644 | 0.275 | 8.931 | 0.573 |
| ca1 | 7.994 | 3.152 | 21.454 | 0.000 |
| ca2 | 16.647 | 4.161 | 76.291 | 0.000 |
| ca3 | 7.397 | 1.505 | 51.815 | 0.024 |
| thalnormal | 1.211 | 0.257 | 5.573 | 0.805 |
| thalreversable | 5.163 | 1.134 | 23.220 | 0.031 |
# Medidas generales del modelo
glance(modelo_log) |>
kable(
caption = "Medidas de ajuste del modelo logístico",
digits = 3
)
| null.deviance | df.null | logLik | AIC | BIC | deviance | df.residual | nobs |
|---|---|---|---|---|---|---|---|
| 409.946 | 296 | -96.739 | 225.478 | 284.577 | 193.478 | 281 | 297 |
Variable respuesta: ahd_bin (presencia
de enfermedad cardiaca)
Variables predictoras: age,
sex, chest_pain, max_hr,
ex_ang, oldpeak, slope,
ca, thal
Odds ratios significativos (p < 0.05):
| Variable | OR | IC 95% | Interpretación |
|---|---|---|---|
sexmasculino |
3.11 | (1.27, 8.00) | Hombres tienen ~3.11 veces más odds de enfermedad |
chest_painnonanginal |
0.16 | (0.06, 0.38) | Menores odds de enfermedad vs categoría referencia |
chest_paintypical |
0.16 | (0.05, 0.53) | Menores odds de enfermedad vs categoría referencia |
ca |
3.11 | (1.94, 5.22) | Por cada vaso adicional, odds se multiplican por ~3.11 |
thalreversable |
4.41 | (1.03, 18.78) | ~4.41 veces más odds vs categoría referencia |
Medidas de ajuste:
AIC = 230.78
Deviance residual = 204.78
El modelo es significativo globalmente
# Calcular probabilidades estimadas
heart <- heart |>
mutate(
prob_ahd = predict(modelo_log, type = "response")
)
# Resumen de probabilidades por grupo
heart |>
group_by(ahd) |>
summarise(
n = n(),
prob_media = mean(prob_ahd),
prob_mediana = median(prob_ahd),
prob_min = min(prob_ahd),
prob_max = max(prob_ahd)
) |>
kable(
caption = "Probabilidades estimadas de enfermedad cardiaca por grupo",
digits = 3
)
| ahd | n | prob_media | prob_mediana | prob_min | prob_max |
|---|---|---|---|---|---|
| Con_enfermedad | 137 | 0.786 | 0.918 | 0.022 | 0.998 |
| Sin_enfermedad | 160 | 0.183 | 0.097 | 0.004 | 0.989 |
# Distribución de probabilidades
ggplot(heart, aes(x = prob_ahd, fill = ahd)) +
geom_histogram(bins = 20, alpha = 0.7, position = "identity") +
labs(
title = "Distribución de probabilidades estimadas de AHD",
x = "Probabilidad estimada de enfermedad cardiaca",
y = "Frecuencia",
fill = "Diagnóstico"
) +
theme_minimal()
Interpretación de probabilidades:
Grupo Con_enfermedad: probabilidad media de
0.771
Grupo Sin_enfermedad: probabilidad media de
0.196
Existe traslape entre grupos, pero el modelo logra separación parcial
# Clasificación con umbral 0.5
heart <- heart |>
mutate(
pred_ahd = if_else(prob_ahd >= 0.5, "Con_enfermedad", "Sin_enfermedad"),
pred_ahd = factor(pred_ahd, levels = levels(ahd))
)
# Matriz de confusión - extraer los datos como tabla
matriz_conf <- heart |>
conf_mat(truth = ahd, estimate = pred_ahd)
# Convertir la matriz a formato tabla para kable
matriz_conf_tabla <- as.data.frame(matriz_conf$table)
# Mostrar matriz de confusión formateada
matriz_conf_tabla |>
pivot_wider(
names_from = Prediction,
values_from = Freq
) |>
kable(
caption = "Matriz de confusión del modelo logístico",
digits = 0
)
| Truth | Con_enfermedad | Sin_enfermedad |
|---|---|---|
| Con_enfermedad | 114 | 23 |
| Sin_enfermedad | 14 | 146 |
# Métricas de clasificación
heart |>
accuracy(truth = ahd, estimate = pred_ahd) |>
kable(
caption = "Exactitud del modelo logístico",
digits = 3
)
| .metric | .estimator | .estimate |
|---|---|---|
| accuracy | binary | 0.875 |
# Métricas adicionales
heart |>
sens(truth = ahd, estimate = pred_ahd) |>
bind_rows(
heart |> spec(truth = ahd, estimate = pred_ahd)
) |>
mutate(metrica = c("Sensibilidad", "Especificidad")) |>
select(metrica, .estimate) |>
kable(
caption = "Sensibilidad y Especificidad del modelo",
digits = 3,
col.names = c("Métrica", "Valor")
)
| Métrica | Valor |
|---|---|
| Sensibilidad | 0.832 |
| Especificidad | 0.912 |
Matriz de confusión:
| Sin_enfermedad | Con_enfermedad | |
|---|---|---|
| Sin_enfermedad | 147 | 13 |
| Con_enfermedad | 28 | 109 |
Métricas:
Exactitud: 86.2% (256/297 clasificados correctamente)
Sensibilidad: 79.6% (109/137 de los que tienen enfermedad)
Especificidad: 91.9% (147/160 de los que no tienen enfermedad)
Interpretación:
El modelo logra una buena separación entre grupos, con una exactitud del 86.2%.
La especificidad es alta (91.9%), lo que indica que el modelo identifica correctamente a la mayoría de los pacientes sin enfermedad.
La sensibilidad es moderada (79.6%), lo que indica que el modelo detecta aproximadamente 8 de cada 10 pacientes con enfermedad.
Existen 28 falsos negativos (pacientes con enfermedad que fueron clasificados como sin enfermedad) y 13 falsos positivos (pacientes sin enfermedad clasificados como con enfermedad).
Limitaciones:
La evaluación se realizó sobre los mismos datos utilizados para ajustar el modelo, por lo que las métricas pueden ser optimistas.
La exactitud no es suficiente para evaluar completamente un modelo de clasificación; es necesario considerar también sensibilidad, especificidad y otras métricas.
El umbral de 0.5 puede no ser óptimo para todos los contextos; podría ajustarse según los costos de los diferentes tipos de error.
Se comparó el modelo completo con un modelo reducido que incluye solo
age, max_hr y oldpeak.
# Modelo reducido
modelo_log_reducido <- glm(
ahd_bin ~ age + max_hr + oldpeak,
data = heart,
family = binomial
)
# Comparación de AIC
AIC(modelo_log_reducido, modelo_log)
## df AIC
## modelo_log_reducido 4 328.5080
## modelo_log 16 225.4775
# Comparación de medidas de ajuste
glance(modelo_log_reducido) |>
bind_rows(glance(modelo_log)) |>
mutate(modelo = c("Reducido", "Completo")) |>
select(modelo, AIC, deviance, df.residual) |>
kable(
caption = "Comparación de modelos logísticos",
digits = 2
)
| modelo | AIC | deviance | df.residual |
|---|---|---|---|
| Reducido | 328.51 | 320.51 | 293 |
| Completo | 225.48 | 193.48 | 281 |
| Modelo | AIC | Deviance | df.residual |
|---|---|---|---|
| Reducido | 328.51 | 320.51 | 293 |
| Completo | 230.78 | 204.78 | 286 |
Interpretación:
El modelo completo tiene un AIC considerablemente menor (230.78 vs 328.51), indicando mejor ajuste.
El aumento de complejidad parece justificado por la mejora en el ajuste.
Sin embargo, el modelo completo debe validarse con datos independientes antes de su uso predictivo.
A continuación se sintetizan los hallazgos más importantes obtenidos a lo largo del proyecto, integrando los resultados del análisis exploratorio, el análisis inferencial y el modelado estadístico.
oldpeak: mostró la diferencia más
clara entre grupos. Los pacientes con enfermedad cardiaca presentaron
valores promedio de 1.59, mientras que los sin enfermedad presentaron
0.60.max_hr: los pacientes sin enfermedad
presentaron frecuencias cardiacas máximas más altas (media 158.6 lpm)
que aquellos con enfermedad (media 139.1 lpm).age: los pacientes con enfermedad son
en promedio ~4 años mayores que los sin enfermedad (56.8 vs 52.6
años).rest_bp y
chol: mostraron diferencias pequeñas entre
grupos.chest_pain: la categoría
asymptomatic concentró la mayor proporción de enfermedad
(72.5%).sex: los hombres presentaron mayor
proporción de enfermedad (55.7%) que las mujeres (26.0%).ca: se observó una relación directa: a
mayor número de vasos afectados, mayor proporción de enfermedad (0
vasos: 15.4%; 3 vasos: 92.3%).thal: la categoría
reversable presentó la mayor proporción de enfermedad
(78.3%).fbs: no mostró diferencias relevantes
entre grupos.age y
max_hr (r = -0.395).max_hr y
oldpeak (r = -0.348).| Variable | p-valor | Conclusión |
|---|---|---|
age |
< 0.001 | Diferencia significativa |
rest_bp |
0.0088 | Diferencia significativa |
chol |
0.165 | No significativa |
max_hr |
< 0.001 | Diferencia significativa |
oldpeak |
< 0.001 | Diferencia significativa |
| Variable | p-valor | Asociación |
|---|---|---|
sex |
< 0.001 | Significativa |
chest_pain |
< 0.001 | Significativa |
fbs |
1.000 | No significativa |
rest_ecg |
0.008 | Significativa |
ex_ang |
< 0.001 | Significativa |
slope |
< 0.001 | Significativa |
ca |
< 0.001 | Significativa |
thal |
< 0.001 | Significativa |
max_hr)age, sex,
rest_bp, chol, oldpeak,
ahdage,
oldpeak, ahd, rest_bpahd)age, sex,
chest_pain, max_hr, ex_ang,
oldpeak, slope, ca,
thalsex,
chest_pain (algunas categorías), ca,
thalreversablemax_hr)age
(-0.86), rest_bp (0.16), oldpeak (-3.57),
ahdSin_enfermedad (14.09)sex,
cholsexmasculino: OR = 3.11chest_painnonanginal: OR = 0.16chest_paintypical: OR = 0.16ca: OR = 3.11 (por cada unidad)thalreversable: OR = 4.41| Modelo | AIC | Deviance | Interpretación |
|---|---|---|---|
| Reducido (3 variables) | 328.51 | 320.51 | Más simple, peor ajuste |
| Completo (9 variables) | 230.78 | 204.78 | Mejor ajuste, más complejo |
A lo largo del proyecto se presentaron diversos problemas técnicos y metodológicos que requirieron atención y solución. A continuación se describen los principales.
Problema: Los nombres de las variables contenían
mayúsculas, minúsculas y espacios (ej. RestBP,
MaxHR).
Solución: Se aplicó la función
clean_names() del paquete janitor para
estandarizar todos los nombres a minúsculas con guión bajo.
Problema: Las variables ca (1.32%) y
thal (0.66%) presentaban valores faltantes.
Solución: Se eliminaron las 6 filas con valores
faltantes utilizando drop_na(), ya que el porcentaje era
muy bajo y no justificaba la imputación.
Problema: Varias variables codificadas como
numéricas representaban categorías clínicas (ej. sex,
chest_pain, thal).
Solución: Se convirtieron a factores utilizando
as.factor() para que R las reconociera como variables
categóricas.
Problema: Las variables de texto presentaban
diferencias en mayúsculas/minúsculas (ej. “Yes” vs “no” en
ahd).
Solución: Se aplicaron str_to_lower() y
str_trim() para estandarizar el texto y eliminar espacios
en blanco.
Problema: La prueba de chi-cuadrada para
rest_ecg mostró una advertencia sobre frecuencias esperadas
pequeñas.
Solución: Se interpretó el resultado con cautela y se
consideró la posibilidad de agrupar categorías en análisis
posteriores.
Problema: Dificultad para diferenciar entre
significancia estadística y relevancia clínica.
Solución: Se complementaron los valores p con
intervalos de confianza, tamaños de efecto y análisis exploratorio para
una interpretación más completa.
Problema: Determinar qué variables incluir en los
modelos sin sobreajustar.
Solución: Se utilizó un enfoque basado en el EDA, el
análisis inferencial y la revisión de la literatura, evitando
seleccionar variables únicamente por su valor p.
Problema: Los modelos fueron ajustados y evaluados
sobre la misma base de datos, lo que puede sobreestimar su
desempeño.
Solución: Se señaló esta limitación explícitamente y se
recomendó realizar validación cruzada o usar datos independientes en
futuras etapas.
Problema: kable() no podía formatear
directamente objetos de clase "conf_mat" de
yardstick.
Solución: Se extrajo la tabla interna del objeto
conf_mat y se transformó a data.frame antes de usar
kable().
El desarrollo de este proyecto permitió aplicar un flujo de trabajo completo de análisis de datos, desde la importación y limpieza de los datos hasta la construcción e interpretación de modelos estadísticos, utilizando el dataset Heart Disease como caso de estudio.
El dataset Heart Disease contiene información clínica de 297
pacientes, con 18 variables que incluyen características demográficas,
fisiológicas y resultados de pruebas diagnósticas. La variable objetivo
ahd clasifica a los pacientes en dos grupos: con y sin
enfermedad cardiaca. El análisis mostró que los pacientes con enfermedad
cardiaca tienden a ser mayores, presentan mayor depresión del segmento
ST (oldpeak), menor frecuencia cardiaca máxima
(max_hr), y con mayor frecuencia son hombres y presentan
dolor torácico de tipo asintomático.
Las variables que mostraron mayor relevancia en el análisis fueron:
oldpeak: la diferencia más clara entre
grupos y uno de los predictores más fuertes.max_hr: inversamente relacionada con
la presencia de enfermedad.chest_pain: fuertemente asociada con
ahd, especialmente la categoría
asymptomatic.ca: relación directa con la enfermedad
(a mayor número de vasos, mayor riesgo).thal: la categoría
reversable mostró la mayor asociación con enfermedad.sex: los hombres presentaron mayor
probabilidad de enfermedad.El EDA permitió: - Identificar diferencias visuales entre grupos,
especialmente en oldpeak, max_hr y
age. - Detectar valores atípicos que requirieron atención.
- Explorar relaciones entre variables mediante gráficos de dispersión y
matrices de correlación. - Generar hipótesis que luego fueron evaluadas
con pruebas inferenciales.
El análisis inferencial permitió: - Confirmar estadísticamente las
diferencias observadas en el EDA. - Identificar qué variables presentan
asociaciones significativas con ahd. - Estimar la magnitud
de las diferencias mediante intervalos de confianza. - Seleccionar
variables candidatas para el modelado con base en evidencia
estadística.
El modelado estadístico permitió: - Cuantificar las asociaciones
entre múltiples variables predictoras y las variables respuesta. -
Construir un modelo de regresión lineal que explica el 31.9% de la
variabilidad de max_hr. - Construir un modelo de regresión
logística con una exactitud del 86.2%. - Identificar que variables como
sex, chest_pain, ca y
thal son particularmente relevantes para predecir la
presencia de enfermedad cardiaca. - Comparar modelos y seleccionar el
más adecuado según criterios de ajuste.
Los resultados deben interpretarse con las siguientes consideraciones:
rest_ecg requieren revisión por frecuencias esperadas
pequeñas.Este proyecto constituye una primera aproximación al análisis de factores asociados con enfermedad cardiaca. Como siguientes pasos, se recomienda:
En conjunto, este proyecto demuestra la utilidad de los métodos estadísticos y de aprendizaje automático para explorar y modelar problemas clínicos, siempre con una interpretación cuidadosa y considerando las limitaciones de los datos y los modelos.
Chapman, P., Clinton, J., Kerber, R., Khabaza, T., Reinartz, T., Shearer, C., & Wirth, R. (2000). CRISP-DM 1.0: Step-by-step data mining guide. SPSS Inc.
James, G., Witten, D., Hastie, T., & Tibshirani, R. (2013). An Introduction to Statistical Learning. Springer.
Kuhn, M. (2020). caret: Classification and Regression Training. R package version 6.0-86.
Kuhn, M., & Johnson, K. (2019). Feature Engineering and Selection: A Practical Approach for Predictive Models. CRC Press.
R Core Team. (2023). R: A Language and Environment for Statistical Computing. R Foundation for Statistical Computing. https://www.R-project.org/
Silge, J., & Robinson, D. (2017). Text Mining with R. O’Reilly Media.
Wickham, H. (2016). ggplot2: Elegant Graphics for Data Analysis. Springer-Verlag New York.
Wickham, H., François, R., Henry, L., & Müller, K. (2021). dplyr: A Grammar of Data Manipulation. R package version 1.0.7.
Wickham, H., & Grolemund, G. (2017). R for Data Science. O’Reilly Media. https://r4ds.had.co.nz/