Riesgo cardiovascular y estimación de hipertensión sin medir la presión: ML sobre datos reales con auditoría de target leakage
Sistema end-to-end de machine learning sobre el dataset real Cardiovascular Disease (Kaggle): entrenamiento comparativo de cinco algoritmos, servicio REST que expone probabilidades de riesgo cardiovascular y de hipertensión estimada sin medir la presión, y dashboard interactivo. Conserva, como evidencia reproducible, el pipeline sintético original y su target leakage.
Este repositorio es una demostración de ingeniería de sistemas de ML, no un predictor clínico válido. La distinción no es un tecnicismo:
El modelo original no predecía hipertensión. Reaprendía una regla que ya conocíamos.
El pipeline original entrena sobre un dataset sintético cuya etiqueta se construye
aplicando un umbral determinista sobre la presión sistólica (PAS) y diastólica
(PAD) — las mismas variables que después se entregan al modelo como entrada. Es un
caso de libro de target leakage: el modelo puede alcanzar métricas casi perfectas
simplemente reproduciendo el if que generó la etiqueta. Ese pipeline sigue en el
repositorio como evidencia, fuera del camino de servicio.
Está verificado numéricamente y el análisis es reproducible:
python scripts/verify_leakage.pyDetalle completo, evidencia y consecuencias en docs/LEAKAGE_ANALYSIS.md.
Estado actual del defecto: corregido en el entrenamiento y en el servicio. Desde
el 2026-09-17 existe un segundo pipeline,
src/train_cardio_real.py, que entrena sobre el dataset
real de Kaggle (68.678 pacientes) excluyendo la presión arterial de las features. Ahí
la pregunta es genuina y la respuesta, modesta: AUC 0,6955 [0,6866 · 0,7049] para
estimar el estado hipertensivo sin medirlo, con una accuracy apenas +3,35 pp sobre el
baseline de clase mayoritaria (modelo seleccionado en DT-4). Verificable con:
python scripts/audit_cardio_leakage.py # criterio de aceptación de DT-1Resultados vigentes en docs/DT4_RESULTS.md (selección por
validación cruzada con protocolo preregistrado); los de la primera corrida, históricos,
en docs/DT1_RESULTS.md. El servicio FastAPI y el dashboard sirven
solo modelos reales (A′ con presión arterial y B1 sin ella), los de DT-4 desde
a7bf9ef; el contrato sintético /predecir se eliminó en 6e59035.
Lo que sí demuestra este repositorio, y demuestra bien:
- Pipeline reproducible de datos → entrenamiento → selección de modelo → artefactos.
- Selección de modelo preregistrada (
docs/DT4_PROTOCOL.md, commiteado antes de entrenar): 25 candidatos por experimento, log-loss en validación cruzada de 5 folds sobre el train, regla de un error estándar con orden de simplicidad declarado, y el test evaluado una sola vez sobre el seleccionado. - Servicio de inferencia con FastAPI, esquema validado con Pydantic y OpenAPI automático: validación del dominio de entrenamiento, contrato anti-leakage (B1 rechaza la presión con 422) y verificación de versiones y sha256 de los artefactos al arrancar.
- Dashboard Streamlit con inferencia individual y masiva vía CSV, cliente HTTP puro.
- EDA sobre un dataset clínico real (70.000 pacientes, Kaggle).
- Auditoría de leakage con control positivo, y una ablación controlada con bootstrap pareado que cuantifica cuánto vale medir la presión arterial: Δ AUC = 0,0991 [0,0919 · 0,1061] (DT-4; el 0,1103 de DT-1 estaba inflado por comparar contra una SVM elegida por macro-F1).
Aviso médico: herramienta con fines educativos. No constituye diagnóstico, consejo médico ni sustituye la valoración de un profesional de la salud.
data/real/cardio/cardio_train.csv src/cardio_features.py
│ (features, derivación, cotas)
▼ │ │
src/train_cardio_dt4.py ◄──────────────────────┘ │
│ │
▼ ▼
models/dt4_*.pkl + dt4_manifest.json ──► api/main.py (FastAPI) ◄── HTTP ── app/dashboard.py
verifica versiones y (Streamlit, cliente
sha256 al arrancar HTTP puro)
Registro de DT-1 (no se sirve):
src/train_cardio_real.py ──► models/dt1_*.pkl + dt1_manifest.json
Evidencia del leakage, fuera del servicio:
src/generate_dataset.py ──► src/train_classical_models.py ──► models/modelo_*.pkl
Descripción detallada de módulos, contratos y flujo de datos en docs/ARCHITECTURE.md.
Nota sobre duplicación: los scripts de la raíz son una versión anterior e incompatible del pipeline (
src/+api/+app/es el activo). Resolverla —migrar o eliminar— sigue pendiente en docs/ROADMAP.md (DT-5).
Requiere Python 3.14 (referencia: 3.14.6). Los .pkl son objetos serializados
de scikit-learn, así que el entorno se instala desde el lock, con versiones exactas.
git clone https://github.com/Elorro/hypertension-ml-system.git
cd hypertension-ml-system
python3.14 -m venv .venv
source .venv/bin/activate # zsh/bash · Windows: .venv\Scripts\activate
pip install -r requirements.lock.txt # entorno completo (entrenamiento, tests, servicio)Instalaciones mínimas: requirements-serve.txt (solo la API, sin pandas ni xgboost) y
requirements-dashboard.txt (solo el dashboard, sin scikit-learn).
Los artefactos entrenados (models/*.pkl) y el dataset real no se versionan. Los
cuatro .pkl que sirve la API (modelo y scaler de A′ y de B1) están publicados como
assets de un GitHub Release: servirlos no requiere el CSV. fetch-models los
descarga y verifica su sha256 contra models/dt4_manifest.json:
export MODELS_BASE_URL=https://github.com/Elorro/hypertension-ml-system/releases/download/models-dt4-41572ed
make fetch-models # descarga y verifica el sha256 de los 4 .pkl servidos
python -m scripts.verify_env --solo-servidos # versiones + sha256 + carga y predicción
make serve-api # servicio en http://127.0.0.1:8000 (docs en /docs)
make serve-dashboard # dashboard en http://localhost:8501 (API_URL, por defecto :8000)Sin make:
export MODELS_BASE_URL=https://github.com/Elorro/hypertension-ml-system/releases/download/models-dt4-41572ed
python -m scripts.fetch_models
python -m scripts.verify_env --solo-servidos
uvicorn api.main:app --host 127.0.0.1 --port 8000
API_URL=http://127.0.0.1:8000 streamlit run app/dashboard.py
make verify-env(sin--solo-servidos) verifica además el experimentocv_sin_pa, que no se publica: solo pasa tras reentrenar localmente. Para reproducir el entrenamiento (requiere el CSV), ver docs/DATA.md y docs/DT4_RESULTS.md.
El servicio carga los modelos una vez al arrancar, tras verificar versiones y sha256 contra
models/dt4_manifest.json. Si falta un.pklo no coincide, no arranca.
curl -X POST http://127.0.0.1:8000/v1/riesgo-cardiovascular \
-H "Content-Type: application/json" \
-d '{"age_years":52,"gender":1,"height":165,"weight":72,"cholesterol":1,"gluc":1,
"smoke":0,"alco":0,"active":1,"ap_hi":130,"ap_lo":85}'
# → {"probabilidad":0.5173…,"prevalencia_base":0.4948…,"modelo":{…},"aviso":"…","clase":1,"umbral":0.5}Referencia completa de endpoints y esquemas en docs/API.md.
| Endpoint | Modelo | Target | AUC test [IC 95 %] | Salida |
|---|---|---|---|---|
POST /v1/riesgo-cardiovascular |
A′ riesgo_cv_con_pa: RF(depth 12, hoja 20) |
cardio, con presión |
0,8019 [0,7938 · 0,8088] | probabilidad + clase (umbral 0,5) |
POST /v1/hipertension-sin-pa |
B1 hta_b1, experimental: RF(depth 8, hoja 100) |
hta, sin presión |
0,6955 [0,6866 · 0,7049] | solo probabilidad |
Seleccionados en DT-4 por log-loss en validación cruzada con la regla de 1 EE; en los
dos, el mínimo de log-loss era un XGBoost poco profundo con el RF dentro de 1 EE. El
test se evaluó una sola vez, pero ya se había observado en DT-1 (limitación declarada).
Recursos medidos en local: RSS de la API con los dos modelos 202 MiB (352 con los de
DT-1), .pkl 32 MB (130). Ficha completa en docs/MODEL_CARD.md.
| Modelo | Implementación | Artefacto |
|---|---|---|
| Regresión Logística | LogisticRegression |
modelo_logreg.pkl |
| SVM (kernel RBF) | SVC(probability=True) |
modelo_svm.pkl |
| Árbol de Decisión | DecisionTreeClassifier |
modelo_tree.pkl |
| Random Forest | RandomForestClassifier |
modelo_rf.pkl |
| Gradient Boosting | XGBClassifier |
modelo_xgb.pkl |
La selección se hace por macro-F1 sobre el conjunto de test y el ganador se
persiste en models/mejor_modelo.txt. Ningún servicio carga ya estos artefactos.
| Modelo | Accuracy | Macro-F1 |
|---|---|---|
| Regresión Logística | 47,70 % | 0,4711 |
| SVM RBF | 68,82 % | 0,6961 |
Árbol de Decisión (max_depth=8) |
94,83 % | 0,9497 |
| Random Forest (300 árboles) | 94,84 % | 0,9498 |
| XGBoost (400 árboles) | 95,32 % | 0,9547 |
Ese 95,3 % no se lee contra el 29,6 % del azar, sino contra el 91,1 % de las
etiquetas del dataset sintético que recupera una regla if determinista, sin
entrenar (scripts/verify_leakage.py::regla_completa): la misma que generó esas
etiquetas. Es el defecto que DT-1 cerró; sobre el dataset real, sin la presión en las
features, la mejor regla determinista que encuentra scripts/audit_cardio_leakage.py
supera al baseline de clase mayoritaria en solo +2,28 pp, mientras que con
ap_hi/ap_lo (control positivo) reconstruye el target al 99,27 %. Y la tabla
contiene dos señales del defecto:
- Un árbol de profundidad 8 llega a 94,83 %, a medio punto de 400 árboles boosteados. Cuando el gradient boosting no se despega de un modelo trivial, no queda estructura estadística que extraer.
- Los modelos lineales y de kernel se hunden a 48–69 % mientras los de árboles
rondan el 95 %. El target es una partición por umbrales: los árboles la
representan nativamente, los lineales no pueden. La geometría del problema es la
de un
if.
Análisis completo en docs/LEAKAGE_ANALYSIS.md y docs/MODEL_CARD_SINTETICO.md (histórica).
| Clase | Etiqueta | Criterio (guías AHA, simplificado) |
|---|---|---|
| 0 | Normal | PAS < 120 y PAD < 80 |
| 1 | Prehipertensión | PAS 120–129 o PAD 80–84 |
| 2 | Hipertensión Grado 1 | PAS 130–139 o PAD 85–89 |
| 3 | Hipertensión Grado 2 | PAS ≥ 140 o PAD ≥ 90 |
Diccionario de variables, procedencia y licencias de los datos en docs/DATA.md.
hypertension-ml-system/
├── api/
│ ├── main.py # Servicio FastAPI: endpoints y carga en lifespan
│ ├── schemas.py # Esquemas Pydantic (entrada en unidades humanas)
│ └── servicio.py # Registro de modelos y matriz de features
├── app/
│ ├── dashboard.py # Dashboard Streamlit (cliente HTTP del servicio)
│ └── explicacion.py # Texto que explica la probabilidad (sin Streamlit)
├── src/
│ ├── cardio_features.py # Fuente única: features, derivación y cotas de DT-1
│ ├── artefactos.py # Verificación de versiones y sha256 de los .pkl
│ ├── train_cardio_dt4.py # DT-4: CV, regla de 1 EE y una evaluación en test (lo que sirve la API)
│ ├── train_cardio_real.py # DT-1: registro del entrenamiento sobre datos reales (no se sirve)
│ ├── generate_dataset.py # Generador sintético (evidencia del leakage)
│ └── train_classical_models.py # Entrenamiento sintético (evidencia; no se sirve)
├── scripts/
│ ├── fetch_models.py # Descarga y verifica los .pkl del release (make fetch-models)
│ ├── verify_env.py # Compuerta del entorno (make verify-env)
│ ├── verify_leakage.py # Auditoría del leakage del dataset sintético
│ └── audit_cardio_leakage.py # Criterio de aceptación de DT-1 (datos reales)
├── tests/ # Contrato e integración (en CI con los .pkl del release); lo que exige el CSV, solo en local
├── notebooks/
│ └── EDA_cardiovascular_real.ipynb # EDA sobre datos reales de Kaggle
├── data/
│ ├── raw/ # Dataset sintético generado (no versionado)
│ └── real/cardio/ # Dataset Kaggle (no versionado; se descarga de la fuente)
├── models/ # Artefactos entrenados (.pkl no versionados)
│ ├── dt4_manifest.json # Manifiesto de DT-4: lo que sirve la API (sí versionado)
│ └── dt1_manifest.json # Evidencia de la corrida DT-1 (sí versionado)
├── deploy/release.sh # Publica los .pkl servidos como assets de un GitHub Release
├── render.yaml # Configuración de Render para la API (todavía sin desplegar)
├── .github/workflows/ci.yml # CI: lint, tests, integración con el release y pipeline
├── docs/ # Documentación técnica
└── [scripts de la raíz] # Pipeline heredado — ver ROADMAP
| Documento | Contenido |
|---|---|
| ARCHITECTURE.md | Módulos, contratos y flujo de datos |
| LEAKAGE_ANALYSIS.md | Auditoría del target leakage con evidencia |
| MODEL_CARD.md | Model card de A′ y B1: uso previsto, límites, riesgos |
| MODEL_CARD_SINTETICO.md | Model card histórica del modelo sintético |
| DATA.md | Diccionario de datos, procedencia y licencias |
| API.md | Referencia de endpoints |
| DT4_PROTOCOL.md | Protocolo preregistrado de selección y evaluación |
| DT4_RESULTS.md | Resultados vigentes: selección por CV y una evaluación en test |
| DT1_RESULTS.md | Resultados históricos de la primera corrida sobre el dataset real |
| ROADMAP.md | Deuda técnica priorizada y trabajo futuro |
| BITACORA.md | Registro cronológico de decisiones |
Fase actual: prototipo funcional sirviendo los modelos de DT-4; compuerta de validación estadística (DT-1 a DT-4) cerrada.
- ✅ DT-1 cerrado (2026-09-17). El entrenamiento migró al dataset real; ninguna regla determinista sobre las features supera al baseline por más de 2,28 pp.
- ✅ DT-3 y DT-4 cerrados: selección por log-loss en validación cruzada de 5 folds
sobre el train y una sola evaluación en test, con protocolo preregistrado (
0d8d4c9), códigoad518f2, manifiesto41572edy servicioa7bf9ef. Limitación: el test ya se había observado en DT-1. - ✅ Servicio migrado (
6e59035,a7bf9ef): la API y el dashboard sirven solo A′ y B1 de DT-4. Sigue abierta la otra mitad de DT-5: el pipeline heredado de la raíz. - 🟡 DT-6 parcial: 125 tests. En CI, 110 corren en el job Tests y 6 más (integración
con los
.pkldel release) en el job Integración; 9 requieren el CSV, no redistribuible, y solo corren en local. Cobertura desrc/+api/: 72 % en local, 65 % en CI. El criterio del 70 % no está automatizado: ningún paso del CI lo exige. - No hay despliegue público: el servicio corre en local.
Ver docs/ROADMAP.md y BITACORA.md.
Este proyecto se desarrolló con asistencia de herramientas de IA: Claude (Anthropic), en
chat y con Claude Code, para el diseño, la revisión y parte de la implementación, y Codex
(OpenAI) para los cambios de presentación del dashboard. Las decisiones de diseño
(protocolo preregistrado, criterios de aceptación, umbrales) y la revisión de cada
cambio antes de commitear son del autor. Las cifras de este repositorio salen de
ejecuciones reproducibles, con manifiestos y sha256 (models/dt4_manifest.json), no de
texto generado. Los commits no llevan trailers de coautoría; el uso de IA se declara aquí.
MIT — ver LICENSE. El dataset de Kaggle no se incluye: su licencia es desconocida, así que se purgó de la historia del repositorio y se descarga de la fuente. Ver docs/DATA.md.
Luis Araque — luisalfredoaraque@gmail.com