PyMC-Marketing预测客户生命周期价值
PyMC-Marketing:预测客户生命周期价值的好帮手
深入探索买到你死(BTYD)建模和实际编码技术
TL; DR:客户终身价值(CLV)模型是客户分析中的关键技术,可帮助公司确定有价值的客户。忽视CLV可能导致对短期客户的过度投资,而这些客户可能只进行一次购买。利用BG/NBD和Gamma-Gamma模型进行“买购至死”建模可以估算CLV。虽然最佳实践因数据规模和建模优先级而异,但PyMC-Marketing是一个建议用于快速实现CLV建模的Python库。
1. 什么是CLV?
CLV的定义是公司在与一个客户的关系中可以预期的总净收入。你们中的一些人可能更熟悉术语“LTV”(终身价值)。是的,CLV和LTV是可互换的。

- 第一个目标是计算和预测未来的CLV,这将帮助你了解每个客户可以预期的金额。
- 第二个目标是识别有利可图的客户。该模型将通过分析高CLV客户的特征来告诉你谁是有价值的客户。
- 第三个目标是基于分析采取营销行动,从而能够相应地优化营销预算分配。

2. 业务背景
以耐克等时尚品牌的电子商务网站为例,该网站可能使用广告和优惠券吸引新客户。现在,假设大学生和上班族是两个主要的重要客户群体。公司在首次购买时,分别为大学生和上班族投入了10美元和20美元的广告费用。而且两个客户群体的购买金额都在100美元左右。
如果你是营销负责人,你会更愿意投资哪个客户群体?你可能会自然而然地认为在大学生群体中投资更合乎逻辑,考虑到他们更低的成本和更高的投资回报率。
- 翻滚时光:人工智能解读古罗马谜题
- 机器人模仿了一个4.5亿年前灭绝的海洋生物
- 莱斯利·奥恩,Trinity Life Sciences公司的总裁兼首席执行官 – 生命科学创新、数据驱动战略、制药业中的人工智能、高管领导力、战略交易、品牌规划以及高管工作与生活的平衡

那么,如果你知道了这些信息呢?
大学生群体往往有很高的流失率,意味着他们在一次购买之后就不再购买,平均花费100美元。另一方面,上班族群体具有更高的再购买率,平均每个客户消费400美元。
在这种情况下,你很可能更愿意在上班族群体中投资,因为它承诺更高的投资回报率。这似乎是一个简单的事情,任何人都可以理解。然而,令人惊讶的是,大多数营销人员关注的是获客成本(CPA),而不考虑从长远来看谁是有利可图的客户。

通过调整“每个获客成本”(CPA),我们可以吸引更多高价值客户并提高我们的回报率。左边的图表代表不考虑CLV的方法。红线代表了CPA,即我们能够为获取新客户而支付的最高成本。为每个客户使用相同的营销预算会导致低价值客户的过度投资和高价值客户的投资不足。
现在,右侧的图表显示了在利用CLV时理想的支出分配。我们为高价值客户设置较高的CPA,为低价值客户设置较低的CPA。

这与招聘过程类似。如果你想聘请前Google员工,提供具有竞争力的薪水是至关重要的,对吗?通过这样做,我们可以在不改变总营销预算的情况下获得更多高价值客户。
3. 所需数据
我介绍的CLV模型只使用销售交易数据。正如您所见,我们有三列数据:customer_id、交易日期和交易价值。在数据量方面,CLV通常需要两到三年的交易数据。

4. 传统CLV公式
4.1 CLV建模方法
让我们首先了解计算CLV的两种广义类型:历史方法和预测方法。在预测方法下,有两个模型:概率模型和机器学习模型。

4.2 传统CLV公式
首先,让我们考虑传统的CLV公式。在这里,CLV可以分解为三个组成部分:平均订单价值、购买频率和客户生命周期。

举个例子,假设一个时尚公司的情况是:
- 每个客户订单平均支出100美元
- 他们每年购物4次
- 他们忠诚3年
在这种情况下,CLV的计算结果是100乘以4乘以3,等于每个客户1200美元。这个公式非常简单,看起来很直观,对吗?然而,它也有一些局限性。
4.3 传统CLV公式的限制

限制1:不是所有客户都是一样的
这个传统公式假设所有客户都是同质的,通过分配一个平均数进行计算。当一些客户进行异常大额购买时,平均数无法代表所有客户的特点。
限制2:初次购买时间的差异
假设我们将最近12个月作为数据收集期。

这个人在大约一年前进行了第一次购买。在这种情况下,我们可以准确计算他的每年购买频率。是8次。
那么两个顾客呢?一个人6个月前开始购买,另一个人3个月前开始购买。每个人购买的速度都一样。然而,当我们看过去一年的总购买次数时,它们是不同的。关键在于我们需要考虑顾客的任期,也就是他们第一次购买以来的时间。
限制 #3:已流失或保持活跃?
确定顾客何时被认为“流失”是棘手的。对于像Netflix这样的订阅服务,一旦他们取消订阅,我们就可以认为顾客已经流失了。然而,在零售或电子商务的情况下,顾客是“活跃”还是“已流失”是模糊的。
顾客的“存活概率”取决于他们过去的购买模式。例如,如果一个通常每个月购买一次的人在接下来的三个月内没有购买任何东西,他们可能会转向另一个品牌。然而,如果一个通常每六个月只购物一次的人在接下来的三个月内没有购买任何东西,就没有必要担心。

5. 买到死 (BTYD) 模型
为了解决这些挑战,我们经常采用 “买到死 (BTYD) ” 建模的方法。这种方法由两个子模型组成:
- BG-NBD 模型:用于预测顾客的活跃程度和交易频率。
2. Gamma-Gamma 模型:用于估计平均订单价值。
通过结合这两个子模型的结果,我们可以有效地预测顾客的终身价值 (CLV)。

5.1 BG/NBD 模型
我们认为顾客的状态包括两个过程:“购买过程”,即顾客积极购买的阶段,和“退订过程”,即顾客停止购买的阶段。
在积极购买阶段,该模型使用“泊松过程”预测顾客的购买频率。
每次购买后,顾客都有可能退订。BG/NBD 模型为这种可能性分配了一个概率‘p’。
请参考下面的图片进行说明。数据显示该顾客进行了五次购买。然而,根据模型的假设,如果该顾客保持活跃,总共应该有八次购买。但是,因为存活的概率在某一时刻下降,我们只看到了五次实际购买。

购买频率在顾客被认为“活跃”时遵循泊松过程。泊松分布通常表示随机发生的事件的计数。这里,“λ”表示每个顾客的购买频率。然而,顾客的购买频率可能有所波动。泊松分布考虑了购买频率的这种变化性。

下面的图表说明了“p”随时间的变化。随着距离上次购买的时间增加(T=31),顾客被认为“存活”的概率减少。当再次发生复购(大约在 T=36附近)时,你会注意到“p”再次增加。

这是图形模型。正如之前提到的,它包括λ和p。这里,λ和p因人而异。为了考虑到这种多样性,我们假设λ的异质性遵循伽马分布,而p的异质性遵循“贝塔分布”。换句话说,这个模型采用了一个受贝叶斯定理启发的分层方法,也被称为贝叶斯分层建模。

5.2 伽马-伽马模型
我们假设伽马分布对平均订单价值进行建模。伽马分布由两个参数来确定:形状参数和尺度参数。正如这个图表所示,通过改变这两个参数,伽马分布的形态可以发生很大的变化。

这个图示说明了使用的图形模型。该模型在贝叶斯分层方法中使用了两个伽马分布。第一个伽马分布表示每个客户的“平均订单价值”。由于这个值在客户之间存在差异,第二个伽马分布捕捉了整个客户群体的平均订单价值的变化。先验分布的参数p、q和γ(伽马)由使用半扁先验方法确定。

6. 样本代码
有用的CLV库
在这里,我给大家介绍两个用于CLV建模的优秀开源库。第一个是PyMC-Marketing,第二个是CLVTools。这两个库都包含了“买到死”的建模。最重要的区别在于,PyMC-Marketing是基于Python的库,而CLVTools是基于R的。PyMC-Marketing是建立在流行的贝叶斯库PyMC上的。之前有一个很有名的库叫做‘Lifetimes’。然而,‘Lifetimes’现在处于维护模式,所以它过渡为PyMC-Marketing。
完整代码
完整的代码可以在我的Github上找到。我的示例代码基于PyMC-Marketing官方快速入门。
GitHub – takechanman1228/Effective-CLV-Modeling
为takechanman1228/Effective-CLV-Modeling的发展做贡献,请在GitHub上创建一个帐户。
github.com
代码步骤
首先,您需要导入pymc_marketing和其他库。
import arviz as az
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import pymc as pm
from arviz.labels import MapLabeller
from IPython.display import Image
from pymc_marketing import clv
您需要从“UCI机器学习库”下载“在线零售数据集”。该数据集包含来自一家总部位于英国的在线零售商的交易数据,使用的许可证是创作共用 署名4.0 国际许可证(CC BY 4.0)。
import requests
import zipfile
import os
# 下载zip文件
url = "https://archive.ics.uci.edu/static/public/352/online+retail.zip"
response = requests.get(url)
filename = "online_retail.zip"
with open(filename, 'wb') as file:
file.write(response.content)
# 解压缩文件
with zipfile.ZipFile(filename, 'r') as zip_ref:
zip_ref.extractall("online_retail_data")
# 找到Excel文件名
for file in os.listdir("online_retail_data"):
if file.endswith(".xlsx"):
excel_file = os.path.join("online_retail_data", file)
break
# 从Excel转换为CSV
data_raw = pd.read_excel(excel_file)
data_raw.head()

数据清洗
需要进行快速的数据清洗。例如,我们需要处理退货订单,过滤掉没有客户 ID 的记录,并通过将数量和单价相乘创建一个“总销售额”列。
#处理退货订单
#提取以"C"开头的InvoiceNo行
cancelled_orders = data_raw[data_raw['InvoiceNo'].astype(str).str.startswith("C")]
#创建一个带有我们要匹配的列的临时DataFrame,并对‘Quantity’列进行否定处理
cancelled_orders['Quantity'] = -cancelled_orders['Quantity']
#将原始DataFrame与临时DataFrame在我们要匹配的列上合并
merged_data = pd.merge(data_raw, cancelled_orders[['CustomerID', 'StockCode', 'Quantity', 'UnitPrice']],
on=['CustomerID', 'StockCode', 'Quantity', 'UnitPrice'],
how='left', indicator=True)
#过滤出合并找到匹配的行,并过滤出原始退回订单
data_raw = merged_data[(merged_data['_merge'] == 'left_only') & (~merged_data['InvoiceNo'].astype(str).str.startswith("C"))]
#删除指示器列
data_raw = data_raw.drop(columns=['_merge'])
#选择相关特征并计算总销售额
features = ['CustomerID', 'InvoiceNo', 'InvoiceDate', 'Quantity', 'UnitPrice', 'Country']
data = data_raw[features]
data['TotalSales'] = data['Quantity'].multiply(data['UnitPrice'])
#删除缺少客户 ID 的交易,因为它们不对个体客户行为做出贡献
data = data[data['CustomerID'].notna()]
data['CustomerID'] = data['CustomerID'].astype(int).astype(str)
data.head()

然后,我们需要使用`clv_summary`函数创建一个汇总表。该函数以RFM-T格式返回数据框。RFM-T表示每个客户的最近购买时间(Recency)、购买频率(Frequency)、总销售额(Monetary)和存续时间(Tenure)。这些指标在购物者分析中很常见。
data_summary_rfm = clv.utils.clv_summary(data, 'CustomerID', 'InvoiceDate', 'TotalSales')
data_summary_rfm = data_summary_rfm.rename(columns={'CustomerID': 'customer_id'})
data_summary_rfm.index = data_summary_rfm['customer_id']
data_summary_rfm.head()

BG/NBD模型
BG/NBD模型在该库中作为BetaGeoModel函数提供。当您执行bgm.fit()时,模型开始训练。
当您执行bgm.fit_summary()时,系统会提供学习过程的统计摘要。例如,该表显示了参数的均值、标准差、高密度区间(HDI)等等。我们还可以检查R-hat值,该值有助于评估马尔可夫链蒙特卡洛(MCMC)模拟是否收敛。如果R-hat小于等于1.1,则认为是可接受的。
bgm = clv.BetaGeoModel(
data = data_summary_rfm,)
bgm.build_model()
bgm.fit()
bgm.fit_summary()

下面的矩阵称为存续概率矩阵。通过它,我们可以推断出可能返回的用户和不太可能返回的用户。X轴代表客户的历史购买频率,y轴代表客户最近一次购买的时间。颜色显示存续的可能性。我们的新客户位于左下角:低频率和高最近购买时间。这些客户有很高的存续概率。我们的忠实客户位于右下角:高频率和高最近购买时间的客户。如果很长时间没有购买,忠诚客户变成了风险客户,其存续概率较低。
clv.plot_probability_alive_matrix(bgm);

下一步,我们可以预测每个客户的未来交易。您可以使用expected_num_purchases函数。在拟合模型后,我们可以询问下一个时期的预期购买数量。
num_purchases = bgm.expected_num_purchases( customer_id=data_summary_rfm["customer_id"], t=365, frequency=data_summary_rfm["frequency"], recency=data_summary_rfm["recency"], T=data_summary_rfm["T"])sdata = data_summary_rfm.copy()sdata["expected_purchases"] = num_purchases.mean(("chain", "draw")).valuessdata.sort_values(by="expected_purchases").tail(4)

Gamma-Gamma模型
接下来,我们将转向Gamma-Gamma模型来预测平均订单价值。我们可以使用”Expected_customer_spend”函数预测预期的“平均订单价值”。
nonzero_data = data_summary_rfm.query("frequency>0")dataset = pd.DataFrame({ 'customer_id': nonzero_data.customer_id, 'mean_transaction_value': nonzero_data["monetary_value"], 'frequency': nonzero_data["frequency"],})gg = clv.GammaGammaModel( data = dataset)gg.build_model()gg.fit();expected_spend = gg.expected_customer_spend( customer_id=data_summary_rfm["customer_id"], mean_transaction_value=data_summary_rfm["monetary_value"], frequency=data_summary_rfm["frequency"],)
下图显示了5位客户的预期平均订单价值。这两位客户的平均订单价值超过500美元,而这三位客户的平均订单价值约为350美元。
labeller = MapLabeller(var_name_map={"x": "customer"})az.plot_forest(expected_spend.isel(customer_id=(range(5))), combined=True, labeller=labeller)plt.xlabel("Expected average order value");

结果
最后,我们可以将两个子模型结合起来估计每个客户的CLV。这里我想提及的一个参数是:折现率(Discount_rate)。该函数使用DCF方法,即“贴现现金流”。当折现率为1%时,一个月后的100美元价值在今天只值99美元。
clv_estimate = gg.expected_customer_lifetime_value( transaction_model=bgm, customer_id=data_summary_rfm['customer_id'], mean_transaction_value=data_summary_rfm["monetary_value"], frequency=data_summary_rfm["frequency"], recency=data_summary_rfm["recency"], T=data_summary_rfm["T"], time=120, # 120个月即10年 discount_rate=0.01, freq="D",)clv_df = az.summary(clv_estimate, kind="stats").reset_index()clv_df['customer_id'] = clv_df['index'].str.extract('(\d+)')[0]clv_df = clv_df[['customer_id', 'mean', 'hdi_3%', 'hdi_97%']]clv_df.rename(columns={'mean' : 'clv_estimate', 'hdi_3%': 'clv_estimate_hdi_3%', 'hdi_97%': 'clv_estimate_hdi_97%'}, inplace=True)# monetary_values = data_summary_rfm.loc[clv_df['customer_id'], 'monetary_value']monetary_values = data_summary_rfm.set_index('customer_id').loc[clv_df['customer_id'], 'monetary_value']clv_df['monetary_value'] = monetary_values.valuesclv_df.to_csv('clv_estimates_output.csv', index=False)
现在,我将展示给你如何改进我们的营销行动。下面的图显示了按国家估计的CLV。
# 计算每笔交易的总销售额
data['TotalSales'] = data['Quantity'] * data['UnitPrice']
customer_sales = data.groupby('CustomerID').agg({
'TotalSales': sum,
'Country': 'first' # 假设一个客户只与一个国家相关联
})
customer_countries = customer_sales.reset_index()[['CustomerID', 'Country']]
clv_with_country = pd.merge(clv_df, customer_countries, left_on='customer_id', right_on='CustomerID', how='left')
average_clv_by_country = clv_with_country.groupby('Country')['clv_estimate'].mean()
customer_count_by_country = data.groupby('Country')['CustomerID'].nunique()
country_clv_summary = pd.DataFrame({
'AverageCLV': average_clv_by_country,
'CustomerCount': customer_count_by_country,
})
# 计算每个国家CLV估计的平均下限和上限
average_clv_lower_by_country = clv_with_country.groupby('Country')['clv_estimate_hdi_3%'].mean()
average_clv_upper_by_country = clv_with_country.groupby('Country')['clv_estimate_hdi_97%'].mean()
# 将这些平均值添加到country_clv_summary数据框中
country_clv_summary['AverageCLVLower'] = average_clv_lower_by_country
country_clv_summary['AverageCLVUpper'] = average_clv_upper_by_country
# 过滤具有超过20个客户的国家
filtered_countries = country_clv_summary[country_clv_summary['CustomerCount'] >= 20]
# 按CustomerCount降序排序
sorted_countries = filtered_countries.sort_values(by='AverageCLV', ascending=False)
# 为误差棒准备数据
lower_error = sorted_countries['AverageCLV'] - sorted_countries['AverageCLVLower']
upper_error = sorted_countries['AverageCLVUpper'] - sorted_countries['AverageCLV']
asymmetric_error = [lower_error, upper_error]
# 创建一个指定大小的新图形
plt.figure(figsize=(12,8))
# 创建一个代表平均CLV的图,其中误差棒表示置信区间
# 我们将索引转换为常规列表,以避免matplotlib处理pandas索引对象的问题
plt.errorbar(x=sorted_countries['AverageCLV'], y=sorted_countries.index.tolist(),
xerr=asymmetric_error, fmt='o', color='black', ecolor='lightgray', capsize=5, markeredgewidth=2)
# 设置标签和标题
plt.xlabel('Average CLV') # x轴标签
plt.ylabel('Country') # y轴标签
plt.title('Average Customer Lifetime Value (CLV) by Country with Confidence Intervals') # 图表标题
# 调整y轴以从顶部向下显示国家
plt.gca().invert_yaxis()
# 显示网格线
plt.grid(True, linestyle='--', alpha=0.7)
# 显示图形
plt.show()

法国的顾客CLV往往较高。另一方面,比利时的顾客CLV较低。根据这个结果,我建议增加在法国获取顾客的营销预算,减少在比利时获取顾客的营销预算。当我们用基于美国的数据进行建模时,我们将使用州而不是国家。
7. 如何提高模型的准确性?
你可能会想:
- 我们能利用其他类型的数据吗,比如访问日志?
- 是否可以将更多特征,如人口统计信息或营销活动,纳入模型中?
基本上,BTYD模型只需要交易数据。如果您想使用其他数据或其他特征,机器学习的方法可能是一个选择。之后,您可以评估贝叶斯模型和机器学习模型的性能,选择提供更好准确性和可解释性的那个。
下面的流程图展示了更好的CLV建模指南。

首先,考虑您的数据大小。如果数据不足或只有交易数据,使用PyMC Marketing进行BTYD建模可能是最佳选择。即使您的数据足够大,我认为一个不错的方法是从BTYD模型开始,如果效果不佳,则尝试其他方法。具体而言,如果准确性优先于可解释性,神经网络、XGboost、LightGBM或集成技术可能会有帮助。如果可解释性对您依然重要,请考虑随机森林或可解释的AI方法。
总之,我建议以PyMC Marketing作为任何情况下的第一步!
8. 结论
以下是一些关键要点。
- 客户终身价值(CLV)是一家公司可以从单个客户在整个关系期间预期获得的净利润总额。
- 我们可以使用BG/NBD模型和Gamma-Gamma模型构建概率模型(BTYD)。
- 如果您熟悉Python,可以从PyMC-Marketing开始。
感谢阅读!如果您有任何问题/建议,请随时在Linkedin上与我联系!同时,如果您关注我的Towards Data Science,我会很高兴。
9. 参考
- BG/NBD模型: https://www.brucehardie.com/papers/bgnbd_2004-04-20.pdf
- Gamma-Gamma模型: https://www.brucehardie.com/notes/025/gamma_gamma.pdf
- PyMC Marketing: https://github.com/pymc-labs/pymc-marketing
- 在线零售数据集(许可证CC BY 4.0):https://archive.ics.uci.edu/dataset/352/online+retail





