Python协同过滤电影推荐系统:算法原理与课程设计全流程指南
简介一套基于Python与协同过滤算法的电影推荐系统完整项目资料针对计算机相关专业毕业设计、课程大作业及推荐系统入门学习者。后端采用Django框架数据存储使用MySQL按管理员与用户双角色设计覆盖电影分类、信息管理、评分与个性化推荐等核心业务管理员可处理个人中心、用户管理、电影分类与评分、系统管理等模块用户侧支持注册登录及评分浏览。压缩包共690个文件、21.8MB其中py/pyc为后端逻辑vue/js/css/svg等构成前端界面sql为数据库脚本mp4为操作演示视频doc为万字论文bat脚本便于一键安装运行。项目已通过本地编译调试可复现运行并同时提供数据库、文档与演示视频有助于理解协同过滤推荐流程、Django项目结构与完整开发思路。已有58人学习下载适合需要完整可运行范例或高分大作业参考的读者。1. 为什么课程设计都选python协同过滤电影推荐先看懂这道题在考什么期末周拿着“基于python和协同过滤算法的电影推荐系统”这个题目多数人第一反应是去搜源码但真正决定你能不能拿高分的是另一件事你清不清楚这道题在考什么。它考的既不是python语法也不是推荐算法本身而是“数据、算法、工程、文档”四件事能不能串起来。用python做数据清洗和接口用协同过滤算法做评分预测用数据库存用户和评分最后用论文和视频把过程讲清楚。对做课程设计的学生来说这个题目的价值在于它复杂度适中不用GPU不用深度学习的黑匣子普通笔记本电脑就能跑完整流程但又比单纯的管理系统多了一个算法核心答辩时有的讲。对想转行做推荐方向的人来说这是入门用户行为建模成本最低的实践路径。一句话这个题能让你在一周内体验一个推荐系统从数据到上线的完整链路。2. 协同过滤电影推荐的算法底子相似度公式、UserCF与ItemCF怎么选2.1 先把“协同过滤”这个词翻译成人话评分预测公式推荐系统里最朴素的一条假设是相似的人会喜欢相似的东西。协同过滤就是把这个假设变成公式。你不用训练一堆参数也不用反向传播只需要把用户对电影的评分当成一个稀疏矩阵然后在里面找“邻居”。矩阵的每一行是一个用户每一列是一部电影交叉点是对应评分。问题是这个矩阵95%以上都是空的用户不可能看完所有电影。协同过滤要做的就是预测那些空格里的值然后把预测分最高的N部电影推荐出去。最常见的UserCF预测公式长这样pred(u,i) r̄_u (∑_{v∈N(u)} sim(u,v) × (r_{v,i} − r̄_v)) / (∑_{v∈N(u)} |sim(u,v)|)这里 r̄_u 是用户u的历史平均分N(u) 是用户u的最近邻集合sim(u,v) 是用户u和用户v的相似度。为什么要减去各自的平均分因为不同用户的打分尺度不一样有人习惯给3分有人习惯给5分。去掉均值之后剩下的就是“这个人相对自己口味的态度”可比性更强。相似度的计算一般用皮尔逊相关系数或者余弦相似度。皮尔逊在稀疏矩阵上对打分偏态更鲁棒适合评分数据余弦相似度更适合隐式反馈场景比如“看过/没看过”。写最小复现代码时用pandas自带的corr方法就能算出皮尔逊相关系数不需要自己一格格遍历import pandas as pd # rating_df 至少包含 user_id, movie_id, rating 三列 pivot rating_df.pivot_table(indexuser_id, columnsmovie_id, valuesrating) user_sim pivot.T.corr() # 按用户两两求皮尔逊相关系数 user_sim user_sim.fillna(0) # 无共同评分视为不相似逻辑说明pivot把长表转成宽表行是用户列是电影空值就是没评分。T是转置corr在转置后按列计算也就是按用户计算皮尔逊相关系数。fillna(0)在这一步是必须的因为两个用户可能没有任何共同评过的电影pandas会给出NaN后续加权时NaN会被污染到整个推荐结果。参数说明corr默认使用皮尔逊如果你的数据里有大量极端的1分和5分皮尔逊会偏向这些极端值可以先对评分做一次clip把范围限制在1到5之间再算。后续如果要切换余弦相似度可以用sklearn的cosine_similarity但需要先把空值填成0这会在稀疏矩阵上引入假信号一般不建议直接套。2.2 UserCF和ItemCF该选哪个电影场景的对比同样是协同过滤基于用户的UserCF和基于物品的ItemCF在推荐逻辑上是反的。UserCF找“和我口味相似的用户”把他们喜欢的电影推给我ItemCF找“和我看过的电影相似的其他电影”把相似电影推给我。电影推荐系统里主流方案是ItemCF原因有两个第一电影是相对稳定的物品内容特征变化很慢物品之间的相似度可以离线算好第二用户数量往往远大于电影数量UserCF的用户相似度矩阵规模更大更新成本更高。但大作业场景里UserCF依然是很多源码模板的主力因为它好讲、好画图、好答辩。你需要做的是两种都实现在论文里放一张对比表让评委看到你理解它们的差异而不是只会跑通一个。维度UserCFItemCF相似度对象用户与用户电影与电影适合场景用户数量少、内容变化快的社区物品数量少、内容稳定的电商/视频每次推荐计算成本实时计算用户相似度代价高物品相似度离线预计算在线只查表可解释性“和你口味相似的人也喜欢”“因为你看过XX所以推荐XX”冷启动新用户无行为完全失效新用户无行为完全失效新电影无评分也失效我的建议是主代码用UserCF跑通全流程在实验章节用ItemCF做对照组。两者共用同一套评分矩阵只是把转置方向换一下。如果你的数据库里电影表只有几百条记录ItemCF的计算代价会小很多推荐结果也更稳定。2.3 读python源码前先翻哪三个文件标题里带“源码”但很多同学下完源码先点开app.py看界面这是顺序错了。这类大作业源码无论用Django、Flask还是纯命令行结构上都高度统一先按这三个文件读能省掉一半调bug的时间第一个是数据加载模块常见的文件名是data_loader.py或者db.py。它的职责是从数据库或CSV文件里读取评分数据转成pandas的DataFrame并且保证user_id、movie_id、rating三个字段的类型正确。我遇到过很多次别的地方没问题、就是推荐结果全乱的情况最后发现问题出在user_id被读成了字符串导致pivot时一个用户被拆成了多行。第二个是相似度计算模块可能叫cf.py或recommend.py。这里会生成用户相似度矩阵或者物品相似度矩阵。你读的时候不用从头看直接找三点相似度用哪种公式、是否做了均值中心化、近邻数量N是怎么控制的。这三个参数直接决定推荐质量也是最容易被答辩老师追问的地方。第三个是主流程或接口层比如main.py、app.py。它负责把推荐结果包装成“用户可读”的形式。注意看它怎么把最终的预测分排序、怎么去掉用户已经看过的电影。很多源码在这一步偷懒直接返回全局热门榜这就是你答辩翻车的地方。读到这个文件时顺手确认一下它有没有过滤掉用户已经看过的电影。读这三个文件的时间控制在半小时左右别逐行读。你要带着“它要解决什么问题”这个视角去读而不是把它当小说。3. 在本地把源码跑通Python环境、MySQL导入、最小推荐演示3.1 Python环境准备虚拟环境里装哪些依赖动手第一步不是直接跑源码而是建一个干净的python环境。用系统全局环境跑这类项目会踩到很多莫名其妙的依赖冲突最常见的是Flask老版本和较新Python版本不兼容。python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install --upgrade pip pip install pandas numpy scikit-learn flask pymysql dbutils虚拟环境装完之后建议把依赖导出到requirements.txt方便交作业时说明运行环境。如果你手头的源码里已经带了requirements.txt那更简单直接执行pip install -r requirements.txt。但要注意requirements里如果有类似“tensorflow”这种跟推荐系统无关的重型依赖可以临时注释掉再装省得下载半小时。这套依赖组合里pandas负责数据清洗和透视表scikit-learn用于可选的SVD和评估指标flask是常见的Web展示层pymysql加上dbutils是数据库访问层。min版本不写死只要保证pip能解析成功即可。3.2 建库和导入数据电影推荐系统的三张核心表这类电影推荐系统的数据库设计几乎没有悬念就是用户表、电影表、评分表三张核心表。评分表是事实表用户表和电影表都是维表。先建库再建表别把顺序弄反。CREATE DATABASE IF NOT EXISTS movie_recommend DEFAULT CHARACTER SET utf8mb4; USE movie_recommend; CREATE TABLE users ( user_id INT NOT NULL AUTO_INCREMENT, user_name VARCHAR(64), gender CHAR(1), age INT, PRIMARY KEY (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE movies ( movie_id INT NOT NULL AUTO_INCREMENT, title VARCHAR(128), genres VARCHAR(255), PRIMARY KEY (movie_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE ratings ( user_id INT NOT NULL, movie_id INT NOT NULL, rating FLOAT NOT NULL, timestamp INT, PRIMARY KEY (user_id, movie_id), KEY idx_movie (movie_id), CONSTRAINT fk_rating_user FOREIGN KEY (user_id) REFERENCES users(user_id), CONSTRAINT fk_rating_movie FOREIGN KEY (movie_id) REFERENCES movies(movie_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明ratings表使用了联合主键(user_id, movie_id)这样同一个用户对同一部电影只能有一条评分记录数据在源头就不会重复。外键约束保证评分表里不会出现不存在的用户或电影。如果只做算法演示外键可以省略但论文里的ER图有外键会显得更规范。参数说明评分表的rating字段用FLOAT而不是INT因为算法预测出来是小数存储时用浮点类型更自然timestamp用INT存秒级时间戳避免在导入阶段处理日期格式。电影表的genres字段是电影类型可以是“|”分隔的字符串不拆表也不影响协同过滤的核心计算。导入数据时不建议一条条INSERT数据量一大就非常慢用MySQL的LOAD DATA命令一次性导入效率更高LOAD DATA LOCAL INFILE ratings.csv INTO TABLE ratings FIELDS TERMINATED BY , IGNORE 1 LINES (user_id, movie_id, rating, timestamp);导入完成后先用三条SQL验证数据查评分总数、查缺失用户、查评分分布。评分总数如果和源文件行数一致说明数据导入干净。我一般会再查一下每个评分分值的人数分布比如5分有多少条、1分有多少条这个分布写论文时也要用到。3.3 最小复现命令从原始数据到三条推荐结果我不想一上来就把整套源码丢给你因为源码文件多了出了问题你反而不知道是哪一环崩的。我通常先写一个不依赖Web框架的最小脚本把“读取数据、计算相似度、输出推荐”走通然后再对照源码把数据库连接和Web层加回去。这个最小脚本大约四十行所有同学都能跑。import pandas as pd # 1. 读取评分数据 ratings pd.read_csv(ratings.csv) # 只需 user_id, movie_id, rating 三列 # 2. 构建用户-电影评分矩阵空值补0 pivot ratings.pivot_table(indexuser_id, columnsmovie_id, valuesrating) # 3. 计算用户相似度矩阵 user_sim pivot.T.corr().fillna(0) # 4. 给某个用户做推荐 target_user 1 scores pd.Series(0.0, indexpivot.columns) for other_user in user_sim.columns: if other_user target_user: continue sim user_sim.loc[target_user, other_user] if sim 0: continue # 当前用户看过且评过分的电影不重复推荐 watched pivot.loc[target_user].notna() # 其他用户评过分的电影 candidate pivot.loc[other_user].notna() ~watched recent_ratings pivot.loc[other_user][candidate] scores[recent_ratings.index] recent_ratings * sim scores scores[pivot.loc[target_user].isna()] # 只看没看过的 top3 scores.sort_values(ascendingFalse).head(3) print(top3)逻辑说明这段代码的核心是拿目标用户和每个其他用户的相似度做加权其他用户评分越高、相似度越大目标用户对应的预测分就越高。watched用来过滤目标用户已经看过的电影这一步非常关键否则推荐会给出用户已经看过的内容。最后只保留评分矩阵里缺失的电影也就是用户还没看过的候选集。参数说明只看sim大于0的邻居避免负相似用户拉低预测分如果数据集足够大还应该限制邻居数量比如只取相似度最高的20到50个用户。这里没有做均值中心化实际预测会偏乐观但对最小演示来说足够直观。跑通之后你可以手动把pivot.T.corr()换成pivot.corr()就是ItemCF的雏形其他代码几乎不动。运行命令也很简单python min_reco.py如果你的数据表在MySQL里把第一行的read_csv改成从pymysql查询结果构造DataFrame即可。这一步跑通后再回头跑完整源码你就知道每一步在干什么了。4. 数据库设计与文档配合连接池、增删改查、万字论文写作结构4.1 三张表的增删改查怎么设计接口标题里有“数据库文档”论文要写数据库设计答辩要演示增删改查所以这三张表的CRUD不能只会用数据库工具点鼠标得会写SQL。我按最常见的需求列一组能直接用的SQL-- 插入新用户 INSERT INTO users (user_name, gender, age) VALUES (test_user, M, 23); -- 给用户添加一条评分模拟用户刚看完一部电影 INSERT INTO ratings (user_id, movie_id, rating, timestamp) VALUES (1, 128, 4.5, UNIX_TIMESTAMP(NOW())) ON DUPLICATE KEY UPDATE rating VALUES(rating); -- 删除一条评分记录 DELETE FROM ratings WHERE user_id 1 AND movie_id 128; -- 查询某个用户看过的所有电影带上电影标题 SELECT m.title, r.rating, r.timestamp FROM ratings r JOIN movies m ON r.movie_id m.movie_id WHERE r.user_id 1 ORDER BY r.timestamp DESC LIMIT 20; -- 统计评分分布这是实验部分最常用的查询 SELECT rating, COUNT(*) AS cnt FROM ratings GROUP BY rating ORDER BY rating;逻辑说明ON DUPLICATE KEY UPDATE是评分表最常见的写法因为联合主键决定了同一条评分记录如果重复插入会被更新而不是报错。这在模拟真实用户行为时很关键用户可能改了评分系统不能出现两条相互矛盾的数据。JOIN查询是推荐系统的核心读取方式算法模块从rating表拿到评分再关联movie表拿到标题展示给用户的时候才不会是movie_id。参数说明UNIX_TIMESTAMP(NOW())把当前时间转成秒级时间戳和表结构里的timestamp字段一致。如果你希望论文里写“时间戳可读性更好”也可以把字段类型改成DATETIME但那样导入数据时要多做一步时间格式转换我一般保留INT配合FROM_UNIXTIME()函数查询时可读。4.2 MySQL连接池配置为什么每次直连会卡死很多源码在数据库访问上犯同一个错每个请求都新建一个pymysql连接用完就关。放在命令行演示里没问题但一上Web页面刷新几次MySQL连接就会被频繁创建销毁拖垮轻则页面卡几秒重则直接报Too many connections。这时要用连接池。from dbutils.pooled_db import PooledDB import pymysql pool PooledDB( creatorpymysql, maxconnections10, mincached2, maxcached5, blockingTrue, hostlocalhost, port3306, userroot, password123456, databasemovie_recommend, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor ) def query_one(sql, paramsNone): conn pool.connection() try: with conn.cursor() as cursor: cursor.execute(sql, params) return cursor.fetchone() finally: conn.close()逻辑说明连接池启动时就预建2到5个MySQL连接业务代码每次只是“借”一个连接用完归还不会被销毁重建。maxconnections10表示池里最多10个连接超过这个数且有blockingTrue时请求会排队等待而不是直接报错。参数说明mincached是池中最少保持的连接数适合频繁读取的推荐服务maxcached是空闲时最多缓存的数量超过这个值的空闲连接会被释放。charset必须写utf8mb4否则中文电影标题在写入和读取时都可能出现乱码。cursorclass使用DictCursor后查询结果直接是字典列表flask里渲染模板时不用挨个下标取值。4.3 万字论文和大作业文档的写作结构评分表结构设计好了文档也要跟上。标题里的“万字论文”听起来吓人但其实有固定套路。我见过的高分论文章节划分几乎都是下面这份表格的样子论文章节建议字数写作重点摘要300字左右一段话说清系统做什么、用什么算法、达到什么效果需求分析与背景1500字推荐系统解决的问题、协同过滤的基本思想、项目目标数据集介绍1000字来源、数据量、字段含义、评分分布图算法设计2500字UserCF公式、相似度定义、评测指标这里是拿分核心数据库与系统设计2000字三张表ER图、接口设计、连接池方案实验结果与分析1500字对比表、TopN指标、典型推荐案例截图总结与展望1000字遇到的问题、改进方向比如冷启动和混合推荐万字论文不是说你每句话都要写满而是建议你在算法和实验这两章补齐内容。算法章把公式推导写清楚实验章给出至少三组对比结果比如K值取10/20/50时推荐准确率的变化。很多同学把论文写成源码说明书这是低分的主要原因。评委更想看“为什么这么设计”“效果差多少”而不是“这个函数调用了那个函数”。文档部分通常是需求分析、数据库设计说明、接口文档三件套。接口文档哪怕只有一个页面也要把请求参数和返回结果写清楚。视频演示脚本则可以按下面的节奏录制前30秒展示数据表和评分分布中间90秒演示用户登录后系统返回的推荐列表最后60秒打开终端看日志证明推荐结果是通过协同过滤算法计算出来的而不是写死数据。5. 避坑协同过滤电影推荐系统最容易翻车的6个地方5.1 冷启动新用户一进来推荐列表为空现象在页面上切换登录用户有的老用户能正常出推荐新注册用户或者只注册没评分的用户推荐区域一片空白控制台报KeyError。原因协同过滤是纯行为驱动的算法用户一条评分都没有算法找不到它的相似用户自然算不出预测分。这是协同过滤的先天缺陷不是代码bug。解决在推荐主流程里加一个兜底逻辑。当目标用户的评分数量少于5条或者相似用户列表为空时直接返回全局热门榜。热门榜可以用评分数和平均分的组合排序比如score avg_rating * log(count 1)避免只有少数人打了满分的小众电影排到最前面。这个方案不用改算法只要在接口层判断一下即可。5.2 相似度矩阵内存爆掉现象数据量从几百个用户涨到几千甚至几万用户时程序运行到计算用户相似度这一步就卡死然后报MemoryError。原因UserCF的相似度矩阵是用户数×用户数的矩阵一万个用户就是1亿个浮点数默认64位浮点大约占用800MB这还不包括中间变量。如果直接对全量用户两两求相关度本机内存很容易扛不住。解决有两个层次的做法。第一层只保留有共同评分的用户对这需要把评分矩阵转成稀疏格式用scipy.sparse或csr_matrix。第二层限制邻居数量比如只对每个用户保留相似度最高的30个邻居其余全部置0。实际工程里没人会算全量相似度矩阵都是先粗筛候选邻居再精算相似度。大作业做到这一步够交了。5.3 预测评分超出1到5分现象推荐结果里出现预测分7.8、甚至15.6这样的数值页面展示出来很滑稽。原因加权公式里相似度权重之和并不等于1。当几个高相似用户的评分都偏高时加权结果自然可能超过5分。另一个常见原因是没做均值中心化用户的绝对评分偏好没有被剔除。解决在输出层加一道规范化。最简单的是把所有预测值clip到[1,5]之间。稍微讲究一点的做法是让权重归一化即每个邻居的相似度除以该用户所有邻居相似度之和。两个方案可以同时用先在算法内部做均值中心化再在输出时clip一次保证展示的数据可用。5.4 MySQL中文乱码出现在电影标题和用户名里现象数据库工具里查数据正常但Web页面显示电影标题全是问号或者在flask日志里看到“ascii codec cant decode”的错误。原因这是三个环节有一环没设对。建表时用了默认latin1字符集或者pymysql连接参数没有指定charsetutf8mb4还或者页面模板本身没声明UTF-8编码。解决三处全部统一。建表语句用DEFAULT CHARACTER SET utf8mb4pymysql连接参数加charsetutf8mb4Web框架层确保flask的响应头是application/json; charsetutf-8。已经在latin1表里的脏数据导出重置不建议在线改字符集容易触发索引重建问题。5.5 MySQL连接报“Public Key Retrieval is not allowed”现象运行源码时数据库连接直接抛异常提示public key retrieval is not allowed。这个错误在使用MySQL 8.0以上版本时特别常见。原因新版MySQL客户端默认使用caching_sha2_password认证插件而连接参数里没有允许客户端向服务端请求公钥导致第一次认证时无法建立加密通道。解决在pymysql连接参数里加allow_public_key_retrievalTrue和use_sslFalse即可。另一个更稳妥的做法是给项目创建一个专用账号并指定mysql_native_password插件这样对源码改动最小。我之前为了省事直接在root账号上跑项目结果折腾半天后来改成专用账号后相关报错就再没出现。5.6 推荐结果全是热门电影个性化无从谈起现象不管切换到哪个用户推荐列表前几名永远是《肖申克的救赎》《霸王别姬》这类高分片两名不同用户的重合度高得吓人。原因协同过滤天然存在热门偏向。高分电影评分数多更容易和很多用户产生相似度同时其他用户评分高的热门片在加权重叠后也被反复推荐。解决分两层处理。第一层把目标用户已经看过的电影全部过滤掉这是底线。第二层在候选生成后加入热门惩罚一个简单做法是预测分减去一个与电影评分数量正相关的惩罚项比如popularity_penalty lambda * log(rating_count)lambda取0.1左右。这样热门片的预测分被压低长尾内容才有机会进入TopN。如果论文里能写一句“通过归一化缓解马太效应”评委看到这个点是会加分的。6. 验收前最后一步论文图表、演示视频和两个加分改动很多同学代码跑通就认为结束了其实答辩和文档才是课程设计拉开差距的地方。先说论文插图相似度矩阵不要截全量截前30个用户的子矩阵热力图直接用seaborn的heatmap画颜色越深代表两个用户口味越接近。再画一张TopN推荐的准确率曲线图横轴是推荐的电影数量N纵轴是PrecisionN。两张图放进去实验章的厚度立刻不一样。演示视频按三段式录先用Navicat或MySQL命令行展示三张表的数据量和评分分布再切到python代码讲一遍UserCF的核心逻辑最后运行系统真实操作一个用户查看推荐结果。全程控制在5分钟以内不要用PPT不要录代码逐行讲解过程比解说重要。加分改动只做两个就够。第一个是UserCF和ItemCF的融合推荐列表按权重合并UserCF占0.7ItemCF占0.3能明显改善结果多样性代码改动不超过10行但论文里可以多写一个小节。第二个是冷启动兜底新用户返回热门榜新电影只出现在相似物品推荐里把这个规则写进系统设计文档。我每次交这类大作业之前都会花二十分钟做一次“新环境演练”删掉venv从空环境开始装依赖、建库、导数据、跑演示脚本。这样能确保你交上去的文档里写的命令是真实可复现的而不是只在你这台电脑能跑。这个习惯帮我避过好几次运行时版本不兼容的尴尬也希望帮到你。本文还有配套的精品资源点击获取