ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于SVR回归的电影票房预测系统实战:特征工程与调参全流程

2026/9/14 4:50:52 拓冰建站 浏览量
基于SVR回归的电影票房预测系统实战:特征工程与调参全流程 简介这是一套基于猫眼电影数据和SVR支持向量回归的电影票房预测实战项目面向机器学习初学者和希望完成数据科学小项目的开发者能帮助读者完整体验从数据获取到模型评估的回归任务落地流程。资源共18个文件核心为7个Python脚本覆盖数据采集、缺失值处理、特征工程、训练集/测试集划分、SVR模型训练与预测等完整环节另有6个pyc编译文件、4个woff字体文件用于数据采集中的字体解析和1个xls历史票房数据整体压缩包仅178KB。项目从SVR原理出发讲解如何将电影类型、主创阵容、上映档期等特征转化为模型输入并通过MSE、MAE等指标评估效果支持调整ε、C等超参数优化预测精度。已有586人学习下载代码结构清晰包含数据预处理、SVM预测等可独立运行的模块方便在此基础上扩展特征或更换回归算法是理解爬虫数据清洗、SVR建模与票房预测的实用参考。整个项目步骤清晰适合作为课程设计或毕业设计参考。1. 电影票房预测为什么从SVR回归而不是神经网络开始猫眼电影想看人数几十万、预售破亿看上去数据很“大”但真正能用来做全周期票房预测的样本一年也就几百部电影。这个体量放在图像任务里连一个 batch 都不够却恰好是 SVR 回归最擅长的小样本区间。很多人做这类人工智能项目实践时第一个动作就是上神经网络结果模型复杂、可解释性差调了两周参数预测误差还不如一条简单基线。SVR 回归的思路完全不同它把票房预测视作带容忍带的回归问题只惩罚真正落在容忍带之外的样本配合 RBF 核函数就能拟合想看数、预售、档期这些特征与票房之间的非线性关系同时把过拟合压得比较低。这篇技术文基于猫眼电影数据把一套票房预测系统从特征清洗、模型训练、参数调优到回测验证完整拆开讲。适合正在做人工智能毕业设计或大作业的人也适合想给宣发团队搭建票房预测系统的后台工程师。2. 猫眼电影数据的字段与清洗先把特征对齐再谈 SVR 回归SVR 回归的核函数再强也只会把你喂进去的特征映射成距离计算。猫眼电影数据里字段多、缺失多、量纲差异大如果不做清洗就直接丢给 sklearn 的 SVR通常跑出来的 R2 低得让人怀疑模型选错了。这一章先把能上模型的字段理清楚再定标签最后给出清洗代码。2.1 猫眼公开数据里值得进入 SVR 的特征字段猫眼专业版和公开影片详情页提供的数据和“电影好不好看”这种主观评分不同大部分是行为数据比如想看人数、预售票房、首日场均人次。我一般会把字段分成三类上映前可得、上映当日可得、上映后更新模型只用前两类因为票房预测系统的价值主要在上映前和上映首日。字段名字段含义对票房预测的作用清洗关注点want_watch上映前累计想看人数强正相关是口碑传播的前置信号右偏严重需 log1p 变换pre_sale_amount首日或首周预售票房与首日和总票房相关性极高预售开启时间不统一需归一化到上映前N天first_day_review首日用户评分非线性作用存在口碑阈值缺失多建议用同档期均值补star_power主演/导演热度指数均值辅助特征解释长尾不同明星口径差异大先做 min-maxcategory影片类型影响票房基线和受众规模多标签类型只取第一个主类型release_slots档期标签春节/暑期/国庆对票房有数量级影响档期需要人为划分不能只看月份exclusive是否为IMAX/巨幕制式抬高票价和首周排片0/1 二元特征这张表没有把影片时长、发行公司放进去原因是特征太多了反而会让小样本上的 SVR 回归变得不稳。实际项目里可以先用这批字段跑通基线再逐步加特征看验证集变化。2.2 标签定义预测首月票房而不是总票房票房预测系统训练时最容易被忽略的是标签口径。直接拿“最终总票房”当 y会让模型在训练阶段就已经“作弊”密钥延期到下映的电影观察期远超其他影片而真正上线预测新片时根本没有这个信息。常见做法是定义一个固定的观测窗口我一般取上映后 28 天累计票房作为标签。理由首月票房和最终总票房的相关系数在公开研究里通常落在 0.9 以上同时 28 天窗口保证所有训练样本的标签口径一致。另一个必须做的是 log1p 变换。SVR 的 RBF 核靠样本间距离计算相似度票房从几百万到几十亿横跨三个数量级直接回归会让大票房样本完全主导误差。取对数后误差从“亿元级绝对值误差”变成“百分比级相对误差”更符合业务上对预测的理解。2.3 用 Pandas 做票房数据清洗与特征列构造以下代码是清洗部分假设你已经按统一约定导出为 CSV。import pandas as pd import numpy as np # release_date 是上映日期box_office_28d 是上映后28天累计票房 df pd.read_csv(maoyan_movies.csv, parse_dates[release_date]) # 只保留有28天标签的样本没有标签的影片不能进训练集 df df.dropna(subset[box_office_28d]) # 想看人数、预售金额都是右偏分布log1p 能把长尾压缩到可控范围 df[want_watch_log] np.log1p(df[want_watch]) df[pre_sale_log] np.log1p(df[pre_sale_amount]) # 首日评分缺失时用同档期的平均评分填充而不是全局均值 df[first_day_review] df[first_day_review].fillna( df.groupby(release_slots)[first_day_review].transform(mean) ) # 日期特征让 SVR 能学习到“周几上映”“哪个月上”的影响 df[release_weekday] df[release_date].dt.weekday df[release_month] df[release_date].dt.month # 只保留回归要用的列减少意外类型干扰 cols [want_watch_log, pre_sale_log, star_power, first_day_review, exclusive, category, release_slots, release_weekday, release_month, box_office_28d] df df[cols].reset_index(dropTrue)dropna(subset[box_office_28d])只清理标签特征缺失留在后面填充因为特征数据量本来就少不要因为一个字段缺失丢掉整行样本。groupby(release_slots)[first_day_review].transform(mean)是分档期填充春节档和暑期档的口碑基线不同用全局均值会抹平档期间差异。对want_watch取对数之前先确认数据里没有负数猫眼公开数据一般不会出现负值但如果从多个数据源合并可能会混入“0 表示无记录”和“负数表示退票”需要先过滤。2.4 类别特征先留到 Pipeline 里处理有人习惯在 DataFrame 里直接pd.get_dummies()把类型和档期一次性展开成几十列。这样做离线训练没有问题但到了线上预测如果新片出现一个训练集里没见过的类型列数不匹配直接崩。SVR 回归器本身不能接受 NaN所以我通常用ColumnTransformer把数值标准化和类别独热编码都塞进 Pipeline这会在第三章代码里出现。注意类别特征编码必须在交叉验证内部完成不能在训练前手动展开否则验证集的信息会提前泄漏进训练流程。泄漏会让网格搜索结果虚高真实票房预测效果反而更差。3. 用 SVR 回归器训练模型的最小闭环从数据划分到评估SVR 全称 Support Vector Regression大家习惯叫 SVR 回归严格说 R 已经表示回归。语言习惯归习惯模型本身要当成一个回归器来对待。很多人直接用SVR().fit(X_train, y_train)然后被糟糕的 R2 打击。问题基本出在两处特征没标准化、类别特征没编码。这一章给出能一次跑通的最小 Pipeline。3.1 从特征矩阵到 SVR 回归器的最小 Pipelinefrom sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.svm import SVR num_cols [want_watch_log, pre_sale_log, star_power, first_day_review, exclusive, release_weekday, release_month] cat_cols [category, release_slots] pre ColumnTransformer([ (num, StandardScaler(), num_cols), (cat, OneHotEncoder(handle_unknownignore), cat_cols), ]) model Pipeline([ (pre, pre), (svr, SVR()), ]) X df[num_cols cat_cols] y np.log1p(df[box_office_28d]) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) model.fit(X_train, y_train) print(model.score(X_test, y_test))StandardScaler不是可选项。RBF 核函数里计算的是exp(-gamma * ||u-v||^2)如果 one-hot 列在 0/1 之间而pre_sale_amount数值在几千万距离完全被金额主导SVR 等于只看了这一个特征。标准化后每个数值特征才拥有相同的尺度。OneHotEncoder(handle_unknownignore)让模型预测新类型时不报错。代价是这种类型对应的所有类别维度都是 0模型只能依据其他特征预测相当于自动退化成“无类型先验”的预测。random_state42固定数据划分是为了让后面 GridSearchCV 的结果可复现。票房数据量小不同随机种子的划分可能会让测试集 MAE 上下浮动 20% 以上。这里先用随机划分跑通闭环严格的时间顺序回测在第 6 章换掉。3.2 在真实票房空间和对数空间同时看误差from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score pred_log model.predict(X_test) pred_amount np.expm1(pred_log) true_amount np.expm1(y_test) print(f真实票房 MAE: {mean_absolute_error(true_amount, pred_amount) / 1e8:.2f} 亿) print(f真实票房 RMSE: {np.sqrt(mean_squared_error(true_amount, pred_amount)) / 1e8:.2f} 亿) print(f对数空间 MAE: {mean_absolute_error(y_test, pred_log):.4f}) print(f对数空间 R2: {r2_score(y_test, pred_log):.3f})np.expm1是exp(x)-1的数值稳定版本和训练前的np.log1p正好互逆。为什么要两个空间都看SVR 的训练目标是对数空间误差所以对数空间 MAE 是模型真实优化目标的考核真实票房 MAE 是业务汇报口径。两套指标同时给出可以定位问题。如果对数空间 R2 超过 0.9 而真实空间 MAE 依然很大说明误差集中在头部大片不能靠继续调 SVR 解决应该增加大片的独立特征。指标所在空间用途MAE对数空间考核模型训练目标MAE真实空间业务沟通口径R2对数空间看整体解释力3.3 缺乏预售和想看数时的兜底特征策略前面代码都假设新片已经有想看数和预售数据。实际预测系统里还有一个更早的使用时间点立项期或定档期此时想看数还没积累出来。常见做法是分两套模型一套用“上映前 7 天数据”预测另一套只依赖类型、档期、导演演员热度。如果特征实在缺失不要直接删列。我一般会把缺失项用同档期同类型影片的中位数填充同时增加一列feature_ready标记该样本的可用特征数量。SVR 不能接受 NaN但通过feature_ready可以把“信息不完整”本身变成特征。这样模型可以学到“缺少预售时预测要更保守”而不是在残缺特征里强行外推。4. SVR回归的三个必调参数C、epsilon、gammaPipeline 跑通只是开始“SVR回归调参”才是决定票房预测系统质量的部分。SVR 参数不多真正要动的一般是 C、epsilon、gamma 三个。先搞懂含义再搜索能省下大量算力。4.1 C、epsilon、gamma 三个参数分别管什么参数作用对象设小的后果设大的后果票房场景候选区间C对容忍带外样本的惩罚欠拟合整体预测趋近均值过拟合把个别扑街或爆款样本背下来1 到 50epsilon容忍带宽度对预售数据里的噪声敏感预测过于平滑错过真实波动0.01 到 0.2对数空间gammaRBF核的样本影响半径决策边界太平滑非线性抓不住样本只影响自身近似最近邻scale 或 0.001 到 0.1三个参数并不是独立的。C 和 epsilon 的共同作用决定了模型复杂度如果把 epsilon 调得很大模型几乎不会产生支持向量此时再加大 C 也改变不了多少。RBF 核的 gamma 则直接影响决策面的粗糙程度在票房这种小样本场景里gamma 大于 0.1 基本就会开始记忆个别样本所以搜索时不要给太大的上界。4.2 用 GridSearchCV 搜索 SVR 参数组合from sklearn.model_selection import GridSearchCV param_grid { svr__C: [0.1, 1, 10, 50], svr__epsilon: [0.01, 0.05, 0.1, 0.2], svr__gamma: [scale, 0.001, 0.01, 0.1], } search GridSearchCV( model, param_grid, scoringneg_mean_absolute_error, cv5, n_jobs-1, ) search.fit(X_train, y_train) print(search.best_params_)为什么model可以直接传进去因为前面它已经是 Pipeline所以参数名带svr__前缀。scoringneg_mean_absolute_error而不是r2SVR 训练本身就基于误差带MAE 更贴近业务损失r2 对票房这种重尾分布过于乐观。64 组参数乘 5 折总共 320 次拟合。几百条样本时很快但如果后续样本量到了几万条优先做两阶段搜索先用大间隔粗搜再在小范围内细搜不要一上来就全网格。4.3 票房场景下调参与分类任务常见的两个差异第一不要直接套 SVC 的思路。SVC 的 C 调参侧重分类边界SVR 还需要关注 epsilon很多人把 epsilon 留在默认值 0.1从头到尾只调 C 和 gamma。在票房这个场景预售数据本身噪声很大适当把 epsilon 放到 0.05 到 0.1反而比无限加大 C 的效果更好。第二不要在固定测试集上反复搜索参数。网格搜索本身已经用了交叉验证如果再用测试集去挑“最好”的一组参数测试集信息就通过人工选择泄漏回模型。我一般建议中间探索阶段用验证集全部定稿后再跑一次测试集且只跑一次。这是小样本调参最容易翻车的地方。5. 把票房预测系统封装成可直接调用的服务模型在 notebook 里 fit 出来只是第一步。真正能给业务用的系统至少要有固定的特征输入格式、可保存/加载的模型文件以及一个不把 NaN 抛给模型的状态。这章给出一个最小封装。5.1 只输出点估计的预测系统很难通过业务验收业务方问“这片子能卖多少”你回一个数字下周票房数据出来差了 30%这个系统就会被判死刑。更好的做法是输出区间用训练集残差的标准差来构造对数空间区间。import joblib import numpy as np import pandas as pd class BoxOfficePredictor: 猫眼票房预测系统的线上封装 def __init__(self, model_path): self.model joblib.load(model_path) self.residual_std 0.2 # 默认值训练后重新设置 def fit_residual_std(self, X_val, y_val): pred_log self.model.predict(X_val) self.residual_std np.std(y_val - pred_log) def predict(self, df): pred_log self.model.predict(df) pred np.expm1(pred_log) lower np.expm1(pred_log - 1.28 * self.residual_std) upper np.expm1(pred_log 1.28 * self.residual_std) return pd.DataFrame({ pred_box_office: pred, lower_80: lower, upper_80: upper, })residual_std必须在 log 空间计算。如果在真实票房空间计算大片残差可能是小片的几百倍一个 std 被爆款片主导所有小片的区间都会宽得失去意义。80% 区间采用 1.28 倍标准差前提是残差近似正态。票房数据并非严格正态但这里要的是业务沟通用的不确定区间不是严谨的分位数回归。如果想更严谨可以用验证集残差的实际分位数来生成区间。提示80% 区间基于正态近似若业务要求更严格可用验证集残差的实际分位数替代。5.2 模型的保存、加载与特征列一致性校验训练完以后用joblib.dump(search.best_estimator_, box_office_svr.joblib)保存整个 Pipeline特征工程参数也就一起存进去了。不要只保存SVR那部分否则线上还得重新写一遍 StandardScaler 和 OneHotEncoder极易出偏差。加载侧有一个容易被忽略的问题Pipeline 里 OneHotEncoder 记住了训练时的列顺序线上输入 DataFrame 的列顺序必须和训练一致。常见做法是把列名清单用 JSON 存下来加载时做一次校验。import json with open(feat_schema.json, w) as f: json.dump({num_cols: num_cols, cat_cols: cat_cols}, f, indent2) def check_schema(df): required num_cols cat_cols missing [c for c in required if c not in df.columns] if missing: raise ValueError(fmissing features: {missing})这个校验能避免“训练用df[[...]]、线上用df.to_dict(records)”之类的低级事故。列顺序在 Pipeline 内部已经固定只要列名齐全ColumnTransformer会按自己的顺序取数。5.3 SVR 模型更新策略不支持在线增量学习sklearn 的SVR是用 libsvm 实现的不支持partial_fit。有些实时票房系统想每 5 分钟更新一次模型这在 SVR 场景下不是合理做法。常见做法是每天或每周拉取一次猫眼数据全量重训并重新做一次小规模网格搜索。由于票房样本量小全量重训的时间成本很低。更新方式适用场景SVR 是否支持全量重训日/周级别更新支持增量更新分钟级在线学习不支持换 SGDRegressor如果真的需要分钟级在线学习应该换用 SGDRegressor 配合 Huber loss或者在线 Ridge而不是继续用 SVR。这个取舍要在系统设计阶段就定下来不要等上线后再临时改模型。如果你在做人工智能学习路径相关的项目实践这一步值得单独记录下来模型选型不只是精度问题还包含更新机制和运维成本。6. 把 SVR 票房预测模型跑上线前的一套验证技巧交叉验证分数好看不代表上线能打。票房数据与时间强相关如果验证方式不对SVR 回归器学到的是“同类片子都在同一个时间段”而不是“票房的普遍规律”。这章讲三个上线前必须做的验证。6.1 用 TimeSeriesSplit 代替随机切分随机切分时测试集里可能混入比训练集还早的影片档期效应和宣发趋势会顺着样本年代泄漏进去。先按上映日期排序再用 TimeSeriesSplit。from sklearn.model_selection import TimeSeriesSplit df df.sort_values(release_date).reset_index(dropTrue) X df[num_cols cat_cols] y np.log1p(df[box_office_28d]) tscv TimeSeriesSplit(n_splits5) for train_idx, test_idx in tscv.split(X): model.fit(X.iloc[train_idx], y.iloc[train_idx]) last_pred_log model.predict(X.iloc[test_idx]) last_y_test_log y.iloc[test_idx] print(mean_absolute_error(last_y_test_log, last_pred_log))TimeSeriesSplit 不 shuffle训练集永远是“过去”的影片测试集永远是“未来”。如果这里的 MAE 比随机切分高很多完全正常说明随机切分的分数里有一部分是虚高的。6.2 按票房量级分组看误差而不是只看总 MAE总 MAE 会被 20 亿以上的大制作淹没一个预测误差 3 亿的大片抵得上几十部小片。分组统计能看出模型到底在哪类影片上崩盘。df_ev pd.DataFrame({ true: np.expm1(last_y_test_log), pred: np.expm1(last_pred_log) }) df_ev[group] pd.cut( df_ev[true], bins[0, 1e8, 5e8, 2e9, np.inf], labels[1亿以下, 1-5亿, 5-20亿, 20亿以上], ) for name, g in df_ev.groupby(group, observedTrue): mae mean_absolute_error(g[true], g[pred]) print(name, f{mae / 1e8:.2f} 亿)如果“1-5亿”组的误差反而大于“5-20亿”组说明模型被少数大片主导了需要重新考虑目标变换或做分层训练。SVR 对小样本的平衡能力没有想象中强。6.3 检查残差是否还和核心特征相关最后一个技巧预测做完之后把 log 空间残差拉出来和核心特征算 Spearman 相关。如果想看数和残差仍有明显相关说明 SVR 的核函数没有把这条非线性关系学干净继续调参不如回头修特征。from scipy.stats import spearmanr test_features X.iloc[test_idx] resid last_y_test_log - last_pred_log print(spearmanr(resid, test_features[want_watch_log]))常见做法是画一张pred_log与resid的散点图如果残差呈现喇叭口形状就说明对数变换还不够优先调整标签而不是继续加参数。这个检查每次重训后都要跑一遍因为它能直接指出下一个优化点在哪一端。本文还有配套的精品资源点击获取