ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

机器学习票房预测实战:回归任务、特征工程与模型选型

机器学习票房预测实战:回归任务、特征工程与模型选型 简介这份毕业设计项目以TMDB电影数据集为基础借助机器学习算法构建电影票房预测模型完整覆盖数据清洗、特征提取、模型选择、参数调优、结果评估与可视化等环节既适合计算机相关专业学生用于毕业设计、课程设计或期末大作业也适合需要项目实战练习的开发者参考学习。项目曾获导师指导认可评审分高达98.5分代码按照基础推荐、集成推荐、个性化推荐等模块清晰组织同时附带报告PDF与电影数据分析文档能够帮助读者掌握算法选型、特征工程和模型对比的关键思路也可作为同类项目的可复用模板。压缩包内共有59个文件主要包括16个Python源码文件、16个CSV数据文件、23张PNG可视化图片以及Markdown笔记、说明文档和报告PDF全部打包后大小约30.95MB数据文件可直接用于训练与验证目录结构清晰便于按需查阅、复现代码或扩展二次开发。目前已有267人学习或下载适合需要参考高分毕业设计、快速搭建票房预测系统并完善论文配图的读者。1. 机器学习算法做票房预测把毕业论文做成一份可复现的数据工程拿 Python 做毕业设计最不缺的就是算法 demo缺的是一个能讲清楚“为什么这样预测”的完整闭环。基于机器学习算法实现电影票房预测恰好是一个标准到可以直接当模板的回归任务数据网上能拿到、特征维度和领域知识有逻辑又不过度复杂、结果好坏一眼看得见。它解决的真实需求是在电影上映前或首日后对总票房做一个提前估计让制片、发行甚至平台排片都有一个量化参考。适合谁正在选题的本科生、想用 sk-learn 和 python 数据分析做个像样项目的入门者以及打算投算法岗、需要一个能完整复现的项目放进作品集的求职者。这篇文章从上手数据讲到踩坑帮你把这个题目做成一份能跑、能讲、能交源码和报告 PDF 的作品。2. 任务定义与建模选型先分清预测目标再谈算法2.1 回归任务的三个先验决策预测什么票房、站在什么时间点、用什么指标评价很多同学拿到题目第一件事就是跑模型结果跑完之后发现导师问的第一句“你预测的是首周还是总票房”都答不上来。票房预测在业务上至少有两个口径上映首日/首周票房和最终累计总票房。前者数据时效性强、受宣发影响大后者是暑期档结束后隔很久才统计完成更接近“终局评价”。毕设里我建议把口径定成“上映后首周已有数据时预测最终总票房”理由很简单公开数据集里首日与首周字段往往最全而“上映前只靠静态信息预测”的数据很多会缺失营销数据做起来一是特征太少二是不好解释。第二个先验决策是站在哪个时间点做预测。时间点直接决定特征集合上映前只能用导演、演员、类型、档期、宣发预算这类静态信息上映后可以再加入首日票房、首日排片占比、首日口碑评分。常见做法是分成两个模型基础预测模型纯静态特征和修正预测模型静态加首日动态特征。报告里同时出现这两个模型时对比本身就是一份很好的实验设计答辩时不用担心没内容可讲。第三个决策是评估指标。票房是一个典型的长尾分布头部爆款拿走大部分票房大量小成本电影只有几百万。如果你只看 R²会被极端值拉得虚高或虚低只看 MAE又容易被几部几亿票房的片子主导。我的习惯是主指标用 MAE 和 RMSE还原到原票房单位后按“万元”看辅指标用 log 空间下的 R²。误差在 30% 以内的模型在报告里已经可以说“具备业务参考价值”。2.2 特征体系票房不能只看“主演有没有名气”票房预测的特征工程核心思路是把“人的判断”翻译成“数字历史表现”。最常用、也最容易出效果的特征是以下五类导演历史票房均值、主演历史票房均值、类型组合、档期、制作体量时长、地区、是否续集。前三个解决“谁来做”后两个解决“什么时候上、卖给谁”。导演和演员的处理是这题的灵魂。常见错误是把演员名、导演名直接做 OneHot 编码——导演上千个、演员几万人OneHot 后特征矩阵爆炸随机森林还会被高基数的离散特征带偏importance 排名几乎全被演员列占据。更稳的做法是均值编码target encoding把导演名和主演名替换成“该导演/该演员过去作品票房的平均值”。这个特征本身就是行业里“票房号召力”的量化版模型理解起来毫不费力。档期也要处理成特征而不是丢给模型自己猜。春节档、暑期档、国庆档这三个时段平均票房明显高于普通周末档用时间规则生成一个四档分类变量比直接丢“几月上映”更贴合业务逻辑。再加上是否续集、是否 IP 改编这类标记位特征总数控制在 15 个以内就够了。特征不是越多越好对几千条训练数据来说十几个干净特征足以把随机森林和 XGBoost 喂饱。2.3 算法选型从线性回归到随机森林与 XGBoost按数据规模决定网上常说的十大机器学习算法里真正适合表格型回归任务的其实就那几个。对票房预测这种“特征有强业务先验”的问题我的选型顺序是先岭回归或 Lasso 做基线再上随机森林最后用 XGBoost 提分。线性模型的价值不是拿第一名而是用最少的调参成本验证特征是否有效——如果线性基线 R² 在 log 空间只有 0.3那多半是特征工程的问题而不是模型的问题先回去修特征。随机森林适合这种“特征量少、缺失不多、量级差异大的表格数据”不需要标准化对非线性关系也扛得住。它最大的短板是外推能力差当一部电影的票房远超训练集历史极值时随机森林预测会偏向历史最高值很难给出更高的数字。XGBoost 在同样的特征下通常能把 R² 再拉高几个点对缺失值容忍度高、正则项内置防过拟合是这个题目里最值得作为最终模型的那一个。选型上有一个实操原则训练样本少于 3000 条时不要一上来就堆复杂集成先跑通简单模型把管线打通再升级算法。报告里放三张模型的对比表基线、随机森林、XGBoost比单放一个最优模型更能体现你做过选型思考。3. 数据清洗与特征工程用 pandas 把乱数据变成训练集3.1 数据体检先看缺失率与字段类型再谈建模拿到数据先别急着写模型。常见的公开数据集字段大致是release_date、director、actors、genre、runtime、region、rate、opening_box、total_box。第一件事是体检哪些列缺失率超过 60%哪些列是 object 类型但其实应该是数值哪些日期字段格式不统一。这些信息决定了后面 80% 的清洗工作。import pandas as pd import numpy as np # 数据源可能是爬虫抓的也可能来自教材附带 CSV编码常是 utf-8 带 BOM df pd.read_csv(movies.csv, encodingutf-8-sig) print(shape:, df.shape) print(columns:, df.columns.tolist()) print(df.info()) # 缺失率降序排列超过 60% 的列基本只能删掉 print(df.isna().mean().sort_values(ascendingFalse).head(10))编码用utf-8-sig是为了去掉 BOM 头避免第一列列名变成\ufeffrelease_date这种隐蔽问题。df.info()能快速看到非空计数和 dtype如果director或rate被读成了 object说明里面有脏值。缺失率排行的用途是区分“可填充列”和“整列删掉”比如budget缺失 70%直接用均值填充会造成伪造数据删掉更诚实而runtime缺失 5%可以用同类型电影的中位数补。3.2 特征工程代码档期、人物均值编码与首日修正日期清洗是这个项目里最常见的翻车点。爬下来的日期可能是“2019-07-08”也可能写成“2019/7/8”甚至“2019年7月8日”。先统一用pd.to_datetime强制转换并把演员列按“/”或“”拆开取前三位。# 日期统一errorscoerce 会把解析失败的置为 NaT后面统一处理 df[release_date] pd.to_datetime(df[release_date], errorscoerce) df[year] df[release_date].dt.year # 主演列一般是 “张译/刘浩存/范伟” 这种斜杠格式 df[actor1] df[actors].str.split(/).str[0].str.strip() df[actor2] df[actors].str.split(/).str[1].str.strip() if df[actors].str.contains(/).any() else np.nan df[actor3] df[actors].str.split(/).str[2].str.strip() if df[actors].str.contains(/).any() else np.nan # 档期四分类春节前后和暑期档、国庆档单独划出 def season_of(dt): if pd.isna(dt): return other m dt.month if m in (12, 1, 2): return spring if m in (6, 7, 8): return summer if m in (9, 10): return national return other df[season] df[release_date].map(season_of)errorscoerce是这里最关键的一个参数裸的pd.to_datetime遇到非法字符串会直接抛异常导致脚本中断。拆主演时先检查“actors”列里有没有分隔符避免对单一演员的数据执行字符串索引而报错。档期规则我用的月份范围如果数据里有农历档期需求可以再做映射扩展但对一个毕业设计来说月份划分已经足够。人物均值编码是整个特征工程里最值钱的一段代码。注意这个函数必须接收已经切分好的训练集再映射到验证集和测试集绝不能在整个数据集上直接groupby transform否则就是数据泄漏后面的模型分数全是幻觉。def fit_target_mean(df_train, df_val, df_test, group_col, target): 均值编码在训练集上统计历史票房均值map 到 val/test global_mean df_train[target].mean() grp df_train.groupby(group_col)[target].mean() for dfx in (df_train, df_val, df_test): dfx[group_col _hist_mean] dfx[group_col].map(grp) # 训练集里没出现过的导演/演员用全局均值兜底而不是填 0 dfx[group_col _hist_mean].fillna(global_mean, inplaceTrue) return df_train, df_val, df_testfillna(global_mean)这个兜底策略是为了让“新导演”“新人演员”也获得一个中性的先验值而不是 0。填 0 的后果是模型学到“没见过的演员 零票房”完全违背常理。调用时对三个对象分别覆盖原始数据df_train[col] ...。这个函数避免了在验证集和测试集上重新计算历史均值从源头堵死泄漏。3.3 数据划分与特征对齐泄漏的问题要在这一刻根治数据划分策略直接决定答辩时能不能站稳。随机train_test_split的问题在于同一个导演或演员的多部作品可能同时出现在训练集和验证集模型等于开卷考试验证分数虚高。更贴近真实业务场景的做法是按年份切分用 2018 年之前的样本训练用 2019 年之后的样本验证模拟“站在过去预测未来”。from sklearn.model_selection import train_test_split # 推荐按时间切分仿照真实场景用旧数据预测新片 df_train df[df[year] 2019].copy() df_val df[df[year] 2019].copy() # 特征清单固定下来训练和验证严格同一批列 FEATURES [runtime, rate, director_hist_mean, actor1_hist_mean, season, opening_box, is_sequel, genre_count] # 标签取 log1p票房右偏严重不取 log 模型会被头部爆款主导 y_train np.log1p(df_train[total_box]) y_val np.log1p(df_val[total_box]) X_train df_train[FEATURES] X_val df_val[FEATURES] # 特征对齐断言跑之前先确认列顺序和列名一致 assert list(X_train.columns) list(X_val.columns), 训练集与验证集特征列不对齐这段代码里最值得说明的是np.log1p和最后的assert。log1p是对数变换把从几百万到几十亿跨越极大的票房压缩到可回归的区间模型训练时不会因为个别大片的残差压过所有小成本电影。assert是防止你前面做了多次特征选择后训练集多了列而验证集忘了同步——这种低级错误在报告里只会体现为一句“模型报错”但在答辩现场非常尴尬。4. 模型训练与参数调优三套代码跑通票房预测全流程4.1 岭回归做基线先把特征是否有效的底线试出来第一个模型一定是最简单的线性回归。票房预测和量化交易策略代码里的套路很相似先用一条简单的规则跑出底线分数再谈复杂模型。普通最小二乘在特征有共线性时方差很大所以这里用岭回归Ridge它对导演均值、演员均值这类本身就相互关联的特征更稳。from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.linear_model import Ridge from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score base make_pipeline(StandardScaler(), Ridge(alpha1.0)) base.fit(X_train, y_train) pred_log base.predict(X_val) # 还原到原票房单位评估才符合业务直觉 pred_origin np.expm1(pred_log) val_origin np.expm1(y_val) print(R2(log):, r2_score(y_val, pred_log)) print(MAE(万):, mean_absolute_error(val_origin, pred_origin) / 1e4) print(RMSE(万):, mean_squared_error(val_origin, pred_origin, squaredFalse) / 1e4)这段代码里有三个细节要解释清楚。第一StandardScaler一定要放在管道里并且只在训练集上拟合不能自己另写一个scaler.fit(X)然后把验证集也标准化——管道会确保 fit 和 transform 的分界正确。第二R²我用 log 空间的值而 MAE/RMSE 用还原后的原始票房值说的是两件事log 空间 R² 看模型对对数票房的解释力原始票房误差才是业务上关心的偏差。第三alpha1.0只是起点不要在这里花太多时间调参基线模型的意义是检验特征管线通没通。4.2 集成模型随机森林和 XGBoost 的参数设定与调参顺序线性基线跑通后升级到集成模型。随机森林的优势是几乎不需要特征缩放对离散、连续混合特征友好XGBoost 的优势是支持缺失值、正则项内置、训练效率高。两个模型用同一份特征和验证集才有对比意义。from sklearn.ensemble import RandomForestRegressor from xgboost import XGBRegressor # 随机森林n_jobs-1 用满所有核random_state 固定保证可复现 rf RandomForestRegressor( n_estimators300, max_depth8, min_samples_leaf5, n_jobs-1, random_state42 ) rf.fit(X_train, y_train) # XGBoost学习率调小树多一点防止一步跨过最优区间 xgb XGBRegressor( n_estimators300, learning_rate0.05, max_depth5, subsample0.8, colsample_bytree0.7, random_state42 ) xgb.fit(X_train, y_train, eval_set[(X_val, y_val)], verboseFalse)这两个模型的参数表格值得写进报告里参数含义要能当场讲出来这是答辩必问项。以下是我常用的起点参数不是定死的最优值超参数我常用的起点作用与调整方向n_estimators300树的数量越大越稳但训练越慢观察 val 误差收敛后再定max_depth随机森林 8XGBoost 5控制树的深度深度过大容易记住训练集的噪声min_samples_leaf5叶子节点最小样本数调大能平滑预测、防过拟合learning_rate0.05XGBoost 步长调小通常要配更大的 n_estimatorssubsample0.8每棵树随机采样 80% 的行缓解过拟合colsample_bytree0.7每棵树随机采样 70% 的特征效果类似特征子空间调参顺序有讲究。先固定 learning_rate 和 n_estimators观察训练曲线是否收敛然后调 max_depth从 3 到 9 逐个试最后再动 subsample 和 colsample_bytree。不要一上来就做全网格搜索几千条数据加上三百棵树全参数网格跑一晚上不出结果毕业论文可等不起。4.3 三模型评估R²、MAE 与 RMSE 怎么一起看三个模型跑完后写一个统一的评估函数输出格式对齐到同一张表里。这里最容易犯的错是只报一个 R²然后宣称模型效果多好。R² 高可能是泄漏R² 低也不代表无用必须结合原始票房误差来看。def evaluate(name, model, X_valid, y_valid): pred_l model.predict(X_valid) pred_o np.expm1(pred_l) y_o np.expm1(y_valid) print(f{name:12s} R2{r2_score(y_valid, pred_l):.3f} fMAE{mean_absolute_error(y_o, pred_o) / 1e4:8.1f}万 fRMSE{mean_squared_error(y_o, pred_o, squaredFalse) / 1e4:8.1f}万) evaluate(Ridge, base, X_val, y_val) evaluate(RandomForest, rf, X_val, y_val) evaluate(XGBoost, xgb, X_val, y_val)对几千条训练数据的项目来说log 空间 R² 落在 0.75 到 0.85 之间属于正常发挥MAE 在 3000 万上下已经算可用。如果 R² 超过 0.95先别高兴回头检查是不是均值编码泄漏了。三个模型放同一张表里报告里再用一句话解释“为什么 XGBoost 优于随机森林”——通常是 XGBoost 对缺失值和特征交互的处理更好以及树模型在 log 标签空间的外推能力比随机森林强。5. 避坑排查票房预测最容易被数据泄漏和金指标带偏的六个细节5.1 长尾标签与虚高 R²为什么预测值全在平均值附近现象模型输出的预测票房全挤在一个狭窄区间几部爆款电影的预测值离真实值差了 5 倍但打印的 R² 却有 0.8 以上。原因票房标签右偏严重头部大片残差平方占主导模型为了最小化整体误差宁愿把预测都压向均值附近也不去冒险预测高票房。解决对标签做np.log1p变换后再训练评估时把预测还原到原票房单位计算 MAE 和 RMSE。同时在报告里补一个误差分布图按真实票房分成低、中、高三档分别看这三档的误差率。如果低档误差率 20%、高档误差率 80%说明模型对小成本片有用、对爆款无能为力这个结论写进报告是很诚实的亮点。5.2 均值编码写错位置验证分数全是幻觉现象验证集 R² 高达 0.95模型换了新数据表现一塌糊涂导师问一句“你的导演均值特征是在哪一步算的”就露馅。原因对全量数据groupby(director)[gross].transform(mean)导致验证集的导演历史均值里包含了自己这部片子的票房训练时等于答案已经写在特征里。解决严格遵循“先切分后编码”用前面写的fit_target_mean函数在训练集上统计验证集和测试集只做map。补充一点如果按时间切分训练集里没有出现过的新导演均值用全局票房均值兜底不要用验证集信息回填。5.3 日期和演员列解析翻车缺行比缺列更隐蔽现象跑完season特征后发现暑期档电影占比只有 20%明显比真实分布少。原因日期列里有“2019年7月8日”这种中文格式pd.to_datetime默认解析失败后整行变成 NaT档期全落到 other。演员列同理有些源数据用中文逗号“”分隔有些用“/”拆列后 actor2、actor3 大量缺失均值编码变成全局均值兜底人物票房号召力信息全丢。解决在to_datetime里加errorscoerce并显式打印解析失败行数比如df[release_date].isna().sum()演员列先print(df[actors].head(20).tolist())看真实分隔符再决定用/还是。研究报告里把“清洗后有效样本数”从原始行数中单独标出来导师会觉得你对数据有掌控感。5.4 高基数类别直接 OneHot特征重要性全是“演员名”的形状现象随机森林feature_importances_排名前 10 里 7 个是主演 OneHot 列模型效果却一般。原因OneHot 后每个演员变成一列 0/1 特征树模型天然偏好高基数离散特征会在每个演员上尝试切分重要性虚高真实有用的导演均值、档期特征反而被淹没。解决演员和导演一律用均值编码替代 OneHot只在genre这种基数十来个的类型字段上保留 OneHot 或者直接做标签编码。如果要佐证可以在报告里放两张 importance 图OneHot 版的混乱排名和均值编码版的清晰排名这本身就是一段好内容。5.5 随机切分让验证集开卷同一导演横跨训练和验证现象按 8:2 随机切分后模型分数好看但把切分方式换成按年份后 MAE 突然涨了 30%。原因随机切分把同一导演、同一系列的电影拆进了训练和验证两个集合模型见过“该导演风格”验证等于开卷考试分数虚高。解决用按年份切分或者按导演做分组划分。毕设项目规模一般不大直接按年份切分最稳妥报告里写明“训练集为 2019 年前上映电影验证集为 2019 年后上映电影”这句话答辩时就是你的护城河。5.6 特征对齐错误训练 20 列、预测 19 列的经典事故现象模型在训练集上正常predict(X_val)突然报feature_names mismatch或者不报错但结果全部离谱。原因训练前特征选择做过几次调整验证集特征列表没同步或者手工把 DataFrame 转 numpy 时列顺序发生了漂移。解决把特征清单定义成全局列表FEATURES训练和预测统一用它取列并在训练前执行assert list(X_train.columns) list(X_val.columns)。XGBoost 训练后输出model.feature_names看一眼预测时用X_val[FEATURES]不要图省事传X_val.values。6. 源码整理与报告 PDF答辩前把“能跑”变成“能讲”6.1 报告 PDF 里最值钱的两张表报告不需要流水账堆字数。导师和评委最想看的是模型对比表、以及“你说你的特征有效证据呢”。模型对比表我从来只保留三行Ridge、RandomForest、XGBoost每行放 R²(log)、MAE(万)、RMSE(万) 三列下面跟一段一句话解释差异原因。特征有效性用均值编码前三名来证明director_hist_mean、actor1_hist_mean、opening_box。如果 opening_box 排第一说明“首日修正模型”比“纯静态预测”信息量大这个结论能直接回答“你这个项目到底预测了什么”。6.2 让源码可复现固定种子、日志、环境说明源码包交出去之前先想一下评审老师会不会真的去跑。我见过太多源码包跑起来缺库、乱码、路径写死。固定三样东西所有模型统一random_state42训练过程打印时间、数据量、每个模型的评估结果requirements.txt 里列清楚 pandas、scikit-learn、xgboost 的版本号。项目根目录放一个run.py从上到下依次执行“读数据 → 清洗 → 特征工程 → 切分 → 训练 → 评估”保证一个命令出全部结果。if __name__ __main__: df load_data() # 数据读取 df clean(df) # 日期、演员、缺失值处理 df build_features(df) # 档期、均值编码、首日特征 cutoff pd.Timestamp(2019-01-01) train, val split_by_time(df, cutoff) result train_and_evaluate(train, val) result.to_csv(result.csv, indexFalse)6.3 用“反向验证”自证模型置信度答辩时最容易被挑战的问题是“你凭什么说模型能用在未来的电影上”我的做法是加一个反向验证把已知数据切成两个时间窗口拿前一个窗口训练去预测后一个窗口的票房然后计算每条样本的误差率最终给出平均误差。这个思路和量化交易里策略回测是同一个套路。def backtest(df, last_train_date, features): tr df[df[release_date] last_train_date].copy() va df[df[release_date] last_train_date].copy() # 重新在 tr 上做均值编码验证集只做 map禁止复用全量编码 tr, va, _ fit_target_mean(tr, va, va.copy(), director, total_box) # 训练与评估仿真“过去不知道未来”的真实决策 model XGBRegressor(n_estimators300, learning_rate0.05, random_state42) model.fit(tr[features], np.log1p(tr[total_box])) va[pred] np.expm1(model.predict(va[features])) va[err] (va[pred] - va[total_box]).abs() / va[total_box] return va[[title, total_box, pred, err]].sort_values(err)这段代码输出的表里挑一两部典型电影在答辩时展开讲为什么高估、为什么低估、有哪些特征没捕获到。这种“我知道模型在哪失效”的态度比一个写着 95% 准确率的模型更能压住场。我做这套设计收尾前最后悔的一件事就是把均值编码写在了数据切分之前验证 R² 漂亮到连自己都膨胀结果换年份一验证直接垮掉。后来改成“先切分再编码”把反向验证表放进报告附录才真正觉得这个题目闭环了。希望帮到你。本文还有配套的精品资源点击获取
返回列表