CTR 平滑方法
# 一、背景
在电商领域中,经常要计算 CTR 或者 CVR,以点击率 CTR 为例,CTR 的值等于点击量(Click)除以曝光量(Exposure)。以 表示点击率,
然而实际应用中,会遇到两个问题:
1. 「新品问题」新商品点击率的预测和计算问题
若曝光 1 次,点击 1 次,则 的值为 1;若曝光 1 次,点击 0 次,则 的值为 0。可以看到在曝光过少时,CTR 的波动较大,不够准确。
2. 「数据不可信问题」不同商品点击率之间的比较
假设有两件商品 A 和 B,,,但这样计算不够合理,从置信度的角度来说,明显 更加可信。
解决以上两个问题可以使用平滑的技术解决。最简单的方法是在计算 CTR 的公式中分子分母同时加上一个数,加上之后可避免这两个问题。
但 和 的值如何确定呢?
首先,这两个数可以人为设定,或者使用历史数据的平均值等,这样可以在一定程度上解决第一个问题。但第二个问题表明, 的值需要根据曝光量、点击量进行动态变化,不能简单地给出一个定值。
即解决的思路为:
引入先验 (解决问题 1:),而且 的取值可以根据曝光量大小变化(似然)而改变(后验)(解决问题 2)。
本文将介绍如何通过历史数据来计算有统计意义的 和 ,即贝叶斯平滑。
# 二、理论基础
对于一件商品或者或者一个广告,对于某次曝光,用户要么点击,要么不点击,符合二项分布。因此下文对于点击率的贝叶斯平滑,都是基于以下假设:
- 对于某件商品或者广告 ,其是否点击是一个伯努利分布(Bernoulli):
- 表示是否点击,当 时表示点击,当 时表示未点击, 表示某件商品被点击的概率,即点击率。
# 三、贝叶斯平滑
首先通过极大似然估计得到初始 的值,作为先验的初始化。
根据贝叶斯公式可知:,即 。
为了让参数 能够随着数据的变化而动态改变,我们需要让后验是先验的共轭分布,这样才能将上一次的后验结果作为本次的先验输入,不断对后验结果进行修正。
由于先验分布 ,因此我们假设后验分布 ,而贝叶斯平滑给出的的公式为:
那么接下来我们的目标就是估计出 和 。
通过二阶矩估计得到,
在工程实现上,需要取连续一段时间的数据,比如一周,然后在每天的数据中计算每件商品或者广告的点击率,之后求这些点击率的均值和方差,然后带入到公式中。
# 四、威尔逊置信区间
置信区间的实质,就是进行可信度的修正,弥补样本量过小的影响。
- 如果样本多,就说明比较可信,不需要很大的修正,所以置信区间会比较窄,下限值会比较大;
- 如果样本少,就说明不一定可信,必须进行较大的修正,所以置信区间会比较宽,下限值会比较小。
二项分布的置信区间有多种计算公式,最常见的是“正态区间”。但是,它只适用于样本较多的情况( 且 ),对于小样本,它的准确性很差。
1927 年,美国数学家 Edwin Bidwell Wilson 提出了一个修正公式,被称为“威尔逊区间”,很好地解决了小样本的准确性问题。
在上面的公式中, 表示样本的 CTR, 表示样本的大小, 表示对应某个置信水平的 统计量,这是一个常数,可以通过查表得到。一般情况下,在 的置信水平下, 统计量的值为 。
我们一般选取置信区间得下限作为估计值,可以看到,当 的值足够大时,这个下限值会趋向 。如果 非常小,这个下限值会大大小于 。
# 五、代码实战
import numpy as np
class BayesianSmoothing(object):
def __init__(self, alpha, beta):
self.alpha = alpha
self.beta = beta
def update_from_data_by_moment(self, pos, n):
"""
使用二阶矩估计,计算 alpha、beta
:param pos: 正例数
:param n: 总数
"""
def __compute_moment(_pos, _n):
_ctr_list = []
_var = 0.0
for _ in range(len(_n)):
if not _n[_]:
continue
_ctr_list.append(float(_pos[_]) / _n[_])
_mean = sum(_ctr_list) / len(_ctr_list)
for _ctr in _ctr_list:
_var += pow(_ctr - _mean, 2)
return _mean, _var / (len(_ctr_list) - 1)
mean, var = __compute_moment(pos, n)
self.alpha = mean * (mean * (1 - mean) / (var + 1e-6) - 1)
self.beta = (1 - mean) * (mean * (1 - mean) / (var + 1e-6) - 1)
class Wilson(object):
@classmethod
def wilsonCI(cls, pos, n, z=1.96):
"""
威尔逊置信区间下限计算函数
:param pos: 正例数
:param n: 总数
:param z: 正态分布的分位数(查表 0.95 的置信区间约等于 1.96)
:return: 威尔逊置信区间下限
"""
p = pos * 1. / n * 1.
return (p + (np.square(z) / (2. * n))
- ((z / (2. * n)) * np.sqrt(4. * n * (1. - p) * p + np.square(z)))) / \
(1. + np.square(z) / n)
if __name__ == '__main__':
clk = [0, 2, 30, 100]
exp = [3, 5, 100, 500]
bs = BayesianSmoothing(1, 1)
bs.update_from_data_by_moment(clk, exp)
print('Bayes 平滑先验分布参数:', bs.alpha, bs.beta)
for i in range(len(clk)):
origin_ctr = clk[i] / exp[i]
bayes_ctr = (clk[i] + bs.alpha) / (exp[i] + bs.alpha + bs.beta)
wilson_ctr = Wilson.wilsonCI(clk[i], exp[i])
print('修正前{},Bayes 修正后{},Wilson 修正后{},合并修正后{}'.format(
round(origin_ctr, 3),
round(bayes_ctr, 3),
round(wilson_ctr, 3),
round((bayes_ctr + wilson_ctr) / 2, 3)
))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
测试结果
Bayes 平滑先验分布参数: 1.1201324526016245 3.858234003405596
修正前0.0,Bayes 修正后0.14,Wilson 修正后0.0,合并修正后0.07
修正前0.4,Bayes 修正后0.313,Wilson 修正后0.118,合并修正后0.215
修正前0.3,Bayes 修正后0.296,Wilson 修正后0.219,合并修正后0.258
修正前0.2,Bayes 修正后0.2,Wilson 修正后0.167,合并修正后0.184
2
3
4
5