协同过滤电影推荐系统实战:从Python实现到MySQL数据库设计

协同过滤电影推荐系统实战:从Python实现到MySQL数据库设计 简介基于协同过滤的Python电影推荐系统是一份面向毕业设计、期末大作业及课程设计的高分项目资料覆盖完整代码、数据库与论文代码注释细致新手也能快速上手曾被导师评价为优秀项目。压缩包内共687个文件以Python源码、SQL数据库脚本、Vue前端组件及HTML/CSS/JS静态页面为主并附带运行与安装批处理脚本支持快速启动整体仅13.19MB目录结构清晰部署简单。目前已有183人学习下载可作为理解协同过滤算法从数据准备、相似度计算到推荐结果生成的完整工程范例适合院校学生对照实践。资料除系统源码和数据库初始化脚本外还提供项目论文与多处注释说明便于读者兼顾实践与理论掌握推荐系统的模块划分、核心流程与调优方向为答辩或演示提供有力支撑。1. 这个标题被低估了协同过滤电影推荐系统的高分重点在数据闭环电影推荐是数据库课程设计和毕业设计里最常出现的选题但大部分提交物的质量差距并不是模型选得有多新而是能不能形成完整的数据闭环。很多人的做法是把 MovieLens 数据导进 Notebook算一个相似度矩阵、跑出几行推荐结果就算完成。这套流程用来交普通作业可以想在答辩时拿高分缺的是三样东西协同过滤实现的工程痕迹、数据库表的真实设计、论文里能复现代码的实验数据。这篇博文会按“算法选型 → Python 实现 → 数据库设计 → 论文组织”的顺序把基于协同过滤的 Python 电影推荐系统拆清楚。重点不是堆公式而是给出能直接运行的代码、参数设置的依据、MySQL 表结构以及答辩时容易被追问的细节。适合正在做课程设计的学生也适合刚接触推荐系统、想快速拿到一套标准实现的上手工程师。2. 协同过滤选型与评分数据处理为什么电影场景默认先试 ItemCF2.1 UserCF 与 ItemCF 的适用边界协同过滤分两类基于用户的 UserCF 和基于物品的 ItemCF。两者的核心逻辑都是“相似的人或物”但适用场景差异很大。UserCF 先找到与当前用户兴趣最相似的一批用户再把这批用户喜欢的物品推荐过来。它的优点是能捕捉用户兴趣的时效变化适合新闻、论坛这类物品更新快、用户兴趣漂移明显的场景。缺点是用户数量增长时用户间相似度矩阵的计算量迅速膨胀且对冷启动非常敏感——新用户没有任何评分记录时相似度完全算不出来。ItemCF 则是先计算物品之间的相似度比如《蝙蝠侠黑暗骑士》和《蝙蝠侠侠影之谜》的评分模式很像那么看过前者的人还没看后者系统就把后者推出来。电影、图书这种物品相对稳定、数量远小于用户的场景ItemCF 有两个显著优势物品相似度矩阵可以离线算好并定期更新新用户只要有几条评分就能立刻得到推荐结果。这也是为什么课程设计做电影推荐系统时默认优先实现 ItemCF——数据量小、容易讲解、答辩时逻辑更清晰。从算法本质来看两者都依赖“共同评分”来建立关联。没有共同评分的两个用户或两部电影相似度是 0这在实际数据里非常常见所以数据处理阶段要考虑稀疏问题。2.2 评分数据的稀疏性与相似度度量选择真实评分数据里大多数用户只看过几百部电影而总片库有几万部。用户-物品矩阵的稀疏度通常在 95% 以上。这意味着相似度计算时“至少要有多少用户共同评过分”这个阈值直接决定矩阵的可靠性。常用的相似度度量有三个余弦相似度把两个物品的评分向量看作多维空间的向量计算向量夹角。它对评分尺度不敏感但没考虑用户对“打分宽严”的习惯差异。皮尔逊相关系数在余弦相似度基础上减去用户各自的平均评分能抵消用户打分偏置。某位用户习惯给 4 分另一位习惯给 2 分皮尔逊系数比余弦更稳。调整余弦相似度对物品评分向量减去用户均分后再计算余弦相似度兼顾了用户偏置和评分向量的模长差异是 ItemCF 里比较常用的做法。实际实现时还有一个“最小共同评分数”参数。假设只有 2 个用户同时看过两部电影算出来的相关系数可能是 1.0 或 -1.0完全不可信。我一般会设置一个阈值比如至少 5 个共同评分的用户才计入相似度这样可以过滤掉大量噪声。评分数据的预处理也很重要。MovieLens 数据里用户 id、电影 id 都是整数但评分时间往往没有被利用。如果你做的是带“时间衰减”的加分项可以把评分时间换算成权重让近期的评分对相似度贡献更大。这个在论文里可以单开一节写。2.3 算法描述怎么写进论文而不被追问课程设计、毕业设计的评审老师通常不会要求提出新算法但会追问算法的输入、输出和计算步骤。论文里描述算法时建议按标准流程写先定义集合 U 为用户集合、I 为电影集合评分矩阵 R 中的元素 r_ui 表示用户 u 对电影 i 的评分。ItemCF 计算物品 i 与物品 j 相似度时只使用同时评分过这两部电影的用户集合 U_ij。然后列出相似度公式、推荐评分预测公式以及 Top-N 推荐列表的生成步骤。只要对这些内容有清晰说明答辩时被问“你的相似度矩阵是怎么算的”就不用慌乱。重点记住一个细节完整的数据集不一定适合做课设。如果你用的是 MovieLens 100k需要说明为什么选用这个数据量、有多少个用户和电影、稀疏度是多少、有没有做数据清洗。这些内容既体现工程意识又是论文中表格的真正控件。3. Python 落地协同过滤从用户-物品矩阵到 Top-N 推荐3.1 数据加载与用户-物品矩阵的构建用 Python 实现协同过滤常见的做法是按 pandas numpy 组合。先读取 CSV 格式的评分数据检查缺失值和评分范围再构造一个行是用户、列是电影的评分矩阵。电影推荐系统的核心数据字段只有四个userId、movieId、rating、timestamp。import pandas as pd import numpy as np # 读取评分数据 ratings pd.read_csv(ml-latest-small/ratings.csv) print(ratings.head()) print(缺失值统计, ratings.isnull().sum().sum()) print(用户数, ratings[userId].nunique()) print(电影数, ratings[movieId].nunique()) # 构造用户-物品评分矩阵未评分的位用 NaN 填充 user_item_matrix ratings.pivot_table(indexuserId, columnsmovieId, valuesrating) print(矩阵形状, user_item_matrix.shape)这里的关键是pivot_table的用法。indexuserId表示矩阵的行是用户columnsmovieId表示列是电影valuesrating指定填充值。缺省位置会被填充为NaN后续计算相似度时不能直接对NaN做算术运算需要跳过。先检查nunique()的目的是确认数据的实际规模避免后续内存爆炸。3.2 ItemCF 的 Python 最小实现ItemCF 的实现思路是先把用户-物品矩阵转置成“物品-用户”矩阵逐对计算电影之间的相似度然后根据相似度对用户已评分电影的加权评分进行求和得到用户对未看过的电影的预测评分。# 物品-用户矩阵行是电影列是用户 item_user_matrix user_item_matrix.T # 计算物品相似度矩阵 def compute_item_similarity(item_user_df, min_common_users5): items item_user_df.index.tolist() sim_matrix pd.DataFrame(np.zeros((len(items), len(items))), indexitems, columnsitems) for i in range(len(items)): for j in range(i 1, len(items)): item_i item_user_df.loc[items[i]] item_j item_user_df.loc[items[j]] common_mask item_i.notna() item_j.notna() common_count common_mask.sum() if common_count min_common_users: continue vec_i item_i[common_mask].values.astype(float) vec_j item_j[common_mask].values.astype(float) # 向量中心化后再算余弦等价于调整余弦相似度 vec_i_centered vec_i - vec_i.mean() vec_j_centered vec_j - vec_j.mean() denom np.sqrt((vec_i_centered ** 2).sum() * (vec_j_centered ** 2).sum()) if denom 0: continue sim (vec_i_centered * vec_j_centered).sum() / denom sim_matrix.loc[items[i], items[j]] sim sim_matrix.loc[items[j], items[i]] sim return sim_matrix item_sim compute_item_similarity(item_user_matrix) print(相似度矩阵形状, item_sim.shape)这段代码里有三个关键参数min_common_users最小共同评分数。设为 5 是经验值如果数据特别稀疏可以降到 3但低于 3 的相似度几乎不可靠。中心化减掉均值后再算余弦就是调整余弦相似度。如果不减均值那些喜欢给高分的用户会主导相似度。denom 0的判断中心化后向量可能全为 0比如某个用户对所有共同电影都打相同的分此时分母为 0需要直接跳过这一步否则会出现 NaN。3.3 推荐生成与 Top-N 参数的设定计算出相似度矩阵后给用户推荐时只需要找到用户已经评过分的电影然后用相似度去加权预测未看过电影的评分。def recommend_items(user_id, user_item_df, sim_df, top_n10): rated_items user_item_df.loc[user_id].dropna().index.tolist() scores {} for candidate in sim_df.index.tolist(): if candidate in rated_items: continue # 候选项与已评分电影的交集 interacted [item for item in rated_items if item in sim_df.columns and not np.isnan(sim_df.loc[candidate, item])] if len(interacted) 0: continue numerator 0.0 denominator 0.0 for item in interacted: sim_val sim_df.loc[candidate, item] rate_val user_item_df.loc[user_id, item] numerator sim_val * rate_val denominator abs(sim_val) if denominator 0: continue scores[candidate] numerator / denominator return sorted(scores.items(), keylambda x: x[1], reverseTrue)[:top_n] # 为用户 1 生成 10 条推荐 recs recommend_items(1, user_item_matrix, item_sim, top_n10) print(推荐结果, recs)推荐结果的排序依据是预测评分。top_n是最终推荐列表的长度interacted变量用来提取候选电影与用户已评分电影的公共部分。因为矩阵很稀疏很多候选电影与已评分电影的相似度全是 NaN这种情况直接跳过不参与计算。预测评分公式使用加权平均而不是直接求和因为不同候选电影的可比较电影数量差异很大加权平均可以避免“拥有更多共同评分的电影天然得高分”的偏差。3.4 效果评估离线测试的 4 个指标推荐系统离线评估常用四个指标精确率、召回率、F1、覆盖率。对课程设计来说这四个指标加一张对比图已经足够撑起实验章节。from sklearn.model_selection import train_test_split train, test train_test_split(ratings, test_size0.2, random_state42) # 用 train 数据构造矩阵test 数据做验证 # 对 test 里每一条 userId-movieId 判断是否出现在推荐列表中这里有个容易被忽略的细节测试集切分时要以用户为粒度做分组否则评分多的大户会同时进入训练集和测试集造成数据泄漏。简单做法是按用户分组后每组做 8:2 切分或者直接按评分记录比例切分并强调“非时序随机划分”的前提。精确率表示推荐列表中用户真正看过的电影占比召回率表示用户看过的电影里被推荐出来的占比。论文中把这两个指标做成随推荐个数变化的折线图比只放一张静态截图更有说服力。4. 数据库设计与数据链路让“数据库”不再是加载 CSV 的装饰4.1 MySQL 表结构设计与索引选择课程设计强调数据库部分意味着你需要建出合理的表结构而不是用 pandas 读完 CSV 就结束了。常见的数据库设计是给 MySQL 建三张核心表和一张中间表实现电影推荐系统的数据持久化。-- 用户表 CREATE TABLE users ( user_id INT PRIMARY KEY, username VARCHAR(64), gender CHAR(1), age INT ); -- 电影表 CREATE TABLE movies ( movie_id INT PRIMARY KEY, title VARCHAR(255) NOT NULL, genres VARCHAR(255) ); -- 评分表联合主键避免同一用户对同一电影重复评分 CREATE TABLE ratings ( user_id INT NOT NULL, movie_id INT NOT NULL, rating DECIMAL(2,1) CHECK (rating BETWEEN 0.5 AND 5.0), timestamp INT, PRIMARY KEY (user_id, movie_id), FOREIGN KEY (user_id) REFERENCES users(user_id), FOREIGN KEY (movie_id) REFERENCES movies(movie_id) ); -- 为评分表增加索引按电影查评分或按用户查评分都会用到 CREATE INDEX idx_ratings_movie ON ratings(movie_id); CREATE INDEX idx_ratings_user ON ratings(user_id);联合主键(user_id, movie_id)是这里的关键设计它既能保证数据唯一性又天然形成覆盖索引。在 MySQL 的 InnoDB 引擎下联合主键的索引结构也可以用于按用户查询、按电影查询。CHECK约束在 MySQL 8.0.16 之后才会真正生效如果使用的是低版本需要在应用层做数据校验这也可以写在论文的数据完整性一节里。ratings表中的timestamp是评分时间。虽然课程设计不强制用时间数据但保留原始字段可以用于实验中论证数据清洗过程比如筛选足够活跃的用户。数据库课程设计的评分标准通常包含“表结构是否满足三范式”上述设计满足第二范式的要求而且可以回答“为什么把用户和电影拆开”的课堂问题。4.2 从 CSV 入库到 Python 读取的完整链路有了表结构还需要把 MovieLens 的 CSV 数据灌进 MySQL。常见做法是使用 pandas 的to_sql但那是简单的方法用 SQL 文件构造的工程痕迹更强一点。# 登录 MySQL 并创建数据库 mysql -u root -p -e CREATE DATABASE IF NOT EXISTS movie_recsys CHARACTER SET utf8mb4;import pandas as pd from sqlalchemy import create_engine engine create_engine(mysqlpymysql://root:your_passwordlocalhost/movie_recsys) # 读取 CSV 并只保留需要的列 ratings pd.read_csv(ml-latest-small/ratings.csv) ratings ratings[[userId, movieId, rating, timestamp]] # 批量写入if_existsappend 表示向已有表追加数据 ratings.to_sql(ratings, engine, if_existsappend, indexFalse, chunksize1000)to_sql是这里效率最高的工具。chunksize1000表示每 1000 条数据作为一个批次提交事务能避免一次性写入大量数据时的锁表问题。写入完成后从 Python 读取数据时就用read_sql_queryquery SELECT r.user_id, r.movie_id, r.rating FROM ratings r JOIN movies m ON r.movie_id m.movie_id df pd.read_sql_query(query, engine)使用 SQL 语句而不是直接读 CSV 来生成输入矩阵才能让你的系统真正“长在数据库上”而不是把数据库当成摆设。读出来的 DataFrame 可以直接接到之前pivot_table那段代码里两套流程可以无缝衔接。4.3 给数据库加分的技术细节MySQL Workbench 是可视化查看表结构和执行 SQL 的常用工具。答辩时评审老师通常关注三点第一表数据量是否符合实际使用场景第二查询语句是否优化过第三数据库与推荐计算是否存在实际交互。这三点里最容易被忽视的是查询效率。-- 查看评分表里评分最多的用户 SELECT user_id, COUNT(*) AS rating_count FROM ratings GROUP BY user_id ORDER BY rating_count DESC LIMIT 10; -- 使用 EXPLAIN 分析查询执行计划 EXPLAIN SELECT * FROM ratings WHERE movie_id 1;当数据量只有 10 万条时不用索引的查询可能只要几十毫秒这会让评审老师感觉不到索引的作用。但如果你在ratings表里额外加了外键约束和索引并且在论文中贴出 EXPLAIN 的结果就能体现出索引设计的合理性。另一个加分项是写一个统计“所有用户平均评分”的视图或者设计一个触发器在插入评分时自动更新用户评分统计表。这些内容虽然简单但在课程设计里属于“有思考深度”的部分。5. 从代码到论文交付前要固定的 5 个实验参数评分数据处理和推荐代码跑通之后大部分人的项目其实已经达到了“及格以上”。但“高分项目”的差距体现在两个地方代码里随手写的参数有没有意义论文里的实验数据能不能用代码复现。推荐系统的关键参数有三个交付前必须固定并写进论文最小共同评分数、推荐列表长度 Top-N、测试集切分的随机种子。举例来说最小共同评分数的值直接决定相似度矩阵的稀疏程度。把它从 5 改成 3推荐结果的精确率会变化而这种变量不对结果的影响需要在论文实验部分分析而不是让评审老师自己去猜。随机种子的作用更重要没有固定随机种子时跑两次实验出来的精确率和召回率可能不一样这会被人质疑实验不可复现。发布时间分析是最后一点值得关注的内容。可以在代码的配置区域增加一个独立参数块集中控制这些实验设置# 实验配置论文第4章实验部分与此保持一致 CONFIG { min_common_users: 5, # 最小共同评分数 top_n: 10, # 推荐列表长度 random_seed: 42, # 测试集切分随机种子 test_size: 0.2, # 测试集比例 }论文的写作顺序建议是数据说明多少用户、多少电影、稀疏度→ 离线评估指标定义 → 相似度参数影响分析 → 不同 Top-N 下的精确率与召回率对比。你会发现在写这部分时上面确定的实验配置正好就是正文表格里的表头。答辩时被问“参数为什么这样设”最直接的回答是给出不同参数下的效果对比表然后用数据说话不要只说“这是经验值”。这才是把代码、数据库和论文串联起来的标准做法也是这类项目的核心价值。本文还有配套的精品资源点击获取