ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

电影票房预测实战:随机森林与GridSearchCV调参全流程

电影票房预测实战:随机森林与GridSearchCV调参全流程 简介面向机器学习与数据科学初学者的电影票房分析预测实战包基于1984至2024年票房数据完整覆盖数据清洗、可视化探索、特征工程、随机森林回归建模与网格搜索调参等流程适合课程设计或入门项目参考。压缩包共8个文件含6个可直接运行的Python源代码、1个CSV格式的票房数据集及1个TXT说明文档整体包体约134KB代码已手工整理且无语法错误。各.py脚本分别承担数据处理、EDA分析、趋势洞察、票房预测与网页数据爬取等任务借助pandas、seaborn、plotly完成图表展示并通过sklearn的Pipeline、OneHotEncoder和SimpleImputer构建预处理与建模管线。随附说明可辅助快速上手CSV数据可直接用于复现实验帮助读者掌握从数据获取、清洗到模型评估的完整实战路径。目前已有106人浏览学习尤其适合希望系统提升票房预测实操能力的开发者。1. 电影票房数据1984-2024这套AI实战包里到底装了什么接到一个新任务要用AI实战的方式把1984到2024年的电影票房数据做一轮分析预测。市面上的教程很多但上手才发现要么用模拟假数据凑合要么模型脚本和特征工程被拆得七零八落。这份资源把40年票房数据、EDA分析、爬虫脚本和随机森林建模流程打包在一起属于能直接跑通的那一类项目包。它解决的是一整条链路数据从哪来、字段怎么清洗、趋势图怎么画、类别特征怎么编码、超参数怎么调、预测误差怎么读。适合刚学完机器学习、想用真实数据练手的人也适合要快速交付票房趋势分析和预测结果的从业者。我用它完整复现了一遍。源码之间有明确分工1号和2号是早期探索版本3号是正式的建模管道4号和6号负责EDA可视化5号是爬虫补数据readme 把运行顺序说明白了。数据量不大单机跑完全没压力。下面按我实际操作顺序把每一步展开讲清楚代码、参数、坑都放在对应位置。2. EDA先行把1984-2024票房趋势与数据质量读透2.1 读CSV之前先把列名和缺失值确认一遍我拿到项目的第一件事不是直接跑模型而是先打开数据文件做一遍体检。4号和6号脚本都是干EDA的我更推荐先跑6号因为从命名看它覆盖的年份范围更全输出也更接近原始数据形态。先把数据读进来确认行数、列名、类型和缺失情况这一步能避免后面建模时到处找错。import pandas as pd df pd.read_csv(data/boxoffice_data_2024.csv, encodingutf-8-sig) print(df.shape) print(df.columns.tolist()) print(df.dtypes) print(df.isna().sum())这里我习惯用utf-8-sig而不是utf-8因为 Windows 下用 Excel 导出或手动编辑过的 CSV 经常带 BOM用utf-8-sig能直接把文件头吃掉。如果打印出来的列名是乱码再换成gbk试试。df.isna().sum()会按列统计缺失值我主要关心目标列比如票房字段有没有空值因为后面建模要用它当 y。2.2 用Plotly画时间序列看四十年票房整体走势EDA脚本里用到plotly.graph_objs和plotly.offline.iplot这是 Notebook 里常用的老接口。我实际画图时更习惯用plotly.express的px.line一行就能出交互图hover 还能直接看每年的具体数值。票房数据带年份字段天然适合时间序列展示。import plotly.express as px import plotly.offline as py py.init_notebook_mode(connectedTrue) fig px.line(df, xyear, ygross, titleBox Office 1984-2024) fig.show()这段代码里x和y参数传的是 DataFrame 的列名字符串所以读数据时列的命名必须一致。如果年份列被读成了字符串或者带格式的日期先做一步pd.to_datetime(df[year])再画。py.init_notebook_mode(connectedTrue)是给旧版iplot用的离线初始化如果你用的 JupyterLab 版本比较新直接fig.show()就能渲染再不行就用fig.write_html(trend.html)导出成 html用浏览器打开效果一样。2.3 字段相关性热力图找到和gross关系最强的特征时间序列只能看整体走势真正决定特征去留的是相关性分析。我用 seaborn 画热力图把数值型字段之间的相关系数一次性铺开。这一步能回答一个关键问题预算、年份、类型这些字段里谁跟票房最相关。import seaborn as sns import matplotlib.pyplot as plt fig, ax plt.subplots(figsize(10, 8)) sns.heatmap(df.corr(numeric_onlyTrue), annotTrue, cmapcoolwarm, fmt.2f, axax) plt.show()注意numeric_onlyTrue这个参数老版本 pandas 在df.corr()遇到非数值列时经常直接报错加上它之后只对数值列算相关性。fmt.2f控制热力图上显示的数值精度annotTrue会在格子里标出具体数值。看到相关系数之后我一般会把强相关的字段先记下来给后面建模做参考。3. 爬虫补数据用requests与BeautifulSoup抓票房表的完整流程3.1 为什么四十年的数据还要爬很多人拿到这份数据后会有个疑问CSV 里已经包含了 1984 到 2024 年的票房数据为什么包里还要放一个爬虫脚本我实际核对下来发现原始表对早期年份的记录经常会少字段比如某些年份缺预算、缺类型标签或者只有年度汇总没有分项数据。5号脚本的作用就是把这些缺口补上。做预测模型最怕的不是数据不准而是字段缺失还硬喂爬虫是最后一道兜底。3.2 先设置请求头再抓表格用 requests 抓取票房页面第一步不是解析而是设置请求头。很多站点对没带User-Agent的请求会直接返回 403看起来像网络问题其实是请求被识别成脚本了。我一般会带上一组桌面浏览器的 UA并设置超时时间避免单次请求卡死。import requests from bs4 import BeautifulSoup url https://www.boxofficemojo.com/year/2024/ headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } resp requests.get(url, headersheaders, timeout10) print(resp.status_code) soup BeautifulSoup(resp.text, html.parser) table soup.find(table) rows table.find_all(tr)[1:]先打印resp.status_code确认返回 200 再往下解析。soup.find(table)找到页面里的第一个表格find_all(tr)[1:]跳过表头拿到数据行。顺着脚本往下读你会发现它的选择器写得不深因为票房网站的表格结构经常调整选择器写太死反而容易失效。3.3 解析每一行并清洗金额表格里每个字段取出来都是文本比如票房会写成$1,234,567,890不处理直接存 CSV后面 pandas 读进来就是 object 类型没法参与计算。清洗的核心是把货币符号和逗号去掉再转成 float。def clean_money(value): return float(value.replace($, ).replace(,, ).strip()) for row in rows: cells row.find_all(td) if len(cells) 2: continue title cells[0].get_text(stripTrue) gross_raw cells[1].get_text(stripTrue) try: gross clean_money(gross_raw) except ValueError: continue print(title, gross)len(cells) 2这个判断很关键表格里偶尔会有空行或者合并单元格不过滤直接取cells[0]会越界。try/except是给脏数据留的后路比如N/A、空字符串这类无法转 float 的直接跳过比报错中断更稳妥。3.4 抓多个年份时加停顿5号脚本抓的是多年数据循环请求时必须控制频率。连续快速请求同一个站点容易被限流常见做法是在循环里加time.sleep(1)每次请求间隔一秒既不会太慢也不会触发风控。import time for year in range(1984, 2025): url fhttps://www.boxofficemojo.com/year/{year}/ resp requests.get(url, headersheaders, timeout10) soup BeautifulSoup(resp.text, html.parser) # 解析逻辑同上逐年提取后 append 到列表 time.sleep(1) # 最终合并保存 df_new pd.DataFrame(records) df_new.to_csv(data/boxoffice_data_2024.csv, indexFalse)range(1984, 2025)是半开区间结尾不包含 2025所以正好覆盖到 2024。to_csv时不写 index避免多出一列没意义的行号。抓完数据我一般会再跑一遍 2.1 的读取代码确认新表能正常读、缺失值没增加再接回建模流程。4. 机器学习建模RandomForest回归管道与GridSearchCV调参实录4.1 为什么选随机森林而不是线性回归票房和预算、类型、年份的关系不是简单的线性关系大投资的片子可能票房爆炸也可能扑街线性回归很难表达这种跳跃性。随机森林是树模型集成对异常值稳健不需要做特征缩放而且几百 KB 这种数据规模训练起来毫无压力。3号脚本里用了RandomForestRegressor叠加GridSearchCV从 1号、2号脚本的探索过程看这是迭代之后确定下来的方案。4.2 切分数据集目标列和特征列必须分开建模第一步是把特征和目标拆开。我用gross做预测目标其他字段进特征矩阵。切分时random_state必须固定不然后面每次跑出来的结果都不一样对比调参效果就失真了。from sklearn.model_selection import train_test_split X df.drop(columns[gross]) y df[gross] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 )test_size0.2意味着 20% 的数据留在测试集用来评估模型在没见过的数据上的表现。如果数据集行数很少建议改成 0.3给测试集多留点样本评估结果会更稳。4.3 用ColumnTransformer把数值缺失和类别编码放进同一个管道建模代码里最值得抄的是管道设计。票房数据既有年份这类数值列也有类型这种文本列分开处理很容易在测试集上漏掉同一套清洗逻辑。用ColumnTransformer把数值列的缺失值填充和类别列的独热编码绑进同一条管道训练时 fit 一次预测时自动复用这才是管道的意义。from sklearn.impute import SimpleImputer from sklearn.preprocessing import OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.ensemble import RandomForestRegressor numeric_cols X.select_dtypes(include[number]).columns.tolist() categorical_cols X.select_dtypes(exclude[number]).columns.tolist() numeric_pipe Pipeline([ (imputer, SimpleImputer(strategymedian)) ]) categorical_pipe Pipeline([ (imputer, SimpleImputer(strategymost_frequent)), (onehot, OneHotEncoder(handle_unknownignore)) ]) preprocessor ColumnTransformer([ (num, numeric_pipe, numeric_cols), (cat, categorical_pipe, categorical_cols) ]) model Pipeline([ (pre, preprocessor), (rf, RandomForestRegressor(random_state42, n_jobs-1)) ])数值列用median而不是mean填充因为票房数据里有极端值均值会拉偏中位数更稳健。类别列用most_frequent填缺失相当于用众数补洞。handle_unknownignore必须加否则测试集里出现训练集没见过的类型值时OneHotEncoder 会直接抛错。管道里模型步骤命名成rf不是随便起的后面网格搜索全靠这个前缀找参数。4.4 GridSearchCV调参先粗后细别一上来就铺大网格调参这块多少带点玄学但 GridSearchCV 的核心逻辑很朴素把参数候选列表排列组合每一组都交叉验证最后给你最好的组合。3号脚本里搜索的是树的数量、深度和最小分割样本数这是随机森林最常调的三个参数。from sklearn.model_selection import GridSearchCV param_grid { rf__n_estimators: [100, 300], rf__max_depth: [10, 20, None], rf__min_samples_split: [2, 5], } search GridSearchCV( model, param_grid, cv3, scoringneg_mean_absolute_error, n_jobs-1, verbose1 ) search.fit(X_train, y_train) print(search.best_params_) print(search.best_score_)rf__n_estimators里的双下划线是把管道外层和内部参数连接起来的约定写法。cv3是 3 折交叉验证数据量小的时候够用折数太多一道网格要跑很久。scoringneg_mean_absolute_error我单独解释一下GridSearchCV 习惯把损失函数取负让分数越大越好主指标用 MAE 而不是 MSE因为票房数值动辄上千万美元个别大片的误差会通过平方被放大MAE 更接近业务直觉。4.5 评估指标MSE、MAE、R2怎么看模型训练完脚本里打印三个指标均方误差、平均绝对误差和 R2。我每次都会把这三个数字同时贴出来看因为单一的指标会骗人。from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score pred search.best_estimator_.predict(X_test) print(MSE:, mean_squared_error(y_test, pred)) print(MAE:, mean_absolute_error(y_test, pred)) print(R2:, r2_score(y_test, pred))MSE 的量纲是票房值的平方数字大不代表模型差看趋势才有意义。MAE 是最直观的评估指标直接告诉你平均预测偏差是多少美元。R2 接近 1 说明模型拟合得好但票房受档期、口碑、竞争对手影响很大R2 到 0.6-0.8 已经是不错的结果。预测完我还会把误差最大的几部电影单独捞出来看原因后面细说。5. 复现排查六个脚本最容易翻车的五个坑5.1 CSV读进来列名全是乱码现象df.columns.tolist()打印出来第一列带\uFEFF或者整列中文名变成乱码。原因Windows 下用 Excel 编辑过 CSV 后保存成了带 BOM 的 UTF-8 编码而脚本里用的是encodingutf-8BOM 被当成列名的一部分读进来。解决读取时统一用encodingutf-8-sig这个参数会跳过文件头部的 BOM。如果还乱码再换成encodinggbk。读完第一时间打印列名确认不要跳过这步直接建模。5.2 plotly的iplot在JupyterLab里显示空白现象脚本执行没报错但输出区域一片空白图就是不出来。原因旧版plotly.offline.iplot和 JupyterLab 的渲染机制不兼容尤其是 Notebook 内核版本较新时离线初始化没有真正生效。解决保留py.init_notebook_mode(connectedTrue)之外直接把画图方式换成fig.show()再不行用fig.write_html(output.html)导出文件用浏览器打开效果完全一样还能交互查看每年的数值。5.3 爬虫返回403或者空表格现象resp.status_code打出 403或者soup.find(table)返回None后面解析直接报错。原因请求没带User-Agent被目标站点识别成脚本拒掉或者页面结构改版表格选择器失效。解决请求时加上headers{User-Agent: Mozilla/5.0 ...}。抓取前先print(resp.text[:500])确认返回的是 HTML 页面而不是错误提示。表格结构变了就打印一段soup.prettify()找到当前真实的表格标签再改选择器。多页循环务必加time.sleep(1)。5.4 模型在测试集上报 unknown category 错误现象训练集上跑得好好的一到predict(X_test)就报错提示found unknown categories。原因测试集里某个类别字段出现了训练集里没见过的值OneHotEncoder 默认对未知类别直接报错。解决在管道里给 OneHotEncoder 加上handle_unknownignore未知类别会全部编码成零向量模型不会因为新类别崩掉。这也是我在 4.3 里强调这个参数的原因。5.5 GridSearchCV 跑到一半内存耗尽现象进程卡死或者内存占用持续上涨最后被系统杀掉。原因参数网格铺太大cv折数多n_jobs-1把全部 CPU 核心都用上管道里的独热编码会复制多份数据内存就爆了。解决用小网格起步例如n_estimators[100, 300]、max_depth[10, 20, None]、cv3跑通了再逐步放大。数据变多之后换成RandomizedSearchCV只采样部分参数组合效果接近但耗时小很多。6. 模型固化与验证把调参结果持久化到下一轮预测6.1 joblib保存管道下次不用重新调参网格搜索跑完拿到best_estimator_最怕的就是关掉 Notebook 之后参数全丢。把整个管道保存成文件下次直接用不用重新 fit 一遍。这里要注意保存的是管道本身而不只是模型参数因为管道里包含独热编码和缺失值填充逻辑缺了任何一个环节直接拿原始数据预测都会出问题。import joblib joblib.dump(search.best_estimator_, box_office_model.joblib) loaded_model joblib.load(box_office_model.joblib) new_pred loaded_model.predict(new_data)joblib.dump对 sklearn 管道的支持比 pickle 更好序列化出来体积也更小。加载预测时有一个很实际的坑如果换了一台机器重新加载报错大概率是 sklearn 版本不一致优先用训练时的同一个环境跑或者直接重新跑一遍训练脚本。6.2 用误差最大的样本回看模型短板模型评估完不要只看平均指标把误差最大的十几条样本拉出来逐条看能直接找到模型的能力边界。我一般是算绝对误差后排序取误差最大的前十条。import numpy as np pred search.best_estimator_.predict(X_test) err np.abs(y_test - pred) idx np.argsort(err)[-10:][::-1] check X_test.iloc[idx].copy() check[actual] y_test.iloc[idx].values check[pred] pred[idx] check[abs_err] err[idx] print(check.to_string())输出的表里如果误差最大的都是高预算大片说明模型对爆款片的预测偏保守常见原因是这类样本在训练集中占比少。做业务预测时这部分误差要单独说明而不是笼统承诺模型精度。从那以后我拿到这种带多个版本脚本的资源第一件事永远是先读 readme再从数据文件反推字段最后才跑建模这个顺序帮我避开了很多重复翻车的时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表