简介:基于Python的疫情数据可视化分析系统毕业设计资料包,面向计算机及相关专业学生,也适合需要完整项目参考的开发者。压缩包共747个文件,大小约21.81MB,文件类型覆盖后端Python脚本(40个py)、前端Vue组件(43个vue)、HTML页面、JavaScript交互脚本、CSS样式,以及SQL数据库文件、docx论文文档、运行辅助脚本和SVG/GIF/PNG等可视化素材。特别是包含安装.bat、运行.bat、2-run.bat、3-build.bat等一键式批处理,可简化环境部署;另有多份.bak备份文件(如index.html.bak、IndexMain.vue.bak),保留了页面改版痕迹,方便对照阅读。整体项目结构清晰,从数据采集、存储到可视化展示形成闭环,涵盖数据清洗、聚合与可视化映射等环节,对理解系统的前后端分层、图表渲染与接口调用均有参考价值。目前已有28人学习下载,整体目录划分细致,适合用于毕业设计选题、系统实现参考或二次开发扩展。
1. 疫情数据可视化分析系统:论文项目为什么总卡在“能跑”和“能讲”之间
如果你手头这个疫情数据可视化分析系统--论文.zip是打算用来做毕业设计、课程设计或者技能认证交付的,那它绝不是“画几张图”那么简单。盲审老师或答辩评委真正想看的是:你如何把一堆原始疫情数据变成可交互的图表,并且每一张图都能支撑论文里的一句结论——这两件事分开做都不难,合在一起才是这类项目的真正门槛。常见做法是采用Flask + ECharts的组合:Python 写数据接口和数据处理逻辑,ECharts 负责渲染,前端不搞复杂框架,三两天就能把主流程跑通。适合的人群很明确:有一定 Python 和 SQL 基础、但对完整 Web 系统不熟、需要快速拿出一套“代码 + 文档 + 数据”闭环的在校生或转开发者。这篇笔记就按这个思路,把从数据清洗到论文配图的整条链路拆开讲。
2. 疫情数据从哪来:公开数据集与预处理管道的三个关键选择
2.1 数据源选择:别一上来就爬卫健委,先看有没有结构化 CSV
做疫情可视化,第一步不是写爬虫,而是确认数据形态。疫情数据的公开来源大致有三类:Github 上开源的疫情数据仓库(如 CSSEGISandData 的 time_series_covid19_confirmed.csv)、各省市卫健委的公告页面、以及第三方聚合站导出的 Excel。我的建议是:论文项目优先用 CSV 数据集,原因是它自带时间维度和地区维度,能直接进 Pandas,不用处理反爬和页面结构变化。爬虫虽然很显技术含量,但对疫情这类历史数据,爬下来还要清洗,半年后裁判再跑你的代码时网站可能已经改版,复现性会很差。
拿到 CSV 之后先做三件小事:用pd.read_csv()读进来、看列名是否规范、确认日期列的格式是否统一。常见翻车点是某些数据集的日期列是"2021/1/5",有些是"2021-01-05",混合格式会在后面画折线图时直接让 x 轴错乱。
2.2 清洗原则:数据透视表比 VLOOKUP 式左连接更顺手
疫情数据通常是“一列一个国家/省份,一行一个日期”的宽表,也有“三列式”的长表(日期、地区、数值)。两者都行,但后续用 ECharts 时更推荐转成长表,因为前端按地区过滤和按日期聚合都更方便。以下是把宽表转长表的常用脚本:
import pandas as pd df = pd.read_csv("time_series_covid19_confirmed.csv") # 宽表转长表:日期列全部堆叠为 date 字段 id_vars = ["Province/State", "Country/Region", "Lat", "Long"] date_cols = [c for c in df.columns if c not in id_vars] df_long = df.melt( id_vars=id_vars, value_vars=date_cols, var_name="date", value_name="confirmed" ) # 日期标准化为 yyyy-mm-dd df_long["date"] = pd.to_datetime(df_long["date"]).dt.strftime("%Y-%m-%d") df_long = df_long[df_long["confirmed"] > 0] # 剔除无数据行 print(df_long.head()) print(df_long["date"].min(), df_long["date"].max())逻辑说明:melt是 Pandas 里宽表转长表的标准接口,id_vars是保留不变的维度字段,value_vars是需要堆叠的日期列,var_name和value_name分别指定新生成的日期列和数值列的名字。最后一步过滤掉确诊为 0 的行,既能减小数据集体积,也避免图表上出现大量前导零的平直线。
参数说明:date_cols的筛选逻辑是“不在 id_vars 里的列都视为日期”,这要求 CSV 里只有地理坐标和日期列两类变量。如果你的数据里混入了“备注”“来源”之类的列,必须先手动剔除,否则melt会把它们也当成日期。
2.3 人话总结:清洗的终点取决于论文要讲哪三个故事
不要为了清洗而清洗。你在论文里大概率只讲三个故事:全国/全球总体趋势(折线图)、省份或国家对比(柱状图/排名)、地理分布(地图)。所以清洗阶段就按这三个故事的维度去聚合——建一个daily_confirmed.csv(每日累计值)、一个province_latest.csv(最新一天各省份值)、一个country_trend.csv(多个国家/地区的日增曲线)。这样后端接口和前端图表都只需读这几个小而干净的文件,而不是每次查询都做全量聚合。
3. 后端接口设计:用 Flask 提供 4 个数据接口,而不是把 CSV 直接丢给前端
3.1 为什么不能直接让前端读 CSV
很多初学者图省事,用 ECharts 的dataset配置项直接加载本地 CSV 文件,页面也能画出图。但论文答辩时评委大概率会问你:“数据更新怎么做?多表关联怎么做?”如果答案是“手动更新CSV”,印象分会打折。更关键的是,浏览器直接读本地 CSV 在跨域和文件路径上有不少限制,部署到服务器后经常变成“本机能跑、别人打不开”。所以我要把“数据读写”和“数据展示”拆开:Flask 负责从 CSV 读数据并返回 JSON,前端只对接 JSON 接口。这样数据源换了,前端不用动。
3.2 最小 Flask 应用与接口定义
下面是我们要实现的四个接口的骨架:
| 接口路径 | 返回内容 | 对应论文图表 |
|---|---|---|
/api/trend | 全国/全球每日累计确诊与新增 | 折线图(趋势分析) |
/api/provinces | 指定日期各省份确诊数排序 | 柱状图(对比分析) |
/api/map | 省份对应的数值(用于地图着色) | 中国地图/世界地图 |
/api/summary | 总量、峰值、增速等统计指标 | 论文摘要数据引用 |
其中/api/trend是最核心的接口,不仅返回累计值,还要算“每日新增”——新增 = 当日累计 - 前一天累计,这个计算放后端做,比前端省事得多。来看接口实现的关键代码:
from flask import Flask, jsonify, request import pandas as pd app = Flask(__name__) # 启动时加载一次,避免每个请求都重新读文件 daily = pd.read_csv("daily_confirmed.csv", parse_dates=["date"]) @app.route("/api/trend", methods=["GET"]) def trend(): country = request.args.get("country", "China") start = request.args.get("start", "2020-01-01") end = request.args.get("end", "2021-12-31") df = daily[(daily["country"] == country)] df = df[(df["date"] >= start) & (df["date"] <= end)].sort_values("date") # 计算每日新增 df["new"] = df["confirmed"].diff().fillna(0) return jsonify({ "dates": df["date"].dt.strftime("%Y-%m-%d").tolist(), "confirmed": df["confirmed"].tolist(), "new": df["new"].tolist() }) if __name__ == "__main__": app.run(debug=True, port=5000)逻辑说明:parse_dates在read_csv阶段就把日期列转成datetime64,这是为了后面的切片比较和diff()计算都能按时间语义走。request.args.get给接口留了参数位,答辩时你可以现场演示“只看 2020 年上半年”“只看美国”之类的交互。diff()是 Pandas 专门做差分计算的函数,.fillna(0)把第一行的空值补成 0,避免接口返回NaN导致前端渲染异常。
参数说明:diff()默认按行索引当前位置与前一行做差,一定保证sort_values("date")在diff()之前执行。如果数据里有重复日期或者某一行的累计值比上一行小(部分数据集修正后会出现负数新增),这段代码就会计算出负值。用真实数据时最好在diff()后面加一行df["new"] = df["new"].clip(lower=0),把修正导致的负新增归零。
3.3 多条件聚合接口的二三事
/api/provinces的写法思路类似,区别在于多了按日期过滤和按数值排序:
@app.route("/api/provinces", methods=["GET"]) def provinces(): date = request.args.get("date", "2021-07-01") df = pd.read_csv("province_latest.csv", parse_dates=["date"]) df = df[df["date"] == date] df = df.sort_values("confirmed", ascending=False).head(20) return jsonify({ "names": df["province"].tolist(), "values": df["confirmed"].tolist() })这里的province_latest.csv是清洗阶段就生成好的文件,字段包括date、province、confirmed。如果不在请求里传日期,接口默认查2021-07-01,前端拿到数据后按名字与数值一一对应即可。这儿要特别注意:province的命名必须和 ECharts 地图的name完全一致,比如“广西壮族自治区”不能简写成“广西”。如果需要前端做映射会非常痛苦,这个坑在 4.3 节再展开。
4. ECharts 可视化落地:折线图、地图、动态排序三个必做图表
4.1 接线层:在 HTML 里用fetch调用后端接口
前端的骨架非常简单,一个index.html加一个app.js。核心逻辑是用fetch向刚才的 Flask 接口拿数据,再塞进 ECharts 的setOption。这是最不容易写错的方式——先在浏览器控制台看接口返回的 JSON 对不对,再去调图表。
<!-- index.html 局部 --> <div id="trend-chart" style="width: 100%; height: 400px;"></div> <div id="map-chart" style="width: 100%; height: 500px;"></div> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> <script src="app.js"></script>4.2 折线图的配置:双 y 轴处理“累计”和“新增”量级差
疫情数据里累计确诊可能是几百万,每日新增只有几千,同轴显示会导致新增曲线被压成一条直线。这里用双 y 轴:左轴给累计值,右轴给新增值,两个系列各自绑定对应的轴。具体配置写法:
// app.js 节选 async function initTrendChart() { const res = await fetch("/api/trend?country=China&start=2020-01-01&end=2021-12-31"); const data = await res.json(); const chart = echarts.init(document.getElementById("trend-chart")); chart.setOption({ tooltip: { trigger: "axis" }, legend: { data: ["累计确诊", "每日新增"] }, xAxis: { type: "category", data: data.dates }, yAxis: [ { type: "value", name: "累计确诊" }, { type: "value", name: "每日新增", splitLine: { show: false } } ], series: [ { name: "累计确诊", type: "line", yAxisIndex: 0, data: data.confirmed, smooth: true }, { name: "每日新增", type: "line", yAxisIndex: 1, data: data.new, smooth: true, areaStyle: { opacity: 0.2 } } ] }); } initTrendChart();逻辑说明:yAxis写成一个数组就是定义多坐标轴,series里的yAxisIndex: 0和yAxisIndex: 1决定数据走哪条轴。areaStyle给新增曲线加了半透明填充,视觉上强调波动。trigger: "axis"让悬浮提示在同一个 x 坐标点上同时显示两条曲线的值,这是趋势类图表最常用的交互。
参数说明:smooth: true会让折线变圆润,但如果时间粒度特别细或数据本身噪声大,建议关掉 smooth,否则会掩盖真实拐点。数据集如果跨年,建议在接口层做好 start/end 过滤,避免前端一次性渲染 600 多个点导致交互卡顿。
4.3 地图图表:注册 GeoJSON 之后再匹配数据
疫情项目里最有视觉冲击力的是中国地图。ECharts 5 本身不带地图数据,需要先加载 GeoJSON 再注册地图。这里有个非常隐蔽的坑:GeoJSON 里properties.name是“北京市”,而后端返回的数据里如果写的是“北京”,地图上就永远只显示一个高亮块,其余省份全部空白。
先注册地图,再做数据映射:
// app.js 节选 - 地图部分 import chinaJson from "./china.json"; // 需要放在项目本地,不能用 CDN echarts.registerMap("china", chinaJson); async function initMapChart() { const res = await fetch("/api/map?date=2021-07-01"); const data = await res.json(); const chart = echarts.init(document.getElementById("map-chart")); chart.setOption({ tooltip: { trigger: "item" }, visualMap: { min: 0, max: 50000, left: "left", text: ["高", "低"], inRange: { color: ["#e0f3f8", "#abd9e9", "#4575b4"] } }, series: [ { name: "确诊数", type: "map", map: "china", roam: true, data: data.items // [{name: "广东省", value: 1000}, ...] } ] }); }逻辑说明:registerMap是全局注册,注册一次直接可复用。visualMap的min和max需要参考实际数据量级来定,如果写死 50000 但实际最大只有 3000,整个地图颜色会几乎不变,毫无区分度。我用的是从后端动态读取 max 值的方式:
// 动态计算最大值 const maxVal = Math.max(...data.values); // setOption 时再用 maxVal 替换 max参数说明:roam: true允许用户拖拽和缩放地图,答辩演示时放大某个省份看细节会显得很专业。ECharts 的颜色范围建议用 3~4 段渐变色就够了,不要炫技式地搞 10 段,低值区域的省份会变成深色,视觉上也会误导读者。
4.4 动态排序图:论文“最靓的仔”和它的两个前提
如果想在论文里加一页“疫情发展过程的动态排名”,可以用 ECharts 的timeline组件或纯setInterval驱动数据更新。这里我推荐轮播柱状图,每天更新一次排名,配合sort: 'desc'属性做倒序排列。整体实现逻辑是:后端一次性返回“每天各省份确诊数”的长表,前端按天切片循环更新 series。这个图表有很好的演示效果,但有两个前提必须满足:一是数据必须覆盖一个完整时间段(比如疫情开始到论文定稿前),二是省份数量建议控制在 10 个以内,超过 15 个排名变化观众根本看不清。
5. 避坑指南:疫情数据可视化项目最容易翻车的 5 个细节
5.1 中文乱码与编码问题导致接口返回“???”
现象:后端返回的 JSON 里省份名全是问号或乱码。
原因:绝大多数是 Windows 环境下 Pandas 读 CSV 时默认按 GBK 解析,而数据文件是 UTF-8 编码;或者 Flask 返回 JSON 时没有声明 UTF-8。
解决:在read_csv里显式指定编码,同时在 Flask 接口上设置响应头:
df = pd.read_csv("province_latest.csv", encoding="utf-8") # 接口返回统一走 make_response 设置 charset from flask import make_response resp = make_response(jsonify(data)) resp.headers["Content-Type"] = "application/json; charset=utf-8"5.2 时间轴的钟摆效应:数据点前后顺序错乱
现象:折线图横轴上的时间顺序不对,先显示 2021 年后显示 2020 年。
原因:后端按字符串排序导致的“字典序”问题,"2021-01-01"排在"2020-12-31"后面本质上没错,但如果数据里有"2020/1/1"这种非标准格式,两种格式混在一起就会乱。
解决:清洗阶段强制统一格式,不要只strftime,要先pd.to_datetime再格式化。判断是否洗干净的验证方法是打印df["date"].dtype,结果必须是datetime64[ns]而不能是object。
5.3 地图上总是少几个省
现象:中国地图上某些省份始终不着色,或者鼠标悬浮时显示不了名字。
原因:后端返回的省份名与 GeoJSON 里的name不一致。比如数据里写“内蒙古”,GeoJSON 里写“内蒙古自治区”。
解决:提供一个映射表统一处理,而不是去改 GeoJSON。写一个normalize_province_name()函数,把简称映射到全称:
province_map = { "北京": "北京市", "内蒙古": "内蒙古自治区", # 其余省略 } def normalize_province_name(name): return province_map.get(name, name)5.4 论文里的图表和正文数据对不上
现象:论文里写“截至 2021 年 7 月 1 日,全国累计确诊 X 万例”,但论文附的图里数值明显不对。
原因:数据集在论文写作期间更新过,或者接口参数默认值不一样导致前端看到的和论文图截的不是同一批数。
解决:答辩前固定数据版本。把接口的默认参数写入清洗阶段的config.yaml,论文里引用的每个数字都在图下方标注“数据来源:某某数据集截至某日”。更稳妥的做法是把生成论文图表的代码单独抽一个paper_charts.py,每次跑完都输出一个带日期的文件,确保截图和数据同步。
5.5 代码压缩包无法直接跑起来
现象:评审老师解压 zip 后运行程序,Flask 启动报错缺依赖,或者地图一片空白。
原因:requirements.txt没有写全,或者 ECharts 用了 CDN 链接但答辩机房没外网。
解决:requirements.txt里至少写 Flask、pandas、numpy 三个;地图的 GeoJSON 文件必须放本地;ECharts 也下载一份 min.js 放静态目录。这样确保断网环境也能完整展示。
6. 论文加分的进阶技巧:从“图表展示”升级为“分析结论”
最后说一个拉开差距的技巧:把可视化从“展示现状”升级成“验证结论”。疫情数据的价值不在图多,而在能不能支撑你论文里的决定性语句。一个常用的进阶方案是增速对比图:取“每日新增”再做 7 日滑动平均,然后把不同国家/区域的滑动平均曲线叠在一张图上面。
滑动平均的意义在于消除周末数据滞后带来的锯齿状波动,让趋势肉眼可见。实现上只需要在 Flask 接口里加一段:
df["new_smooth"] = df["new"].rolling(window=7, min_periods=3).mean()这行代码算出来的new_smooth序列可以直接以另一条 series 加进图表。但不要把它做成文章里的大图表,而是做成辅助图,放在趋势图下方,作为“疫情高峰拐点”的论据。论文的文字表述可以是“2020 年 2 月中旬起,7 日平均新增数持续下降,表明第一波传播得到控制”——这就是一张图支撑一个判断的分析型写法。
第二个进阶是加一个简单的排名变换动画作为答辩开场演示。后端提供一个/api/topn/daily接口,返回每天排名前 10 的省份与数值,前端用setInterval每隔 300 毫秒更新一次柱状图数据。这个动画做出来的整体效果非常抓人,评审在答辩前 30 秒就能看到你的系统不是死图。
我自己在带类似项目时,见过太多人把时间花在改图表颜色和多加几张饼图上,却很少有人在“数据结论”上做文章。实际上你只要能把数据、图表、文字三者串成一条完整论证链——数据说明什么、图表证明了什么、论文里怎么表述——这比任何炫技都有效。技术方面,代码能跑通只是第一步,加上“数据版本固定”“断网可演示”“参数可交互”这三件事,才是论文项目最该有的底气。希望帮到你。
本文还有配套的精品资源,点击获取