ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Django新冠疫情数据分析系统毕业设计完整实战指南

Django新冠疫情数据分析系统毕业设计完整实战指南

又到了一年一度的毕业设计选题季,我身边陆续有人来问同一类问题:“基于XX框架做一个数据分析系统”这类题目到底能不能做?好不好通过?今天就用“django新冠疫情数据分析系统”这个题目作为例子,把这个课题从选题逻辑、技术选型、数据建模到核心功能实现的完整思路都拆一遍。

先说结论:这类题目属于典型的“数据驱动型Web应用”,技术栈成熟、需求边界清晰、演示效果好,非常适合作为计算机专业的毕业设计或者课程设计。它本质上不是一个科研命题,而是一个工程化系统,核心考察点在于:你能不能把数据采集、数据清洗、后台管理、可视化分析这几件事串成一个完整的闭环。

这篇文章主要面向正在选题或已经选了这个题目的同学,也适合想用Django快速练手数据可视化项目的开发者。我会从项目为什么要这么做讲起,一直讲到实际开发中的坑和排查技巧,尽量给你一份可以照着做的参考方案。

1. 项目定位与功能模块拆解

1.1 这类课题的底层逻辑

在做系统之前,先要把题目“翻译”成技术语言。新冠疫情数据分析系统,拆开来看其实就三件事:数据从哪来、数据怎么存、数据怎么展示。

数据从哪来,对应爬虫采集或者文件导入;数据怎么存,对应关系型数据库的表设计和ORM模型;数据怎么展示,对应图表可视化和前端交互。再往细了说,还包含用户登录、后台管理、权限控制这些通用功能,但那些都是Django框架自带的能力,真正需要自己动脑子的就是数据分析这条主线。

这个题目受欢迎的原因也在于此:它不像纯算法类课题那样需要很强的数学功底,也不像纯商城类系统那样功能清单无穷无尽。它有一个明确的业务主题,用到的技术又全是Django体系里的常见操作,难度曲线平滑,做出来的效果却很容易做出“数据大屏感”,答辩演示非常加分。

1.2 功能模块怎么划分

我一般建议把系统划分为六个功能模块,按用户角色来梳理:

  • 用户模块:注册、登录、退出;普通用户和管理员区分权限。
  • 数据管理模块:支持从文件导入疫情数据,也可以在后台手动维护数据记录,这是整个系统的数据来源入口。
  • 数据分析模块:按时间趋势、地区分布、维度对比等条件查询和统计核心指标。
  • 可视化展示模块:折线图、柱状图、饼图、地图等,把数据库里的数字变成直观的图表。
  • 报表导出模块:把筛选后的统计结果导出为Excel或CSV文件。
  • 后台管理模块:用Django Admin管理用户、数据表、轮播图和系统日志。

在实际开发中,我发现很多同学容易忽略数据管理模块,一上来就想着做花哨的图表。这是本末倒置的。没有稳定的数据入口,图表做得再好看也是空中楼阁。我给的建议是:数据管理模块用Excel导入作为主路径,手写增删改查作为补充,这样既不需要过多依赖外部接口,又能保证系统在任何环境下都能演示。

1.3 角色权限要提前定好

权限设计不需要做得很复杂,但一定要有。我的建议是设置两类角色:

  • 普通用户:可以浏览数据看板、查看图表详情、导出统计报表。
  • 管理员:在普通用户权限基础上,额外拥有数据导入、数据编辑、用户管理等能力。

这么设计的理由是:毕业设计答辩时,老师一定会问“你的系统安全性体现在哪里”,权限管理就是最直观的回答。Django自带的认证系统加上@login_required装饰器,十几行代码就能完成,性价比非常高。

2. 技术选型解析:为什么是Django

2.1 Django在这个项目里的核心优势

数据分析系统用Java、用Flask、用Node都能做,但用Django确实是同类题目里开发效率最高的方案之一。原因有三个。

第一是ORM。Django的ORM可以直接把数据库表映射成Python对象,写一行EpidemicData.objects.filter(date__gte=start_date)就能完成带时间条件的查询,不需要手写SQL,对不熟悉数据库操作的同学来说特别友好。

第二是自带Admin后台。Django Admin是框架自带的,注册模型后自动生成增删改查界面。这意味着系统的数据管理模块在一开始就完成了一半,省下来的时间全部可以投入到分析页面的打磨上。

第三是认证系统。登录、注册、会话管理、密码加密都是现成的,继承一个AbstractUser就能扩展自定义字段。对比从零手写用户系统,这个优势不能忽视。

有人会问,那用Flask不是更轻量吗?对纯API项目来说Flask确实更轻,但一旦涉及Admin后台、表单处理、权限中间件这些模块,Flask就需要自己去装一堆扩展插件,组装成本并不低。毕设项目要求的是“功能完整、能跑通、能演示”,Django的“全家桶”思路恰恰是这个场景下的最优解。

2.2 数据库与可视化方案怎么配

数据库层面,我推荐正式环境用MySQL,开发阶段先用SQLite。SQLite是Django默认支持的,零配置文件,初始化开发时特别方便;MySQL则更贴近真实生产环境,答辩时提到“迁移到MySQL”也会加分。两者的切换只需要改settings.py里的DATABASES配置,前期完全可以用SQLite先把功能跑通,后期再换。

可视化层面,首选ECharts。数据可视化前端库其实有很多,Chart.js、Highcharts、AntV都可以,我推荐ECharts的理由很实际:中文文档全、示例丰富、地图支持好、答辩效果直观。尤其是地图功能,疫情数据按地区展示时,用ECharts地图要比普通表格有说服力得多。

另外提示一点:ECharts的渲染方式我建议采用Ajax异步加载JSON数据,而不是在页面刷新时一次性嵌入所有图表数据。这样页面首屏加载更快,筛选条件切换时也不需要整页刷新,交互体验上有明显提升。

2.3 项目目录结构和开发环境建议

开发环境用的东西很简单,Python 3.9以上、Django 4.x LTS版本、MySQL或者SQLite、Vue或者原生HTML模板都行。考虑到这个系统本身以服务端渲染为主,直接使用Django模板系统加上少量ECharts前端代码就够了,不需要额外引入前端框架增加复杂度。

目录结构我建议按App拆分:

project/ ├── manage.py ├── requirements.txt ├── config/ # 项目配置 │ ├── settings.py │ ├── urls.py ├── apps/ │ ├── users/ # 用户认证 │ ├── data_manage/ # 数据导入与维护 │ ├── analysis/ # 数据分析与图表 │ └── dashboard/ # 首页看板 └── static/

按App拆分的好处是职责清晰,一个App只做一件事,后期扩展也方便。很多同学图省事把所有逻辑写在一个App里,结果代码量上来以后根本没法维护,答辩时被问到某个文件是干什么的也说不清楚。

3. 数据从哪里来:采集、清洗与建模

3.1 数据源的三种获取方式

关于数据源,实际项目里通常有三种选择:爬虫采集、公共API接口、本地文件导入。对这个毕业设计题目,我的建议是混合使用。

爬虫采集可以写一个小脚本,用requests从公开数据渠道抓取历史数据,解析后存入数据库。这里要注意,疫情数据的格式在不同来源里差异很大,有的字段叫“确诊”,有的叫“感染”,有的字段是累计数量,有的是每日新增,解析逻辑必须做好兼容。

公共API接口是最省事的方案,但存在两个隐患:一是接口不稳定,临近答辩突然挂掉就麻烦了;二是一些公开接口的数据口径无法保证,直接影响系统分析结果的准确性。所以不推荐把项目核心功能完全依赖在外部接口上。

更稳妥的方案是本地文件导入。在数据管理页面提供Excel上传入口,规定好列名和日期格式,用户在后台上传Excel文件,系统读取后写入数据库。这样做的好处是数据可控,答辩演示时不用担心网络问题,数据质量也可以自己保证。

3.2 数据库表结构设计

数据模型是整个系统的地基,字段设计得好不好,直接影响后面所有查询代码的复杂度。参照业务需求,核心数据表设计如下:

from django.db import models class EpidemicData(models.Model): region = models.CharField(max_length=100, verbose_name="地区") date = models.DateField(verbose_name="日期") confirmed = models.IntegerField(default=0, verbose_name="累计确诊") cured = models.IntegerField(default=0, verbose_name="累计治愈") dead = models.IntegerField(default=0, verbose_name="累计死亡") class Meta: verbose_name = "疫情数据" verbose_name_plural = verbose_name ordering = ["-date"] indexes = [ models.Index(fields=["region", "date"]), ] def __str__(self): return f"{self.region} - {self.date}"

这个模型包含了一个典型数据分析系统所需要的全部要素:地区维度、时间维度、核心统计指标。region字段存地区名称,可以是国家、省份、城市,粒度根据数据来源决定。所有数值字段都设置默认值0,防止导入不完整数据时出现空值。

为什么要把地区和时间做成两个独立字段而不是直接拼成一个字符串?因为后期的所有统计操作都是围绕这两个维度展开的。按时间趋势分析要用date做聚合,按地区分布对比要用region做聚合,字段不拆开的话,统计分析代码会变得非常痛苦。

3.3 数据清洗与初始化脚本

在把数据导入真实数据库之前,有一道必须做的工序:数据清洗。这里分享几个我实际踩过的坑。

第一是日期格式不统一。有的数据源日期是2023-01-01,有的是2023/1/1,还有的是2023.01.01。清洗时统一用dateutil.parser.parse解析后再格式化为标准日期。

第二是缺失值处理。某个地区某天没有上报数据,记录中可能出现空值。清洗规则是:数值字段为空时补0,地区字段为空时丢弃该条记录。第三个常见的坑是地区名称不一致,同一省份有的数据源叫“省份A”,有的叫“A省”,需要做一次地区名称映射。我建议维护一个地区别名字典,清洗时统一替换。

清洗脚本可以放在一个独立的scripts/data_clean.py文件里,通过python manage.py shell < scripts/data_clean.py方式执行。这样可以随时重跑,不用污染项目主代码。

4. 核心业务逻辑:查询、统计与可视化

4.1 按时间维度的趋势分析

数据分析系统的核心能力就是查询统计,而最直观的分析维度是时间趋势。比如我想看全国范围内确诊数量的变化趋势,对应的Django ORM写法是:

from django.db.models import Sum from django.db.models.functions import TruncDate from apps.data_manage.models import EpidemicData def trend_data(request): city = request.GET.get("city", "") queryset = EpidemicData.objects.all() if city: queryset = queryset.filter(region=city) trend = ( queryset.annotate(day=TruncDate("date")) .values("day") .annotate(total=Sum("confirmed")) .order_by("day") ) data = [ {"date": item["day"].strftime("%Y-%m-%d"), "value": item["total"]} for item in trend ] return JsonResponse(data)

这里用TruncDate把datetime字段截断为日期,再按day分组求和。注意values("day")和annotate(total=Sum("confirmed"))的顺序不能写反——values决定了分组字段,annotate中的聚合结果会附加到每一条分组数据上。

前端用ECharts折线图渲染这段数据就非常顺手:

fetch('/analysis/trend_data/') .then(response => response.json()) .then(data => { const chart = echarts.init(document.getElementById('trendChart')); chart.setOption({ xAxis: { type: 'category', data: data.map(d => d.date) }, yAxis: { type: 'value' }, series: [{ type: 'line', data: data.map(d => d.value), smooth: true }] }); });

4.2 地区分布与横向对比

除了时间趋势,地区分布是另一个核心分析维度。比如我想看各省份累计确诊的对比,可以用按region分组聚合:

from django.db.models import Sum def region_rank(request): rank_data = ( EpidemicData.objects.values("region") .annotate(total=Sum("confirmed")) .order_by("-total")[:20] ) data = [ {"region": item["region"], "value": item["total"]} for item in rank_data ] return JsonResponse(data)

这类数据最适合用柱状图展示,数据从多到少排序,一眼就能看出哪些地区排名靠前。如果数据源包含省市级的地理坐标,还可以用ECharts地图,把统计结果填充到地图对应区域的颜色深浅上,视觉效果远好于柱状图。

做地图展示时有一个容易踩的坑:ECharts的map类型图表需要提前注册地图GeoJSON数据,而不是直接用type: 'map'就行。比如国内省市地图,需要先加载对应的GeoJSON并注册:

fetch('/static/geo/regions.json') .then(res => res.json()) .then(geoJson => { echarts.registerMap('regions', geoJson); // 然后才能使用 type: 'map', map: 'regions' });

4.3 视图、模板与图表的数据流串联

数据分析系统的另一条关键链路,是视图函数、模板和前端JS之间如何配合。我的建议是:页面主体用Django模板渲染,数据全部通过Ajax请求JSON接口获取。这样页面结构和服务端逻辑清晰分离,调试时互不干扰。

具体流程是:首先在urls.py里配置路由,一个返回HTML页面,一个返回JSON数据:

from django.urls import path from apps.analysis import views urlpatterns = [ path("analysis/", views.analysis_index, name="analysis_index"), path("analysis/trend_data/", views.trend_data, name="trend_data"), path("analysis/region_rank/", views.region_rank, name="region_rank"), ]

然后在模板中通过JS发起请求,拿到数据后渲染图表。模板本身只需要放几个<div>容器作为图表挂载点。

我这里要特别强调一个细节:JsonResponse默认只支持字典类型,如果直接传入列表需要加safe=False参数。很多同学第一次写接口时都会在这里报错In order to allow non-dict objects to be serialized set the safe parameter to False,其实这就是最典型的低级错误。

5. 管理后台与权限控制

5.1 定制Django Admin后台

Django Admin是整个系统里“白送”的功能,但如果完全不配置就直接用,界面粗糙不说,管理效率也很低。我建议花半小时做一轮简单定制,成本低,效果明显。

在admin.py中注册模型并配置列表页字段:

from django.contrib import admin from apps.data_manage.models import EpidemicData @admin.register(EpidemicData) class EpidemicDataAdmin(admin.ModelAdmin): list_display = ("region", "date", "confirmed", "cured", "dead") list_filter = ("date", "region") search_fields = ("region",) date_hierarchy = "date"

这样配置之后,后台列表页可以直接按地区筛选、按日期筛选,还能按日期的层级维度钻取。对于动辄几千条疫情数据的数据表来说,这个功能比手写一个爬虫管理页面实用得多。

5.2 用户认证与权限区分

自定义用户模型是很多毕业设计的必选项。默认的auth.User只有用户名、密码、邮箱等字段,通常不能满足项目对用户手机号、头像等信息的存储需求。扩展方式很简单:

from django.contrib.auth.models import AbstractUser class User(AbstractUser): phone = models.CharField(max_length=11, blank=True, verbose_name="手机号") avatar = models.ImageField(upload_to="avatars/", blank=True, verbose_name="头像")

然后记得在settings.py里设置:

AUTH_USER_MODEL = "users.User"

这一步一定在第一次migrate之前完成,否则后期切换用户模型会很麻烦,需要重新初始化数据库。

控制页面访问权限时,用Django自带装饰器就够了:

from django.contrib.auth.decorators import login_required, user_passes_test @login_required def dashboard(request): ... def is_admin(user): return user.is_superuser or user.is_staff @user_passes_test(is_admin) def data_import(request): ...

这么做之后,未登录用户无法访问数据看板,非管理员无法进入数据导入页面。前后端两侧的权限防线都补齐了,答辩时被问安全问题也能对答如流。

5.3 文件导入的完整链路

关于Excel导入数据,我给一个可以直接落地的思路。前端页面提供文件上传表单,后端接收文件后用openpyxl解析,逐行校验后写入数据库。核心逻辑类似这样:

import openpyxl from django.shortcuts import render, redirect def upload_excel(request): if request.method == "POST": excel_file = request.FILES.get("file") wb = openpyxl.load_workbook(excel_file) sheet = wb.active for row in sheet.iter_rows(min_row=2, values_only=True): region, date_str, confirmed, cured, dead = row EpidemicData.objects.update_or_create( region=region, date=datetime.strptime(date_str, "%Y-%m-%d").date(), defaults={ "confirmed": confirmed or 0, "cured": cured or 0, "dead": dead or 0, }, ) return redirect("data_list") return render(request, "data_manage/upload.html")

这里用update_or_create而不是create,是为了避免同一地区同一天的数据重复导入时产生脏数据。这个细节很多人一开始想不到,等数据出现重复时才回来补。

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

6.1 高频问题速查表

整个开发过程中,我发现以下几个问题出现频率最高,整理成速查表方便对照:

问题现象常见原因解决办法
页面能开但图表空白前端拿到JSON的键名和后端返回的键名不一致打开浏览器控制台打印返回的data,逐字段核对
数据库中文乱码MySQL连接没指定utf8mb4字符集settings中DATABASES配置增加OPTIONS中charset设置或建库时指定utf8mb4
图表数据为0日期筛选参数类型是字符串,没转换日期视图里先datetime.strptime再过滤
登录后跳转404LOGIN_URL配置了不存在的路由检查settings.py和urls.py中的登录地址
导入Excel报错表头有不可见字符或空行openpyxl读取前先strip表头,跳过全空行
静态文件404没配置STATICFILES_DIRS或者没执行collectstatic开发环境配置STATICFILES_DIRS指向static目录

6.2 时区问题导致的日期偏移

再分享一个隐蔽的坑。如果数据表里保存日期用的是DateTimeField,而settings.py里设置了USE_TZ = True,那么写入时间和读取时间会发生时区偏移,导致按日期聚合时分错组。

解决思路有两种:一种是数据表日期字段直接用DateField而不是DateTimeField,日期本身不涉及时区转换;另一种是保持USE_TZ = False,让Django放弃UTC自动转换。从数据统计分析的角度看,DateField更简洁,推荐优先使用。

6.3 大数据量下的查询性能优化

当数据量涨到十万条级别时,普通的ORM聚合查询会开始变慢,页面切换筛选条件时能明显感到卡顿。我当时做的优化有三个。

第一是给常用查询字段加联合索引,这是最简单也最有效的优化。就是数据模型Meta类里indexes = [models.Index(fields=["region", "date"])]那段配置。

第二是筛选结果加缓存。时间趋势和地区排名这类数据在短期内不会变化,可以用Django的cache框架缓存几分钟,减少重复计算。

第三是统计口径尽量用SQL层的聚合,不要在Python里循环累加。有些同学喜欢先用all()把数据全部查出来再遍历统计,十万条记录会慢到怀疑人生。用values().annotate()写出来的分组查询,交给数据库执行,效率完全不一样。

写在最后的一些经验

最后聊一点我的个人体会。做这类毕业设计系统,最容易犯的错就是把摊子铺得太大。我今天见过不止一个同学,系统规划了十几个模块,结果到中期发现核心功能都还没做完,最后草草收尾。如果你也在做类似的系统,我给你一个建议:先把“数据导入→数据存储→时间趋势图→地区分布图→后台管理”这条主线跑到通,剩下的模块再一个一个往上加,加一个稳一个,而不是一开始就想着做大而全的平台。

另外,答辩演示前一定要准备一份规模适中的干净数据。数据量太少,图表看起来没有说服力;数据量太大,现场查询又容易卡顿。我当时准备的是近一年时间跨度、覆盖几十个地区的数据,演示时既流畅又直观,效果比堆几万条杂乱数据好得多。

这个系统后续还可以往很多方向扩展,比如接入实时数据刷新、加入趋势预测算法、部署到云服务器提供公网访问。但那些都是后话,先把基础闭环跑通,你的毕业设计就已经成功了一大半。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进