DEV Community

Ivan Morell
Ivan Morell

Posted on

Cómo construí un modelo de probabilidad de recompra con Python durante mi Master en Evolve

Del CSV crudo al modelo serializado: el proceso, las decisiones que tomé y los insights que llevaron a una propuesta de uso real para el equipo de negocio

On this page
🎯 El planteamiento
🤔 Decisión #1: ¿Por qué Python en notebooks y no una herramienta no-code?
🧹 Decisión #2: Cómo definir que un cliente "ha vuelto"
✂️ Decisión #3: Qué excluir del dataset y por qué
⚖️ Decisión #4: Class weights vs SMOTE
🔍 Los insights que salieron del análisis
🏆 LightGBM ganó por la mínima
⏰ Recency es la variable nº 1
🛒 El cliente que devuelve también compra
📊 52% de recompradores
🚫 Lo que descarté
🔭 Qué haría diferente
📦 Repo y entregables
🙋 ¿De dónde sale este proyecto?

"¿Hace falta machine learning para algo que parece sentido común?"
Esa pregunta me la hicieron varias veces cuando conté que había construido un modelo predictivo solo para responder "qué clientes van a volver a comprar". Mi respuesta corta es: el sentido común te dice quiénes son los clientes evidentes; el modelo te dice quiénes son los clientes recuperables. Mi respuesta larga es este artículo.
Construí un modelo completo de probabilidad de recompra sobre un retailer británico online — 24 meses de transacciones, 1.067.371 líneas de venta y casi 6.000 clientes únicos. Partí del dataset público Online Retail II y terminé con un modelo serializado, dos notebooks reproducibles y una presentación corporativa lista para defensa. Lo interesante de este proyecto no es el "qué" — los resultados se ven en cualquier resumen — sino el "por qué" de cada decisión: qué excluí, qué prioricé, y qué aprendí del proceso.
Si trabajas con datos y te interesa el oficio del análisis por encima del último framework de moda, este post te va a interesar.

🎓 Este proyecto forma parte de mi recorrido en el Master de Evolve, donde estoy enfocando mi carrera hacia el machine learning aplicado, la analítica avanzada y la traducción de modelos en decisiones de negocio.

🎯 El planteamiento
El dataset elegido fue Online Retail II — todas las transacciones de un comercio online registrado en Reino Unido entre diciembre de 2009 y diciembre de 2011. La empresa vende artículos de regalo únicos; muchos de sus clientes son mayoristas europeos.
El brief del equipo de negocio (ficticio para el ejercicio) era directo: identificar a los clientes con alta probabilidad de volver a comprar en los próximos seis meses, para alimentar acciones de fidelización (alta probabilidad) y de retención (baja probabilidad). El entregable: notebook reproducible + presentación corta para defensa interna.
Quería que el proyecto cumpliera tres cosas:

Que el output fuera una probabilidad calibrada, no solo una clasificación binaria. Una probabilidad permite ordenar y segmentar; una etiqueta sí/no, no.
Que el proceso fuera trazable end-to-end. Cualquier revisor tenía que poder reproducir cada decisión.
Que el modelo viniera acompañado de una propuesta de uso concreta para negocio. Un AUC sin acción es ruido.

Las tres condiciones acabaron marcando todas las decisiones del proyecto.
🤔 Decisión #1: ¿Por qué Python en notebooks y no una herramienta no-code?
El mercado actual ofrece alternativas no-code que parecen prometer atajos. Quería forzarme a contestar honestamente por qué iba a pagar el coste de escribir Python si podía dejar que un AutoML lo hiciera por mí.
Decisión: Python en notebooks, para un ejercicio cuyo objetivo es enseñar el proceso analítico, una caja negra de AutoML elimina precisamente lo que tengo que enseñar. La velocidad inicial que regala AutoML la pagas después, cuando tienes que justificar en una sala por qué la variable X pesa más que la variable Y y la única respuesta disponible es "porque el modelo lo dice".
Para un dataset de un millón de filas, un GBM se entrena en segundos sobre una laptop estándar. La ganancia de tiempo de AutoML era marginal; la pérdida de control, total.

💡 Lección: la velocidad inicial de las herramientas no-code se paga cara en la fase de defensa. Si tienes que justificar tus decisiones delante de alguien, hazlas tú.

🧹 Decisión #2: Cómo definir que un cliente "ha vuelto"
Aquí está la decisión más analítica de todo el proyecto, y la que más se tarda en explicar.
El problema parece trivial — "¿el cliente recompra o no?" — hasta que te sientas a operacionalizarlo. ¿Vuelve en una semana, en un mes, en un año? ¿Cuento solo la primera compra posterior, o también las renovaciones puntuales? ¿Qué hago con los clientes que se incorporan a mitad del periodo y no tienen historial suficiente?
Tenía tres opciones razonables:

Ventana corta (1–3 meses). Target muy desbalanceado (pocos vuelven en tan poco tiempo), pero útil para acciones operativas inmediatas.
Ventana media (6 meses). Balancea volumen de positivos con valor de la predicción para fidelización.
Dos cutoffs sucesivos. Entrenar con cutoff₁ → testear con cutoff₂ posterior. Validación más realista pero código mucho más complejo.

Elegí la segunda — cutoff = 9 de junio de 2011, ventana de observación de 6 meses. Razonamiento:

Tenía 24 meses de histórico. Dejando los 6 últimos como ventana de target, me sobraban 18 meses para construir features — un ciclo navideño completo de historia de cliente.
6 meses captura también un ciclo navideño completo en el target, evitando que un mes pico distorsione la etiqueta.
La tasa de recompra resultante fue del 52% — target balanceado, audiencia suficiente para iniciativas de fidelización y retención.

La tercera opción la descarté conscientemente: hubiera duplicado el feature engineering por motivos que no afectan al insight final. La dejé documentada en "próximos pasos".

💡 Lección: la definición del target es la decisión que más impacta en el resultado del modelo. Si la haces mal, ningún hiperparámetro te salva.

✂️ Decisión #3: Qué excluir del dataset y por qué
El EDA reveló cinco grupos de registros problemáticos:
GrupoVolumenDecisiónTickets sin Customer ID243.007 (22,8%)ExcluirCancelaciones (Invoice "C…")19.494 (1,8%)ExcluirQuantity ≤ 0 / Price ≤ 029.157ExcluirCódigos administrativos (POST, BANK CHARGES…)10.739ExcluirDuplicados exactos34.335 (3,2%)Excluir
La decisión más controvertida fue la primera. Casi una cuarta parte del dataset son tickets sin identificador de cliente. ¿Realmente excluyo el 23% sin más?
Sí. Y la razón es brutalmente simple: si el problema es predecir la recompra del cliente, necesito identificar al cliente. Un ticket anónimo no se puede etiquetar como recomprador ni puede entrar en una agregación RFM por cliente. La alternativa — imputar un identificador sintético — habría sido peor que descartar.
Lo importante para el equipo de negocio no es la decisión técnica, es la implicación: ese 23% de tickets anónimos revela un problema operativo de captura de identidad que el modelo no puede resolver. Es información valiosa para la defensa.

💡 Lección: las exclusiones cuentan una historia sobre la madurez del dato. Documentarlas no es burocracia, es transparencia analítica.

⚖️ Decisión #4: Class weights vs SMOTE
Cuando construí el target, la tasa de positivos fue del 52% — un desbalanceo muy moderado. Aun así, había que decidir cómo trabajar la clase minoritaria. Las dos opciones estándar:

SMOTE (Synthetic Minority Over-sampling). Genera "clientes sintéticos" a partir de combinaciones lineales de los reales. Funciona bien para clasificación pura.
Class weights. Penaliza más fuertemente los errores en la clase minoritaria al entrenar, sin tocar el dataset.

Elegí class weights por una razón concreta: el output del modelo se va a usar como probabilidad, no como etiqueta. La probabilidad va a alimentar una segmentación por deciles para decidir qué acción comercial recibe cada cliente. Y SMOTE — al generar registros sintéticos — descalibra esa probabilidad: el modelo aprende sobre una distribución artificialmente equilibrada que no se parece a la realidad operativa.
Si todo lo que necesitara fuera un sí/no a un umbral fijo, SMOTE habría sido equivalente o mejor. Pero entrego un score para priorizar acciones, no una clasificación binaria. La calibración importa.

💡 Lección: el método de manejo del desbalanceo tiene que elegirse en función de cómo se va a usar el output, no de la métrica con la que vas a evaluarlo.

🔍 Los insights que salieron del análisis
Estos no son el corazón del artículo, pero merecen un repaso rápido para mostrar a dónde llevó todo el proceso:
🏆 LightGBM ganó por la mínima
Probé cuatro modelos en paralelo: Logistic Regression, LightGBM, XGBoost y CatBoost. Resultado final en ROC AUC sobre test hold-out: 0,820 / 0,819 / 0,817 / 0,812. Una décima de diferencia entre el mejor y el peor. → Cuando los datos y las features son las mismas, el modelo deja de ser el factor diferencial. La elección de LightGBM como ganador fue por velocidad de inferencia y robustez, no por AUC.
⏰ Recency es la variable nº 1
SHAP confirmó lo que sospechábamos: la recencia (días desde la última compra) es la variable que más pesa en la predicción, seguida de frecuencia y gasto. → Las clásicas del RFM siguen funcionando 30 años después de que el modelo se inventara para mailing directo.
🛒 El cliente que devuelve también compra
Las features de devolución (return_rate, n_cancellations) aportaron señal, pero al revés de lo intuitivo: los clientes con más devoluciones también tienen mayor probabilidad de recompra. → Es un efecto de volumen — clientes activos compran y devuelven más. La feature por sí sola no separa "problemáticos" de "buenos".
📊 52% de recompradores → audiencia balanceada
La tasa base de recompra a 6 meses es del 52%. → No estamos ante un problema de minoría rara: hay audiencia suficiente para iniciativas masivas, tanto de fidelización como de retención.
Cada insight vino acompañado, en la presentación, de una acción comercial concreta asociada a un decil de probabilidad. Eso es lo que diferencia un modelo cerrado de un modelo entregable.
🚫 Lo que descarté
Cuatro cosas que estuvieron sobre la mesa pero no llegaron al entregable final:

  1. SMOTE y demás técnicas de oversampling. Ya razonado en la Decisión #4. Hubiera distorsionado la calibración del output.
  2. Redes neuronales. Tentación clásica: si tengo Python y un dataset tabular, ¿por qué no probar un MLP o un TabNet? Lo descarté: con ~5.000 muestras y 19 features, una red neuronal no aporta nada sobre un GBM bien tuneado. La complejidad sin ganancia es deuda técnica futura.
  3. Validación temporal estricta con dos cutoffs. Habría duplicado el feature engineering. Es la mejora número uno para una v2 — está en próximos pasos —, pero para un MVP de una semana, un split aleatorio estratificado a nivel cliente es suficiente.
  4. Tuning con más de 100 trials de Optuna. Probé 30 trials por modelo y el AUC se estabilizó a partir del 15. Más trials habrían sido más cómputo sin retorno significativo. Saber cuándo parar de optimizar es parte del oficio.

💡 Lección: la disciplina de no añadir es lo que diferencia un proyecto terminado de un proyecto que se eterniza buscando la décima.

🔭 Qué haría diferente

Validación con dos cutoffs sucesivos. Es la mejora más ambiciosa. Mediría cómo de bien generaliza el modelo de un periodo a otro — más realista que un split aleatorio.
Calibración isotónica del output. Si el score se va a usar para priorizar, no solo para rankear, conviene re-calibrarlo. El Brier score de 0,17 invita a ello.
Multi-horizonte. Entrenar modelos paralelos a 1, 3, 6 y 12 meses para apoyar acciones comerciales con distintos tiempos de reacción.
Optimizar uplift económico, no AUC. Incorporar margen por cliente y coste de la acción comercial. Maximizar valor neto en vez de capacidad discriminatoria pura.

Estas cuatro mejoras están documentadas en la slide de "Próximos pasos" de la presentación. La capacidad de mirar atrás y ver con claridad lo que no hiciste es parte del entregable.
📦 Repo y entregables
El proyecto está organizado como un repo estándar de data science:

📓 notebooks/01_EDA.ipynb — análisis exploratorio completo (53 celdas)
📓 notebooks/02_Modeling.ipynb — target, features, tuning Optuna, SHAP (58 celdas)
📁 src/ — módulos reusables: data_loader, features, model
🤖 models/best_model_lightgbm.joblib — pipeline serializado listo para inferencia
📊 reports/presentation/Online_Retail_II_Recompra.pptx — presentación corporativa de 5 slides
📄 docs/ — enunciado del ejercicio y documentación

Stack: Python 3.11, pandas, scikit-learn, LightGBM, XGBoost, CatBoost, Optuna, SHAP, joblib, python-pptx.
🙋 ¿De dónde sale este proyecto?
Este artículo y el proyecto son parte de mi formación en el Master de Evolve. Es de los proyectos que más me han enseñado sobre lo que sí y lo que no se debe hacer en un análisis predictivo: no perseguir la décima de AUC cuando lo que falta es definir bien el target; no aplicar técnicas que distorsionan el output por costumbre; no quedarte en la métrica si no la puedes traducir a una acción comercial concreta.
Si trabajas en data science, machine learning aplicado o estrategia comercial, me encantaría escuchar tu opinión — sobre todo si has tomado decisiones parecidas con dilemas distintos. Puedes encontrarme en LinkedIn.
Si te ha resultado útil, déjale un 💙 al artículo y un follow — voy a publicar más posts sobre los proyectos del Master, incluyendo un análisis de ventas con Excel y un par de proyectos de visión por computador.
Iván Morell 🌐 portfolio-ivan-ms.vercel.app · 💼 LinkedIn · 💻 GitHub

Top comments (0)