Introducción: El Paradigma del Aprendizaje Supervisado

Como clínicos e investigadores, estamos acostumbrados a la estadística tradicional para encontrar asociaciones. Sin embargo, cuando el objetivo no es solo explicar, sino predecir eventos futuros en pacientes individuales basados en historiales complejos, necesitamos el Aprendizaje Automático (Machine Learning).

En este taller, abordaremos un problema de Aprendizaje Supervisado: cada paciente en nuestra cohorte tiene una etiqueta conocida (falleció o sobrevivió), y nuestro modelo aprenderá el mapeo matemático desde los síntomas clínicos (features) hasta ese desenlace clínico.

Nuestra Cohorte: Registros clínicos de insuficiencia cardíaca (299 pacientes, 105 fallecimientos, recabados en Faisalabad, Pakistán en 2015). Usaremos 11 predictores clínicos para predecir la muerte intrahospitalaria.


Paso 0: Setup (El Quirófano Estéril)

Imagina que el programa R recién instalado es un quirófano completamente vacío. Tiene luz y una camilla, pero no puedes operar un trasplante de corazón sin traer instrumentos especializados (bisturís, monitores, suturas). En R, estos instrumentos especializados se llaman “Paquetes” (Packages).

Como científicos, nuestro primer paso es asegurar un entorno de trabajo con garantía de reproducibilidad absoluta. Si el código funciona en nuestra computadora, debe funcionar idéntico en la de nuestros colegas.

Para lograrlo, ejecutaremos un “Checklist Prequirúrgico Automatizado”. En lugar de instalar cada herramienta manualmente, hemos programado un algoritmo inteligente que:

  • Revisa el inventario de tu computadora.

  • Identifica qué instrumentos matemáticos te faltan.

  • Los descarga automáticamente de la farmacia central (CRAN).

  • Nos da luz verde para ingresar los datos del paciente (nuestra base de datos clínica) usando rutas seguras que nunca se rompen (here y rio).

Copia, pega y ejecuta este bloque completo de código. Solo tendrás que hacerlo una vez.

# -------------------------------------------------------------------------
# CHECKLIST PREQUIRÚRGICO: Verificación e Instalación Automática
# -------------------------------------------------------------------------

# 1. Definimos el "inventario" de todo el instrumental necesario
paquetes_requeridos <- c(
  # Herramientas base y manejo de datos
  "tidyverse", "here", "rio", "janitor",
  # Análisis clínico descriptivo
  "gtsummary", 
  # Ecosistema de Machine Learning
  "tidymodels", 
  # Motores algorítmicos (El Zoológico)
  "glmnet", "kknn", "kernlab", "ranger",
  # Herramientas para reportes reproducibles
  "rmarkdown", "knitr"
)

# 2. Comparamos el inventario requerido con lo que ya está instalado en la PC
paquetes_nuevos <- paquetes_requeridos[!(paquetes_requeridos %in% installed.packages()[,"Package"])]

# 3. Lógica Condicional (IF): Si faltan paquetes, instálalos; si no, avisa que todo está bien
if(length(paquetes_nuevos) > 0) {
  message("Faltan instrumentos. Descargando e instalando: ", paste(paquetes_nuevos, collapse = ", "))
  # dependencies = TRUE asegura que también se descarguen paquetes secundarios necesarios
  install.packages(paquetes_nuevos, dependencies = TRUE)
  message("¡Instalación completada! El quirófano está equipado y listo.")
} else {
  message("Inventario completo. Todos los paquetes requeridos ya están instalados. ¡Listo para operar!")
}

# 4. (Opcional) Cargar las librerías base para comprobar que funcionan
library(tidyverse)
library(tidymodels)

# Importamos la cohorte clínica usando una ruta estable
ruta_datos <- here::here("hf_raw.csv")
hf_raw <- rio::import(ruta_datos)

Paso 1: Importar y Transformar (La Historia Clínica)

Los datos clínicos crudos suelen engañarnos de dos maneras: formatos desordenados y la temida fuga de datos (Data Leakage).

El Data Leakage ocurre cuando un predictor codifica en secreto el desenlace. En nuestra base, tenemos la variable time (días de seguimiento). Clínicamente, los pacientes que fallecen temprano tienen un tiempo de seguimiento corto. Si dejamos esta variable, el modelo “predecirá” la muerte leyendo su propia respuesta, viéndose perfecto en el laboratorio pero fracasando en el hospital real. Un predictor debe ser conocible en el momento de la admisión.

Usaremos el pipe nativo de R (|>), que se lee como “y entonces…”. En lugar de alterar los datos originales, esta sintaxis funcional crea copias limpias paso a paso.

library(tidyverse)
library(janitor)

hf <- hf_raw |> 
  clean_names() |> # Estandariza los nombres de las variables
  select(-time) |> # ELIMINAMOS EL LEAKAGE: El tiempo de seguimiento es trampa
  mutate(
    # Convertimos el evento a factor. Regla: El evento de interés ("died") va primero.
    outcome = factor(if_else(death_event == 1, "died", "survived"), levels = c("died", "survived"))
  ) |> 
  select(-death_event) |> 
  mutate(across(c(anaemia, diabetes, high_blood_pressure, sex, smoking), factor))

glimpse(hf)
## Rows: 299
## Columns: 12
## $ age                      <dbl> 75, 55, 65, 50, 65, 90, 75, 60, 65, 80, 75, 6…
## $ anaemia                  <fct> 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, …
## $ creatinine_phosphokinase <int> 582, 7861, 146, 111, 160, 47, 246, 315, 157, …
## $ diabetes                 <fct> 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, …
## $ ejection_fraction        <int> 20, 38, 20, 20, 20, 40, 15, 60, 65, 35, 38, 2…
## $ high_blood_pressure      <fct> 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, …
## $ platelets                <dbl> 265000, 263358, 162000, 210000, 327000, 20400…
## $ serum_creatinine         <dbl> 1.90, 1.10, 1.30, 1.90, 2.70, 2.10, 1.20, 1.1…
## $ serum_sodium             <int> 130, 136, 129, 137, 116, 132, 137, 131, 138, …
## $ sex                      <fct> 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, …
## $ smoking                  <fct> 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, …
## $ outcome                  <fct> died, died, died, died, died, died, died, die…

Paso 2: Análisis Exploratorio y el Desequilibrio de Clases

Antes de modelar, debemos leer a nuestra cohorte. Un modelo es tan honesto como nuestra comprensión de sus datos.

Generaremos la clásica “Tabla 1” clínica usando gtsummary. Al leerla, notaremos que los fallecimientos representan aproximadamente el 32% de la cohorte. Este desequilibrio dicta nuestra elección de métricas diagnósticas:

  • AUC-ROC: Evalúa el ranking global, pero su línea base sin habilidad es siempre 0.5.

  • AUC-PR (Precisión-Recall): Se enfoca en la clase de interés (mortalidad). Su línea base no es 0.5, sino la prevalencia clínica (\(\approx 0.32\)). Nos protege del falso optimismo.

library(gtsummary)

# La Tabla 1 clínica, estratificada por desenlace
hf |> tbl_summary(by = outcome)
Characteristic died
N = 96
1
survived
N = 203
1
age 65 (55, 75) 60 (50, 65)
anaemia

    0 50 (52%) 120 (59%)
    1 46 (48%) 83 (41%)
creatinine_phosphokinase 259 (129, 582) 245 (109, 582)
diabetes

    0 56 (58%) 118 (58%)
    1 40 (42%) 85 (42%)
ejection_fraction 30 (25, 38) 38 (35, 45)
high_blood_pressure

    0 57 (59%) 137 (67%)
    1 39 (41%) 66 (33%)
platelets 258,500 (197,000, 312,000) 263,000 (219,000, 302,000)
serum_creatinine 1.30 (1.05, 1.90) 1.00 (0.90, 1.20)
serum_sodium 136 (133, 139) 137 (135, 140)
sex

    0 34 (35%) 71 (35%)
    1 62 (65%) 132 (65%)
smoking

    0 66 (69%) 137 (67%)
    1 30 (31%) 66 (33%)
1 Median (Q1, Q3); n (%)

Paso 3: La Columna Vertebral Logística (tidymodels)

En R moderno no usamos scripts aislados para cada algoritmo. Usamos el ecosistema tidymodels para construir una “columna vertebral” reutilizable de 5 pasos que evita la contaminación de datos: 1. rsample: Divide quién entrena y quién se examina. 2. recipes: La receta de preprocesamiento (escalar, variables dummy). 3. parsnip: Especifica el motor matemático. 4. workflows: Empaqueta la receta y el motor para que nada se filtre del test al train. 5. yardstick: Calcula las métricas finales.

library(tidymodels)

# 1. División Train / Test
set.seed(2026)
hf_split <- initial_split(hf, prop = 0.80, strata = outcome)
hf_train <- training(hf_split)
hf_test  <- testing(hf_split)

# 2. La Receta (Normalización / Estandarización Z-score)
hf_rec <- recipe(outcome ~ ., data = hf_train) |> 
  step_dummy(all_nominal_predictors()) |> 
  step_zv(all_predictors()) |> 
  step_normalize(all_numeric_predictors())

# 3. El Motor (Regresión Logística Base)
log_spec <- logistic_reg() |> set_engine("glm") |> set_mode("classification")

# 4. El Workflow
log_wf <- workflow() |> add_recipe(hf_rec) |> add_model(log_spec)

Paso 4: El Zoológico de Modelos (Afinación y Validación Cruzada)

Aquí es donde cuatro familias de algoritmos compiten de manera justa. Cada una tiene hiperparámetros (perillas que controlan el balance entre sesgo y varianza):

  • Logística Penalizada: \(\lambda\) (fuerza de penalización) y \(\alpha\) (mezcla Ridge/Lasso).
  • \(k\)-NN (Vecinos Cercanos): \(k\) dicta cuántos pacientes previos consultar. Un \(k\) pequeño sobreajusta (alta varianza), un \(k\) alto subajusta (alto sesgo).
  • SVM: Fronteras curvas. \(C\) (costo de errores) y \(\sigma\) (alcance de la influencia local).
  • Random Forest: mtry (variables por decisión) y min_n (pacientes por nodo final). El número de árboles (trees) es solo para convergencia, no se afina.

Para evaluarlos sin engañarnos por un solo Test Set pequeño, usaremos Validación Cruzada de 5 pliegues (5-fold CV).

# 1. Validación Cruzada (El Tribunal Imparcial)
set.seed(2026)
cv_folds <- vfold_cv(hf_train, v = 5, strata = outcome)

# 2. Especificación de los Motores Competidores (Perillas en "tune")
pen_log_spec <- logistic_reg(penalty = tune(), mixture = tune()) |> 
  set_engine("glmnet") |> set_mode("classification")

knn_spec <- nearest_neighbor(neighbors = tune()) |> 
  set_engine("kknn") |> set_mode("classification")

svm_spec <- svm_rbf(cost = tune(), rbf_sigma = tune()) |> 
  set_engine("kernlab") |> set_mode("classification")

rf_spec <- rand_forest(mtry = tune(), min_n = tune(), trees = 1000) |> 
  set_engine("ranger") |> set_mode("classification")

# 3. Empaquetar el Zoológico
zoo_workflows <- workflow_set(
  preproc = list(base = hf_rec),
  models = list(logistica = pen_log_spec, knn = knn_spec, svm = svm_spec, forest = rf_spec)
)

# 4. Afinación de todos los algoritmos simultáneamente
set.seed(2026)
zoo_tune <- zoo_workflows |> 
  workflow_map("tune_grid", resamples = cv_folds, grid = 8, metrics = metric_set(roc_auc, pr_auc))
## maximum number of iterations reached 0.00477708 -0.004749117maximum number of iterations reached 0.002751684 -0.002724162maximum number of iterations reached 0.00481657 -0.004793014maximum number of iterations reached 0.003266553 -0.003254962maximum number of iterations reached 0.00245379 -0.002448697maximum number of iterations reached 0.002440783 -0.002433203maximum number of iterations reached 6.752957e-05 -6.744515e-05maximum number of iterations reached 0.004609966 -0.004582715maximum number of iterations reached 0.003595814 -0.003537578maximum number of iterations reached 0.003715073 -0.003692037maximum number of iterations reached 0.003534648 -0.003525042maximum number of iterations reached 1.015522e-05 -1.014087e-05
# 5. GRÁFICO COMPARATIVO: Mostramos el rendimiento de los modelos en la Validación Cruzada
autoplot(zoo_tune) + 
  theme_minimal() +
  labs(title = "Rendimiento del Zoológico de Modelos (Validación Cruzada)")


Paso 5: El Veredicto Final y Reporte Reproducible

La evaluación en una sola partición de \(\approx 75\) pacientes (nuestro Test Set) está sujeta a la suerte estadística (ruido). Elegimos a nuestro campeón basados en el promedio riguroso de la validación cruzada.

# Seleccionamos matemáticamente la mejor configuración del Random Forest
best_rf <- extract_workflow_set_result(zoo_tune, "base_forest") |> select_best(metric = "roc_auc")

final_wf <- extract_workflow(zoo_tune, "base_forest") |> finalize_workflow(best_rf)

# Examen final en el Test Set aislado
final_fit <- last_fit(final_wf, hf_split, metrics = metric_set(roc_auc, pr_auc))

collect_metrics(final_fit) |> 
  select(.metric, .estimate) |> 
  knitr::kable(col.names = c("Métrica", "Puntuación de Prueba (Test)"))
Métrica Puntuación de Prueba (Test)
roc_auc 0.6939024
pr_auc 0.5242923

Este documento en sí mismo representa la reproducibilidad: One Render. El código ejecuta el análisis mientras lo grafica. La documentación y las matemáticas son el mismo objeto exacto.


Cuestionario de Validación Metodológica

Aseguremos la comprensión de los principios fundamentales de esta arquitectura:

1. Tubería de datos: hf_raw |> clean_names() |> select(...) |> filter(...). ¿Qué sucede realmente aquí? Respuesta: El flujo es funcional y devuelve un tibble completamente nuevo. Los datos originales (hf_raw) permanecen intactos, evitando la modificación in-place.

2. ¿Por qué es crítico observar el AUC-PR y no solo el AUC-ROC? Respuesta: El AUC-PR está anclado a la prevalencia del evento (\(\approx 0.32\)). Evalúa estrictamente el desempeño en la clase minoritaria (muerte), exponiendo deficiencias clínicas que un AUC-ROC halagador podría ocultar.

3. ¿Cuál es el cambio mínimo necesario en tidymodels para pasar de una regresión logística a un Bosque Aleatorio? Respuesta: Cambiar exclusivamente la especificación del modelo (parsnip). La división de datos, la receta y el workflow son independientes del algoritmo, previniendo así errores de preprocesamiento.

4. En el modelo de Vecinos Más Cercanos (\(k\)-NN), ¿cómo afecta el parámetro \(k\) al sesgo y la varianza? Respuesta: Un \(k\) pequeño sobreajusta memorizando el ruido local (bajo sesgo, alta varianza). Un \(k\) grande suaviza demasiado e ignora detalles clínicos (alto sesgo, baja varianza).

5. Prediga el orden de los valores de AUC: (A) Resustitución en Train, (B) Validación Cruzada CV, (C) Test Set. ¿Cuál es el mayor? Respuesta: (A) será drásticamente el mayor, ya que evaluar un modelo con sus propios datos de entrenamiento es un autoengaño. La diferencia entre (B) y (C) en cohortes pequeñas es en gran parte ruido estadístico, por lo que confiamos en la estabilidad del CV.

6. ¿Qué elementos constituyen una verdadera reproducibilidad científica para entregar a un colega? Respuesta: La combinación estricta de: Entorno fijado (renv) + Rutas estables (here) + Semilla aleatoria fija (set.seed) + Un reporte ejecutable que re-renderiza el análisis determinísticamente.


Créditos del Taller: Adaptado del tutorial “Practical Artificial Intelligence for Medical Data Analyses with R” elaborado por Corrado Lanera (Unit of Biostatistics, Epidemiology & Public Health, PhD course in Translational Specialistic Medicine “G.B. Morgagni” · University of Padova). Dataset original: Heart-failure clinical records (Chicco & Jurman, 2020).