on
合约广告(二):库存分配
合约广告有CPD和CPM两种,CPD其实是不需要库存分配的,因为它是购买某个广告位某个时段指定定向的所有流量,也就是这个广告位在特定时间, 特定定向的流量进来,只能命中这一条广告,所以不存在分配问题。
合约CPM广告,它是购买某个广告位某个时段内,指定定向的部分流量,如购买101广告位、1月1号-1月2号、北京20-25岁女性,100w次曝光。这种 购买方式,就可能会出现一条流量命中多个合约CPM广告。在竞价广告中,广告引擎会根据广告的eCPM排序,价高者得。但是对于合约CPM不能这样, 因为我们需要保量,即无论价格高低,媒体方都要给广告主跑满合约中的曝光量,并且要平滑的完成整个合约,即让曝光均匀的出现在合约的每一天, 而不是合约前几天疯狂曝光,导致合约期还没到,就完成曝光量了。所以库存分配其实是两个目的:保量和平滑。它的具体问题是: 一条流量命中了多个广告,曝光哪一个?
举个例子,比如现在的合约广告信息如下:
- 北京或河北、男性、爱好体育,需要100w曝光
- 河北、爱好体育,需要200w曝光
- 湖南或湖北或河北、20-30岁、男性,需要200w曝光
- 湖南或湖北或江西、男性,需要150w曝光
- 广东、女性,需要150w曝光
那么这时,如果有一个河北的爱好体育的20-30岁男性流量,会命中1、2、3广告;有一个湖北的20岁男性的流量,会命中3、4广告。这时,流量如何分配呢? 下面介绍下,一个常用的合约广告库存分配算法:HWM算法。
HWM算法
从直觉上来说,一条流量命中多个广告时,为了保量,我们应该优先把流量给那些相对难以达成合约的广告,如:广告1和广告2,他们的定向完全相同, 广告1需要100w的量,广告2需要200w的量,直觉上我们应该优先把流量分给广告2。再比如,广告3和广告4,他们的定向不同,且这俩都需要50w的曝光, 通过库存预测得知,符合广告3的流量有100w,符合广告4的流量有200w,那么当一条流量命中3和4时,我们应该优先曝光3,因为3更加难以达成。优先给广告3 不代表一定给广告3,只是本次广告3的曝光概率要大一些。
HWM算法就是根据上面那个思路来进行库存分配的。HWM的输入是一组demand、一组supply。demand就是广告,supply是特定定向条件下的一组流量。 先建立supply与demand的关系,然后再将supply按一定比例分配给demand。这个比例就是HWM的输出,我们称之为每个广告的播放概率。
具体算法如下:
- 计算每个广告合约的难以达成程度,公式为:广告i的需求量 / sum(满足广告i的supply的剩余流量),这个值越高,说明合约越难以达成, 然后将广告按该值降序排列。
- 依次计算广告的播放概率,计算方式就是:广告i的需求量 = sum(min(满足广告i的supply的剩余流量, 播放概率p * 满足广告i的supply)), 然后更新一遍supply的剩余流量,为的是减去广告i已经占有的量,公式为:supply的剩余流量 -= supply * p
- 重复执行步骤1,直至遍历完所有的广告
下面给出实现代码
package hwm
import "sort"
// Demand 广告需求
type Demand struct {
Id int32
Value int64 // 需要的流量
Priority float64 // 求解播放概率时的优先级
Supplies []*Supply // 可供给该广告的所有supply
Probability float64 // 播放概率,HWM算法的结果
}
// Supply 流量供给
type Supply struct {
Id int32
Value int64 // 总库存
RemainValue int64 // 剩余库存,初始时与总库存相等
}
func HWM(demands []*Demand) {
if len(demands) == 0 {
return
}
// 计算demand的priority,并以此逆序排序
for _, demand := range demands {
var sum int64
for _, supply := range demand.Supplies {
sum += supply.RemainValue
}
demand.Probability = float64(demand.Value) / float64(sum)
}
sort.Slice(demands, func(i, j int) bool {
return demands[i].Probability > demands[j].Probability
})
demand := demands[0]
demand.Probability = solveProb(demand, demand.Supplies)
// 更新supply的RemainValue
for _, supply := range demand.Supplies {
supply.RemainValue -= int64(float64(supply.Value) * demand.Probability)
supply.RemainValue = max(supply.RemainValue, 0)
}
// 计算下一个demand
HWM(demands[1:])
}
func solveProb(demand *Demand, supplies []*Supply) float64 {
/**
求解方程:demand value = sum(min(supply_i remain_value, 播放概率p * supply_i value))
有两种求解方式:
1. 根据每个supply,构造一个分段函数,这种方式比较麻烦。
2. p的取值范围为[0, 1],我们直接对p通过二分查询求解
*/
left, right := 0.0, 1.0
// 二分查找20次,p的精度可以精确到百万分之一了
for i := 0; i < 20; i++ {
p := (left + right) / 2
var sum int64
for _, supply := range supplies {
sum += min(int64(float64(supply.Value)*p), supply.RemainValue)
}
diff := sum - demand.Value
// 误差小于10 就认为找到结果了
if abs(diff) < 10 {
return p
}
if diff > 0 {
right = p
} else {
left = p
}
}
return 0
}
func abs(i int64) int64 {
if i < 0 {
return -i
}
return i
}
func min(i, j int64) int64 {
if i < j {
return i
}
return j
}
func max(i, j int64) int64 {
if i > j {
return i
}
return j
}
上面我们讨论了HWM算法的解法,但是却忽视一个点,算法的输入,也就是supply和demand如何构造呢?demand其实就是广告,一个广告对应一个demand。 而supply是需要根据demand的定向条件来构造出来的。下面给出具体的构造方式:
假设有4个广告:
demand 1: geo(北京) && age(20)
demand 2: geo(北京||上海) && age(20)
demand 3: geo(北京) && sex(男)
demand 4: age(20)
1. 获取每个定向维度的数据:
a. geo: 北京、上海、非北京上海
b. age:20、非20
c. sex: 男、非男
2. 对上一步的数据进行笛卡尔积,得到所有的supply:
supply a: geo(北京) && age(20) && sex(男)
supply b: geo(北京) && age(20) && sex(非男)
supply c: geo(北京) && age(非20) && sex(男)
supply d: geo(北京) && age(非20) && sex(非男)
supply e: geo(上海) && age(20) && sex(男)
supply f: geo(上海) && age(20) && sex(非男)
supply g: geo(上海) && age(非20) && sex(男)
supply h: geo(上海) && age(非20) && sex(非男)
supply i: geo(非北京上海) && age(20) && sex(男)
supply j: geo(非北京上海) && age(20) && sex(非男)
supply k: geo(非北京上海) && age(非20) && sex(男)
supply l: geo(非北京上海) && age(非20) && sex(非男)
3. 遍历ad和supply,建立demand和supply的关系,将关联不上demand的supply丢弃:
demand 1: supply a、supply b
demand 2: supply a、supply b、supply e、supply f
demand 3: supply a、supply c
demand 4: supply a、supply b、supply e、supply f、supply i、supply j
上面这个构造supply的方式,本质上找出demand之间重叠的流量。
在线播放
上面介绍了合约CPM广告的库存分配问题,通过HWM算法,可以得到每个广告的播放概率,那么这个播放概率如何使用呢,其实很简单:按照播放概率随机选择一个。 举个例子,广告引擎同时检索到ad1、ad2、ad3,播放概率分别是:0.6、0.25、0.05。在[0, 1]区间内取一个随机数,如果落在[0, 0.6]上,那么选择ad1; 如果落在[0.6, 0.85]上,选择ad2;如果落在[0.85, 0.9]上,选择ad3;如果落在[0.9, 1]上,那么ad1、ad2、ad3都不选择。
这里有3个问题:
- 为什么ad1、ad2、ad3的播放概率相加小于1呢?原因在于这三个广告没有把库存全部都占用。
- ad1、ad2、ad3都不选择,那么本次广告请求难道不召回广告了?其实是仅仅不召回合约广告,广告引擎仍然可以选择竞价广告召回。
- 会不会出现播放概率相加大于1呢?会出现的,观察我们求解播放概率的公式:
demand value = sum(min(supply_i remain_value, 播放概率p * supply_i value))
因为求解时,会取剩余库存和p*总库存之间的最小值,所以当剩余库存较小时,那么此时求出的p就可能会导致最终出现相加大于1的情况。即使当大于1的情况, 生成随机数时,也要在[0, 1]范围内生成。