
简介面向Python数据分析与可视化课程期末项目的完整源码包适合需要完成电影数据类大作业的高校学生及爬虫/可视化入门者。资源围绕豆瓣电影Top250展开覆盖数据爬取、清洗处理、ECharts图表分析、词云绘制及Flask网页展示的完整链路可直接运行或二次扩展。包体共8个文件含4个Python程序、2个资源压缩包、1个SQLite数据库与1份文档说明整体仅3.5MB轻量且结构清晰。目前已有1683人学习下载说明项目具备较强的参考价值。通过该源码包可掌握网络爬虫抓取、数据清洗处理、ECharts可视化图表、词频统计与词云制作以及Flask搭建简易展示网站的方法为同类数据分析项目提供可复用的思路与代码骨架。项目目录划分明确便于定位源码、数据与文档尤其对完成期末大作业、快速搭建可演示项目具有直接帮助。1. 电影数据可视化分析系统是什么一条从 CSV 到浏览器大屏的完整链路把“基于Python的电影数据可视化分析系统源码文档说明高分期末大作业.zip”翻译成人话它交付的是一套完整的数据分析闭环从数据源拿到电影数据经过清洗、统计、聚合最后在浏览器里以图表大屏的形式展示出来。这个方向在期末大作业里拿高分的原因通常不在图表画得多花哨而是数据从“原始状态”到“可被解读”的处理逻辑有没有讲清楚答辩时能不能把每一步为什么这么做说透。适合正在做期末大作业、课程设计或者想在简历里放一个能现场演示的 Python 项目的人。前提是你会一点 Python 基础语法剩下的就是把下面这套链路复现一遍。2. 技术选型与目录结构Flask ECharts 的取舍依据2.1 为什么不用 Jupyter也不用 Pyecharts很多同学第一反应是用 Jupyter Notebook 做数据可视化因为 pandas 直接 plot 最省事。但期末大作业的交付物是“系统”不是“笔记”。Jupyter 的每个 cell 独立运行老师拿到手不知道先点哪个而且它没法做成带交互的网页应用答辩演示效果会打折扣。我一般建议把 Jupyter 留给前期探索最终交付用 Web 方案。前端可视化这个位置常见做法是 Pyecharts 或原生 ECharts。Pyecharts 的优势是能用 Python 直接生成图表但它的版本割裂很严重1.x 和 2.x 的 API 差异大生成的 HTML 是静态文件后期想做“点击柱状图联动表格”这类交互时绕回 JavaScript 的成本很高。原生 ECharts 只有一套前端配置后端只需要返回 JSON职责清晰答辩时你可以说“前端负责渲染、后端负责聚合计算”这句话本身就是加分项。后端框架选 Flask 而不是 Django理由很简单这个项目的数据量通常在万条以下不需要数据库不需要后台管理系统一个 app.py 加几个路由就能承载全部接口。Django 的惯例式目录结构对这种单机演示项目反而显得笨重。数据存储直接用 CSV 文件省去 MySQL 连接配置避免“在老师电脑上跑不起来”这种事故。2.2 目录结构每个文件干什么提前定好源码包的目录结构直接决定你后面写代码顺不顺手。我见过不少翻车现场清洗脚本、Web 代码、数据文件全堆在根目录改一个字段名要全局搜索三遍。下面这套结构是电影可视化项目最常见的组织方式数据、逻辑、展示三层分离。movie_analysis/ ├── app.py # Flask 主程序所有 API 路由都在这里 ├── requirements.txt # 依赖清单 ├── data/ │ ├── movies.csv # 原始数据爬虫或数据集导出的落盘文件 │ └── clean_movies.csv # 清洗后数据clean.py 的运行产物 ├── analyze/ │ ├── __init__.py # 空文件标记为 Python 包 │ ├── clean.py # 数据清洗脚本处理缺失值和字段格式 │ └── stats.py # 统计聚合逻辑生成图表所需 JSON ├── templates/ │ └── index.html # 可视化页面主体占位 div ECharts 初始化 ├── static/ │ ├── css/style.css # 页面样式控制图表布局 │ └── js/main.js # 前端交互逻辑fetch 接口并渲染图表 └── docs/ └── 说明文档.md # 大作业文档写选型理由和运行步骤这个结构有几个设计点值得说。data 目录下保留原始数据和处理后数据两份文件是为了文档里能写“清洗前后对比”这也是期末评分的一个观察点。analyze 目录单独拆成清洗和统计两个模块是因为答辩时老师大概率会问“这两部分如果分开跑结果能不能对得上”拆分后每步产物都可以独立验证。templates 和 static 是 Flask 的默认模板目录按惯例放不会出错。2.3 依赖环境用 requirements.txt 锁版本依赖管理比很多人想象的更重要。期末项目最常见的翻车就是“在我电脑上能跑到老师电脑上报错”十次里有八次是 pandas 或 Flask 版本不一致。我的习惯是创建虚拟环境后把关键依赖写死大版本。python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install flask pandas numpy pip freeze requirements.txtrequirements.txt 里建议至少包含以下核心包版本号以你实际安装为准flask3.0 pandas2.0 numpy1.26安装时如果默认源慢可以临时走国内镜像pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple注意不要盲目升级到最新版 pandas。有些教学视频里的 df.append() 在 pandas 2.x 里已经被移除了后面避坑章节会专门讲。装好依赖后先跑一遍python analyze/clean.py再启动python app.py浏览器打开 http://127.0.0.1:5000 能看到页面说明链路基本通了。3. 数据清洗与统计口径把 5000 条垃圾数据变成 4 组可展示 JSON3.1 加载数据与字段预处理先处理编码和缺失值大部分电影数据集来自爬虫或公开数据集字段名可能是英文的也可能混着中文格式五花八门。拿到数据第一件事不是分析而是先看形状和列名确认手里的牌长什么样。我习惯先把原始数据落成 CSV字段统一用英文小写加下划线避免 Flask 渲染模板时处理中文键名。import pandas as pd import numpy as np # 读取原始数据utf-8-sig 兼容 Excel 保存的中文 CSV df pd.read_csv(data/movies.csv, encodingutf-8-sig) print(df.shape) print(df.columns.tolist()) print(df.head(3)) # 标题和评分是分析主键缺失直接丢弃 df df.dropna(subset[title, rating]) # 上映日期形如 2023-07-14提取年份 df[year] df[release_date].astype(str).str[:4] df[year] pd.to_numeric(df[year], errorscoerce) df df.dropna(subset[year]) df[year] df[year].astype(int)这段代码有四个关键点。第一encodingutf-8-sig解决中文乱码问题后面避坑章节会展开。第二dropna(subset[title, rating])只针对分析必需字段做删除不要一上来整行 dropna否则数据量少一大截。第三astype(str).str[:4]是防御性写法有些行可能是时间戳或者格式异常直接切字符串最稳。第四errorscoerce会把无法解析的年份变成 NaN之后再统一删除避免 groupby 时把异常年份当成一个独立分组。3.2 类型字段的多值拆分一条电影属于多个分类电影类型字段是数据清洗里最典型的坑。原始数据里 genres 可能是“喜剧,爱情”也可能是“喜剧|爱情”还有可能是“喜剧, 爱情”带空格。如果不做拆分后面统计“喜剧电影数量”时df[df[genres] 喜剧]只能捞到类型恰好只有“喜剧”一部词的电影大量“喜剧爱情”被漏掉。# 统一分隔符先把中文逗号、竖线都替换成英文逗号 df[genres] df[genres].fillna(未知) df[genres] df[genres].replace({、: ,, |: ,, : ,}, regexTrue) df[genres] df[genres].apply(lambda x: ,.join([g.strip() for g in x.split(,) if g.strip()])) # explode 拆成一行一类型便于统计各类型电影数量 genre_df df.assign(genredf[genres].str.split(,)).explode(genre) genre_count genre_df[genre].value_counts() genre_top10 genre_count.head(10) print(genre_top10)replace这一步是很多人忽略的。爬虫数据的分隔符经常不统一不先归一化后面的 explode 和 value_counts 都会统计出“喜剧 ”、“喜剧”两个类别。strip()去掉类型名两端的空格if g.strip()过滤空字符串这两行能省掉后续大量手工修数据的痛苦。explode 是 pandas 里非常实用的多值展开方法一行代码就把一对多关系变成标准的一行一条记录。3.3 四个统计口径期末作业的标准分析维度数据清洗完统计口径决定了可视化面板上放哪几张图。我通常保留四个维度既覆盖评分、时间、类型、主创这几个电影分析的基本面又能对应四张图不会让页面显得太空。# 维度一评分分布按 1 分档统计 bins list(range(0, 11, 1)) labels [f{i}-{i1} for i in range(10)] df[score_bin] pd.cut(df[rating], binsbins, labelslabels, rightFalse) score_dist df[score_bin].value_counts().sort_index() # 维度二年份趋势每年电影产量和平均评分 year_group df.groupby(year).agg( movie_count(title, count), avg_rating(rating, mean) ).reset_index() # 维度三类型 TOP10直接复用拆分后的 genre_df genre_top10 genre_df[genre].value_counts().head(10) # 维度四导演产量 TOP15 director_top15 df[director].value_counts().head(15)pd.cut的rightFalse参数是易错点它表示区间左闭右开即 [0, 1)、[1, 2) 这样分。如果不设这个参数评分恰好等于 8.0 的电影会被归到 7-8 这一档而不是 8-9 档图表的边界情况和文档描述对不上。groupby(year).agg()一次性算两个指标避免分开 groupby 两次。这种聚合结果导出的 JSON 结构就是{year: [2019, 2020], movie_count: [32, 45]}前端 ECharts 直接取走就能绑定到坐标轴。统计完成后建议把结果用to_json()落盘或打印出来人工核对一遍。比如评分最高的年份是不是某一年类型 TOP1 是否符合认知。这个环节叫“人工抽样验证”文档里写上“统计结果已抽样核对”会让报告显得严谨很多。4. Flask 接口与 ECharts 联调四张核心图表的落地代码4.1 Flask 路由设计一个页面配四个只读接口后端接口设计遵循一个原则前端页面需要的每个数据块对应一个独立路由。这样前端代码可读性好答辩时讲接口职责也清楚。下面给出评分分布和年份趋势两个完整路由另外两个照葫芦画瓢。from flask import Flask, render_template, jsonify import pandas as pd app Flask(__name__) # 模块加载时读一次 cleaned 数据避免每次请求都读盘 df pd.read_csv(data/clean_movies.csv, encodingutf-8-sig) app.route(/) def index(): return render_template(index.html) app.route(/api/score_dist) def api_score_dist(): bins list(range(0, 11, 1)) labels [f{i}-{i1} for i in range(10)] dist pd.cut(df[rating], binsbins, labelslabels, rightFalse) value dist.value_counts().sort_index().astype(int) return jsonify({ labels: value.index.tolist(), counts: value.values.tolist() }) app.route(/api/year_trend) def api_year_trend(): group df.groupby(year).agg( movie_count(title, count), avg_rating(rating, mean) ).reset_index() return jsonify({ years: group[year].tolist(), counts: group[movie_count].tolist(), ratings: round(group[avg_rating], 2).tolist() })两个最重要的细节在返回语句里。astype(int)把 numpy 的 int64 转成 Python int否则 jsonify 会报 “int64 is not JSON serializable” 的错。round(group[avg_rating], 2)是为了避免平均评分输出一长串小数前端 tooltip 显示时难看。df在模块加载时读入内存而不是每次请求都读是因为函数内每次pd.read_csv会有明显的磁盘开销数据量小的时候虽然无感但这是一个“系统设计”层面的加分项。4.2 前端模板ECharts 初始化与数据绑定前端页面是查看结果的地方代码量不大但有几个细节直接决定演示是否顺畅。核心是fetch拿到 JSON 后调用setOption。!DOCTYPE html html langzh-CN head meta charsetUTF-8 title电影数据分析面板/title script srchttps://cdn.jsdelivr.net/npm/echarts5.5.0/dist/echarts.min.js/script style .chart-box { display: flex; flex-wrap: wrap; gap: 16px; } .chart-item { width: 46%; height: 380px; border: 1px solid #eee; padding: 8px; } /style /head body div classchart-box div idscoreChart classchart-item/div div idyearChart classchart-item/div /div script const scoreChart echarts.init(document.getElementById(scoreChart)); fetch(/api/score_dist) .then(res res.json()) .then(data { scoreChart.setOption({ title: { text: 电影评分分布 }, tooltip: { trigger: axis }, xAxis: { type: category, data: data.labels }, yAxis: { type: value, name: 数量 }, series: [{ name: 电影数量, type: bar, data: data.counts, itemStyle: { color: #5470c6 } }] }); }); /script /body /htmlchart-item的宽度用百分比加gap弹性布局适配不同分辨率的笔记本避免拔掉外接显示器后图表被挤成一条线。echarts.init必须在页面拿到对应 div 的宽高之后调用所以 CSS 里显式给height: 380px否则初始化出来是 0 高度的白块。这里的 CDN 路径可以换成你项目里 static 目录下自己下载的 echarts.min.js避免演示现场没网导致图表全部白屏——这个坑下文会再提。4.3 启动流程与数据流检查整套系统跑起来只需要两条命令但我建议按顺序做一次数据流验证而不是直接跳到最后结果。python analyze/clean.py python app.py第一条命令生成 data/clean_movies.csv如果控制台没有报错先打开这个文件扫一眼确认年份是四位数字、类型字段没有裸露的竖线分隔符。第二条命令启动 Flask默认监听 5000 端口。浏览器访问首页后打开开发者工具F12切到 Network 面板刷新页面检查 /api/score_dist 和 /api/year_trend 两个请求是否返回 200。如果某个接口返回 500点开响应体能看到具体报错堆栈通常是数据类型转换问题。这一步操作在答辩前做一遍相当于把数据端到端验证了一次隐患提前暴露。5. 避坑记录中文乱码、白屏和 JSON 序列化的 5 条排查路径5.1 CSV 读取中文乱码或直接抛 UnicodeDecodeError现象pd.read_csv(movies.csv)报UnicodeDecodeError或者读出来表格里中文变成“鐢靛奖”这类乱码字符。原因爬虫导出的 CSV 是 UTF-8 编码但 Excel 打开后另存为时会变成 GBKANSI反过来有人用 Excel 从 GBK 另存的 CSVpandas 默认按 UTF-8 解析就崩了。解决项目内约定所有 CSV 统一保存为utf-8-sig编码读取时写死encodingutf-8-sig。如果你的原始数据已经乱码用记事本打开后“另存为 UTF-8”是一次性补救但最好还是在清洗脚本里把编码作为参数显式传入这样别人拿到源码在你电脑之外运行也不会翻车。5.2 双击 index.html 打开是空白页图表全部不渲染现象从项目目录里直接双击 templates/index.html浏览器标签页标题显示了但图表区域一片空白。原因fetch(/api/score_dist)用的是相对路径浏览器以 file:// 协议打开时没有 http 服务端请求直接被拦截控制台报跨域相关错误。ECharts 的 JS 文件可能加载了但数据根本没拿到。解决永远通过python app.py启动然后访问 http://127.0.0.1:5000。如果现场网络环境不能用 CDN把 echarts.min.js 下载到 static/js 目录模板里改引用本地文件路径换台没有外网的电脑也能演示。5.3 Flask 接口报错Object of type int64 is not JSON serializable现象浏览器访问 /api/score_dist 直接显示 500 Internal Server ErrorFlask 终端打印TypeError: Object of type int64 is not JSON serializable。原因pandas 的value_counts()结果 index 和 values 都是 numpy.int64 类型Python 自带的 json 模块不认识 numpy 的任何类型jsonify 就炸了。解决返回前显式转换类型。推荐写一个统一函数包裹所有统计数据比如def jsonify_data(index, values): return jsonify({labels: index.tolist(), values: [int(v) for v in values]})之后每个接口调它。这里用int()而不是.astype(int)因为后者仍然返回 numpy 类型只是精度变了序列化照样报错。5.4 类型统计漏数喜剧电影数量比预期少一半现象统计类型 TOP10 时“喜剧”数量明显偏低检查原始数据发现很多电影 genres 字段是“喜剧,爱情”。原因统计代码写了df[df[genres] 喜剧]这个条件要求字段完全等于“喜剧”而实际字段是逗号连接的多值字符串。解决用df[genres].str.contains(喜剧)或者更规范的做法是在清洗阶段就explode拆分让每一行只保留一个类型。这个坑在期末答辩时极易被老师点出来建议在文档里主动写一句“类型字段包含多值已做拆分为‘电影-类型’一对多结构”。5.5 pandas 2.x 里 df.append() 无影无踪现象按网上的老教程写df.append({year: 2024}, ignore_indexTrue)运行直接报AttributeError: DataFrame object has no attribute append。原因pandas 2.0 正式移除了 append 方法官方推荐用pd.concat。解决改成pd.concat([df, pd.DataFrame([new_row])], ignore_indexTrue)。同理旧教程里的df.iloc[:, [0]]、df.ix[]这类写法也已经过时遇到报错先查 pandas 版本再对照官方文档调整。顺带一个关联坑年份字段如果用object类型直接 groupby会把 NaN 单独分一组记得清洗阶段做dropna。6. 进阶玩法点击柱状图联动下方电影列表基础图表做完只能拿“完成分”想冲高分建议加一个联动交互。思路是点击评分分布柱状图的某个分数区间页面下方展示该区间的电影列表。这个功能能向老师展示你对前后端数据流的完整掌控而且代码量不大。后端加一个带参数的路由接收区间上下界from flask import request app.route(/api/score_range) def api_score_range(): lo float(request.args.get(lo, 0)) hi float(request.args.get(hi, 10)) sub df[(df[rating] lo) (df[rating] hi)] result sub[[title, year, rating, director]].sort_values(rating, ascendingFalse).head(20) return jsonify(result.to_dict(orientrecords))前端在图表初始化之后绑定 click 事件把区间名解析成参数再拉数据scoreChart.on(click, params { const [lo, hi] params.name.split(-).map(Number); fetch(/api/score_range?lo${lo}hi${hi}) .then(res res.json()) .then(list { const html list.map(item trtd${item.title}/tdtd${item.year}/tdtd${item.rating}/td/tr ).join(); document.getElementById(movieTable).innerHTML html; }); });这个联动的价值在于把“数据可视化”从单向展示变成了“可视化 下钻分析”。我做过几个期末项目最大的教训是不要在最后一天改数据源字段名清洗脚本和前端接口会一起废掉先固定列名再写分析逻辑等于给自己留了后悔药。图表配色、标题这些细节最后留半小时调就行。希望帮到你。本文还有配套的精品资源点击获取