比赛来源:图灵邦联 视频点击预测大赛
感谢大佬开源TOP1方案:
本博客只是单纯的对TOP方案进行整理学习,以便后期参考学习。
赛题
要求
希望通过用户行为数据,用户特征,以及视频特征,可以在充足数据基础上精准的推荐给用户喜欢的视频类型。训练集总共给出了从2019.11.08 - 2019.11.10的三天数据,需要预测2019.11.11的用户点击行为。
数据
1、train.csv
id:代表数据集的第几条数据,从1到11376681。
target:代表该视频是否被用户点击了,1代表点击,0代表未点击。
timestamp:代表改用户点击改视频的时间戳,如果未点击则为NULL。
deviceid:用户的设备id。 newsid:视频的id。 二者都是数据的唯一标识。
guid:用户的注册id
pos:视频推荐位置。
app_version:app版本。
device_vendor:设备厂商。
netmodel:网络类型。
osversion:操作系统版本。
lng:经度。
lat:维度。
device_version:设备版本。
ts:视频暴光给用户的时间戳。
2、app.csv
deviceid:用户设备id。
applist:用户所拥有的app,我们已将app的名字设置成了app_1,app_2…的形式。
3、user.csv
deviceid:用户设备id。
guid:用户注册id。
outertag:用户画像用|分隔,冒号后面的数字代表对该标签的符合程度,分数越高代表该标签越符合该用户。
tag:同outertag。
level:用户等级。
personidentification:1表示劣质用户 0表示正常用户。
followscore:徒弟分(好友分)。
personalscore:个人分。
gender:性别。
数据探索
正负样本分布很不平衡:
各个pos位,数据分布:
说明123推荐位置位于app首页,其余的是位于视频详情页的pos。
TOP3
解决方案只总结特征工程部分。
穿越特征
1、计算视频曝光的时间差
理由:假如一个人点击了某个视频,那么必然会观看一段时间,那么距离下一次视频的曝光就会久一点,ts 差值也较大。
时间戳转成时间
# 排序
df_feature = df_train.sort_values(['deviceid', 'ts']).reset_index().drop('index', axis=1)
# 时间戳转时间
df_feature['ts_datetime'] = df_feature['ts'] + 8 * 60 * 60 * 1000
# df_feature['ts_datetime'] = df_feature['ts']
df_feature['ts_datetime'] = pd.to_datetime(df_feature['ts_datetime'], unit='ms')
df_feature['day'] = df_feature['ts_datetime'].dt.day
df_feature['hour'] = df_feature['ts_datetime'].dt.hour
df_feature['minute'] = df_feature['ts_datetime'].dt.minute
df_feature['minute10'] = (df_feature['minute'] // 10) * 10
df_feature['hourl'] = df_feature['day'] * 24 + df_feature['hour']
df_feature['hourl'] = df_feature['hourl'] - df_feature['hourl'].min()
对每个deviceid当前视频曝光时间与上一次视频曝光时间的时间间隔,并对其进行log平滑:
group = df_feature.groupby('deviceid')
df_feature['ts_before'] = group['ts'].diff()
df_feature['ts_before'] = df_feature['ts_before'].fillna(3 * 60 * 1000)
df_feature['ts_before'] = np.log(df_feature['ts_before'] // 1000 + 1)
对每个deviceid当前视频曝光时间与下一次视频曝光时间的时间间隔,并对其进行log平滑:
df_feature['ts_after'] = diff(-1)
df_feature['ts_after'] = df_feature['ts_after'].fillna(3 * 60 * 1000)
df_feature['ts_after'] = np.log(df_feature['ts_after'] // 1000 + 1)
2、下一次(上一次曝光) pos
理由:假如不是直接在首页点击播放视频,而是点击进入该视频的详情页,同样会触发视频观看,同时在视频详情页下会出现相关推荐视频,pos 中某些取值对应于相关推荐位,所以当下次视频曝光位置为上述相关推荐位,则表示当次视频一定是被用户点击观看的。(注:pos取值1-8,其中1-3是指视频首页的推荐位,其余的是详情页的推荐)
下一次(上一次)曝光视频pos
# 下一次 pos
df_feature['before_pos'] = df_feature.groupby(['deviceid'])['pos'].shift(1)
# 上一次 pos
df_feature['next_pos'] = df_feature.groupby(['deviceid'])['pos'].shift(-1)
# 两个pos差
df_feature['diff_pos'] = df_feature['next_pos'] - df_feature['pos']
3、下一次视频曝光的网络环境和基于经纬度的位置变化。
# 每个deviceid两次视频曝光的距离变化
df_feature['next_lat'] = df_feature.groupby(['deviceid'])['lat'].shift(-1)
df_feature['next_lng'] = df_feature.groupby(['deviceid'])['lng'].shift(-1)
df_feature['dist_diff'] = (df_feature['next_lat'] - df_feature['lat']
) ** 2 + (df_feature['lng'] - df_feature['next_lng']) ** 2
del df_feature['next_lat']
del df_feature['next_lng']
# 下一次 网络
df_feature['next_netmodel'] = df_feature.groupby(['deviceid'])[
'netmodel'].shift(-1)
历史特征
历史特征主要用过去一个时间单位的数据进行统计,然后作为当前时刻的特征。
构造了前一天数据统计特征。涉及到各种点击率的构造,构造前一天点击率避免了标签泄露和数据穿越问题。
1、数据有两个时间特征,一个是视频曝光时间 ts 和视频假如被点击时的点击时间 timestamp。二者的差值可以表示用户的反应时间。反应时间越短说明用户越喜欢该类视频。针对反应时间分别以 deviceid 和 newsid 为单位构造统计特征,构造方式包括:max,min,mean,std,median,kurt 和 quantile。反应时间从用户侧提取,所以针对 newsid 做统计误差较大,效果不明显,所以只保留了 std 统计。
统计前一天有点击行为的deviceid的视频点击时间-视频曝光时间的统计量:mean,max,std,media,kurtosis等。
# 对前一天的样本的所有反应时间进行统计量提取
df_temp = df_feature[df_feature['target'] == 1]
# 视频点击时间-视频曝光时间
df_temp['click_minus'] = df_temp['timestamp'] - df_temp['ts']
col = 'deviceid'
col2 = 'click_minus'
df_temp = df_temp.groupby([col, 'day'], as_index=False)[col2].agg({
'yesterday_{}_{}_max'.format(col, col2): 'max',
'yesterday_{}_{}_mean'.format(col, col2): 'mean',
'yesterday_{}_{}_min'.format(col, col2): 'min',
'yesterday_{}_{}_std'.format(col, col2): 'std',
'yesterday_{}_{}_median'.format(col, col2): 'median',
'yesterday_{}_{}_kurt'.format(col, col2): kurtosis,
'yesterday_{}_{}_q3'.format(col, col2): lambda x: np.quantile(x, q=0.75),
})
df_temp['day'] += 1
df_feature = df_feature.merge(df_temp, on=[col, 'day'], how='left')
del df_temp
gc.collect()
统计前一天有点击行为的newsid的视频击时间-视频曝光时间的统计量:mean,max,std,media,kurtosis等。
# 对前一天的 newsid 所有反应时间进行统计量提取
df_temp = df_feature[df_feature['target'] == 1]
df_temp['click_minus'] = df_temp['timestamp'] - df_temp['ts']
col = 'newsid'
col2 = 'click_minus'
df_temp = df_temp.groupby([col, 'day'], as_index=False)[col2].agg({
'yesterday_{}_{}_std'.format(col, col2): 'std',
})
df_temp['day'] += 1
df_feature = df_feature.merge(df_temp, on=[col, 'day'], how='left')
del df_temp
gc.collect()
2、每个deviceid前一天的点击次数,点击率
# 昨日 deviceid 点击次数,点击率
col = 'deviceid'
df_temp = df_feature.groupby([col, 'day'], as_index=False)['target'].agg({
'yesterday_{}_click_count'.format(col): 'sum',
'yesterday_{}_count'.format(col): 'count',
})
df_temp['yesterday_{}_ctr'.format(col)] = df_temp['yesterday_{}_click_count'.format(col)] \
/ df_temp['yesterday_{}_count'.format(col)]
df_temp['day'] += 1
del df_temp['yesterday_{}_count'.format(col)]
df_feature = df_feature.merge(df_temp, on=[col, 'day'], how='left')
del df_temp
gc.collect()
3、每个deviceid前一天每个小时的点击次数,点击率
# 昨日小时点击率
groups = ['deviceid', 'hour']
df_temp = df_feature.groupby(groups + ['day'], as_index=False)['target'].agg({
'yesterday_{}_click_count'.format('_'.join(groups)): 'sum',
'yesterday_{}_count'.format('_'.join(groups)): 'count',
})
df_temp['yesterday_{}_ctr'.format('_'.join(groups))] = df_temp['yesterday_{}_click_count'.format('_'.join(groups))] \
/ df_temp['yesterday_{}_count'.format('_'.join(groups))]
df_temp['day'] += 1
del df_temp['yesterday_{}_click_count'.format('_'.join(groups))]
del df_temp['yesterday_{}_count'.format('_'.join(groups))]
df_feature = df_feature.merge(df_temp, on=groups + ['day'], how='left')
del df_temp
gc.collect()
4、每个deviceid前一天的曝光 pos 平均值
# 昨日曝光 pos 平均值
col = 'deviceid'
df_temp = df_feature.groupby([col, 'day'], as_index=False)['pos'].agg({
'yesterday_{}_pos_mean'.format(col): 'mean',
})
df_temp['day'] += 1
df_feature = df_feature.merge(df_temp, on=[col, 'day'], how='left')
del df_temp
gc.collect()
5、每个deviceid前一天的 netmodel 点击率
# 昨日 deviceid netmodel 点击率
groups = ['deviceid', 'netmodel']
df_temp = df_feature.groupby(groups + ['day'], as_index=False)['target'].agg({
'yesterday_{}_click_count'.format('_'.join(groups)): 'sum',
'yesterday_{}_count'.format('_'.join(groups)): 'count',
})
df_temp['yesterday_{}_ctr'.format('_'.join(groups))] = df_temp['yesterday_{}_click_count'.format('_'.join(groups))] \
/ df_temp['yesterday_{}_count'.format('_'.join(groups))]
df_temp['day'] += 1
df_feature = df_feature.merge(df_temp, on=groups + ['day'], how='left')
df_feature['yesterday_deviceid_netmodel_click_ratio'] = df_feature['yesterday_deviceid_netmodel_click_count'] / \
df_feature['yesterday_deviceid_click_count']
del df_feature['yesterday_{}_click_count'.format('_'.join(groups))]
del df_feature['yesterday_{}_count'.format('_'.join(groups))]
del df_temp
gc.collect()
6、 前一天newsid 点击次数,点击率
# 昨日 newsid 点击次数,点击率
col = 'newsid'
df_temp = df_feature.groupby([col, 'day'], as_index=False)['target'].agg({
'yesterday_{}_click_count'.format(col): 'sum',
'yesterday_{}_count'.format(col): 'count',
})
df_temp['yesterday_{}_ctr'.format(col)] = df_temp['yesterday_{}_click_count'.format(col)] \
/ df_temp['yesterday_{}_count'.format(col)]
df_temp['day'] += 1
del df_temp['yesterday_{}_count'.format(col)]
df_feature = df_feature.merge(df_temp, on=[col, 'day'], how='left')
del df_temp
gc.collect()
7、前一天next_pos点击率
# 昨日 next_pos 点击率
col = 'next_pos'
df_temp = df_feature.groupby([col, 'day'], as_index=False)['target'].agg({
'yesterday_{}_click_count'.format(col): 'sum',
'yesterday_{}_count'.format(col): 'count',
})
df_temp['yesterday_{}_ctr'.format(col)] = df_temp['yesterday_{}_click_count'.format(col)] \
/ df_temp['yesterday_{}_count'.format(col)]
df_temp['day'] += 1
del df_temp['yesterday_{}_count'.format(col)]
del df_temp['yesterday_{}_click_count'.format(col)]
df_feature = df_feature.merge(df_temp, on=[col, 'day'], how='left')
del df_temp
gc.collect()
8、deviceid和netmodel的计数特征
cat_list = tqdm([['deviceid', 'netmodel']])
for f1, f2 in cat_list:
df_feature['t_{}_count'.format(f1)] = df_feature.groupby([f1, 'day'])[
'id'].transform('count')
df_feature['t_{}_count'.format(f2)] = df_feature.groupby([f2, 'day'])[
'id'].transform('count')
df_feature['t_{}_count'.format('_'.join([f1, f2]))] = df_feature.groupby([
f1, f2, 'day'])['id'].transform('count')
df_feature['{}_coratio'.format('_'.join([f1, f2]))] = (df_feature['t_{}_count'.format(
f1)] * df_feature['t_{}_count'.format(f2)]) / df_feature['t_{}_count'.format('_'.join([f1, f2]))]
df_feature['yesterday_{}_coratio'.format('_'.join([f1, f2]))] = df_feature.groupby(
[f1, f2, 'day'])['{}_coratio'.format('_'.join([f1, f2]))].shift()
del df_feature['t_{}_count'.format(f1)]
del df_feature['t_{}_count'.format(f2)]
del df_feature['t_{}_count'.format('_'.join([f1, f2]))]
del df_feature['{}_coratio'.format('_'.join([f1, f2]))]
gc.collect()
9,交叉特征
# 类别交叉特征
df_feature['devicevendor_osv'] = df_feature['device_vendor'].astype(
'str') + '_' + df_feature['osversion'].astype('str')
10,以小时为单位的统计特征
# 三天内,一小时之前 deviceid 点击次数,点击率
col = 'deviceid'
df_temp = df_feature.groupby([col, 'hourl'], as_index=False)['id'].agg({
'pre_hour_{}_count'.format(col): 'count',
})
df_temp['hourl'] += 1
df_feature = df_feature.merge(df_temp, on=[col, 'hourl'], how='left')
del df_temp
gc.collect()
计数count统计特征
以不同时间单位进行 count 统计,时间单位包括:10分钟,小时,天,全局。不同于历史特征,这部分 count 统计往往会出现数据穿越问题。count 特征主要反应偏好属性,具有一定曝光度的作用,比如今天哪个视频曝光量大,用户倾向于在哪个时间,哪个网络环境下使用 APP。
1、分别以10分钟,小时,天,全局为单位,对某些特征进行计数。
cat_list = [['deviceid'], ['guid'], ['newsid'], ['deviceid', 'pos'], ['newsid', 'pos'],
['deviceid', 'guid', 'newsid'], ['deviceid', 'next_pos']]
# 分组计数
for f in tqdm(cat_list):
df_feature['{}_day_count'.format('_'.join(f))] = df_feature.groupby([
'day'] + f)['id'].transform('count')
cat_list = [['deviceid'], ['guid'], [ 'deviceid', 'pos'], ['deviceid', 'netmodel']]
for f in tqdm(cat_list):
df_feature['{}_minute10_count'.format('_'.join(f))] = df_feature.groupby(
['day', 'hour', 'minute10'] + f)['id'].transform('count')
cat_list = [['deviceid', 'netmodel']]
for f in tqdm(cat_list):
df_feature['{}_hour_count'.format('_'.join(f))] = df_feature.groupby([
'hourl'] + f)['id'].transform('count')
cat_list = [['deviceid', 'group', 'pos']]
for f in tqdm(cat_list):
df_feature['{}_count'.format('_'.join(f))] = df_feature.groupby(f)[
'id'].transform('count')
2、求出每个deviceid的ts_before上一次曝光时间和ts_after后一次曝光时间的统计特征
col = 'deviceid'
df_temp = df_feature.groupby([col], as_index=False)['ts_before'].agg({
'{}_ts_before_mean'.format(col): 'mean',
'{}_ts_before_std'.format(col): 'std'
})
df_feature = df_feature.merge(df_temp, on=col, how='left')
del df_temp
gc.collect()
col = 'deviceid'
df_temp = df_feature.groupby([col], as_index=False)['ts_after'].agg({
'{}_ts_after_mean'.format('deviceid'): 'mean',
'{}_ts_after_std'.format('deviceid'): 'std',
'{}_ts_after_median'.format('deviceid'): 'median',
'{}_ts_after_skew'.format('deviceid'): 'skew',
})
df_feature = df_feature.merge(df_temp, on=col, how='left')
del df_temp
gc.collect()
3、未来一小时 deviceid, netmodel 曝光数量
cat_list = [['deviceid', 'netmodel']]
for f in tqdm(cat_list):
df_feature['temp'] = df_feature.groupby(
['hourl'] + f)['id'].transform('count')
df_feature['next_{}_hour_count'.format('_'.join(f))] = df_feature.groupby(f)[
'temp'].shift(-1)
del df_feature['temp']
ts相关特征
1、每个deviceid 前(后)x次曝光到当前的时间差
sort_df = df_feature.sort_values('ts').reset_index(drop=True)
for f in [['deviceid','netmodel']]:
tmp = sort_df.groupby(f)
# 前x次曝光到当前的时间差
for gap in tqdm([2, 3, 4, 5, 8, 10, 20, 30]):
sort_df['{}_prev{}_exposure_ts_gap'.format(
'_'.join(f), gap)] = tmp['ts'].shift(0) - tm
p['ts'].shift(gap)
tmp2 = sort_df[
f + ['ts', '{}_prev{}_exposure_ts_gap'.format('_'.join(f), gap)]
].drop_duplicates(f + ['ts']).reset_index(drop=True)
df_feature = df_feature.merge(tmp2, on=f + ['ts'], how='left')
del tmp2, sort_df, tmp
gc.collect()
sort_df = df_feature.sort_values('ts').reset_index(drop=True)
for f in [['deviceid']]:
tmp = sort_df.groupby(f)
# 后x次曝光到当前的时间差
for gap in tqdm([2, 3, 4, 5, 8, 10, 20, 30, 50]):
sort_df['{}_next{}_exposure_ts_gap'.format(
'_'.join(f), gap)] = tmp['ts'].shift(-gap) - tmp['ts'].shift(0)
tmp2 = sort_df[
f + ['ts', '{}_next{}_exposure_ts_gap'.format('_'.join(f), gap)]
].drop_duplicates(f + ['ts']).reset_index(drop=True)
df_feature = df_feature.merge(tmp2, on=f + ['ts'], how='left')
del tmp2, sort_df, tmp
gc.collect()
2、后x次曝光到当前的时间差
sort_df = df_feature.sort_values('ts').reset_index(drop=True)
for f in [['pos', 'deviceid']]:
tmp = sort_df.groupby(f)
# 后x次曝光到当前的时间差
for gap in tqdm([1, 2]):
sort_df['{}_next{}_exposure_ts_gap'.format(
'_'.join(f), gap)] = tmp['ts'].shift(-gap) - tmp['ts'].shift(0)
tmp2 = sort_df[
f + ['ts', '{}_next{}_exposure_ts_gap'.format('_'.join(f), gap)]
].drop_duplicates(f + ['ts']).reset_index(drop=True)
df_feature = df_feature.merge(tmp2, on=f + ['ts'], how='left')
del tmp2, sort_df, tmp
gc.collect()
3、
lng_lat ‘pos’, ‘netmodel’ 后x次曝光到当前的时间差
user 表特征
以视频为单位,找出被推荐的用户列表,每个列表作为句子喂到 Word2Vec 模型得到每个用户的 embedding 向量,用视频所有被推荐的用户的 embedding 向量平均值表示视频,从而刻画部分用户群特征。得到 embedding 之后,就可以度量两个视频之间的相似度,所以也就产生了另外一种思路,当一个新视频被推荐给用户后,计算新视频与之前用户被推荐过(或者看过)的视频的平均相似度,平均相似度越大,相似用户群点击的可能性越大。
1、刻画用户基本特征
df_tag = df_user[['deviceid', 'tag']].copy()
node_pairs = []
# 将每个id的标签分数提取出来,存到dataframe中:'deviceid', 'tag', 'score'。
for item in tqdm(df_user[['deviceid', 'tag']].values):
deviceid = str(item[0])
tags = item[1]
if type(tags) != float:
tags = tags.split('|')
for tag in tags:
try:
key, value = tag.split(':')
except Exception:
pass
node_pairs.append([deviceid, key, value])
df_tag = pd.DataFrame(node_pairs)
df_tag.columns = ['deviceid', 'tag', 'score']
df_tag['score'] = df_tag['score'].astype('float')
# 计算分数的统计特征。
df_temp = df_tag.groupby(['deviceid'])['score'].agg({'tag_score_mean': 'mean',
'tag_score_std': 'std',
'tag_score_count': 'count',
'tag_score_q2': lambda x: np.quantile(x, q=0.5),
'tag_score_q3': lambda x: np.quantile(x, q=0.75),
}).reset_index()
df_feature = df_feature.merge(df_temp, how='left')
del df_temp
del df_tag
gc.collect()
2、embedding
from gensim.models import Word2Vec
# df_feature,'newsid', 'deviceid'
def emb(df, f1, f2):
emb_size = 16
print('====================================== {} {} ======================================'.format(f1, f2))
# 对newsid 进行分组,得到每个newsid对应的多个deviceid,将这些deviceid组成的预料sentences输入word2vec进行训练。得到模型。
tmp = df.groupby(f1, as_index=False)[f2].agg(
{'{}_{}_list'.format(f1, f2): list})
sentences = tmp['{}_{}_list'.format(f1, f2)].values.tolist()
del tmp['{}_{}_list'.format(f1, f2)]
for i in range(len(sentences)):
sentences[i] = [str(x) for x in sentences[i]]
model = Word2Vec(sentences, size=emb_size, window=5,
min_count=5, sg=0, hs=1, seed=2019)
emb_matrix = []
# 根据模型将其转为词向量。
for seq in sentences:
vec = []
for w in seq:
if w in model:
vec.append(model[w])
if len(vec) > 0:
emb_matrix.append(np.mean(vec, axis=0))
else:
emb_matrix.append([0] * emb_size)
# 生成dataframe : newsid,sentence(相对应的deviceid的向量表示),并返回
df_emb = pd.DataFrame(emb_matrix)
df_emb.columns = ['{}_{}_emb_{}'.format(
f1, f2, i) for i in range(emb_size)]
tmp = pd.concat([tmp, df_emb], axis=1)
del model, emb_matrix, sentences
return tmp
将df_feature表中’deviceid’, ‘newsid’, 以及二者的交叉特征作为语料,训练word2vec模型
将df_feature表中’lng’, ‘lat’, 以及二者的交叉特征作为语料,训练word2vec模型