图灵邦联视频点击预测大赛TOP方案学习


比赛来源:图灵邦联 视频点击预测大赛

感谢大佬开源TOP1方案:

视频点击大赛TOP3

视频点击大赛TOP5

本博客只是单纯的对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模型

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值