ARTICLE DETAIL

资讯详情

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

电影票房预测实战:从数据清洗到LightGBM模型部署

电影票房预测实战:从数据清洗到LightGBM模型部署 简介这是一套面向毕业设计、课程设计与期末大作业的电影票房预测系统完整项目基于Python与机器学习算法实现解决了从原始电影数据到票房预测结果的全流程建模问题。项目参考IMDb、豆瓣等数据源涵盖了数据清洗、异常值处理、文本特征编码、特征工程、模型选择与评估等关键环节并给出线性回归、决策树、随机森林等算法的实现思路。资源共59个文件包含16个Python源码含数据可视化、协同过滤、SVD、KNN等推荐算法、16个CSV数据文件、23个PNG可视化图表、2个Markdown分析文档及1份PDF说明打包大小约30.95MB目录结构清晰便于按模块学习。目前已吸引522人学习下载。项目代码注释详细新手也能快速部署使用文档中还包含电影数据分析与推荐系统模块可作为机器学习实战范本对毕业设计答辩或竞赛评优有较高参考价值。1. 毕业设计选电影票房预测选的是数据、调参与答辩说服力电影票房预测是机器学习在结构化数据上的经典落地场景也是这几年毕业设计里出镜率很高的选题。它的优势很直接数据集好找、特征维度可控、模型可以从线性回归一路试到随机森林和 XGBoost能完整经历数据清洗、特征工程、模型训练、误差分析和系统封装的全流程比那些一上来就做图像识别的题目更容易做出闭环也更容易在答辩时讲清楚。这个标题里提到的“源码文档数据”核心就是交付一套从爬虫到 Flask 展示页面的可运行系统模型本身反而不是最拉开差距的部分。适合对 Python 语法有基本了解、想把机器学习流程在一周内真跑通的同学也适合作为课程设计的二次开发底子。下面按我实际做这类项目的顺序来拆数据、特征、模型、封装最后是几个踩过的坑。2. 拿到一份能用的票房数据公开数据源、爬虫脚本与清洗逻辑2.1 票房数据集从哪里来接口选型与字段取舍常见做法是优先找开放的电影数据接口或整理好的数据集比如 TMDB、IMDb 这类带电影元信息的平台再配合猫眼或豆瓣的票房数据做补充。我一般不推荐直接搜“票房数据集.csv”下载来用因为网上流传的版本要么字段残缺要么年份老旧要么存在大量重复条目后期清洗成本反而更高。更稳妥的路径是先用接口批量抓取电影基础信息再按片名和时间做匹配把票房字段补上。这里的核心问题不是“能不能爬到”而是“爬哪些字段”。我做的时候只保留以下维度片名、上映日期、类型可多选、制片国家/地区、时长、预算制作成本、导演、主演按番位取前3、发行公司、档期属性暑期档/春节档/平日档。票房字段则按需求选“首周票房”或“总票房”毕业设计一般用总票房因为回归目标更直观也和公开榜单口径一致。import requests import json import time import csv # TMDB API 获取电影元信息按上映年份分页拉取 def fetch_movies_by_year(year, api_key, pages5): base_url https://api.themoviedb.org/3/discover/movie movies [] for page in range(1, pages 1): params { api_key: api_key, language: zh-CN, primary_release_year: year, sort_by: revenue.desc, # 按票房降序先拿到高票房样本 page: page } resp requests.get(base_url, paramsparams, timeout10) if resp.status_code 200: data resp.json() movies.extend(data.get(results, [])) time.sleep(0.5) # 控制请求频率避免触发限流 return movies # 这里只拉取了电影基础列表后续还要按 movie_id 补导演和演员信息 # 如果对实时性要求不高可以只保留字段齐全的样本减少二次请求逻辑说明这个脚本按年份拉取影片列表并用revenue.desc排序目的是让高票房样本优先进入数据集。按年分批拉取的好处是容易控制数据规模和请求次数不会一次把接口打爆。参数说明pages控制每个年份拉取多少页默认5页约100部time.sleep(0.5)是必要的限速没有这句话连续高频请求大概率被临时封 IPlanguagezh-CN只影响返回字段的中文化不影响核心数值。2.2 清洗不是删空行是把脏数据修成模型能吃的样子爬下来的数据不会直接能用。最典型的问题是票房字段为空、上映日期格式不统一、预算单位为美元且大量缺失、类型是[动作, 喜剧]这样的列表字符串。很多人在这里直接 dropna样本量立刻少一半后面模型就瞎了。我的处理顺序是先统一日期格式并计算“上映到某参考日期的天数”作为辅助特征再把类型字段拆成多列虚拟变量最后针对缺失值分列处理——预算用同档期影片的中位数填充导演和主演缺失则单独标记一列“是否缺失”。不要用 0 去填充演员这类语义特征模型会把“缺失”误学成“某种固定状态”。import pandas as pd import numpy as np from datetime import datetime df pd.read_csv(movies_raw.csv) # 统一日期并提取档期月份 df[release_date] pd.to_datetime(df[release_date], errorscoerce) df[release_month] df[release_date].dt.month # 类型字段从 动作/喜剧 拆成多个 0/1 列 genres df[genres].str.split(/, expandTrue).stack().unique() for g in genres: df[fgenres_{g}] df[genres].fillna().apply(lambda x: 1 if g in x else 0) # 预算缺失用中位数填充并保留缺失标记 df[budget_missing] df[budget].isna().astype(int) df[budget] df[budget].fillna(df[budget].median()) # 主演缺失同样加标记 df[top_actor_missing] df[top_actor].isna().astype(int)逻辑说明pd.to_datetime(errorscoerce)会把无法解析的日期置为 NaT避免后续整形报错类型字段按/拆分后逐列映射成 0/1是机器学习里处理多标签字段最直接的手段fillna选择中位数而不是均值是因为预算分布右偏严重均值会被几部超级大片带偏。这一步做完数据就从“能看”变成了“能进模型”。3. 特征工程与模型选型票房预测到底该用哪个机器学习算法3.1 从原始字段到有效特征导演票房均值、档期热度与档期间隔电影票房预测的特征工程有个核心原则特征必须在上映前就可知。这句话决定了你不能用“上映后一周的评分”“观众评论数”这类信息做特征否则就构成数据泄露模型在训练集上分数很好看真实场景里完全没法用。我一贯的做法是构造三类特征。第一类是导演和主演的历史票房均值。计算方式是把训练集里所有同导演的电影票房取均值作为该导演的“号召力分”。这里要注意做目标编码时的防过拟合我一般只在训练集内计算再映射到验证集和测试集避免用全量数据统计导致信息穿越。如果某个导演在训练集里一部作品都没有就用全局票房均值兜底。第二类是档期热度。把上映日期映射为“是否暑期档”“是否春节档”“是否国庆档”三个二值特征。国内票房受档期影响极大春节档和暑期档的平均票房量级和平日档差了数倍这几乎是模型里最强的一批特征。第三类是上映前宣传热度。这块数据比较难拿可以退而求其次用“从定档日到上映日的间隔天数”作为代理变量间隔越长通常意味着宣发投入越高。# 构造导演历史票房均值特征 def build_director_feature(df_train, df_test): director_stats df_train.groupby(director)[box_office].agg([mean, count]) director_stats.columns [director_avg_box, director_movie_count] # 训练集映射 df_train df_train.merge(director_stats, ondirector, howleft) # 测试集里出现了训练集中不存在的导演用训练集全局均值填充 df_test df_test.merge(director_stats, ondirector, howleft) global_mean df_train[box_office].mean() df_test[director_avg_box] df_test[director_avg_box].fillna(global_mean) return df_train, df_test # 注意这里只演示了导演特征主演特征是同样的思路逻辑说明这段代码体现的其实是目标编码加全局均值回退的经典做法在结构化数据里非常实用。howleft保证不丢失原始样本的条数.fillna(global_mean)是处理“冷启动导演”的兜底策略。这里有个细节值得留意director_stats只能在训练集上计算如果把测试集也并进来统计均值验证指标就不能反映真实上线效果。3.2 传统机器学习模型与深度学习模型的取舍为什么先用 LightGBM很多同学一上来就想着用深度学习来做票房预测把问题复杂化。但票房预测是典型的“样本量一两千条、特征几十个”的中小规模表格任务深度学习在这里通常打不过经过调参的树模型而且答辩时解释性差。我在这个题目上推荐直接用两类模型做对比线性回归/岭回归作为基线LightGBM 或 XGBoost 作为主力模型。前者用来证明特征有效、给出可解释的系数后者用来冲指标。选择树模型的核心理由是它能自动处理特征之间的非线性交互比如“春节档 高预算 知名导演”的组合效应线性模型很难表达这种交互。同时它对缺失值处理更加鲁棒即使前面清洗漏掉了一些脏数据模型也不至于直接崩掉。import lightgbm as lgb from sklearn.model_selection import train_test_split from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score import numpy as np # 划分特征和标签 feature_cols [c for c in df.columns if c not in [box_office, title, release_date]] X df[feature_cols].select_dtypes(include[np.number]) # 只保留数值特征 y df[box_office] # 按时间排序后切分避免随机打乱造成信息穿越 df[_idx] range(len(df)) train_idx df[df[release_date] 2022-01-01].index test_idx df[df[release_date] 2022-01-01].index X_train, X_test X.loc[train_idx], X.loc[test_idx] y_train, y_test y.loc[train_idx], y.loc[test_idx] model lgb.LGBMRegressor( n_estimators800, learning_rate0.05, max_depth6, num_leaves31, subsample0.8, colsample_bytree0.8, random_state42 ) model.fit(X_train, y_train) y_pred model.predict(X_test) print(fMAE: {mean_absolute_error(y_test, y_pred):.2f}) print(fRMSE: {mean_squared_error(y_test, y_pred, squaredFalse):.2f}) print(fR2: {r2_score(y_test, y_pred):.4f})逻辑说明切分方式特意用了“按时间顺序”而不是train_test_split默认的随机切分这是因为电影票房存在明显的年度和档期演化规律随机打散会让模型偷看到未来的数据得到的评估指标偏乐观。参数说明max_depth6和num_leaves31控制模型复杂度适合千级样本量再深就过拟合subsample0.8与colsample_bytree0.8是随机抽样比例n_estimators800配合learning_rate0.05算是树模型里比较保守的配置。如果想跑一组对比实验把同一个 X_train/y_train 塞进LinearRegression和RandomForestRegressor输出三个模型的 MAE 和 R²这张对比表可以直接放进毕业论文的“实验设计”章节。对比的意义不是为了证明 LightGBM 碾压一切而是为了说明“不同复杂度模型在这个任务上的表现差异”这是答辩老师最爱问的问题之一。4. 把模型变成系统Flask 封装预测接口与可视化展示4.1 模型持久化与 Flask 最小接口训练完模型之后光有一个 .ipynb 文件是不够的毕业设计要交付的是“系统”。最省事的方案是用 Flask 起一个本地服务把模型用pickle或joblib保存下来前端页面提交电影特征后端返回预测票房。这一层不需要做得重但必须把流程走通。我通常会在项目目录里建三个文件train_model.py负责训练并保存模型、app.py负责启动 Flask 服务、templates/index.html负责页面展示。这样结构清晰答辩时也能按模块讲解。# train_model.py 片段训练结束后保存模型和特征列名 import joblib joblib.dump(model, box_office_model.pkl) joblib.dump(feature_cols, feature_columns.pkl) # 保存特征列名很重要前端提交数据时才能按顺序对齐# app.pyFlask 预测接口 from flask import Flask, request, jsonify, render_template import joblib import pandas as pd import numpy as np app Flask(__name__) model joblib.load(box_office_model.pkl) feature_cols joblib.load(feature_columns.pkl) app.route(/, methods[GET]) def index(): return render_template(index.html) app.route(/predict, methods[POST]) def predict(): data request.get_json() # 前端传来的字段可能不齐逐个补齐为0保证维度一致 input_dict {col: data.get(col, 0) for col in feature_cols} df_input pd.DataFrame([input_dict]) pred model.predict(df_input)[0] return jsonify({predicted_box_office: round(float(pred), 2)}) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)逻辑说明data.get(col, 0)这一步是保险操作避免前端少传字段时后端直接报 KeyError。pd.DataFrame([input_dict])包一层方括号是为了让单个样本变成二维表结构符合 sklearn/LightGBM 的输入要求。返回结果用jsonify序列化前端 JavaScript 直接消费。前端页面不必复杂一个表单加上几个下拉框就够。我一般会在页面里放一排输入框预算、导演历史均值、档期月份、是否春节档、是否暑期档、主演历史均值、发行公司经验值再加一个“预测”按钮。这里有个细节前端输入的特征必须和训练时的feature_columns.pkl完全对应否则维度对不上模型直接报 ValueError。这也是我把特征列名单独持久化的原因。!-- templates/index.html 核心片段 -- form idpred-form label预算美元/label input namebudget typenumber value50000000 label导演票房均值/label input namedirector_avg_box typenumber value200000000 label是否春节档/label select namegenres_春节档? option value1是/option option value0否/option /select button typesubmit预测票房/button /form p idresult/p script document.getElementById(pred-form).addEventListener(submit, async function(e) { e.preventDefault(); const formData new FormData(e.target); const data Object.fromEntries(formData.entries()); for (let key in data) data[key] parseFloat(data[key]); const resp await fetch(/predict, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify(data) }); const result await resp.json(); document.getElementById(result).innerText 预测票房: result.predicted_box_office; }); /script逻辑说明Object.fromEntries(formData.entries())把表单内容转成对象再统一转成数字类型避免 POST 请求传字符串导致模型输入类型不匹配。e.preventDefault()阻断表单默认提交跳转保证页面停留在当前界面展示结果。4.2 用 flask_cors 解决跨域问题与本地演示的注意事项如果在答辩演示时把前后端拆到不同端口或者直接把前端静态页拖进浏览器打开Flask 默认是不允许跨域请求的。解决方式很简单装一个flask-cors扩展在初始化时全局开启即可。另外要提醒的是演示机器上如果端口被占用Flask 会启动失败这时改端口号就行不需要改代码。from flask_cors import CORS app Flask(__name__) CORS(app) # 允许所有域的跨域请求本地演示足够参数说明CORS(app)默认放行所有来源这是本地演示最方便的方式生产环境肯定要做域名白名单但毕业设计答辩场景下全局放行没有安全风险。端口建议固定写 5000并在文档里写清楚启动命令是python app.py访问地址是http://localhost:5000。5. 票房预测系统避坑指南数据泄露、极端值与调参过度的真实教训5.1 数据泄露那条看不见的“未来信息”现象模型在验证集上 R² 高达 0.93输出预测曲线几乎贴合真实值但换到 2023 年或 2024 年的新片数据上做简单测试预测值严重偏离误差甚至比直接猜票房均值还大。原因训练或验证集里混入了上映后才知道的信息最常见的是把电影评分、评论数、上映首日排片比当作特征这些变量本身就和票房高度相关模型直接拿结果预测结果。另外切分数据时用了随机打乱而不是按时间排序也会让模型“看到”未来的票房分布。解决把特征限定在上映前可获得的字段这是硬性标准。另外验证集切分必须按时间顺序拿过去预测未来而不是在时间序列数据上随机抽样。这里有个自查技巧把特征列名全部列出来问自己“如果我是在电影上映当天做预测能不能拿到这个字段”答不上来的就删掉。5.2 票房极端值一部《战狼2》毁掉整个模型现象平均绝对误差达到 1.8 亿模型预测中低成本电影时误差只有几千万但预测到高票房影片时误差直接拉满。查看残差直方图时发现有几个点的残差是其他样本的 20 倍以上。原因票房分布极度右偏少数大制作占据票房总量的大头。模型会为迁就这些极端样本而牺牲大量常规样本的拟合精度这是均方误差损失函数的天然缺陷。解决我推荐分三步处理。第一步对票房标签做对数变换np.log1p缩小量纲差异模型改在 log 域上回归第二步用对数域上的 MAE/RMSE 做评估指标第三步如果对阈值型预测更感兴趣可以把票房离散化成“高/中/低”三档做分类任务拉平问题难度。这一步在答辩时也很加分因为老师会看到你理解了数据本身的性质而不是机械套用模型。y_train_log np.log1p(y_train) y_test_log np.log1p(y_test) model.fit(X_train, y_train_log) y_pred_log model.predict(X_test) y_pred np.expm1(y_pred_log) # 还原到原始量纲 print(fLog MAE: {mean_absolute_error(y_test_log, y_pred_log):.4f})逻辑说明.log1p对 0 值友好expm1是对称还原函数。注意评估时要在 log 域上看误差而不是还原后再算原始误差不然极端值会重新主导。5.3 调参过度追求高 R²模型精度成了玄学现象用网格搜索把 LightGBM 的max_depth调到 12、n_estimators调到 3000、learning_rate调到 0.01训练集 R² 接近 0.99测试集 R² 反而从 0.72 掉到 0.61。原因树模型在小样本上复杂度太高几乎记住了训练集里的所有模式和噪声。电影票房数据只有千级样本到五千级样本模型一旦开大过拟合非常凶猛。解决控制模型复杂度先用默认参数跑通再调一两个关键参数。我的经验值是max_depth不超过 8num_leaves不超过 64n_estimators在早停机制下自动决定。用 LightGBM 的早期停止代替手动指定迭代次数在验证集上评估每 50 轮的表现连续 100 轮不提升就停止训练。这个做法比盲目调参可靠得多。from sklearn.model_selection import train_test_split # 从训练集中再切一份做早停验证 X_train_inner, X_val, y_train_inner, y_val train_test_split( X_train, y_train_log, test_size0.15, random_state42 ) model lgb.LGBMRegressor( n_estimators3000, # 设大上限让早停来截断 learning_rate0.05, max_depth6, num_leaves31, subsample0.8, colsample_bytree0.8, random_state42 ) model.fit( X_train_inner, y_train_inner, eval_set[(X_val, y_val)], eval_metricmae, callbacks[lgb.early_stopping(stopping_rounds100, verboseTrue)] )参数说明n_estimators设大但不让它跑满真正的迭代次数由早停决定stopping_rounds100表示连续 100 轮验证集 MAE 不下降就停止eval_metricmae是回归任务的核心监控指标比默认的 l2 更抗极端值干扰。5.4 演员和导演的缺失值处理不当现象预测样本数量少、票房接近中位数的小成本电影时误差反而很大。打开特征贡献度后发现top_actor_missing和director_missing两个标记的权重异常高模型似乎在用“缺失”本身做判断而不是忽略缺失。原因缺失演员和导演信息的小成本电影通常也意味着低宣发投入、小制作规模这类电影的票房普遍偏低模型学到了“缺失低票房”的关联。但这并不是稳定规律一旦有例外样本出现模型就崩。解决与其用标记列不如直接把缺失的演员用“未知演员”字符串表示让分组统计时自然聚合到一起而不是单独开一列。另一个方案是删除那些演员和导演同时缺失的样本这类样本通常在公开数据里质量也差删掉对模型稳健性更友好。我后来统一用“未知”填充并关闭缺失标记问题就缓解了。5.5 特征顺序不一致导致模型报错现象训练完成保存模型后重启 Flask 服务前端输入数据点预测后端报ValueError: Number of features of the model must match the input核对半天也不知道哪里传错了。原因保存模型时没有同步保存特征列顺序。pandas 在训练时会把列名排序后输入模型而前端手动输入时字段顺序没有经过排序对齐维度明明相同顺序却是乱的。解决在train_model.py中把feature_cols列表单独序列化保存Flask 端加载后按列表顺序构造输入样本不要依赖字典的自然顺序。这是新手最容易忽略但线上系统里必须处理好的问题。6. 答辩加分技巧用特征重要性解释模型给预测结果补误差区间到这一步系统已经能跑通了。但我建议你在交付之前再做两件事这两件事能明显提升毕业设计的“完成度”也是答辩时最容易和老师展开讨论的点。第一件是输出特征重要性并配上可解释性说明。LightGBM 自带feature_importances_用model.booster_.feature_importance(importance_typegain)获取每个特征的增益贡献再到特征列表里完成名称映射。这里我习惯做一个一眼能看懂的表格按重要性排序展示前 10 个特征并在每个特征后面补一句业务解释。比如“主演历史票房均值”重要性排最高业务解释就是“观众对演员的认知度显著影响首周开画规模”。import pandas as pd importance model.booster_.feature_importance(importance_typegain) feat_imp_df pd.DataFrame({ feature: feature_cols, importance: importance }).sort_values(importance, ascendingFalse).head(10) print(feat_imp_df.to_string(indexFalse))# 给预测补一个误差区间使用验证集残差的标准差 from sklearn.metrics import mean_squared_error import numpy as np y_val_pred_log model.predict(X_val) residual_std np.std(y_val_log - y_val_pred_log) def predict_with_interval(input_dict, model, feature_cols, residual_std): df_input pd.DataFrame([{col: input_dict.get(col, 0) for col in feature_cols}]) pred_log model.predict(df_input)[0] pred np.expm1(pred_log) lower np.expm1(pred_log - 1.96 * residual_std) upper np.expm1(pred_log 1.96 * residual_std) return pred, lower, upper逻辑说明residual_std是验证集上对数票房残差的标准差近似认为预测误差服从正态分布用 1.96 倍标准差构造约 95% 的置信区间。真实票房落在区间内的比例会接近 95%这个口径写进文档里能让老师直观感受到你理解模型的不确定性。注意这里构造的区间是在对数域对称、原始域非对称的这是一个值得写进论文的小细节。第二件是做一个“预测误差可视化”页面把验证集的真实票房和预测票房画成散点图对角线附近是预测准确的样本偏离对角线越远误差越大。再画一张误差分布的直方图看到大多数样本误差集中在小范围顺便标出误差最大的几条影片名。我用自己代码里常配的方案实现过这套可视化非常简单用matplotlib画图保存成 PNG 放进项目static目录然后在index.html里追加一个img标签展示不需要额外搭建图表框架。import matplotlib.pyplot as plt y_test_pred_log model.predict(X_test) y_test_pred np.expm1(y_test_pred_log) y_test_actual np.expm1(y_test_log[test_idx]) plt.figure(figsize(6, 6)) plt.scatter(y_test_actual, y_test_pred, alpha0.6) plt.plot( [y_test_actual.min(), y_test_actual.max()], [y_test_actual.min(), y_test_actual.max()], ls--, colorred ) plt.xlabel(Actual Box Office) plt.ylabel(Predicted Box Office) plt.savefig(static/pred_vs_actual.png, dpi120)这段代码的要点是把“真实值”和“预测值”画到同一坐标系里对角线越密集说明模型越准。答辩时拿着这张图解释“哪些类型预测得好、哪些偏差大”比讲一堆损失函数术语要直观得多。最后说一个我自己的习惯项目收尾前一定要从头按 README 完整跑一遍。从创建虚拟环境、安装依赖、运行爬虫、训练模型、启动 Flask 到浏览器点预测每一步都记录时间。分类变量里出现训练集没见过的新值也是上线后大概率遇到的情况处理方式是在 Flask 端先做一次合法值校验不合法就给定一个默认特征向量并返回提示避免接口报错。这个“从零到预测”的复现清单既是交付文档的一部分也是你自己对系统边界最真实的体检。希望这一套流程和避坑记录能帮到你把票房预测这个题目做得更像一个完整产品而不只是一份代码。本文还有配套的精品资源点击获取
返回列表