☰
Gamma-Gamma模型实战:用Python预测客户期望交易金额与贝叶斯收缩
2026/10/4 6:23:59 网站建设 项目流程

做用户增长和商业分析的朋友应该都有过这种经历:销售报表里拉出一批用户的平均客单价,然后直接用这个数去估算未来能赚多少钱,结果到了年底一算,发现高估了一大截。这个问题的根源不在于算术,而在于你用“历史均值”这种点估计替代了“期望值”这种概率估计。这篇是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.0,q = 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、γ和真实参数比一比,差异越小说明优化算法执行得越成功。

第二个维度是预测误差。对每个客户,计算历史平均金额与真实水平的绝对误差,再计算模型预测值与真实水平的绝对误差,然后比较两组误差的平均值。如果模型有效,预测值的误差应该明显小于历史均值的误差。

第三个维度是校准图。把所有客户的真实水平、历史均值、模型预测值放在同一张散点图里,横轴是真实水平,纵轴是估计值。散点越贴在对角线y=x上,说明估计越准。这张图也是给业务方汇报时最有说服力的证据。

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 plt

3.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, scale=gamma_true) lambda_true = gamma_dist.rvs(a=q_true, scale=gamma_true, size=n_customers) # 每个客户随机生成 3 到 20 笔订单 x_i = np.random.randint(3, 21, size=n_customers) # 生成每笔订单金额 amounts = [] for i in range(n_customers): customer_amounts = gamma_dist.rvs( a=p_true, scale=lambda_true[i], size=x_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.0,v初始化为一个接近整体客单价均值或略大的数。比如客户平均订单金额大概是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), method='L-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.2
11236.238.537.1
21861.460.260.5
3579.090.381.2
41444.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], alpha=0.4, s=10) lim = [0, df[['true_lambda', col]].max().max() * 1.1] ax.plot(lim, lim, 'r--', linewidth=1) 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_coef=0.0) ggf.fit( frequency=df['frequency'], monetary_value=df['average_amount'] ) df['lifetimes_pred'] = ggf.conditional_expected_average_profit( df['frequency'], df['average_amount'] )

跑完和手工实现的结果几乎一致。我个人的建议是:第一次接触模型时,至少手工实现一遍,否则你永远不知道库里输出的数字是怎么来的;跑通之后再用库,效率更高,代码也更不容易出错。

4. 结果怎么用:从模型到业务动作

4.1 读懂模型输出的三个层次

估计完参数之后,模型输出至少可以从三个层面去解读。

参数层面,p、q、γ本身有业务含义。p越大,说明单个客户的单笔金额越稳定,波动越小;q和γ组合起来描述的是整个客户群体的金额分布,q相对γ的比例决定了分布的形状。比如q=3.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的用户怎么办?严格说,x=1时历史平均金额就是这一单的金额,波动极大。模型仍然会对它做收缩预测,但你要知道这类用户的预测值置信度很低。实践中可以先看看这部分用户占比,如果占比很高,说明样本数据覆盖的观察窗口太短,需要拉长观察期再建模。

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计算,二是对不同渠道来源的用户分别建模,看不同渠道带来的用户金额质量差异,三是引入协变量,把用户的城市、会员等级等信息加进模型里。每一步扩展都能让模型的业务解释力上一个台阶,但前提是先把手里的基础模型跑扎实。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询