Python & Data Science
MLOps En revisión

Monitoreo del rendimiento de modelos en producción (sin herramientas sofisticadas)

El problema del ‘fallo silencioso’

La última vez, Dev notó que la temporada de compras navideñas había tomado por sorpresa a su recomendador. La prueba KS de sus características de usuario se disparó, la puntuación de deriva superó su umbral de 0.1 y llegó la alerta de Slack. Construyó una clase DriftMonitor que comparaba las distribuciones de características de producción contra los datos de entrenamiento, aprendió la diferencia entre la deriva de datos (desplazamiento de P(X)P(X)) y la deriva de concepto (desplazamiento de P(YX)P(Y|X)), y añadió la distancia de Wasserstein y el PSI a su conjunto de herramientas. Pero esas alertas llegaron la mañana después de que la deriva ya hubiera comenzado a costarle conversiones. Había encontrado la brecha: detectar la deriva en un proceso por lotes sobre los datos de ayer no es lo mismo que saber si el modelo está funcionando correctamente ahora mismo.

Supongamos que el recomendador de Dev ha estado en producción durante tres meses. El endpoint de la API se ejecuta sin errores. Sin alertas, sin caídas. Todo parece estar bien.

Excepto que el recomendador ha estado devolviendo silenciosamente el mismo artículo predeterminado —un par de calcetines de algodón genéricos— para cada uno de sus diez millones de usuarios durante las últimas seis semanas. Las predicciones se muestran seguras y consistentes. Y le están costando a la empresa miles de dólares al día en conversiones perdidas.

El software falla de forma ruidosa. Lanza un error, la aplicación se cae y alguien recibe un mensaje de alerta a las 2 a.m. Los modelos fallan de manera diferente. Te dan respuestas incorrectas con total confianza, y no te das cuenta de que algo anda mal hasta que los ingresos caen o un cliente se queja.

Una brújula rota no emite pitidos. Simplemente te señala en la dirección equivocada mientras caminas con confianza fuera de tu rumbo.

Así se ve esto en código:

import numpy as np
from datetime import datetime, timedelta

# Simulate a model that's broken but doesn't know it
def broken_model_prediction(user_data):
    # This model always predicts 0 (no purchase)
    # It never throws an error. It just... fails.
    return 0

# Simulate a week of predictions
today = datetime.now()
predictions = []

for day in range(7):
    current_date = today - timedelta(days=day)
    # Imagine 1000 users per day
    for user_id in range(1000):
        pred = broken_model_prediction({'user_id': user_id, 'date': current_date})
        predictions.append({
            'date': current_date,
            'user_id': user_id,
            'prediction': pred,
            'confidence': 0.95  # The model is very confident it's right
        })

# Check what happened
unique_predictions = set([p['prediction'] for p in predictions])
print(f"Unique predictions made: {unique_predictions}")
print(f"Total predictions: {len(predictions)}")
print(f"Percentage predicting 'no purchase': {sum(1 for p in predictions if p['prediction'] == 0) / len(predictions) * 100:.1f}%")

Esto simula el tipo de fallo silencioso que podría ocurrirle al recomendador de Dev: un modelo que nunca lanza un error, nunca se cae, pero que colapsó silenciosamente para devolver siempre la misma predicción inútil para cada usuario.

  • import numpy as np — NumPy para operaciones numéricas. En el recomendador real de Dev, las predicciones provendrían del endpoint de inferencia de su modelo, no de una función sintética, pero la lógica de monitoreo es la misma.
  • from datetime import datetime, timedelta — importa utilidades de fecha y hora para simular una semana de tráfico de producción.
  • def broken_model_prediction(user_data): — un sustituto del recomendador de Dev que ha fallado de manera silenciosa. En el mundo real, esto podría ser un modelo cuyos pesos se corrompieron, un pipeline de características que empezó a enviar ceros, o un contenedor de servicio que recurrió a una respuesta de respaldo por defecto: devolviendo el mismo artículo para cada usuario.
  • return 0 — siempre devuelve 0. En el contexto del recomendador, esto equivale a devolver siempre el mismo artículo predeterminado sin importar quién esté navegando: cada usuario recibe la misma recomendación inútil, igual que en el escenario de “calcetines de algodón para todos”.
  • today = datetime.now() / predictions = [] — establece la fecha de inicio de la simulación y una lista vacía para recolectar los registros de predicciones.
  • for day in range(7): — simula 7 días de tráfico de producción, iterando hacia atrás desde hoy.
  • for user_id in range(1000): — simula 1,000 usuarios por día. El recomendador real de Dev atiende a diez millones, pero el patrón es el mismo, solo que con más predicciones incorrectas idénticas.
  • pred = broken_model_prediction({'user_id': user_id, 'date': current_date}) — llama al modelo roto para cada usuario. En producción, esta sería la llamada de inferencia real del modelo.
  • predictions.append({...}) — registra cada predicción con su fecha, ID de usuario, valor de predicción y una puntuación de confianza de 0.95. La confianza es clave: el modelo está muy seguro de que es correcto, aunque devuelva el mismo valor para todos.
  • 'confidence': 0.95 — el modelo reporta 95% de confianza. Esto es lo que hace que los fallos silenciosos sean tan peligrosos: el modelo no sabe que está roto. No está lanzando un error ni devolviendo una puntuación de baja confianza. Simplemente se equivoca con total confianza.
  • unique_predictions = set([p['prediction'] for p in predictions]) — recolecta el conjunto de valores de predicción únicos en todas las 7,000 predicciones. Si el modelo está sano, este conjunto debería contener múltiples valores. Si está colapsado, contendrá solo uno: {0}.
  • print(f"Unique predictions made: {unique_predictions}") — imprime el conjunto único. Para el modelo roto, esto muestra {0}: una sola predicción única en 7,000 solicitudes.
  • print(f"Total predictions: {len(predictions)}") — imprime 7,000 (7 días × 1,000 usuarios).
  • print(f"Percentage predicting 'no purchase': ...") — calcula qué porcentaje de predicciones son 0. Para el modelo roto, esto es 100.0%: absolutamente todas las predicciones son iguales.

Al ejecutar esto, el modelo hace 7,000 predicciones: todas son ‘0’, con 95% de confianza. Sin errores. Sin advertencias. Simplemente incorrectas.

La pregunta no es si tu modelo se romperá. Se romperá. Entonces, ¿cómo lo sabrás?

El bucle de retroalimentación: por qué volamos a ciegas

En el entrenamiento, conoces las respuestas. Tienes un conjunto de datos con características y etiquetas. Puedes medir la exactitud, la precisión, el recall—todo eso. Estás trabajando con una hoja de calificación donde cada respuesta ya está evaluada.

En producción, estás volando a ciegas.

¿Por qué? Porque la respuesta real, lo que llamamos ‘ground truth’, a menudo tarda mucho tiempo en llegar. Esta es la parte más difícil del monitoreo de modelos, y no es un problema técnico. Es un problema de negocio.

Digamos que construyes un modelo para predecir si un cliente incumplirá un préstamo. Lo despliegas el lunes. El modelo hace predicciones para miles de personas. Pero no sabrás si esas predicciones fueron correctas hasta meses después, cuando el préstamo se pague o entre en incumplimiento. No puedes esperar seis meses para descubrir que tu modelo está roto.

Compáralo con un problema diferente: predecir si alguien hará clic en un anuncio. Despliegas el modelo. Alguien ve el anuncio. Hace clic o no lo hace. Sabes la respuesta en milisegundos. Retroalimentación inmediata.

Desde la perspectiva del monitoreo, estos dos problemas son completamente diferentes. Uno tiene un bucle de retroalimentación medido en segundos. El otro tiene un bucle de retroalimentación medido en meses. La velocidad de la retroalimentación determina qué tan rápido puedes detectar los problemas.

Cuando no tienes el ‘ground truth’, tienes que ser creativo. Buscas ‘proxies’: pistas que te indican que algo está mal sin tener que esperar por la respuesta verdadera. Si estás prediciendo incumplimientos de préstamos, podrías monitorear si el modelo predice lo mismo para clientes similares (consistencia). Si estás prediciendo la rotación de clientes (churn), podrías monitorear si los clientes que el modelo marcó como ‘bajo riesgo’ realmente se quedan (un proxy rápido de corrección).

Simulemos cómo se ve esto:

import pandas as pd
import numpy as np

# Simulate a production data stream
# Imagine we're predicting loan defaults
np.random.seed(42)

data = []
for i in range(1000):
    data.append({
        'customer_id': i,
        'prediction': np.random.choice([0, 1], p=[0.8, 0.2]),  # 80% predicted 'no default'
        'prediction_date': '2024-01-15',
        'actual_default': None  # We don't know yet—it's a loan that hasn't matured
    })

df = pd.DataFrame(data)

# Check the feedback loop
print(f"Total predictions made: {len(df)}")
print(f"Predictions with ground truth available: {df['actual_default'].notna().sum()}")
print(f"Percentage of predictions with known answers: {df['actual_default'].notna().sum() / len(df) * 100:.1f}%")
print(f"\nThis means we're flying blind on {df['actual_default'].isna().sum()} predictions.")

# Now let's use a proxy: consistency
# Group by prediction and count
print(f"\nProxy metric - Distribution of predictions:")
print(df['prediction'].value_counts())
print(f"\nThis tells us what the model is doing, even without ground truth.")

Esto simula el problema del bucle de retroalimentación que Dev enfrenta cada vez que su recomendador hace predicciones cuyos resultados tardan en medirse: ¿el usuario realmente hizo clic? ¿Se convirtió? ¿Volverá la próxima semana?

  • import pandas as pd / import numpy as np — pandas para operaciones de DataFrame, NumPy para muestreo aleatorio.
  • np.random.seed(42) — fija la semilla aleatoria para la reproducibilidad.
  • data = [] / for i in range(1000): — construye una lista de 1,000 registros de predicción, simulando un lote de predicciones de producción.
  • 'prediction': np.random.choice([0, 1], p=[0.8, 0.2]) — asigna aleatoriamente a cada predicción un 0 o 1 con una probabilidad de 80% de que sea 0. En el ejemplo de incumplimiento de préstamo, 0 significa “sin incumplimiento”; en el recomendador de Dev, esto podría ser “el usuario hará clic” frente a “el usuario no hará clic”.
  • 'actual_default': None — la etiqueta de ground truth es None porque el resultado aún no ha ocurrido. Este es el meollo del problema del bucle de retroalimentación: el modelo hizo una predicción, pero la respuesta no llegará durante semanas o meses.
  • df = pd.DataFrame(data) — convierte la lista de diccionarios en un DataFrame de pandas para un análisis más sencillo.
  • df['actual_default'].notna().sum() — cuenta cuántas predicciones tienen un ‘ground truth’ no nulo. En esta simulación, es 0 — cada etiqueta es None.
  • df['actual_default'].notna().sum() / len(df) * 100 — calcula el porcentaje de predicciones con respuestas conocidas. Aquí es 0.0%, lo que significa que el modelo está volando completamente a ciegas.
  • df['actual_default'].isna().sum() — cuenta cuántas predicciones tienen un ‘ground truth’ None (desconocido). Aquí es 1,000 — todas ellas.
  • print(df['prediction'].value_counts()) — la métrica proxy: en lugar de verificar la exactitud (que requiere el ‘ground truth’), Dev revisa la distribución de las predicciones. Si el modelo de repente empieza a predecir 1 (incumplimiento/clic/conversión) para el 99% de los usuarios cuando solía predecir 1 para solo el 20%, algo está mal, incluso sin conocer las respuestas verdaderas.

Cuando ejecutes esto, verás que tenemos 1,000 predicciones pero cero etiquetas de ‘ground truth’. Estamos 100% a ciegas. Pero aún podemos monitorear la distribución de las predicciones; ese es nuestro proxy. Si el modelo empieza de repente a predecir ‘1’ (incumplimiento) para el 99% de los clientes, esa es una señal de alarma, incluso sin conocer las respuestas verdaderas.

Deriva de datos: cuando el mundo cambia, pero tu modelo no

Digamos que administras una tienda de ropa. Entrenas un modelo para predecir qué talla de camisa comprará un cliente. Recopilas datos de junio a agosto: verano. Tus datos de entrenamiento están llenos de personas que compran ropa ligera y holgada. Así que el modelo aprende: la gente quiere camisas grandes y transpirables.

Luego lo despliegas en diciembre. Ahora los clientes están comprando abrigos pesados, suéteres ajustados y capas térmicas. El modelo todavía predice “grande y transpirable” porque es todo lo que sabe. El mundo ha cambiado. Los datos de entrada han cambiado.

Esto es deriva de datos. Tu modelo no está roto. El problema para el que fue entrenado ya no existe.

Un concepto relacionado es la deriva de concepto, donde la relación entre las entradas y las salidas cambia. Digamos que tu modelo de verano aprendió que las personas altas compran camisas grandes. En invierno, las personas altas compran chaquetas medianas porque se abrigan con capas. La relación cambió. Las reglas cambiaron.

Lo que esto significa: tu modelo está resolviendo el problema de ayer.

Visualicemos esto:

import matplotlib.pyplot as plt
import numpy as np

# Simulate training data (summer)
np.random.seed(42)
training_ages = np.random.normal(loc=35, scale=12, size=1000)  # Average age 35

# Simulate production data (winter, 6 months later)
# The customer base has shifted—younger people are buying more
production_ages = np.random.normal(loc=28, scale=10, size=1000)  # Average age 28

# Create a simple comparison
print("Training data (summer):")
print(f"  Average customer age: {training_ages.mean():.1f}")
print(f"  Age range: {training_ages.min():.1f} to {training_ages.max():.1f}")

print("\nProduction data (winter, 6 months later):")
print(f"  Average customer age: {production_ages.mean():.1f}")
print(f"  Age range: {production_ages.min():.1f} to {production_ages.max():.1f}")

print("\nThe customer base has shifted younger by 7 years on average.")
print("Your model learned to predict for 35-year-olds.")
print("Now it's predicting for 28-year-olds.")
print("That's data drift.")

# Visualize it
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))

ax1.hist(training_ages, bins=30, alpha=0.7, color='blue', edgecolor='black')
ax1.set_title('Training Data Distribution (Summer)')
ax1.set_xlabel('Customer Age')
ax1.set_ylabel('Count')
ax1.axvline(training_ages.mean(), color='blue', linestyle='--', linewidth=2, label=f'Mean: {training_ages.mean():.1f}')
ax1.legend()

ax2.hist(production_ages, bins=30, alpha=0.7, color='red', edgecolor='black')
ax2.set_title('Production Data Distribution (Winter)')
ax2.set_xlabel('Customer Age')
ax2.set_ylabel('Count')
ax2.axvline(production_ages.mean(), color='red', linestyle='--', linewidth=2, label=f'Mean: {production_ages.mean():.1f}')
ax2.legend()

plt.tight_layout()
plt.savefig('drift_example.png', dpi=100, bbox_inches='tight')
print("\nVisualization saved as 'drift_example.png'")

Esto visualiza el tipo de cambio en la distribución que Dev vio cuando la base de usuarios de su recomendador cambió entre temporadas — el mismo patrón que activó sus alertas de la prueba KS en el artículo anterior.

  • import matplotlib.pyplot as plt / import numpy as np — matplotlib para graficar, NumPy para generar datos sintéticos.
  • np.random.seed(42) — fija la semilla aleatoria para reproducibilidad.
  • training_ages = np.random.normal(loc=35, scale=12, size=1000) — genera 1,000 muestras de una distribución normal con media 35 y desviación estándar 12. Esto simula la distribución de edades de los datos de entrenamiento de Dev (compradores de verano).
  • production_ages = np.random.normal(loc=28, scale=10, size=1000) — genera 1,000 muestras con media 28 y desviación estándar 10. La media ha bajado 7 años — el tipo de cambio que Dev ve cuando una nueva demografía comienza a usar la plataforma.
  • training_ages.mean() / production_ages.mean() — imprime la media de cada distribución. La brecha de 7 años (35 frente a 28) es la señal visible de la deriva.
  • fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4)) — crea una figura con dos subgráficos uno al lado del otro para comparar visualmente las dos distribuciones.
  • ax1.hist(training_ages, bins=30, ...) / ax2.hist(production_ages, bins=30, ...) — grafica los histogramas de cada distribución con 30 contenedores. La brecha visual entre los dos histogramas es el aspecto que tiene la deriva.
  • ax1.axvline(training_ages.mean(), ...) / ax2.axvline(production_ages.mean(), ...) — dibuja una línea vertical discontinua en la media de cada distribución, haciendo que el cambio de 7 años sea visible de inmediato.
  • plt.savefig('drift_example.png', dpi=100, bbox_inches='tight') — guarda la figura en un archivo PNG. En el sistema de producción de Dev, este tipo de gráfico se generaría automáticamente y se publicaría en un canal de Slack o se guardaría en un bucket de S3 para el informe de monitoreo diario.

Cuando ejecutes esto, verás dos histogramas uno al lado del otro. Los datos de entrenamiento se centran alrededor de la edad 35. Los datos de producción se centran alrededor de la edad 28. Las distribuciones han cambiado. Eso es la deriva de datos en el mundo real.

La ‘prueba de humo’ para modelos: monitoreo de distribuciones

No necesitas un panel de control ni un doctorado en estadística para detectar la deriva. Compara dos distribuciones y hazte una pregunta sencilla: ¿son iguales?

La prueba de Kolmogorov-Smirnov, o prueba KS, está diseñada exactamente para esto. Piensa en ella como una puntuación de similitud entre 0 y 1. Cerca de 0, las distribuciones son básicamente las mismas. Cerca de 1, son muy diferentes.

La prueba también te da un valor p. Uno minúsculo, digamos 0.001, significa que casi no hay posibilidad de que las dos distribuciones coincidan por accidente: los datos han cambiado. Un valor p grande, como 0.5, sugiere que probablemente sean las mismas.

Apliquémoslo para monitorear nuestro modelo:

from scipy import stats
import numpy as np

# Training data (what we trained on)
training_ages = np.random.normal(loc=35, scale=12, size=1000)

# Production data (what we're seeing now)
production_ages = np.random.normal(loc=28, scale=10, size=1000)

# Run the KS test
ks_statistic, p_value = stats.ks_2samp(training_ages, production_ages)

print(f"KS Statistic: {ks_statistic:.3f}")
print(f"P-value: {p_value:.6f}")

# Interpret the results
if p_value < 0.05:
    print("\n⚠️  WARNING: Data has drifted!")
    print(f"The distributions are significantly different (p-value: {p_value:.6f}).")
    print(f"The KS statistic is {ks_statistic:.3f}, meaning the maximum difference between")
    print(f"the two distributions is about {ks_statistic*100:.1f}%.")
else:
    print("\n✓ OK: Distributions look similar.")
    print(f"No significant drift detected (p-value: {p_value:.6f}).")

Esta es la prueba KS que Dev usa para comprobar si los datos de producción de su recomendador han sufrido una deriva respecto a los datos de entrenamiento — la misma prueba que integró en su clase DriftMonitor en el artículo anterior, ahora aplicada como una revisión puntual.

  • from scipy import stats — importa el módulo de estadísticas de SciPy, que contiene la función ks_2samp.
  • training_ages = np.random.normal(loc=35, scale=12, size=1000) — datos sintéticos de entrenamiento: 1,000 muestras de una distribución normal con media 35.
  • production_ages = np.random.normal(loc=28, scale=10, size=1000) — datos sintéticos de producción: 1,000 muestras con media 28. El desplazamiento de la media de 7 unidades es la señal de deriva.
  • ks_statistic, p_value = stats.ks_2samp(training_ages, production_ages) — ejecuta la prueba de Kolmogorov-Smirnov para dos muestras. Devuelve el estadístico KS (la diferencia máxima entre las dos CDF empíricas, entre 0 y 1) y un valor p (la probabilidad de que dos muestras tan diferentes provengan de la misma distribución).
  • if p_value < 0.05: — el umbral de significancia estándar. Si el valor p es inferior a 0.05, la prueba declara una deriva. En el DriftMonitor de producción de Dev, él establece el umbral en el estadístico (> 0.1) en lugar del valor p, porque el valor p se vuelve hipersensible con tamaños de muestra grandes. Para esta revisión puntual y rápida, el umbral del valor p es suficiente.
  • print(f"The KS statistic is {ks_statistic:.3f}, meaning the maximum difference ...") — interpreta el estadístico como un porcentaje: un estadístico KS de 0.15 significa que las dos CDF difieren hasta en un 15% en su punto más amplio.

Ejecuta esto y obtendrás un estadístico KS de alrededor de 0.15 a 0.25, dependiendo de la aleatoriedad, más un valor p cercano a 0. Las distribuciones son diferentes. Tus datos han sufrido una deriva.

En la práctica, establecerías un umbral. Muchos equipos usan p < 0.05 como alarma; otros son más estrictos con p < 0.01. Puedes automatizar esta comprobación en cinco líneas de código.

Construyendo un ‘tablero casero’ en Python

Aquí está el detalle: no necesitas Datadog, Grafana o Azure para empezar a monitorear. Un bucle y un archivo CSV bastarán.

Construyamos algo real. La idea: cada hora (o día, dependiendo de tu volumen), ejecutar un lote de datos a través de tu modelo, calcular algunas métricas de salud y registrarlas en un archivo. Ábrelo en Excel y verifica si algo se ve mal.

import pandas as pd
import numpy as np
from scipy import stats
from datetime import datetime
import os

# Simulate training data (this is what we trained on)
training_data = pd.DataFrame({
    'age': np.random.normal(loc=35, scale=12, size=1000),
    'income': np.random.normal(loc=50000, scale=20000, size=1000)
})

# Function to generate a batch of production data
def get_production_batch():
    """Simulate getting a batch of new data from production."""
    return pd.DataFrame({
        'age': np.random.normal(loc=32, scale=11, size=500),  # Slightly different
        'income': np.random.normal(loc=48000, scale=19000, size=500)
    })

# Function to calculate health metrics
def calculate_health_metrics(training_df, production_df, batch_number):
    """Calculate drift metrics for a batch."""
    metrics = {'batch': batch_number, 'timestamp': datetime.now()}
    
    # Check each feature for drift
    for column in training_df.columns:
        ks_stat, p_value = stats.ks_2samp(training_df[column], production_df[column])
        metrics[f'{column}_ks_stat'] = ks_stat
        metrics[f'{column}_p_value'] = p_value
        metrics[f'{column}_drifted'] = 'YES' if p_value < 0.05 else 'NO'
    
    return metrics

# Main monitoring loop (simulate 5 batches)
health_log = []

for batch_num in range(1, 6):
    print(f"\n--- Processing Batch {batch_num} ---")
    
    # Get new production data
    prod_batch = get_production_batch()
    
    # Calculate metrics
    metrics = calculate_health_metrics(training_data, prod_batch, batch_num)
    health_log.append(metrics)
    
    # Print a human-readable health report
    print(f"Timestamp: {metrics['timestamp']}")
    print(f"Age drift: {metrics['age_drifted']} (KS: {metrics['age_ks_stat']:.3f})")
    print(f"Income drift: {metrics['income_drifted']} (KS: {metrics['income_ks_stat']:.3f})")
    
    if metrics['age_drifted'] == 'YES' or metrics['income_drifted'] == 'YES':
        print("⚠️  ALERT: Drift detected!")
    else:
        print("✓ All clear.")

# Save the log to a CSV
health_df = pd.DataFrame(health_log)
health_df.to_csv('model_health_log.csv', index=False)
print(f"\nHealth log saved to 'model_health_log.csv'")
print("\nHealth Summary:")
print(health_df[['batch', 'age_drifted', 'income_drifted']])

Este es el bucle de monitoreo que Dev construye para comprobar la salud de su recomendador de manera programada — una alternativa ligera, que no requiere infraestructura, a una plataforma completa de observabilidad.

  • import pandas as pd / import numpy as np / from scipy import stats / from datetime import datetime / import os — la pila completa de importaciones: pandas para DataFrames, NumPy para datos sintéticos, SciPy para la prueba KS, datetime para las marcas de tiempo, y os para operaciones de archivos.
  • training_data = pd.DataFrame({...}) — los datos de referencia (entrenamiento) con dos características: age (media 35) e income (media 50,000). En el pipeline real de Dev, este sería el snapshot de los datos de entrenamiento guardado junto con la versión del modelo en MLflow.
  • def get_production_batch(): — simula la obtención de un nuevo lote de datos de producción. En el sistema real de Dev, esto consultaría el feature store o los registros de producción para los inputs del recomendador de la última hora.
  • 'age': np.random.normal(loc=32, scale=11, size=500)age de producción con media 32 (ligeramente desplazada de la media de entrenamiento de 35). El desplazamiento es lo suficientemente pequeño como para que un solo lote no active una alarma, pero un drift consistente a través de múltiples lotes sí lo haría.
  • def calculate_health_metrics(training_df, production_df, batch_number): — la función principal de monitoreo. Toma la referencia de entrenamiento, un nuevo lote de producción y un número de lote, y devuelve un diccionario de métricas de salud.
  • metrics = {'batch': batch_number, 'timestamp': datetime.now()} — comienza a construir el diccionario de métricas con el número de lote y la marca de tiempo actual.
  • for column in training_df.columns: — itera sobre cada característica en los datos de entrenamiento. Para el recomendador de Dev, estas podrían ser session_duration, items_viewed, time_of_day, price_range, etc.
  • ks_stat, p_value = stats.ks_2samp(training_df[column], production_df[column]) — ejecuta la prueba KS en cada característica, comparando la distribución de entrenamiento con la distribución de producción.
  • metrics[f'{column}_drifted'] = 'YES' if p_value < 0.05 else 'NO' — marca la característica como con drift si el valor p está por debajo de 0.05. Nota: esto utiliza el umbral del valor p, mientras que el DriftMonitor de producción de Dev (del artículo anterior) utiliza como umbral el estadístico KS (> 0.1) para evitar el problema de sensibilidad de muestras grandes.
  • health_log = [] / for batch_num in range(1, 6): — simula 5 lotes de monitoreo. En producción, este bucle se ejecutaría una vez por hora o una vez por día.
  • health_df.to_csv('model_health_log.csv', index=False) — guarda el registro completo de salud en un archivo CSV. Este es el “tablero del pobre”: un CSV que Dev puede abrir en Excel o en un notebook para detectar tendencias. No es glamoroso, pero atrapa los fallos silenciosos.

Cuando ejecutes esto, obtendrás un archivo CSV con una fila por lote. Cada fila muestra si se detectó drift en cada característica. Ábrelo en Excel, ordénalo por la columna ‘drifted’ y detecta de un vistazo qué lotes tuvieron problemas.

Esto no es sofisticado. No es un tablero de Grafana. Pero funciona. Prográmalo para que corra cada noche, y por la mañana sabrás si algo se rompió.

¿Qué debería monitorear realmente Dev? — cuatro señales, cada una detectando un modo de falla diferente:

SeñalLo que detectaLo que pasa por altoCuándo usarla
Distribución de prediccionesColapso del modelo (siempre devuelve el mismo elemento), desequilibrio repentino de clases, anomalías en el rango de salidaDegradación silenciosa de la precisión donde la distribución parece bien pero las predicciones son incorrectasVerificación de primera línea — económico, no requiere ground truth, detecta la falla de “calcetines de algodón para todos”
Distribuciones de características de entrada (prueba KS)Deriva de datos — el mundo cambió y las entradas del modelo ya no coinciden con las del entrenamientoDeriva de concepto donde las entradas se ven igual pero la relación entrada→salida cambióVerificación de segunda línea — detecta el cambio estacional que Dev vio cuando la navegación de verano dio paso a las compras navideñas
Puntajes de confianzaPicos de incertidumbre del modelo (el modelo está viendo entradas que no reconoce)Predicciones incorrectas con exceso de confianza (el modelo se equivoca pero no lo sabe)Complemento a las verificaciones de distribución — una caída en la confianza promedio es un indicador adelantado de que el modelo se encuentra con datos desconocidos
KPIs de negocio (tasa de conversión, CTR, ingresos)El impacto final de cualquier problema del modelo — si el KPI cae, algo está malIdentificación de la causa raíz — una caída del KPI te dice que el modelo está perjudicando al negocio, no por quéLa red de seguridad definitiva — pero es un indicador retardado. Para cuando la conversión cae, el daño ya está ocurriendo

Construir tu propia solución vs. adoptar una herramienta dedicada de monitoreo de ML:

El enfoque de “dashboard del pobre” en este artículo — un bucle en Python, una prueba KS y un archivo CSV — es el punto de partida correcto. Es gratuito, es transparente y obliga a Dev a entender lo que significa cada métrica. Pero tiene límites:

EnfoqueProsContrasCuándo evolucionar
Construir tu propia solución (script de Python + CSV + cron)Gratuito, totalmente personalizable, sin dependencia de proveedor, obliga a entender las métricasSin alertas más allá de instrucciones de impresión, sin visualización más allá de Excel, manual de extender, difícil de compartir con stakeholders no técnicosEtapa actual de Dev — un modelo, un equipo, haciendo bien lo básico
Dashboard ligero (Grafana + Prometheus, Metabase, Streamlit)Dashboards visuales, alertas, compartible con stakeholders, sigue siendo autoalojadoSigue requiriendo construir el pipeline de datos; sin funciones específicas de ML (sin detección automática de drift, sin comparación de modelos)Cuando Dev necesita mostrar el monitoreo a gerentes de producto o ejecutivos
Monitoreo de ML dedicado (Evidently, Arize, Fiddler, WhyLabs)Detección de drift preconstruida, dashboards de rendimiento del modelo, verificaciones de calidad de datos, alertas automáticas, se integra con registros de modelosCosto (a menudo precios por modelo o por volumen de datos), dependencia de proveedor, curva de aprendizaje, puede abstraer el razonamiento que Dev necesita entenderCuando Dev tiene 3+ modelos en producción, un equipo de 5+ ingenieros y el costo de fallas silenciosas supera el costo de las herramientas

Regla general para el recomendador de Dev: Comienza con el dashboard del pobre (el enfoque de este artículo). Añade Grafana para visualización cuando los stakeholders lo pidan. Recurre a una herramienta dedicada de monitoreo de ML (Evidently para open-source, Arize o WhyLabs para versiones administradas) cuando el número de características monitoreadas × modelos × entornos exceda lo que un script de Python puede manejar razonablemente — o cuando una segunda falla silenciosa se escape por las grietas del enfoque basado en CSV.

Qué hacer cuando suena la alarma

Así que tu script de monitoreo acaba de marcar deriva. La alarma está sonando. ¿Y ahora qué?

No entres en pánico. Aquí tienes una guía de tres pasos:

Paso 1: ¿Es un error de datos o un cambio en el mundo?

Primero, revisa el pipeline. ¿Alguien cambió el código de recolección de datos o se rompió un sensor? Tal vez el esquema de la base de datos cambió. Revisa los logs. Habla con el equipo de ingeniería de datos. La mitad de las veces, lo que parece deriva es en realidad un error del pipeline — no un cambio real en el mundo.

Paso 2: El reentrenamiento no siempre es la respuesta, pero es lo primero que debes intentar.

Si el mundo realmente cambió (no es un error), reentrena tu modelo con datos recientes. A menudo, esta es la decisión correcta. Pero ten cuidado: si reentrenas de manera demasiado agresiva, podrías sobreajustar al ruido. Yo optaría por reentrenar cuando veas una deriva constante en múltiples lotes, no después de una sola alarma.

Paso 3: El ‘Humano en el Bucle’ — cuándo pedirle a una persona que revise las etiquetas.

Si no estás seguro de lo que está pasando, pídele a alguien que revise manualmente una muestra de predicciones. Haz que etiqueten 100 ejemplos aleatorios del lote de producción. Calcula la precisión en esos 100. Si la precisión sigue siendo alta, la deriva podría ser inofensiva. Si disminuyó, tienes un problema real.

Así es como se ve eso en código:

import pandas as pd
import numpy as np
from scipy import stats

# Simulate a scenario where drift is detected
training_data = pd.DataFrame({
    'feature_1': np.random.normal(loc=100, scale=15, size=1000)
})

production_data = pd.DataFrame({
    'feature_1': np.random.normal(loc=105, scale=15, size=1000)  # Shifted by 5 units
})

# Check for drift
ks_stat, p_value = stats.ks_2samp(training_data['feature_1'], production_data['feature_1'])

print("=== DRIFT DETECTION ALERT ===")
print(f"P-value: {p_value:.6f}")
print(f"KS Statistic: {ks_stat:.3f}")

if p_value < 0.05:
    print("\n⚠️  DRIFT DETECTED")
    
    # Step 1: Check for data bugs
    print("\nStep 1: Checking for data pipeline issues...")
    print(f"  - Training data mean: {training_data['feature_1'].mean():.2f}")
    print(f"  - Production data mean: {production_data['feature_1'].mean():.2f}")
    print(f"  - Difference: {abs(training_data['feature_1'].mean() - production_data['feature_1'].mean()):.2f}")
    print("  ✓ No obvious pipeline bug detected.")
    
    # Step 2: Decide on retraining
    print("\nStep 2: Considering retraining...")
    mean_shift = abs(training_data['feature_1'].mean() - production_data['feature_1'].mean())
    if mean_shift > 10:
        print(f"  ⚠️  Large shift detected ({mean_shift:.2f} units).")
        print("  Recommendation: Retrain the model on recent data.")
    else:
        print(f"  Small shift detected ({mean_shift:.2f} units).")
        print("  Recommendation: Monitor for 1-2 more batches before retraining.")
    
    # Step 3: Human in the loop
    print("\nStep 3: Manual validation...")
    print("  Recommendation: Have a human label 100 random predictions.")
    print("  If accuracy on those 100 is still >90%, the drift is probably harmless.")
    print("  If accuracy drops below 85%, retrain immediately.")

Este es el árbol de decisiones que Dev sigue cuando su sistema de monitoreo marca deriva en el sistema de recomendación: primero buscar errores, luego decidir si reentrenar, y luego decidir si pedirle a un humano que haga una verificación aleatoria.

  • training_data = pd.DataFrame({'feature_1': np.random.normal(loc=100, scale=15, size=1000)}) — datos de entrenamiento sintéticos para una característica con media 100 y desviación estándar 15.
  • production_data = pd.DataFrame({'feature_1': np.random.normal(loc=105, scale=15, size=1000)}) — datos de producción sintéticos con media 105. El cambio de 5 unidades es la señal de deriva.
  • ks_stat, p_value = stats.ks_2samp(training_data['feature_1'], production_data['feature_1']) — ejecuta la prueba KS para detectar el cambio. Con 1,000 muestras y un cambio de media de 5 unidades, el valor p será muy pequeño (muy por debajo de 0.05), lo que confirma la deriva.
  • if p_value < 0.05: — la alarma de deriva se dispara. Todo lo que está dentro de este bloque es la guía de respuesta.
  • print("Step 1: Checking for data pipeline issues...") — Paso 1: descartar un error de datos antes de tocar el modelo. Dev verifica si el pipeline de features está enviando los datos en las unidades correctas, el formato correcto y el rango correcto. Una confusión entre Celsius y Fahrenheit, un cambio de esquema o un sensor roto parecerían todos deriva.
  • print(f" - Training data mean: {training_data['feature_1'].mean():.2f}") / print(f" - Production data mean: {production_data['feature_1'].mean():.2f}") — imprime las medias de ambas distribuciones para una verificación rápida. Una diferencia de 5 unidades (100 vs 105) es visible pero no extrema — podría ser un cambio real en el mundo o un error sutil del pipeline.
  • mean_shift = abs(training_data['feature_1'].mean() - production_data['feature_1'].mean()) — calcula la diferencia absoluta de las medias. Esta es una heurística rápida para la magnitud del cambio, la cual informa la decisión de reentrenamiento.
  • if mean_shift > 10: — Paso 2: si el cambio es grande (> 10 unidades), reentrena inmediatamente. Si es pequeño, monitorea algunos lotes más antes de reentrenar — un solo cambio pequeño podría ser ruido.
  • print("Step 3: Manual validation...") — Paso 3: pídele a un humano que etiquete 100 predicciones aleatorias y revise la precisión. Si la precisión sigue siendo > 90%, la deriva probablemente sea inofensiva. Si cayó por debajo del 85%, reentrena inmediatamente. Esta es la red de seguridad del “humano en el bucle” para cuando las estadísticas son ambiguas.

Cuando ejecutes esto, verás un árbol de decisiones: primero buscar errores, luego decidir si reentrenar, y luego decidir si pedirle ayuda a un humano. Ese es el flujo de trabajo real que funciona en producción.

Conclusiones

Lo que has aprendido:

  1. Los modelos fallan silenciosamente. Respuestas incorrectas pero con seguridad. No lo sabrás a menos que lo monitorees.

  2. El retraso del ground truth es la parte más difícil. La respuesta verdadera puede tardar semanas o meses en llegar. Los proxies y el monitoreo de distribuciones llenan ese vacío.

  3. El data drift es real. El mundo cambia; tu modelo no. Detecta cuando las entradas se desplazan.

  4. No necesitas herramientas sofisticadas para empezar. Una prueba de KS, un bucle y un archivo CSV detectarán la mayoría de los problemas.

  5. Cuando suene la alarma, sigue el playbook. Primero revisa si hay errores, luego considera el reentrenamiento y después consulta a un humano.

El monitoreo no requiere un doctorado en DevOps. Requiere disciplina, algo de Python y el hábito de revisar tu modelo regularmente. Empieza de forma sencilla, empieza hoy. Detecta un problema antes de que le cueste dinero a la empresa, y tu yo del futuro te lo agradecerá.


Dev ahora tiene una capa de monitoreo que detecta fallos silenciosos casi en tiempo real. La prueba de KS se dispara cuando las distribuciones de entrada cambian, la verificación de la distribución de predicciones detecta el colapso del modelo, y el registro de salud expone los problemas antes de que se agraven. Pero aquí está lo que le inquieta: cada alerta que ha construido es reactiva. La alarma suena después de que el modelo ya ha empezado a degradarse, después de que las tasas de conversión han caído, después de que los usuarios ya han estado recibiendo malas recomendaciones. ¿Y si pudiera reentrenar el recomendador antes de que se dispare la alarma de drift, programando el reentrenamiento basándose en una señal que prediga la degradación en lugar de esperar a que ocurra? Esa es la próxima pregunta: ¿cuándo deberías reentrenar y cómo construyes un disparador que sea proactivo en lugar de reactivo?

Comprueba tu comprensión

Las preguntas a continuación avanzan desde el simple recuerdo hasta el diseño abierto, siguiendo aproximadamente la Taxonomía de Bloom.

Remember ¿Por qué el artículo dice que los modelos “fallan silenciosamente” de una manera en la que el software tradicional no lo hace?

Understand En tus propias palabras, explica la diferencia entre el ejemplo de incumplimiento de préstamos y el ejemplo de clics en anuncios en términos de la velocidad del “lazo de retroalimentación”, y por qué esa diferencia cambia cómo monitorearías cada uno.

Apply Usando la guía de tres pasos del artículo (verificación de errores → decisión de reentrenamiento → humano en el lazo), si una prueba KS señala deriva pero descubres que el pipeline de datos estaba enviando accidentalmente la temperatura en Celsius en lugar de Fahrenheit, ¿qué paso resuelve esto y requiere reentrenamiento?

Analyze El artículo distingue la Deriva de Datos (P(X)P(X) cambiando — las entradas se desplazan) de la Deriva de Concepto (la relación entre entradas y salidas cambia — “las personas altas compran chaquetas medianas porque se abrigan en capas”). Explica paso a paso por qué la prueba KS en las distribuciones de características (como age) detectaría la Deriva de Datos pero pasaría por completo por alto un escenario puro de Deriva de Concepto donde la distribución de edad se mantiene idéntica pero el comportamiento de compra de cada grupo de edad se invierte.

Evaluate La “métrica proxy” del artículo para el modelo de incumplimiento de préstamos monitorea la distribución de predicciones (p. ej., “si el modelo de repente comienza a predecir incumplimiento para el 99% de los clientes, es una señal de alerta”). Critica este proxy: describe un modo de falla donde el modelo ahora está silenciosa y gravemente equivocado, pero la distribución de predicciones parece completamente normal y no activaría esta verificación específica.

Create Diseña un plan de monitoreo para un nuevo escenario con un lazo de retroalimentación largo: un modelo que predice si un candidato a un empleo seguirá empleado en la empresa después de 1 año. Usando el conjunto de herramientas del artículo (prueba KS en características, métricas proxy, verificaciones puntuales con humano en el lazo), propón qué monitorearías semanalmente versus lo que solo podrías validar después de un año, y una métrica proxy que podría revelar un problema de forma temprana.


Artículos relacionados

Referencias y lecturas adicionales

  • Breck, E., Bai, Y., Gulcehre, C., & Sculley, D. (2017). The ML Test Score: A Rubric for ML Production Readiness and Technical Debt Reduction. SE4ML: Software Engineering for Machine Learning Workshop at ICSE. — el artículo seminal sobre la preparación para producción de ML, que incluye prácticas de monitoreo y pruebas para modelos desplegados. Los autores (en Google) introdujeron el concepto de “fallas silenciosas” en sistemas de ML y la rúbrica para evaluar si un modelo está verdaderamente listo para producción.
  • Documentación de SciPy ks_2samphttps://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.ks_2samp.html — referencia oficial para la prueba de Kolmogorov-Smirnov de dos muestras utilizada en el código de monitoreo de este artículo.

Esta traducción fue generada automáticamente y puede contener errores. Si el idioma inglés es tu preferencia, puedes leer el artículo original en inglés .

«Aplica lo que aprendiste» es para suscriptores Supporter e Insider.

Suscríbete para desbloquear los ejercicios de este artículo.

Ver planes

¿Buscas otra cosa?

Busca en todos los artículos por título, resumen o tema.