写在前面
"该赛道的数据集强调电商推荐系统的公平性,尤其是流量较少的广大中小商家所面临的“有好货缺无人问津”的困境。数据横跨十余天,中间还穿插了某次全网促销活动,涵盖了一些商品从上新时无人问津、到逐渐成为高潜力爆款的历程。欲获胜的队伍需格外关注曝光不足的商品上的推荐准确度,需探索“如何抵消掉历史点击数据的选择性偏差以便避免只推爆款”、“如何注意数据分布随时间的变化以便及时发现高潜力冷门好货”、“如何利用多模态图文商品信息来辅助商品冷启动”等重要课题。"--解题关键
首先感谢青禹小生的开源,本文是在其基础上进行更多的细化, 也将对更多优化方向和建模思路进行简单介绍。
关联商品打分
代码语言:javascript复制def get_sim_item(df_, user_col, item_col, use_iif=False):
df = df_.copy()
user_item_ = df.groupby(user_col)[item_col].agg(list).reset_index()
user_item_dict = dict(zip(user_item_[user_col], user_item_[item_col]))
user_time_ = df.groupby(user_col)['time'].agg(list).reset_index() # 引入时间因素
user_time_dict = dict(zip(user_time_[user_col], user_time_['time']))
sim_item = {}
item_cnt = defaultdict(int) # 商品被点击次数
for user, items in tqdm(user_item_dict.items()):
for loc1, item in enumerate(items):
item_cnt[item] = 1
sim_item.setdefault(item, {})
for loc2, relate_item in enumerate(items):
if item == relate_item:
continue
t1 = user_time_dict[user][loc1] # 点击时间提取
t2 = user_time_dict[user][loc2]
sim_item[item].setdefault(relate_item, 0)
if not use_iif:
if loc1-loc2>0:
sim_item[item][relate_item] = 1 * 0.7 * (0.8**(loc1-loc2-1)) * (1 - (t1 - t2) * 10000) / math.log(1 len(items)) # 逆向
else:
sim_item[item][relate_item] = 1 * 1.0 * (0.8**(loc2-loc1-1)) * (1 - (t2 - t1) * 10000) / math.log(1 len(items)) # 正向
else:
sim_item[item][relate_item] = 1 / math.log(1 len(items))
sim_item_corr = sim_item.copy() # 引入AB的各种被点击次数
for i, related_items in tqdm(sim_item.items()):
for j, cij in related_items.items():
sim_item_corr[i][j] = cij / ((item_cnt[i] * item_cnt[j]) ** 0.2)
return sim_item_corr, user_item_dict
这里在原有基础上考虑了两点因素,关联位置因素和关联时间因素。强关联的发生是有向的、有位置的和有时间的。
- 比如我们先买了手机,那下一次买手机壳的关联,和先买手机壳再买手机的关联,这两种很明显,A到B大于B到A,这是有向性;
- 我先买了手机,然后买了手机壳,又买了耳机,很明显,手机和手机壳的关联性大于手机与耳机的关联性,这是位置性;
- 那么如果再加上时间这层因素,时间相隔越远的关联性肯定是不高的。
这三点因素就可以组成我们的优化思路,有向性打分*位置打分*时间打分,得到最终关联打分。
交互行为打分
代码语言:javascript复制def recommend(sim_item_corr, user_item_dict, user_id, top_k, item_num):
rank = {}
interacted_items = user_item_dict[user_id]
interacted_items = interacted_items[::-1]
for loc, i in enumerate(interacted_items):
for j, wij in sorted(sim_item_corr[i].items(), reverse=True)[0:top_k]:
if j not in interacted_items:
rank.setdefault(j, 0)
rank[j] = wij * (0.7**loc)
return sorted(rank.items(), key=lambda d: d[1], reverse=True)[:item_num]
这里的优化也很符合我们的主观认识,距离下次点击月近的行为,相关性越接近,所有可以根据位置远近考虑重要性,添加权重因子。当然还可以添加时间权重因子。
优化方向
切勿陷入思维定势,也许我的优化方向和baseline会使大家产生一个误区。就像之前安泰杯的比赛,决赛中评委也说到了baseline,很大程度影响的大家的思维方向。没有说baseline不好,而这只能作为无数解题思路中的一小部分,一个分支。不是沿着这个分支走下去,而是去创造更多分支。这就如同多路召回,之将其当作融合的一部分罢了。
目前只是从关联的角度解决问题,这个角度可以做的更细。也可以考虑其它方向,目前大家做的还都是起点,而不是终点。向量召回、模型召回,以及后面的排序都需要进一步尝试。
建模思路
没有相关经验的同学可能会问正负样本怎么来,那其实很简单,需要我们去构造label。这里分为两步:
- 样本提取。我们线下验证的时候,一般是用户最后一次点击进行验证,会进行召回50个商品,然后观察召回率。这样的50个商品及对应的用户就是样本数据。
- 样本打标。召回的50个商品中是最后一次点击的商品labael是1,反之为0。
这样我们就能得到训练集,测试集构造方式一样,只不过需要去预测其label,最后将label的概率进行排序,top50就是最终建模得到的结果。
为了防止数据泄露,构造特征时一定要用历史的点击行为进行构造。最后我们就可以像一般的二分类问题进行解题了。
完整代码
代码语言:javascript复制import pandas as pd
from tqdm import tqdm
from collections import defaultdict
import math
# fill user to 50 items
def get_predict(df, pred_col, top_fill):
top_fill = [int(t) for t in top_fill.split(',')]
scores = [-1 * i for i in range(1, len(top_fill) 1)]
ids = list(df['user_id'].unique())
fill_df = pd.DataFrame(ids * len(top_fill), columns=['user_id'])
fill_df.sort_values('user_id', inplace=True)
fill_df['item_id'] = top_fill * len(ids)
fill_df[pred_col] = scores * len(ids)
df = df.append(fill_df)
df.sort_values(pred_col, ascending=False, inplace=True)
df = df.drop_duplicates(subset=['user_id', 'item_id'], keep='first')
df['rank'] = df.groupby('user_id')[pred_col].rank(method='first', ascending=False)
df = df[df['rank'] <= 50]
df = df.groupby('user_id')['item_id'].apply(lambda x: ','.join([str(i) for i in x])).str.split(',', expand=True).reset_index()
return df
now_phase = 4
train_path = './data/underexpose_train'
test_path = './data/underexpose_test'
recom_item = []
whole_click = pd.DataFrame()
for c in range(now_phase 1):
print('phase:', c)
click_train = pd.read_csv(train_path '/underexpose_train_click-{}.csv'.format(c), header=None, names=['user_id', 'item_id', 'time'])
click_test = pd.read_csv(test_path '/underexpose_test_click-{}.csv'.format(c,c), header=None, names=['user_id', 'item_id', 'time'])
all_click = click_train.append(click_test)
whole_click = whole_click.append(all_click)
whole_click = whole_click.drop_duplicates(subset=['user_id','item_id','time'],keep='last')
whole_click = whole_click.sort_values('time')
item_sim_list, user_item = get_sim_item(whole_click, 'user_id', 'item_id', use_iif=False)
for i in tqdm(click_test['user_id'].unique()):
rank_item = recommend(item_sim_list, user_item, i, 500, 500)
for j in rank_item:
recom_item.append([i, j[0], j[1]])
# find most popular items
top50_click = whole_click['item_id'].value_counts().index[:50].values
top50_click = ','.join([str(i) for i in top50_click])
recom_df = pd.DataFrame(recom_item, columns=['user_id', 'item_id', 'sim'])
result = get_predict(recom_df, 'sim', top50_click)
result.to_csv('baseline.csv', index=False, header=None)