
简介一套基于猫眼电影数据和SVR回归器实现的电影票房预测系统覆盖数据爬取、特征分析与票房预测的完整流程。项目源自个人毕业设计代码已通过运行测试并获答辩均分96分适合计算机相关专业学生用于毕设、课程设计或项目立项演示也可帮助具备Python基础的学习者理解回归建模与爬虫实践。压缩包共18个文件整体仅186KB包含7个Python源码脚本对应数据抓取、预处理、特征分析、SVR预测等模块、6个pyc编译缓存、4个woff字体文件用于猫眼反爬字体解析及1个xls电影特征数据表结构清晰便于对照源码快速理顺项目流程。目前已有157人学习作者可提供远程教学支持遇到运行问题可私聊解决。包内附有README说明文件便于快速掌握项目整体结构作为真实业务型机器学习回归样例也可在其上扩展新特征或调整预测目标适合练习完整项目链路。1. 做电影票房预测为什么绕不开这套技术栈接到「预测电影首周票房」这类需求时我第一反应就是把猫眼电影数据爬下来再做特征分析最后扔进 SVR 回归器里跑。这不是拍脑袋而是目前中短期票房预估里最稳妥、复现成本最低的组合爬虫解决数据来源特征分析把「想看人数、评分、档期、主演」这些杂信息变成数值特征SVR 回归器用径向基核去拟合票房和特征之间那条明显非线性的曲线。很多人用线性回归做票房预测结果在爆款和高冷文艺片上同时翻车就是因为线性模型扛不住这种两头极端的分布。这套基于猫眼电影数据和 SVR 回归器的电影票房预测系统适合手里有 Python 基础、想做数据采集到建模全流程的工程师也适合想用一份可复现代码快速验证票房预测可行性的分析师。下面我从数据采集开始把每一步的代码和边界都讲清楚。2. 用 requests 爬取猫眼票房数据接口字段与反爬应对2.1 猫眼票房榜的公开接口长什么样常见做法是直接请求猫眼的票房榜接口它返回的是结构化 JSON不需要走浏览器渲染比解析 HTML 省事很多。猫眼票房榜的接口路径通常是带有 offset 和 limit 参数的分页结构返回内容里包含电影名、上映日期、实时票房、累计票房、场次、人次、平均票价、上座率等字段。需要注意这个接口给的是「实时票房」口径不是猫眼专业版里的最终票房所以写爬虫时字段名要对齐接口返回的 key不能凭页面显示猜。我一般会先把接口返回的 JSON 结构打印出来确认字段后再写解析逻辑。票房榜接口常见的字段包括字段名类型说明movieNamestring电影名称releaseInfostring上映日期与地区信息boxInfostring实时票房带格式sumBoxInfostring累计票房带格式boxRatestring票房占比showInfostring场均人次与上座率splitInfostring场次信息注意这些字段大多是字符串而不是数字落地时需要统一清洗成 float。除了票房榜接口想看人数、评分这类字段通常要去电影详情接口拿常见做法是按电影 ID 逐个请求详情页再把两张表按电影名或 ID 合并。写爬虫前先想清楚最后要建什么模型特征决定字段不要一股脑全爬。2.2 最小爬虫代码Session 复用、请求头和限速爬猫眼这类站点最忌讳上来就无脑循环请求。我常用的模板是复用 requests.Session带上 User-Agent 和 Referer然后控制请求频率。下面是能直接跑通的最小实现以票房榜接口为例import time import requests import pandas as pd session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://piaofang.maoyan.com/, }) rows [] for offset in range(0, 100, 10): url fhttps://piaofang.maoyan.com/api/ajax/box?offset{offset}limit10 resp session.get(url, timeout10) resp.raise_for_status() data resp.json() # 常见接口结构data.list 里是当前页的电影数据 rows.extend(data.get(list, [])) time.sleep(1.2) df pd.DataFrame(rows) print(df.shape) print(df.head(3))这段代码里有两个关键点。第一Session 复用连接避免每次请求都重新握手也能统一携带头部信息Referer 字段对猫眼这类站点尤其重要少了它容易被判定为异常请求。第二time.sleep(1.2)是给自己留的缓冲猫眼对请求频率敏感爬太快会直接返回空列表。offset按 10 递增是分页逻辑具体步长以接口实际返回的 pageSize 为准有的是 10有的是 30。跑完先看df.shape如果行数远小于预期优先怀疑被反爬拦截了。2.3 清洗落盘字符串转数值和缺失值处理拿到 DataFrame 后直接建模是不行的原因在于票房榜接口大量字段是「1.2亿」「23.4万」这种带格式的字符串还有可能缺字段。常见做法是写一个清洗函数把中文字符统一转成数值单位把空字符串替换成 NaN再决定填充策略。import numpy as np def parse_amount(s): if isinstance(s, str): s s.strip() if s.endswith(亿): return float(s[:-1]) * 1e8 if s.endswith(万): return float(s[:-1]) * 1e4 try: return float(s) except ValueError: return np.nan return s # 只保留建模可能用到的列 keep_cols [movieName, releaseInfo, boxInfo, sumBoxInfo, boxRate] df_clean df[keep_cols].copy() for col in [boxInfo, sumBoxInfo, boxRate]: df_clean[col] df_clean[col].map(parse_amount) # 缺失处理票房字段缺失直接丢弃这部电影没有预测价值 df_clean df_clean.dropna(subset[sumBoxInfo]) df_clean.to_csv(maoyan_boxes.csv, indexFalse, encodingutf-8-sig) print(f清洗后剩余 {len(df_clean)} 条记录)parse_amount里有个容易被忽略的细节float(s[:-1])之前要先做strip()因为接口返回的字符串偶尔带空格。编码用utf-8-sig是为了让 Excel 打开 CSV 不乱码后续如果用 pandas 读回encodingutf-8-sig同样适用。这里的数据是建模的原料宁可去掉缺失行也不要让脏数据混进特征。3. 特征分析把原始字段变成 SVR 回归器能消化的数值特征3.1 特征分类与目标变量的选择票房预测的特征一般分成三类影片属性、上映前热度、上映后表现。影片属性包括类型、时长、制片地区、是否系列片上映前热度包括想看人数、预售票房、物料播放量上映后表现包括首日票房、首周末口碑评分、排片占比。SVR 回归器本身不吃文本类型、档期、主演这些必须编码成数值。目标变量我一般选首周票房而不是总票房理由是首周票房受上映前特征和首日表现影响最大预测边界清晰总票房要覆盖更长的时间窗口受口碑扩散和后续排片影响误差会显著变大。对猫眼抓下来的数据能直接当数值特征的是评分、想看人数、场次、人次。类型是典型的类别特征动作、喜剧、科幻这些标签需要 one-hot。档期也要拆暑期档、国庆档、春节档对票房的影响差别巨大一个小成本片子放在春节档和放在冷门档期首周票房可能差一个数量级。3.2 类别编码和时间特征的构建类型和档期的编码我一般直接用 pandas 的get_dummies。这里有个关键点get_dummies默认会把所有类别列展开成多列如果类型字段是「动作,冒险」这种逗号分隔的多标签要先str.split再展开否则整串会变成一个独立类别等于没拆分。主创信息不能简单做 one-hot当红花旦和小透明演员出现在同一列里one-hot 会让模型学到「这个演员出现过」但学不到热度差异。常见做法是把主演名字映射成近三年的平均票房热度这个需要离线统计代码里演示一下思路df_clean[genre_list] df_clean[genres].str.split(,) genre_dummies df_clean[genre_list].apply(lambda x: pd.Series([1] * len(x), indexx)).fillna(0) df_feat pd.concat([df_clean, genre_dummies], axis1) # 上映日期拆成星期几和月份 df_feat[release_date] pd.to_datetime(df_feat[releaseInfo].str[:10]) df_feat[release_weekday] df_feat[release_date].dt.dayofweek df_feat[release_month] df_feat[release_date].dt.month # 周末上映标记周四周五上映有利于首周票房 df_feat[is_weekend_release] df_feat[release_weekday].isin([3, 4]).astype(int) # 主演热度合并外部统计表若缺失用中位数填充 actor_heat pd.read_csv(actor_heat.csv) df_feat df_feat.merge(actor_heat, onlead_actor, howleft) df_feat[actor_heat] df_feat[actor_heat].fillna(df_feat[actor_heat].median())这段代码有几个地方值得展开。apply(lambda x: pd.Series(...))这种展开方式会把多标签类型拆成多个一行向量缺点是当某个标签出现次数极少时fillna(0)后整列几乎全零SVR 会把它们当噪声处理后续可以过滤掉出现次数小于 5 的标签。上映日期从字符串切片取前 10 位是因为releaseInfo常常是「2024-06-08 中国大陆」这种格式直接pd.to_datetime会报错。主演热度缺失用中位数填充而不是用 0理由是中位数不会拉偏分布。3.3 相关性分析与偏态处理特征做完先看相关性再进模型。这里有两个硬性任务一是看特征和目标变量之间的相关性排除掉那种「看着有关实际无关」的字段二是检查票房标签的分布。票房数据天然偏态头部爆款几亿尾部片子几百万如果不处理SVR 回归器会被少数高票房样本牵着走。import seaborn as sns import matplotlib.pyplot as plt from sklearn.preprocessing import StandardScaler numeric_cols [boxInfo, sumBoxInfo, boxRate, actor_heat, is_weekend_release] sns.heatmap(df_feat[numeric_cols].corr(), annotTrue, cmapcoolwarm) plt.savefig(feature_corr.png, dpi150, bbox_inchestight) # 标签取 log压缩偏态 df_feat[target_week1] np.log1p(df_feat[boxInfo]) # 标准化数值特征SVR 对量纲敏感 num_feats [boxRate, actor_heat, release_weekday] scaler StandardScaler() df_feat[num_feats] scaler.fit_transform(df_feat[num_feats]) print(df_feat[[target_week1, boxRate, actor_heat]].describe())np.log1p而不是np.log是因为boxInfo可能为 0log1p等价于log(1x)避免负无穷。标准化放在相关性分析之后的理由是corr()本身对量纲不敏感但 SVR 的径向基核函数计算距离时量纲差异会直接压垮小数值特征。如果画完热力图发现两个特征相关系数超过 0.85我一般会去掉其中一个因为 SVR 对冗余特征没有天然的惩罚机制共线性会让支持向量的选择变得不稳定。这一节做完特征应该是一张纯数值的宽表SVR 回归器才能顺利消化。4. SVR 回归器建模核函数、参数调节与时间序列划分4.1 为什么选 SVR 而不是线性回归票房和特征之间的关系是典型的非线性想看人数到了一定规模票房增速放缓评分很高但排片少票房依旧上不去。线性回归在这种场景下表现很差因为它假设特征是独立线性累加。SVR 回归器的思路不是拟合所有样本点而是拟合一个「间隔带」只有落在间隔带外面的样本才会成为支持向量损失函数也只惩罚这些样本。一旦选 SVR就要面对四个参数kernel、C、epsilon、gamma。kernel我默认选rbf它能把特征映射到高维空间去拟合非线性关系poly在特征维度高时容易过拟合线性核在票房预测这种小样本场景下拟合能力不够。C是正则化系数越大越不愿意容忍误差但也越容易过拟合epsilon是间隔带半宽越大支持向量越少模型越平滑gamma控制径向基核的作用半径越大单个样本的影响范围越小。这四个参数没有固定值必须靠网格搜索。参数默认值调节方向常见区间kernelrbf首选 rbf线性核做对比rbf / linearC1.0过拟合就调小欠拟合就调大1 ~ 100epsilon0.1噪声大就调大想拟合更细就调小0.01 ~ 1gammascale特征多时用 scale特征少时试固定值scale / 0.01 ~ 0.1gamma的scale选项会根据特征数量自动计算是 sklearn 给的安全默认值。特征少于 20 个时我通常手动试小数值区间特征多时用scale起步。4.2 建模管线与网格搜索一份可以抄作业的代码SVR 的建模流程我会写成 Pipeline把标准化和模型放在一起避免网格搜索交叉验证时数据泄漏。这里给出完整示例from sklearn.svm import SVR from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.model_selection import GridSearchCV from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score import numpy as np feature_cols [c for c in df_feat.columns if c not in [movieName, releaseInfo, genres, release_date, target_week1]] X df_feat[feature_cols].values y df_feat[target_week1].values # 时间序列划分按上映日期排序后切分不打乱 split int(len(df_feat) * 0.8) X_train, X_test X[:split], X[split:] y_train, y_test y[:split], y[split:] pipe Pipeline([ (scale, StandardScaler()), (svr, SVR(kernelrbf, epsilon0.1)), ]) param_grid { svr__C: [1, 10, 100], svr__epsilon: [0.01, 0.1, 1], svr__gamma: [scale, 0.01, 0.1], } grid GridSearchCV(pipe, param_grid, cv5, scoringneg_mean_absolute_error, n_jobs-1) grid.fit(X_train, y_train) best grid.best_estimator_ y_pred_log best.predict(X_test) y_pred np.expm1(y_pred_log) y_true np.expm1(y_test) print(best params:, grid.best_params_) print(MAE:, mean_absolute_error(y_true, y_pred)) print(RMSE:, np.sqrt(mean_squared_error(y_true, y_pred))) print(R2:, r2_score(y_true, y_pred))三个地方需要重点说明。第一Pipeline里的StandardScaler会在交叉验证的每一折内重新 fit不会用全量数据的均值和方差去污染训练集这是网上很多 SVR 示例代码踩坑的地方。第二GridSearchCV的scoring用的是neg_mean_absolute_error因为 MAE 在票房场景下更直观「平均差多少万票房」比「误差平方」好解释。第三预测结果做了一次np.expm1还原因为训练时对标签做了log1p还原后的y_pred才是真实的票房量级。C和epsilon的搜索区间要根据数据量调整。几百条样本时C超过 100 极容易过拟合训练集分数很好测试集一塌糊涂epsilon调太大模型会变得过于平滑直接丢失对爆款电影的预测能力。网格搜索跑完后不要只看 best params把grid.cv_results_里每个参数组合的得分打印出来看看趋势比单点最优值更有参考价值。4.3 时间序列划分为什么不能随机 split票房预测天然是时间序列问题用 2023 年之前的数据训练去预测 2023 年之后的电影。很多人建模时直接train_test_split(test_size0.2, random_state42)这个操作在票房场景下是错的。随机划分会让相近时间上映的电影一部分进训练集、一部分进测试集而同期电影在类型、档期、大盘热度上高度相似测试集分数会被虚高。更隐蔽的是如果样本里包含「上映后」才产生的特征比如累计票房、上映第二周的场次随机划分等于把未来信息泄进了训练集。我一般按上映日期排序后取前 80% 做训练、后 20% 做测试代码就是上面split的写法。更严谨一点的做法是留出法按时间窗口滚动训练集用第 1 到 12 个月测试集用第 13 个月然后滑到第 2 到 13 个月、测试第 14 个月。对猫眼这种数据量不大的场景固定切分足够评估模型了滚动窗口留给特征工程稳定后的精调。注意排序要用原始上映日期而不是 DataFrame 的索引df_feat在前面的特征处理中可能已经被concat改变了行序。5. 避坑清单从爬虫到 SVR 回归器的 6 个翻车点5.1 爬虫返回空列表接口被反爬拦截现象请求接口后data.get(list)一直是空数组但浏览器里打开接口地址却能看到数据。原因缺少必要的请求头或者请求频率太高被服务端熔断。解决先加User-Agent和Referer再用time.sleep把请求间隔拉大到 1 秒以上。血泪经验是Referer 字段比 User-Agent 更容易被忽略但很多站点恰恰优先校验它。如果加了头还不行检查一下请求里是否带了Cookie猫眼有些接口对未登录会话会返回空数据此时可以用session.get先访问一次榜单页面再请求接口让 Session 持有合法的会话状态。5.2 票房标签偏态导致预测值全部堆在均值附近现象模型在训练集上 MAE 很低测试集上预测出来的票房全都在几千万上下高分爆款和低分冷门都没预测出来。原因票房标签严重右偏SVR 用对称的epsilon间隔带拟合头部大票房的样本太少模型为了最小化整体损失倾向于把所有样本预测到分布中心。解决对标签做log1p变换后再训练预测完用expm1还原。这一步是票房预测 SVR 建模里性价比最高的一处调整没有之一。做了 log 变换后误差评估也要同步在还原后的空间里算不能拿y_pred_log和y_test_log直接报告 MAE那个数字在 log 空间里没有业务含义。5.3 随机划分训练集导致测试集分数虚高现象用train_test_split建模R2 到 0.9 以上换成时间排序切分后 R2 掉到 0.5。原因同期电影特征相似随机划分把「好友」分到了测试集模型相当于开卷考试。解决严格按上映日期排序切分。更稳妥的做法是把「月份」特征显式加进建模让模型学到季节性。踩过这个坑之后我再也不信任何没有时间切分的票房回归结果这个领域的评估指标如果不按时间划分就没有参考价值。5.4 One-Hot 展开后特征维度过大训练慢到怀疑人生现象类型字段做get_dummies后多出 50 多列加上档期、地区特征维度破百SVR 训练时间从几秒涨到几分钟。原因低频类别全部被展开成独立列大部分列非零值极少径向基核在稀疏高维特征上计算开销成倍增长。解决先统计各标签出现次数只保留出现次数大于 5 的标签其余归为「其他」。类别编码不是越全越好SVR 在小样本场景下特征维度尽量控制在 30 以内超过这个阈值优先考虑主成分分析降维或者把低频标签手工合并。5.5 特征里混入未来信息模型「作弊」而不自知现象模型 MAE 低得离谱直到用真实上线数据预测才发现完全不准。原因特征表里混进了上映后才产生的字段比如「累计票房」出现在训练特征里但真实预测时这个值根本拿不到。解决建模前把特征分成「上线前可得」和「上线后可得」两组。上线前特征包括想看人数、预售、档期、主演热度上线后特征包括首日票房、首日排片、开画评分。如果目标是预测首周票房特征只能用到首日及之前的数据这是猫眼票房预测系统最容易掉进去的坑。我一般会在特征表里加一列available_from标记数据可用时间点调模型时按时间点过滤能省掉很多后期排查的麻烦。5.6 网格搜索全用默认评分结果被极端值绑架现象GridSearchCV默认用 R2 或 MSE 评分选出来的参数预测普通片子很准但一到爆款电影就崩。原因MSE 对离群点极度敏感票房分布里头部爆款数量少但量级大模型为了压低 MSE把大量参数预算花在拟合爆款上。解决评估指标改用neg_mean_absolute_error或neg_median_absolute_error。中位数绝对误差对离群点更稳健在票房这种长尾分布下比 MAE 更值得参考。调参前先想清楚业务上你最在意哪类片子如果在意的是「普通片子预测准」MAE 优先如果在意「爆款不跑偏」建议把 MAE 和 P90 误差一起打印出来看。6. 验证模型和时间切片回测把 SVR 预测误差变成一张可用图表SVR 回归器通过网格搜索拿到最优参数后先别急着写报告。我最后一步固定做时间切片回测验证模型在不同时间窗口的稳定性。方法很简单把数据按上映月份分成 12 个窗口每次用前 11 个月做训练、后 1 个月做测试循环得到 12 组误差再画一张月度 MAE 的折线图。这张图能直接看出模型在暑期档和冷门档期的表现差异如果某个月的 MAE 突然飙高基本可以定位到特征里缺少那个月的关键变量比如春节档的「票补力度」。from sklearn.svm import SVR from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.metrics import mean_absolute_error df_feat[year_month] df_feat[release_date].dt.to_period(M) months sorted(df_feat[year_month].unique()) results [] for i in range(6, len(months)): train_mask df_feat[year_month].isin(months[:i]) test_mask df_feat[year_month] months[i] X_train df_feat.loc[train_mask, feature_cols] y_train df_feat.loc[train_mask, target_week1] X_test df_feat.loc[test_mask, feature_cols] y_true df_feat.loc[test_mask, sumBoxInfo] pipe Pipeline([ (scale, StandardScaler()), (svr, SVR(kernelrbf, C10, epsilon0.1, gammascale)), ]) pipe.fit(X_train, y_train) y_pred np.expm1(pipe.predict(X_test)) results.append({ month: str(months[i]), mae: mean_absolute_error(y_true, y_pred), }) # 月度误差可视化定位模型失效的时间窗口 result_df pd.DataFrame(results) print(result_df.sort_values(mae, ascendingFalse).head(5))时间切片回测的代码逻辑和普通训练几乎一样区别只在数据划分。results里记录的是每个月的 MAE排完序后看一眼最大的几个月就能知道模型在哪些时段不可靠。我习惯把这张图直接放进项目文档里比单纯贴一个 R2 数字有说服力得多。如果还想往深走可以给 SVR 模型加一层解释性分析。用permutation_importance或 SHAP 计算每个特征对预测结果的贡献把「想看人数贡献最大」这类结论写成给业务方看的报告。我自己常用的方式是排列重要性它对 SVR 这种黑匣子模型友好逻辑也很好讲随机打乱某个特征列观察预测误差上升多少上升越多说明特征越重要。这比 SHAP 的核估计快数据量大时更实用。这套「爬数据 → 做特征 → SVR 建模 → 时间切片验证」的流程我已经用在不同数据源的多个项目上。这里分享一个教训如果你在网格搜索时发现最优参数总落在搜索区间的边界比如gamma的最优值恰好是0.01的最小值就该考虑扩大搜索范围而不是直接取这个值边界最优通常意味着真正的极值不在你的候选集里。先做到这一步再谈优化也不迟。希望帮到你。本文还有配套的精品资源点击获取