AI票房预测实战:从爬虫到随机森林的完整数据分析链路 简介一套面向机器学习入门者与数据分析爱好者的电影票房实战项目包基于1984-2024年票房数据覆盖从数据采集、清洗、EDA可视化和特征工程到随机森林回归预测的完整流程可帮助读者快速掌握用Python开展真实数据项目的方法。压缩包共8个文件包含6个可直接运行的.py源代码、1份约289 KB的CSV数据集和1个说明文档总大小134KB代码涉及pandas、seaborn、plotly、scikit-learn等常用库另附基于requests与BeautifulSoup编写的网页票房数据爬取脚本。资源已有106人学习下载胜在环节完整、组织结构清晰特别适合希望模仿完整项目套路、理解票房影响因素并尝试调参与预测的读者。1. AI 实战四十年电影票房数据六段代码一条能跑通的预测链路做票房预测这件事大家总觉得重头戏在模型真上手才发现四十年数据从采集、清洗到特征工程才是吃掉大部分时间的地方。这份 AI 实战资源把整条链路拆成了 6 个源代码文件配一份 1984 到 2024 年的票房数据集从网页采集、EDA 可视化到随机森林预测每个环节都有独立脚本可以直接复现。我拆完一遍的感觉是如果你是机器学习初学者、正在做课程设计或想用真实数据练手这份资源比较合适——它不追求算法多前沿而是把“数据分析师拿到票房数据后会怎么做”这件事完整走了一遍。2. 数据通道怎么搭requestsbs4 爬虫脚本与 CSV 文件的取舍2.1 boxoffice_data_2024.csv 里到底有什么解压之后readme.txt 在最前面建议先看它。它把 6 个 py 文件的分工说明白了两个是做探索性分析和趋势洞察的一个是爬虫另外三个围绕建模预测展开。数据集只有一个文件boxoffice_data_2024.csv288.93 KB记录的是 1984 到 2024 年间的电影票房数据。这个体量不算大加载速度很快比较适合用来理解完整的数据分析流程而不至于像 ImageNet 那类数据集一样光是预处理就要写一堆分布式逻辑。文件定位6-Box Office 1984 2024 EDA.py探索性数据分析入口4-Box Office Analysis Trends and Insights 19842024.py趋势与洞察可视化5-Web Scraping Box Office Data.pyrequests bs4 网页爬虫3-AI project.py模型训练与预测主脚本1-Project VI.py / 2-Project V.py建模流程辅助脚本boxoffice_data_2024.csv1984-2024 票房数据集readme.txt文件说明与运行顺序我一般会先用 pandas 读一遍列名和类型确认每个字段是什么含义再做后续处理。从脚本的读取逻辑看字段大致包含年份、片名、票房、类型、发行公司这几类核心信息时间跨度四十多年单位、缺失值、异常年份这类问题一定存在。这反而是好事——数据太干净你反而练不到清洗和判断的功夫。注意读取 CSV 时先别急着打印结果先用 df.dtypes 看一眼每列的类型很多后面排查半天的 bug 根源都在这。2.2 爬虫脚本的常见实现requests bs4 的解析套路5-Web Scraping Box Office Data.py 用的是 requests 和 BeautifulSoup 这一套经典组合。这个组合的定位是“轻量级静态页面抓取”requests 负责把网页拉下来BeautifulSoup 负责把 HTML 解析成 Python 对象再用选择器提取表格行和单元格。它的优点是依赖少、容易调试不用像 Selenium 那样起浏览器缺点是遇到动态渲染页面会抓不到内容这点后面避坑部分会细说。常见的实现思路是这样的import requests from bs4 import BeautifulSoup import pandas as pd import time HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } def fetch_table_rows(url): resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() # 状态码不是 200 时直接抛异常 soup BeautifulSoup(resp.text, html.parser) rows [] for tr in soup.select(table tbody tr): # 按表格行选择器定位 cells [td.get_text(stripTrue) for td in tr.find_all(td)] if cells: rows.append(cells) return rows all_rows [] for page in range(1, 6): rows fetch_table_rows(fhttps://example.com/boxoffice?page{page}) all_rows.extend(rows) time.sleep(1.5) # 请求间隔避免高频访问 df pd.DataFrame(all_rows) df.to_csv(boxoffice_scraped.csv, indexFalse, encodingutf-8-sig)逻辑说明脚本先给请求头加上 User-Agent模拟浏览器访问BeautifulSoup 用 select 定位表格里的数据行get_text(stripTrue) 去掉单元格文本里的换行和空格逐页抓取时用时间间隔控制请求频率。这里的核心是把「网络请求」和「解析提取」拆成两个独立环节哪个出问题都能单独排查。参数说明timeout10 是等待响应的最大秒数超过就放弃sleep(1.5) 是每次翻页之间的间隔具体值要按目标网站的承载能力调整不一定越大越好但太小容易被封。如果你抓的目标页面结构和这个示例不完全一样需要改的是 soup.select 括号里的选择器这是这类脚本最常见要动的参数。2.3 直接用 CSV 还是先跑爬虫两条通道的关系资源里同时保留了数据集和爬虫脚本有人会觉得多此一举其实这两条通道各有用途。单个场景下的分析直接用 CSV 就够了读进来就是干净的结构化表格省掉网络请求的不确定性爬虫脚本的价值在于复现数据更新流程——比如你想把数据更新到 2025 年或者换一个票房数据源重新抓一遍脚本就是你的操作模板改掉 URL 和选择器就能跑。我的习惯是优先用现成 CSV 把主流程跑通确认模型不再报错之后再回头改爬虫脚本去抓新数据。这样能避免“爬虫反爬报错”和“模型报错”两个问题纠缠在一起排查起来会少掉许多误会。readme.txt 里对文件关系的说明建议当成项目地图用而不是看一遍就丢。一个额外的判断标准如果你只跑一次分析用现成 CSV 是效率最高的如果你要做滚动更新比如每个月追加一批数据那爬虫脚本就必须维护好。资源把这个选择权同时给你其实是给了两条从数据到模型的可选路径怎么走取决于你手头任务的时效性要求。3. 四十年票房拆解EDA 脚本里的趋势、异常与可视化逻辑3.1 时间趋势线1984-2024 的票房体量变化6 号脚本6-Box Office 1984 2024 EDA.py是标准的探索性分析入口。EDA 的目的不是直接建模而是先回答三个问题整体趋势是什么样、分布有什么异常、哪些变量值得进入模型。对票房数据来说第一条一定要看时间维度——1984 到 2024 四十年间市场体量、票价水平、影片数量都在变不先看清这个背景后面所有分析都容易失真。用 plotly.express 画年度总票房趋势是比较直接的方式import pandas as pd import plotly.express as px df pd.read_csv(boxoffice_data_2024.csv, encodingutf-8) df[year] pd.to_datetime(df[release_date], errorscoerce).dt.year yearly df.groupby(year, as_indexFalse)[box_office].sum() yearly yearly.dropna(subset[box_office]) fig px.line(yearly, xyear, ybox_office, markersTrue, title1984-2024 年度票房总量趋势) fig.show()逻辑说明pd.to_datetime 把发布日期列统一转成时间类型errorscoerce 让无法解析的日期变成 NaT而不是直接报错中断dt.year 提取年份后 groupby 按年聚合求和。这里最关键的细节是 dropna——如果数据里存在缺失票房记录不删除的话会出现某一年趋势线骤降看起来像市场崩了其实是缺失值捣乱。参数说明encodingutf-8 对应 CSV 的保存编码如果 readme 里说明文件是其他编码比如 gbk这里就要跟着改markersTrue 是为了在趋势线上标记出各年份的数据点方便观察单点异常。3.2 分布视图老牌 plotly 写法与新写法的兼容问题4 号脚本4-Box Office Analysis Trends and Insights 19842024.py在 EDA 的基础上往洞察方向走了一步。它用到了 seaborn、matplotlib.pyplot 和 plotly 全家桶三个可视化库混着用这个选择本身有价值plotly 适合交互式探索鼠标悬停能看具体数值seaborn 和 matplotlib 适合出静态图表写报告、做 PPT 时更可控。同一个图表用两种方式各画一遍数据分析里是很常见的工作习惯。资源里出现了 plotly.graph_objs、plotly.offline.iplot 和 plotly.express 三类写法前两者是 Notebook 时代的老接口后者是现在的标准接口。老接口的典型写法是这样import plotly.graph_objs as go from plotly.offline import iplot trace go.Scatter(xyearly[year], yyearly[box_office], modelinesmarkers) layout go.Layout(title1984-2024 Box Office Trend) fig go.Figure(data[trace], layoutlayout) iplot(fig) # 旧版 Jupyter 渲染方式新版 plotly 中可能失效逻辑说明先用 graph_objs 构造 trace 和 layout再把它们组装成 Figure最后通过 iplot 渲染到 Notebook 里。这套思路在新版 plotly 里依然成立只是渲染入口变了——用 fig.show() 替代 iplot 即可。除了交互式图表静态分布图还是要交给 seaborn 和 matplotlibimport matplotlib.pyplot as plt import seaborn as sns fig, ax plt.subplots(1, 2, figsize(14, 5)) sns.histplot(df[box_office], bins50, axax[0]) ax[0].set_title(Box Office Distribution) ax[0].set_xlabel(Box Office ($)) top10 df.nlargest(10, box_office) sns.barplot(datatop10, xbox_office, ytitle, axax[1]) ax[1].set_title(Top 10 Movies by Box Office) plt.tight_layout() plt.show()逻辑说明左侧直方图看整体分布是否偏态票房数据基本都是严重右偏的——少数大片占掉大部分票房做模型时这个偏态分布会影响损失函数的表现后面预测章节会提到用 log 变换缓解右侧 nlargest(10, box_office) 取出票房前十的影片看头部效应有多明显。这里的 ytitle 是电影片名如果实际列名不同比如叫 movie_name要先 rename。参数说明bins50 控制直方图分箱数量箱数太少会看不清分布细节太多又会被噪声淹没50 对几千条记录是比较稳妥的起点figsize 控制整张图的宽高写报告时可以按页面宽度调整。3.3 异常年份与单位陷阱EDA 最容易翻车的地方看趋势图时如果发现某一年票房突然降到接近零或者某部电影票房数值大得离谱大概率不是市场出了什么问题而是数据本身有坑。这类数据里最常见的坑有三个单位不统一有的字段按美元记有的按人民币记甚至同一列里混着“万”和“亿”日期解析失败导致年份变成 NaN整年数据被 groupby 丢掉还有少数影片的票房被填成 0表示缺失但 0 和真实低票房在数值上一样很容易被模型当成有效样本。处理方式上我一般分两步走。第一步是数据层面把单位统一后再做聚合0 值根据上下文决定是剔除还是作为缺失值填补第二步是分析层面在 EDA 报告里单独标出异常年份和异常影片让看报告的人知道这些点是数据问题不是业务规律。这个习惯能帮你省掉后面建模阶段大量无意义的排查。EDA 阶段还有一个容易被忽略的动作把年份、票房、类型这些核心字段的缺失率列出来。如果某一列的缺失率超过 20%就要考虑是否值得保留如果缺失集中在某几年更要警惕那几年的数据整体质量。把这些结论写在分析笔记里建模阶段就能直接参照而不是等模型训练完再回头找原因。4. 预测模型怎么建从特征编码到随机森林回归4.1 为什么选随机森林而不是线性回归资源里的 1 号、2 号、3 号三个脚本都围绕建模预测展开核心模型用的是 sklearn.ensemble.RandomForestRegressor。选型理由很实际票房数据特征以类别变量为主——电影类型、发行公司、上映档期这些变量和票房之间的关系是非线性的线性回归会因为特征之间的共线性和非线性关系而表现不稳定随机森林是集成树模型对非线性关系、特征之间的交互作用都有不错的容忍度对异常值和缺失值也不敏感。对于课程设计和入门级项目这个选型是一个比较稳妥的基线。另外一点是随机森林不需要做特征缩放。线性模型、神经网络对特征的量纲很敏感需要标准化树模型是按特征值做切分的票房数字是十万还是十亿对树的切分点选择没有本质影响。这意味着数据预处理阶段可以少做一些标准化工作把精力放在缺失值填充和类别编码上。4.2 特征工程管线用 ColumnTransformer 把预处理和模型接起来建模最忌讳把预处理和模型分开写、手动执行——一旦数据换了一批预处理步骤很容易被漏掉。资源里用 sklearn.compose.ColumnTransformer 和 sklearn.pipeline.Pipeline 把这些环节串成一条链路这个做法值得单独提一下。ColumnTransformer 允许对不同列应用不同变换Pipeline 又把这些变换和最终模型打包成一个整体。这样你在预测时只需要调用 pipeline.predict()不需要记得中间做过多少次编码和填充。import pandas as pd from sklearn.compose import ColumnTransformer from sklearn.impute import SimpleImputer from sklearn.pipeline import Pipeline from sklearn.preprocessing import OneHotEncoder from sklearn.ensemble import RandomForestRegressor categorical_cols [genre, distributor] numeric_cols [budget, screens] preprocessor ColumnTransformer( transformers[ (num, SimpleImputer(strategymedian), numeric_cols), (cat, OneHotEncoder(handle_unknownignore), categorical_cols), ] ) pipeline Pipeline(steps[ (preprocessor, preprocessor), (model, RandomForestRegressor(n_estimators300, random_state42)), ])逻辑说明ColumnTransformer 里定义了两路变换数值列走 SimpleImputer用中位数填充缺失值中位数比均值更适合有异常值的数据类别列走 OneHotEncoder把电影类型和发行公司这类文本变量展开成 0/1 矩阵。Pipeline 的妙处在于后续做 GridSearchCV 时可以直接用 model__n_estimators 这样的参数路径去访问树模型的超参数算法会自动在整个预处理加建模链路上做交叉验证。参数说明handle_unknownignore 必须加——测试集里如果出现训练集没见过的电影类型OneHotEncoder 默认会报错ignore 会让它自动忽略而不是中断。n_estimators300 是森林里的树数量经验上 200 到 500 之间收益递减明显300 是一个性能和耗时的折中。4.3 训练测试切分时间序列数据别用随机切分train_test_split 是 sklearn.model_selection 里的标准工具但它默认随机打乱数据这对票房这种带时间顺序的数据是个隐患。随机切分会把 2010 年的数据同时放进训练集和测试集模型等于提前“见过”了同一时期的同类影片测试集上的误差会虚低一旦拿到真实未来数据去预测效果会明显缩水。常见做法是按时间顺序切分前 80% 做训练后 20% 做测试。from sklearn.model_selection import train_test_split df_sorted df.sort_values(year).reset_index(dropTrue) split_idx int(len(df_sorted) * 0.8) train df_sorted.iloc[:split_idx] test df_sorted.iloc[split_idx:] feature_cols [year, genre, distributor, budget, screens] X_train train[feature_cols] y_train train[box_office] X_test test[feature_cols] y_test test[box_office] pipeline.fit(X_train, y_train) y_pred pipeline.predict(X_test)逻辑说明sort_values(year) 先把数据按年份排序reset_index(dropTrue) 重置索引避免 iloc 切分时行号错位。split_idx 是训练集和测试集的切分点前 80% 是历史数据后 20% 模拟“未来”。这种切分的意义是让评估结果更接近真实预测场景——你不可能用未来的票房去训练现在的模型。参数说明feature_cols 里的具体列要和前面 ColumnTransformer 的 categorical_cols、numeric_cols 对应起来比如这里有 genre管线里就得包含对应的类别编码处理如果特征列和管线不匹配Pipeline 的 fit 阶段就会直接报 KeyError这也是新手最容易卡住的地方。4.4 GridSearchCV 调参参数网格和评估指标怎么定资源里的 3 号脚本用到了 sklearn.model_selection.GridSearchCV这是随机森林调参的标准做法。网格搜索的要点是别一上来就把网格设得太大——随机森林的超参数组合是乘法关系n_estimators 取 4 个值、max_depth 取 4 个值、min_samples_leaf 再取 3 个值组合数就是 48 组每组还要跑 5 折交叉验证一共 240 次训练。数据集不算大但时间也会拖得很难受。from sklearn.model_selection import GridSearchCV from sklearn.metrics import mean_absolute_error, mean_squared_error param_grid { model__n_estimators: [200, 300], model__max_depth: [None, 20, 30], model__min_samples_leaf: [1, 2, 4], } grid GridSearchCV( pipeline, param_grid, cv3, scoringneg_mean_absolute_error, n_jobs-1, ) grid.fit(X_train, y_train) print(best params:, grid.best_params_) print(MAE:, mean_absolute_error(y_test, grid.predict(X_test))) print(RMSE:, mean_squared_error(y_test, grid.predict(X_test), squaredFalse))逻辑说明param_grid 的 key 带有 model__ 前缀这是 Pipeline 的命名规范——model 是 Pipeline 第二步的名字两个下划线后面才是 RandomForestRegressor 真正的参数名。scoring 选了负平均绝对误差负数是因为 sklearn 的评分规则是“越大越好”所以把误差取负号。n_jobs-1 表示用所有 CPU 核心并行跑这是提速的最直接参数。参数说明cv3 用的是 3 折交叉验证而不是默认的 5 折数据量不是特别大的情况下 3 折能明显节省时间且和随机切分一样交叉验证的误差不见得比按时间切分更有实际参考意义所以最终评估还是要看时间切分出来的测试集结果。mean_squared_error 里 squaredFalse 是 sklearn 新版本返回 RMSE 的写法旧版本直接用 mean_squared_error 后开根号不同版本差别很大跑之前确认一下你的 sklearn 版本。5. 避坑手册这份资源最容易翻车的五个现场5.1 现象plotly.offline.iplot 在 Jupyter 里报错原因资源里用到了 plotly.offline.iplot 这种旧式写法它是 plotly 3.x/4.x 时代配合 Jupyter Notebook 的笔记本渲染接口。新版 plotly 5.x 之后官方推荐直接使用 plotly.express 或 graph_objects 的 fig.show()iplot 相关的兼容接口在新环境里经常会失效报错信息一般是 module 找不到或者方法不存在。解决把 iplot 调用全部替换成 plotly.express 的可视化函数。如果只是在本地跑直接用 fig.show() 就会在浏览器里打开交互图。如果你确实需要旧版行为也可以 pip install plotly5.0 降级但我不推荐——旧版对新版 pandas 的兼容会有新坑不如花十分钟改代码。5.2 现象按年份筛选数据时筛选结果总是空原因read_csv 默认会把纯数字列推断成 int64 或 float64如果你的年份列在 CSV 里带了引号或者空格pandas 解析后类型会混乱。还有一种情况是日期列被解析成了 datetime但你用整数去比较类型对不上导致匹配失败。解决读取时显式指定类型用 dtype{year: str} 或 parse_dates[release_date]。养成习惯是读完先跑一行 df.dtypes确认每一列类型和你预期一致再往下写。这个检查花十秒钟能省掉后面查找错误的好几个小时。5.3 现象GridSearchCV 跑了半小时还没结束原因参数网格组合太多且没有启用并行。上面 4.4 提到过组合数是乘法关系如果你把 n_estimators 和 max_depth 各给了 5 个候选值再叠加 5 折交叉验证训练次数轻松破百单次训练再遇到特征维度高的情况时间会完全失控。解决先跑小网格比如 n_estimators 只给 [100, 200]验证整体流程能通再逐步扩大范围。加上 n_jobs-1 利用多核同时把 cv 从 5 降到 3。调参的目标不是找到理论最优而是在可接受时间内找到提升明显的参数组合。网格搜索的结果一般不会比默认参数差太多别把时间全部耗在这上面。5.4 现象爬虫脚本第二次运行抓回来的表是空的原因目标网站可能加了反爬措施检测到高频请求直接返回空白页或验证页也可能是网页改版表格结构变化导致 BeautifulSoup 的选择器匹配不到节点。还有一个很隐蔽的问题——网站返回的内容是 JavaScript 动态渲染的requests 拿到的只是空壳 HTML。解决先检查响应内容resp.text 里打印前 500 个字符看返回的到底是数据表格还是验证页面。如果是反爬加 User-Agent、降低请求频率如果确认是动态渲染requestsbs4 这套方案就失效了应该改用 Selenium 或 Playwright。绝大多数情况下这个数据集本身已经够用不必非要和爬虫较劲。5.5 现象模型在测试集上 MAE 和 RMSE 都很大而且 RMSE 远大于 MAE原因RMSE 是误差平方后取均值再开方对极端误差特别敏感。票房数据本身右偏少数头部影片的预测误差会非常大把 RMSE 整体拉高。RMSE 远大于 MAE 说明测试集里存在预测偏差很大的样本。这种结果不一定是你模型错了更可能是目标变量的分布问题。解决对 y 做 log1p 变换后重新训练让目标值分布更接近正态预测完再取 expm1 还原。同时检查测试集里是否有史诗级大片——单部票房几十亿的影片误差天然巨大可以单独分析不必强求整体指标好看。票房预测这类任务MAE 比 RMSE 更贴近业务直觉报告时优先说 MAE。6. 从复现到改进把预测结果往前推一步复现跑通之后我还会再做三件事把结果往前推一小步。第一件是看特征重要性。随机森林有 feature_importances_ 属性Pipeline 训练完可以直接取。这不仅能验证业务直觉——比如发行公司、档期对票房影响大还能帮你筛掉无用特征减少后续迭代的噪声。model grid.best_estimator_.named_steps[model] importances model.feature_importances_ # OneHotEncoder 会展开很多列这里按特征重要性降序查看前 5 个 for name, imp in sorted(zip(feature_names, importances), keylambda x: -x[1])[:5]: print(f{name}: {imp:.4f})第二件是检查目标变量的分布如果不幸是右偏对 y 做 log1p 变换再训练会直观改善 MAE。第三件是加上滞后特征——前一部同类型影片的票房、去年同一档期的平均票房这类时间上下文特征对票房预测往往比当前模型里的静态特征更有效。那次我在做类似项目时跑完随机森林之后直接看指标还觉得很满意直到把模型拿去做下一年的预测才发现随机切分带来的虚低误差骗了我。从那以后我每次做票房或任何带时间顺序的预测都会强制走一遍按时间切分的流程再补一个滞后特征看效果。这几步改动不大但能让复现的完整度提升一个台阶希望帮到你。本文还有配套的精品资源点击获取