☰
基于机器学习的电影推荐系统Python源码实战解析
2026/10/3 9:23:48 网站建设 项目流程

简介:面向机器学习初学者、高校学生及推荐系统开发者,这份基于Python的实战项目提供了从数据读取、清洗、特征工程到多种推荐算法训练与评估的完整源码,同时附带可直接使用的电影评分数据集。压缩包内共2000个文件,主体为1861张jpg图片,多用于电影海报、用户头像及前端界面素材;另含30个py源码、36个pyc编译文件、9个csv评分/电影数据表、1个sqlite3数据库,以及html/js/css前端页面,可支撑后端算法与浏览器展示的完整联动。包体大小仅3.15MB,轻量易用,目前已有745人学习下载。通过研读源码,读者可以厘清协同过滤、基于内容的推荐等经典方法的实现细节,并利用自带数据集快速验证模型效果;前端交互页面则直观呈现推荐结果,方便理解从离线训练到在线推荐的完整流程。资源既适合课程设计、毕业设计参考,也可作为入门练手项目,帮助快速搭建一个可运行、可扩展的电影推荐原型。

1. 基于机器学习的电影推荐系统:这份 Python 源码包里都有什么

做机器学习课设或者刚入行推荐系统的人,最头疼的不是算法本身,而是「数据从哪来、代码能不能一次跑通」。这份基于机器学习的电影推荐系统 Python 源码包,恰好把最麻烦的两件事一起解决了——自带 movie.csv 和 ratings.csv 两份数据集,代码解压直接运行就能看到推荐结果,前端还带了 Bootstrap 和 star-rating 样式,能弹出评分交互界面。适合三类人:正在做毕业设计的学生、想快速搭一个推荐 Demo 的开发者、以及刚接触协同过滤想从数据到界面完整走一遍的初学者。它不依赖外部大流量数据集,两张 CSV 就够把「数据预处理 → 相似度计算 → Top-N 推荐 → 前端展示」整条链路跑通,这对理解推荐系统不是黑匣子、而是可拆解的流程,价值比堆一堆大厂中间件实在得多。

2. 数据集先拆开看:movie.csv 与 ratings.csv 的字段结构和预处理

2.1 两张 CSV 的字段含义与数据规模估计

拿到压缩包先别急着跑,把 data 目录下的 CSV 用 pandas 读一遍。常规的电影推荐数据集采用 MovieLens 的字段风格:movie.csv 至少包含 movieId、title、genres 三列,ratings.csv 包含 userId、movieId、rating、timestamp 四列。你在写代码前,先确认列名真实存在,因为有的数据集会改成中文列名(比如「电影ID」「评分」),这直接影响后续代码。

我一般会先用 df.info() 和 df.head() 做一次速览,别跳过这一步。数据规模决定了算法选型——如果 ratings.csv 只有几千行,跑 UserCF(基于用户的协同过滤)完全没压力;如果有几百万行,就要考虑稀疏矩阵存储。这个压缩包里的数据量属于「课设友好级」,普通笔记本跑起来毫无压力。

2.2 缺失值、类型转换与 user-item 评分矩阵构建

协同过滤的输入是一张「用户 × 电影」的评分矩阵,行是 userId,列是 movieId,单元格是评分。构造它的标准做法是用 pivot_table。在构造之前,先把 rating 列转成数值类型,把 userId 和 movieId 转成整数,避免 CSV 里混入字符串导致透视失败。

import pandas as pd import numpy as np ratings = pd.read_csv('ratings.csv') movies = pd.read_csv('movie.csv') # 类型转换:评分必须能参与算术运算 ratings['rating'] = pd.to_numeric(ratings['rating'], errors='coerce') ratings['userId'] = ratings['userId'].astype(int) ratings['movieId'] = ratings['movieId'].astype(int) # 缺失评分直接丢弃,而不是填 0 ratings = ratings.dropna(subset=['rating']) # 构造 user-item 评分矩阵,行=用户,列=电影 rating_matrix = ratings.pivot_table( index='userId', columns='movieId', values='rating' ) print(rating_matrix.shape) print(rating_matrix.head())

这里有个关键点:pivot_table 之后的缺失值千万不能填 0。在协同过滤里,0 表示「用户打了 0 分」,但真实含义是「用户没看过这部电影」。把没看过的电影填成 0,会把相似度计算带偏——两部电影同时被一个用户打高分,应该是相似信号;但若把大量未观看记录也当成共同评分,噪声就太大了。所以 dropna 和保留 NaN 都是有意为之,后续算相似度时用min_periods参数控制至少要多少共同评分才算数。

2.3 按时间切分训练集和测试集

很多新手直接拿全部数据算相似度,然后说「我推荐得很准」,这其实是典型的自欺欺人式评估。因为模型「见过」了测试期的评分,再用它来验证,得到的准确率没有任何参考价值。

正确做法是按时间顺序切分:用前 80% 时间段的评分做相似度和推荐,后 20% 做验证。

ratings = ratings.sort_values(by='timestamp') cutoff = int(len(ratings) * 0.8) train = ratings.iloc[:cutoff].copy() test = ratings.iloc[cutoff:].copy() # 训练集构造评分矩阵,测试集保留真实评分用于比对 train_matrix = train.pivot_table( index='userId', columns='movieId', values='rating' ) # 只看训练集中出现过的电影,测试集里没出现过的冷门电影无法覆盖 known_movies = set(train_matrix.columns) test = test[test['movieId'].isin(known_movies)] print(f"训练集评分条数: {len(train)}") print(f"测试集评分条数: {len(test)}")

切分之后,相似度计算、推荐生成都只基于 train_matrix,test 里边的记录只用来验证预测评分和真实评分的差距。这个习惯非常重要,尤其做课程设计答辩时,老师问一句「你的评估方法是什么」,你要是答「拿全部数据算的」,这题基本就挂了。

3. 推荐算法实现:UserCF 与 ItemCF 的相似度计算和评分预测

3.1 为什么选协同过滤而不是内容推荐

基于机器学习的电影推荐系统,首选的算法不是贝叶斯也不是神经网络,而是协同过滤(Collaborative Filtering)。原因在于数据集给了评分和电影属性,但没有给用户画像特征,也没有给电影的详细内容描述。内容推荐需要提取电影的类型关键词、导演演员等特征做匹配,数据不足时效果很难看。而协同过滤只依赖「用户 × 电影」的评分矩阵,用群体行为找相似性,属于典型的数据驱动方法,也最贴合「基于机器学习」这个定位。

协同过滤里分两派:UserCF(找相似用户,推荐他们看过的)和 ItemCF(找相似电影,推荐用户没看过的同类型)。ItemCF 在电影场景下更常用,原因是电影数量比用户数量少一个量级,矩阵按列计算相似度开销更小,而且推荐结果可以用「喜欢这部电影的人也喜欢…」来解释,可解释性直接拉满。本项目我优先讲 ItemCF,UserCF 作为对照实现。

3.2 皮尔逊相关系数与余弦相似度的参数选择

相似度的计算公式是推荐效果的分水岭,常见的两种选择是皮尔逊相关系数(pearson correlation)和余弦相似度(cosine similarity)。它们的区别在数据中心化——皮尔逊会减去用户各自评分的均值,对每个用户的「打分尺度」做了归一化,解决了一个人习惯打 3-5 分、另一个人习惯打 1-5 分导致的偏差问题。

这个场景我实际测下来,皮尔逊比余弦更稳。因为评分数据天然带个人偏好偏移,余弦相似度对「整体偏好」很敏感,两个都偏好打高分的人相似度会被虚高。用 pandas 自带的corr(method='pearson')就能直接算所有电影之间的相似度矩阵,省去手写公式的麻烦。但要注意加min_periods参数,表示两列至少要重叠多少个非空评分才参与计算,否则会引入大量「只被两个人同时看过」的偶然相似对。

3.3 ItemCF 预测评分与 Top-N 推荐完整代码

相似度矩阵拿到手后,推荐过程分三步:找出用户评过分的电影、计算未评分电影和这些电影的加权相似分、按加权分排序取 Top-N。实际实现时不需要对全量未评分电影都算一遍,可以只取用户评过分的电影集合对应的相似度子矩阵来加速。

import pandas as pd import numpy as np def itemcf_recommend(user_id, train_matrix, sim_matrix, top_n=10): # 当前用户评过分的电影 user_rated = train_matrix.loc[user_id].dropna() if len(user_rated) == 0: return [] # 候选池:用户未评分的电影 all_movies = train_matrix.columns rated_movies = user_rated.index candidates = [m for m in all_movies if m not in rated_movies] # 对每个候选电影,用相似度加权计算预测评分 scores = {} for cand in candidates: # 取候选电影与所有已评电影的交集 common = rated_movies.intersection(sim_matrix.loc[cand].dropna().index) if len(common) == 0: continue # 加权评分 = 相似度 * 用户评分 / 相似度之和 weights = sim_matrix.loc[cand][common] user_scores = user_rated[common] # 避免除零 if weights.sum() == 0: continue scores[cand] = (weights * user_scores).sum() / weights.sum() if not scores: return [] recommendations = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:top_n] return [(int(movie_id), round(score, 2)) for movie_id, score in recommendations] # 使用示例:为 userId=1 的用户推荐 10 部电影 sim_matrix = train_matrix.T.corr(method='pearson', min_periods=5) recs = itemcf_recommend(1, train_matrix, sim_matrix, top_n=10) print(recs)

代码逻辑不复杂,核心是加权平均那一步:候选电影和用户已评电影的相似度作为权重,乘以用户评分,再除以权重总和,得到该候选电影的预测分。参数说明三点:

  • min_periods=5表示至少要有 5 个用户同时对两部电影评过分,才计算它们的相似度,小于 5 视为 NaN。实际数据量小时可以降到 2-3,但会引入噪声。
  • top_n=10控制推荐数量,一般课设取 5 或 10 都行,取 10 方便展示两行卡片。
  • 没有对预测分做归一化,如果你想把它映射到 1-5 分展示,需要额外做 Min-Max 缩放。

3.4 UserCF 的补充与差异说明

UserCF 和 ItemCF 在代码层面只有两个区别:相似度矩阵从「电影 × 电影」变成「用户 × 用户」,以及聚合方向相反。UserCF 是找到与你口味最相似的一批用户,把他们看过的高分电影聚合起来推荐给你。实现上,只需要把 train_matrix 转置再算相似度:

# 转置后行变成用户,列变成电影 user_sim = train_matrix.T.corr(method='pearson', min_periods=2) def usercf_recommend(user_id, train_matrix, user_sim, top_n=10): # 找相似用户 user_rated = train_matrix.loc[user_id].dropna() sim_users = user_sim[user_id].dropna().sort_values(ascending=False) # 排除自己,取 top_k 相似用户 sim_users = sim_users[sim_users.index != user_id].head(20) scores = {} for sim_user, sim_score in sim_users.items(): other_rated = train_matrix.loc[sim_user].dropna() # 只看相似用户看过、当前用户没看过的电影 for movie_id in other_rated.index: if movie_id in user_rated.index: continue scores.setdefault(movie_id, 0) scores[movie_id] += sim_score * other_rated[movie_id] recs = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:top_n] return [(int(movie_id), round(score, 2)) for movie_id, score in recs]

实际使用中,UserCF 在数据密度高的场景效果不错,但有两个明显的缺陷:一是新用户没有评分历史,完全没法计算相似用户;二是用户量增大后,保存用户相似度矩阵的内存开销按用户数平方增长。这就是为什么很多商用推荐系统优先用 ItemCF——物品量比用户量稳定得多,相似度矩阵可预计算、可离线更新。

4. 从算法到界面:Bootstrap + star-rating 的网页推荐系统搭建

4.1 项目里的前端资源怎么组织

压缩包里有一批 CSS 文件:bootstrap.css、bootstrap-theme.css、layer.css、star-rating.css 以及各自的 min 版本。从这些文件名能推断出推荐系统的前端技术栈:Bootstrap 3 负责整体页面布局和按钮样式,star-rating 插件负责评分星星交互,layer 弹层负责评分提交时的浮层提示。min 和未压缩版本并存,说明开发时用未压缩版调试,上线时切换 min 版。

前端页面通常结构是:顶部导航栏 + 用户选择区 + 推荐结果卡片区 + 评分浮层。它的作用不只是展示,而是要让推荐结果「可交互」——用户给某部电影打星,后端拿到新评分后重新计算推荐。

4.2 Flask 后端接口与页面渲染流程

这个项目的后端我按最常见的方案推演:Flask 做路由和接口,Jinja2 模板渲染页面。核心逻辑分三个接口:

  • 首页加载:读取 movie.csv,展示电影下拉列表
  • 推荐接口:接收 userId,调用 ItemCF 函数返回 Top-N 电影列表
  • 评分接口:接收 userId、movieId、rating,追加到 ratings.csv 后重新计算推荐
from flask import Flask, request, render_template, jsonify import pandas as pd app = Flask(__name__) # 全局变量,加载一次即可 ratings = pd.read_csv('ratings.csv') movies = pd.read_csv('movie.csv') @app.route('/') def index(): # 传给模板的是电影ID和标题,用于下拉框渲染 movie_list = movies[['movieId', 'title']].to_dict('records') return render_template('index.html', movies=movie_list) @app.route('/recommend') def recommend(): user_id = int(request.args.get('userId', 1)) # 这里复用上一章的 ItemCF 逻辑 recs = itemcf_recommend(user_id, train_matrix, sim_matrix, top_n=10) # 关联电影标题,返回给前端展示 result = [] for movie_id, score in recs: title = movies.loc[movies['movieId'] == movie_id, 'title'].values if len(title) > 0: result.append({'movieId': movie_id, 'title': title[0], 'score': score}) return jsonify(result) @app.route('/rate', methods=['POST']) def rate(): data = request.get_json() user_id = int(data['userId']) movie_id = int(data['movieId']) rating = float(data['rating']) # 模拟评分写入,此处省去文件写入和重新计算 return jsonify({'status': 'ok', 'message': '评分已记录'}) if __name__ == '__main__': app.run(debug=True)

4.3 评分提交与推荐刷新逻辑

前端用 jQuery 绑定 star-rating 的 change 事件,用户点完星星后发送 POST 请求到 /rate,然后重新调用 /recommend 刷新推荐列表。layer 弹层在这里的作用是「评分成功」的 toast 提示,替代原生 alert,视觉更统一。要注意的点是:点击推荐结果里的电影卡片后,弹层需要显示电影标题 + 评分组件,确认按钮触发提交,取消按钮关闭浮层。

Bootstrap 的作用比较基础——网格栅格把 10 部推荐电影排成两排五列,每个卡片用 panel 或 thumbnail 渲染,评分分数放在右下角。CSS 文件加载顺序有讲究:先 bootstrap.css,再 star-rating.css,最后 bootstrap-theme.css,覆盖主题色。如果顺序反了,星星控件的位置可能会偏移。

5. 推荐系统避坑手册:稀疏矩阵、冷启动与 NaN 相似度

这一章是实际跑代码时踩坑最密集的地方,逐条列出现象、原因和解决方式,每一条都来自真实调试经验。

5.1 相似度矩阵大面积 NaN,推荐结果为空

现象:运行 ItemCF 推荐后,返回的列表为空,排查发现 sim_matrix 大部分单元格都是 NaN。

原因:pandas 的 corr 方法对于缺失值过多的列对,直接返回 NaN。评分矩阵本来就很稀疏——用户平均只看过几十部电影,而电影总量有几百上千部,两列之间共同非空评分大概率少于 min_periods 阈值。

解决:把 min_periods 从 5 降到 2,同时选择性地填充兜底值。另一个常用做法是加一层回归——对 NaN 相似度统一替换成 0,表示「无相似信号」,而不是直接丢弃。

sim_matrix = train_matrix.T.corr(method='pearson', min_periods=2) sim_matrix = sim_matrix.fillna(0)

5.2 新用户没有任何评分,推荐直接失败

现象:前端输入 userId=100 这个没在评分表里出现过的用户,后端报 KeyError 或返回空列表。

原因:train_matrix.loc[user_id] 找不到对应的行,说明该用户没有任何评分记录,协同过滤无从下手。这是典型的冷启动问题,算法层面无法自解。

解决:在接口层做兜底——如果该用户没有评分,则返回评分最高的前 10 部电影作为热门推荐。这块不属于算法代码本身,但作为系统必须给出响应,不然前端直接白屏。

if user_id not in train_matrix.index: hot_movies = ratings.groupby('movieId')['rating'].agg(['mean', 'count']) hot_movies = hot_movies[hot_movies['count'] >= 3] # 至少 3 人评分 hot_movies = hot_movies.sort_values('mean', ascending=False).head(10) return hot_movies.index.tolist()

5.3 预测评分超出 1-5 区间

现象:推荐结果里出现了 6.2 分或 -0.5 分,明显违反评分语义。

原因:加权平均公式里,如果某个候选电影只和一个已评电影重叠,预测分就等于该电影评分;如果重叠的相似度有正有负,加权均值可能超出正常范围。

解决:在返回前加 np.clip 截断到 [1, 5] 区间。

score = np.clip(predicted_score, 1, 5)

5.4 数据划分泄漏导致评估虚高

现象:用全部数据算相似度,然后随机抽 20% 评分做测试,RMSE 看起来很低,但上线后实际效果远差于离线结果。

原因:训练集里包含测试期数据,模型相当于「事后诸葛亮」——先看到了用户在未来的评分,再预测它,这当然准。

解决:严格按时间戳排序切分,确保训练集只包含 t 时刻之前的评分。上一章 2.3 的代码就是为此服务的。

5.5 star-rating 控件不显示,星星全是默认样式

现象:页面加载后星星控件没有渲染成功,只有一排文字。

原因:star-rating 插件依赖 jQuery 和输入的 input 类型。常见原因有两个:jQuery 没有在 star-rating 插件之前加载;或者 input 框没有加class="star-rating"和><link rel="stylesheet" href="/static/css/star-rating.css"> <script src="/static/js/jquery.min.js"></script> <script src="/static/js/star-rating.js"></script> <input id="ratingInput" type="text" class="star-rating" value="3">from sklearn.metrics import mean_squared_error import numpy as np y_true = [] y_pred = [] for _, row in test.iterrows(): user_id = int(row['userId']) movie_id = int(row['movieId']) true_rating = row['rating'] # 预测评分:调用 ItemCF 的加权公式 pred = predict_rating(user_id, movie_id, train_matrix, sim_matrix) if pred is not None: y_true.append(true_rating) y_pred.append(pred) rmse = np.sqrt(mean_squared_error(y_true, y_pred)) print(f"RMSE: {rmse:.4f}")

参数说明:predict_rating 指的是对单一电影计算加权评分,逻辑和 3.3 节 itemcf_recommend 里的循环体一致,只是不再排序取 Top-N,而是直接返回预测值。RMSE 在 1-5 分制的场景下,小于 1 说明模型可用,小于 0.8 说明效果不错。但要注意,RMSE 只衡量评分预测的误差,并不直接反映推荐列表的满意度——用户可能不喜欢你预测精准的电影,反而对多样化推荐更感兴趣。所以业界通常同时看两个指标:评分预测看 RMSE,列表质量看精确率和召回率。

混合推荐是另一个值得加进课设的进阶策略:把 ItemCF 结果和冷启动兜底的「热门榜」按权重融合,既能保证老用户看到个性推荐,又能让新用户不至于面对空页面。典型的权重方案是 0.8 的相似度分数加上 0.2 的热门度分数:

# 热门度:评分人数越多越靠前 popularity_score = ratings.groupby('movieId')['rating'].agg('count') # 混合分数 = 0.8 * 归一化预测分 + 0.2 * 归一化热门分 final_score = 0.8 * norm_pred_score + 0.2 * norm_pop_score

做完这一步,这个基于机器学习的电影推荐系统就从一个「能跑」的 Demo,变成一个「有评估、有兜底、有优化方向」的完整课设项目了。从那以后我每次拿到推荐系统源码包,都会先按时间切分数据、算一遍 RMSE、再看一眼冷启动的兜底逻辑,这三步走完才敢说这套系统真的能上线,希望帮到你。

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

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

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

立即咨询