☰
用Python构建电影票房数据分析系统:从采集到预测的完整指南
2026/10/1 20:43:23 网站建设 项目流程

简介:基于Python的电影票房数据分析系统设计与实现源码包,面向课程作业、毕业设计及数据分析入门等场景,适合需要快速搭建完整项目的学习者。系统围绕票房数据采集、导入、统计展示与对比分析等环节,实现用户注册登录与权限管理、实时票房统计、折线图柱状图饼图等趋势展示、按省市或影院查看地区分布,并支持同档期多片对比等实用功能。压缩包共578个文件、约26.75MB,核心代码涵盖Python后端脚本(py/pyc)、Vue前端组件(vue/js)、HTML页面与CSS样式,同时包含SVG图表素材、SQL数据库初始化脚本、运行安装批处理工具,以及Word版说明文档。整体目录结构清晰,便于直接导入开发环境运行调试和二次开发;已有166人学习下载。资源附带完整项目源码与部署脚本,可帮助理解从数据获取、清洗、存储到前端可视化展示的全链路设计;也可作为课程设计、毕业设计的项目基础,方便在此基础上扩展算法或改进界面,为论文撰写或答辩提供工程参考。

1. 先立框架:一部电影的票房是怎么被拆开看的

如果你准备开发一个“基于 Python 的电影票房数据分析系统”,第一件事不是急着写爬虫或者画图,而是先定义清楚这个系统到底输出什么。票房不是一个孤零零的数字,而是一组可以被拆解、排序、关联的字段:上映日期、档期、类型、评分、首周票房、总票房。系统设计的目标,就是把这些原始字段变成两类结论——某类电影为什么卖得好、某部新片大概能卖多少。这个方向适合三类人:刚学完 pandas 想找完整练手项目的开发者,需要做数据分析项目交作业的学生,以及想看数据找发行感觉的从业者。本文给出的落地路径包含 6 个环节,从工程骨架一直走到预测验证,每段都能照着复现。

2. 系统骨架先行:技术选型与工程目录设计

2.1 为什么用 Python 当主引擎而不是 Excel 和 BI 工具

票房数据看起来是整齐的表格,真拿到手你会发现,原始数据里同时混着“3.2亿”“8645万”“上映日期没写全”的脏值。Excel 处理万行级别就开始卡,Power BI 这类工具做交互图和报表很顺手,但你要在里面做特征工程和回归预测,就得绕回 Python。pandas 负责框选、清洗、聚合,matplotlib 和 seaborn 负责出图,scikit-learn 负责建模。用 Python 的另一个原因是整个数据链路都在同一个语言环境里,从 requests 抓页面到 sklearn 出模型,中间不需要把数据导出成 CSV 再倒腾进另一个工具,类型和空值的语义不会在传递中丢失。

选择 Python 还有一个现实原因:招聘市场里“Python数据分析”是高频词,这套系统的代码在求职或课程设计展示时可以直接讲成完整项目。用 Python 做电影票房数据分析系统,既要能分析,也要能设计——这里的“设计”更多指工程上把模块切干净,而不是画一堆类图。很多项目死在代码全揉在一个 notebook 里,后面想改一处要翻几百行,改完又不知道会影响哪一段。模块化不是为了好看,是为了让每条数据从采集到预测只走一条固定路径,出错时能定位到具体环节。

2.2 系统模块划分:采集、清洗、分析、预测四段式

整个项目按“数据单向流动”的原则组织。scraper 只负责把公开页面变成原始 CSV;cleaner 只读 CSV,输出结构化 parquet 或新 CSV;analysis 读干净数据做统计;predict 在特征表之上做模型训练和推理。模块之间不互相调用,只通过中间文件交换。这样设计的好处是:任何一个环节出错,只需要重跑那一段,不影响已经生成的数据。比如清洗规则改了,analysis 和 predict 可以继续用上一次的干净数据对比差异,不用从头抓一遍网。

movie_boxoffice_system/ ├── data/ │ ├── raw/ # 原始下载数据,只读不写 │ └── processed/ # 清洗后的结构化数据 ├── src/ │ ├── scraper.py # 采集公开票房榜单页 │ ├── cleaner.py # 清洗与特征构造 │ ├── analysis.py # 多维统计与可视化 │ ├── predict.py # 模型训练与推理 │ └── utils.py # 公共工具,如单位转换 ├── notebooks/ # 探索用脚本,不进主流程 ├── output/ │ ├── figures/ # 输出图表 │ └── report/ # 汇总报告 └── requirements.txt

这个目录结构是从实际项目里收敛出来的。data/raw 必须保持原始文件不改动,因为采集脚本跑出来的数据是“证据”,清洗错了还有机会回滚;notebooks 只承担探索功能,摸索阶段画图没问题,但正式运行以 src 下的脚本为准。常见项目把这两者混在一起,结果就是模型引用了一个手工改过的 CSV,复现时找不出数据是谁改的。如果你拿到的已经是整理好的票房数据集,不需要自己抓取,那就把 scraper.py 删掉,数据直接放 data/raw,从 cleaner.py 开始接入。

2.3 环境准备:虚拟环境与依赖清单

我一般会把依赖隔离进虚拟环境,尤其当电脑里同时开着多个数据分析项目时,pandas 版本冲突是真实会出现的问题。创建环境并安装依赖的命令在项目根目录执行:

python -m venv venv source venv/bin/activate # Windows 上改用 venv\Scripts\activate pip install -r requirements.txt

requirements.txt 我这里不锁小版本,安装时以当前最新稳定版为准。原因是指定太老的版本会和新的 Python 解释器冲突,指定太新的版本又可能在半年后装不上,保持主版本最新是维护成本最低的做法。

pandas numpy requests beautifulsoup4 matplotlib seaborn scikit-learn

依赖为什么要这几样?requests 和 beautifulsoup4 用来抓公开票房榜单页,pandas 负责清洗和透视,seaborn 基于 matplotlib 画分类型对比图,scikit-learn 提供回归模型和交叉验证。numpy 是 pandas 的底层依赖,虽然不直接调用,但必须显式声明,避免环境安装顺序导致 numpy 版本被隐式抬高或压低。装完后可以用一行命令验证环境:python -c "import pandas, sklearn; print(pandas.__version__)",能正常输出就说明环境没问题。

3. 数据从哪来:采集与清洗,做成一张能用的票房总表

3.1 公开榜单页采集:用 requests 拿到表格

这里按公开票房榜单页的场景说明。目标页面通常是一张表格,包含片名、总票房、上映日期、类型、评分等字段。用 requests 带参数请求,再用 BeautifulSoup 解析表格,是稳定的做法,不需要模拟浏览器,也不依赖重型渲染框架。如果你所在地区无法访问某些站点,不要绕路,直接换一个可访问的公开数据源,国内也有整理好的票房榜单页可供采集。爬取公开页面时控制请求频率,间隔 1~2 秒,遵守站点条款,这是做数据采集的基本红线。

import requests from bs4 import BeautifulSoup import pandas as pd from time import sleep def fetch_public_pages(url, pages=1, per_page=30, delay=1.5): """抓公开榜单表格,返回 DataFrame。delay 是请求间隔秒数。""" rows = [] for page in range(1, pages + 1): params = {"page": page, "limit": per_page} resp = requests.get(url, params=params, timeout=10) resp.raise_for_status() soup = BeautifulSoup(resp.text, "html.parser") for tr in soup.select("table tbody tr"): cells = [td.get_text(strip=True) for td in tr.find_all("td")] if cells: rows.append(cells) sleep(delay) return pd.DataFrame(rows)

逻辑上,每页请求之间睡 delay 秒,是给目标站点留出间隔,避免短时间高频请求被封或给服务器添压。timeout=10 表示连接和读取超时都按 10 秒算,网络抖动时会抛出异常而不是让进程卡死。resp.raise_for_status() 在状态码 4xx/5xx 时直接中断,防止把错误页当成正常数据解析。返回值用 DataFrame 接着往下传,表格缺失字段时会得到 None,交给 cleaner 处理。

这里有一个常见误用:对选中的 HTML 写死“第 3 列是片名,第 5 列是票房”。一旦目标页结构调整,解析就全盘翻车。更稳的做法是把表头也抓下来,用列名定位字段,这样列位置变了也能对上。如果采集过程中被断网或者页面改版,不要立刻重跑——先把已经抓到的部分存成 CSV,再考虑增量补抓,这是数据采集里最基础的后悔药。

3.2 清洗:票房单位统一、日期解析、空值处理

拿到表格后,问题就开始暴露了:“3.21亿”“9458万”“1,470元”混在一列里;上映日期有“2023-12-01”也有“2023.12.01”。清洗的目标是把这堆字符串变成数值和日期,否则后续所有聚合计算全是错的。票房单位不统一是这类数据最典型的脏点,直接画折线图会出现断崖式跳动,看起来像某个月的电影全部扑街,实际只是单位换算漏了。

import pandas as pd def normalize_boxoffice(df): df = df.copy() df.columns = ["rank", "title", "boxoffice", "release_date", "genre", "score"] def to_yuan(value): value = str(value).strip().replace(",", "") if value.endswith("亿"): return float(value[:-1]) * 1e8 if value.endswith("万"): return float(value[:-1]) * 1e4 return float(value) df["boxoffice_yuan"] = df["boxoffice"].map(to_yuan) df["release_date"] = pd.to_datetime(df["release_date"], errors="coerce") df = df.dropna(subset=["release_date", "boxoffice_yuan"]) return df

to_yuan 函数先去掉千分位逗号,再按后缀决定乘数。亿和万在票房数据里是最常见的单位,处理时要连“1,470元”这种带逗号的裸数值一起考虑。release_date 用 to_datetime 解析,errors="coerce" 表示无法解析的日期变成 NaT,而不是抛异常终止整个流程;NaT 在 dropna 时被一并去掉。这里的取舍是:宁可丢弃没有日期的少量记录,也不让脏日期污染档期分析。

清洗完要做一次质量校验:清洗后的行数应该和清洗前相差不大,如果 dropna 把超过 10% 的数据洗没了,说明采集环节有字段缺失,应该回头检查 scraper 而不是直接往下跑。我习惯在 cleaner 里加一句print(f"保留 {len(df)} / {len(raw)} 行"),每次跑完都确认这个比例,数据流水线的第一道门槛就从这里算起。

3.3 构造口径字段:年度、档期、类型分组

原始表里一般没有“档期”这一列,只有发布日期。做分析时,需要把从日期派生的字段固定下来,这样后面的分析代码不用重复写同样的逻辑。档期的定义需要先想明白:春节档是农历日期,不是固定公历,粗略方案用月份近似,精细方案要维护一张节假日公历日期表。

def build_features(df): df = df.copy() df["year"] = df["release_date"].dt.year df["month"] = df["release_date"].dt.month df["is_holiday"] = df["month"].isin([1, 2, 5, 10]) df["score_group"] = pd.cut(df["score"], [0, 6, 8, 10], labels=["low", "mid", "high"]) return df

is_holiday 用月份近似判断档期,1 月和 2 月含元旦和春节,5 月有五一,10 月有国庆。严格口径需要考虑农历春节的公历日期漂移,这套系统如果只做整体趋势分析,月份近似够用;如果要精细分析春节档,就得额外维护一个“公历节假日表”,把精确日期传进来替换 is_holiday。score_group 把评分切成低、中、高三档,方便后面做分组聚合,pd.cut 的 bins 边界按业务认知定为 0-6、6-8、8-10,实际分布如果偏移可以再调。

建完特征,建议把结果落盘成 parquet 格式,压缩率高,而且能保留 categorical 和 datetime 类型,不会像 CSV 那样把日期读回字符串。落盘这一步看似多余,却能省掉后续每次分析都从头跑一遍 clean+feature 的时间。

4. 让票房说话:多维拆解与可视化,找出影响票房的因子

4.1 分布规律:票房市场是二八分化,不是正态分布

很多新人拿到票房数值,第一件事是画一张直方图看分布。画完会发现右拖尾极其严重,少数爆款把均值拉得很高,均值并不代表“常见水平”。更稳的拆解是先排序,看头部占比,这个数字直接决定了后面所有分析的视角——如果你关注平均票房,你会被几部超级大片骗得以为所有电影都能卖钱。

share = df.sort_values("boxoffice_yuan", ascending=False) top_20_count = int(len(share) * 0.2) top_20_share = share.head(top_20_count)["boxoffice_yuan"].sum() / share["boxoffice_yuan"].sum() print(f"前20%的影片贡献了约{top_20_share:.1%}的总票房")

这个比例在不同年份的票房市场里通常都很高,常年在 70%~85% 之间。打印出来之后,结论就出来了:这个系统如果用于商业分析,重点观察的是头部影片,而不是平均票房。后续所有分组对比,建议都先做头部和长尾分开再看,否则类型均值会被几部超级大片带偏。长尾部分的电影再分析才有价值,因为那里藏着被埋没但口碑不错的中小成本片。

4.2 类型与档期对比:箱线图比柱状图更诚实

比较不同类型票房的平均水平,柱状图加均值是最常见的做法,但会掩盖离散程度。箱线图能同时看到中位数、四分位和离群点,更适合右上角有异常大票房的场景。类型字段通常是字符串的枚举,比如“剧情”“喜剧”“动作”,用 seaborn 的 boxplot 可以一行画出来,关键是要处理中文显示。

import seaborn as sns import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False fig, axes = plt.subplots(1, 2, figsize=(14, 5)) sns.boxplot(data=df, x="genre", y="boxoffice_yuan", ax=axes[0]) axes[0].set_yscale("log") axes[0].set_xticklabels(axes[0].get_xticklabels(), rotation=45) sns.boxplot(data=df, x="is_holiday", y="boxoffice_yuan", ax=axes[1]) axes[1].set_yscale("log") axes[1].set_xticklabels(["非档期", "档期"]) plt.tight_layout() plt.savefig("output/figures/genre_holiday_box.png", dpi=150)

这里有两个参数值得解释。set_yscale("log") 是因为票房跨了几个数量级,线性坐标上非头部影片几乎挤成一条线,对数尺度下箱体和须的对比才看得清。dpi=150 是给图片定分辨率,报告中要打印或缩放展示时不至于糊。如果不用对数尺度,你会看到几个触须无限长的箱体,剩下大部分样本的差异被压成看不见的短线。数据可视化里,尺度选择直接影响读图人的判断,输出图表前一定要先问自己:我是在展示分布,还是在展示个别极值?

类型和档期两幅图并列,作用不只是看单张图,而是对照着读——2023 年春节档的动画片票房中位数,和国庆档的剧情片可能差出几倍。箱线图把群组差异和组内离散一起呈现,这是柱状图做不到的。如果发现某类影片样本量太少,图上的箱体会很扁,解读时要标注样本数,避免拿 3 部片子代表整个类型。

4.3 相关性矩阵:数值字段间的线性关系

把票房、评分、年份这些数值字段放一起算相关系数,是检查“什么因素和票房走得近”最直接的做法。pandas 一行就能出矩阵,但解读要小心。相关系数高不代表因果,相关系数低也不代表没关联,这两句话放在票房这种噪声很大的数据上尤其适用。

num_cols = ["boxoffice_yuan", "score", "year", "month"] corr = df[num_cols].corr() print(corr["boxoffice_yuan"].sort_values(ascending=False))

结果里票房和评分的相关系数通常是正的,但不会高到足以单独解释票房。这说明评分的方差只能解释票房变化的一部分,单抓一个特征去预测是不成立的。有些入门项目看到相关系数 0.3 就下结论“评分越高票房越高”,这是过度解读。相关系数只衡量线性相关,年度、档期这类离散变量还要回到分组对比里看。把相关矩阵当成线索,而不是证据,这条准则在建模时同样适用。

相关性分析的意义在于筛特征,不在结论。如果两个特征之间的相关系数超过 0.8,建模时它们就是冗余的,比如“票房”和“分账票房”高度共线,同时放进模型反而会把回归系数搅乱。相关性矩阵在这里的价值是给第 5 章建模前做一轮变量初筛,把明显冗余的字段排除在特征列表之外。

5. 避坑与排查:跑通数据流程时最常遇到的5个问题

数据项目里,跑通了才是起点,不出错是不可能的。这一章写我调整这套电影票房数据分析系统时反复踩过的 5 个坑,每条按“现象 -> 原因 -> 解决”列出来,后面做类似项目可以直接对照排查。前四个坑集中在数据准备阶段,最后一个坑在建模阶段,都是真实改代码时踩过的泥。

5.1 CSV 用 Excel 打开后乱码

现象:采集到的 CSV 中文列名用 pandas 读没问题,但用 Excel 打开全是乱码,或者转给别人后变成问号。

原因:pandas 写文件默认编码是 utf-8,而 Excel 老版本默认按 gbk 解析,编码对不上就花屏。

解决:写 CSV 时统一编码为 utf_8_sig。这个编码会带上 BOM 头,Excel 识别 BOM 后就不会按 gbk 猜了。

df.to_csv("data/raw/boxoffice.csv", index=False, encoding="utf_8_sig")

另外,读取时也用明确编码,不要依赖默认值。如果已经乱码,用pd.read_csv(path, encoding="gbk")可能救回来,前提是数据当初确实以 gbk 落盘。这套系统如果想发给同伴协作,编码统一是第一步,否则对方打开花的直接过来问“你这数据是不是坏了”,解释成本比写代码还高。

5.2 票房单位混用导致折线图出现诡异跳水

现象:按月份聚合票房后画折线,某几个月突然低得离谱,像数据缺失。

原因:原始字段里有的票房是“1.2亿”,有的是“8000万”,清洗时只按字符串转 float,单位换算漏了,数值直接差了一万倍。

解决:用第 3 章里的 to_yuan 函数强制统一单位。额外建议在清洗后加一条断言,检查是否还有字段以“万”“亿”结尾,防止新增数据绕过规则。

assert not df["boxoffice"].astype(str).str.endswith(("万", "亿")).any(), "存在未换算的票房"

这条断言成本低,效果明显,任何清洗函数跑完都能立刻自检。数据流水线最怕的是“看起来没问题”,断言就是给流程上的一道保险。如果你在排查时发现票房数值比合理区间离谱,先查单位,再查精度。我因为这个坑重新跑过两遍全流程,从那以后清洗模块里必定带单位断言。

5.3 日期解析失败导致档期分析为空

现象:pivot_table 按 is_holiday 分组时,档期组全是空,或者报 ValueError。

原因:上映日期里有“2023/12/01”和“2023.12.01”混排,pd.to_datetime 默认格式解析不了其中一种,errors="coerce" 把整批转成 NaT,dropna 后档期分析数据被抽干。

解决:to_datetime 先指定主格式,再兜底解析,或者用 infer_datetime_format 让它自动推断。

df["release_date"] = pd.to_datetime(df["release_date"], errors="coerce", infer_datetime_format=True)

实际中命中的是第二种写法。infer_datetime_format 遇到格式不一致时会尝试常见分隔符,速度稍慢但更稳妥。排错时先看df["release_date"].isna().mean(),如果缺失比例异常,再用df[df["release_date"].isna()]["release_date"].head(20)看看没解析出来的是什么格式,别整文件重来。日期格式问题在国产数据源里极常见,因为很多人手工录入时没按统一格式。

5.4 内存占用过高,处理大量数据直接死机

现象:加了几列衍生特征后,脚本跑到一半内存占满,电脑卡死。

原因:pandas 的 object 类型列每格都压着 Python 字符串对象,几千部电影感觉不大,但如果按日粒度去采集,数据量会到百万行,object 和 float64 的差别就放大了。

解决:读取时先指定 dtypes,把重复的文本字段降成 category 类型,日期解析成 datetime64,数值字段用 float32 而不是默认 float64。

df = pd.read_csv("boxoffice.csv", low_memory=False) df["genre"] = df["genre"].astype("category") for col in ["score", "boxoffice_yuan"]: df[col] = df[col].astype("float32")

category 类型在“类型”“档期”这种取值有限的字段上能省不少内存。换 dtype 之后,同样的数据量内存占用能降一半以上,而且不会改变分析结果的语义。排查时如果脚本突然变慢,先看df.info(memory_usage="deep")里哪列占得最多,通常就是 object 列。特别注意 pandas 2.x 里读 CSV 默认对字符串列做字符串推断,不指定 dtype 就会变成 object。

5.5 用默认参数跑回归,预测结果几乎不变

现象:训练完回归模型,预测值集中在一个窄区间,仿佛模型在“躺平”。

原因:票房数值横跨数量级,评分、年份和票房量纲差异巨大,线性类模型在量纲差异下会偏向取值大的特征;更常见的是没处理极端值,少数爆款主导了损失,模型整体变成了“预测均值”。

解决:先做特征缩放,再用对离群点更稳健的模型或变换目标变量。例如对票房取对数,或者用 Huber 回归损失。

import numpy as np from sklearn.preprocessing import StandardScaler y = np.log1p(df["boxoffice_yuan"]) # 对票房取对数 X = df[["score", "year", "month"]].fillna(0) X_scaled = StandardScaler().fit_transform(X)

取对数之后,爆款和普通片的差距从几百倍压缩到几倍,模型不再被少数样本牵着走。这个坑在票房预测里尤其明显,因为票房天然是长尾分布,谁的模型能感知到这一层,预测效果才会有明显差别。排查时先看预测值的 std,如果比训练集 y 的 std 小一个数量级,基本就是目标变量没处理。模型“躺平”不是模型坏了,是数据形态没配合好。

6. 把系统再往前推一步:验证预测结果与落地使用技巧

6.1 时间序列划分验证:不要随机打散

票房预测和一般表格分类不同,电影的上映日期有时间顺序,随机把样本打散,会让模型用“未来”的数据去预测“过去”的票房,看起来评估分数很高,上线后就露馅。验证时用 TimeSeriesSplit 按时间顺序切训练集和测试集,既保持时序又复用了样本。

from sklearn.model_selection import TimeSeriesSplit from sklearn.ensemble import RandomForestRegressor tscv = TimeSeriesSplit(n_splits=5) for fold, (train_idx, val_idx) in enumerate(tscv.split(X)): model = RandomForestRegressor(random_state=42, n_estimators=200) model.fit(X.iloc[train_idx], y.iloc[train_idx]) print(f"fold{fold}: R2 = {model.score(X.iloc[val_idx], y.iloc[val_idx]):.3f}")

n_estimators=200 是随机森林的树数量,这里取 200 是效率与稳定性的平衡,调成 500 可能只有 1% 以内的收益,却要让每折训练时间翻倍。输出每一折的 R2,如果前几折高后几折低,说明模型的规律在新样本上漂移,需要回去看特征有没有过期。这个验证方式不适合调参调过头,每一折的测试集严格在训练集之后,才能模拟“用它预测未来”的真实场景。

6.2 让预测结果回到业务语言

模型输出的是对数票房,报告里要对数换回“亿元”,并给出预测区间而不是单一值。我习惯用分位数表示“合理波动范围”,再配合“预期中性/乐观/悲观”的描述,直接交给运营同事判断。

pred_test = np.expm1(model.predict(X.iloc[val_idx])) print(f"单部预测:{np.mean(pred_test)/1e8:.2f} 亿")

我踩过最深的教训是:把 R2 做得好看,就满怀信心把预测结果贴到报告里,结果被问“区间呢?依据呢?”时哑口无言。业内的票房看的是档期和类型的组合预期,不是一个单点。从那以后我习惯了对所有预测附上样本量和区间,项目结论才真正站得住脚。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询