MLE Workflow · Producción

Churn Prediction — LeadFlow SaaS

Sistema de ML en producción: contrato de datos, pipeline reproducible, puertas de calidad, artefacto deployable y monitorización
ClienteLeadFlow SaaS B2B
Tipo modeloXGBoost Classifier
ServingBatch diario 06:00 UTC
MRR en riesgo~45k€/mes
Ventana predicción30 días
Skillmle-workflow v1.0
Churn actual
5.8%
mensual · benchmark sector 3.5%
▼ objetivo 4.2%
MRR recuperable
45k€
estimado/mes si churn → 4.2%
▲ ROI 6 meses
Cuentas en scope
3.4k
activas · top 15% → 510 alertas/día
▲ accionable por CS
SLA batch
< 3m
CPU · XGBoost · sin GPU
● SHAP explicable
1
Iteration Compact
GoalPredecir en 30 días si una cuenta va a cancelar; activar retención proactiva por CS antes de la cancelación
Who caresVP Customer Success (alerta), CFO (MRR), equipo CS (lista accionable), Product (señales de engagement)
Decision ownerHead of CS — decide si el modelo promueve a producción y si activa campañas
Acción del sistemaScore diario de riesgo por cuenta → top 15% entra en cola CS → outreach automatizado + llamada manual
Success metricRecall@15% ≥ 0.65 (capturar ≥ 65% de futuros churners en el top 15% de score)
Guardrail metricsPrecision ≥ 0.35 (evitar fatiga CS), AUC ≥ 0.80, latencia batch < 3 min, calibration error < 0.06
Mistake budgetFP tolerables: CS contacta cuenta sana (costo: 10 min CS). FN alto costo: cuenta se va sin intervención (costo: MRR × LTV)
Errores inaceptablesScore alto a cuenta que acaba de renovar (genera desconfianza). Score sin explicación (CS no puede actuar)
SupuestosSeñales de producto preceden al churn ≥ 14 días. CS puede actuar en cuentas Growth/Pro; Starter solo email
RestriccionesSin GPU en prod. Sin PII en modelo. Explicabilidad SHAP obligatoria. Rollback < 15 min
Labels y snapshotcancelled_at en subscriptions. Label: cancelación en t+30d. Snapshot: últimos 18 meses. Umbral label: conf ≥ 1.0 (no hay ambigüedad)
BaselineRegla heurística actual: sin login en 15 días → alerta. Recall@15%=0.31, Precision=0.22
Señales candidatasFrecuencia login 7/14/30d, emails enviados, secuencias activas, integraciones, tickets abiertos, MRR, días desde último evento
Plan thresholdOptimizar threshold sobre val set para Recall@15%. Revisión mensual en slice por plan
Eval slicesPlan (Starter/Growth/Pro), company_size (1-10/11-50/50+), country (ES/MX/otros), cohort mes de alta
Riesgos conocidosLeakage si se incluyen eventos post-fecha de referencia. Label delay: cancelaciones tardan 3-7d en confirmarse
Próximo experimentoAñadir señal de NPS/satisfaction_score de tickets → hipótesis: satisfacción baja precede al churn 21d
Rollback/fallbackSi modelo falla: volver a artefacto v-1 (almacenado en S3/GCS con config + preprocessing). Fallback manual: heurística 15-días sin login. Switch < 15 min vía config flag
2
Contrato de Datos
Esquema de entidad y label
CampoTipoDescripciónNotas
account_idUUIDPK — entidad de predicciónNo PII
reference_dateDATEFecha de referencia del score= fecha batch run
label_churn_30dBOOL¿Canceló en t+30d?Delay: 3-7 días conf
label_available_atDATEFecha en que label es confiablereference_date + 37d
splitENUMtrain/val/test/backtesttemporal, no random
Política de splits (temporal, sin leakage)
SplitPeríodoCuentasChurn %
Trainene 2024 – jun 2025~9.800 snapshots5.6%
Validationjul – sep 2025~2.1005.9%
Testoct – dic 2025~2.4006.1%
Backtestene – mar 2024~8004.8%
Regla anti-leakage: Todos los features deben ser calculados con datos estrictamente anteriores a reference_date. Joins con events usan WHERE timestamp < reference_date. Tickets usan WHERE opened_at < reference_date.
3
Features — Hipótesis de Separación
Familias de señales y teoría de separación
FeatureFamiliaHipótesis de separaciónRiesgo leakagePrioridad
logins_last_7d Engagement Caída de logins precede abandono 14-21d. Usuarios sin sesión en 7d tienen 3× más churn. Bajo ★★★
emails_sent_l30d_trend Uso core Tendencia decreciente de emails enviados refleja abandono del workflow principal. Bajo ★★★
sequences_active Producto 0 secuencias activas = no hay outreach en curso = valor percibido bajo. Bajo ★★★
integrations_connected Stickiness Más integraciones = mayor switching cost. 0 integraciones → churn 2.8× mayor. Bajo ★★★
days_since_last_event Recencia Silencio prolongado es la señal más directa de abandono inminente. Bajo ★★★
open_tickets_last_30d Fricción Acumulación de tickets no resueltos correlaciona con frustración y churn. Bajo ★★
avg_ticket_satisfaction NPS proxy Score < 3/5 en últimos tickets es indicador leading de cancelación intención. Medio ★★
account_age_days Ciclo vida Nuevas cuentas (<60d) tienen churn por mal onboarding; cuentas >12m son más estables. Bajo ★★
plan_encoded Plan Starter tiene mayor churn por menor compromiso. Pro tiene churn bajo pero alto impacto MRR. Bajo
contacts_imported_l90d Activación Importación de contactos = setup activo. Cero en 90d = posible abandono de setup. Bajo
avg_ticket_satisfaction — verificar que satisfaction_score no se carga retroactivamente. Si el score se asigna después de la resolución, y la resolución puede ser posterior a reference_date, excluir o usar solo tickets resueltos antes de reference_date.
4
Pipeline Reproducible
Paso 1
Data Extraction
SQL point-in-time join con WHERE timestamp < reference_date. Versiona el snapshot como churn_features_{date}.parquet.
idempotente
Paso 2
Feature Transform
Pipeline scikit-learn: imputer (median), scaler (StandardScaler), OHE plan. Guardado con joblib junto al modelo.
train=serve
Paso 3
Train XGBoost
Config dataclass frozen. seed=42. early_stopping=50. scale_pos_weight para imbalance. Log a MLflow/CSV.
reproducible
Paso 4
Eval + Gates
assert_promotion_ready() sobre val set. Slice checks por plan. SHAP feature importance. Revisión humana top-FP.
fail-closed
Paso 5
Package Artifact
Bundle: model.joblib + preprocessor.joblib + config.json + schema.json + artifact_meta.json. Upload a S3.
versionado
Paso 6
Batch Scoring
Cron 06:00 UTC. Carga artifact versionado. Score 3.4k cuentas en <3 min. Top 15% → tabla churn_alerts. SHAP values en JSON.
SLA <3 min
# training_config.py — LeadFlow Churn Prediction from dataclasses import dataclass from pathlib import Path import hashlib @dataclass(frozen=True) class ChurnTrainingConfig: dataset_uri: str = "s3://leadflow-ml/snapshots/churn_features_2025-12-31.parquet" model_dir: Path = Path("artifacts/churn_v2") seed: int = 42 learning_rate: float = 0.05 n_estimators: int = 800 max_depth: int = 5 subsample: float = 0.8 scale_pos_weight: float = 15.8 # ~94/6 class balance label_col: str = "label_churn_30d" prediction_horizon_days: int = 30 PROMOTION_GATES = { "auc_roc": ("min", 0.80), "recall_at_15pct": ("min", 0.65), "precision_at_15pct": ("min", 0.35), "calibration_error": ("max", 0.06), "batch_latency_sec": ("max", 180), # 3 min } def artifact_name(cfg: ChurnTrainingConfig, code_sha: str) -> str: key = f"{cfg.dataset_uri}:{cfg.seed}:{cfg.learning_rate}:{cfg.n_estimators}" cfg_hash = hashlib.sha256(key.encode()).hexdigest()[:12] return f"churn-{code_sha[:12]}-{cfg_hash}"
5
Puertas de Promoción — Resultados Val Set
AUC-ROC
Mínimo: 0.800.847
✓ PASA · +0.047 sobre gate
Recall@15%
Mínimo: 0.650.71
✓ PASA · captura 71% churners
Precision@15%
Mínimo: 0.350.41
✓ PASA · tolerable para CS
Calibration Error
Máximo: 0.060.058
⚠ MARGINAL · monitorizar post-deploy
Batch Latency
Máximo: 180s47s
✓ PASA · 3.4k cuentas en 47s
Veredicto Final
PROMOVER
5/5 gates pasadas · calibración monitorizar
Baseline: Recall@15%=0.31 → 0.71 (+129%)
Métricas por slice — Val Set (oct-dic 2025)
SliceN cuentasChurn %AUCRecall@15%Precision@15%Estado
Starter1.2808.2%0.8310.680.39✓ OK
Growth8704.8%0.8620.740.44✓ OK
Pro2502.9%0.8910.720.51✓ OK
Empresa 1-101.1506.8%0.8390.690.38✓ OK
Empresa 11-509405.1%0.8510.720.43✓ OK
Cuenta < 60d18014.4%0.7940.610.32⚠ Bajo umbral
Cuentas nuevas (<60d): Recall 0.61 bajo gate (0.65). Causa probable: pocas features de engagement disponibles. Acción: añadir onboarding_score como feature; mantener heurística fallback para <30d hasta que el modelo mejore este slice.
6
Explicabilidad — SHAP Feature Importance
Top 8 features globales (mean |SHAP|)
days_since_last_event 0.31
sequences_active 0.24
logins_last_7d 0.19
integrations_connected 0.17
emails_sent_l30d_trend 0.13
open_tickets_last_30d 0.09
account_age_days 0.07
plan_encoded 0.04
Ejemplo de score explicado (cuenta ID: acc_7f2a9b)
Score de riesgo 0.87
Plan: Growth · 14 meses activa · ES · 12 empleados
Razones (SHAP locales)
days_since_last_event +0.28 — Sin actividad 23 días
sequences_active +0.21 — 0 secuencias activas
open_tickets_last_30d +0.18 — 3 tickets abiertos sin resolver
integrations_connected -0.08 — 2 integraciones activas (protege)
account_age_days -0.06 — 14 meses = no es nuevo
Mensaje para CS: Cuenta inactiva 23d con tickets sin resolver. Llamar esta semana antes del día 30. Prioridad: resolver tickets + reactivar secuencia.
7
Artefacto de Serving — Esquema Bundle
Contenido del bundle (S3: leadflow-ml/artifacts/)
ArchivoDescripción
model.joblibXGBoost pipeline serializado (preprocessor + classifier)
config.jsonTrainingConfig completo · hiperparámetros · rutas
schema_input.jsonJSON Schema de validación de entrada (tipos, rangos, nulls)
schema_output.jsonEnvelope de respuesta: account_id, score, shap_values, model_version
artifact_meta.jsoncode_sha, config_hash, dataset_uri, metrics_val, created_at, promoted_by
promotion_report.jsonTodos los gate results + slice metrics + human reviewer
shap_background.npyDataset de background para SHAP TreeExplainer (1000 muestras)
Validaciones del serving path
Schema validation: Rechaza features con tipos incorrectos, rangos imposibles (ej. logins < 0) o columnas faltantes obligatorias
Stale feature check: Si days_since_last_event > 90 días, score se emite con flag stale_features=true
Batch timeout: Límite 180s con timeout explícito; si se supera → fallo controlado + alert
Fallback: Si artifact no carga → usar artefacto anterior (config flag CHURN_MODEL_VERSION)
Prediction log: Solo account_id, score, model_version, shap_top3, timestamp. Sin PII.
Model version en output: Cada predicción lleva model_version para joins con labels futuros
Empty batch guard: Si no hay cuentas activas → log warning, no escribir tabla, no silenciar
8
Plan de Monitorización
Señales de sistema y calidad
🔴
Batch job success/failure
Alert si falla el cron. Slack + PagerDuty. SLA: detectado <5 min
cron 06:00
📊
Distribución de scores
P10/P50/P90 del score diario. Alert si media se desvía >15% vs rolling 7d
diario
⚗️
Feature drift (PSI)
Population Stability Index para top 5 features. Alert si PSI > 0.25
diario
🏷️
Label arrival health
% de cancelaciones confirmadas en t+37d. Alert si cobertura <80%
semanal
📈
Delayed quality metrics
Recall@15% y Precision@15% con labels reales (37d delay). Trigger retrain si Recall cae <0.58
mensual
Criterios de retrain y rollback
TriggerTipoAcción
Recall@15% < 0.58 (delayed)CalidadRetrain inmediato
PSI > 0.25 en top featureDriftAnálisis + retrain 7d
Batch falla 2× consecutivoSistemaRollback a v-1
Score dist media +15%DriftInspección + alerta CS
Labels delay > 14dDataCongelar métricas + aviso
Retrain programadoRutinaCada 6 semanas
Rollback en < 15 min: CHURN_MODEL_VERSION=churn-prev en env → reiniciar worker. Artefacto anterior siempre disponible en S3. No requiere reentrenamiento.
9
Observation Ledger — Iteración v2.0
Iteración
v2.0 — XGBoost con SHAP + 10 features de engagement
Cambio
Pasar de heurística 15d-sin-login a modelo XGBoost con features de uso, engagement, soporte y stickiness
Por qué importó
Baseline heurístico capturaba solo 31% de churners. Dejaba escapar cuentas con login pero sin uso real del producto.
Movimiento métrico
AUC: baseline n/a → 0.847. Recall@15%: 0.31 → 0.71 (+129%). Precision@15%: 0.22 → 0.41 (+86%)
Movimiento por slice
Mejor en todos los slices excepto cuentas <60d (Recall 0.61, bajo gate). Starter mejoró más en términos absolutos.
Falsos positivos notables
Cuentas en pausa activa (vacaciones equipo). Feature nueva propuesta: flag account_paused en subscriptions
Falsos negativos notables
Cuentas nuevas (<60d) que churnan rápido por mal onboarding. Modelo no tiene suficiente historia de uso.
Decisión
PROMOVER con monitorización reforzada en calibración y slice <60d. Añadir fallback heurístico para cuentas <30d.
Tradeoff aceptado
Precision 0.41 significa que ~6 de cada 10 cuentas en la lista CS no churnarán. Aceptado: coste de FP < coste de FN.
Lección capturada
sequences_active es la segunda feature más importante pero no estaba en el baseline. Explorar producto antes de modelar.
Regresión añadida
Test: score de cuenta que no se ha logueado en 23 días debe ser > 0.70. Test: batch completo en < 60 segundos.
Deuda creada
Slice <60d sin solución. avg_ticket_satisfaction sin verificar timing. Canary rollout pendiente (actualmente hard switch).
Próxima iteración
v2.1: añadir onboarding_completion_pct + avg_ticket_satisfaction (verificado). Resolver slice <60d. Canary 20% tráfico.
10
Anti-Patrones Detectados y Mitigados
🚫
Leakage en splits: El pipeline original usaba random split. Corregido a split temporal (train pre-jul 2025, val jul-sep, test oct-dic).
🚫
Estado de notebook: El modelo v1 requería ejecutar celdas en orden específico. Ahora: script Python puro, config frozen dataclass, reproducible con python train.py.
🚫
Preprocessing duplicado: Transformaciones de features en notebook de entrenamiento y en función de serving. Ahora: mismo preprocessor.joblib para train y batch.
🚫
Monitorización solo uptime: El sistema anterior solo alertaba si el cron fallaba. Ahora: drift features, distribución scores, delayed recall mensual.
🚫
Rollback requería retrain: Si algo fallaba había que reentrenar. Ahora: artefacto anterior en S3, rollback en <15 min vía env var.
🚫
Umbral tuneado en test set: Threshold original elegido viendo el test set. Ahora: threshold se optimiza sobre val set, test set es caja negra hasta el final.
11
Checklist de Revisión Final
Contrato de predicción explícito y testeable (target, owner, latencia, fallback)
Contrato de datos: entity grain, label timing, feature timing, snapshot versión
Riesgos de leakage verificados contra disponibilidad en prediction time
Entrenamiento reproducible desde código, config, versión de datos y seed
Métricas comparadas contra baseline y modelo de producción actual
Slice metrics e indicadores de guardrail para cohorts de alto riesgo
Puertas de promoción automatizadas y fail-closed
Transformaciones de train y serving compartidas (mismo joblib)
Artefacto lleva versión, config, referencia a datos y preprocessing
Serving path valida inputs, timeout, fallback y rollback
Monitorización cubre salud de sistema, drift de features, drift de predicciones y labels retrasados
⚠️
Datos sensibles excluidos de artefactos y logs — avg_ticket_satisfaction: verificar timing pendiente