简介:面向毕业设计、课程设计与期末大作业场景,这份基于机器学习算法的电影票房预测系统完整项目,提供可直接运行的Python源码与配套文档数据,适合具备一定Python基础、希望快速落地完整项目的学习者。包体共59个文件,包含16个Python脚本、16个CSV数据集、23张分析图表、2份Markdown/PDF文档等,覆盖数据清洗、文本特征编码、特征工程、训练集测试集划分、模型训练与评估全流程,目录按数据、可视化、推荐与预测模块划分清晰,便于系统学习。项目实现线性回归、决策树、随机森林等算法,并引入KNN、SVD等协同过滤推荐手段,从tmdb_5000电影数据集出发,完成票房预测与个性化推荐双任务。附带的图表与文档直观呈现特征分析与模型对比过程,可辅助答辩讲解。源码注释详细,部署简单,已有522人学习下载,适合需要完整项目参考、快速复用或二次开发的读者。
1. 电影票房预测为什么比想象中难:这个系统到底在解决什么问题
一部电影还没上映,投资方最关心的问题只有一个:这票房能到多少。这个数字决定了宣发预算、排片谈判、回本周期,甚至续集立项。但票房不是按公式算出来的,它受档期、类型、演员号召力、口碑发酵速度、竞片密度等多重因素影响,连业内资深发行都经常看走眼。基于 Python 和机器学习算法做票房预测系统,就是把这些因素量化成特征,让模型学习历史规律,在新片上映前给出一个参考区间。这个方向非常适合做毕业设计,因为它的任务边界清楚——回归预测,数据可获取,模型可迭代,最终还能用可视化把结果讲明白。适合的人群也很明确:有 Python 和基础机器学习知识,想做一个完整落地项目但不想碰那些烂大街的电商评论分类或鸢尾花识别选题的同学。
整个系统的核心难点不在模型本身,而在数据处理和特征设计。票房预测本质上是在用历史公开数据做外推,如果特征构造不仔细,模型在训练集上跑得再好看,遇到真实的未上映电影照样翻车。这篇文章会从问题定义、数据准备、特征工程、模型选型、参数调优到最后的解释性分析,把整套流程拆开讲清楚。
2. 预测目标与数据准备:为什么不能直接拿“总票房”当标签
2.1 把预测目标拆成可学习的回归任务
电影票房预测系统首先要回答的问题不是“用什么算法”,而是“预测什么”。如果直接预测最终总票房,模型的学习压力非常大,因为一部电影的总票房高度依赖上映后的口碑和排片,而这两者在上映前根本拿不到真实数据。常见做法是把目标拆成更可控的变量:首周票房、首日票房或上映前 30 天的总票房预测。首周票房与上映前的热度数据、排片规模、同档期竞品数量关联度最高,更适合作为机器学习任务的监督标签。
另一个关键细节是对标签做对数变换。票房数据的量纲差异极大,小成本文艺片几百万,商业大片几十亿,分布严重右偏。直接用原始票房训练,模型会被少数高票房样本带偏,对中低票房影片的预测误差巨大。对票房取 log1p 之后,目标分布更接近正态,模型的收敛速度和准确率都会有明显提升。
import pandas as pd import numpy as np df = pd.read_csv("movie_data.csv") df["release_date"] = pd.to_datetime(df["release_date"]) # 用首周票房作为标签,对缺失值做删除处理 df = df.dropna(subset=["first_week_box_office"]) df["log_box_office"] = np.log1p(df["first_week_box_office"])这里np.log1p是对数据加 1 后取自然对数,作用是避免 0 票房的样本在取对数时变成无穷小。后续训练时模型拟合的是log_box_office,得出预测结果后再用np.expm1还原成真实票房区间。如果你更关心首日票房而不是首周,只需要把标签列替换掉,整个训练流程不需要改动。
2.2 数据字段的选取:哪些列值得进入模型
公开的票房数据集中常见字段包括:电影名称、类型、导演、主演、制片国家、上映日期、预算、发行公司、档期、排片场次、想看人数、预售票房、评分、评论数、同档期竞片数量等。这些字段并不是全部都要喂给模型,需要人工做一次筛选。预算和发行公司属于强信号,但要确认它的发布时间是否在预测时间点之前。上映日期可以拆成星期几、月份、是否节假日、是否暑期档等特征,不能直接用日期字符串。
一个常见的字段选择策略是分成三类:静态属性(类型、导演、主演、国家、预算)、时间属性(档期、月份、星期)、上映前可观测的动态属性(想看人数、预售票房、社交媒体讨论量、预告片播放量)。其中第三类数据最难得,但价值也最高,因为预售和想看数直接反映市场需求。
2.3 数据分割必须按时间切,不能随机打乱
这是票房预测项目里最容易被忽略的一步。如果用train_test_split(shuffle=True)随机分割,模型会看到未来数据的信息,测试集成绩虚高,一旦拿去预测真正的新片就严重缩水。票房数据有天然的时间序列属性,应该按上映日期排序,用前 80% 的时间段做训练,后 20% 做验证。
df = df.sort_values("release_date").reset_index(drop=True) train_size = int(len(df) * 0.8) train = df.iloc[:train_size] test = df.iloc[train_size:] print(f"训练集样本数: {len(train)}") print(f"测试集样本数: {len(test)}") print(f"训练集时间范围: {train['release_date'].min()} ~ {train['release_date'].max()}") print(f"测试集时间范围: {test['release_date'].min()} ~ {test['release_date'].max()}")参数说明:train_size设成 0.8 是常见选择,但如果数据量偏少,可以调整到 0.85,测试集保留 15% 的样本也足够评估。关键点在于分割之前必须先排序、再切分。很多毕业设计在答辩时被问到“你怎么防止数据泄露”,答出这一步就是亮点。
3. 特征工程:把电影属性变成模型能理解的数字
3.1 静态特征的编码思路
电影类型、制片国家、发行公司都是类别型特征,直接塞进模型是行不通的。常见的处理方式是:高基数类别(如发行公司)用频次编码,低基数类别(如类型)用独热编码。比如一部电影可以是“动作 + 冒险 + 科幻”,类型字段本身是列表,需要先展开再编码。
from sklearn.preprocessing import MultiLabelBinarizer df["genres"] = df["genres"].apply(lambda x: x.split("|")) mlb = MultiLabelBinarizer() genres_encoded = mlb.fit_transform(df["genres"]) genres_df = pd.DataFrame(genres_encoded, columns=mlb.classes_) df = pd.concat([df, genres_df], axis=1)这里的MultiLabelBinarizer解决了“一部电影属于多个类型”的编码问题,输出结果是每个类型一个布尔列。如果你做的是简单标签编码,多个类型就会被当成一个整数,模型的表达能力会弱很多。对于导演和主演这类高基数特征,我一般会用主演前三人在历史票房上的均值作为替代指标,而不是直接编码几百个明星姓名。
3.2 从已有字段中派生新特征:票房预测的核心提升点
原始字段带来的信息量有限,真正让模型精度上台阶的是派生特征。举几个实际项目里非常管用的例子:导演近三部作品的票房均值,主演中最大票房贡献者的历史票房,同一发行公司近一年内上线影片的平均票房,以及电影上映档期与热门档期的重合度。
# 假设已有历史票房表 actor_history actor_history = pd.read_csv("actor_box_office_history.csv") def get_actor_power(actor_name): hist = actor_history[actor_history["actor"] == actor_name] if len(hist) == 0: return None # 取最近三部作品票房的加权均值,时间越近权重越高 weights = np.array([0.2, 0.3, 0.5]) return np.average(hist.head(3)["box_office"], weights=weights) df["actor_power"] = df["main_actor"].apply(get_actor_power)这个例子里的权重[0.2, 0.3, 0.5]是有讲究的:越晚近的作品越能反映演员当前的号召力,但也别只取最近一部,因为一部作品可能受题材影响波动太大。用加权均值既平滑了噪声,又保留了时间衰减的特性。如果没有演员历史表,可以把这条跳过或改用导演维度的同样逻辑。
3.3 时间特征与数值特征的归一化
上映日期能拆出大量有效信号。举例来说,暑期档 6—8 月的票房均值明显高于 3—4 月,春节档更不用提;周五上映的电影通常首周表现好,因为能吃到完整的周末红利。这些规律模型自己学不出来,必须显式拆成特征。月份、星期几都可以直接编码为数值或哑变量。
数值特征这一块,预算、排序长度、预售票房等字段通常用StandardScaler做标准化,让不同量纲的特征统一到均值 0、方差 1 的范围,对基于距离的算法和正则化的线性模型尤其重要。
from sklearn.preprocessing import StandardScaler num_cols = ["budget", "runtime", "pre_sale_volume", "wish_count"] scaler = StandardScaler() df[num_cols] = scaler.fit_transform(df[num_cols])代码逻辑是把四个连续型特征统一标准化。如果后续要上线预测,记得把这套scaler保存下来,对新的单个样本也要做相同的转换。
注意:
fit_transform只能用在训练集上。对测试集或未来的推理样本,应该用已经拟合好的scaler.transform(),防止数据从测试集“漏”到参数里。
4. 模型选型与调参:从基线到 XGBoost 的提升路径
4.1 用线性回归跑基线,确定模型下限
很多同学一上来就直接上 LightGBM 或神经网络,其实正确的做法是先写一个简单的线性回归,把数据流程和评估代码跑通,得到一个基线分数。然后在这个基础上逐步加大模型复杂度,对比每次改动是否真的带来了提升。
from sklearn.linear_model import Ridge from sklearn.metrics import mean_squared_error, mean_absolute_error feature_cols = [c for c in train.columns if c not in ("movie_name", "release_date", "first_week_box_office", "log_box_office")] X_train = train[feature_cols].fillna(-999) y_train = train["log_box_office"] X_test = test[feature_cols].fillna(-999) y_test = test["log_box_office"] ridge = Ridge(alpha=1.0) ridge.fit(X_train, y_train) pred = ridge.predict(X_test) rmse = mean_squared_error(y_test, pred, squared=False) mae = mean_absolute_error(y_test, pred) print(f"Ridge RMSE: {rmse:.4f}, MAE: {mae:.4f}")这里面的关键在于fillna(-999):缺失值的样本不是丢掉,而是用一个不在正常范围的值填充,让树模型能够学到“这里是缺失值”这个信号。alpha=1.0是格子正则化强度,之前用默认值时方差偏大,调大到 1 之后稳定一些。
4.2 用随机森林看特征重要性,找到真正有用的变量
线性回归的 RMSE 出来以后,下一步我会跑随机森林。不是为了追求精度,而是为了看特征重要性排序,用来检查特征工程里是否有什么直觉误判。
from sklearn.ensemble import RandomForestRegressor rf = RandomForestRegressor( n_estimators=300, max_depth=15, min_samples_leaf=3, random_state=42, n_jobs=-1 ) rf.fit(X_train, y_train) # 输出前10个重要特征 importances = sorted( zip(feature_cols, rf.feature_importances_), key=lambda x: x[1], reverse=True ) for name, score in importances[:10]: print(f"{name}: {score:.4f}")n_estimators=300是综合考虑了精度和训练时间的折中,min_samples_leaf=3能防止叶子节点过小导致的过拟合。注意这里没有做交叉验证,先通过重要性排序做粗筛,再决定是否删除那些重要性几乎为零的特征。如果某个字段在随机森林里的重要性低于 0.005,可以考虑去掉,减少后续模型的输入维度。
4.3 XGBoost 调参:三个必调参数
在毕业设计里,XGBoost 往往能达到精度与复杂度的最佳平衡。三个必调的参数是n_estimators、max_depth、learning_rate。learning_rate越小,模型需要更多的树来拟合,但泛化能力通常更好;max_depth控制树的深度,太深容易过拟合,对于量级在几千条的票房数据,深度给 4 到 6 就够。
import xgboost as xgb model = xgb.XGBRegressor( n_estimators=500, max_depth=5, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, reg_alpha=0.1, reg_lambda=1.0, random_state=42 ) model.fit(X_train, y_train, eval_set=[(X_test, y_test)], verbose=False) pred_xgb = model.predict(X_test) rmse_xgb = mean_squared_error(y_test, pred_xgb, squared=False) print(f"XGBoost RMSE: {rmse_xgb:.4f}")参数说明:subsample=0.8表示每棵树随机采样 80% 的数据进行训练,colsample_bytree=0.8表示每棵树随机使用 80% 的特征,这两个参数是 XGBoost 防止过拟合的核心。reg_alpha是 L1 正则项,加上它之后模型对噪声特征的容忍度降低,在小数据集上表现更稳。如果跑出来发现训练集和测试集 RMSE 差距很大,优先调大reg_alpha或减小max_depth。
4.4 用网格搜索寻找最优参数组合
手动调参找到的方向基本靠谱后,可以用GridSearchCV做一次小范围的精调。注意网格别铺太大,每组参数组合都要跑多折交叉验证,时间成本很高。
from sklearn.model_selection import GridSearchCV param_grid = { "max_depth": [4, 5, 6], "learning_rate": [0.03, 0.05, 0.08], "subsample": [0.7, 0.8, 0.9] } gs = GridSearchCV( xgb.XGBRegressor(n_estimators=200, random_state=42), param_grid, cv=5, scoring="neg_root_mean_squared_error", n_jobs=-1 ) gs.fit(X_train, y_train) print(f"最佳参数: {gs.best_params_}") print(f"最佳得分: {-gs.best_score_:.4f}")scoring="neg_root_mean_squared_error"用了负的 RMSE,因为GridSearchCV的得分是越大越好,真实 RMSE 加个负号就可以直接比较。cv=5是五折交叉验证,数据量万一太小,改成 3 也行。
5. 避坑指南:票房预测项目里最常见的 5 个翻车现场
5.1 数据泄露:用上映后的数据预测上映前
这是整个项目里最隐蔽也最致命的坑。现象是模型在测试集上 RMSE 低到离谱,但拉到几个未上映的真实案例一试,误差大到完全不能用。原因是训练特征里混入了只有电影上映之后才能拿到的数据,比如豆瓣评分、上映后的评论数、观众评分均值。这些字段在上映前根本不存在,模型却把它们的强相关性学进去了。解决方法是严格按“预测时间点可得性”筛选特征,每加入一个特征前先问自己:如果这部电影明天上映,我现在能拿到这个数据吗?拿不到就删掉。
5.2 类别特征基数过高导致过拟合
现象:把主演姓名直接标签编码丢进模型,训练集效果不错,测试集直接垮掉。原因是几百个子演员类别被编码成从 0 到几百的整数,模型无法理解数字大小的语义,同时还为每个类别学到了大量噪声。解决方法是换成目标编码或回溯统计特征。例如计算演员历史票房均值、演员参演影片数量的平均值等,把原始类别映射成一个个有业务含义的标量。
5.3 标签分布右偏导致指标虚高
现象:打印出 RMSE 后发现是 0.8 左右,看起来还行,但可视化时发现模型对预测低票房电影全部偏高。原因是大部分样本是低票房的小成本影片,模型为了最小化整体误差会选择“预测折中值”,个别千万级票房的真实区间被忽略。解决方法是标签取对数变换后训练,评估时同时报出还原后的 MAE,对比任意取 log 前后哪个更适合当前场景。
5.4 时间分割被忽视
现象:测试集随机切,模型精度高得令人兴奋,拿到答辩现场被老师一追问才发现训练集里混了未来数据。解决方式在第 2 章已经给出,按上映日期排序后严格用时间前后切分,并且在论文里写明这个操作。这个细节在评委老师眼里是“这个学生真的理解机器学习应用”。
5.5 缺失值处理过于粗暴
现象:预算字段有 20% 的缺失,直接dropna()删除行,发现样本量少了五分之一,模型效果变差。原因是缺失本身可能携带信息——没有预算数据的电影大概率是小成本制作或非公开投资。解决方法是保留行,用 -999 或单独一列“是否缺失”标记。
| 缺失字段 | 错误做法 | 正确做法 |
|---|---|---|
| 预算 | 直接删除行 | 填充 -999 并增加 was_budget_missing 标记列 |
| 预售票房 | 用均值填充 | 填充 0,因为预售为 0 代表无热度 |
| 导演历史票房 | 删除该样本 | 填充全球导演历史票房均值,保留弱信号 |
6. 用 SHAP 解释模型:让答辩和报告更有说服力
模型跑完,RMSE 达标还不够。毕业设计和实际上线有一个显著区别:毕业设计评委会问你“为什么是这几个特征最重要”“模型凭什么给出这个预测”。这个时候硬说“XGBoost 的 feature_importance 排出来的”并不够严谨,因为树的增益型重要性存在偏差,对高基数类别特征有偏向。建议用 SHAP 值做解释性分析。
import shap explainer = shap.TreeExplainer(model) shap_values = explainer.shap_values(X_test) shap.summary_plot(shap_values, X_test, feature_names=feature_cols, max_display=15)SHAP 的原理是计算每个特征对每个样本的预测值贡献的边际平均值,比树的特征重要性更稳定,而且可以直观看出特征影响的方向——比如“预算”这个特征的值越大,SHAP 值越正,代表票房预测值被推高;“上映日期是周一”的 SHAP 值往往为负,代表对票房有负面影响。在论文里放进这张图,你就能直接说出“档期对票房预测影响排名第二,暑期档和春节档贡献最大”这类结论。
还要提醒一点:SHAP 的可解释性只针对模型本身,不是说它揭示了真实的票房规律。比如模型告诉你“发行公司”很重要,实际上可能只是因为你数据里华语片和好莱坞片的发行渠道差异被它当成了某种代理变量。在写分析报告时要区分“模型学到的规律”和“业务上的因果规律”,措辞上留一个余地。
最后分享一个我的习惯:每次拿到新电影的数据做预测时,我会先跑一遍基线模型看平均值,再跑调好的模型看差值。如果模型给出的预测比基线低,我会主动检查是不是缺了预售数据或档期特征填错了;如果比基线高很多,我会怀疑是不是把点映阶段的评分提前混进来了。这套交叉验证的习惯帮我挡掉过多次错误上线。
从实践来看,票房预测系统做不难,做好看的是数据治理和特征设计的细节。提前把这些脏活做扎实,模型分数不会差,毕业答辩也能讲出真正的技术含量,希望帮到你。
本文还有配套的精品资源,点击获取