news 2026/9/10 17:28:53

Python电影推荐系统与票房预测项目:从数据处理到模型融合

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python电影推荐系统与票房预测项目:从数据处理到模型融合

简介:基于Python实现的电影推荐与票房预测毕业设计项目,面向计算机相关专业正在做毕设的学生,也适合需要项目实战练习的机器学习初学者。项目经导师指导并通过,评审分达98.5分,包含完整源码、文档PDF与全部数据。压缩包共60个文件,包括16个Python源文件、16个CSV数据集、1份PDF文档,以及Markdown笔记、TXT记录和23张可视化图片,整体约30.94MB,目录按推荐算法、票房预测、数据分析与报告模块划分。推荐部分基于TMDB 5000与MovieLens数据,实现了从人口统计、内容、关键词到KNN、SVD及多种集成策略的推荐算法,覆盖不同推荐思路的搭建与评估;票房预测部分使用TMDB 7398条电影信息,对预算、评分、时长、热度等特征做可视化分析与回归建模,完整呈现从数据清洗到结果输出的流程。已有260人学习,可作为课程设计、期末大作业或毕设立项参考。

1. 一个能同时做票房预测与推荐系统的 Python 项目

做毕设和简历项目时,最怕的是“只有一个模型跑通”。这个电影项目把两件经常分开的事放在了一个源码包里:一边用 TMDB 的票房、演职员数据做机器学习回归预测,另一边用 MovieLens 用户评分矩阵做电影推荐系统。推荐部分拆成 naive_recommender、personal_recommender、ensemble_recommender 三套递进方案,冷启动和稀疏矩阵问题都有对应解法;票房预测则单独使用 prediction/train.csv 与 test.csv 做训练和验证。这套源码能拿高分,胜在数据清洗、特征工程、三套推荐算法和融合策略形成完整链路,而不是单个模型新。适合正在做毕业设计、课程设计,或者想用 Python 把推荐系统完整落一遍的人参考。下面从数据层开始拆。

2. TMDB 数据清洗与特征工程:票房预测前先看数据分布

2.1 先理解文件结构:谁给票房预测用,谁给推荐用

拿到压缩包后会看到 data、prediction、naive_recommender、personal_recommender、ensemble_recommender、FeatureEDA 和 report。最容易踩的第一个坑是“把所有 csv 当成一个数据集来灌模型”。实际上,票房预测和推荐系统用的数据源不同:预测侧依赖 Kaggle 的 TMDB Box Office Prediction 给出的 train.csv/test.csv,推荐侧才是 data 下的 tmdb_5000_movies.csv 与 tmdb_5000_credits.csv,再加上 personal_recommender 目录里的评分记录 train.csv/test.csv。

文件路径角色
data/tmdb_5000_movies.csv电影侧元数据:预算、票房、语言、时长、评分、简介、热度
data/tmdb_5000_credits.csv演职员与关键词,按电影标题关联
prediction/train.csv票房预测训练集,含真实票房 revenue
prediction/test.csv票房预测评估集,结构对齐 train
personal_recommender/train.csvMovieLens 风格评分矩阵:userId、movieId、rating
personal_recommender/test.csv评分预测的测试集
FeatureEDA/single_feature_visual.py单特征可视化脚本,输出 figures 图

理解完分工后再合并数据。这里有个容易被忽略的点:tmdb_5000_credits.csv 里没有独立的 id 列时,用 title 关联是唯一选择,但 title 并不是严格唯一的。合并后最好按 “title + release_date” 再查一次重复,避免后面算相似电影时出现同一部电影的两个副本。项目里 report/电影数据分析.md 已经把这些检查和结论写进去了,复现时可以先读它再动手。

2.2 合并 movies 与 credits 并解析 JSON 列

tmdb_5000_movies.csv 里 genres、keywords、production_countries 之类字段是一串 JSON 字符串,直接进模型只能当作文本垃圾。常见做法是用 json.loads 把每个元素里的 name 抽出来,转成 Python 列表。

import json import pandas as pd movies = pd.read_csv("data/tmdb_5000_movies.csv") credits = pd.read_csv("data/tmdb_5000_credits.csv") df = movies.merge(credits, on="title") def parse_names(value): if isinstance(value, float): return [] try: return [item["name"] for item in json.loads(value)] except Exception: return [] df["genres"] = df["genres"].apply(parse_names) df["keywords"] = df["keywords"].apply(parse_names) df["production_countries"] = df["production_countries"].apply(parse_names)

这段代码的要点是 parse_names 里同时处理了三种异常:空值(float NaN)、JSON 里缺 name 字段、以及少量脏数据导致的解析异常。没有这个兜底,单条脏数据会让整个清洗中断。genres 和 keywords 后面会直接用于 Keyword 推荐器,所以这里宁可返回空列表,也不要让整行数据被丢弃。

2.3 预算、日期、运行时长的特征工程

机器学习的票房预测任务中,预算、上映时间、时长是最常见的三个数值特征。TMDB 数据里 budget 存在大量 0,0 不是真实预算,而是“未收录”。直接把 0 喂给回归模型,等于告诉模型这些电影预算为 0,会拉低预测结果。我一般按年份做中位数填充:同一年份的电影制作成本更可比。

import numpy as np df["release_date"] = pd.to_datetime(df["release_date"], errors="coerce") df["year"] = df["release_date"].dt.year df["month"] = df["release_date"].dt.month df["budget"] = df["budget"].replace(0, np.nan) df["budget"] = df.groupby("year")["budget"].transform(lambda x: x.fillna(x.median())) df["runtime"] = df["runtime"].fillna(df["runtime"].median())

这里的 groupby + transform 不会改变索引,能直接把同一年份的中位数放回每一行。如果担心上映月份信息量不够,可以把它做成节假日标签而不是单纯传数字给模型。比如 5-7 月、11-12 月作为暑期档/假日档的二值特征,效果通常比 month 数值本身更稳定。另一个项目里常见的操作是把 year 单独作为特征,但在预测未来的票房时,year 越接近训练集边界越容易过拟合,要留意时间泄漏。

2.4 用 single_feature_visual.py 观察偏态分布

开始建模前先看数据分布。FeatureEDA/single_feature_visual.py 做的事情很朴素:把每个特征画成直方图,再把原图和 log1p 变换后的图放在一起对比。

import matplotlib.pyplot as plt def single_feature_visual(data, col, save_path): fig, axes = plt.subplots(1, 2, figsize=(10, 4)) data[col].hist(bins=50, ax=axes[0]) axes[0].set_title(f"raw {col}") data[col].apply(np.log1p).hist(bins=50, ax=axes[1]) axes[1].set_title(f"log1p {col}") plt.tight_layout() plt.savefig(save_path, dpi=120, bbox_inches="tight") plt.close(fig) single_feature_visual(df, "budget", "figures/budget_dist.png")

注意这里只对非负特征用 np.log1p,负数取 log1p 没有意义。看完图后会发现 budget、revenue、popularity 都是典型长尾分布:少数大片占掉绝大多数票房。这意味着预测目标设为原始数值时,均方误差会被高票房样本主导,回归器会拼命去拟合那几个大片,忽略腰部电影。所以第 3 章建模时,我不直接用 revenue,而是用 log1p(revenue) 作为目标。

3. 票房预测:用对数目标与随机森林复现回归流程

3.1 先分清训练集和测试集字段

票房预测的训练数据在 prediction/train.csv,评估数据在 prediction/test.csv。train 里包含 id、budget、popularity、runtime、release_date、revenue 等字段;test 与 train 字段一致,唯一区别是没有 revenue。读取后先做列对齐,不要在 train 上 fit 后又发现 test 少一列。

train = pd.read_csv("prediction/train.csv") test = pd.read_csv("prediction/test.csv") print(train.shape, test.shape) print(train.columns == test.columns)

训练集和测试集的规模不一样是正常现象。Kaggle 的 TMDB Box Office Prediction 的 train/test 划分并不完全按时间,所以复现时要清楚这是“比赛划分”而不是“时间划分”。项目里 report 之所以强调 7398 条,是因为这是 TMDB Box Office 全量数据中完成合并后的条数,tmdb_5000_movies.csv 是另一份 5000 量级的推荐元数据,不要混谈。

3.2 数值特征补全与类别特征的均值编码

特征处理需要同时照顾数值列和类别列。像 original_language、belongs_to_collection 这种类别特征,高基数但无法直接顺序编码,最稳妥的做法是均值编码,也就是用训练集里该类别对应的目标均值代替类别本身。

num_features = ["budget", "popularity", "runtime"] cat_features = ["original_language", "belongs_to_collection"] for col in num_features: train[col] = train[col].fillna(train[col].median()) test[col] = test[col].fillna(train[col].median()) for col in cat_features: train[col] = train[col].fillna("None") test[col] = test[col].fillna("None") means = train.groupby(col)["revenue"].transform("mean") train[col + "_target"] = means test[col + "_target"] = test[col].map(means).fillna(train.groupby(col)["revenue"].mean().mean())

这里的关键是 test 的 fillna 必须用 train 的中位数,不能直接 test 列自己的 fillna,否则训练和测试分布不一致。类别列的新类别用全局均值兜底,是均值编码最常见的防泄漏写法。注意不要直接把原始 revenue 参与均值编码,再用同一个 revenue 去训练,会形成数据泄漏;这里只对训练集用 transform,对测试集用 map,逻辑上没有问题。

特征处理方式说明
budget按年份中位数填充避免 0 值干扰
runtime全局中位数填充缺失占比小的列
original_language均值编码 + 新类别兜底高频类别才有统计意义
release_date拆成 year/month保留周期信息

3.3 随机森林基线:参数和评估指标

把预测目标变成 log1p(revenue) 后,就可以跑基线模型。推荐用随机森林而不是线性回归作为第一个模型,因为它可以处理均值编码后的浮点列、缺失值容忍度也更好,不需要做标准化,省掉很多 pipeline 调试时间。

import numpy as np from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import cross_val_score features = [c for c in train.columns if c not in ["id", "revenue"]] X = train[features] y = np.log1p(train["revenue"]) model = RandomForestRegressor( n_estimators=300, max_depth=12, min_samples_leaf=3, max_features=0.6, n_jobs=-1, random_state=42, ) scores = cross_val_score(model, X, y, cv=5, scoring="neg_root_mean_squared_error") print("RMSE(log):", -scores.mean(), "+/-", scores.std())

参数含义:n_estimators 设置 300 是因为再大收益有限,而训练时间成倍增长;max_depth=12 防止树对噪声特征过拟合;min_samples_leaf=3 让叶节点至少包含 3 个样本,能明显提升泛化;max_features=0.6 随机挑选 60% 特征参与分裂,降低树与树之间的相关性。这个基线如果 log 空间 RMSE 在 2 附近,说明模型基本捕获了数量级,但误差仍然巨大。原因是票房预测本身受宣传、档期、口碑等外部因素影响,不在特征里。

3.4 把预测结果还原成票房,并处理预测列缺失

在 commit 前要保证 test 和 train 的特征完全一致。test 里可能存在 train 没有缺失的值,在 random forest 的 predict 前统一填充:

test_X = test[features].fillna(train[features].mean()) pred_log = model.predict(test_X) pred_revenue = np.expm1(pred_log) submission = pd.DataFrame({"id": test["id"], "revenue": pred_revenue}) submission.to_csv("prediction/result.csv", index=False)

np.expm1 是 log1p 的逆变换;如果训练时用的是 np.log1p,预测就必须用 np.expm1,否则预测结果是“对数票房”而不是“票房”。一个常见的错误是对 test_X 直接用 test 的均值填充,而不是用 train 的均值,这会让分布偏移。另一个容易踩的坑是预算为 0 的电影,均值编码和填充后,模型会默认它们是低预算作品,实际这类电影可能是大制作但 TMDB 没录入预算,所以建议在 2.3 节按年份填充后再进模型。

4. 推荐系统三套代码:从全局热榜到 SVD 个性化

4.1 Demographic 加权评分:解决“新用户没历史”的基线

推荐系统首先要面对冷启动。新用户没有任何评分历史,协同过滤没有输入,所以项目里先放一个 naive_recommender/Demographic.py,用全局评分做兜底。它复刻的是 IMDb Top 250 的加权分公式:

m = df["vote_count"].quantile(0.9) C = df["vote_average"].mean() R = df["vote_average"] v = df["vote_count"] df["demographic_score"] = (v / (v + m)) * R + (m / (v + m)) * C top = df.nlargest(20, "demographic_score")[["title", "demographic_score"]]

m 是投票数 90 分位数,低于它的电影权重会向全局平均分 C 靠拢,避免出现“只有一个 10 分但没人看过”的电影排在前面。这个公式不需要训练,线上可以直接算,适合做推荐系统的默认列表。它的问题也很明显:结果偏向大众电影,没有个性化,所以只能作为第一层。

4.2 Content 与 Keyword:从简介和关键词里找相似度

个人化推荐的第一步是把电影表示成文本向量。Content.py 基于 overview 简介,Keyword.py 基于 keywords 关键词列表,两者都走 TF-IDF + 余弦相似度。

from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import linear_kernel tfidf = TfidfVectorizer(stop_words="english", max_features=20000) overview_matrix = tfidf.fit_transform(df["overview"].fillna("")) overview_sim = linear_kernel(overview_matrix, overview_matrix) def recommend_by_overview(title, n=10): idx = df.index[df["title"] == title][0] candidates = overview_sim[idx].argsort()[::-1][1: n + 1] return df.iloc[candidates]["title"].tolist()

TfidfVectorizer 里的 max_features=20000 限制词典规模,stop_words="english" 去掉无意义的虚词;linear_kernel 在稀疏矩阵上计算余弦相似度比 cosine_similarity 更快,结果等价。Keyword.py 的思路一样,只是把 df["keywords"] 先通过空格连接成字符串再进入向量化。实际项目中我会把两者按 0.7 和 0.3 加权合并成混合相似度:剧情相似和标签相似各管一个维度,比只用一个字段更稳。

4.3 personal_recommender:MovieLens 评分矩阵上的 KNN 与 SVD

personal_recommender 目录下的 KNN_user.py、KNN_movie.py、Personal_SVD.py 是真正的个性化推荐部分。数据来自 MovieLens 风格的评分文件 train.csv,包含 userId、movieId、rating,用 surprise 库加载最方便。

from surprise import Dataset, Reader, SVD, KNNBasic import pandas as pd ratings = pd.read_csv("personal_recommender/train.csv") reader = Reader(rating_scale=(1, 5)) data = Dataset.load_from_df(ratings[["userId", "movieId", "rating"]], reader) svd = SVD(n_factors=120, n_epochs=40, lr_all=0.005, reg_all=0.02) knn_user = KNNBasic(k=40, min_k=5, sim_options={"name": "pearson_baseline", "user_based": True}) knn_movie = KNNBasic(k=40, min_k=5, sim_options={"name": "pearson_baseline", "user_based": False})

SVD 的 n_factors 控制隐向量维度,120 对 MovieLens 常见规模够用;n_epochs=40 太大容易过拟合,如果训练集只有几万条评分,30 更好;lr_all 是学习率,0.005 偏保守,配合 reg_all=0.02 能稳住训练。KNNBasic 的 user_based 决定找相似用户还是相似电影;在评分矩阵稀疏时,KNN_movie 通常比 KNN_user 稳,因为电影的相似度比用户相似度更稳定。交叉验证时用 RMSE 即可,surprise 自带实现:

from surprise.model_selection import cross_validate for algo in [svd, knn_user, knn_movie]: cv = cross_validate(algo, data, measures=["RMSE"], cv=5, verbose=True)

注意:随机切分的交叉验证不能代表冷启动场景。要验证冷启动,用 personal_recommender/test.csv 做离线预测,效果更接近线上。

推荐器代表文件适合场景主要局限
Demographicnaive_recommender/Demographic.py新用户首页无个性化
Contentnaive_recommender/Content.py剧情相似推荐依赖文本质量
Keywordnaive_recommender/Keyword.py类型标签推荐关键词覆盖不全
KNN Userpersonal_recommender/KNN_user.py用户历史丰富稀疏时相似度不可靠
KNN Moviepersonal_recommender/KNN_movie.py评分矩阵稀疏新电影无评分
SVDpersonal_recommender/Personal_SVD.py评分数据充足冷启动与解释性不足

5. ensemble_recommender 融合调优:动态权重比换模型更划算

ensemble_recommender 目录里的 KNN_SVD_ensemble.py,做的就是 KNN 与 SVD 的加权融合;KNN_usr_keywords.py 再引入用户历史喜欢电影的关键词,把候选集扩展出来;KNN_movie_usr_ensemble.py 则把用户维度、电影维度和混合内容分合并到一起。与其追求更复杂的模型,不如先解决“不同用户该信任哪一路信号”的问题。

def blend_rating(user_id, movie_id, n_history): svd_score = svd.predict(user_id, movie_id).est knn_score = knn.predict(user_id, movie_id).est content_score = content_sim(user_id, movie_id) if n_history < 20: w_cf = 0.4 # 协同过滤信号不足,提高内容权重 else: w_cf = 0.7 # 历史足够,矩阵分解更可靠 return w_cf * (0.5 * svd_score + 0.5 * knn_score) + (1 - w_cf) * content_score

n_history 是用户历史评分数量。这个动态权重的核心是:冷启动用户不要强行用 KNN/SVD,给内容推荐更高比例;历史丰富的用户则让 SVD 主导。权重参数不必随机拍,项目里常见做法是在 personal_recommender/result.csv 上做网格搜索,把 0.1 到 0.9 按步长 0.1 扫一遍,选 RMSE 最低的组合。

SVD 本身可以通过 surprise 的 GridSearchCV 调参:

from surprise.model_selection import GridSearchCV param_grid = { "n_factors": [80, 120], "n_epochs": [30, 50], "lr_all": [0.005, 0.01], "reg_all": [0.02, 0.05], } gs = GridSearchCV(SVD, param_grid, measures=["rmse"], cv=5, n_jobs=-1) gs.fit(data) print(gs.best_params["rmse"])

n_factors 建议从 80 开始而不是一开始就 200,因为 MovieLens 评分矩阵稀疏度很高,维度太大只会放大低频电影的噪声;lr_all 调大后必须同步调大 reg_all,否则训练末期 loss 会抖动。GridSearchCV 跑完后再去 ensemble_recommender/result.csv 里看残差,把残差按 movieId 排序后,误差最大的一定是内容信号不足的冷门片。先把这部分电影的 content_score 权重往上调 0.1,再跑一次 result.csv 对比 RMSE,比直接换模型更有效。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 17:25:54

CANN/ge图引擎API:获取依赖资源键

GetReliedOnResourceKeys 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、T…

作者头像 李华
网站建设 2026/9/10 17:25:18

Airflow Task State Store 数据如何定期清理与设置保留期?

Airflow Task State Store 数据如何定期清理与设置保留期&#xff1f; 【免费下载链接】airflow Apache Airflow - A platform to programmatically author, schedule, and monitor workflows 项目地址: https://gitcode.com/GitHub_Trending/ai/airflow 如果你在用 Air…

作者头像 李华
网站建设 2026/9/10 17:25:09

AI动态日志采样:高并发下日志系统的弹性治理方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 17:22:30

2026年9月郑州办公空间定制沙发选型:避坑要点,合同明细

2026年9月郑州办公空间定制沙发选型&#xff1a;避坑要点&#xff0c;合同明细 办公空间定制沙发不仅是企业门面工程的核心要素&#xff0c;更是直接影响员工办公效率与客户接待体验的关键载体。选对沙发&#xff0c;能瞬间提升空间质感与品牌调性&#xff1b;选错沙发&#xf…

作者头像 李华
网站建设 2026/9/10 17:22:10

Web漏洞扫描的范式革命:从规则引擎到智能攻击面管理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华