第一次参加数据竞赛是什么体验?面对30万用户行为数据和36万篇新闻文章的庞大规模,很多新手的第一反应可能是"无从下手"。别担心,这篇指南将带你从零开始,用Python一步步拆解天池新闻推荐大赛的Baseline代码。我们不仅会跑通整个流程,更重要的是理解每个环节的设计逻辑,以及你可能遇到的典型报错和解决方案。
在开始之前,确保你的Python环境已安装以下核心库:
python复制# 基础数据处理
pip install pandas numpy scipy
# 进度显示
pip install tqdm
# 序列化存储
pip install pickle-mixin
注意:建议使用Python 3.7+版本,避免某些库的兼容性问题。如果遇到SSL证书错误,可以尝试添加--trusted-host pypi.org --trusted-host files.pythonhosted.org参数。
为避免路径混乱导致文件读取失败,建议按以下结构组织项目:
code复制/news_recommend/
├── data_raw/ # 原始数据
│ ├── train_click_log.csv
│ ├── testA_click_log.csv
│ ├── articles.csv
│ └── articles_emb.csv
├── tmp_results/ # 中间结果
└── baseline.ipynb # 代码文件
常见踩坑点:
os.path.join()构造路径使用Pandas的memory_usage()方法查看数据内存占用:
python复制import pandas as pd
def peek_data(filepath):
df = pd.read_csv(filepath)
print(f"Shape: {df.shape}")
print(f"Memory usage: {df.memory_usage().sum()/1024**2:.2f} MB")
return df.head(2)
peek_data('./data_raw/train_click_log.csv')
输出示例:
code复制Shape: (3000000, 8)
Memory usage: 45.67 MB
原始数据默认使用64位存储,但实际数据范围往往更小。reduce_mem函数通过动态检测数据范围,降级数据类型实现内存节省:
python复制def reduce_mem(df):
numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64']
for col in df.columns:
col_type = df[col].dtypes
if col_type in numerics:
c_min = df[col].min()
c_max = df[col].max()
if str(col_type)[:3] == 'int': # 整数类型处理
if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
df[col] = df[col].astype(np.int8)
# 其他int类型判断...
else: # 浮点类型处理
if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:
df[col] = df[col].astype(np.float16)
# 其他float类型判断...
return df
优化效果对比表:
| 数据类型 | 内存占用(字节) | 适用数值范围 |
|---|---|---|
| int64 | 8 | -2^63 ~ 2^63-1 |
| int32 | 4 | -2^31 ~ 2^31-1 |
| int8 | 1 | -128 ~ 127 |
| float64 | 8 | 双精度浮点 |
| float16 | 2 | 半精度浮点 |
对于内存有限的开发环境,可以使用采样模式快速验证代码:
python复制def get_all_click_sample(data_path, sample_nums=10000):
all_click = pd.read_csv(data_path + 'train_click_log.csv')
sample_users = np.random.choice(all_click.user_id.unique(), size=sample_nums)
return all_click[all_click['user_id'].isin(sample_users)]
提示:调试阶段建议设置sample_nums=5000,完整运行时再改为None使用全量数据
构建用户-物品-时间的交互字典是推荐系统的基石:
python复制def get_user_item_time(click_df):
click_df = click_df.sort_values('click_timestamp')
user_item_time = click_df.groupby('user_id').apply(
lambda x: list(zip(x['click_article_id'], x['click_timestamp']))
).to_dict()
return user_item_time
生成的字典结构示例:
json复制{
"用户A": [(文章1, 时间戳1), (文章2, 时间戳2),...],
"用户B": [(文章3, 时间戳3),...]
}
核心公式:$sim(i,j) = \frac{|N(i) \cap N(j)|}{\sqrt{|N(i)| \cdot |N(j)|}}$
其中$N(i)$表示喜欢物品i的用户集合,代码实现加入时间衰减因子:
python复制def itemcf_sim(df):
user_item_time = get_user_item_time(df)
i2i_sim = {}
item_cnt = defaultdict(int)
for user, items in user_item_time.items():
for i, time_i in items:
item_cnt[i] += 1
i2i_sim.setdefault(i, {})
for j, time_j in items:
if i == j: continue
# 加入时间衰减因子
time_weight = 1/(1 + abs(time_i - time_j)/3600)
i2i_sim[i][j] = i2i_sim[i].get(j,0) + time_weight/math.log(len(items)+1)
# 归一化处理
for i, related_items in i2i_sim.items():
for j in related_items:
i2i_sim[i][j] /= math.sqrt(item_cnt[i]*item_cnt[j])
return i2i_sim
相似度矩阵存储技巧:
python复制import pickle
pickle.dump(i2i_sim, open('./tmp_results/itemcf_i2i_sim.pkl', 'wb'))
# 加载时使用
i2i_sim = pickle.load(open('./tmp_results/itemcf_i2i_sim.pkl', 'rb'))
基于物品协同过滤的召回核心逻辑:
python复制def item_based_recommend(user_id, user_item_time, i2i_sim, sim_topk=10, recall_num=10, hot_items=[]):
user_hist = [i for i,_ in user_item_time[user_id]]
item_rank = {}
# 基于相似物品扩展
for hist_item in user_hist:
for related_item, sim_score in sorted(i2i_sim[hist_item].items(),
key=lambda x:x[1], reverse=True)[:sim_topk]:
if related_item in user_hist:
continue
item_rank[related_item] = item_rank.get(related_item,0) + sim_score
# 热门物品兜底
if len(item_rank) < recall_num:
for i, item in enumerate(hot_items):
if item not in item_rank and item not in user_hist:
item_rank[item] = -i-100 # 确保排在正常推荐之后
if len(item_rank) == recall_num:
break
return sorted(item_rank.items(), key=lambda x:x[1], reverse=True)[:recall_num]
符合比赛要求的CSV格式化输出:
python复制def format_submission(recall_df):
recall_df['rank'] = recall_df.groupby('user_id')['pred_score'].rank(ascending=False)
submit = recall_df[recall_df['rank'] <= 5].pivot(
index='user_id', columns='rank', values='click_article_id'
).reset_index()
submit.columns = ['user_id'] + [f'article_{i}' for i in range(1,6)]
return submit
典型错误处理:
user_id和article_1到article_5的列使用joblib加速相似度计算:
python复制from joblib import Parallel, delayed
def parallel_sim_calc(user_items, i2i_sim):
def _update_sim(i, items):
for j in items:
if i != j:
i2i_sim[i][j] = i2i_sim[i].get(j,0) + 1/math.log(len(items)+1)
Parallel(n_jobs=4)(
delayed(_update_sim)(i, items)
for user, items in user_items.items()
for i in items
)
当新数据到来时,只需更新受影响的部分相似度:
python复制def incremental_update(new_interactions, i2i_sim):
for user, (new_item, time) in new_interactions.items():
for hist_item in user_histories[user]:
# 更新新物品与历史物品的相似度
i2i_sim[new_item][hist_item] = i2i_sim[new_item].get(hist_item,0) + 1/math.log(len(user_histories[user])+1)
i2i_sim[hist_item][new_item] = i2i_sim[new_item][hist_item] # 对称更新
return i2i_sim
MRR(Mean Reciprocal Rank)的Python实现:
python复制def calculate_mrr(submit, ground_truth):
rr = []
for user in ground_truth:
for rank, (_, article) in enumerate(submit[user], 1):
if article == ground_truth[user]:
rr.append(1/rank)
break
else:
rr.append(0)
return np.mean(rr)
特征工程增强:
模型融合策略:
实时性优化:
在本地测试时,可以先将测试集分为验证集和测试集,用验证集调整参数后再在真正的测试集上运行。实践中发现,加入用户最近点击的类别偏好特征,能使MRR提升约15%。