/ 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

Resumen del proyectoWiki

¿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.

El resultado es un pipeline replicable con datos abiertos que cualquier persona puede ejecutar, pero que refleja fielmente la complejidad de un problema real de retail.

¿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.

Los tres módulos del proyecto

OB1

Limpieza y preparación de datos

Exploración, selección de columnas, generación de datos sintéticos con pandas y numpy, lag features y split temporal.

OB2

Predicción de ventas

Forecasting mensual por producto con Decision Tree, Random Forest, Gradient Boosting, XGBoost y AdaBoost. Evaluación con MAPE.

OB3

Detección de anomalías

Tres métodos comparados: IQR estadístico, K-Medias con distancia al centroide y DBSCAN con ε calculado por k-NN.

OB4

Segmentación de clientes

RFM (Recencia, Frecuencia, Monto) + transformación Box-Cox + StandardScaler + K-Means con análisis de Silhouette.

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)

Setup y datosConfiguración del sistema

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 de enero 2020 a diciembre 2024
fechas = pd.date_range(start="2020-01-01", end="2024-12-31", freq="D")
festivos_peru = holidays.country_holidays("PE", years=range(2020, 2025))

# Filtrar: solo días hábiles (lunes a sábado, sin festivos)
fechas_validas = [
    f for f in fechas
    if f.weekday() != 6              # excluir domingos
    and f not in festivos_peru       # excluir festivos
]

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")

# Máscara: fechas dentro del período de pandemia
mascara = (
    (synthetic_data["Fecha_Emision"] >= inicio_pandemia) &
    (synthetic_data["Fecha_Emision"] <= fin_pandemia)
)

# Reducir ventas al 30-60% del valor normal durante 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)

Feature EngineeringPreparación de datos

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.

OB1 — 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"])

# Seleccionar columnas relevantes
data_productos = data[["Fecha_Emision", "Codigo_Producto", "Cantidad"]].copy()

# Agrupar ventas por producto y ordenar
productos_agrupados = (
    data_productos
    .groupby("Codigo_Producto")["Cantidad"]
    .sum()
    .reset_index()
    .sort_values(by="Cantidad", ascending=False)
)

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

OB2 — Lag features con análisis ACF

El ACF (Autocorrelation Function) mide la correlación de la serie con sus versiones pasadas. Los lags fuera del intervalo de confianza del 95% (banda azul) tienen correlación significativa y deben incluirse como features. En este proyecto se usaron 5 lags mensuales.

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

# Agrupar por mes
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

# Graficar ACF para ver lags significativos
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)
    # Rellenar NaN con cero (primeras filas sin historial)
    df.fillna(0, inplace=True)
    diccionario_dataframes[codigo] = df

# Verificar estructura
print(diccionario_dataframes[codigos_top10[0]].head(7))

OB4 — 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). Luego se aplica Box-Cox para normalizar la distribución antes del clustering.

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()

# Fecha de referencia: día siguiente al último registro
snapshot_date = df_clientes["Fecha_Emision"].max() + timedelta(days=1)

# Calcular RFM
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()

# Transformación Box-Cox para normalizar cada variable
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())

Modelado de los tres módulosEntrenamiento

OB2 — Benchmarking de modelos de predicción

Se entrenan cinco modelos para cada uno de los 10 productos top, evaluados con MAPE. El modelo con menor MAPE promedio se selecciona como el modelo de forecasting del pipeline.

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  # shuffle=False: respetar orden temporal
    )

    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

# Calcular MAPE promedio por modelo sobre todos los productos
import pandas as pd
df_errores = pd.DataFrame(errores_mape).T
print("MAPE promedio por modelo:")
print(df_errores.mean().sort_values())

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

Se comparan tres métodos de detección de anomalías sobre las series de ventas de cada producto.

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
import pandas as pd
from sklearn.cluster import DBSCAN
from sklearn.neighbors import NearestNeighbors
from sklearn.preprocessing import StandardScaler

def calcular_epsilon(X_scaled, k=5):
    """Calcula ε óptimo usando la distancia al k-ésimo vecino más cercano."""
    nbrs = NearestNeighbors(n_neighbors=k).fit(X_scaled)
    distancias, _ = nbrs.kneighbors(X_scaled)
    dist_k = np.sort(distancias[:, -1])
    # Punto de mayor curvatura como ε
    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_
    # Etiqueta -1 = ruido = anomalía
    anomalias = df_result[df_result["cluster"] == -1]
    diccionario_anomalias_dbscan[codigo] = anomalias
    print(f"{codigo}: {len(anomalias)} anomalías con DBSCAN (ε={epsilon:.4f})")

OB4 — K-Means para segmentación RFM

Se usa el método del codo (SSE) y el score de Silhouette para determinar el número óptimo de clusters. Con k=3 se obtienen los mejores resultados en este dataset.

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)

# Método del codo: SSE para k=1..10
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()

# Score de Silhouette para k=2..5
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}")

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

Métricas y comparaciónEvaluación

OB2 — 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

OB3 — 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

OB4 — 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)
# Ejemplo de resultado:
# 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

ConclusionesHallazgos

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 (frecuencias, probabilidades, eventos como pandemia) 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.