从数据清洗到可视化:电影数据分析系统的完整实现指南

发布时间:2026/10/18 19:14:47
从数据清洗到可视化:电影数据分析系统的完整实现指南 简介一套基于Python的电影数据可视化分析系统毕业设计源码包面向计算机相关专业毕业生及数据分析学习者。项目围绕豆瓣电影数据完整覆盖爬虫采集、Sqlite存储、Flask后端服务与EChartsBootstrapWordCloud前端展示评审分数达95分以上且经严格调试可放心运行。压缩包共2000个文件以Python脚本1684个py为主辅以HTML页面、JavaScript交互、CSS样式、TXT数据说明及PDF文档等整体约76.51MB目录结构清晰便于按功能模块阅读和二次开发。资源已有1019人学习使用适合需要参考完整毕设工程、快速搭建电影数据可视化网页的读者。通过源码可掌握requests爬取豆瓣电影、数据入库与清洗、Flask路由设计、ECharts图表配置、词云生成等关键实现并借助附带文档理解项目部署与调优思路为自身课设或毕业设计提供可直接复用的框架。1. 这个毕设题目的分量数据清洗比画图更花时间电影数据可视化分析系统听起来就是抓一批电影数据画几个柱状图和饼图再套一个网页框架。真做起来工作量和想象是反着的数据清洗占四成图表调参占三成页面集成占两成真正写“分析结论”只占一成。这个选题在毕业设计里很常见常见到容易让人低估——但这恰恰是它的价值难度适中、展示性强每一块技术都有明确的落点。适合正在挑毕设题目、想快速建立一套完整数据链路的学习者也适合想把手头的电影数据做成可交互看板的人。它不追求算法深度追求的是“数据怎么变成分析结论”这条链路你能从头到尾讲清楚。2. 技术选型与项目骨架先定数据链路再谈界面先说选型的逻辑。这个系统的核心链路是读取数据 → 清洗 → 聚合 → 画图 → 网页展示。每个环节在 Python 生态里都有成熟方案读取与清洗Pandas这是事实标准没有之一聚合与统计Pandas 的 groupby、value_counts 足够用不需要额外引入 SQL 引擎可视化matplotlib 和 pyecharts 双轨并行一个负责静态图一个负责交互图Web 框架Flask 足够Streamlit 也可以但不建议上 Django对这个体量来说太重。为什么不用现成的 BI 工具因为毕业设计要展示的是代码能力和数据处理思路BI 工具拖拽两下就出图答辩时你连“数据从哪来、怎么处理”都讲不透。选 Flask 不选 FastAPI也是同样的道理——Flask 的模板渲染和静态资源处理对新手更直接调试时能看到完整的报错堆栈。2.1 先跑通最小数据链路再搭完整项目我一般会先写一个最小的数据链路再回头搭项目结构读取一个 CSV → 打印几行 → 画一张测试图。链路通了说明环境没问题这时候再动手拆分模块。这个习惯可以帮你避开“装了十几个库最后发现版本冲突”的尴尬。Python 版本建议用 3.10 以上的稳定版Pandas、matplotlib、Flask、pyecharts 直接 pip 安装即可没必要追求最新版本。python -m venv .venv source .venv/bin/activate # Windows 下用 .venv\Scripts\activate pip install pandas matplotlib flask pyecharts pip freeze requirements.txt这里解释三个关键动作。第一python -m venv 创建独立虚拟环境避免系统 Python 里的包互相污染这是项目级开发的底线习惯。第二pip freeze 把当前环境所有依赖连同版本号写入 requirements.txt换机器时一条 pip install -r requirements.txt 就能复现环境答辩前在实验室电脑上临时部署也靠它。第三requirements.txt 是给“后人”看的把核心依赖的版本上限注释在文件里比如 pandas2.0能省掉不少未来升级的麻烦。2.2 项目骨架目录结构决定你后面加班多少目录结构我建议按“数据、逻辑、展示”三层拆。数据归数据代码归代码HTML 模板和生成好的图表文件单独放这样调页面样式的时候不会误碰数据处理脚本。movie_analysis/ ├── app.py # Flask 入口负责路由和页面渲染 ├── data/ │ ├── raw/ # 原始数据只读不改 │ └── clean/ # 清洗后的数据可被分析脚本直接加载 ├── src/ │ ├── clean_data.py # 数据清洗脚本输出干净 DataFrame │ ├── analyze.py # 聚合统计逻辑比如 Top10、年度趋势 │ └── charts.py # 所有图表生成函数统一出口 ├── static/ │ ├── css/ │ └── js/ ├── templates/ │ ├── index.html # 主页 │ └── charts/ # pyecharts 生成的 HTML 文件放这里 └── requirements.txt把清洗脚本从 app.py 里拆出来是最值得坚持的一个决定。很多同学一开始图省事把所有代码堆在 app.py 里结果后期加图表时每次都要在几百行代码里翻找。src/clean_data.py 只管“脏数据进、干净数据出”src/analyze.py 只管“干净数据进、统计结果出”src/charts.py 只管“统计结果进、图表出”。每一层只依赖上一层的输出排查问题的时候能直接定位到是哪一层出了毛病。提示data/raw 下的原始文件不要手工改动。每次清洗都从原文件重新跑脚本保证清洗逻辑可重复这是数据项目的基本素养。3. 从原始数据到能画图的表清洗脚本与类型处理的细节这一章是整个系统的重头戏。大多数拿到电影数据集的人第一步就是画图然后被一堆乱码、错位、排序错误折腾到怀疑人生。先清洗再画图顺序不能反。3.1 数据从哪来公开数据集优先爬虫只做补充我的建议是第一步先找公开数据集比如 MovieLens 这类常用的电影评分数据集或者通过 TMDB 这类公开 API 按条件拉取。选数据集时重点看字段是否齐全电影名称、上映年份、评分、类型、票房或评分人数至少要有这五类否则后面分析维度会很受限。不要一上来就写爬虫。爬虫的时间成本是公开数据集的十倍不止而且还要处理反爬、断网、字段解析一堆额外问题。如果确实需要补充数据用公开 API控制请求频率每次请求之间加 sleep不要对任何目标站做高频请求。拿到数据后第一步不是画图而是“侦查”。先看看数据到底长什么样import pandas as pd # 读取原始数据先不指定 dtype让 pandas 自己推断 df pd.read_csv(data/raw/movies.csv, encodingutf-8) print(df.shape) # (行数, 列数)先确认数据量级 print(df.dtypes) # 每一列的类型重点看年份和评分 print(df.head(3)) # 前 3 行直观检查内容是否正常 print(df.isnull().sum()) # 每列缺失值数量这段代码是清洗前的四个侦查动作。df.shape 告诉你数据规模如果只有几百条后续图表会很单薄需要换数据源df.dtypes 是最关键的一步年份列如果是 object 类型后面排序必乱df.head(3) 让你肉眼确认中文是否乱码、日期格式是否统一df.isnull().sum() 直接暴露哪些列有缺失值。这四个打印结果看完清洗方案基本就定了。3.2 清洗的四个固定动作缺失值、重复行、类型、异常值清洗不是把所有空值都删掉而是根据字段的重要性决定去留。我的固定动作是先删关键字段为空的行再去重再转类型最后过滤异常值。def clean_movies(df: pd.DataFrame) - pd.DataFrame: # 1) 删除关键字段为空的记录 # title 为空没法分析year 为空没法按时间聚合所以这两列必须删 df df.dropna(subset[title, year]) # 2) 按名称去重保留第一次出现的记录 # 很多数据集里有同一部电影重复录入的情况 df df.drop_duplicates(subsettitle, keepfirst) # 3) 修正类型评分转 float年份转 int # errorscoerce 表示遇到非法值直接变成 NaN后面统一处理 df[rating] pd.to_numeric(df[rating], errorscoerce) df[year] pd.to_numeric(df[year], errorscoerce) df[year] df[year].astype(Int64) # 可空整数类型避免 NaN 被强转出错 # 4) 日期字段统一成 datetime避免字符串比较出问题 df[release_date] pd.to_datetime(df[release_date], errorscoerce) # 5) 过滤异常值评分范围 0-10年份在 1900 之后 df df[(df[rating] 0) (df[rating] 10)] df df[df[year] 1900] # 6) 删掉类型转换后仍为空的记录然后重置索引 df df.dropna(subset[rating, year]) df df.reset_index(dropTrue) return df这段代码有三个参数值得单独讲。第一dropna 的 subset 参数只检查指定列不会误删其他列有缺失但核心字段完整的行。第二errorscoerce 是处理脏数据的核心它把“abc”这样的非法评分变成 NaN而不是直接让程序崩溃。第三astype(Int64) 用的是 pandas 的可空整数类型普通 astype(int) 在有 NaN 时会报错Int64 可以安全过渡。清洗完之后逻辑还没结束。要验证清洗结果是否符合预期print(df_clean.shape) print(df_clean[year].describe()) # 看年份的 min/max 是否合理 print(df_clean[rating].describe()) # 看评分的 min/max 是否在 0-10 内 print(df_clean.isnull().sum()) # 所有列缺失值应为 0describe() 输出的 min、max、mean 是最快的验证手段。如果年份最小值是 1895、最大值是 2026说明还有异常值漏网需要回头调整过滤条件。这一步做完数据才算“能用”。3.3 清洗结果存哪里SQLite 比 CSV 更适合这个场景清洗后的数据要持久化。存 CSV 最简单但 Flask 每次启动都要全量读一遍而且按条件查询要自己在内存里过滤。SQLite 是更好的选择它不需要额外安装服务Python 内置 sqlite3 模块写入和查询都很直观。import sqlite3 # 保存清洗后的数据 df_clean.to_sql( movies, conn, if_existsreplace, # 每次清洗后覆盖旧表保证数据最新 indexFalse, # 不把 pandas 索引写进数据库 ) conn.close()to_sql 的三个参数是常规配置。if_existsreplace 适合这个场景因为原始数据不变每次重新清洗后覆盖即可不需要保留历史版本。indexFalse 一定要写否则 pandas 默认的行号会被写成 id 列查询时多一列干扰。保存到 SQLite 之后Flask 路由里可以用一条 SQL 按年份或类型筛选比全量读 CSV 再过滤高效得多数据量到几万条时差距很明显。4. 图表怎样才算“能看”可视化参数与页面衔接的修正数据干净了终于可以画图。但“画出来”和“能看”是两回事。默认样式下的图表标题居中、坐标轴拥挤、颜色刺眼放在论文里答辩现场会直接拉低整体印象分。4.1 matplotlib 还是 pyecharts按使用场景分开用我的原则很明确论文插图用 matplotlib网页交互用 pyecharts。两者不是二选一的关系而是各管一段。对比项matplotlibpyecharts输出形式PNG/SVG 图片或 Figure 对象HTML/JS 文件交互能力无tooltip、缩放、图例筛选中文显示需要手动指定字体内置处理相对完善适用场景论文插图、答辩 PPT网页看板、动态演示为什么不在网页里全用 matplotlib因为 matplotlib 输出的是静态图片用户没法把鼠标悬停在柱子上看具体数值也没法缩放。pyecharts 底层是 ECharts交互是浏览器原生能力几乎零成本就能拿到。反过来论文插图如果用 pyecharts导出高清图片反而麻烦matplotlib 直接 savefig 更顺手。4.2 用 pyecharts 画 Top10 评分柱状图参数逐个调以“评分 Top10 电影”为例这是毕设里最常出现的图。默认代码三行就能出图但标题重叠、横轴文字挤在一起的问题马上会暴露。参数要逐个调from pyecharts.charts import Bar from pyecharts import options as opts top10 df_clean.nlargest(10, rating) # 按评分取前 10 bar ( Bar() .add_xaxis(top10[title].tolist()) .add_yaxis(评分, top10[rating].round(1).tolist()) .set_global_opts( title_optsopts.TitleOpts(title评分 Top10 电影), xaxis_optsopts.AxisOpts( axislabel_optsopts.LabelOpts(rotate30) # 横轴文字旋转 30 度 ), yaxis_optsopts.AxisOpts(name评分), ) .set_series_opts( label_optsopts.LabelOpts(positiontop, formatter{c} 分) ) ) bar.render(templates/charts/top10_rating.html)三个参数是关键。第一rotate30电影名称通常有 5 到 10 个汉字横排会挤成一团旋转 30 度后每个标题都能完整显示。第二formatter{c} 分把柱子上方的数值标签格式化成“8.5 分”比光秃秃的数字更完整答辩时评委一眼就能读出含义。第三nlargest(10, rating) 而不是先 sort_values 再 head(10)是因为 nlargest 性能更好写法也更直接。如果要用 matplotlib 画同样一张图中文乱码问题必须先处理import matplotlib.pyplot as plt # Windows 下用 SimHeiLinux 下换成 WenQuanYi 之类的中文字体 plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False # 解决负号显示为方块的问题 fig, ax plt.subplots(figsize(10, 6)) ax.bar(top10[title], top10[rating], color#3b82f6) ax.set_title(评分 Top10 电影) ax.set_ylabel(评分) plt.xticks(rotation30) plt.tight_layout() plt.savefig(static/top10_rating.png, dpi200)plt.rcParams 的两行设置必须放在画图之前且要全局生效。axes.unicode_minus 设置为 False是为了避免坐标轴上的负号显示成方块这个坑在画年度票房差异图时必踩。figsize(10, 6) 直接决定图片长宽比论文插图一般用这个比例savefig 的 dpi200 是打印质量的下限答辩 PPT 投到投影仪上不会糊。4.3 Flask 页面怎么把图表和数据传出去两种路径pyecharts 生成的是 HTML 文件Flask 接入有两种路径。第一种最简单把 render 出来的 HTML 放在 templates/charts 目录下Flask 路由直接返回这个模板。第二种更灵活把聚合结果转 JSON 传给前端前端用 ECharts 自己渲染。from flask import Flask, render_template, jsonify app Flask(__name__) app.route(/) def index(): return render_template(index.html) app.route(/chart/top10) def chart_top10(): # 方式一直接返回 pyecharts 生成好的 HTML 内容 return render_template(charts/top10_rating.html) app.route(/api/yearly) def api_yearly(): # 方式二把年度评分趋势的聚合结果返回 JSON供前端异步加载 yearly df_clean.groupby(year)[rating].mean().round(2) return jsonify({ years: yearly.index.tolist(), ratings: yearly.values.tolist() })方式一适合静态图表数据不变图表文件生成一次就够。方式二适合需要和前端交互的场景比如用户点击“只看 2015 年之后”前端重新请求 /api/yearly?start2015后端过滤后返回新 JSON前端刷新图表。两种方式不冲突我一般混着用核心展示图表用方式一带筛选条件的面板用方式二。5. 避坑指南这个选题最容易翻车的 5 个地方这个选题技术难度不高但坑不少而且每个坑都出人意料。下面这 5 条是我最常遇到的情况每条都按现象、原因、解决三个步骤说明。5.1 CSV 一读进来中文全是乱码现象df.head() 打印出来电影名称变成一堆“鍝堝埄”之类的乱码。原因数据集的编码不是 UTF-8而是 GBK 或者 UTF-8-BOM。Windows 保存的 CSV 经常带 BOM 头直接按 UTF-8 读会把 BOM 字符一并读进来。解决读取时先试 utf-8-sig不行再换 gbk# 优先用 utf-8-sig兼容带 BOM 的文件 df pd.read_csv(data/raw/movies.csv, encodingutf-8-sig) # 如果还乱码试试 GBK # df pd.read_csv(data/raw/movies.csv, encodinggbk)utf-8-sig 会自动剥离 BOM 头而普通 utf-8 不会。这个坑不踩一次很难记住我现在的习惯是拿到 CSV 先看 head再决定编码。5.2 年份字段读进来是 float2020 变成 2020.0现象画 x 轴年份时刻度出现 2018.5、2019.5 这种非整数。原因CSV 里年份列存在空值pandas 读取时为了保留空值把整列推断为 float 类型。解决先转数值再处理空值最后转整数注意用可空整数类型df[year] pd.to_numeric(df[year], errorscoerce) df df.dropna(subset[year]) df[year] df[year].astype(Int64) # 重要不能用 astype(int)astype(Int64) 是 pandas 提供的可空整数类型直接 astype(int) 在理论上没问题但如果数据里还有漏网的空值会直接抛异常。用 Int64 先把空值和正常值共存确认 dropna 之后再做最终转换。5.3 评分排序结果完全不对10 分排到了 9 分前面现象按评分降序排序结果是 10、9、8.5、8字符串排序的特征非常明显。原因评分列被读成了字符串字符串排序按字符逐位比较所以 10 排在 9 前面。解决排序前先打印 df[rating].dtype如果是 object先转数值再排序print(df[rating].dtype) # 如果显示 object说明是字符串 df[rating] pd.to_numeric(df[rating], errorscoerce) df df.dropna(subset[rating]) top10 df.nlargest(10, rating)这个坑的根本教训是任何排序操作之前先确认列的类型。df.dtypes 输出看一遍能省掉后面所有排序问题。5.4 pyecharts 图表在网页里空白F12 一看 JS 404现象render_template 返回页面正常但图表区域空白打开浏览器开发者工具看到一堆 404指向 echarts.min.js 之类的外部资源。原因pyecharts 生成的 HTML 默认引用的是 CDN 上的 JS 文件或者引用的路径和你 Flask 的静态目录对不上。评委电脑没网或者 Flask 的 static 路由配置不对图表就渲染不出来。解决把 pyecharts 生成的 HTML 文件放到 templates 下并检查其中 JS 引用路径# 生成时指定更合适的资源路径避免 CDN 失效 bar.render(templates/charts/top10_rating.html)然后在 Flask 里检查模板中 script src 的路径。如果引用的是绝对 CDN 地址建议下载 echarts.min.js 放到 static/js 里手动修改 HTML 中的引用。答辩现场的教训是任何时候都不要依赖外网 CDN本地化才是安全的。5.5 下拉框筛选后图表还是全量数据现象页面上下拉框可以选择年份但选完图表不变或者只有数字在变图表根本没刷新。原因前端只把选中值显示在了页面文字上没把参数传给后端或者后端接口接收了参数但重新查询逻辑写错了。解决筛选条件通过 GET 参数传给后端后端重新聚合后返回新图表或新 JSONapp.route(/chart/by_year/int:year) def chart_by_year(year): subset df_clean[df_clean[year] year] if subset.empty: return 该年份无数据 # 用 subset 重新生成图表 chart build_chart(subset) return chart关键点在于筛选必须在后端重新执行而不是在前端做假隐藏。前端点击下拉框后通过 location 跳转或 fetch 请求带上参数后端根据参数重新生成图表数据页面才能真正联动。这是“系统”两个字的核心体验值得多花时间调通。6. 一个进阶技巧用配置字典管理图表答辩现场加图也不慌做到前面五章的程度系统已经可以演示了。但有一个实际问题答辩时评委可能随口问“能不能看看不同类型电影的平均评分对比”如果你每张图都写一段独立代码现场加图做不到只能尴尬地说“这个没做”。我的解法是把图表定义抽成配置字典用数据驱动生成函数。这样新增一张图只需要加一段配置不需要新写代码。# charts.py from pyecharts.charts import Bar, Line, Pie from pyecharts import options as opts def render_chart(kind: str, df: pd.DataFrame, config: dict): 按配置字典生成图表。 kind: 图表类型bar / line / pie config: 包含 x, y, title, rotate 四个键 x_data df[config[x]].tolist() y_data df[config[y]].tolist() if kind bar: chart ( Bar() .add_xaxis(x_data) .add_yaxis(config.get(series_name, 数值), y_data) .set_global_opts( title_optsopts.TitleOpts(titleconfig[title]), xaxis_optsopts.AxisOpts( axislabel_optsopts.LabelOpts(rotateconfig.get(rotate, 30)) ), ) ) elif kind line: chart ( Line() .add_xaxis(x_data) .add_yaxis(config.get(series_name, 数值), y_data, is_smoothTrue) .set_global_opts(title_optsopts.TitleOpts(titleconfig[title])) ) else: chart ( Pie() .add(, [list(pair) for pair in zip(x_data, y_data)]) .set_global_opts(title_optsopts.TitleOpts(titleconfig[title])) ) return chart调用时是这样config { x: year, y: rating, title: 年度平均评分趋势, rotate: 30, } chart render_chart(line, yearly_df, config) chart.render(templates/charts/yearly_trend.html)这才是这个技巧的核心价值图表类型、数据列、标题全部参数化。评委说“按年份看评分”你把 kind 换成 liney 换成 rating评委说“看看类型分布”你把 kind 换成 piex 换成 genres。改配置不改代码答辩现场加图就是几分钟的事。我做完这个系统后最大的后悔是没在第一天就把图表模块设计成配置化。前面每张图都是独立函数后面重构花了一整晚。这个教训后来变成了我的固定习惯凡是重复出现的图形结构第一版就抽成配置字典。希望这个习惯也能帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询