
简介这份资源用Python实现了基于物品与基于用户两种协同过滤推荐算法面向推荐系统入门者、进阶学习者以及需要完成课程设计、大作业或毕设项目的同学帮助理解相似度计算、邻居选择与评分预测等核心环节。压缩包共4个文件包含2个py脚本分别对应Item_CF与User_CF两套算法实现1个csv数据文件用于读取用户-物品评分矩阵另有1个gitignore配置文件整体约6KB结构精简便于快速定位与调试。目前已有449人学习下载可作为推荐算法入门的参考范例。读者可从中获得两种协同过滤思路的完整代码框架、数据组织方式与算法落地流程并在此基础上自行调整相似度度量、补充功能或迁移到自己的数据集适合具备一定Python基础、能独立调试代码的学习者参考使用。1. 从零手写协同过滤为什么我不建议你直接调库很多做推荐系统的朋友第一次接触协同过滤都是被“调个 surprise 库三行出结果”带进门的。但真到了业务里你会发现库封得太死相似度算法想换、冷启动想加规则、评分矩阵想稀疏化处理处处掣肘。我见过太多人拿着现成 API 跑通 MovieLens 就以为掌握了推荐结果一上真实数据就翻车——用户量一上来内存直接爆掉。所以这篇笔记我打算用 Python 从零把基于物品的协同过滤和基于用户的协同过滤各实现一遍不依赖任何推荐专用库只用 numpy 和 pandas。你会看到相似度矩阵怎么算、邻居怎么选、评分怎么预测以及两个算法在什么场景下该选谁。适合已经会 Python 基础语法、想真正搞懂推荐算法内部运转逻辑的工程师也适合正在做校园项目、电商 demo 需要落地推荐模块的同学。读完你手里会有一套能直接跑、能改参数、能接自己数据的代码骨架。2. 基于用户的协同过滤找和你口味相似的人2.1 核心逻辑与相似度选型基于用户的协同过滤英文叫 User-Based Collaborative Filtering思路非常直白如果用户 A 和用户 B 对一批物品的历史评分高度一致那 A 喜欢但 B 没看过的物品就可以推给 B。这里的关键是“高度一致”怎么量化。常见做法是余弦相似度、皮尔逊相关系数、修正余弦相似度三种。余弦相似度只看向量夹角对评分绝对值不敏感皮尔逊会减去用户平均分能抵消不同用户打分尺度差异——有人习惯全打 4 分有人习惯全打 2 分皮尔逊能把这层偏差去掉。我一般会优先用皮尔逊因为真实评分数据里用户偏置太常见了。但皮尔逊有个坑如果两个用户只共同评过一个物品相关系数无法计算分母为零。所以工程上要么设最小共同评分阈值要么回退到余弦。下面这段代码构建用户-物品评分矩阵并计算用户间皮尔逊相似度import numpy as np import pandas as pd from sklearn.metrics.pairwise import cosine_similarity # 构造示例评分数据user_id, item_id, rating data { user_id: [1,1,1,2,2,2,3,3,3,4,4,4], item_id: [101,102,103,101,102,104,101,103,104,102,103,104], rating: [5,3,4,4,2,5,2,4,3,3,5,4] } df pd.DataFrame(data) # 构建用户-物品评分矩阵缺失值填0 rating_matrix df.pivot_table( indexuser_id, columnsitem_id, valuesrating ).fillna(0) print(评分矩阵形状:, rating_matrix.shape) print(rating_matrix) # 皮尔逊相似度对每行用户向量计算相关系数 user_sim np.corrcoef(rating_matrix) user_sim_df pd.DataFrame( user_sim, indexrating_matrix.index, columnsrating_matrix.index ) print(\n用户相似度矩阵:) print(user_sim_df.round(3))这段代码里pivot_table把长表转成宽表行是用户、列是物品缺失评分填 0。注意填 0 不是“评了 0 分”而是“没评过”后续预测时要靠掩码把未评分位置排除掉。np.corrcoef直接对矩阵行向量算皮尔逊系数得到对称相似度矩阵。参数上fillna(0)是最粗暴的做法数据稀疏时会导致大量零向量参与计算相似度失真。更稳的做法是只对共同评分物品计算后面避坑章节会展开。2.2 邻居选择与评分预测拿到相似度矩阵后预测用户 u 对物品 i 的评分公式是取 u 最相似的 K 个用户中评过 i 的那些人用他们的评分做加权平均权重就是相似度。K 的选择很讲究K 太小邻居代表性不够预测抖动大K 太大会把不相似的人也拉进来稀释信号。经验值在 20 到 50 之间数据量大可以到 100。另外要设一个相似度下限比如只取相似度大于 0.3 的邻居否则负相关或接近零的用户会污染结果。def predict_user_cf(user_id, item_id, rating_matrix, user_sim_df, K3): 基于用户的协同过滤评分预测 if user_id not in rating_matrix.index: return rating_matrix[item_id].mean() # 冷启动回退 # 取出该用户与其他用户的相似度排除自己 sims user_sim_df[user_id].drop(user_id) # 只保留评过目标物品的用户 rated_users rating_matrix[rating_matrix[item_id] 0].index sims sims[sims.index.isin(rated_users)] # 按相似度降序取前K个 top_k sims.sort_values(ascendingFalse).head(K) if len(top_k) 0 or top_k.sum() 0: return rating_matrix[item_id].mean() # 加权平均 numerator sum( top_k[u] * rating_matrix.loc[u, item_id] for u in top_k.index ) denominator top_k.abs().sum() return numerator / denominator # 预测用户1对物品104的评分 pred predict_user_cf(1, 104, rating_matrix, user_sim_df, K3) print(f用户1对物品104的预测评分: {pred:.2f})函数先做冷启动判断如果用户不在矩阵里直接返回物品全局均分。然后从相似度序列里排除自身再筛出对目标物品有评分的人。top_k.abs().sum()用绝对值求和是为了处理负相似度的情况否则正负抵消会导致分母异常。这里 K3 只是演示真实场景要交叉验证调参。注意rating_matrix[item_id] 0这个条件依赖填 0 的约定如果评分范围包含 0 分需要改用掩码矩阵单独记录“是否评分”。3. 基于物品的协同过滤找和你买过的东西相似的物品3.1 物品相似度为什么通常比用户相似度更稳基于物品的协同过滤Item-Based CF逻辑是如果物品 A 和物品 B 被同一批用户喜欢那喜欢 A 的人大概率也会喜欢 B。它和 User-Based 最大的区别在于相似度矩阵的维度——用户数通常远大于物品数而且用户兴趣会漂移今天喜欢数码明天可能看母婴但物品之间的关联相对稳定。所以工业界推荐系统里Item-Based 的占比远高于 User-Based。亚马逊早年那篇经典论文就是把 Item-to-Item 推上台面的。计算物品相似度时我一般用修正余弦相似度因为它能减去用户平均评分消除用户打分偏置。公式是在余弦基础上每个评分先减去该用户对所有物品的平均分。# 构建物品-用户矩阵评分矩阵转置 item_user_matrix rating_matrix.T print(物品-用户矩阵形状:, item_user_matrix.shape) # 修正余弦先减用户均分 user_means rating_matrix.replace(0, np.nan).mean(axis1) adjusted rating_matrix.copy() for u in rating_matrix.index: mask rating_matrix.loc[u] 0 adjusted.loc[u, mask] rating_matrix.loc[u, mask] - user_means[u] # 转置后计算物品间余弦相似度 item_sim cosine_similarity(adjusted.T) item_sim_df pd.DataFrame( item_sim, indexrating_matrix.columns, columnsrating_matrix.columns ) print(\n物品相似度矩阵:) print(item_sim_df.round(3))这里先把评分矩阵转成物品-用户视角然后对每个用户的评分减去他的平均分得到修正后的矩阵。replace(0, np.nan).mean(axis1)只对真实评分求均值避免 0 拉低平均数。修正后再转置算余弦得到物品间相似度。参数上cosine_similarity默认按行计算所以要先转置让物品成为行向量。如果数据量很大这个全量相似度矩阵会占 O(n²) 内存后面会讲怎么用稀疏矩阵和 Top-N 截断来优化。3.2 基于物品相似度的推荐生成预测用户 u 对物品 i 的评分时Item-Based 的做法是找到 i 最相似的 K 个物品看用户 u 对这些相似物品的评分用相似度加权平均。和 User-Based 的区别在于这里遍历的是物品邻居而不是用户邻居。好处是物品相似度矩阵可以离线算好、定期更新线上只做查表和加权响应速度极快。def predict_item_cf(user_id, item_id, rating_matrix, item_sim_df, K3): 基于物品的协同过滤评分预测 if item_id not in rating_matrix.columns: return rating_matrix.loc[user_id].replace(0, np.nan).mean() # 取目标物品与其他物品的相似度 sims item_sim_df[item_id].drop(item_id) # 只保留该用户评过分的物品 user_rated rating_matrix.loc[user_id] rated_items user_rated[user_rated 0].index sims sims[sims.index.isin(rated_items)] top_k sims.sort_values(ascendingFalse).head(K) if len(top_k) 0 or top_k.sum() 0: return user_rated.replace(0, np.nan).mean() numerator sum( top_k[i] * user_rated[i] for i in top_k.index ) denominator top_k.abs().sum() return numerator / denominator # 预测用户2对物品103的评分 pred_item predict_item_cf(2, 103, rating_matrix, item_sim_df, K3) print(f用户2对物品103的预测评分: {pred_item:.2f}) # 给用户2生成Top-N推荐 def recommend_top_n(user_id, rating_matrix, item_sim_df, N2, K3): 给用户生成Top-N推荐列表 unrated rating_matrix.columns[rating_matrix.loc[user_id] 0] scores {} for item in unrated: scores[item] predict_item_cf( user_id, item, rating_matrix, item_sim_df, K ) ranked sorted(scores.items(), keylambda x: x[1], reverseTrue) return ranked[:N] recs recommend_top_n(2, rating_matrix, item_sim_df, N2) print(用户2的Top-2推荐:, recs)predict_item_cf的结构和 User-Based 版本对称只是把“相似用户”换成“相似物品”。recommend_top_n遍历用户未评分的物品逐个预测打分再排序。这里有个性能隐患如果物品有上万件每次推荐都要循环调用预测函数线上肯定扛不住。常见优化是离线预计算物品相似度 Top-K 列表线上只对候选集打分。参数 K 控制邻居数量N 控制推荐条数两个要分开调。4. 两个算法怎么选场景、数据量和效果对比4.1 用户维度与物品维度的成本账选 User-Based 还是 Item-Based第一笔账是计算成本。假设平台有 M 个用户、N 个物品。User-Based 要算 M×M 的用户相似度矩阵Item-Based 要算 N×N 的物品相似度矩阵。电商场景里 M 通常是 N 的几十倍甚至上百倍所以 Item-Based 的矩阵小得多内存和计算都更省。第二笔账是更新频率新用户不断注册用户相似度矩阵需要频繁重算但物品上新速度相对慢物品相似度可以一天甚至一周更新一次。第三笔账是解释性Item-Based 能直接说“因为你买了 A所以推荐相似的 B”用户容易理解User-Based 说“和你相似的人还买了 C”解释链路更长。对比维度User-Based CFItem-Based CF相似度矩阵规模用户数²物品数²更新频率高用户增长快低物品相对稳定冷启动表现新用户差新物品差可解释性较弱较强适用场景用户少、物品多用户多、物品少线上响应需实时算邻居可离线预计算这张表不是绝对的。我见过用户量只有几千的垂直社区User-Based 效果反而更好因为用户兴趣集中、共同评分多。所以选型前先看数据稀疏度共同评分比例低于 1% 时两个算法都容易退化得先做降维或引入内容特征。4.2 用同一份数据跑通两套流程为了让你直观对比我用同一份评分数据把两个算法的推荐结果都跑出来。下面代码把前面的函数串起来输出每个用户的推荐列表# 对每个用户分别用两种算法生成推荐 for uid in rating_matrix.index: user_recs recommend_top_n(uid, rating_matrix, item_sim_df, N2) print(f\n用户{uid} 的 Item-Based 推荐: {user_recs}) # User-Based 推荐生成 def recommend_user_cf(user_id, rating_matrix, user_sim_df, N2, K3): unrated rating_matrix.columns[rating_matrix.loc[user_id] 0] scores {} for item in unrated: scores[item] predict_user_cf( user_id, item, rating_matrix, user_sim_df, K ) ranked sorted(scores.items(), keylambda x: x[1], reverseTrue) return ranked[:N] for uid in rating_matrix.index: user_recs recommend_user_cf(uid, rating_matrix, user_sim_df, N2) print(f用户{uid} 的 User-Based 推荐: {user_recs})跑完你会发现两个算法给出的推荐列表可能有重叠也可能完全不同。重叠部分通常是数据里信号最强的关联差异部分则反映两个算法的偏好Item-Based 更偏向推荐与用户历史物品相似的User-Based 更偏向推荐相似用户群里的热门物品。实际业务里可以做成融合推荐各取 Top-N 后按权重合并去重。参数 N 和 K 需要根据业务指标调比如点击率、转化率不能只凭离线 RMSE 决定。5. 避坑与排查协同过滤落地时最容易翻车的五个点5.1 稀疏矩阵填 0 导致相似度失真现象相似度矩阵里大量用户或物品之间相似度接近 0 或 NaN推荐结果随机。原因评分矩阵极度稀疏填 0 后零向量参与余弦或皮尔逊计算分母被拉大真实关联被淹没。解决不要全局填 0改用掩码矩阵记录有效评分相似度只对共同评分项计算。或者用 scipy 稀疏矩阵存储计算时只遍历非零元素。5.2 热门物品霸榜推荐列表现象不管什么用户推荐结果里总是那几个爆款。原因热门物品被评分次数多与任何物品的共同评分都高相似度天然偏大。解决在相似度计算时对热门物品做惩罚比如除以物品评分人数的对数或者用 TF-IDF 思路降低高频物品权重。也可以在推荐排序阶段加多样性重排。5.3 新用户和新物品冷启动无解现象新注册用户没有评分记录推荐接口返回空或全站热门。原因协同过滤完全依赖历史行为没有行为就没有向量。解决新用户走热门推荐或引导选择兴趣标签用内容特征做过渡新物品用属性相似度找邻居或者给少量曝光收集初始评分。工程上常见做法是混合推荐协同过滤只负责老用户老物品。5.4 相似度矩阵内存溢出现象用户量到十万级时程序 OOM 崩溃。原因全量相似度矩阵是稠密的十万用户就是 100 亿个浮点数内存根本放不下。解决用稀疏矩阵只存 Top-K 邻居或者用矩阵分解降维后再算相似度。线上服务一般只加载目标用户的邻居列表不加载全量矩阵。5.5 离线指标好线上效果差现象离线 RMSE 很低上线后点击率没涨甚至跌。原因离线评估用的是已评分数据但线上推荐的是未评分物品存在选择偏差而且离线没考虑位置、时效、多样性。解决离线只做粗筛线上做 A/B 测试关注点击率、转化率、停留时长等业务指标。推荐列表里混入一定比例的新物品做探索。6. 进阶技巧用稀疏矩阵和 Top-K 截断把性能压下来前面代码为了可读性用了稠密矩阵真实数据上必须换稀疏存储。scipy 的csr_matrix可以只存非零评分内存占用跟评分数量成正比而不是用户数乘物品数。相似度计算也可以用sklearn的pairwise_distances配合稀疏输入或者自己写只遍历共同评分项的循环。另一个技巧是 Top-K 截断算完相似度后每个物品只保留最相似的 K 个邻居其余置零这样相似度矩阵也变成稀疏的。K 一般取 20 到 100具体看数据密度。from scipy.sparse import csr_matrix # 转稀疏矩阵 sparse_matrix csr_matrix(rating_matrix.values) print(稀疏矩阵非零元素数:, sparse_matrix.nnz) print(稠密矩阵元素数:, rating_matrix.size) print(内存压缩比:, rating_matrix.size / sparse_matrix.nnz) # Top-K 截断示例每个物品只保留相似度最高的K个邻居 def top_k_similarity(sim_df, K2): result pd.DataFrame(0, indexsim_df.index, columnssim_df.columns) for idx in sim_df.index: row sim_df.loc[idx].drop(idx) top_k row.nlargest(K) result.loc[idx, top_k.index] top_k.values return result truncated_sim top_k_similarity(item_sim_df, K2) print(\n截断后的物品相似度矩阵:) print(truncated_sim.round(3))csr_matrix把稠密矩阵转成压缩稀疏行格式nnz是非零元素个数。示例数据里压缩比可能不明显但真实场景评分矩阵稀疏度通常在 95% 以上压缩比能到几十倍。top_k_similarity函数遍历每个物品只保留相似度最高的 K 个邻居其余填零。这样后续预测时只查这几个邻居计算量大幅下降。注意截断会损失一部分长尾关联K 太小推荐会变得保守需要根据业务容忍度调。我自己的习惯是任何推荐算法上线前先用稀疏矩阵跑一遍全量数据看内存和耗时能不能接受再决定要不要上分布式。协同过滤不是银弹但它是最容易理解、最容易解释、最容易改的推荐基线。把这套代码吃透后面上矩阵分解或深度模型时你才知道每一步在优化什么。希望帮到你。本文还有配套的精品资源点击获取