基于Python的协同过滤电影推荐系统:从评分矩阵到Top-N推荐实战

发布时间:2026/10/13 20:19:14
基于Python的协同过滤电影推荐系统:从评分矩阵到Top-N推荐实战 简介一份基于Python和协同过滤算法的电影推荐系统完整毕业设计项目面向计算机专业毕业生、需要完成推荐系统课题或学习Django框架开发的学习者。项目采用Django框架与MySQL数据库实现管理员和用户双角色覆盖个人中心、用户管理、电影分类、电影信息、电影评分、系统管理等模块可帮助理解协同过滤算法在真实业务场景中的落地方式。资源包共690个文件压缩后21.92MB主要包含Python源码、Vue前端组件、HTML/CSS/JS静态资源、SQL数据库脚本、mp4运行演示视频以及doc格式的万字论文和配套文档结构清晰便于按模块查阅。目前已有66人学习下载。源码经过本地编译调试可正常运行评审分达95分以上上手难度适中既可作为毕业设计直接提交也可作为推荐系统实践项目的参考模板适合希望快速获取完整可运行方案的学习者。1. 基于Python和协同过滤算法的电影推荐系统评分数据才是真正的黑匣子别把这个标题当成一道「算法题」——协同过滤的公式十分钟能讲完但真正动手做你会发现评分数据的清洗、相似度矩阵的存储、推荐结果的去重才是消耗八成开发时间的部分。这套方案的技术栈很典型Python 做计算、协同过滤做推荐核心、MySQL 存评分与用户数据再配上界面和文档正好覆盖一个课程设计或毕业设计的完整交付形态。它能让你在一份真实评分数据集上从零跑通「数据入库 → 相似度计算 → Top-N 推荐」的整条链路。适合正在选毕业设计方向的学生也适合想快速验证协同过滤效果的工程师。2. 从评分矩阵到相似度计算协同过滤的 Python 实现与选型2.1 评分矩阵的形态从 CSV 长表到 User-Item 宽表协同过滤的输入本质上是一个二维矩阵行对应用户列对应电影格子里的值是 15 的评分。但绝大多数公开数据集的原始形态根本不是宽表而是一行一条评分记录的「长表」结构无非是 user_id、movie_id、rating、timestamp 四列。算法能直接消费的必须是宽表所以第一步永远是形态转换——这一步做错后面所有计算都是错上加错。我在项目里通常会先用 Pandas 的 pivot_table 把长表转成矩阵。这一步有两个容易忽略的细节一是原始数据可能包含同一用户对同一部电影的重复评分pivot_table 默认对重复值取均值这会掩盖数据质量问题二是转换后矩阵会产生大量 NaN如何区分「用户没看过」和「打了 0 分」直接决定了相似度函数的写法。import pandas as pd # 原始评分数据三列分别是用户ID、电影ID、评分 df pd.read_csv(data/ratings.csv) # 转成宽表行用户列电影值是评分 mat df.pivot_table( indexuser_id, columnsmovie_id, valuesrating ) print(矩阵形状, mat.shape) print(稀疏度, 1 - mat.notna().sum().sum() / mat.shape[0] / mat.shape[1])pivot_table 的 index 指定行键columns 指定列键values 是填充值。这里没写 aggfunc默认就是 mean——一旦数据里有重复评分它会直接把同一条评分平均掉轻则改变分布重则让后续相似度计算偏离。我的习惯是在转表之前先按 user_id 和 movie_id 去重再进 pivot_table避免平均值掩盖脏数据。矩阵转完后建议顺手算一下稀疏度真实数据集里稀疏度通常在 95% 以上这个数字会直接影响你对 K 值和过滤阈值的判断。2.2 相似度的两种公式与 Python 实现余弦和 Pearson 怎么选协同过滤无论是 user-based 还是 item-based核心都在「相似度怎么算」。最常见的两种度量余弦相似度只管向量夹角不关心两个用户评分的整体尺度皮尔逊相关系数在计算前把每个向量减去自身均值相当于消除了「有人打分偏高、有人打分偏低」的习惯差异。电影场景里评分习惯差异非常普遍所以我默认用 Pearson只用公共评分的坐标参与计算。两种公式可以直接用 NumPy 手写不需要调 sklearn 的 pairwise_distances。自己写的好处是能精确控制缺失值处理和边界条件出了结果也更容易排查import numpy as np def cosine_sim(vec1, vec2): # 只取两个向量都非缺失的坐标 mask ~np.isnan(vec1) ~np.isnan(vec2) v1, v2 vec1[mask], vec2[mask] if len(v1) 2 or np.linalg.norm(v1) 0 or np.linalg.norm(v2) 0: return 0.0 return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2)) def pearson_sim(vec1, vec2): # 只看公共评分先各自去均值再算相关系数 mask ~np.isnan(vec1) ~np.isnan(vec2) v1, v2 vec1[mask], vec2[mask] if len(v1) 2: return 0.0 v1, v2 v1 - v1.mean(), v2 - v2.mean() denom np.sqrt(np.sum(v1 ** 2) * np.sum(v2 ** 2)) return np.dot(v1, v2) / denom if denom 0 else 0.0两个函数都先用 isnan 掩码取出公共坐标再做相似度计算。注意边界条件公共坐标少于 2 个时直接返回 0防止一个共同评分就给出 ±1 的极端结论denom 为 0 时也返回 0这种情况常见于某个用户对共同看过的电影全部打了相同分数去均值后向量变成全零。实战里我一般会把公共评分数的阈值提到 5相似度低于 0.1 的对也直接丢弃宁可少一些相似关系也不在噪声上建立边。2.3 Item-based 还是 User-based电影场景的选型逻辑两种协同过滤在代码上只差一次转置但选型理由决定了后续所有性能与效果。User-based 找的是「兴趣相似的邻居用户」再用邻居的评分推导目标用户Item-based 找的是「物品之间的相似关系」把这个用户的评分按物品相似度加权求和。电影推荐里同一个用户可能会因为口味变化、心情不同打出差异很大的分但电影与电影之间的关系相对稳定——喜欢《盗梦空间》的人大概率也喜欢《星际穿越》这条关系不会随用户变化而漂移。所以这个项目我默认走 Item-based先算电影与电影的相似度矩阵再对每个用户看他已评过的电影去推断他没看过的电影的得分。如果做新闻推荐或电商推荐用户兴趣漂移非常快User-based 会更合适但工程点是一样的相似度函数可以直接复用。选型的关键并非「哪个更高级」而是「哪个的假设在业务场景里更站得住」Item-based 在电影场景里站得住的主要原因是物品集合相对稳定相似度矩阵可以离线算好、定时更新线上只查表。3. 从预测评分到 Top-N 推荐完整流程与可复现代码3.1 数据清洗管线去重、过滤低频、再转宽表拿到评分数据不要急着算相似度。清洗顺序我一般固定为去重 → 过滤低频用户 → 过滤低频电影 → 转宽表。为什么要过滤一个用户总共打过 3 条评分他与别人的相似度基本是碰运气一部电影只有 2 个人看过它与别的电影的相似度同样没有统计意义。学术上叫支持度过滤工程上就是「样本量不足不参与计算」。# 清洗去掉重复记录、低频用户和低频电影 df df.drop_duplicates(subset[user_id, movie_id]) user_cnt df.groupby(user_id)[movie_id].count() movie_cnt df.groupby(movie_id)[user_id].count() df df[df[user_id].isin(user_cnt[user_cnt 5].index)] df df[df[movie_id].isin(movie_cnt[movie_cnt 10].index)] mat df.pivot_table(indexuser_id, columnsmovie_id, valuesrating)drop_duplicates 按 user_id 和 movie_id 联合去重避免同一对记录多次进入计算。groupby 分别统计每个用户评了多少部电影、每部电影被多少人评过阈值分别设 5 和 10。这两个数不是定死的数据量大的时候我会把用户阈值提到 10、电影阈值提到 20相似度会更稳定数据本身稀少时阈值设太大会把矩阵切得支离破碎推荐列表都拼不出来。判断标准很简单过滤后如果剩余评分不足原来的 60%说明阈值过猛需要回调。3.2 构建 Item-Item 相似度矩阵内存和算力的第一道坎相似度矩阵是整个项目里最费算力的一步。最直白的实现是双重循环对每两部电影调用 pearson_sim把所有评分向量两两算一遍复杂度是 O(n²)。几千部电影能接受超过一万部就开始卡了所以这一步的代码和后续的数据结构要一起设计。def build_sim_matrix(mat, sim_funcpearson_sim): items mat.columns n len(items) sim np.eye(n) # 对角线是自己与自己相似 mat_t mat.values.T # 转置后每一行是一部电影在所有用户上的评分 for i in range(n): for j in range(i 1, n): s sim_func(mat_t[i], mat_t[j]) sim[i, j] sim[j, i] s return pd.DataFrame(sim, indexitems, columnsitems)mat.values.T 把 User-Item 矩阵转成 Item-User后面每次取一部电影的评分向量都是连续内存读取比在 DataFrame 上按列切片快不少。对角线上是自己和自己相似度自然是 1所以先 np.eye 初始化。这个实现的问题也很明显电影数到 8000 时双重循环要跑 3 千万次 Python 级调用速度会让人怀疑人生。想提速最简单的是用 multiprocessing 按列分块并行或者把评分向量拼成矩阵后用 numpy 广播批量算但那样对内存又提出了更高要求。实际项目里我更推荐在算完相似度后立刻截断每部电影只保留相似度最高的 50 个邻居其余全部置 0再用 scipy.sparse.csr_matrix 压缩存储。这样做内存能降一个数量级而且预测阶段取 Top-K 时天然就是按稀疏行取不用再在全矩阵上排序。3.3 评分预测与 K 值选择K 近邻不是玄学到这里才进入真正的「推荐」环节。目标用户 u 对未看过电影 i 的预测分取的是与 i 最相似、且 u 有评分的 K 部电影按相似度加权平均。注意候选集有两个条件缺一不可相似度高而且用户看过。def predict_score(mat, sim_mat, user_idx, item_idx, k20): user_ratings mat.iloc[user_idx].values n_items mat.shape[1] # 候选用户看过、非目标电影本身 candidates np.where( ~np.isnan(user_ratings) (np.arange(n_items) ! item_idx) )[0] sims np.array([sim_mat.iloc[item_idx, c] for c in candidates]) # 负相似度直接丢弃反口味推荐体验太差 pos sims 0 candidates, sims candidates[pos], sims[pos] top_k np.argsort(sims)[::-1][:k] if len(top_k) 0: return np.nan s sims[top_k] r user_ratings[candidates[top_k]] return np.sum(s * r) / np.sum(s)注释里 pos sims 0 那行是重点负相似度意味着「口味相反」如果硬参与加权推荐结果会变成「因为你不喜欢 A所以推你讨厌 B 的最不爱片」用户看到只会困惑。所以负相似度直接砍掉。K 值有人说 10 好、有人说 30 好我的经验是从结果反推如果推荐列表全是同类型电影说明 K 偏大噪声进来了降到 15如果推荐结果冷门到用户不可能看过说明 K 偏小候选太窄加到 30 再跑。K 值在不同数据集上不通用本质取决于评分矩阵的密度。3.4 推荐列表后处理去重、排序、组装可复用接口算法算完推荐结果还需要一层「包装」。最常见的问题是系列电影刷屏《复仇者联盟》系列互相相似度天然极高不做处理会集体霸榜另一个是热门电影霸榜因为大众评分让它们和什么都相似。这两个问题靠算法调参解决不了要在后处理阶段处理。def generate_recommendation(mat, sim_mat, user_idx, n10): predictions [] user_row mat.iloc[user_idx].values for item_idx in range(mat.shape[1]): if not np.isnan(user_row[item_idx]): continue # 已经看过的电影不推荐 score predict_score(mat, sim_mat, user_idx, item_idx) if not np.isnan(score): predictions.append((mat.columns[item_idx], score)) predictions.sort(keylambda x: x[1], reverseTrue) return [int(mid) for mid, _ in predictions[:n]]逐一遍历所有未见电影、预测、排序、切 Top-N。这个函数输入 user_id、输出电影 ID 列表可以直接包成一个接口给上层调用。推荐结果不需要实时重算用户维度缓存 24 小时就行评分表更新后才触发重新生成。至于系列电影去重我会在拿到 movie_id 后关联电影表里的 title 和 series_id 字段同一系列只保留预测分最高的一部这个逻辑放在 SQL 层做比放到 Python 层更省事。4. 数据库设计与数据入库推荐系统的数据底座4.1 三张核心表用户、电影、评分协同过滤跑得再快数据最终还是躺在数据库里。这类系统最常见的是三张表movies 存电影元数据users 存用户信息ratings 存评分行为。核心是 ratings它的表结构设计直接决定查询效率和后续导入是否顺手。索引方面有两个硬要求按 (user_id, movie_id) 建联合主键既能防重复评分又能支撑「按用户取全部评分」的查询再建一个 (movie_id, user_id, rating) 的辅助索引给 Item-based 的「按电影取评分列表」用。CREATE TABLE movies ( movie_id INT PRIMARY KEY, title VARCHAR(255) NOT NULL, genres VARCHAR(100), release_year SMALLINT ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE users ( user_id INT PRIMARY KEY, username VARCHAR(64), created_at DATETIME ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE ratings ( user_id INT NOT NULL, movie_id INT NOT NULL, rating FLOAT NOT NULL, timestamp INT NOT NULL, PRIMARY KEY (user_id, movie_id), INDEX idx_movie (movie_id, user_id, rating) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;ratings 的联合主键天然屏蔽重复插入就算后端脚本对同一对记录写两遍MySQL 会在主键层直接拒绝第二遍。idx_movie 索引把 movie_id 放最左因为相似度矩阵构建阶段要高频执行「查某部电影都有谁评过分」的逆向查询没有这个索引就是全表扫描。timestamp 我故意用 INT 不用 DATETIMEMovieLens 这类公开数据集的原始时间戳就是 Unix 整数用 INT 导入零转换时间范围过滤也快。字符集用 utf8mb4 是为了保证中文片名和特殊字符不出现乱码。4.2 Python 读写 MySQL批量导入的两个性能陷阱连接 MySQL 我用 pymysql如果项目没有引入 ORM 的刚需不推荐为了「面向对象」硬上 SQLAlchemy。读数据直接 pandas.read_sql 会把查询结果接成 DataFrame正好对上前面 pivot_table 的输入。写入这件事有个经典翻车点一条一条 execute 在几万条数据面前慢到无法接受必须用 executemany 批量提交。import pymysql conn pymysql.connect( host127.0.0.1, port3306, userroot, passwordyour_password, databasemovie_recommender, charsetutf8mb4 ) data pd.read_sql(SELECT user_id, movie_id, rating, timestamp FROM ratings, conn) rows [tuple(x) for x in data[[user_id, movie_id, rating, timestamp]].values] cursor conn.cursor() cursor.executemany( INSERT INTO ratings (user_id, movie_id, rating, timestamp) VALUES (%s, %s, %s, %s) ON DUPLICATE KEY UPDATE ratingVALUES(rating), timestampVALUES(timestamp) , rows ) conn.commit()executemany 的第二个参数是列表套元组pymysql 底层会把它拼成一条多值 INSERT 执行速度比逐条 execute 快一个量级。ON DUPLICATE KEY UPDATE 解决的是数据幂等问题同一对 (user_id, movie_id) 重复导入时直接覆盖评分和时间戳脚本重跑多少次都不会产生脏数据。我见过有人用逐条 INSERT 导 5 万条评分泡杯茶回来看还在跑换成批量插入后基本就是秒级完成。如果运行中需要调整表结构比如把 rating 从 INT 改成 FLOATALTER TABLE 一行能解决但表数据量大时会有锁表风险尽量放在低峰期操作。4.3 环境与部署换台 Ubuntu 机器跑不起来的常见原因这套项目常年在 Windows 上开发完、再拿到 Ubuntu 服务器上跑热词里「ubuntu 电影推荐系统」的关注度一直不低。最常见的坑是 Python 版本错位Ubuntu 20.04 自带的 Python 3.8开发机可能装的是 3.11pandas 版本一不同代码可能连启动都做不到。不要直接用系统 Python必须建虚拟环境。python3 -m venv .venv source .venv/bin/activate pip install --upgrade pip pip install numpy pandas pymysql scikit-learn python recommend.pyvenv 够用不必上 conda。如果 pip install numpy 时出现编译错误十有八九是 pip 版本太旧导致装的源包不兼容先升级 pip 再重试别急着换 Python 版本。requirements.txt 里的依赖建议锁大版本号比如 numpy1.24,2.0不锁的话下次在新机器上重建环境可能装出完全不兼容的新版。这一整套环境问题属于那种「处理过就知道没处理过能磨半天」的典型所以提前固定解释器和依赖版本能省掉大量无效时间。5. 协同过滤项目的常见坑从清洗到部署的避坑清单5.1 把 NaN 填成 0相似度矩阵全面崩坏现象相似度矩阵算完后几乎所有电影两两相似度都偏高推荐结果和热门榜单没有区别。原因这是新手最容易踩的坑之一。为了能跑 sklearn有人把评分矩阵里的 NaN 用 0 填充再交给余弦相似度计算。0 被当成了「用户打了 0 分」但用户其实只是没看过这部电影。缺失和负样本是两个完全不同的语义大量虚假的「共同 0 分」把所有电影都拉近了距离。解决全程用 isnan 掩码只取两个向量都非缺失的坐标参与计算。前面写的 cosine_sim 和 pearson_sim 都是这个思路这也是为什么我宁可自己写相似度函数也不用 sklearn 的默认实现因为 sklearn 不知道你的矩阵里 NaN 是「没看过」还是「数据缺失」。5.2 相似度矩阵撑爆内存现象电影数量从 2000 涨到 8000程序内存占用一路飙升到几个 G动不动就卡死。原因全量相似矩阵是 n×n 的稠密浮点数组。8000 部电影就是 6400 万个浮点数约 512MB算上中间过程产生的临时数组内存直接翻倍甚至更多。解决算完相似度后立即截断每部电影只保留 Top-50 个相似邻居其余置 0然后用 scipy.sparse.csr_matrix 压缩存储内存可以降到原来的几十分之一。如果数据量再大一个量级考虑用 faiss 这类近似最近邻库做 ANN 检索但那属于进阶优化做毕设或小规模工程用稀疏矩阵截断就够了。5.3 冷启动新电影永远进不了推荐列表现象数据库里新入库的电影不管质量多好推荐列表里永远见不到它。原因协同过滤是纯数据驱动一部电影一条评分都没有它的相似度全是 0预测函数只能返回 NaN自然进不了 Top-N。这是协同过滤的结构性缺陷不是参数能调好的。解决给新物品加一个兜底策略。电影上架的第一周先用基于内容的规则推荐比如同导演、同类型、同主演的相似影片顶上等到评分数量积累超过 20 条再切换到协同过滤。这个规则写在业务逻辑里不要混进算法模型保证模型部分永远是纯粹的数据驱动。5.4 评分通胀和评分尺度不一致现象部分用户只打 1 分或 5 分评分分布两极分化他和任何人的相似度都被极端评分习惯带偏。原因不同用户对「好看」的标尺完全不同。有人普遍给 4 分以上有人最多给 3 分。余弦相似度无法自动消除尺度偏移它只关心向量方向。解决优先用 Pearson 相关系数替代余弦相似度因为它先对每列做去均值处理把用户的评分习惯差异削掉。想要更彻底的归一化可以在相似度计算前做 z-score但 z-score 会增加稀疏矩阵中的无穷值风险处理起来更麻烦我一般用 Pearson 就够了。5.5 预测结果全是 NaN推荐列表为空现象generate_recommendation 跑完返回空列表或者预测分全是 nan。原因常见三种一是目标用户看过的电影里没有一部与目标电影同时拥有足够公共评分候选在相似度过滤时被全部淘汰二是清洗阶段把用户整个过滤掉了三是相似度矩阵里两列根本没有交集。解决在 predict_score 里加一个兜底逻辑——候选数为空时返回用户评分均值而不是返回 NaN。调试时在函数里打印候选数量和相似度数量确认数据流走到哪一步断了。另外要盯紧索引对齐mat.iloc 是按位置取值mat.loc 是按标签取值混着用就会随机错位越往后结果越离谱。6. 离线评估与验证用一份测试集说服你的评委和老板推荐系统做完最容易被问的一句话是「你怎么证明它有用」。回答不能靠感觉要靠离线评估。做法是先把评分数据按 80/20 切分成训练集和测试集模型只允许在训练集上学习相似度然后在测试集上对比预测分和真实分这样评估出来的指标才代表真实泛化能力。切分时建议按用户维度切同一用户的评分不要跨集合否则会造成数据泄漏——模型偷看到了答案分数虚高答辩一追问就露馅。from sklearn.model_selection import train_test_split train, test train_test_split(df, test_size0.2, random_state42) train_mat train.pivot_table(indexuser_id, columnsmovie_id, valuesrating)评估指标最常用的是 RMSE 和 PrecisionK。RMSE 衡量预测分与真实分的偏差值越小越好电影领域能跑到 1.0 以内就算不错PrecisionK 衡量推荐列表里有多少部是用户真正看过的反映的是推荐列表实际可用度。这两个指标一个看数值准确度、一个看结果命中率缺一不可。指标计算逻辑参考区间RMSE预测分与真实分之差的均方根1.0 可接受Precision10推荐列表命中测试集的部数 / 100.1~0.3 属正常Coverage推荐覆盖电影占全库比例高于 20% 才好我早年在项目里只盯 RMSE把 K 值调到误差异常漂亮就交付了结果推荐列表死板且高度同质化全推的是大众热门片。后来养成了另一个习惯每次调参都把 PrecisionK 和 Coverage 一起打印出来对比宁可放弃 0.05 的 RMSE 精度也要保证推荐列表有足够的多样性。如果 Coverage 低于 20%说明算法基本只在头部电影里打转长尾内容永远没有曝光机会协同过滤的核心价值就丢了。这一套验证方法在你做论文和给团队演示时都能直接用至少在被问「准不准」的时候你手里有数据而不是一句「感觉还行」。希望这套思路能帮你少走我走过的弯路。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询