
疫情数据分析系统这个毕设题目在计算机毕业设计里属于典型的热门选题但真正能把它做出数据分析味道的工程其实不多大多数人最后交出来的只是一个套着ECharts外壳的增删改查页面。我当年做这个题目时同样踩过不少坑从数据获取、清洗入库、指标计算到Django后端接口设计和前端图表联动整个链路走通之后才发现这份源码真正值钱的地方不在于页面样式而在于数据处理闭环的完整性。这篇内容就围绕Django疫情数据分析系统的工程实现展开给准备复现、改进或拿源码做二次开发的同学一份可落地的参考。1. 先搞清楚毕设真正要做什么疫情数据分析系统的需求边界1.1 名义是数据分析本质是全栈数据闭环很多同学接到这个题目时的第一反应是去网上找个疫情数据接口拉到前端用图表画出来就完事了。但如果你真去查过几所学校的毕业设计评分标准就会发现这类题目的核心考核点根本不是图表画得多炫而是你有没有完成一条完整的数据处理链路原始数据怎么获取、怎么清洗、怎么存储、怎么按业务维度聚合、怎么通过定义良好的接口对外暴露、最后才是怎么可视化。所以我在动手写代码之前把系统的定位改成了一句大白话一个支持多维度筛选、能回答某段时间内数据如何变化、哪个区域情况最突出这类问题的分析工具。这句话直接决定了后面的功能范围和技术选型。与此同时作为一份毕业设计源码工程它还需要满足几个隐性要求代码结构清晰、能演示、能答辩、能跑通并且有合理的扩展空间。纯粹堆功能没有意义老师打开工程一看models散落、逻辑全写死在视图里第一印象就砸了。1.2 必须涵盖的四大核心功能组结合题目的数据分析定位我把系统功能拆成四个组每一组都对应一个独立的价值点数据管理组负责历史数据导入、校验、去重以及行政区划数据的维护。这是整个系统的基础没有这一组后面所有分析都是空谈。统计指标组按日期、地区维度计算累计值、新增值、治愈率、增长率等核心指标并且支持时间范围筛选。这是分析二字的直接体现。可视化展示组通过折线图、柱状图、饼图、全国地图等形式呈现数据变化趋势与分布特征。虽然这类页面在校园里撞脸率很高但它仍然是数据表达效率最高的方式。系统配置组登录认证、用户管理、操作日志等。这部分内容虽然无聊但在毕设评分里往往能补足系统完整性的分数。这样的分组也顺带解决了另一个问题功能列表足够答辩用。数据管理体现工程能力指标计算体现业务思考可视化体现前端水平系统配置体现综合能力——四个方向全部覆盖答辩时就不怕老师问你这个系统哪里体现了工作量。1.3 需求到模块的落地映射可答辩的功能清单为了不让自己在开发中迷失方向我把上述功能组进一步细化成了可以直接编码的功能点清单全国及各省份的疫情历史数据导入与查询按开始日期、结束日期筛选任意时间区间累计确诊、累计治愈、累计死亡、新增确诊、治愈率、死亡率六大核心指标时间趋势折线图可选择单省或多省对比地区分布地图全国视角下各省数据着色排行分析指定日期下Top10区域柱状图数据导出按条件筛选后导出CSV文件这个功能很加分的登录与权限校验非登录用户只能看部分页面这份清单看着简单但每一项背后都有对应的数据表和查询逻辑而不是前端写死一堆假数据。我强烈建议所有准备做类似题目的同学开工前先花两个小时把这样的清单写出来——它会成为你后面排优先级、估算工作量的唯一依据。2. Django项目怎么搭才配得上数据分析系统这个名字2.1 为什么是Django而不是Flask选型这一步看似不起眼实际上决定了后续开发的舒适度。网上关于Django和Flask的对比文章很多但从毕设工程的角度看Django有几个不可替代的优势第一自带Admin后台。项目还没开始写业务页面前Admin就能让你快速维护数据表内容尤其是地区表、用户表这类基础数据。中期调试接口时Admin也是一个很好的数据观察窗口。第二自带ORM和迁移机制。疫情数据的特点是时间序列 地区维度查询时频繁涉及按日期聚合、按地区分组、范围过滤。Django ORM的aggregate和annotate方法写这类查询非常顺手而且迁移文件migrations让数据库结构变更可控对着源码复现的人也不需要手动去建表。第三内置认证体系。登录、会话、权限校验都是现成的拿来就能用。这个对毕设系统来说太省事了——你不需要自己写密码哈希和session管理安全性和工作量两头都顾到了。第四工程结构规范。Django通过项目应用的方式强制拆分业务模块这种结构天然适合展示给答辩老师看。Flask不是不行但自由度太高容易写成一坨单文件代码。2.2 工程结构与App划分方案我最终采用的工程结构是这样的project_root/ ├── manage.py ├── config/ # 项目配置文件settings、urls ├── apps/ │ ├── users_app/ # 用户登录与权限 │ ├── data_app/ # 疫情数据管理、导入、清洗 │ ├── stats_app/ # 指标计算模块 │ └── charts_app/ # 可视化接口与页面渲染 ├── static/ # 前端静态资源ECharts、CSS、JS ├── templates/ # 页面模板 └── scripts/ ├── import_data.py # 数据导入脚本 └── clean_data.py # 数据预清洗脚本为什么要单独拆一个stats_app而不是把所有指标计算都写在data_app里因为数据分析的核心逻辑是计算而不是存储。数据模型只管存取指标计算是一个独立层次它依赖但又不能耦合数据模型。拆开之后后续如果要把系统改成实时对接爬虫数据只需要替换stats_app里的计算来源不需要动模型。2.3 数据模型设计时序数据的三张核心表疫情数据的实体关系其实非常清晰我用三张表就覆盖了所有业务需求# apps/data_app/models.py from django.db import models class Region(models.Model): 行政区划表支持省市两级 name models.CharField(名称, max_length128, uniqueTrue) code models.CharField(行政区划代码, max_length32, uniqueTrue) parent models.ForeignKey(self, nullTrue, blankTrue, on_deletemodels.CASCADE, verbose_name上级区域) class Meta: ordering [code] def __str__(self): return self.name class DailyStat(models.Model): 每日疫情统计快照表 region models.ForeignKey(Region, on_deletemodels.CASCADE, related_namestats, verbose_name区域) stat_date models.DateField(统计日期) confirmed models.IntegerField(累计确诊, default0) cured models.IntegerField(累计治愈, default0) death models.IntegerField(累计死亡, default0) suspect models.IntegerField(累计疑似, default0) class Meta: unique_together (region, stat_date) ordering [stat_date] indexes [ models.Index(fields[region, stat_date]), ] def __str__(self): return f{self.region.name} {self.stat_date} 确诊{self.confirmed}设计这张DailyStat表时有几个关键决策值得展开说存累计值而不是新增值。很多初学者以为表里应该直接存每日新增实际上原始数据源提供的往往就是累计值新增值是通过两日累计值相减算出来的。存累计值还有一个好处任意时间段的新增都能通过首尾相减得到数据不会丢失。唯一约束unique_together。同一天、同一个地区的记录只允许存在一条这从数据库层面挡住了重复导入的脏数据。联合索引indexes。查询永远逃不掉按地区 按日期这两个条件联合索引能让聚合查询快一个量级。虽然毕设数据量不大全国每天就几十条但这个习惯到真实项目里非常值钱。2.4 ORM查询与聚合操作的实战写法有了模型下一步就是让数据会说话。举两个我在开发中反复用到的查询场景。场景一计算某个省份在指定时间区间内的累计增量from django.db.models import Q from .models import DailyStat def calc_increment(region_code, start_date, end_date): qs DailyStat.objects.filter( region__coderegion_code, stat_date__range[start_date, end_date] ).order_by(stat_date) if not qs.exists(): return 0, 0, 0 first qs.first() last qs.last() return ( last.confirmed - first.confirmed, last.cured - first.cured, last.death - first.death )这里用first()和last()而不是qs[0]和qs[-1]是因为Django的QuerySet有缓存机制按日期排好序后取首尾数据量不大时效率完全没问题。场景二获取某一天所有省份的累计值排行from django.db.models import Sum def daily_rank(stat_date, top_n10): rows ( DailyStat.objects .filter(stat_datestat_date, region__parent__isnullFalse) .values(region__name) .annotate(total_confirmedSum(confirmed)) .order_by(-total_confirmed)[:top_n] ) return list(rows)注意这里先filter(region__parent__isnullFalse)表示我们只看省级数据排除掉全国汇总那一条。过滤条件要尽量靠近查询起点这是ORM调优的第一原则否则先全表计算再过滤性能会差很多。来看这张表总结一下三个核心指标的计算口径指标名称计算方式注意点累计确诊直接取confirmed字段数据源本身为累计口径新增确诊当日累计 - 前日累计时间区间内需要取首尾两天差值治愈率cured / confirmed * 100分母为0时需要特殊处理避免除零在实际代码里除零问题很容易被忽略。我封装了一个小函数def safe_rate(numerator, denominator): if not denominator: return 0.0 return round(numerator / denominator * 100, 2)写完之后所有指标计算都走这个入口不管数据源给我什么脏数据视图层拿到的永远是合法数值。3. 从数据进来到图表渲染的完整处理链路3.1 数据源与导入策略用管理命令把CSV塞进数据库数据分析系统再好没有数据就是空中楼阁。毕设阶段不建议搞实时爬虫一方面难度大、容易被反爬拦住另一方面爬虫代码在答辩时反而会给老师留下不稳定的印象。更稳妥的方案是从公开数据仓库下载整理好的历史CSV数据通过Django管理命令management command批量导入数据库。我在工程里放了一个专门的数据导入模块核心逻辑大致长这样python manage.py import_covid_data --filedata/covid_history.csv --region全国对应实现的精简版# apps/data_app/management/commands/import_covid_data.py import csv from datetime import datetime from django.core.management.base import BaseCommand from apps.data_app.models import Region, DailyStat class Command(BaseCommand): help 导入疫情历史CSV数据 def add_arguments(self, parser): parser.add_argument(--file, requiredTrue, typestr) parser.add_argument(--region, requiredTrue, typestr) def handle(self, *args, **options): file_path options[file] region_name options[region] region, _ Region.objects.get_or_create( nameregion_name, defaults{code: fCUSTOM-{region_name}} ) with open(file_path, r, encodingutf-8-sig) as f: reader csv.DictReader(f) records [] for row in reader: stat_date datetime.strptime(row[date], %Y-%m-%d).date() records.append(DailyStat( regionregion, stat_datestat_date, confirmedint(row[confirmed]), curedint(row[cured]), deathint(row[death]), )) # 用 bulk_create 批量插入避免逐条 insert DailyStat.objects.bulk_create(records, ignore_conflictsTrue) self.stdout.write(self.style.SUCCESS( f成功导入 {region_name} 数据共 {len(records)} 条 ))这段代码有几个细节我特别想强调encodingutf-8-sigCSV文件如果是从Excel另存的通常带着BOM头不这么处理第一个字段名会解析异常。bulk_create(..., ignore_conflictsTrue)配合模型的unique_together约束重复导入同一份文件不会报错而是静默跳过彻底解决手滑导两次的问题。Region.objects.get_or_create导入前自动建立区域记录防止外键缺失。3.2 清洗和标准化的几个关键细节真实的疫情历史数据远没有教科书写得那么干净我在清洗阶段处理过下面几类问题每一条都是血泪经验日期格式不统一有的源用2023/1/1有的用2023-01-01还有的用20230101。统一在导入时用datetime.strptime解析并标准化成date对象入库后所有日期都是ISO格式。数值字段为空某些地区某天可能没有上报数据CSV里直接是空字符串。我在转换时加了防御性解析空值填0def safe_int(value): try: return int(float(value)) except (TypeError, ValueError): return 0重复记录同一日期、同一地区的记录出现两次。前面说的unique_together约束在这一步直接兜底。地区名称不一致同一个省份在不同资料里可能叫XX省和XX。我在Region表里单独维护了code字段用行政区划代码作为唯一标识name字段只是展示用统计查询全部走code。清洗之后一定要做数据质量验证。我在导入完成后会跑一段验证代码检查是否存在同一日期多区域数据缺失、是否出现累计值比前一天还少这类逻辑错误。别看这些检查土答辩时老师随口问一句你怎么保证数据准确性你能拿出这套校验逻辑印象分直接不一样。3.3 指标计算的聚合逻辑累计值、新增值与增长率前面模型设计里我提到存累计值这节讲清楚它怎么被榨出价值。计算时间序列的每日新增最直观的办法是循环日期的两两相减。Django里可以这样写def daily_increments(region_code, start_date, end_date): qs ( DailyStat.objects .filter(region__coderegion_code, stat_date__range[start_date, end_date]) .order_by(stat_date) ) result [] prev_confirmed None for stat in qs: if prev_confirmed is not None: inc stat.confirmed - prev_confirmed result.append({ date: stat.stat_date, new_confirmed: inc if inc 0 else 0, confirmed: stat.confirmed, }) prev_confirmed stat.confirmed return result这里把负增量强制归零是合理的业务决策由于数据修订某些地区会出现累计值回调但对外展示时负的新增会让人困惑不如置零并保持累计值原样。我在代码注释里写明了这个决策而不是默默处理掉——在毕设工程里你做了什么决策比你没做什么重要得多。增长率指标我用的是环比算法计算上一周和本周的累计值变化def weekly_growth(region_code, stat_date): end DailyStat.objects.filter( region__coderegion_code, stat_datestat_date ).first() start DailyStat.objects.filter( region__coderegion_code, stat_datestat_date - timedelta(days7) ).first() if not end or not start or start.confirmed 0: return 0.0 return (end.confirmed - start.confirmed) / start.confirmed * 100这些计算逻辑统一收口在stats_app里视图和模板层不直接碰模型字段后续要调整指标口径只改一个地方即可。4. 可视化层实现ECharts和Django接口的配合套路4.1 接口返回格式的约定数据分析系统的前端不需要更复杂的框架Django模板 ECharts就足够。但前后端的数据契约必须定义好否则前端写起来会非常痛苦。我给所有图表接口定了一个统一返回结构{ code: 200, message: success, data: { dates: [2023-01-01, 2023-01-02], series: [ {name: 新增确诊, data: [10, 23, 45]}, {name: 累计确诊, data: [100, 123, 168]} ] } }对应Django视图的写法# apps/charts_app/views.py from django.http import JsonResponse from apps.stats_app.services import daily_increments def trend_chart_api(request): region_code request.GET.get(region_code, 全国) start request.GET.get(start, 2023-01-01) end request.GET.get(end, 2023-12-31) increments daily_increments(region_code, start, end) dates [item[date].isoformat() for item in increments] new_confirmed [item[new_confirmed] for item in increments] confirmed [item[confirmed] for item in increments] return JsonResponse({ code: 200, message: success, data: { dates: dates, series: [ {name: 新增确诊, data: new_confirmed}, {name: 累计确诊, data: confirmed}, ] } })前端就非常无脑拿到data.series直接塞给ECharts// static/js/charts.js async function loadTrendChart() { const params new URLSearchParams({ region_code: 全国, start: 2023-01-01, end: 2023-12-31 }); const resp await fetch(/api/trend?${params}); const result await resp.json(); if (result.code ! 200) return; const chart echarts.init(document.getElementById(trendChart)); chart.setOption({ tooltip: { trigger: axis }, legend: { data: result.data.series.map(s s.name) }, xAxis: { type: category, data: result.data.dates }, yAxis: { type: value }, series: result.data.series.map(s ({ name: s.name, type: line, smooth: true, data: s.data })) }); }4.2 折线图、柱状图和全国地图的落地写法折线图主要用于时间趋势分析它解决的问题是变化。在我实际开发中多省份对比是答辩时最出效果的场景——老师只要动一下下拉框选择两个省份两条曲线的走势一眼就能看出来缓冲和反弹的差别。所以我在页面里加了一个multiple select筛选器支持同时勾选多个省份。柱状图用于排行分析展示指定日期下确诊数Top10区域的对比。这类图逻辑最简单但数值标签、颜色渐变这些细节做好后视觉冲击力很强。地图是全场视觉担当。实现方式是在HTML中引入ECharts地图JS然后通过Ajax获取JSON格式的区域数据// static/js/map.js async function loadMapData(statDate) { const resp await fetch(/api/region-map?date${statDate}); const result await resp.json(); if (result.code ! 200) return; const chart echarts.init(document.getElementById(mapChart)); chart.setOption({ visualMap: { min: 0, max: result.data.max, left: left, text: [高, 低], calculable: true }, series: [{ type: map, map: china, roam: true, label: { show: false }, data: result.data.items }] }); }后端对应接口def region_map_api(request): stat_date request.GET.get(date, 2023-12-31) rows ( DailyStat.objects .filter(stat_datestat_date, region__parent__isnullFalse) .values(region__name, region__code) .annotate(valueF(confirmed)) .order_by(-value) ) # 确保全国地图能匹配到地区名 items [{name: row[region__name], value: row[value]} for row in rows] return JsonResponse({ code: 200, data: { items: items, max: max([i[value] for i in items], default0) } })特别注意ECharts的地图数据要求name必须和注册地图JS里的地区名完全一致。CSV导入时的XX省如果和地图JS里的XX省有差异地图上就会空白。我的处理方案是在Region表里额外维护一个map_name字段专门存放ECharts地图可识别的名称导入时和地图JS名称做一次对齐映射这一步能省下大量联调时间。4.3 前后端联调中最容易翻车的几个点跨域问题如果你用了前后端分离Django只写API前端单独跑Vite dev server就一定会遇到CORS。我调试时直接用Django的开发服务器同时服务API和静态页面绕开了跨域。如果确实要分离装一个django-cors-headers把白名单配好即可。日期参数格式前端传日期时容易传成2023-1-1后端strptime只认%Y-%m-%d就会报错。解决办法是后端参数解析时做一次归一化def parse_date_param(raw): if not raw: return None try: return datetime.strptime(raw, %Y-%m-%d).date() except ValueError: return datetime.strptime(raw.replace(/, -), %Y-%m-%d).date()时区导致日期偏移Django默认开启USE_TZTrue如果数据库存的是date类型还好一旦用了DateTimeField前端拿到的日期可能被转换成UTC导致差8小时。我的项目里明确规定stat_date是纯粹的DateField不涉及时间彻底绕开这个坑。如果你在别的功能里必须要存时间点记得统一在settings里配TIME_ZONE Asia/Shanghai并保持USE_TZ False毕设项目完全可以这么做。数据量过大导致图表卡顿如果用户选了全国全年来渲染日粒度折线图几千个点会让浏览器吃力。我加了一层降采样策略超过200个日期点就改为周粒度聚合逻辑放在stats_app的服务层前端无感知。这个优化虽然只有几行代码但在演示时很能体现工程思维。5. 运行演示、答辩加分与源码工程避坑清单5.1 本地跑起来的环境准备拿到源码工程第一件事是先把它跑起来而不是急着看代码。我建议按以下顺序操作# 1. 创建虚拟环境 python -m venv venv source venv/bin/activate # Windows 为 venv\Scripts\activate # 2. 安装依赖 pip install -r requirements.txt # 3. 初始化数据库 python manage.py makemigrations python manage.py migrate # 4. 创建管理员账号 python manage.py createsuperuser # 5. 导入测试数据 python manage.py import_covid_data --filedata/covid_history.csv --region全国 # 6. 启动开发服务器 python manage.py runserver依赖文件requirements.txt我通常锁定主版本Django3.2,5.0 django-cors-headers3.14 requests2.28 pandas1.5避坑提醒Python版本建议3.9或3.10Django 4.x以下对Python新版支持良好但如果你用了Django 5.x部分第三方面板库可能还没跟上反而给自己添麻烦。毕设不是追新稳就是最大的快。5.2 演示时的故事线设计很多人忽视演示流程的设计结果现场手忙脚乱。我按照数据导入 - 指标验证 - 可视化分析 - 导出报告的顺序串了一条演示故事线基本等于把整个系统的核心链路走一遍先打开Admin后台展示数据库中已有的数据记录说明数据来源和导入机制。进入数据查询页面选择某省某时间段展示明细列表和统计卡片总确诊、总治愈、死亡率等。切换到趋势分析页面同时选择两三个省份对比新增确诊曲线走势。打开地图页面点击某一天观察全国各省颜色深浅分布然后点省份下钻查看详情。设置筛选条件导出CSV文件展示导出结果。这条故事线的好处是每个页面只用一到两分钟但把数据从哪里来、怎么计算、怎么展示、怎么输出讲得明明白白。老师通常不会打断你看完整流程因为这个系统已经自证了整个链路是通的。5.3 给源码工程加分的几处细节有些细节代码量不大但对源码工程的整体评价提升明显README.md写得足够清晰。在README里写清楚项目简介、运行环境、安装步骤、目录结构和功能列表。源码工程评分时老师第一眼看的往往不是代码而是这份文档。我见过太多人忽略这个非常可惜。应用层加操作日志。在登录、导出、导入这类敏感操作上记录日志用Django的信号signals或者简单装饰器实现。虽然毕设系统没有安全审计要求但日志模块的存在证明了你有工程化思维。给统计指标加上环比提示。在趋势页面的数值卡片上显示较前一天 12或较上周 -5%。这种细节能让页面信息密度瞬间提高也说明你是真的在分析数据而不是单纯展示。数据导出功能放到显眼位置。我用的是Django内置的csv模块生成StreamingHttpResponse几行代码就能实现下载但很多同学压根不会想到做这个功能。在答辩演示里导出CSV被问到频率非常高。5.4 我踩过的坑和最终吸取的教训整个开发过程中对我影响最大的一次踩坑是数据导入时没有考虑地区层级导致的统计翻倍。我最初只设计了一张表把全国、各省、各市的数据全部平铺进去。做全国地图时我只按region__parent__isnullTrue过滤掉省级侥幸控制住了。但做全国汇总指标时我把全国行和各省的行加了一遍数据直接翻倍。后来实在忍不了把地区表分成了父子层级所有统计统一按region__parent__isnullFalse只看省级全国汇总单独通过全国这个根的parent__isnullTrue来取才算彻底理顺。另外一个反复出现的坑在数据文件编码上。有次从网上抓的数据文件用GBK编码导出我导入时按UTF-8读取中文地区名全部乱码。后来在导入命令里加了参数自动探测编码import chardet def detect_encoding(file_path): with open(file_path, rb) as f: raw f.read(1024 * 1024) return chardet.detect(raw).get(encoding) or utf-8这个改进很小但从此再也没被编码坑过第二次。如果你准备直接拿这套源码工程改造我强烈建议你先完整跑通一遍再动手。很多同学拿到代码第一件事就是急着改页面样式结果后面发现动一处坏一处。正确顺序是跑通 - 读懂- 记录 - 再改。改的时候优先动stats_app里的计算逻辑和charts_app里的页面模板模型的字段轻易不要增删因为涉及迁移和数据兼容性一旦改错又要回去重新导数据。最后分享一个个人看法毕业设计源码的价值在于它能证明你完整走通了一个项目的全生命周期。这个疫情数据分析系统从需求拆解、模型设计、数据导入、指标计算、接口开发到可视化呈现每一步都能在答辩中讲出为什么这样做的决策依据——这本身就比单纯会调接口画图高一个层次。把链路每段的堵点都梳理清楚这套源码在你手里就不只是交付物而是一份能举一反三的能力凭证。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。