
简介基于Python的在线电影推荐系统是一份面向毕业设计、课程设计及初学者的完整项目资源可用于理解推荐系统从数据采集到结果展示的落地流程。资源包含数据爬取、预处理、特征提取、相似度计算、推荐算法等核心模块并以Django搭建可视化展示界面。压缩包共392个文件涵盖Python源码.py、编译后的.pyc、Django配置、网页模板HTML/JS/CSS、素材图片jpg/png/gif及SQL数据库脚本等整体约9.76MB结构清晰便于直接运行与二次开发。目前已有255人学习下载。通过该项目读者可掌握基于内容与协同过滤的推荐思路学会电影元数据清洗与余弦相似度计算并了解如何将模型封装为Web应用同时附带的SQL数据文件和多类型静态资源也为本地环境搭建与界面调整提供了便利适合用于论文撰写、系统演示或作为进一步优化推荐算法的起点。1. 从一个 zip 说起这个电影推荐系统拿到手先该看什么很多第一次接触这个标题的人解压完 zip 之后的第一反应是找入口文件、装依赖、启动服务结果卡在数据上没有用户评分数据推荐结果是空的页面上只有一个空壳。这类基于 Python 的在线电影推荐系统本质是「推荐算法 Web 展示」两件事算法负责基于用户历史评分算出 Top-N 候选Web 层负责把结果送到浏览器里。它真正解决的是「用户没耐心翻片单系统替他筛」的问题适合课程设计、毕业设计也适合想入门推荐系统的人拿它当第一个可运行的项目。但你要有心理准备这类项目的核心不在前端样式而在评分矩阵的稀疏程度、相似度怎么算、冷启动怎么办。这些坑不提前摸清楚推荐结果就是一堆热门电影轮播跟不做推荐没区别。下文按我实际做推荐系统的顺序来讲先选型再处理数据再写算法再暴露到 Web 上最后把冷启动和评估这两个最容易翻车的环节单独拎出来说。2. 推荐算法选型为什么协同过滤是这类项目绕不开的默认答案2.1 三种主流算法的落地对比常见的推荐方案有三条路基于内容、协同过滤、混合推荐。基于内容的做法是给电影建画像用流派、导演、演员、关键词这些标签算电影之间的相似度用户看了 A 就推和 A 画像接近的 B协同过滤不看内容只看用户和电影之间的评分关系混合推荐就是把两者按权重拼起来。有一个多数人一开始想不明白的点基于内容看起来更“聪明”实际落地反而麻烦。你首先得有一份质量过得去的电影标签数据然后要处理标签的更新、冷启动新电影还要面对“同一个导演拍的烂片和神片被推荐成一样”的尴尬。协同过滤只用一张评分表用户看了什么、给了几分别的什么都不用知道数据结构天然适合起步。维度基于内容UserCF基于用户的协同过滤ItemCF基于物品的协同过滤输入数据物品标签/画像用户-物品评分矩阵用户-物品评分矩阵可解释性强可展示“因为你看过 X”强可展示“和你口味相似的人看过”强可展示“因为你看过 X所以推荐 Y”冷启动新物品一般标签不完整时就废了差没有评分就没有推荐差没有评分就没有推荐工程复杂度中需要维护画像低低相似度矩阵可离线预计算适合量级内容特征明确的场景用户数少、物品多的场景用户数多、物品少的场景电影这种场景用户量通常大于电影量所以 ItemCF 在工业界更常见推荐结果也更稳定。但课程设计和毕设里UserCF 因为 “人以群分” 的解释逻辑更容易讲清楚两套都实现一遍再融合是更稳的答辩话术。2.2 协同过滤的两个变体UserCF 与 ItemCF 的适用边界UserCF 的核心逻辑是找到“和我口味相似的一群人”把这群人看过而我没看过的电影按相似度加权排序推给我。它的问题在于用户和用户的相似度矩阵要实时算用户量一大就扛不住而且新用户第一二次评分进来很难立刻找到稳定的邻居。ItemCF 的核心逻辑是“和我评分过的电影相似的电影”物品之间的相似关系相对稳定可以离线算好存起来用户请求时只做一次查表和加权聚合延迟低很多。电影推荐场景里用户的兴趣是稳定的电影的数量也远小于用户数量ItemCF 更合理。真正落地时我不会只选一个。常见的做法是离线分别算好 UserCF 和 ItemCF 的候选线上用一个权重参数混合比如 0.4 的 UserCF 结果加 0.6 的 ItemCF 结果。这个权重不需要玄学直接在验证集上跑一遍哪个指标高就往那边偏。2.3 选型的量化判断离线指标与工程约束选哪条路不能靠感觉要落到两个维度。第一是离线指标精确率、召回率、覆盖率。精确率看推荐的 Top-N 里用户真的会看的有几个召回率看用户看过的电影里被推荐出来的比例覆盖率看推荐结果有没有集中在头部热门片。第二是工程约束数据量多大、训练内存够不够、接口能容忍多少延迟。“一文看懂推荐系统”这类入门文章里普遍也是把协同过滤当第一条基线深度学习方案是后面才提的进阶内容。这个 zip 项目如果直接上 SVD 或者双塔模型数据量就撑不起来训练一次要几分钟意义不大。先把 UserCF 和 ItemCF 跑通把精确率和覆盖率打出来再决定要不要上更复杂的模型这是我个人的顺序。3. 数据准备MovieLens 数据清洗与评分矩阵构建3.1 数据集字段与业务含义做电影推荐第一件事是找数据。这个领域最常用的公开数据集是 MovieLens它提供了用户对电影的评分字段不多但非常规整。新版 ml-latest-small 里 ratings.csv 和 movies.csv 是最核心的两张表tags.csv 一般用不上。文件字段说明ratings.csvuserId, movieId, rating, timestamp评分是 0.5 到 5.0步长 0.5movies.csvmovieId, title, genres流派字段是管道符分隔的字符串如 “Action|Adventure”这两张表通过 movieId 关联。算法侧只需要 ratings.csv构建用户-电影评分矩阵展示侧需要 join movies.csv否则推荐出来的 movieId 没法在页面上显示标题。数据格式看着简单但直接读进内存用的人多了后续矩阵构建、相似度计算的性能问题就全暴露出来。3.2 数据清洗逻辑与 pandas 实现不要拿到 CSV 就直接 pivot原始数据里藏着几个会坑你的细节。重复评分要按“最后一次评分为准”去重评分值域要在 0.5 到 5.0 之间不在范围内的直接剔除只评分过一两次的用户对邻居计算没有任何统计意义建议过滤掉。import pandas as pd ratings pd.read_csv(ml-latest-small/ratings.csv) # 第一步检查缺失值评分表一般没有缺失但无妨确认一下 print(ratings.isnull().sum()) # 第二步同一用户对同一电影可能评过分多次保留最后一次 ratings ratings.drop_duplicates( subset[userId, movieId], keeplast ) # 第三步过滤评分值域脏数据只会在矩阵里制造噪音 ratings ratings[ (ratings[rating] 0.5) (ratings[rating] 5.0) ] # 第四步把评分次数少于 5 次的用户剔除 user_cnt ratings[userId].value_counts() keep_users user_cnt[user_cnt 5].index ratings ratings[ratings[userId].isin(keep_users)] # 第五步用户评分次数超过 500 的也剔除防止看片大户稀释全局相似度 user_cnt ratings[userId].value_counts() ratings ratings[ratings[userId].isin(user_cnt[user_cnt 500].index)] print(清洗后规模:, ratings.shape)代码逻辑不复杂但第五步很多人会漏掉。看片大户一个人贡献了上千条评分他一个人就能把电影相似度矩阵拉向自己的口味整个推荐结果被他带偏。上限设多少看数据规模500 是我在多个数据集上的经验值数据量大时可以放宽到 1000。清洗完后要打印一下规模确认过滤没有把数据删得太狠一般来说清洗掉 5% 到 10% 是正常范围。3.3 构建 user-item 评分矩阵与稀疏度评估清洗完的 ratings 是长表算法需要的是宽矩阵行是用户列是电影值是评分。pandas 的 pivot_table 一行就能转但要注意两点空位用 0 填充因为协同过滤里 1 到 5 分明是有意义的值0 就代表“没看过”矩阵转完尽量用 scipy 的稀疏矩阵存不要用稠密 DataFrame 硬扛。import numpy as np from scipy.sparse import csr_matrix # 构建稠密矩阵用于建模后续转稀疏 user_movie ratings.pivot_table( indexuserId, columnsmovieId, valuesrating ).fillna(0) # 转成稀疏矩阵非零元素才代表有评分 sparse_m csr_matrix(user_movie.values) print(矩阵形状:, sparse_m.shape) # 稀疏度 1 - 非零元素占比电影推荐场景都在 90% 以上 n_rated sparse_m.nnz sparsity 1 - n_rated / (sparse_m.shape[0] * sparse_m.shape[1]) print(f非零评分数量: {n_rated}, 稀疏度: {sparsity:.4f})这里有个关键认知用户-电影矩阵永远极度稀疏ml-latest-small 数据集的稀疏度通常在 93% 以上。这意味着绝大多数用户和电影之间没有评分关系绝大多数“相似度”计算都是在大量零值上做的。理解这一点后面遇到相似度矩阵内存爆掉、推荐结果全是热门片你就知道根因在哪了。4. 核心代码用 Python 实现 UserCF、ItemCF再变成在线接口4.1 UserCF评分归一化、邻居选取与预测打分UserCF 的流程可以拆成四步计算用户间相似度、取 Top-K 邻居、用邻居的评分做加权、过滤掉已经看过的电影。相似度可以用皮尔逊相关系数但稀疏评分矩阵上余弦相似度更省事两者的效果差异在这个量级的数据上不明显。from sklearn.metrics.pairwise import cosine_similarity # 输入是稀疏矩阵直接算用户相似度矩阵 user_sim cosine_similarity(sparse_m) np.fill_diagonal(user_sim, 0) # 自己不能当自己的邻居 # 构建 userId 到矩阵行号的映射 user_ids user_movie.index.tolist() user_index {uid: i for i, uid in enumerate(user_ids)} def recommend_by_user_cf(user_id, top_n10, k20): if user_id not in user_index: return [] uid user_index[user_id] sim_row user_sim[uid] # 取相似度最高的 k 个用户作邻居 neighbor_ids np.argsort(sim_row)[::-1][:k] scores {} for nid in neighbor_ids: w sim_row[nid] if w 0: continue # 邻居评分过的电影及分数 rated_items sparse_m[nid].indices rated_vals sparse_m[nid].data for item, r in zip(rated_items, rated_vals): scores[item] scores.get(item, 0.0) w * r # 过滤掉当前用户已经看过的 watched set(sparse_m[uid].indices) ranked sorted(scores.items(), keylambda x: x[1], reverseTrue) return [item for item, _ in ranked if item not in watched][:top_n]这里的逻辑要点是加权求和来聚合邻居偏好。k 控制邻居数量太小则邻居不具代表性太大则低相似度用户带进噪音常见区间是 10 到 30。评分这里直接用原始分加权严格做法是减去用户均值做归一化再加权你可以把vals换成vals - user_mean[nid]试一下对评分标准严苛的用户更公平但对 Top-N 排序的实际影响有限。4.2 ItemCF物品相似度矩阵与 Top-N 生成ItemCF 的实现思路和 UserCF 几乎对称把稀疏评分矩阵转置行变成电影计算电影之间的相似度再对用户已评分的电影做加权聚合。# 稀疏矩阵转置行是电影列是用户 item_user sparse_m.T item_sim cosine_similarity(item_user) np.fill_diagonal(item_sim, 0) def recommend_by_item_cf(user_id, top_n10, candidate_k30): if user_id not in user_index: return [] uid user_index[user_id] # 当前用户评分过的电影 rated_items sparse_m[uid].indices rated_vals sparse_m[uid].data scores {} for idx, item in enumerate(rated_items): r rated_vals[idx] sim_row item_sim[item] # 每个已看过的电影只找最相似的 30 个候选 candidates np.argsort(sim_row)[::-1][:candidate_k] for c in candidates: w sim_row[c] if w 0 or c item: continue scores[c] scores.get(c, 0.0) w * r watched set(rated_items) ranked sorted(scores.items(), keylambda x: x[1], reverseTrue) return [item for item, _ in ranked if item not in watched][:top_n]candidate_k 是每个已评分电影拉取的候选数量设成 30 是为了避免用户看过 200 部电影时循环内部做 200 次全量数组扫描。这是个典型的空间换时间的取舍相似度矩阵可以离线算好存内存线上的预测只需要查表和累加。ItemCF 的推荐结果天然带有物以类聚的解释性页面展示时可以直接写“因为你评价过《XXX》”。4.3 Flask 接口与前后端联调把推荐变成在线服务算法跑通只是第一步标题里有“在线”二字就必须把它变成能通过浏览器访问的服务。一般这种项目用 Flask 就够不用上 Django。目录结构通常是应用入口、推荐服务模块、模板三块。project/ ├── app.py # Flask 入口 ├── recommend/ │ ├── __init__.py │ ├── data_loader.py # 数据读取与矩阵构建 │ ├── cf.py # UserCF / ItemCF 实现 │ └── hybrid.py # 混合策略与热门回退 ├── templates/ │ └── index.html ├── static/ └── requirements.txtFlask 的接口设计我一般会做两件事一个是 POST JSON 而不是 GET避免把用户 ID 塞进 URL 被日志记录另一个是把异常分支写清楚用户不存在时别报 500直接回退到热门榜。from flask import Flask, request, jsonify, render_template from recommend.cf import recommend_by_item_cf from recommend.hybrid import hybrid_recommend, popular_items app Flask(__name__) app.route(/) def index(): return render_template(index.html) app.route(/api/recommend, methods[POST]) def recommend(): data request.get_json() user_id data.get(user_id) top_n int(data.get(top_n, 10)) if not user_id: return jsonify({code: 1, msg: user_id 不能为空}), 400 try: item_ids hybrid_recommend(user_id, top_ntop_n, w0.6) return jsonify({code: 0, data: item_ids}) except KeyError: # 冷启动兜底新用户直接推荐热门片 return jsonify({code: 0, data: popular_items(top_n)}) if __name__ __main__: # debugTrue 只用于本地部署到公网务必关掉 app.run(host0.0.0.0, port5000, debugTrue)接口层的参数 top_n 由前端传入意味着前端可以做成滑块或者下拉框让用户自己选想看几条。常见的做法是前端用 fetch 把 user_id 和 top_n 以 JSON 格式 POST 到接口拿到返回的电影 ID 列表后再通过 movies.csv 映射成标题和海报路径渲染成卡片列表。这里要注意一个工程边界真正的在线推荐系统还需要用户注册登录、行为埋点、AB 测试zip 里的教学项目一般只能做到表单输入用户 ID 和推荐结果展示这一层。不用给自己加戏但要把这一层的职责想清楚算法负责候选排序Flask 负责参数解析和响应包装模板负责渲染。5. 避坑指南冷启动、稀疏矩阵、相似度更新和热门霸榜5.1 冷启动新用户进入系统推荐接口直接返回空现象用户完成注册后首次登录前端页面一片空白刷新也没有内容。原因协同过滤的模式是“从历史行为找相似”新用户没有评分记录相似度矩阵里没有他的位置加权聚合的结果就是空列表。解决回退到热度兜底用全局最受欢迎的电影来填充。popular_items的排序不要只看平均分要看评分人数和平均分的组合否则会出现一部只有 3 个人打了满分就排第一的极端情况。def popular_items(top_n10, min_votes10): stats ratings.groupby(movieId)[rating].agg([mean, count]) stats stats[stats[count] min_votes] # 平均分和评分人数的加权公式可自行调整 stats[score] stats[mean] * stats[count] / (stats[count] 50) stats stats.sort_values(score, ascendingFalse) return stats.head(top_n).index.tolist()除热度兜底外更稳妥的做法是在推荐结果里固定插入一两部高口碑新片给新电影一个曝光机会否则新片永远进不了评分循环。线上系统还会用内容画像来缓解冷启动但这个项目的数据量不太撑得起。5.2 稀疏矩阵相似度计算慢到怀疑人生现象数据从 100k 换成 1M 之后cosine_similarity卡了十几秒才出结果有时候直接内存报错。原因评分矩阵 93% 以上是零值但如果你用的是稠密 numpy 数组余弦相似度实现会把稀疏矩阵转成稠密矩阵再算。n_user1 万人、电影 5000 部item_sim 矩阵就有 5000 × 5000 个浮点数内存 200MB 起步。解决确认输入是csr_matrix并且相似度输出也用稀疏结构。更实用的是只保留 Top-K 邻居把相似度矩阵剪枝成稀疏字典牺牲一点精度换取数量级的内存收益。from collections import defaultdict def build_sparse_sim(sparse_matrix, k20, threshold0.1): sim cosine_similarity(sparse_matrix) out defaultdict(dict) for i in range(sim.shape[0]): row sim[i] # 只保留前 k 个邻居且相似度要大于阈值 top np.argsort(row)[::-1][:k] for j in top: if row[j] threshold: out[i][j] float(row[j]) return out剪枝后的相似度矩阵可以直接用 pickle 或 np.save 存到磁盘启动时加载不用每次重启都重算。参数上k20、threshold0.1是我常用的组合在稀疏场景下低于 0.1 的相似度基本都是随机噪声。5.3 相似度矩阵更新数据一直在变推荐结果永远不变现象系统跑了一周新评分加进来不少但用户每天看到的推荐列表和上线第一天完全一样。原因相似度矩阵是启动时一次性算好的后续新增的评分数据根本没有回流到矩阵里。解决把“离线计算相似度”和“在线推荐”彻底解耦。每天凌晨用定时任务重算相似度矩阵写入文件应用定期重新加载。数据量在万级以内时全量重算成本很低完全没有必要做实时增量数据量增大后再考虑增量更新比如只更新新增评分涉及的行列。架构上建议把相似度矩阵的加载和推荐逻辑拆成两个模块矩阵文件更新时应用能热加载而不是每次都要重启 Flask 进程。重算期间接口短暂不可用的问题用双缓冲解决写入临时文件写完后原子替换加载逻辑优先读新文件。5.4 热门霸榜推荐结果永远是大片冷门佳片毫无存在感现象无论用户对文艺片多感兴趣推荐列表里永远是《战狼》《流浪地球》这类高热度电影。原因加权求和的公式里一部电影被评分的次数越多它出现在候选里的频率就越高ItemCF 的相似度矩阵也会向热门电影聚拢。这不是 bug是协同过滤的天生倾向。解决对评分做时间衰减近期的行为给予更高权重避免三年前的一次高分影响今天的推荐对候选做降热惩罚统一乘以log(1 / popularity)压低热门电影的权重或者按热度分桶把电影切分成高中低三档每档至少保留一个推荐位。热度分桶效果最直接但需要你维护一个热度分桶表。6. 最后再验证一次离线切分评估与增量更新的小闭环推荐系统做完了不能光看界面顺眼就收工。你需要一个可重复的评估流程否则每次调参都是开盲盒。常见做法是模拟线上场景按用户维度切分出训练集和测试集训练集用来构建相似度矩阵测试集用来验证推荐效果即用户真实看过的电影里有多大比例出现在推荐榜单里。from sklearn.model_selection import train_test_split # 按用户分组切分同一个用户的评分只落在一侧 train_parts, test_parts [], [] for uid, group in ratings.groupby(userId): if len(group) 2: continue tr, te train_test_split(group, test_size0.2, random_state42) train_parts.append(tr) test_parts.append(te) train_df pd.concat(train_parts) test_df pd.concat(test_parts) # 用训练集构建评分矩阵和相似度矩阵流程与第 3、4 章相同 # 评估测试集里用户真实看过的电影出现在 Top-N 中的比例就是召回率 def evaluate(rec_func, test_df, top_n10): hits, total 0, 0 for uid, group in test_df.groupby(userId): actual set(group[movieId].values) recs rec_func(uid, top_ntop_n) hits len(actual.intersection(recs)) total len(actual) return hits / total if total else 0.0这个评估脚本最大的价值是让调参脱离玄学。k20 比 k30 好还是 w0.6 比 w0.5 好跑一遍脚本看数字比自己翻推荐列表猜靠谱得多。实践中我一般会同时打印精确率和覆盖率精确率看推荐准不准覆盖率看推荐列表有没有被头部电影垄断。增量更新方面我的个人习惯是不做实时增量而是在系统里留一个追加评分文件白天用户产生的新评分先落到 CSV凌晨定时任务统一合并进评分表、重算相似度矩阵、写缓存应用侧定期重载。数据量到百万级之前这套朴素方案比任何花哨的在线学习都稳定。另外我会在推荐接口里埋一行日志记录每次请求的 user_id、返回条数、耗时第二天翻一遍日志哪个用户的推荐列表连续几天都一样哪个用户调接口一直报错一目了然。这类项目最容易犯的错是为了炫技引入复杂模型结果数据量撑不起来效果反而比协同过滤差。先把协同过滤的评估闭环跑顺再谈换模型是我做推荐系统这几年最受用的经验。希望帮到你。本文还有配套的精品资源点击获取