1. 背景
    在实际广告投放过程中,我们常常会碰到一个问题:媒体流量比较大,广告主预算消耗过快,有些中小广告主甚至在开始投放的几分钟内就把预算消耗完。这会导致广告主早早退出后续流量的竞争,不仅会影响广告主体验(无法触达到更多的优质用户),也导致整个广告不平稳(竞争都集中在早期,而后期又竞争不足)。

预算控制(Budget Pacing)的作用就是平稳花掉广告主的预算,并帮助广告主优化转化效果。所以我们预算控制要完成如下目标:

广告匀速播放:通过广告日预算、当前消耗以及日曝光曲线来控制广告投放速度

提升广告主ROI:帮助广告主以更低的价钱拿到更多优质曝光量:通过PCTR分层,对优质客户优先展示广告。
本文是基于Probabilistic throttling(概率节流PTR),通常做法是通过某种方式丢弃一定量流量,丢弃的流量不参加竞价,进而控制预算花费速率;
为了这个目标我们参考了论文《Smart Pacing for Effective Online Ad Campaign
Optimization》,LinkedIn 在 2014 年发表的一篇论文,提出的预算控制策略并不复杂,并且具有很强的实践性和工程性。

接下来介绍下我们的这个算法,其主要思想跟LinkedIn算法比较类似。我们的主要思路就是将推广计划的消耗趋势与大盘曝光趋势保持一致,以天为时间单位,推广计划为预算控制单位。

首先根据历史数据,预测出当天大盘总曝光数。然后基于其曝光情况,在当前时间片,假如已消耗 / 当天预的比例大于大盘已曝光 / 大盘总曝光的比例,则说明预算已经消耗过快,需要减小消耗的速度,反之则要加快消耗的速度。

(1) 首先我们会把每个广告计划的所有请求会被分成 L 层,则在第 t-1 个时间片内各层的参竞率记为:

各层消耗记为:

一天的总预算 B 会根据时间片划分成 K份小预算,即:
但是实际投放中不能保证每个时间片的消耗都刚好达到分配的预算值,因此如果前面的时间片中出现了少投或超投情况,就要把少投或超投那些部分均摊到后面的时间片中,所以每个时刻的预算需要根据前面的花费来调整,调整后的消耗记为:

上式中的 BmB_{m}Bm​表示经过了 m 个时间片后剩余的实际预算,

表示经过了 m 个时间片后剩余的实际预算,分子 Bm−∑i=1KBiB_{m}-\sum_{i=1}^{K}B^{i}Bm​−∑i=1K​Bi表示当前预算花费是否超过了预期(<0)或者不满足预期(>0),并通过分母均摊到后面的 K−m 个时间片中。(这里你可以看做该时刻的预算与实际时刻预算的残差)

则调整各层参竞率的控制算法如下图所示,图中的,R=C^t−Ct−1R=\hat C^{t}-C^{t-1}R=C^t−Ct−1表示如果按照前一时间片的参竞率投放时,当前时间片的预算能否花完。则当 R>0 时,需要提高当前时间片的参竞率,反之需要降低 这一时间片的参竞率。Ct−1C^{t-1}Ct−1表示上一时间段的实际消耗。

上面算法还有几点细节需要注意:

L层表示参竞率最大的层, [公式] 层表示参竞率为非0的最小的层
当需要提升参竞率最时,是从第 L层到 [公式] 层进行的;而需要降低参竞率时,是从第[公式]层到第 L层进行的,其目的都是优先提升ctr高的层的参竞率,优先降低ctr低的层的参竞率,从而达到最小化成本的目的
trial rate 的目的是让参竞率非 0 的最小层的下一层以一个很小的参竞率进行参竞(参竞率会随着层数增加而增加),本来这一层的参竞率应该是 0 的,但是这里给了一个很小的 trail rate,目的是为了方便后面加快预算花费做准备(代码实现上也不用特殊处理)

在实际处理中,我们要有很多情况需要考虑的,以下列举部分:

上文的核心代码部分如下所示,具体技术细节,暂时保密:


float pvPdfLast = Float.parseFloat(args[0]); //当前前一时刻累计曝光比例,例如0.5421
float pvPdfNow = Float.parseFloat(args[1]); //当前时刻累计曝光比例,例如0.5556
float costPdf = Float.parseFloat(args[2]); //当前时刻累计花费例如520
int layerLen = args[6];//读取层数,3
int curT = args[7];//当前时刻1440
float[] cost = new float[layerLen];
if (StringUtils.isNotEmpty(args[3])) {cost = args[3].split( "#");//每层花费,0#10#30#50,已经按照PCTR从小到大} else {for (int i = 0; i < layerLen; i++) {cost[i] = 0.0f;}}float[] r = new float[layerLen];if (StringUtils.isNotEmpty(args[4])) {r = args[4].split( "#");//上一时刻每层的消耗cost,按照#分割,已经按照PCTR从小到大} else {for (int i = 0; i < layerLen; i++) {r[i] = 0.0f;} }float curBudget = 0.0f;if (StringUtils.isNotEmpty(args[5])) {curBudget = Float.parseFloat(args[5]);//预算}float bt = curBudget * (pvPdfNow - pvPdfLast); //本时刻单元预算float ct = bt + ((curBudget - costPdf) - curBudget * (1 - pvPdfNow)) / (1320 - curT);//调整后的消耗记为Ct//B-costPdf:剩余花费  B * (1 - pvPdfNow):计划剩余花费float lastC = 0;//上一时刻总花费for (int i = 0; i < cost.length; i++) {lastC += cost[i];}float restC = ct - lastC;float[] rNext = r; //上一时刻每层的竞参率r 应该是一串数据,按照_$_分割,已经按照PCTR从小到大,r.length:分成多少层float trialRate;//目的是让参竞率非 0 的最小层的下一层以一个很小的参竞率进行参竞if (restC < 0) {  // 需要减速for (int i = 0; i < r.length; i++) {//先从低质流量减速if (r[i] > 0.0f)//等于0的情况下不用减速{float x = (r[i] + 0.01f) * (cost[i]  + restC) / (cost[i] + 1);//防止分母出现0,cost[i]+1//r[i]防止启动失败,+0.01frNext[i] = x <= 0? 0.000000f: x;}}for (int i = 1; i < r.length - 1; i++) {trialRate = 0.01f * ct / (cost[i] + 1) * r[i];//防止cost[i]为0trialRate = trialRate > 1? 1.00f: trialRate;rNext[i] = rNext[i + 1] > trialRate && rNext[i] < trialRate? trialRate: rNext[i];}} else if (restC > 0) {// 提高当前时间片的参竞率for (int i = r.length - 1; i >= 0; i--) {//先从高流量提速if (r[i] < 1.0f) {//等于1的情况下不用提速float x = (r[i]+0.01f) * (cost[i] + restC) / (cost[i] + 1);rNext[i] = x >= 1? 1.000000f: x;}}for (int i = 0; i < r.length - 1; i++) {trialRate = 0.05f * ct / (cost[i] + 1) * r[i];//防止cost[i]为0trialRate = trialRate > 1? 1.00f: trialRate;rNext[i] = rNext[i + 1] > trialRate && rNext[i] < trialRate? trialRate: rNext[i];}}

rNext就是下次竞参率,scala简易版如下:


public class AdjustWithoutPerfGoal {public static void main(String[] args){float pv_pdf = 0.501f;float pv_pdf_t = 0.534f;float cost_pdf = 0.514f;String cost = "3#5#7#9";String r = "0.3#0.5#0.7#0.9";float B = 50f;float goal = 0.5f;System.out.println(PTR(new String[]{String.valueOf(pv_pdf),String.valueOf(pv_pdf_t),String.valueOf(cost_pdf),cost,r,String.valueOf(B),String.valueOf(goal)}));}public static float[] PTR(String[] args){/**输入:累计曝光pv_pdf//cost_pdf当前时刻累计花费比例//上一时刻的消耗cost/上一时刻竞参率r//B当天单元预算*/float pv_pdf = Float.parseFloat(args[0]); //当前时刻累计曝光比例float pv_pdf_t = Float.parseFloat(args[1]); //下一时刻累计曝光比例float cost_pdf = Float.parseFloat(args[2]); //当前时刻累计花费比例float[] cost = strToFloatArrBySep(args[3], "#");float[] r = strToFloatArrBySep(args[4],"#");//上一时刻每层的消耗cost,应该是一串数据,按照#分割,已经按照PCTR从小到大float[] r_new = r; //上一时刻每层的竞参率r 应该是一串数据,按照_$_分割,已经按照PCTR从小到大,r.length:分成多少层float B = Float.parseFloat(args[5]); //当天单元预算float B_Next = B*(pv_pdf_t-pv_pdf); //下一时刻单元预算float R = (pv_pdf-cost_pdf)*B_Next; //比例if(R<0){  // 比例小了,需要减速for (int i=0; i<r.length; i++) {//先从低质流量减速if(r[i]>0){float x =r[i]*(cost[i]+R)/cost[i];if (x<=0){//更新0r_new[i] = 0;}else {r_new[i] = x;}}}}else if(R>0){// 比例大了,需要提速for (int i=r.length-1; i>=0; i--) {//先从高流量提速if(r[i]>0){float x =r[i]*(cost[i]+R)/cost[i];if (x>=1){r_new[i] = 1;}else {r_new[i] = x;}}}}return r_new;}public static float[] strToFloatArrBySep(String val, String sep){String[] elems = val.trim().split(sep);float[] valFlt = new float[elems.length];for (int idx=0; idx<elems.length; idx++) {if ("".equals(elems[idx].trim())){valFlt[idx] = 0.0f;}else{valFlt[idx] = Float.parseFloat(elems[idx]);}}return valFlt;}}

有目标的优化如下:

public class AdjustWithPerfGoal {public static void main(String[] args){float pv_pdf = 0.501f;float pv_pdf_t = 0.534f;float cost_pdf = 0.514f;String cost = "3_$_5_$_7_$_9";String r = "0.3_$_0.5_$_0.7_$_0.9";String ecpc = "0.2_$_0.4_$_0.2_$_0.4";float B = 50f;float goal = 0.5f;System.out.println(PTR(new String[]{String.valueOf(pv_pdf),String.valueOf(pv_pdf_t),String.valueOf(cost_pdf),cost,r,ecpc,String.valueOf(B),String.valueOf(goal)}));}public static float[] PTR(String[] args){/**输入:累计曝光pv_pdf//cost_pdf当前时刻累计花费比例//上一时刻的消耗cost/上一时刻竞参率r//B当天单元预算*/float pv_pdf = Float.parseFloat(args[0]); //当前时刻累计曝光比例float pv_pdf_t = Float.parseFloat(args[1]); //下一时刻累计曝光比例float cost_pdf = Float.parseFloat(args[2]); //当前时刻累计花费比例float[] cost = strToFloatArrBySep(args[3], "_\\$_");float[] r = strToFloatArrBySep(args[4],"_\\$_");//上一时刻每层的消耗cost,应该是一串数据,按照_$_分割,已经按照PCTR从小到大float[] r_new = r; //上一时刻每层的竞参率r 应该是一串数据,按照_$_分割,已经按照PCTR从小到大,r.length:分成多少层float[] ecpc = strToFloatArrBySep(args[5],"_\\$_"); //上一时刻每层ecpc 应该是一串数据,按照_$_分割,已经按照PCTR从小到大float B = Float.parseFloat(args[6]); //当天单元预算float B_Next = B*(pv_pdf_t-pv_pdf); //下一时刻单元预算float R = (pv_pdf-cost_pdf)*B_Next; //比例float goal = Float.parseFloat(args[6]); //目标,在本程序里,为整体ecpcif(R<0){  // 比例小了,需要减速for (int i=0; i<r.length; i++) {//先从低质流量减速if(r[i]>0){float x =r[i]*(cost[i]+R)/cost[i];if (x<=0){//更新0r_new[i] = 0;}else {r_new[i] = x;}}}}else if(R>0){// 比例大了,需要提速for (int i=r.length-1; i>=0; i--) {//先从高流量提速if(r[i]>0){float x =r[i]*(cost[i]+R)/cost[i];if (x>=1){r_new[i] = 1;}else {r_new[i] = x;}}}}//判断整体ecpc是否高于目标float joint_ecpc = ExpPerf(r,r_new,ecpc,cost,0);if(joint_ecpc > goal){for (int j=0; j<r.length; j++){if(ExpPerf(r,r_new,ecpc,cost,j+1) > goal){r_new[j] = 0;}else {float[] x1 = fltSlice(cost,j+1,r.length);float[] x4 = fltSlice(ecpc,j+1,r.length);for (int i=0; i<x4.length; i++){x4[i] = goal/(x4[i]-1);}float x = sumByZippedMutil(x1, x4, null);float y =cost[j]*(1-goal/ecpc[j]);r_new[j] = r[j] * x/y;}}}return r_new;}/*** 计算j层到最优层的联合ecpc* @param r* @param r_new* @param ecpc* @param cost* @param j* @return*/public static float ExpPerf(float[] r,float[] r_new,float[] ecpc,float[] cost,int j) {float[] x1 = fltSlice(cost, j, r.length);float[] x2 = fltSlice(r_new, j, r.length);float[] x3 = fltSlice(r, j, r.length);float[] x4 = fltSlice(ecpc, j, r.length);float molecule = sumByZippedMutil(x1,x2,x3); //分子float[] Denominator1 = arrByZipped(x1, x2,"Mutilp");float[] Denominator2 = arrByZipped(x3, x4,"Mutilp");float[] Denominator = arrByZipped(Denominator1,Denominator2,"Div");  //分母float dem_sum = 0.0f;for (float el: Denominator) {dem_sum += el;}return molecule/dem_sum;}public static float[] arrByZipped(float[] f1, float[] f2, String flag){float[] res = new float[f1.length];for (int i=0; i< f1.length; i++){if("Mutilp".equals(flag)){res[i] = f1[i]*f2[i];}else {res[i] = f1[i]/f2[i];}}return res;}public static float[] strToFloatArrBySep(String val, String sep){String[] elems = val.trim().split(sep);float[] valFlt = new float[elems.length];for (int idx=0; idx<elems.length; idx++) {if ("".equals(elems[idx].trim())){valFlt[idx] = 0.0f;}else{valFlt[idx] = Float.parseFloat(elems[idx]);}}return valFlt;}public static float sumByZippedMutil(float[] f1, float[] f2, float[] f3){float sum = 0.0f;for (int i=0; i< f1.length; i++){if(f3.length == 0){sum += f1[i]*f2[i];}else{sum += f1[i]*f2[i]/f3[i];}}return sum;}public static float[] fltSlice(float[] flt,int start, int end){float[] res = new float[end - start];for (int i=start;i<end;i++){res[i] = flt[i];}return res;}
}

浅谈广告系统预算控制(SMART PACING)与核心代码实现相关推荐

  1. 浅谈秒杀系统架构设计

    秒杀是电子商务网站常见的一种营销手段. 原则 不要整个系统宕机. 即使系统故障,也不要将错误数据展示出来. 尽量保持公平公正. 实现效果 秒杀开始前,抢购按钮为活动未开始. 秒杀开始时,抢购按钮可以点 ...

  2. [原创] 浅谈ETL系统架构如何测试?

    [原创] 浅谈ETL系统架构如何测试? 来新公司已入职3个月时间,由于公司所处于互联网基金行业,基金天然固有特点,基金业务复杂,基金数据信息众多,基金经理众多等,所以大家可想一下,基民要想赚钱真不容易 ...

  3. 浅谈Android系统进程间通信(IPC)机制Binder中的Server和Client获得Service Manager接口之路

    原文地址: http://blog.csdn.net/luoshengyang/article/details/6627260 在前面一篇文章浅谈Service Manager成为Android进程间 ...

  4. 《浅谈-Android系统越用反应越慢的问题》

    <浅谈-Android系统越用反应越慢的问题> android应用程序和iphone应用程序不一样,用过iphone的都知道,点击图标进入程序后,如果还想用其他程序,必须先按返回退出然后进 ...

  5. 浅谈SpaceBuilder系统的缓存机制_缓存思想

    在前面的文章中也提及到为了提高系统的性能,SpaceBuilder在内部做了大量的工作,而数据缓存就是其中非常高效的处理方式. 我们知道SpaceBuilder采用了多层架构的处理模式,数据通过业务实 ...

  6. 东方木2020浅谈win10系统还原怎么操作

    编辑:东方木影院 地点:武汉 时间:2020年2月14日 东方木2020浅谈win10系统还原怎么操作,如果电脑物理内存不足时,会导致工作效率非常的低,我们可以调整win10 32位系统虚拟内存来加快 ...

  7. android 系统升级 方法,安卓系统怎么升级 浅谈安卓系统更新升级的几种方法

    最近有网友问小编"安卓系统怎么升级?",针对该问题,笔者也在网上查找了下相关资料,不过并没有找到什么有价值的相关介绍,多数都是介绍如何自动升级.或者下载升级版包等等方法,对于一些常 ...

  8. 浅谈 Linux 系统中的 SNMP Trap 【转】

    文章来源:浅谈 Linux 系统中的 SNMP Trap 简介 本文讲解 SNMP Trap,在介绍 Trap 概念之前,首先认识一下 SNMP 吧. 简单网络管理协议(Simple Network ...

  9. 【游戏客户端】浅谈装备系统

    [游戏客户端]浅谈装备系统 大家好,我是Lampard~~       不知道大家一开始接触到游戏里面的装备系统是什么时候,什么感觉.对于我来说,我第一次清晰认知到游戏里面的装备,是我小时候玩DNF. ...

最新文章

  1. __cplusplus的用处
  2. Hotspot垃圾回收
  3. 现代中小企业IT基础平台建设 - 完整案例实战(00_序)
  4. python作品_Python爬取图虫网摄影作品
  5. win7安装matlab的问题,安装matlab7.0出现问题,我是win7+64位系统,求解
  6. VSCode瞎折腾记
  7. java 实现nfa的化简_NFA的实现
  8. java中.length得到结果_Java length()方法:获取字符串的长度
  9. 还原真实的 cache recovery
  10. 理想气体的质量流量计算
  11. 【组队学习】【37期】组队学习内容详情
  12. Defect Detection论文合集、代码和数据集
  13. 3.并列句的起源与本质
  14. 国内主流短信验证码平台收费价格对比「二」:亿佰云、秒嘀科技、极光短信、华信云通信
  15. Let's Use Chinaese in Flex Successfully
  16. 中文文本纠错任务简介
  17. Underscore使用方法
  18. Could not find parameter map
  19. 4米乘以12米CAD图_设备时序图的绘制方法
  20. 编程十年 (7):科班?自学?

热门文章

  1. Sakura编辑器设置显示空格符,tab符,换行符
  2. 2021-常见PHP面试题型大全汇总并且附上答案哦!整理不易,有用记得收藏哈!
  3. 2021年中国1,4-丁二醇(BDO)市场供需分析,“限塑令”背景需求与价格持续提升「图」
  4. 解决警告——有符号 无符号不匹配
  5. 双击鼠标HOOK学习
  6. PPPOE拨号错误速查表大全
  7. 腾讯金融云首发服务全景图 嘉宾分享金融云发展最新动态
  8. 解决Linux关闭终端(关闭SSH等)后运行的程序自动停止
  9. 中国石油大学《软件工程》第二次在线作业
  10. PTA竞速 7-4 哥德巴赫猜想