Modelo gamma-gamma#
In this notebook we show how to fit a Gamma-Gamma model in PyMC-Marketing. The model is presented in the paper: Fader, P. S., & Hardie, B. G. (2013). The Gamma-Gamma model of monetary value. February, 2, 1-9.
Preparar el cuaderno#
import arviz as az
import matplotlib.pyplot as plt
import pandas as pd
from pymc_marketing import clv
# Plotting configuration
az.style.use("arviz-darkgrid")
plt.rcParams["figure.figsize"] = [10, 6]
plt.rcParams["figure.dpi"] = 100
plt.rcParams["figure.facecolor"] = "white"
%load_ext autoreload
%autoreload 2
%config InlineBackend.figure_format = "retina"
Cargar Datos#
Comenzamos cargando el conjunto de datos CDNOW.
data_path = "https://raw.githubusercontent.com/pymc-labs/pymc-marketing/main/data/clv_quickstart.csv"
summary_with_money_value = pd.read_csv(data_path)
summary_with_money_value["customer_id"] = summary_with_money_value.index
summary_with_money_value.head()
| frequency | recency | T | monetary_value | customer_id | |
|---|---|---|---|---|---|
| 0 | 2 | 30.43 | 38.86 | 22.35 | 0 |
| 1 | 1 | 1.71 | 38.86 | 11.77 | 1 |
| 2 | 0 | 0.00 | 38.86 | 0.00 | 2 |
| 3 | 0 | 0.00 | 38.86 | 0.00 | 3 |
| 4 | 0 | 0.00 | 38.86 | 0.00 | 4 |
Para el modelo Gamma-Gamma, necesitamos filtrar a los clientes que han realizado solo una compra.
returning_customers_summary = summary_with_money_value.query("frequency > 0")
returning_customers_summary.head()
| frequency | recency | T | monetary_value | customer_id | |
|---|---|---|---|---|---|
| 0 | 2 | 30.43 | 38.86 | 22.35 | 0 |
| 1 | 1 | 1.71 | 38.86 | 11.77 | 1 |
| 5 | 7 | 29.43 | 38.86 | 73.74 | 5 |
| 6 | 1 | 5.00 | 38.86 | 11.77 | 6 |
| 8 | 2 | 35.71 | 38.86 | 25.55 | 8 |
Especificación del modelo#
Aquí describimos brevemente las suposiciones y la parametrización del modelo Gamma-Gamma del artículo mencionado.
El modelo de gasto por transacción se basa en las siguientes tres suposiciones generales:
El valor monetario de una transacción dada por un cliente varía aleatoriamente en torno a su valor promedio de transacción.
Los valores promedio de transacción varían entre los clientes, pero no varían con el tiempo para ningún individuo en particular.
La distribución de los valores promedio de transacción entre los clientes es independiente del proceso de transacción.
Para un cliente con x transacciones, sea \(z_1, z_2, \ldots, z_x\) el valor de cada transacción. El valor promedio de transacción observado del cliente por
Ahora describamos la parametrización:
Asumimos que \(z_i \sim \text{Gamma}(p, ν)\), con \(E(Z_i| p, ν) = \xi = p/ν\).
– Dadas las propiedades de convolución de la gamma, se deduce que el gasto total en x transacciones se distribuye como \(\text{Gamma}(px, ν)\).
– Dada la propiedad de escalado de la distribución gamma, se deduce que \(\bar{z} \sim \text{Gamma}(px, νx)\).
Asumimos \(ν \sim \text{Gamma}(q, \gamma)\).
Estamos interesados en estimar los parámetros \(p\), \(q\) y \(ν\).
Nota
El modelo Gamma-Gamma asume que no existe relación entre el valor monetario y la frecuencia de compra. Podemos verificar esta suposición calculando la correlación entre el gasto promedio y la frecuencia de compras.
returning_customers_summary[["monetary_value", "frequency"]].corr()
| monetary_value | frequency | |
|---|---|---|
| monetary_value | 1.000000 | 0.113884 |
| frequency | 0.113884 | 1.000000 |
El valor de esta correlación es cercano a \(0.11\), lo cual en la práctica se considera lo suficientemente bajo como para proceder con el modelo.
Implementación de PyMC-Marketing#
Podemos utilizar la implementación preconstruida de PyMC-Marketing del modelo Gamma-Gamma, que también ofrece métodos de representación gráfica y predicción agradables:
Podemos construir el modelo para que podamos ver la especificación del modelo:
model = clv.GammaGammaModel()
model.build_model(data=returning_customers_summary)
model
Gamma-Gamma Model (Mean Transactions)
p ~ HalfFlat()
q ~ HalfFlat()
v ~ HalfFlat()
likelihood ~ Potential(f(q, p, v))
Nota
No es necesario construir el modelo antes de ajustarlo. Podemos ajustar el modelo directamente.
Usando MAP#
Para comenzar, utilicemos un optimizador numérico (L-BFGS-B) de scipy.optimize para encontrar la estimación de máxima a posteriori (MAP) de los parámetros.
idata_map = model.fit(method="map").posterior.to_dataframe()
idata_map
| p | q | v | ||
|---|---|---|---|---|
| chain | draw | |||
| 0 | 0 | 6.248787 | 3.744591 | 15.447813 |
MCMC#
También podemos utilizar MCMC para muestrear de la distribución posterior de los parámetros. MCMC es un método más robusto que MAP y proporciona estimaciones de incertidumbre para los parámetros.
sampler_kwargs = {
"draws": 2_000,
"target_accept": 0.9,
"chains": 4,
"random_seed": 42,
}
idata_mcmc = model.fit(**sampler_kwargs)
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 2 jobs)
NUTS: [p, q, v]
Sampling 4 chains for 1_000 tune and 2_000 draw iterations (4_000 + 8_000 draws total) took 449 seconds.
idata_mcmc
-
<xarray.Dataset> Size: 208kB Dimensions: (chain: 4, draw: 2000) Coordinates: * chain (chain) int64 32B 0 1 2 3 * draw (draw) int64 16kB 0 1 2 3 4 5 6 ... 1994 1995 1996 1997 1998 1999 Data variables: p (chain, draw) float64 64kB 4.713 4.829 7.353 ... 5.578 6.317 5.654 q (chain, draw) float64 64kB 4.289 4.288 3.402 ... 4.176 3.863 3.836 v (chain, draw) float64 64kB 24.09 23.53 11.76 ... 20.22 16.49 17.34 Attributes: created_at: 2024-07-01T15:48:01.851699 arviz_version: 0.17.1 inference_library: pymc inference_library_version: 5.14.0 sampling_time: 448.93818640708923 tuning_steps: 1000 -
<xarray.Dataset> Size: 992kB Dimensions: (chain: 4, draw: 2000) Coordinates: * chain (chain) int64 32B 0 1 2 3 * draw (draw) int64 16kB 0 1 2 3 4 ... 1996 1997 1998 1999 Data variables: (12/17) n_steps (chain, draw) float64 64kB 47.0 11.0 ... 35.0 23.0 acceptance_rate (chain, draw) float64 64kB 0.9899 0.9385 ... 1.0 reached_max_treedepth (chain, draw) bool 8kB False False ... False False lp (chain, draw) float64 64kB -4.051e+03 ... -4.051e+03 process_time_diff (chain, draw) float64 64kB 0.02783 ... 0.03698 diverging (chain, draw) bool 8kB False False ... False False ... ... tree_depth (chain, draw) int64 64kB 6 4 6 6 5 2 ... 3 5 5 2 6 5 energy (chain, draw) float64 64kB 4.052e+03 ... 4.052e+03 perf_counter_start (chain, draw) float64 64kB 7.433e+04 ... 7.471e+04 index_in_trajectory (chain, draw) int64 64kB -19 5 -50 10 ... -27 1 16 -9 step_size_bar (chain, draw) float64 64kB 0.05982 ... 0.06508 perf_counter_diff (chain, draw) float64 64kB 0.04164 0.00677 ... 0.0375 Attributes: created_at: 2024-07-01T15:48:01.937737 arviz_version: 0.17.1 inference_library: pymc inference_library_version: 5.14.0 sampling_time: 448.93818640708923 tuning_steps: 1000 -
<xarray.Dataset> Size: 45kB Dimensions: (index: 946) Coordinates: * index (index) int64 8kB 0 1 5 6 8 10 ... 2347 2348 2349 2353 2355 Data variables: frequency (index) int64 8kB 2 1 7 1 2 5 10 1 3 2 ... 1 2 1 2 7 1 2 5 4 recency (index) float64 8kB 30.43 1.71 29.43 ... 21.86 24.29 26.57 T (index) float64 8kB 38.86 38.86 38.86 ... 27.0 27.0 27.0 monetary_value (index) float64 8kB 22.35 11.77 73.74 ... 18.56 44.93 33.32 customer_id (index) int64 8kB 0 1 5 6 8 10 ... 2347 2348 2349 2353 2355
Podemos ver algunas estadísticas de la distribución posterior de los parámetros.
model.fit_summary()
| mean | sd | hdi_3% | hdi_97% | mcse_mean | mcse_sd | ess_bulk | ess_tail | r_hat | |
|---|---|---|---|---|---|---|---|---|---|
| p | 6.403 | 1.303 | 4.217 | 8.710 | 0.032 | 0.023 | 1686.0 | 1783.0 | 1.0 |
| q | 3.781 | 0.290 | 3.238 | 4.338 | 0.007 | 0.005 | 1753.0 | 2187.0 | 1.0 |
| v | 16.051 | 4.243 | 8.050 | 23.907 | 0.107 | 0.076 | 1563.0 | 1746.0 | 1.0 |
Visualicemos las distribuciones posteriores y el gráfico de rangos:
axes = az.plot_trace(
data=model.idata,
compact=True,
kind="rank_bars",
backend_kwargs={"figsize": (12, 9), "layout": "constrained"},
)
plt.gcf().suptitle("Gamma-Gamma Model Trace", fontsize=18, fontweight="bold");
We can compare the MCMC posterior with the MAP estimation.
fig, axes = plt.subplots(
nrows=3, ncols=1, figsize=(12, 10), sharex=False, sharey=False, layout="constrained"
)
for i, var_name in enumerate(["p", "q", "v"]):
ax = axes[i]
az.plot_posterior(
idata_mcmc.posterior[var_name].values.flatten(),
color="C0",
point_estimate="mean",
hdi_prob=0.95,
ax=ax,
label="MCMC",
)
ax.axvline(x=idata_map[var_name].item(), color="C2", linestyle="-.", label="MAP")
ax.legend(loc="upper right")
plt.gcf().suptitle("Gamma-Gamma Model Parameters", fontsize=18, fontweight="bold");
We see that the MAP estimates are close to the mean of the posterior distribution obtained by MCMC.
Gasto Esperado del Cliente#
Una vez que tengamos la distribución posterior de los parámetros, podemos utilizar el método expected_average_profit para calcular la expectativa condicional del beneficio promedio por transacción para un grupo de uno o más clientes.
expected_spend = model.expected_customer_spend(data=summary_with_money_value)
Veamos cómo se ve para un subconjunto de clientes.
az.summary(expected_spend.isel(customer_id=range(10)), kind="stats")
| mean | sd | hdi_3% | hdi_97% | |
|---|---|---|---|---|
| x[0] | 24.706 | 0.512 | 23.839 | 25.762 |
| x[1] | 18.994 | 1.311 | 16.641 | 21.547 |
| x[2] | 35.195 | 0.924 | 33.447 | 36.887 |
| x[3] | 35.195 | 0.924 | 33.447 | 36.887 |
| x[4] | 35.195 | 0.924 | 33.447 | 36.887 |
| x[5] | 71.387 | 0.599 | 70.165 | 72.409 |
| x[6] | 18.994 | 1.311 | 16.641 | 21.547 |
| x[7] | 35.195 | 0.924 | 33.447 | 36.887 |
| x[8] | 27.318 | 0.394 | 26.627 | 28.107 |
| x[9] | 35.195 | 0.924 | 33.447 | 36.887 |
ax, *_ = az.plot_forest(
data=expected_spend.isel(customer_id=(range(10))), combined=True, figsize=(8, 7)
)
ax.set(xlabel="Expected Spend (10 Customers)", ylabel="Customer ID")
ax.set_title("Expected Spend", fontsize=18, fontweight="bold");
Finalmente, veamos algunas estadísticas y la distribución para todo el conjunto de datos.
az.summary(expected_spend.mean("customer_id"), kind="stats")
| mean | sd | hdi_3% | hdi_97% | |
|---|---|---|---|---|
| x | 35.268 | 0.629 | 34.107 | 36.442 |
fig, ax = plt.subplots()
az.plot_dist(expected_spend.mean("customer_id"), label="Mean over Customer ID", ax=ax)
ax.axvline(x=expected_spend.mean(), color="black", ls="--", label="Overall Mean")
ax.legend(loc="upper right")
ax.set(xlabel="Expected Spend", ylabel="Density")
ax.set_title("Expected Spend", fontsize=18, fontweight="bold");
%load_ext watermark
%watermark -n -u -v -iv -w -p pymc,pytensor
Last updated: Mon Jul 01 2024
Python implementation: CPython
Python version : 3.10.13
IPython version : 8.22.2
pymc : 5.14.0
pytensor: 2.20.0
arviz : 0.17.1
pymc_marketing: 0.6.0
matplotlib : 3.8.3
pandas : 2.2.1
Watermark: 2.4.3