3. Monitoreo de deriva
Proyecto Integrador — Heart Failure Prediction¶
Nombres: Juan Camilo Oñoro Araujo - 200177329 y María Carolina Cantillo Orozco - 200179105¶
Fecha: 30/09/2026¶
MACHINE LEARNING¶
Notebook 3 — Etapa 6: Monitoreo de deriva de datos con Evidently
Un modelo se entrena con los datos de un momento y de un lugar. En producción llegan pacientes nuevos, y con el tiempo esos pacientes pueden ser distintos a los de entrenamiento: otro hospital, otra población, otro equipo de medición o un error en el registro. A eso se le llama deriva de datos (data drift).
Cuando hay deriva, el modelo sigue respondiendo, pero sus predicciones pueden dejar de ser confiables sin que nadie lo note, porque en producción no se conoce el diagnóstico real de inmediato. Por eso se monitorea: se comparan los datos que llegan con los de entrenamiento, y si la distribución cambia, es una señal para revisar el modelo o reentrenarlo.
Evidently hace esa comparación variable por variable con pruebas estadísticas y genera un reporte HTML. En este cuaderno se hacen dos comparaciones:
Train contra test, como pide el enunciado. Los dos vienen del mismo split aleatorio, así que no debería haber deriva. Sirve como control.
Train contra un lote de pacientes con deriva simulada, para comprobar que el monitoreo sí detecta un cambio cuando lo hay.
Sería bueno como oportunidad de mejora evaluar no solo la edad, sino el año de nacimiento para una determinada fecha de prueba. POr ejemplo, no es lo mismo una persona de 60 años en 1990 que en 2050, porque las costumbres y las propagandas o culturas de ejercicio o dieta son distintas. Aún si la edad es unf actor de riesgo, el grupo considera que también la época, por ejemplo, antes fumar era recomendado o que le asbesto era un amterial seguro, hoy en día, a pesar de que está presenta, hay una mayor conciencia.
Parte 1. Librerías y rutas
import json
import warnings
from pathlib import Path
import joblib
import numpy as np
import pandas as pd
from IPython.display import display
import evidently
from evidently import ColumnMapping
from evidently.report import Report
from evidently.metric_preset import DataDriftPreset
warnings.filterwarnings("ignore")
pd.set_option("display.float_format", "{:.4f}".format)
ROOT = Path.cwd().parent if Path.cwd().name == "notebooks" else Path.cwd()
DATA_DIR = ROOT / "data"
SEED = 42
print(f"evidently {evidently.__version__} | pandas {pd.__version__}")evidently 0.4.40 | pandas 2.1.0
Parte 2. Datos de referencia y datos actuales
La referencia es el conjunto de entrenamiento, que es lo que el modelo conoce. Los datos actuales son el conjunto de prueba, que hace el papel de los pacientes que llegan en producción. Los dos se guardaron en data/ en el notebook 2.
Además de las 11 variables, se agrega la probabilidad que predice el modelo exportado (app/model.joblib). Así también se monitorea la deriva de las predicciones, que es lo que pide el enunciado con “entre entrenamiento y predicción”: si el modelo empieza a dar probabilidades muy distintas a las habituales, algo cambió.
TARGET = "HeartDisease"
train = pd.read_csv(DATA_DIR / "train.csv")
test = pd.read_csv(DATA_DIR / "test.csv")
modelo = joblib.load(ROOT / "app" / "model.joblib")
X_train, X_test = train.drop(columns=TARGET), test.drop(columns=TARGET)
X_train["prediction"] = modelo.predict_proba(X_train)[:, 1]
X_test["prediction"] = modelo.predict_proba(X_test)[:, 1]
# Se indica a Evidently el tipo de cada columna, para que use la prueba estadística correcta
COLS_NUM = ["Age", "RestingBP", "Cholesterol", "MaxHR", "Oldpeak"]
COLS_CAT = ["Sex", "ChestPainType", "FastingBS", "RestingECG", "ExerciseAngina", "ST_Slope"]
column_mapping = ColumnMapping(numerical_features=COLS_NUM, categorical_features=COLS_CAT,
prediction="prediction", target=None)
print(f"Referencia (train): {X_train.shape[0]} pacientes | Actual (test): {X_test.shape[0]} pacientes")
display(X_test.head(3))Referencia (train): 734 pacientes | Actual (test): 184 pacientes
Parte 3. Reporte de deriva: train contra test
Es el código del enunciado. DataDriftPreset compara cada columna entre la referencia y los datos actuales. Con menos de 1,000 filas, Evidently usa por defecto la prueba de Kolmogorov-Smirnov para las numéricas, chi cuadrado para las categóricas y una prueba Z de proporciones para las binarias. Una columna tiene deriva si el p-value es menor que 0.05, y el dataset completo se marca con deriva si la mitad o más de las columnas la tienen.
report = Report(metrics=[DataDriftPreset()])
report.run(reference_data=X_train, current_data=X_test, column_mapping=column_mapping)
report.save_html(str(ROOT / "drift_report.html"))
print(f"Reporte guardado en {ROOT / 'drift_report.html'}")
def resumen_deriva(reporte):
"""Extrae del reporte de Evidently el resultado global y el de cada columna."""
metricas = reporte.as_dict()["metrics"]
global_ = metricas[0]["result"]
por_columna = pd.DataFrame([{
"variable": col,
"tipo": r["column_type"],
"prueba": r["stattest_name"],
"p_value": r["drift_score"],
"¿deriva?": "Sí" if r["drift_detected"] else "No",
} for col, r in metricas[1]["result"]["drift_by_columns"].items()]).set_index("variable").sort_values("p_value")
print(f"Columnas con deriva: {global_['number_of_drifted_columns']} de {global_['number_of_columns']} "
f"({global_['share_of_drifted_columns']:.0%}) · ¿deriva en el dataset?: "
f"{'Sí' if global_['dataset_drift'] else 'No'}")
return por_columna
deriva_test = resumen_deriva(report)
display(deriva_test.style.format({"p_value": "{:.4f}"}))Reporte guardado en E:\Documents\MachineLearningUN\heart-disease-mlops\drift_report.html
Columnas con deriva: 1 de 12 (8%) · ¿deriva en el dataset?: No
Evidently no encuentra deriva en el dataset: solo 1 de las 12 columnas (8 %) sale con deriva, lejos del umbral del 50 %. Es lo esperado, porque train y test vienen del mismo split aleatorio. La predicción del modelo tampoco cambia (p = 0.41).
La única columna marcada es cholesterol (p = 0.017). Test tiene 43 ceros de 184 pacientes (23 %) y train 129 de 734 (18 %), y la prueba de Kolmogorov-Smirnov es muy sensible a esa diferencia en la cantidad de ceros. Es muy probablemente un falso positivo. Con 12 pruebas al 5 % se espera, por puro azar, alrededor de 0.6 columnas marcadas aunque no haya ningún cambio real. Por eso una sola columna con deriva no debe alarmar, pero sí conviene vigilarla en los siguientes lotes.
El reporte completo, con los histogramas de cada variable, está en drift_report.html, en la raíz del proyecto. Se abre con cualquier navegador.
Parte 4. Escenario con deriva simulada
Para comprobar que el monitoreo funciona, se crea un lote de 184 pacientes que representa un cambio realista: el modelo empieza a usarse en un hospital con una población distinta. Se parte de los pacientes de test y se modifican así:
Los pacientes tienen en promedio 8 años más.
En la mitad de los pacientes no se registra el colesterol (queda en 0), como pasaba en algunos de los hospitales originales.
Hay más pacientes con dolor de pecho asintomático (ASY).
El resto de variables no se toca. Este lote no tiene diagnóstico real, igual que los datos de producción.
rng = np.random.default_rng(SEED)
simulado = test.drop(columns=TARGET).copy()
simulado["Age"] = (simulado["Age"] + rng.normal(8, 3, len(simulado))).round().clip(28, 90).astype(int)
sin_colesterol = rng.random(len(simulado)) < 0.5
simulado.loc[sin_colesterol, "Cholesterol"] = 0
cambiar_dolor = rng.random(len(simulado)) < 0.4
simulado.loc[cambiar_dolor, "ChestPainType"] = "ASY"
simulado["prediction"] = modelo.predict_proba(simulado.drop(columns="prediction", errors="ignore"))[:, 1]
report_sim = Report(metrics=[DataDriftPreset()])
report_sim.run(reference_data=X_train, current_data=simulado, column_mapping=column_mapping)
report_sim.save_html(str(ROOT / "drift_report_simulado.html"))
print(f"Reporte guardado en {ROOT / 'drift_report_simulado.html'}")
deriva_sim = resumen_deriva(report_sim)
display(deriva_sim.style.format({"p_value": "{:.4f}"}))
print("Probabilidad media predicha:")
print(f" train : {X_train['prediction'].mean():.3f}")
print(f" test : {X_test['prediction'].mean():.3f}")
print(f" simulado : {simulado['prediction'].mean():.3f}")Reporte guardado en E:\Documents\MachineLearningUN\heart-disease-mlops\drift_report_simulado.html
Columnas con deriva: 4 de 12 (33%) · ¿deriva en el dataset?: No
Probabilidad media predicha:
train : 0.552
test : 0.548
simulado : 0.637
Evidently detecta el cambio. Marca con deriva exactamente las tres variables modificadas: age, cholesterol y chestpaintype, las tres con p < 0.001. También la marca en la predicción del modelo, cuya probabilidad media sube de 0.55 a 0.64. Las otras 8 variables, que no se tocaron, conservan los mismos p-values del escenario de test y siguen sin deriva.
Aunque 4 columnas tienen deriva, el reporte dice que no hay deriva en el dataset, porque 4 de 12 (33 %) no llega al umbral por defecto del 50 %. En un modelo clínico ese umbral es demasiado permisivo. Aquí el cambio en la población hace que el modelo prediga enfermedad con más frecuencia, y eso ya justifica revisarlo. En producción conviene alertar por columna, sobre todo en las variables más importantes y en la predicción, y no solo por el resultado global.
Este reporte está en drift_report_simulado.html.
Parte 5. Comparación de los dos escenarios
comparacion = pd.DataFrame({
"p_value_test": deriva_test["p_value"],
"deriva_test": deriva_test["¿deriva?"],
"p_value_simulado": deriva_sim["p_value"],
"deriva_simulado": deriva_sim["¿deriva?"],
}).sort_values("p_value_simulado")
display(comparacion.style.format({"p_value_test": "{:.4f}", "p_value_simulado": "{:.4f}"}))Conclusiones de la Etapa 6¶
El reporte del enunciado (drift_report.html) compara train con test y no encuentra deriva en el dataset: solo cholesterol sale marcada (p = 0.017), muy probablemente por azar, dado que se hacen 12 pruebas.
En el escenario simulado, Evidently detecta las tres variables alteradas y el cambio en las predicciones del modelo, lo que confirma que el monitoreo funciona.
El umbral global por defecto (50 % de las columnas) no marcó el escenario simulado como deriva. Para este problema es mejor vigilar columna por columna, en especial st_slope, chestpaintype y cholesterol, que son las variables más importantes del modelo (Parte 10 del notebook 2), y la distribución de las predicciones.
En un despliegue real, este análisis se correría periódicamente con los datos que recibe la API, por ejemplo cada semana. Si aparece deriva, el siguiente paso es conseguir diagnósticos reales de esos pacientes, medir si el modelo perdió precisión y reentrenarlo si hace falta. Con el Pipeline y el CI ya montados, reentrenar y volver a desplegar es repetir el notebook 2 y hacer push.