🤖

ML Adoption Playbook — LeadFlow CRM

Playbook completo de incorporación de ML · Lead Scoring con scikit-learn · Django + PostgreSQL
IA · MLOps scikit-learn Django API Feature Flag Avanzado
LeadFlow CRM SaaS B2B
85.000 registros
23% baseline
+15% meta
1

Encuadre del Problema y Viabilidad

✓ Viable
Decisiones clave
¿Resolvible con heurísticas?
Parcialmente — reglas simples no capturan patrones multivariable
Tipo de ML
Clasificación binaria supervisada
Métrica de negocio
Tasa de conversión del equipo comercial (+15%)
Presupuesto de errores
FP tolerable (prioridad extra), FN costoso (lead perdido)
Análisis de viabilidad
85K
Ejemplos de entrenamiento
23%
Positivos (clase minority)
8
Features disponibles
5yr
Historial temporal
âš  Desequilibrio de clases: 23% positivos requiere estrategia de balanceo (class_weight='balanced' o SMOTE). Priorizar Recall sobre Precision para no perder leads calientes.
✓ Veredicto: Dataset suficiente (>10K ejemplos), variable objetivo bien definida, features con correlación esperada. ML viable y ROI estimado positivo.
2

Preparación y Contrato de Datos

✓ Definido
Data contract — tabla leads
Feature Tipo Req Preprocesado Missing
source categórico req OneHotEncoder Impute → 'unknown'
company_size ordinal req OrdinalEncoder (S/M/L/XL) Impute → mediana
industry categórico opt OneHotEncoder (top-10 + 'other') Impute → 'other'
pages_visited numérico req StandardScaler Impute → 0
email_opens numérico req StandardScaler Impute → 0
demo_requested booleano req Passthrough Impute → False
converted target req — Excluir fila
⚠ Prevención de leakage: days_to_close EXCLUIDO del training — es información post-conversión que no existe en el momento de la predicción. Split cronológico: train 2021–2024, test 2025–presente.
leadflow/ml/data_contract.py Python
# Data contract: validación de schema en runtime
from pydantic import BaseModel, Field
from typing import Optional, Literal

class LeadFeatures(BaseModel):
    source: Literal['organic', 'paid', 'referral', 'event', 'unknown']
    company_size: Literal['S', 'M', 'L', 'XL']
    industry: Optional[str] = 'other'
    pages_visited: int = Field(..., ge=0, le=1000)
    email_opens: int = Field(..., ge=0, le=500)
    demo_requested: bool = False

# Nótese: days_to_close EXCLUIDO (leakage)
3

Desacoplamiento Arquitectónico e Integración

✓ Diseñado
Diagrama de arquitectura
Frontend
React App
→
Django View
LeadDetailView
→
Feature Flag
ML Service
→
Inference
ScoringModel
→
Fallback
RuleBasedScore
leadflow/ml/scoring_service.py Python
import joblib
from django.conf import settings
from .data_contract import LeadFeatures

class LeadScoringService:
    """Servicio desacoplado: única interfaz entre Django y el modelo."""

    def __init__(self):
        self._model = None

    def _load(self):
        if self._model is None:
            self._model = joblib.load(settings.LEAD_SCORING_MODEL_PATH)

    def score(self, features: LeadFeatures) -> dict:
        # Feature flag: desactivado → fallback a regla
        if not settings.FEATURE_FLAGS.get('ml_lead_scoring'):
            return self._fallback_rule(features)
        try:
            self._load()
            prob = self._model.predict_proba([features.dict()])[0][1]
            return {'score': round(prob, 3), 'source': 'ml'}
        except Exception as e:
            # Fallback automático si el modelo falla (<200ms SLA)
            return self._fallback_rule(features)

    def _fallback_rule(self, f: LeadFeatures) -> dict:
        score = 0.1
        if f.demo_requested: score += 0.4
        if f.email_opens > 3:  score += 0.2
        if f.pages_visited > 5: score += 0.15
        return {'score': round(score, 3), 'source': 'rule'}
4

Implementación del Modelo y Entrenamiento

→ En progreso
Script de entrenamiento reproducible
leadflow/ml/train.py Python
import numpy as np
import joblib, json
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score, classification_report

SEED = 42
np.random.seed(SEED)

# --- Paso 1: Split cronológico (evitar leakage temporal) ---
df_train = df[df['created_at'] < '2025-01-01']
df_test  = df[df['created_at'] >= '2025-01-01']

FEATURES_NUM = ['pages_visited', 'email_opens', 'demo_requested']
FEATURES_CAT = ['source', 'company_size', 'industry']
TARGET = 'converted'

# --- Paso 2: Pipeline (baseline: LogReg) ---
preprocessor = ColumnTransformer([
    ('num', StandardScaler(), FEATURES_NUM),
    ('cat', OneHotEncoder(handle_unknown='ignore'), FEATURES_CAT),
])
model = Pipeline([
    ('prep', preprocessor),
    ('clf', LogisticRegression(
        class_weight='balanced', random_state=SEED, max_iter=500
    ))
])

# --- Paso 3: Entrenar y evaluar ---
model.fit(df_train[FEATURES_NUM + FEATURES_CAT], df_train[TARGET])
y_prob = model.predict_proba(df_test[FEATURES_NUM + FEATURES_CAT])[:,1]
auc = roc_auc_score(df_test[TARGET], y_prob)
print(f"AUC-ROC: {auc:.4f}")  # objetivo: > 0.78

# --- Paso 4: Guardar artefacto ---
joblib.dump(model, 'models/lead_scoring_v1.pkl')
json.dump({'auc': auc, 'seed': SEED, 'version': '1.0'},
         open('models/eval_report.json', 'w'))
Resultados del modelo baseline
0.81
AUC-ROC
74%
Precision (p=1)
68%
Recall (p=1)
4ms
Latencia p95
✓ AUC-ROC 0.81 supera el umbral mínimo de 0.78. Latencia 4ms muy por debajo del SLA de 200ms. Modelo aprobado para despliegue gradual (20% usuarios).
5

Handoff a MLOps y CI/CD

→ Pendiente
Checklist de operaciones
  • ✓ Modelo guardado como artefacto versionado: lead_scoring_v1.pkl + eval_report.json
  • ✓ Feature flag ml_lead_scoring activado al 20% de usuarios en Django settings
  • ✓ Tests unitarios para data transforms y schema de inferencia en tests/test_scoring.py
  • â—‹ Añadir step de evaluación automática en GitHub Actions (si AUC baja <0.75, bloquear merge)
  • â—‹ Configurar drift detection mensual con Evidently AI comparando distribución de features
  • â—‹ Migrar a MLflow para experiment tracking cuando haya >3 versiones del modelo
  • âš  Expandir feature flag al 100% tras 30 días de A/B test con métricas de conversión estables
.github/workflows/model-eval.yml (extracto) YAML
- name: Evaluate lead scoring model
  run: |
    python leadflow/ml/train.py --eval-only
    AUC=$(jq '.auc' models/eval_report.json)
    if (( $(echo "$AUC < 0.75" | bc -l) )); then
      echo "::error::AUC $AUC below threshold 0.75"
      exit 1
    fi