简介:这是一套面向计算机相关专业学生与项目实战学习者的电影数据可视化分析系统,可作为大作业、毕业设计参考或数据分析练手项目。系统围绕豆瓣电影数据展开,涵盖数据采集、清洗、存储、可视化与预测等环节,难度适中,适合具备Python基础、希望积累完整项目经验的学习者。资源包共37个文件,约5.17MB,包含6个Python脚本、3个Jupyter Notebook、1个SQL建库脚本,以及24张可视化结果图、1份PDF说明文档和Git配置文件,源码经本地编译调试,可运行。目前已有173人学习下载。读者可从中获得完整的数据分析流程方案、数据库建表与查询脚本、可视化与预测代码,以及配套文档和结果图,便于快速理解项目结构、复现实验并迁移到自己的课题中。
1. 电影数据可视化分析系统:从 CSV 到可交互看板,一条能跑通的落地路径
手头有一份两三万行的电影 CSV,老板或导师丢过来一句“做个可视化分析系统”,很多人第一反应是打开 Jupyter 画几张柱状图交差。但真到答辩或汇报现场,静态图撑不住追问:票房随年份怎么变、类型和评分的相关性到底多强、哪个导演是“高产又高口碑”——这些都需要一个能点、能筛、能联动的看板。基于 Python 的电影数据可视化分析系统,本质就是把「数据清洗 → 指标计算 → 图表渲染 → 交互筛选」串成一条流水线,用 Pandas 做计算、Plotly 或 Pyecharts 做交互图、Streamlit 或 Flask 做外壳。它适合数据分析入门者练完整链路,也适合需要交付课程设计或内部小工具的人。下面按我实际搭过几套的经验,把选型、代码、参数和翻车点讲清楚,你照着能复现出一套自己的系统。
2. 数据层与选型:为什么用 Pandas + Plotly 而不是 Excel 透视表
2.1 电影数据集的字段结构和清洗重点
常见的电影数据集(比如 TMDB、Kaggle 上的 movies metadata)字段大致分四类:标识类(id、imdb_id)、文本类(title、overview、genres)、数值类(budget、revenue、runtime、vote_average、vote_count)、时间类(release_date)。真正能直接拿来画图的字段没几个,大部分要清洗。
清洗重点有三个。第一,genres这类字段在原始 CSV 里往往是 JSON 字符串或竖线分隔,比如"Action|Adventure|Science Fiction",需要炸开成多行或做 one-hot。第二,budget和revenue里大量为 0,这些不是“零成本”,而是缺失,直接参与均值计算会把结果拉垮。第三,release_date有空值,转 datetime 时会报错,得先errors='coerce'再丢弃。
import pandas as pd import numpy as np # 读取时指定 dtype,避免 id 被读成 float 出现 .0 后缀 df = pd.read_csv("movies.csv", dtype={"id": str}, parse_dates=["release_date"]) # 1. 预算/票房为 0 视为缺失 df["budget"] = df["budget"].replace(0, np.nan) df["revenue"] = df["revenue"].replace(0, np.nan) # 2. 类型字段炸开:一行电影变多行,便于按类型聚合 df["genres"] = df["genres"].fillna("").str.split("|") df_exploded = df.explode("genres") df_exploded = df_exploded[df_exploded["genres"] != ""] # 3. 提取年份,丢弃无日期的记录 df["year"] = df["release_date"].dt.year df = df.dropna(subset=["year"]) df["year"] = df["year"].astype(int) print(df.shape, df_exploded.shape)这段代码的逻辑是:先修正类型,再处理缺失语义,最后做维度展开。参数上,dtype={"id": str}很关键,很多人忽略它,结果 id 变成550.0,后续做关联时对不上。explode之后行数会膨胀,一部电影有几个类型就变几行,这是正常的,但做“电影总数”统计时要回到原表,别用炸开后的表去count唯一 id,否则会重复计数。
2.2 可视化库选型:Plotly、Pyecharts、Matplotlib 的边界
Matplotlib 适合出静态图、写论文插图,但做交互看板很吃力;Pyecharts 中文文档友好、图表样式偏国内审美,适合交付给国内评审;Plotly 的交互能力最强,和 Streamlit 集成几乎零成本,hover、缩放、图例点击筛选都是内置的。我一般主推 Plotly + Streamlit 组合,原因是开发速度快,一个下午能出可演示版本。
选型时还要考虑一个现实问题:如果最终要嵌到 Web 系统里,Flask + ECharts 更常见;如果只是本地跑或内网演示,Streamlit 足够。不要一上来就上 Django,杀鸡用牛刀,调试成本会吃掉你所有时间。
| 方案 | 交互能力 | 开发速度 | 适合场景 |
|---|---|---|---|
| Matplotlib | 无 | 快 | 静态报告、论文 |
| Pyecharts | 中 | 中 | 国内答辩、HTML 导出 |
| Plotly + Streamlit | 强 | 快 | 本地看板、快速演示 |
| Flask + ECharts | 强 | 慢 | 需嵌入现有 Web 系统 |
2.3 项目目录结构:让源码和文档能对得上
一套能交付的系统,目录不能乱。我习惯这样组织:data/放原始和清洗后数据,src/放清洗和指标计算脚本,app/放看板入口,docs/放说明文档,output/放导出的图表和 PDF。这样别人拿到源码,看目录就知道从哪跑。
movie_analysis/ ├── data/ │ ├── raw/movies.csv │ └── processed/movies_clean.csv ├── src/ │ ├── clean.py │ └── metrics.py ├── app/ │ └── dashboard.py ├── docs/ │ └── 使用说明.md └── output/ └── figures/清洗脚本和指标脚本分开,是为了让“数据变了重跑清洗”和“只调指标”互不干扰。很多人把全部逻辑塞进一个 notebook,改一个参数要重跑半小时,血泪经验。
3. 核心指标计算:票房、评分、类型三维度怎么算才不误导
3.1 票房与通胀:名义票房和实际购买力
直接拿revenue排序,排出来的全是近十年的片子,因为老电影的名义票房天然低。如果分析目的是“哪部电影最成功”,必须做通胀调整。常见做法是用 CPI 折算,但电影数据集一般不带 CPI,可以退而求其次用年份分组做“同年代内排名”,或者引入外部 CPI 表做归一化。
# 简化方案:按年代分组,计算组内票房排名 df["decade"] = (df["year"] // 10) * 10 df["revenue_rank_in_decade"] = df.groupby("decade")["revenue"].rank( ascending=False, method="min" ) # 若引入 CPI 表(year, cpi_index),折算到基准年 cpi = pd.read_csv("cpi.csv") base = cpi[cpi["year"] == 2020]["cpi_index"].values[0] df = df.merge(cpi, on="year", how="left") df["revenue_real"] = df["revenue"] * base / df["cpi_index"]参数说明:method="min"表示并列时取最小名次,避免出现跳号。revenue_real才是可跨年代比较的口径。如果 CPI 缺失,merge后会有 NaN,画图前要dropna,否则 Plotly 会静默丢点,你以为图对了其实少了一半数据。
3.2 评分可信度:vote_count 阈值怎么定
vote_average是 10 分制,但只有 5 个人打分的 9.5 分和 5 万人打分的 8.5 分,可信度天差地别。直接按评分排序,前排全是冷门高分片,这是典型的“小样本偏差”。常见做法是设一个vote_count阈值,比如 50、100、500,只保留超过阈值的电影再排序。
阈值怎么定?看数据分布。可以先画vote_count的直方图,找中位数或 25 分位。我一般用 50 作为最低门槛,做“高分榜”时用 500。还可以用贝叶斯平均:(vote_count * vote_average + m * C) / (vote_count + m),其中 C 是全站平均分,m 是阈值。这样小样本会被拉向均值,更稳健。
C = df["vote_average"].mean() m = 500 df["weighted_score"] = ( df["vote_count"] * df["vote_average"] + m * C ) / (df["vote_count"] + m)m越大,小样本被压制得越狠。做榜单时用weighted_score排序,比裸vote_average靠谱得多。
3.3 类型与导演聚合:explode 后的坑
按类型统计平均票房时,用炸开后的表没问题,但要注意一部电影会同时计入多个类型,所以“各类型票房之和”会大于总票房,这是正常的,别拿去和总营收对账。按导演聚合时,导演字段可能有多个,比如“科恩兄弟”,要么保留原样,要么拆分,拆分逻辑要统一。
# 按类型统计:平均评分、电影数、平均票房 genre_stats = df_exploded.groupby("genres").agg( movie_count=("id", "nunique"), avg_rating=("vote_average", "mean"), avg_revenue=("revenue", "mean") ).reset_index() # 过滤掉样本过少的类型,避免长尾噪声 genre_stats = genre_stats[genre_stats["movie_count"] >= 20] genre_stats = genre_stats.sort_values("avg_revenue", ascending=False)nunique而不是count,是因为炸开后同一部电影在同一类型下只出现一次,但保险起见用唯一计数。movie_count >= 20这个过滤很重要,否则会出现“某类型只有 2 部电影但平均票房极高”的误导性结论。
4. 看板搭建:用 Streamlit 把图表串成可交互系统
4.1 最小可运行看板:从读数据到出图
Streamlit 的好处是脚本即应用,不用写前端。核心结构是:侧边栏放筛选器,主区域放图表。筛选器改变时,Streamlit 会自动重跑脚本,所以数据过滤要放在缓存之后。
import streamlit as st import plotly.express as px import pandas as pd st.set_page_config(layout="wide", page_title="电影数据可视化分析") @st.cache_data def load_data(): return pd.read_csv("data/processed/movies_clean.csv") df = load_data() # 侧边栏筛选 st.sidebar.header("筛选条件") year_range = st.sidebar.slider( "年份范围", int(df["year"].min()), int(df["year"].max()), (2000, 2020) ) genres = st.sidebar.multiselect( "类型", options=sorted(df["genres"].dropna().unique()) ) mask = df["year"].between(*year_range) if genres: mask &= df["genres"].isin(genres) filtered = df[mask] st.title("电影数据可视化分析看板") col1, col2 = st.columns(2) with col1: fig1 = px.scatter( filtered, x="budget", y="revenue", color="genres", hover_data=["title", "year"], log_x=True, log_y=True, title="预算 vs 票房(对数轴)" ) st.plotly_chart(fig1, use_container_width=True) with col2: yearly = filtered.groupby("year")["revenue"].mean().reset_index() fig2 = px.line(yearly, x="year", y="revenue", title="年度平均票房趋势") st.plotly_chart(fig2, use_container_width=True)逻辑说明:@st.cache_data避免每次交互都重读 CSV,数据大时差别明显。log_x/log_y用对数轴,是因为预算和票房跨度几个数量级,线性轴会把小成本片挤在角落。use_container_width=True让图表自适应,不然宽屏下图会很小。
参数上,slider的默认值我设成(2000, 2020),因为太老的片子数据缺失多,默认全范围会让首屏图很乱。multiselect为空时表示不筛选,这个逻辑要在mask里处理,否则空选会过滤掉所有数据。
4.2 图表联动与筛选器设计
联动是看板的灵魂。Streamlit 本身不支持图表点击回传筛选(截至我写这套方案时),所以联动要靠侧边栏筛选器实现。常见设计是:年份滑块 + 类型多选 + 评分区间 + 关键词搜索。关键词搜索用str.contains,注意case=False和na=False。
keyword = st.sidebar.text_input("片名关键词") if keyword: mask &= df["title"].str.contains(keyword, case=False, na=False) rating_range = st.sidebar.slider("评分区间", 0.0, 10.0, (6.0, 10.0), 0.1) mask &= df["vote_average"].between(*rating_range)na=False必须加,否则 title 为空的行会报错或行为异常。评分默认(6.0, 10.0)是过滤掉低分噪声,让首屏图更干净。这些默认值不是拍脑袋,是根据“演示时第一眼要好看”来定的。
4.3 导出 PDF 报告:把看板结论固化下来
系统交付往往要一份 PDF。常见做法是用kaleido把 Plotly 图导出成静态图片,再用reportlab或fpdf拼成 PDF。注意kaleido在无头环境需要额外依赖,Windows 上一般直接能用。
import plotly.io as pio from fpdf import FPDF # 导出单张图 pio.write_image(fig1, "output/figures/scatter.png", width=1200, height=600, scale=2) # 拼 PDF pdf = FPDF() pdf.add_page() pdf.set_font("Arial", size=14) pdf.cell(0, 10, "Movie Analysis Report", ln=True) pdf.image("output/figures/scatter.png", w=180) pdf.output("output/report.pdf")scale=2提高分辨率,避免 PDF 里图糊。width/height要和看板里比例接近,不然图会变形。如果中文标题导出乱码,fpdf需要额外加载中文字体,这是常见翻车点,建议图表标题用英文或提前注册字体。
5. 避坑与排查:那些让看板“看起来对但结论错”的细节
5.1 现象:票房趋势图某年突然暴跌 → 原因:缺失值被当成 0 → 解决:先过滤再聚合
早期我用groupby("year")["revenue"].mean()直接出图,某年平均值断崖式下跌。查了半天发现那几年很多电影revenue为 0,被当成真实值参与均值。解决是在聚合前df = df[df["revenue"] > 0],或者用mean()前先replace(0, np.nan)。Pandas 的mean默认跳过 NaN,但不会跳过 0,这个区别是血泪教训。
5.2 现象:类型分布饼图加起来超过 100% → 原因:一部电影多个类型 → 解决:改用条形图或注明多标签
饼图适合互斥分类,电影类型不互斥,一部片子既是 Action 又是 Sci-Fi。硬用饼图会让人误以为占比有问题。正确做法是改用横向条形图,或者在图注里写明“一部电影可属于多个类型,占比之和大于 100%”。我一般直接换条形图,省得解释。
5.3 现象:Streamlit 改了筛选器图表不更新 → 原因:数据被缓存但过滤逻辑写在缓存函数里 → 解决:缓存只包读数据,过滤放外面
@st.cache_data应该只装饰load_data,不要把过滤逻辑也包进去。如果把filtered的计算放进缓存函数,筛选器变化时缓存命中旧结果,图就不动。这个坑很隐蔽,因为页面不报错,只是“没反应”。排查方法是打印filtered.shape,看是否随筛选变化。
5.4 现象:PDF 导出中文变方块 → 原因:fpdf 默认字体不支持中文 → 解决:注册中文字体或图表用英文
fpdf内置字体只有 Latin。要显示中文,需要下载一个 ttf 字体并用add_font注册。更省事的做法是图表标题和 PDF 正文用英文,中文说明放在 Markdown 文档里。如果必须中文,用reportlab配合TTFont更稳。
5.5 现象:本地跑得好好的,换台机器就报错 → 原因:依赖版本不一致 → 解决:锁定 requirements.txt
Plotly、Streamlit、Pandas 的 API 在不同版本间有差异,比如st.experimental_rerun后来改名。交付时一定要pip freeze > requirements.txt,并在文档里写明 Python 版本。我一般还会在 README 里写一句“建议用虚拟环境”,避免污染全局环境导致玄学报错。
6. 进阶技巧:用参数化配置让系统能换数据集复用
一套只针对一份 CSV 的系统,复用价值有限。我后来习惯把字段映射抽成配置文件,换数据集时只改配置不改代码。比如用 YAML 定义“哪列是票房、哪列是评分、哪列是类型”,清洗和指标脚本读配置决定行为。
import yaml with open("config/fields.yaml", "r", encoding="utf-8") as f: cfg = yaml.safe_load(f) revenue_col = cfg["revenue"] # 例如 "revenue" rating_col = cfg["rating"] # 例如 "vote_average" genre_col = cfg["genre"] # 例如 "genres" date_col = cfg["date"] # 例如 "release_date" df[revenue_col] = df[revenue_col].replace(0, np.nan) df["year"] = pd.to_datetime(df[date_col], errors="coerce").dt.year这样换一份“天气分析系统”或“音乐管理系统”的数据,只要字段能对应上,改 YAML 就能跑。参数说明:errors="coerce"保证脏日期变 NaT 而不是抛异常;配置文件用 UTF-8 读取,避免中文注释乱码。
验证系统是否真的可复用,我有个笨办法:拿一份结构完全不同的 CSV,只改配置,看能不能在 10 分钟内出一个能看的图。如果超过 10 分钟,说明耦合还是太重。这个习惯帮我省了很多重复劳动。
最后说个我自己的教训:早期做这类系统,我总想把所有图表堆在一个页面,结果首屏加载慢、重点不突出。后来改成“总览 + 分维度下钻”两页,演示效果反而更好。图表不在多,在于每个都能回答一个具体问题。希望帮到你。
本文还有配套的精品资源,点击获取