/ Proyectos / Negocios / Ventas · Segmentación · Anomalías

Predicción de ventas, segmentación de clientes y detección de anomalías en retail automotriz

Tres módulos en un pipeline: forecasting de demanda por producto con lag features y XGBoost, segmentación RFM con K-Means y detección de anomalías con IQR, K-Medias y DBSCAN sobre datos sintéticos de retail.

25 min de lectura Python · Scikit-learn · XGBoost Datos: retail automotriz 2020–2024 (sintéticos) Aplicable a tesis de negocios o ingeniería industrial

Punto de partidaBrief del proyecto

Eres analista de datos en un retailer automotriz con cuatro sucursales. El gerente comercial te pide tres cosas: saber cuánto va a vender cada producto el próximo mes, identificar qué clientes están en riesgo de perderse y detectar meses de ventas anómalas antes de que lleguen al reporte financiero.

Los datos reales del cliente son confidenciales. Tu primera decisión es cómo desarrollar y publicar el proyecto sin comprometer información sensible.

Qué está en juego

Un modelo de forecasting que sobreestima la demanda genera sobrestock. Uno que subestima genera roturas de inventario. La segmentación incorrecta dirige campañas de reactivación al segmento equivocado. Y una detección de anomalías con demasiados falsos positivos hace que el equipo deje de confiar en el sistema.


Resultados de aprendizajeLo que aprendes aplicando este proyecto

  • Cómo generar datos sintéticos que preservan la distribución estadística real cuando los datos del cliente son confidenciales — una contribución metodológica válida para una tesis.
  • Por qué usar shuffle=False en el split de series temporales y cómo el ACF determina cuántos lags incluir como features.
  • La diferencia real entre IQR, K-Medias y DBSCAN para detectar anomalías, y cuándo cada uno falla — no solo cómo implementarlos.
  • Cómo transformar RFM con Box-Cox antes de K-Means y por qué sin esa transformación el clustering produce segmentos sin sentido.

Al terminar este proyecto sabrás hacer esto

  • Construir un pipeline completo de ML con tres módulos distintos integrados en un mismo dataset transaccional.
  • Seleccionar el modelo de forecasting correcto comparando cinco algoritmos con MAPE sobre datos reales.
  • Segmentar clientes en perfiles accionables (VIP, en riesgo, regular) con RFM y K-Means.
  • Detectar y comparar anomalías en series de ventas con tres métodos distintos, justificando cuál usar según el contexto.
  • Justificar cada decisión metodológica ante un comité de tesis o un equipo de negocios.
Modos de uso: Tesis — integra tres problemas distintos en un flujo coherente con contribución metodológica (datos sintéticos) · Trabajo — pipeline replicable con cualquier dataset transaccional de retail · Portafolio — demuestra forecasting, clustering y detección de anomalías en un solo proyecto

Resumen del proyecto¿Qué construyes?

¿Qué encontrarás en este proyecto?

  • Cómo generar datos sintéticos realistas para retail cuando no puedes compartir datos reales del cliente.
  • Forecasting de ventas por producto usando lag features, ACF y comparación de modelos (Decision Tree, RF, GBM, XGBoost, AdaBoost).
  • Segmentación RFM de clientes con K-Means, método del codo y score de Silhouette.
  • Detección de anomalías con tres métodos: IQR estadístico, K-Medias y DBSCAN.

Este proyecto desarrolla tres módulos de ML sobre datos de un retail del sector automotriz con cuatro sucursales. Como los datos reales del cliente son confidenciales, se construyó un generador de datos sintéticos que preserva la distribución estadística original: patrones de demanda por sucursal, impacto de la pandemia (2020–2021), probabilidades de compra por cliente y por producto.

¿Por qué este proyecto es valioso para una tesis? Integra tres problemas distintos (predicción, clustering, anomalías) en un flujo coherente. La generación de datos sintéticos es una contribución metodológica en sí misma. Y la comparación de cinco modelos de predicción con análisis de ACF muestra rigor en la selección del enfoque.

Stack tecnológico

  • Python 3.10+ — lenguaje principal
  • pandas / numpy — manipulación de datos y generación sintética
  • scikit-learn — Random Forest, KMeans, DBSCAN, StandardScaler, Silhouette
  • xgboost — modelo de predicción principal
  • statsmodels — autocorrelación (ACF) para selección de lags
  • scipy — transformación Box-Cox para normalización RFM
  • matplotlib / seaborn — visualizaciones
  • holidays — generación de fechas válidas (sin festivos ni domingos)

Fase 1Entendimiento del problema

Antes de abrir un notebook, necesitas definir exactamente qué problema de ML estás resolviendo en cada módulo. Este proyecto tiene tres — y cada uno tiene su propio tipo de variable objetivo, su propia métrica y sus propios riesgos.

Decisión clave — tipo de problema

Forecasting de ventas: regresión. El target es una cantidad continua (unidades vendidas por mes por producto).

Segmentación de clientes: clustering no supervisado. No hay etiquetas — el modelo encuentra los grupos por similitud en RFM.

Detección de anomalías: también no supervisado. No hay anomalías etiquetadas — los métodos definen qué es anómalo según criterios estadísticos o de densidad.

Decisión clave — datos confidenciales

¿Usar datos reales o generar sintéticos?

Datos reales: ideal, pero implica acuerdo de confidencialidad y no puedes publicarlos.
Datos sintéticos: permiten publicar y replicar el proyecto. Son válidos si preservan la distribución estadística real.

Se eligió: generar datos sintéticos que replican las frecuencias de ventas por sucursal, probabilidades de producto y el impacto de pandemia 2020–2021. La metodología de generación es en sí una contribución del proyecto.

El proyecto trabaja sobre datos sintéticos generados a partir de la distribución estadística de datos reales de un retailer automotriz con 4 sucursales. La generación preserva patrones reales incluyendo el impacto de pandemia (2020–2021).

Instalación de dependencias

Terminal — instalar dependencias
pip install pandas numpy scikit-learn xgboost statsmodels scipy matplotlib seaborn holidays openpyxl

Estructura del proyecto

Estructura de carpetas recomendada
proyecto-retail/
├── data/
│   ├── raw/                        # datos originales del cliente (confidencial)
│   └── data_productos_sint.csv     # datos sintéticos generados
├── notebooks/
│   ├── 01_generacion_sintetica.ipynb
│   ├── 02_prediccion_ventas.ipynb
│   ├── 03_anomalias.ipynb
│   └── 04_segmentacion_rfm.ipynb
├── src/
│   ├── data_generator.py
│   ├── forecasting.py
│   ├── anomaly_detection.py
│   └── segmentation.py
└── requirements.txt

Generación de datos sintéticos

Cuando los datos del cliente son confidenciales, se generan datos sintéticos que preserven la distribución estadística. La estrategia incluye: frecuencias históricas de ventas por sucursal, probabilidades de cliente y producto, impacto de pandemia como máscara multiplicativa y solo fechas válidas (sin festivos de Perú ni domingos).

Python — generar fechas válidas sin festivos
import pandas as pd
import holidays

fechas = pd.date_range(start="2020-01-01", end="2024-12-31", freq="D")
festivos_peru = holidays.country_holidays("PE", years=range(2020, 2025))

fechas_validas = [
    f for f in fechas
    if f.weekday() != 6
    and f not in festivos_peru
]

print(f"Días hábiles 2020-2024: {len(fechas_validas)}")
Python — reducir ventas en período de pandemia
import numpy as np
import pandas as pd

synthetic_data["Fecha_Emision"] = pd.to_datetime(synthetic_data["Fecha_Emision"])

inicio_pandemia = pd.to_datetime("2020-03-01")
fin_pandemia    = pd.to_datetime("2021-03-31")

mascara = (
    (synthetic_data["Fecha_Emision"] >= inicio_pandemia) &
    (synthetic_data["Fecha_Emision"] <= fin_pandemia)
)

factor_pandemia = np.random.uniform(0.3, 0.6, mascara.sum())
synthetic_data.loc[mascara, "Cantidad"] = (
    synthetic_data.loc[mascara, "Cantidad"] * factor_pandemia
).astype(int)

Fase 2Datos

La preparación de datos para los tres módulos es diferente: el forecasting requiere lag features por producto, la detección de anomalías trabaja sobre series de tiempo individuales, y la segmentación necesita variables RFM a nivel cliente.

Decisión clave — lag features

¿Cuántos lags incluir como features del modelo de forecasting?

El ACF (Autocorrelation Function) mide la correlación de la serie con sus versiones pasadas. Los lags fuera del intervalo de confianza del 95% tienen correlación significativa. En este proyecto el análisis mostró correlación significativa hasta el lag 5.

Se eligió: 5 lags mensuales. Incluir más no mejora el modelo y añade ruido; incluir menos pierde patrones de demanda estacional.

Selección de columnas y top 10 productos

Python — selección de datos y top productos
import pandas as pd

data = pd.read_csv("data/data_productos_sint.csv")
data["Fecha_Emision"] = pd.to_datetime(data["Fecha_Emision"])

data_productos = data[["Fecha_Emision", "Codigo_Producto", "Cantidad"]].copy()

productos_agrupados = (
    data_productos
    .groupby("Codigo_Producto")["Cantidad"]
    .sum()
    .reset_index()
    .sort_values(by="Cantidad", ascending=False)
)

top10 = productos_agrupados.head(10)
codigos_top10 = top10["Codigo_Producto"].tolist()
print(f"Top 10 productos: {codigos_top10}")

Lag features con análisis ACF

Python — ACF para selección de lags
from statsmodels.graphics.tsaplots import plot_acf
import matplotlib.pyplot as plt

data_productos["Mes"] = data_productos["Fecha_Emision"].dt.to_period("M")
diccionario_dataframes = {}

for codigo in codigos_top10:
    df_prod = (
        data_productos[data_productos["Codigo_Producto"] == codigo]
        .groupby("Mes")["Cantidad"]
        .sum()
        .reset_index()
    )
    df_prod.columns = ["Fecha", "Cantidad"]
    diccionario_dataframes[codigo] = df_prod

fig, axes = plt.subplots(5, 2, figsize=(14, 18))
for ax, (codigo, df) in zip(axes.flatten(), diccionario_dataframes.items()):
    plot_acf(df["Cantidad"], lags=12, ax=ax, title=f"ACF — {codigo}")
plt.tight_layout()
plt.show()
Python — construcción de lag features
NUM_LAGS = 5

for codigo, df in diccionario_dataframes.items():
    for lag in range(1, NUM_LAGS + 1):
        df[f"lag_{lag}"] = df["Cantidad"].shift(lag)
    df.fillna(0, inplace=True)
    diccionario_dataframes[codigo] = df

print(diccionario_dataframes[codigos_top10[0]].head(7))

Variables RFM para segmentación

RFM convierte el historial de transacciones en tres métricas por cliente: Recencia (días desde la última compra), Frecuencia (número de compras) y Monto (valor total).

Error común

Aplicar K-Means directamente sobre variables RFM sin transformar. RFM tiene distribuciones sesgadas — pocos clientes con monto muy alto distorsionan los centroides. Sin transformación Box-Cox y normalización, K-Means produce clusters dominados por los outliers de monto, perdiendo la segmentación real.

Python — calcular RFM por cliente
from datetime import timedelta
from scipy import stats
import pandas as pd

df_clientes = data[["Fecha_Emision", "Cliente", "Valor_Venta", "Documento"]].copy()

snapshot_date = df_clientes["Fecha_Emision"].max() + timedelta(days=1)

customers = df_clientes.groupby("Cliente").agg(
    Ultimo    = ("Fecha_Emision", lambda x: (snapshot_date - x.max()).days),
    Frecuencia= ("Documento",     "nunique"),
    Monto     = ("Valor_Venta",   "sum")
).reset_index()

customers_fix = pd.DataFrame()
customers_fix["Ultimo"]     = stats.boxcox(customers["Ultimo"])[0]
customers_fix["Frecuencia"] = stats.boxcox(customers["Frecuencia"])[0]
customers_fix["Monto"]      = stats.boxcox(customers["Monto"])[0]

print(customers_fix.describe())

Fase 3Modelado

Decisión clave — split temporal

¿Por qué usar shuffle=False en el split de series temporales?

En series de tiempo, el orden importa. Si mezclas los datos antes del split, el modelo puede ver ventas del mes 10 para predecir el mes 5 — filtrando información del futuro al pasado (data leakage). shuffle=False garantiza que el test siempre es el período más reciente, nunca algo que ocurrió antes del entrenamiento.

Benchmarking de modelos de predicción

Se entrenan cinco modelos para cada uno de los 10 productos top, evaluados con MAPE.

Python — benchmarking de 5 modelos de predicción
from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import GradientBoostingRegressor, AdaBoostRegressor, RandomForestRegressor
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_percentage_error
from xgboost import XGBRegressor
import numpy as np

modelos = {
    "Decision Tree":      DecisionTreeRegressor(random_state=42),
    "Random Forest":      RandomForestRegressor(random_state=42),
    "Gradient Boosting":  GradientBoostingRegressor(random_state=42),
    "XGBoost":            XGBRegressor(random_state=42, verbosity=0),
    "AdaBoost":           AdaBoostRegressor(random_state=42),
}

errores_mape = {}

for codigo, df in diccionario_dataframes.items():
    features = [f"lag_{i}" for i in range(1, NUM_LAGS + 1)]
    X = df[features]
    y = df["Cantidad"]

    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.2, shuffle=False
    )

    errores_producto = {}
    for nombre, modelo in modelos.items():
        modelo.fit(X_train, y_train)
        y_pred = modelo.predict(X_test)
        mape = mean_absolute_percentage_error(y_test, y_pred) * 100
        errores_producto[nombre] = round(mape, 2)

    errores_mape[codigo] = errores_producto

import pandas as pd
df_errores = pd.DataFrame(errores_mape).T
print("MAPE promedio por modelo:")
print(df_errores.mean().sort_values())

Detección de anomalías: IQR, K-Medias y DBSCAN

Decisión clave — qué método de anomalías usar

IQR es simple y sin parámetros, pero asume distribución simétrica — falla con series asimétricas. K-Medias detecta anomalías relativas al contexto del cluster, pero requiere definir k y un umbral de distancia manual. DBSCAN con ε calculado por k-NN se adapta a cada serie individualmente y no requiere definir el número de clusters.

Conclusión: en este proyecto DBSCAN fue el más robusto. Pero conocer los tres es lo que permite justificar esa elección.

Python — anomalías con IQR estadístico
diccionario_anomalias_iqr = {}

for codigo, df in diccionario_dataframes.items():
    Q1  = df["Cantidad"].quantile(0.25)
    Q3  = df["Cantidad"].quantile(0.75)
    IQR = Q3 - Q1

    limite_inferior = Q1 - 1.5 * IQR
    limite_superior = Q3 + 1.5 * IQR

    anomalias = df[
        (df["Cantidad"] < limite_inferior) |
        (df["Cantidad"] > limite_superior)
    ]
    diccionario_anomalias_iqr[codigo] = anomalias
    print(f"{codigo}: {len(anomalias)} anomalías detectadas con IQR")
Python — anomalías con DBSCAN (ε calculado por k-NN)
import numpy as np
from sklearn.cluster import DBSCAN
from sklearn.neighbors import NearestNeighbors
from sklearn.preprocessing import StandardScaler

def calcular_epsilon(X_scaled, k=5):
    nbrs = NearestNeighbors(n_neighbors=k).fit(X_scaled)
    distancias, _ = nbrs.kneighbors(X_scaled)
    dist_k = np.sort(distancias[:, -1])
    diff = np.diff(dist_k)
    epsilon = dist_k[np.argmax(diff)]
    return epsilon

diccionario_anomalias_dbscan = {}

for codigo, df in diccionario_dataframes.items():
    X = df[["Cantidad"]].values
    scaler = StandardScaler()
    X_scaled = scaler.fit_transform(X)

    epsilon = calcular_epsilon(X_scaled)
    db = DBSCAN(eps=epsilon, min_samples=3).fit(X_scaled)

    df_result = df.copy()
    df_result["cluster"] = db.labels_
    anomalias = df_result[df_result["cluster"] == -1]
    diccionario_anomalias_dbscan[codigo] = anomalias
    print(f"{codigo}: {len(anomalias)} anomalías con DBSCAN (ε={epsilon:.4f})")

K-Means para segmentación RFM

Python — K-Means con método del codo y Silhouette
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import silhouette_score
import matplotlib.pyplot as plt

scaler = StandardScaler()
customers_normalized = scaler.fit_transform(customers_fix)

sse = {}
for k in range(1, 11):
    kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
    kmeans.fit(customers_normalized)
    sse[k] = kmeans.inertia_

plt.figure(figsize=(8, 4))
plt.plot(list(sse.keys()), list(sse.values()), marker="o")
plt.xlabel("Número de clusters (k)")
plt.ylabel("SSE")
plt.title("Método del codo — Segmentación RFM")
plt.grid(True, alpha=0.3)
plt.show()

for k in range(2, 6):
    km = KMeans(n_clusters=k, random_state=42, n_init=10).fit(customers_normalized)
    score = silhouette_score(customers_normalized, km.labels_)
    print(f"k={k}: Silhouette = {score:.4f}")

model = KMeans(n_clusters=3, random_state=42, n_init=10)
model.fit(customers_normalized)
customers["Cluster"] = model.labels_

Fase 4Evaluación

Decisión clave — qué métrica reportar

¿MAPE, RMSE o MAE para el forecasting?

RMSE penaliza errores grandes de forma cuadrática — sensible a outliers. MAE es más robusto pero no da error porcentual. MAPE expresa el error en porcentaje, lo que permite comparar productos con escalas de ventas muy diferentes (un producto que vende 5 unidades y otro que vende 500 son comparables con MAPE, no con RMSE).

Se eligió MAPE porque el cliente quiere saber "¿cuánto me equivoco en porcentaje?", no "¿cuántas unidades me equivoco?".

Error común

Usar Silhouette como único criterio para k en K-Means. El Silhouette score maximiza la separación entre clusters, pero no garantiza que los clusters sean interpretables. Siempre combínalo con el método del codo (SSE) y con el perfil de los clusters resultantes. Un k=4 con Silhouette ligeramente mejor pero clusters sin sentido de negocio es peor que un k=3 claro.

Comparación de modelos de predicción

Modelo MAPE promedio (%) Observaciones
Decision Tree ~28% Alta varianza, sobreajuste con pocos datos
AdaBoost ~24% Sensible a outliers en la serie
Random Forest ~19% Robusto, buen baseline de ensemble
Gradient Boosting ~17% Mejor captura de tendencias locales
XGBoost ~15% Mejor modelo — seleccionado para producción

Comparación de métodos de detección de anomalías

Método Tipo Ventaja Limitación
IQR Estadístico Simple, interpretable, sin parámetros Univariado, asume distribución simétrica
K-Medias Clustering Detecta anomalías relativas al contexto del cluster Requiere definir k y umbral de distancia
DBSCAN Densidad No requiere k, robusto a ruido, detecta clusters no esféricos Sensible a ε; se calcula con k-NN para automatizar

Fase 5Aplicación real

El modelo entrenado no es el producto final. El producto es la decisión que habilita. Aquí defines cómo los resultados de cada módulo se conectan con decisiones concretas de negocio.

Insight de experto

Un MAPE del 15% puede ser aceptable o inaceptable según el margen del producto. Si el margen es 10%, un error del 15% en forecasting puede hacer que el modelo cueste más de lo que aporta. Siempre convierte el error del modelo a impacto financiero antes de presentarlo al cliente o comité.

Perfil de los clusters RFM

Python — perfil de clusters por media de RFM
perfil_clusters = customers.groupby("Cluster").agg(
    Ultimo_promedio    = ("Ultimo",     "mean"),
    Frecuencia_promedio= ("Frecuencia", "mean"),
    Monto_promedio     = ("Monto",      "mean"),
    N_clientes         = ("Cliente",    "count")
).round(1)

print(perfil_clusters)
# Cluster 0: Reciente, alta frecuencia, alto monto  → Clientes VIP
# Cluster 1: Alejados, baja frecuencia, bajo monto  → En riesgo de churn
# Cluster 2: Frecuencia media, monto medio          → Clientes regulares
Cluster Recencia (días) Frecuencia Monto promedio Perfil
0 — VIP ~15 días Alta Alto Clientes activos de alto valor — retener y premiar
1 — En riesgo ~180 días Baja Bajo Sin compras recientes — campañas de reactivación
2 — Regular ~45 días Media Medio Clientes estables — potencial de upselling

Hallazgos principales

Hallazgo 1

XGBoost con lag features es el mejor predictor de demanda mensual

Sobre datos sintéticos de retail automotriz con 10 productos top, XGBoost con 5 lag features mensuales obtuvo un MAPE promedio de ~15%, superando a Random Forest (~19%) y Gradient Boosting (~17%). La clave fue usar shuffle=False en el split para respetar el orden temporal.

Hallazgo 2

DBSCAN con ε calculado por k-NN es el método de anomalías más robusto

IQR detecta demasiados falsos positivos en series con distribuciones asimétricas. K-Medias requiere ajuste manual del umbral de distancia. DBSCAN con ε calculado automáticamente por k-NN se adapta a cada serie individualmente y etiqueta como anomalía solo los puntos genuinamente aislados.

Hallazgo 3

K-Means con k=3 produce los segmentos RFM más interpretables

El método del codo y el score de Silhouette coinciden en k=3 como óptimo. Los tres clusters tienen perfiles claros y accionables: VIP (retener), En riesgo (reactivar) y Regular (upselling). La transformación Box-Cox previa fue crítica para que K-Means convergiera correctamente sobre las variables RFM sesgadas.

Hallazgo 4

La generación de datos sintéticos es una contribución metodológica válida

Cuando los datos del cliente son confidenciales, generar sintéticos que preservan la distribución estadística original permite desarrollar y publicar el proyecto sin comprometer información sensible. Esta estrategia es replicable en cualquier proyecto con restricciones de confidencialidad.

Limitaciones y trabajo futuro

  • Los modelos se entrenan por producto de forma independiente — un modelo multi-output que aprenda patrones entre productos podría mejorar el MAPE.
  • No se incorporaron variables exógenas como promociones, estacionalidad del sector o precio del combustible, que afectan significativamente la demanda automotriz.
  • La segmentación RFM no considera el canal de compra (sucursal) — segmentar por sucursal daría insights más accionables para equipos de ventas locales.
  • DBSCAN con ε por k-NN puede ser computacionalmente costoso con millones de registros — Isolation Forest sería más escalable en producción.
¿Cómo adaptar este proyecto a tu tesis? El pipeline funciona con cualquier dataset transaccional de retail: e-commerce, supermercados, farmacias. Solo necesitas columnas de fecha, cliente, producto, cantidad y valor. La generación sintética es opcional — si tienes datos reales y permiso para usarlos, simplemente reemplaza el CSV de entrada.

GitHubReplica este proyecto

El código completo está disponible en GitHub: notebooks organizados por módulo, datos sintéticos listos para usar y requirements.txt para reproducir el entorno exacto.

FuzzyFrogAI / ml-sales-forecasting-xgboost-rfm-dbscan

Forecasting de ventas con XGBoost, segmentación RFM con K-Means y detección de anomalías con DBSCAN sobre retail automotriz.

github.com/FuzzyFrogAI/ml-sales-forecasting-xgboost-rfm-dbscan