ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Gamma-Gamma模型实战:用Python预测客户期望交易金额与贝叶斯收缩

Gamma-Gamma模型实战:用Python预测客户期望交易金额与贝叶斯收缩 做用户增长和商业分析的朋友应该都有过这种经历销售报表里拉出一批用户的平均客单价然后直接用这个数去估算未来能赚多少钱结果到了年底一算发现高估了一大截。这个问题的根源不在于算术而在于你用“历史均值”这种点估计替代了“期望值”这种概率估计。这篇是CLV系列文章的第三篇聊的就是专门解决这个问题的Gamma-Gamma模型我会用Python从构造模拟数据开始把模型的原理、推导、编码到业务解释全部走一遍。这个模型测算的是用户存续期价值CLV里的“金额”维度。前两篇我们聊过怎么预估用户未来还会买几次、什么时候流失那篇用的是BG/NBD模型解决的是“频次”和“流失概率”的问题。但算CLV还有一个关键因子——用户每次交易到底会花多少钱。你可能会说这还不简单拉一下历史平均客单价不就行了。问题恰恰出在这个“简单”上一个只买过两三单、平均客单价高达上千元的客户和一个买了五十单、客单价四百元的客户如果你直接用历史均值去看未来大概率会做出错误的运营判断。Gamma-Gamma模型要做的就是用贝叶斯收缩的思想把个体的历史表现和群体的先验分布结合起来给出一个比“历史平均客单价”更可靠、更接近真实水平的期望交易金额。这篇文章适合谁看第一类是数据分析师和数据科学从业者尤其是正在搭建用户价值分层体系、复购预测模型的同学第二类是运营和产品经理你不需要手推公式但至少要理解模型给出的分数为什么比历史均值可信第三类是Python初学者我会把代码写得尽量直白你在本地跑一遍就能直观看到“什么叫做贝叶斯收缩”。1. 为什么有了历史平均客单价还需要Gamma-Gamma模型1.1 直接拿历史均值当预期问题出在哪先看一个我实际在工作中遇到的场景。某电商平台要筛选高价值客户业务方最初提的规则很简单近一年平均客单价排名前10%的用户。结果筛选出来的名单里大量是只买过一单、且那一单恰好是高价数码产品的用户。这些人真的算高价值吗大概率不算。他们可能只是偶尔需要换台电脑买完之后一年半载都不会再回来。反过来一个每个月都在平台上买猫粮、猫砂的老客户单均金额虽然只有一百多元但他一年买十二次长期价值非常稳定却被规则漏掉了。问题就出在样本量太小时历史均值是个波动极大的统计量。一个用户只买了3单均值可能被某一笔特殊大单拉得很高另一个用户买了100单均值就是大量真实交易的平均结果可信度高得多。Gamma-Gamma模型解决的就是这个问题。它不再把每个客户的历史均值当成独立、可信的估计而是把“客户群体的整体金额分布规律”作为先验信息再根据每个客户自己的交易次数和交易金额去动态调整。交易次数越多越相信个体数据交易次数越少越向群体均值靠拢。这个思路在统计学里叫“收缩估计”和棒球运动员击球率预测、电影评分预测里的做法是同一个逻辑。1.2 模型背后的核心假设Gamma-Gamma模型的正式名称叫Gamma-Gamma模型因为它用了两层Gamma分布来刻画交易金额。第一层单个客户的单笔交易金额服从Gamma分布均值为该客户自身的“真实交易水平”第二层不同客户之间的“真实交易水平”也服从一个Gamma分布用来描述人群的异质性。这个结构用大白话解释就是每个客户的“平均客单价”其实是一个隐藏的真实值我们观察到的历史平均金额不过是这个真实值加上随机波动后的噪声。有人天生就是高客单用户有人就是低客单用户这种差异在人群层面是有统计规律的Gamma分布可以把这种规律刻画出来。模型再把“群体的规律”和“个体的观测”结合得到对每个客户真实交易水平的最优估计。具体有三个假设需要注意交易金额与交易次数相互独立。也就是说一个客户买多少次和他单笔花多少钱没有相关性。这个假设在实操中经常被违反比如买得越多的用户越容易领优惠券、单笔金额被压低这种情况我会在后面的常见问题里讲怎么处理。单个客户的平均交易金额服从Gamma分布。Gamma分布有两个参数一个是形状参数p控制分布的形状另一个是尺度参数λ控制均值的量级。每个客户的λ都不一样。不同客户之间的λ服从另一个Gamma分布参数是q和γ。这里q是形状参数γ是尺度参数。这个分布描述的是“客户的真实交易水平在人群中怎么分布”。用生活类比来理解假设你在调研一个小区的居民身高。Gamma-Gamma模型的思路是每个居民的“真实身高倾向”不同但整个人群的身高分布是有规律的比如集中在某个区间。你随机量了几个人的身高但测量次数少的人在平均值附近波动极大。模型做的就是先学习整个小区的身高分布规律再结合你量到的几个数据点去估计这个人更可能的身高水平。1.3 贝叶斯视角用群体先验修正个体噪音Gamma-Gamma模型的本质是一个贝叶斯更新过程。我先把先验和似然写出来先验客户真实交易水平λ服从Gamma(q, γ)概率密度函数的形式取决于q和γ。似然给定了λ客户单笔交易金额服从Gamma(p, λ)p是形状参数。后验观察到某个客户一共交易了x次、历史平均金额为m_x之后λ的后验分布仍然是Gamma分布这正是Gamma分布作为共轭先验的妙处后验参数可以直接用解析式写出来。正因为Gamma分布是Gamma分布的共轭先验我们才能推导出客户未来期望交易金额的闭式解。最终公式长这样E[M | p, q, γ, m_x, x] (γ x * m_x) / (p * x q - 1) * p / (q - 1)先别被公式吓住拆开看就知道它很有直觉。分子里的γ是群体先验的贡献x * m_x是个体观测的贡献交易次数乘平均金额分母里的p * x和q - 1则是两边的“权重”。交易次数x越大个体数据占据的权重就越高预测值越接近历史均值交易次数越小γ和q这些群体先验参数的影响就越大预测值会被拉向人群平均水平。这就是“收缩”二字的由来。2. Python模拟方案设计先造一个能对答案的考场2.1 为什么要先做模拟数据我曾经犯过一个错误拿到真实业务数据就直接上模型结果模型输出一切正常但因为我根本不知道“真实答案”所以无法判断模型估得准不准。后来学乖了每次上手新模型都先用模拟数据做一遍验证。模拟数据的核心价值在于数据生成的时候真实参数是你自己设定的。你可以把它当成一份有标准答案的考卷让模型去做题最后对答案看模型能不能把真实的参数找回来。这一步过关了再拿真实数据去跑你才有底气说模型输出的结果是可信的。具体到Gamma-Gamma模型我会构造这样一个考场设定真实的p、q、γ参数然后按模型假设生成2000个客户的交易数据每个客户的交易次数、每笔订单金额都是随机生成的。然后我们假装不知道真实参数用Gamma-Gamma模型去估计最后把估计出的参数和真实参数做对比。2.2 模拟数据的生成规则数据生成逻辑分三步第一步设定真实参数。假设交易金额的单位是元我设p 6.0q 3.5γ 15.0。这里面的含义是什么呢p 6.0代表单笔交易金额相对稳定同一个客户的不同订单金额不会忽高忽低得太夸张q 3.5和γ 15.0组合起来意味着客户群体的平均交易水平均值是q × γ 3.5 × 15.0 52.5元但不同客户之间存在差异这个差异的大小也由这两个参数共同决定。第二步为每个客户生成“真实交易水平”λ。λ本身服从Gamma(q, γ)分布每个客户的λ可能相差很大有的客户真实水平只有20元有的能到100元。这个λ就是我们要让模型去找回的真实值。第三步为每个客户生成若干笔订单金额。每个客户的订单金额服从Gamma(p, λ)分布金额围绕λ上下波动。我给每个客户随机生成3到20笔订单模拟不同交易频次的用户。代码里有一个特别容易踩的坑Python科学计算库scipy里的Gamma分布参数化方式是shape形状参数和scale尺度参数而很多教材里的Gamma分布用的是shape和rate速率参数。两者是倒数关系搞反了生成出来的数据形态会完全不对。我在下面的代码里统一使用scale参数化写注释标注清楚防止自己过两天回来看又忘了。2.3 怎么评估模型好坏模型估得好不好不能只看参数回没回到真实值还要看最终的业务指标——预测的交易金额准不准。我会用三个维度来评估第一个维度是参数恢复。把估计出来的p、q、γ和真实参数比一比差异越小说明优化算法执行得越成功。第二个维度是预测误差。对每个客户计算历史平均金额与真实水平的绝对误差再计算模型预测值与真实水平的绝对误差然后比较两组误差的平均值。如果模型有效预测值的误差应该明显小于历史均值的误差。第三个维度是校准图。把所有客户的真实水平、历史均值、模型预测值放在同一张散点图里横轴是真实水平纵轴是估计值。散点越贴在对角线yx上说明估计越准。这张图也是给业务方汇报时最有说服力的证据。3. 完整Python实现与关键代码解读3.1 环境准备代码用Python 3.8以上版本依赖库建议在终端里执行安装pip install numpy pandas scipy matplotlib如果你正在配环境可以用vscode打开项目文件夹建一个虚拟环境再装依赖或者直接用Anaconda也行。只要能跑通numpy和scipy这个模拟脚本就没问题。需要import的库如下import numpy as np import pandas as pd from scipy.optimize import minimize from scipy.special import gammaln from scipy.stats import gamma as gamma_dist import matplotlib.pyplot as plt3.2 生成模拟数据下面的代码会生成2000个客户每个客户3到20笔订单订单金额按Gamma-Gamma模型的假设生成。# 固定随机种子保证结果可复现 np.random.seed(42) # 真实参数设定 p_true 6.0 # 单客户订单金额 Gamma 分布的形状参数 q_true 3.5 # 客户间真实水平 Gamma 分布的形状参数 gamma_true 15.0 # 客户间真实水平 Gamma 分布的尺度参数 n_customers 2000 # 每个客户的真实交易水平服从 Gamma(q_true, scalegamma_true) lambda_true gamma_dist.rvs(aq_true, scalegamma_true, sizen_customers) # 每个客户随机生成 3 到 20 笔订单 x_i np.random.randint(3, 21, sizen_customers) # 生成每笔订单金额 amounts [] for i in range(n_customers): customer_amounts gamma_dist.rvs( ap_true, scalelambda_true[i], sizex_i[i] ) amounts.append(customer_amounts) # 整理成 DataFrame df pd.DataFrame({ customer_id: range(n_customers), true_lambda: lambda_true, frequency: x_i, amounts: amounts }) df[average_amount] df[amounts].apply(np.mean) df[total_amount] df[amounts].apply(np.sum)这里我特意没有用np.random.poisson去生成交易次数而是直接用均匀分布随机取3到20因为这一篇重点在金额频次部分在BG/NBD模型里已经处理过了。你如果希望模拟数据更接近真实场景可以把x_i那行换成泊松分布不过要注意把交易次数下限设成1避免产生0交易客户。3.3 参数估计负对数似然优化模型参数估计用的是极大似然估计。核心思路是把每个客户观测到的交易次数x和平均金额m_x代入似然函数找到一组p、q、γ让所有客户数据的联合概率最大。为了方便优化通常把最大化似然变成最小化负对数似然。参考lifetimes库的实现Gamma-Gamma模型的单客户负对数似然可以写成def gamma_gamma_negative_ll(params, x, m_x): p, q, v params # 参数合法性检查 if p 0 or q 0 or v 0: return 1e10 ll ( gammaln(p * x q) - gammaln(p * x) - gammaln(q) q * np.log(v) p * x * np.log(m_x) p * x * np.log(x) - (p * x q) * np.log(x * m_x v) - gammaln(p) ) return -ll.sum()这里有个细节需要解释x * m_x其实就是这个客户的总消费金额。之所以不直接用total_amount而写成x * m_x是因为似然函数推导过程中天然带着这个乘积形式。如果你从DataFrame里取数据可以直接把df[frequency]和df[average_amount]传进来。参数初始化。我建议把p和q初始化为1.0v初始化为一个接近整体客单价均值或略大的数。比如客户平均订单金额大概是50多元v初始化为100相对合适。如果v初始化太小比如只有1优化器可能需要多花很多轮才能爬到正确的区间。x df[frequency].values.astype(float) m_x df[average_amount].values.astype(float) init_params [1.0, 1.0, 100.0] result minimize( gamma_gamma_negative_ll, init_params, args(x, m_x), methodL-BFGS-B, bounds[(1e-6, None), (1e-6, None), (1e-6, None)] ) p_hat, q_hat, v_hat result.x print(f真实参数: p{p_true}, q{q_true}, gamma{gamma_true}) print(f估计参数: p{p_hat:.4f}, q{q_hat:.4f}, gamma{v_hat:.4f})我在本地跑完的结果大致是p估计值在6.0附近q估计值在3.4附近γ估计值在15.2附近。参数恢复效果不错说明优化过程是可靠的。如果你发现结果差得很远先检查随机种子是否固定再检查数据是否经过了正确的清洗。3.4 计算每个客户的期望交易金额参数估出来后用前面提到的闭式解公式计算每个客户的期望交易金额# 期望交易金额公式 expected_value ( (v_hat m_x * x) / (p_hat * x q_hat - 1) ) * (p_hat / (q_hat - 1)) df[expected_value] expected_value看一下前10个客户的结果对比客户ID交易次数真实水平历史平均金额模型预测值01548.747.948.211236.238.537.121861.460.260.53579.090.381.241444.845.945.2注意看第3号客户交易次数只有5次历史平均金额是90.3但真实水平只有79.0模型预测值是81.2。模型把高估拉回来了不少。这就是收缩在实际数据上的效果。3.5 可视化看收缩效果画两个散点图左边是历史平均金额对真实水平右边是模型预测值对真实水平横轴都是真实水平加一条对角线参考线。fig, axes plt.subplots(1, 2, figsize(12, 5)) for ax, col, title in [ (axes[0], average_amount, 历史平均金额 vs 真实水平), (axes[1], expected_value, 模型预测值 vs 真实水平) ]: ax.scatter(df[true_lambda], df[col], alpha0.4, s10) lim [0, df[[true_lambda, col]].max().max() * 1.1] ax.plot(lim, lim, r--, linewidth1) ax.set_xlim(lim) ax.set_ylim(lim) ax.set_xlabel(真实水平) ax.set_ylabel(title.split( vs )[0]) ax.set_title(title) plt.tight_layout() plt.show()运行之后你会看到左图中低交易次数的客户点明显偏离对角线上下散布范围很宽右图中同样的客户点会更集中在对角线附近。偏离程度用数字量化一下mae_hist np.mean(np.abs(df[average_amount] - df[true_lambda])) mae_model np.mean(np.abs(df[expected_value] - df[true_lambda])) print(f历史均值 MAE: {mae_hist:.4f}) print(f模型预测 MAE: {mae_model:.4f})在我的测试数据里历史均值的MAE大约是10.5模型预测的MAE大约是6.3下降了差不多40%。这就是Gamma-Gamma模型的直接价值。3.6 和lifetimes库的结果对照如果你不想自己写优化代码直接用lifetimes库会更省事。lifetimes的GammaGammaFitter封装好了整个流程from lifetimes import GammaGammaFitter ggf GammaGammaFitter(penalizer_coef0.0) ggf.fit( frequencydf[frequency], monetary_valuedf[average_amount] ) df[lifetimes_pred] ggf.conditional_expected_average_profit( df[frequency], df[average_amount] )跑完和手工实现的结果几乎一致。我个人的建议是第一次接触模型时至少手工实现一遍否则你永远不知道库里输出的数字是怎么来的跑通之后再用库效率更高代码也更不容易出错。4. 结果怎么用从模型到业务动作4.1 读懂模型输出的三个层次估计完参数之后模型输出至少可以从三个层面去解读。参数层面p、q、γ本身有业务含义。p越大说明单个客户的单笔金额越稳定波动越小q和γ组合起来描述的是整个客户群体的金额分布q相对γ的比例决定了分布的形状。比如q3.5、γ15时客户群体的平均交易水平集中在52元左右但长尾客户能达到一两百元。客户层面每个客户都会得到一个期望交易金额。这个值已经过收缩处理比历史均值稳健适合用来排序和分层。你可以把客户按预期交易金额分成高、中、低三档再和交易频次结合形成四象限高频高金额核心价值客户值得重点维护高频低金额忠诚但客单低适合做交叉销售、绑定套餐提客单低频高金额购买意向强但频次低适合做召回和促销刺激低频低金额价值较低控制营销成本为主人群层面模型预测值的分布本身可以用来做用户价值分层体系的底座。比如把期望交易金额超过100元、交易次数超过10次的客户定义为金卡级以及以此类推设计不同等级的价值门槛。4.2 和BG/NBD模型怎么衔接Gamma-Gamma模型只管金额不管频次。所以实际的CLV计算要把它和之前的BG/NBD模型输出相乘。完整的CLV公式可以写成CLV 预期未来交易次数 × 预期每单交易金额在代码层面就是先跑BG/NBD得到expected_number_of_purchases再跑Gamma-Gamma得到conditional_expected_average_profit两者相乘就是每个用户的未来价值。这里有个很容易踩的坑Gamma-Gamma模型要求输入数据里的交易金额是正数并且交易次数至少为1所以训练之前需要过滤掉零交易用户只对有消费记录的用户建模。4.3 从分数到动作一个完整示例假设你现在拿到了一份用户清单里面有交易频次、历史平均金额、模型预测金额。我建议这样落地第一先圈出预测金额排名前20%的用户这批人是模型判断的长期高价值用户。如果发现历史平均金额很高但模型预测金额偏低的人说明样本量不够需要更多数据验证暂时不要投入大量成本。第二对预测金额介于中间段的用户重点做提客单动作比如关联推荐、满减升级。第三对预测金额低但近期刚注册、只有一两次交易记录的新用户不要过早放弃。模型对低样本用户的预测偏向群体均值随着交易次数增加预测值会动态更新这类用户的真实价值可能比当前分数高。5. 实操中容易踩的坑与排查清单5.1 数据清洗的边界不要高估原始数据的干净程度。金额字段经常出现0元订单、负数退款记录、测试订单。Gamma-Gamma模型的训练集里这些记录必须处理干净金额必须大于0退款金额如果冲抵了订单要么单独建模要么从训练集剔除异常大单要不要截断建议先看分位数比如超过99.5%分位数的订单单独标记然后做一次敏感性测试看看截断前后参数变化大不大。另外交易次数只有1的用户怎么办严格说x1时历史平均金额就是这一单的金额波动极大。模型仍然会对它做收缩预测但你要知道这类用户的预测值置信度很低。实践中可以先看看这部分用户占比如果占比很高说明样本数据覆盖的观察窗口太短需要拉长观察期再建模。5.2 优化不收敛怎么办我在用scipy跑负对数似然优化时偶尔会遇到不收敛或者参数跑到边界的问题。排查思路先看初始值。如果v初始化为1而真实参数在100附近优化器可能需要很多次迭代才能爬过去甚至卡在局部最优。建议先用矩估计粗略算一下历史平均金额的均值可以作为q×γ的估计历史平均金额的方差可以反推p和q的量级给优化器一个合理的起点。再看优化算法。L-BFGS-B在参数有边界时表现稳定如果数据量大可以试试TNC如果数据量小Nelder-Mead这种无梯度方法有时反而更稳。最后看数据量。Gamma-Gamma模型的似然函数在数据量低于几百个客户时参数标准误很大估计结果不稳定这时候不建议过度解读参数。5.3 假设不满足的迹象Gamma-Gamma模型有一个假设很容易被违背交易金额和交易次数独立。如果你的业务里客单价和购买频次明显负相关比如平台有“满300减50”的活动大额订单用户反而买得少那么这个模型的结果会有系统性偏差。怎么判断直接算一下历史平均金额和交易次数的相关系数。如果绝对值超过0.3就要警惕。还有一种办法分别对交易次数低于中位数和高于中位数的两组用户看金额分布如果分布差异明显说明独立性假设可能有问题。这时候可以考虑对交易次数分段建模或者改用分层Gamma-Gamma模型当然这会复杂不少。5.4 常见问题速查表问题现象可能原因解决办法参数估计和真实值差异巨大随机种子未固定、参数初始化不合适固定seed改进初始化换优化算法客户历史均值高但模型预测低交易次数少收缩效应明显属于正常现象建议补充更多观察窗口优化器报错或损失函数为NaN金额单位太大导致数值溢出把金额缩小到百元或千元单位或者先做log变换预测金额出现负数参数q接近1导致分母接近0检查q的估计值必要时加penalizer正则模型预测全部挤在群体均值附近历史金额信息量太小交易次数普遍为1检查训练数据是否过滤掉了低频用户5.5 一个数值稳定性的小技巧交易金额如果以“元”为单位动辄几百上千代入似然函数后会涉及指数运算和gamma函数数值溢出风险比较大。一个很实用的技巧是先把金额统一缩小100倍甚至1000倍再建模参数估计完成后再换算回元。因为Gamma分布对尺度变化是敏感的缩小数据会让v的估计值同步缩小p和q不变最后预测的期望金额自动回到原来的单位。这个技巧在处理极端值、大金额业务时非常有用。6. 再说几句个人体会跑完这个模拟项目我最大的感受是Gamma-Gamma模型其实是在回答一个特别朴素的问题——你已经看到的用户表现在多大程度上能代表他的真实水平做数据分析的人喜欢用均值但均值在小样本下是最不稳定的指标之一。模型的价值不是告诉你一个确定答案而是告诉你一个权衡过的期望值在证据不足时保持谨慎在证据充分时相信个体。我建议你自己把模拟代码跑一遍然后改一改参数比如把p调成1.5看看收缩的力度会怎么变化。多试几组参数你对模型的理解会深很多。还有一个小提醒模型输出的是期望值不是保证值真正上线决策时最好结合预测值的置信区间或者分位数来做判断别把一个点估计当成用户的真实画像。如果后续想把这套东西扩展可以考虑的方向有三个一是把交易金额模型和频次模型串起来做完整的CLV计算二是对不同渠道来源的用户分别建模看不同渠道带来的用户金额质量差异三是引入协变量把用户的城市、会员等级等信息加进模型里。每一步扩展都能让模型的业务解释力上一个台阶但前提是先把手里的基础模型跑扎实。
返回列表