☰
Django疫情数据分析系统实战:从数据建模到可视化看板
2026/10/1 14:43:41 网站建设 项目流程

最近好几个准备毕业设计的读者来问我这类题目:用Django做一个疫情数据分析系统,还让我帮着看看源码。这题目确实经典,但真正动手的人很容易卡在“数据表建好了却不知道下一步干嘛”上。今天我就拿我经手过的一套“源码16103”疫情数据分析系统为例,从需求拆解、Django建模、数据清洗、可视化看板到WebSocket推送,把完整思路和踩过的坑都摊开聊一聊。这篇不是官方文档复读,更像是一个过来人带项目时的现场记录,适合正在做B/S架构毕设、或想从零搭一套数据统计分析Python项目的同学。

1. 项目整体设计与技术选型

1.1 疫情数据分析系统到底要解决什么问题

毕设题目里带“数据分析”四个字,不是只做个增删改查就能过关的。疫情数据分析系统的核心价值,是把多源、零散、不断变化的疫情数据,用统一的结构存起来,再按照日期、地区、病种状态这些维度做统计和对比,最终用折线图、柱状图、热力地图等形式把结论直观呈现出来。

我一般带项目时,第一件事不是打开代码编辑器,而是拉着学生把需求写清楚。这套系统至少要覆盖四类角色场景:普通访客能查看总览看板、搜索指定日期和区域的统计数据;管理员能登录后台维护基础数据、审核导入内容;系统本身要周期性更新数据,并把“累计值、新增值、治愈率、死亡率”这些指标计算出来;最后要支持数据导出,方便论文里贴报表和分析图。

把需求拆成文档后,你才知道哪些地方用Django自带能力,哪些地方要自己写。比如用户登录可以直接用django.contrib.auth,数据管理可以借助Django Admin快速生成后台,统计查询用ORM的aggregate和annotate,可视化交给前端ECharts。这样一来,核心工作量就落在“数据清洗”和“统计口径设计”上,这才是毕设真正值得写进论文章节里的部分。

1.2 为什么选Django而不是Flask或Node

很多新手会纠结技术栈,其实选型逻辑很直接:毕设要求你体现工程化的Web开发能力,Django自带“全家桶”属性,学习曲线虽然比Flask陡一些,但一旦跑通,省下的是大量重复造轮子的时间。

Django默认就有完整的MVT分层、ORM、Admin后台、表单校验、Session、分页、中间件机制。做数据类项目时,ORM尤为重要,它能把SELECT SUM(confirmed) ... GROUP BY date这类聚合查询写成Python代码,避免在项目里到处拼接原生SQL。Flask虽轻,但用户认证、后台管理都要自己找第三方库,毕设答辩时评委问“为什么不用Flask”,你说“因为Django自带Admin和ORM,能更高效率实现后台数据维护和分析查询”,这就是很合理的工程理由。

另外,Django的项目结构天然适合论文画架构图。一个项目下拆多个app,比如users管用户,covid管疫情数据,visual管图表接口,每个app职责单一,论文里画系统模块图、功能结构图都方便。相比之下,如果全堆在Flask的单个app.py里,代码可能很简短,但复现和答辩讲逻辑时就比较痛苦。

1.3 系统架构与功能模块划分

我习惯把系统分成三层:数据接入层、业务分析层、展示层。数据接入层负责CSV导入、爬虫采集或手工录入,通常会写一个data_importer.py,把原始文件清洗后写入MySQL或SQLite。业务分析层就是Django的views、models、services,负责所有统计口算和权限控制。展示层则是模板页面加Ajax接口,用Bootstrap搭框架,图表统一走ECharts。

功能模块可以按后台功能和前台功能两类划分。后台功能主要有:用户管理、地区管理、疫情数据管理、数据导入导出、系统日志。前台功能主要有:数据总览看板、按省市区联动查询、趋势折线图、地区排行柱状图、地图分布、最新疫情速报。这里说的“省市区”是通用区域层级概念,并不限定具体国家。

具体到源码目录,常见的结构是这样的:

manage.py requirements.txt config/ settings.py urls.py asgi.py apps/ users/ covid/ visual/ static/ templates/ data/ import_data.py

用startapp创建子应用时,我建议直接用python manage.py startapp covid,别把代码全放到一个app里。比如地图和图表接口如果混在covid/views.py里,后期加一个功能就要改一大片代码,拆成visual独立app后,前后端联调要清晰很多。

1.4 数据库模型设计:一张表装不下所有分析

疫情数据的核心表一定要设计好,这决定了后面的统计查询能不能流畅做出来。我见过很多毕设源码,所有字段堆在一张表里,地区名直接写成字符串,日期也用字符串存储,结果用ORM按时间聚合时各种踩坑。

建议至少拆两张表:一张Region地区表,记录地区编码和层级关系;一张CovidData疫情数据表,记录某地区某一天的累计确诊、疑似、治愈、死亡等数值。这样设计的好处很明显:地区名称修改时不需要改动数据表;按省、市不同层级汇总时,可以用parent外键递归关联;统计全国数据时,只要查parent is null的地区再聚合。

模型代码大致如下:

from django.db import models class Region(models.Model): name = models.CharField(max_length=100, verbose_name='地区名称') code = models.CharField(max_length=32, unique=True, verbose_name='地区编码') parent = models.ForeignKey( 'self', null=True, blank=True, on_delete=models.CASCADE, verbose_name='上级地区' ) level = models.SmallIntegerField(default=1, verbose_name='层级') class Meta: verbose_name = '地区' verbose_name_plural = verbose_name def __str__(self): return self.name class CovidData(models.Model): region = models.ForeignKey(Region, on_delete=models.CASCADE, verbose_name='地区') date = models.DateField(verbose_name='日期') confirmed = models.PositiveIntegerField(default=0, verbose_name='累计确诊') suspected = models.PositiveIntegerField(default=0, verbose_name='累计疑似') cured = models.PositiveIntegerField(default=0, verbose_name='累计治愈') dead = models.PositiveIntegerField(default=0, verbose_name='累计死亡') created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') class Meta: verbose_name = '疫情数据' verbose_name_plural = verbose_name unique_together = ('region', 'date') indexes = [ models.Index(fields=['region', 'date']), ] def __str__(self): return f'{self.region.name}-{self.date}'

unique_together这里很重要,能保证同一个地区同一天只存在一条记录,不然导入数据时很容易出现重复行。配合索引,按日期范围查某地区趋势时,速度也能接受。如果你的毕设希望用更轻量的方式,直接用SQLite也行,但要把迁移文件和初始数据提交到源码里,方便答辩时当场还原。

2. 数据从哪来,怎么清洗和入库

2.1 数据采集:优先用公开数据集,而不是硬啃爬虫

疫情数据系统的核心是数据,但毕设阶段我不建议把太多精力放在爬虫上。一个反爬策略就能让你卡好几天,而且爬下来的数据往往还要做大量清洗。更稳妥的做法是下载疫情公开数据集的CSV文件,然后用脚本导入数据库。如果找不到合适的CSV,也可以自己写一个模拟数据生成器,按日期、地区、人数范围生成足够多的样本,只要后续分析逻辑能跑通,效果是一样的。

当然,如果你想在论文里体现爬虫能力,可以针对一个权威公开数据页面写爬虫。用requests获取页面,BeautifulSoup解析表格,每次请求之间加随机延时,并将抓取结果转为结构化DataFrame。这里特别要注意:只爬公开、非敏感数据,不要碰个人信息,也不要高频请求。毕设重在展示技术,不在数据体量。

2.2 数据清洗的SOP:pandas是主力

原始CSV里的问题就那么几类:日期格式不统一、数值列里面有逗号或中英文混合、一些空值代表“无数据”而不是“0”、地区名称有前后空格。我一般用pandas跑一遍清洗流程:

import pandas as pd df = pd.read_csv('covid_data.csv', encoding='utf-8-sig') # 日期标准化 df['date'] = pd.to_datetime(df['date'], errors='coerce') df = df.dropna(subset=['date']) # 字符串数值转整数 for col in ['confirmed', 'suspected', 'cured', 'dead']: df[col] = ( df[col] .astype(str) .str.replace(',', '') .str.replace('--', '0') .str.replace('', '0') .fillna('0') .astype(int) ) # 地名去空格,避免重复 df['region'] = df['region'].str.strip() # 删除完全重复行 df = df.drop_duplicates(subset=['region', 'date'], keep='last') print(df.info())

这里有个细节:str.replace('', '0')虽然笨,但对半角全角空格都有兼容效果。清洗结束后,再通过Django的ORM逐行get_or_create写入数据库。逐行写入百万级会慢,但毕设数据量一般不大,完全够用。如果想更快,可以用bulk_create分批插入。

2.3 从表数据到分析指标:聚合查询不能靠Excel

表里存的通常是累计值,但看板展示时需要“每日新增”和“环比变化”。这个计算不能在模板里一条条做,而应在后端用Django ORM聚合。

最典型的是全国每日累计走势:过滤出所有省级地区,按日期分组,把每个日期的confirmed加起来。用ORM写就是:

from django.db.models import Sum from apps.covid.models import CovidData, Region national_trend = ( CovidData.objects .filter(region__parent__isnull=True) .values('date') .annotate(total=Sum('confirmed')) .order_by('date') )

region__parent__isnull=True表示取全国一级地区,因为它们的parent外键为空。如果想让表结构更简单,也可以不用递归外键,而是在Region表加一个level字段,然后按level=1过滤。推荐递归外键的原因是它足够灵活,论文里还能解释“树形结构建模”。

每日新增的算法,是把后一天的累计值减前一天累计值。实际操作时,我会把这部分计算逻辑放到一个statistics.py服务里,循环日期列表计算,并把结果缓存到内存或Redis,而不是每次都临时算。这样不仅前端响应快,答辩时也能解释“为什么要设计缓存层”。

3. 核心功能实现与实操要点

3.1 Django Admin:后台管理的“免费午餐”

如果只给两个字符的忠告,那就是“用Admin”。django.contrib.admin已经把数据后台搭好了,只需要注册模型、配置字段,就能快速管理地区表和疫情数据表。

from django.contrib import admin from apps.covid.models import Region, CovidData @admin.register(Region) class RegionAdmin(admin.ModelAdmin): list_display = ('name', 'code', 'level', 'parent') search_fields = ('name', 'code') @admin.register(CovidData) class CovidDataAdmin(admin.ModelAdmin): list_display = ('region', 'date', 'confirmed', 'suspected', 'cured', 'dead') list_filter = ('date', 'region__parent') search_fields = ('region__name',) date_hierarchy = 'date'

配置完以后,管理员登录后台就能直接按日期过滤数据、按地区搜索记录。对毕设来说,这是展示“用户权限管理”最省力的地方。

但有三个坑要提醒:第一,线上环境一定要关掉DEBUG=True,不然一旦报错会泄露配置;第二,Admin的密码不要用弱口令,毕业设计一般都会演示后台,弱口令很减分;第三,如果业务量小,不要在Admin里展示所有数据,分页和搜索都设置好,否则后台加载会卡。

3.2 数据看板:Ajax + JSON + ECharts

看板页面我建议前后端分离数据接口,也就是页面渲染由Django模板负责,图表数据通过fetch异步获取。这样做的好处是:数据刷新不需要重新加载整个页面;接口可以单独测试;后面如果要加移动端或大屏,直接复用接口即可。

后端写一个返回趋势数据的接口:

from django.http import JsonResponse from django.db.models import Sum from apps.covid.models import CovidData def trend_api(request): rows = ( CovidData.objects .filter(region__parent__isnull=True) .values('date') .annotate(total=Sum('confirmed')) .order_by('date') ) data = [{ 'date': item['date'].strftime('%Y-%m-%d'), 'total': item['total'] } for item in rows] return JsonResponse({'code': 0, 'data': data})

注意date必须转成字符串,否则JsonResponse会报TypeError: Object of type date is not JSON serializable。前端拿到数据后交给ECharts:

fetch('/api/trend/') .then(response => response.json()) .then(res => { const dates = res.data.map(item => item.date); const totals = res.data.map(item => item.total); const chart = echarts.init(document.getElementById('trendChart')); chart.setOption({ tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: dates }, yAxis: { type: 'value' }, series: [{ name: '累计确诊', type: 'line', data: totals }] }); });

首页建议同时展示三个图:全国趋势折线图、地区排行柱状图、区域分布地图。地图组件引用echarts对应地图数据时,注意文件体积。我第一次加载时把全国地图JS全部引进来,页面卡了快三秒,后来改成异步加载按需注册地图,体验才正常。

3.3 查询、分页与删除对象:QuerySet里的四个常见细节

数据量稍微上来后,查询性能就很重要了。Django的QuerySet是惰性的,真正执行SQL是在迭代、判断或序列化的时候。很多新人写列表页时每次循环都触发一次数据库查询,这就是经典的N+1问题。

引入select_related和prefetch_related能显著减少查询次数。列表页查询示例:

from django.core.paginator import Paginator from apps.covid.models import CovidData def covid_data_list(request): queryset = ( CovidData.objects .select_related('region') .order_by('-date') ) paginator = Paginator(queryset, 20) page_obj = paginator.get_page(request.GET.get('page')) return render(request, 'covid/list.html', {'page_obj': page_obj})

因为CovidData通过外键关联Region,select_related('region')会生成一条JOIN查询,模板中用item.region.name就不会额外发起查询。

删除对象时也要注意外键约束。如果Region下面关联了很多CovidData,直接删除某个地区,默认会被外键约束挡住。要级联删除可以设置on_delete=models.CASCADE,但在业务上我们一般只做软删除,给表加个is_deleted字段,避免误删后数据无法恢复。执行批量删除时,CovidData.objects.filter(date__lt='2024-01-01').delete()会返回删除数量,但如果有外键依赖,同样要小心级联范围。

3.4 WebSocket实时推送:后台一更新,前端看板就刷新

纯轮询接口也能实现数据刷新,但不够优雅。项目里如果预留了爬虫定时采集,那么后台写入新数据后,最好能主动通知前端页面更新。这里我用的是Django Channels实现的WebSocket推送。

首先要安装channels,然后在项目的asgi.py里配置协议:

import os from channels.routing import ProtocolTypeRouter, URLRouter from django.core.asgi import get_asgi_application from apps.visual.routing import websocket_urlpatterns os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') application = ProtocolTypeRouter({ 'http': get_asgi_application(), 'websocket': URLRouter(websocket_urlpatterns), })

消费者端,把连接加入分组covid:

from channels.generic.websocket import AsyncJsonWebsocketConsumer class CovidConsumer(AsyncJsonWebsocketConsumer): async def connect(self): await self.channel_layer.group_add('covid', self.channel_name) await self.accept() async def disconnect(self, close_code): await self.channel_layer.group_discard('covid', self.channel_name) async def covid_update(self, event): await self.send_json({'type': 'covid.update'})

后端数据更新后,通过信号或者服务函数调用推送:

from channels.layers import get_channel_layer from asgiref.sync import async_to_sync def notify_covid_update(): channel_layer = get_channel_layer() async_to_sync(channel_layer.group_send)( 'covid', {'type': 'covid.update'} )

前端用WebSocket对象监听消息,收到covid.update就重新请求接口刷新图表。毕设阶段如果不做实时推送,这项功能也是重要的加分项,推荐在论文功能模块里单独列一个小节。

4. 常见问题与排查技巧实录

4.1 迁移和建表问题

最烦的问题就是改了模型却忘了迁移。启动项目时报no such table,基本说明你只改了models.py,没执行python manage.py makemigrations和python manage.py migrate。如果在原来的SQLite里有测试数据,建议先备份,再重置数据库,否则字段冲突会越改越乱。

另一个容易踩的是默认值。给confirmed等字段加PositiveIntegerField(default=0)没问题,但如果历史数据里有NULL,又没设置空值时,统计接口会返回None,前端拿到空值后图表坐标轴直接归零。导入脚本里最好统一用fillna(0),保证数据库里不存在空数值。

4.2 图表不显示,先查接口再查前端

出现“后台数据有,前端图表空白”的情况,我一般按三步排查。第一步,直接在浏览器访问数据接口,确认返回的JSON结构和想要的一致,尤其注意date是字符串而不是null。第二步,打开浏览器控制台看fetch请求,如果接口报500,多半是后端字段名写错了,比如模型里叫confirmed,接口里写成confirm。第三步,看ECharts配置里的data和xAxis是否对应上。

还有一个常见问题:开发时Django跑在8000端口,前端页面在另一个端口打开,fetch('/api/trend/')会走不同域,触发跨域。毕设简单处理可以统一用Django模板渲染页面,让接口和页面同源;如果分开了,就安装django-cors-headers配置允许来源。

4.3 部署上线容易挂的原因

本地runserver很流畅,一部署就白屏,大概率是静态文件没配好。部署前务必检查ALLOWED_HOSTS是否包含你的域名或服务器IP,DEBUG必须设为False,然后执行python manage.py collectstatic,让CSS、JS、图片汇总到统一目录。用Uvicorn或Gunicorn启动时,也要记得给WebSocket单独配置ASGI服务器,不然实时推送功能直接失效。

时区问题也容易被忽略。Django默认USE_TZ=True时,时间存的是UTC,前端展示若不转换,日期会差8小时。分析类系统最好统一口径:要么USE_TZ=False,所有日期直接用本地时间;要么存入UTC,前端转换展示。我带的毕设里统一用了USE_TZ=False,简单易懂,答辩也不会被问晕。

4.4 答辩高频问题怎么答

评委经常问“你这个系统和Excel做分析有什么区别”。要抓重点回答:Django系统能把数据采集、存储、计算、展示放在一个Web闭环里,支持多用户权限管理,将来接实时数据源也非常方便。另一个高频问题是“数据准确性怎么保障”,可以从清洗脚本、后台人工审核、唯一约束和日期校验四个角度说明。

如果评委追问“数据量达到百万级怎么优化”,不要慌。回答方向是:给region_id + date加联合索引;列表页分页;聚合结果用缓存或预计算;高频查询走Redis;爬虫定时任务用Celery异步处理,避免阻塞Web服务。这些虽然不一定会全部实现,但至少证明你懂性能优化方向。

5. 实操心得与扩展建议

5.1 我踩过的坑和最终改进方案

这套系统我在几个不同版本的源码里反复改过。最开始的版本把清洗脚本放在视图函数里,每次请求都重新清洗,页面慢得一塌糊涂。后来拆成了独立脚本,数据只导入一次,页面查询才变得流畅。这个小改动让我明白,毕设项目里“分层”不是抽象概念,而是直接影响性能的实操问题。

另一个让我印象深刻的坑是地图组件。最初用高德地图API,结果开发时没有申请到正式Key,页面局部空白。后来换成了纯ECharts内置地图,完全离线,答辩现场即使没网也能正常展示。所以如果有条件,尽量选离线可用的可视化方案,毕业设计演示环境不一定有外网。

数据校验这块我也补了一版。在Admin的CovidDataAdmin里重写了save_model,当累计值比前一天小时,自动拦截并给出提示。虽然是简单校验,但答辩时作为“系统健壮性”的实例,比单纯展示增删改查有说服力得多。

5.2 从“能跑”到“有亮点”的三个扩展方向

毕设拿高分不一定要做得很复杂,但要有清晰亮点。我个人推荐的三个扩展方向:

第一,趋势预测模块。不引入复杂的深度学习,用移动平均、指数平滑或线性回归做一个简单的“未来七日趋势预估”,能在需求分析和算法设计两章里都写出内容。第二,区域对比分析。支持选择两个或多个地区,把它们同期的治愈率、增长率放在同一个图表里对比,这比单地区折线图更接近“分析”。第三,导出报告功能。后端生成PDF或Excel分析报告,便于论文写实验结论,也是企业里很常见的需求。

这些扩展都不是重新做一套系统,而是在现有看板页面上增加接口和组件。每条扩展花一到两天就能做完,性价比很高。

5.3 拿到“源码16103”后应该怎么读

很多同学下载这类毕设源码后,第一反应就是直接跑,跑不通就开始慌。正确的阅读路径应该是:先看README或部署文档,确认Python版本、依赖包、数据库配置;第二步用pip install -r requirements.txt安装依赖,初始化数据库;第三步找一个登录账号进入后台,先观察页面功能;最后再对着源码目录,从urls.py路由开始,追踪一个完整请求的流向。

读代码时不要急着改功能,先把“数据流”串起来:CSV或后台录入的数据进库以后,如何被视图函数读取,如何被模板或接口使用,如何最终呈现在图表上。只要这条链路在自己脑子里跑通,答辩时被问任何一个模块都能接得上话。

最后提个建议:源码只是参考,不要直接原封不动交上去。至少把项目名称、数据库字段、页面文案改成自己的,再增加一两个独立小功能,这样既能避免查重风险,也能真正理解项目逻辑。疫情数据分析系统这个题目,技术点本身不难,难的是把数据思维和Web工程能力结合起来,而这一点,恰恰是答辩和作品集最看重的东西。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询