ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python电影票房分析:从数据清洗到预测模型实战

Python电影票房分析:从数据清洗到预测模型实战 简介基于Python的电影票房数据分析系统设计与实现专为课程作业与毕业设计打造面向计算机相关专业学生也可供电影行业从业者、投资者参考其业务分析路径。项目聚焦票房数据全流程处理支持用户注册登录与角色权限管理覆盖实时票房抓取、人工导入、趋势图统计、城市区域分布以及同档期影片对比等功能是一套具备完整业务闭环的数据分析展示方案。资源包共578个文件压缩后26.75MB主要文件类型包括Python后端脚本py、Vue前端组件vue、JavaScript逻辑文件、SVG图标、SQL数据库脚本以及doc/docx说明文档前后端分层清晰并带有初始化、安装与运行脚本可快速搭建本地演示环境。目前已有166人学习下载。通过本项目可掌握数据采集、清洗、入库、基于ECharts的可视化、接口联调等常见技术点完整源码和清晰的目录结构方便二次修改适合直接作为课程设计或毕业设计答辩展示项目。另外数据库初始化脚本与一键运行脚本能显著降低环境配置成本目录按前后端与工具文档模块化拆分便于按需学习和替换。1. 电影票房数据分析比预测票房更值钱的是看懂票房为什么涨跌电影票房数据分析说白了就是拿 Python 把一部电影从定档、开画到下映的全生命周期数据拉通拆成排片、上座率、口碑、档期、类型这些可量化的变量再回答一个实际问题这片子为什么卖得好、为什么扑街、下一部该怎么办。我在做这个方向时最大的感受是很多人一头扎进“预测票房”里最后发现预测模型在首周末之后就废了反而是把历史数据清洗干净、把趋势和边界条件讲清楚对排片和宣发的帮助更直接投入产出比更高。这篇笔记就把我常用的分析链路——数据清洗、特征拆解、可视化和系统封装——按能直接复现的方式写出来。适合刚接触 Python 数据分析的从业者也适合想拿真实项目练手的同学新手能跟步骤走熟手可以直接跳到第 5 章看坑。2. 搭建分析底座票房数据从哪来怎么洗成能用的数据集2.1 数据源选型公开数据集优先爬虫只做补充做票房数据分析第一个绕不开的问题是数据从哪来。我一般把数据源分成三级第一级是公开数据集比如 Kaggle 上的 TMDB 电影数据集、国内一些高校和培训机构整理的 csv 票房历史数据优点是字段齐全、格式稳定、省去了大量的清洗时间缺点是更新滞后热门档期的新片基本没有。第二级是半结构化接口比如 TMDB API能按需拉取电影的基本信息、预算、评分但票房明细字段不全需要自己拼。第三级才是写爬虫去猫眼、淘票票、艺恩这些站点抓实时票房这也是最容易翻车的一层反爬策略、页面结构变动、数据字段不一致每一样都够折腾半天的而且合规风险要自己掂量。我个人的建议是第一版系统直接用公开数据集跑通全流程把分析模型和可视化框架搭扎实等整个链路稳定了再考虑用接口或爬虫做增量更新。这个顺序能帮你把“数据问题”和“分析问题”分开避免一上来就被脏数据劝退。具体到字段设计我一般会至少保留这些列电影名称、上映日期、下映日期、类型可能复合、制片国家/地区、导演、主演、总票房、首日票房、场均人次、排片占比、上座率、评分豆瓣/猫眼、评分人数、档期标签春节档/暑期档/国庆档/普通档。其中票房单位建议统一成“万元”日期统一成YYYY-MM-DD格式评分和评分人数分开存因为这两个字段一个反映口碑质量一个反映口碑扩散广度分析时经常要分开看。如果你打算用爬虫补充实时数据注意控制请求频率尽量用公开接口而不是绕过反爬手段这是底线。我见过太多人一上来就写高并发爬虫结果 IP 被封了数据没抓到还得花时间处理封禁问题得不偿失。2.2 数据清洗票房单位、日期格式和缺失值是三大硬伤公开数据集也未必干净。我做过的几个版本里票房单位不统一是最常见的坑——有的表用“万元”有的用“亿”有的是字符串带逗号比如12,345.6日期字段有2019-02-05也有2019/2/5还有20190205八位数字缺失值更玄学有的票房为空是因为电影还没下映有的是真没数据。这些如果不处理后面 group by 出来的结果全是错的。我的清洗脚本固定分三步走。第一步是统一类型和格式第二步是处理缺失值第三步是去重和异常值过滤。下面这段代码是第一步和第二步的常见做法import pandas as pd import numpy as np # 读取原始数据 df pd.read_csv(movie_boxoffice_raw.csv, encodingutf-8) # 统一票房单位为万元把亿转成万去掉千分位逗号 def normalize_boxoffice(value): if pd.isna(value): return np.nan if isinstance(value, str): value value.replace(,, ).strip() if value.endswith(亿): return float(value[:-1]) * 10000 if value.endswith(万): return float(value[:-1]) return float(value) return float(value) df[total_boxoffice_wan] df[total_boxoffice].apply(normalize_boxoffice) # 统一日期格式为 datetime df[release_date] pd.to_datetime(df[release_date], format%Y-%m-%d, errorscoerce) df[end_date] pd.to_datetime(df[end_date], format%Y-%m-%d, errorscoerce) # 处理缺失值票房缺失且下映日期存在 - 视为0未下映 - 保留NaN后续分析剔除 df.loc[df[end_date].notna() df[total_boxoffice_wan].isna(), total_boxoffice_wan] 0 # 去重同一部电影同一上映日期只保留一条记录 df df.drop_duplicates(subset[movie_name, release_date], keeplast) print(df.shape) print(df.dtypes)这段逻辑里normalize_boxoffice处理了最常见的单位混用问题errorscoerce会把无法解析的日期置为 NaT方便后面统一过滤。去重时keeplast是因为我假设后写入的数据更新、更准如果你用的是爬虫增量更新这个假设要按你的数据源实际情况调整。缺失值处理上“下映了但票房为空置为 0”这个规则只适用于确实没产生票房的电影如果你发现一部知名大片票房为 0那大概率不是真 0而是数据源缺失这时候要把这种记录单独拎出来人工核对不能一刀切。第三步异常值过滤我会放在特征工程之后做因为很多时候要结合类型和档期才能判断一个值是“异常”还是“真实”——比如春节档动画片票房冲到 10 亿放在全年看是异常值但放在春节档动画的交叉维度看就是正常波动提前删掉反而丢失了信息。这也是做数据分析的一个方法论先定分析的粒度再做异常值处理而不是反过来。3. 核心分析维度票房走势、档期和类型偏好怎么量化3.1 首日票房和次周跌幅衡量一部片子“后劲”的两个核心指标票房分析不能只看总票房一个数因为总票房是结果不是原因。我看一部电影的票房表现习惯先拆两个时间维度的指标首日票房开画当天的收入和次周跌幅第二周票房相对首周的下降比例。首日票房反映的是「映前热度」跟 IP 基础、主演号召力、宣发力度强相关次周跌幅反映的是「口碑转化」跌幅小说明口碑在发酵跌幅大说明是粉丝向电影路人盘接不住。这两个指标放在一起看能直接把电影分成四类高热高口碑首日高、跌幅小、高热低口碑首日高、暴跌、低热高口碑首日低、跌幅小甚至逆跌、低热低口碑全面扑街。分析代码也不复杂核心是构造一个按日汇总的透视表然后计算环比。下面是我常用的写法# 假设 df_daily 包含列movie_name, date, daily_boxoffice df_daily df_daily.sort_values([movie_name, date]) # 计算每部电影的首日票房上映第1天和次周票房第8~14天 first_day df_daily.groupby(movie_name).first().reset_index() first_day first_day.rename(columns{daily_boxoffice: first_day_boxoffice}) # 上周票房 第1~7天求和本周票房 第8~14天求和 df_daily[week_flag] df_daily.groupby(movie_name).cumcount() // 7 weekly df_daily.groupby([movie_name, week_flag])[daily_boxoffice].sum().reset_index() weekly_pivot weekly.pivot(indexmovie_name, columnsweek_flag, valuesdaily_boxoffice) weekly_pivot[week2_drop] (weekly_pivot[1] - weekly_pivot[0]) / weekly_pivot[0] # 合并首日票房和次周跌幅 result pd.merge(first_day[[movie_name, first_day_boxoffice]], weekly_pivot[[week2_drop]], onmovie_name)这里的cumcount() // 7是按记录顺序把每一天归入第 0 周或第 1 周前提是你的df_daily已经按电影和日期排好序并且每部电影每天都有一条记录。如果你的数据里有“某天无排片”导致的空行要先补零不然 cumcount 会错位。次周跌幅这个指标本质上是个比率我建议同时输出绝对票房本周票房金额和相对跌幅因为小体量电影的跌幅波动极大首周 10 万、次周 2 万就是 80% 跌幅但绝对值没什么参考意义要结合体量分桶看。3.2 档期和类型的交叉分析单一维度的结论基本是错的只看类型或者只看档期都会得出偏颇的结论。举一个典型例子单看“喜剧”平均票房可能不如“科幻”但这不代表做喜剧没前途——因为喜剧的发片量巨大、成本低、腰部作品多拉低了均值头部喜剧的回报率可能比科幻还高。要解决这个问题必须做交叉分析把档期切成几段春节档、暑期档、国庆档、贺岁档、普通档把类型按主类型提取然后用数据透视表看“某种类型在某个档期的票房中位数、头部占比、爆款率”。我常用的代码是这样# 提取主类型类型字段可能是剧情/喜剧/爱情这种复合格式 df[main_genre] df[genres].str.split(/).str[0] # 构造档期标签 def mark_schedule(date): month_day date.strftime(%m-%d) if 01-01 month_day 01-03: return 元旦档 if 02-01 month_day 02-15: return 春节档 # 这里按实际年份的农历日期调整示例用固定区间 return 普通档 df[schedule] df[release_date].apply(mark_schedule) # 交叉透视类型 x 档期统计票房中位数、样本量、亿元以上占比 pivot df.pivot_table( indexmain_genre, columnsschedule, valuestotal_boxoffice_wan, aggfunc[median, count, lambda x: (x 10000).mean()] ) print(pivot)注意mark_schedule里的春节档我用的是固定日期区间这是偷懒写法真实项目里春节档每年日期都不同农历正月初一到十五建议直接维护一张年份-春节起止日期的映射表用merge关联而不是写死函数。这段代码的输出能直接看出在普通档剧情片的中位数票房可能是喜剧的两倍但在春节档喜剧的中位数票房反而更高。这就是交叉分析的价值它帮你去掉“档期红利”这个混杂因素看到类型本身的相对竞争力。这个分析有个边界要说明白它是描述性分析不是因果推断。交叉表只能告诉你“春节档喜剧票房中位数高”不能告诉你“因为它是春节档所以票房高”——后者还需要排除档期内头部影片虹吸效应、票价上涨、场次供给增加这些干扰。我在实际项目里会把交叉表的结果当成“待验证假设”而不是“结论”下一步用具体案例去验证。4. 可视化与系统封装把分析结果变成排片和宣发能用的工具4.1 可视化选型matplotlib 打底pyecharts 做交互分析做完结果只有自己看得到那这个系统还停留在“脚本”阶段。要做成“系统”关键是把分析结果可视化、可交互、能按条件筛选。可视化选型上我一般分两档内部快速探索用 matplotlib seaborn胜在快、可控、不会因为数据量太大卡死对外演示或给业务方用用 pyecharts 生成 HTML 交互图表胜在自带缩放、悬停提示、时间轴滑动不需要前端写一堆代码。这里分享一个 matplotlib 画票房走势对比的常见模板适合同时看多部电影的票房曲线import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] # 解决中文乱码 plt.rcParams[axes.unicode_minus] False # 解决负号显示为方块 fig, ax plt.subplots(figsize(12, 6)) for movie in target_movies: sub df_daily[df_daily[movie_name] movie] # 把上映日期对齐为第 N 天而不是日历日期方便对比生命周期 sub sub.copy() sub[day_index] (sub[date] - sub[date].min()).dt.days 1 ax.plot(sub[day_index], sub[daily_boxoffice], labelmovie, linewidth2) ax.set_xlabel(上映天数) ax.set_ylabel(单日票房万元) ax.set_title(多部电影票房走势对比) ax.legend() plt.tight_layout() plt.savefig(boxoffice_trend.png, dpi150)这段代码的关键是day_index的构造——把日历日期替换成“上映第 N 天”这样暑期档上映的电影和国庆档上映的电影才能放在同一条时间轴上对比。如果不做这一步直接拿日期做 x 轴不同档期的电影天然错开曲线没法直接比较。另外plt.rcParams里中文字体设置几乎每次都要写不写就是满屏方块字这是 matplotlib 的老坑了。4.2 系统结构Flask 图表渲染前后端分离不要过度设计票房数据分析系统的“系统”二字不一定要上重型框架。我在实际项目中验证过一个 Flask 应用 预生成 HTML 图表 简单的筛选表单就能覆盖 90% 的使用场景。数据量在几万行以内完全不需要 Hadoop、Spark 这些数据分析和可视化都在内存里用 pandas 算完把图表结果输出成静态文件Flask 只负责把文件和筛选参数拼起来返回给浏览器这个架构简单、稳定、好维护。常见的最小实现是这样from flask import Flask, render_template, request import pandas as pd import json app Flask(__name__) df pd.read_csv(cleaned_boxoffice.csv, encodingutf-8) app.route(/) def index(): # 读取前端传来的筛选条件 genre request.args.get(genre, 全部) schedule request.args.get(schedule, 全部) filtered df.copy() if genre ! 全部: filtered filtered[filtered[main_genre] genre] if schedule ! 全部: filtered filtered[filtered[schedule] schedule] # 聚合计算各档期票房总和 result filtered.groupby(schedule)[total_boxoffice_wan].sum().reset_index() chart_data result.to_dict(orientrecords) return render_template(index.html, chart_datajson.dumps(chart_data, ensure_asciiFalse)) if __name__ __main__: app.run(debugTrue, host0.0.0.0, port8000)这个实现的取舍是用request.args从 URL 拿筛选参数前端就是一个带下拉框的表单提交后刷新页面后端重新计算并渲染。好处是逻辑全在后端前端模板只需要用 Vue 或原生 JS 把chart_data交给图表库渲染就行缺点是高并发场景下每次都重新聚合但内网工具一般同时在线不超过十个人这个性能完全够用。如果你想更省事可以把filtered.groupby(...)的结果直接转成 pyecharts 能接收的 JSON 结构后端生成完整 HTML 片段返回前端就只留一个div容器。这个阶段最大的陷阱是过度设计。我见过有人一上来就规划用户登录、权限管理、定时任务调度结果核心分析功能还没跑通先把系统架构搭了一堆没用的壳子。数据分析系统的核心交付物是“能回答业务问题的图表”不是“一个看起来像大厂中台的网站”。先把分析做透系统外壳越薄越好。5. 票房数据分析避坑指南5 条从数据到结论的踩坑记录5.1 现象首日票房数据比别的平台低一大截分析结论全偏原因不同数据源对“首日票房”的定义不一致——有的算凌晨点映有的算零点场有的只算正式开画日的 00:00 到 24:00还有的算到次日凌晨 2 点含午夜场。 解决统一口径在数据清洗阶段增加is_first_day标记字段并在分析脚本里写明口径定义。对比外部数据时先核对两部已知电影的数值确认口径一致再做全量分析。5.2 现象次周跌幅算出来是负的电影越卖越多原因跨年上映的电影比如 12 月底上映首周横跨两个自然周按自然周分组会拆出不完整周导致次周数据大于首周。 解决不要用自然周分桶用“上映第 N 天”的偏移量分桶也就是第 3.1 节里用cumcount() // 7的做法。如果必须按自然周汇报要把“上映首周不足 7 天”的电影单独打标。5.3 现象类型交叉表里动画片平均票房奇高结论不可信原因样本量太小的类型均值会被极端值拉飞。动画片头部爆款和腰部作品差距巨大均值被《哪吒》这类电影带高中位数才是更稳的统计量。 解决交叉分析时同时输出count和median并且只对样本量大于阈值我一般取 30的格子下结论。低于阈值的格子标注“样本不足仅供参考”不做排名。5.4 现象春节档标签打错2023 年和 2024 年的日期对不上原因农历日期每年都变用固定02-01到02-15这种区间切分档期碰到春节在 1 月下旬或 2 月中旬的年份就错位。 解决维护一张档期表包含年份、档期名、开始日期、结束日期用 SQL 的BETWEEN或 pandas 的merge_asof关联。别在分析代码里硬编码日期区间。5.5 现象预测模型训练集 R 方 0.9上线后一测就翻车原因特征里混入了“上映后才知道”的变量比如实际排片占比、首日口碑评分训练时这些特征是未来信息预测时拿不到模型当然崩。 解决做特征工程前把每个特征按“上映前可得/上映后可得”打标预测模型只用上映前特征IP 类型、主创阵容、档期、宣发指数等。这本质上是个数据泄露问题我在第一次做票房预测时就掉进这个坑里后来养成习惯建特征时先问一句“这个数现在拿得到吗”。6. 进阶从“看历史”到“猜未来”用一个线性回归验证你的分析假设分析做到后面业务方一定会问一个问题“那下一部片子能卖多少”这时候简单的历史统计就不够用了需要建一个粗粒度预测模型。我常用的做法是线性回归加 log 变换标签不追求精确值目标是给出一个量级判断和主要影响因子排序。from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.preprocessing import StandardScaler import numpy as np # 构造特征档期热度、类型、主演历史票房均值、IP改编、上映月份 features [schedule_hot, main_genre_encoded, star_avg_boxoffice, is_sequel, release_month] X df[features].fillna(0) y np.log1p(df[total_boxoffice_wan]) # log变换压制极端值 # 划分训练集和测试集 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) # 标准化特征 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) model LinearRegression() model.fit(X_train_scaled, y_train) # 输出特征重要性线性回归用系数绝对值近似衡量 coef_df pd.DataFrame({ feature: features, coef: model.coef_ }).sort_values(coef, keylambda x: x.abs(), ascendingFalse) print(R^2:, model.score(X_test_scaled, y_test)) print(coef_df)注意log1p变换是票房预测的常规操作因为票房分布严重右偏直接拟合原始值会把精力浪费在少数爆款上。预测结果要解读成“量级”——比如模型输出 9.2真实票房是exp(9.2) - 1 ≈ 9900 万你要说“预计在 1 亿上下”而不是说“预计 9900 万整”。另外这个模型只能用在续集、有明确主创班底的商业片上对于纯新人团队、无 IP 的原创剧本线性回归的预测区间太宽基本等于猜这时候直接说“历史数据无法支撑预测”反而更专业。我在这类项目上养成的习惯是每出一个分析结论都留一个“验证位”——要么用留出集回测要么等下一部电影上映后回填实际数据对比。数据分析和做工程不一样工程上线了能自测分析结论对错要等时间给答案这时候流程里有没有留验证位就是专业和业余的分界线。希望这篇笔记能帮你少走几段弯路把票房分析做成能落地、能解释、能验证的实用系统。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进