ARTICLE DETAIL

资讯详情

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

Python电影票房预测源码实战:从数据清洗到模型调参的避坑指南

Python电影票房预测源码实战:从数据清洗到模型调参的避坑指南 简介这份资源是面向计算机相关专业学生与项目实战学习者的电影数据可视化及票房影响因素分析与预测完整项目包可直接用于毕业设计、课程设计或期末大作业。项目围绕电影数据展开涵盖数据采集与存储、可视化展示以及票房影响因素分析与预测等环节帮助读者理解从数据处理到建模预测的完整流程。压缩包共38个文件约5.18MB包含6个Python源码文件、3个Jupyter Notebook、24张可视化结果图、1份SQL建表脚本、1份PDF说明文档及README等源码与文档配套齐全便于对照学习与二次开发。目前已有425人学习下载。项目经过严格调试下载即用读者可据此掌握数据清洗、图表绘制、数据库操作与票房预测建模等技能并参考目录结构与结果图快速复现实验适合作为毕设模板或实战练习素材。1. 电影票房预测这套源码到底能帮你省下多少试错时间拿到「基于python的电影数据可视化及票房影响因素分析与预测源码文档说明.zip」这个包多数人的第一反应是解压、装依赖、跑 main.py然后对着报错发呆。我见过太多人卡在 pandas 版本冲突和中文乱码上最后把整个项目扔进回收站。其实这套东西的价值不在「跑通」而在于它把电影行业里最脏的那段数据清洗逻辑和特征工程思路摊开给你看了——票房预测从来不是调个模型就完事选什么特征、怎么处理档期、如何量化演员号召力这些才是决定预测误差是 15% 还是 40% 的分水岭。这篇文章面向两类人一是想拿它当课程设计或毕设底稿的学生二是想快速验证「票房到底能不能被预测」这个命题的从业者。我会按数据流走向从环境配置、可视化拆解、特征构造、模型对比一路讲到避坑和调参每一步都给出可复现的命令和参数含义让你拿到包之后知道先动哪里、后动哪里、哪里千万别动。2. 环境配置与数据加载把 zip 跑起来的第一公里2.1 依赖安装的版本锁定策略这类源码包最常见的翻车点不是代码逻辑而是依赖版本漂移。作者写代码时的 pandas 可能是 1.3.x你本地装个 2.2.xappend方法直接没了inplace参数行为也变了。我一般会先看包里有没有 requirements.txt有就照着装没有就按下面这个组合来这是我在多个类似项目里验证过能兼容大部分旧代码的版本区间。# 创建独立虚拟环境避免污染全局 python -m venv movie_env # Windows 激活 movie_env\Scripts\activate # macOS/Linux 激活 source movie_env/bin/activate # 安装核心依赖锁定大版本 pip install pandas1.5.3 numpy1.24.3 matplotlib3.7.1 seaborn0.12.2 scikit-learn1.2.2 pip install jieba0.42.1 wordcloud1.9.1 openpyxl3.1.2这里锁 pandas 到 1.5.3 是因为它同时支持append虽然已弃用但还能用和新的concat写法给旧代码留了缓冲。numpy 锁 1.24.3 是为了避开 1.25 之后某些 C 扩展与旧版 scipy 的 ABI 冲突。scikit-learn 用 1.2.2 是因为 1.3 之后mean_squared_error的squared参数被移除很多旧代码会直接报 TypeError。装完先跑一句python -c import pandas; print(pandas.__version__)确认版本对得上。2.2 数据文件的编码探测与列名对齐电影数据来源通常是豆瓣、猫眼或 TMDB 的爬取结果编码以 GBK 和 UTF-8 混杂居多。直接pd.read_csv(movies.csv)大概率报UnicodeDecodeError。我的习惯是先探测编码再读同时把列名统一成英文小写下划线格式后面写特征工程时不用反复切输入法。import pandas as pd import chardet # 探测文件编码 with open(movies.csv, rb) as f: raw f.read(100000) encoding chardet.detect(raw)[encoding] print(f检测到编码: {encoding}) # 用探测到的编码读取 df pd.read_csv(movies.csv, encodingencoding) # 列名标准化去空格、转小写、替换特殊字符 df.columns [c.strip().lower().replace( , _).replace(, ().replace(, )) for c in df.columns] print(df.columns.tolist()) print(df.shape) print(df.head(3))chardet.detect只读前 100KB 是为了速度电影数据通常几万行全量探测没必要。列名标准化这一步看着简单但能省掉后面无数次KeyError。如果列名里有中文比如「上映时间」「票房(万)」标准化后变成上映时间和票房(万)后续用df[票房(万)]就能直接取。注意replace链式调用里括号的全半角要写对否则替换不生效。2.3 缺失值与异常值的首轮扫描数据加载完别急着画图先做一轮缺失率和描述性统计扫描。票房数据里最常见的异常是「票房为 0」和「上映日期为未来时间」前者可能是数据未更新后者可能是点映场次。我一般用下面这段代码一次性输出关键列的缺失情况和分位数心里有数之后再决定填充还是剔除。# 缺失率统计 missing df.isnull().sum() / len(df) * 100 print(缺失率超过5%的列) print(missing[missing 5].sort_values(ascendingFalse)) # 关键数值列的描述性统计 key_cols [box_office, budget, runtime, rating] existing_cols [c for c in key_cols if c in df.columns] print(df[existing_cols].describe(percentiles[0.01, 0.25, 0.5, 0.75, 0.99])) # 票房为0或负值的记录 if box_office in df.columns: zero_mask df[box_office] 0 print(f票房非正记录数: {zero_mask.sum()}) print(df[zero_mask][[title, box_office]].head())describe里加percentiles参数是为了看 1% 和 99% 分位比只看 min/max 更能发现长尾异常。票房为 0 的记录如果占比低于 2%我一般直接剔除如果占比高就要考虑是不是数据源本身有问题。这一步的输出结果直接决定后面可视化时要不要做对数变换——票房分布几乎必然是右偏的取 log 之后画直方图才看得出形状。3. 可视化拆解从图表反推特征工程方向3.1 票房分布与对数变换的时机判断拿到票房列第一件事是画分布图但直接画原始值大概率得到一根贴着左边的柱子加一条长尾。这时候要不要取对数取决于你后面用的是什么模型。树模型随机森林、XGBoost对单调变换不敏感取不取都行线性回归和神经网络则强烈建议取 log否则损失函数会被高票房样本主导。我通常两个都画对比着看。import matplotlib.pyplot as plt import numpy as np import seaborn as sns # 设置中文字体Windows 用 SimHeimacOS 用 Arial Unicode MS plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False fig, axes plt.subplots(1, 2, figsize(14, 5)) # 原始票房分布 sns.histplot(df[box_office], bins50, axaxes[0], kdeTrue) axes[0].set_title(原始票房分布) axes[0].set_xlabel(票房) # 对数变换后分布 log_box np.log1p(df[box_office]) sns.histplot(log_box, bins50, axaxes[1], kdeTrue) axes[1].set_title(log1p 变换后票房分布) axes[1].set_xlabel(log1p(票房)) plt.tight_layout() plt.savefig(box_office_dist.png, dpi150) plt.show()np.log1p而不是np.log是为了处理票房为 0 的情况log1p(0)0不会产生负无穷。kdeTrue叠加核密度曲线能更直观看出分布形状。如果对数变换后的分布接近正态那后面用线性模型时就可以放心把 log 票房作为目标变量。保存图片时dpi150是论文和报告里比较清晰的档位再高文件就太大了。3.2 类型、档期与票房的交叉分析电影类型和上映档期是票房分析里绕不开的两个维度。类型通常是多值字段一部电影可能同时是「剧情/动作/科幻」需要先拆分成哑变量再聚合。档期则要从上映日期里提取月份和是否属于热门档期春节、暑期、国庆、贺岁。下面这段代码把类型拆开算平均票房同时标记档期。# 拆分多值类型字段 if genres in df.columns: # 假设 genres 列格式为 剧情/动作/科幻 genre_dummies df[genres].str.get_dummies(sep/) genre_stats [] for genre in genre_dummies.columns: mask genre_dummies[genre] 1 genre_stats.append({ genre: genre, count: mask.sum(), avg_box: df.loc[mask, box_office].mean(), median_box: df.loc[mask, box_office].median() }) genre_df pd.DataFrame(genre_stats).sort_values(avg_box, ascendingFalse) print(genre_df.head(10)) # 档期标记 if release_date in df.columns: df[release_date] pd.to_datetime(df[release_date], errorscoerce) df[month] df[release_date].dt.month # 定义热门档期2月春节、7-8月暑期、10月国庆、12月贺岁 hot_months [2, 7, 8, 10, 12] df[is_hot_season] df[month].isin(hot_months).astype(int) print(df.groupby(is_hot_season)[box_office].agg([mean, median, count]))str.get_dummies(sep/)是 pandas 里处理分隔符多值字段最简洁的写法比手写循环快得多。pd.to_datetime加errorscoerce是为了把无法解析的日期变成 NaT避免整列报错。档期标记这里我用的是月份粗筛更精细的做法是按具体日期区间判断但月份粒度对树模型来说已经够用。输出结果里如果某个类型的count小于 30它的平均票房参考价值就很有限后面建模时可以考虑合并或剔除。3.3 相关性热力图与特征初筛在正式建模前画一张数值特征与票房的相关系数热力图能快速筛掉一批无关特征。注意 Pearson 相关系数只能捕捉线性关系对树模型来说参考价值有限但用来排除「明显无关」的变量还是够用的。我一般会把票房本身也放进去看哪些特征和它的相关系数绝对值超过 0.1。# 选取数值型特征 numeric_cols df.select_dtypes(include[np.number]).columns.tolist() # 排除票房本身和 id 类列 exclude [box_office, id, movie_id] feature_cols [c for c in numeric_cols if c not in exclude] # 计算相关系数矩阵 corr_matrix df[feature_cols [box_office]].corr() # 画热力图 plt.figure(figsize(12, 10)) sns.heatmap(corr_matrix, annotTrue, fmt.2f, cmapRdBu_r, center0, squareTrue, linewidths0.5) plt.title(数值特征与票房相关系数热力图) plt.tight_layout() plt.savefig(correlation_heatmap.png, dpi150) plt.show() # 输出与票房相关性最高的特征 box_corr corr_matrix[box_office].drop(box_office).sort_values(keyabs, ascendingFalse) print(与票房相关性最高的特征) print(box_corr.head(10))cmapRdBu_r是红蓝发散色系center0让 0 值对应白色正负相关一眼可辨。sort_values(keyabs)是按绝对值排序这样负相关强的特征也能排前面。如果发现某个特征和票房的相关系数超过 0.8要警惕多重共线性后面用线性模型时得做 VIF 检验或直接剔除。热力图上如果两个特征之间相关系数超过 0.9保留其中一个就行。4. 票房影响因素的特征工程与模型对比4.1 从演员和导演字段构造号召力特征原始数据里演员和导演通常是字符串直接做哑变量维度爆炸且稀疏。我一般用「历史平均票房」来量化号召力对每个演员计算他/她参演电影的平均票房然后映射回原数据。这里要注意用训练集统计、测试集映射避免数据泄露。下面是一个简化版实现。from sklearn.model_selection import train_test_split # 假设有 actors 列格式为 演员A/演员B/演员C def build_actor_power(df, actor_colactors, targetbox_office, min_count3): 构造演员号召力特征演员历史平均票房 # 先拆分训练测试集 train_df, test_df train_test_split(df, test_size0.2, random_state42) # 展开演员列表 actor_records [] for idx, row in train_df.iterrows(): if pd.isna(row[actor_col]): continue for actor in str(row[actor_col]).split(/): actor actor.strip() if actor: actor_records.append({actor: actor, box_office: row[target]}) actor_df pd.DataFrame(actor_records) # 只保留出现次数达标的演员 actor_stats actor_df.groupby(actor)[box_office].agg([mean, count]) actor_stats actor_stats[actor_stats[count] min_count] actor_power actor_stats[mean].to_dict() # 映射回训练集和测试集 def map_actor_power(row): if pd.isna(row[actor_col]): return np.nan powers [actor_power.get(a.strip(), np.nan) for a in str(row[actor_col]).split(/)] powers [p for p in powers if not np.isnan(p)] return np.mean(powers) if powers else np.nan train_df[actor_power] train_df.apply(map_actor_power, axis1) test_df[actor_power] test_df.apply(map_actor_power, axis1) return train_df, test_df, actor_power train_df, test_df, actor_power build_actor_power(df) print(f训练集演员号召力缺失率: {train_df[actor_power].isnull().mean():.2%}) print(f测试集演员号召力缺失率: {test_df[actor_power].isnull().mean():.2%})min_count3是为了过滤掉只参演过一两部电影的演员他们的平均票房噪声太大。映射时取多个演员的均值而不是最大值是为了避免单个高票房演员主导整部电影的号召力评分。测试集里如果出现训练集没见过的演员actor_power.get返回 NaN后面用中位数填充即可。这个特征在树模型里通常能排进重要性前五但在线性模型里要注意标准化。4.2 预算、时长与评分的非线性处理预算和票房的关系几乎必然是非线性的——低预算电影票房天花板低但高预算电影也可能亏本。我一般会把预算做分箱处理同时保留原始值和对数值让模型自己选。时长和评分则常用多项式展开来捕捉边际效应递减。from sklearn.preprocessing import PolynomialFeatures, KBinsDiscretizer # 预算分箱按分位数切成5档 if budget in train_df.columns: kb KBinsDiscretizer(n_bins5, encodeordinal, strategyquantile) train_df[budget_bin] kb.fit_transform(train_df[[budget]]) test_df[budget_bin] kb.transform(test_df[[budget]]) # 同时对数值做对数变换 train_df[log_budget] np.log1p(train_df[budget]) test_df[log_budget] np.log1p(test_df[budget]) # 时长和评分的多项式特征 poly PolynomialFeatures(degree2, include_biasFalse) for col in [runtime, rating]: if col in train_df.columns: train_poly poly.fit_transform(train_df[[col]]) test_poly poly.transform(test_df[[col]]) # 只取二次项索引2一次项保留原列 train_df[f{col}_squared] train_poly[:, 2] test_df[f{col}_squared] test_poly[:, 2]KBinsDiscretizer用strategyquantile保证每档样本数接近避免某些档位样本过少。encodeordinal输出整数编码适合树模型如果要用线性模型改成onehot。多项式特征这里只取了二次项因为三次以上容易过拟合且解释性差。注意PolynomialFeatures的列顺序是 [x, x²]所以索引 2 对应 x²索引 0 是 x索引 1 是另一个特征的一次项这里只传了一列所以索引 1 就是 x²但为了通用性我写成索引 2 并只传单列时需调整。实际使用时建议先print(poly.get_feature_names_out())确认列顺序。4.3 三种回归模型的训练与评估对比特征工程做完用线性回归、随机森林、XGBoost 各跑一遍对比 RMSE 和 R²。线性回归作为基线随机森林看特征重要性XGBoost 通常效果最好但要注意调参。下面这段代码把三个模型串起来输出对比表格。from sklearn.linear_model import LinearRegression from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error, r2_score import xgboost as xgb # 准备特征矩阵 feature_cols [log_budget, runtime, rating, actor_power, is_hot_season, budget_bin, runtime_squared, rating_squared] feature_cols [c for c in feature_cols if c in train_df.columns] X_train train_df[feature_cols].fillna(train_df[feature_cols].median()) X_test test_df[feature_cols].fillna(train_df[feature_cols].median()) y_train np.log1p(train_df[box_office]) y_test np.log1p(test_df[box_office]) models { LinearRegression: LinearRegression(), RandomForest: RandomForestRegressor(n_estimators200, max_depth10, random_state42), XGBoost: xgb.XGBRegressor(n_estimators300, learning_rate0.05, max_depth6, random_state42) } results [] for name, model in models.items(): model.fit(X_train, y_train) y_pred model.predict(X_test) rmse np.sqrt(mean_squared_error(y_test, y_pred)) r2 r2_score(y_test, y_pred) results.append({model: name, rmse: rmse, r2: r2}) print(f{name}: RMSE{rmse:.4f}, R2{r2:.4f}) result_df pd.DataFrame(results).sort_values(rmse) print(result_df)fillna用训练集中位数而不是均值是因为中位数对异常值更稳健。目标变量取log1p后 RMSE 的含义变成「对数票房的均方根误差」解释时要注意还原。随机森林的max_depth10是防止过拟合的常用起点XGBoost 的learning_rate0.05配n_estimators300是慢学习率多轮次的经典组合。如果 XGBoost 的 RMSE 比线性回归还差大概率是特征里有噪声或缺失值填充方式有问题先回去检查actor_power的缺失率。5. 避坑与排查那些让预测结果崩掉的细节5.1 中文乱码导致类型字段全部变成 NaN现象读取 CSV 后genres列全是 NaN但用 Excel 打开明明有内容。原因文件是 GBK 编码pandas 默认用 UTF-8 读中文字段解析失败变成 NaN。解决用chardet探测编码后指定encodingGBK重新读取或者在read_csv里加encodinggb18030GBK 的超集兼容性更好。如果已经读进来了可以用df[genres].fillna(未知)临时补救但根源还是编码问题。5.2 上映日期解析失败导致档期特征全为 0现象pd.to_datetime之后month列全是 NaNis_hot_season全为 0。原因日期格式不统一有的写「2023-01-15」有的写「2023/1/15」还有的写「2023年1月15日」。解决先用df[release_date].str.replace(年, -).str.replace(月, -).str.replace(日, )统一分隔符再pd.to_datetime(..., formatmixed)pandas 2.0 支持。如果 pandas 版本低就写个自定义解析函数逐条处理。解析完先print(df[release_date].isnull().sum())确认缺失数量。5.3 演员号召力特征在测试集上大面积缺失现象训练集actor_power缺失率 5%测试集缺失率 40%。原因测试集里有很多训练集没出现过的新演员actor_power.get返回 NaN。解决不要用 0 填充0 表示号召力极低会误导模型用训练集中位数填充同时加一个is_new_actor哑变量标记是否为新人。如果缺失率超过 30%说明数据量太小或演员字段太稀疏考虑放弃这个特征改用导演号召力或制片公司特征。5.4 对数变换后预测值还原时出现负票房现象模型预测的 log 票房还原后出现负数。原因np.expm1对负的预测值会返回负数而 log 票房理论上不应该为负。解决在还原前先y_pred np.maximum(y_pred, 0)截断或者用np.expm1(np.clip(y_pred, 0, None))。更根本的办法是检查特征里有没有极端负值导致模型输出越界比如标准化后的特征如果有 -5 以下的异常值先做 winsorize 缩尾处理。5.5 随机森林特征重要性全被预算主导现象feature_importances_里log_budget占了 0.7 以上其他特征加起来不到 0.3。原因预算和票房的相关性确实最强但这也意味着模型可能忽略了其他因素的交互作用。解决试试在树模型里加max_features0.5限制每次分裂考虑的特征比例或者用 permutation importance 替代默认的基于不纯度的 importance。另外可以把预算分箱后的budget_bin和原始log_budget同时放入观察重要性是否被分散。6. 把预测误差压到 20% 以内的调参习惯模型跑通只是起点真正决定这套源码能不能用在报告或论文里的是你能不能把测试集 RMSE 对应的票房误差控制在可解释范围内。我一般会做三件事第一用GridSearchCV对 XGBoost 的max_depth、learning_rate、subsample三个参数做粗网格搜索范围分别取 [4,6,8]、[0.03,0.05,0.1]、[0.7,0.8,0.9]交叉验证折数设 5评分用neg_root_mean_squared_error。第二画预测值 vs 真实值的散点图看误差是均匀分布还是集中在高票房区间——如果是后者说明模型对头部电影欠拟合可以给高票房样本加权重。第三把特征重要性前五的特征单独拿出来逐个做部分依赖图PDP确认每个特征对预测的影响方向符合业务直觉比如预算越高预测票房越高、评分越高预测票房越高。如果某个特征的 PDP 出现反直觉的拐点大概率是数据里有脏样本或特征构造逻辑有误。from sklearn.model_selection import GridSearchCV # XGBoost 粗网格搜索 param_grid { max_depth: [4, 6, 8], learning_rate: [0.03, 0.05, 0.1], subsample: [0.7, 0.8, 0.9] } xgb_model xgb.XGBRegressor(n_estimators300, random_state42) grid GridSearchCV(xgb_model, param_grid, cv5, scoringneg_root_mean_squared_error, n_jobs-1) grid.fit(X_train, y_train) print(f最佳参数: {grid.best_params_}) print(f最佳CV RMSE: {-grid.best_score_:.4f}) # 用最佳参数重新训练并评估测试集 best_model grid.best_estimator_ y_pred_best best_model.predict(X_test) print(f测试集 RMSE: {np.sqrt(mean_squared_error(y_test, y_pred_best)):.4f}) print(f测试集 R2: {r2_score(y_test, y_pred_best):.4f})n_jobs-1用满所有 CPU 核心加速搜索但如果你在笔记本上跑且同时开其他程序设成n_jobs2更稳。neg_root_mean_squared_error是因为 sklearn 的 GridSearchCV 默认取最大值所以误差类指标要取负。搜索完别直接用best_estimator_就完事一定要在独立测试集上再验一次因为 CV 分数和测试分数之间可能有 5% 到 10% 的差距。如果测试集 RMSE 比 CV RMSE 高出一大截说明数据划分有问题或者特征里有泄露。最后说个我自己的习惯每次调完参把feature_importances_和上一次的结果对比如果排名前三的特征换了我会回去检查是不是某次数据预处理改了填充策略或分箱边界。这个交叉验证的习惯帮我抓到过好几次「因为改了缺失值填充方式导致模型行为突变」的隐蔽问题。票房预测这件事模型选择的影响远小于特征质量和数据清洗的严谨度把 80% 的时间花在前两步剩下的 20% 调参才有意义。希望帮到你。本文还有配套的精品资源点击获取
返回列表