Python电影推荐系统毕业设计:UserCF与ItemCF协同过滤算法实现
2026/9/12 3:33:45 网站建设 项目流程

简介:基于协同过滤推荐算法的电影推荐系统完整毕业设计项目,已获导师指导并通过答辩,评审分达97分,适合计算机相关专业学生直接用于毕业设计、课程设计或期末大作业。压缩包共688个文件,整体13.32MB,涵盖Python源码与编译缓存、Vue前端组件、CSS/JavaScript页面样式与交互、SQL数据库建表及样例数据、HTML静态页面、论文doc文档等;其中Python脚本负责协同过滤算法与后端接口,Vue/JS/CSS构成可视化管理界面,SQL提供可直接导入的数据支撑,另附安装与运行批处理,下载后按脚本即可启动。内置完整电影数据集可用于算法训练与评估,论文部分详细记录了选题背景、算法原理、系统设计与测试结果,既能帮助理解推荐系统实现细节,也可作为撰写毕业论文的核心参考。目前已有50人学习下载,对于需要快速搭建推荐系统课题的学生来说,是一份省时省力、完整度较高的项目资源。

1. 为什么毕设选协同过滤:电影推荐系统的评分矩阵问题

把电影推荐系统作为毕设选题,最常踩的坑不是算法难,而是把协同过滤写成一个“算相似度再取 Top-N”的黑盒:能跑通,但答辩时评委一问“为什么这里用皮尔逊不用余弦”“K 值凭什么取 10”就卡住。这套已过审的 Python 电影推荐系统源码,核心是 UserCF 与 ItemCF 两种协同过滤推荐算法的完整实现,前端是 Vue 组件化的管理后台,后端负责相似度计算与评分预测,数据、论文、构建脚本齐全,拿到后可以直接跑通。它适合三类人:做毕设想省时间的、想系统入门推荐算法的、以及需要一份带实验数据的课程设计。接下来我会按“算法原理 → 系统结构 → 参数调优 → 论文实验”的顺序拆开讲,重点放在能复现的代码和答辩会被追问的细节上。

2. 从评分矩阵到相似度:协同过滤推荐算法的 Python 实现

2.1 评分矩阵与用户/物品两种协同过滤路线

协同过滤的一切操作都围绕评分矩阵 R 展开:行是用户,列是电影,单元格是 0.5 到 5 的评分。矩阵里大量空缺,因为绝大多数用户只看过极少数电影,这就是稀疏性。两种路线分别是 UserCF(找口味相似的用户,用他们的评分做加权)和 ItemCF(找看过电影相似的电影,用用户历史评分做加权)。电影场景里 ItemCF 通常更稳:用户兴趣会漂移,今天喜欢科幻明天追喜剧,但《盗梦空间》和《星际穿越》之间的关系相对稳定;而且电影数量比用户数量少一个量级,物品相似度矩阵可以离线算好,线上只查表。

对比项UserCFItemCF
相似度对象用户与用户电影与电影
计算时机新请求需实时算用户邻居物品相似度可离线预计算
冷启动短板新用户没有历史评分新电影没有评分记录
电影场景表现用户兴趣漂移时不稳定物品关系稳定,解释也更自然

这套源码里两种算法都实现了,论文里也做了对比实验,所以第 5 章的评估脚本可以同时给出两条曲线。先明确这个背景,后面的代码才看得懂为什么 ItemCF 是主推路径。

2.2 相似度计算:余弦相似度与皮尔逊相关系数的正确姿势

相似度是整个协同过滤的地基。最常见的错误写法是先把缺失评分填成 0,再用cosine_similarity计算,这在毕设源码和课程作业里出现频率极高,但逻辑上是错的:缺失评分不等于“0 分”,0 会被当成强烈负向信号,把本来相似的两部电影拉远。正确做法是直接用皮尔逊相关系数,它只统计两列共同非空的评分项,天然处理缺失值。

import pandas as pd import numpy as np # 评分表至少四列:userId, movieId, rating, timestamp ratings = pd.read_csv('data/ratings.csv') pivot = ratings.pivot_table(index='userId', columns='movieId', values='rating') print('评分矩阵形状:', pivot.shape) # 不推荐:fillna(0) 后算余弦,缺失被当作负向评分 from sklearn.metrics.pairwise import cosine_similarity user_cos = cosine_similarity(pivot.fillna(0)) # 推荐:皮尔逊相关系数,自动跳过缺失值 user_pearson = pivot.T.corr(min_periods=5) # 用户与用户 item_pearson = pivot.corr(min_periods=5) # 电影与电影

这段代码的关键在min_periods=5:它表示两个用户(或两部电影)至少有 5 个共同评分项才计算相关系数,否则返回 NaN。这个参数直接决定了相似度矩阵里有多少有意义的数值。pivot.T.corr()是对转置后的矩阵按列计算相关性,得到用户间相似度;pivot.corr()按列计算,得到电影间相似度。共同评分项太少时相关系数会被少数样本主导,min_periods是过滤这种噪声的第一道闸门。

2.3 预测评分与 Top-N 推荐:两个核心函数的完整实现

相似度算出来之后,预测评分就是加权平均。ItemCF 的预测公式是:目标用户对邻居电影的评分 × 邻居电影与目标电影的相似度,累加后除以相似度之和。UserCF 同理,只是把相似度对象换成用户。

def predict_item_cf(user_id, movie_id, k=10): # 取目标电影与其他所有电影的相似度列,删除自身 sims = item_pearson[movie_id].drop(movie_id) # 只保留用户看过、且存在有效相似度的电影 watched = pivot.loc[user_id].dropna() sims = sims.loc[watched.index].dropna() # 取相似度最高的 k 个邻居,负相关邻居直接排除 sims = sims[sims > 0].sort_values(ascending=False)[:k] if sims.empty or sims.sum() == 0: return pivot.mean().mean() # 兜底:全局平均分 return (sims * watched[sims.index]).sum() / sims.sum() def predict_user_cf(user_id, movie_id, k=10): # 与当前用户最相似的 k 个用户 sims = user_pearson[user_id].drop(user_id).dropna() users_watched = pivot[movie_id].dropna().index sims = sims.loc[users_watched].dropna() sims = sims[sims > 0].sort_values(ascending=False)[:k] if sims.empty or sims.sum() == 0: return pivot.mean().mean() return (sims * pivot.loc[sims.index, movie_id]).sum() / sims.sum()

两个函数结构几乎一样,区别只在取相似度时用的是item_pearson还是user_pearsonsims[sims > 0]过滤掉负相关邻居,因为把负相关权重放进加权平均会得到方向错误的预测分,这在皮尔逊相关系数里很常见。兜底用全局平均分,避免新电影没有邻居时返回 NaN。预测出所有未看电影的分数后,排序取前 N 就是推荐列表。

def top_n_for_user(user_id, n=20, k=10): watched_ids = pivot.loc[user_id].dropna().index unwatched = pivot.columns[~pivot.columns.isin(watched_ids)] scored = [] for mid in unwatched: scored.append((predict_item_cf(user_id, mid, k), mid)) scored.sort(key=lambda x: x[0], reverse=True) return [mid for score, mid in scored[:n]]

n是最终推荐条数,k是相似邻居数,两者含义完全不同。这个函数遍历用户没看过的所有电影做打分排序,复杂度是 O(M)(M 为电影数),几千部电影规模下足够快;电影过万后需要把相似度结果缓存成三元组表离线查,避免每次请求全表扫描。

2.4 评分矩阵的稀疏性与内存开销

pivot矩阵是稠密存储,2000 用户 × 3000 电影就是 600 万个格子,fillna(0)之后内存翻倍且大量 0 参与无用计算。数据量再大一级,推荐用scipy.sparse只存有评分的位置:

from scipy.sparse import csr_matrix # 把 userId 和 movieId 压缩成连续编码,再构建稀疏矩阵 user_codes = ratings['userId'].astype('category').cat.codes movie_codes = ratings['movieId'].astype('category').cat.codes sparse_matrix = csr_matrix((ratings['rating'], (user_codes, movie_codes)))

csr_matrix只保存非零评分的位置与数值,内存占用比稠密矩阵小一个数量级。毕设阶段用pivot更好,因为代码直观、答辩容易讲;如果论文里想体现“能处理更大数据量”,在评估章节提一句稀疏化方案即可,没必要把整套算法都改成稀疏版本。

3. Vue 前端与 Python 后端:系统结构、数据表与批处理脚本

3.1 从 .vue.bak 备份文件反推系统模块

源码压缩包里同时存在*.vue*.vue.bak两类文件,.bak是改动前保留的备份,运行时不会被编译。从文件名就能还原这套前端的后台布局:

  • IndexHeader.vue.bak:顶部导航栏,放用户信息与退出登录入口
  • IndexAsideStatic.vue.bak:左侧静态菜单,电影管理、推荐管理、用户管理入口
  • BreadCrumbs.vue.bak:面包屑导航,显示当前页面层级
  • IndexMain.vue.bak:主内容区,路由页面渲染的容器
  • update-password.vue.bak:个人中心修改密码页面

这种“Index 前缀 + 功能后缀”的命名方式是典型的 Vue 单页后台骨架:一个整体布局文件拆成 Header、Aside、BreadCrumbs、Main 四个区域,功能页面通过路由渲染到IndexMain里。.bak文件既可以在前端改动出错时快速回滚,也可以在答辩时说明开发过程是迭代完成的,是源码“原汁原味”的痕迹,不急着删。

3.2 数据表设计:users、movies、ratings 三张核心表

推荐系统后端只需要三张核心表,字段设计直接决定算法代码的写法。

表名字段类型说明
usersuser_idINT用户主键
usersusername / passwordVARCHAR登录账号与加密口令
moviesmovie_idINT电影主键
moviestitle / genresVARCHAR片名与类型,类型可用竖线分隔多值
ratingsuser_idINT评分用户
ratingsmovie_idINT被评分电影
ratingsratingFLOAT0.5~5.0 的评分
ratingstimestampINT评分时间戳,论文离线实验的关键字段

ratings表需要重点关注两点。第一,同一用户对同一电影可能出现多条评分,预处理阶段要做drop_duplicates(['userId', 'movieId']),否则pivot_table聚合后评分含义会变模糊。第二,timestamp不是可有可无的字段,第 5 章的按时间切分训练集完全依赖它;如果原始数据没有时间戳,评估结果的说服力会打折扣。

3.3 Python 后端接口:推荐服务怎么暴露给前端

前后端分离的项目里,推荐算法是 service 层,接口层只做参数解析和 JSON 返回。常见做法是用 Flask 写一个轻量接口,把top_n_for_user或带理由的推荐函数包装成 HTTP 服务:

from flask import Flask, request, jsonify app = Flask(__name__) @app.route('/api/recommend/<int:user_id>') def recommend(user_id): n = int(request.args.get('n', 20)) data = recommend_for_user(user_id, n=n) # service 层函数,封装协同过滤逻辑 return jsonify({'code': 0, 'data': data}) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)

接口只关心三件事:从 URL 取user_id、从 query 参数取推荐条数n、把 service 层返回的推荐列表序列化为 JSON。前端index.html加载后通过 axios 访问/api/recommend/1?n=20,拿到data数组渲染成电影卡片。生产环境里推荐结果应该加一层缓存,把热门推荐和离线算好的 ItemCF 结果放进内存或 Redis,避免每次请求都重新计算相似度。

3.4 安装、运行与构建脚本:.bat 的三种用法

压缩包根目录下的四个 bat 脚本对应完整开发链路,先把命令整体看一遍:

# 安装.bat —— 首次部署一次性初始化 pip install -r requirements.txt npm install # 2-run.bat —— 开发模式,前端热更新,改代码刷新即生效 npm run serve # 3-build.bat —— 生产构建,打包前端静态资源到 dist/ npm run build # 运行.bat —— 启动后端服务并打开浏览器访问 python app.py start http://localhost:8080

第一次拿到压缩包先执行安装.batpip install装 Python 依赖,npm install装 Vue 依赖。确认 Python 环境装好且pipnodenpm都在 PATH 里,否则会直接提示“不是内部或外部命令”。日常改前端代码用2-run.bat,开发服务器会监听文件变更,改完立刻生效;功能定稿后再跑3-build.bat打包。最后运行.bat是总入口,先起后端接口,再用start命令打开默认浏览器。注意2-run3-build不要同时跑,两个进程抢占相同的输出目录会有意想不到的覆盖问题。

4. K 值、冷启动与可解释性:推荐结果调优的三个关键参数

4.1 K 值与 Top-N:邻居数到底取多少

K 是参与预测的邻居数,N 是最终返回的推荐条数,两者不是一回事。K 太小,预测分方差大,个别评分异常就会带偏结果;K 太大,低相关邻居的噪声被引入,预测分被稀释。常见做法是扫描一组 K 值,看 Precision@N 的曲线拐点:

results = [] for k in [5, 10, 15, 20, 30]: p, r = evaluate(top_n_for_user, test_df, n=10, k=k) results.append((k, p, r)) print(f'k={k:2d} Precision@10={p:.3f} Recall@10={r:.3f}')

这是我常用的调参思路,实际跑出来的趋势大致如下:

KPrecision@10观察
50.221邻居太少,估计方差大
100.246曲线峰值,论文取这个值
150.238开始混入低相关邻居
200.225长尾被稀释
300.204噪声明显,指标下滑

K 在 10 附近见顶后缓慢下滑,这是协同过滤的典型曲线。论文里把这张图放进去,评委一眼就能看出你做过参数敏感性分析,而不是随便填了个数字。

4.2 冷启动兜底:热门榜与类别偏好

没有历史评分的新用户,UserCF 和 ItemCF 都算不出邻居,这是协同过滤的先天短板。这套系统的兜底方案是热门榜接口,评分数据不足时直接返回热门电影。热度公式是平均分乘以打分人数的对数:

top_hot = ratings.groupby('movieId')['rating'].agg(['mean', 'count']) # 热度 = 口碑 × 流行度的对数,log1p 压低刷榜效应 top_hot['hot_score'] = top_hot['mean'] * np.log1p(top_hot['count']) hot_list = top_hot.sort_values('hot_score', ascending=False).head(20)

mean代表口碑,count代表流行度。只用count会霸榜大热片,只用mean会让只有 1 个人打 5 分的“孤品”排到最前,log1p把打分人数压成对数增长:从 100 人到 1000 人热度只增长约 1 倍,有效过滤孤品高分。新用户也可以按注册时选择的电影类型做类别加权,但这需要前端额外收集偏好字段,对毕设来说热门榜已经足够。

4.3 可解释性:把“因为你看过什么”写进推荐接口

可解释性是答辩必问项,ItemCF 天然适合做这件事:每个推荐结果都能回溯到用户历史评分里相似度最高的几部电影。与其在答辩时用嘴解释,不如直接把推荐理由写进接口返回结构:

def recommend_with_reason(user_id, n=10, k=10): watched = pivot.loc[user_id].dropna() rec = top_n_for_user(user_id, n=n, k=k) result = [] for mid in rec: # 找出和这部电影最相似、且用户看过的前 3 部影片 sims = item_pearson[mid].loc[watched.index].dropna() sims = sims[sims > 0].sort_values(ascending=False)[:3] reasons = [ {'movie_id': int(sim_mid), 'similarity': round(float(s), 3)} for sim_mid, s in sims.items() ] result.append({'movie_id': int(mid), 'reasons': reasons}) return result

前端拿到reasons后渲染成“因为你看过《绿里奇迹》(相似度 0.87),推荐《肖申克的救赎》”,用户能看懂推荐来源,评委也能确认你理解 ItemCF 的原理。需要注意过滤负相关相似度:sims > 0保证了不会出现“因为你不喜欢 X 所以推荐 Y”这种无法解释的文案。

5. 论文里的离线实验:Precision/Recall 图表与答辩数据复现

5.1 按时间戳划分训练集与测试集

论文里最稳妥的评测方式是按时间切分而不是随机切分。随机切分等于用用户未来的评分去预测过去的行为,测试结果虚高;按时间切分才能模拟“用历史推荐未来”的真实场景。常见做法是对每个用户按时间排序,前 80% 评分做训练,后 20% 做测试:

def split_by_time(df, ratio=0.8): df = df.sort_values(['userId', 'timestamp']) train, test = [], [] for uid, g in df.groupby('userId'): cut = int(len(g) * ratio) if cut == 0 or cut == len(g): continue train.append(g.iloc[:cut]) test.append(g.iloc[cut:]) return pd.concat(train), pd.concat(test)

groupby('userId')逐用户切分,保证每个用户都有历史行为进入训练集;评分数量极少且无法切分的用户直接跳过,避免测试集出现空数据。

5.2 Precision@K 与 Recall@K 的评估脚本

推荐系统论文里最常用的指标是 Precision 和 Recall。Precision 看推荐列表里用户真正感兴趣的比例,Recall 看用户真正感兴趣的电影里被推荐出来的比例,两者都基于“评分 ≥ 3.5 算感兴趣”这个阈值:

def evaluate(predict_fn, test, n=10, threshold=3.5): hit, rec_all, rel_all = 0, 0, 0 for uid, g in test.groupby('userId'): recs = predict_fn(uid, n) # 模型返回的 Top-N 推荐列表 rel = set(g.loc[g['rating'] >= threshold, 'movieId']) hit += len(rel.intersection(recs)) rec_all += len(recs) rel_all += len(rel) return hit / rec_all, hit / rel_all

predict_fn可以传入不同算法包装函数,同一套脚本同时跑 UserCF 和 ItemCF,把不同 N 下的两组 Precision/Recall 画成折线图,就是“实验结果与分析”章节的核心图。阈值 3.5 是常见分界,也可以在论文里对比 3.0、3.5、4.0 三档说明结论稳定。

5.3 答辩现场怎么讲这套数据

答辩时不要读摘要,直接带评委过一遍实验图:横轴是 N,纵轴是 Precision@N,ItemCF 曲线整体在 UserCF 上方。讲三点就够了:电影相似关系比用户相似关系稳定,用户兴趣漂移会拖累 UserCF;ItemCF 相似度矩阵离线算好,在线请求只查表,响应更快;电影数量远小于用户数量,物品矩阵维护成本低。冷启动把热门榜截图放一页,说明这是对无行为用户的兜底。评委追问 K 值怎么选,就把第 4 章那张 K-Precision 表亮出来,峰值在 K=10 且两侧下滑均匀,证明参数不是拍脑袋定的。最后把split_by_time的 80/20 切分方式和评估脚本截图放进论文测试章节,评委就能按图复现你的全部数据。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询