
简介一套完整的基于Python的电影推荐系统项目代码覆盖物品协同过滤、用户协同过滤与基于内容的推荐三种主流算法并实现混合推荐逻辑可直接作为毕业设计或推荐系统入门实践。压缩包共2000个文件其中包含11个Python源码、5个HTML交互页面、配套CSS/JS前端样式、以及大量图片数据集整体体积约257MB目录结构清晰便于直接运行与二次开发。项目实现了用户-物品评分矩阵构建、余弦相似度/皮尔逊相关系数/奇异值分解SVD等关键算法并整合数据预处理、特征提取、模型训练与评价模块前端可展示推荐结果、历史记录与影片详情帮助学习者从原理到工程完整掌握推荐系统构建。当前已有112人学习使用适合具备Python基础、希望深入理解推荐算法或快速搭建毕设项目的人群。1. 电影推荐系统三种算法都实现不只是为了凑毕设工作量一个反直觉的事实最能让用户觉得“系统懂我”的推荐往往不是基于用户喜好分析出来的而是基于“和我口味一样的人还看了什么”算出来的。物品协同过滤和用户协同过滤本质上是同一份评分矩阵的两个方向基于内容的推荐却完全不看评分只看电影本身长什么样。三种算法看起来目标一致失效的场景却截然不同。这套基于 Python 的电影推荐系统把物品协同过滤、用户协同过滤和基于内容的推荐放在同一个项目里数据从 index.html 进入经过后端模型后由 recommend.html 输出推荐列表detail.html 展示电影详情history.html 保存用户行为evaluate.html 做离线效果评估。它要解决的问题很具体评分数据永远稀疏新电影永远存在用户兴趣随时在变任何一种单独算法都应付不了这三种情况。搞懂这套代码不只是为了完成一件毕业设计。从用户-物品矩阵的构造到相似度计算、SVD 降维、KNN 近邻再到混合推荐和离线评估你会把推荐引擎最核心的底层机制串起来。这也是我接下来要拆的内容先看数据底座再看三种算法各自怎么 work最后落到它们怎么在一套系统里互相兜底。2. 从打分记录到二维矩阵稀疏矩阵为什么是三种算法的共同战场推荐系统所有协同过滤逻辑的起点都避不开一张用户-物品评分矩阵。很多人第一次接触推荐算法时注意力都放在“余弦相似度”“SVD”这些名词上却忽略了评分矩阵本身的构造方式会决定后续一切计算的含义。如果矩阵里缺失值被粗暴填成 0算出来的推荐往往会出现“用户没看过什么就给他推什么”的翻车现场。2.1 用户-物品评分矩阵怎么构造从三列原始记录到二维表原始行为数据通常是长表格每一行是一条记录user_id、movie_id、rating。rating 可以是用户主动打分也可以是隐式反馈比如播放次数、停留时长。推荐系统第一步就是把这些行为记录透视成二维矩阵行是用户列是电影单元格是对应用户对电影的评分。import pandas as pd # ratings.csv 至少有三列user_id, movie_id, rating ratings pd.read_csv(ratings.csv) # pivot_table 将长表转成宽表 # indexuser_id 生成行columnsmovie_id 生成列 # 缺失值表示“用户没有对这部电影产生任何评分行为” matrix ratings.pivot_table( indexuser_id, columnsmovie_id, valuesrating, aggfuncmean # 同一个用户对同一部电影多次评分时取平均 ) # 矩阵形状 用户数 x 电影数 print(matrix.shape) print(matrix.iloc[:5, :8])这段代码里的 pivot_table 本质上是在做数据结构的重排原始表是“事件流”矩阵是“特征空间”。aggfuncmean 保证了同一用户重复评分时不会因为一天点进去三次就把分数重复计算。构建完成后matrix 中绝大多数格子都是 NaN这就是后续所有稀疏性问题的源头。有了矩阵之后稀疏度可以直接量化# count() 统计每个单元格非空数量sum() 得到总的有效评分个数 non_null matrix.count().sum() total matrix.shape[0] * matrix.shape[1] sparsity 1 - non_null / total print(f有效评分数: {non_null}) print(f稀疏度: {sparsity:.2%})在常见电影数据集里稀疏度基本都超过 95%有的甚至达到 99%。这意味着矩阵中每一百个格子里能找到的评分不足一个。协同过滤要在这片大量缺失的数据上找相似关系所以“缺失值怎么处理”就不是一个可以拍脑袋决定的问题。2.2 稀疏度与冷启动评分缺失真的等于 0 吗处理缺失值最容易犯的错误就是直接用 0 去填充稀疏矩阵然后丢给余弦相似度计算。0 在评分语义里代表“极度不喜欢”但缺失值并不等于用户讨厌这部电影更常见的解释是“用户还没看过”或“用户看了但没打分”。把两种语义混在一起相似度会被严重扭曲一部 8 分的电影和一部 3 分的电影可能因为大量共同“未看”的 0 被算成高度相似。较保守的做法是填充用户自身评分的均值或者不填充直接让稀疏矩阵进入分解类算法。比如用 scikit-learn 的 cosine_similarity 时它内部并不会自动处理 NaN常见做法是先做坐标压缩或者用均值填充后再计算from sklearn.metrics.pairwise import cosine_similarity # 用户均值填充保留用户评分倾向的基线 user_mean matrix.mean(axis1) filled matrix.apply(lambda row: row.fillna(user_mean[row.name]), axis1) # 物品均值填充保留电影本身的口碑基线 item_mean matrix.mean(axis0) filled matrix.apply(lambda row: row.fillna(item_mean), axis1) # 最后把仍然可能存在的 NaN 补为全局均值确保相似度能算 global_mean matrix.stack().mean() filled filled.fillna(global_mean) sim cosine_similarity(filled.T) # 物品间相似度选择用户均值填充还是物品均值填充背后是两种假设用户均值填充认为“一个用户的口味偏好在不同电影上是一致的”物品均值填充认为“同一部电影在不同用户心中有一个稳定的平均口碑”。实际项目中通常两者都算一遍再在离线评估里选分数更高的那套而不是凭感觉定一个方案。我一般会这么理解填充的取舍均值填充是把“未知”推向中性比推向极端要合理但任何填充都会给数据注入噪声。真正稳健的方案是让算法自己拒绝低置信度样本比如在 SVD 分解时赋予观察到的评分更高权重而不是先补成密集矩阵再用同样权重看待。2.3 网页端与离线评估模块evaluate.html 到底在评估什么这套项目里 index、recommend、detail、history、evaluate 五个页面不是随便摆的。index 是入口推荐由后端计算后渲染到 recommenddetail 展示电影内容特征给基于内容的推荐提供特征来源history 把用户历史行为交给协同过滤evaluate 则是把三种算法放在同一份数据集上跑离线对比。离线评估最常用的是留一法把每个用户的评分随机抽出一部电影藏起来用剩余数据训练模型然后看模型能否把藏起来的那部电影排在推荐列表靠前的位置。这个流程可以写成一段很直观的伪代码import numpy as np def leave_one_out_evaluate(matrix, recommend_func): hit 0 total 0 for user_id in matrix.index: user_ratings matrix.loc[user_id].dropna() if len(user_ratings) 2: continue # 随机选一部用户实际看过的电影模拟它从未出现过 test_item np.random.choice(user_ratings.index, 1)[0] train_row matrix.loc[user_id].copy() train_row[test_item] np.nan # 训练时把这一格隐藏 rec_list recommend_func(matrix, train_row, user_id, top_k10) if test_item in rec_list: hit 1 total 1 return hit / total # 评估指标命中率 / 覆盖率 / 平均排名倒数这个评估逻辑虽然简单但它逼着你定义清楚“推荐成功”的含义。命中率只关心目标电影是否出现在 Top-N 里平均排名倒数则更严格要求目标电影排得越靠前越好。写毕设时通常要把多个指标都算出来命中率、召回率、覆盖率、多样性才能在论文里说明不同算法各有优劣。evaluate.html 的作用就是把后端计算出的这些指标展示成可读的对比表格。3. 物品协同过滤为什么“喜欢这个的人还喜欢那个”能变成推荐物品协同过滤的核心思想一句话能讲完如果用户 A 和用户 B 都喜欢电影 X那么 X 和用户们共同喜欢的另一部电影 Y 之间就有了潜在关联。把这些关联聚合成物品之间的相似度推荐时就找用户已看过电影的相似电影按打分加权排序。这套逻辑在电商里对应“看了又看”“买了还买”在电影里就是“喜欢《A》的人也喜欢《B》”。它之所以比用户协同过滤更容易上线是因为物品之间相似度通常比用户之间稳定。用户的兴趣会漂移但《盗梦空间》和《星际穿越》的相似关系不会因为某个用户转性而大幅变化。离线计算一遍物品相似度矩阵后线上只需要做查表和聚合实时压力小得多。3.1 物品相似度计算余弦相似度的直觉、公式与失真场景把评分矩阵的每一列看成该部电影在所有用户上的评分向量两个电影向量的余弦相似度衡量的是它们的方向一致性。公式不复杂import numpy as np def cosine_similarity_two(a, b): # a, b 是两个电影的评分向量只取两者都非空的维度 mask ~(np.isnan(a) | np.isnan(b)) a a[mask] b b[mask] if len(a) 0: return 0.0 dot np.dot(a, b) norm_a np.linalg.norm(a) norm_b np.linalg.norm(b) if norm_a 0 or norm_b 0: return 0.0 return dot / (norm_a * norm_b)这个公式有个很直观的特点它不关心评分的绝对高低只关心评分曲线的形状。比如两部电影都被同一个用户打了 4 分余弦相似度会贡献一个正值但如果一个打 5 分、另一个打 1 分贡献就是负值。这样算出来的相似度天然带有“口味方向”的意味和用户是否挑剔无关。但在真实矩阵里余弦相似度有一个隐患两个电影共同被评分的用户数可能很少。极端情况下一部冷门片只被两个用户评分而这两个用户恰好在另一部冷门片上也有评分计算出的相似度可能高达 1.0。这不是因为它们真的相似而是因为共同样本太少随机噪声被放大了。3.2 SVD 矩阵分解共同评分太少时相似度为什么失真解决失真的常用思路是降维。SVD 把用户-物品评分矩阵分解成三个矩阵相乘U 表示用户隐特征Sigma 表示特征强度Vt 表示物品隐特征。分解之后每部电影从原始的高维评分向量压缩成一个短向量比如 20 维的隐向量。短向量里每一维不再是某个具体用户的评分而是一个无法直接解释的潜在因子可能对应“特效”“剧情复杂度”“情感浓度”。from sklearn.decomposition import TruncatedSVD from sklearn.metrics.pairwise import cosine_similarity # 先用均值填充成密集矩阵让 SVD 能处理 filled matrix.fillna(global_mean) # 物品协同的场景下对矩阵做分解 svd TruncatedSVD(n_components20, random_state42) item_factors svd.fit_transform(filled.T) # 形状: 电影数 x 20 # 在隐向量空间计算物品相似度 item_sim cosine_similarity(item_factors) print(item_sim.shape) # 电影数 x 电影数分解后计算相似度的好处是20 维隐向量把所有用户的信息压缩在一起两个电影只要在某些潜在因子上表现相近即使没有共同评分用户也会得到一定相似度。这能缓解冷启动但不能根治冷启动如果一部新电影完全不在矩阵里它根本没有可分解的向量。SVD 解决的是“共同评分少但并非完全没数据”的场景不是“完全没数据”的场景。n_components 的选择是个经验活。设置太大会保留过多噪声设置太小会丢掉区分度。通常的做法是区间扫描在离线评估里比较 10、20、50、100 维的效果而不是拍脑袋写死。对电影场景几十万用户量级下 20 到 50 维已经能覆盖足够多的隐因子超过 100 维经常只在训练集上漂亮测试集上反而掉点。3.3 从相似物品到推荐列表加权打分与排序有了物品相似度矩阵推荐生成阶段只需要把用户已评分的电影作为“种子”找出它们最相似的若干电影再按“用户评分 * 物品相似度”聚合候选列表。这个加权过程把用户自己的偏好强度也纳入了排序用户给《黑客帝国》打了 5 分那么与《黑客帝国》相似度 0.8 的候选电影会得到 4.0 的加权分如果一部种子电影用户只打了 2 分它就算相似度很高贡献也被压低。def recommend_from_item_sim(user_rated, item_sim, top_k10): # user_rated: 用户已评分电影及分数如 {movie_id: 5.0} scores {} reason {} for movie_id, rating in user_rated.items(): sim_scores item_sim[movie_id] # 该电影与所有电影的相似度 for candidate, sim_val in enumerate(sim_scores): if candidate movie_id: continue if sim_val 0: continue # 核心公式评分 x 相似度 scores[candidate] scores.get(candidate, 0) rating * sim_val reason[candidate] movie_id # 记录推荐来源便于可解释 # 按加权分排序取 Top-N ranked sorted(scores.items(), keylambda x: x[1], reverseTrue)[:top_k] return ranked这个公式看着简单但它隐含一个问题一部热门电影和大量种子电影都有不低的相似度所以它会反复出现在很多候选里最终挤掉那些和用户兴趣更垂直的长尾电影。于是推荐列表容易偏向大热门。代码里用 reason 记录来源是为了解释功能也是论文里可用的卖点但如果要做多样性还得在排序时加入惩罚项比如 MMR而不是只按分排。物品协同过滤的实现到这里就能跑通了它的瓶颈永远是那个稀疏矩阵。评分数据密集时相似度可信评分数据稀疏时再精致的排序公式也无济于事。这也是为什么项目里要同时保留另外两种算法用它们来补物品协同的盲区。4. 用户协同过滤与内容推荐KNN、特征向量的两条互补路径物品协同过滤站在物品角度找关联用户协同过滤则站在用户角度先找到和目标用户口味最接近的一群用户再把这群用户看过的且目标用户没看过的电影当作候选。基于内容的推荐则完全换了一条路它不碰评分矩阵只看电影自身的特征。三种算法的数据依赖不同失效边界也正好互补。4.1 用户协同 KNN先找“口味相近的人”再聚合他们的选择用户协同过滤的最近邻实现非常直白把每个用户的历史评分当作一个向量计算用户与用户之间的相似度找到和目标用户最相似的 K 个用户后聚合这些邻近用户的评分来预测目标用户对某部电影的偏好。from sklearn.metrics.pairwise import cosine_similarity user_sim cosine_similarity(filled) # 计算用户间相似度 def predict_rating(user_id, movie_id, user_sim, matrix, k20): # 找到和目标用户最相似的 K 个已有评分的用户 sim_scores list(enumerate(user_sim[user_id])) sim_scores sorted(sim_scores, keylambda x: x[1], reverseTrue) sim_scores [(i, s) for i, s in sim_scores if i ! user_id and s 0][:k] numerator 0 denominator 0 for neighbor_id, sim in sim_scores: neighbor_rating matrix.iloc[neighbor_id][movie_id] if not np.isnan(neighbor_rating): numerator sim * neighbor_rating denominator sim if denominator 0: return global_mean return numerator / denominator这段代码里最关键的是 K 的选取。K 太小找出的邻居只有一个两个预测近乎碰运气K 太大所有不相关用户都被拉进来预测退化成全局均值。设置 K 的方式通常是可视化用户相似度分布如果大部分用户相似度集中在 0.1 以下那么 K 设 20 和设 80 的差异不会太大如果相似度集中在较高区间K 就应该大一些保留更多有效邻居。用户协同过滤的效果受制于评价偏见有人习惯给高分有人习惯给低分单纯用原始评分计算相似度会把“评分尺度不同”误认为“口味不同”。这时可以给每个用户的评分做均值中心化把评分减去该用户自己的平均分再计算相似度。中心化之后比较的是用户打分高于或低于自身平均水平的模式而不是绝对分值这也是许多项目里比原始余弦效果更稳定的做法。4.2 基于内容的推荐把电影变成特征向量绕过冷启动基于内容的推荐不在评分矩阵里工作它构建另一个矩阵电影-特征矩阵。特征来源可以是类型、导演、演员、关键词甚至是海报图片的视觉特征。对电影来说最简单的做法是把类型做成 one-hot喜剧是 1喜剧不是 0也可以用 TF-IDF 处理电影简介把文本变成向量。每个电影因此得到一个固定长度的特征向量两个电影之间的相似度直接用特征向量的余弦计算。import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity # 假设 movies 表里有 movie_id, genres, overview movies pd.read_csv(movies.csv) movies[features] movies[genres] movies[overview] # 用 TF-IDF 把文本特征转成数值向量 vectorizer TfidfVectorizer(stop_wordsenglish) movie_vectors vectorizer.fit_transform(movies[features]) # 计算电影间相似度 content_sim cosine_similarity(movie_vectors)TF-IDF 的关键在于它能降低高频词的影响。类型字段里的“剧情”“爱情”几乎每部电影都有如果只算词频这些词会把所有电影都拉近TF-IDF 给它们更低的权重让“科幻”“悬疑”这类有区分度的词主导相似度计算。相比 one-hot 编码TF-IDF 对文本描述能保留更多语义信息但也要做中文分词处理英文则可以直接用空格分词。内容推荐的命中逻辑和协同过滤不同用户看了一部科幻片系统找到特征向量上最接近的几部科幻片推过去。它对新电影非常友好因为新电影即使没有任何评分特征向量也在但它很难帮用户发现新类型因为推荐结果永远和用户过去看过的内容在特征上相近。这就是摘要里说的“推荐结果可能过于狭窄”的根源。4.3 混合推荐加权、切换、级联三种策略的取舍三种算法有各自的强项和盲区现实中做混合推荐通常不是把它们的结果平均一下就完事而是根据用户状态选择策略。最常见的三种混合方式是加权式、切换式和级联式。加权式是指三种算法各给出一个预测评分或排序分然后乘以固定权重再相加。权重可以在离线评估里搜索比如内容推荐权重 0.3、物品协同 0.4、用户协同 0.3。切换式则根据用户历史行为数量判断新用户只有一两条记录时直接用内容推荐老用户行为丰富时用协同过滤覆盖更多长尾。级联式是分阶段过滤先用内容推荐做粗召回得到几百个候选再用用户协同或物品协同对这些候选做精排既照顾了新物品又利用了用户行为信息。我一般会这么理解混合推荐它不是在算法层面追求某个公式而是在产品层面给推荐套上了多道保险。单一算法出问题时混入的另一种算法可能刚好能兜住。比如用户协同过滤在冷启动用户上失效但内容推荐不需要历史数据物品协同过滤容易偏热门但内容推荐天然能保留长尾。把它们以级联方式组合往往是毕设里最容易被演示、也最好解释的组合方式。5. 常见问题排查稀疏度超过 95%推荐系统还能不能信推荐系统代码调通容易调准很难。很多项目第一次跑通时推荐结果看着合理但仔细一测全是问题。这一章整理几个我在类似项目里踩过的高频坑每一条都是现象、原因、解决三个层次顺下来的排查时可以直接对照。5.1 缺失值填 0 后推荐结果全是怪片现象把评分矩阵的 NaN 全部填成 0再跑余弦相似度推荐列表里出现大量用户没看过也不感兴趣的电影离线指标掉得很惨。原因0 在显式评分里代表“最低分”或“不喜欢”但缺失值代表“没有行为”。余弦相似度会把两个电影向量里大量的共同 0当成口味一致的正向证据。一个打 5 分、一个不存在的电影和用户真正看过并打 4 分的电影之间可能因为同样地“都没被看”而产生高相似度。解决显式评分矩阵不要填 0。至少用均值填充或者保留 NaN在计算相似度时只让两个电影的非共同评分参与计算。如果一定要填充优先考虑用户均值或物品均值然后把填充过程写进数据预处理代码里方便复现。5.2 新电影上线后推荐不出去现象向系统里加了一部刚上映的电影它在详情页存在但所有用户推荐列表里完全看不到它像是没有这部片子一样。原因协同过滤的核心是评分矩阵新电影在矩阵中那一列全空和任何物品的相似度都为 0。SVD 分解时新电影没有隐向量即使插到矩阵里也无法参与相似度计算。这就是冷启动行为数据还没产生算法无从下手。解决把基于内容的推荐放在最前面做兜底。新电影只要有导演、类型、简介这些元数据就能算出和已有电影的文本相似度立即进入候选池。更进阶的做法是给新物品一个临时探索权重在推荐列表里插入少量随机或相似内容电影收集反馈后再逐步交给协同过滤。5.3 两部冷门片的相似度虚高越推越离谱现象两部都只有两三个人评分的电影余弦相似度算出接近 1.0于是看过其中一部的用户立刻被推荐另一部看起来毫无关联。原因共同评分数越少相似度统计越不稳定。极端时两个向量只有一个共同维度方向完全由这一个评分决定如果这个评分恰好相同相似度就冲到 1.0。这是小样本下的噪声不是真实关联。解决给相似度计算设定最小共同评分门槛比如至少 5 个用户同时评过两部电影才认可相似度计算公式。更稳健的方法是用皮尔逊相关系数代替余弦相似度因为皮尔逊会减去各自均值对评分尺度的敏感度更低。必要时再加入 shrunk 系数把基于小样本的相似度向全局均值收缩降低随机高分。5.4 推荐列表全是热门大片个性化不明显现象看推荐结果时永远是用户评分榜前列的那些高分热门电影用户感觉“这不是给我推的是给所有人推的”。原因评分数据呈长尾分布少数热门电影拥有大量评分因此它们的物品相似度很高、进入候选的频率也高。协同过滤的加权排序公式天然偏爱高热度物品因为它累积了更多相似度贡献。冷门的优质电影虽然特征上匹配却因为共同评分数太少而被埋没。解决在排序最后加多样性惩罚比如 MMR 公式每选出一个推荐电影就把与已选电影相似度过高的候选分整体下调。也可以直接限制一个相似种子在最终 Top-N 中最多出现几次防止某部热门电影通过多个渠道反复被推荐。混合内容推荐也能帮助引入长尾因为内容特征不看热度。5.5 离线评估指标很高上线效果却很差现象留一法测试里命中率达到 30%看着不错真实用户试了一圈反馈“推荐和我没关系”。原因离线评估和在线环境不是一个分布。留一法隐藏的评分往往来自用户看过的老电影它们本身就有热度偏差离线指标高可能只说明系统猜透了热门而不是猜透了用户。另外线上有位置偏差、展示顺序、用户惰性等问题离线数据无法模拟。解决离线评估不要只看命中率还要看新电影覆盖率、长尾推荐占比。上线前用一小部分真实流量做 A/B 对比观察点击率、播放时长这些行为指标。对毕设来说至少要在论文里说明离线指标的局限别把测试集分数当成系统真实水平的代表。6. 设计取舍三种算法如何在一套项目里互相兜底我一般会这么理解这套电影推荐系统它不是三个算法堆在一起凑功能而是沿着“用户-物品-内容”三条线去描述一部电影。用户协同描述的是“谁和谁口味接近”物品协同描述的是“什么电影和什么电影关系紧密”内容推荐描述的是“这部电影本身像什么”。三条线在不同数据阶段各管一段才能覆盖从新用户到新物品的完整链路。项目里的五个页面恰好对应这个设计index 负责入口用户进来选电影、给评分history 把行为写入日志recommend 把三种算法的结果混合后展示detail 提供内容特征来源evaluate 把算法切开做离线比较。这个结构本身就能当毕设的架构图数据从历史记录进入经过预处理模块变成评分矩阵和内容特征向量在算法层并行计算出三份候选最后在排序层合并。从实现取舍上看最值得借鉴的是把三种算法按用户状态做切换。用户行为历史少于 5 条内容推荐占主导因为此时评分矩阵不可信行为历史超过 50 条协同过滤占主导因为内容推荐已经陷入“永远推同类型”的怪圈中间状态用加权混合让相似度置信度决定权重。这个策略并不复杂但它在离线评估里往往比单一算法稳定得多也更容易解释给答辩老师听。我以前做一个模拟项目 X 时为了把某个算法的命中率刷高在 SVD 维度上反复调参最后离线指标涨了一截上线体验却一塌糊涂。后来才想明白推荐系统的瓶颈从来不在某一个算法的精调而在于冷启动兜底、稀疏数据下的相似度可信度、以及推荐结果多样性这些更底层的取舍。从那以后我每次拿到一套推荐系统源码都会强制走一遍先看评分矩阵怎么构造再看冷启动没有兜底最后才看算法效果怎么评估。希望帮到你这套基于 Python 的电影推荐系统正好保留了三种算法的完整实现和评估页面作为入手和参照比从零去读零散代码省力得多。本文还有配套的精品资源点击获取