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.
In [1]:
# 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.

In [2]:
# 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)
In [3]:
# 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:

  1. Se inicializa una lista vacía donde se acumulan los registros.
  2. Se realizan llamadas repetidas a la API usando paginación (limit y offset).
  3. En cada llamada:
    • Se solicitan únicamente los campos necesarios mediante display.
    • Se añade el bloque de datos obtenido a la lista acumulada.
  4. El proceso finaliza cuando la API devuelve una respuesta vacía.
  5. 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.
In [4]:
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)
In [5]:
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)
In [6]:
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)

In [7]:
# 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

In [8]:
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.

In [9]:
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()
Out[9]:
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.

In [10]:
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).

In [11]:
#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()
Out[11]:
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.

In [27]:
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
Out[27]:
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
In [28]:
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()
No description has been provided for this image

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.

In [14]:
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
Out[14]:
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
In [15]:
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()
No description has been provided for this image

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.

In [16]:
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).

In [17]:
# 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.

In [18]:
# 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()
)
In [19]:
# 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()
No description has been provided for this image

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.

In [20]:
coef_variacion = (
    ventas_top
    .groupby("product_name")["product_quantity"]
    .std()
    /
    ventas_top.groupby("product_name")["product_quantity"].mean()
)
coef_variacion.sort_values(ascending=False)
Out[20]:
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.

In [21]:
# 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.

In [22]:
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)
Out[22]:
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.

In [23]:
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()
Out[23]:
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.

In [24]:
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()
Out[24]:
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)

In [25]:
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()
Out[25]:
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.

In [26]:
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()
No description has been provided for this image

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.