ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

CLV模型实战:从BG/NBD到Gamma-Gamma,让CMO不再耸肩

2026/8/30 11:01:35 拓冰建站 浏览量
CLV模型实战:从BG/NBD到Gamma-Gamma,让CMO不再耸肩 先讲一个真实的场景你花了整整一周清洗数据、做特征工程、训练 BG/NBD 模型、调 Gamma-Gamma 的参数最后把 CLVCustomer Lifetime Value客户终身价值预测结果做成一张精美的报表周一例会上兴致勃勃地发给 CMO。CMO 看了一眼礼貌地说了一句“挺好的”然后就切到了下一个议题。你站在原地心里只剩下一个问题我这一周到底做了什么这不是段子而是很多数据岗位同学真实经历过的事。标题里那句 “the CMO shrugged”CMO 耸了耸肩之所以能引发共鸣是因为它戳中了数据建模工作里最隐蔽的痛点技术正确不等于业务有效。本文不打算只做情绪共鸣而是把这一整条路重新走一遍从 CLV 模型的概念、数据准备、模型训练、评估到“如何让 CMO 不再耸耸肩”的输出设计完整拆解成一套可以照着落地的实战方案。如果你正在做用户增长、会员运营、精细化营销或者你刚进入数据分析/数据科学岗位这篇文章能帮你补上一块关键拼图怎么把 CLV 从学术概念变成业务方真正愿意用的决策工具。1. CLV 模型到底是什么CMO 为什么无感1.1 客户终身价值的概念CLV 的全称是 Customer Lifetime Value翻译过来是客户终身价值。它衡量的是一个客户从第一次接触你的产品开始到彻底流失为止整个生命周期内为你贡献的毛利总和。我们经常听到的指标是“客单价”“复购率”“月消费金额”这些指标其实都是“切片式”的它们只能回答“客户现在值多少钱”。而 CLV 回答的是一个更前置的问题这个客户未来还能值多少钱从数学上看CLV 可以拆成两个核心部分客户在未来的存续期内预计会发生多少次购买行为每次购买行为预计能带来多少毛利。所以 CLV 的计算公式可以简洁地写成CLV 预测购买次数 × 平均每个订单贡献的毛利这个拆解非常重要因为后面我们用 BG/NBD 模型预测购买次数用 Gamma-Gamma 模型预测单次毛利两个模型拼在一起就得到了完整的 CLV 预测。1.2 CLV 适用的业务场景CLV 不是所有公司都需要立刻上马它最典型的应用场景包括业务场景具体问题CLV 能做什么新客获取花多少钱买一个用户不亏本计算出新客的预期 LTV倒推 CAC 上限老客运营哪些客户值得投入更多权益筛选高潜力客户做重点运营流失预警哪些客户即将流失识别沉默风险客户提前触达会员分层如何给客户分层基于 CLV 分群定制不同权益预算分配营销预算花在哪个渠道按渠道 CLV 对比把钱投向高价值渠道1.3 为什么开发完模型CMO 却耸了耸肩这是整篇文章最值得停下来想的部分。CMO 无感的根本原因通常不是模型不准而是模型没有回答他真正关心的问题。CMO 每天被问的问题是下个季度营收目标怎么完成营销预算怎么分要不要做降价促销如果模型只输出一个“客户 A 的 CLV 是 356 元”的数字CMO 是无法行动的。他需要知道的是这周应该给哪 5000 个客户发优惠券哪个渠道来的用户 90 天内最可能再次购买高价值客户有什么共同特征能不能照着这个画像去投放所以问题不在 CLV 模型本身而在于我们交付模型的时候是否把“预测值”翻译成了“业务动作”。这篇文章后面会用一整节来拆解这个翻译过程。2. 环境准备与数据集说明2.1 运行环境本文示例使用 Python 实现推荐使用 Python 3.9 及以上版本。核心依赖库如下pip install pandas numpy matplotlib scipy pip install lifetimes这里重点说一下lifetimes库。它是 Python 生态中最常用的 CLV 建模库封装了 BG/NBD、Beta-Geometric 等经典模型底层依赖 SciPy 做参数估计接口设计得很简洁很适合快速构建 CLV 预测模型。如果你在安装lifetimes时遇到依赖冲突可以创建独立的虚拟环境python -m venv clv_env source clv_env/bin/activate # Windows 下使用 clv_env\Scripts\activate pip install pandas numpy matplotlib scipy lifetimes版本需要根据你的实际环境调整本文重点演示建模思路和全链路流程。2.2 数据集说明本文使用经典的 CDNOW 数据集。这个数据集记录了一个 CD 销售网站的客户交易明细是 CLV 建模领域公认的公开示例数据包含 1997 年 1 月到 1998 年 6 月共 18 个月的交易记录。lifetimes库内置了该数据集加载方式如下from lifetimes.datasets import load_cdnow_summary # 该函数返回每日交易数据 data load_cdnow_summary(index_col[0]) print(data.head())如果无法自动下载也可以从公开渠道手动下载后读取本地 CSV 文件。2.3 项目结构我们按照下面的结构组织代码和输出clv_project/ ├── data/ │ └── cdnow.csv # 原始交易数据 ├── notebook/ │ └── clv_model.ipynb # 建模分析笔记本 ├── src/ │ ├── clv_model.py # 核心模型训练脚本 │ └── utils.py # 数据加载和预处理工具 └── output/ ├── clv_predictions.csv # 客户级 CLV 预测结果 └── segments.csv # 客户分层输出3. 数据准备与特征指标计算3.1 理解 CLV 建模对数据格式的要求BG/NBD 模型对输入数据有标准化的要求。它不需要你把每一笔订单的明细都喂进去而是要求每个客户汇总成四个指标frequency重复购买次数。注意这里不是总购买次数而是重复购买次数所以需要减去 1。recency最近一次购买与第一次购买之间的时间间隔。T观察期长度即第一次购买到观察期截止日的时间间隔。monetary_value每次购买的平均金额通常用毛利或订单金额表示。如果你对这四个指标的意义感到模糊对照下面的例子理解客户 A 在 1 月 1 日第一次下单在 3 月 1 日和 5 月 10 日又分别下单一次。观察期截止到 6 月 30 日。那么 frequency 2recency 5 月 10 日 - 1 月 1 日 130 天T 6 月 30 日 - 1 月 1 日 180 天monetary_value 三次订单的平均金额。3.2 原始交易数据的清洗首先加载原始交易明细并进行清洗。CDNOW 数据集格式比较简单每行包含客户编号、购买日期和消费金额。import pandas as pd # 读取原始交易数据 df pd.read_csv( data/cdnow.csv, headerNone, names[customer_id, order_date, price, quantity], sep,, ) # 将日期列转换为 datetime 类型 df[order_date] pd.to_datetime(df[order_date], format%Y%m%d) # 查看数据基本结构 print(df.head()) print(数据形状, df.shape) print(唯一客户数, df[customer_id].nunique())清洗逻辑通常包括以下几个步骤移除customer_id为空的行移除price小于等于 0 的异常记录检查是否存在未来日期的异常数据如有重复订单记录根据需要去重。# 去除异常值 df df[df[customer_id].notna()] df df[df[price] 0] df df[df[order_date] pd.Timestamp(1998-06-30)] print(清洗后数据形状, df.shape)3.3 使用 lifetimes 生成客户汇总表lifetimes提供了summary_data_from_transaction_data函数可以一步到位生成建模所需的四个指标。from lifetimes.utils import summary_data_from_transaction_data # 生成客户维度汇总数据 summary summary_data_from_transaction_data( transactionsdf, customer_id_colcustomer_id, datetime_colorder_date, monetary_value_colprice, observation_period_end1998-06-30, ) print(summary.head())生成结果大致如下customer_idfrequencyrecencyTmonetary_value1213018035.84200850.00313020025.60稍微解释一下输出frequency表示重复购买次数recency是最近一次购买相对第一次购买的时间间隔T是第一次购买到观察期结束的时间长度monetary_value是平均交易金额注意对于 frequency 为 0 的客户这个值可能为 0。到这里数据准备阶段就完成了。整个过程中最容易被忽视的是observation_period_end参数它决定了你的观察期边界。如果你随意填写模型会认为客户还有未来交易记录造成训练数据泄漏。4. 构建 BG/NBD Gamma-Gamma 模型4.1 为什么选 BG/NBD 和 Gamma-GammaCLV 建模方法有很多从简单的平均值法、RFM 打分法到回归模型、生存分析模型再到深度学习模型。本文选择 BG/NBD Gamma-Gamma 组合是因为它是业界应用最成熟、可解释性强、计算成本低的经典方案。BG/NBDBeta Geometric / Negative Binomial Distribution用来预测客户在未来一段时间内的购买次数。它假设客户在活跃状态时购买行为服从泊松过程同时每个客户的购买率和流失率都服从各自的分布。Gamma-Gamma 用来预测客户的平均交易金额。它假设每个客户的平均订单金额服从 Gamma 分布模型会利用客户已有的交易金额和交易次数来估计客户未来的订单金额水平。之所以要拆成两个模型是因为购买频率和交易金额通常不是独立同分布的关系。实践中常见的情况是高频率购买的用户单笔金额反而偏低低频用户一旦购买单笔金额可能较高。分开建模能更好地捕捉这种差异。4.2 训练 BG/NBD 模型预测购买次数先剔除 frequency 为 0 的客户因为 BG/NBD 模型在拟合时需要至少一次重复购买记录才能识别客户的行为模式。from lifetimes import BetaGeoFitter # 过滤无重复购买记录的客户 summary_with_repeat summary[summary[frequency] 0].copy() # 训练 BG/NBD 模型 bgf BetaGeoFitter(penalizer_coef0.0) bgf.fit( frequencysummary_with_repeat[frequency], recencysummary_with_repeat[recency], Tsummary_with_repeat[T], ) print(BG/NBD 模型拟合完成) print(bgf.params_)参数说明penalizer_coef正则化惩罚系数用于防止过拟合。对于数据量较大的场景可以设为 0如果样本量较小或出现收敛警告可以尝试设置为 0.01 到 0.1 之间的值。frequency、recency、T三个参数必须传入数值型数组。模型训练完成后我们就可以预测客户在未来一段时间内的购买次数。比如预测未来 90 天内的购买次数from lifetimes.utils import calculate_alive_path # 未来 90 天 t 90 summary_with_repeat[predicted_purchases_90d] bgf.conditional_expected_number_of_purchases_up_to_time( t, summary_with_repeat[frequency], summary_with_repeat[recency], summary_with_repeat[T], ) print(summary_with_repeat[[frequency, recency, T, predicted_purchases_90d]].head())这里conditional_expected_number_of_purchases_up_to_time是 BG/NBD 模型的核心预测方法。它返回的是每个客户在给定时间窗口内的期望购买次数这个值已经考虑到了客户可能已经流失的概率。4.3 训练 Gamma-Gamma 模型预测平均交易金额接下来训练 Gamma-Gamma 模型。这里需要使用frequency大于 0 且有monetary_value的客户。from lifetimes import GammaGammaFitter # 条件frequency 0 且 monetary_value 0 summary_gg summary_with_repeat[summary_with_repeat[monetary_value] 0].copy() # 训练 Gamma-Gamma 模型 ggf GammaGammaFitter(penalizer_coef0.0) ggf.fit( frequencysummary_gg[frequency], monetary_valuesummary_gg[monetary_value], ) print(Gamma-Gamma 模型拟合完成) print(ggf.params_)Gamma-Gamma 模型的输入只需要frequency和monetary_value。它的输出是客户未来平均交易金额的期望值。summary_gg[predicted_avg_order_value] ggf.conditional_expected_average_profit( summary_gg[frequency], summary_gg[monetary_value], ) print(summary_gg[[frequency, monetary_value, predicted_avg_order_value]].head())4.4 组合计算客户 CLV把两个模型的预测结果拼接在一起计算完整 CLV。# 合并购买次数预测与金额预测 clv_data summary_gg.join( summary_with_repeat[[predicted_purchases_90d]], howleft, ) # 计算未来 90 天 CLV clv_data[clv_90d] ( clv_data[predicted_purchases_90d] * clv_data[predicted_avg_order_value] )这里有一个业务上的选择如果用 90 天作为生命周期窗口得到的是“90 天客户价值”如果用 360 天得到的是“年度客户价值”。实际业务中建议先按 90 天和 360 天各算一版观察稳定性。输出客户级 CLV 文件clv_output clv_data.reset_index() clv_output.to_csv(output/clv_predictions.csv, indexFalse) print(clv_output.head())4.5 预测结果解读前几行预测结果大致长这样customer_idfrequencymonetary_valuepredicted_purchases_90dpredicted_avg_order_valueclv_90d1235.840.8236.0229.533125.600.4326.1011.225459.911.5660.1593.83可以看出predicted_purchases_90d不一定都是整数这是正常的。它表示的是“期望值”如果样本量足够大所有客户的期望购买次数加起来可以理解为总体预计订单量。5. 模型评估与调优思路5.1 使用校准周期验证模型准确性建模之后必须回答一个问题模型预测得准不准最常用的方法是把数据按时间切成两部分训练期使用前 18 个月的数据训练模型校准期用后若干个周期的真实数据对比模型预测值。实际演练中可以缩短时间窗口。比如用前 12 个月训练用后 6 个月验证。lifetimes也提供了类似的思想我们可以手动实现。# 示例用前 12 个月训练预测后 6 个月购买次数 observation_end pd.Timestamp(1997-12-31) summary_train summary_data_from_transaction_data( transactionsdf[df[order_date] observation_end], customer_id_colcustomer_id, datetime_colorder_date, monetary_value_colprice, observation_period_endobservation_end, ) # 真实后 6 个月购买次数 actual_end pd.Timestamp(1998-06-30) summary_actual df[(df[order_date] observation_end) (df[order_date] actual_end)] actual_counts summary_actual.groupby(customer_id).size() # 合并对比 train_data summary_train[summary_train[frequency] 0].copy() train_data[actual_future_purchases] train_data.index.map(actual_counts).fillna(0) # 重新训练 bgf_cal BetaGeoFitter() bgf_cal.fit(train_data[frequency], train_data[recency], train_data[T]) train_data[predicted_future_purchases] bgf_cal.conditional_expected_number_of_purchases_up_to_time( 180, train_data[frequency], train_data[recency], train_data[T], ) # 计算平均误差 mae (train_data[predicted_future_purchases] - train_data[actual_future_purchases]).abs().mean() print(fMAE: {mae:.4f})这里通过平均绝对误差MAE来衡量模型的预测偏差。如果 MAE 过大需要检查数据质量或者调整模型的正则化参数。5.2 高频客户与低频客户的误差差异实际评估时建议把客户按 frequency 分组分别计算误差。高频客户的预测通常更准因为他们的历史行为更多低频客户预测误差会大一些这是模型本身的局限。理解这一点可以帮助你在业务侧设置合理预期不要对低频客户的 CLV 预测值过度解读。5.3 调优方向调整观察期如果业务有明显季节性观察期至少覆盖一个完整周期调整预测窗口90 天、180 天、360 天分别测试调整penalizer_coef出现拟合警告时从 0.01 起步尝试引入客户其他特征比如渠道来源、注册天数、活跃度等级但需要结合更复杂的模型比如 XGBoost 或回归模型。6. 从模型到业务动作让 CMO 不耸耸肩的关键6.1 把 CLV 翻译成人群分层纯 CLV 数值报表缺乏可操作性业务方更习惯“分层运营”。我们可以按预测 CLV 的高低把客户分成年货、大众层、潜力层、高价值层、VIP 层。import numpy as np # 定义分层规则 def assign_segment(clv): if clv 0: return 沉默客户 elif clv 20: return 低价值客户 elif clv 50: return 潜力客户 elif clv 100: return 高价值客户 else: return VIP客户 clv_output[segment] clv_output[clv_90d].apply(assign_segment) # 输出分层统计 segment_stats clv_output.groupby(segment).agg( customer_count(customer_id, count), total_clv(clv_90d, sum), avg_clv(clv_90d, mean), ).reset_index() print(segment_stats)到这里你就从“模型预测”走向了“运营策略”。CMO 看到的不再是冷冰冰的数字而是可以直接分配给运营团队执行的分层名单。6.2 把模型结果变成营销动作建议每一层客户应该对应不同的运营策略。比如分层特征运营动作触达方式VIP 客户CLV 100专属权益、专属客服、新品尝鲜一对一私聊或短信高价值客户CLV 50-100高门槛优惠券、会员积分加倍App Push 短信潜力客户CLV 20-50中额优惠券、爆款推荐App Push 邮件低价值客户CLV 20小额券或免邮券控制成本邮件沉默客户CLV 0召回测试设置频率阈值避免打扰少发或不发营销预算就可以按照这个分层来分配VIP 客户虽然人数少但贡献高投入产出比最高低价值客户不投入太多成本避免营销费用超过客户未来价值。6.3 用“流失概率”补充 CLV 输出CLV 预测的同时BG/NBD 模型还顺带输出了一个非常有用的指标客户仍然活跃的概率。# 计算客户活跃概率 summary_with_repeat[alive_probability] bgf.conditional_probability_alive( summary_with_repeat[frequency], summary_with_repeat[recency], summary_with_repeat[T], ) # 将活跃概率与 CLV 拼接 clv_data clv_data.join(summary_with_repeat[[alive_probability]], howleft) # 观察重要客户CLV 高但活跃概率低 潜在流失高价值客户 at_risk clv_data[(clv_data[clv_90d] 50) (clv_data[alive_probability] 0.3)] print(at_risk.sort_values(clv_90d, ascendingFalse).head())这个组合输出的价值在于它告诉 CMO 的不是“这个客户有价值”而是“这个客户有价值且即将流失需要立刻干预”。这才是运营团队真正愿意立刻行动的指令。6.4 输出一页纸业务解读建模分析的最后不要只丢 CSV 文件。建议额外生成一页业务摘要包含客户总数与分层占比高价值客户占比与未来价值总和需要优先挽回的客户数量建议的运营动作与预估成本。这样交付的是一份“决策建议”而不只是一个数据文件。这也是解决“CMO 耸耸肩”最有效的方式让他看到模型和增长目标之间的通路。7. 常见问题与排查思路实际建模过程中最容易遇到的问题集中在数据格式、模型报错和结果不合理三个方面。这里整理一张速查表问题现象常见原因解决方案summary_data_from_transaction_data报错日期列不是 datetime 类型先执行pd.to_datetime()frequency 全部为 0观察期截止时间设置太早延长观察期或检查过滤条件monetary_value 大量为 0用了利润而不是金额且部分订单成本倒挂检查业务口径统一金额定义模型训练提示收敛失败数据量太少或正则化系数为 0增加penalizer_coef如 0.01预测金额过高Gamma-Gamma 模型受离群值影响剔除极端高金额订单或用中位数代替预测结果与经验判断不符观察期窗口不合理或业务有季节性调整时间窗口纳入季节性因素低频客户预测波动大样本量不足模型不确定性高对低频客户谨慎使用结合其他特征判断7.1 数据泄漏问题观察期截止时间必须早于验证期开始时间。如果你用全量数据训练又用同一段时间验证模型的预测准确率会被高估上线后表现会明显变差。这是 CLV 建模中最隐蔽也最要命的错误之一。7.2 金额口径问题CLV 模型里应该用“毛利”还是“订单金额”学术上建议使用毛利率换算后的数值因为订单金额不反映真实利润营销活动真正要考虑的是利润空间。如果暂时无法获得成本数据可以先用订单金额但在业务汇报时必须说明这一点避免误导决策。7.3 新客户没有足够历史数据怎么办新客户没有足够的历史交易数据BG/NBD 模型无法给出可靠预测。建议的做法有用同类渠道、同类注册来源的新客均值代替使用首单金额和注册渠道做简单回归模型预测首年价值先观察新客 3 到 6 个月等积累了 2 次以上购买后再纳入 CLV 模型。8. 最佳实践与工程建议8.1 模型层面先跑通基线模型再考虑复杂度。BG/NBD Gamma-Gamma 是足够好的起点不要一上来就用深度学习模型除非数据量和业务复杂度确实需要。记录每一次训练的样本口径、时间窗口和参数方便复现和迭代。定期重训模型。客户的消费习惯会随时间变化建议每月或每季度重训一次。对预测结果设置置信区间。至少要对高 CLV 客户做人工抽样验证避免模型异常导致策略误判。8.2 数据工程层面建立统一的客户交易宽表字段至少包含customer_id、order_date、order_amount、order_profit、channel。金额字段统一为“分”或统一为“元”避免精度混乱。保留历史数据快照方便回溯模型表现。在模型上线前将数据 pipeline 的每一步输出都做计数校验防止上游数据缺失导致模型输入异常。8.3 业务落地层面不要一次性把所有客户都纳入精准运营先选一个细分人群做 A/B 测试用测试结果说服业务方。CLV 分层要跟现有的会员体系打通避免出现两套分层互相矛盾的情况。营销动作的成本要记录后续需要做 ROI 复盘验证 CLV 模型驱动的策略是否真正提升了产出。8.4 汇报层面汇报时先讲洞察再讲模型。先说“我们发现 10% 的高价值客户贡献了 60% 的未来价值”再解释模型是如何找到他们的。避免一次性输出过多复杂的统计术语。重点告诉决策者根据模型哪类客户值得投入哪类客户应该放弃。保留一个“反例集”即哪些客户模型预测不准体现出你对模型边界的理解这比只报喜更有说服力。9. 更进一步CLV 模型的进阶方向你已经跑通了一套完整的 CLV 模型链路。如果想在业务和简历上都更进一步可以考虑下面三个进阶方向。第一从客户级模型走向渠道级模型。抛开单个客户按渠道聚合 CLV比如对比付费广告、自然搜索、社交媒体带来的客户在 12 个月内的价值差异。这样可以直接指导渠道预算分配。第二从 BG/NBD 走向基于机器学习的 CLV 模型。用 XGBoost、LightGBM 直接预测客户未来 90 天购买金额。这种方法的优势是可以自然融入用户画像、行为特征、渠道特征缺点是模型可解释性下降需要配合 SHAP 等工具做特征解释。第三引入增量建模。当你做了营销触达后用增量模型Uplift Model衡量触达本身带来的 CLV 增量而不是简单对比触达和未触达客户的 CLV 差异。这是精细化运营的高级阶段适合有较强数据基础和实验条件的团队。回看标题里的那个场景花了一周构建 CLV 模型CMO 耸了耸肩。其实问题的核心从来不是模型该不该做而是我们有没有把模型的结果翻译成业务听得懂、能执行的动作。技术价值要变成业务价值中间需要一座桥。这座桥的起点是数据清洗和模型训练终点则是一份可以直接指导运营动作的决策方案。如果你正准备做 CLV 模型试着从一开始就把这个问题放在脑子里我输出的东西能让决策者说出“这个我们下周就用上”吗如果能模型这一周的时间就没有白费。