Kaggle&天池-新闻推荐Baseline解析
Keep Team Lv4

一、赛题理解

1.1 赛题简介

​ 此次比赛是新闻推荐场景下的用户行为预测挑战赛, 该赛题是以新闻APP中的新闻推荐为背景, 目的是要求我们根据用户历史浏览点击新闻文章的数据信息预测用户未来的点击行为, 即用户的最后一次点击的新闻文章, 这道赛题的设计初衷是引导大家了解推荐系统中的一些业务背景, 解决实际问题。

1.2 数据概况

​ 该数据来自某新闻APP平台的用户交互数据,包括30万用户,近300万次点击,共36万多篇不同的新闻文章,同时每篇新闻文章有对应的embedding向量表示。为了保证比赛的公平性,从中抽取20万用户的点击日志数据作为训练集,5万用户的点击日志数据作为测试集A,5万用户的点击日志数据作为测试集B。具体数据表和参数, 大家可以参考赛题说明。下面说一下拿到这样的数据如何进行理解, 来有效的开展下一步的工作。

1.3评价方式理解

​ 理解评价方式, 我们需要结合着最后的提交文件来看, 根据sample.submit.csv, 我们最后提交的格式是针对每个用户, 我们都会给出五篇文章的推荐结果,按照点击概率从前往后排序。 而真实的每个用户最后一次点击的文章只会有一篇的真实答案, 所以我们就看我们推荐的这五篇里面是否有命中真实答案的。比如对于user1来说, 我们的提交会是:

user1, article1, article2, article3, article4, article5.

二、基于ItemCF的Baseline

2.1导包

1
2
3
4
5
6
7
8
9
10
11
import time, math, os 
from tqdm import tqdm # 加载进度条
import pickle #序列化与反序列化,用于存储和加载数据
import random
from datetime import datetime
import numpy as np
import pandas as pd
import warnings
from collections import defaultdict #简化字典操作
import collections
warnings.filterwarnings('ignore')

2.2 下载数据文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
articles_url = 'http://tianchi-competition.oss-cn-hangzhou.aliyuncs.com/531842/articles.csv'
articles_emb_url = 'http://tianchi-competition.oss-cn-hangzhou.aliyuncs.com/531842/articles_emb.csv'
testA_click_url = 'http://tianchi-competition.oss-cn-hangzhou.aliyuncs.com/531842/testA_click_log.csv'
train_click_url = 'http://tianchi-competition.oss-cn-hangzhou.aliyuncs.com/531842/train_click_log.csv'

data_urls = [articles_url, articles_emb_url, testA_click_url, train_click_url]
files_path = ["articles.csv","articles_emb.csv","testA_click_log.csv","train_click_log.csv"]

for i in range(len(data_urls)):
response = requests.get(data_urls[i])
if response.status_code == 200:
with open(f"./data/{files_path[i]}", "wb") as f:
f.write(response.content)
print("文件下载成功")
else:
print("文件下载失败")

# 设置数据路径和保存路径
data_path = "./data/"
save_path = "./save/"

2.3读取采样和全量数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def get_all_click_sample(data_path,sample_nums=10000):
'''
训练集采样一部分数据调试
data_path: 原数据的存储路径
sample_nums: 采样数据数量
'''
all_click = pd.read_csv(data_path + 'train_click_log.csv')
all_users_ids = all_click.user_id.unique()

sample_user_ids = np.random.choice(all_users_ids,size=sample_nums,replace=False)
all_click = all_click[all_click["user_id"].isin(sample_user_ids)]

all_click = all_click.drop_duplicates(['user_id','click_article_id','click_timestamp'])
return all_click

1、读取训练集文件

2、获取所有不同的user_id

3、使用np.random.choice函数在所有的id中选择sample_nums个样本id,replace=False表示选择不可重复

4、在所有点击中选择所有采样出来的user_id所在行。

5、drop_duplicates()保证参数列表是唯一的。(相当于主键)

1
2
3
4
5
6
7
8
9
10
11
12
13

def get_all_click_df(data_path='data',offline=True):
if offline:
all_click = pd.read_csv(data_path + 'train_click_log.csv')
else:
trn_click = pd.read_csv(data_path + 'train_click_log.csv')
tst_click = pd.read_csv(data_path + 'testA_click_log.csv')
all_click = pd.concat([trn_click,tst_click])
all_click = all_click.drop_duplicates(['user_id','click_article_id','click_timestamp'])
return all_click

all_click_df = get_all_click_df(data_path,offline=False)
all_click_df.head()

读取线上或线下数据

2.4获取用户-文章-时间的字典

1
2
3
4
5
6
7
8
9
10
def get_user_item_time(click_df):
click_df = click_df.sort_values('click_timestamp')

def make_item_time_pair(df):
return list(zip(df['click_article_id'], df['click_timestamp']))

user_item_time_df = click_df.groupby('user_id')[['click_article_id', 'click_timestamp']].apply(lambda x: make_item_time_pair(x))\
.reset_index().rename(columns={0:'item_time_list'})
user_item_time_dict = dict(zip(user_item_time_df['user_id'], user_item_time_df['item_time_list']))
return user_item_time_dict

1、按照时间顺序排序。

2、将数据按照user_id分组,只选择文章id和点击时间两列,对每一个user_id下的两列数据,使用apply()对其表示成list列表后,使用reset_index()重置索引,并且由于物品时间对(article_id,time)的列名默认是0,需要将其rename()改成item_time_list,最后形如:

user_id item_time_list

0 199999 [(160417, 1507029570190), (5408, 1507029571478), ...]

1 199998 [(157770, 1507029532200), (96613, 1507029671831), ...]

3、将user_id和item_time_list改成字典结构并返回

2.5 itemCF的物品相似度计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def itemcf_sim(df):
user_item_time_dict = get_user_item_time(df)
i2i_sim = {}
item_cnt = defaultdict(int)
for user,item_time_list in tqdm(user_item_time_dict.items()):
for i, i_click_time in item_time_list:
item_cnt[i] += 1
i2i_sim.setdefault(i,{})
for j, j_click_time in item_time_list:
if i==j:
continue
i2i_sim[i].setdefault(j,0)
i2i_sim[i][j] += 1/math.log(len(item_time_list) + 1)

i2i_sim_ = i2i_sim.copy()
for i, related_items in i2i_sim_.items():
for j, wij in related_items.items():
i2i_sim_[i][j] = wij / math.sqrt(item_cnt[i] * item_cnt[j])

pickle.dump(i2i_sim_, open(save_path + 'itemcf_i2i_sim.pkl', 'wb'))

return i2i_sim_
i2i_sim = itemcf_sim(all_click_df)

1、先获取用户-文章-时间的字典,第一层循环先遍历字典对形如:

user_id item_time_list

(199999 [(160417, 1507029570190), (5408, 1507029571478), ...])

(199998 [(157770, 1507029532200), (96613, 1507029671831), ...])

2、第二层循环遍历item_time_list列表,其中i表示文章,i_click_time表示点击时间,

每遍历到一个item,给相应的item_cnt[i] 加一。

3、第三层循环遍历item_time_list列表,计算物品ij的相似度(i != j),i2i_sim形如:

1
2
3
4
5
6
i2i_sim = {
101: {102: 0.721, 103: 0.721},
102: {101: 0.721, 103: 1.443, 104: 0.721},
103: {101: 0.721, 102: 1.443, 104: 0.721},
104: {102: 0.721, 103: 0.721}
}

4、遍历相似度矩阵,利用记录到item_cnt进行归一化,并保存到.pkl文件中。

2.6 获取最热门的k个文章id

1
2
3
def get_item_topk_click(click_df,k):
topk_click = click_df['click_article_id'].value_counts().index[:k]
return topk_click

例如点击文章的id为[101, 102, 101, 103, 101, 102]

执行value_counts()结果是

1
2
3
4
101  3
102 2
103 1
dtype: int64

index结果是101 102 103

2.7基于ItemCF做推荐召回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def item_based_recommend(user_id,user_item_time_dict, i2i_sim, sim_item_topk, recall_item_num, item_topk_click):
"""
基于物品的推荐
:user_id: 用户id
:user_item_time_dict: 用户对物品的点击时间字典
:i2i_sim: 物品相似度矩阵
:sim_item_topk: 相似物品topk
:recall_item_num: 召回物品数量
:item_topk_click: 用户点击物品
return: 召回的文章列表 {item1: score1, item2:score2 ...}
"""
user_hist_items = user_item_time_dict[user_id]
user_hist_items_ = {item_id for item_id, _ in user_hist_items}
item_rank = {}
for loc, (i,click_time) in enumerate(user_hist_items):
for j, wij in sorted(i2i_sim[i].items(),key=lambda x: x[1],reverse=True)[:sim_item_topk]:
if j in user_hist_items_:
continue
item_rank.setdefault(j,0)
item_rank[j] += wij

if len(item_rank) < recall_item_num:
for i,item in enumerate(item_topk_click):
if item in item_rank.items():
continue
item_rank[item] = - i - 100
if len(item_rank) == recall_item_num:
break

item_rank = sorted(item_rank.items(),key=lambda x: x[1],reverse=True)[:recall_item_num]
return item_rank #[(item1, score1),(item2, score2),...]

对于输入参数的user_id,推荐recall_item_num个文章返回。

1、user_hist_items 是某个键值为user_idlistuser_hist_items_是所有文章id的集合。

2、遍历列表的每个元组(i, click_time) 找到对应物品i的相似矩阵(字典),并且遍历转换成items()后的元组列表,j表示文章,wij表示相似度,并且要求相关的相似物品按照相似度从大到小排列,取前sim_topk_item个。还要要求不能出现在已知的训练集中,即不在user_hist_items_,最后将与直接交互的文章i相似的物品j的相似度加入item_rank[j]中。

3、遍历完成后,如果推荐的item_rank的长度小于需要recall_item_num的数量,就用推荐的热门文章来补全,并且热门物品不能已经出现在item_rank中,如果不在,则加入item_rank中,并对相似度取负数,意思是在推荐排序中位于最后。如果达到召回要求的长度,直接break

4、最后对结果按照分数大小排序,返回结果

2.8 给每个用户根据物品的协同过滤文章

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
user_recall_items_dict = collections.defaultdict(dict)
user_item_time_dict = get_user_item_time(all_click_df)

i2i_sim = pickle.load(open(save_path + 'itemcf_i2i_sim.pkl','rb'))

sim_item_topk = 10

recall_item_num = 10

item_topk_click = get_item_topk_click(all_click_df, k=50)

for user in tqdm(all_click_df['user_id'].unique()):
user_recall_items_dict[user] = item_based_recommend (user, user_item_time_dict, i2i_sim,sim_item_topk, recall_item_num,item_topk_click)



user_item_score_list = []

for user,items in tqdm(user_recall_items_dict.items()):
for item,score in items:
user_item_score_list.append([user,item,score])

recall_df = pd.DataFrame(user_item_score_list,columns=['user_id','click_article_id','pred_score'])

1、对每个用户做推荐,结果保存在user_recall_items_dict

2、转换成dataframe 的pd格式保存csv中

2.9生成提交文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def submit(recall_df,topk=5,model_name='None'):
recall_df = recall_df.sort_values(by=['user_id','pred_score'])
recall_df['rank'] = recall_df.groupby(['user_id'])['pred_score'].rank(ascending=False,method='first')
tmp = recall_df.groupby('user_id').apply(lambda x: x['rank'].max())
assert tmp.min() >= topk

del recall_df['pred_score']
submit = recall_df[recall_df['rank'] <= topk].set_index(['user_id','rank']).unstack(-1).reset_index()
submit.columns = [int(col) if isinstance(col,int) else col for col in submit.columns.droplevel(0)]
submit = submit.rename(columns={'': 'user_id', 1: 'article_1', 2: 'article_2',
3: 'article_3', 4: 'article_4', 5: 'article_5'})
save_name = save_path + model_name + '_' + datetime.today().strftime('%m-%d') + '.csv'
submit.to_csv(save_name,index=False,header=True)


tst_click = pd.read_csv(data_path + 'testA_click_log.csv')
tst_users = tst_click['user_id'].unique()
tst_recall = recall_df[recall_df['user_id'].isin(tst_users)]
submit(tst_recall, topk=5,model_name='itemcf_baseline')