简介:一套基于Python与机器学习算法完成的电影推荐与票房预测毕业设计项目,评审分为98分,主要面向计算机专业毕业生、课程设计及期末大作业学生,也适合需要项目实战经验的学习者。资源包共59个文件,约30.94MB,包含16个Python源码文件、16个表格数据文件、23张分析图表、2份说明文档、1份电子版报告和1个文本辅助文件。源码覆盖基于人口统计、内容关键词、近邻算法、矩阵分解及集成策略的多种推荐模型,表格数据涵盖电影票房与演职员等真实信息,分析图表与文档可支撑从数据处理、特征分析到模型评估的完整流程。内容涉及电影特征工程、协同过滤推荐、融合模型和票房预测思路,通过源码与报告可复现项目结果,也可参考目录划分与图表分析方法用于二次开发。目前已有274人学习下载,可作为计算机相关专业毕业设计与课程实践的参考范例。
1. 一个标题里装着两套系统:推荐与票房预测,工作量藏在数据里
每年毕业季,“python使用机器学习算法的电影推荐系统以及票房预测系统源码+报告PDF”这类标题都会刷屏。但把类似源码包跑一遍就知道,卡住你的不是算法,而是数据清洗和评估口径——评分矩阵稀疏得像渔网,票房特征里藏着时间穿越,这两处是翻车高发地。
这套项目要解决两件事:给定用户评分数据,一是推荐用户接下来喜欢的电影,二是预测新片票房。前者是排序,后者是回归,正好覆盖机器学习最典型的两个任务分支,再配上报告PDF,就是一套能拿去答辩的完整流程。
适合正在挑题目的本科生、做课设的研究生,以及想完整走一遍机器学习流程的转行者。下面按这类源码最常见的实现路径,把代码、参数和坑拆开讲。
2. 先把算法选明白:协同过滤、回归模型与两份数据的路线图
写代码之前,先把选型逻辑讲清楚,答辩时也要按这个逻辑讲。
2.1 推荐系统的三条路线:协同过滤的假设、内容特征与SVD隐向量
推荐系统的实现路径就三条。第一条是协同过滤,核心假设是“相似的人喜欢相似的东西”,它分成用户协同过滤(UserCF)和物品协同过滤(ItemCF)。UserCF先算用户之间的相似度,找到跟你口味最接近的一批用户,把他们评过而你还没看过的电影挑出来;ItemCF反过来,先算电影之间的相似度,你给《流浪地球》打了高分,系统就把和它最像的《疯狂的外星人》推给你。对毕设来说,这两条最容易讲清楚,也最容易出图,机器学习入门阶段把这两条跑明白,比什么都强。
第二条是基于内容的推荐。它不依赖用户评分,而是把电影本身变成特征——类型、导演、演员、关键词——先给每部电影算特征向量,再算电影向量与用户历史偏好向量的余弦距离。它的价值在于冷启动:新用户一个评分都没有,协同过滤完全失效,但基于内容的方法在他只看过一两部电影时就能给出推荐。
第三条是矩阵分解,典型代表是SVD,把用户-物品评分矩阵拆成两个低维矩阵的乘积。好处是能缓解稀疏问题,但解释性差,隐向量到底代表什么,很难向答辩老师讲清楚。高分毕设里比较常见的组合是:ItemCF做主力,基于内容的相似度兜底冷启动,SVD作为对比模型写进实验章。这样既覆盖经典算法,又能自然写出“三种模型效果对比”的实验数据。比抱着《机器学习》周志华那本或各类图解机器学习算法PDF从头啃公式更有效的做法,是先跑通这三条路线,再回头补理论。
2.2 票房预测的模型梯队:线性回归、随机森林与LightGBM怎么选
票房预测本质是回归任务,目标变量是电影票房收入。入门常用线性回归,它可解释性强,能直接看出每个特征对票房的影响,比如预算每增加一个单位、票房平均增加多少。但电影票房和特征之间高度非线性——大制作不一定卖座,小成本也能爆冷——线性模型很容易欠拟合。
常用升级路线是树模型,随机森林和梯度提升树(XGBoost、LightGBM)。树模型能自动处理特征交互和非线性关系,不需要标准化,对缺失值也相对宽容。随机森林还能直接输出特征重要性,写报告时非常好用。LightGBM这类模型上限通常更高,但调参成本高,黑匣子特征明显,新手很容易陷入调参泥潭还说不清原理。选型对比看这张表:
| 模型 | 可解释性 | 非线性能力 | 调试成本 | 毕设推荐度 |
|---|---|---|---|---|
| 线性回归 | 高 | 弱 | 低 | 适合做基线 |
| 随机森林 | 中 | 强 | 中 | 主力推荐 |
| XGBoost/LightGBM | 低 | 强 | 高 | 有基础再上 |
| SVR/神经网络 | 低 | 强 | 高 | 不推荐 |
我一般建议主力用随机森林,把线性回归作为基线写进实验章,最后用网格搜索给随机森林调一次参。这一套在大量公开的票房预测实践里是最省力、效果也够看的组合。标题里说的“机器学习算法”,用这个模型梯队就完全站得住,不需要硬凑深度学习。
2.3 一份数据喂两个系统:MovieLens与TMDB的字段设计
这个标题最常见的数据来源是公开数据集。推荐系统这边,MovieLens系列(100k、1M、10M)最常用,字段只有四个:用户ID、电影ID、评分、时间戳。票房预测这边,TMDB 5000 Movie Dataset这类数据集包含预算、题材、时长、上映日期、演员表、职员表,标签列是revenue。两份数据可以通过电影ID关联起来。如果不用现成数据集,就得靠python爬虫去抓票房网站,但抓下来的数据清洗成本极高,格式乱、字段缺、去重要写一大堆,不建议毕设阶段自找麻烦。
设计字段时有一个原则:给两个系统各自准备独立的视图,但共用一张清洗过的原始表。常见做法是这样:
import pandas as pd import json # 推荐系统侧:评分表只有四列,timestamp用于后面按时间切分 ratings = pd.read_csv('ml-1m/ratings.dat', sep='::', names=['userId', 'movieId', 'rating', 'timestamp'], engine='python') # 票房预测侧:电影元数据和演员职员表分开存 movies = pd.read_csv('tmdb_5000_movies.csv') credits = pd.read_csv('tmdb_5000_credits.csv') # 先检查空值和重复值,再决定过滤策略 print(movies.isnull().sum()) print(ratings.duplicated().sum())这段代码有三个关键点。第一,MovieLens的ratings.dat分隔符是双冒号::,read_csv必须指定engine='python',否则直接报错。第二,TMDB的genres、cast、crew是JSON字符串,后面要json.loads解析。第三,timestamp列是推荐评估的关键——它允许你按时间把评分分成训练集和测试集,避免“用未来预测过去”。很多源码包在这一步就埋了坑,比如直接用随机切分,后面实验数据会被答辩老师一眼看穿。数据这关过了,下一章开始写推荐系统的实现。
3. 推荐系统落地:从评分矩阵到Top-N列表的完整实现
3.1 数据清洗与评分矩阵构建:去重、过滤和pivot_table
评分数据进模型前先做三件事:去重、过滤、构建矩阵。去重的逻辑是同一用户对同一部电影只保留最新一条评分。过滤则处理两类极端样本:评分少于20部的用户直接删掉,他们对相似度计算的贡献几乎为零,还占内存;评分次数少于5次的冷门电影,早期也一并过滤,等有足够数据再处理冷启动。
# 去重:同一用户对同一电影只保留最新评分 ratings = ratings.drop_duplicates(subset=['userId', 'movieId'], keep='last') # 过滤稀疏用户和稀疏电影 user_counts = ratings['userId'].value_counts() movie_counts = ratings['movieId'].value_counts() ratings = ratings[ratings['userId'].isin(user_counts[user_counts >= 20].index)] ratings = ratings[ratings['movieId'].isin(movie_counts[movie_counts >= 5].index)] # 构建用户-物品评分矩阵:行是用户,列是电影 rating_matrix = ratings.pivot_table(index='userId', columns='movieId', values='rating')pivot_table构建出的矩阵,未评分单元格是NaN。这里有个容易混淆的点:直接fillna(0)会让“没看过”和“打了0分”变成同一件事,在余弦相似度里,两个只看过完全不同电影的用户的相似度可能变成正数——因为共同0项太多了。所以后面计算相似度时,要么只用共同评过分的电影,要么先做去均值归一化。这一步是推荐效果的第一个分水岭,处理方式写不写清楚,直接决定报告里实验数据的可信度。
3.2 用户协同过滤(UserCF):去均值归一化与余弦相似度
UserCF的核心就两步:算用户相似度、按相似度加权汇总邻居评分。相似度用余弦相似度,它比皮尔逊相关系数更直观,也更好讲。
from sklearn.metrics.pairwise import cosine_similarity # 去均值归一化:每个评分减去该用户的平均打分习惯 mean_ratings = rating_matrix.mean(axis=1) normalized = rating_matrix.sub(mean_ratings, axis=0).fillna(0) # 计算用户间余弦相似度 user_sim = cosine_similarity(normalized) user_sim_df = pd.DataFrame(user_sim, index=rating_matrix.index, columns=rating_matrix.index) # 自己和自己的相似度置0,避免自推荐 np.fill_diagonal(user_sim, 0)为什么要减均值再算相似度?没有归一化时,一个习惯打5星的用户和一个只打3星的用户,即使口味完全一致,余弦相似度也会偏低。减掉各自的平均分后,比较的是“相对偏好”。
提示:归一化要针对每一行(用户)做减法,而不是对整张矩阵减同一个数。
fill_diagonal把对角线置0,是防止预测时把用户自己评过的电影又推回来,这一行容易忘。加权预测公式:目标用户对某部电影的预测评分,等于与他最相似的k个用户对该电影评分的加权平均,权重就是相似度。相似度为负的邻居建议直接丢弃,他们的口味和目标用户相反,硬算进来只会拉低质量。k值一般取20到50,太小受噪声影响,太大又回到全量平均。
3.3 物品协同过滤(ItemCF):离线相似度表与Top-N打分
ItemCF和UserCF代码结构几乎一样,只是相似度矩阵从“用户×用户”变成“电影×电影”。实际上ItemCF的推荐效果通常比UserCF更可靠,因为“电影和电影相似”比“用户和用户相似”更稳定,尤其当用户数远大于电影数的时候。而且ItemCF可以离线算好相似度矩阵,用户一来请求,直接查表生成推荐,延迟低,这在答辩时能讲成工程亮点。
# 电影间相似度:转置后计算余弦 item_sim = cosine_similarity(normalized.T) item_sim_df = pd.DataFrame(item_sim, index=rating_matrix.columns, columns=rating_matrix.columns) np.fill_diagonal(item_sim, 0) def itemcf_recommend(user_id, rating_matrix, item_sim_df, k=10, topn=10): if user_id not in rating_matrix.index: return [] watched = rating_matrix.loc[user_id].dropna() if watched.empty: return [] # 对用户看过的每部电影,取最相似的k部并累加得分 scores = {} for movie_id, rating in watched.items(): sims = item_sim_df[movie_id].drop(labels=[movie_id]).sort_values(ascending=False).head(k) for idx, sim in sims.items(): scores[idx] = scores.get(idx, 0) + sim * rating # 去掉已看过,按得分排序取TopN watched_ids = set(watched.index) candidates = [(mid, s) for mid, s in scores.items() if mid not in watched_ids] candidates.sort(key=lambda x: x[1], reverse=True) return [mid for mid, _ in candidates[:topn]]这里得分算成相似度和评分的乘积累加,而不是再套一层平均。为什么累加?因为用户给高分且和候选电影相似的那部电影,对候选电影的“贡献”应该更大。参数说明:k是每部已看影片取几个近邻,topn是最终推荐数量,k取10到20比较常见。如果用户只看过一两部电影,候选池会非常短,这是冷启动的直接表现,需要用基于内容的相似度来兜底,而不是硬凑结果。
3.4 推荐评估:Precision@10与Recall@10怎么算、怎么看
推荐系统的评估和分类回归不一样,它关心的是Top-N列表里有多少命中。标准做法是把评分按时间排序,前80%做训练,后20%做测试。测试集里把评分大于等于4的电影当作“用户真正喜欢”的正样本,然后对每个用户生成Top-10推荐,计算命中率。
def precision_at_k(reco_list, held_out, k=10): """reco_list: 推荐的电影列表; held_out: 用户真正喜欢的电影集合""" hits = len(set(reco_list[:k]) & held_out) return hits / k def recall_at_k(reco_list, held_out, k=10): hits = len(set(reco_list[:k]) & held_out) return hits / len(held_out) if len(held_out) > 0 else 0 # 时间切分:前80%训练,后20%测试 ratings = ratings.sort_values('timestamp') cut = int(len(ratings) * 0.8) train_ratings, test_ratings = ratings.iloc[:cut], ratings.iloc[cut:] # 用训练集重建矩阵与相似度表 train_matrix = train_ratings.pivot_table(index='userId', columns='movieId', values='rating').fillna(0) item_sim_train = cosine_similarity(train_matrix.T) item_sim_train_df = pd.DataFrame(item_sim_train, index=train_matrix.columns, columns=train_matrix.columns) # 对测试用户逐人生成推荐并计算指标 precisions, recalls = [], [] for uid in test_ratings['userId'].unique(): held = set(test_ratings[(test_ratings['userId'] == uid) & (test_ratings['rating'] >= 4)]['movieId']) reco = itemcf_recommend(uid, train_matrix, item_sim_train_df, k=10, topn=10) if len(held) == 0 or len(reco) == 0: continue precisions.append(precision_at_k(reco, held, 10)) recalls.append(recall_at_k(reco, held, 10)) print(f"Precision@10: {sum(precisions)/len(precisions):.4f}") print(f"Recall@10: {sum(recalls)/len(recalls):.4f}")两个指标的区别要讲清楚:Precision@10衡量推荐的10部电影里用户真正喜欢了几部,是“推得准不准”;Recall@10衡量用户真正喜欢的电影里推荐出来了多少,是“推得全不全”。毕设报告里两个都要写,只看一个容易被追问。阈值4不是写死的,一般在3.5到4.5之间,报告里写清楚取值和理由即可。另外注意test_users要限定为训练集里也出现过的用户,否则模型没有历史数据可推荐,这属于评估口径的隐含条件。
4. 票房预测落地:特征工程、随机森林与三个评估指标
4.1 票房特征工程:预算对数、类型one-hot与导演历史票房
票房预测真正的难点是特征工程。TMDB原始数据里有budget、genres、runtime、release_date、cast、crew。很多人把原始列直接塞进模型,效果一塌糊涂,因为原始字段不是模型能理解的形态。常见的有效特征是:预算取对数(防止大制作数值把其他特征全部压没)、类型做one-hot、导演和演员做历史票房统计(比one-hot效果好得多)、上映日期转化成档期月份。
# 解析genres,转成多列one-hot编码 def parse_genres(genres_str): try: items = json.loads(genres_str) return [x['name'] for x in items] except Exception: return [] movies['genre_list'] = movies['genres'].apply(parse_genres) genre_dummies = movies['genre_list'].apply(lambda x: pd.Series(1, index=x)) \ .fillna(0).astype(int)这段代码直接以类型名为列生成0/1矩阵。导演特征更实用的做法是统计导演历史平均票房:先把crew解析出导演,再用groupby对全体电影做自关联统计。有一点必须强调:统计历史票房只能使用目标电影上映日期之前的数据,不能把未来的票房也平均进去,否则就是特征泄漏。这是整个票房预测项目里最容易被扣分的技术点,也是老师最爱追问的地方。
4.2 随机森林训练与网格搜索:时间切分、random_state和参数三件套
特征整理成数值矩阵后,先做划分。这里强烈建议用时间划分而不是随机划分——把上映日期靠后的20%电影作为测试集,模拟“用过去预测未来”的真实场景。很多初学者用train_test_split默认的随机切分,答辩时被问一句“你怎么防止时间穿越”就答不上来。
from sklearn.ensemble import RandomForestRegressor # 票房取对数:长尾分布拉平,模型更好拟合 X = feature_df.drop(columns=['revenue', 'title', 'release_date']) y = feature_df['revenue'].apply(lambda x: np.log1p(x)) # 按上映日期排序后切分,严格的"过去预测未来" feature_df = feature_df.sort_values('release_date') cut = int(len(feature_df) * 0.8) X_train, X_test = X.iloc[:cut], X.iloc[cut:] y_train, y_test = y.iloc[:cut], y.iloc[cut:] model = RandomForestRegressor(n_estimators=200, max_depth=12, min_samples_leaf=3, random_state=42) model.fit(X_train, y_train)为什么要对revenue取对数?电影票房服从长尾分布,几部爆款把均值拉得极高,直接用原始值训练,模型会疯狂拟合那几个头部样本,对绝大多数普通电影失效。log1p让目标更接近正态分布,模型学起来更平稳。random_state=42是硬性要求,两个人跑同一个源码必须得到同一份数字,否则连对错基准都没有。max_depth=12限制树深,min_samples_leaf=3避免叶子样本太少过拟合,这两个参数是控制过拟合的关键旋钮。
注意:X和y要跟着排序后的feature_df一起切,先排序再切,否则行索引对不上,模型学到的就不是时间序列关系。
网格搜索用来定参数,不要拍脑袋:
from sklearn.model_selection import GridSearchCV param_grid = { 'n_estimators': [100, 200, 300], 'max_depth': [8, 12, 16], 'min_samples_leaf': [2, 3, 5], } gs = GridSearchCV(RandomForestRegressor(random_state=42), param_grid, cv=5, scoring='r2', n_jobs=-1) gs.fit(X_train, y_train) print(gs.best_params_)网格搜索的代价是训练时间成倍增长,三组参数交叉排列,每组五折交叉验证,等于45次训练,数据量上万条时跑起来还是要等一会儿。如果电脑内存紧张,把n_jobs降到2,或者把参数网格改成不等距的[50, 100, 200]先粗后细跑两轮。scoring='r2'表示交叉验证每次都以R²作为打分标准,这和后面评估口径保持一致,别用默认的mse,否则你选出来的参数和报告里的指标不在一个坐标系上。
4.3 回归评估:R²、MAE、RMSE各看什么,特征重要性怎么写进报告
回归任务的评估我一般三件套都算。R²衡量模型对比“总是预测均值”的基线提升了多少,接近1说明拟合好,为负说明还不如直接猜均值。MAE是平均绝对误差,直观但受大值影响小。RMSE对大误差惩罚更重,能暴露模型在爆款电影上的极端失误。
from sklearn.metrics import r2_score, mean_absolute_error, mean_squared_error y_pred = model.predict(X_test) print("R2:", round(r2_score(y_test, y_pred), 4)) print("MAE:", round(mean_absolute_error(y_test, y_pred), 4)) print("RMSE:", round(np.sqrt(mean_squared_error(y_test, y_pred)), 4)) # 特征重要性Top10,报告里的关键配图 imp = pd.Series(model.feature_importances_, index=X.columns) print(imp.sort_values(ascending=False).head(10))特征重要性是随机森林独有的一大优势,报告里放一张Top10重要性柱状图,比放十页公式推导更有说服力。注意因为目标做了对数变换,MAE和RMSE的单位是log美元,报告里要么注明,要么用np.expm1换算回美元再写。这块常有人踩坑:直接拿对数误差去和别人论文里的美元误差比,数字对不上,以为模型坏了,其实只是量纲没统一。写完这两套系统的实现,下一章集中列一下高频翻车点。
5. 毕设避坑指南:五个高频翻车点与后悔药
以下五条都是这类项目里出现频率最高的踩坑记录,每条按现象、原因、解决三步对照,动手之前先扫一遍能省下大量调试时间。
5.1 推荐列表全是热门电影:稀疏矩阵与流行度偏差
现象:跑出来的Top-N推荐清一色是《阿凡达》《泰坦尼克号》这类大热门,冷门好片永远出不来。原因:评分矩阵稀疏度常高达95%以上,余弦相似度把“没评过分”当“评了0分”,共同0项占比极大,热门电影因为被评次数多、相似度天然偏高。解决:先做去均值归一化再算相似度;更彻底的办法是相似度只在共同评分的电影交集上计算,或先用SVD降维缓解稀疏。对推荐得分除以评分数量的对数做流行度惩罚,也能明显改善多样性。
5.2 训练集R²虚高、测试集为负:时间穿越与特征泄漏
现象:票房预测在训练集上R²高达0.9,测试集却出现负值。原因:最常见的是把上映后才产生的数据(用户评分、评论数、实际票房相关统计)放进了训练特征,或者用随机切分造成时间穿越。解决:按release_date排序后再切分,把“预测时刻拿不到”的列全部删掉;导演和演员的历史票房特征,只能统计目标电影上映日期之前的数据。
注意:做特征自关联统计时,用groupby会整表计算,要先把目标电影排除,否则自己的票房也被算进“历史”里。
5.3 两次运行结果对不上:随机种子没固定
现象:同一份源码,两次运行推荐结果不同,同学复现也对不上数。原因:train_test_split、随机森林、pandas抽样都有随机性,没有固定种子。解决:在入口处统一固定random_state,包括所有模型的random_state参数、sklearn和numpy的全局种子。源码包里最好在main函数的第一行写死,并把这个做法写进报告的环境说明,这属于规范的复现习惯,答辩时是加分项。
5.4 ratings.dat报错与中文乱码:分隔符、编码与python环境
现象:pandas读MovieLens的ratings.dat直接报错,读TMDB的CSV出现中文乱码。原因:双冒号分隔符没指定engine='python',或文件编码是gbk、utf-8不匹配。解决:read_csv指定sep='::'、engine='python';乱码就依次试encoding='utf-8'、'gbk'、'latin1'。如果你是在vscode里跑这套源码,先把python环境配置好——新建虚拟环境、固定pandas和sklearn版本,避免低版本pandas对::分隔符处理不友好的情况。
5.5 RMSE好看但推荐不实用:指标与业务目标错位
现象:评分预测的RMSE很低,模型评估很漂亮,但生成的推荐列表用户不买账。原因:RMSE衡量“预测评分和真实评分的误差”,推荐系统真正要的是“Top-N里用户喜欢的比例”,两者不是一回事。解决:在报告里同时给出RMSE和Precision@10/Recall@10两组指标,分别说明各自反映什么。这一条在答辩里特别加分,很多源码包只写了RMSE,你多写一组排序指标,立刻显得比原项目完整。
6. 让这套源码真正拿到高分:调参曲线、baseline和答辩QA
前五章把推荐和预测的主干讲完了,这套源码距离“高分毕设”还差最后四笔。
第一笔,调参不要靠玄学,画曲线。把ItemCF的k从5跑到50,每5取一个点,画一张Precision@10随k变化的折线图,你会看到它先升后降。报告里放这张图,说明你理解了参数和效果的权衡,比写一句“经过多次试验选定k=20”有力得多。同样的方法用在随机森林的max_depth上,一组参数对应一条曲线,一目了然。
第二笔,给票房预测加baseline。用一个“永远预测训练集均值”的模型作为底线,再把随机森林的R²、MAE和它对比。如果随机森林只比均值高一点点,说明特征工程没做好;如果高出一大截,实验结论就立住了。这招成本极低,但在答辩里能有效回应“你的模型到底好在哪”。
第三笔,报告PDF的结构。高分毕设的报告一般按这个顺序走:问题定义与数据集说明、推荐系统算法设计与实验、票房预测特征工程与实验、总结与展望。重点在实验章,每张表都要有模型、参数、指标三样齐全。对比表写法是一行一个模型、一列一个指标,最后一行永远放baseline。数据来源、随机种子、环境版本全部写进附录,不要在正文里大段抄公式推导。
第四笔,准备几条能扛住追问的QA。答辩老师最爱问的是:冷启动怎么办——答基于内容的相似度兜底;为什么用随机森林而不用线性回归——答非线性能力强且能输出特征重要性;数据量这么小会不会过拟合——答用了时间切分的验证集和网格搜索交叉验证。把这些整理成答辩稿里的QA清单,比临时翻代码从容得多。
我自己当年做毕设时,最后一周才发现训练集和测试集的切分方式有问题,所有实验数据重跑了一遍,差点没赶上提交。从那以后,我拿到任何一份源码,第一件事不是跑模型,而是先读它的评估函数和切分逻辑。代码跑得通只代表能运行,不代表实验有效。如果你正准备拿这个方向做毕设,建议先把第五章的五个坑逐个对照一遍,再动手改代码。希望帮到你。
本文还有配套的精品资源,点击获取