Sección 1: Importación, exportación y gestión de datos

Ejercicio 1. Dataset HeartStat

1.1. importe los datos del archivo en un data frame llamado HeartStat y muestre los 6 primeros registros (head) y los 6 últimos (tail).

El archivo heart.csv se ha descargado previamente y se encuentra almacenado en el directorio data/ de esta práctica.

Antes de crear el data frame HeartStat, comprobamos que el archivo heart.csv está disponible en el directorio indicado. Si existe, se importa mediante la función read.csv(). En caso contrario, se detiene la ejecución y se muestra un mensaje de error.

Una vez importados los datos, se muestran las seis primeras y las seis últimas observaciones mediante las funciones head() y tail(), respectivamente.

# Definir la ruta del archivo CSV
file_path <- file.path("data", "heart.csv")

# Verificar que el archivo existe antes de importarlo
if (!file.exists(file_path)) {
  stop("No se encuentra el archivo heart.csv en el directorio data/")
}

# Importar los datos en un data frame llamado HeartStat
HeartStat <- read.csv(file_path)

# Verificar que el objeto creado es un data frame
stopifnot(is.data.frame(HeartStat))

# Mostrar las seis primeras observaciones
head(HeartStat)
##   age sex cp trestbps chol fbs restecg thalach exang oldpeak slope ca thal
## 1  70   1  3      130  322   0       2     109     0     2.4     1  3    1
## 2  67   0  2      115  564   0       2     160     0     1.6     1  0    3
## 3  57   1  1      124  261   0       0     141     0     0.3     0  0    3
## 4  64   1  3      128  263   0       0     105     1     0.2     1  1    3
## 5  74   0  1      120  269   0       2     121     1     0.2     0  1    1
## 6  65   1  3      120  177   0       0     140     0     0.4     0  0    3
##   target
## 1      1
## 2      0
## 3      1
## 4      0
## 5      0
## 6      0
# Mostrar las seis últimas observaciones
tail(HeartStat)
##     age sex cp trestbps chol fbs restecg thalach exang oldpeak slope ca thal
## 265  48   1  1      110  229   0       0     168     0     1.0     2  0    3
## 266  52   1  2      172  199   1       0     162     0     0.5     0  0    3
## 267  44   1  1      120  263   0       0     173     0     0.0     0  0    3
## 268  56   0  1      140  294   0       2     153     0     1.3     1  0    1
## 269  57   1  3      140  192   0       0     148     0     0.4     1  0    2
## 270  67   1  3      160  286   0       2     108     1     1.5     1  3    1
##     target
## 265      1
## 266      0
## 267      0
## 268      0
## 269      0
## 270      1

1.2. Describa las características del conjunto de datos: nombre de las variables,tipos de datos, tamaño de la muestra (número de filas y columnas) y prospección de valores nulos. Proponga qué tipo de análisis exploratorio y gráficos serán adecuados según la naturaleza de las variables.

Iniciamos el analisis del dataframe:

# Nombres de las variables
names(HeartStat)
##  [1] "age"      "sex"      "cp"       "trestbps" "chol"     "fbs"     
##  [7] "restecg"  "thalach"  "exang"    "oldpeak"  "slope"    "ca"      
## [13] "thal"     "target"
# Estructura y tipos de datos
str(HeartStat)
## 'data.frame':    270 obs. of  14 variables:
##  $ age     : int  70 67 57 64 74 65 56 59 60 63 ...
##  $ sex     : int  1 0 1 1 0 1 1 1 1 0 ...
##  $ cp      : int  3 2 1 3 1 3 2 3 3 3 ...
##  $ trestbps: int  130 115 124 128 120 120 130 110 140 150 ...
##  $ chol    : int  322 564 261 263 269 177 256 239 293 407 ...
##  $ fbs     : int  0 0 0 0 0 0 1 0 0 0 ...
##  $ restecg : int  2 2 0 0 2 0 2 2 2 2 ...
##  $ thalach : int  109 160 141 105 121 140 142 142 170 154 ...
##  $ exang   : int  0 0 0 1 1 0 1 1 0 0 ...
##  $ oldpeak : num  2.4 1.6 0.3 0.2 0.2 0.4 0.6 1.2 1.2 4 ...
##  $ slope   : int  1 1 0 1 0 0 1 1 1 1 ...
##  $ ca      : int  3 0 0 1 1 0 1 1 2 3 ...
##  $ thal    : int  1 3 3 3 1 3 2 3 3 3 ...
##  $ target  : int  1 0 1 0 0 0 1 1 1 1 ...
# Dimensiones: número de filas y columnas
dim(HeartStat)
## [1] 270  14
# Tamaño de la muestra
nrow(HeartStat)
## [1] 270
# Número de variables
ncol(HeartStat)
## [1] 14
# Valores ausentes por variable
colSums(is.na(HeartStat))
##      age      sex       cp trestbps     chol      fbs  restecg  thalach 
##        0        0        0        0        0        0        0        0 
##    exang  oldpeak    slope       ca     thal   target 
##        0        0        0        0        0        0
# Número total de valores ausentes
sum(is.na(HeartStat))
## [1] 0

En base a las caracteristicas del dataset proponemos el siguiente analisis exploratorio:

Naturaleza estadística Variables Análisis exploratorio Gráficos propuestos
Cuantitativas age, trestbps, chol, thalach, oldpeak Media, mediana, desviación estándar, cuartiles y valores atípicos Histogramas y diagramas de cajas
Cuantitativa discreta ca Frecuencias, media y distribución del número de vasos Diagrama de barras
Categóricas binarias sex, fbs, exang, target Frecuencias absolutas y proporciones Diagramas de barras
Categóricas con varios niveles cp, restecg, slope, thal Frecuencias y proporciones por categoría Diagramas de barras
Cuantitativa y categórica thalach y target Comparación de distribuciones entre grupos Diagramas de cajas
Dos categóricas cp y target Tablas de contingencia y proporciones condicionadas Barras agrupadas o mapas de calor
Dos cuantitativas age y thalach Correlación y análisis de asociación Diagramas de dispersión

Para las variables cuantitativas podriamos estudiar la tendencia central, la dispersión y la forma de sus distribuciones. Mientras que para las variables categóricas, seria interesante hacer un analisis de frecuencias y proporciones.

Adicionalmente, los análisis bivariantes nos permitirian explorar posibles asociaciones entre las características clínicas de los pacientes y la presencia o ausencia de enfermedad cardíaca.

1.3. Supongamos que en este dataset detectamos valores ausentes en una variable.La práctica habitual de “eliminar todas las filas con un valor ausente” (complete-caseanalysis) puede introducir un sesgo. Explique qué riesgo puede comportar descartarsistemáticamente a pacientes que no tienen el expediente médico completo. 10-15 líneas.

Antes de eliminar observaciones con valores ausentes, es importante considerar el mecanismo que ha generado dichos valores.

En estadística se distinguen tres mecanismos:

Mecanismo Significado Ejemplo clínico
MCAR Ausencia completamente aleatoria Se pierde una analítica por un fallo técnico independiente del paciente.
MAR Ausencia explicable por variables observadas Los pacientes de mayor edad tienen menos registros completos.
MNAR Ausencia relacionada con información no observada, incluso tras considerar las variables disponibles Los pacientes con resultados especialmente desfavorables tienen mayor probabilidad de no disponer de esa medición.

Si los valores ausentes siguen un mecanismo MCAR, eliminar los registros incompletos generalmente no introduce sesgo de selección, aunque reduce el tamaño muestral y la precisión estadística.

En cambio, bajo MAR, la ausencia depende de variables observadas. Si, por ejemplo, los pacientes de mayor edad presentan más expedientes incompletos, su exclusión podría alterar la composición de la muestra y sesgar las estimaciones de enfermedad cardíaca.

En el caso MNAR, la ausencia depende de información no observada, incluso después de considerar las variables disponibles, lo que puede producir estimaciones sistemáticamente sesgadas.

Por lo tanto, antes de aplicar un complete-case analysis, es imprescindible investigar los patrones de los valores ausentes

Ejercicio 2. Exploración del entorno RStudio y paquetes asociados.

2.1 Explique qué información contiene el conjunto de datos esoph y sus características más relevantes. Sugiera y justifique alguna acción de preparación o transformación de los datos (p. ej., creación de nuevas variables categóricas o agrupación de niveles de consumo de tabaco/alcohol).

Para conocer su estructura, examinamos las dimensiones, los tipos de variables, los primeros registros y un resumen descriptivo del conjunto de datos.

# Cargar el conjunto de datos incluido en R
data("esoph", package = "datasets")

# Dimensiones del conjunto de datos
dim(esoph)
## [1] 88  5
# Estructura y tipos de variables
str(esoph)
## 'data.frame':    88 obs. of  5 variables:
##  $ agegp    : Ord.factor w/ 6 levels "25-34"<"35-44"<..: 1 1 1 1 1 1 1 1 1 1 ...
##  $ alcgp    : Ord.factor w/ 4 levels "0-39g/day"<"40-79"<..: 1 1 1 1 2 2 2 2 3 3 ...
##  $ tobgp    : Ord.factor w/ 4 levels "0-9g/day"<"10-19"<..: 1 2 3 4 1 2 3 4 1 2 ...
##  $ ncases   : num  0 0 0 0 0 0 0 0 0 0 ...
##  $ ncontrols: num  40 10 6 5 27 7 4 7 2 1 ...
# Primeras seis observaciones
head(esoph)
##   agegp     alcgp    tobgp ncases ncontrols
## 1 25-34 0-39g/day 0-9g/day      0        40
## 2 25-34 0-39g/day    10-19      0        10
## 3 25-34 0-39g/day    20-29      0         6
## 4 25-34 0-39g/day      30+      0         5
## 5 25-34     40-79 0-9g/day      0        27
## 6 25-34     40-79    10-19      0         7
# Resumen descriptivo
summary(esoph)
##    agegp          alcgp         tobgp        ncases         ncontrols     
##  25-34:15   0-39g/day:23   0-9g/day:24   Min.   : 0.000   Min.   : 0.000  
##  35-44:15   40-79    :23   10-19   :24   1st Qu.: 0.000   1st Qu.: 1.000  
##  45-54:16   80-119   :21   20-29   :20   Median : 1.000   Median : 4.000  
##  55-64:16   120+     :21   30+     :20   Mean   : 2.273   Mean   : 8.807  
##  65-74:15                                3rd Qu.: 4.000   3rd Qu.:10.000  
##  75+  :11                                Max.   :17.000   Max.   :60.000
# Categorías de consumo de alcohol y tabaco
levels(esoph$alcgp)
## [1] "0-39g/day" "40-79"     "80-119"    "120+"
levels(esoph$tobgp)
## [1] "0-9g/day" "10-19"    "20-29"    "30+"
Variable Nombre completo ¿Qué representa? Tipo estadístico
agegp Age group Grupo de edad de los participantes Categórica ordinal
alcgp Alcohol consumption group Nivel de consumo de alcohol, en gramos por día Categórica ordinal
tobgp Tobacco consumption group Nivel de consumo de tabaco, en gramos por día Categórica ordinal
ncases Number of cases Número de personas con cáncer de esófago Cuantitativa discreta
ncontrols Number of controls Número de personas del grupo de control, sin cáncer de esófago Cuantitativa discreta

Como acción de preparación de los datos, proponemos crear una nueva variable categórica denominada alc_group, agrupando los cuatro niveles originales de consumo de alcohol en dos categorías: menor consumo (menos de 80 g/día) y mayor consumo (80 g/día o más).

Esta transformación permite, por ejemplo, simplificar las comparaciones entre grupos y facilita la elaboración de tablas de contingencia y gráficos de barras que, a su vez, nos puede servir para explorar la asociación entre el consumo de alcohol y los casos de cáncer de esófago.

Hay que tener en cuenta que esta agregación, también implica una pérdida de información, ya que estamos elimimando las diferencias entre niveles originales. Además, el umbral de 80 g/día lo hemos elegido de forma arbitraria, aprovechando las categorías existentes en el dataset, pero sin aplicar ningún criterio estadístico ni justificarlo mediante evidencia clínica.

# Creamos una copia para preservar los datos originales
esoph_modificat <- esoph

# Agrupamos los niveles de consumo de alcohol
esoph_modificat$alc_group <- factor(
  ifelse(
    esoph_modificat$alcgp %in% c("0-39g/day", "40-79"),
    "Menor consumo",
    "Mayor consumo"
  ),
  levels = c("Menor consumo", "Mayor consumo")
)

# Comprobamos la nueva clasificación
table(esoph_modificat$alcgp, esoph_modificat$alc_group)
##            
##             Menor consumo Mayor consumo
##   0-39g/day            23             0
##   40-79                23             0
##   80-119                0            21
##   120+                  0            21
# Revisamos la estructura del dataset modificado
str(esoph_modificat)
## 'data.frame':    88 obs. of  6 variables:
##  $ agegp    : Ord.factor w/ 6 levels "25-34"<"35-44"<..: 1 1 1 1 1 1 1 1 1 1 ...
##  $ alcgp    : Ord.factor w/ 4 levels "0-39g/day"<"40-79"<..: 1 1 1 1 2 2 2 2 3 3 ...
##  $ tobgp    : Ord.factor w/ 4 levels "0-9g/day"<"10-19"<..: 1 2 3 4 1 2 3 4 1 2 ...
##  $ ncases   : num  0 0 0 0 0 0 0 0 0 0 ...
##  $ ncontrols: num  40 10 6 5 27 7 4 7 2 1 ...
##  $ alc_group: Factor w/ 2 levels "Menor consumo",..: 1 1 1 1 1 1 1 1 2 2 ...

El conjunto de datos esoph contiene información de un estudio de casos y controles sobre cáncer de esófago en Francia. Sus 88 filas representan estratos definidos por la combinación de grupo de edad, consumo de alcohol y consumo de tabaco; por tanto, no corresponden a 88 personas.

En cada estrato, ncases indica el número de casos de cáncer de esófago y ncontrols el número de controles. Para obtener el número de participantes de un grupo, debemos sumar estos recuentos.

La tabla que comprueba la nueva variable alc_group cuenta cuántos estratos originales se asignan a cada categoría de consumo. Sirve para verificar la transformación, pero no muestra el número de personas de cada grupo.

2.2. Exporte el data frame esoph modificado a un archivo en formato .csv(llamado esoph_modificat.csv) especificando la ruta de destino y asegurándose de no incluir el índice de filas.

Exportamos el data frame esoph_modificat al directorio data/ que previamente habiamos creado, le llamamos: esoph_modificat.csv. Antes de exportarlo, comprobamos que el directorio existe y, en caso contrario, lo creamos.

El argumento row.names = FALSE permite guardar los datos sin incluir el índice de las filas.

# Definir el directorio de destino
data_dir <- "data"

# Comprobar si existe y crearlo en caso contrario
if (!dir.exists(data_dir)) {
  dir.create(data_dir, recursive = TRUE)
}

# Definir la ruta del archivo
output_path <- file.path(data_dir, "esoph_modificat.csv")

# Exportar el data frame sin incluir índices de filas
write.csv(
  esoph_modificat,
  file = output_path,
  row.names = FALSE
)

# Verificar que el archivo se ha creado
if (file.exists(output_path)) {
  cat("Archivo CSV creado correctamente.\n")
  cat("Ruta:", normalizePath(output_path), "\n")
} else {
  stop("Error: no se ha podido crear el archivo CSV.")
}
## Archivo CSV creado correctamente.
## Ruta: /Users/pedro/Work/bioinformatics-r/PEC1/data/esoph_modificat.csv

2.3. El dataset esoph recoge información sobre hábitos de consumo de alcohol ytabaco relacionados con una enfermedad grave. Cuando los algoritmos de salud utilizan datos de estilo de vida para evaluar riesgos o priorizar tratamientos, ¿cuál es el límite ético entre la prevención médica y la discriminación o estigmatización de determinados colectivos? 10-15 líneas.

Es verdad que los datos sobre hábitos de consumo de alcohol y tabaco pueden ayudar a identificar riesgos y mejorar la prevención de enfermedades.

También es cierto que existe un marco regulatorio, concretamente el Reglamento General de Protección de Datos (RGPD), que considera los datos de salud como información especialmente protegida.

Esto implica que su tratamiento requiere no solo una base jurídica válida, sino también una finalidad legítima y medidas adecuadas de confidencialidad y seguridad.

Sin embargo, el cumplimiento legal no garantiza por sí solo un uso ético de los datos.

De ahí que, si queremos hablar de ética en el uso de algoritmos, quizás deberíamos proponer que sigan los principios bioéticos de beneficencia, no maleficencia, autonomía y justicia. Esto es: no sería éticamente aceptable buscar una mejora general de la prevención sanitaria a costa de discriminar o perjudicar injustificadamente a determinados colectivos.

Sección 2: Análisis básico del conjunto de dato

Considerando el data frame HeartStat, resuelva las siguientes cuestiones (incluya código y resultados):

3.1 Calcule la proporción de pacientes según el tipo de dolor torácico (cp),agrupados por la presencia o ausencia de enfermedad cardíaca (target). Comente los resultados

Tenemos dos variables: - cp: tipo de dolor torácico. Es una variable categórica con cuatro modalidades. - target: diagnóstico de enfermedad cardíaca. Es una variable binaria: 0 (ausencia) y 1 (presencia).

# Mostrar las primeras observaciones de las dos variables
head(HeartStat[, c("cp", "target")])
##   cp target
## 1  3      1
## 2  2      0
## 3  1      1
## 4  3      0
## 5  1      0
## 6  3      0
# Comprobar los valores que toma cada variable
unique(HeartStat$cp)
## [1] 3 2 1 0
unique(HeartStat$target)
## [1] 1 0
# Frecuencias absolutas de cada variable
table(HeartStat$cp)
## 
##   0   1   2   3 
##  20  42  79 129
table(HeartStat$target)
## 
##   0   1 
## 150 120
# Construir la tabla de frecuencias absolutas
tabla_cp_target <- table(
  cp = HeartStat$cp,
  target = HeartStat$target
)

# Mostrar la tabla
tabla_cp_target
##    target
## cp   0  1
##   0 15  5
##   1 35  7
##   2 62 17
##   3 38 91
# Calcular proporciones por grupo de diagnóstico
prop_cp_target <- prop.table(tabla_cp_target, margin = 2)

# Mostrar los porcentajes
round(prop_cp_target * 100, 2)
##    target
## cp      0     1
##   0 10.00  4.17
##   1 23.33  5.83
##   2 41.33 14.17
##   3 25.33 75.83
# Comprobar que cada columna suma 100 %
colSums(prop_cp_target) * 100
##   0   1 
## 100 100

En base a lo que muestran los datos, podemos destacar el siguiente patron:

  • Entre los pacientes sin enfermedad cardíaca (target = 0): la categoría más frecuente es cp = 2, con un 41,33 %, seguida de cp = 3, con un 25,33 %.
  • Entre los pacientes con enfermedad cardíaca (target = 1): predomina claramente cp = 3, con un 75,83 %. Las demás categorías presentan proporciones considerablemente menores. Estos resultados sugieren una asociación entre el tipo de dolor torácico y la presencia de enfermedad cardíaca. Sin embargo, se trata de un análisis descriptivo y no podemos establecer una relación causal a partir de estas proporciones.

3.2 Muestre la relación entre el sexo de los pacientes (sex: 1 = hombre, 0 = mujer) y la presencia de enfermedad cardíaca (target). Represente un gráfico de barras que compare estas proporciones.

Tenemos dos variables:

  • sex: sexo del paciente. Es una variable categórica binaria: 0 (mujer) y 1 (hombre).
  • target: diagnóstico de enfermedad cardíaca. Es una variable categórica binaria: 0 (ausencia) y 1 (presencia).

Analizaremos la distribución del diagnóstico cardíaco según el sexo de los pacientes.

# Mostrar las primeras observaciones
head(HeartStat[, c("sex", "target")])
##   sex target
## 1   1      1
## 2   0      0
## 3   1      1
## 4   1      0
## 5   0      0
## 6   1      0
# Comprobar los valores de ambas variables
unique(HeartStat$sex)
## [1] 1 0
unique(HeartStat$target)
## [1] 1 0
# Mostrar las frecuencias absolutas
table(HeartStat$sex)
## 
##   0   1 
##  87 183
table(HeartStat$target)
## 
##   0   1 
## 150 120
# Construir la tabla de frecuencias absolutas
tabla_sex_target <- table(
  sex = HeartStat$sex,
  target = HeartStat$target
)

# Mostrar la tabla
tabla_sex_target
##    target
## sex   0   1
##   0  67  20
##   1  83 100
# Calcular las proporciones dentro de cada sexo
prop_sex_target <- prop.table(tabla_sex_target, margin = 1)

# Mostrar las proporciones en porcentaje
round(prop_sex_target * 100, 2)
##    target
## sex     0     1
##   0 77.01 22.99
##   1 45.36 54.64
# Comprobar que cada fila suma 100 %
rowSums(prop_sex_target) * 100
##   0   1 
## 100 100
# Preparar los porcentajes para el gráfico
porcentajes <- t(prop_sex_target) * 100

# Gráfico de barras agrupadas
posiciones <- barplot(
  porcentajes,
  beside = TRUE,
  col = c("#7EBEA3", "#3498DB"),
  names.arg = c("Mujeres", "Hombres"),
  legend.text = c("Sin enfermedad", "Con enfermedad"),
  args.legend = list(x = "topright", bty = "n"),
  main = "Presencia de enfermedad cardíaca según el sexo",
  xlab = "Sexo",
  ylab = "Porcentaje de pacientes (%)",
  ylim = c(0, 100)
)

# Mostrar los porcentajes encima de cada barra
text(
  x = posiciones,
  y = porcentajes + 3,
  labels = paste0(sprintf("%.1f", porcentajes), "%"),
  cex = 0.9
)

Los datos sugieren una asociación decriptiva ente sexo y diagnostico: 54,64 % de los hombres presenta enfermedad cardíaca, frente al 22,99 % de las mujeres

3.3 Evalúe la presencia de enfermedad cardíaca según los niveles de azúcar ensangre en ayunas (fbs), contando el número absoluto de pacientes por cada categoría.

Tenemos dos variables:

  • fbs: nivel de azúcar en sangre en ayunas. Es una variable categórica binaria: 0 (glucosa ≤ 120 mg/dL) y 1 (glucosa > 120 mg/dL).
  • target: diagnóstico de enfermedad cardíaca. Es una variable categórica binaria: 0 (ausencia) y 1 (presencia).

Analizaremos la presencia de enfermedad cardíaca según las categorías de fbs, utilizando frecuencias absolutas.

# Mostrar las primeras observaciones
head(HeartStat[, c("fbs", "target")])
##   fbs target
## 1   0      1
## 2   0      0
## 3   0      1
## 4   0      0
## 5   0      0
## 6   0      0
# Comprobar los valores de ambas variables
unique(HeartStat$fbs)
## [1] 0 1
unique(HeartStat$target)
## [1] 1 0
# Mostrar las frecuencias absolutas
table(HeartStat$fbs)
## 
##   0   1 
## 230  40
table(HeartStat$target)
## 
##   0   1 
## 150 120
# Construir la tabla de frecuencias absolutas
tabla_fbs_target <- table(
  fbs = HeartStat$fbs,
  target = HeartStat$target
)

# Mostrar la tabla
tabla_fbs_target
##    target
## fbs   0   1
##   0 127 103
##   1  23  17
# Comprobar el número total de pacientes
sum(tabla_fbs_target)
## [1] 270

La tabla de contingencia muestra que, entre los pacientes con niveles de glucosa en ayunas iguales o inferiores a 120 mg/dL (fbs = 0), 103 presentan enfermedad cardíaca y 127 no presentan la enfermedad.

Entre los pacientes con niveles superiores a 120 mg/dL (fbs = 1), 17 presentan enfermedad cardíaca y 23 no la presentan.

En términos absolutos, encontramos más pacientes con enfermedad cardíaca en la categoría fbs = 0. Sin embargo, esta categoría también contiene un número considerablemente mayor de pacientes (230 frente a 40).

Si consideramos las proporciones dentro de cada categoría, observamos que el 44,78 % de los pacientes con fbs = 0 presenta enfermedad cardíaca, frente al 42,50 % de los pacientes con fbs = 1.

Por tanto, en esta muestra no observamos grandes diferencias descriptivas en la proporción de enfermedad cardíaca entre las dos categorías de glucosa en ayunas.

3.4 Muestre en una tabla el número de pacientes con diagnóstico positivo de enfermedad cardíaca, agrupados por tipo de angina de esfuerzo (exang), que sean hombres y cuya presión arterial en reposo sea superior a la media de la muestra

En este ejercicio utilizamos cuatro variables:

  • target: diagnóstico de enfermedad cardíaca, donde 1 indica presencia de la enfermedad.
  • sex: sexo del paciente, donde 1 corresponde a hombre.
  • trestbps: presión arterial en reposo, variable cuantitativa.
  • exang: presencia de angina inducida por el ejercicio, donde 0 indica ausencia y 1 presencia.

Seleccionaremos los pacientes con diagnóstico positivo, de sexo masculino y con una presión arterial en reposo superior a la media de toda la muestra. Posteriormente, contaremos los pacientes según la categoría de exang.

# Calcular la presión arterial media de toda la muestra
media_trestbps <- mean(HeartStat$trestbps, na.rm = TRUE)

# Mostrar la media
media_trestbps
## [1] 131.3444
# Explorar las categorías de angina de esfuerzo
table(HeartStat$exang)
## 
##   0   1 
## 181  89

Una vez calculada la media, utilizaremos subset() para seleccionar los pacientes que cumplen las tres condiciones y table() para contar los casos por categoría de exang.

# Seleccionar los pacientes que cumplen las condiciones
pacientes_filtrados <- subset(
  HeartStat,
  target == 1 &
  sex == 1 &
  trestbps > media_trestbps
)

# Contar pacientes por categoría de angina de esfuerzo
tabla_exang <- table(
  exang = factor(pacientes_filtrados$exang, levels = c(0, 1))
)

# Mostrar los resultados
tabla_exang
## exang
##  0  1 
## 18 24
# Comprobar el total de pacientes seleccionados
nrow(pacientes_filtrados)
## [1] 42
# Presentar los recuentos con etiquetas descriptivas
tabla_resultado <- data.frame(
  Angina = c("Sin angina", "Con angina"),
  Pacientes = as.integer(tabla_exang)
)

knitr::kable(
  tabla_resultado,
  caption = "Pacientes con diagnóstico positivo, hombres y presión arterial superior a la media"
)
Pacientes con diagnóstico positivo, hombres y presión arterial superior a la media
Angina Pacientes
Sin angina 18
Con angina 24

La presión arterial media en reposo de la muestra es de 131,34 mmHg.

Al seleccionar los pacientes hombres con diagnóstico positivo de enfermedad cardíaca y una presión arterial en reposo superior a la media, obtenemos un total de 42 pacientes.

De estos, 18 no presentan angina inducida por el ejercicio (exang = 0), mientras que 24 sí la presentan (exang = 1). Por lo tanto, dentro del grupo seleccionado, hay más pacientes con angina de esfuerzo que sin ella.

3.5 En muchos modelos aplicados a la cardiología, la variable “sexo” o “etnia” mejora la capacidad predictiva del modelo, pero puede generar sesgos de atención si el modelo se ha entrenado mayoritariamente con un solo perfil de paciente (por ejemplo, hombres de mediana edad). ¿Qué precauciones deberían tomarse antes de implementar un algoritmo de ayuda al diagnóstico cardíaco en un hospital real?

Existe una expresión, «Garbage in, garbage out» (GIGO), que hace referencia a que, si los datos de entrada son deficientes o están sesgados, los resultados del modelo también pueden verse afectados.

Esto no solo se refiere a la calidad de los datos, sino también a la representatividad de las distintas categorías de la muestra. En términos prácticos, antes de implementar un algoritmo, debemos comprobar la calidad de los datos y si la muestra representa adecuadamente a la población del hospital, incluyendo los distintos grupos de sexo, edad y etnia.

Después, es necesario evaluar el rendimiento por subgrupos mediante la sensibilidad, la especificidad y las tasas de falsos negativos y falsos positivos. Esto permite identificar si el algoritmo omite más casos de enfermedad o genera más falsas alarmas en determinados colectivos. Además, debemos considerar el tamaño de cada subgrupo y la incertidumbre de las estimaciones.

También debemos comprobar la calibración del modelo por subgrupos, es decir, si las probabilidades de enfermedad que predice se corresponden con las frecuencias observadas.

Una prueba chi-cuadrado entre sexo y diagnóstico puede detectar una asociación en los datos, pero no permite determinar por sí sola si el algoritmo presenta sesgos en sus predicciones.

Finalmente, debemos realizar una validación externa con pacientes de otros centros y una evaluación en el hospital donde se utilizará. El algoritmo debe contar con supervisión clínica y, tras su implantación, requiere seguimiento para detectar cambios en su rendimiento y posibles desigualdades entre grupos.

seccion 3: Estadística descriptiva y gráficos

El objetivo de este apartado es trabajar los conceptos de estadística descriptiva y de representación de gráficos.

Ejercicio 4 Considerando el data frame HeartStat, resuelva las siguientes cuestiones (incluya código y resultados):

4.1 Obtenga las principales estadísticas descriptivas (media, desviación estándar, mediana, cuartiles y rango intercuartílico) de las variables cuantitativas continuas del dataset (age, trestbps, chol, thalach). Comente los resultados destacando si observa asimetría o valores atípicos.

Antes de construir la tabla estadística, comprobamos los datos y sus posibles valores ausentes.

# Seleccionar las variables cuantitativas
variables_cuantitativas <- HeartStat[, c(
  "age", "trestbps", "chol", "thalach"
)]

# Mostrar las primeras observaciones
head(variables_cuantitativas)
##   age trestbps chol thalach
## 1  70      130  322     109
## 2  67      115  564     160
## 3  57      124  261     141
## 4  64      128  263     105
## 5  74      120  269     121
## 6  65      120  177     140
# Obtener un resumen inicial
summary(variables_cuantitativas)
##       age           trestbps          chol          thalach     
##  Min.   :29.00   Min.   : 94.0   Min.   :126.0   Min.   : 71.0  
##  1st Qu.:48.00   1st Qu.:120.0   1st Qu.:213.0   1st Qu.:133.0  
##  Median :55.00   Median :130.0   Median :245.0   Median :153.5  
##  Mean   :54.43   Mean   :131.3   Mean   :249.7   Mean   :149.7  
##  3rd Qu.:61.00   3rd Qu.:140.0   3rd Qu.:280.0   3rd Qu.:166.0  
##  Max.   :77.00   Max.   :200.0   Max.   :564.0   Max.   :202.0
# Comprobar si existen valores ausentes
colSums(is.na(variables_cuantitativas))
##      age trestbps     chol  thalach 
##        0        0        0        0

Una vez comprobados los datos, calculamos los estadísticos solicitados:

# Calcular las estadísticas descriptivas de cada variable
tabla_descriptiva <- data.frame(
  Variable = names(variables_cuantitativas),
  Media = sapply(variables_cuantitativas, mean, na.rm = TRUE),
  Desv_Estandar = sapply(variables_cuantitativas, sd, na.rm = TRUE),
  Mediana = sapply(variables_cuantitativas, median, na.rm = TRUE),
  Q1 = sapply(variables_cuantitativas, quantile,
              probs = 0.25, na.rm = TRUE),
  Q3 = sapply(variables_cuantitativas, quantile,
              probs = 0.75, na.rm = TRUE),
  IQR = sapply(variables_cuantitativas, IQR, na.rm = TRUE),
  row.names = NULL
)

# Mostrar la tabla con dos decimales
knitr::kable(
  tabla_descriptiva,
  digits = 2,
  caption = "Estadísticas descriptivas de las variables cuantitativas"
)
Estadísticas descriptivas de las variables cuantitativas
Variable Media Desv_Estandar Mediana Q1 Q3 IQR
age 54.43 9.11 55.0 48 61 13
trestbps 131.34 17.86 130.0 120 140 20
chol 249.66 51.69 245.0 213 280 67
thalach 149.68 23.17 153.5 133 166 33

Podemos identificar algunos patrones:

  • age: media y mediana muy próximas (54,43 y 55), lo que no sugiere una asimetría marcada.
  • trestbps: la media es ligeramente superior a la mediana, lo que podría indicar cierta asimetría positiva.
  • chol: la media supera a la mediana y encontramos un máximo de 564 mg/dL, muy alejado del tercer cuartil (280). Hay indicios de valores extremos en la cola derecha.
  • thalach: la media es inferior a la mediana, lo que podría indicar asimetría negativa, es decir, una cola más pronunciada hacia valores bajos.

Para identificar los posibles valores atípicos usamos la regla de Tukey Q1 − 1,5 × IQR y Q3 + 1,5 × IQR.

# Calcular los límites de Tukey y contar los valores atípicos
tabla_atipicos <- data.frame(
  Variable = names(variables_cuantitativas),
  Limite_inferior = NA_real_,
  Limite_superior = NA_real_,
  N_atipicos = NA_integer_
)

for (i in seq_along(variables_cuantitativas)) {
  x <- na.omit(variables_cuantitativas[[i]])

  q1 <- quantile(x, 0.25)
  q3 <- quantile(x, 0.75)
  iqr <- IQR(x)

  li <- q1 - 1.5 * iqr
  ls <- q3 + 1.5 * iqr

  tabla_atipicos$Limite_inferior[i] <- li
  tabla_atipicos$Limite_superior[i] <- ls
  tabla_atipicos$N_atipicos[i] <- sum(x < li | x > ls)
}

# Mostrar la tabla de valores atípicos
knitr::kable(
  tabla_atipicos,
  digits = 2,
  caption = "Detección de valores atípicos mediante el criterio de Tukey"
)
Detección de valores atípicos mediante el criterio de Tukey
Variable Limite_inferior Limite_superior N_atipicos
age 28.5 80.5 0
trestbps 90.0 170.0 9
chol 112.5 380.5 5
thalach 83.5 215.5 1

Tambien lo podemos hacer graficamente usando boxplots:

# Crear una cuadrícula de 2 filas y 2 columnas
par(mfrow = c(2, 2), mar = c(4, 4, 3, 2))

variables <- names(variables_cuantitativas)
unidades <- c("Años", "mmHg", "mg/dL", "Latidos/min")

for (i in seq_along(variables)) {
  boxplot(
    variables_cuantitativas[[i]],
    main = paste("Boxplot de", variables[i]),
    ylab = unidades[i],
    col = "#7EBEA3",
    border = "#347A65",
    pch = 19,
    cex = 0.8
  )
}

# Restaurar la configuración gráfica
par(mfrow = c(1, 1))

Para evaluar la asimetría de las distribuciones, calculamos el coeficiente de asimetría (skewness):

# Calcular el coeficiente de asimetría de cada variable
tabla_asimetria <- data.frame(
  Variable = names(variables_cuantitativas),
  Asimetria = sapply(
    variables_cuantitativas,
    moments::skewness,
    na.rm = TRUE
  ),
  row.names = NULL
)

# Mostrar los resultados
knitr::kable(
  tabla_asimetria,
  digits = 3,
  caption = "Coeficientes de asimetría de las variables cuantitativas"
)
Coeficientes de asimetría de las variables cuantitativas
Variable Asimetria
age -0.163
trestbps 0.719
chol 1.177
thalach -0.525

Esto anterior tambien lo podemos verificar miranod los histrogramas:

# Crear una cuadrícula de 2 filas y 2 columnas
par(mfrow = c(2, 2), mar = c(4, 4, 3, 2))

for (i in seq_along(variables)) {

  x <- variables_cuantitativas[[i]]

  hist(
    x,
    breaks = "FD",
    probability = TRUE,
    main = paste("Distribución de", variables[i]),
    xlab = unidades[i],
    ylab = "Densidad",
    col = "#85B8E0",
    border = "white"
  )

  # Añadir curva de densidad estimada
  lines(
    density(x, na.rm = TRUE),
    col = "#C0392B",
    lwd = 2
  )

  # Añadir media y mediana
  abline(v = mean(x), col = "#2471A3", lwd = 2, lty = 2)
  abline(v = median(x), col = "#229954", lwd = 2, lty = 3)

  legend(
    "topright",
    legend = c("Media", "Mediana", "Densidad"),
    col = c("#2471A3", "#229954", "#C0392B"),
    lty = c(2, 3, 1),
    lwd = 2,
    bty = "n",
    cex = 0.75
  )
}

par(mfrow = c(1, 1))

A partir de los resultados obtenidos, podemos observar que las cuatro variables presentan diferentes comportamientos:

  • age: la media (54,43) y la mediana (55) son muy próximas. El coeficiente de asimetría (−0,163) suguiere que la distribución es aproximadamente simétrica. No se han detectado valores atípicos según el criterio de Tukey.

  • trestbps: presenta una media de 131,34 mmHg y una mediana de 130 mmHg. El coeficiente de asimetría (0,719) indica una asimetría positiva moderada, con una cola hacia valores elevados. Se han detectado 9 valores atípicos superiores a 170 mmHg.

  • chol: presenta una media de 249,66 mg/dL y una mediana de 245 mg/dL. Es la variable con mayor asimetría positiva (1,177), lo que indica una cola más pronunciada hacia valores elevados de colesterol. Se han identificado 5 valores atípicos superiores a 380,5 mg/dL, con un máximo de 564 mg/dL.

  • thalach: presenta una media de 149,68 y una mediana de 153,5 latidos por minuto. El coeficiente de asimetría (−0,525) indica una asimetría negativa moderada. Se ha detectado un valor atípico inferior a 83,5 latidos por minuto.

En conclusión, la variable age presenta una distribución aproximadamente simétrica, mientras que trestbps y chol muestran asimetría positiva y thalach asimetría negativa. La variable trestbps presenta el mayor número de valores atípicos, aunque chol tiene el coeficiente de asimetría más elevado.

Finalmente, debemos tener en cuenta que los valores atípicos identificados son observaciones estadísticamente extremas, pero no necesariamente errores en los datos.

4.2 Realice un diagrama de cajas que relacione la frecuencia cardíaca máxima alcanzada con la presencia o ausencia de enfermedad cardíaca. ¿Qué conclusión puede extraer?

# Convertir target en una variable categórica con etiquetas
grupo_enfermedad <- factor(
  HeartStat$target,
  levels = c(0, 1),
  labels = c("Sin enfermedad", "Con enfermedad")
)

# Diagrama de cajas
boxplot(
  HeartStat$thalach ~ grupo_enfermedad,
  col = c("#7EBEA3", "#3498DB"),
  border = "#347A65",
  main = "Frecuencia cardíaca máxima según enfermedad cardíaca",
  xlab = "Diagnóstico de enfermedad cardíaca",
  ylab = "Frecuencia cardíaca máxima (latidos/min)",
  pch = 19
)

A partir del diagrama de cajas, observamos que los pacientes sin enfermedad cardíaca presentan una mediana de frecuencia cardíaca máxima de aproximadamente 160 latidos por minuto, mientras que los pacientes con enfermedad cardíaca presentan una mediana próxima a 141 latidos por minuto.

También observamos que existe cierta dispersión en ambos grupos y que sus distribuciones se solapan parcialmente. Además, aparecen algunos valores atípicos en la parte inferior de ambas distribuciones.

Por tanto, podemos concluir que, en esta muestra, los pacientes con enfermedad cardíaca tienden a presentar una frecuencia cardíaca máxima alcanzada inferior a la de los pacientes sin enfermedad.

Sin embargo, esta relación es descriptiva y no permite establecer causalidad ni afirmar que la diferencia sea estadísticamente significativa sin realizar un contraste adicional.

4.3 Construya una tabla de frecuencias cruzadas que relacione el resultado del electrocardiograma en reposo con la presencia de enfermedad cardíaca. A continuación, represente esta información mediante un mapa de calor (heatmap) para identificar visualmente qué patrones son los más frecuentes.

Tenemos dos variables:

  • restecg Resultado del electrocardiograma en reposo 0, 1, 2
  • target Presencia de enfermedad cardíaca 0 = No, 1 = Sí

Primero calculamos las frecuencias absolutas mediante table()

# Construir la tabla de frecuencias cruzadas
tabla_restecg_target <- table(
  restecg = HeartStat$restecg,
  target = HeartStat$target
)

# Asignar nombres descriptivos a las columnas
colnames(tabla_restecg_target) <- c(
  "Sin enfermedad",
  "Con enfermedad"
)

# Asignar nombres descriptivos a las filas
rownames(tabla_restecg_target) <- c(
  "ECG 0",
  "ECG 1",
  "ECG 2"
)

# Mostrar la tabla
knitr::kable(
  tabla_restecg_target,
  caption = "Frecuencias cruzadas entre ECG en reposo y enfermedad cardíaca"
)
Frecuencias cruzadas entre ECG en reposo y enfermedad cardíaca
Sin enfermedad Con enfermedad
ECG 0 85 46
ECG 1 1 1
ECG 2 64 73

Utilizaremos ggplot2, que permite construir un mapa de calor con las frecuencias escritas dentro de cada celda.

# Convertir la tabla de contingencia en data frame
datos_heatmap <- as.data.frame(tabla_restecg_target)

# Mantener el orden de las categorías
datos_heatmap$target <- factor(
  datos_heatmap$target,
  levels = c("Sin enfermedad", "Con enfermedad")
)

# Construir el mapa de calor
ggplot2::ggplot(
  datos_heatmap,
  ggplot2::aes(
    x = target,
    y = restecg,
    fill = Freq
  )
) +
  ggplot2::geom_tile(color = "white", linewidth = 1) +
  ggplot2::geom_text(
    ggplot2::aes(label = Freq),
    size = 5
  ) +
  ggplot2::scale_fill_gradient(
    low = "#E3F2FD",
    high = "#1565C0"
  ) +
  ggplot2::labs(
    title = "ECG en reposo y presencia de enfermedad cardíaca",
    x = "Diagnóstico de enfermedad cardíaca",
    y = "Resultado del ECG en reposo",
    fill = "Pacientes"
  ) +
  ggplot2::theme_minimal(base_size = 12)

A partir de la tabla de frecuencias cruzadas y del mapa de calor, observamos que la combinación más frecuente corresponde a los pacientes con restecg = 0 y sin enfermedad cardíaca, con 85 observaciones.

En los pacientes con restecg = 2, encontramos una mayor frecuencia de diagnósticos positivos (73) que negativos (64). Por el contrario, en restecg = 0 predominan los diagnósticos negativos (85 frente a 46).

La categoría restecg = 1 presenta únicamente dos observaciones, por lo que no permite extraer conclusiones relevantes.

Estos resultados sugieren diferencias en la distribución del diagnóstico según el resultado del electrocardiograma en reposo.

4.4 Represente un histograma de la variable nivel de colesterol (chol) para los pacientes con enfermedad cardíaca. Superponga la curva de densidad empírica y la curva de densidad normal teórica. Evalúe si la variable sigue una distribución normal y comente el resultado.

Lo primero es seleccionar los pacientes con enfermedad cardiaca. (arget = 1 identifica a los pacientes con diagnóstico positivo.)

# Seleccionar el colesterol de pacientes con enfermedad cardíaca
chol_enfermedad <- na.omit(
  HeartStat$chol[HeartStat$target == 1]
)

# Comprobar el tamaño de la muestra y sus estadísticas
length(chol_enfermedad)
## [1] 120
summary(chol_enfermedad)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   149.0   227.2   255.5   256.5   286.5   409.0
sd(chol_enfermedad)
## [1] 47.96917

Antes del código, conviene entender qué estamos comparando.

  • Histograma: representa cómo se distribuyen los niveles de colesterol observados.
  • Densidad empírica: es una estimación suavizada de la distribución real de los datos.
  • Densidad normal teórica: representa cómo sería la distribución si siguiera una normal con la misma media y desviación estándar que los datos seleccionados.

Si ambas curvas tienen formas similares, tenemos evidencia visual compatible con la normalidad. Si presentan diferencias importantes, la hipótesis de normalidad resulta menos plausible.

# Estadísticos de los pacientes con enfermedad
media_chol <- mean(chol_enfermedad)
sd_chol <- sd(chol_enfermedad)

# Histograma en escala de densidad
hist(
  chol_enfermedad,
  breaks = "FD",
  probability = TRUE,
  col = "#85B8E0",
  border = "white",
  main = "Distribución del colesterol en pacientes con enfermedad cardíaca",
  xlab = "Colesterol (mg/dL)",
  ylab = "Densidad"
)

# Curva de densidad empírica
lines(
  density(chol_enfermedad),
  col = "#C0392B",
  lwd = 2
)

# Curva de densidad normal teórica
curve(
  dnorm(x, mean = media_chol, sd = sd_chol),
  add = TRUE,
  col = "#2471A3",
  lwd = 2,
  lty = 2
)

# Leyenda
legend(
  "topright",
  legend = c("Densidad empírica", "Normal teórica"),
  col = c("#C0392B", "#2471A3"),
  lty = c(1, 2),
  lwd = 2,
  bty = "n"
)

El gráfico es una primera aproximación, pero podemos complementarlo con dos procedimientos estadísticos:

  • Gráfico Q-Q: Un gráfico Q-Q compara los cuantiles observados con los que esperaríamos bajo una distribución normal.

  • Test de Shapiro-Wilk: donde \[H_0:\text{Los datos proceden de una distribución normal}\]

\[H_1:\text{Los datos no proceden de una distribución normal}\] interpretación habitual con un nivel de significación del 5 %: - Si p-valor menor que 0,05, rechazamos la hipótesis de normalidad. - Si p-valor mayor o igual que 0,05, no rechazamos la hipótesis de normalidad, pero eso no demuestra que los datos sean normales.

A. Gráfico Q-Q:

qqnorm(
  chol_enfermedad,
  main = "Q-Q plot del colesterol en pacientes con enfermedad",
  pch = 19,
  col = "#3498DB"
)

qqline(
  chol_enfermedad,
  col = "#C0392B",
  lwd = 2
)

B. Test de Shapiro-Wilk:

# Contraste de normalidad de Shapiro-Wilk
shapiro.test(chol_enfermedad)
## 
##  Shapiro-Wilk normality test
## 
## data:  chol_enfermedad
## W = 0.98607, p-value = 0.2558

A partir del histograma observamos que la distribución del colesterol en los pacientes con enfermedad cardíaca presenta una forma aproximadamente acampanada. La curva de densidad empírica y la curva normal teórica muestran un ajuste visual bastante próximo, especialmente en la parte central de la distribución.

En el gráfico Q-Q observamos que la mayoría de los puntos siguen aproximadamente la línea de referencia, aunque existen algunas desviaciones en la cola derecha, correspondientes a valores elevados de colesterol.

Para complementar el análisis gráfico, realizamos el test de normalidad de Shapiro-Wilk, obteniendo un estadístico W = 0,98607 y un p-valor = 0,2558.

Dado que el p-valor es superior al nivel de significación del 5 %, no rechazamos la hipótesis nula de normalidad.

Por tanto, podemos concluir que la distribución del colesterol en los pacientes con enfermedad cardíaca es razonablemente compatible con una distribución normal, aunque presenta algunas desviaciones en sus valores extremos. Esto no implica que hayamos demostrado la normalidad, sino que no encontramos evidencia estadística suficiente para rechazarla.

4.5 Calcule la matriz de correlación lineal entre las variables cuantitativas deldataset y represéntela mediante un gráfico de correlación. Comente qué dos variables muestran la correlación más fuerte (positiva o negativa) e interprete su significado clínico.

Analizamos las variables cuantitativas age, trestbps, chol, thalach y oldpeak, junto con ca, que es cuantitativa discreta y representa el número de vasos principales observados mediante fluoroscopia. Las variables categóricas se excluyen aunque estén codificadas mediante números.

El coeficiente de Pearson (\(r\)) toma valores entre −1 y +1:

  • Próximo a +1 Relación lineal positiva fuerte
  • Próximo a −1 Relación lineal negativa fuerte
  • Próximo a 0 Relación lineal débil o inexistente
# Seleccionar las variables cuantitativas del dataset
variables_correlacion <- HeartStat[, c(
  "age", "trestbps", "chol", "thalach", "oldpeak", "ca"
)]

# Calcular la matriz de correlación de Pearson
matriz_correlacion <- cor(
  variables_correlacion,
  method = "pearson",
  use = "complete.obs"
)

# Mostrar la matriz con tres decimales
knitr::kable(
  matriz_correlacion,
  digits = 3,
  caption = "Matriz de correlación de Pearson"
)
Matriz de correlación de Pearson
age trestbps chol thalach oldpeak ca
age 1.000 0.273 0.220 -0.402 0.194 0.356
trestbps 0.273 1.000 0.173 -0.039 0.223 0.086
chol 0.220 0.173 1.000 -0.019 0.028 0.127
thalach -0.402 -0.039 -0.019 1.000 -0.349 -0.265
oldpeak 0.194 0.223 0.028 -0.349 1.000 0.255
ca 0.356 0.086 0.127 -0.265 0.255 1.000

representamos gráficamente la matriz:

# Representar la matriz de correlación
corrplot::corrplot(
  matriz_correlacion,
  method = "color",
  type = "upper",
  order = "original",
  addCoef.col = "black",
  number.digits = 2,
  tl.col = "black",
  tl.srt = 45,
  diag = FALSE,
  col = colorRampPalette(
    c("#C0392B", "white", "#2471A3")
  )(200)
)

La correlación lineal más fuerte en valor absoluto corresponde a age y thalach, con r = −0,402. Esta asociación negativa de magnitud moderada indica que, en esta muestra, los pacientes de mayor edad tienden a alcanzar una frecuencia cardíaca máxima menor. El resultado es coherente con la disminución de la frecuencia cardíaca máxima asociada al envejecimiento.

La correlación positiva más elevada se observa entre age y ca (r = 0,356): los pacientes de mayor edad tienden a presentar valores más altos en el número de vasos principales observados mediante fluoroscopia. También destaca la asociación negativa entre thalach y oldpeak (r = −0,349), que indica que una mayor depresión del segmento ST inducida por el ejercicio tiende a acompañarse de una menor frecuencia cardíaca máxima alcanzada.