☰
基于Python爬虫与神经网络的电影票房预测系统实现
2026/10/9 9:52:50 网站建设 项目流程

简介:一套完整的机器学习电影票房预测毕业设计论文,面向需要了解票房预测建模、神经网络应用或撰写相关课题论文的高校学生与研究者。论文以Python爬虫采集中国电影网历史票房数据为起点,系统介绍了数据清洗、特征工程、模型训练到票房预测的完整流程;预测模型融合卷积层、BottleNeck结构、注意力机制及多输入融合策略,并针对导演、演员等基础信息与海报等视觉内容分别设计输入分支,可帮助读者掌握票房预测系统的整体实现思路。压缩包内只有一个doc文档(约1.6MB),内容涵盖中英文摘要、目录、正文、参考文献等毕业设计论文必备模块,结构完整,便于直接查阅或按章节学习。已有236人学习,整体而言,这份论文适合高校学生用于课程设计、毕业设计对照参考,也可作为开发者开展票房预测项目的方案蓝本。

1. 票房预测这事,先别急着上模型

影院排片经理最怕的不是烂片,而是自己拍板重排的片子扑得悄无声息,同期小成本黑马反而场场爆满——这种靠经验的排片决策,本质上是在赌。这份毕业设计论文就是冲着这个痛点来的:用 Python 爬虫把中国电影网的历史票房数据抓下来,走完数据清洗、特征工程、多模型对比、神经网络搭建的完整链路,最终交付一个能对已上映一段时间的影片做票房预测的系统。它适合两类人:一是做毕设或课设、需要一套完整可复现流程的学生,二是想了解票房预测从数据到模型落地的从业者。论文里最值得看的部分不是算法堆砌,而是数据预处理和那个双输入神经网络的设计思路,下面逐一拆开讲。

2. 数据从哪来:爬虫架构与字段设计

2.1 为什么选爬虫而不是手工整理数据集

论文里明确交代了数据来源是中国电影网,用网络爬虫采集而非直接下载现成数据集。这个选型是有现实原因的:公开的电影票房数据集要么滞后严重,要么字段残缺,要么只覆盖北美市场,而国内院线票房数据更新频繁,手工整理根本跟不上。爬虫方案的核心优势在于可更新——系统可以定期增量抓取,保持模型输入的新鲜度。

另一个容易被忽略的点是爬虫抓下来的数据天然带时间戳,这对后面的时间序列验证很有价值。如果你用别人整理好的 CSV,往往丢失了精确的上映日期和抓取日期,模型的评估会失真。论文里的系统设计是把爬虫结果落库,再通过 SQL 或 Pandas 导出训练集,这样数据血缘清晰,出问题能追溯。

我一般会建议在爬虫层就做字段校验,而不是等数据入库后再处理。比如票房字段必须是数字,日期字段必须能解析,这个习惯能帮你省掉后面大量清洗的时间。

2.2 数据字段设计:九个字段对应九个预测因子

从论文的研究框架反推,爬虫需要采集的字段大致如下表:

字段类型说明
电影名称文本主键之一,用于去重
导演类别导演影响力特征
主演类别演员阵容,可展开为阵容强度
类型多标签喜剧/动作/科幻等,可独热编码
上映日期日期决定档期属性
首周票房数值关键特征,反映初期热度
累计票房数值标签列,预测目标
评分数值口碑指标
评论数数值社交热度代理变量

这里有个设计细节值得注意:预测目标是累计票房,但特征中包含首周票房。这意味着模型解决的不是“上映前的预测”,而是“上映一段时间后的走势判断”——论文摘要里明确说了这一点。这个定位让预测问题从纯先验估计变成了时序外推,准确率会显著提升,但也埋了一个坑(后面第 5 章会说)。

爬虫代码的常见写法是分两层:先抓列表页拿到电影详情页链接,再逐个请求详情页提取字段。伪代码如下:

import requests import time from bs4 import BeautifulSoup def fetch_movie_list(page_url): """请求列表页,返回本页电影详情页链接列表""" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "Referer": "http://www.chinafilm.com/" } resp = requests.get(page_url, headers=headers, timeout=10) soup = BeautifulSoup(resp.text, "html.parser") links = [] for a in soup.select("div.movie-list a[href]"): links.append(a["href"]) return links def fetch_movie_detail(detail_url): """请求详情页,返回结构化字段字典""" payload = { "title": None, "director": None, "actors": None, "genre": [], "release_date": None, "first_week_boxoffice": None, "total_boxoffice": None } # 解析逻辑省略,核心是用 BeautifulSoup 按 DOM 结构定位 time.sleep(0.5) # 限速,避免给对方服务器造成压力 return payload # 主流程:遍历列表页 → 抓详情 → 存 CSV all_movies = [] for page in range(1, 20): list_page = f"http://www.chinafilm.com/movies?page={page}" detail_links = fetch_movie_list(list_page) for link in detail_links: movie = fetch_movie_detail(link) all_movies.append(movie) time.sleep(1)

代码里最关键的参数是time.sleep(0.5)和time.sleep(1),这两个限速区间是我试出来的相对安全值。过快会被反爬机制封 IP,过慢则抓取效率太低。headers里的User-Agent和Referer是模拟浏览器行为的基础配置,缺失这两项很多站点会直接拒绝响应。

3. 数据预处理:清洗策略与特征工程

3.1 缺失值、异常值与量纲问题的处理顺序

论文里多次提到预处理的重要性,但没给具体处理顺序,我这里按实操经验补全。拿到原始数据后的标准顺序是:先处理缺失值,再处理异常值,最后做量纲统一。顺序不能反,因为很多异常值检测算法对量纲敏感,先归一化再检测会导致异常被“洗掉”。

缺失值处理上,数值型字段我用中位数填充,类别型字段用众数填充。为什么不用均值?因为票房数据的分布是长尾的,少数爆款会把均值拉得很高,用均值填充会让普通影片的缺失值全部偏向爆款水平,这属于典型的引入偏差。

异常值方面,票房数据里经常出现极端值——比如一部影片首周票房异常高或者异常低。常见的处理方式不是直接删除,而是用上下限截断(Winsorize),把超出 P99 和 P1 的值压到边界处。这样既保留了数据分布的形状,又不会让极端值过度影响回归系数。

量纲统一论文里明确提到了去均值和归一化。去均值是让特征以原点为中心,归一化是把幅度压缩到统一范围。实际操作我用StandardScaler或MinMaxScaler看情况,树模型对量纲不敏感,但 SVM 和神经网络对量纲高度敏感,scaling 必须做。

import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler, LabelEncoder df = pd.read_csv("movies_raw.csv") # 1. 缺失值:数值列用中位数,类别列用众数 num_cols = df.select_dtypes(include=[np.number]).columns cat_cols = df.select_dtypes(include=[object]).columns df[num_cols] = df[num_cols].fillna(df[num_cols].median()) df[cat_cols] = df[cat_cols].fillna(df[cat_cols].mode().iloc[0]) # 2. 票房列做 log1p 变换,压缩长尾 df["total_boxoffice_log"] = np.log1p(df["total_boxoffice"]) # 3. 类别字段编码:类型字段用独热,导演字段用频数编码 genre_dummies = pd.get_dummies(df["genre"], prefix="genre") director_freq = df["director"].value_counts().to_dict() df["director_freq"] = df["director"].map(director_freq) # 4. 数值特征标准化 scaler = StandardScaler() feature_cols = ["first_week_boxoffice_log", "rating", "comment_count", "director_freq"] df_scaled = scaler.fit_transform(df[feature_cols])

np.log1p是这个代码块里最值得解释的一行。票房数据跨度极大,从几十万到几十亿,如果不取对数,模型会过度关注大票房的样本,小成本影片的误差完全被淹没。取对数之后,预测目标从“预测绝对票房”变成“预测票房的量级”,这在回归任务里是常规操作,评估指标也该在 log 空间算。director_freq用的是频数编码——导演的出现次数越多,说明该导演越活跃或越有资源,比直接独热编码几百个导演列高效得多。

3.2 特征相关性检验:避免把标签泄漏带进模型

预处理阶段还有一步容易被跳过的操作:特征相关性检验。特别是要检查首周票房与累计票房之间的相关性,这两列在数学上高度相关,但如果你的预测目标是累计票房,而特征里已有首周票房,模型很容易退化成“首周票房乘以某个系数”的线性外推。这不算错误,但会让模型变得没有解释力。

我一般会先计算相关系数矩阵,再决定是否保留某些特征。如果两个特征的相关系数超过 0.9,考虑只保留其中一个,或者做一个降维合并。论文里提到了 PCA 降维的选项,也确实用在了部分特征上,但 PCA 的缺点是降维后的主成分不可解释,调试模型时看不出哪个因子起了作用。我的做法是:先做相关性筛选,再做 PCA 兜底。

另一个值得做的预处理是档期特征构造。上映日期不能直接作为数值输入,我通常把它拆成“月份”和“是否节假日档期”两个特征。暑期档、国庆档、春节档的票房量级完全不是一个层次,不拆的话模型会糊涂。

4. 算法选型:从线性回归到神经网络的梯度对比

4.1 五个候选模型各自适合什么场景

论文第 4 章列了五个算法:回归分析、支持向量机、决策树、随机森林、神经网络。下面按我的实际使用体验逐一说清楚。

线性回归是最容易上手的基线模型,可解释性最好。它的核心假设是特征与目标之间是线性关系,但票房数据的非线性特征非常明显——口碑对票房的影响存在阈值效应,评分从 7.5 到 8.5 的票房差异远大于从 6.5 到 7.5,线性模型根本无法捕捉这种非线性。所以线性回归在这里的价值是当基线,给后续复杂模型提供一个参照点。

SVM(支持向量机)在样本量不大时效果很好,特别是 RBF 核能拟合比较复杂的决策边界。但它的缺点也突出:对特征缩放非常敏感,参数调优成本高(C和gamma的搜索空间很大),而且在样本量过万之后训练速度会严重下降。论文里用的票房数据规模在几千到一万条这个区间,SVM 还扛得住,再大就不建议了。

决策树可解释性强、不需要特征缩放,但单棵树的方差很大,容易过拟合,深度稍微调深一点训练集 R² 虚高、测试集惨不忍睹。随机森林作为决策树的 Bagging 集成,方差问题被显著缓解,是“默认先跑一把看效果”的首选模型。它不用做特征缩放、能处理非线性、还能输出特征重要性——这在论文的“影响因素分析”章节里会被直接用到。

神经网络是这几个模型里唯一能自动做特征交互的。票房的预测因子之间是高度耦合的,比如“导演影响力 × 档期 × 类型”三方交互,树模型需要靠深度来强行拟合这种交互,而神经网络天然具备这种拟合能力。代价就是数据量要求高,调参玄学,训练时间长。

4.2 基线对比实验的实操脚本

实际做对比实验时,我不会一上来就调参,而是先用默认参数跑一遍,把五个模型的误差摆在一起看。下面这段代码就是标准的对比脚本:

from sklearn.linear_model import LinearRegression from sklearn.svm import SVR from sklearn.tree import DecisionTreeRegressor from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error, r2_score X = df_scaled y = df["total_boxoffice_log"].values X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) models = { "LinearRegression": LinearRegression(), "SVR_linear": SVR(kernel="linear", C=1.0), "SVR_rbf": SVR(kernel="rbf", C=1.0, gamma="scale"), "DecisionTree": DecisionTreeRegressor(max_depth=8, random_state=42), "RandomForest": RandomForestRegressor( n_estimators=200, max_depth=12, random_state=42 ), } for name, model in models.items(): model.fit(X_train, y_train) pred = model.predict(X_test) rmse = mean_squared_error(y_test, pred, squared=False) r2 = r2_score(y_test, pred) print(f"{name:<16} RMSE={rmse:.4f} R2={r2:.4f}")

参数选择的依据:SVR 我给了C=1.0和gamma="scale",这是多数场景下的稳妥起点;C太小会欠拟合,太大容易过拟合。决策树和随机森林的max_depth分别是 8 和 12,太深会过拟合到训练集的噪声上。random_state=42固定下来,保证对比实验可复现。

跑完这段代码你大概率会发现:随机森林的 RMSE 最低,决策树的 R² 方差大,SVM 表现取决于是否做特征缩放。这时候自然就要问了——随机森林已经这么强了,为什么论文还要上神经网络?答案在前面说过:树模型拟合的是特征间的规则组合,而神经网络能拟合高维连续的交互函数。如果只是做预测,随机森林可能够用;但如果要做“影响因素分析”和泛化能力验证,神经网络的上限更高。

5. 双输入神经网络与排坑实战

5.1 模型结构还原:卷积层 + BottleNeck + 多输入融合

论文摘要里提到的双输入神经网络是这份资源里技术含量最高的部分。我把模型结构还原成可执行的 Keras 代码,先看整体设计:模型接收两个输入源,Input1 是电影基本信息(导演、演员、类型、档期等结构化特征),Input2 是视觉内容(海报、预告片等图像特征)。两个输入不合并,而是先各自走独立的特征提取分支,最后再融合——这是多模态学习的标准架构。

Input1 分支的关键操作是先过最大池化(Maxpool),论文里没有展开原因,我解释一下:结构化特征往往带有明显的“是否”语义,比如是否续集、是否大导演,最大池化能强化这类显著性特征。Input2 分支用平均池化(Avgpool),图像特征需要保留整体分布信息,平均池化更合适。

Input1 分支的骨干网络是 3×3 卷积 + 14 层 BottleNeck 堆叠,这个结构直接借鉴了 MobileNet v2 的设计思路。BottleNeck 的核心是一个 1×1 卷积降维 → 3×3 卷积 → 1×1 卷积升维的倒残差结构:

import tensorflow as tf from tensorflow.keras import layers, Model def bottle_neck_block(x, filters, expansion=6, stride=1): """BottleNeck 结构:1x1 升维 -> 3x3 深度卷积 -> 1x1 降维""" identity = x # 第一层 1x1 卷积升维 x = layers.Conv2D(filters * expansion, 1, padding="same")(x) x = layers.BatchNormalization()(x) x = layers.ReLU(6.0)(x) # ReLU6 激活,与论文一致 # 第二层 3x3 深度可分离卷积 x = layers.DepthwiseConv2D(3, strides=stride, padding="same")(x) x = layers.BatchNormalization()(x) x = layers.ReLU(6.0)(x) # 第三层 1x1 卷积降维回原通道数 x = layers.Conv2D(filters, 1, padding="same")(x) x = layers.BatchNormalization()(x) if stride == 1 and x.shape[-1] == identity.shape[-1]: x = layers.add([x, identity]) # 残差连接 return x def build_dual_input_model(input1_dim, input2_shape): # Input1: 结构化特征分支 inp1 = layers.Input(shape=(input1_dim,), name="Input1") x1 = layers.Reshape((1, 1, input1_dim))(inp1) x1 = layers.Conv2D(32, 3, padding="same", activation="relu")(x1) x1 = layers.MaxPool2D(pool_size=(1, 1))(x1) for i in range(14): # 14 层 BottleNeck 堆叠 x1 = bottle_neck_block(x1, filters=32 if i < 7 else 64) x1 = layers.GlobalAveragePooling2D()(x1) # Input2: 视觉特征分支 inp2 = layers.Input(shape=input2_shape, name="Input2") x2 = layers.Conv2D(32, 3, padding="same", activation="relu")(inp2) x2 = layers.AvgPool2D(pool_size=(2, 2))(x2) # 注意力机制:SE 模块,给通道加权 se = layers.GlobalAveragePooling2D()(x2) se = layers.Dense(32 // 4, activation="relu")(se) se = layers.Dense(32, activation="sigmoid")(se) se = layers.Reshape((1, 1, 32))(se) x2 = layers.multiply([x2, se]) x2 = layers.GlobalAveragePooling2D()(x2) # 多输入融合 concat = layers.concatenate([x1, x2]) out = layers.Dense(128, activation="relu")(concat) out = layers.Dropout(0.3)(out) out = layers.Dense(1, activation="linear", name="boxoffice_pred")(out) model = Model(inputs=[inp1, inp2], outputs=out) return model model = build_dual_input_model(input1_dim=12, input2_shape=(64, 64, 3)) model.compile(optimizer="adam", loss="mse", metrics=["mae"])

代码里的关键参数:expansion=6是 MobileNet v2 的推荐值,filters=32到64的递增控制了模型容量,ReLU6激活函数是论文里明确提过的,它在数值上把输出钳制在 0 到 6 之间,量化部署时更友好。SE 模块(Squeeze-and-Excitation)是注意力机制的一种,通过全局平均池化生成通道权重,再对每层特征图做加权,让模型自己决定更关注哪些通道。

训练这个模型时要注意,Input1 的结构化特征维度不能太大,我一般控制在 10-20 维。如果做过独热编码导致维度爆炸,先用 PCA 压到 12 维再喂进去。Input2 的图像需要统一缩放到 64×64 大小,太大的分辨率会拖慢训练,太小的会丢失细节信息。

5.2 避坑记录:从数据到实现的五个常见问题

这篇论文的资源里包含完整代码,但代码能跑通不代表结果可用。以下五条是我复现这类项目时踩过的坑,每条都按现象、原因、解决三段式说明。

现象一:爬虫抓了几百条数据后就再也抓不到了,返回 403 或验证码。原因:请求频率过高,触发服务器反爬机制。很多站点对单个 IP 的请求频率有限制,连续请求几十次后就会被识别为异常流量。 解决:在两次请求之间加随机延时,time.sleep(random.uniform(0.5, 1.5)),并随机轮换 User-Agent 池。最稳妥的办法是开启抓取重试机制,遇到 403 时退避一段时间再继续。论文里没有写反爬策略细节,如果你自己跑,务必加限速。

现象二:首周票房和累计票房同时作为特征,模型 R² 高达 0.95,但实际做预测时完全不可用。原因:特征泄漏。首周票房与累计票房高度相关,模型学会了用首周票房直接推累计票房,而这个关系是天然的、规则性的,模型没学任何真正的市场规律。 解决:要么把预测目标改为“上映 N 周后的票房增量”,要么把首周票房从特征中移除。论文里定位是“对上映一段时间后的影片进行票房预测”,首周数据可以用,但必须清楚这会压缩模型的意义——它更像走势预测而非票房预估。

现象三:原始数据里电影名称出现重复,导致同一部电影的票房被累计计算,训练集里出现大量重复的“高票房”样本。原因:爬虫脚本里没有加去重逻辑,重复请求详情页会重复写入。 解决:入库时以电影名称为主键做唯一约束,或使用 Pandas 的drop_duplicates()按名称去重。这个坑很少有人提前注意,因为它不影响代码运行,只会悄悄污染训练数据。

现象四:不同类型电影的票房相差太大,模型在测试集上的 RMSE 看着还行,但所有影片的预测值都偏向中位数。原因:没有做 log1p 变换或变换粒度不够。模型为了降低整体损失,会倾向于输出一个折中的票房值,导致爆款被严重低估。 解决:标签列必须做 log 变换,评估时也统一在 log 空间算 RMSE。论文里明确做了预处理,但你要确保变换全部做完,并保留反变换的脚本,预测完要还原回真实票房数值。

现象五:双输入模型训练时,Input1 分支和 Input2 分支的 loss 下降速度不一致,最终整体 loss 不收敛。原因:两个输入的量纲差异太大。结构化特征的数值范围在 0-1,而图像分支的像素值在 0-255,反向传播时梯度被像素分支主导。 解决:对两个分支设置不同的学习率。可以用model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.001))做全局学习率,同时保证 Input2 分支的输入图像先做归一化x2 = x2 / 255.0。这个处理必须在数据管道里完成,不能在模型层面偷懒。

6. 验证模型的最后一步:按时间切分而不是随机切分

很多复现者拿到这份资源后,直接用train_test_split随机切分数据集,跑完就宣称模型可用。这是票房预测项目里最大的一个方法论陷阱——随机切分会导致时间穿越(用未来的数据训练,再评估过去的样本)。电影票房的分布随年份变化,疫情前后、档期规则调整都会导致分布偏移,正确的做法是按时间顺序切分。

我一般会把数据按上映日期升序排列,取前 80% 做训练、后 20% 做验证,而不是随机抽样。这段代码就是标准的时序切分:

df_sorted = df.sort_values("release_date", ascending=True) split_idx = int(len(df_sorted) * 0.8) train = df_sorted.iloc[:split_idx] test = df_sorted.iloc[split_idx:] X_train_ts = train[feature_cols].values y_train_ts = train["total_boxoffice_log"].values X_test_ts = test[feature_cols].values y_test_ts = test["total_boxoffice_log"].values model_ts = RandomForestRegressor(n_estimators=200, max_depth=12, random_state=42) model_ts.fit(X_train_ts, y_train_ts) pred_ts = model_ts.predict(X_test_ts) rmse_ts = mean_squared_error(y_test_ts, pred_ts, squared=False) r2_ts = r2_score(y_test_ts, pred_ts) print(f"时间序列切分: RMSE={rmse_ts:.4f}, R2={r2_ts:.4f}")

跑完这段代码,R² 大概率会比随机切分时低一截——这是正常的。随机切分的 R² 是虚高的,因为它让模型“偷看”了未来数据。真正的预测场景里,你只能看到历史数据去预测未来,所以时间序列切分的指标才有参考价值。

评估指标的选择上,我建议同时看三个:RMSE 反映绝对误差的大小,R² 反映模型对目标波动的拟合程度,MAE 反映中位数误差。如果 RMSE 远大于 MAE,说明模型在少数极端值上误差很大——这通常就是爆款影片预测不准。做影响因素分析时,随机森林的feature_importances_属性可以直接输出每个特征的贡献排序,我一般会跑完模型后先看这个,再决定是否需要调整特征集合。

还有一个习惯我一直保留:每次跑完实验,把切分方式、特征集合、模型参数、评估指标完整地记录一次。这套论文资源里已经包含了完整流程,但复现时改一版就记录一版。从那以后我每次做这类预测项目,第一件事都是先看数据的时间跨度再决定验证方式,这个习惯帮我避免过不少虚高指标的误导。希望这份拆解能让你少踩几个坑,资源本身值得下载跑一遍——亲手复现一次,比读十遍论文都有用。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询