
简介面向计算机专业毕业设计的疫情数据可视化分析系统完整项目包基于Python与Django框架构建为需要完成毕设或入门Web开发与数据分析的学习者提供可直接运行与二次开发的范本。资源涵盖完整源码、数据库文件、配置教程及开发说明系统利用Pandas和NumPy处理疫情数据借助Django的MVC架构搭建后端服务并以MySQL作为数据存储方案清晰呈现了从数据处理到可视化的实现链路。压缩包共745个文件大小约15.81MB主要包含Python后端代码、Vue前端页面、HTML/CSS/JS静态资源、SQL数据库脚本、MD格式开发文档以及一键安装与运行的批处理脚本结构清晰便于按模块查阅。开发说明详细梳理了设计思路与功能实现配置教程可降低上手门槛数据库文件则提供了关键疫情数据使分析与可视化能够直接展开。目前已有61人学习下载对于正在准备毕业设计、希望积累项目经验或提升编程能力的学生而言是一份兼顾实用性与教学价值的参考资料。1. 为什么这类毕业设计值得花时间跑通能让你真正练到 Django 全栈的项目包答辩现场最怕听到的一句话不是“你这个功能太简单”而是“这个页面是你写的还是从某个压缩包解出来的”。基于 PythonDjango 的疫情数据可视化分析系统恰恰是那种“看起来能被一眼看穿”的选题它同时牵扯后端框架 Django、数据库建模、ORM 聚合查询和 ECharts 数据可视化任何一个环节答不上来都会被追问下一句。能跑起来只是及格线能讲清楚数据怎么进库、接口怎么吐 JSON、图表怎么绑定数据才是这份源码真正值钱的地方。这篇笔记就沿着这条线拆一遍适合拿这类项目包交差、但还没把工程真正吃透的人。2. 把压缩包变成在线系统两小时落地路线与版本选型2.1 先看版本再写代码Python 和 Django 为什么必须对齐先说一句实在话。我从别人手里接这类项目包时第一件事不是打开代码看逻辑而是看两个地方README 里标注的 Django 版本以及 requirements.txt 里的依赖清单。自己机器上装的是 Django 5.1项目写的是 Django4.2直接pip install -r requirements.txt会把版本降回去降级过程中还可能连带pandas、matplotlib装的版本不兼容一启动就报ModuleNotFoundError。这类项目包里最常见的组合是Python 3.10/3.11 Django 4.2个别老古董用 Django 2.2那个版本在 Python 3.8 以上的环境里跑python manage.py runserver会有兼容性小毛病。所以第一步不是急着安装而是先用虚拟环境把版本隔离出来别污染你自己平时写代码的 Python 环境。# 1. 在项目根目录创建虚拟环境Windows 和 Linux 命令略有区别 cd 项目根目录 python -m venv venv # 2. 激活虚拟环境 source venv/bin/activate # Linux / macOS venv\Scripts\activate # Windows 下用这个 # 3. 安装项目依赖装之前先看 requirements.txt 里锁了哪些版本 pip install -r requirements.txt # 4. 确认当前环境里 Django 版本和项目要求一致 python -m django --version参数说明venv是 Python 内置的虚拟环境工具不需要额外安装激活后终端前面会多一个(venv)前缀看到这个前缀就说明环境切对了。第四条python -m django --version比django-admin --version更可靠因为它读的是当前虚拟环境里的版本不会误读到全局安装的 Django。如果你用 VSCode 写这个项目记得按CtrlShiftP调出命令面板选择 Python 解释器指向venv目录里那个 Python。不然编辑器里import django可能会报红但终端里又能跑这种“编辑器红、命令行不红”的割裂状态最容易让人误判环境没问题。2.2 从源码到跑起来迁移、超级用户、开发服务器环境对齐之后整个项目真正能启动只需要三条命令。别急着去读代码先按顺序执行看到页面出来再说。# 1. 执行数据库迁移生成 auth_user、session 等 Django 内置表 python manage.py migrate # 2. 创建管理员账号登录 /admin 后台维护数据时要用 python manage.py createsuperuser # 3. 启动开发服务器0.0.0.0 表示局域网内别的机器也能访问 python manage.py runserver 0.0.0.0:8000逻辑说明migrate是根据项目里已有的migrations目录把 models 里定义的模型同步到数据库里。新解压的项目没有自己的数据表第一次运行会生成一堆 Django 内置表包括用户表、权限表、会话表这些是后面登录注册功能正常工作的前提。为什么createsuperuser很重要多数这类项目的后台管理页面/admin都注册了数据模型评审老师习惯点进去看有没有数据、能不能编辑没有管理员账号就进不去。命令执行时会交互式问你用户名、邮箱、密码密码输入不显示是正常的。runserver 0.0.0.0:8000里的0.0.0.0让服务监听所有网卡地址这样同一局域网下其他人的浏览器能用你电脑的 IP 加 8000 端口访问答辩演示时很实用如果只想本机看默认的127.0.0.1就够了。跑完这三步浏览器访问http://127.0.0.1:8000看到页面就说明基础环境通了。如果报错大概率栽在依赖版本上先回头检查pip freeze和requirements.txt的差异。2.3 换一台机器后必改的四个配置数据库、时区、静态文件、ALLOWED_HOSTS项目在自己电脑上跑通不代表换到答辩机器上也能跑。配置文件里最常出问题的就是 settings.py动手改之前先看这几个值。配置项常见值需要注意什么DATABASESsqlite3 默认换成 MySQL 要额外装驱动并改引擎TIME_ZONEUTC / Asia/Shanghai影响时间统计的“今天”边界USE_TZTrue / False是折线图日期“少一天”的头号原因STATICFILES_DIRS未配置 / BASE_DIR/static配错则 ECharts 的 JS 加载 404ALLOWED_HOSTS空列表 / [*]DEBUGFalse 时不配直接报错这些配置里我一般建议优先检查STATICFILES_DIRS。这个系统的主要图表依赖 ECharts它是纯前端库文件放在static/js/目录下面。如果这个配置缺失模板里{% static js/echarts.min.js %}拼出来的 URL 就是坏链路页面会白屏而且后端日志一点报错都没有。还有一个容易被忽略的点SECRET_KEY不要动它是 Django 加密会话、密码哈希的基础DEBUG如果是False又没配ALLOWED_HOSTS浏览器访问时 Django 会直接拒绝请求这是从源码包切到“生产部署模式”最典型的翻车现场。调试阶段保持DEBUGTrue把ALLOWED_HOSTS配成[*]能省掉大量排查时间。3. 数据链路是怎么搭的疫情数据入库与聚合口径3.1 数据从哪来种子 CSV 和同步脚本可视化系统最重要的不是前端画了几张图而是数据进库的链路是否可靠。以这类项目最常见的形态来说数据不会真的让你手动一条条录包里通常会带一个data目录里面是整理好的 CSV 文件字段大致是日期、省份、城市、累计确诊、疑似、治愈、死亡。把 CSV 灌进数据库常见的做法是写一个独立的导入脚本放到项目根目录用 Django 的环境入口来执行。下面这个脚本就是最通用的一种形态你在自己项目里只需要改模型名和字段映射。# load_csv.py —— 把 data/covid_daily.csv 导入数据库 import csv import os import django # 手动拉起 Django 环境否则下面的 import models 会报错 os.environ.setdefault(DJANGO_SETTINGS_MODULE, config.settings) django.setup() from stats.models import DailyReport # 改成你项目里的 app 名和模型名 def run(): path os.path.join(data, covid_daily.csv) with open(path, encodingutf-8-sig) as f: # 注意编码用了 utf-8-sig reader csv.DictReader(f) objs [] for row in reader: objs.append(DailyReport( daterow[date], provincerow[province], cityrow[city], confirmedint(row[confirmed]), suspectedint(row[suspected]), curedint(row[cured]), deadint(row[dead]), )) DailyReport.objects.bulk_create(objs, batch_size500) print(f写入 {len(objs)} 条) if __name__ __main__: run()这段代码有两个细节值得讲。第一是os.environ.setdefault(DJANGO_SETTINGS_MODULE, config.settings)和django.setup()这两行是独立脚本连接 Django 项目必备的启动步骤少了它直接from stats.models import DailyReport会报ImproperlyConfigured。第二是encodingutf-8-sigExcel 另存的 CSV 文件通常带 BOM 头用普通 utf-8 读会导致第一个字段名变成\ufeffdate后面取row[date]直接 KeyError。踩过这个坑的人会自然养成写utf-8-sig的习惯。bulk_create是一次性批量插入比循环create()快一个数量级几万条数据也就一两秒。batch_size500控制每次写入事务的大小数据量不大时可以不设但设了能让内存占用更平稳。3.2 模型要不要做成宽表一张 DailyReport 还是拆事实表疫情数据可视化的核心数据无非就是“每天、每个地区有几个数”这种数据用一张宽表就够了。很多教材会讲维度建模、事实表、维表拆分但那是针对海量数据的数仓思路毕业设计这个量级拆成三四张表反而给自己添乱联表查询变多、聚合 SQL 变复杂、答辩时还要解释为什么这么设计。我见过的绝大多数正常项目模型都长这样# stats/models.py —— 在 startapp 生成的 stats 应用里写模型 from django.db import models class DailyReport(models.Model): date models.DateField(db_indexTrue, verbose_name日期) province models.CharField(max_length32, verbose_name省份) city models.CharField(max_length32, verbose_name城市, blankTrue) confirmed models.IntegerField(default0, verbose_name累计确诊) suspected models.IntegerField(default0, verbose_name疑似) cured models.IntegerField(default0, verbose_name治愈) dead models.IntegerField(default0, verbose_name死亡) class Meta: db_table daily_report unique_together (date, province, city) verbose_name 疫情日报 def __str__(self): return f{self.date} {self.province} {self.city}字段选择上有几个关键点。日期用DateField而不是DateTimeField因为疫情统计口径是按“天”来的不需要时分秒DateTimeFieldUSE_TZTrue会引入时区换算问题后面章节会细说。db_indexTrue给日期加了索引因为后续几乎所有聚合查询都按日期范围筛选这个索引能让查询快不少。unique_together (date, province, city)是防重复导入的关键约束。数据更新脚本如果被重复执行同一天同一个城市会插入两遍聚合出来的折线图就会在某一天突然冒出一个异常的“山峰”。加了联合唯一约束后重复插入直接报错至少让你能发现数据有问题。3.3 ORM 聚合的正确姿势一次查询拿到全序列可视化接口的核心其实是聚合查询。拿“全国累计确诊趋势图”举例逻辑是按日期分组把所有省份当天的确诊数累加起来。新手容易犯的错是在 Python 里做循环先取日期列表再对每个日期查一次数据库——数据量小的时候看不出问题数据量上百天、几千条时接口响应速度会肉眼可见地变慢。正确的方式是让数据库完成聚合Django ORM 一句就能搞定# stats/views.py —— 全国每日累计趋势的 JSON 接口 from django.db.models import Sum from django.http import JsonResponse from .models import DailyReport def national_trend(request): # 先把查询结果取到内存里避免后面多轮循环触发重复查询 rows list( DailyReport.objects .values(date) .annotate(confirmedSum(confirmed), curedSum(cured), deadSum(dead)) .order_by(date) ) # 数据库返回的是 date 对象JSON 序列化不了手动转字符串 data { dates: [r[date].strftime(%Y-%m-%d) for r in rows], confirmed: [r[confirmed] for r in rows], cured: [r[cured] for r in rows], dead: [r[dead] for r in rows], } return JsonResponse(data)逻辑说明values(date)表示按日期分组annotate里的三个Sum是每组内的聚合结果order_by(date)保证输出按时间排序。这里最容易被忽略的是list(queryset)这一步——Django 的 QuerySet 是惰性的如果不先转成列表后面四次循环都会重新执行同一条 SQL等于一次页面加载把同一个聚合查了四遍。先list()拉回内存后面无论怎么遍历都只碰一次数据库。strftime(%Y-%m-%d)是第二个关键点。date对象不能直接进JsonResponse不转的话接口直接 500日志报TypeError: Object of type date is not JSON serializable。把日期的格式化放在视图层而不是前端是为了让接口返回的数据稳定——前端不用关心后端存的是 Date 还是 DateTime。4. 数据可视化不玄学从 ORM 聚合到 ECharts 图表的完整链路4.1 图表方案怎么选模板直出还是前后端分离到了可视化这块最核心的问题不是“用什么库画图”而是“数据怎么喂给前端”。常见做法有三种第一种是 Django 模板渲染页面前端用原生 fetch 请求后端 JSON 接口再由 ECharts 接管绘图第二种是前后端完全分离Vue/React 单独跑一套开发服务器通过跨域请求拿数据第三种是直接用 Django 模板的模板变量在页面里输出 JSON。毕业设计我一般建议选第一种。它在 Django 生态内自洽不需要跨域配置不需要 node 环境答辩时你既能讲后端接口设计也能讲前端图表配置中间的数据格式转换还可以当“难点”展开。前后端分离听起来时代感强但面试时老师更可能追问 CORS、前端路由、打包部署一个月内补不完这些坑。三种方式的取舍可以参考下面这张对比方案环境依赖可讲性答辩风险Django 模板 原生 JS ECharts只需要 Python 环境后端和数据可视化都能讲低Django 模板 jQuery ECharts只需要 Python 环境偏陈旧低前后端分离 Vue/React需要 node 环境前端能讲很多高环境问题概率大结论是别给自己挖坑环境越少毕业设计跑通的概率越高。原包里如果已经用了 jQuery 也无所谓功能上没有任何区别。4.2 一条折线图的完整链路模板、静态文件、数据绑定选定方案后整条链路分四层模板层放页面骨架和图表容器静态文件层放 ECharts 本体视图层返回聚合 JSON前端 JS 层把 JSON 映射成图表的 series。先看模板!-- stats/templates/dashboard.html -- {% load static %} !DOCTYPE html html langzh-cn head meta charsetUTF-8 title疫情数据可视化分析/title !-- ECharts 必须用 {% static %} 标签引用不能写死路径 -- script src{% static js/echarts.min.js %}/script /head body div idtrendChart stylewidth: 100%; height: 480px;/div !-- 页面自己的脚本放在 echarts.min.js 之后保证先加载库再调用 -- script src{% static js/dashboard.js %}/script /body /html注意{% load static %}必须写在文件最顶部否则{% static %}模板标签会报错。图表容器div必须显式设置宽高ECharts 在初始化时会读取容器尺寸容器高度为 0 会导致图表渲染出来是空白控制台还不报错这是可视化项目里最隐蔽的白屏原因。真正的数据绑定在dashboard.js里// static/js/dashboard.js —— 请求后端接口并完成折线图渲染 fetch(/api/trend/) .then(resp resp.json()) .then(data { // 容器 id 必须和模板里 div 的 id 一致 const chart echarts.init(document.getElementById(trendChart)); chart.setOption({ title: { text: 全国累计确诊趋势 }, tooltip: { trigger: axis }, xAxis: { type: category, data: data.dates }, yAxis: { type: value }, series: [{ name: 累计确诊, type: line, smooth: true, data: data.confirmed }] }); });fetch(/api/trend/)请求的是第 3 章那个视图对应的 URL返回的 JSON 结构有三组平行数组dates、confirmed、cured、dead。ECharts 的xAxis.data接收的就是类别数组所以后端返回“日期的字符串数组”是配合得最好的格式不需要前端再做任何转换。smooth: true让折线变成平滑曲线视觉上更柔和trigger: axis让鼠标悬浮时按整列显示提示框多序列对比时更好看。这些参数都是 ECharts 官方配置改数值就能调效果比如把line改成bar就是柱状图同一份数据不用改接口。4.3 日期序列化和接口设计三个小细节做了这么多次数据可视化我总结出接口设计里最容易出问题的三个细节。第一个是日期格式。后端返回日期字符串时保持%Y-%m-%d这种纯文本格式千万别返回时间戳。ECharts 的 x 轴如果收到时间戳默认按数值轴处理刻度分布会变得很怪就算你把它转成时间轴前端还得自己写格式化函数。第二个是数据口径。接口返回的字段名要跟前端代码保持绝对一致比如后端叫confirmed前端就别写case前后端各写各的最后对不上图表上显示出来的数据全是undefined。调试时先打开浏览器开发者工具看 Network 面板里接口返回的 JSON 字段名再对照 JS 里的取值逻辑。第三个是缓存。数据可视化系统里的聚合接口每次请求都现算一次全表数据量到几十万条时响应就会慢。开发阶段无所谓但演示时如果每次都卡几秒观感很差。常见的做法是在视图上直接加 Django 的缓存装饰器这个放到最后一章细讲。5. 避坑清单六个让系统“跑不起来”的配置与代码问题5.1 migrate 报 relation already exists现象第一次执行python manage.py migrate就报relation auth_user already exists或者迁移执行到一半中断。原因项目包里自带了db.sqlite3文件数据库里已经有一份完整的表和数据。迁移文件检测到表已存在和迁移记录对不上就会中断。解决备份旧数据后删除db.sqlite3重新执行migrate。如果不想删数据就跳过 migrate 直接runserver但要清楚你现在连的是旧库。最彻底的验证方法是把数据库文件删掉、迁移文件保留重新走一遍全流程这样能顺便测试“干净机器上能不能跑通”。5.2 ECharts 页面白屏控制台报 404现象页面打开后图表区域空白浏览器开发者工具的 Console 报GET /static/js/echarts.min.js 404后端日志没有任何异常。原因STATICFILES_DIRS没有配置或者 ECharts 文件没有放在static/js/目录下。Django 开发服务器默认只从每个 app 的 static 目录找静态文件项目根目录的 static 不算数。解决在 settings.py 里显式声明根目录import os STATIC_URL /static/ STATICFILES_DIRS [ os.path.join(BASE_DIR, static), ]改完重启runserver重新加载页面。这个问题的迷惑性在于页面本身能打开只有图表区域是空的而且后端日志完全干净属于典型的“前端问题后端无感知”。5.3 CSV 表头第一列变成 \ufeffdate现象执行导入脚本报KeyError: date打印字段列表发现第一个键是\ufeffdate。原因Excel 另存的 CSV 是 UTF-8 with BOM 编码BOM 字节被 Python 的 csv 模块读成了字段名的一部分。这个现象在 Windows 上尤其常见直接打开 CSV 看不出任何问题。解决open()时用encodingutf-8-sig这个参数会告诉 Python 自动剥离 BOM 头。如果项目里已经用了open(path, encodingutf-8)把编码改掉就行。顺带一提从 Excel 导出的数据最好检查一下“城市”列有没有尾随空格否则unique_together可能因为武汉和武汉 被认为是两条不同数据而失效。5.4 折线图上某天突然出现一座“山峰”现象全国趋势图整体平滑但某一天的数据突然比前后高出一大截数值恰好是正常值的两倍左右。原因数据导入脚本执行了两次同一天同一个城市的数据被插了两遍聚合求和时被重复计算。如果数据量是真实的两倍说明整个表被重复导入如果只有个别城市异常往往是脚本中断后重新执行导致的重复插入。解决模型层加unique_together只是兜底真正的修法是把导入逻辑改成“有则更新无则创建”DailyReport.objects.update_or_create( daterow[date], provincerow[province], cityrow[city], defaults{ confirmed: int(row[confirmed]), suspected: int(row[suspected]), cured: int(row[cured]), dead: int(row[dead]), } )update_or_create的查找条件有三个字段日期、省份、城市这三个字段能唯一定位一行数据defaults里放的是要更新的数值字段。脚本重复执行时只会更新对应数据不会新增记录。这是数据同步类脚本的标准写法比“先 delete 再 create”安全得多。5.5 日期显示总是“少一天”或“多一天”现象折线图横轴最后一天显示的是昨天或者某个日期的数据跑到了前一天的位置上。原因很可能模型用了DateTimeField同时又开着USE_TZTrue。Django 存数据库时会按 UTC 时间转换查询出来再转回本地时区日期的“天”边界就错位了。如果后端返回的是带时区的 ISO 字符串前端解析时还会再偏移一次问题叠加后大部分图都对不上。解决疫情数据是“按天”的统计口径模型直接用DateField就能从根上避掉这个问题。接口返回时统一用strftime(%Y-%m-%d)生成纯字符串前端不做任何时间对象转换直接给 x 轴用。这条规则适用于所有日期型图表别让“时间数据”在前后端之间传来传去谁转换谁出问题。5.6 自己代码没问题部署到别的机器上启动报错现象项目在自己的电脑上跑得好好的拷到答辩机器上python manage.py runserver启动失败报的是 Python 解释器或依赖相关的错误。原因目标机器上的 Python 版本和项目开发环境不一致或者根本没有安装项目依赖。最常见的场景是目标机器装了 Python 3.6requirements.txt 里要求的 Django 4.2 只支持 Python 3.8 以上装都装不上。解决与其现场折腾不如直接准备一个可交互的迁移方案。到答辩机器上先python --version确认版本再依次装依赖。经验之谈是不要在答辩前最后一晚干这件事提前至少一天把项目拷到目标机器上完整跑一遍问题早暴露早解决。这类问题的坑不在技术难度而在预演不充分。6. 从“能跑”到“敢答辩”数据验证与演示彩蛋页面能打开、图表能显示这只是项目的一半。真到了答辩老师最常见的做法是随便指着一个数字问你“这个数怎么算出来的”。所以我把最后一章放在验证方法上这也是我自己做项目时最容易忽略、后来吃亏最多的地方。验证思路很简单准备一组已知的测试数据插入数据库后手工计算预期值再和页面显示的数字对比。具体操作分三步。第一步把数据库重置成干净状态删掉旧库、重新 migrate、重新导入原始 CSV保证数据来源可追溯。第二步抽查最近三天的数据用数据库查询语句单独算一遍总和比如执行SELECT date, SUM(confirmed) FROM daily_report GROUP BY date ORDER BY date DESC LIMIT 3;和页面图表上的值逐一对比。第三步验证接口返回的 JSON 和数据库聚合结果一致——直接访问/api/trend/把结果复制到文本编辑器里核对几个关键节点。这一步做到位“这段数据是不是你自己跑出来的”这种问题就能答得理直气壮。验证脚本也有一个简单的版本可以放在项目里# verify_data.py —— 抽查数据库聚合值与页面数字是否一致 import os import django os.environ.setdefault(DJANGO_SETTINGS_MODULE, config.settings) django.setup() from django.db.models import Sum from stats.models import DailyReport # 数据库聚合作为“标准答案” total DailyReport.objects.aggregate(sSum(confirmed))[s] print(f数据库累计确诊总数: {total}) # 按日期抽查找最近三天 latest_three ( DailyReport.objects.values(date) .annotate(day_totalSum(confirmed)) .order_by(-date)[:3] ) for row in latest_three: print(row[date], row[day_total])脚本打印出的数字和页面折线图最后一截做对比吻合就说明数据链路是完整的。这里不需要额外工具一个print就够了。进阶一步给聚合接口加缓存。数据量增长后每次请求都全表聚合会很浪费Django 自带的缓存装饰器一行就能解决from django.views.decorators.cache import cache_page cache_page(60 * 5) # 缓存 5 分钟 def national_trend(request): # 视图内部代码不变依然走 ORM 聚合 passcache_page的参数是缓存秒数60 * 5就是 5 分钟。加了这个之后5 分钟内的重复请求直接读缓存不再查询数据库。开发环境默认用的是本地内存缓存演示场景已经够用不需要额外配置 Redis。答辩演示的彩蛋我的个人习惯是准备三个“对比视角”全国趋势图、省份累计排行、最近一周环比变化。前面的折线图解决了全国视角排行可以做成横向柱状图环比变化可以用 ECharts 的双 Y 轴三张图共用同一个/api/trend/数据源只是前端setOption的配置不同。这样给老师展示的不只是“画了几张图”而是“同一份数据用不同维度展开”。最后说一句做这类项目的个人习惯拿到任何一个可视化源码包我第一步永远是删库、重新导入、重新跑一遍确认它在一台干净的机器上能活过来。这个过程逼着我把数据的来龙去脉过一遍心里有数答辩才能讲得稳。希望这条经验能帮到你少走点我当时走过的弯路。本文还有配套的精品资源点击获取