冠军代码开源地址:
bettenW/2018-iFLYTEK-Marketing-Algorithms-Competition-Finals-Rank1github.com
首先很幸运能够拿到这次冠军,有两位大佬队友是这次获胜的关键,再次感谢鹏哥和阿水。
同时希望我的分享与总结能给大家带来些许帮助,并且一起交流学习。
接下来将会呈现ppt内容和部分代码
本次大赛提供了讯飞AI营销云的海量广告投放数据,参赛选手通过人工智能技术构建预测模型预估用户的广告点击概率,即给定广告点击相关的广告、媒体、用户、上下文内容等信息的条件下预测广告点击概率。
这又是一道关于CTR的问题,对于CTR问题而言,广告是否被点击的主导因素是用户,其次是广告信息。所以我们要做的是充分挖掘用户及用户行为信息,然后才是广告主、广告等信息。
赛题特征:广告信息、媒体信息、用户信息、上下文信息
提供数据:共1001650初赛数据 和 1998350条复赛数据(复赛训练数据为初赛数据和复赛数据)
评估指标:通过logloss评估模型效果(越小越好),公式如下:
其中N表示测试集样本数量,
表示测试集中第i个样本的真实标签,
表示第i个样本的预估转化率。这类评估函数常用logloss和AUC,简单的说logloss更关注和观察数据的吻合程度,AUC更关注rank order。如果是按照概率期望来进行收费投放的话就用logloss,如果定投一定量就用AUC,主要还是和业务相关。
这一部分将会对部分数据进行分析,另外获取每个类别特征的转化率分布情况判断特征效果,看分布可以有一个很好的初步验证作用。
不同时刻的曝光量和点击率变化,将一天分成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的用户最多,而这两种机型的用户点击率也高于其他手机的用户
由于数据噪音比较多,所以细致的预处理能够是模型更具泛化性,同时挖掘更多特征。
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
框内为身份证前六位,51代表广东省,04代表广州市,10代表白云区
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
最终我们选择了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)