DISEÑO DEL PROYECTO¶
Este proyecto versa sobre la optimización de un ecommerce, concretamente, indicar que se trata de un ecommerce del sector de los comésticos en Rusia.
Esta empresa ha teniendo una evolución plana durante los últimos meses. Se requiere analizar sus datos transaccionales e implementar acciones CRO personalizadas a su situación en base a dicho análisis.
OBJETIVO¶
Analizar los datos transaccionales para intentar potenciales acciones CRO que incrementen visitas, conversiones y ticket medio, y por tanto incrementar la facturación global del ecommerce.
Crear activos analíticos avanzados como una segmentación RFM y un sistema de recomendación que impulsen la consecución del objetivo.
PALANCAS¶
from IPython import display
display.Image("customerjourney.png")
El primer paso es cuando un usuario llega a la web del ecommerce. Normalmente vendrá desde:
- Campañas de pago: paid ads como Facebook Ads o Google Ads
- Contenido orgánico: blog, rrss, ...
- Tráfico directo: conoce la url y la introduce en el navegador
Ese tráfico se llama visitas, y las páginas que van viendo se llaman páginas vistas, aunque en nuestro caso lo llamaremos views.
El usuario navega por la web y cuando le gusta un producto lo mete en el carrito.
Finalmente puede sacar productos del carrito, salir sin comprar nada, o finalmente hacer el pedido.
Un proceso común es la venta cruzada, en la cual se recomiendan al usuario otros productos que también podrían interesarle.
Incluso cuando se ha ido podemos volver a contartar al usuario mediante retargeting o email marketing.
Todo este proceso se llama funnel o también customer journey.
En el entorno online prácticamente todo se puede registrar.
El registro del usuario puede ser logueado o no.
La secuencia de acciones que hace un usuario en la misma sesión de navegación se llama sesión.
El ratio de compras sobre las visitas se llama ratio de conversión.
Además existen otras métricas clave que tenemos que dominar para gestionar correctamente un ecommerce:
- CPA
- AOV
- Frecuencia de compra
- LTV
- Churn
CONCEPTO CLAVE: Solo existen 3 formas de incrementar un negocio:
- Más clientes: esto implica conseguir más visitas y mayor conversión
- Más frecuencia: esto implica conseguir que los mismos clientes compren más veces
- Mayor ticket medio: esto implica conseguir que se compre más o más caro en la misma sesión de compra
Para conseguir esos 3 efectos trabajamos sobre las siguientes palancas operativas:
- Customer journey: cómo podemos optimizar cada uno de los pasos del proceso
- Clientes: cómo podemos usar la info disponible de los clientes para optimizar las campañas que realicemos
- Productos: cómo podemos optimizar el catálogo de productos e identificar de manera personalizada qué productos tenemos que poner delante de cada cliente
Entenderemos en nuestro caso CRO de manera amplia, es decir como la disciplina que pone en práctica acciones para trabajar sobre las palancas y conceptos anteriores.
KPIs¶
- Visitas
- Conversión
- Frecuencia de compra
- Ticket medio
- Tasa abandono carrito
- LTV
ENTIDADES Y DATOS¶
En nuestro caso las entidades que tenemos en la granularidad de los datos son:
- Usuarios
- Clientes
- Sesiones
- Eventos
- Productos
PREGUNTAS SEMILLA¶
Habiendo entendido las palancas, kpis y entidades ya podemos plantear las preguntas semilla:
Sobre el customer journey:
- ¿Cómo es un proceso típico de compra?
- ¿Cuántos productos se ven, se añaden al carro, se abandonan y se compran de media en cada sesión?
- ¿Cómo ha sido la tendencia de estos indicadores en los últimos meses?
Sobre los clientes:
- ¿Cuántos productos compra cada cliente?
- ¿Cuánto se gasta cada cliente?
- ¿Hay "mejores clientes" que haya que identificar y tratar de forma diferente?
- ¿Los clientes repiten compras en los siguientes meses?
- ¿Cual es el LTV medio de un cliente?
- ¿Podemos diseñar campañas personalizas al valor del cliente?
Sobre los productos:
- ¿Cuales son los productos más vendidos?
- ¿Hay productos que no se venden?
- ¿Existe relación entre el precio del producto y su volumen de ventas?
- ¿Hay productos que se visiten pero no se compren?
- ¿Hay productos que se saquen recurrentemente del carrito?
- ¿Se podrían hacer recomendaciones personalizadas de productos para cada cliente?
ANALISIS E INSIGHTS (TOTAL)¶
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
import plotly.io as pio
import plotly.offline as pyo
pyo.init_notebook_mode(connected=True)
#Automcompletar rápido
%config IPCompleter.greedy=True
#Formato de display
pd.options.display.float_format = '{:15.2f}'.format
sns.set_theme(style="whitegrid")
df = pd.read_pickle('tablon_analitico.pickle')
df
usuario | sesion | categoria | evento | producto | precio | date | año | mes | dia | hora | minuto | segundo | festivo | black_friday | san_valentin | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
fecha | ||||||||||||||||
2019-10-01 00:01:46 | 462033176 | a18e0999-61a1-4218-8f8f-61ec1d375361 | 1487580005092295511 | view | 5843665 | 9.44 | 2019-10-01 | 2019 | 10 | 1 | 0 | 1 | 46 | 0 | 0 | 0 |
2019-10-01 00:01:55 | 514753614 | e2fecb2d-22d0-df2c-c661-15da44b3ccf1 | 1487580013069861041 | cart | 5868461 | 3.57 | 2019-10-01 | 2019 | 10 | 1 | 0 | 1 | 55 | 0 | 0 | 0 |
2019-10-01 00:02:50 | 527418424 | 86e77869-afbc-4dff-9aa2-6b7dd8c90770 | 1487580006300255120 | view | 5877456 | 122.22 | 2019-10-01 | 2019 | 10 | 1 | 0 | 2 | 50 | 0 | 0 | 0 |
2019-10-01 00:03:41 | 555448072 | b5f72ceb-0730-44de-a932-d16db62390df | 1487580013749338323 | view | 5649270 | 6.19 | 2019-10-01 | 2019 | 10 | 1 | 0 | 3 | 41 | 0 | 0 | 0 |
2019-10-01 00:03:44 | 552006247 | 2d8f304b-de45-4e59-8f40-50c603843fe5 | 1487580005411062629 | view | 18082 | 16.03 | 2019-10-01 | 2019 | 10 | 1 | 0 | 3 | 44 | 0 | 0 | 0 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
2020-02-29 23:58:49 | 147995998 | 5ff96629-3627-493e-a25b-5a871ec78c90 | 1487580006317032337 | cart | 5815662 | 0.92 | 2020-02-29 | 2020 | 2 | 29 | 23 | 58 | 49 | 0 | 0 | 0 |
2020-02-29 23:58:57 | 147995998 | 5ff96629-3627-493e-a25b-5a871ec78c90 | 1487580006317032337 | view | 5815665 | 0.59 | 2020-02-29 | 2020 | 2 | 29 | 23 | 58 | 57 | 0 | 0 | 0 |
2020-02-29 23:59:05 | 147995998 | 5ff96629-3627-493e-a25b-5a871ec78c90 | 1487580006317032337 | cart | 5815665 | 0.59 | 2020-02-29 | 2020 | 2 | 29 | 23 | 59 | 5 | 0 | 0 | 0 |
2020-02-29 23:59:28 | 619841242 | 18af673b-7fb9-4202-a66d-5c855bc0fd2d | 1487580010872045658 | view | 5817692 | 0.79 | 2020-02-29 | 2020 | 2 | 29 | 23 | 59 | 28 | 0 | 0 | 0 |
2020-02-29 23:59:54 | 619841242 | 18af673b-7fb9-4202-a66d-5c855bc0fd2d | 1487580010872045658 | view | 5716351 | 0.79 | 2020-02-29 | 2020 | 2 | 29 | 23 | 59 | 54 | 0 | 0 | 0 |
2074026 rows × 16 columns
Entendiendo los eventos¶
¿Cómo está funcionando el Customer Journey?¶
eventos = df.evento.value_counts()
eventos
evento view 961558 cart 574547 remove_from_cart 410357 purchase 127564 Name: count, dtype: int64
kpi_visualizaciones_p = 100
kpi_carrito_p = eventos.loc['cart'] / eventos.loc['view'] * 100
kpi_abandono_p = eventos.loc['remove_from_cart'] / eventos.loc['cart'] * 100
kpi_compra_p = eventos.loc['purchase'] / eventos.loc['cart'] * 100
kpis = pd.DataFrame({'kpi':['visitas','carrito','compra'],
'valor':[kpi_visualizaciones_p,kpi_carrito_p,kpi_compra_p]})
kpis
kpi | valor | |
---|---|---|
0 | visitas | 100.00 |
1 | carrito | 59.75 |
2 | compra | 22.20 |
import plotly.graph_objects as go
import plotly.io as pio
# Asegura que el renderizador es HTML para exportación
pio.renderers.default = "iframe"
fig = go.Figure(go.Funnel(
y=kpis.kpi,
x=kpis.valor.round(2),
marker={'color': ['red', 'blue', 'green']},
opacity=0.3
))
fig.update_layout(title='Funnel Conversión Inicial')
# Guarda el gráfico como un archivo HTML independiente
fig.write_html("grafico_funnel.html")
Funnel de Conversión
Este gráfico muestra la conversión de los usuarios en cada etapa.
Conclusiones:
- Las tasas de partida son un 60% de carrito sobre visualiazaciones y un 22% de compra sobre carrito
- Por tanto existe un 40% de visitas sobre las que hay que trabajar para conseguir más carritos, y un 78% de carritos sobre los que trabajar para conseguir más compras
¿Cuántos productos se ven, se añaden al carro, se abandonan y se compran de media en cada sesión?¶
A diferencia del análisis macro del funnel este análisis es por sesión, lo cual lo hace más operativo.
Conocer los principales kpis por sesión nos permite establecer la línea base para ir midiendo los resultados de las acciones de CRO.
Primero creamos un dataframe con la granularidad a nivel de sesión y evento que necesitamos.
sesion_prod = df.groupby(['sesion','evento']).producto.count()
sesion_prod
sesion evento 0000597b-de39-4a77-9fe5-02c8792ca14e view 3 0000645a-8160-4a3d-91bf-154bff0a22e3 view 2 000090e1-da13-42b1-a31b-91a9ee5e6a88 view 1 0000b3cb-5422-4bf2-b8fe-5c1831d0dc1b view 1 0000de26-bd58-42c9-9173-4763c76b398e view 1 .. ffff6695-b64d-4a67-aa14-34b3b7f63c3f view 2 ffff7d69-b706-4c64-9d6d-da57a04bc32b view 1 ffff8044-2a22-4846-8a72-999e870abbe9 view 1 ffff91d4-7879-4a4b-8b26-c67915a27dc8 view 1 ffffbe0a-d2c2-47c7-afab-680bfdfda50d view 1 Name: producto, Length: 581763, dtype: int64
Pasamos los eventos a columnas.
sesion_prod = sesion_prod.unstack().fillna(0)
sesion_prod
evento | cart | purchase | remove_from_cart | view |
---|---|---|---|---|
sesion | ||||
0000597b-de39-4a77-9fe5-02c8792ca14e | 0.00 | 0.00 | 0.00 | 3.00 |
0000645a-8160-4a3d-91bf-154bff0a22e3 | 0.00 | 0.00 | 0.00 | 2.00 |
000090e1-da13-42b1-a31b-91a9ee5e6a88 | 0.00 | 0.00 | 0.00 | 1.00 |
0000b3cb-5422-4bf2-b8fe-5c1831d0dc1b | 0.00 | 0.00 | 0.00 | 1.00 |
0000de26-bd58-42c9-9173-4763c76b398e | 0.00 | 0.00 | 0.00 | 1.00 |
... | ... | ... | ... | ... |
ffff6695-b64d-4a67-aa14-34b3b7f63c3f | 0.00 | 0.00 | 0.00 | 2.00 |
ffff7d69-b706-4c64-9d6d-da57a04bc32b | 0.00 | 0.00 | 0.00 | 1.00 |
ffff8044-2a22-4846-8a72-999e870abbe9 | 0.00 | 0.00 | 0.00 | 1.00 |
ffff91d4-7879-4a4b-8b26-c67915a27dc8 | 0.00 | 0.00 | 0.00 | 1.00 |
ffffbe0a-d2c2-47c7-afab-680bfdfda50d | 0.00 | 0.00 | 0.00 | 1.00 |
446054 rows × 4 columns
Para comprobar calculamos los totales y debería darnos los mismos que a nivel global.
sesion_prod.sum()
evento cart 574547.00 purchase 127564.00 remove_from_cart 410357.00 view 961558.00 dtype: float64
Reordenamos las columnas.
sesion_prod = sesion_prod[['view','cart','remove_from_cart','purchase']]
sesion_prod
evento | view | cart | remove_from_cart | purchase |
---|---|---|---|---|
sesion | ||||
0000597b-de39-4a77-9fe5-02c8792ca14e | 3.00 | 0.00 | 0.00 | 0.00 |
0000645a-8160-4a3d-91bf-154bff0a22e3 | 2.00 | 0.00 | 0.00 | 0.00 |
000090e1-da13-42b1-a31b-91a9ee5e6a88 | 1.00 | 0.00 | 0.00 | 0.00 |
0000b3cb-5422-4bf2-b8fe-5c1831d0dc1b | 1.00 | 0.00 | 0.00 | 0.00 |
0000de26-bd58-42c9-9173-4763c76b398e | 1.00 | 0.00 | 0.00 | 0.00 |
... | ... | ... | ... | ... |
ffff6695-b64d-4a67-aa14-34b3b7f63c3f | 2.00 | 0.00 | 0.00 | 0.00 |
ffff7d69-b706-4c64-9d6d-da57a04bc32b | 1.00 | 0.00 | 0.00 | 0.00 |
ffff8044-2a22-4846-8a72-999e870abbe9 | 1.00 | 0.00 | 0.00 | 0.00 |
ffff91d4-7879-4a4b-8b26-c67915a27dc8 | 1.00 | 0.00 | 0.00 | 0.00 |
ffffbe0a-d2c2-47c7-afab-680bfdfda50d | 1.00 | 0.00 | 0.00 | 0.00 |
446054 rows × 4 columns
Calculamos la media de cada evento por sesión.
media_eventos_sesion = sesion_prod.mean()
media_eventos_sesion
evento view 2.16 cart 1.29 remove_from_cart 0.92 purchase 0.29 dtype: float64
Conclusión:
En cada sesión, de media:
- Se ven 2.2 productos
- Se añaden 1.3 productos al carrito
- Se eliminan 0.9 productos del carrito
- Se compran 0.3 productos
Como decíamos, éstos son los números que deberemos incrementar con las acciones de CRO.
¿Existen diferencias entre los eventos por horas?¶
Creamos el dataframe a granularidad evento y hora.
eventos_hora = df.groupby(['evento','hora']).producto.count()
eventos_hora
evento hora cart 0 6475 1 5555 2 6433 3 8544 4 11242 ... view 19 63730 20 57311 21 38905 22 23043 23 13307 Name: producto, Length: 96, dtype: int64
Pasamos los eventos a columnas.
eventos_hora = eventos_hora.unstack(level = 0)
eventos_hora
evento | cart | purchase | remove_from_cart | view |
---|---|---|---|---|
hora | ||||
0 | 6475 | 962 | 3238 | 8731 |
1 | 5555 | 1128 | 3930 | 7280 |
2 | 6433 | 1220 | 3509 | 8378 |
3 | 8544 | 1535 | 5331 | 11807 |
4 | 11242 | 2389 | 8095 | 18365 |
5 | 16890 | 3491 | 11913 | 27438 |
6 | 21993 | 5125 | 16223 | 38055 |
7 | 27069 | 5951 | 17883 | 46072 |
8 | 29526 | 7158 | 21156 | 49587 |
9 | 32095 | 7593 | 21680 | 54185 |
10 | 32901 | 7816 | 23982 | 56458 |
11 | 33284 | 8495 | 25496 | 57594 |
12 | 34258 | 8250 | 23714 | 57530 |
13 | 31996 | 8133 | 22852 | 55534 |
14 | 30451 | 7122 | 21835 | 52184 |
15 | 28789 | 6485 | 20162 | 49809 |
16 | 28775 | 6531 | 19791 | 51055 |
17 | 32525 | 6242 | 24330 | 55667 |
18 | 36435 | 8211 | 30551 | 59533 |
19 | 39609 | 7435 | 27666 | 63730 |
20 | 34828 | 7256 | 24985 | 57311 |
21 | 23228 | 4606 | 17396 | 38905 |
22 | 13589 | 2883 | 8680 | 23043 |
23 | 8057 | 1547 | 5959 | 13307 |
Vamos a visualizar cómo se distribuyen los eventos por hora.
eventos_hora.plot()
plt.xticks(ticks = eventos_hora.index);
Existe una pauta global como era de esperar.
Pero para ver mejor las diferencias podemos crear una nueva variable que sea el ratio de compras por visita en cada hora.
eventos_hora['compras_visitas'] = eventos_hora.purchase / eventos_hora.view * 100
eventos_hora
evento | cart | purchase | remove_from_cart | view | compras_visitas |
---|---|---|---|---|---|
hora | |||||
0 | 6475 | 962 | 3238 | 8731 | 11.02 |
1 | 5555 | 1128 | 3930 | 7280 | 15.49 |
2 | 6433 | 1220 | 3509 | 8378 | 14.56 |
3 | 8544 | 1535 | 5331 | 11807 | 13.00 |
4 | 11242 | 2389 | 8095 | 18365 | 13.01 |
5 | 16890 | 3491 | 11913 | 27438 | 12.72 |
6 | 21993 | 5125 | 16223 | 38055 | 13.47 |
7 | 27069 | 5951 | 17883 | 46072 | 12.92 |
8 | 29526 | 7158 | 21156 | 49587 | 14.44 |
9 | 32095 | 7593 | 21680 | 54185 | 14.01 |
10 | 32901 | 7816 | 23982 | 56458 | 13.84 |
11 | 33284 | 8495 | 25496 | 57594 | 14.75 |
12 | 34258 | 8250 | 23714 | 57530 | 14.34 |
13 | 31996 | 8133 | 22852 | 55534 | 14.65 |
14 | 30451 | 7122 | 21835 | 52184 | 13.65 |
15 | 28789 | 6485 | 20162 | 49809 | 13.02 |
16 | 28775 | 6531 | 19791 | 51055 | 12.79 |
17 | 32525 | 6242 | 24330 | 55667 | 11.21 |
18 | 36435 | 8211 | 30551 | 59533 | 13.79 |
19 | 39609 | 7435 | 27666 | 63730 | 11.67 |
20 | 34828 | 7256 | 24985 | 57311 | 12.66 |
21 | 23228 | 4606 | 17396 | 38905 | 11.84 |
22 | 13589 | 2883 | 8680 | 23043 | 12.51 |
23 | 8057 | 1547 | 5959 | 13307 | 11.63 |
Reordenamos las variables
eventos_hora = eventos_hora[['view','cart','remove_from_cart','purchase','compras_visitas']]
eventos_hora
evento | view | cart | remove_from_cart | purchase | compras_visitas |
---|---|---|---|---|---|
hora | |||||
0 | 8731 | 6475 | 3238 | 962 | 11.02 |
1 | 7280 | 5555 | 3930 | 1128 | 15.49 |
2 | 8378 | 6433 | 3509 | 1220 | 14.56 |
3 | 11807 | 8544 | 5331 | 1535 | 13.00 |
4 | 18365 | 11242 | 8095 | 2389 | 13.01 |
5 | 27438 | 16890 | 11913 | 3491 | 12.72 |
6 | 38055 | 21993 | 16223 | 5125 | 13.47 |
7 | 46072 | 27069 | 17883 | 5951 | 12.92 |
8 | 49587 | 29526 | 21156 | 7158 | 14.44 |
9 | 54185 | 32095 | 21680 | 7593 | 14.01 |
10 | 56458 | 32901 | 23982 | 7816 | 13.84 |
11 | 57594 | 33284 | 25496 | 8495 | 14.75 |
12 | 57530 | 34258 | 23714 | 8250 | 14.34 |
13 | 55534 | 31996 | 22852 | 8133 | 14.65 |
14 | 52184 | 30451 | 21835 | 7122 | 13.65 |
15 | 49809 | 28789 | 20162 | 6485 | 13.02 |
16 | 51055 | 28775 | 19791 | 6531 | 12.79 |
17 | 55667 | 32525 | 24330 | 6242 | 11.21 |
18 | 59533 | 36435 | 30551 | 8211 | 13.79 |
19 | 63730 | 39609 | 27666 | 7435 | 11.67 |
20 | 57311 | 34828 | 24985 | 7256 | 12.66 |
21 | 38905 | 23228 | 17396 | 4606 | 11.84 |
22 | 23043 | 13589 | 8680 | 2883 | 12.51 |
23 | 13307 | 8057 | 5959 | 1547 | 11.63 |
Visualizamos para ver si hay horas en las que se compra proporcionalmente más.
plt.figure(figsize = (12,6))
sns.lineplot(data = eventos_hora, x = eventos_hora.index, y = 'compras_visitas')
plt.xticks(eventos_hora.index);
Conclusiones:
- Las horas en las que la gente compra más son la 1, las 8, de 11 a 13 y las 18
- Las horas en las que la gente no compra son las 24, de 3 a 7, de 14 a 17 y de 19 a 23
Vamos a analizar ahora no de forma proporcional, si no en absoluto si existen o no horas más frecuentes para cada tipo de evento.
plt.figure(figsize = (12,12))
sns.heatmap(data = eventos_hora);
El problema es que como cada evento tiene diferente escala este gráfico no nos permite diferenciar bien los patrones.
Para solucionarlo podemos usar la tipificación de variables que aprendimos en el módulo de estadística.
def tipificar(variable):
media = variable.mean()
dt = variable.std()
return(variable.apply(lambda x: (x - media) / dt))
eventos_hora_tip = eventos_hora.apply(tipificar)
eventos_hora_tip
evento | view | cart | remove_from_cart | purchase | compras_visitas |
---|---|---|---|---|---|
hora | |||||
0 | -1.60 | -1.56 | -1.63 | -1.62 | -1.83 |
1 | -1.68 | -1.64 | -1.54 | -1.56 | 1.91 |
2 | -1.62 | -1.56 | -1.59 | -1.53 | 1.13 |
3 | -1.45 | -1.37 | -1.38 | -1.41 | -0.17 |
4 | -1.11 | -1.13 | -1.06 | -1.09 | -0.17 |
5 | -0.65 | -0.63 | -0.61 | -0.68 | -0.41 |
6 | -0.10 | -0.17 | -0.10 | -0.07 | 0.22 |
7 | 0.31 | 0.28 | 0.09 | 0.24 | -0.24 |
8 | 0.49 | 0.50 | 0.48 | 0.69 | 1.03 |
9 | 0.72 | 0.73 | 0.54 | 0.85 | 0.67 |
10 | 0.84 | 0.80 | 0.81 | 0.93 | 0.53 |
11 | 0.90 | 0.83 | 0.99 | 1.19 | 1.29 |
12 | 0.89 | 0.92 | 0.78 | 1.09 | 0.95 |
13 | 0.79 | 0.72 | 0.67 | 1.05 | 1.20 |
14 | 0.62 | 0.58 | 0.56 | 0.67 | 0.37 |
15 | 0.50 | 0.43 | 0.36 | 0.44 | -0.16 |
16 | 0.56 | 0.43 | 0.32 | 0.45 | -0.35 |
17 | 0.80 | 0.77 | 0.85 | 0.35 | -1.67 |
18 | 1.00 | 1.11 | 1.58 | 1.08 | 0.49 |
19 | 1.21 | 1.40 | 1.24 | 0.79 | -1.29 |
20 | 0.88 | 0.97 | 0.93 | 0.72 | -0.46 |
21 | -0.06 | -0.06 | 0.03 | -0.26 | -1.14 |
22 | -0.87 | -0.92 | -0.99 | -0.91 | -0.58 |
23 | -1.37 | -1.42 | -1.31 | -1.40 | -1.32 |
plt.figure(figsize = (12,12))
sns.heatmap(data = eventos_hora_tip);
Vamos a sacar también los gráficos de líneas para verlo más claramente.
eventos_hora_tip.plot(subplots = True, sharex = False, figsize = (12,12),xticks = eventos_hora_tip.index);
Conclusiones:
- INSIGHT #1: Todas las métricas se maximzan en las franjas entre las 9 y las 13 y entre las 18 y las 20
- Esta info es muy relevante por ejemplo de cara a paid ads, tanto de generación de tráfico como de retargeting
- Además, parece haber algún subtipo de usuario que compra a la 1 de la mañana, que aunque no sea muy frecuente sí compra mucho
¿Cúal es la media de facturación mensual?¶
df.loc[df.evento == 'purchase'].groupby('mes').precio.sum().mean()
np.float64(124309.92)
¿Cúal es la tendencia en los últimos meses?¶
tendencia = df.groupby('evento').resample('W').evento.count().unstack(level = 0)
tendencia
evento | cart | purchase | remove_from_cart | view |
---|---|---|---|---|
fecha | ||||
2019-10-06 | 31483 | 4440 | 14647 | 36353 |
2019-10-13 | 28151 | 5422 | 17989 | 44410 |
2019-10-20 | 23920 | 5033 | 15303 | 39486 |
2019-10-27 | 25651 | 5665 | 18411 | 40383 |
2019-11-03 | 24087 | 5746 | 16491 | 39365 |
2019-11-10 | 29142 | 6663 | 24008 | 46177 |
2019-11-17 | 25335 | 5141 | 17215 | 41170 |
2019-11-24 | 38069 | 9754 | 27973 | 56477 |
2019-12-01 | 31994 | 7493 | 23106 | 48883 |
2019-12-08 | 23265 | 5105 | 19443 | 42055 |
2019-12-15 | 24636 | 5953 | 18246 | 45874 |
2019-12-22 | 19927 | 4701 | 15452 | 39237 |
2019-12-29 | 17051 | 3705 | 11102 | 32803 |
2020-01-05 | 16735 | 3294 | 13464 | 31909 |
2020-01-12 | 26264 | 5589 | 17956 | 46873 |
2020-01-19 | 28402 | 6913 | 22945 | 50210 |
2020-01-26 | 26353 | 6359 | 18544 | 48478 |
2020-02-02 | 29193 | 7120 | 21102 | 52432 |
2020-02-09 | 28796 | 5853 | 20050 | 48422 |
2020-02-16 | 27836 | 6332 | 22601 | 47213 |
2020-02-23 | 25619 | 6000 | 18146 | 43627 |
2020-03-01 | 22638 | 5283 | 16163 | 39721 |
tendencia = tendencia[['view','cart','remove_from_cart','purchase']]
tendencia
evento | view | cart | remove_from_cart | purchase |
---|---|---|---|---|
fecha | ||||
2019-10-06 | 36353 | 31483 | 14647 | 4440 |
2019-10-13 | 44410 | 28151 | 17989 | 5422 |
2019-10-20 | 39486 | 23920 | 15303 | 5033 |
2019-10-27 | 40383 | 25651 | 18411 | 5665 |
2019-11-03 | 39365 | 24087 | 16491 | 5746 |
2019-11-10 | 46177 | 29142 | 24008 | 6663 |
2019-11-17 | 41170 | 25335 | 17215 | 5141 |
2019-11-24 | 56477 | 38069 | 27973 | 9754 |
2019-12-01 | 48883 | 31994 | 23106 | 7493 |
2019-12-08 | 42055 | 23265 | 19443 | 5105 |
2019-12-15 | 45874 | 24636 | 18246 | 5953 |
2019-12-22 | 39237 | 19927 | 15452 | 4701 |
2019-12-29 | 32803 | 17051 | 11102 | 3705 |
2020-01-05 | 31909 | 16735 | 13464 | 3294 |
2020-01-12 | 46873 | 26264 | 17956 | 5589 |
2020-01-19 | 50210 | 28402 | 22945 | 6913 |
2020-01-26 | 48478 | 26353 | 18544 | 6359 |
2020-02-02 | 52432 | 29193 | 21102 | 7120 |
2020-02-09 | 48422 | 28796 | 20050 | 5853 |
2020-02-16 | 47213 | 27836 | 22601 | 6332 |
2020-02-23 | 43627 | 25619 | 18146 | 6000 |
2020-03-01 | 39721 | 22638 | 16163 | 5283 |
tendencia.plot(subplots = True, figsize = (12,6), sharex = True, xticks = tendencia.index, x_compat=True, rot = 90);
La tendencia es plana en todas las métricas, lo que confirma la necesidad de las acciones de CRO.
Existe un pico significativo en la semana del 24, obviamente por black friday, vamos a hacer el mismo análisis pero diario y solo para noviembre y dicienbre para ver el efecto.
tendencia_diaria = df.loc['2019-11':'2019-12'].groupby('evento').resample('D').evento.count().unstack(level = 0)
tendencia_diaria
evento | cart | purchase | remove_from_cart | view |
---|---|---|---|---|
fecha | ||||
2019-11-01 | 3565 | 709 | 2810 | 5352 |
2019-11-02 | 3015 | 912 | 2124 | 4857 |
2019-11-03 | 3540 | 755 | 2622 | 5583 |
2019-11-04 | 4652 | 676 | 4854 | 6248 |
2019-11-05 | 4118 | 753 | 2711 | 7213 |
... | ... | ... | ... | ... |
2019-12-27 | 2023 | 507 | 1335 | 4058 |
2019-12-28 | 1744 | 329 | 1193 | 3704 |
2019-12-29 | 2134 | 263 | 1149 | 3939 |
2019-12-30 | 1364 | 258 | 823 | 3434 |
2019-12-31 | 563 | 114 | 447 | 1724 |
61 rows × 4 columns
tendencia_diaria = tendencia_diaria[['view','cart','remove_from_cart','purchase']]
tendencia_diaria
evento | view | cart | remove_from_cart | purchase |
---|---|---|---|---|
fecha | ||||
2019-11-01 | 5352 | 3565 | 2810 | 709 |
2019-11-02 | 4857 | 3015 | 2124 | 912 |
2019-11-03 | 5583 | 3540 | 2622 | 755 |
2019-11-04 | 6248 | 4652 | 4854 | 676 |
2019-11-05 | 7213 | 4118 | 2711 | 753 |
... | ... | ... | ... | ... |
2019-12-27 | 4058 | 2023 | 1335 | 507 |
2019-12-28 | 3704 | 1744 | 1193 | 329 |
2019-12-29 | 3939 | 2134 | 1149 | 263 |
2019-12-30 | 3434 | 1364 | 823 | 258 |
2019-12-31 | 1724 | 563 | 447 | 114 |
61 rows × 4 columns
tendencia_diaria.plot(subplots = True, figsize = (16,10), sharex = True, xticks = tendencia_diaria.index, x_compat=True, rot = 90);
Conclusiones:
- Efectivamente el pico coincide con el black friday (día 29)
- Pero aún hay un pico mayor unos días antes, el día 22, posiblemente por el inicio de la semana black friday
- Sorprendemente los propios días de Navidad tienen una tendencia decreciente, lo que significa que los consumidores claramente han adelantado sus compras
Vamos a hacer el mismo análisis para Enero y Febrero.
tendencia_diaria = df.loc['2020-01':'2020-02'].groupby('evento').resample('D').evento.count().unstack(level = 0)
tendencia_diaria = tendencia_diaria[['view','cart','remove_from_cart','purchase']]
tendencia_diaria.plot(subplots = True, figsize = (16,10), sharex = True, xticks = tendencia_diaria.index, x_compat=True, rot = 90);
Conclusiones:
- Durante la semana de Reyes tampoco hay pico de ventas
- Ni los días previos a San Valentín
- Pero sí hay un pico muy pronunciado el 27 de Enero, seguramente algún evento local
INSIGHT #2 La gran conclusión es que todo el pastel de las compras navideñas se reparte en la semana del black friday
¿Momentos de la verdad?¶
¿Podríamos llegar a identificar momentos a nivel de día-hora en los que se producen el mayor número de compras?
Sería muy útil para concentrar gran parte de la inversión de campañas justo en esos momentos.
compras_dia_hora = df.loc[df.evento == 'purchase'].groupby(['date','hora']).evento.count().unstack(level = 0).fillna(0)
compras_dia_hora
date | 2019-10-01 | 2019-10-02 | 2019-10-03 | 2019-10-04 | 2019-10-05 | 2019-10-06 | 2019-10-07 | 2019-10-08 | 2019-10-09 | 2019-10-10 | ... | 2020-02-20 | 2020-02-21 | 2020-02-22 | 2020-02-23 | 2020-02-24 | 2020-02-25 | 2020-02-26 | 2020-02-27 | 2020-02-28 | 2020-02-29 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
hora | |||||||||||||||||||||
0 | 13.00 | 18.00 | 1.00 | 2.00 | 0.00 | 0.00 | 0.00 | 0.00 | 3.00 | 23.00 | ... | 28.00 | 0.00 | 0.00 | 0.00 | 0.00 | 55.00 | 5.00 | 40.00 | 0.00 | 0.00 |
1 | 0.00 | 0.00 | 5.00 | 0.00 | 4.00 | 24.00 | 3.00 | 0.00 | 16.00 | 0.00 | ... | 0.00 | 5.00 | 0.00 | 2.00 | 21.00 | 11.00 | 0.00 | 5.00 | 26.00 | 33.00 |
2 | 0.00 | 0.00 | 0.00 | 24.00 | 0.00 | 0.00 | 0.00 | 18.00 | 4.00 | 18.00 | ... | 9.00 | 0.00 | 0.00 | 10.00 | 0.00 | 34.00 | 0.00 | 0.00 | 0.00 | 8.00 |
3 | 0.00 | 24.00 | 10.00 | 0.00 | 0.00 | 26.00 | 2.00 | 20.00 | 16.00 | 55.00 | ... | 0.00 | 14.00 | 13.00 | 0.00 | 5.00 | 15.00 | 0.00 | 10.00 | 0.00 | 0.00 |
4 | 15.00 | 0.00 | 45.00 | 27.00 | 3.00 | 24.00 | 12.00 | 46.00 | 56.00 | 0.00 | ... | 6.00 | 22.00 | 34.00 | 17.00 | 7.00 | 16.00 | 10.00 | 148.00 | 16.00 | 0.00 |
5 | 49.00 | 9.00 | 6.00 | 17.00 | 1.00 | 14.00 | 21.00 | 10.00 | 0.00 | 43.00 | ... | 23.00 | 8.00 | 28.00 | 0.00 | 14.00 | 103.00 | 6.00 | 48.00 | 11.00 | 3.00 |
6 | 23.00 | 34.00 | 18.00 | 10.00 | 10.00 | 13.00 | 38.00 | 35.00 | 14.00 | 12.00 | ... | 7.00 | 22.00 | 5.00 | 46.00 | 26.00 | 20.00 | 94.00 | 26.00 | 58.00 | 35.00 |
7 | 26.00 | 60.00 | 26.00 | 54.00 | 58.00 | 20.00 | 63.00 | 27.00 | 26.00 | 59.00 | ... | 98.00 | 67.00 | 52.00 | 10.00 | 30.00 | 23.00 | 30.00 | 53.00 | 38.00 | 65.00 |
8 | 28.00 | 71.00 | 129.00 | 49.00 | 20.00 | 37.00 | 27.00 | 41.00 | 89.00 | 62.00 | ... | 30.00 | 95.00 | 35.00 | 52.00 | 30.00 | 17.00 | 120.00 | 80.00 | 67.00 | 25.00 |
9 | 24.00 | 34.00 | 90.00 | 61.00 | 44.00 | 8.00 | 56.00 | 43.00 | 28.00 | 78.00 | ... | 76.00 | 32.00 | 58.00 | 27.00 | 68.00 | 21.00 | 38.00 | 92.00 | 20.00 | 22.00 |
10 | 15.00 | 62.00 | 43.00 | 22.00 | 58.00 | 37.00 | 52.00 | 14.00 | 65.00 | 57.00 | ... | 31.00 | 45.00 | 10.00 | 44.00 | 55.00 | 88.00 | 66.00 | 29.00 | 64.00 | 19.00 |
11 | 95.00 | 80.00 | 83.00 | 36.00 | 33.00 | 44.00 | 53.00 | 66.00 | 47.00 | 13.00 | ... | 75.00 | 54.00 | 28.00 | 58.00 | 42.00 | 19.00 | 80.00 | 69.00 | 86.00 | 63.00 |
12 | 9.00 | 43.00 | 100.00 | 67.00 | 75.00 | 37.00 | 58.00 | 48.00 | 80.00 | 18.00 | ... | 107.00 | 71.00 | 48.00 | 38.00 | 38.00 | 82.00 | 52.00 | 57.00 | 40.00 | 58.00 |
13 | 16.00 | 76.00 | 69.00 | 18.00 | 31.00 | 40.00 | 44.00 | 109.00 | 11.00 | 67.00 | ... | 24.00 | 93.00 | 10.00 | 21.00 | 45.00 | 49.00 | 50.00 | 59.00 | 6.00 | 23.00 |
14 | 74.00 | 31.00 | 38.00 | 36.00 | 39.00 | 12.00 | 44.00 | 22.00 | 142.00 | 44.00 | ... | 34.00 | 34.00 | 66.00 | 47.00 | 27.00 | 34.00 | 70.00 | 51.00 | 26.00 | 44.00 |
15 | 25.00 | 10.00 | 45.00 | 28.00 | 128.00 | 116.00 | 9.00 | 31.00 | 93.00 | 39.00 | ... | 37.00 | 11.00 | 49.00 | 44.00 | 11.00 | 31.00 | 51.00 | 28.00 | 44.00 | 46.00 |
16 | 99.00 | 21.00 | 33.00 | 42.00 | 49.00 | 24.00 | 24.00 | 40.00 | 8.00 | 35.00 | ... | 74.00 | 196.00 | 6.00 | 4.00 | 72.00 | 82.00 | 55.00 | 10.00 | 14.00 | 59.00 |
17 | 88.00 | 80.00 | 55.00 | 31.00 | 8.00 | 1.00 | 53.00 | 34.00 | 10.00 | 8.00 | ... | 26.00 | 46.00 | 37.00 | 9.00 | 123.00 | 26.00 | 6.00 | 32.00 | 27.00 | 34.00 |
18 | 53.00 | 24.00 | 35.00 | 54.00 | 8.00 | 13.00 | 65.00 | 109.00 | 10.00 | 39.00 | ... | 161.00 | 56.00 | 11.00 | 37.00 | 19.00 | 42.00 | 98.00 | 220.00 | 46.00 | 55.00 |
19 | 29.00 | 25.00 | 19.00 | 14.00 | 31.00 | 47.00 | 97.00 | 70.00 | 87.00 | 46.00 | ... | 21.00 | 11.00 | 39.00 | 22.00 | 30.00 | 44.00 | 28.00 | 85.00 | 56.00 | 21.00 |
20 | 53.00 | 22.00 | 63.00 | 17.00 | 8.00 | 15.00 | 142.00 | 34.00 | 18.00 | 44.00 | ... | 41.00 | 38.00 | 8.00 | 37.00 | 42.00 | 36.00 | 85.00 | 7.00 | 12.00 | 15.00 |
21 | 1.00 | 55.00 | 25.00 | 42.00 | 12.00 | 9.00 | 38.00 | 8.00 | 9.00 | 21.00 | ... | 5.00 | 7.00 | 18.00 | 16.00 | 33.00 | 90.00 | 44.00 | 22.00 | 16.00 | 17.00 |
22 | 33.00 | 10.00 | 0.00 | 42.00 | 38.00 | 0.00 | 20.00 | 19.00 | 7.00 | 12.00 | ... | 10.00 | 7.00 | 33.00 | 10.00 | 20.00 | 15.00 | 5.00 | 49.00 | 2.00 | 21.00 |
23 | 0.00 | 0.00 | 7.00 | 0.00 | 0.00 | 26.00 | 0.00 | 0.00 | 16.00 | 1.00 | ... | 6.00 | 2.00 | 6.00 | 7.00 | 6.00 | 0.00 | 0.00 | 6.00 | 5.00 | 1.00 |
24 rows × 152 columns
plt.figure(figsize = (20,14))
sns.heatmap(compras_dia_hora);
Entendiendo los clientes¶
Para analizar a nivel de cliente lo mejor es crear un dataframe de solo compradores con granularidad cliente y las variables que nos interesan.
Hay que tener cuidado con la función de agregación que usamos en cada una.
clientes = df.loc[df.evento == 'purchase'].groupby(['usuario']).agg({'producto':'count',
'sesion':'nunique',
'precio': 'mean',
'date': 'max'})
clientes
producto | sesion | precio | date | |
---|---|---|---|---|
usuario | ||||
25392526 | 3 | 1 | 7.38 | 2019-12-18 |
27756757 | 1 | 1 | 20.63 | 2020-01-27 |
50748978 | 9 | 1 | 1.11 | 2019-12-14 |
52747911 | 3 | 1 | 7.67 | 2019-10-10 |
65241811 | 5 | 1 | 8.36 | 2019-11-11 |
... | ... | ... | ... | ... |
621995551 | 5 | 1 | 2.09 | 2020-02-29 |
622021687 | 1 | 1 | 13.33 | 2020-02-29 |
622041514 | 3 | 1 | 0.63 | 2020-02-29 |
622042698 | 3 | 1 | 28.04 | 2020-02-29 |
622065819 | 4 | 1 | 5.12 | 2020-02-29 |
11040 rows × 4 columns
Renombramos
clientes.columns = ['productos_tot_num','compras_tot_num','precio_medio_prod','ult_compra']
clientes
productos_tot_num | compras_tot_num | precio_medio_prod | ult_compra | |
---|---|---|---|---|
usuario | ||||
25392526 | 3 | 1 | 7.38 | 2019-12-18 |
27756757 | 1 | 1 | 20.63 | 2020-01-27 |
50748978 | 9 | 1 | 1.11 | 2019-12-14 |
52747911 | 3 | 1 | 7.67 | 2019-10-10 |
65241811 | 5 | 1 | 8.36 | 2019-11-11 |
... | ... | ... | ... | ... |
621995551 | 5 | 1 | 2.09 | 2020-02-29 |
622021687 | 1 | 1 | 13.33 | 2020-02-29 |
622041514 | 3 | 1 | 0.63 | 2020-02-29 |
622042698 | 3 | 1 | 28.04 | 2020-02-29 |
622065819 | 4 | 1 | 5.12 | 2020-02-29 |
11040 rows × 4 columns
Vamos a calcular variables adicionales.
clientes['gasto_tot'] = clientes.productos_tot_num * clientes.precio_medio_prod
clientes['productos_por_compra'] = clientes.productos_tot_num / clientes.compras_tot_num
clientes
productos_tot_num | compras_tot_num | precio_medio_prod | ult_compra | gasto_tot | productos_por_compra | |
---|---|---|---|---|---|---|
usuario | ||||||
25392526 | 3 | 1 | 7.38 | 2019-12-18 | 22.14 | 3.00 |
27756757 | 1 | 1 | 20.63 | 2020-01-27 | 20.63 | 1.00 |
50748978 | 9 | 1 | 1.11 | 2019-12-14 | 10.01 | 9.00 |
52747911 | 3 | 1 | 7.67 | 2019-10-10 | 23.02 | 3.00 |
65241811 | 5 | 1 | 8.36 | 2019-11-11 | 41.79 | 5.00 |
... | ... | ... | ... | ... | ... | ... |
621995551 | 5 | 1 | 2.09 | 2020-02-29 | 10.46 | 5.00 |
622021687 | 1 | 1 | 13.33 | 2020-02-29 | 13.33 | 1.00 |
622041514 | 3 | 1 | 0.63 | 2020-02-29 | 1.90 | 3.00 |
622042698 | 3 | 1 | 28.04 | 2020-02-29 | 84.13 | 3.00 |
622065819 | 4 | 1 | 5.12 | 2020-02-29 | 20.48 | 4.00 |
11040 rows × 6 columns
¿Cómo se distribuyen los clientes en cuanto a gasto?¶
sns.histplot(data = clientes, x = 'gasto_tot', bins = 50)
plt.xlim([0,300]);
La gran mayoría de los clientes han gastado menos de 50€ en el período.
¿Cómo se distribuyen los clientes en cuanto al número de compras?¶
sns.countplot(data = clientes, x = 'compras_tot_num');
INSIGHT #3 La gran mayoría de los clientes sólo hace una compra.
Existe gran recorrido para mejorar este ratio mediante:
- email marketing con newletters y ofertas personalizadas
¿Cuántos productos compra un cliente de media en cada compra?¶
clientes.productos_por_compra.describe()
count 11040.00 mean 7.79 std 9.49 min 1.00 25% 3.00 50% 5.00 75% 10.00 max 219.00 Name: productos_por_compra, dtype: float64
INSIGHT #4 La compra mediana incluye 5 productos.
Pero un 25% de los clientes compran más de 10 productos en la misma compra.
Existe gran recorrido para mejorar este ratio mediante:
- sistemas de recomendación en el momento de la compra
¿Qué clientes nos han generado más ingresos?¶
clientes.nlargest(n = 10, columns = 'gasto_tot')
productos_tot_num | compras_tot_num | precio_medio_prod | ult_compra | gasto_tot | productos_por_compra | |
---|---|---|---|---|---|---|
usuario | ||||||
573823111 | 268 | 2 | 5.82 | 2020-02-21 | 1559.21 | 134.00 |
539751397 | 236 | 13 | 6.16 | 2020-02-19 | 1453.37 | 18.15 |
556579890 | 506 | 4 | 2.75 | 2020-02-27 | 1392.45 | 126.50 |
442763940 | 195 | 8 | 6.37 | 2019-12-23 | 1241.53 | 24.38 |
561592095 | 94 | 3 | 11.81 | 2019-10-31 | 1109.70 | 31.33 |
527739278 | 244 | 13 | 4.39 | 2020-02-16 | 1071.00 | 18.77 |
527806771 | 195 | 13 | 4.86 | 2020-02-20 | 948.01 | 15.00 |
430220205 | 190 | 6 | 4.99 | 2020-02-29 | 947.30 | 31.67 |
491009486 | 219 | 1 | 4.32 | 2020-02-12 | 946.20 | 219.00 |
520501669 | 64 | 11 | 14.27 | 2020-01-17 | 913.01 | 5.82 |
Para calcular calculamos el gasto total medio por cliente.
clientes.gasto_tot.describe()
count 11040.00 mean 56.30 std 81.73 min 0.13 25% 16.22 50% 32.74 75% 60.30 max 1559.21 Name: gasto_tot, dtype: float64
INSIGHT #5 Existen clientes con gasto medio decenas de veces superior a la media.
Hay que fidelizar estos clientes mediante programas de fidelización.
¿Cual es la supervivencia de los clientes?¶
Dado que solo tenemos 5 meses de histórico vamos a crear análisis de cohortes a 3 meses vista, lo cual nos da para hacer 3 cohortes.
Preparamos un dataframe solo con compradores y con las variables usuario y mes.
c = df.loc[df.evento == 'purchase', ['usuario','mes']]
c
usuario | mes | |
---|---|---|
fecha | ||
2019-10-01 00:26:49 | 536128518 | 10 |
2019-10-01 00:26:49 | 536128518 | 10 |
2019-10-01 00:26:49 | 536128518 | 10 |
2019-10-01 00:26:49 | 536128518 | 10 |
2019-10-01 00:26:49 | 536128518 | 10 |
... | ... | ... |
2020-02-29 22:29:19 | 622065819 | 2 |
2020-02-29 22:29:19 | 622065819 | 2 |
2020-02-29 22:29:19 | 622065819 | 2 |
2020-02-29 22:29:19 | 622065819 | 2 |
2020-02-29 23:26:42 | 610361057 | 2 |
127564 rows × 2 columns
Pasamos los meses a columnas.
c = pd.crosstab(c.usuario,c.mes).reset_index()
c
mes | usuario | 1 | 2 | 10 | 11 | 12 |
---|---|---|---|---|---|---|
0 | 25392526 | 0 | 0 | 0 | 0 | 3 |
1 | 27756757 | 1 | 0 | 0 | 0 | 0 |
2 | 50748978 | 0 | 0 | 0 | 0 | 9 |
3 | 52747911 | 0 | 0 | 3 | 0 | 0 |
4 | 65241811 | 0 | 0 | 0 | 5 | 0 |
... | ... | ... | ... | ... | ... | ... |
11035 | 621995551 | 0 | 5 | 0 | 0 | 0 |
11036 | 622021687 | 0 | 1 | 0 | 0 | 0 |
11037 | 622041514 | 0 | 3 | 0 | 0 | 0 |
11038 | 622042698 | 0 | 3 | 0 | 0 | 0 |
11039 | 622065819 | 0 | 4 | 0 | 0 | 0 |
11040 rows × 6 columns
Renombramos y eliminamos el usuario que ya no lo necesitamos.
c.columns = ['usuario','c4','c5','c1','c2','c3']
c.drop(columns = 'usuario', inplace = True)
c
c4 | c5 | c1 | c2 | c3 | |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 3 |
1 | 1 | 0 | 0 | 0 | 0 |
2 | 0 | 0 | 0 | 0 | 9 |
3 | 0 | 0 | 3 | 0 | 0 |
4 | 0 | 0 | 0 | 5 | 0 |
... | ... | ... | ... | ... | ... |
11035 | 0 | 5 | 0 | 0 | 0 |
11036 | 0 | 1 | 0 | 0 | 0 |
11037 | 0 | 3 | 0 | 0 | 0 |
11038 | 0 | 3 | 0 | 0 | 0 |
11039 | 0 | 4 | 0 | 0 | 0 |
11040 rows × 5 columns
La primera cohorte será la del mes 2, ya que queremos seleccionar "nuevos" clientes (al menos que no estuvieran el mes anterior)
c2 = c.loc[(c.c1 == 0) & (c.c2 > 0)]
c2
c4 | c5 | c1 | c2 | c3 | |
---|---|---|---|---|---|
4 | 0 | 0 | 0 | 5 | 0 |
6 | 0 | 0 | 0 | 10 | 0 |
8 | 0 | 0 | 0 | 27 | 17 |
9 | 0 | 0 | 0 | 3 | 0 |
13 | 0 | 0 | 0 | 4 | 0 |
... | ... | ... | ... | ... | ... |
7702 | 0 | 0 | 0 | 5 | 0 |
7703 | 0 | 5 | 0 | 2 | 0 |
7705 | 0 | 0 | 0 | 1 | 0 |
7708 | 0 | 0 | 0 | 5 | 6 |
7709 | 0 | 0 | 0 | 1 | 0 |
2640 rows × 5 columns
Pasamos a un dataframe binario ya que solo nos importa si ese cliente ha comprado o no en cada mes.
def binarizar(variable):
variable = variable.transform(lambda x: 1 if (x > 0) else 0)
return(variable)
c2_b = c2.apply(binarizar)
c2_b
c4 | c5 | c1 | c2 | c3 | |
---|---|---|---|---|---|
4 | 0 | 0 | 0 | 1 | 0 |
6 | 0 | 0 | 0 | 1 | 0 |
8 | 0 | 0 | 0 | 1 | 1 |
9 | 0 | 0 | 0 | 1 | 0 |
13 | 0 | 0 | 0 | 1 | 0 |
... | ... | ... | ... | ... | ... |
7702 | 0 | 0 | 0 | 1 | 0 |
7703 | 0 | 1 | 0 | 1 | 0 |
7705 | 0 | 0 | 0 | 1 | 0 |
7708 | 0 | 0 | 0 | 1 | 1 |
7709 | 0 | 0 | 0 | 1 | 0 |
2640 rows × 5 columns
Calcumamos el porcentaje de clientes de esta cohorte que han seguido comprando en los siguientes meses.
c2_f = c2_b.sum() / c2_b.shape[0]
c2_f = c2_f.sort_index()
c2_f
c1 0.00 c2 1.00 c3 0.10 c4 0.10 c5 0.08 dtype: float64
Replicamos todo el proceso para la cohorte 3
c3 = c.loc[(c.c2 == 0) & (c.c3 > 0)]
c3_b = c3.apply(binarizar)
c3_f = c3_b.sum() / c3_b.shape[0]
c3_f = c3_f.sort_index()
c3_f['c1'] = 0
c3_f
c1 0.00 c2 0.00 c3 1.00 c4 0.10 c5 0.08 dtype: float64
Replicamos todo el proceso para la cohorte 4
c4 = c.loc[(c.c3 == 0) & (c.c4 > 0)]
c4_b = c4.apply(binarizar)
c4_f = c4_b.sum() / c4_b.shape[0]
c4_f = c4_f.sort_index()
c4_f['c1'] = 0
c4_f['c2'] = 0
c4_f
c1 0.00 c2 0.00 c3 0.00 c4 1.00 c5 0.12 dtype: float64
Creamos el dataframe de cohortes.
cohortes = pd.DataFrame({'c2':c2_f,'c3':c3_f,'c4':c4_f})
cohortes
c2 | c3 | c4 | |
---|---|---|---|
c1 | 0.00 | 0.00 | 0.00 |
c2 | 1.00 | 0.00 | 0.00 |
c3 | 0.10 | 1.00 | 0.00 |
c4 | 0.10 | 0.10 | 1.00 |
c5 | 0.08 | 0.08 | 0.12 |
cohortes = cohortes.drop(index = 'c1').T
cohortes
c2 | c3 | c4 | c5 | |
---|---|---|---|---|
c2 | 1.00 | 0.10 | 0.10 | 0.08 |
c3 | 0.00 | 1.00 | 0.10 | 0.08 |
c4 | 0.00 | 0.00 | 1.00 | 0.12 |
plt.figure(figsize = (12,6))
sns.heatmap(cohortes,annot = True, fmt = '.0%', cmap='Greys');
INSIGHT #6: El 90% de que los nuevos clientes no vuelve a comprar en los meses posteriores
¿Cual es el LTV de los clientes?¶
Teniendo en cuenta el 90% de que los nuevos clientes no vuelve a comprar en los meses posteriores podemos calcular el LTV con el histórico que tenemos sin miedo a equivocarnos mucho.
Para ello vamos a coger a los clientes de la cohorte 2 y calcular el total de sus compras.
maestro_ltv = df.loc[(df.evento == 'purchase') & (df.mes != 10) & (df.mes == 11),'usuario'].to_list()
maestro_ltv
[549319657, 549319657, 549319657, 549319657, 549319657, 549319657, 549319657, 549319657, 549319657, 549319657, 549319657, 549319657, 549319657, 549319657, 549319657, 549319657, 549319657, 549319657, 549319657, 566322866, 566322866, 566322866, 566322866, 566322866, 566322866, 566322866, 566322866, 491040843, 491040843, 491040843, 491040843, 491040843, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 448281768, 483964876, 483964876, 483964876, 483964876, 483964876, 483964876, 483964876, 497964079, 497964079, 497964079, 497964079, 566332999, 551059137, 539216862, 539216862, 539216862, 539216862, 539216862, 539216862, 539216862, 539216862, 539216862, 539216862, 539216862, 539216862, 539216862, 539216862, 539216862, 539216862, 539216862, 465338762, 465338762, 465338762, 465338762, 465338762, 531620122, 531620122, 531620122, 531620122, 558863145, 558863145, 558863145, 509739809, 509739809, 509739809, 509739809, 509739809, 509739809, 509739809, 509739809, 509739809, 364758489, 364758489, 364758489, 364758489, 364758489, 364758489, 364758489, 364758489, 364758489, 364758489, 364758489, 364758489, 364758489, 364758489, 364758489, 364758489, 364758489, 364758489, 364758489, 364758489, 537770351, 537770351, 537770351, 537770351, 537770351, 537770351, 537770351, 537770351, 566346244, 566346244, 566346244, 566346244, 566346244, 566346244, 566346244, 566346244, 566346244, 564767268, 564767268, 564767268, 564767268, 564767268, 564767268, 564767268, 564767268, 564767268, 564767268, 564767268, 564767268, 564767268, 566371070, 566371070, 566371070, 566371070, 566371070, 566371070, 566371070, 566371070, 566371070, 566371070, 566371070, 566371070, 566371070, 552987940, 552987940, 552987940, 549507685, 549507685, 549507685, 549507685, 541923025, 545903799, 545903799, 545903799, 545903799, 545903799, 545903799, 545903799, 545903799, 545903799, 545903799, 545903799, 545903799, 545903799, 566341169, 525016637, 525016637, 525016637, 525016637, 566408058, 566408058, 566408058, 566408058, 566408058, 566341169, 427260620, 427260620, 427260620, 427260620, 427260620, 318277599, 318277599, 514961719, 565874028, 565874028, 565874028, 565061712, 565061712, 565061712, 565061712, 565061712, 565061712, 566418296, 566418296, 566418296, 565712511, 565712511, 565712511, 565712511, 565712511, 565712511, 565712511, 565712511, 565712511, 413094093, 413094093, 413094093, 413094093, 413094093, 413094093, 413094093, 413094093, 413094093, 413094093, 558100520, 536853200, 536853200, 536853200, 536853200, 536853200, 566442321, 566442321, 566442321, 566442321, 566442321, 566442321, 566442321, 545715278, 545715278, 545715278, 531724849, 531724849, 531724849, 531724849, 531724849, 531724849, 531724849, 531724849, 531724849, 531724849, 531724849, 531724849, 531724849, 531724849, 531724849, 531724849, 531724849, 278908478, 278908478, 278908478, 278908478, 278908478, 278908478, 278908478, 442255674, 442255674, 442255674, 442255674, 451770301, 451770301, 451770301, 451770301, 451770301, 451770301, 451770301, 451770301, 451770301, 566457558, 566457558, 455884270, 455884270, 455884270, 455884270, 455884270, 493511762, 493511762, 562550434, 562550434, 562550434, 562550434, 562550434, 562550434, 562550434, 549804843, 549804843, 409566492, 409566492, 409566492, 409566492, 548199852, 548199852, 129089988, 562550434, 562550434, 562550434, 562550434, 562550434, 562550434, 490402605, 490402605, 490402605, 490402605, 490402605, 490402605, 566402792, 566402792, 566402792, 566402792, 566402792, 566402792, 489063092, 489063092, 489063092, 378588742, 378588742, 566452644, 566452644, 566452644, 566452644, 566452644, 566452644, 566452644, 549804843, 549804843, 549804843, 549804843, 549804843, 549804843, 374211459, 374211459, 374211459, 374211459, 374211459, 374211459, 374211459, 374211459, 374211459, 378588742, 288721487, 301829686, 566332413, 566332413, 566332413, 566332413, 543791901, 543791901, 543791901, 543791901, 543791901, 543791901, 543791901, 543791901, 566519740, 566521152, 566521152, 566521152, 554284521, 554284521, 554284521, 554284521, 554284521, 554284521, 554284521, 554284521, 554284521, 554284521, 554284521, 554284521, 554284521, 562000064, 562000064, 562000064, 551116298, 551116298, 554833056, 554833056, 554833056, 554833056, 554833056, 552510951, 552510951, 552510951, 552510951, 552510951, 552510951, 412262395, 469431599, 469431599, 538166206, 566532552, 566532552, 566532552, 566532552, 566532552, 566555308, 562691482, 562691482, 562691482, 562691482, 562691482, 562691482, 562691482, 562691482, 562691482, 562691482, 562691482, 562691482, 562691482, 562691482, 562691482, 525729260, 525729260, 525729260, 525729260, 525729260, 525729260, 525729260, 525729260, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 527739278, 565932689, 476731744, 476731744, 476731744, 476731744, 512106285, 512106285, 512106285, 512106285, 512106285, 512106285, 566539426, 566539426, 566539426, 566539426, 566539426, 566539426, 566539426, 566539426, 566539426, 566539426, 566539426, 566539426, 566539426, 566539426, 566539426, 566539426, 566539426, 566539426, 566547942, 566547942, 566547942, 566547942, 566547942, 566547942, 566547942, 566547942, 566547942, 566547942, 566547942, 566547942, 566547942, 566547942, 566547942, 452799836, 452799836, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 561003158, 436122236, 436122236, 436122236, 436122236, 436122236, 436122236, 436122236, 436122236, 436122236, 436122236, 436122236, 436122236, 436122236, 436122236, 436122236, 513908715, 513908715, 513908715, 396078835, 396078835, 396078835, 396078835, 396078835, 501732943, 501732943, 501732943, 286065273, 286065273, 286065273, 286065273, 286065273, 286065273, 286065273, 286065273, 286065273, 286065273, 286065273, 286065273, 286065273, 286065273, 286065273, 286065273, 286065273, 286065273, 286065273, 286065273, 286065273, 286065273, 286065273, 286065273, 286065273, 286065273, 286065273, 286065273, 538225813, 538225813, 538225813, 538225813, 538225813, 538225813, 538225813, 538225813, 538225813, 538225813, 538225813, 538225813, 538225813, 538225813, 538225813, 538225813, 538225813, 538225813, 446755357, 446755357, 446755357, 446755357, 446755357, 446755357, 446755357, 446755357, 446755357, 446755357, 446755357, 446755357, 446755357, 446755357, 566586005, 566586005, 416178424, 416178424, 416178424, 416178424, 566587999, 566587999, 566587999, 566587999, 566587999, 566587999, 566587999, 566587999, 566587999, 541411060, 541411060, 541411060, 541411060, 541411060, 541411060, 541411060, 541411060, 541411060, 541411060, 541411060, 541411060, 541411060, 541411060, 541411060, 541411060, 541411060, 438582678, 438582678, 438582678, 438582678, 438582678, 438582678, 438582678, 438582678, 438582678, 438582678, 438582678, 438582678, 438582678, 438582678, 566612684, 566612684, 566612684, 566612684, 566612684, 566612684, 566612684, 566612684, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 562867290, 566632944, 566632944, 566632944, 566632944, 566632944, 566632944, 566632944, 566632944, 566632944, 566632944, 566632944, 566641963, 566641963, 566641963, 566641963, 566641963, 566641963, 566641963, 566641963, 566641963, 566641963, 405969740, 405969740, 405969740, 405969740, 405969740, 405969740, 405969740, 405969740, 405969740, 405969740, 405969740, 405969740, 454177715, 454177715, 454177715, 454177715, 454177715, 454177715, 454177715, 476622879, 476622879, 470068651, 470068651, 470068651, 470068651, 470068651, 470068651, 470068651, 470068651, 457740912, 457740912, 457740912, 457740912, 457740912, 457740912, 457740912, 566655609, 566655609, 566655609, 566655609, 566655609, 566655609, 566655609, 566655609, 557312952, 557312952, 557312952, 557312952, 557312952, 557312952, 557312952, 557312952, 557312952, 557312952, 557312952, 557312952, 566668289, 566668289, 566668289, 542112882, 542112882, 542112882, 542112882, 542112882, 542112882, 542112882, 541344889, 541344889, 541344889, 516921923, 516921923, 475746194, 475746194, 475746194, 475746194, 475746194, 475746194, 475746194, 475746194, 475746194, 475746194, 475746194, 475746194, 475746194, 475746194, 475746194, 475746194, 475746194, 475746194, 475746194, 475746194, 475746194, 475746194, 542112882, 542112882, 550090959, 550090959, 550090959, 550090959, 550090959, 550090959, 566694605, 523388043, 523388043, 523388043, 523388043, 523388043, 523388043, 523388043, 523388043, 523388043, 523388043, 523388043, 523388043, 566694956, 566694956, 566694956, 566694956, 566694956, 566694956, 566694956, 566694956, 566694956, 566699471, 566699471, 566699471, 566699471, 566699471, 553363925, 553363925, 553363925, 553363925, 553363925, 553363925, 553363925, 498980732, 498980732, 431821216, 431821216, 431821216, 431821216, 431821216, 431821216, 431821216, 431821216, 431821216, 431821216, 431821216, 483922031, 483922031, 483922031, 558768594, 558768594, 558768594, 558768594, 558768594, 558768594, 558768594, 558768594, 558768594, 566677676, 566677676, 468111817, 468111817, 468111817, 468111817, 468111817, 468111817, 544416380, 544416380, 544416380, 544416380, 544416380, 544416380, 544416380, 544416380, 544416380, 544416380, 544416380, 544416380, 566733694, 566733694, 566733694, 566733694, 566733694, 469446476, 469446476, 469446476, 469446476, 469446476, 469446476, 469446476, 469446476, 469446476, 469446476, 469446476, 469446476, 566620039, 375439563, 375439563, 375439563, 375439563, 375439563, 375439563, 375439563, 375439563, 548600335, 548600335, 548600335, 548600335, 548600335, 511265307, 511265307, 511265307, 511265307, 511265307, 511265307, 511265307, 511265307, 511265307, 511265307, 444915600, 444915600, 444915600, 444915600, 444915600, 444915600, 510126861, 510126861, 510126861, 510126861, 510126861, 510126861, 440684807, ...]
clientes_ltv = clientes.loc[clientes.index.isin(maestro_ltv)]
clientes_ltv
productos_tot_num | compras_tot_num | precio_medio_prod | ult_compra | gasto_tot | productos_por_compra | |
---|---|---|---|---|---|---|
usuario | ||||||
65241811 | 5 | 1 | 8.36 | 2019-11-11 | 41.79 | 5.00 |
80577370 | 10 | 2 | 10.62 | 2019-11-29 | 106.24 | 5.00 |
88211255 | 22 | 4 | 4.86 | 2020-02-25 | 106.87 | 5.50 |
93279832 | 44 | 2 | 3.19 | 2019-12-19 | 140.51 | 22.00 |
94390236 | 3 | 1 | 9.73 | 2019-11-07 | 29.20 | 3.00 |
... | ... | ... | ... | ... | ... | ... |
579798049 | 5 | 1 | 2.10 | 2019-11-30 | 10.52 | 5.00 |
579813390 | 7 | 2 | 2.98 | 2020-02-04 | 20.83 | 3.50 |
579834429 | 1 | 1 | 27.14 | 2019-11-30 | 27.14 | 1.00 |
579900887 | 11 | 2 | 5.67 | 2019-12-02 | 62.34 | 5.50 |
579903865 | 1 | 1 | 8.43 | 2019-11-30 | 8.43 | 1.00 |
3105 rows × 6 columns
clientes_ltv.gasto_tot.describe()
count 3105.00 mean 79.62 std 113.62 min 0.13 25% 20.29 50% 41.49 75% 90.00 max 1453.37 Name: gasto_tot, dtype: float64
Dada la variabilidad de la media sería más seguro coger la mediana.
INSIGHT #7: El LTV medio es de 42€.
Aplicando nuestro margen sobre esa cifra y el % que queremos dedicar a captación nos sale el importe máximo a invertir en CPA.
Aplicar las acciones de CRO permitirá incrementar el LTV y por tanto también el CPA, siendo una ventaja estratégica muy importante.
¿Sobre qué clientes ejecutar las próximas campañas (RFM)?¶
Vamos a aprender una técnica llamada RFM (Recency - Frequency - Monetary).
Esta técnica es muy potente para contextos de retail y por tanto también en ecommerce.
Permite dar respuesta a necesidades como:
- Cuál es la proporción de clientes que realizan un solo pedido y clientes frecuentes
- Cuales son los clientes VIP (que potencialmente necesitan programas de fidelización y atención personalizada)
- Cuál es la cantidad de clientes nuevos (a incentivar para que vuelvan a realizar un pedido)
- Cuántos y cuáles son los clientes que no realizan compras hace tiempo
- Cuántos y cuáles son los clientes en los cuales no vale la pena invertir más tiempo y recursos
- Etc
Pese a su potencia es muy sencilla de construir, por tanto es casi obligatoria en este tipo de análisis.
Lo primero es identificar las variables con las que crear cada una de las dimensiones:
- Recency: ult_compra
- Frequency: compras_tot_num
- Monetary: gasto_tot
Y discretizar cada una de ellas.
Vamos a dejar la recencia para el final porque requerirá una transformación previa.
Comenzamos por Frequency
clientes['F'] = clientes.compras_tot_num.transform(lambda x: pd.cut(x,5, labels = False)) + 1
clientes
productos_tot_num | compras_tot_num | precio_medio_prod | ult_compra | gasto_tot | productos_por_compra | F | |
---|---|---|---|---|---|---|---|
usuario | |||||||
25392526 | 3 | 1 | 7.38 | 2019-12-18 | 22.14 | 3.00 | 1 |
27756757 | 1 | 1 | 20.63 | 2020-01-27 | 20.63 | 1.00 | 1 |
50748978 | 9 | 1 | 1.11 | 2019-12-14 | 10.01 | 9.00 | 1 |
52747911 | 3 | 1 | 7.67 | 2019-10-10 | 23.02 | 3.00 | 1 |
65241811 | 5 | 1 | 8.36 | 2019-11-11 | 41.79 | 5.00 | 1 |
... | ... | ... | ... | ... | ... | ... | ... |
621995551 | 5 | 1 | 2.09 | 2020-02-29 | 10.46 | 5.00 | 1 |
622021687 | 1 | 1 | 13.33 | 2020-02-29 | 13.33 | 1.00 | 1 |
622041514 | 3 | 1 | 0.63 | 2020-02-29 | 1.90 | 3.00 | 1 |
622042698 | 3 | 1 | 28.04 | 2020-02-29 | 84.13 | 3.00 | 1 |
622065819 | 4 | 1 | 5.12 | 2020-02-29 | 20.48 | 4.00 | 1 |
11040 rows × 7 columns
Comprobamos
clientes.groupby('F').compras_tot_num.mean()
F 1 1.31 2 7.06 3 12.00 4 16.50 5 23.50 Name: compras_tot_num, dtype: float64
Ahora Monetary
clientes['M'] = clientes.gasto_tot.transform(lambda x: pd.cut(x,5, labels = False)) + 1
clientes.groupby('M').gasto_tot.mean()
M 1 48.36 2 410.98 3 765.18 4 1043.96 5 1468.34 Name: gasto_tot, dtype: float64
Para la recencia tenemos que transformar la fecha a un número, por ejemplo la distancia en días de cada fecha a la fecha más reciente disponible.
mas_reciente = clientes.ult_compra.max()
clientes['ult_compra_dias'] = clientes.ult_compra.transform(lambda x: mas_reciente - x)
clientes
productos_tot_num | compras_tot_num | precio_medio_prod | ult_compra | gasto_tot | productos_por_compra | F | M | ult_compra_dias | |
---|---|---|---|---|---|---|---|---|---|
usuario | |||||||||
25392526 | 3 | 1 | 7.38 | 2019-12-18 | 22.14 | 3.00 | 1 | 1 | 73 days |
27756757 | 1 | 1 | 20.63 | 2020-01-27 | 20.63 | 1.00 | 1 | 1 | 33 days |
50748978 | 9 | 1 | 1.11 | 2019-12-14 | 10.01 | 9.00 | 1 | 1 | 77 days |
52747911 | 3 | 1 | 7.67 | 2019-10-10 | 23.02 | 3.00 | 1 | 1 | 142 days |
65241811 | 5 | 1 | 8.36 | 2019-11-11 | 41.79 | 5.00 | 1 | 1 | 110 days |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
621995551 | 5 | 1 | 2.09 | 2020-02-29 | 10.46 | 5.00 | 1 | 1 | 0 days |
622021687 | 1 | 1 | 13.33 | 2020-02-29 | 13.33 | 1.00 | 1 | 1 | 0 days |
622041514 | 3 | 1 | 0.63 | 2020-02-29 | 1.90 | 3.00 | 1 | 1 | 0 days |
622042698 | 3 | 1 | 28.04 | 2020-02-29 | 84.13 | 3.00 | 1 | 1 | 0 days |
622065819 | 4 | 1 | 5.12 | 2020-02-29 | 20.48 | 4.00 | 1 | 1 | 0 days |
11040 rows × 9 columns
Nos ha creado un timedelta, tenemos que pasarlo a número de días.
clientes['ult_compra_dias'] = clientes.ult_compra_dias.dt.days
clientes
productos_tot_num | compras_tot_num | precio_medio_prod | ult_compra | gasto_tot | productos_por_compra | F | M | ult_compra_dias | |
---|---|---|---|---|---|---|---|---|---|
usuario | |||||||||
25392526 | 3 | 1 | 7.38 | 2019-12-18 | 22.14 | 3.00 | 1 | 1 | 73 |
27756757 | 1 | 1 | 20.63 | 2020-01-27 | 20.63 | 1.00 | 1 | 1 | 33 |
50748978 | 9 | 1 | 1.11 | 2019-12-14 | 10.01 | 9.00 | 1 | 1 | 77 |
52747911 | 3 | 1 | 7.67 | 2019-10-10 | 23.02 | 3.00 | 1 | 1 | 142 |
65241811 | 5 | 1 | 8.36 | 2019-11-11 | 41.79 | 5.00 | 1 | 1 | 110 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
621995551 | 5 | 1 | 2.09 | 2020-02-29 | 10.46 | 5.00 | 1 | 1 | 0 |
622021687 | 1 | 1 | 13.33 | 2020-02-29 | 13.33 | 1.00 | 1 | 1 | 0 |
622041514 | 3 | 1 | 0.63 | 2020-02-29 | 1.90 | 3.00 | 1 | 1 | 0 |
622042698 | 3 | 1 | 28.04 | 2020-02-29 | 84.13 | 3.00 | 1 | 1 | 0 |
622065819 | 4 | 1 | 5.12 | 2020-02-29 | 20.48 | 4.00 | 1 | 1 | 0 |
11040 rows × 9 columns
Ya podemos crear la R, pero hay que tener en cuenta que en este caso lo mejor son los valores más bajos.
clientes['R'] = clientes.ult_compra_dias.transform(lambda x: pd.cut(x,5, labels = False)) + 1
clientes.groupby('R').ult_compra_dias.mean()
R 1 14.62 2 43.04 3 75.94 4 103.85 5 135.91 Name: ult_compra_dias, dtype: float64
Para estandarizar su intrepretación con el resto de las dimensiones vamos a darle la vuelta.
clientes['R'] = 6 - clientes.R
clientes.groupby('R').ult_compra_dias.mean()
R 1 135.91 2 103.85 3 75.94 4 43.04 5 14.62 Name: ult_compra_dias, dtype: float64
Integramos en un dataframe rfm.
clientes
productos_tot_num | compras_tot_num | precio_medio_prod | ult_compra | gasto_tot | productos_por_compra | F | M | ult_compra_dias | R | |
---|---|---|---|---|---|---|---|---|---|---|
usuario | ||||||||||
25392526 | 3 | 1 | 7.38 | 2019-12-18 | 22.14 | 3.00 | 1 | 1 | 73 | 3 |
27756757 | 1 | 1 | 20.63 | 2020-01-27 | 20.63 | 1.00 | 1 | 1 | 33 | 4 |
50748978 | 9 | 1 | 1.11 | 2019-12-14 | 10.01 | 9.00 | 1 | 1 | 77 | 3 |
52747911 | 3 | 1 | 7.67 | 2019-10-10 | 23.02 | 3.00 | 1 | 1 | 142 | 1 |
65241811 | 5 | 1 | 8.36 | 2019-11-11 | 41.79 | 5.00 | 1 | 1 | 110 | 2 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
621995551 | 5 | 1 | 2.09 | 2020-02-29 | 10.46 | 5.00 | 1 | 1 | 0 | 5 |
622021687 | 1 | 1 | 13.33 | 2020-02-29 | 13.33 | 1.00 | 1 | 1 | 0 | 5 |
622041514 | 3 | 1 | 0.63 | 2020-02-29 | 1.90 | 3.00 | 1 | 1 | 0 | 5 |
622042698 | 3 | 1 | 28.04 | 2020-02-29 | 84.13 | 3.00 | 1 | 1 | 0 | 5 |
622065819 | 4 | 1 | 5.12 | 2020-02-29 | 20.48 | 4.00 | 1 | 1 | 0 | 5 |
11040 rows × 10 columns
Creamos variables adicionales.
clientes['valor'] = clientes.R + clientes.F + clientes.M
clientes['RFM'] = clientes.apply(lambda x: str(x.R) + str(x.F) + str(x.M), axis = 1)
clientes
productos_tot_num | compras_tot_num | precio_medio_prod | ult_compra | gasto_tot | productos_por_compra | F | M | ult_compra_dias | R | valor | RFM | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
usuario | ||||||||||||
25392526 | 3 | 1 | 7.38 | 2019-12-18 | 22.14 | 3.00 | 1 | 1 | 73 | 3 | 5 | 311 |
27756757 | 1 | 1 | 20.63 | 2020-01-27 | 20.63 | 1.00 | 1 | 1 | 33 | 4 | 6 | 411 |
50748978 | 9 | 1 | 1.11 | 2019-12-14 | 10.01 | 9.00 | 1 | 1 | 77 | 3 | 5 | 311 |
52747911 | 3 | 1 | 7.67 | 2019-10-10 | 23.02 | 3.00 | 1 | 1 | 142 | 1 | 3 | 111 |
65241811 | 5 | 1 | 8.36 | 2019-11-11 | 41.79 | 5.00 | 1 | 1 | 110 | 2 | 4 | 211 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
621995551 | 5 | 1 | 2.09 | 2020-02-29 | 10.46 | 5.00 | 1 | 1 | 0 | 5 | 7 | 511 |
622021687 | 1 | 1 | 13.33 | 2020-02-29 | 13.33 | 1.00 | 1 | 1 | 0 | 5 | 7 | 511 |
622041514 | 3 | 1 | 0.63 | 2020-02-29 | 1.90 | 3.00 | 1 | 1 | 0 | 5 | 7 | 511 |
622042698 | 3 | 1 | 28.04 | 2020-02-29 | 84.13 | 3.00 | 1 | 1 | 0 | 5 | 7 | 511 |
622065819 | 4 | 1 | 5.12 | 2020-02-29 | 20.48 | 4.00 | 1 | 1 | 0 | 5 | 7 | 511 |
11040 rows × 12 columns
Sobre este dataframe ya podemos hacer infinidad de análisis.
Por ejemplo combinándolo con la técnica del minicubo podemos obtener todo tipo de insights.
#Paso 1: Seleccionar qué variables serán la métricas y cuales las dimensiones
metricas = ['productos_tot_num','compras_tot_num','gasto_tot']
dimensiones = ['R','F','M','RFM','valor']
minicubo = clientes[dimensiones + metricas]
minicubo
R | F | M | RFM | valor | productos_tot_num | compras_tot_num | gasto_tot | |
---|---|---|---|---|---|---|---|---|
usuario | ||||||||
25392526 | 3 | 1 | 1 | 311 | 5 | 3 | 1 | 22.14 |
27756757 | 4 | 1 | 1 | 411 | 6 | 1 | 1 | 20.63 |
50748978 | 3 | 1 | 1 | 311 | 5 | 9 | 1 | 10.01 |
52747911 | 1 | 1 | 1 | 111 | 3 | 3 | 1 | 23.02 |
65241811 | 2 | 1 | 1 | 211 | 4 | 5 | 1 | 41.79 |
... | ... | ... | ... | ... | ... | ... | ... | ... |
621995551 | 5 | 1 | 1 | 511 | 7 | 5 | 1 | 10.46 |
622021687 | 5 | 1 | 1 | 511 | 7 | 1 | 1 | 13.33 |
622041514 | 5 | 1 | 1 | 511 | 7 | 3 | 1 | 1.90 |
622042698 | 5 | 1 | 1 | 511 | 7 | 3 | 1 | 84.13 |
622065819 | 5 | 1 | 1 | 511 | 7 | 4 | 1 | 20.48 |
11040 rows × 8 columns
#Paso 2: pasar a transaccional las dimensiones
minicubo = minicubo.melt(id_vars = metricas)
minicubo
productos_tot_num | compras_tot_num | gasto_tot | variable | value | |
---|---|---|---|---|---|
0 | 3 | 1 | 22.14 | R | 3 |
1 | 1 | 1 | 20.63 | R | 4 |
2 | 9 | 1 | 10.01 | R | 3 |
3 | 3 | 1 | 23.02 | R | 1 |
4 | 5 | 1 | 41.79 | R | 2 |
... | ... | ... | ... | ... | ... |
55195 | 5 | 1 | 10.46 | valor | 7 |
55196 | 1 | 1 | 13.33 | valor | 7 |
55197 | 3 | 1 | 1.90 | valor | 7 |
55198 | 3 | 1 | 84.13 | valor | 7 |
55199 | 4 | 1 | 20.48 | valor | 7 |
55200 rows × 5 columns
#Paso 3: Agregar las métricas por "variable" y "valor" con las funciones deseadas
minicubo = minicubo.groupby(['variable','value'], as_index = False)[metricas].mean()
minicubo
variable | value | productos_tot_num | compras_tot_num | gasto_tot | |
---|---|---|---|---|---|
0 | F | 1 | 10.61 | 1.31 | 52.09 |
1 | F | 2 | 71.42 | 7.06 | 320.47 |
2 | F | 3 | 123.64 | 12.00 | 643.20 |
3 | F | 4 | 156.75 | 16.50 | 560.15 |
4 | F | 5 | 124.00 | 23.50 | 652.42 |
... | ... | ... | ... | ... | ... |
58 | valor | 9 | 98.02 | 7.25 | 491.71 |
59 | valor | 10 | 140.89 | 10.22 | 625.93 |
60 | valor | 11 | 291.00 | 5.75 | 1189.31 |
61 | valor | 12 | 189.80 | 16.60 | 833.43 |
62 | valor | 13 | 179.00 | 18.00 | 1136.70 |
63 rows × 5 columns
Para analizar cada dimensión la seleccionamos.
minicubo[minicubo.variable == 'F']
variable | value | productos_tot_num | compras_tot_num | gasto_tot | |
---|---|---|---|---|---|
0 | F | 1 | 10.61 | 1.31 | 52.09 |
1 | F | 2 | 71.42 | 7.06 | 320.47 |
2 | F | 3 | 123.64 | 12.00 | 643.20 |
3 | F | 4 | 156.75 | 16.50 | 560.15 |
4 | F | 5 | 124.00 | 23.50 | 652.42 |
Y la analizamos de forma gráfica.
minicubo[minicubo.variable == 'F'].set_index('value').plot.bar(subplots = True, sharex = False, figsize = (12,12))
plt.tight_layout();
minicubo[minicubo.variable == 'R']
variable | value | productos_tot_num | compras_tot_num | gasto_tot | |
---|---|---|---|---|---|
10 | R | 1 | 8.15 | 1.08 | 41.56 |
11 | R | 2 | 9.25 | 1.18 | 45.58 |
12 | R | 3 | 9.54 | 1.29 | 47.25 |
13 | R | 4 | 11.72 | 1.44 | 58.19 |
14 | R | 5 | 16.83 | 1.82 | 79.04 |
minicubo[minicubo.variable == 'R'].set_index('value').plot.bar(subplots = True, sharex = False, figsize = (12,12))
plt.tight_layout();
minicubo[minicubo.variable == 'M']
variable | value | productos_tot_num | compras_tot_num | gasto_tot | |
---|---|---|---|---|---|
5 | M | 1 | 10.12 | 1.34 | 48.36 |
6 | M | 2 | 74.28 | 4.31 | 410.98 |
7 | M | 3 | 138.50 | 6.86 | 765.18 |
8 | M | 4 | 189.50 | 7.33 | 1043.96 |
9 | M | 5 | 336.67 | 6.33 | 1468.34 |
minicubo[minicubo.variable == 'M'].set_index('value').plot.bar(subplots = True, sharex = False, figsize = (12,12))
plt.tight_layout();
minicubo[minicubo.variable == 'RFM']
variable | value | productos_tot_num | compras_tot_num | gasto_tot | |
---|---|---|---|---|---|
15 | RFM | 111 | 7.97 | 1.08 | 39.28 |
16 | RFM | 112 | 37.38 | 1.50 | 397.98 |
17 | RFM | 114 | 94.00 | 3.00 | 1109.70 |
18 | RFM | 211 | 8.94 | 1.17 | 43.16 |
19 | RFM | 212 | 47.00 | 1.73 | 391.14 |
20 | RFM | 213 | 35.50 | 1.00 | 662.84 |
21 | RFM | 221 | 85.67 | 6.33 | 194.48 |
22 | RFM | 311 | 8.98 | 1.26 | 43.91 |
23 | RFM | 312 | 53.46 | 2.46 | 376.38 |
24 | RFM | 321 | 39.00 | 6.50 | 141.28 |
25 | RFM | 322 | 121.00 | 6.50 | 389.19 |
26 | RFM | 324 | 195.00 | 8.00 | 1241.53 |
27 | RFM | 411 | 10.37 | 1.36 | 49.99 |
28 | RFM | 412 | 60.69 | 2.54 | 404.22 |
29 | RFM | 413 | 130.50 | 2.50 | 868.30 |
30 | RFM | 421 | 41.00 | 6.92 | 205.31 |
31 | RFM | 422 | 92.88 | 7.12 | 440.47 |
32 | RFM | 423 | 153.33 | 7.33 | 672.97 |
33 | RFM | 433 | 64.00 | 11.00 | 913.01 |
34 | RFM | 511 | 11.94 | 1.50 | 56.07 |
35 | RFM | 512 | 78.79 | 3.05 | 399.18 |
36 | RFM | 513 | 133.00 | 3.14 | 742.48 |
37 | RFM | 514 | 219.00 | 1.00 | 946.20 |
38 | RFM | 515 | 387.00 | 3.00 | 1475.83 |
39 | RFM | 521 | 47.88 | 6.83 | 192.19 |
40 | RFM | 522 | 89.51 | 7.51 | 442.50 |
41 | RFM | 523 | 184.67 | 7.33 | 766.90 |
42 | RFM | 524 | 190.00 | 6.00 | 947.30 |
43 | RFM | 531 | 58.25 | 11.50 | 233.84 |
44 | RFM | 532 | 94.00 | 12.50 | 448.09 |
45 | RFM | 533 | 200.00 | 11.00 | 858.26 |
46 | RFM | 534 | 219.50 | 13.00 | 1009.51 |
47 | RFM | 535 | 236.00 | 13.00 | 1453.37 |
48 | RFM | 541 | 121.50 | 16.50 | 288.65 |
49 | RFM | 543 | 192.00 | 16.50 | 831.65 |
50 | RFM | 552 | 126.00 | 24.00 | 484.81 |
51 | RFM | 553 | 122.00 | 23.00 | 820.04 |
minicubo[minicubo.variable == 'RFM'].set_index('value').plot.bar(subplots = True, sharex = False, figsize = (12,12))
plt.tight_layout();
minicubo[minicubo.variable == 'valor']
variable | value | productos_tot_num | compras_tot_num | gasto_tot | |
---|---|---|---|---|---|
52 | valor | 3 | 7.97 | 1.08 | 39.28 |
53 | valor | 4 | 9.04 | 1.17 | 44.41 |
54 | valor | 5 | 9.31 | 1.27 | 46.08 |
55 | valor | 6 | 10.73 | 1.38 | 53.07 |
56 | valor | 7 | 12.64 | 1.54 | 60.42 |
57 | valor | 8 | 66.13 | 5.06 | 313.01 |
58 | valor | 9 | 98.02 | 7.25 | 491.71 |
59 | valor | 10 | 140.89 | 10.22 | 625.93 |
60 | valor | 11 | 291.00 | 5.75 | 1189.31 |
61 | valor | 12 | 189.80 | 16.60 | 833.43 |
62 | valor | 13 | 179.00 | 18.00 | 1136.70 |
minicubo[minicubo.variable == 'valor'].set_index('value').plot.bar(subplots = True, sharex = False, figsize = (12,12))
plt.tight_layout();
Se podría mejorar el análisis porque en F y M los atípicos hacen que se concentren la mayoría de los datos en la categoría 1.
Lo que habría que hacer es eliminar esos atípicos y volver a realizar el ejercicio.
Te lo dejo como tarea de práctica.
Pero con este análisis somos capaces de identificar los clientes que con mayor probabilidad responderán mejor a nuevas campañas, además de obtener un montón de insights valiosos para el negocio.
Entendiendo los productos¶
Vamos a crear un dataframe a nivel de producto para poder analizar esta dimensión.
Primero calculamos los conteos de cada evento en cada producto.
prod = df.groupby(['producto','evento']).size()
prod
producto evento 3752 view 10 3762 cart 127 purchase 28 remove_from_cart 59 view 258 ... 5932538 view 1 5932540 cart 1 view 2 5932578 view 1 5932585 view 2 Length: 137068, dtype: int64
prod = prod.unstack(level = 1).fillna(0)
prod
evento | cart | purchase | remove_from_cart | view |
---|---|---|---|---|
producto | ||||
3752 | 0.00 | 0.00 | 0.00 | 10.00 |
3762 | 127.00 | 28.00 | 59.00 | 258.00 |
3763 | 10.00 | 2.00 | 2.00 | 51.00 |
3771 | 0.00 | 0.00 | 0.00 | 9.00 |
3774 | 26.00 | 7.00 | 13.00 | 76.00 |
... | ... | ... | ... | ... |
5932537 | 1.00 | 0.00 | 0.00 | 1.00 |
5932538 | 0.00 | 0.00 | 0.00 | 1.00 |
5932540 | 1.00 | 0.00 | 0.00 | 2.00 |
5932578 | 0.00 | 0.00 | 0.00 | 1.00 |
5932585 | 0.00 | 0.00 | 0.00 | 2.00 |
45327 rows × 4 columns
Vamos a incorporar el precio, para ello primero creamos un maestro de precios por producto.
maestro_precios = df.groupby('producto', as_index = False).precio.mean()
maestro_precios
producto | precio | |
---|---|---|
0 | 3752 | 15.71 |
1 | 3762 | 19.29 |
2 | 3763 | 16.00 |
3 | 3771 | 15.08 |
4 | 3774 | 15.92 |
... | ... | ... |
45322 | 5932537 | 1.43 |
45323 | 5932538 | 1.43 |
45324 | 5932540 | 1.43 |
45325 | 5932578 | 6.02 |
45326 | 5932585 | 6.33 |
45327 rows × 2 columns
prod = pd.merge(left = prod, right = maestro_precios, how = 'left', on = 'producto')
prod
producto | cart | purchase | remove_from_cart | view | precio | |
---|---|---|---|---|---|---|
0 | 3752 | 0.00 | 0.00 | 0.00 | 10.00 | 15.71 |
1 | 3762 | 127.00 | 28.00 | 59.00 | 258.00 | 19.29 |
2 | 3763 | 10.00 | 2.00 | 2.00 | 51.00 | 16.00 |
3 | 3771 | 0.00 | 0.00 | 0.00 | 9.00 | 15.08 |
4 | 3774 | 26.00 | 7.00 | 13.00 | 76.00 | 15.92 |
... | ... | ... | ... | ... | ... | ... |
45322 | 5932537 | 1.00 | 0.00 | 0.00 | 1.00 | 1.43 |
45323 | 5932538 | 0.00 | 0.00 | 0.00 | 1.00 | 1.43 |
45324 | 5932540 | 1.00 | 0.00 | 0.00 | 2.00 | 1.43 |
45325 | 5932578 | 0.00 | 0.00 | 0.00 | 1.00 | 6.02 |
45326 | 5932585 | 0.00 | 0.00 | 0.00 | 2.00 | 6.33 |
45327 rows × 6 columns
Reordenamos los nombres.
prod
producto | cart | purchase | remove_from_cart | view | precio | |
---|---|---|---|---|---|---|
0 | 3752 | 0.00 | 0.00 | 0.00 | 10.00 | 15.71 |
1 | 3762 | 127.00 | 28.00 | 59.00 | 258.00 | 19.29 |
2 | 3763 | 10.00 | 2.00 | 2.00 | 51.00 | 16.00 |
3 | 3771 | 0.00 | 0.00 | 0.00 | 9.00 | 15.08 |
4 | 3774 | 26.00 | 7.00 | 13.00 | 76.00 | 15.92 |
... | ... | ... | ... | ... | ... | ... |
45322 | 5932537 | 1.00 | 0.00 | 0.00 | 1.00 | 1.43 |
45323 | 5932538 | 0.00 | 0.00 | 0.00 | 1.00 | 1.43 |
45324 | 5932540 | 1.00 | 0.00 | 0.00 | 2.00 | 1.43 |
45325 | 5932578 | 0.00 | 0.00 | 0.00 | 1.00 | 6.02 |
45326 | 5932585 | 0.00 | 0.00 | 0.00 | 2.00 | 6.33 |
45327 rows × 6 columns
prod = prod[['producto','view','cart','remove_from_cart','purchase','precio']]
prod
producto | view | cart | remove_from_cart | purchase | precio | |
---|---|---|---|---|---|---|
0 | 3752 | 10.00 | 0.00 | 0.00 | 0.00 | 15.71 |
1 | 3762 | 258.00 | 127.00 | 59.00 | 28.00 | 19.29 |
2 | 3763 | 51.00 | 10.00 | 2.00 | 2.00 | 16.00 |
3 | 3771 | 9.00 | 0.00 | 0.00 | 0.00 | 15.08 |
4 | 3774 | 76.00 | 26.00 | 13.00 | 7.00 | 15.92 |
... | ... | ... | ... | ... | ... | ... |
45322 | 5932537 | 1.00 | 1.00 | 0.00 | 0.00 | 1.43 |
45323 | 5932538 | 1.00 | 0.00 | 0.00 | 0.00 | 1.43 |
45324 | 5932540 | 2.00 | 1.00 | 0.00 | 0.00 | 1.43 |
45325 | 5932578 | 1.00 | 0.00 | 0.00 | 0.00 | 6.02 |
45326 | 5932585 | 2.00 | 0.00 | 0.00 | 0.00 | 6.33 |
45327 rows × 6 columns
¿Cuales son los productos más vendidos?¶
prod.sort_values('purchase',ascending = False)[0:20]
producto | view | cart | remove_from_cart | purchase | precio | |
---|---|---|---|---|---|---|
16807 | 5809910 | 9195.00 | 2796.00 | 1249.00 | 764.00 | 5.21 |
28178 | 5854897 | 624.00 | 2486.00 | 793.00 | 483.00 | 0.32 |
6644 | 5700037 | 1150.00 | 2603.00 | 716.00 | 361.00 | 0.40 |
314 | 5304 | 516.00 | 1184.00 | 426.00 | 341.00 | 0.32 |
9900 | 5751422 | 2204.00 | 1119.00 | 625.00 | 331.00 | 10.87 |
15394 | 5802432 | 701.00 | 2495.00 | 745.00 | 322.00 | 0.32 |
16809 | 5809912 | 3059.00 | 1352.00 | 863.00 | 321.00 | 5.19 |
18415 | 5815662 | 1219.00 | 1697.00 | 653.00 | 310.00 | 0.91 |
9862 | 5751383 | 2341.00 | 1035.00 | 550.00 | 298.00 | 10.24 |
14043 | 5792800 | 1527.00 | 911.00 | 512.00 | 285.00 | 10.25 |
26312 | 5849033 | 2099.00 | 1035.00 | 583.00 | 278.00 | 10.25 |
5386 | 5686925 | 344.00 | 1677.00 | 499.00 | 231.00 | 0.35 |
6653 | 5700046 | 432.00 | 1376.00 | 381.00 | 215.00 | 0.40 |
1761 | 5528035 | 1146.00 | 719.00 | 401.00 | 200.00 | 9.44 |
22111 | 5833330 | 680.00 | 576.00 | 359.00 | 194.00 | 0.92 |
16808 | 5809911 | 1923.00 | 828.00 | 599.00 | 189.00 | 5.21 |
18525 | 5816170 | 1642.00 | 751.00 | 532.00 | 182.00 | 5.22 |
5420 | 5687151 | 508.00 | 540.00 | 288.00 | 179.00 | 1.90 |
8232 | 5729864 | 160.00 | 505.00 | 211.00 | 176.00 | 0.41 |
24787 | 5843836 | 165.00 | 1007.00 | 265.00 | 172.00 | 0.38 |
Posiblemente lograríamos incrementar las ventas y el ticket medio simplemente destacando estos productos en la tienda.
¿Hay productos que no se venden y podríamos eliminar del catálogo?¶
prod[prod.purchase == 0]
producto | view | cart | remove_from_cart | purchase | precio | |
---|---|---|---|---|---|---|
0 | 3752 | 10.00 | 0.00 | 0.00 | 0.00 | 15.71 |
3 | 3771 | 9.00 | 0.00 | 0.00 | 0.00 | 15.08 |
6 | 3790 | 10.00 | 0.00 | 0.00 | 0.00 | 7.92 |
8 | 3809 | 2.00 | 0.00 | 0.00 | 0.00 | 12.54 |
9 | 3812 | 1.00 | 0.00 | 0.00 | 0.00 | 12.54 |
... | ... | ... | ... | ... | ... | ... |
45322 | 5932537 | 1.00 | 1.00 | 0.00 | 0.00 | 1.43 |
45323 | 5932538 | 1.00 | 0.00 | 0.00 | 0.00 | 1.43 |
45324 | 5932540 | 2.00 | 1.00 | 0.00 | 0.00 | 1.43 |
45325 | 5932578 | 1.00 | 0.00 | 0.00 | 0.00 | 6.02 |
45326 | 5932585 | 2.00 | 0.00 | 0.00 | 0.00 | 6.33 |
21850 rows × 6 columns
Guauu, hemos encontrado una buena!
INSIGHT #8: Casi la mitad de los productos no han tenido ninguna venta en los 5 meses del histórico.
Se podría comenzar todo un nuevo análisis sobre estos productos:
- ¿No se ven?
- ¿Se ven pero no se compran?
- ¿Es porque se sustituyen por otros productos propios?
- ¿Es porque están mucho más baratos en la competencia?
- Etc
Se podrían eliminar del catálogo, o como mínimo de la tienda, newsletter, etc, para que no ocupen espacio de los productos que sí se venden.
¿Cual es la relación entre el precio y el volumen de ventas?¶
Ya que este análisis incluye las ventas vamos a eliminar los productos que no han tenido ninguna.
sns.scatterplot(data = prod[prod.purchase > 0], x = 'precio', y = 'purchase', hue = 'precio');
Sí que existe una clara relación decreciente.
Vamos a hacer zoom por ejemplo por debajo de 50€ para entenderlo mejor.
sns.scatterplot(data = prod[(prod.purchase > 0) & (prod.precio < 50)], x = 'precio', y = 'purchase', hue = 'precio');
¿Hay productos de los que los clientes se arrepienten y eliminan más del carrito?¶
prod.loc[prod.cart > 30].sort_values('remove_from_cart_porc', ascending = False)[0:30]
producto | view | cart | remove_from_cart | remove_from_cart_porc | purchase | precio | |
---|---|---|---|---|---|---|---|
14330 | 5797131 | 26.00 | 38.00 | 136.00 | 357.89 | 7.00 | 4.43 |
37937 | 5893670 | 36.00 | 35.00 | 109.00 | 311.43 | 3.00 | 4.90 |
29128 | 5858481 | 41.00 | 31.00 | 64.00 | 206.45 | 7.00 | 4.55 |
16658 | 5809346 | 8.00 | 34.00 | 62.00 | 182.35 | 7.00 | 0.78 |
26120 | 5848412 | 34.00 | 37.00 | 66.00 | 178.38 | 12.00 | 0.79 |
37944 | 5893677 | 64.00 | 41.00 | 70.00 | 170.73 | 10.00 | 4.69 |
8416 | 5731470 | 39.00 | 34.00 | 58.00 | 170.59 | 10.00 | 6.32 |
3244 | 5635127 | 41.00 | 32.00 | 52.00 | 162.50 | 10.00 | 4.43 |
3217 | 5635096 | 32.00 | 32.00 | 52.00 | 162.50 | 11.00 | 4.42 |
21617 | 5830537 | 35.00 | 37.00 | 60.00 | 162.16 | 8.00 | 1.73 |
39359 | 5900645 | 47.00 | 33.00 | 52.00 | 157.58 | 8.00 | 4.39 |
6222 | 5696152 | 81.00 | 41.00 | 64.00 | 156.10 | 12.00 | 2.37 |
29629 | 5859474 | 44.00 | 43.00 | 67.00 | 155.81 | 12.00 | 1.72 |
31887 | 5867624 | 26.00 | 46.00 | 70.00 | 152.17 | 10.00 | 3.89 |
9227 | 5741027 | 89.00 | 35.00 | 53.00 | 151.43 | 7.00 | 5.19 |
2123 | 5560972 | 73.00 | 51.00 | 76.00 | 149.02 | 11.00 | 2.98 |
6235 | 5696184 | 38.00 | 41.00 | 61.00 | 148.78 | 7.00 | 2.37 |
17716 | 5813067 | 48.00 | 43.00 | 63.00 | 146.51 | 5.00 | 1.72 |
221 | 4874 | 26.00 | 39.00 | 57.00 | 146.15 | 10.00 | 0.37 |
27643 | 5853242 | 47.00 | 43.00 | 62.00 | 144.19 | 6.00 | 3.15 |
33396 | 5875280 | 53.00 | 32.00 | 46.00 | 143.75 | 5.00 | 5.53 |
34325 | 5877765 | 62.00 | 35.00 | 50.00 | 142.86 | 9.00 | 9.02 |
23183 | 5837619 | 128.00 | 81.00 | 115.00 | 141.98 | 18.00 | 1.73 |
23833 | 5839637 | 55.00 | 43.00 | 61.00 | 141.86 | 12.00 | 2.37 |
13942 | 5789608 | 57.00 | 32.00 | 45.00 | 140.62 | 7.00 | 4.69 |
28667 | 5857018 | 33.00 | 32.00 | 45.00 | 140.62 | 7.00 | 3.15 |
3005 | 5619864 | 80.00 | 47.00 | 66.00 | 140.43 | 8.00 | 2.84 |
3205 | 5635081 | 26.00 | 35.00 | 49.00 | 140.00 | 4.00 | 4.40 |
30741 | 5863821 | 42.00 | 51.00 | 71.00 | 139.22 | 1.00 | 4.49 |
10701 | 5760769 | 38.00 | 31.00 | 43.00 | 138.71 | 6.00 | 2.62 |
Habría que ver por qué estos productos se eliminan más veces de las que se añaden:
- Si el motivo tiene sentido: revisar qué pasa con estos productos (otros productos alternativos, etc.)
- Si no lo tiene eliminar estos registros y analizar únicamente los que tienen remove_from_cart_porc menor o igual a 100
¿Cuales son los productos más vistos?¶
prod.view.sort_values(ascending = False)[0:20].plot.bar();
Posiblemente lograríamos incrementar las ventas y el ticket medio simplemente destacando estos productos en la tienda.
Siempre que además de ser vistos también se vendan.
¿Hay productos deseados pero no comprados?¶
Por ejemplo productos que miran muchos clientes pero que luego no los compran.
Si los encontráramos habría que revisar qué pasa con ellos.
sns.scatterplot(data = prod, x = 'view', y = 'purchase');
Vamos a quitar el atípico y hacer zoom en la ventana de muchas vistas pocas compras.
sns.scatterplot(data = prod.loc[prod.view < 4000], x = 'view', y = 'purchase', hue = 'precio')
plt.xlim(1000,3000)
plt.ylim(0,150)
(0.0, 150.0)
Hay una oportunidad con estos productos, porque por algún motivo generan el interés de los clientes, pero finalmente no los compran.
Habría que hacer un análisis sobre ellos.
Construyendo un sistema de recomendación¶
Uno de los activos que más pueden incrementar las ventas de un ecommerce es un sistema de recomendación.
Ya podríamos aplicar uno básico con los análisis de más visto y más vendido realizados anteriormente.
Pero la verdadera potencia viene cuando creamos un recomendador que personaliza para cada compra.
Tipos de sistemas de recomendación:
- Filtrado colaborativo:
- Basados en items
- Basados en usuario
- De contenido
En nuestro caso vamos a desarrollar uno con filtrado colaborativo basado en items.
Los pasos a seguir son:
- Crear el dataframe con el kpi de interés
- Reducir la dimensión (opcional)
- Seleccionar una métrica de distancia
- Calcular la matriz item-item
- Crear la lógica de priorización
Crear el dataframe con el kpi de interés¶
En este caso usaremos lo que se llama un kpi implícito, que será el número de veces que los productos han sido comprados por el mismo usuario.
Kpis explícitos sería por ejemplo estrellas o puntuaciones del 1 al 10.
Dado que este es una algoritmo que tarda en calcularse vamos a reducir el problema y calcularlo solo para los 100 productos más vendidos.
Además, por motivos didácticos vamos a hacer el paso a paso manualmente. En un uso real se recomienda usar un paquete ya preconstruído que posiblemente estará más optimizado en cuanto a rendimiento.
Primero calculamos un maestro con los 100 productos más vendidos.
mas_vendidos = prod.sort_values('purchase', ascending = False).producto[0:100]
mas_vendidos
16807 5809910 28178 5854897 6644 5700037 314 5304 9900 5751422 ... 30395 5862564 22751 5835859 9778 5749720 9732 5749149 22116 5833335 Name: producto, Length: 100, dtype: int64
Creamos un dataframe temporal filtrando por estos productos.
temp = df.loc[df.producto.isin(mas_vendidos)]
temp
usuario | sesion | categoria | evento | producto | precio | date | año | mes | dia | hora | minuto | segundo | festivo | black_friday | san_valentin | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
fecha | ||||||||||||||||
2019-10-01 00:26:49 | 536128518 | a31f0991-645e-4472-a012-95eb2f814568 | 1487580006317032337 | purchase | 5815662 | 0.92 | 2019-10-01 | 2019 | 10 | 1 | 0 | 26 | 49 | 0 | 0 | 0 |
2019-10-01 00:46:20 | 555415545 | b9cc1771-9062-4e08-a3ad-363314cd17d8 | 1602943681873052386 | view | 5809912 | 5.24 | 2019-10-01 | 2019 | 10 | 1 | 0 | 46 | 20 | 0 | 0 | 0 |
2019-10-01 00:48:13 | 555415545 | b9cc1771-9062-4e08-a3ad-363314cd17d8 | 1602943681873052386 | view | 5816170 | 5.24 | 2019-10-01 | 2019 | 10 | 1 | 0 | 48 | 13 | 0 | 0 | 0 |
2019-10-01 00:52:39 | 555415545 | b9cc1771-9062-4e08-a3ad-363314cd17d8 | 1487580005092295511 | view | 5815730 | 10.95 | 2019-10-01 | 2019 | 10 | 1 | 0 | 52 | 39 | 0 | 0 | 0 |
2019-10-01 01:33:26 | 555456891 | b3239dc3-f107-4034-a507-4c41f646e38a | 1487580005092295511 | view | 5849033 | 10.32 | 2019-10-01 | 2019 | 10 | 1 | 1 | 33 | 26 | 0 | 0 | 0 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
2020-02-29 23:11:44 | 615102046 | 17b94398-0397-4c59-bc84-fe91dde0a8ec | 1487580006509970331 | cart | 5793703 | 2.22 | 2020-02-29 | 2020 | 2 | 29 | 23 | 11 | 44 | 0 | 0 | 0 |
2020-02-29 23:12:40 | 615102046 | 17b94398-0397-4c59-bc84-fe91dde0a8ec | 2195085255034011676 | cart | 5550302 | 1.21 | 2020-02-29 | 2020 | 2 | 29 | 23 | 12 | 40 | 0 | 0 | 0 |
2020-02-29 23:12:50 | 599909613 | 1c6c708d-135d-487b-afa9-4bbcfd28db4d | 1602943681873052386 | cart | 5809911 | 5.24 | 2020-02-29 | 2020 | 2 | 29 | 23 | 12 | 50 | 0 | 0 | 0 |
2020-02-29 23:20:21 | 231719601 | a7467d5c-e848-406f-97f4-fcb6a4113e68 | 1602943681873052386 | view | 5816170 | 5.24 | 2020-02-29 | 2020 | 2 | 29 | 23 | 20 | 21 | 0 | 0 | 0 |
2020-02-29 23:58:49 | 147995998 | 5ff96629-3627-493e-a25b-5a871ec78c90 | 1487580006317032337 | cart | 5815662 | 0.92 | 2020-02-29 | 2020 | 2 | 29 | 23 | 58 | 49 | 0 | 0 | 0 |
168170 rows × 16 columns
Creamos la matriz usuario-item.
usuario_item = temp.loc[temp.evento == 'purchase'].groupby(['usuario','producto']).size().unstack(level = 1).fillna(0)
usuario_item
producto | 4497 | 4600 | 4768 | 4938 | 4958 | 5013 | 5304 | 5526 | 5528034 | 5528035 | ... | 5848909 | 5849033 | 5854812 | 5854897 | 5855332 | 5857007 | 5857360 | 5862564 | 5862943 | 5889300 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
usuario | |||||||||||||||||||||
25392526 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | ... | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 |
50748978 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | ... | 0.00 | 0.00 | 0.00 | 1.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 |
74332980 | 0.00 | 0.00 | 0.00 | 1.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | ... | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 |
80577370 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | 1.00 | ... | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 |
88211255 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | ... | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
621646584 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | 0.00 | 0.00 | 0.00 | ... | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 |
621788730 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | ... | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 | 0.00 |
621925941 | 1.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | ... | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 |
621974977 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | ... | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 |
622021687 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | ... | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 |
5064 rows × 100 columns
Reducir la dimensión (opcional)¶
Vemos que nos ha salido una matriz sparse.
Posiblemente sería conveniente reducir la dimensión con técnicas como SVD, pero eso requería un minicurso en sí mismo.
Aquí vamos a continuar sin hacer la reducción.
Seleccionar una métrica de distancia¶
Métricas más comunes:
- Distancia euclídea
- Correlación
- Coseno
En este caso vamos a coger por ejemplo la distancia euclídea.
La operativizamos mediante la función spatial.distance.euclidean de Scipy.
from scipy import spatial
Calcular la matriz item-item¶
Creamos el recomendador que toma como input una matriz usuario-item y devuelve una matriz item-item con la distancia euclídea como dato.
def recomendador(dataframe):
def distancia(producto):
return(dataframe.apply(lambda x: spatial.distance.euclidean(x,producto)))
return(dataframe.apply(lambda x: distancia(x)))
item_item = recomendador(usuario_item)
item_item
producto | 4497 | 4600 | 4768 | 4938 | 4958 | 5013 | 5304 | 5526 | 5528034 | 5528035 | ... | 5848909 | 5849033 | 5854812 | 5854897 | 5855332 | 5857007 | 5857360 | 5862564 | 5862943 | 5889300 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
producto | |||||||||||||||||||||
4497 | 0.00 | 14.42 | 14.49 | 15.62 | 15.91 | 17.58 | 23.39 | 15.23 | 16.16 | 20.30 | ... | 15.72 | 24.45 | 14.97 | 26.10 | 15.30 | 15.07 | 15.78 | 15.17 | 16.40 | 15.33 |
4600 | 14.42 | 0.00 | 10.68 | 14.49 | 14.59 | 16.46 | 22.69 | 13.93 | 14.87 | 19.70 | ... | 14.39 | 23.87 | 13.49 | 25.75 | 13.78 | 13.60 | 14.46 | 13.86 | 14.73 | 13.89 |
4768 | 14.49 | 10.68 | 0.00 | 14.56 | 14.73 | 16.28 | 22.74 | 14.00 | 14.93 | 19.75 | ... | 14.18 | 24.00 | 13.27 | 25.63 | 13.86 | 13.75 | 14.39 | 13.86 | 14.80 | 14.11 |
4938 | 15.62 | 14.49 | 14.56 | 0.00 | 15.52 | 16.03 | 22.96 | 14.76 | 15.65 | 20.25 | ... | 15.13 | 24.17 | 14.35 | 25.87 | 14.76 | 14.59 | 15.46 | 14.97 | 15.72 | 15.13 |
4958 | 15.91 | 14.59 | 14.73 | 15.52 | 0.00 | 17.49 | 22.27 | 15.33 | 15.81 | 20.32 | ... | 15.49 | 24.35 | 14.66 | 26.15 | 14.87 | 14.97 | 15.94 | 15.26 | 16.12 | 15.03 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
5857007 | 15.07 | 13.60 | 13.75 | 14.59 | 14.97 | 16.79 | 22.80 | 14.59 | 15.10 | 19.62 | ... | 14.49 | 23.98 | 13.89 | 25.61 | 14.32 | 0.00 | 14.83 | 14.46 | 15.17 | 14.42 |
5857360 | 15.78 | 14.46 | 14.39 | 15.46 | 15.94 | 16.06 | 23.24 | 15.39 | 16.12 | 20.32 | ... | 15.43 | 23.85 | 14.73 | 25.46 | 15.20 | 14.83 | 0.00 | 13.75 | 16.19 | 15.43 |
5862564 | 15.17 | 13.86 | 13.86 | 14.97 | 15.26 | 15.72 | 22.91 | 14.70 | 15.39 | 19.90 | ... | 15.00 | 23.32 | 14.00 | 25.12 | 14.35 | 14.46 | 13.75 | 0.00 | 15.39 | 14.66 |
5862943 | 16.40 | 14.73 | 14.80 | 15.72 | 16.12 | 17.66 | 23.62 | 15.33 | 16.00 | 20.42 | ... | 14.90 | 24.43 | 14.87 | 26.42 | 15.26 | 15.17 | 16.19 | 15.39 | 0.00 | 15.23 |
5889300 | 15.33 | 13.89 | 14.11 | 15.13 | 15.03 | 17.03 | 23.24 | 14.32 | 15.43 | 19.77 | ... | 14.90 | 23.90 | 14.04 | 25.69 | 14.04 | 14.42 | 15.43 | 14.66 | 15.23 | 0.00 |
100 rows × 100 columns
Crear la lógica de priorización¶
Ya tenemos listo el recomendador.
Lo que tendríamos que hacer es una llamada a esta tabla cada vez que un usuario mire un producto o lo meta en el carrito.
Pero para que sea más efectivo podríamos usar toda la info acumulada de la sesión o incluso de todo el usuario si está logado.
Eso significa que necesitamos un sistema para recomendar productos tanto si el input es de un solo producto como de varios.
Y que a la vez devuelva varias recomendaciones, para cubrir todos los "huecos" de recomendación que nuestra web pudiera tener.
Aplicaremos un algoritmo muy sencillo que hará:
- Crear un array con los productos de entrada para extraer sus vectores de la matriz item-item
- Calcular la suma de distancias de todos los productos
- Quitarse a ellos mismos para no autorecomendarse.
- Devolver los 10 con menor distancia
#En el caso de varios productos vendrá del servidor web como una cadena separada con punto y coma
def priorizador(productos, devuelve=10):
#crear array con productos de entrada
array = np.int64(productos.split(';'))
#extraer sus vectores de la matriz total
matriz = item_item[array]
#calcular la suma de distancias
suma_distancias = matriz.agg("sum", axis=1) # Cambiado sum por "sum"
#eliminar los productos input
suma_distancias = suma_distancias.loc[~suma_distancias.index.isin(list(array))]
#Devolver los 10 con menor distancia
return(suma_distancias.sort_values()[0:devuelve])
Comprobamos cómo funciona si le pasamos un producto
priorizador('4497')
producto 5724230 14.39 4600 14.42 5550302 14.49 4768 14.49 5749149 14.56 5833318 14.63 5824810 14.70 5835859 14.70 5833335 14.73 5809303 14.73 dtype: float64
Comprobamos cómo funciona si le pasamos varios productos
priorizador('4497;4600;4768')
producto 5749149 40.25 5833318 40.47 5833335 40.81 5809303 40.81 5724230 41.00 5824810 41.08 5835859 41.23 5550302 41.47 5816169 41.51 5844894 41.55 dtype: float64
CONCLUSIONES¶
La tendencia actual es plana en todas las métricas, lo que confirma la necesidad de las acciones de CRO.
Tras el análisis realizado sobre los datos transaccionales se ha desarrollado un plan CRO de 12 iniciativas concretas organizadas en 5 grandes palancas de negocio que con alta probabilidad van a incrementar los baselines consiguiendo un incremento global de los ingresos del ecommerce.
Baseline¶
En cada sesión, de media:
- KPIs por sesión: Se ven 2.2 productos
- KPIs por sesión: Se añaden 1.3 productos al carrito
- KPIs por sesión: Se eliminan 0.9 productos del carrito
- KPIs por sesión: Se compran 0.3 productos
- Venta cruzada: mediana de 5 productos por compra
- Recurrencia: el 10% de los clientes vuelve a comprar tras el primer mes
- Conversión: 60% de añadir al carrito sobre visualizaciones
- Conversión: 22% de compra sobre añadidos a carrito
- Conversión: 13% de compra sobre visualizaciones
- Facturación media mensual: 125.000€
Acciones de incremento de visualizaciones¶
- Revisar las campañas de paid (generación y retargeting) para concentrar la inversión en franjas entre las 9 y las 13 y entre las 18 y las 20
- Concentrar la inversión del período navideño y post-navideño en la semana del black friday
- Incrementar la inversión hasta llegar al CPA máximo en base al LTV que hemos identificado
Acciones de incremento de conversión¶
- Preconfigurar la home con los productos identificados en los análisis most viewed y most sold.
- Trabajar sobre los productos con alta tasa de abandono de carrito
- Trabajar sobre los productos muy vistos pero poco comprados
Acciones de incremento de venta cruzada¶
- La compra mediana incluye 5 productos
- Incrementar este ratio mediante la recomendación en tiempo real con el nuevo recomendador
Acciones de incremento de frecuencia de compra¶
- El 90% de los clientes sólo hace una compra
- Crear una newsletter periódica con el nuevo recomendador para incrementar la frecuencia de visita
- Campañas promocionales sobre los segmentos top de la segmentación RFM
Acciones de fidelización de clientes¶
- Crear un programa de fidelización segmentado por la nueva segmentación RFM