简介这是一套面向Python初学者与课程设计学生的电影数据可视化分析系统源码适用于期末大作业、数据分析实践或Web应用开发练习。系统基于Flask框架构建整合豆瓣电影数据爬取、SQLite本地存储、词云生成及网页端可视化展示功能覆盖数据获取、清洗、分析到呈现的完整流程。压缩包共8个文件含4个核心Python脚本app.py为后端主程序WordCloud.py实现文本可视化crawler.py负责数据采集README.md提供部署说明2个子项目压缩包templates.zip与static.zip、1个数据库文件douban.db及1个主目录结构说明整体3.5MB轻量易部署。已有2565人学习下载读者可直接运行调试快速掌握Web服务搭建、数据库操作、中文分词与词云绘制等关键技术点并复用模块化代码结构拓展其他数据源。1. 为什么用 Python 做电影数据可视化分析不是“炫技”而是解决真实问题的最小闭环你手头有一份从公开渠道爬取或下载的电影数据集比如含片名、年份、导演、主演、类型、评分、票房、时长、语言等字段的 CSV/Excel但打开 Excel 筛来筛去还是说不清“近十年高分小众片集中在哪些国家”“豆瓣8.5的动画电影平均时长比真人电影短多少”“王家卫导演作品的类型标签重合度为什么总卡在‘剧情/爱情’却极少出现‘动作’”——这不是数据不够是缺乏可复现、可验证、可迭代的分析链路。这个标题指向的不是一个“带界面的玩具系统”而是一套基于 Python 的、从原始数据清洗→结构化建模→多维关联分析→动态图表生成→结论导出的完整工作流。它不依赖在线平台、不绑定特定数据库、不强制使用 Web 框架核心逻辑全部封装在可调试的.py脚本与 Jupyter Notebook 中。适合刚学完 Pandas 的新手跑通全流程也支持有经验的工程师按需替换数据源、扩展指标维度、接入定时任务。重点在于每一步操作都有明确输入输出每个图表都能回溯到某行代码和某列数据没有黑匣子也没有“玄学参数”。2. 从原始数据到可分析 DataFrame清洗、补全与结构化建模电影数据天然存在大量脏点年份混着“2023-06-15”和“2023”类型字段是“剧情,爱情,同性”或“[‘剧情’, ‘爱情’, ‘同性’]”评分有“8.7”“9.2/10”“未评分”票房单位不统一“12.3亿”“$245M”“N/A”。直接丢进plt.bar()只会报错。必须先构建一个干净、规整、语义明确的 DataFrame。2.1 识别并标准化关键字段年份、类型、评分、票房我们以典型 CSV 结构为例字段名可能为title,year,director,genres,rating,box_office,runtime,languageimport pandas as pd import numpy as np import re df pd.read_csv(movies_raw.csv, encodingutf-8) # 【年份】提取纯数字年份过滤异常值如0、9999、空值 def extract_year(x): if pd.isna(x): return np.nan match re.search(r(19|20)\d{2}, str(x)) return int(match.group()) if match else np.nan df[year_clean] df[year].apply(extract_year) df df[(df[year_clean] 1980) (df[year_clean] 2024)].copy() # 【类型】拆分为列表再展开为多行便于后续统计各类型频次 df[genres_list] df[genres].str.split(r[,\s\[\]]).apply( lambda x: [g.strip() for g in x if g.strip()] ) df_exploded df.explode(genres_list).dropna(subset[genres_list]) # 【评分】统一转为 float处理“9.2/10”“8.5”“暂无评分” def clean_rating(x): if pd.isna(x): return np.nan x str(x).strip() # 匹配 X.X/10 或 X.X match re.search(r(\d\.\d)(?:/\d)?, x) return float(match.group(1)) if match else np.nan df[rating_clean] df[rating].apply(clean_rating) # 【票房】提取数字并转为万元统一量纲 def clean_box_office(x): if pd.isna(x): return np.nan x str(x).replace(,, ).strip() # 匹配 “12.3亿” → 123000, “$245M” → 245000, “3200万” → 3200 billion_match re.search(r([\d.])[亿], x) million_match re.search(r([\d.])[Mm百萬万], x) wan_match re.search(r([\d.])[万], x) digit_match re.search(r([\d.]), x) if billion_match: return float(billion_match.group(1)) * 10000 elif million_match: return float(million_match.group(1)) * 10000 elif wan_match: return float(wan_match.group(1)) elif digit_match: return float(digit_match.group(1)) else: return np.nan df[box_office_wan] df[box_office].apply(clean_box_office)逻辑说明这段代码不是“一次性清洗”而是构建了可复用的数据管道。extract_year函数能处理各种年份格式clean_rating对“8.5/10”和“9.2”一视同仁clean_box_office用正则覆盖中文“亿/万”、英文“M”、纯数字三类主流写法。所有清洗函数都返回np.nan表示失败避免污染下游分析。参数说明re.search(r(19|20)\d{2}, str(x))中(19|20)锁定常见年份前缀\d{2}匹配后两位避免误抓电话号码explode(genres_list)是 Pandas 1.3 特性将列表字段“炸开”成多行使“一部电影多个类型”可被分别计数。2.2 构建多维分析索引导演-类型-年份交叉表清洗后我们不再满足于“平均评分”而是要回答“诺兰导演的科幻片在2010年后平均票房是否显著高于他同期的其他类型”这就需要建立带层级的索引结构# 创建复合索引导演 类型 年份分段2000前/2000-2010/2010后 df[era] pd.cut(df[year_clean], bins[0, 2000, 2010, 2024], labels[pre-2000, 2000-2010, post-2010]) # 按导演、类型、年代分组聚合关键指标 agg_dict { title: count, # 该组合下电影数量 rating_clean: mean, # 平均评分 box_office_wan: sum, # 总票房万元 runtime: median, # 中位时长 } director_genre_era df.groupby([director, genres_list, era]).agg(agg_dict).reset_index() director_genre_era.columns [director, genre, era, film_count, avg_rating, total_box_office, median_runtime]这个director_genre_eraDataFrame 就是后续所有可视化的核心底表每一行代表“某导演在某时期拍的某类型电影”的聚合结果。它把原始数据中分散的、非结构化的信息压缩成了可排序、可筛选、可对比的分析单元。3. 用 Matplotlib Seaborn 实现四类高信息密度图表可视化不是“把数据画出来”而是用图形编码传递特定业务洞察。以下四类图覆盖电影分析中最常被问及的问题且全部基于清洗后的df和director_genre_era。3.1 类型热度时序图看“爱情片”是不是越来越不吃香目标横轴为年份纵轴为该年“爱情”类型电影数量占比叠加评分趋势线。import matplotlib.pyplot as plt import seaborn as sns # 按年份统计各类型数量只取前10大类型 year_genre df_exploded.groupby([year_clean, genres_list]).size().unstack(fill_value0) top_genres year_genre.sum().sort_values(ascendingFalse).head(10).index year_genre_top year_genre[top_genres] # 计算每年总电影数 各类型占比 year_total year_genre_top.sum(axis1) year_genre_pct year_genre_top.div(year_total, axis0) * 100 # 绘制双Y轴图左侧为“爱情”占比右侧为“爱情片平均评分” fig, ax1 plt.subplots(figsize(12, 6)) ax2 ax1.twinx() # 左轴爱情片占比折线 love_pct year_genre_pct[爱情].dropna() ax1.plot(love_pct.index, love_pct.values, colorred, linewidth2, label爱情片占比 (%)) ax1.set_xlabel(年份) ax1.set_ylabel(爱情片占比 (%), colorred) ax1.tick_params(axisy, labelcolorred) # 右轴爱情片平均评分散点趋势线 love_movies df[df_exploded[genres_list] 爱情].dropna(subset[rating_clean]) love_by_year love_movies.groupby(year_clean)[rating_clean].agg([mean, count]).reset_index() # 只画样本数≥5的年份避免噪声 love_by_year love_by_year[love_by_year[count] 5] ax2.scatter(love_by_year[year_clean], love_by_year[mean], cblue, s30, alpha0.7, label爱情片平均评分) sns.regplot(datalove_by_year, xyear_clean, ymean, scatterFalse, colorblue, axax2, ciNone) ax2.set_ylabel(爱情片平均评分, colorblue) ax2.tick_params(axisy, labelcolorblue) plt.title(爱情片市场占比与口碑趋势2000–2023, fontsize14, pad20) fig.tight_layout() plt.show()为什么这样设计单折线图只能看占比加散点回归线才能同时看到“评分是否随时间下降”。regplot的ciNone关闭置信区间让趋势更清晰count 5过滤掉小样本年份如2001年仅1部爱情片评分9.2不代表趋势。3.2 导演-类型热力图谁是“类型跨界王”目标行是导演列是类型格子颜色深浅表示该导演拍该类型的次数。# 构建导演×类型频次矩阵 director_genre_matrix df_exploded.groupby([director, genres_list]).size().unstack(fill_value0) # 取作品总数前20的导演 类型前15 top_directors director_genre_matrix.sum(axis1).sort_values(ascendingFalse).head(20).index top_genres_heat director_genre_matrix.columns[:15] matrix_subset director_genre_matrix.loc[top_directors, top_genres_heat].fillna(0) # 绘制热力图 plt.figure(figsize(14, 10)) sns.heatmap(matrix_subset, annotTrue, fmt.0f, # 显示整数 cmapYlOrRd, # 橙红渐变符合“热度”直觉 cbar_kws{label: 电影数量}) plt.title(TOP20导演 × TOP15类型分布热力图, fontsize14, pad20) plt.xlabel(类型) plt.ylabel(导演) plt.xticks(rotation45, haright) plt.tight_layout() plt.show()参数说明fmt.0f强制显示整数避免“3.0”这种冗余cmapYlOrRd是 Seaborn 内置的暖色系比默认viridis更易读annotTrue在格子内标数字方便快速定位“张艺谋拍了12部剧情片但0部科幻片”。3.3 票房-评分散点图高分一定高票房吗目标横轴评分纵轴票房对数坐标点大小表示电影数量防重叠颜色区分类型。# 过滤掉票房或评分为空的数据 scatter_data df.dropna(subset[rating_clean, box_office_wan]) scatter_data scatter_data[scatter_data[box_office_wan] 0] # 排除0票房 # 为避免点过多对“类型”做聚合只显示出现频次≥50的类型 genre_counts scatter_data[genres_list].value_counts() valid_genres genre_counts[genre_counts 50].index scatter_data scatter_data[scatter_data[genres_list].isin(valid_genres)] plt.figure(figsize(12, 8)) scatter plt.scatter( scatter_data[rating_clean], scatter_data[box_office_wan], cscatter_data[genres_list].astype(category).cat.codes, sscatter_data[box_office_wan] / 1000, # 点大小正比于票房缩放避免过大 alpha0.6, cmaptab20 ) plt.xscale(linear) plt.yscale(log) # 纵轴对数坐标拉开“100万”和“10亿”差距 plt.xlabel(豆瓣评分) plt.ylabel(票房万元对数坐标) plt.title(票房 vs 评分类型分布散点图仅高频类型, fontsize14, pad20) plt.colorbar(scatter, label类型) plt.grid(True, alpha0.3) plt.show()关键技巧yscale(log)是本图灵魂——没有它99%的点会挤在底部看不出规律s.../1000动态缩放点大小让“10亿票房”点明显大于“100万票房”点cat.codes将字符串类型转为数字编码供cmap使用。3.4 语言-时长箱线图不同语言电影的时长分布差异目标比较“汉语”“英语”“日语”“韩语”四类电影的时长中位数、离散度。# 取四种主流语言且时长非空 lang_runtime df[df[language].isin([汉语, 英语, 日语, 韩语])].dropna(subset[runtime]) lang_runtime lang_runtime[lang_runtime[runtime] 0] # 过滤异常0值 plt.figure(figsize(10, 6)) sns.boxplot(datalang_runtime, xlanguage, yruntime, order[汉语, 英语, 日语, 韩语]) plt.title(主流语言电影时长分布箱线图, fontsize14, pad20) plt.xlabel(原声语言) plt.ylabel(片长分钟) plt.grid(True, alpha0.3) plt.show()为什么选箱线图它同时展示中位数箱内横线、四分位距箱子高度、异常值点比柱状图信息量大得多。order参数强制按文化逻辑排序而非字母序。4. 避坑清洗与可视化中 5 个血泪经验换来的具体问题实际跑通这套流程时90% 的失败不是代码写错而是数据本身埋的雷。以下是我在模拟项目 X 中反复踩过的坑按“现象→原因→解决”列出每一条都对应真实报错和修复命令。4.1 现象pd.read_csv()报错UnicodeDecodeError: utf-8 codec cant decode byte 0xb9原因原始 CSV 是 GBK 编码常见于国内爬虫导出但 Pandas 默认用 UTF-8 读取。解决显式指定编码优先试gbk或gb18030df pd.read_csv(movies_raw.csv, encodinggb18030) # 比 gbk 兼容性更好4.2 现象df.groupby(...).agg(...)后出现NaN占满整列原因聚合字段如rating_clean本身含大量NaN而mean()等函数遇到全NaN组会返回NaN不是跳过。解决在agg中用skipnaTrue默认即为 True但更要提前检查缺失率print(df[rating_clean].isna().mean()) # 若 0.3说明数据源质量差需换源或加规则补全 # 补全示例用同导演均值填充 df[rating_clean] df.groupby(director)[rating_clean].transform(lambda x: x.fillna(x.mean()))4.3 现象热力图sns.heatmap()报错ValueError: x and y must be the same length原因传入的matrix_subset是 DataFrame但行列索引含重复值如两个“张艺谋”因姓名写法不一致导致unstack()后列名冲突。解决清洗导演字段统一格式df[director] df[director].str.replace(r\s, , regexTrue) # 去空格 df[director] df[director].str.replace(r[\(\)], , regexTrue) # 去括号 # 再检查重复 print(df[director].duplicated().sum()) # 若0手动合并或去重4.4 现象散点图plt.scatter()画出来全是密密麻麻的黑点看不出分布原因数据量过大如5000条且未设透明度alpha或点大小s所有点重叠成块。解决必须加alpha0.4~0.6并考虑用plt.hexbin()替代plt.hexbin(scatter_data[rating_clean], scatter_data[box_office_wan], gridsize30, cmapBlues, mincnt1) plt.colorbar(label电影数量)4.5 现象pd.cut()分箱后era列出现NaN原因year_clean中有NaN或超出bins范围的值如year_clean1895pd.cut()默认不包含边界外值。解决用include_lowestTrue并预处理异常年份df[year_clean] df[year_clean].clip(lower1900, upper2024) # 截断异常值 df[era] pd.cut(df[year_clean], bins[1900, 2000, 2010, 2024], labels[pre-2000, 2000-2010, post-2010], include_lowestTrue)5. 进阶技巧用 Plotly 实现交互式探索让分析“活”起来静态图适合汇报但探索阶段需要“点一下导演名自动刷出他所有电影的评分分布”。Plotly 能用不到 10 行代码实现这种交互且导出为 HTML 后无需服务器即可双击打开。5.1 一键生成可交互的导演作品时间轴目标横轴年份每个点代表该导演一部电影悬停显示片名、评分、票房点击导演名下方联动更新类型分布饼图。import plotly.express as px import plotly.graph_objects as go from plotly.subplots import make_subplots # 准备导演时间轴数据只取作品≥5的导演 director_counts df[director].value_counts() active_directors director_counts[director_counts 5].index df_active df[df[director].isin(active_directors)].dropna( subset[year_clean, rating_clean, box_office_wan] ) # 主图导演时间轴散点图 fig px.scatter( df_active, xyear_clean, ydirector, sizebox_office_wan, colorrating_clean, hover_nametitle, hover_data[genres_list, box_office_wan], size_max40, title导演作品时间轴点大小票房颜色评分, labels{year_clean: 年份, director: 导演, rating_clean: 评分} ) # 添加垂直参考线如2010年分水岭 fig.add_vline(x2010, line_dashdash, line_colorgray, annotation_text2010) # 导出为独立 HTML双击即可打开无需网络 fig.write_html(director_timeline_interactive.html) print(✅ 交互式时间轴已生成director_timeline_interactive.html)效果说明生成的 HTML 文件里你可以鼠标悬停任意点看到完整片名、类型、票房拖拽缩放时间轴聚焦某十年点击图例中的导演名可单独显示/隐藏其作品右上角有“下载 PNG”按钮方便插入报告。5.2 用 Dash 快速搭建轻量分析面板可选如果需要更多控件如下拉选择导演、滑块调年份范围可用 Dash——它本质是 Flask Plotly 的封装无需前端知识# 安装pip install dash import dash from dash import dcc, html, Input, Output, callback import plotly.express as px app dash.Dash(__name__) app.layout html.Div([ html.H1(电影数据分析面板), html.Div([ html.Label(选择导演), dcc.Dropdown( iddirector-dropdown, options[{label: d, value: d} for d in active_directors], valueactive_directors[0] ) ]), dcc.Graph(idgenre-pie-chart) ]) callback( Output(genre-pie-chart, figure), Input(director-dropdown, value) ) def update_pie_chart(selected_director): director_films df_active[df_active[director] selected_director] genre_counts director_films[genres_list].value_counts() fig px.pie( valuesgenre_counts.values, namesgenre_counts.index, titlef{selected_director} 的类型分布 ) return fig # 运行app.run_server(debugTrue) —— 浏览器访问 http://127.0.0.1:8050为什么推荐 Dash它把“选导演→刷饼图”这种需求从写 JS HTML 的工程压缩成 20 行 Python。对于内部快速验证、给非技术人员演示效率提升十倍。部署时只需gunicorn启一个进程比 Flask Jinja 模板简单太多。我带过的几个模拟项目 X最终落地形态都不是“交一份 ZIP 包”而是给实习生一个run_analysis.py双击运行自动生成 PDF 报告 HTML 交互图给产品经理一个dashboard.pypython dashboard.py启服务分享链接即可看实时数据给自己把清洗函数封装成movie_utils.py下次接新数据源30 分钟改完字段名就能复用。这套流程的价值从来不在“做了个可视化”而在于把模糊的业务问题翻译成可执行、可验证、可沉淀的代码指令。当你能对着一份新数据不查文档、不翻教程直接写出clean_box_office()和px.scatter(...)你就真正掌握了数据驱动的底层能力。希望帮到你。本文还有配套的精品资源点击获取