Saltar al contenido

lección 7

Alertas y monitorización: enterarte ANTES de que el CEO pregunte

Qué monitorizar (frescura, volumen, distribuciones), cómo alertar (Slack, email), umbrales estáticos y dinámicos.

50 min

### El pipeline perfecto que nadie vigila

Tienes validaciones. Tienes contratos. Tienes Great Expectations ejecutándose cada día. Genial. Pero... ¿quién VE los resultados? Si tu suite de GE falla a las 3 de la mañana un sábado y el resultado queda en un log que nadie lee hasta el lunes, ¿de qué sirve? De nada. Una validación sin alerta es como una alarma de incendios sin sirena — detecta el fuego pero nadie se entera.

La monitorización de datos es el último eslabón de la cadena de calidad. Completa el ciclo: defines expectativas (contrato) → verificas que se cumplen (validación) → alertas cuando NO se cumplen (monitorización). Sin los tres eslabones, la cadena se rompe.

Analogía: imagina un hospital. Los pacientes están monitorizados con máquinas que miden pulso, presión, oxígeno. Si algo se sale del rango normal, la máquina PITA. No espera a que una enfermera pase por ahí y mire la pantalla — avisa activamente. Tus datos necesitan lo mismo: monitorización activa con alertas inmediatas.

### ¿Qué monitorizar? Las 4 señales vitales de los datos

  1. 01.FRESCURA: ¿los datos están actualizados? Si tu pipeline debería ejecutarse cada hora, verifica que realmente lo hizo. Si los datos tienen más de X horas, alerta.
  2. 02.VOLUMEN: ¿llegó la cantidad esperada de datos? Si normalmente recibes 10.000 pedidos diarios y hoy llegaron 200, algo está mal. Si llegaron 500.000, también.
  3. 03.DISTRIBUCIÓN: ¿los valores se comportan "normalmente"? Si el importe medio siempre es ~50€ y hoy es 5.000€, hay un problema. Si el % de nulos sube del 1% habitual al 30%, hay un problema.
  4. 04.SCHEMA: ¿la estructura cambió? Si apareció una columna nueva o desapareció una existente, alerta (posible breaking change del upstream).
Una alerta sin contexto es ruido. Una alerta con contexto es acción.

### Umbrales estáticos vs dinámicos

Un umbral estático es simple: "si hay más del 5% de nulos, alerta". Funciona, pero tiene un problema: no se adapta. Si un dataset normalmente tiene 0.1% de nulos y sube a 3%, el umbral estático no se dispara (está por debajo del 5%), pero claramente algo cambió. Los umbrales dinámicos se basan en el HISTORIAL: "si el % de nulos es 3 desviaciones estándar mayor que la media de los últimos 30 días, alerta".

1import pandas as pd
2import numpy as np
3from datetime import datetime, timedelta
4
5class MonitorDatos:
6 """Monitor de calidad con umbrales estáticos y dinámicos."""
7
8 def __init__(self, nombre_dataset: str):
9 self.nombre = nombre_dataset
10 self.historial: list[dict] = []
11
12 def registrar_metricas(self, metricas: dict):
13 """Registra las métricas del día (simula historial)."""
14 metricas['timestamp'] = datetime.now()
15 self.historial.append(metricas)
16
17 def check_estatico(self, metrica: str, valor: float, umbral: float) -> dict:
18 """Check simple: ¿el valor supera el umbral fijo?"""
19 dispara = valor > umbral
20 return {
21 'tipo': 'estático',
22 'metrica': metrica,
23 'valor': valor,
24 'umbral': umbral,
25 'alerta': dispara,
26 'mensaje': f"{metrica} = {valor:.2f} ({'🔴 > ' if dispara else '✅ <= '}{umbral})",
27 }
28
29 def check_dinamico(self, metrica: str, valor_actual: float,
30 historial_valores: list[float], sigma: float = 3.0) -> dict:
31 """Check dinámico: ¿el valor se desvía del comportamiento histórico?"""
32 if len(historial_valores) < 7:
33 return {'tipo': 'dinámico', 'metrica': metrica, 'alerta': False,
34 'mensaje': 'Historial insuficiente (< 7 días)'}
35
36 media = np.mean(historial_valores)
37 std = np.std(historial_valores)
38
39 if std == 0:
40 limite_superior = media * 1.5
41 limite_inferior = media * 0.5
42 else:
43 limite_superior = media + sigma * std
44 limite_inferior = media - sigma * std
45
46 fuera = valor_actual > limite_superior or valor_actual < limite_inferior
47
48 return {
49 'tipo': 'dinámico',
50 'metrica': metrica,
51 'valor': valor_actual,
52 'media_historica': round(media, 2),
53 'rango_normal': f"[{limite_inferior:.1f}, {limite_superior:.1f}]",
54 'alerta': fuera,
55 'mensaje': f"{metrica} = {valor_actual:.1f} vs media {media:.1f} "
56 f"{'🔴 ANOMALÍA' if fuera else '✅ Normal'}",
57 }
58
59# Ejemplo: monitorizar volumen diario
60monitor = MonitorDatos("pedidos_diarios")
61
62# Historial de los últimos 14 días (volumen normal: ~10000 ± 1000)
63historial_volumen = [9800, 10200, 10100, 9900, 10500, 10300, 9700,
64 10100, 10400, 9600, 10200, 10000, 10300, 9800]
65
66# Hoy: solo llegaron 3200 pedidos (¡anomalía!)
67volumen_hoy = 3200
68
69# Check estático (umbral fijo: mínimo 5000)
70resultado_estatico = monitor.check_estatico("volumen", volumen_hoy, 5000)
71print(f"Estático: {resultado_estatico['mensaje']}")
72
73# Check dinámico (basado en historial)
74resultado_dinamico = monitor.check_dinamico("volumen", volumen_hoy, historial_volumen)
75print(f"Dinámico: {resultado_dinamico['mensaje']}")
76print(f" Rango normal: {resultado_dinamico.get('rango_normal', 'N/A')}")

El umbral dinámico detecta la anomalía aunque el estático no se dispare — se adapta al comportamiento real

### Implementar alertas: Slack, email y más

En producción real, las alertas se envían a canales donde el equipo las vea inmediatamente. Los más comunes son: Slack (un canal #data-alerts), email (al equipo on-call), PagerDuty (para incidentes críticos a las 3 AM). Aquí vamos a implementar un sistema de alertas SIMULADO que muestra la arquitectura correcta — en producción solo cambiarías el "enviar" por la integración real.

1from dataclasses import dataclass
2from datetime import datetime
3from enum import Enum
4
5class Severidad(Enum):
6 INFO = "info"
7 WARNING = "warning"
8 CRITICAL = "critical"
9
10@dataclass
11class Alerta:
12 """Una alerta de calidad de datos."""
13 dataset: str
14 metrica: str
15 mensaje: str
16 severidad: Severidad
17 valor_actual: float
18 valor_esperado: float
19 timestamp: datetime = None
20
21 def __post_init__(self):
22 if self.timestamp is None:
23 self.timestamp = datetime.now()
24
25class SistemaAlertas:
26 """Sistema de alertas multi-canal."""
27
28 def __init__(self):
29 self.alertas_enviadas: list[Alerta] = []
30
31 def enviar(self, alerta: Alerta):
32 """Enruta la alerta al canal apropiado según severidad."""
33 self.alertas_enviadas.append(alerta)
34
35 if alerta.severidad == Severidad.CRITICAL:
36 self._enviar_slack(alerta)
37 self._enviar_email(alerta)
38 self._enviar_pagerduty(alerta)
39 elif alerta.severidad == Severidad.WARNING:
40 self._enviar_slack(alerta)
41 else:
42 self._log(alerta)
43
44 def _enviar_slack(self, alerta: Alerta):
45 """Simula envío a Slack (en producción: webhook o SDK)."""
46 emoji = "🔴" if alerta.severidad == Severidad.CRITICAL else "⚠️"
47 print(f"[SLACK #data-alerts] {emoji} {alerta.dataset}: {alerta.mensaje}")
48 print(f" Valor: {alerta.valor_actual} (esperado: {alerta.valor_esperado})")
49 # En producción:
50 # requests.post(SLACK_WEBHOOK_URL, json={"text": mensaje})
51
52 def _enviar_email(self, alerta: Alerta):
53 """Simula envío de email (en producción: SMTP o SES)."""
54 print(f"[EMAIL → data-oncall@empresa.com] CRÍTICO: {alerta.dataset}")
55 print(f" {alerta.mensaje}")
56 # En producción:
57 # import smtplib / boto3.client('ses').send_email(...)
58
59 def _enviar_pagerduty(self, alerta: Alerta):
60 """Simula envío a PagerDuty (en producción: API)."""
61 print(f"[PAGERDUTY] 📟 Incidente creado: {alerta.dataset}{alerta.metrica}")
62 # En producción:
63 # requests.post("https://events.pagerduty.com/v2/enqueue", json=payload)
64
65 def _log(self, alerta: Alerta):
66 """Solo log para alertas informativas."""
67 print(f"[LOG] ℹ️ {alerta.dataset}: {alerta.mensaje}")
68
69# Uso
70alertas = SistemaAlertas()
71
72# Alerta crítica: pipeline no se ejecutó
73alertas.enviar(Alerta(
74 dataset="pedidos_diarios",
75 metrica="frescura",
76 mensaje="Datos no actualizados en 6 horas (SLA: 1h)",
77 severidad=Severidad.CRITICAL,
78 valor_actual=6.0,
79 valor_esperado=1.0,
80))
81
82print()
83
84# Alerta warning: volumen bajo
85alertas.enviar(Alerta(
86 dataset="pedidos_diarios",
87 metrica="volumen",
88 mensaje="Volumen 40% menor que la media histórica",
89 severidad=Severidad.WARNING,
90 valor_actual=6000,
91 valor_esperado=10000,
92))

La severidad determina el canal: INFO → log, WARNING → Slack, CRITICAL → Slack + email + PagerDuty

Consejo de senior sobre alertas: la regla de oro es "si una alerta no requiere ACCIÓN, no debería ser una alerta". Las alertas informativas que nadie mira generan "fatiga de alertas" — el equipo deja de prestarles atención y cuando llega una real, la ignoran. Menos alertas, más accionables = mejor.

Error fatal que he visto múltiples veces: enviar TODAS las alertas al mismo canal con la misma severidad. Si el canal de Slack tiene 50 mensajes diarios de "todo OK" mezclados con 1 mensaje de "pipeline roto", nadie va a ver el importante. Separa los canales por severidad y SÍ SOLO envía al canal lo que requiere atención.

### Anatomía de una buena alerta

Una alerta sin contexto es inútil. "Pipeline falló" no le dice nada al ingeniero que la recibe a las 3 AM. Una buena alerta incluye:

  1. 01.QUÉ falló: nombre del dataset y métrica específica
  2. 02.CUÁNDO: timestamp exacto del fallo
  3. 03.CUÁNTO se desvía: valor actual vs esperado (no solo "está mal" sino "es un 40% menor")
  4. 04.DESDE CUÁNDO: ¿es nuevo o lleva horas/días?
  5. 05.IMPACTO: ¿a quién afecta? (dashboard X, informe Y, equipo Z)
  6. 06.RUNBOOK: link a documentación de cómo investigar/resolver este tipo de fallo

Lo que le diría a mi yo de hace 5 años: crea un runbook para CADA tipo de alerta. Un runbook es un documento que dice: "si recibes esta alerta, haz estos pasos: 1) verifica X, 2) mira el log Y, 3) si es Z reinicia el pipeline, 4) si no se resuelve escala a fulano". A las 3 AM no quieres estar pensando desde cero.

Con validaciones, contratos y alertas tienes el stack completo de calidad de datos. En la próxima (y última) lección lo vamos a juntar TODO en un proyecto real: un pipeline completo con validación, alertas y reporte de calidad.

## ejercicios

[01]

Construir un monitor de anomalías de volumen

Tu pipeline ingiere datos de 5 tablas. Cada tabla tiene un volumen "normal" (media ± desviación). Construye un monitor que detecte anomalías comparando el volumen de hoy con el historial y genere alertas con la severidad correcta.

💡 Resultado esperado

=== MONITOR DE VOLUMEN — 2024-01-16 08:00 ===

Tabla           Hoy        Media      Desv%      Z-Score    Estado
----------------------------------------------------------------------
Cargando editor...
[02]

Sistema de alertas con routing por severidad

Implementa un sistema de alertas que envíe las notificaciones al canal correcto según la severidad: INFO → solo log, WARNING → Slack, CRITICAL → Slack + email + crear ticket. Incluye rate-limiting para no spamear.

💡 Resultado esperado

=== PROCESANDO ALERTAS DEL DÍA ===

  💬 [SLACK] pedidos/frescura: Datos no actualizados (2h)
  📧 [EMAIL] pedidos/frescura: Datos no actualizados (2h)
Cargando editor...
[03]

Generar un reporte de calidad para stakeholders

El equipo de BI te pide un reporte diario de "salud de los datos" que puedan entender personas no técnicas. Genera un reporte en texto formateado con semáforos, tendencias y recomendaciones.

💡 Resultado esperado

╔════════════════════════════════════════════════════════╗
║              📊 REPORTE DIARIO DE CALIDAD               ║
║                    2024-01-16 08:00                    ║
╚════════════════════════════════════════════════════════╝
Cargando editor...

Regístrate para guardar tu progreso.

## comentarios

Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.

Inicia sesión para comentar y responder.

cargando comentarios...