资讯详情

资讯详情

电影票房预测实战:从数据清洗到随机森林回归的完整机器学习流程

简介回归预测是机器学习中最基础也最常被误用的任务类型其核心在于理解特征与目标变量之间的映射关系。实际工程中模型效果往往不是取决于算法选得多高级而是数据清洗、特征构造和评估策略是否扎实。以票房预测这类典型回归问题为例需要先处理缺失值并对右偏分布的目标变量做对数压缩再通过历史均值编码将导演、主演等类别信息转化为有效特征。模型选型上先用线性回归建立基线再引入随机森林捕捉非线性关系同时避免时间穿越和未来信息泄露。该思路广泛适用于销售预测、用户增长预估等回归场景本文以电影票房预测为切入完整演示从数据预处理到模型调优的全链路实践方法。1. 电影票房预测平台一个能跑通的机器学习课程设计长什么样每年期末都能看到一堆人在做“电影票房预测”这个题目因为它数据公开、任务直观、毕设和课程设计都好交差。但这个项目的翻车率其实很高我见过最多的情况是代码能跑结果全错——训练集R²有0.9测试集预测值却离谱到没法看。这个平台资源把源码、数据集、文档三者配齐从数据清洗到特征工程、模型训练、误差分析整条链路是完整闭环的不是那种只贴一个model.fit()就完事的半成品。适合三类人期末大作业需要完整交付的同学、课程设计想现场演示模型效果的同学以及想从数据层面理解回归建模全流程的初学者。接下来我按实际拆项目的顺序把每条路怎么走通、哪里会翻车一处处说清楚。2. 先处理数据票房数据清洗与目标变量处理的三个关键动作2.1 票房数据集的字段构成与目标变量确认我拆这个项目时数据集文件是标准的CSV格式里面大致包含这么几类字段电影名称、类型、导演、主演、制片成本、宣发费用、上映日期、上映档期、首周排片场次、评分网站开画评分、评分人数以及最终要预测的票房收入。目标变量是连续金额所以这是典型的回归任务不是分类任务——很多同学一上来就想着“预测是否破亿”把一个回归问题硬切成二分类这是第一个常见的思路偏差。拿到数据以后我一般先不做任何清洗直接打印shape、dtypes和head()确认字段类型和数据量。这一步的目的不是看数据长什么样而是确认后续的清洗方案数值列和类别列的处理方式完全不同上映日期是字符串需要解析成时间特征导演和主演是类别需要转编码。先摸清类型再动手能少走很多弯路。import pandas as pd import numpy as np df pd.read_csv(boxoffice_dataset.csv, encodingutf-8-sig) print(数据规模:, df.shape) print(df.dtypes) print(df.head())这段代码里encodingutf-8-sig是处理中文列名的关键参数。很多从Excel导出的CSV文件自带BOM头用默认的utf-8读会报错或者列名出现\ufeff前缀这是一个非常隐蔽的坑。打印dtypes是为了快速区分数值列和类别列后续填充策略就是根据这个输出决定的。2.2 缺失值与异常值处理填充策略和量纲压缩票房数据集的缺失情况通常是这样的制片成本和排片场次基本完整但评分和评分人数会有一定比例缺失因为部分小成本电影上映前没有足够的评分样本。处理方式按列类型分两路数值列用中位数填充中位数对离群值不敏感类别列用众数填充用出现次数最多的类别来填不会改变分布的总体形态。这个策略不是随便定的如果数值列用均值填充一旦预算字段里混入几个超高成本大片均值会被拉高导致大部分样本的填充值偏大。num_cols [budget, promotion_cost, first_week_screens, rating, rating_count] cat_cols [genre, director, release_season] for col in num_cols: df[col] df[col].fillna(df[col].median()) for col in cat_cols: df[col] df[col].fillna(df[col].mode()[0])填充完之后下一步操作我建议优先做对目标变量取对数。票房数据的分布极度右偏少数几个爆款电影占据了几十亿票房大部分电影只有几千万。如果不压缩量纲模型会把几乎所有注意力都放在那几个大片的拟合上对普通电影完全视而不见。这里用np.log1p而不是np.log是因为log1p对0值友好log(0)会得到负无穷log1p(0)得到0。df[log_boxoffice] np.log1p(df[boxoffice]) print(票房列描述统计:) print(df[boxoffice].describe()) print(对数票房列描述统计:) print(df[log_boxoffice].describe())为什么这一步这么重要因为后续所有模型训练、损失计算、指标评估都发生在log_boxoffice这个列上。预测结果是对数空间的值做反变换np.expm1才能还原成真实票房。很多人在最后一步忘了反变换交上去的预测值全是几十亿上百亿直观上就觉得不对劲。量纲压缩这一步属于做不做都会“有反应”的操作——做了模型稳定收敛不做模型被极端值牵着走这是我从这个项目里第一个血泪经验。3. 特征工程与模型选型从导演票房到回归模型的完整链路3.1 特征构造原始字段到模型输入的完整映射原始字段不能直接喂给模型特征工程的本质是把人类直观判断“这部电影会不会卖座”的依据转化成模型能理解的数值。以这个项目为例类型字段是纯类别需要做one-hot编码导演和主演是高基数类别直接做one-hot会产生几百个稀疏列我一般用“历史平均票房”这种目标编码方式来压缩维度上映日期要转成档期特征比如春节档、暑期档、国庆档、平时档制片成本和宣发费用本身就是强相关特征可以直接保留。# 类型 one-hot 编码 genre_dummies pd.get_dummies(df[genre], prefixgenre) df pd.concat([df, genre_dummies], axis1) # 导演历史平均票房作为导演能力特征 director_hist df.groupby(director)[boxoffice].transform(mean) df[director_avg_boxoffice] director_hist # 主演历史平均票房取前三位主演的均值 actor_cols [actor1, actor2, actor3] actor_avg_list [] for _, row in df.iterrows(): actor_values [row[col] for col in actor_cols if pd.notna(row[col])] if actor_values: actor_avg df[df[actor_name].isin(actor_values)][boxoffice].mean() actor_avg_list.append(actor_avg) else: actor_avg_list.append(np.nan) df[actor_avg_boxoffice] actor_avg_list df[actor_avg_boxoffice] df[actor_avg_boxoffice].fillna(df[actor_avg_boxoffice].median())注意第二段代码里用了iterrows()遍历这个写法在数据集小的时候没问题但如果数据超过几万行性能会明显下降。我在文档里给了另一个向量化方案先把所有主演拆成长表算每个人历史票房均值再映射回去速度能快几十倍。实战中建议优先用向量化方案iterrows只适合快速验证思路。# 向量化版主演历史票房 actor_exploded df[[movie_id, actor1, actor2, actor3]].melt( id_varsmovie_id, value_nameactor_name ) actor_mean actor_exploded.merge( df[[actor_name, boxoffice]], onactor_name, howleft ).groupby(actor_name)[boxoffice].mean() df[actor_avg_boxoffice] df[[actor1, actor2, actor3]].apply( lambda row: actor_mean.reindex(row).mean(), axis1 )特征构造的最终目标是把所有信息压缩成一张数值表每行一个电影样本每列一个数值特征。这个阶段记住一个原则模型只能看到你构造出来的特征你构造的特征质量直接决定模型上限模型选型只是逼近这个上限。类型one-hot、导演历史票房、主演历史票房、档期编码、预算、宣发、排片场次这些特征是互相不冗余的每种都代表了不同的票房驱动力。3.2 模型选型为什么先跑线性回归再跑随机森林模型选型的逻辑不是“哪个准用哪个”而是“先用简单模型建立baseline再用复杂模型验证是否能超过baseline”。这个项目我推荐先跑LinearRegression它速度快、可解释性强、结果可以直接反推每个特征的影响方向如果线性模型能到0.7以上的R²说明特征构造是成功的。然后再上RandomForestRegressor它对非线性关系和非单调关系的拟合能力强很多而且不需要做特征缩放。from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score feature_cols [col for col in df.columns if col not in [movie_name, boxoffice, log_boxoffice, release_date, director, genre, actor1, actor2, actor3]] X df[feature_cols] y df[log_boxoffice] model LinearRegression() model.fit(X_train, y_train) y_pred_lr model.predict(X_test)第一轮跑完线性回归看测试集的R²和RMSE如果你的数据质量正常线性模型的R²大约在0.6到0.75之间RMSE在对数空间里约0.7到1.0。这个数值意味着预测误差在指数维度上大约是2倍左右也就是预测1亿实际可能是5000万到2亿——这已经是能接受的baseline表现。接下来用随机森林对比。rf RandomForestRegressor( n_estimators200, max_depth12, min_samples_leaf2, n_jobs-1, random_state42 ) rf.fit(X_train, y_train) y_pred_rf rf.predict(X_test)这里n_estimators200是决策树数量max_depth12限制每棵树的深度防止过拟合min_samples_leaf2要求叶子节点至少包含2个样本n_jobs-1让模型用满所有CPU核心。随机森林通常能比线性回归高5到10个百分点的R²但注意看训练集和测试集的差距——如果训练集R²到了0.98而测试集只有0.75说明树太深了把训练集的噪声也背下来了。3.3 训练集与测试集划分时间序列数据的切分陷阱票房数据有一个隐含的时间属性上映日期。如果直接用train_test_split默认的随机切分方式训练集和测试集会出现时间穿越——用2023年的数据训练然后用2019年的数据做测试模型相当于“看过未来”。这在课程设计答辩时是致命伤评审老师稍微问一下数据划分逻辑就露馅了。df df.sort_values(release_date).reset_index(dropTrue) train_size int(len(df) * 0.8) train_df df.iloc[:train_size] test_df df.iloc[train_size:] X_train train_df[feature_cols] y_train train_df[log_boxoffice] X_test test_df[feature_cols] y_test test_df[log_boxoffice]按时间排序后取前80%作为训练集后20%作为测试集。这样模型训练时确实“没见过未来”。但这个做法的代价是如果票房市场随时间有趋势性变化比如2020年整个行业受冲击那测试集分布和训练集分布差异会很大模型性能可能被严重低估。这属于正常现象因为实际部署场景中你的模型本来就是在预测未来你面对的永远是分布漂移的数据。报告里最好把时间切分和随机切分的对比都做一遍让人能看到两种方案的差距。4. 评估与调优用RMSE和交叉验证把预测误差降下来4.1 回归任务的评估指标RMSE、MAE、R²怎么选做完训练第一件事不是急着调参而是把三个指标全部打出来看。它们各自的侧重点完全不同R²衡量模型解释了目标变量多少方差越接近1越好关注的是相对拟合优度RMSE衡量预测值和真实值的平均欧氏距离因为取了平方对大误差的惩罚特别重——一个样本差10倍贡献的误差是差1倍样本的100倍MAE是绝对误差的均值对大误差的惩罚线性增长不受极端值主导。同一个模型R²高但RMSE也高说明整体拟合不错但个别样本预测崩了这种情况通常由极端大片导致。def evaluate_model(y_true, y_pred, name): rmse np.sqrt(mean_squared_error(y_true, y_pred)) mae mean_absolute_error(y_true, y_pred) r2 r2_score(y_true, y_pred) print(f{name}: RMSE{rmse:.4f}, MAE{mae:.4f}, R²{r2:.4f}) return rmse, mae, r2 _, _, _ evaluate_model(y_test, y_pred_lr, LinearRegression) _, _, _ evaluate_model(y_test, y_pred_rf, RandomForest)注意我在这个项目里所有指标计算都发生在log_boxoffice空间里。解读方式和真实票房空间完全不一样对数空间RMSE0.5意味着真实票房的预测误差大约在expm1(0.5)倍左右换算成倍数大约是差40%到60%之间。千万不要在真实票房空间算RMSE那样会有几个几十亿的大片把误差拉到一个毫无解释意义的数字我曾经见过有人报RMSE8亿这个数字本身没有业务含义因为它是被三四个爆款撑起来的。4.2 交叉验证与网格搜索参数调优的标准流程随机森林有四个参数需要重点调树的数量、树的最大深度、叶子节点最小样本数、特征选择比例。手调的话一遍遍试效率太低标准做法是直接上GridSearchCV让它自动遍历参数组合。设置参数网格时注意范围别太大我第一次跑的时候网格里同时放了n_estimators[500, 800]、max_depth[20, 30]组合数量直接爆炸一次网格搜索跑了一个小时才出结果纯属浪费算力。合理的起始范围是树的数量100到300深度8到16叶子最小样本数1到4。from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [100, 200, 300], max_depth: [8, 12, 16], min_samples_leaf: [1, 2, 4] } grid GridSearchCV( RandomForestRegressor(random_state42), param_grid, cv5, scoringneg_mean_squared_error, n_jobs-1 ) grid.fit(X_train, y_train) print(最佳参数:, grid.best_params_) print(最优得分:, grid.best_score_)注意scoringneg_mean_squared_error——网格搜索内部默认评分逻辑是越大越好所以MSE要取负数-MSE越大代表MSE越小。这里没有直接用R²做评分标准原因是R²在不同数据规模下受样本量影响大MSE更直接反映了误差的实际水平。cv5表示5折交叉验证每一折都用训练集的一部分做验证最终得分是5折的平均值防止只靠一次划分导致参数选择碰运气。交叉验证跑出来以后把最优参数重新在全部训练数据上训练一次然后看测试集表现。如果在交叉验证得分是0.81测试集直接掉到0.72说明你过度调参了模型参数被调成专门适配验证集的样子。这种情况回到上一章优先怀疑数据泄露再看特征里是否有高噪声列最后才是缩小参数范围。5. 避坑排查票房预测项目里最容易翻车的五个细节5.1 现象训练集R²高达0.95测试集R²只有0.6原因训练集和测试集划分时用了随机切分导致同一导演、同一系列电影的数据同时出现在两边模型在训练集里“记住”了这些电影的票房水平测试集里遇到同导演的片子直接照搬。这不是模型的预测能力是记忆能力。解决改成时间序切分按上映日期排序后取前80%做训练后20%做测试。如果你已经做完随机切分发现结果虚高别犹豫重新划分再跑。这个坑不发生则以一旦发生答辩时被老师问一句“你的测试集和训练集是什么关系”就会原地暴露。5.2 现象特征里混入了未来信息预测值异常准原因数据集中存在评分、评分人数这些“上映后才有”的字段。电影还没上映哪来的观众评分直接用评分预测票房相当于拿答案去考试。我在拆项目时发现数据集里有电影的豆瓣开画评分和最终评分数字如果把最终评分当特征模型等于拿到了未来的剧本。解决检查特征列凡是需要电影上映之后才能产生的数据全部从特征里剔除。只保留上映前就能确定的信息制片成本、宣发费用、排片场次上映前定的、导演、主演、类型、档期。这个坑最隐蔽因为特征看起来都很合理但仔细一推时间线就穿帮。我建议在特征矩阵构造完成后挨个问自己电影上映之前这个数字存在吗5.3 现象上映日期字段解析全部报错时间特征全变NaN原因CSV里的日期格式是2023-07-15我的代码却用pd.to_datetime(df[release_date], format%Y/%m/%d)去解析格式对不上自然全是缺失值。日期格式是这个项目里最不值一提但最常见的问题。解决先用df[release_date].head(10)查看实际格式再写对应的解析代码。如果格式混乱比如有的行是2023/07/15有的是20230715先统一格式再解析。# 先检测前一天日期格式 print(df[release_date].head(10).tolist()) # 根据实际格式解析常见两种 df[release_date] pd.to_datetime(df[release_date], format%Y-%m-%d, errorscoerce) # 如果还有解析失败用 errorscoerce 转成 NaT 再排查5.4 现象所有特征不缩放直接喂给模型梯度类模型训练很慢原因制片成本动辄上亿排片场次是几千档期编码是0和1量纲差了几个数量级。线性回归和随机森林不需要特征缩放但如果换成SVM、神经网络不先做StandardScaler梯度下降很容易在量纲大的方向上震荡收敛极慢甚至不收敛。解决如果沿用随机森林不需要缩放如果换模型先对数值特征做标准化。判断标准很简单看你的feature_cols里数值列的范围跨度差3个数量级以上就要处理。from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)5.5 现象预测结果全是负数低票房电影预测值连零都不到原因模型在log_boxoffice空间训练预测值可能小于0反变换后就是负票房。这是我见过最尴尬的输出因为真实世界里票房不可能是负的。本质上是因为有些低票房电影的对数值接近0甚至负值模型线性外推时压过了零点。解决反变换之后加一层np.clip把负值截断到0。虽然clip是在掩耳盗铃但模型的预测分布本身就是在对数空间做的回到真实空间后截断是合理的后处理。另一个思路是把目标变量换成np.log1p它的反变换np.expm1天然保证输出大于等于0比np.log更稳健。我后来所有回归项目默认都用log1p这是用一次惨痛教训换来的习惯。6. 进阶用法用特征重要性和残差分析让模型结果可解释模型训练完课程设计还没完——你需要让答辩老师看一眼就知道你理解了你的模型。两个最实用的工具是特征重要性和残差分析。随机森林自带feature_importances_属性把每个特征对预测的贡献量画成水平条形图一眼就能看出预算和导演历史票房是不是主导特征。这一步输出一张图抵得过几百字描述。import matplotlib.pyplot as plt feature_importance pd.Series( rf.feature_importances_, indexfeature_cols ).sort_values(ascendingTrue) feature_importance.plot.barh(figsize(10, 8)) plt.xlabel(Feature Importance) plt.tight_layout() plt.savefig(feature_importance.png, dpi150)残差分析的逻辑是把预测值和真实值的差画出来看误差是否随机分布。如果残差图和预测值之间呈现明显喇叭形——预测值越大残差越散——说明模型对高票房影片的预估能力显著弱于低票房影片。这是回归任务里最常见的异方差现象对策是在日志空间建一个分桶模型低票房区间用整体模型识别出极端大片后单独拟合一个高票房专项模型。我当时就是用这个思路做了一个两层预测整体指标提升了大概3个百分点的R²。residual y_test - y_pred_rf plt.scatter(y_pred_rf, residual, alpha0.5) plt.axhline(y0, colorred, linestyle--) plt.xlabel(Predicted log Boxoffice) plt.ylabel(Residual) plt.savefig(residual_analysis.png, dpi150)从那以后我每次做回归项目都强制走一遍特征重要性排序加残差图双验证流程不再直接看R²就收工——毕竟模型黑匣子你不拆开看它就像个黑匣子一样对你保持神秘。这几个可视化代码和调参路径在完整文档里都有对应的章节照着跑就能复现出我上面描述的效果。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →