CLV Inicio Rápido#

El Valor del Tiempo de Vida del Cliente (CLV) es la medida de la contribución de un cliente a un negocio a lo largo del tiempo. Esta métrica se utiliza para informar los niveles de gasto en la adquisición de nuevos clientes, la retención y otros esfuerzos de marketing y ventas, por lo que una estimación fiable es esencial.

PyMC-Marketing proporciona herramientas para segmentar a los clientes según su comportamiento pasado (ver Segmentación RFM) así como los siguientes modelos probabilísticos Buy Till You Die (BTYD) para predecir el comportamiento futuro:

Esta tabla contiene un desglose de los cuatro dominios de modelado BTYD y ejemplos para cada uno:

No contractual

Contractual

Continuo

compras en línea

tiempo de conversión de anuncios

Discreto

conciertos y eventos deportivos

suscripciones recurrentes

En este cuaderno, demostraremos cómo estimar la actividad de compra futura y el CLV con el conjunto de datos de CDNOW, un conjunto de datos de referencia popular en la investigación de CLV y BTYD. Los datos están disponibles {aquí}, con detalles adicionales {aquí}.

import arviz as az
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from arviz.labels import MapLabeller
from pymc_extras.prior import Prior

from pymc_marketing import clv
az.style.use("arviz-darkgrid")

%config InlineBackend.figure_format = "retina" # nice looking plots

1.1 Requisitos de Datos#

Para todos los modelos, se utiliza la siguiente nomenclatura:

  • customer_id representa un identificador único para cada cliente.

  • frequency representa el número de compras repetidas que un cliente ha realizado, es decir, uno menos que el total de compras.

  • T representa la «edad» de un cliente, es decir, la duración entre la primera compra de un cliente y el final del período de estudio. En este cuaderno de ejemplo, las unidades de tiempo están en semanas.

  • recency representa el período de tiempo en el que un cliente realizó su compra más reciente. Esto es igual a la duración entre la primera y la última compra de un cliente. Si un cliente ha realizado solo 1 compra, su recency es 0.

  • monetary_value representa el valor promedio de las compras repetidas de un cliente dado. Los clientes que solo han realizado una única compra tienen valores monetarios de cero.

La función rfm_summary se puede utilizar para preprocesar datos de transacciones en bruto para la modelización:

raw_trans = pd.read_csv(
    "https://raw.githubusercontent.com/pymc-labs/pymc-marketing/main/data/cdnow_transactions.csv"
)

raw_trans.head(5)
_id id date cds_bought spent
0 4 1 19970101 2 29.33
1 4 1 19970118 2 29.73
2 4 1 19970802 1 14.96
3 4 1 19971212 2 26.48
4 21 2 19970101 3 63.34
rfm_data = clv.utils.rfm_summary(
    raw_trans,
    customer_id_col="id",
    datetime_col="date",
    monetary_value_col="spent",
    datetime_format="%Y%m%d",
    time_unit="W",
)

rfm_data
customer_id frequency recency T monetary_value
0 1 3.0 49.0 78.0 23.723333
1 2 1.0 2.0 78.0 11.770000
2 3 0.0 0.0 78.0 0.000000
3 4 0.0 0.0 78.0 0.000000
4 5 0.0 0.0 78.0 0.000000
... ... ... ... ... ...
2352 2353 2.0 53.0 66.0 19.775000
2353 2354 5.0 24.0 66.0 44.928000
2354 2355 1.0 44.0 66.0 24.600000
2355 2356 6.0 62.0 66.0 31.871667
2356 2357 0.0 0.0 66.0 0.000000

2357 rows × 5 columns

Es importante señalar que estas definiciones difieren de las utilizadas en la segmentación RFM, donde se incluye la primera compra, no se utiliza T, y recencia es el número de períodos de tiempo desde la compra más reciente de un cliente.

Para visualizar datos en formato RFM, podemos trazar la recencia y T de los clientes con la función plot_customer_exposure. Observamos que un gran porcentaje (>60%) de los clientes no ha realizado otra compra en un tiempo.

fig, ax = plt.subplots(figsize=(10, 5))
(
    rfm_data.sample(n=100, random_state=42)
    .sort_values(["recency", "T"])
    .pipe(clv.plot_customer_exposure, ax=ax, linewidth=0.5, size=0.75)
);

Predicción del Comportamiento de Compra Futuro con el Modelo BG/NBD#

This dataset is an example of continuous time, non-contractual transactions because it comprises purchases from an online music store. PyMC-Marketing provides several models for this use case:

Usaremos el modelo BG/NBD en este cuaderno porque funciona bien para casos de uso básicos. Para un modelado más completo, los modelos Pareto/NBD y MBG/NBD tienen una funcionalidad ampliada y menos limitaciones.

Veamos la estructura subyacente del modelo BG/NBD:

bgm = clv.BetaGeoModel()

bgm.build_model(data=rfm_data)
bgm.graphviz()
../../_images/ba760052a47a6bf6f8ec11769ffc524137e54b9d4db3054a74e116d8c2e7e92a.svg

Los priors predeterminados para los parámetros de tasa de compra r y alpha siguen una distribución HalfFlat, que es una distribución positiva no informativa. Los parámetros de abandono a y b se agrupan con los priors jerárquicos kappa_dropout y phi_dropout para mejorar el rendimiento.

Se pueden especificar priors más informativos en una configuración de modelo personalizada pasando un diccionario con claves para los nombres de los priors y valores como distribuciones definidas con la clase Prior en PyMC-Marketing.

model_config = {
    "a": Prior("HalfNormal", sigma=10),
    "b": Prior("HalfNormal", sigma=10),
    "alpha": Prior("HalfNormal", sigma=10),
    "r": Prior("HalfNormal", sigma=10),
}
bgm = clv.BetaGeoModel(model_config=model_config)
bgm.build_model(data=rfm_data)
bgm
BG/NBD
            alpha ~ HalfNormal(0, 10)
                a ~ HalfNormal(0, 10)
                b ~ HalfNormal(0, 10)
                r ~ HalfNormal(0, 10)
recency_frequency ~ BetaGeoNBD(a, b, r, alpha, <constant>)
bgm.graphviz()
../../_images/763e7af5d041cea21686f5df79e1c3e9edbf0873d928664851981143b5c04102.svg

Habiendo especificado el modelo, ahora podemos ajustarlo.

Ajuste de Modelo con MAP#

Por defecto, fit genera posteriors bayesianos completos a través del muestreo de MCMC. Para conjuntos de datos extremadamente grandes donde no se necesitan estimaciones de incertidumbre y/o MCMC es demasiado lento, se puede utilizar máximo a posteriori para estimar estimaciones puntuales de los parámetros del modelo.

Utilice rfm_train_test_split para dividir en entrenamiento/prueba. Aquí estamos entrenando con 52 semanas de datos y reteniendo las 26 semanas restantes para las pruebas:

rfm_train_test_data = clv.utils.rfm_train_test_split(
    raw_trans,
    customer_id_col="id",
    datetime_col="date",
    monetary_value_col="spent",
    train_period_end="19980101",
    datetime_format="%Y%m%d",
    time_unit="W",
)

rfm_train_test_data
customer_id frequency recency T monetary_value test_frequency test_monetary_value test_T
0 1 3.0 49.0 52.0 23.723333 0.0 0.000 26.0
1 2 1.0 2.0 52.0 11.770000 0.0 0.000 26.0
2 3 0.0 0.0 52.0 0.000000 0.0 0.000 26.0
3 4 0.0 0.0 52.0 0.000000 0.0 0.000 26.0
4 5 0.0 0.0 52.0 0.000000 0.0 0.000 26.0
... ... ... ... ... ... ... ... ...
2352 2353 0.0 0.0 40.0 0.000000 2.0 19.775 26.0
2353 2354 5.0 24.0 40.0 44.928000 0.0 0.000 26.0
2354 2355 0.0 0.0 40.0 0.000000 1.0 24.600 26.0
2355 2356 4.0 26.0 40.0 33.317500 2.0 28.980 26.0
2356 2357 0.0 0.0 40.0 0.000000 0.0 0.000 26.0

2357 rows × 8 columns

Tenga en cuenta que cualquier cliente que haya realizado su primera compra durante el período de prueba será excluido. Utilice rfm_summary para retener a todos los clientes para el modelo final.

bgm_map = clv.BetaGeoModel()

bgm_map.fit(
    data=rfm_train_test_data,
    method="map",
)
bgm_map.fit_summary()

alpha            6.754
phi_dropout      0.219
kappa_dropout    2.171
r                0.283
a                0.475
b                1.696
Name: value, dtype: float64

Evaluemos el rendimiento del modelo al rastrear las predicciones en comparación con las compras históricas.

clv.plot_expected_purchases_over_time(
    model=bgm_map,
    purchase_history=raw_trans,
    datetime_col="date",
    customer_id_col="id",
    datetime_format="%Y%m%d",
    time_unit="W",
    t=78,
    set_index_date=True,
    t_start_eval=52,
);

Parece que MAP está sobreestimando el número de compras durante el período de prueba, y estas predicciones tampoco tienen estimaciones de incertidumbre.

Ajuste de Modelo con MCMC#

Se recomienda el muestreo MCMC para ilustrar la incertidumbre en las estimaciones de parámetros y crear intervalos de credibilidad en torno a las predicciones.

bgm.fit()
bgm.fit_summary()
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [alpha, a, b, r]

Sampling 4 chains for 1_000 tune and 1_000 draw iterations (4_000 + 4_000 draws total) took 7 seconds.
mean sd hdi_3% hdi_97% mcse_mean mcse_sd ess_bulk ess_tail r_hat
alpha 7.092 0.509 6.106 8.020 0.012 0.009 1727.0 2042.0 1.0
a 0.687 0.156 0.431 0.972 0.004 0.004 1667.0 1616.0 1.0
b 3.259 0.977 1.763 5.103 0.025 0.030 1624.0 1739.0 1.0
r 0.276 0.012 0.254 0.299 0.000 0.000 1558.0 2098.0 1.0

Podemos utilizar ArviZ, una biblioteca de Python diseñada para producir visualizaciones de modelos bayesianos, para trazar la distribución posterior de cada parámetro.

az.plot_posterior(bgm.fit_result);

1.2.1. Visualizando Predicciones a lo Largo del Tiempo#

Al observar nuevamente los gráficos de tiempo, podemos ver que los resultados se ajustan mucho más de cerca cuando se utilizan MCMC:

clv.plot_expected_purchases_over_time(
    model=bgm,
    purchase_history=raw_trans,
    datetime_col="date",
    customer_id_col="id",
    datetime_format="%Y%m%d",
    time_unit="W",
    t=78,
);

Purchases can be plotted cumulatively as well as incrementally:

clv.plot_expected_purchases_over_time(
    model=bgm,
    purchase_history=raw_trans,
    datetime_col="date",
    customer_id_col="id",
    datetime_format="%Y%m%d",
    time_unit="W",
    t=78,
    set_index_date=True,
    plot_cumulative=False,
);

Observe cómo el modelo es capaz de responder a los cambios de tendencia a lo largo del tiempo.

Visualizando Matrices de Predicción#

clv.plot_frequency_recency_matrix(bgm);

Podemos ver que nuestros mejores clientes han estado activos durante más de 60 semanas y han realizado más de 20 compras (esquina inferior derecha). Observe la «cola» que se eleva hacia la esquina superior izquierda; estos clientes son poco frecuentes y/o pueden no haber comprado recientemente. ¿Cuál es la probabilidad de que sigan activos?

clv.plot_probability_alive_matrix(bgm)
<Axes: title={'center': 'Probability Customer is Alive,\nby Frequency and Recency of a Customer'}, xlabel="Customer's Historical Frequency", ylabel="Customer's Recency">
../../_images/6f092e8738883a7adacfefda2808725e16719bc1570f9919aed04a690dcf64f3.png

Tenga en cuenta que todos los clientes no recurrentes tienen una probabilidad de vida de 1, lo cual es una de las peculiaridades de BetaGeoModel. En muchos casos de uso, esta sigue siendo una suposición válida, pero si los clientes no recurrentes son un enfoque clave en su caso de uso, puede que desee probar ParetoNBDModel en su lugar.

Al observar la matriz de probabilidad de permanencia, podemos inferir que los clientes que han realizado menos compras tienen menos probabilidades de regresar, y puede ser conveniente enfocarse en ellos para la retención.

Clasificando a los clientes de mejor a peor#

Having fit the model, we can ask what is the expected number of purchases for our customers over the next 10 time periods. Let’s look at the four most promising customers.

num_purchases = bgm.expected_purchases(future_t=10)

sdata = rfm_data.copy()
sdata["expected_purchases"] = num_purchases.mean(("chain", "draw")).values
sdata.sort_values(by="expected_purchases").tail(4)
customer_id frequency recency T monetary_value expected_purchases
812 813 30.0 72.0 74.0 35.654000 3.442797
1202 1203 32.0 71.0 72.0 47.172187 3.813601
156 157 36.0 74.0 77.0 30.603611 3.900803
1980 1981 35.0 66.0 68.0 46.748857 4.307768

También podemos trazar intervalos de credibilidad para las compras esperadas:

ids = [813, 1203, 157, 1981]
ax = az.plot_posterior(num_purchases.sel(customer_id=ids), grid=(2, 2))
for axi, id in zip(ax.ravel(), ids, strict=False):
    axi.set_title(f"Customer: {id}", size=20)
plt.suptitle("Expected Number of Purchase over 10 Time Periods", fontsize=28, y=1.05);

Predicción del comportamiento de compra de un nuevo cliente#

Podemos utilizar el modelo ajustado para predecir el número de compras de un nuevo cliente.

az.plot_posterior(bgm.expected_purchases_new_customer(t=10).sel(customer_id=1))
plt.title("Expected purchases of a new customer in the first 10 periods");

Historiales de Probabilidad de Clientes#

Dada la historia de transacciones de un cliente, podemos calcular su probabilidad histórica de estar vivo, según nuestro modelo entrenado.

Let’s look at active customer 1516 and assess the change in probability that the user will ever return if they do no other purchases in the next 9 time periods.

customer_1516 = rfm_data.loc[1515]
customer_1516
customer_id       1516.000000
frequency           27.000000
recency             67.000000
T                   70.000000
monetary_value      51.944074
Name: 1515, dtype: float64
customer_1516_history = pd.DataFrame(
    dict(
        customer_id=np.arange(10),
        frequency=np.full(10, customer_1516["frequency"], dtype="int"),
        recency=np.full(10, customer_1516["recency"]),
        T=(np.arange(0, 10) + customer_1516["recency"]).astype("int"),
    )
)
customer_1516_history
customer_id frequency recency T
0 0 27 67.0 67
1 1 27 67.0 68
2 2 27 67.0 69
3 3 27 67.0 70
4 4 27 67.0 71
5 5 27 67.0 72
6 6 27 67.0 73
7 7 27 67.0 74
8 8 27 67.0 75
9 9 27 67.0 76
p_alive = bgm.expected_probability_alive(data=customer_1516_history)
az.plot_hdi(customer_1516_history["T"], p_alive, color="C0")
plt.plot(customer_1516_history["T"], p_alive.mean(("draw", "chain")), marker="o")
plt.axvline(
    customer_1516_history["recency"].iloc[0], c="black", ls="--", label="Purchase"
)

plt.title("Probability Customer 1516 will purchase again")
plt.xlabel("T")
plt.ylabel("p")
plt.legend();

We can see that, if no purchases are being made in the next 9 weeks, the model has low confidence that the customer will ever return. What if they had done one purchase in between?

customer_1516_history.loc[7:, "frequency"] += 1
customer_1516_history.loc[7:, "recency"] = customer_1516_history.loc[7, "T"] - 0.5
customer_1516_history
customer_id frequency recency T
0 0 27 67.0 67
1 1 27 67.0 68
2 2 27 67.0 69
3 3 27 67.0 70
4 4 27 67.0 71
5 5 27 67.0 72
6 6 27 67.0 73
7 7 28 73.5 74
8 8 28 73.5 75
9 9 28 73.5 76
p_alive = bgm.expected_probability_alive(data=customer_1516_history)
az.plot_hdi(customer_1516_history["T"], p_alive, color="C0")
plt.plot(customer_1516_history["T"], p_alive.mean(("draw", "chain")), marker="o")
plt.axvline(
    customer_1516_history["recency"].iloc[0], c="black", ls="--", label="Purchase"
)
plt.axvline(customer_1516_history["recency"].iloc[-1], c="black", ls="--")

plt.title("Probability Customer 1516 will purchase again")
plt.xlabel("T")
plt.ylabel("p")
plt.legend();

Del gráfico anterior, se puede decir que el cliente 1516 realiza una compra en la semana 73.5, poco más de 6 semanas después de haber realizado su última compra registrada. ¡Podemos ver que la probabilidad de que el cliente regrese rápidamente vuelve a aumentar!

Estimando el Valor de Vida del Cliente Utilizando el Modelo Gamma-Gamma#

Hasta ahora nos hemos centrado principalmente en las frecuencias y probabilidades de transacción, pero para estimar el valor económico podemos utilizar el modelo Gamma-Gamma.

El modelo Gamma-Gamma asume que se ha observado al menos 1 transacción repetida por cliente. Por lo tanto, filtramos aquellos con cero compras repetidas.

nonzero_data = rfm_data.query("frequency>0")
nonzero_data
customer_id frequency recency T monetary_value
0 1 3.0 49.0 78.0 23.723333
1 2 1.0 2.0 78.0 11.770000
5 6 14.0 76.0 78.0 76.503571
6 7 1.0 5.0 78.0 11.770000
7 8 1.0 61.0 78.0 26.760000
... ... ... ... ... ...
2351 2352 1.0 47.0 66.0 14.490000
2352 2353 2.0 53.0 66.0 19.775000
2353 2354 5.0 24.0 66.0 44.928000
2354 2355 1.0 44.0 66.0 24.600000
2355 2356 6.0 62.0 66.0 31.871667

1126 rows × 5 columns

Si está calculando el valor monetario a partir de sus propios datos, tenga en cuenta que es la media del valor de un cliente dado, no la suma. monetary_value se puede utilizar para representar ganancias, ingresos o cualquier valor siempre que se calcule de manera consistente para cada cliente.

The Gamma-Gamma model relies on the important assumption that there is no relationship between the monetary value and the purchase frequency. In practice we need to check whether the Pearson correlation is less than 0.3:

nonzero_data[["monetary_value", "frequency"]].corr()
monetary_value frequency
monetary_value 1.000000 0.052819
frequency 0.052819 1.000000

Las frecuencias de transacción y los valores monetarios no están correlacionados; ahora podemos ajustar nuestro modelo Gamma-Gamma para predecir el gasto promedio y los valores de vida esperados de nuestros clientes.

El modelo Gamma-Gamma recibe un parámetro “data”, un DataFrame de pandas con 3 columnas que representan el ID del cliente, el gasto promedio de compras repetidas y el número de compras repetidas para ese cliente. Al igual que con el modelo BG/NBD, estos parámetros se les asignan priors HalfFlat, que pueden ser demasiado difusos para conjuntos de datos pequeños. Para este ejemplo, utilizaremos los priors predeterminados, pero se pueden especificar otros priors de la misma manera que en el ejemplo de BG/NBD mencionado anteriormente.

gg = clv.GammaGammaModel()
gg.build_model(data=nonzero_data)
gg
Gamma-Gamma Model (Mean Transactions)
         p ~ HalfFlat()
         q ~ HalfFlat()
         v ~ HalfFlat()
likelihood ~ Potential(f(q, p, v))
gg.fit();
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [p, q, v]

Sampling 4 chains for 1_000 tune and 1_000 draw iterations (4_000 + 4_000 draws total) took 4 seconds.
gg.fit_summary()
mean sd hdi_3% hdi_97% mcse_mean mcse_sd ess_bulk ess_tail r_hat
p 4.799 0.750 3.456 6.195 0.025 0.022 929.0 1099.0 1.0
q 3.933 0.280 3.374 4.409 0.010 0.008 893.0 928.0 1.0
v 23.707 5.264 14.324 33.157 0.189 0.186 818.0 970.0 1.0
az.plot_posterior(gg.fit_result);

Predicción del valor de gasto de los clientes#

Habiendo ajustado nuestro modelo, ahora podemos utilizarlo para predecir el valor promedio de vida condicional esperado de nuestros clientes, incluidos aquellos con cero compras repetidas.

expected_spend = gg.expected_customer_spend(data=rfm_data)
az.summary(expected_spend.isel(customer_id=range(10)), kind="stats")
mean sd hdi_3% hdi_97%
x[1] 26.115 0.438 25.339 26.975
x[2] 21.643 1.325 19.206 24.174
x[3] 37.612 0.891 35.961 39.305
x[4] 37.612 0.891 35.961 39.305
x[5] 37.612 0.891 35.961 39.305
x[6] 74.829 0.368 74.169 75.483
x[7] 21.643 1.325 19.206 24.174
x[8] 30.901 0.605 29.739 32.028
x[9] 36.430 0.153 36.148 36.727
x[10] 37.612 0.891 35.961 39.305
labeller = MapLabeller(var_name_map={"x": "customer"})
az.plot_forest(
    expected_spend.isel(customer_id=(range(10))), combined=True, labeller=labeller
)
plt.xlabel("Expected mean spend");

También podemos analizar el gasto medio esperado promedio entre todos los clientes.

az.summary(expected_spend.mean("customer_id"), kind="stats")
mean sd hdi_3% hdi_97%
x 37.998 0.562 36.966 39.073
az.plot_posterior(expected_spend.mean("customer_id"))
plt.axvline(expected_spend.mean(), color="k", ls="--")
plt.title("Expected mean spend of all customers");

Predicción del valor de gasto de un nuevo cliente#

az.plot_posterior(gg.expected_new_customer_spend())
plt.title("Expected mean spend of a new customer");

Estimando el CLV#

Finalmente, podemos combinar el GG con el modelo BG/NBD para obtener una estimación del valor de vida del cliente. Esto se basa en el modelo de flujo de caja descontado, ajustando por el costo de capital.

Si se encuentran problemas computacionales, utilice el método thin_fit_result antes de estimar el CLV.

bgm.thin_fit_result(keep_every=2)
BG/NBD
            alpha ~ HalfNormal(0, 10)
                a ~ HalfNormal(0, 10)
                b ~ HalfNormal(0, 10)
                r ~ HalfNormal(0, 10)
recency_frequency ~ BetaGeoNBD(a, b, r, alpha, <constant>)
clv_estimate = gg.expected_customer_lifetime_value(
    transaction_model=bgm,
    data=rfm_data,
    future_t=12,  # months
    discount_rate=0.01,  # monthly discount rate ~ 12.7% annually
    time_unit="W",  # original data is in weeks
)
az.summary(clv_estimate.isel(customer_id=range(10)), kind="stats")
mean sd hdi_3% hdi_97%
x[1] 29.244 1.049 27.306 31.182
x[2] 3.102 0.311 2.540 3.708
x[3] 5.615 0.224 5.205 6.047
x[4] 5.615 0.224 5.205 6.047
x[5] 5.615 0.224 5.205 6.047
x[6] 501.233 16.738 470.280 531.458
x[7] 4.080 0.352 3.414 4.735
x[8] 16.210 0.437 15.358 16.984
x[9] 46.878 1.312 44.499 49.302
x[10] 5.615 0.224 5.205 6.047
az.plot_forest(
    clv_estimate.isel(customer_id=range(10)), combined=True, labeller=labeller
)
plt.xlabel("Expected CLV");

Según nuestros modelos, el cliente[6] tiene un CLV esperado mucho más alto. También hay una gran variabilidad en esta estimación que surge únicamente de la incertidumbre en los parámetros de los modelos BG/NBD y GG.

En general, estos modelos tienden a inducir una fuerte correlación entre el CLV esperado y la incertidumbre. Este modelado de la incertidumbre puede ser muy útil al tomar decisiones de marketing.

%load_ext watermark
%watermark -n -u -v -iv -w -p pymc,pytensor
Last updated: Thu Dec 04 2025

Python implementation: CPython
Python version       : 3.12.11
IPython version      : 9.3.0

pymc    : 5.25.1
pytensor: 2.31.7

matplotlib    : 3.10.3
arviz         : 0.21.0
pymc_extras   : 0.4.0
pymc_marketing: 0.15.1
numpy         : 2.2.0
pandas        : 2.3.0

Watermark: 2.5.0