简介票房预测是电影行业数据分析中的典型应用场景但由于样本量有限且特征关系复杂传统线性回归难以捕捉非线性模式。支持向量回归SVR通过核函数将低维特征映射到高维空间在中小规模数据集上表现出稳定且泛化能力强的优势。实际工程中需结合Python爬虫从猫眼等平台采集影片基本信息与市场表现数据并完成数据清洗、单位标准化、类别特征编码等预处理步骤。在特征工程中合理使用目标编码、时间序列切分和相关性筛选能有效避免数据泄露与过拟合。通过网格搜索关键参数C、epsilon、gamma并采用时序交叉验证可构建具备可信度的票房预测模型。这种方法不仅适用于猫眼数据也为影视内容排期、营销策略等业务决策提供数据支撑。1. 用Python去爬猫眼电影数据再交给SVR回归器做票房预测是很多数据分析团队的常见起步方案一部电影上映前一周制片方拿到的预估票房是内部决策的重要依据。直接用线性回归票房和特征之间复杂的非线性关系拟合不足上深度学习几千条样本规模根本不给你机会。SVR支持向量回归刚好卡在中间它靠核函数把特征映射到高维空间对中小样本回归任务非常稳。这篇文章要讲的就是怎么用Python把猫眼电影数据爬下来做完特征分析再用SVR回归器搭出一套票房预测系统从代码到参数、从坑到调优一步步讲清楚。适合正在做数据分析项目、课程设计或想转行做预测建模的从业者。2. 从猫眼抓数据爬虫设计、反爬应对与数据清洗2.1 先从票房预测倒推字段清单别把页面能看见的全抓了很多新手写爬虫的第一步就是打开猫眼页面看到什么抓什么。等爬到一半才发现真正建模要用的是字段之间的关系以及字段的时间口径。做票房预测至少要分清三类字段。第一类是影片基础属性比如片名、类型、制片地区、导演、主演、上映日期。第二类是上映后的表现包括首日票房、首周票房、累计票房、上映天数、评分、想看人数。第三类是外部环境因素比如档期、同档期竞争影片数量。这里有个容易被忽略的边界上映前做预测只能用到第一类和第三类第二类完全不可用。你不可能在电影上映前就知道首日票房这是很多课程设计一上来就翻车的根源。我一般会把抓取拆成两个阶段先抓上映前的静态属性再按日期抓每日票房数据最后通过电影ID关联。这样同一个ID在不同阶段累积的数据都保留下来特征工程阶段才有得选。下面这张字段表是我常用的一份清单建模用途和时间口径都标好了字段类型建模用途可用时点片名文本名称实体上映前类型类别独热编码上映前导演/主演类别历史票房均值上映前上映日期日期距离开映天数上映前想看人数数值热度信号上映前首日票房数值早期热度上映后累计票房数值回归目标上映后评分数值口碑信号上映后这张表直接决定了爬虫的解析范围。把网页里的非结构化字段整理成上面这样的结构化数据是爬虫环节的核心目标。数据库用sqlite3就够了这是Python连接数据库最轻的方案后面要换MySQL或者Oracle只需要改连接对象清洗和建模代码不用动。2.2 爬虫核心代码请求、解析、入库一步到位定好字段后开始写爬虫。猫眼有Web端页面也有App接口常见的做法是先请求网页版列表页拿到电影ID再请求详情页把类型、导演、演员、评分这些字段补齐。新项目直接用requests解析用lxml配合BeautifulSoup。这里给一个完整可跑的清单页抓取骨架import requests import pandas as pd import sqlite3 import time from bs4 import BeautifulSoup HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://www.maoyan.com/, } def fetch_movie_list(offset0): 抓取猫眼电影列表页返回电影的ID列表 url https://www.maoyan.com/films?showType2offset{}.format(offset) try: resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() except requests.RequestException as e: print([请求失败] offset{}: {}.format(offset, e)) return [] soup BeautifulSoup(resp.text, lxml) items [] for a in soup.select(a.movie-poster-data): movie_id a.get(data-val) if movie_id: items.append({ movie_id: movie_id, fetched_at: time.strftime(%Y-%m-%d %H:%M:%S) }) return items def save_to_sqlite(items, db_pathmovie.db): conn sqlite3.connect(db_path) df pd.DataFrame(items) df.to_sql(movie_list, conn, if_existsappend, indexFalse) conn.close() if __name__ __main__: for offset in range(0, 100, 10): items fetch_movie_list(offset) if not items: break save_to_sqlite(items) time.sleep(1.5)这段代码的逻辑是猫眼列表页按offset翻页每页固定条数返回的data-val属性就是猫眼内部电影ID后面所有详情请求都要拿这个ID去拼URL。HEADERS里的User-Agent和Referer必须带上空UA很容易被拦截。sleep时间从1.5秒起如果触发限流就加大到3秒这段操作在调试时别省。fetch_movie_list里有个细节值得说明列表页的数据密度太低只有海报链接里的电影ID所以还要再补一个详情页请求。详情页解析代码大概长这样def fetch_movie_detail(movie_id): url https://www.maoyan.com/films/{}.format(movie_id) try: resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() except requests.RequestException: return None soup BeautifulSoup(resp.text, lxml) # 先检查是否被反爬页拦截避免灾难性空解析 if 验证 in resp.text: return {movie_id: movie_id, error: blocked} row {movie_id: movie_id} type_el soup.select_one(.type-name) if type_el: row[genres] type_el.get_text(stripTrue) actors [] for name_el in soup.select(.actor-name a): actors.append(name_el.get_text(stripTrue)) row[actors] /.join(actors[:3]) return row这里有两个点要牢记。第一猫眼页面结构改版比较频繁CSS选择器只反映写稿时的结构落地时如果发现选择器失效优先检查返回的HTML是不是反爬验证页而不是怀疑解析代码。第二详情字段要单独存一张表用movie_id和列表表关联这样某个字段在部分影片上缺失时不会影响其他记录的完整性。在VS Code里运行之前先确认解释器选对了项目虚拟环境如果解释器指向了conda的base环境而项目依赖装在虚拟环境里import bs4会直接报错这是用VS Code配置Python开发环境时最典型的坑。2.3 数据清洗票房单位、日期口径、重复记录一起处理爬下来的数据至少要过三关。第一关是重复记录。列表页翻页偶尔会返回和上一页相同的电影用movie_id做去重建表时给movie_id加唯一索引是最稳的。第二关是单位统一。猫眼网页上显示的票房经常是“1.2亿”或“8600万”这种字符串直接把字符串丢给SVR会因为类型转换失败而报错。这里有一个我一直在用的解析函数def parse_box_office(text): 把1.2亿8600万统一换算成万元 if not isinstance(text, str): return None text text.strip() if 亿 in text: return float(text.replace(亿, )) * 10000 if 万 in text: return float(text.replace(万, )) if text or text 暂无: return None return float(text)第三关是日期对齐。猫眼详情页给的日期格式可能是“2024-05-01”也可能是“2024年5月1日”统一转成datetime并计算出“距开映天数”和“所在档期”两个新列。这两个列后面在特征分析阶段直接用得上比在建模时现算省事。字段缺失的处理也有讲究。导演或演员缺失先填充“未知”再入库而不是直接删行。SVR对缺失值敏感删行会在小样本条件下直接损失训练数据后期每一条样本都弥足珍贵。清洗干净后如果想导出Excel副本给业务方看一行df.to_excel(clean_movie.xlsx)就能搞定这一步能省去很多不必要的口舌之争。3. 特征分析把影片属性变成SVR能学的特征矩阵3.1 从原始字段里能挖出哪些有效特征清洗完的数据还在数据库里待着SVR只认数值矩阵所以特征分析的核心任务是把数据库字段变成有解释力的数值特征。这个环节决定模型上限后面SVR参数调得再好也补不回糟糕的特征。我常用的特征体系分成三组。第一组是基础数值特征片长、评分、想看人数、预告片播放量。第二组是类别特征类型、制片地区、档期、是否系列片、是否IP改编类别特征需要编码后才能进模型。第三组是衍生统计特征导演上一部电影的票房均值、主演出演过的最高票房电影、同档期竞争影片数量。衍生特征在票房预测里作用最大。一个导演过去三部电影的平均票房比单纯把导演名字做成one-hot更符合SVR需求。名字one-hot在样本少时会产生大量稀疏列而且完全没有泛化能力——新导演的名字不在训练集里就直接失效。统计特征基于历史数据计算新导演也能有值比如用全体导演的均值做填充。同档期竞争影片数量这个特征要按上映日滚动统计这是特征分析环节最容易出数据泄露的地方。某部电影上映时的竞争格局只包含它上映日前后一段时间内开画的电影绝不能把整年所有影片都算进去。我一般会写一个窗口函数以当前电影上映日为锚点统计前后7天内的开画数量。3.2 类别特征编码实操one-hot与target encoding怎么选类别特征进入SVR之前有几种编码方式可选。最常见的是pandas的get_dummies直接做one-hot简单粗暴def encode_categorical(df, category_cols): df_encoded pd.get_dummies(df, columnscategory_cols, dummy_naFalse) return df_encodedget_dummies用起来方便但有两个现实问题。第一高基数的类别会产生大量稀疏列。演员列如果有300个不同名字one-hot后就是300列而训练集只有几百条样本SVR瞬间陷入高维稀疏困境。第二对未来数据不友好测试集里出现新演员训练好的模型会因列数不一致而报错。所以我更常使用目标编码把类别转成该类别历史上票房目标列的均值def target_encode(train_df, col, targettotal_box_office): 目标编码用类别在训练集上的票房均值作为新特征 mapping train_df.groupby(col)[target].mean() train_df[col _target] train_df[col].map(mapping) return train_df, mapping train_df, actor_mapping target_encode(train_df, actor1) test_df[actor1_target] test_df[actor1].map(actor_mapping) test_df[actor1_target] test_df[actor1_target].fillna( train_df[actor1_target].mean() )目标编码的坑是容易过拟合解决办法是加平滑系数或者只在类别出现次数大于等于5时生效否则回归到全局均值。在样本量不到2000条的票房数据上我会限制目标编码只对高频类别生效低频类别一律用全体均值填充。日期和档期这类有序特征更建议直接转成连续数值。档期离春节越近热度越高本质上是有序变量硬做one-hot会丢掉顺序关系。SVR计算样本距离时one-hot对每一维平等对待顺序信息被彻底抹平模型也就学不到“元旦档比普通周末更能拉票房”这层规律。3.3 相关性检查与特征筛选别把无关噪声喂给模型特征做完后别急着开训先做一轮相关性检查把明显重复或强相关的特征去掉。强相关特征在SVR里不会像线性回归那样引发严重共线性问题但会稀释样本在特征空间中的密度让模型在同等样本量下学得更吃力。常用的检查套路是计算皮尔逊相关系数矩阵把相关系数绝对值大于0.9的特征对挑出来。最典型的是首周票房和累计票房二者相关系数常常在0.95以上这时候只保留首周票房作为上映后阶段的代表特征就够了。import pandas as pd import numpy as np def check_correlation(feature_df, threshold0.9): corr feature_df.corr().abs() upper corr.where(np.triu(np.ones(corr.shape), k1).astype(bool)) to_drop [col for col in upper.columns if any(upper[col] threshold)] return to_drop跑完这个函数后要人工过一遍待删列确认重复信息来自哪个业务字段再决定删哪个。“首日票房”和“首周票房”高度相关但业务上首周票房更受口碑影响保留哪个取决于你的预测时点。除了相关系数我还会看每个特征与票房目标的单变量相关性把绝对值低于0.05的特征列去掉。这个操作不是为了提升SVR拟合能力而是为了缩短训练时间、降低过拟合风险。特征分析做完后把最终特征列固定成一个清单文件后续训练和部署都按这份清单读取避免实验阶段反复改特征列导致结果不可比。绘制特征相关性热力图时记得控制横坐标密度票房数据跨度大直接画图会出现横坐标太密集的问题设置plt.xticks(rotation45)和适当步长能缓解。4. SVR回归器的参数选择与训练验证流程4.1 C、epsilon、gammaSVR三个核心参数的直观理解开始跑模型前先确认环境里装好了scikit-learn也就是常见的python安装sklearn库这一步。没装的话一行pip install scikit-learn就搞定装完再import sklearn检查版本。SVR做的事情是在特征空间里找一条回归曲线让大部分样本点落在以该曲线为中心、宽度为2倍epsilon的管道内。管道宽度决定对误差的容忍程度三个参数直接影响最终模型行为必须理解了再调。C是惩罚系数控制对管道外样本的惩罚力度。C越大模型越努力拟合每一个样本点代价是过拟合C太小模型过度平滑连明显的趋势都抓不住。epsilon是管道宽度epsilon越大允许误差越大模型越平缓但会丢失细节模式。gamma只在RBF核下有意义它控制单个样本的影响半径gamma越小决策边界越平滑gamma越大样本点附近的变化越剧烈。票房数据属于典型的中间样本量、噪声偏大的回归问题。经验取值方向是kernel优先用RBFC从10起步epsilon在0.01到0.1之间试gamma不要用默认的auto直接设定为1除以特征数。这个基准组合能保证第一轮训练不会跑偏后续再按验证结果微调。4.2 时序切分验证票房预测不能做随机打散这一类项目里最致命的错误是用train_test_split默认的随机切分来评估模型。票房数据有强烈的时间属性2023年的电影和2024年的电影在特征分布上并不独立。如果把打散后的样本放进验证集模型相当于提前看到了部分未来信息验证分数虚高一部署就原形毕露。正确做法是按时序切分训练集用前80%时间段的电影验证集用后20%时间段的电影并且保证训练集里所有电影的上映日期都早于验证集里最早的上映日期。from sklearn.svm import SVR from sklearn.metrics import mean_absolute_error, r2_score from sklearn.preprocessing import StandardScaler df_sorted df.sort_values(release_date).reset_index(dropTrue) cut_idx int(len(df_sorted) * 0.8) train_df df_sorted.iloc[:cut_idx] test_df df_sorted.iloc[cut_idx:] feature_cols [genres_target, actor1_target, director_target, wish_count, release_days, screen_count] X_train train_df[feature_cols].values y_train train_df[total_box_office].values X_test test_df[feature_cols].values y_test test_df[total_box_office].values scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) model SVR(kernelrbf, C10.0, epsilon0.05, gammascale) model.fit(X_train_scaled, y_train) y_pred model.predict(X_test_scaled) print(MAE: {:.2f} 万元.format(mean_absolute_error(y_test, y_pred))) print(R2: {:.3f}.format(r2_score(y_test, y_pred)))这是整套项目里最重要的一段代码。StandardScaler有个隐蔽的坑只能在训练集上fit在验证集上只做transform。如果对整个数据集先标准化再切分验证集的均值和方差已经包含了测试集信息SVR在这种场景下的表现同样虚高。数据处理里所有“先整体、后切分”的操作都要警惕这条规则同样适用于后面的目标编码。4.3 网格搜索与评估指标怎么找参数怎么确认性能时序切分确定后用GridSearchCV做参数搜索。注意这里的交叉验证也要做时序切分直接使用默认KFold会造成同样的数据泄露。sklearn提供了TimeSeriesSplit返回按时间顺序递增的训练集和验证集索引正好适合票房数据。from sklearn.model_selection import TimeSeriesSplit, GridSearchCV tscv TimeSeriesSplit(n_splits5) param_grid { C: [1, 10, 50, 100], epsilon: [0.01, 0.05, 0.1, 0.5], gamma: [scale, 0.01, 0.1], } gs GridSearchCV( SVR(kernelrbf), param_grid, cvtscv, scoringneg_mean_absolute_error, n_jobs-1, ) gs.fit(X_train_scaled, y_train) print(best params:, gs.best_params_) best_model gs.best_estimator_评估指标别只看R²。R²在票房这种数值范围很大的回归问题上很容易到0.9以上但绝对误差可能仍然高达几百万。我一般同时打印MAE平均绝对误差单位万元和MAPE平均百分比误差。对票房项目来说MAPE在20%以内算可用超过30%说明特征或参数有问题。网格搜索结果不要盲信。GridSearchCV选出的参数是在验证折上平均表现最好的受折数影响明显。真正部署时回头看往往会发现一个更平滑的参数组合反而更稳。比如搜索出来是C100换到C30之后滚动预测时误差波动明显更小。这是SVR项目里最玄学但也最常见的情况——验证集分数高不等于实战表现好。4.4 预测结果落盘与误差分析模型不只是输出一个数模型训练完后我习惯把验证集预测结果和真实值拼成一个DataFrame按误差绝对值排序重点检查误差最大的20条样本长什么样。这一步能快速发现问题误差最大的往往集中于极端票房影片比如大制作特效片票房超出模型预期或者小众文艺片票房远低于热度信号暗示的水平。result_df pd.DataFrame({ movie_id: test_df[movie_id].values, actual: y_test, predict: y_pred, error: np.abs(y_test - y_pred) }).sort_values(error, ascendingFalse) print(result_df.head(20))这一步的价值在于区分系统性偏差和个别异常。如果误差最大的20条全是同档期影片大概率是档期特征没做好如果全是导演或演员知名度极高的片子说明热度类特征需要加权。看完误差分布再回头调特征比直接调SVR参数更有效。5. 避坑指南猫眼票房预测项目里的5个高频翻车点5.1 反爬导致字段静默缺失模型训练直接报错现象详情页请求返回后电影类型、导演、演员字段全部为空请求状态码却是200程序没有抛任何异常后续建模时特征矩阵出现大量NaN。原因猫眼返回了反爬验证页面页面结构和正常详情页完全不同CSS选择器匹配不到内容。最迷惑人的是状态码仍然正常报错被Python静默吞掉了。解决解析详情页前先检查页面里有没有“验证”类文本一旦命中就丢弃该请求并重试。重试时轮换User-Agent加上随机延时代码前面fetch_movie_detail里的if 验证 in resp.text这个分支就是干这件事的。5.2 票房单位“亿”和“万”混用误差凭空放大一个数量级现象模型训练完预测值比真实值高出一个数量级或者MAE大得离谱让整个项目看起来毫无价值。原因爬下来的票房数据一部分是“3.2亿”另一部分是“5800万”没统一单位就丢给SVR。模型把3.2当成3.2元、把5800当成5800元去学两个数量级的数值混在一起SVR的损失函数直接被大数值样本主导。解决清洗阶段强制统一字符串进库前先过parse_box_office输出统一以万元为单位。入库后随机抽查几条确认没有“亿”“万”残留。这一步看着小翻车率极高我见过太多项目在这里耗掉半天调试时间。5.3 随机切分造成时间泄露测试集指标虚高现象用train_test_split默认随机切分验证集MAE很低R²漂亮得像示范案例部署到真实预测后误差暴增。原因随机切分让同一部电影或同一档期的电影同时出现在训练集和测试集。SVR学到了档期和时间热度模式但这些模式在真正的未来数据上并不稳定模型其实偷偷记住了验证集里的时间上下文。解决统一使用按上映日期排序后的时序切分。特征工程里的统计型特征也必须只用训练集计算再把映射应用到测试集这一点在4.2里已经强调过。每次换特征后都检查一遍切分逻辑不要想当然。5.4 上映前和上映后的特征混训模型两头不讨好现象同一套特征和参数预测上映首周票房效果还行预测上映前票房效果极差完全没法在生产环境里用。原因上映前后的特征空间差异很大。上映前只有想看人数、预售、导演历史表现上映后有了首日票房、口碑、评论数。用一套特征矩阵训练一个SVR等于逼模型在两种空间里同时找映射关系结果两边都学不精。解决把预测拆成两个阶段分别训练模型。第一阶段只使用上映前可用特征预测首日或首周票房第二阶段加入累计票房、评分等上映后特征预测最终累计票房。两个模型的特征列不同参数也分开搜索这样每个模型的分布假设才干净。5.5 SVR在小样本上过拟合训练R²高而预测失效现象训练集R²到0.98测试集R²只有0.3模型完全是记忆而不是泛化。原因样本量不到千条时把C调得过大gamma用autoRBF核在高维特征空间里找了条贴着每个样本点的曲线。SVR在高维空间里的表达能力很强样本不够时这种能力就变成了死记硬背。解决缩小C的搜索范围从0.1、1、10试起gamma固定为1除以特征数。同时监控训练集和测试集的分数差距差距过大就降低C或者增大epsilon。宁可让模型看起来“笨一点”也不能让它把训练集背下来。过拟合在票房数据上不是理论问题是每天都在发生的现实问题。6. 让预测结果真正可用滚动预测与误差带模型训练完离“能用的预测系统”还差两步一是随着新数据到来持续更新二是给预测结果一个可信区间否则业务方看见一个具体数字就当真后面错得离谱。滚动预测是我在票房项目里最常用的做法。把上映后数据按周切成窗口每过一周就用最新数据重新训练一次SVR预测下一个周期。如果你的样本量还不够可以只更新特征统计值而不重新训练。这就像写python量化交易策略不能对着同一段历史数据反复调参一样票房预测的回测也要留出真正没参与过选择的未来窗口否则指标全是自欺欺人。误差带的做法是SVR本身输出一个点估计但可以用验证集上的残差分布来构建区间。把验证集每一条样本的预测误差存下来取95%分位数作为波动范围预测结果表达成区间而非单点。业务上区间比单点有用得多制片方更容易接受“预计5到8亿”而不是“7亿”。import numpy as np residuals y_test - y_pred lo -np.percentile(residuals, 97.5) hi np.percentile(residuals, 97.5) pred_new best_model.predict(new_sample_scaled)[0] interval (pred_new lo, pred_new hi) print(预测票房区间: {:.0f} ~ {:.0f} 万元.format(interval[0], interval[1]))误差带发布前要解释清楚含义让使用者理解95%区间不是保证范围而是模型不确定性的量化表达。这个方案落地过程中我最深的教训是不要迷信R²不要迷信GridSearchCV自动挑出的参数更不要在上映前后用同一套特征。每当急着给模型“加料”多塞一列one-hot或者把C调到100测试集的表现反而更差。最后我总会回到最朴素的检查清单数据口径有没有对齐、特征有没有泄露、切分有没有按时间。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?