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.
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)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…
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 = 961 |
survived N = 2031 |
|---|---|---|
| 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 (%) | ||
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)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):
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)")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.
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).