ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

电影票房1984-2024分析预测:Python随机森林与GridSearchCV实战

电影票房1984-2024分析预测:Python随机森林与GridSearchCV实战 简介电影票房数据1984-2024分析预测实例是一份面向机器学习入门者与数据分析爱好者的Python实战资源围绕票房数据抓取、探索性分析和预测建模展开适合希望快速走通完整数据项目流程的读者。压缩包共8个文件包含6个可运行Python源代码、1个CSV完整数据集和1个readme说明文档整体仅134KB代码覆盖requests与BeautifulSoup数据爬取、pandas与seaborn/plotly可视化分析、RandomForest回归与GridSearchCV调参预测等关键环节。数据集提供1984至2024年票房信息脚本之间逻辑清晰可对照readme快速复现每一步结果省去自行采集与清洗的时间。资源已有106人学习适合按脚本顺序理解从爬虫到建模的实现思路也可作为课程设计或项目参考。1. 电影票房1984-2024分析预测这份资源包到底装了什么做电影数据分析最怕拿到的是PPT式演示代码全是截图。这份「AI实战-电影票房数据1984-2024分析预测实例」zip包拆开之后我反而放心了6个源代码覆盖了网页抓取、探索性分析、特征处理、随机森林回归与调参全流程1个288.93 KB的CSV数据集装着1984到2024年的票房记录readme.txt把脚本之间的执行关系写得比较清楚。它不是教学片段是一套能独立运行的Python工程骨架。适合做数据科学作业或作品集的同学——把EDA和GridSearchCV的现成实现改改特征就能跑通也适合想快速了解40年票房规律的分析师——脚本里的seaborn和plotly逻辑能帮你直接定位趋势拐点。跑法也很灵活先跑爬虫再走EDA和模型或者跳过爬虫直接用现成CSV。2. 数据解析与EDA先搞清楚这张40年的表长什么样在代码工程里EDA脚本往往是最不受尊重的那一个但它决定了后面模型的走向。这个包里EDA相关的脚本占了两个4号脚本「Box Office Analysis Trends and Insights 19842024.py」偏趋势洞察6号脚本「Box Office 1984 2024 EDA.py」做整体探索。拆包时我注意到数据文件只有1个CSV288.93 KB说明数据已经是清理过的EDA的重心在理解字段分布而不是花大量时间清洗脏数据。2.1 字段类型与缺失值检查拿到任何一张表第一件事是把列名、类型、缺失值打出来。下面代码演示的是这类项目里的标准操作实际脚本里的字段名以read_csv后读出的columns为准。import pandas as pd import numpy as np import seaborn as sns import matplotlib.pyplot as plt df pd.read_csv(data/boxoffice_data_2024.csv) print(shape:, df.shape) print(columns:, df.columns.tolist()) print(df.dtypes) print(missing summary:) print(df.isnull().sum().sort_values(ascendingFalse))这个片段看起来基础但能暴露大部分数据质量问题。shape先确认行数和列数像这种1984到2024的年度票房数据如果行数少得离谱就要怀疑数据源抓漏了年份。dtypes告诉你哪些列被读成了object电影票房分析里最常翻车的就在这里——年份列被读成字符串或者票房列里混入了美元符号导致gross变成object。缺失值部分isnull().sum()按列统计空值。我的经验是先看缺失集中在哪几列如果电影类型缺失超过三成后面OneHotEncoder填充unknown就没意义还不如直接把该列丢掉。如果缺失值都在次要字段上用SimpleImputer填充就够。2.2 时间趋势与分布结构看完字段类型下一步是画趋势图。40年的票房数据如果不聚合到年份维度画出来就是一片噪声所以通常先按年份汇总。sns.set_style(whitegrid) plt.figure(figsize(16, 5)) # 按年份聚合票房总量 yearly df.groupby(year)[gross].sum().reset_index() sns.lineplot(datayearly, xyear, ygross) plt.title(Total Box Office Gross by Year (1984-2024)) plt.tight_layout() plt.savefig(eda_yearly_total.png, dpi150) # 票房分布直方图 plt.figure(figsize(10, 5)) sns.histplot(df[gross], bins50, kdeTrue) plt.tight_layout() plt.savefig(eda_gross_distribution.png, dpi150)groupby(year)[gross].sum()是把同一年的所有电影票房加总这个聚合粒度可以看大盘趋势。reset_index()把year从索引拉回普通列方便后续用sns.lineplot直接映射x和y。直方图则用来判断gross分布是否右偏——票房数据基本一定右偏头部大片和长尾小片差距极大。实际跑这段的时候重点看两个东西一是趋势线上有没有断崖或突变比如2020年前后的全球票房都有明显下探这是模型训练时必须留意的异常年份二是分布图如果拖出很长的右尾说明模型的目标变量不适合直接用原始值后面在评估部分要考虑log变换。2.3 EDA结论对模型设计的三个直接影响EDA做完模型设计基本就有方向了。我总结成三条经验。第一数据切分方式不能随机打乱。这是时间序列数据年份代表时间顺序如果train_test_split直接shuffle模型会在训练时偷看未来信息导致在测试集上的指标虚高。后面模型部分要按时间顺序切分。第二特征选择围绕一部电影上映前能拿到什么信息来定。年份、档期、类型、预算这些在分析时有意义但像排名这种本身就是结果变量的字段放进特征里就属于数据泄漏。第三右偏的票房目标变量评估指标不能只看MSE。后面会展开说这里先记住先看分布再决定要不要对y做对数变换。3. 预测模型核心Pipeline、随机森林与GridSearchCV调参核心的建模脚本是3号「3-AI project.py」它把特征处理、模型训练、网格搜索串成了一条完整的链路。用到的模块很清晰RandomForestRegressor做回归主体ColumnTransformer和Pipeline做特征预处理GridSearchCV做超参数搜索。这套组合在表格类数据竞赛里相当常见它的优势在于把数据处理和模型耦合在一个Pipeline里交叉验证时不会因为预处理步骤漏在训练集外面导致数据泄漏。3.1 特征划分与预处理管道先把特征和目标变量分开然后按时间顺序切分训练集和测试集。这是时间序列预测和普通回归最大的区别。from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split, GridSearchCV from sklearn.preprocessing import OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.impute import SimpleImputer # 假设gross是目标列其余是特征 X df.drop(gross, axis1) y df[gross] # 时间序列数据按行顺序切分不做随机shuffle X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, shuffleFalse )shuffleFalse在这段代码里是最关键的一笔。默认情况下train_test_split会随机打乱数据但电影票房带年份属性随机打乱等于让2024年的数据出现在训练集里测试集里放的是1984年的老数据预测就没有实际意义了。按顺序切分之后前80%年份的数据训练最后20%年份的数据做验证模拟的是拿历史预测未来的真实场景。接下来构建特征预处理管道。数值列和分类列分开处理的原因很简单RandomForest本身不需要特征缩放但分类列必须转成数值编码而缺失值的填充策略在数值列和分类列上也应该不同。numeric_features [runtime, budget] categorical_features [genre, distributor] numeric_transformer Pipeline(steps[ (imputer, SimpleImputer(strategymedian)) ]) categorical_transformer Pipeline(steps[ (imputer, SimpleImputer(strategyconstant, fill_valueunknown)), (onehot, OneHotEncoder(handle_unknownignore)) ]) preprocessor ColumnTransformer(transformers[ (num, numeric_transformer, numeric_features), (cat, categorical_transformer, categorical_features) ]) model Pipeline(steps[ (preprocessor, preprocessor), (regressor, RandomForestRegressor(n_estimators300, random_state42)) ])SimpleImputer(strategymedian)对数值列用中位数填充比均值更抗离群值——票房数据里有不少极端值中位数更稳。分类列用constant策略填上unknown因为类别特征没有平均的概念硬填众数反而会扭曲真实分布。OneHotEncoder(handle_unknownignore)这个参数容易忽略但非常重要假设训练集里某类电影出现在测试集里而训练集没见过默认行为是报错加上ignore之后会把这列全填0程序不会中断。ColumnTransformer把数值和分类两条处理链路整合成一个整体然后在它外面再包一层Pipeline最后接RandomForestRegressor。这样交叉验证时每一折都会重新做填充和编码不会把训练集的信息带到验证集。3.2 随机森林参数范围怎么设才合理参数搜索是这个项目的重头戏。网格搜索本身不复杂复杂度全在参数组合的选取上。param_grid { regressor__n_estimators: [200, 300], regressor__max_depth: [None, 20, 30], regressor__min_samples_leaf: [2, 4] } grid_search GridSearchCV( model, param_grid, cv5, scoringneg_mean_squared_error, n_jobs-1 ) grid_search.fit(X_train, y_train) print(best params:, grid_search.best_params_) print(best score:, grid_search.best_score_)参数名里的regressor__前缀是因为整个模型是Pipeline参数要通过步骤名__参数名的格式往底层传。n_estimators从100到500之间取200和300是我在这个数据量下的常用起点max_depth给None表示不限制深度让树自由生长但配合min_samples_leaf防止过拟合min_samples_leaf设2或4作用是限制叶子节点的最少样本数对抑制噪声很有效。scoringneg_mean_squared_error要注意GridSearchCV里的MSE是负的数值越接近0越好输出的是一个负数很多新手一看-9e16就以为模型崩了。另外cv5乘以6组参数等于30次完整训练如果数据量大这个搜索可能要跑十几分钟。我一般会先用df.sample(5000)跑一遍验证Pipeline没写错再上全量数据。3.3 评估指标的落地解读MSE与MAE要一起看模型训练完评估环节在3号脚本里用的是mean_squared_error和mean_absolute_error两个指标。from sklearn.metrics import mean_squared_error, mean_absolute_error y_pred grid_search.best_estimator_.predict(X_test) mse mean_squared_error(y_test, y_pred) mae mean_absolute_error(y_test, y_pred) print(fMSE: {mse:.2f}) print(fMAE: {mae:.2f})MSE因为平方项的存在对预测偏差大的样本极其敏感——某部大片实际票房5亿美元模型只猜了1亿这一个样本贡献的误差抵得上几十个普通样本。MAE则是所有误差的绝对值平均单位就是美元业务上更好理解。我的习惯是两者都看如果MSE异常大而MAE还行说明模型对极少数票房大片预测严重失准如果两者都大那就要回头检查特征里是不是漏掉了关键信息或者目标变量要不要做log变换。3.4 1-Project VI.py与2-Project V.py的版本演进再看1号和2号脚本。从文件名看Project V和Project VI是同一套逻辑的迭代版本VI应该是V的更新版。这种命名习惯在个人维护的项目里很常见版本号直接写进标题一眼就能找到最新脚本。我推测两个脚本最核心的差异在特征处理和调参策略上V版本可能还是手动逐列处理特征VI版本改成了ColumnTransformerPipeline的自动化流程调参上V版本可能固定参数直接训练VI版本引入了GridSearchCV。如果两个脚本都能跑优先以VI版本为主。4. Web Scraping模块requests与BeautifulSoup的实战边界5号脚本「Web Scraping Box Office Data.py」在整套工程里有点特殊——它不是每次都需要跑但却是数据来源的起点。这个模块用requests抓页面用BeautifulSoup解析HTML表格逻辑本身不复杂但写法和容错处理值得单独拆开讲。4.1 爬虫脚本的结构拆解基础结构分三步发请求、解析HTML、提取字段。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_page(year): url fhttps://example.com/box-office/{year} try: resp requests.get(url, headersheaders, timeout15) if resp.status_code 200: return resp.text else: print(f{year} 返回状态码: {resp.status_code}) return None except requests.exceptions.Timeout: print(f{year} 请求超时) return Noneheaders里的User-Agent是必须的很多站点对裸requests请求直接返回403伪装成浏览器能省掉一半麻烦。timeout15必须设置不设的话requests会一直挂着脚本跑到某个年份卡住整批数据都停了。status_code检查是低成本的保险页面被重定向到登录页时状态码一般不是200这时候直接跳过比解析一堆垃圾HTML再报错好。4.2 解析策略与返回内容确认解析HTML表格时最容易踩的坑是选择器写得太死。站点结构只要调整一下class名解析结果立刻变成空列表。所以我一般先在解析前把返回内容的前500个字符打出来确认。def parse_table(html, year): soup BeautifulSoup(html, html.parser) rows soup.select(table tbody tr) records [] for row in rows: cells row.find_all(td) if len(cells) 2: continue title cells[0].get_text(stripTrue) gross_text cells[1].get_text(stripTrue).replace($, ).replace(,, ) try: gross float(gross_text) except ValueError: continue records.append({ year: year, title: title, gross: gross }) return recordsstripTrue是BeautifulSoup的get_text()里非常实用的参数会同时去掉文本首尾的空白和换行不加它的话片名里可能带着一堆\n。替换美元符号和逗号是因为英文票房页面的金额格式通常是$1,234,567直接转float会报错。try-except包住转换过程碰到N/A这类脏数据就跳过这一行避免整个脚本崩溃。4.3 数据落盘与后续交接抓完所有年份的数据后统一合并成DataFrame并落盘成CSV这一步扮演的是和下游交接的角色。all_records [] for year in range(1984, 2025): html fetch_page(year) if html: records parse_table(html, year) all_records.extend(records) time.sleep(1) # 控制请求频率避免给目标站压力 df_new pd.DataFrame(all_records) df_new.to_csv(boxoffice_data_2024.csv, indexFalse)time.sleep(1)不是可有可无的。单年份页面抓取间隔1秒40年也就多花40秒但能明显降低触发反爬的概率。indexFalse保证落地文件里不写入多余的索引列这样和原始数据集的列结构才能对齐。爬虫抓到的数据直接覆盖或者追加到boxoffice_data_2024.csv后面再跑EDA和模型脚本就不用回头碰爬虫了。5. 避坑指南六个脚本联调时最常见的五个坑这一章是血泪经验区。六个脚本单独跑都正常一旦串起来联调问题一个接一个冒出来。我把最常遇到的五个坑按现象、原因、解决的顺序写在这里都是可以抄作业的排查路径。5.1 GridSearchCV在Pipeline里跑得极慢现象参数网格里n_estimators只有两个值、max_depth三个值、min_samples_leaf两个值一共12个组合加上cv5也就60次训练但脚本跑了半小时没出结果。原因Pipeline里的OneHotEncoder会把分类特征展开成大量稀疏列特征维度一下子膨胀了许多倍随机森林每次训练都要面对这个高维输入再加上GridSearchCV是穷举式搜索每一组参数都要完整走一遍预处理加训练。双重压力叠加慢是必然的。解决先用小样本验证链路比如df.sample(5000)跑通Pipeline确认特征处理没有语法错误。再缩小参数网格12组参数砍到4组优先搜索max_depth和min_samples_leafn_estimators固定在300左右就行。另外n_jobs-1要放对位置在Windows下跑的话GridSearchCV最好包进if __name__ __main__:否则多进程会递归报错。5.2 plotly的iplot在脚本环境里不显示现象4号脚本跑到涉及plotly的那一步终端没有任何输出没有报错也没有图表弹出。原因plotly.offline.iplot这个接口是给Jupyter Notebook用的依赖notebook内核环境的交互协议。在纯.py脚本里它不会触发浏览器或者保存文件的动作结果就是所有绘图代码执行了但图消失了。解决脚本环境改用plotly.express加write_html()或者用offline.plot()直接打开浏览器。import plotly.express as px fig px.line(yearly, xyear, ygross, titleYearly Box Office Trend) fig.write_html(box_office_trend.html)这样生成的HTML文件可以直接用浏览器打开图表交互能力一点没丢。5.3 年份字段被读成字符串现象df[year].dtype显示object而不是int64groupby(year)出来的年份顺序是1984、1985、1999、2000这种字符串排序而不是数据上的时间顺序。原因CSV文件里年份列可能存在肉眼不可见的格式残留比如1984带个空格、\t制表符或者文件本身带UTF-8 BOM头pandas会把这种列整体读成object类型。解决读文件之后做一个强制转换同时把无法解析的值过滤掉。df[year] pd.to_numeric(df[year], errorscoerce) df df.dropna(subset[year]) df[year] df[year].astype(int)errorscoerce会把任何转不了数字的内容变成NaN再通过dropna把脏行清掉最后astype(int)落地成真正的年份整数。5.4 BeautifulSoup选择器失效现象昨天爬虫还能抓到表格数据今天再跑返回的全是空列表或者只抓到页头页脚。原因目标网站改版或者加入了请求频率限制返回的页面根本不是原先的表格结构。还有一种情况是页面内容通过JavaScript动态渲染requests拿到的HTML里根本没有表格数据。解决先打印resp.text[:500]看返回的到底是什么是正常的表格、登录页重定向、还是一个空壳页面。如果页面结构变了用浏览器开发者工具重新定位选择器如果是JS渲染requests和BeautifulSoup这套组合就不适用了要考虑模拟浏览器。最稳妥的方式是检查目标站点是否有公开的数据源或API比盯着HTML结构稳定得多。5.5 只盯着MSE忽略量纲和业务含义现象模型评估输出MSE: 9.6e16天文数字第一反应是模型废了。原因票房的原始值是美元计价动辄上亿平方之后量级直接到10的16次方。MSE里的这个e16不代表模型表现差只代表误差单位的平方太大了。如果数据里有几部超高票房的片子没猜中MSE会被拉得更夸张。解决同时看MAE它的单位和票房原值一致比如MAE: 34500000意味着平均误差3450万美元这个数字才是有业务含义的。再把预测结果和实际值画散点图对比对角线的分布。如果目标变量右偏明显对y做np.log1p()变换后再训练预测完再np.expm1()还原价格MSE和MAE的解读就正常了。6. 模型解释技巧把特征重要性落到具体列名上最后一个技巧来自我对3号脚本的二次开发。随机森林模型训练完成后误差指标只能告诉你模型好不好而特征重要性告诉你哪些因素真正驱动票房。但这里有个坑如果特征经过了ColumnTransformer里的OneHotEncoderfeature_importances_输出的索引对应的是编码展开后的列。这些列是稠密的不带原始列名直接用的话只能看到一堆数字根本对不上业务含义。处理方式是把OneHotEncoder展开后的列名取出来拼上数值列名再和重要性数组对齐。best_model grid_search.best_estimator_ rf best_model.named_steps[regressor] # 取出分类分支里OneHotEncoder展开后的特征名 cat_encoder ( best_model.named_steps[preprocessor] .transformers_[1][1] .named_steps[onehot] ) cat_names cat_encoder.get_feature_names_out(categorical_features).tolist() # 数值列保持原字段名 feature_names cat_names numeric_features importances rf.feature_importances_ feat_imp pd.DataFrame({ feature: feature_names, importance: importances }).sort_values(importance, ascendingFalse) print(feat_imp.head(10))transformers_[1][1]这个索引是ColumnTransformer在fit之后生成的分支列表[1]取的是构造时transformers列表里第二个元组也就是分类特征分支[1]再取到该分支的Pipeline对象顺着named_steps[onehot]拿到编码器。这里的索引顺序依赖构造ColumnTransformer时元组的排列位置如果数值分支和分类分支调换顺序索引也要跟着改。get_feature_names_out()是sklearn新版的方法名老版本叫get_feature_names()。如果脚本运行时提示方法不存在先检查sklearn版本两个方法返回的都是分类特征展开后的列名列表。特征重要性排出来之后怎么用我一般会先看排序和业务常识是否一致。比如年份和预算排在前列那很正常但如果某部片子的主演名字排第一就要带着问号去回查数据了有没有把不该放进特征的结果变量串进来。有一次我把这个脚本从票房迁移到另一个短视频播放量预测项目特征重要性输出后排名前三的竟然是发布时段和封面人脸数量跟业务方的经验判断完全吻合靠这个结果让业务方接受了模型的预测逻辑而不是当成黑匣子硬塞给他们。从那以后我每次训练完模型都强制输出一遍特征重要性如果特征排序和业务直觉出现严重冲突先别急着调参回头看数据才是正路。光看误差曲线会骗人特征重要性谱系不会。希望帮到你。本文还有配套的精品资源点击获取
返回列表