时间: 2020-09-03 00:08:26 人气: 2294 评论: 0
bettenW/Tencent2019_Finals_Rank1stgithub.com

在本篇文章中,我将给出2019腾讯广告算法大赛的基本思路分享,将包括初赛方案分享和复赛方案分享,由于赛题的特殊性,初赛和复赛做法上的差异非常大,如果只从特征上来看,初赛和复赛的特征完全不一样。
幸运的是我们团队在初赛和复赛均是冠军,在后续文章中我也将详细解读赛题,并从赛题分析、数据探索性分析、特征工程、算法建模进行分析,同时我也将分享更多从赛题中映射出来的知识点和经验分享。(决赛后分享)
主要内容:
初赛:赛题分析、赛题难点、探索性数据分析、数据预处理、特征工程、算法建模、模型融合
腾讯效果广告采用的是GSP(Generalized Second-Price)竞价机制,广告的实际曝光取决于广告的流量覆盖大小和在竞争广告中的相对竞争力水平。其中广告的流量覆盖取决于广告的人群定向(匹配对应特征的用户数量)、广告素材尺寸(匹配的广告位)以及投放时段、预算等设置项。而影响广告竞争力的主要有出价、广告质量等因素(如 pctr/pcvr 等), 以及对用户体验的控制策略。通常来说,基本竞争力可以用千次曝光收益 ecpm = 1000 * cpc_bid * pctr = 1000 * cpa_bid * pctr * pcvr (cpc, cpa 分别代表按点击付费模式和按转化付费模式)。综上,其中前者决定广告能参与竞争的次数以及竞争对象,后者决定在每次竞争中的胜出概率。二者最终决定广告每天的曝光量。 本次竞赛将提供历史n天的曝光广告的数据(特定流量上采样),包括对应每次曝光的流量特征(用户属性和广告位等时空信息)以及曝光广告的设置和竞争力分数;测试集是新的一批广告设置(有完全新的广告id,也有老的广告id修改了设置),要求预估这批广告的日曝光 。( 出于业务数据安全保证的考虑,所有数据均为脱敏处理后的数据。)
可以看出,本次赛题的目标是通过对广告的历史信息预测未来某一天广告的日曝光量,我们可以将其看作是回归问题,更进一步可以看出时间序列回归问题。
官方给出的文件有曝光历史数据表、曝光用户的属性数据、广告静态数据表、广告操作数据表和待预估广告数据表。
评价指标分别为smape和单调性得分,其中smape和常见的mae和mse有一定的区别,主要是来评估准确性的,即smape越小越好。主要是单调相关指标,由题目描述“由于竞价机制的特性,在广告其他特征不变的前提下,随着出价的提升,预估曝光值也单调提升才符合业务直觉。”
我们是很容易得到60分的,然后仅使用一行代码就能得到79+。代码如下:
test.set_index('sample_id')[['ad_id', 'bid']].groupby('ad_id')['bid'].apply(lambda row: pd.Series(dict(zip(row.index, row.rank()/6)))).round(4).to_csv('submission.csv', header=None)
在这一小节我们对本次赛题中存在的难点进行分析和总结,我们将本次赛题的难点归结如下三点:
1)赛题并没有给出明确的训练集和标签,那么如何构建训练集和标签成为第一个需要翻越的障碍。
2)测试集是新的一批广告设置(有完全新的广告id,也有老的广告id修改了设置),面对新广告该如何预测。
从A榜到B榜,从初赛到复赛,新广告的占比越来越大,能够同时兼顾新旧广告成为取得胜利的关键。
初赛A 总广告:1954 旧广告: 1361 新广告:593 新广告占比:30.348%
初赛B 总广告:3750 旧广告: 1382 新广告:2368 新广告占比:63.147%
3)对于最后提交结果,如何保证出价单调性,而不是最终对结果进行修正。
如果只是在最后进行单调修正,比如,有三个相同广告id的样本,其出价是{1,10,100},曝光量是{1,1.1,1.2},可以看出随着出价的提升,曝光值也在提升,符合单调性,故满分。虽然单调性上满分,但这并不是实际业务想要的结果。我们目的是需要通过训练得到单调性。
由于训练集构建的方式不同,首先明确下,我个人在初赛的广告ID均是从广告操作表中提取的,即提取广告操作表中有初始出价的广告ID,并且在日志数据白表中出价唯一的广告ID,这里我们已初始B榜为准。下面将给出提取训练集代码。
首先对totalExposureLog数据进行基本的处理,为什么这样处理,将在数据预处理部分说明:
totalExposureLog = totalExposureLog.drop_duplicates(subset=['aid','uid','aid_location','request_time'], keep='last') totalExposureLog = totalExposureLog.loc[(totalExposureLog.pctr<=1000)] totalExposureLog = totalExposureLog.loc[(totalExposureLog.quality_ecpm>=0)] totalExposureLog = totalExposureLog.loc[(totalExposureLog.totalEcpm<=120000)] totalExposureLog = totalExposureLog.loc[(totalExposureLog.quality_ecpm<=80000)] totalExposureLog = totalExposureLog.loc[(totalExposureLog.bid<=15000)]
接下来构造训练集:
ad_static_feature = pd.read_table(path + 'testA/ad_static_feature.out', names=['aid', 'create_time', 'account_id', 'goods_id', 'goods_type', 'industry_id', 'aid_size']) ad_operation = pd.read_table(path + 'testA/ad_operation.dat', names=['aid', 'update_time', 'type', 'update_key', 'update_value']) # 对广告操作表中有初始出价的广告id进行标记 tmp = ad_operation.loc[(ad_operation.update_time==0)&(ad_operation.update_key==2), ['aid','update_value']] tmp.columns = ['aid','bid'] ad_static_feature = ad_static_feature.merge(tmp, on='aid', how='left') data = data.merge(ad_static_feature, on='aid', how='left') data['bid'] = data['bid'].fillna(-999) # 对出价唯一的广告id进行标记 bid_nuni = totalExposureLog .groupby(['aid'])['bid'].agg({'nunique'}).reset_index() bid_nuni.columns = ['aid','nuni'] bid_nuni = bid_nuni[bid_nuni['nuni']==1] data = data.merge(bid_nuni, on='aid', how='left') data['nuni'] = data['nuni'].fillna(-999) # 同时满足两个条件 data = data.loc[(data.period!=-999)|(data.nuni!=-999)]
那么最终会得到233195行训练集样本(代码是可以优化的)。训练集有了确定的数目后,我们就可以进行一些基本的数据分析。
训练集标签基本统计信息:
日志曝光数据基本可视化:
结合基本的数据分析,数据预处理部分主要剔除一些异常样本和噪音,这里对整体日志曝光数据进行了简单的清洗。
# 移除相同 样本 totalExposureLog = totalExposureLog.drop_duplicates(subset=['aid','uid','aid_location','request_time'], keep='last') # 移除pctr高于密集区的样本 totalExposureLog = totalExposureLog.loc[(totalExposureLog.pctr<=1000)] # 移除quality_ecpm高于密集区的样本 totalExposureLog = totalExposureLog.loc[(totalExposureLog.quality_ecpm>=0)&(totalExposureLog.quality_ecpm<=80000)] # 移除totalEcpm高于密集区的样本 totalExposureLog = totalExposureLog.loc[(totalExposureLog.totalEcpm<=120000)] # 移除bid高于密集区的样本 totalExposureLog = totalExposureLog.loc[(totalExposureLog.bid<=15000)]
在初赛中,初始特征分为类别特征和数值特征,基本上我们都会使用的,只不过会重新构造一下。
# 类别特征 categorical_features = ['aid_size','goods_type','goods_id','industry_id','account_id','crowd', 'period','area', 'behavior'] # 数值特征 numerical_features = ['pctr','quality_ecpm','totalEcpm','ecmp']
对于基本类别特征,除了投放人群crowd和投放时段外period,其余的直接进行onehot。
下面将对投放人群和投放时段单独处理:
投放时段构造代码:
def get_fill_period(item): if item != -999: item = item.split(',')[3] item = list(bin(int(item))[2:]) item.reverse() item = "".join(item) l = len(item) item = '0'*(48-l) + item else: item = '2'*48 return item # 投放时段分为七部分,正好一周,不过基本相同,所以默认选择的周四的投放时段 # 别问我为什么,修改这个bug后,反而掉分了 data['period'] = data['period'].apply(get_fill_period) test['period'] = test['period'].apply(get_fill_period) # 将48个时段全部展开,构造48个二值特征 binary_columns = [] for i in range(0,48): data[str(i)+'_period'] = data['period'].apply(lambda x: int(x[i])) test[str(i)+'_period'] = test['period'].apply(lambda x: int(x[i])) binary_columns.append(str(i)+'_period')
投放人群构造代码:
def get_open_crowd(df_): df = df_.copy() crowd_data = [] crowd_type = [] df['crowd_list'] = df['crowd'].apply(lambda x:str(x).split('|')) for i in range(df.shape[0]): line = df['crowd_list'][i:i+1][i] crowd_dict = {'area':np.nan,'age':np.nan,'status':np.nan,'gender':np.nan,'behavior':np.nan,'connectionType':np. 技术沙龙 教程文章 热点综合