Ciencia de datos · Machine learning
El umbral importó más que el modelo: revisando un clasificador de marketing bancario.
En 2023, mi equipo participó en una competencia de Kaggle para predecir qué clientes de un banco canadiense responderían a una campaña de depósitos. Nuestro mejor envío obtuvo 0.63851 de F1. Tres años después volví al código, encontré siete errores, reconstruí el pipeline y probé nuevos modelos y ensambles. La mayor mejora no vino de un mejor modelo. Vino de un número que nadie había ajustado.
Nuestros modelos originales usaban el umbral de probabilidad por defecto, 0.5, pero la competencia se calificaba con F1. Bajar el umbral a cerca de 0.35 subió todos los modelos de cerca de 0.62 a cerca de 0.676 de F1 en validación cruzada. Corregir los errores, ajustar hacia árboles pequeños y regularizados y combinar modelos sumó una mejora menor. El mejor resultado local es ahora 0.6770 de F1, aún sin confirmar en Kaggle. El resultado más útil para el negocio es el ranking: el 10% de clientes mejor puntuados por el modelo respondió el 75% de las veces, contra el 50% al azar. Código: GitHub.
El problema
El banco "A" quería ofrecer a sus clientes tasas de interés promocionales para atraer depósitos. Contactar a todos los clientes es caro, así que la tarea era encontrar a los que tenían más probabilidad de responder. Los datos venían de un banco internacional real, anonimizados para proteger la privacidad: saldo de la tarjeta de crédito, productos, las últimas nueve transacciones, cuentas externas, indicadores de actividad y las tasas ofrecidas a cada cliente.
La competencia fue AML-1413 (invierno de 2023), una competencia de Kaggle para la clase, del 13 de marzo al 17 de abril de 2023. Participamos como Group Theta: Dev Makwana, Aparna Manjalavil, Yasar Mansuri, Amarnadh Movva y yo.
Lo que construimos en 2023
Exploramos los datos, creamos variables agregadas (total de productos, suma de transacciones, tasa competitiva promedio), añadimos variables de clústeres K-Means elegidos con el método del codo y aplicamos one-hot encoding a las columnas discretas. Probamos seis métodos para eliminar valores atípicos; todos bajaron el puntaje, así que conservamos todas las filas. Después ajustamos seis familias de modelos con búsqueda en malla y aleatoria sobre una división 75/25.
| Modelo (2023) | F1 público en Kaggle |
|---|---|
| GradientBoostingClassifier, búsqueda en malla | 0.63851 |
| CatBoostClassifier, búsqueda en malla | 0.62837 |
| RandomForestClassifier, búsqueda en malla | 0.61824 |
| LogisticRegression, búsqueda en malla | 0.60135 |
| DecisionTreeClassifier | 0.52702 |
Ganó el boosting, pero todos los modelos serios quedaron entre 0.60 y 0.64, aproximadamente. En ese momento concluimos que las variables importaban más que el algoritmo.
De vuelta al código: siete errores
Al leer el notebook de nuevo en 2026, encontré problemas que cambiaron cuánto confiaba en nuestros propios resultados:
- Clústeres que significaban cosas distintas. K-Means y el escalador se ajustaron por separado en los datos de entrenamiento y de prueba, así que el clúster 3 de prueba no era el mismo grupo que el clúster 3 de entrenamiento.
- Unas 600 columnas dispersas.
ActivityIndicator, un conteo con 611 valores distintos, se codificó con one-hot. Los valores de prueba que no aparecían en entrenamiento quedaban en cero sin aviso. - La métrica equivocada para ajustar. Los modelos se ajustaron para ROC AUC o accuracy y usaban un umbral de 0.5, mientras la competencia calificaba con F1.
- Una sola división con ruido. Cada puntaje venía de una sola división 75/25, y el modelo final solo vio el 75% de los datos.
- Gráficas que mostraban posiciones. La función de importancia de variables graficaba las posiciones de
argsort(), no las importancias. - Predicciones prestadas. Las métricas de una regresión logística se calcularon con las predicciones del gradient boosting.
- Una variable muerta.
PreviousCampaignResultsiempre vale 0 en los datos de prueba.
Ninguno de estos errores es raro en un proyecto de estudiantes con fecha límite. Varios, como ajustar una transformación por separado en entrenamiento y prueba, son igual de comunes en código de producción. Dejé el notebook original sin cambios, porque sus puntajes de Kaggle salieron de ese código exacto, y reconstruí el pipeline como un script aparte y reproducible.
La reconstrucción
La nueva versión ajusta todo lo que aprende de los datos, incluidos los clústeres K-Means, dentro de cada partición de la validación cruzada. Usa validación cruzada estratificada de 5 particiones, prueba ocho modelos (desde regresión logística hasta LightGBM, XGBoost y CatBoost), ajusta los modelos de boosting con Optuna y los combina de cuatro formas: promedio simple, promedio de rangos, mezcla ponderada y stacking. Lo más importante: elige el umbral que maximiza F1 sobre las predicciones fuera de partición.
Antes de añadir complejidad, medí cuánto ayudaba realmente cada idea. El AUC variaba cerca de ±0.003 entre particiones, así que cualquier diferencia menor era ruido:
| Experimento (LightGBM, 5 particiones) | ROC AUC | Veredicto |
|---|---|---|
| Solo columnas originales | 0.6725 | Línea base |
| + variables creadas | 0.6728 | Sin mejora real |
| + clústeres K-Means | 0.6722 | Sin mejora, más lento |
| Árboles pequeños, regularización fuerte | 0.6747 | Ayuda |
| Ajustado con Optuna (40 pruebas) | 0.6752 | Ayuda |
La ingeniería de variables de la que estaba orgulloso en 2023 casi no aportó a los modelos de boosting: la señal ya estaba en las columnas originales. Los clústeres sí ayudaron a la regresión logística (+0.005 de AUC), pero no a los árboles. Y una validación adversarial, un modelo que intenta distinguir filas de entrenamiento y de prueba, obtuvo 0.498 de AUC: los dos conjuntos se parecen, así que los puntajes de validación cruzada deberían trasladarse a Kaggle.
El hallazgo: ajustar primero el umbral
Esto es lo que se me escapó en 2023. Los datos de entrenamiento están balanceados, así que predecir que todos los clientes responden da una precisión de cerca de 0.50 y un recall de 1.0, es decir, un F1 de cerca de 0.667. Esa regla trivial supera nuestro mejor envío de 2023. Nuestros modelos no ordenaban mal a los clientes; el umbral de 0.5 los volvía demasiado cautelosos para una métrica que premia el recall.
F1 = 2 × precisión × recall / (precisión + recall)
"todos responden": precisión ≈ 0.50, recall = 1.0 → F1 ≈ 0.667
modelos de 2023: umbral 0.5 → F1 ≈ 0.62
mismos modelos: umbral ≈ 0.35 → F1 ≈ 0.676
| Modelo (2026, VC de 5 particiones) | ROC AUC | F1 con 0.5 | F1 con el mejor umbral |
|---|---|---|---|
| Ensamble por stacking | 0.6757 | 0.6187 | 0.6770 (0.350) |
| Mezcla ponderada | 0.6758 | 0.6188 | 0.6768 (0.350) |
| LightGBM, ajustado | 0.6752 | 0.6195 | 0.6768 (0.355) |
| CatBoost, ajustado | 0.6756 | 0.6185 | 0.6765 (0.350) |
| Regresión logística, ajustada | 0.6637 | 0.6143 | 0.6712 (0.360) |
| Todos responden | 0.5000 | 0.6665 | – |
Frente al mejor modelo original en su propia división de prueba (0.6572 de F1), el nuevo mejor suma +0.020. La mayor parte viene del umbral. El límite honesto es igual de claro: el mejor modelo supera la regla "todos responden" por solo 0.01 de F1, aproximadamente. Con un AUC cercano a 0.675, la señal en estos datos es débil, y la estrategia que maximiza F1 es contactar a cerca del 80% de los clientes.
Antes de comparar modelos, revisa cuánto obtiene una regla trivial con tu métrica. Si te supera, el problema no es el modelo.
Lo que el banco realmente podría usar
F1 era la métrica de la competencia, no la del banco. Un equipo de marketing tiene presupuesto para cierto número de llamadas, y la pregunta útil es: si solo podemos contactar a algunos clientes, ¿a cuáles? Ordenar a los clientes por la probabilidad de la mezcla ponderada lo responde:
| Clientes contactados (los primeros) | Tasa de respuesta | Respondedores encontrados | Lift frente al azar |
|---|---|---|---|
| 10% | 75.1% | 15.0% | 1.50× |
| 20% | 72.4% | 29.0% | 1.45× |
| 30% | 69.3% | 41.6% | 1.39× |
| 50% | 63.1% | 63.1% | 1.26× |
Si el banco solo puede llamar al 10% de sus clientes, los elegidos por el modelo responden el 75% de las veces en lugar del 50%. Con ese resultado un equipo de marketing puede planear un presupuesto, y dice mucho más que un F1 de 0.677.
Lo que aprendí
- Optimiza para la métrica con la que te califican. El umbral es parte del modelo. Dejarlo en 0.5 con una métrica F1 nos costó más que cualquier elección de algoritmo.
- Empieza con una línea base trivial. "Todos responden" nos habría mostrado en cinco minutos que nuestros F1 eran más débiles de lo que parecían.
- Mide el ruido antes de celebrar una mejora. Con ±0.003 entre particiones, muchas de las "mejoras" que probé no eran reales.
- Ajusta con los datos de entrenamiento y aplica a los datos nuevos. Escaladores, codificadores y clústeres van dentro del ciclo de validación cruzada.
- Reporta lo que el negocio puede usar. Una tabla de lift responde "¿a quién llamamos primero?". Un F1 no.
- Conserva el original. Reconstruir junto al notebook antiguo, en lugar de editarlo, mantuvo honestos los puntajes de 2023 y justa la comparación.
Dónde encajan los asistentes de IA
Usé un asistente de programación con IA para revisar el notebook de 2023 y escribir el nuevo pipeline. Fue rápido para leer un notebook largo, detectar patrones sospechosos como las predicciones prestadas y escribir el código repetitivo de la validación cruzada. Las decisiones sobre qué probar, qué contaba como ruido y qué valía la pena reportar fueron mías. Es la misma división del trabajo que describo en De analista de datos a ingeniero de datos.
Próximos pasos
- Enviar el nuevo ensamble a Kaggle, si se permiten envíos tardíos, para confirmar la mejora con los datos de prueba reales.
- Usar las variables de clústeres solo en los modelos lineales, donde ayudaron.
- Probar target encoding de los indicadores de actividad, ajustado dentro de cada partición.
El proyecto y el código
- Competencia
- Target Marketing for Canadian Bank, competencia de Kaggle para la clase AML-1413, invierno de 2023, organizada por Ashish Gupta
- Equipo
- Group Theta: Erwin Mongui (autor principal), Dev Makwana, Aparna Manjalavil, Yasar Mansuri, Amarnadh Movva
- Mejor puntaje en Kaggle (2023)
- 0.63851 de F1 público, GradientBoostingClassifier con búsqueda en malla
- Mejor puntaje local (2026)
- 0.6770 de F1, ensamble por stacking, validación cruzada de 5 particiones (aún sin enviar a Kaggle)
- Tecnologías
- Python, pandas, scikit-learn, LightGBM, XGBoost, CatBoost, Optuna
- Código
- training-ml-python-marketing en GitHub (en inglés): el notebook original, el pipeline reconstruido y los parámetros ajustados
Recibe mis próximos casos y artículos en la Búsqueda de Google.