冠军代码开源地址:
bettenW/2018-iFLYTEK-Marketing-Algorithms-Competition-Finals-Rank1github.com
写在前面
首先很幸运能够拿到这次冠军,有两位大佬队友是这次获胜的关键,再次感谢鹏哥和阿水。
同时希望我的分享与总结能给大家带来些许帮助,并且一起交流学习。
接下来将会呈现ppt内容和部分代码
- 赛题分析
- 探索性分析(EDA代码)
- 数据预处理
- 特征工程
- 算法模型
- 思考总结
1.赛题分析
本次大赛提供了讯飞AI营销云的海量广告投放数据,参赛选手通过人工智能技术构建预测模型预估用户的广告点击概率,即给定广告点击相关的广告、媒体、用户、上下文内容等信息的条件下预测广告点击概率。
这又是一道关于CTR的问题,对于CTR问题而言,广告是否被点击的主导因素是用户,其次是广告信息。所以我们要做的是充分挖掘用户及用户行为信息,然后才是广告主、广告等信息。
赛题特征:广告信息、媒体信息、用户信息、上下文信息
提供数据:共1001650初赛数据 和 1998350条复赛数据(复赛训练数据为初赛数据和复赛数据)
评估指标:通过logloss评估模型效果(越小越好),公式如下:
其中N表示测试集样本数量,
表示测试集中第i个样本的真实标签,
表示第i个样本的预估转化率。这类评估函数常用logloss和AUC,简单的说logloss更关注和观察数据的吻合程度,AUC更关注rank order。如果是按照概率期望来进行收费投放的话就用logloss,如果定投一定量就用AUC,主要还是和业务相关。
2. 探索性分析
这一部分将会对部分数据进行分析,另外获取每个类别特征的转化率分布情况判断特征效果,看分布可以有一个很好的初步验证作用。
不同时刻的曝光量和点击率变化,将一天分成4个时段的曝光量和点击率情况
代码语言:javascript复制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);
代码语言:javascript复制hourDF = train_df.groupby(['hour_seg', 'click'])['hour_seg'].count().unstack('click').fillna(0)
hourDF[[0,1]].plot(kind='bar', stacked=True);
训练集正负样本比例,大约为1:4,应该经过了降采样。
代码语言:javascript复制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()
广告的长宽是很重要的特征,正负样本中关于这两个特征的分布存在较为明显的区别。
代码语言:javascript复制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. 数据预处理
由于数据噪音比较多,所以细致的预处理能够是模型更具泛化性,同时挖掘更多特征。
- 初复赛训练数据合并后去重(7361条)
- 提取广告投放时间信息,日期、小时以及早中晚时间段
0-6>--1 | 7-12>--2 | 13-18>--3 | 19-24>--4
- 细分广告主行业与媒体广告位,去除只有一个取值的字段
102400_102401>--102400 102401
- 清洗手机品牌和机型字段,对同类进行合并
iphone>--apple | redmi>--xiaomi | honor>--huawei
- 对操作系统及其版本、名称进行更细粒度的刻画
5.1.1>--5 1 1 | 6.0.1>--6 0 1
- 构造虚拟用户组别,对用户标签和其他类别特征进行编码
- 对city特征进行切分,如
框内为身份证前六位,51代表广东省,04代表广州市,10代表白云区
- 缺失值填充,对不同类型的数据填充不同类型的值
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共计投放次数)
代码语言:javascript复制# 对交叉特征的求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投放比例)
代码语言:javascript复制# 这里会考虑所有的组合,当然也可以考虑进行一波特征选择
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)
代码语言:javascript复制# 此处参考@有风的冬
## 广告
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])
点击率:这里使用的历史点击率,来挖掘历史点击信息,同时防止过拟合
代码语言:javascript复制# 和当初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特征我们分别用了卡方检验和特征重要性。
代码语言:javascript复制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
代码语言:javascript复制# 通过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,得到的结果并做了最终的加权融合。
代码语言:javascript复制# 模型参数及五折构造结果
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,由于效果一般而没有坚持改进,大部分精力放在了数据理解与特征挖掘上。