简介:这份资源面向推荐算法入门与进阶学习者,提供基于Python实现的协同过滤推荐算法代码,涵盖基于物品与基于用户两种经典思路,可作为课程设计、毕业设计、大作业或工程实训的参考项目。压缩包共4个文件,以2个Python脚本为核心,分别对应Item_CF与User_CF两套算法实现,另附一份csv数据文件用于读取与验证推荐结果,以及一个gitignore配置项,整体约6KB,体量轻便,便于快速阅读与本地调试。目前已有449人学习下载,说明其在同类教学资源中具备一定参考价值。读者可借此理解相似度计算、邻居选取与评分预测等关键环节,对照代码梳理两种协同过滤的差异与适用场景,并在此基础上自行调整数据、添加功能或优化实现。需注意代码仅作参考,建议具备Python基础后再进行调试与二次开发。
1. 从一份只有四个文件的压缩包说起:Python 协同过滤到底能跑出什么
打开这个叫Collaborative-Filtering-Pytho.zip的包,里面干净得有点反直觉:User_CF.py、Item_CF.py、一个新建文本文档.csv,再加一个.gitignore。没有 requirements、没有 README、没有 Flask 界面,也没有任何可视化。很多人第一次看到这种结构会犯嘀咕——就这?但恰恰是这种「裸算法」包,最适合拿来把协同过滤的两条主线一次性吃透:基于用户的协同过滤(UserCF)和基于物品的协同过滤(ItemCF)。
它解决的不是「搭一个推荐系统平台」这种大问题,而是让你在一份可读的 Python 代码里,亲眼看到用户相似度矩阵、物品相似度矩阵、评分预测、TopN 推荐这几步是怎么一步步算出来的。适合谁?正在做毕设、课程设计、大作业的学生,或者想从零手写一遍推荐算法、不想一上来就被 Surprise、LightFM 这些库的黑匣子挡住的进阶学习者。下面我按「先跑通、再拆原理、最后避坑」的顺序,把这份资源拆开讲。
2. 环境准备与数据格式:让两个脚本先跑起来
2.1 依赖与 Python 版本选择
这份代码是纯 Python 实现,没有用到深度学习框架,核心依赖基本只有pandas和numpy。常见做法是建一个干净的虚拟环境,避免和你机器上已有的库版本打架。如果你还在纠结 python 安装教程、vscode python 环境配置这些前置问题,先把解释器装好、把编辑器指向正确的解释器路径,再回来跑算法,否则报错会混在一起,排查起来很痛苦。
# 建议 Python 3.8 及以上,3.10/3.11 实测都能跑 python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate pip install pandas numpy逻辑说明:虚拟环境把这份项目的依赖和你系统里的其他项目隔离,pandas负责读 CSV 和做表格运算,numpy负责矩阵和向量化计算。参数上没什么可调的,版本别太老即可。如果你用的是 PyCharm,配置解释器时记得选到venv里的那个 python,而不是系统全局的,否则会出现「命令行能跑、IDE 里报 ModuleNotFoundError」这种经典翻车。
2.2 CSV 数据长什么样,字段怎么对应
包里的新建文本文档.csv是唯一的数据源。协同过滤的输入本质就是「谁 对 什么 打了 几分」这三元组,所以 CSV 通常是三列:用户 ID、物品 ID、评分。不同人拿到的文件列名可能不一样,代码里一般会写死列名或用位置索引读取,这就是第一个要确认的点。
| 列 | 含义 | 典型取值 | 注意 |
|---|---|---|---|
| user_id | 用户唯一标识 | 1, 2, 3… | 不能有重复行冲突 |
| item_id | 物品唯一标识 | 101, 102… | 类型要和代码一致 |
| rating | 用户对物品评分 | 1~5 | 缺失值要提前处理 |
先跑一段探查代码,确认数据能被正确读进来,再动算法:
import pandas as pd # 读取数据,注意编码,中文 Windows 下常见 gbk df = pd.read_csv("新建文本文档.csv", encoding="utf-8") print(df.head()) print(df.shape) print(df.dtypes) print(df["user_id"].nunique(), df["item_id"].nunique())逻辑说明:head()看前几行确认列对齐,shape看数据量,dtypes确认 ID 是不是被读成了 float(一旦有 NaN,pandas 会把整列变 float,后面做字典键会出问题)。参数上encoding是最容易踩的,utf-8 报 UnicodeDecodeError 就换gbk或gb18030。如果列名和代码里写的不一致,要么改 CSV 表头,要么改代码里的列名,别硬扛。
2.3 两个脚本的入口与运行方式
User_CF.py和Item_CF.py是两套独立实现,各自能单独运行。常见做法是直接python User_CF.py,脚本内部读 CSV、算相似度、打印推荐结果。运行前先确认脚本里的文件路径是相对路径还是绝对路径——很多人把脚本和 CSV 放在不同目录,结果一跑就 FileNotFoundError。
python User_CF.py python Item_CF.py逻辑说明:两个脚本互不依赖,可以分别验证。如果只想先看一个,建议从User_CF.py入手,因为「找相似用户」的直觉比「找相似物品」更好理解。运行后如果只打印了相似度矩阵却没打印推荐列表,说明脚本里的推荐输出部分可能被注释掉了,需要自己取消注释或补一段 TopN 逻辑,这在学生作业类代码里非常常见。
3. UserCF 拆解:用户相似度矩阵是怎么算出来的
3.1 用户-物品评分矩阵的构建
UserCF 的第一步是把长表转成宽表,行是用户、列是物品、格子里是评分。这一步决定了后面所有计算的形状。没打分的格子是 NaN,代表「未知」,不是 0——把未知当 0 是新手最常犯的错,会让相似度整体失真。
import pandas as pd import numpy as np df = pd.read_csv("新建文本文档.csv", encoding="utf-8") # 长表转宽表:行=用户,列=物品 user_item = df.pivot_table( index="user_id", columns="item_id", values="rating" ) print(user_item.shape) print(user_item.isna().sum().sum()) # 统计缺失格子数逻辑说明:pivot_table默认对重复的 (user, item) 做均值聚合,如果你的数据里同一用户对同一物品有多条评分,这里会自动合并,避免报错。isna().sum().sum()告诉你矩阵有多稀疏——稀疏度往往在 90% 以上,这正是协同过滤要面对的现实。参数上values必须是评分列,index和columns换成你实际的列名。
3.2 余弦相似度与皮尔逊相似度的选择
用户相似度衡量的是「两个人口味像不像」。常见两种算法:余弦相似度和皮尔逊相关系数。余弦看的是评分向量的方向,皮尔逊先去均值再算相关,能抵消「有人习惯打高分、有人习惯打低分」的偏置。这份代码里通常实现的是其中一种,你需要看懂它用的是哪个。
from numpy.linalg import norm def cosine_sim(u, v): # 只在对两个用户都非空的维度上计算,避免 NaN 污染 mask = ~np.isnan(u) & ~np.isnan(v) if mask.sum() == 0: return 0.0 uu, vv = u[mask], v[mask] denom = norm(uu) * norm(vv) return float(np.dot(uu, vv) / denom) if denom else 0.0 # 对前两个用户算一下 mat = user_item.values print(cosine_sim(mat[0], mat[1]))逻辑说明:关键在mask——只在两人都评过分的物品上算相似度,否则 NaN 会传染整个结果。denom为 0 说明有人一个分都没打,直接返回 0。参数上,如果你换成皮尔逊,就是先对uu、vv各自减均值再算余弦,代码结构一样,只多两行去均值。选哪个?数据量小、评分尺度统一用余弦就够;评分偏置明显时皮尔逊更稳。
3.3 评分预测与 TopN 推荐生成
有了相似用户,就能预测「目标用户对没看过的物品会打几分」:找和他最像的 K 个用户,用他们的评分加权平均。权重就是相似度,相似度越高话语权越大。
def predict_user_cf(user_item, sim_matrix, target_idx, item_idx, k=5): sims = sim_matrix[target_idx].copy() sims[target_idx] = -1 # 排除自己 # 只保留对该物品有评分的用户 rated = ~np.isnan(user_item[:, item_idx]) sims[~rated] = -1 top_k = np.argsort(sims)[-k:] top_k = [i for i in top_k if sims[i] > 0] if not top_k: return np.nan num = sum(sims[i] * user_item[i, item_idx] for i in top_k) den = sum(abs(sims[i]) for i in top_k) return num / den逻辑说明:sims[target_idx] = -1把自己排除,rated过滤掉没评过该物品的用户,argsort取相似度最高的 K 个。num/den是加权平均,分母用绝对值防止负相似度抵消。参数k是邻居数,太小推荐不稳,太大引入不相关的人,一般 5~20 之间调。生成 TopN 时,对目标用户所有未评分物品算预测分,排序取前 N 即可。
4. ItemCF 拆解:物品相似度与推荐的可解释性
4.1 物品-用户矩阵与相似度计算
ItemCF 把矩阵转置过来看:行是物品,列是用户,衡量的是「两个物品被同一批人喜欢的程度」。它比 UserCF 更稳定——物品的数量和属性变化慢,用户口味变化快,所以工业界 ItemCF 用得更广。
# 转置:行=物品,列=用户 item_user = user_item.T item_mat = item_user.values def item_similarity(item_mat): n = item_mat.shape[0] sim = np.zeros((n, n)) for i in range(n): for j in range(i + 1, n): mask = ~np.isnan(item_mat[i]) & ~np.isnan(item_mat[j]) if mask.sum() == 0: continue a, b = item_mat[i][mask], item_mat[j][mask] denom = norm(a) * norm(b) if denom: sim[i, j] = sim[j, i] = float(np.dot(a, b) / denom) return sim逻辑说明:双重循环对每对物品算余弦,mask同样只取共同被评分的用户。sim[j, i] = sim[i, j]保证对称,省一半计算。参数上物品多时这个 O(n²) 会慢,常见优化是先算共现次数、只对共现过的物品对算相似度,能砍掉大量无效计算。
4.2 基于物品相似度的推荐打分
ItemCF 的推荐逻辑更符合直觉:你喜欢物品 A,那就推和 A 最像的物品。预测分是「你评过分的物品」与「候选物品」相似度的加权和。
def recommend_item_cf(user_item, item_sim, target_idx, top_n=5): scores = {} rated_items = np.where(~np.isnan(user_item[target_idx]))[0] for i in rated_items: for j in range(item_sim.shape[0]): if np.isnan(user_item[target_idx, j]): # 只推没评过的 scores[j] = scores.get(j, 0) + item_sim[i, j] * user_item[target_idx, i] ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True) return ranked[:top_n]逻辑说明:外层遍历用户评过分的物品,内层把相似度加权累加到候选物品上,if np.isnan保证不推已经看过的。item_sim[i, j] * user_item[target_idx, i]里,评分高的物品对推荐贡献更大。参数top_n控制返回条数。这套逻辑的好处是可解释——你能直接说「因为你看过 A,而 B 和 A 很像」。
4.3 UserCF 与 ItemCF 的适用边界
两者不是谁替代谁,而是场景不同。下面这张表是我实际选型时会对照的:
| 维度 | UserCF | ItemCF |
|---|---|---|
| 相似度对象 | 用户之间 | 物品之间 |
| 实时性 | 用户变化快,需频繁更新 | 物品稳定,更新慢 |
| 可解释性 | 较弱(相似的人还看了…) | 强(和你看过的很像) |
| 冷启动 | 新用户难,新物品易 | 新物品难,新用户易 |
| 适用场景 | 社交、新闻 | 电商、视频 |
逻辑说明:用户数远小于物品数时 UserCF 计算量小;物品数可控、需要强解释时选 ItemCF。很多毕设会两个都实现做对比,这份资源正好给了两套代码,可以直接跑同一份数据看推荐结果的差异。
5. 避坑与排查:跑不通时先看这几条
5.1 现象:UnicodeDecodeError 读 CSV 直接崩
原因:CSV 是中文 Windows 下用 Excel 存的,默认编码是 gbk,而代码里写的是 utf-8。解决:把encoding改成gbk或gb18030,或者用 Excel 另存为 utf-8 编码的 CSV。别用记事本另存,容易带 BOM,pandas 读出来第一列列名会多个\ufeff。
5.2 现象:相似度全是 0 或全是 NaN
原因:ID 列被读成了 float,或者评分列里有空值导致整列 NaN 传染。解决:读入后用df.dropna()清掉缺失行,用df["user_id"] = df["user_id"].astype(int)强制转整型。如果相似度全 0,检查是不是把「未评分」当成了 0 参与计算。
5.3 现象:推荐结果里出现用户已经看过的物品
原因:生成 TopN 时没有过滤已评分物品,或者过滤条件写反了。解决:在打分阶段加if np.isnan(user_item[target_idx, j])这类判断,确保只对未评分物品排序。这是推荐系统最基本的约束,漏了会让结果看起来很蠢。
5.4 现象:脚本跑得极慢,几分钟没输出
原因:ItemCF 的双重循环在物品数上千时是 O(n²),纯 Python 循环扛不住。解决:先用小样本(比如前 100 个物品)验证逻辑,再考虑用 numpy 向量化或只对共现物品对计算。别一上来就拿全量数据硬跑,容易以为代码坏了。
5.5 现象:两个脚本结果差异巨大,怀疑有一个错了
原因:UserCF 和 ItemCF 本来就是两种视角,结果不同是正常的,不一定是 bug。解决:先确认两者用的是同一份数据、同一个相似度定义,再对比。如果差异大到离谱,检查是不是一个用了余弦、一个用了皮尔逊,或者 K 值、TopN 设置差太多。
6. 进阶技巧:把裸算法改成能验证、能扩展的版本
跑通只是起点。这份代码最大的价值是「结构透明」,你可以直接在它上面做实验。我一般会先加一个离线评估,用留一法把每个用户的一条评分藏起来,看推荐能不能命中,这样调 K 值和相似度算法才有依据,而不是凭感觉。
def evaluate_user_cf(user_item, sim_matrix, k=5): hits, total = 0, 0 for u in range(user_item.shape[0]): rated = np.where(~np.isnan(user_item[u]))[0] if len(rated) < 2: continue # 留一:藏起最后一个评分 test_item = rated[-1] train_row = user_item[u].copy() train_row[test_item] = np.nan # 用训练数据预测 test_item pred = predict_user_cf( np.vstack([user_item[:u], train_row, user_item[u+1:]]), sim_matrix, u, test_item, k ) total += 1 if not np.isnan(pred): hits += 1 return hits / total if total else 0.0逻辑说明:留一法把每个用户最后一条评分当测试集,用剩下的数据预测,命中率就是评估指标。total统计有效用户数,hits是预测成功的次数。参数k可以循环几个值跑一遍,看命中率曲线。注意这里为了演示简化了相似度矩阵的重算,实际用的时候相似度矩阵也要基于训练数据重算,否则有信息泄漏——这是评估里最隐蔽的坑,我第一次做的时候就被它坑过,指标虚高得离谱。
另一个扩展方向是把结果落成 CSV 或接一个轻量网页端。比如用 Flask 起一个接口,输入用户 ID 返回 TopN 推荐,前端简单渲染一下,就能从「命令行脚本」变成「能演示的系统」,毕设答辩时这点很加分。但别本末倒置,先把算法逻辑和评估跑对,界面只是壳。
从那以后我每次拿到这种裸算法包,都强制先跑通、再留一法评估、最后才动界面,顺序反了就会在错误的地基上盖楼。希望帮到你。
本文还有配套的精品资源,点击获取