Análisis de ventas con PrestaShop y pandas¶
En esta práctica se analizan los datos de una tienda PrestaShop real a partir de ficheros JSON obtenidos mediante la API oficial de PrestaShop.
El trabajo se divide conceptualmente en dos partes:
Parte A. Obtención de datos (API PrestaShop)
En un entorno real de producción, la descarga de datos desde la API debería realizarse en una lógica independiente, por ejemplo mediante una aplicación de escritorio o un servicio programado.
Esto permite controlar tiempos de ejecución, errores, límites de la API y evitar bloqueos innecesarios en el análisis.
No obstante, con fines didácticos, esta práctica incluye también la ejecución de las llamadas a la API dentro del propio notebook, para que el alumnado pueda comprender el flujo completo de trabajo: desde la extracción de datos hasta su análisis.
Parte B. Análisis de datos con pandas y matplotlib
Una vez descargados los datos, se utilizan las librerías pandas y matplotlib para responder a preguntas de negocio reales, siguiendo un enfoque claro, reproducible y orientado al análisis exploratorio.
El objetivo final es entender cómo transformar datos brutos de una plataforma e-commerce en información útil para la toma de decisiones.
A. Extracción de archivos mediante API PrestaShop¶
1. Uso de ficheros JSON en lugar de consulta directa a la API¶
En esta práctica no se consultan los datos directamente desde la API de PrestaShop con pandas, sino que se realiza una descarga previa y persistente en ficheros JSON.
Pandas no está diseñado para consumir APIs REST complejas¶
Aunque pandas puede leer JSON desde una URL, no gestiona bien:
- Autenticación (API key, headers).
- Paginación (
limit,offset). - Inconsistencias de formato (lista vs diccionario en PrestaShop).
- Reintentos y control de errores HTTP.
Forzar estas tareas dentro de pandas empeora la legibilidad y la robustez del análisis.
Rendimiento y estabilidad¶
Consultar la API repetidamente desde un notebook:
- Es lento, especialmente con grandes volúmenes de datos.
- Sobrecarga el servidor de PrestaShop.
- Introduce dependencias externas (red, latencia, caídas).
Al trabajar con ficheros JSON locales:
- El análisis es inmediato.
- Se pueden repetir pruebas sin volver a descargar datos.
- El notebook se vuelve reproducible y estable.
# API configuration (mostrado solo con fines educativos al ser en local; no usar en producción)
BASE_URL = "http://localhost/api"
API_KEY = "9b3e7f0a6e1c4d2b8f9a0c7e5d4b3a21"
PAGE_SIZE = 5000
ORDERS_FILE = "data/orders.json"
LINES_FILE = "data/order_lines.json"
CUSTOMERS_FILE = "data/customers.json"
Función de persistencia de datos en formato JSON¶
En esta celda se define una función auxiliar encargada de guardar datos en disco en formato JSON.
¿Qué hace la función?¶
- Recibe una ruta de archivo y una estructura de datos de Python (listas o diccionarios).
- Crea automáticamente los directorios necesarios si no existen.
- Guarda los datos en formato JSON legible, con codificación UTF-8 y sangrado.
¿Por qué JSON?¶
- Es un formato estándar, ligero y fácil de leer.
- pandas puede cargarlo directamente sin transformación previa.
- Permite inspeccionar los datos manualmente si es necesario.
- Es independiente de la API una vez generado.
Esta función se reutiliza en distintos procesos de extracción de datos (pedidos, líneas de pedido, clientes), evitando duplicar código y facilitando el mantenimiento del proyecto.
# Función para persistencia, guarda datos en un archivo JSON
import os
import json
def guardar_json(ruta, datos):
os.makedirs(os.path.dirname(ruta), exist_ok=True)
with open(ruta, "w", encoding="utf-8") as f:
json.dump(datos, f, ensure_ascii=False, indent=4)
# Módulo necesario para realizar solicitudes a la API de PrestaShop
import requests
Extracción de datos desde la API de PrestaShop¶
En las siguientes celdas se definen varias funciones encargadas de descargar datos desde la API de PrestaShop y prepararlos para su análisis posterior con pandas.
Concretamente, se trabajan tres tipos de información:
- Pedidos (
update_orders) - Líneas de pedido (
update_order_lines) - Clientes (
update_customers)
Funcionamiento común de las funciones de descarga¶
Todas las funciones siguen la misma lógica general:
- Se inicializa una lista vacía donde se acumulan los registros.
- Se realizan llamadas repetidas a la API usando paginación (
limityoffset). - En cada llamada:
- Se solicitan únicamente los campos necesarios mediante
display. - Se añade el bloque de datos obtenido a la lista acumulada.
- Se solicitan únicamente los campos necesarios mediante
- El proceso finaliza cuando la API devuelve una respuesta vacía.
- El resultado se guarda en un archivo JSON para su uso posterior.
Este enfoque permite:
- Controlar el consumo de recursos.
- Evitar llamadas repetidas a la API durante el análisis.
- Reproducir el análisis tantas veces como sea necesario sin depender de la conexión a la tienda.
def update_orders():
"""
Descarga todos los pedidos desde la API de PrestaShop
y los guarda en un archivo JSON para su posterior análisis
con pandas.
"""
pedidos = []
offset = 0
while True:
response = requests.get(
f"{BASE_URL}/orders",
auth=(API_KEY, ""),
params={
"display": "[id,id_customer,total_paid,date_add]",
"limit": f"{offset},{PAGE_SIZE}",
"output_format": "JSON",
},
timeout=30,
)
response.raise_for_status()
json_data = response.json()
# PrestaShop puede devolver lista o diccionario según versión/configuración
if isinstance(json_data, dict):
data = json_data.get("orders", [])
else:
data = json_data
if not data:
break
pedidos.extend(data)
offset += PAGE_SIZE
guardar_json(ORDERS_FILE, pedidos)
def update_order_lines():
"""
Descarga todas las lineas de pedidos desde la API de PrestaShop
y los guarda en un archivo JSON para su posterior análisis
con pandas.
"""
lineas = []
offset = 0
while True:
response = requests.get(
f"{BASE_URL}/order_details",
auth=(API_KEY, ""),
params={
"display": "[id,id_order,product_id,product_name,product_quantity,total_price_tax_incl]",
"limit": f"{offset},{PAGE_SIZE}",
"output_format": "JSON",
},
timeout=30,
)
response.raise_for_status()
json_data = response.json()
# PrestaShop puede devolver lista o diccionario según versión/configuración
if isinstance(json_data, dict):
data = json_data.get("order_details", [])
else:
data = json_data
if not data:
break
lineas.extend(data)
offset += PAGE_SIZE
guardar_json( LINES_FILE, lineas)
def update_customers():
"""
Descarga todos los pedidos desde la API de PrestaShop
y los guarda en un archivo JSON para su posterior análisis
con pandas.
"""
clientes = []
offset = 0
while True:
response = requests.get(
f"{BASE_URL}/customers",
auth=(API_KEY, ""),
params={
"display": "[id,firstname,lastname,email,date_add]",
"limit": f"{offset},{PAGE_SIZE}",
"output_format": "JSON",
},
timeout=30,
)
response.raise_for_status()
json_data = response.json()
# PrestaShop puede devolver lista o diccionario según versión/configuración
if isinstance(json_data, dict):
data = json_data.get("customers", [])
else:
data = json_data
if not data:
break
clientes.extend(data)
offset += PAGE_SIZE
guardar_json(CUSTOMERS_FILE, clientes)
Nota sobre el formato de respuesta de la API¶
Dependiendo de la versión o configuración de PrestaShop, la API puede devolver:
- Un diccionario que contiene los datos bajo una clave específica.
- O directamente una lista de registros.
Por ello, las funciones comprueban el tipo de respuesta antes de procesarla, garantizando compatibilidad entre entornos.
Estas funciones no realizan análisis, solo preparan los datos para las siguientes fases del notebook.
Ahora nos quedaría ejecutarlas, cuidado porque en una tienda real puede llevar hasta 15 minutos (Puede ser buena idea ejecutar solo una llamada cada vez)
# Para no llamar a la API por error cada vez que ejecutamos el cuaderno debemos comentar siempre una linea para que funcione
ACTUALIZAR_DATOS = True
ACTUALIZAR_DATOS = False
if ACTUALIZAR_DATOS:
update_orders()
update_order_lines()
update_customers()
B. Análisis de datos (ventas y pedidos)¶
Carga de librerías y datos¶
Cargamos las librerías necesarias
import pandas as pd
import matplotlib.pyplot as plt
Leemos los archivos JSON generados previamente por el proceso de extracción desde la API de PrestaShop.
orders = pd.read_json("data/orders.json")
order_lines = pd.read_json("data/order_lines.json")
customers = pd.read_json("data/customers.json")
orders.head()
| id | id_customer | date_add | total_paid | |
|---|---|---|---|---|
| 0 | 3 | 5 | 2012-08-21 15:07:58 | 42.43 |
| 1 | 5 | 7 | 2012-08-28 12:17:24 | 35.42 |
| 2 | 6 | 8 | 2012-08-28 13:37:23 | 92.56 |
| 3 | 7 | 10 | 2012-09-02 01:41:03 | 32.56 |
| 4 | 9 | 12 | 2012-09-06 16:45:00 | 57.84 |
Preparación y limpieza de datos¶
Convertimos fechas y campos numéricos a los tipos adecuados para poder analizarlos correctamente.
orders["date_add"] = pd.to_datetime(orders["date_add"])
orders["total_paid"] = orders["total_paid"].astype(float)
order_lines["product_quantity"] = order_lines["product_quantity"].astype(int)
order_lines["total_price_tax_incl"] = order_lines["total_price_tax_incl"].astype(float)
customers["date_add"] = pd.to_datetime(customers["date_add"])
Cruce de pedidos con clientes¶
Relacionamos los pedidos con los datos de clientes para poder identificar personas concretas (nombre, apellido y email).
#Eliminamos la fecha de creación del cliente que no es necesaria para el análisis
customers_reduced = customers[[
"id",
#"firstname",
# "lastname",
# "email"
]].rename(columns={"id": "id_customer"})
# Para no mostrar datos privados de los clientes vamos a anonimizar nombres con fines didácticos evitando cargar o mostrar datos reales
customers_reduced["customer_name"] = (
"Cliente_" + customers_reduced["id_customer"].astype(str)
)
orders_clientes = orders.merge(
customers_reduced,
on="id_customer",
how="left"
)
orders_clientes.head()
| id | id_customer | date_add | total_paid | customer_name | |
|---|---|---|---|---|---|
| 0 | 3 | 5 | 2012-08-21 15:07:58 | 42.43 | Cliente_5 |
| 1 | 5 | 7 | 2012-08-28 12:17:24 | 35.42 | Cliente_7 |
| 2 | 6 | 8 | 2012-08-28 13:37:23 | 92.56 | Cliente_8 |
| 3 | 7 | 10 | 2012-09-02 01:41:03 | 32.56 | Cliente_10 |
| 4 | 9 | 12 | 2012-09-06 16:45:00 | 57.84 | Cliente_12 |
Inicio del análisis
1. ¿Cual es la trayectoria de la tienda cronológicamente?¶
Analizamos el importe total vendido agrupando los pedidos por meses.
En este bloque se crea una nueva columna llamada month a partir de la fecha del pedido (date_add), utilizando periodos mensuales (to_period("M")).
A continuación, los datos se agrupan por mes (groupby) y se calcula el importe total vendido en cada periodo mediante la suma (sum).
Finalmente, los resultados se ordenan cronológicamente (sort_index) para obtener una serie temporal de ventas mensuales, que permite analizar la evolución del negocio a lo largo del tiempo y detectar tendencias de crecimiento, estancamiento o cambios estructurales.
orders_clientes["month"] = orders_clientes["date_add"].dt.to_period("M")
ventas_historicas_por_mes = (
orders_clientes.groupby("month")["total_paid"]
.sum()
.sort_index()
)
ventas_historicas_por_mes
month
2012-08 170.4100
2012-09 565.3800
2012-10 2389.4500
2012-11 2199.5500
2012-12 1809.3600
...
2020-03 80415.1478
2020-04 114321.6605
2020-05 146925.1954
2020-06 13659.3394
2020-07 84.0200
Freq: M, Name: total_paid, Length: 96, dtype: float64
ventas_historicas_por_mes.plot(kind="bar",figsize=(12,6))
plt.title("Ventas por mes")
plt.ylabel("Importe total")
plt.xlabel("Mes")
plt.xticks(fontsize=6, rotation=70, ha="right")
plt.tight_layout()
plt.show()
Conclusiones sobre la evolución temporal de las ventas¶
El análisis de las ventas mensuales permite observar con claridad la trayectoria de crecimiento de la tienda a lo largo del tiempo.
En una primera etapa, correspondiente a los primeros meses de actividad, el volumen de ventas es reducido y relativamente estable. Esto es habitual en fases iniciales de una tienda online, donde el catálogo, la base de clientes y la visibilidad todavía son limitados.
A partir de ese periodo inicial se aprecia un crecimiento progresivo y sostenido, con incrementos cada vez más pronunciados en el importe total vendido. Esta tendencia indica una consolidación del negocio, probablemente asociada a un aumento del número de clientes, una mejora del posicionamiento o una ampliación del catálogo de productos.
En los últimos meses analizados, las ventas alcanzan valores significativamente más altos y parecen mostrar picos recurrentes, lo que sugiere una mayor madurez de la tienda y una capacidad estable de generar ingresos elevados de forma continuada. También se observan oscilaciones entre meses, lo cual es normal en entornos de comercio electrónico y puede estar relacionado con estacionalidad, campañas comerciales o promociones puntuales. Deberíamos comprobarlo en siguientes apartados para analizar oportunidades de mejora.
En conjunto, la gráfica refleja una trayectoria claramente ascendente, sin caídas estructurales prolongadas, lo que indica que el negocio ha evolucionado positivamente en el tiempo y presenta signos de crecimiento sostenido y estabilidad en su fase más reciente.
2 ¿Las ventas son estacionales?¶
Analizamos el importe vendido agrupando por meses para saber en cuales se vende más.
En esta celda se configura el entorno regional (locale) para que los nombres de los meses se generen en castellano.
A partir de la fecha del pedido (date_add) se crea una nueva columna month_name que contiene el nombre completo del mes (strftime("%B")).
Posteriormente, los datos se agrupan por nombre de mes (groupby) y se calcula el importe total vendido en cada uno mediante la suma (sum).
Finalmente, se reordena explícitamente el resultado usando reindex con el orden natural de los meses del año. Este paso es para que los meses no aparecezcan ordenados alfabéticamente, lo que dificultaría el análisis de estacionalidad.
import locale
locale.setlocale(locale.LC_TIME, "es_ES.UTF-8")
orders_clientes["month_name"] = orders_clientes["date_add"].dt.strftime("%B")
ventas_por_meses = (
orders_clientes
.groupby("month_name")["total_paid"]
.sum()
.reindex([
"enero", "febrero", "marzo", "abril", "mayo", "junio",
"julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"
])
)
ventas_por_meses
month_name enero 277833.9956 febrero 302343.6328 marzo 325004.6032 abril 371851.4248 mayo 414629.9361 junio 300309.3881 julio 322660.6206 agosto 357346.0203 septiembre 423996.6698 octubre 360795.5293 noviembre 401056.0627 diciembre 267391.8702 Name: total_paid, dtype: float64
ax = ventas_por_meses.plot(kind="bar", figsize=(10, 4))
media = ventas_por_meses.mean()
ax.axhline(media, linestyle="--")
ventas_por_meses.plot(
kind="line",
ax=ax,
marker="o"
)
ax.set_title("Ventas por mes con media anual")
ax.set_ylabel("Importe total")
plt.show()
El análisis de las ventas agregadas por mes, junto con la media anual, permite identificar patrones estacionales claros en el comportamiento de la tienda.
Se observa que los meses de primavera y principios de otoño concentran los mayores volúmenes de ventas. En particular, abril, mayo, septiembre y noviembre se sitúan consistentemente por encima de la media anual, lo que indica periodos de alta demanda recurrentes.
Por el contrario, los meses de verano y final de año muestran un comportamiento más débil. Destacan especialmente junio y diciembre, que se encuentran claramente por debajo de la media anual. Esto sugiere una posible reducción de la actividad comercial en estos periodos, ya sea por factores estacionales, cambios en los hábitos de consumo o menor intensidad de campañas comerciales.
El patrón observado no es aleatorio, sino que se repite de forma consistente, lo que confirma la existencia de una estacionalidad moderada en las ventas. Esta información es especialmente útil para:
- Planificar campañas de marketing en los meses de mayor rendimiento.
- Ajustar stock y logística en periodos de alta y baja demanda.
- Anticipar caídas estacionales y diseñar acciones correctivas.
Podemos asegurar que las ventas sí presentan estacionalidad, con meses claramente más favorables que otros, lo que refuerza la importancia de analizar los datos históricos para apoyar la toma de decisiones comerciales.
3. Productos más vendidos¶
Identificamos los productos con mayor volumen de ventas a partir de las líneas de pedido.
En esta celda se agrupan las líneas de pedido por nombre de producto (groupby("product_name")) y se calcula el total de unidades vendidas de cada uno sumando las cantidades (sum).
A continuación, los resultados se ordenan de mayor a menor (sort_values(ascending=False)) para identificar los productos con mayor volumen de ventas.
Se muestran por pantalla los diez productos más vendidos utilizando head(10).
Finalmente, se extrae el índice de esos diez primeros resultados mediante .index. Este índice contiene únicamente los nombres de los productos y se almacena en top_productos, lo que permite usarlo posteriormente como filtro para analizar solo los productos más relevantes.
productos_mas_vendidos = (
order_lines.groupby("product_name")["product_quantity"]
.sum()
.sort_values(ascending=False)
)
print (productos_mas_vendidos.head(10))
top_productos = productos_mas_vendidos.head(10).index
product_name Frasco de vidrio para Miel 1 Kg Celdillas - Formato : Pack 6347 Grapas metálicas Inox de agarre para envases modelos Weck - Formato : Pack 4446 Separador de plástico 3631 TAPONES PLÁSTICO PARA ENTRETAPAS 3573 Tapas metálicas para frascos Diámetro: 77 mm - Color : Blanca, Formato : Pack, Tipo de compuesto (cocción) : Esterilizable (121º) 3248 Tapas metálicas para frascos Diámetro: 77 mm - Color : Blanca, Formato : Pack, Tipo de cocción : Pasteurizable (100º) 2947 Tapas metálicas para frascos Diámetro: 63 mm - Color : Blanca, Formato : Pack, Tipo de compuesto (cocción) : Esterilizable (121º) 2920 Botella "verano" 2796 Tapa metálica para frascos Diámetro: 43 mm - Color : Dorada, Formato : Pack, Tipo de cocción : Pasteurizable (100º) 2725 Tapas metálicas para frascos Diámetro: 66 mm - Color : Negra, Formato : Pack, Tipo de cocción : Pasteurizable (100º) 2669 Name: product_quantity, dtype: int64
4. Estacionalidad de productos¶
Con esto sabemos ya cuales son los productos más vendidos, sin embargo, anteriormente averiguamos que la tienda tiene una alta estacionalidad ¿podemos relacionarlo?
Un producto es estacional si:
Sus ventas se concentran en determinados meses.
Tiene picos claros y meses con muy poca actividad.
Su patrón mensual no es uniforme.
Esto se puede medir con los datos actuales. Vamos a cruzar las líneas de pedido con sus fechas
Primero se realiza un cruce (merge) entre order_lines y orders, usando el identificador del pedido (id_order en las líneas y id en los pedidos). De este modo, cada línea de producto hereda la fecha en la que se realizó el pedido (date_add).
A continuación, a partir de esa fecha se extrae el nombre del mes mediante dt.month_name() y se guarda en una nueva columna (month).
# Fecha del pedido para cada línea
ventas_producto_mes = (
order_lines
.merge(
orders[["id", "date_add"]],
left_on="id_order",
right_on="id"
)
)
ventas_producto_mes["month"] = ventas_producto_mes["date_add"].dt.month_name()
Ahora se filtran las líneas de venta para quedarse solo con los productos incluidos en top_productos, es decir, el conjunto de artículos con mayor volumen total de ventas. Esto permite centrar el análisis en los productos realmente relevantes para el negocio.
A continuación, se agrupan los datos por nombre de producto y por mes (groupby(["product_name", "month"])) y se suma la cantidad vendida (product_quantity). El resultado es una tabla donde cada fila representa cuántas unidades de un producto concreto se han vendido en un mes determinado.
Finalmente, reset_index() convierte los índices utilizados para agrupar los datos (producto y mes) en columnas de la tabla, dejando el conjunto de datos listo para su uso en gráficos sin tener que usar índices.
# Agrupamos por producto y mes
ventas_top = (
ventas_producto_mes[
ventas_producto_mes["product_name"].isin(top_productos)
]
.groupby(["product_name", "month"])["product_quantity"]
.sum()
.reset_index()
)
# Analizamos el producto más vendido para ver su estacionalidad de manera directa
producto = top_productos[0]
datos_producto = ventas_top[ventas_top["product_name"] == producto]
plt.figure()
plt.bar(datos_producto["month"], datos_producto["product_quantity"])
plt.title(f"Estacionalidad del producto: {producto}")
plt.xticks(rotation=45)
plt.show()
Si analizamos el producto más vendido vemos que presenta una estacionalidad moderada-alta, no es extrema, pero sí es claramente identificable.
Existen máximos claros en septiembre y octubre, con el valor más alto del año y un segundo bloque fuerte en julio y agosto.
No parecen picos aislados, sino un comportamiento coherente con campañas de recolección y envasado.
A pesar de ello, mantiene ventas durante todo el año, lo que indica que se trata de un producto estructural del catálogo cuya demanda se intensifica en determinados periodos, en lugar de depender exclusivamente de campañas puntuales.
Para analizar un conunto mayor de productos usaremos el coeficiente de variación.
El coeficiente de variación (CV) mide cuánto varían las ventas de un producto a lo largo del tiempo en relación con su nivel medio de ventas. No mide volumen, mide irregularidad.
El siguiente código separa los datos por producto (groupby), calcula la desviación estándar (std) y la divide por la media (mean). Esto es clave porque un producto que vende mucho siempre tendrá más desviación absoluta pero el dividir la variación entre la media permite comparar productos grandes y pequeños.
coef_variacion = (
ventas_top
.groupby("product_name")["product_quantity"]
.std()
/
ventas_top.groupby("product_name")["product_quantity"].mean()
)
coef_variacion.sort_values(ascending=False)
product_name Botella "verano" 1.133645 TAPONES PLÁSTICO PARA ENTRETAPAS 1.094625 Separador de plástico 0.982422 Tapas metálicas para frascos Diámetro: 77 mm - Color : Blanca, Formato : Pack, Tipo de cocción : Pasteurizable (100º) 0.915096 Tapas metálicas para frascos Diámetro: 77 mm - Color : Blanca, Formato : Pack, Tipo de compuesto (cocción) : Esterilizable (121º) 0.699183 Tapa metálica para frascos Diámetro: 43 mm - Color : Dorada, Formato : Pack, Tipo de cocción : Pasteurizable (100º) 0.553464 Frasco de vidrio para Miel 1 Kg Celdillas - Formato : Pack 0.519135 Tapas metálicas para frascos Diámetro: 63 mm - Color : Blanca, Formato : Pack, Tipo de compuesto (cocción) : Esterilizable (121º) 0.512163 Grapas metálicas Inox de agarre para envases modelos Weck - Formato : Pack 0.408615 Tapas metálicas para frascos Diámetro: 66 mm - Color : Negra, Formato : Pack, Tipo de cocción : Pasteurizable (100º) 0.281390 Name: product_quantity, dtype: float64
Valores elevados indican ventas concentradas en determinados meses, mientras que valores bajos reflejan un comportamiento estable a lo largo del año.
Una regla práctica (muy usada en análisis real):
Coeficiente Interpretación:
CV < 0.4 → producto muy estable
0.4 ≤ CV < 0.7 → estabilidad media
0.7 ≤ CV < 1.0 → estacionalidad clara
CV ≥ 1.0 → muy estacional / dependiente de campañas
No es una norma, es un criterio estadístico razonable.
Lectura del conjunto de productos¶
1. Productos extremadamente estacionales (CV > 1)¶
Botella "verano" (1.13)
Tapones plástico para entretapas (1.09)
Ventas muy concentradas en pocos meses.
Dependientes de temporada o uso puntual.
Riesgo alto de sobrestock si no se planifica bien.
Candidatos claros a:
- Producción bajo demanda.
- Campañas muy focalizadas.
- Stock dinámico.
2. Productos claramente estacionales (0.7 < CV < 1)¶
Separador de plástico (0.98)
Tapas 77 mm pasteurizable (0.91)
- Siguen ciclos productivos del cliente.
- Buen volumen, pero con picos marcados.
- Necesitan previsión estacional, pero son menos volátiles que los anteriores.
3. Productos base con estacionalidad moderada (0.5 < CV < 0.7)¶
Tapas 77 mm esterilizable (0.69)
Tapa 43 mm dorada (0.55)
Frasco miel 1 kg (0.52)
Tapas 63 mm esterilizable (0.51)
- Núcleo del catálogo.
- Venden todo el año.
- Se refuerzan en ciertas épocas, pero no dependen de ellas.
4. Productos muy estables (CV < 0.4)¶
Grapas inox Weck (0.41)
Tapas 66 mm negras (0.28)
Conclusión:
- Demanda constante.
- Ideal para: Packs, Cross-selling o Productos “acompañantes” en carrito.
- Poco riesgo logístico.
Insight de negocio clave¶
La tienda:
No es estacional por debilidad, sino por especialización.
Tiene productos:
- Que disparan ventas en temporada.
- Y otros que mantienen ingresos el resto del año.
Eso es una estructura sana de catálogo.
C. Análisis de clientes RFM (Recencia, Frecuencia, Valor Monetario)¶
Este análisis permite identificar clientes importantes y detectar oportunidades de reactivación.
Como nuestra tienda tiene mucho tiempo acotaremos el filtrado a pedidos de menos de 2 años, y buscarmeos clientes que hicieron al menos 2 pedidos en este periodo.
Primero se define una fecha de referencia (now), que se establece como el día posterior a la última fecha de pedido registrada. Esto permite calcular correctamente cuántos días han pasado desde la última compra de cada cliente.
# Fecha máxima del dataset (referencia temporal)
now = orders_clientes["date_add"].max() + pd.Timedelta(days=1)
# Acotamos a los últimos 24 meses
fecha_corte = now - pd.DateOffset(months=24)
orders_filtrados = orders_clientes[
orders_clientes["date_add"] >= fecha_corte
]
# Nos quedamos solo con clientes con al menos 2 pedidos
orders_filtrados = (
orders_filtrados
.groupby("id_customer")
.filter(lambda x: len(x) >= 2)
)
A continuación, se agrupan los pedidos por cliente (groupby("id_customer")) y se calculan tres métricas clave:
- Recency: número de días transcurridos desde el último pedido del cliente. Se obtiene restando la fecha de su última compra a la fecha de referencia.
- Frequency: número total de pedidos realizados por el cliente, usando un recuento de IDs de pedido.
- Monetary: importe total gastado por el cliente, sumando el valor de todos sus pedidos.
Finalmente, se renombran las columnas para usar la terminología estándar del análisis RFM, obteniendo una tabla lista para segmentar clientes, identificar clientes VIP o detectar clientes valiosos que llevan tiempo sin comprar.
rfm = (
orders_filtrados
.groupby("id_customer")
.agg({
"date_add": lambda x: (now - x.max()).days, # Recencia
"id": "count", # Frecuencia
"total_paid": "sum", # Valor monetario
"customer_name": "first" # Identificador anónimo
})
.rename(columns={
"date_add": "recency",
"id": "frequency",
"total_paid": "monetary",
})
)
rfm.sort_values("monetary", ascending=False).head(10)
| recency | frequency | monetary | customer_name | |
|---|---|---|---|---|
| id_customer | ||||
| 26404 | 40 | 53 | 14368.0763 | Cliente_26404 |
| 11920 | 215 | 31 | 13657.4955 | Cliente_11920 |
| 9752 | 35 | 113 | 12242.5257 | Cliente_9752 |
| 22938 | 120 | 49 | 7789.9800 | Cliente_22938 |
| 5851 | 154 | 25 | 7570.3767 | Cliente_5851 |
| 18167 | 40 | 66 | 6105.3909 | Cliente_18167 |
| 17435 | 56 | 24 | 5669.9733 | Cliente_17435 |
| 352 | 134 | 13 | 5385.7876 | Cliente_352 |
| 29141 | 69 | 17 | 4930.1333 | Cliente_29141 |
| 35189 | 214 | 3 | 4755.7968 | Cliente_35189 |
1. Clientes VIP inactivos¶
Clientes con alto valor histórico que llevan tiempo sin realizar compras.
Se aplican tres filtros simultáneos sobre la tabla RFM:
Frecuencia alta: clientes cuya frecuencia de compra está por encima del percentil 75. Esto selecciona a los clientes que han realizado más pedidos que la mayoría.
Valor monetario alto: clientes cuyo gasto total está por encima del percentil 75. Así nos quedamos con los que más dinero han aportado a la tienda.
Alta recencia: clientes cuyo valor de recencia también está por encima del percentil 75. Como la recencia mide los días desde la última compra, valores altos indican que hace mucho tiempo que no compran.
La combinación de estas tres condiciones permite detectar clientes que fueron muy importantes para el negocio, pero que actualmente están inactivos y podrían ser objetivo de campañas de reactivación.
Finalmente, se ordenan los resultados por valor monetario de forma descendente para visualizar primero a los clientes con mayor impacto económico potencial.
vip_dormidos = rfm[
(rfm["frequency"] > rfm["frequency"].quantile(0.75)) &
(rfm["monetary"] > rfm["monetary"].quantile(0.75)) &
(rfm["recency"] > rfm["recency"].quantile(0.75))
]
vip_dormidos.sort_values("monetary", ascending=False).head()
| recency | frequency | monetary | customer_name | |
|---|---|---|---|---|
| id_customer | ||||
| 23872 | 652 | 6 | 3741.6922 | Cliente_23872 |
| 12301 | 441 | 18 | 2413.6400 | Cliente_12301 |
| 21814 | 435 | 19 | 2139.8174 | Cliente_21814 |
| 16265 | 386 | 5 | 1925.4252 | Cliente_16265 |
| 22042 | 704 | 7 | 1881.8125 | Cliente_22042 |
2. Clientes en riesgo¶
Clientes que compraban con cierta frecuencia, gastaban un importe razonable pero llevan mucho tiempo sin comprar.
Es decir: antes eran buenos, ahora están inactivos.
Criterio:
Frecuencia alta (por encima del percentil 50)
Monetary medio-alto (por encima del percentil 50)
Recency alta (percentil 75, hace mucho que no compran)
Este segmento es especialmente importante para acciones de reactivación, ya que son clientes con potencial probado que podrían recuperarse con campañas específicas.
clientes_en_riesgo = rfm[
(rfm["frequency"] > rfm["frequency"].quantile(0.50)) &
(rfm["monetary"] > rfm["monetary"].quantile(0.50)) &
(rfm["recency"] > rfm["recency"].quantile(0.75))
]
clientes_en_riesgo.sort_values("recency", ascending=False).head()
| recency | frequency | monetary | customer_name | |
|---|---|---|---|---|
| id_customer | ||||
| 22515 | 714 | 3 | 278.2167 | Cliente_22515 |
| 22042 | 704 | 7 | 1881.8125 | Cliente_22042 |
| 19449 | 700 | 3 | 157.8504 | Cliente_19449 |
| 22436 | 691 | 3 | 218.2864 | Cliente_22436 |
| 4781 | 690 | 3 | 313.1370 | Cliente_4781 |
3. Nuevos prometedores¶
Clientes que han comprado recientemente pero todavía no han comprado muchas veces y ya muestran cierto gasto.
Son los clientes a cuidar y fidelizar.
Criterio:
Recency baja (percentil 25, compra reciente)
Frequency baja-media (≤ percentil 50)
Monetary medio (≥ percentil 50)
nuevos_prometedores = rfm[
(rfm["recency"] <= rfm["recency"].quantile(0.25)) &
(rfm["frequency"] <= rfm["frequency"].quantile(0.50)) &
(rfm["monetary"] > rfm["monetary"].quantile(0.50))
]
nuevos_prometedores.sort_values("monetary", ascending=False).head()
| recency | frequency | monetary | customer_name | |
|---|---|---|---|---|
| id_customer | ||||
| 28610 | 73 | 2 | 1268.2358 | Cliente_28610 |
| 20029 | 85 | 2 | 732.4531 | Cliente_20029 |
| 18000 | 46 | 2 | 699.5815 | Cliente_18000 |
| 37098 | 116 | 2 | 667.2578 | Cliente_37098 |
| 20626 | 87 | 2 | 650.6790 | Cliente_20626 |
4. Grafico de clientes¶
Vamos a visualizar los clientes s en un scatter plot R vs M
¿Qué representa un scatter R vs M?
Eje X (Recency) Días desde la última compra:
Izquierda = compraron hace poco
Derecha = llevan mucho tiempo sin comprar
Eje Y (Monetary) Gasto total del cliente:
Abajo = poco valor
Arriba = mucho valor
Cada punto = un cliente.
plt.figure(figsize=(8,6))
plt.scatter(rfm["recency"], rfm["monetary"], alpha=0.5)
plt.xlabel("Recency (días desde última compra)")
plt.ylabel("Monetary (gasto total)")
plt.title("Distribución de clientes según Recencia y Valor")
plt.show()
La distribución muestra una fuerte concentración de clientes de bajo valor y una minoría de clientes con alto impacto económico. El gasto elevado se asocia principalmente a clientes relativamente recientes, mientras que los clientes valiosos inactivos son escasos pero estratégicamente críticos. Este patrón justifica el uso del modelo RFM y el análisis por percentiles para segmentar clientes de forma realista.
Conclusiones¶
Con estos tres segmentos (VIP dormidos, clientes en riesgo y nuevos prometedores) se cubren los perfiles clave del ciclo de vida del cliente.
El análisis RFM permite priorizar acciones comerciales sin necesidad de modelos complejos, basándose únicamente en datos históricos de pedidos.
A partir de los datos extraídos de la API de PrestaShop se han obtenido indicadores claros sobre:
- patrones de venta
- productos más relevantes y su tipología
- clientes estratégicos
Este enfoque permite tomar decisiones de negocio basadas en datos reales de la tienda.