train_test_split(random_state=42),
que mezcla datos de distintos periodos. El modelo de churn predice en el futuro;
si el conjunto de test contiene clientes cuya actividad futura ya está en el conjunto de train,
el AUC-ROC reportado (0.847) está inflado y no refleja el rendimiento real en producción.
BLOCK — No desplegar a producción
3 riesgos críticos deben resolverse antes de cualquier merge
Riesgos primarios:
⚠ Fuga de datos (data leakage) ⚠ Serving inseguro (pickle sin validación) ⚠ Entrenamiento irreproducible ↑ Transformaciones desincronizadas train/serve ~ Monitorización incompleta
⚠ Fuga de datos (data leakage) ⚠ Serving inseguro (pickle sin validación) ⚠ Entrenamiento irreproducible ↑ Transformaciones desincronizadas train/serve ~ Monitorización incompleta
Crítico
● Crítico
Split aleatorio en datos temporales — fuga de datos garantizada
training/train.py : 112
Issue
El PR reemplaza el split temporal por
# ❌ INCORRECTO — mezcla periodos temporales
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# ✅ CORRECTO — split temporal basado en fecha de corte
cutoff = pd.Timestamp('2026-03-01')
train_mask = features_df['cohort_date'] < cutoff
X_train, y_train = X[train_mask], y[train_mask]
X_test, y_test = X[~train_mask], y[~train_mask]
Fix
Restaurar split temporal o por entidad (customer_id). Re-evaluar AUC-ROC y F1
con el split correcto antes de cualquier comparación con el modelo en producción.
● Crítico
Deserialización pickle sin validación de integridad — RCE en serving
serving/predict.py : 8–10
Issue
El serving carga el artefacto con
pickle.load()
sin verificar hash, firma digital ni procedencia del fichero. Un artefacto comprometido
en el bucket S3 ejecutaría código arbitrario al arrancar el servidor. Además, el PR guarda
el modelo en models/ con ruta hardcoded y sin versión explícita en el nombre.
# ❌ Sin verificación de integridad
with open('models/churn_model_v2.1.pkl', 'rb') as f:
model = pickle.load(f) # RCE si artefacto comprometido
# ✅ Migrar a joblib con hash SHA-256 pre-carga
import hashlib, joblib
EXPECTED_SHA256 = "abc123..." # del artifact registry
artifact_path = os.environ["MODEL_ARTIFACT_PATH"] # sin hardcode
with open(artifact_path, 'rb') as f:
data = f.read()
assert hashlib.sha256(data).hexdigest() == EXPECTED_SHA256, "Artifact tampered"
model = joblib.load(artifact_path)
Fix
Migrar a
joblib o MLflow model registry.
Verificar SHA-256 del artefacto antes de cargarlo. Parametrizar la ruta por variable de entorno.
Añadir model_version en el output de predicción.
● Crítico
Transformaciones train/serve desincronizadas — predicciones silenciosamente incorrectas
serving/predict.py : 14–21 vs training/feature_engineering.py : 23–35
Issue
El serving reimplementa las features manualmente en vez de importar
feature_engineering.build_features().
Las features RFM (recency_score, frequency_score, monetary_score)
no se calculan en serving; en su lugar hay un df.get('rfm_score', 0) que cae silenciosamente
a 0 para todos los clientes. El modelo recibe vectores de features distintos a los de entrenamiento.
# ❌ En serving — rfm_score siempre 0, feature drift silencioso
df['rfm_score'] = df.get('rfm_score', 0)
# ✅ Importar el mismo módulo de features
from training.feature_engineering import build_features
def predict_churn(customer_data: dict) -> dict:
df = pd.DataFrame([customer_data])
df = build_features(df) # misma transformación que en train
score = model.predict_proba(df[FEATURE_COLS])[:, 1][0]
return {"churn_probability": score, "model_version": MODEL_VERSION}
Fix
Unificar transformaciones en un módulo compartido
shared/feature_engineering.py
importado tanto en training como en serving. Añadir test de equivalencia
assert build_features(sample) == serve_features(sample) en CI.
Alto
● Alto
Threshold 0.38 tuneado en notebook local — promoción sin gate reproducible
notebooks/threshold_analysis.ipynb / training/train.py : 132
Issue
El umbral de decisión se eligió manualmente en un notebook ejecutado en local,
sin dataset versionado ni artefacto de resultado. La justificación del PR es "ver notebook".
El notebook no está en CI y puede tener estado oculto. Si el notebook se re-ejecuta
sobre datos distintos, el threshold podría cambiar sin dejar rastro.
Fix
Convertir el análisis de threshold en un script Python con dataset versionado y output
guardado como artefacto (threshold.json). El threshold debe declararse en config
(
config/model.yaml) con comentario de tradeoff F1/recall, no como constante mágica.
● Alto
Evaluación sin slice metrics — posible regresión en segmentos clave no detectada
training/train.py : 135–142
Issue
El PR solo reporta AUC-ROC y F1 globales. En churn B2B, los segmentos de alto valor
(enterprise vs SMB), antigüedad de cliente y verticales pueden comportarse de forma
muy diferente. Una mejora global puede enmascarar regresión en clientes enterprise
(alto LTV, alto coste de falso positivo).
Fix
Añadir evaluación por segmento:
customer_tier (enterprise/mid/smb),
industry_vertical, y clientes con antigüedad >12 meses.
Documentar gates de promoción: si AUC enterprise cae >2pp respecto a producción, BLOCK.
Medio
● Medio
Monitoring limitado a uptime — sin drift de features ni calidad de predicciones
monitoring/alerts.yaml
Issue
El fichero de alertas solo monitoriza uptime del servicio. No hay alertas para:
(1) drift de distribución en features de entrada, (2) proporción de predicciones
positivas (prediction drift), (3) latencia de inferencia por percentil,
(4) llegada tardía de etiquetas reales para evaluación online.
Fix
Añadir alertas: PSI >0.2 en features RFM, tasa de churn predicha fuera de rango [0.05, 0.35],
p99 latencia >200ms, ausencia de etiquetas reales en ventana de 7 días.
● Medio
3 errores mypy silenciados con # type: ignore sin justificación
training/train.py (múltiples líneas)
Issue
Los
# type: ignore no documentados ocultan posibles errores de tipo
en el pipeline de features. En producción, errores de tipo silenciados pueden
causar comportamiento inesperado sin fallo explícito (ej. float vs int en scores RFM).
Fix
Añadir comentario explicativo a cada
# type: ignore o resolver los errores.
Habilitar --strict en mypy para módulos críticos de ML.
Bajo
● Bajo
model_version ausente en el output de predicción
serving/predict.py : 24
Issue
El endpoint devuelve
{"churn_probability": ..., "churn_flag": ...}
sin incluir la versión del modelo. Esto impide unir predicciones pasadas con el
modelo que las generó cuando se haga rollback o comparación A/B.
Fix
Añadir
"model_version": MODEL_VERSION al response.
Leer MODEL_VERSION de variable de entorno o de metadata del artefacto.