2018科大讯飞AI营销算法大赛总结及完整代码(冠军)

2018科大讯飞AI营销算法大赛总结及完整代码(冠军)

冠军代码开源地址:

bettenW/2018-iFLYTEK-Marketing-Algorithms-Competition-Finals-Rank1github.com图标

写在前面

首先很幸运能够拿到这次冠军,有两位大佬队友是这次获胜的关键,再次感谢鹏哥和阿水。

同时希望我的分享与总结能给大家带来些许帮助,并且一起交流学习。

接下来将会呈现ppt内容和部分代码

  1. 赛题分析
  2. 探索性分析(EDA代码)
  3. 数据预处理
  4. 特征工程
  5. 算法模型
  6. 思考总结

1.赛题分析

本次大赛提供了讯飞AI营销云的海量广告投放数据,参赛选手通过人工智能技术构建预测模型预估用户的广告点击概率,即给定广告点击相关的广告、媒体、用户、上下文内容等信息的条件下预测广告点击概率。

这又是一道关于CTR的问题,对于CTR问题而言,广告是否被点击的主导因素是用户,其次是广告信息。所以我们要做的是充分挖掘用户及用户行为信息,然后才是广告主、广告等信息。

赛题特征:广告信息、媒体信息、用户信息、上下文信息

提供数据:共1001650初赛数据 和 1998350条复赛数据(复赛训练数据为初赛数据和复赛数据)

评估指标:通过logloss评估模型效果(越小越好),公式如下:

其中N表示测试集样本数量,y_i 表示测试集中第i个样本的真实标签, p_i 表示第i个样本的预估转化率。这类评估函数常用logloss和AUC,简单的说logloss更关注和观察数据的吻合程度,AUC更关注rank order。如果是按照概率期望来进行收费投放的话就用logloss,如果定投一定量就用AUC,主要还是和业务相关。

2. 探索性分析

这一部分将会对部分数据进行分析,另外获取每个类别特征的转化率分布情况判断特征效果,看分布可以有一个很好的初步验证作用。

不同时刻的曝光量和点击率变化,将一天分成4个时段的曝光量和点击率情况

import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)
import matplotlib.pyplot as plt
% matplotlib inline
import seaborn as sns
sns.set(style="whitegrid", color_codes=True)
sns.set(font_scale=1)

def getSeg(x):
    if x >=0 and x<= 6:
        return 1
    elif x>=7 and x<=12:
        return 2
    elif x>=13 and x<=18:
        return 3
    elif x>=19 and x<=23:
        return 4
train_df['hour_seg'] = train_df['hour'].apply(lambda x: getSeg(x))

hourDF = train_df.groupby(['hour', 'click'])['hour'].count().unstack('click').fillna(0)
hourDF[[0,1]].plot(kind='bar', stacked=True);
hourDF = train_df.groupby(['hour_seg', 'click'])['hour_seg'].count().unstack('click').fillna(0)
hourDF[[0,1]].plot(kind='bar', stacked=True);

训练集正负样本比例,大约为1:4,应该经过了降采样。

labels = [0,1]
sizes = train_df.click.value_counts().values
explode=[0.1,0]
colors = [  'lightcoral','gold']
patches, texts,autotexts= plt.pie(sizes, labels=labels,colors=colors,explode=explode,autopct="%1.1f%%",startangle=90)
plt.title("click")
plt.show()

广告的长宽是很重要的特征,正负样本中关于这两个特征的分布存在较为明显的区别。

sns.stripplot(train_df["click"],train_df["creative_height"],jitter=True,order=order)
plt.title("click Vs creative_height");

在训练集中的部分热门广告,adid为1537089的广告在训练集中曝光次数超过了12万次。

在训练集中的部分热门广告,adid为1537080的广告在训练集中点击率最高,接近0.5。

OPPO和 vivo的用户最多,而这两种机型的用户点击率也高于其他手机的用户

2. 数据预处理

由于数据噪音比较多,所以细致的预处理能够是模型更具泛化性,同时挖掘更多特征。

  1. 初复赛训练数据合并后去重(7361条)
  2. 提取广告投放时间信息,日期、小时以及早中晚时间段

0-6>--1 | 7-12>--2 | 13-18>--3 | 19-24>--4

3. 细分广告主行业与媒体广告位,去除只有一个取值的字段

102400_102401>--102400 102401

4. 清洗手机品牌和机型字段,对同类进行合并

iphone>--apple | redmi>--xiaomi | honor>--huawei

5. 对操作系统及其版本、名称进行更细粒度的刻画

5.1.1>--5 1 1 | 6.0.1>--6 0 1

6. 构造虚拟用户组别,对用户标签和其他类别特征进行编码

7. 对city特征进行切分,如

框内为身份证前六位,51代表广东省,04代表广州市,10代表白云区

8. 缺失值填充,对不同类型的数据填充不同类型的值

3. 特征工程

3.1 特征构造

1. 基础特征:原始特征(广告信息 媒体信息 用户信息 上下文信息)

2. One-hot:将类别特征离散化

由于最后融合三套代码的结果,所以有的代码进行了one-hot,有的没有这样做,而是直接labelencoder。

3. user_tags多值特征:因为包含用户的属性信息,所以完美的表达user_tags至关重要,提取有效属性,减少冗余

从下图可以看出user_tags中标签属性的重要性分布情况

4. 统计特征:

统计特征我们用的都是常规操作,如count、ratio、nunique和ctr相关特征。

count:一维+二维count计数特征(如广告主id共计投放次数)

# 对交叉特征的求count
# add cross feature
first_feature = ['app_cate_id', 'f_channel', 'app_id']
second_feature = ["make", "model", "osv1", "osv2", "osv3", "adid", "advert_name", "campaign_id", "creative_id",
                  "carrier", "nnt", "devtype", "os"]
cross_feature = []
for feat_1 in first_feature:
    for feat_2 in second_feature:
        col_name = "cross_" + feat_1 + "_and_" + feat_2
        cross_feature.append(col_name)
        data[col_name] = data[feat_1].astype(str).values + '_' + data[feat_2].astype(str).values
# 求count计数特征

ratio:类别偏好的ratio比例特征(如广告主id的某个广告id投放比例)

# 这里会考虑所有的组合,当然也可以考虑进行一波特征选择
label_feature = ['advert_id', 'advert_industry_inner', 'advert_name', 'campaign_id', 'creative_height',
                 'creative_tp_dnf', 'creative_width', 'province', 'f_channel',
                 'carrier', 'creative_type', 'devtype', 'nnt',
                 'adid', 'app_id', 'app_cate_id', 'city', 'os', 'orderid', 'inner_slot_id', 'make', 'osv',
                 'os_name', 'creative_has_deeplink', 'creative_is_download', 'hour', 'creative_id', 'model']

mean:用户标签与其他字段的组合mean特征(如广告id对用户性别的投放比例)

nunique: 类别变量的nunique特征(如广告主id有多少个不同的广告id)

# 此处参考@有风的冬
## 广告
adid_nuq = ['model', 'make', 'os', 'city', 'province', 'user_tags', 'f_channel', 'app_id', 'carrier', 'nnt', 'devtype',
            'app_cate_id', 'inner_slot_id']
for feat in adid_nuq:
    gp1 = data.groupby('adid')[feat].nunique().reset_index().rename(columns={feat: "adid_%s_nuq_num" % feat})
    gp2 = data.groupby(feat)['adid'].nunique().reset_index().rename(columns={'adid': "%s_adid_nuq_num" % feat})
    data = pd.merge(data, gp1, how='left', on=['adid'])
    data = pd.merge(data, gp2, how='left', on=[feat])
## 广告主
advert_id_nuq = ['model', 'make', 'os', 'city', 'province', 'user_tags', 'f_channel', 'app_id', 'carrier', 'nnt',
                 'devtype',
                 'app_cate_id', 'inner_slot_id']
for fea in advert_id_nuq:
    gp1 = data.groupby('advert_id')[fea].nunique().reset_index().rename(columns={fea: "advert_id_%s_nuq_num" % fea})
    gp2 = data.groupby(fea)['advert_id'].nunique().reset_index().rename(
        columns={'advert_id': "%s_advert_id_nuq_num" % fea})
    data = pd.merge(data, gp1, how='left', on=['advert_id'])
    data = pd.merge(data, gp2, how='left', on=[fea])
## app_id
app_id_nuq = ['model', 'make', 'os', 'city', 'province', 'user_tags', 'f_channel', 'carrier', 'nnt', 'devtype',
              'app_cate_id', 'inner_slot_id']
for fea in app_id_nuq:
    gp1 = data.groupby('app_id')[fea].nunique().reset_index().rename(columns={fea: "app_id_%s_nuq_num" % fea})
    gp2 = data.groupby(fea)['app_id'].nunique().reset_index().rename(columns={'app_id': "%s_app_id_nuq_num" % fea})
    data = pd.merge(data, gp1, how='left', on=['app_id'])
    data = pd.merge(data, gp2, how='left', on=[fea])

点击率:这里使用的历史点击率,来挖掘历史点击信息,同时防止过拟合

# 和当初baseline所用一样
# add ctr feature
data['period'] = data['day']
data['period'][data['period'] < 27] = data['period'][data['period'] < 27] + 31
for feat_1 in ['advert_id', 'advert_industry_inner', 'advert_name', 'campaign_id', 'creative_height',
               'creative_tp_dnf', 'creative_width', 'province', 'f_channel']:
    res = pd.DataFrame()
    temp = data[[feat_1, 'period', 'click']]
    for period in range(27, 35):
        if period == 27:
            count = temp.groupby([feat_1]).apply(
                lambda x: x['click'][(x['period'] <= period).values].count()).reset_index(name=feat_1 + '_all')
            count1 = temp.groupby([feat_1]).apply(
                lambda x: x['click'][(x['period'] <= period).values].sum()).reset_index(name=feat_1 + '_1')
        else:
            count = temp.groupby([feat_1]).apply(
                lambda x: x['click'][(x['period'] < period).values].count()).reset_index(name=feat_1 + '_all')
            count1 = temp.groupby([feat_1]).apply(
                lambda x: x['click'][(x['period'] < period).values].sum()).reset_index(name=feat_1 + '_1')
        count[feat_1 + '_1'] = count1[feat_1 + '_1']
        count.fillna(value=0, inplace=True)
        count[feat_1 + '_rate'] = round(count[feat_1 + '_1'] / count[feat_1 + '_all'], 5)
        count['period'] = period
        count.drop([feat_1 + '_all', feat_1 + '_1'], axis=1, inplace=True)
        count.fillna(value=0, inplace=True)
        res = res.append(count, ignore_index=True)
    print(feat_1, ' over')
    data = pd.merge(data, res, how='left', on=[feat_1, 'period'])

可以看出这些都是常规操作,如果能够顺利的完成这些就能得到不错的分数

3.2 特征选择

首先推荐学习:特征选择 方法

这里我们主要用了卡方检验和特征重要性,由于三套代码,所有使用的方法并不相同。

user_tags特征我们分别用了卡方检验和特征重要性。

train_new = pd.DataFrame()
test_new = pd.DataFrame()
cntv = CountVectorizer()
cntv.fit(data['user_tags'])
train_a = cntv.transform(train['user_tags'])
test_a = cntv.transform(test['user_tags'])
train_new = sparse.hstack((train_new, train_a), 'csr', 'bool')
test_new = sparse.hstack((test_new, test_a), 'csr', 'bool')
# 卡方检验
SKB = SelectPercentile(chi2, percentile=95).fit(train_new, train_y)
train_new = SKB.transform(train_new)
test_new = SKB.transform(test_new)

3.3 stacking特征

  • 交叉统计特征太多内存不够怎么办?
  • 如何才能在减少特征维度的同时最大限度地保留所有特征的区分度信息?

train_stack = train1 & train2 & train3 & train4 & train5

test_stack = (test1+test2+test3+test4+test5) / 5

# 通过stacking获取新的特征,减少内存的同时,又能保留完整特征的信息
def getStackFeature(df_,seed_):
    skf = StratifiedKFold(n_splits=5,random_state=seed_,shuffle=True)
    train = df_.loc[train_index]
    test = df_.loc[test_index]
    train_user = pd.Series()
    test_user = pd.Series(0,index=list(range(test_x.shape[0])))
    for train_part_index,evals_index in skf.split(train,train_y):
        EVAL_RESULT = {}
        train_part = lgb.Dataset(train.loc[train_part_index],label=train_y.loc[train_part_index])
        evals = lgb.Dataset(train.loc[evals_index],label=train_y.loc[evals_index])
        bst = lgb.train(params_initial,train_part, 
              num_boost_round=NBR, valid_sets=[train_part,evals], 
              valid_names=['train','evals'],early_stopping_rounds=ESR,
              evals_result=EVAL_RESULT, verbose_eval=VBE)
        train_user = train_user.append(pd.Series(bst.predict(train.loc[evals_index]),index=evals_index))
        test_user = test_user+pd.Series(bst.predict(test))
    return train_user,test_user

5 算法模型

  • GBDT模型记忆性更强,记忆特征和标签相关特征组合能力强,因此在小数据集上有很好的结果 。
  • FFM和DeepFFM初期尝试并未得到很好的效果

最终我们选择了XGBoost和LightGBM,得到的结果并做了最终的加权融合。

# 模型参数及五折构造结果
lgb_clf = lgb.LGBMClassifier(boosting_type='gbdt', num_leaves=48, max_depth=-1, learning_rate=0.02, n_estimators=6000, max_bin=425, subsample_for_bin=50000, objective='binary', min_split_gain=0,min_child_weight=5, min_child_samples=10, subsample=0.8, subsample_freq=1,colsample_bytree=0.8, reg_alpha=3, reg_lambda=0.1, seed=1000, n_jobs=-1, silent=True)
skf = list(StratifiedKFold(y_loc_train, n_folds=5, shuffle=True, random_state=1024))
baseloss = []
loss = 0
for i, (train_index, test_index) in enumerate(skf):
    print("Fold", i)
    lgb_model = lgb_clf.fit(X_loc_train[train_index], y_loc_train[train_index],
                            eval_names=['train', 'valid'],
                            eval_metric='logloss',
                            eval_set=[(X_loc_train[train_index], y_loc_train[train_index]),
                                      (X_loc_train[test_index], y_loc_train[test_index])], early_stopping_rounds=100)
    baseloss.append(lgb_model.best_score_['valid']['binary_logloss'])
    loss += lgb_model.best_score_['valid']['binary_logloss']
    test_pred = lgb_model.predict_proba(X_loc_test, num_iteration=lgb_model.best_iteration_)[:, 1]
    print('test mean:', test_pred.mean())
    res['prob_%s' % str(i)] = test_pred
print('logloss:', baseloss, loss / 5)

6 思考总结

  • 由于本次比赛数据中缺乏用户id这一关键信息,用户画像难以得到清晰地建立,因此如何充分挖掘用户标签中所包含的信息至关重要。
  • 即使是同样的业务场景,在不同的数据收集背景下,同样的特征完全可能会起到完全相反的效果,这也是一种数据陷阱。
  • 匿名化数据需要对数据进行充分理解分析,甚至可以尝试根据业务理解进行反编码,这样能够为特征工程指明方向。
  • 建模过程中充分考虑了用户标签与其他信息的交互作用,并采用Stacking抽取特征信息的方式减少维度与内存的使用,对广告与用户交互信息的充分挖掘,也使得模型在AB榜测试相对稳定。
  • 模型缺乏差异性和创新性,最开始尝试过deepffm,由于效果一般而没有坚持改进,大部分精力放在了数据理解与特征挖掘上。

写在最后

竞赛社区(数据竞赛的一站式服务

近期我们公众号和国内的开源组织Datawhale还有杰少一起成立了一个数据竞赛知识星球,并且邀请了国内的很多知名实战高手和赛圈的大佬,在推出的三天中也已经有了500多的用户报名,如果你真的对实战感兴趣而且希望好好学习的话,欢迎通过扫描下面的二维码进行报名,这样可以帮助您省下9元的报名费用,

知识星球嘉宾(部分)

范晶晶:开源组织Datawhale创始人

张 杰:南京大学LAMDA硕士,天池数据科学家,KDD2019全球亚军

谈志旋:北京大学硕士,社交app算法负责人

刘 洋:在读博士,IJCAI/KDD/ICME等顶会比赛前三,天池数据科学家

钱 乾:资深算法工程师,Kaggle Grand Master

数据竞赛群

为了将热爱机器学习的大家聚在一起,推荐大家一个“数据竞赛”交流学习群,进群可与行业top级人物交流,可获得很强势的各方资源,大家有需要的可以进群哦

一年半的竞赛经历,收获了两冠四亚一季的成绩。在这一年半,不仅坚持比赛,同时也坚持不断的分享。在我看来,分享是一个自我总结的一个过程。当然,这也是我与更多选手交流的一个平台,是一个相互学习提升的机会。愿我的分享能够帮助到你。

知乎专栏目的传播更多机器学习干货,数据竞赛方法。欢迎投稿!

ML理论&实践zhuanlan.zhihu.com图标数与码zhuanlan.zhihu.com图标

路漫漫其修远兮,吾将上下而求索。

编辑于 2019-08-23

文章被以下专栏收录