
简介这是一套面向计算机相关专业学生的机器学习实战项目资料以电影票房预测为主题适合正在做毕业设计、课程设计或期末大作业的学习者参考。项目经导师指导并通过评审获得98分成绩可作为完整的高分毕设范例。资源包共59个文件包含16个Python源码文件、16个CSV数据集、23张PNG图表、2份Markdown说明、1份PDF文档及1份TXT文件压缩包约30.95MB。源码覆盖数据预处理、特征工程、KNN与SVD协同过滤、集成推荐及多模型对比等模块数据集包含tmdb_5000电影与演职员信息图表与PDF文档则记录了特征分析与实验结论。目前已有371人学习下载。读者可从中获得完整的赛题实现方案、可复现的训练与测试数据、模块化的代码结构以及数据分析报告便于快速理解机器学习预测流程并迁移到自己的课题中。1. 电影票房预测平台从数据集到可运行系统的落地路径很多同学做毕业设计时第一反应是找个现成模板改改界面结果答辩时被问“你的模型为什么用随机森林而不是 XGBoost”就卡住了。电影票房预测这个题目之所以每年都有人选是因为它同时踩中了机器学习、数据清洗、特征工程和 Web 可视化四个可展示的模块而且数据集相对好找。但真正能跑通并讲清楚逻辑的项目并不多大部分卡在数据泄漏、特征维度爆炸和前后端联调上。这篇笔记按“数据怎么处理 → 模型怎么选 → 平台怎么搭 → 坑怎么避”的顺序把一套可复现的 Python 毕业设计方案拆开讲。适合正在做机器学习方向毕业设计、需要完整源码和数据集支撑的同学也适合想快速验证票房预测可行性的从业者。2. 票房预测的数据集处理与特征工程从原始表格到模型输入2.1 数据集来源与字段取舍常见做法是组合 TMDB 或 Kaggle 上的电影元数据加上国内票房平台的公开榜单。我一般会保留这几类字段预算、类型、上映档期、制片公司、导演和主演的历史票房均值、预告片热度、社交媒体讨论量。注意上映后的票房数据绝对不能作为特征这是最典型的标签泄漏。很多网上的“高精度”方案就是把首周票房当输入去预测总票房答辩时一问就露馅。处理流程分三步先做缺失值统计预算缺失超过 30% 的样本直接丢弃再把多值字段如类型、演员做 one-hot 或 target encoding最后按时间切分训练集和测试集而不是随机切分。时间切分能模拟真实预测场景——用 2018 年之前的电影训练预测 2019 年之后的票房。import pandas as pd from sklearn.model_selection import train_test_split # 读取原始数据 df pd.read_csv(movie_box_office.csv) # 丢弃预算缺失过多的样本 df df[df[budget].notna()] df df[df[budget] 0] # 按上映年份做时间切分 train df[df[release_year] 2018] test df[df[release_year] 2018] # 选择数值特征和类别特征 num_features [budget, popularity, runtime, release_month] cat_features [genre, production_company] # 对类别特征做 one-hot train pd.get_dummies(train, columnscat_features) test pd.get_dummies(test, columnscat_features) # 对齐列防止训练集和测试集特征维度不一致 train, test train.align(test, joinleft, axis1, fill_value0) print(f训练集样本数{len(train)}测试集样本数{len(test)})这段代码的关键在align那一步。如果不做列对齐测试集可能缺少训练集里某些类型的 one-hot 列导致模型输入维度对不上。参数上release_month从日期字段拆出来因为档期对票房影响很大——春节档和暑期档的票房基数明显不同。2.2 特征工程中的三个必调参数第一个是目标变量变换。票房分布严重右偏直接回归会让模型偏向预测高票房。我一般对票房取对数np.log1p(box_office)预测后再用np.expm1还原。这样评估指标如 RMSE会更稳定。第二个是类别特征的编码方式。类型字段用 one-hot 没问题但制片公司有几百个取值one-hot 会导致维度爆炸。常见做法是只保留出现次数前 20 的公司其余归为 “Other”或者用 target encoding 把公司替换成其历史电影的平均票房。注意 target encoding 必须在训练集上计算均值再映射到测试集否则会泄漏。第三个是数值特征的标准化。预算和热度量纲差异大树模型虽然不敏感但如果你用线性回归或神经网络必须做 StandardScaler 或 MinMaxScaler。我一般统一做标准化方便后续换模型对比。import numpy as np from sklearn.preprocessing import StandardScaler # 目标变量取对数 train[log_box] np.log1p(train[box_office]) test[log_box] np.log1p(test[box_office]) # 数值特征标准化 scaler StandardScaler() train[num_features] scaler.fit_transform(train[num_features]) test[num_features] scaler.transform(test[num_features]) # 制片公司 target encoding company_mean train.groupby(production_company)[log_box].mean() train[company_encoded] train[production_company].map(company_mean) test[company_encoded] test[production_company].map(company_mean) test[company_encoded].fillna(train[log_box].mean(), inplaceTrue)这里fit_transform只在训练集上调用测试集用transform这是避免数据泄漏的基本功。company_encoded的缺失值用训练集均值填充防止测试集出现训练集没见过的公司时产生 NaN。3. 票房预测模型选型与训练随机森林、XGBoost 和 LightGBM 的对比3.1 为什么树模型是票房预测的基线首选电影票房和特征之间大量是非线性关系——预算增加到一定程度后边际收益递减档期和类型的交互效应也很明显。线性回归在这类数据上表现通常很差而树模型能自动捕捉这些非线性。随机森林作为基线训练快、调参少、不容易过拟合XGBoost 和 LightGBM 在结构化数据上通常是精度最高的。我一般会同时跑三个模型用交叉验证对比 RMSE 和 R²。如果时间紧直接上 LightGBM它的训练速度比 XGBoost 快不少而且对小数据集更友好。注意不要用深度学习模型电影票房数据集通常只有几千条样本神经网络很容易过拟合答辩时也讲不清楚为什么用 MLP 而不是 GBDT。import lightgbm as lgb from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error, r2_score # 准备特征矩阵 feature_cols [c for c in train.columns if c not in [box_office, log_box, release_year, production_company]] X_train, y_train train[feature_cols], train[log_box] X_test, y_test test[feature_cols], test[log_box] # 随机森林基线 rf RandomForestRegressor(n_estimators200, max_depth8, random_state42) rf.fit(X_train, y_train) rf_pred rf.predict(X_test) # LightGBM lgbm lgb.LGBMRegressor(n_estimators500, learning_rate0.05, max_depth6, random_state42) lgbm.fit(X_train, y_train) lgbm_pred lgbm.predict(X_test) # 评估 for name, pred in [(RandomForest, rf_pred), (LightGBM, lgbm_pred)]: rmse mean_squared_error(y_test, pred, squaredFalse) r2 r2_score(y_test, pred) print(f{name} — RMSE: {rmse:.4f}, R²: {r2:.4f})参数上n_estimators从 200 到 500 足够再大收益很小。max_depth控制在 6 到 8防止单棵树太深导致过拟合。learning_rate设 0.05 配合 500 棵树是 LightGBM 的常用组合。如果 RMSE 在训练集上远低于测试集说明过拟合了优先降max_depth或加min_child_samples。3.2 交叉验证与超参数搜索的实操细节单次切分评估波动大我一般用 5 折交叉验证。注意时间序列数据不能随机 K 折要用TimeSeriesSplit保证每折的训练集都在测试集之前。超参数搜索用GridSearchCV或Optuna但毕业设计时间有限手动调几组关键参数就够了。from sklearn.model_selection import TimeSeriesSplit, GridSearchCV tscv TimeSeriesSplit(n_splits5) param_grid { n_estimators: [300, 500], max_depth: [5, 7], learning_rate: [0.03, 0.05] } grid GridSearchCV( lgb.LGBMRegressor(random_state42), param_grid, cvtscv, scoringneg_root_mean_squared_error, n_jobs-1 ) grid.fit(X_train, y_train) print(f最佳参数{grid.best_params_})TimeSeriesSplit的每一折都保证训练集时间早于测试集这比随机 K 折更贴近真实预测场景。scoring用负 RMSE因为 sklearn 的 GridSearchCV 默认最大化分数所以取负值。n_jobs-1用满 CPU 核数加速搜索。4. 预测平台的后端搭建Flask 接口与模型加载4.1 用 Flask 暴露预测接口的最小实现模型训练完要能对外提供服务最常见的是 Flask 写一个/predict接口。前端传 JSON 格式的电影特征后端加载训练好的模型文件返回预测票房。注意模型文件用joblib或pickle保存不要每次请求都重新训练。from flask import Flask, request, jsonify import joblib import numpy as np app Flask(__name__) # 启动时加载模型和标准化器 model joblib.load(lgbm_model.pkl) scaler joblib.load(scaler.pkl) feature_cols joblib.load(feature_cols.pkl) app.route(/predict, methods[POST]) def predict(): data request.get_json() # 构造特征向量 features [] for col in feature_cols: features.append(data.get(col, 0)) features np.array(features).reshape(1, -1) # 标准化 features scaler.transform(features) # 预测并还原对数 log_pred model.predict(features)[0] box_office np.expm1(log_pred) return jsonify({predicted_box_office: round(box_office, 2)}) if __name__ __main__: app.run(debugTrue, port5000)这段代码的关键是feature_cols的顺序必须和训练时完全一致否则预测结果会错乱。我一般把特征列名也保存下来加载时按顺序构造向量。debugTrue只在开发时用部署时要关掉。4.2 前端页面与接口联调的注意点前端可以用 Vue 或 React 写一个表单用户输入预算、类型、档期等点击预测后调用/predict。注意跨域问题Flask 需要加flask-cors否则浏览器会拦截请求。另外前端传的类别字段要和训练时的 one-hot 列对应比如类型 “Action” 要转成genre_Action: 1其他类型列置 0。这一步容易出错建议在后端做一层映射前端只传原始值。from flask_cors import CORS CORS(app) # 类别映射示例 genre_map {Action: genre_Action, Comedy: genre_Comedy, Drama: genre_Drama} app.route(/predict, methods[POST]) def predict(): data request.get_json() features {col: 0 for col in feature_cols} # 数值特征直接填 for col in [budget, popularity, runtime, release_month]: features[col] data.get(col, 0) # 类别特征做 one-hot genre data.get(genre, ) if genre in genre_map: features[genre_map[genre]] 1 # 后续预测逻辑同上这样前端只需要传{budget: 100000000, genre: Action, ...}后端自动展开成模型需要的特征向量。联调时先用 Postman 测接口确认返回正常再接前端。5. 票房预测平台避坑记录数据泄漏、维度错位和部署翻车5.1 现象测试集 R² 高达 0.95答辩时被问住了原因把上映后的票房相关字段如首周票房、总观影人次当成了特征。解决检查特征列确保所有字段在预测时点之前就能获取。我一般会列一个时间线标注每个字段的可得时间上映后的数据一律剔除。5.2 现象Flask 接口返回的预测值忽大忽小和训练时完全对不上原因前端传的特征顺序和训练时的feature_cols不一致或者标准化器没有正确加载。解决把feature_cols保存成文件加载时按顺序构造向量标准化器用训练集拟合的那个不要重新 fit。5.3 现象LightGBM 训练时报 “Input contains NaN”原因target encoding 后测试集出现训练集没见过的公司映射后产生 NaN。解决map之后用fillna填充训练集均值或者直接丢弃这些样本。更稳妥的做法是用category_encoders库的 TargetEncoder它自带平滑和缺失值处理。5.4 现象本地跑得好好的部署到服务器后接口超时原因Flask 默认单线程模型加载在每次请求时重复执行。解决模型在应用启动时加载一次用全局变量持有生产环境用 gunicorn 加多 workergunicorn -w 4 -b 0.0.0.0:5000 app:app。5.5 现象前端页面预测按钮点击没反应原因跨域请求被浏览器拦截或者接口返回的 JSON 字段名和前端读取的不一致。解决后端加flask-cors前端在fetch里检查response.ok打印错误信息。字段名统一用下划线避免大小写混淆。6. 模型可解释性与预测结果验证SHAP 值分析和残差检查答辩时老师最常问“你这个模型为什么预测这个票房”光说“LightGBM 精度高”是不够的。我一般用 SHAP 值做特征重要性分析它能给出每个特征对单条预测的贡献度。比如某部电影预测票房高SHAP 会告诉你主要是预算和档期拉高的类型的影响很小。这样解释起来有理有据。import shap explainer shap.TreeExplainer(lgbm) shap_values explainer.shap_values(X_test) # 全局特征重要性 shap.summary_plot(shap_values, X_test, plot_typebar) # 单条预测解释 shap.force_plot(explainer.expected_value, shap_values[0], X_test.iloc[0])TreeExplainer对 LightGBM 和 XGBoost 都适用计算速度快。summary_plot的 bar 图能看出哪些特征整体影响大force_plot能解释单条预测。注意SHAP 值反映的是特征对预测的贡献不是因果关系答辩时别说“预算导致票房高”要说“预算对预测结果有正向贡献”。残差检查也不能少。把测试集的预测值和真实值画散点图如果残差在低票房区间偏大说明模型对中小成本电影预测不准可以考虑分档训练或加更多特征。我一般还会看残差是否随预测值变化如果呈喇叭形说明存在异方差取对数那一步可能没做彻底。最后说个血泪经验毕业设计的代码要能一键跑通别把路径写死。我习惯在项目根目录放一个config.py所有路径用os.path.join拼接数据集和模型文件放在data/和models/下。这样换台电脑也能直接运行答辩现场不会翻车。希望帮到你。本文还有配套的精品资源点击获取