ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

电影票房数据分析系统设计与实现:从脏数据到可视化看板

电影票房数据分析系统设计与实现:从脏数据到可视化看板 简介这是一份基于Python的电影票房数据分析系统完整项目资源面向计算机相关专业的毕业设计、课程作业及对数据可视化感兴趣的开发者。系统涵盖用户注册登录与权限管理、票房数据采集与导入、实时票房与趋势图表展示、同档期对比及地区分布分析等核心模块可帮助理解从数据抓取、存储到可视化呈现的完整闭环。资源包共578个文件、约26.75MB以Vue前端组件、Python后端逻辑与数据脚本为主包含92个vue文件、58个py文件及对应pyc编译文件另有大量svg图标、png/jpg图片、js/css样式及HTML页面附SQL数据库脚本、YAML配置与一键安装、运行批处理结构清晰便于直接部署学习。已有166人学习下载适合需要快速搭建项目基架、参考前后端交互与数据分析思路的读者可从中获取完整的系统设计文档与可运行代码。1. 先想清楚电影票房数据分析系统解决什么问题从一张票房表到能用的看板“电影票房数据分析”如果只用来画一张票房 Top 10 的柱状图那根本轮不到“系统设计”这四个字。我接触过的几个课设和毕设项目真正卡人的是数据链路日期有“2024-05-01”也有“2024/5/1”票房有“1.5亿”也有“15000万”同一部影片在不同数据来源里的片名还不完全一致。把这些脏数据变成一张能回答问题的事实表再用 Python 做聚合、对比和可视化才是这套系统的核心价值。这篇文章按我自己的实现顺序展开先定数据层和存储再做清洗与分析最后收敛到避坑和可交付。适合正在做类似系统设计、想少走弯路的人。2. 系统设计与数据层字段、主键、数据来源和存储选型这类系统最忌讳一上来就写画图代码。票房数据的口径、粒度、主键如果没定清楚后面每一张图都可能得出互相矛盾的结论。我一般先把系统拆成数据层、分析层、展示层再把事实表字段和存储方式定下来最后才开始写 Python 处理逻辑。2.1 单机架构把系统边界画清楚后端、分析脚本和可视化分开“系统设计”听起来重但毕业设计和课程设计不需要微服务也不需要搞实时数仓。常见的可靠做法是单机架构数据文件放在 data 目录清洗脚本独立成模块分析逻辑单独放Flask 或 Streamlit 只负责展示。这样老师问起“某个指标怎么算出来的”你能直接定位到对应函数而不是在一坨 Notebook 里翻单元格。我建议的最小目录结构如下box_office_system/ ├── data/ │ ├── box_raw.csv │ └── box_office.db ├── src/ │ ├── clean_data.py │ ├── analysis.py │ └── app.py ├── output/ │ └── charts/ └── requirements.txt这个结构的逻辑是data 放原始 CSV 和清洗后落库的数据src 下每个脚本只做一件事output 只放图表和导出报表。clean_data.py负责把脏数据变成可分析的表analysis.py负责指标计算app.py负责启动界面。这样分开以后哪怕后面换了数据来源也不需要重写分析逻辑。有人会把清洗和分析全写在一个文件里省事是省事但数据一旦变化你要同时改清洗、改聚合、改图表排错成本翻倍。我自己的习惯是函数尽量单一一个函数只做日期解析另一个只做单位换算测试的时候也能单独验证。2.2 票房事实表字段设计主键选错了后面全在收拾残局票房数据最核心的表不是某部电影的总票房而是“某天某影院某部电影的票房明细”。这种按时间、按主体逐行记录的表在数据建模里叫事实表。设计这张表时最重要的不是字段多而是粒度清楚。粒度就是一行代表什么是一天的全国总票房还是某天某片某影城的票房。我常用的一张票房事实表字段如下字段名类型粒度说明movie_idTEXT唯一影片不要直接用片名做主键movie_titleTEXT影片维度冗余字段便于人工核对stat_dateTEXT天统一存成 YYYY-MM-DDprovince / cityTEXT地域没有地域数据时可留空cinema_idTEXT影院可配合影院维表使用screeningsINTEGER日场次排片场次audience_cntINTEGER日人次观影人次box_officeREAL元统一按“元”存储avg_priceREAL日平均票价可后算occupancyREAL日上座率可后算主键建议用movie_id stat_date cinema_id。如果拿到的数据只有全国每日票房没有影院维度那主键就要退化成movie_id stat_date并且先按影片和日期做一次汇总避免同一部电影同一天出现多行。要是数据来自多个平台还要再加一个source_id字段进主键。对应的建表 SQL 可以是这样CREATE TABLE box_office_daily ( movie_id TEXT NOT NULL, movie_title TEXT NOT NULL, stat_date TEXT NOT NULL, province TEXT, city TEXT, cinema_id TEXT, screenings INTEGER, audience_cnt INTEGER, box_office REAL, avg_price REAL, occupancy REAL, PRIMARY KEY (movie_id, stat_date, cinema_id) );这里把stat_date定义为 TEXT 而不是 DATETIME是因为票房数据本身只到“天”这个粒度用 YYYY-MM-DD 字符串做范围查询和排序都稳定也不会受时区影响。等数据进到 pandas 里再转成日期类型做计算存储层保持简单。2.3 数据来源选型CSV导入、公开API与脚本采集怎么选很多人在这一步纠结要不要写爬虫。我的判断是如果目标是完成一个数据分析系统优先用 CSV 或公开历史数据爬虫只做补充不做主链路。因为爬虫会遇到反爬、字段缺失、编码错乱、接口签名等一系列问题这些问题和“票房分析”本身没有关系却会消耗掉大半时间。三种来源的对比很直接来源适合场景主要坑点CSV 离线数据课设、毕设、复现需要确认字段说明和单位公开 API少量动态数据有访问限制字段可能变更脚本采集补充缺失字段反爬、页面改版、维护成本高如果老师要求必须有“爬虫模块”我建议把它做成独立的spider.py爬完直接落成一个和 CSV 结构一致的中间文件不要让它和清洗分析逻辑耦合。这样即使网站改版你也只需要改爬虫不影响分析部分。2.4 存储选型SQLite、MySQL 与 Parquet毕业设计该选哪个存储方案我一般直接推荐 SQLite。它不是玩具支持 SQL、支持事务、单文件备份pandas 读写都方便。MySQL 适合多人并发访问但部署和授权多一层折腾Parquet 适合大规模列式存储但没法直接跑 SQL。对于票房数据这个体量SQLite 是最稳的选择。读写示例很简单import sqlite3 import pandas as pd conn sqlite3.connect(data/box_office.db) df pd.read_sql_query(SELECT * FROM box_office_daily;, conn) conn.close()sqlite3.connect里的路径就是数据库文件位置read_sql_query把 SQL 查询结果直接变成 DataFrame用完记得close。我习惯把连接封装成函数避免每个页面都写一遍连接逻辑。后续所有分析都基于这张表清洗结果也通过df.to_sql写回去保证原始数据和加工数据分离。3. 用pandas做票房清洗与预处理从原始表到分析宽表原始票房数据很少是干净的。这一章把清洗路径拆成五步每一步都能单独跑也能单独验证。下面代码都基于 pandas假设你已经把原始数据放到data/box_raw.csv。3.1 先用pandas读入原始数据并做字段体检清洗之前先看数据长什么样。我通常会先打印 shape、字段类型、缺失值统计而不是直接画图。这一步能省掉后面一半的翻车。import pandas as pd raw pd.read_csv(data/box_raw.csv, encodingutf-8-sig) print(shape:, raw.shape) print(columns:, raw.columns.tolist()) print(raw.dtypes) print(缺失统计:) print(raw.isna().sum())encodingutf-8-sig是为了兼容 Windows 下 Excel 导出的 CSV文件头带 BOM 时也不会解析出错。raw.dtypes能快速看出哪些数字列被读成了 object例如box_office如果是 object基本可以判断里面混了“亿”“万”这样的单位后缀。缺失统计则是决定后面是用dropna还是fillna的依据。3.2 日期解析把混合格式统一成YYYY-MM-DD日期格式不统一是票房数据的第一大坑。2024-05-01、2024/5/1、20240501三种写法可能同时出现在同一列里。pandas 自动推断时遇到混用格式往往会全部变成 object排序就会变成字符串排序画出来的趋势图是乱的。我习惯写一个独立解析函数按常见格式逐个尝试from datetime import datetime def parse_stat_date(value): if pd.isna(value): return pd.NaT value str(value).strip() for fmt in (%Y-%m-%d, %Y/%m/%d, %Y.%m.%d, %Y%m%d): try: return datetime.strptime(value, fmt).date() except ValueError: continue return pd.NaT raw[stat_date] raw[stat_date].map(parse_stat_date)map会对每一行调用函数返回NaT表示解析失败。这里刻意不用pd.to_datetime的errorscoerce一把梭是因为coerce会把2024-13-40这种非法日期也静默变成空值你根本不知道是哪几行出了问题。用循环尝试格式解析失败至少能定位到具体值和具体行。解析完之后一定要检查 min 和 max确认没有跑到 1970 年或超出数据范围print(raw[stat_date].min(), raw[stat_date].max())3.3 票房金额标准化把“1.2亿”和“15000万”统一成元票房列是最容易出“数字爆炸”的地方。有的行是1.2亿有的行是15000万还有的是120,000,000。直接用pd.to_numeric会得到一堆 NaN。我用的单位换算函数是显式分支避免用eval执行字符串表达式安全也能自解释def parse_box_office(value): if pd.isna(value): return float(nan) text str(value).strip().replace(,, ).replace(元, ) if text.endswith(亿): return float(text[:-1]) * 100_000_000 if text.endswith(万): return float(text[:-1]) * 10_000 return float(text) raw[box_office] raw[box_office].map(parse_box_office) raw[avg_price] pd.to_numeric(raw[avg_price], errorscoerce)text[:-1]是去掉最后的“亿”或“万”再乘对应的倍数。这里统一换算成“元”存储而不是“万元”因为后续计算总票房、平均票价、同比环比时单位统一能少很多*10000的临时换算。avg_price用errorscoerce遇到空值变成 NaN后续计算也不会报错。换算完可以做一次描述性统计判断数量级是否正常print(raw[box_office].describe())单日单部电影票房如果在几十万到几亿之间基本合理如果出现几十亿或几百亿一定是单位换算漏了。3.4 去重与主键校验不先查重复后面关联全翻倍重复数据可能导致总票房虚高。常见情况是同一个票房快照被采集脚本运行了两遍同一行数据在 CSV 里出现两次。如果不去重直接 groupby结果会翻倍。我一般先查重复行再决定保留哪一行key [movie_id, stat_date, cinema_id] dup_mask raw.duplicated(subsetkey, keepFalse) print(重复行数:, dup_mask.sum()) print(raw[dup_mask].sort_values(key).head(20)) raw raw.drop_duplicates(subsetkey, keeplast)keepFalse是为了把重复的所有行都标出来方便看完整上下文drop_duplicates里的keeplast表示保留最后一次采集到的数据。如果数据里没有cinema_id这个 key 必须改成[movie_id, stat_date, source_id]否则同一天来自不同平台的数据会被误删。3.5 构建分析宽表把事实表和维表merge起来清洗完事实表后还需要把电影的基础信息关联进来比如上映日期、影片类型。这样后续算“上映第几天票房”“首周票房占比”时不需要每张图都重新 join 一遍。常见做法是先做每日聚合再关联维表daily ( raw.groupby([movie_id, movie_title, stat_date], as_indexFalse) .agg( box_office(box_office, sum), screenings(screenings, sum), audience_cnt(audience_cnt, sum), ) ) daily[avg_price] daily[box_office] / daily[audience_cnt] movie_dim raw[ [movie_id, movie_title, release_date, genre] ].drop_duplicates(movie_id) analysis pd.merge( daily, movie_dim, onmovie_id, howleft, suffixes(, _dim) )groupby里的as_indexFalse会让分组字段保留为普通列后面 merge 更方便。.agg后面的元组写法是 pandas 2.x 推荐的方式左边是列名右边是聚合函数。这里按movie_id stat_date聚合是因为后续分析基本都是“一部电影一天一个值”。drop_duplicates(movie_id)用来保证维表里每个电影只出现一次。最后merge用howleft确保 daily 表的所有行都保留维表缺少信息只显示 NaN不会丢数据。4. 票房分析模块实现核心指标、时间序列与Top N计算清洗完成只是开始。分析模块要做的是把“数据”变成“结论”。这一章我按照最常用的几个分析需求展开核心指标、票房衰减曲线、档期对比、Top N 榜单和周环比。4.1 核心指标口径总票房、平均票价、场均人次先算影片级别的汇总这是后面所有榜单的基础。需要注意平均票价和场均人次要自己定义清楚不然答辩时说不出口径。summary ( analysis.groupby(movie_id, as_indexFalse) .agg( movie_title(movie_title, first), release_date(release_date, first), total_box_office(box_office, sum), total_audience(audience_cnt, sum), total_screenings(screenings, sum), active_days(stat_date, nunique), ) ) summary[avg_price] summary[total_box_office] / summary[total_audience] summary[audience_per_screening] summary[total_audience] / summary[total_screenings]指标口径上总票房就是所有日期的box_office求和平均票价用总票房除以总观影人次而不是把每天的avg_price再做平均后者会被上映天数长短干扰场均人次用总观影人次除以总场次反映单位排片效率。active_days是统计这部电影在榜单里出现了多少天后面算日均票房时要用。4.2 单部影片票房衰减曲线按上映第几天计算电影票房不是平均分布通常首周末高、之后逐步下跌。要对比两部不同档期的电影不能直接拿自然日期对比而要让它们都回到“上映第 1 天”这个起跑线上。实现方式是把影片按movie_id筛出来再算release_dayone_movie analysis[analysis[movie_id] M001].copy() one_movie[release_day] ( pd.to_datetime(one_movie[stat_date]) - pd.to_datetime(one_movie[release_date]) ).dt.days 1 trend ( one_movie.groupby(release_day, as_indexFalse)[box_office].sum() )release_day从 1 开始代表上映首日。减去release_date后得到的是相差天数加 1 是为了让首日显示为第 1 天而不是第 0 天。这里会暴露一个隐藏问题如果原始数据缺失某些天曲线中间会出现断层。处理时不要着急填充先确认那几天是没采集还是票房为 0判断清楚再决定是否fillna(0)。4.3 档期同期对比把日期对齐到“上映日序数”两部电影档期不一样直接画两条按自然日期的折线没有可比性。把横轴换成上映第几天再画在一起才能看出第二部是不是比第一部后劲更足。def add_release_day(df): out df.copy() out[release_day] ( pd.to_datetime(out[stat_date]) - pd.to_datetime(out[release_date]) ).dt.days 1 return out selected pd.concat([ add_release_day(analysis[analysis[movie_id] M001]), add_release_day(analysis[analysis[movie_id] M002]), ], ignore_indexTrue) pivot selected.pivot_table( indexrelease_day, columnsmovie_title, valuesbox_office, aggfuncsum, ).fillna(0)pivot_table会把上映第几天作为行片名作为列值就是当日票房。.fillna(0)是必要的因为不同电影上映天数不同某部电影第 30 天已经没数据了另一部还可能在映空值不填 0折线图会断掉。4.4 Top N榜单与周环比让分析结果能自证榜单看似简单但要注意单位。我输出的 Top 10 表里除了总票房还会带上观影人次和平均票价方便别人判断“高票房是因为票价贵还是看的人多”。top10 summary.nlargest(10, total_box_office)[ [movie_title, release_date, total_box_office, total_audience, avg_price] ] daily_total ( analysis.groupby(stat_date, as_indexFalse)[box_office].sum() .sort_values(stat_date) ) daily_total[week] pd.to_datetime(daily_total[stat_date]).dt.to_period(W).astype(str) weekly daily_total.groupby(week, as_indexFalse)[box_office].sum() weekly[环比] weekly[box_office].pct_change() * 100nlargest(10, total_box_office)按总票房取前 10比sort_values再head(10)更直白。周环比这里用pct_change()得到的是百分比小数乘以 100 转成百分比。这个方法算的是“本周票房相对上周的变化幅度”如果第一周没有上周数据环比会为空这是正常的不需要强补。5. 票房数据系统的避坑指南5个高频问题和修复步骤下面五条是我做这类系统时真实踩过、也帮人排查过的坑。每一条都按“现象、原因、解决”写你可以直接对号入座。5.1 日期解析错误导致票房曲线断线现象每日票房趋势图出现锯齿状某几天票房掉到 0甚至 2024 年 1 月和 2025 年 1 月的数据交错排在一起。原因stat_date列里有多种格式pandas 没解析成日期类型绘图时按字符串排序看起来就是乱的。解决用 3.2 节的解析函数统一格式并在绘制前强制转成 datetimeanalysis[stat_date] pd.to_datetime(analysis[stat_date]) analysis analysis.sort_values(stat_date)转换后检查日期范围是否合理。如果仍然断线再用asfreq(D)看缺失日期而不是急着填 0。5.2 票房单位“万”“亿”混用导致数字爆炸现象某天全国总票房算出来 2 万亿或者某部电影单日票房 350 亿图表 Y 轴上全是科学计数法。原因原始数据里一部分单位是“万”一部分是“元”直接用 float 转换后两个量级的数字被加到了一起。解决清洗阶段把所有金额统一成“元”换算函数里禁止用eval用显式分支处理“万”“亿”后缀。换算完打印describe()如果 max 和 mean 相差超过 4 个数量级马上查数据源。5.3 主键重复导致关联翻倍现象某部电影总票房算出来是公开数据的 2 倍但单独看每一天都不大。原因CSV 里有重复行或者数据源每天更新时把前一天的历史快照又追加了一份groupby把同一笔数据算了两次。解决清洗阶段先按业务主键查重复再决定去重策略。如果数据可能来自多个平台一定要把source_id加进 keykey [movie_id, stat_date, source_id] print(analysis.duplicated(subsetkey).sum())这里source_id不是可选字段而是区分“同一部电影同一天是否有多条数据”的关键。5.4 影片重名/系列片导致统计串片现象榜单里出现两部同名的“海底总动员”或“速度与激情”它们的票房被合并成了一行导致总量虚高。原因代码里用movie_title做 join 或 groupby遇到系列片和重拍片就会串。解决所有内部计算都使用movie_id而不是片名。如果原始数据没有稳定的movie_id就自己构造一个组合键analysis[movie_key] ( analysis[movie_title] _ analysis[release_date].astype(str) )这样至少能把同名不同期的电影区分开。展示给用户看的时候再显示movie_title内部计算始终用它对应的 key。5.5 可视化“缺失”和“0”混淆导致图表比例失真现象饼图里某部电影占比很低但实际上是数据缺失柱状图某日没有柱子但那天其实有排片。原因数据源里“没采集到”和“真实为 0”在清洗后被统一fillna(0)处理了。解决区分缺失和 0。对票房、场次、人次来说缺失先保留为 NaN分析时不参与求和只有当你知道那几天已经确认无排片时才用 0 填充。画图前再单独把缺失天数列出来missing_days analysis[analysis[box_office].isna()] print(len(missing_days), 天缺失票房)如果缺失天数很多回到采集环节补数据不要靠填充硬撑图表完整性。6. 进阶与验收把“跑通”变成“可交付”的几个习惯系统能出图只是第一步真正让它在答辩或团队里站得住的是可验证性和可重复性。我现在做任何数据分析系统都会在交付前加一层回归校验用源表行数、抽样总和、图表趋势三个维度互相验证。先做行数校验。清洗后的数据进入数据库前源表行数和库表行数应当一致或只有少量重试差异source_count len(raw) db_count pd.read_sql_query( SELECT COUNT(*) AS cnt FROM box_office_daily, conn ).iloc[0][cnt] assert abs(source_count - db_count) 5, fcount mismatch: {source_count} vs {db_count}再随机抽一部电影手工加总它某几天的票房和系统输出对照。这个方法能发现单位换算遗漏、维度冗余、数据重复三类问题。项目目录里除了代码我还会留三样东西一份requirements.txt锁定 pandas、flask、pyecharts 等核心库的版本一份README.md写明数据来源、字段口径、如何重跑一份output/charts的截图参考图方便自己不重新跑也能快速检查。我现在的习惯是交付前一天不看新功能只做回归跑一遍清洗、算一遍 Top10、看一遍票房走势图再找个没参与项目的同学问“这张图说明什么” 。多数翻车都发生在这一步因为自己看自己做的系统容易默认一切正确。希望帮到你。本文还有配套的精品资源点击获取
返回列表