简介:一套基于机器学习的电影票房预测平台项目,面向机器学习初学者以及需要完成毕业设计、课程设计的高校开发者。项目整合 TMDB 电影数据集,覆盖数据清洗、特征工程、线性回归/决策树/随机森林等模型训练、票房预测与可视化展示完整流程,并附带协同过滤与推荐系统模块,代码含详细注释,便于二次开发与本地部署。压缩包共58个文件,大小30.71MB,包含Python源码(16个py)、CSV数据集(16个csv)、效果截图(23个png)以及Markdown/Word图文文档,目录划分清晰,可按模块快速查阅。已有502人学习下载,适合需要搭建可演示项目、撰写实验报告或进行答辩展示的开发者。文档中细致记录数据分析与建模思路,能帮助理解影响票房的关键因素及不同推荐算法的实现细节,是较为完整的高分参考项目。
1. 基于机器学习的电影票房预测:这门课设到底在解决什么问题
一个刚接触机器学习的同学,最容易把“电影票房预测”当成一个分类问题——预测票房是“高”还是“低”。但真实的榜单和工业需求里,甲方要的从来不是“会爆”还是“扑街”,而是一个能进预算表的数字:上映首周大概能收多少、总票房落在什么区间。基于机器学习的电影票房预测平台,本质上是一条“数据清洗 → 特征工程 → 回归建模 → 评估解释”的完整流水线,而不是某个一跑就出结果的魔法脚本。这篇笔记按我自己的落地经验,把平台最常被问到的几个环节拆开:数据集怎么选、票房特征怎么构造、回归模型怎么调、结果怎么交待给不懂机器学习的人,以及那些让项目从 80 分掉到 60 分的隐藏坑。适合正在做课程设计、毕业设计,或者想快速验证“机器学习能不能预测文娱消费大盘”的同学,照着复现大概一个下午能跑通,但不建议直接把数字拿去排片,后面会讲为什么。
2. 数据集准备与票房特征工程:先把“预测什么”定义清楚
2.1 票房目标变量:回归还是分类,先看评分标准
很多课程设计卡在第一步:题目写“票房预测”,但评分的人到底想看到回归结果还是分类结果?我的建议是两类都做,回归为主、分类为辅。回归模型给出连续票房预测值,方便计算 RMSE、MAE;而分类模型(比如把票房切成“低 / 中 / 高”三档)用来回答“这部片会不会成为爆款”这种模糊问题,也方便做可视化展示。
数据集的来源,常见做法是公开影评数据平台,比如 TMDB 的年度电影数据、某高校实验室开放的电影元信息集合,或者 Kaggle 上的 TMDB Box Office Prediction 比赛数据。首选含字段比较全的数据集,至少要有:电影标题、上映日期、预算、时长、评分、语种、制作国家、发行公司、类型数组。如果数据集里直接给了票房,那目标变量就有了;如果只给了评分和元信息,那需要自己外接公开榜单数据按标题匹配。
关于目标变量的一个关键决策:直接预测“全球总票房”“北美首周票房”还是“国内累计票房”。不同口径的数据分布完全不同,而且某些口径下部分电影没有值。我常用的做法是只保留“至少覆盖 80% 样本”的票房口径作为主目标,其余口径留作扩展验证。预测之前记得做标签平滑,因为好莱坞大片和小众文艺片的票房能差三个数量级,回归目标直接丢进线性模型会被极值带偏。
2.2 清洗与字段统一:把“日期”和“金额”拉回同一起跑线
拿到原始 CSV 后,第一步不是建模,而是看 dtypes 和缺失率。常见数据集会出现以下问题:日期字段是字符串混合格式,有的带小时分钟,有的只有年月日;预算字段有 0、None、空字符串三种“缺失写法”;类型字段是 JSON 数组字符串;ID 字段在这一份数据里是整数、在另一份里是带前缀的字符串。这些不统一,后面 join 和特征交叉时全都会翻车。
import pandas as pd import numpy as np from datetime import datetime import json df = pd.read_csv("movies.csv") print(df.dtypes) print(df.isnull().sum() / len(df)) # 统一日期字段:从 release_date 里提取年份和季节 def parse_release_date(series): s = series.astype(str).str.strip() s = s.replace({"NaT": np.nan, "None": np.nan, "": np.nan}) dt = pd.to_datetime(s, errors="coerce", format="mixed") return dt df["release_date"] = parse_release_date(df["release_date"]) df["release_year"] = df["release_date"].dt.year df["release_month"] = df["release_date"].dt.month # 预算字段:缺失按中位数填补,0 值单独打标 df["budget_zero_flag"] = (df["budget"] == 0).astype(int) budget_median = df.loc[df["budget"] > 0, "budget"].median() df["budget"] = df["budget"].replace(0, np.nan) df["budget"] = df["budget"].fillna(budget_median) # 类型字段:从 JSON 字符串解析出类型名称 def parse_genres(x): try: items = json.loads(x) return [item["name"] for item in items] except Exception: return [] df["genre_list"] = df["genres"].apply(parse_genres) print(df[["release_year", "release_month", "budget", "budget_zero_flag"]].describe())这里有两个容易忽略的细节。第一,日期解析我用format="mixed",不同数据集的日期格式未必统一,强制指定格式会直接抛错,而 mixed 会把能解析的解析掉、不能解析的变成 NaT,再做后续填充。第二,预算为 0 不等于没预算,很多低预算独立电影就是 0,直接中位数填补会把独立电影误抬成商业片,所以我单独留一个budget_zero_flag让模型自己去学“这个数字是不是可信”。
如果数据集里预算单位不一致(有的数据集给美元、有的给人民币),一定要先统一单位再进特征工程。我见过一份数据把“万”和“亿”混在同一个数字列里,建模之前没发现,最后 top 特征全是“发行公司名称”,因为单位混乱导致的数值噪声把真正信号都盖住了。检查方法也很简单:对每个疑似金额字段做分位数统计,看最大值和 25 分位之间是否差出 10 的幂次。
2.3 票房预测特征集合:除了预算和评分,还能加什么
很多模板项目的特征只有“预算 + 时长 + 评分”,跑完发现验证集 RMSE 低得离谱,以为是模型强,实际是过拟合了历史数据。我一般会把特征按信息来源分成四组,并在代码里预留开关,方便对比“只用元数据”和“加上内容特征”的差异。
- 影片元数据:预算、时长、语种、制作国家数量、是否为续集、发行月份、发行年份、发行季。发行月份是强特征,暑期档和贺岁档的票房表现天然有周期性。
- 阵容特征:导演最近三部作品的票房均值、主演最近五部作品的票房均值。这部分需要另外建表,初版平台可以不用,但要留接口。
- 市场特征:同档期上映影片数量、前一周票房冠军的票房数值。注意这类特征极容易引入数据泄漏,预测时点之后的数值不能参与训练。
- 内容侧特征:以类型集合做 multi-hot、以制作公司做目标编码、以关键词做词频统计后降维。
from sklearn.preprocessing import MultiLabelBinarizer mlb = MultiLabelBinarizer() genre_mat = mlb.fit_transform(df["genre_list"]) genre_df = pd.DataFrame(genre_mat, columns=mlb.classes_, index=df.index) # 与主表拼接 df = pd.concat([df, genre_df], axis=1) # 核心特征开关:字典控制哪些特征进入模型 feature_flags = { "budget": True, # 预算 "runtime": True, # 时长(分钟) "release_month": True, # 发行月份 "release_year": True, # 发行年份 "genre_multi_hot": True, # 类型 multi-hot "vote_average": True, # 观众评分 "budget_zero_flag": True, # 预算是否缺失标记 }MultiLabelBinarizer会把“剧情 / 喜剧 / 爱情”变成一个多行多列的 0/1 矩阵,而不是把整个列表当成一个字符串。这里有个性能提醒:如果数据集里有几千部电影、几十种类型,这个矩阵还不算大;但如果后续上关键词特征,矩阵会膨胀到上万维,届时要么用 PCA 压缩、要么进树模型时换用SparseMatrix。课程设计阶段不建议做太重的文本特征,评审老师更看重你能解释特征含义,而不是堆了十个模型调出 A 类分数。
3. 回归模型构建与对比:从线性回归到集成树的升级路径
3.1 数据切分与基准模型:为什么不做随机打散
常见项目喜欢用train_test_split(df, test_size=0.2, random_state=42)一把梭,但票房预测有时间属性,随机切分会把 2019 年的电影混进训练集、2022 年的混进验证集,造成“用未来预测过去”的数据泄漏。做时间序列型回归,我一般用按年份排序的方式切分:前 80% 年份作训练、后 20% 年份作验证,这样才能真实模拟“拿历史数据预测未来新片”的场景。
from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression df = df.sort_values("release_date").reset_index(drop=True) split_idx = int(len(df) * 0.8) train_df = df.iloc[:split_idx] valid_df = df.iloc[split_idx:] # 特征矩阵统一构建 feature_cols = [] if feature_flags["budget"]: feature_cols.append("budget") if feature_flags["runtime"]: feature_cols.append("runtime") if feature_flags["release_month"]: feature_cols.append("release_month") if feature_flags["genre_multi_hot"]: feature_cols += genre_df.columns.tolist() X_train = train_df[feature_cols] y_train = np.log1p(train_df["revenue"]) X_valid = valid_df[feature_cols] y_valid = np.log1p(valid_df["revenue"]) model_lr = LinearRegression() model_lr.fit(X_train, y_train)对票房取log1p不是可选项,而是必需项。票房分布极端右偏,绝大多数电影票房在千万到几亿之间,少数爆款冲到几十亿,这种分布直接放进线性回归,损失函数会被尾部样本主导,预测结果集中在均值附近,完全失去区分度。取对数之后分布趋近正态,模型优化的目标也更符合直觉:误差 1 倍和误差 10 倍之间的相对差异被拉平。
这里还要注意feature_cols里放的是 DataFrame 的列名列表,如果genre_df和主表拼接时索引没有对齐,训练时不会报错,但预测时会出现特征顺序错位。我在拼接后都会加一行assert X_train.isnull().sum().sum() == 0,把缺失情况提前暴露出来。
3.2 随机森林与边界参数:三要素比调参更重要
第二种模型选随机森林,原因是它对特征缩放的容忍度高、自带特征重要性、不容易把 log 后的目标变量学成线性拟合。但随机森林在票房预测上有个特点:它对“见过类似特征的样本”预测准,对“没见过的小众类型”预测偏差大。这不代表模型不好,而是提醒你不要只看整体指标,要分类型看误差。
from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score model_rf = RandomForestRegressor( n_estimators=300, max_depth=16, min_samples_leaf=4, max_features="sqrt", n_jobs=-1, random_state=42, ) model_rf.fit(X_train, y_train) pred_lr = model_lr.predict(X_valid) pred_rf = model_rf.predict(X_valid) def evaluate(y_true, y_pred, name): rmse = float(np.sqrt(mean_squared_error(y_true, y_pred))) mae = float(mean_absolute_error(y_true, y_pred)) r2 = float(r2_score(y_true, y_pred)) print(f"{name}: RMSE={rmse:.4f}, MAE={mae:.4f}, R2={r2:.4f}") return {"model": name, "rmse": rmse, "mae": mae, "r2": r2} result_lr = evaluate(y_valid, pred_lr, "linear") result_rf = evaluate(y_valid, pred_rf, "random_forest")这几个参数不是拍脑袋定的。n_estimators=300是多数公开数据集上误差开始平稳的拐点;max_depth=16防止树无限生长导致训练集得分过高;min_samples_leaf=4强制每个叶子至少有 4 个样本,减少对离群票房电影的过拟合;max_features="sqrt"让每棵树只看特征子集,提升树之间的多样性。如果你发现验证 R2 很低,先检查是不是没有对目标变量取对数,再检查特征里是否混入了上映日期这类天然递增字段。
3.3 结果回译输出:训练在 log 空间,交报告要还原到真实票房
模型在 log 空间训练,预测结果也是 log 值,交报告时必须转为真实票房,否则评审老师看到“预测票房 18.4”会直接问“单位是什么”。回译时用np.expm1(),并且在转化后做一次 per-sample 四舍五入到十万元位,方便列表展示。
pred_revenue_lr = np.expm1(pred_lr) pred_revenue_rf = np.expm1(pred_rf) # 抽样展示 10 条,包括实际票房、预测票房、绝对误差 sample_show = valid_df[["title", "release_date", "revenue"]].copy() sample_show["pred_lr"] = pred_revenue_lr sample_show["pred_rf"] = pred_revenue_rf sample_show["abs_err_rf"] = (sample_show["pred_rf"] - sample_show["revenue"]).abs() show_cols = ["title", "release_date", "revenue", "pred_lr", "pred_rf", "abs_err_rf"] print(sample_show[show_cols].head(10).to_string(index=False))这里有个容易被忽略的细节:abs_err_rf的分布通常长尾明显,少量爆款片贡献了大头误差。写文档时不要只放 RMSE,要放“中位数绝对误差”和“P90 误差”。因为对排片方来说,预测误差的分布形状比平均值更重要——误差集中在少数爆款片上,比误差均匀分布在中腰部影片上更危险。
4. 平台周边与验证:前端表格、评分脚本与模型可解释性
4.1 把预测过程变成评审可见的流程
很多高分项目的源码不止一个模型脚本,而是一个微型平台:上传新片元数据 → 后台调起模型 → 前端展示预测结果与特征贡献。它的价值不在 UI 多好看,而在把“模型怎么思考”暴露给使用的人。我习惯在源码里单独留一个analyze_one_sample函数,接受一行输入元数据,输出每个特征对预测值的正负贡献。
import eli5 from eli5.sklearn import PermutationImportance # 用置换重要性代替默认特征重要性,更稳 perm = PermutationImportance(model_rf, scoring="neg_root_mean_squared_error", n_iter=10, random_state=42) perm.fit(X_valid, y_valid) eli5.show_weights(perm, feature_names=feature_cols) # 单样本解释:观察某部新片的特征偏离方向 new_movie = pd.DataFrame([{ "budget": 80000000, "runtime": 118, "release_month": 7, "budget_zero_flag": 0, }]) for col in genre_df.columns: new_movie[col] = 1 if col == "Action" else 0 new_movie[col] = new_movie[col].astype(int) new_X = new_movie[feature_cols] log_pred = model_rf.predict(new_X)[0] pred_value = np.expm1(log_pred) print(f"新片预测总票房(约): {pred_value / 100000000:.2f} 亿元")置换重要性有一个比默认重要性更稳的解读:它会随机打乱某个特征后重新计算误差,误差上升越多说明该特征越重要。对票房预测这个场景,预算和发行年份的重要性通常靠前,类别特征里的“类型 multi-hot”重要性很低,这是合理的——类型会影响排片和宣发,但很难直接决定一个准确票房数字。
4.2 文档说明的撰写组织:源码之外,评审看什么
“高分项目”的文档说明通常决定最终印象分。我有一次看过两个模型效果差不多的项目,一个文档里只有 README 和训练日志,另一个按“数据集说明 → 特征字典 → 模型评估 → 复现步骤”四部分组织,后者被当成参考模板。建议至少包含以下表格,不要让评审去代码里反推字段含义。
| 文件 | 作用 | 关键内容 |
|---|---|---|
data/ | 存放原始数据集与清洗脚本 | 数据来源说明、字段单位 |
features.py | 特征工程模块 | 特征字典、缺失值处理策略 |
train.py | 训练入口 | 三种模型对比结果 |
predict.py | 单样本预测接口 | 返回值与 log 还原逻辑 |
requirements.txt | 运行环境 | 版本尽量与测试环境一致 |
字段字典表是重点。不要只写“revenue: 票房”,而写成“revenue: 目标变量,单位美元,取值范围 0 到 28 亿,缺失率 2%,已做 log1p 变换”。这种描述让评审一眼看出你处理过数据,而不是把数据读进来就跑了。
4.3 可视化与结果判断标准
提交之前,必做两个图:真实票房与预测票房的散点图,以及残差分布直方图。前者用来看大范围是否正确,后者用来看是否存在系统性高估或低估。如果残差图出现明显偏态,比如低票房区域模型全部高估,说明特征里缺少资产规模或发行规模的信号,这时不应盲目换模型,而是回头补特征。
import matplotlib.pyplot as plt plt.figure(figsize=(8, 6)) plt.scatter(np.expm1(y_valid), np.expm1(pred_rf), alpha=0.5, s=12) plt.xscale("log") plt.yscale("log") plt.xlabel("实际票房 (美元)") plt.ylabel("预测票房 (美元)") plt.plot([1e6, 1e9], [1e6, 1e9], "r--", lw=2) plt.tight_layout() plt.savefig("pred_vs_actual.png", dpi=150)注意两个坐标轴都用了 log 尺度。如果不用 log 尺度,少数几部大片的点会把其他样本全部挤在左下角,图面信息完全丢失。散点图配合对角线虚线,能直观看出模型在哪个量级开始失真——如果低端大量点落在对角线上方,说明模型对冷门片预测过于乐观;如果高端大量点落在线下方,说明爆款片被系统性低估。这两种毛病对应的修法截然不同,前者加预算特征、后者需要加营销侧或前作票房特征。
5. 票房预测平台避坑:五个最容易影响项目评价的问题
5.1 一跑代码报 KeyError:特征列顺序悄悄变了
现象:训练时model.fit(X_train, y_train)正常,预测时model.predict(X_valid)报KeyError,或者不报错但结果全部是同一值。
原因:feature_cols是手动拼出来的列表,中间某一步用pd.concat拼 genre 特征时索引错位,或者特征列里混入了 boolean 类型而拼接时被自动转成 object。
解决:在训练前做一次列对齐检查,不要依赖 DataFrame 列的自然顺序。
assert sorted(X_train.columns.tolist()) == sorted(feature_cols) assert sorted(X_valid.columns.tolist()) == sorted(feature_cols) X_train = X_train[feature_cols] X_valid = X_valid[feature_cols]如果仍然报错,用df.columns.tolist()打印出来看,排查是否有列名重复。我遇过一个数据集里有两列都叫id,一个表示电影 ID、一个表示发行商 ID,pd.concat后自动改名成id和id_1,手动特征列表没跟上就直接错位了。
5.2 验证指标很漂亮,提交时被打回:切分方式不对
现象:本地交叉验证 RMSE 很低,但用新增年份数据做外推时预测值严重偏移。
原因:直接用train_test_split随机切分,同一部系列电影的续集被同时分进训练和验证,模型从标题等特征里记住了答案。票房预测必须按时间切分,至少按年份,更严格按上映日期。
解决:把切分代码改成按日期排序后的位置切分,并在文档里写清楚训练集时间范围。课程设计阶段评分标准如果没强调时间维度,交随机切分也能跑通,但答辩时被问到“2019 年的信息出现在训练集里是否合理”,就会陷入被动。
5.3 模型只有线性回归能跑,树模型结果反而更差
现象:线性回归验证 R2 能到 0.6,随机森林只有 0.3,甚至负值。
原因:概率分布差异。票房取对数后,随机森林对特征空间的划分仍然偏保守,而线性回归对 log1p 目标天然拟合不错。另一个常见原因是特征里含有太多 0/1 标志变量,树模型对纯稀疏特征不敏感。
解决:检查n_estimators是否太小,max_depth是否过深;尝试把连续特征标准化后重新训练;如果随机森林仍然差,把min_samples_leaf降到 2 再试。也有可能是验证集年份里出现了新的类型组合,树模型对这种样本完全外推失败。
5.4 中文乱码与编码灾难
现象:代码在本地一切正常,放到评审环境打开 CSV 全乱码,或者plt图里中文标题变成方块。
原因:Windows 下读写文件默认编码是 GBK,而数据集是 UTF-8;matplotlib 默认字体不支持中文。
解决:所有文件读写统一加encoding="utf-8-sig",让 Excel 打开 CSV 时能正确识别;matplotlib 绘图时直接指定黑体字体。
plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False如果评审环境是 Linux 服务器,SimHei 并不存在,这时不要硬指定字体,而是在代码里根据操作系统做一次判断,或者干脆图表全部用英文标签。很多模板项目不改这两个设置,跑出来的混淆矩阵和直方图全是方框,观感非常差。
5.5 “预测准确率”被当成分类指标来质疑
现象:答辩老师问“准确率多少”,而你交的是回归 RMSE。
原因:票房预测项目的标题出现在课程设计列表里,不少评审默认“预测 = 分类”,没有意识到这是个回归问题。
解决:在项目说明里明确写两套指标并存:回归用 RMSE/MAE/R2,分类用把预测值离散化成三档后的准确率。这里给一个简单回译:
bins = [0, 5e7, 2e8, np.inf] # 单位美元 y_valid_bin = pd.cut(y_valid_true, bins=bins, labels=["low", "mid", "high"]) pred_bin = pd.cut(pred_revenue_rf, bins=bins, labels=["low", "mid", "high"]) acc = (y_valid_bin == pred_bin).mean()分箱边界要按业务定义,不要按数据分位数。低票房 0 到 5000 万美元、中票房 5000 万到 2 亿、高票房 2 亿以上,这样解释给非技术人员时更直观。如果只按分位数切三份,会出现“中等票房包含 1 亿元电影”这种反直觉结论。
6. 进阶落点:从“跑通模型”到“让别人愿意用”的最后一公里
一个课程设计或毕业设计,模型分数再高,如果别人拿到源码后不知道怎么跑、不知道换一批数据要怎么调,价值就大打折扣。我自己的习惯是给项目留一个“一键复现”入口和一个人工校验用例。
一键复现脚本的核心是固定随机种子和参数文件。把上面散落的random_state=42、n_estimators=300、max_depth=16全部收进一个config.yaml,训练脚本从配置文件读取而不是写在代码里:
data: path: "data/movies.csv" target: "revenue" time_col: "release_date" split: method: "time" train_ratio: 0.8 model: rf_n_estimators: 300 rf_max_depth: 16 rf_min_samples_leaf: 4 rf_max_features: "sqrt" output: log_target: true pred_transfer: "expm1"这样做的第二个好处是,评审老师想对比随机森林与梯度提升树时,只需要在配置里换一个模型名,而不用钻进代码改到处散落的硬编码。我见过太多项目,调参全靠改 train.py 里的六个位置,改完一跑发现漏了一个random_state没改,结果复现出来的指标和文档里完全对不上。
人工校验用例可以从训练集里挑 5 部结构清晰的电影,比如一部低成本独立片、一部中等预算剧情片、一部高预算动作片,把它们的最终票房预测值和真实值做一次对照,写进 README 的开头。这个用例有双重价值:对你自己,它验证了数据从 CSV 到模型预测的链路没有中途断掉;对使用的人,它提供了一个“输出应该长什么样”的参照物。
我最得意的一次改进来自这个习惯。某次模拟项目里,团队成员一直怀疑随机森林预测结果偏低,但没人能准确指出低在哪。我加了这个手工用例后,发现低成本独立片全部被高估、高预算动作片全部被低估。之前大家都想在调参上找解法,后来才发现是预算特征里混入了几部预算填错单位的样本。这个教训让我后面所有项目都在文档里放样例对照表,而不是只贴一个总指标。
做电影票房预测平台,最大的收益不是学会调几个模型,而是理解一个从“模糊问题”到“可计算问题”的完整转化过程。票房预测永远不会像天气预报一样被完全信任,它的价值更多在于给决策者提供一个“如果不出意外会落在哪个区间”的参考。而作为开发者,你要做的不是让模型永不犯错,而是让它在犯错时,你能清楚说出它为什么错、错在哪类样本上。希望这篇笔记能帮你在自己的课程设计或实战项目里少踩几个坑。
本文还有配套的精品资源,点击获取