简介:一份基于Django框架的城市PM2.5空气质量数据可视化分析源码,面向Python学习者和高校课程设计场景,完整涵盖数据采集、存储、后端处理与前端展示流程。压缩包共64个文件,主要包含17个Python代码文件、24个CSV数据文件、HTML页面模板及SQL数据库脚本等,整体大小12.38MB,目录结构清晰,易于直接运行和二次开发。目前已有328人学习下载。项目以上海、北京、广州、成都、沈阳五城市六年PM2.5汇总数据为基础,实现时间序列逐年趋势、不同季度/月/时段变化,以及与温度、露点、相对湿度、风向、大气压等要素的关系可视化;同时附带Django项目配置、登录注册模板和MySQL数据库文件,可帮助初学者理解数据分析与可视化项目的完整搭建方式。该源码系个人高分大作业作品,代码完整可直接部署,适合用于期末设计、课程报告或实战练手。
1. 城市PM2.5空气质量数据可视化分析:这个课程设计方案为什么值得自己重写一遍
把「Python基于Django城市PM2.5空气质量数据可视化分析」和「95分以上」放在一起,基本就是高校课程设计或毕业设计里最常见的一类选题:后端用Django把数据管起来,前端用图表把数据亮出来,再配上一点“分析”的味道。这套系统真正解决的诉求很简单——让一份零散的PM2.5数据集变成能答辩、能演示、能讲清楚技术链路的完整项目。它适合正在做Django项目实战的新手,也适合想快速搭一个数据可视化Demo、又不想被前端框架拖住的人。我见过不少学生拿到这类源码包后改不动、答不上来,问题往往不在代码本身,而在没搞懂数据是怎么从CSV一路流到图表的。这篇文章就把这条链路完整拆开,照着做,你也能交出一份能站在讲台上讲明白的方案。
2. Python+Django的MVT骨架:为什么课程设计首选Django而不是Flask
2.1 Django自带的那套东西,恰好覆盖了课程设计的全部得分点
城市PM2.5空气质量数据可视化分析这类项目,评审老师真正会看的就三件事:数据能不能存进去、页面能不能跑起来、图表能不能跟着数据走。Django的MVT架构(Model-View-Template)几乎是冲着这三点设计的:Model层用ORM管数据库,View层写业务逻辑,Template层渲染页面。你不需要像Flask那样自己拼ORM、自己接模板引擎、自己做后台管理,Django默认全部配好。
这里有一个很实际的选型理由:Django的Admin后台是白送的。数据导进去之后,Admin里能直接看到每一行记录、能按城市筛、能按日期排序,这等于让你在答辩前多了一个“数据管理”的展示页,而且一行代码不用写。Flask做不到这一点,你要么装Flask-Admin,要么自己写一个管理页面,工作量直接翻倍。对一门课程设计来说,时间成本是很真实的成本。
版本选择上,课程设计我一般建议用Django 3.2 LTS,而不是最新的4.x或5.x。原因很简单:3.2是长期支持版,网上的教程、博客、还有你同学已经踩过的坑,绝大多数都集中在这个版本。4.x以后删掉了一些旧写法,比如url()函数、django.conf.urls里的include用法,新手照着老教程抄很容易当场翻车。如果老师没硬性要求新版,就选3.2,稳。当然,如果你已经装了新版也没关系,第5章会讲版本差异导致的典型报错怎么处理。
2.2 图表选型:为什么我不用pyecharts,而是直接写ECharts
数据可视化是这类项目的门面,选错图表库是最致命的。市面上常见的方案有四类,我用一个对比表说清楚:
| 方案 | 适合场景 | 动态交互 | 新手友好度 | 答辩风险 |
|---|---|---|---|---|
| ECharts(原生JavaScript) | Web页面图表 | 强,setOption增量更新 | 中,需要写一点JS | 低,数据传递逻辑自己可控 |
| pyecharts | Python生成图表页面 | 弱,改配置要重新生成HTML | 高,全程Python | 中,老师追问“数据怎么到前端”容易卡壳 |
| Matplotlib | 本地静态图 | 无 | 高 | 高,本科阶段少见用它做Web项目 |
| Highcharts | 商业项目 | 强 | 中 | 低,但商用有授权问题 |
网上大量教程推荐pyecharts,因为它“全程写Python,不用碰前端”。但我自己做这类项目时不会选它——pyecharts本质是把ECharts的配置封装成Python对象,再渲染成一段HTML字符串。问题是:一旦答辩现场老师让你“把折线图换成柱状图”,或者“点击城市后图表联动”,pyecharts要走“改Python代码→重新生成页面→刷新浏览器”这条路,调试效率极低;而且pyecharts 1.x和2.x的API差异很大,很多老代码在新版本里直接跑不起来。
原生ECharts看着要多写一点JavaScript,但恰恰是这一点“多一点”,让你能讲清楚整个数据链路:视图函数把数据转成JSON→塞进模板→前端拿到JSON→chart.setOption()画图。每一步都在你手里,答辩时老师问什么你都能接住。城市PM2.5空气质量数据可视化分析的核心是“分析”,不是“背一个封装好的调用”,所以我会把宝押在ECharts上。
2.3 一个Django可视化项目的最小目录结构
不管外面那层zip壳叫什么名字,解压之后能跑起来的Django项目,骨架一定是下面这个样子。我建议你亲手敲一遍,而不是直接抄别人的项目结构:
air_quality/ ├── manage.py ├── config/ │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── core/ │ ├── __init__.py │ ├── models.py │ ├── views.py │ ├── urls.py │ ├── admin.py │ └── migrations/ ├── templates/ │ └── dashboard.html └── data/ └── pm25.csvconfig是项目配置目录,core是业务应用目录,templates放页面模板,data放原始数据集。拆分逻辑是:配置和业务分开,代码和数据分开。很多新手喜欢把一切塞进core里,短期没问题,但数据导入脚本、临时CSV文件、页面模板混在一起,后期改起来很痛苦。这个结构不是我发明的,是Django项目的常规布局,你在任何一本Django实战入门教程里都能看到类似影子。先把骨架搭对,后面每一步都顺。
3. 把PM2.5数据装进Django:数据表设计、CSV导入脚本与三条验证命令
3.1 PM2.5数据集的常规字段,以及为什么AQI和等级也要入库
要做城市PM2.5空气质量数据可视化分析,第一步是搞清楚数据长什么样。一份能支撑可视化页面的城市空气质量数据集,最少要有这几个字段:城市名称、监测日期、PM2.5浓度值、AQI指数、空气质量等级。城市和日期是维度,PM2.5是核心指标,AQI和等级用来做辅助分析,比如“近30天优的天数”“污染日集中在哪几个城市”。
我见过有人只存PM2.5数值,其他全丢。短期看页面也能画图,但答辩时老师问“AQI怎么算的”“优、良、轻度污染怎么划分”,你答不上来,因为数据里根本没有这些信息。宁可多存几个字段也不用后面补数据,这是做数据类项目的一条铁律。数据来源方面,常见做法是找公开的城市历史空气质量数据集(CSV格式),或者自己写爬虫抓公开页面;课程设计阶段我建议用公开数据集先跑通全链路,爬虫有时间再补,别一上来就卡在采集上。
3.2 建虚拟环境、装依赖、创建项目和App
动手之前先把环境隔离好。很多Python+Django项目跑不起来,不是代码的问题,是全局环境里Django版本和依赖互相打架。以下命令在项目根目录执行:
python -m venv venv source venv/bin/activate pip install django pandas django-admin startproject config . python manage.py startapp core第一行创建虚拟环境,第二行激活它(Windows下是venv\Scripts\activate)。pip install django pandas里pandas是为了后面写CSV导入脚本用的,Django本身不需要它。django-admin startproject config .末尾这个点很关键,表示在当前目录生成config配置目录,而不是再套一层文件夹,这样manage.py就待在项目根目录。最后一行startapp core创建业务应用,所有模型、视图都写在这个app里。
如果这里报django-admin: command not found,大概率是虚拟环境没激活,或者pip安装路径不在PATH里。检查办法是pip show django看装没装上,装上了就用python -m django代替django-admin,效果一样。跑完这几条命令,你已经有一个最小可运行的Django项目了,python manage.py runserver启动后浏览器打开127.0.0.1:8000能看到默认火箭页。
3.3 模型字段这样设计,导入和查询都省心
打开core/models.py,定义一张城市空气质量数据表。课程设计的数据量一般就是几万条到几十万条,SQLite完全扛得住,不需要上MySQL。模型我通常写成这样:
from django.db import models class CityAirQuality(models.Model): city = models.CharField(max_length=64, db_index=True, verbose_name='城市') monitor_date = models.DateField(db_index=True, verbose_name='监测日期') pm25 = models.FloatField(verbose_name='PM2.5浓度') aqi = models.IntegerField(verbose_name='AQI指数') quality = models.CharField(max_length=16, blank=True, default='', verbose_name='空气质量等级') class Meta: ordering = ['monitor_date'] unique_together = ('city', 'monitor_date') verbose_name = '城市空气质量数据' def __str__(self): return f'{self.city}-{self.monitor_date}'这里有几个参数值得注意。db_index=True给城市和日期加了数据库索引,按城市筛选、按时间范围排序会明显变快,课程设计数据量虽然小,但养成建索引的习惯没有坏处。unique_together = ('city', 'monitor_date')是去重约束,保证同一个城市同一天的记录只有一条,这比在导入脚本里手动if判断可靠得多。ordering = ['monitor_date']让所有查询默认按日期升序,后面写视图时不用每次都想起来加order_by。
FloatField用来存PM2.5浓度,因为监测值经常出现78.5这样的带小数数据;AQI是整数,用IntegerField。quality字段设置blank=True, default='',是因为有些数据源可能没给等级,硬性null=False会导致导入失败。模型定义完执行迁移:
python manage.py makemigrations core python manage.py migratemakemigrations生成迁移文件,migrate把表结构真正建到数据库里。执行完这两条命令后,Django会自动生成SQLite数据库文件db.sqlite3,不需要你手动建库。
3.4 CSV导入脚本:用pandas批量写入,而不是逐条insert
数据文件格式各家不一样,但城市空气质量CSV通常长这样:第一行是列名,后面每行一条记录,列包括城市、日期、PM2.5、AQI、等级。导入脚本我一般放在项目根目录下import_data.py,和Django项目同层,脚本内容如下:
import os import django import pandas as pd os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') django.setup() from core.models import CityAirQuality df = pd.read_csv('data/pm25.csv', encoding='utf-8-sig') df.columns = [c.strip().lower() for c in df.columns] objs = [] batch_size = 1000 for idx, row in df.iterrows(): objs.append(CityAirQuality( city=str(row['city']).strip(), monitor_date=pd.to_datetime(row['date']).date(), pm25=float(row['pm25']), aqi=int(row['aqi']), quality=str(row['quality']).strip(), )) if len(objs) >= batch_size: CityAirQuality.objects.bulk_create(objs, ignore_conflicts=True) objs.clear() if objs: CityAirQuality.objects.bulk_create(objs, ignore_conflicts=True) print(f'导入完成,共 {CityAirQuality.objects.count()} 条记录')脚本前三行是Django独立运行脚本的标准写法:设置环境变量、调用django.setup(),否则模型无法访问数据库。encoding='utf-8-sig'是专门针对Windows下编辑的CSV文件,带BOM头,用utf-8直接读会把\ufeff混进第一列列名。pd.to_datetime()统一日期格式,float()和int()做类型转换,避免字符串入库。
这段代码的核心是bulk_create批量写入,一次性提交1000条,比在循环里逐条save()快一个数量级。ignore_conflicts=True配合模型里的unique_together约束,重复导入时不会报错,重复记录会被自动跳过。最后一行打印总记录数,这就是第一条验证命令——看到它,说明数据真正落库了。
3.5 三条验证命令:确认数据真的导对了
数据导入完,别急着写页面,先用三条命令确认数据质量。打开Django shell:
python manage.py shellfrom core.models import CityAirQuality CityAirQuality.objects.count() CityAirQuality.objects.filter(city='北京').count() CityAirQuality.objects.values('quality').annotate(cnt=models.Count('id'))第一条命令看总记录数,第二条看指定城市有没有数据,第三条按空气质量等级分组统计数量。如果第三条跑出来有None空值,说明CSV里有些行没给等级,回到导入脚本把空值处理掉。这三条命令能在30秒内告诉你数据链路通不通,比直接写到页面里再排错快得多。我每次做完导入都会跑一遍这三条,确认无误才往下走,省下的排错时间远多于敲这几行命令的时间。
4. 从QuerySet到ECharts图表:视图函数、JSON结构与交互式折线图的完整链路
4.1 先定数据接口的JSON结构,再写页面
新手写可视化页面最容易犯的错,是把数据加工逻辑全塞在模板里,前端想要什么就临时凑什么。正确的做法是先定接口:后端返回什么结构的JSON,前端就按这个结构消费。城市PM2.5空气质量数据可视化分析的主页面需要两块数据:一是图表数据,二是城市下拉列表。
我一般把图表数据定义成三个平行数组:日期列表、PM2.5浓度列表、AQI列表。ECharts的折线图x轴要的就是一个数组,series也对应一个数组,三数组结构天然契合,前端拿到后不需要二次加工。
from django.http import JsonResponse from django.shortcuts import render import json from core.models import CityAirQuality def dashboard(request): city = request.GET.get('city', '北京') rows = CityAirQuality.objects.filter(city=city).order_by('monitor_date')[-30:] data = { 'dates': [r.monitor_date.strftime('%Y-%m-%d') for r in rows], 'pm25': [r.pm25 for r in rows], 'aqi': [r.aqi for r in rows], } cities = CityAirQuality.objects.values_list('city', flat=True).distinct() return render(request, 'dashboard.html', { 'chart_data': json.dumps(data, ensure_ascii=False), 'current_city': city, 'cities': cities, }) def api_pm25(request): city = request.GET.get('city', '') if not city: return JsonResponse({'error': '缺少city参数'}, status=400) rows = CityAirQuality.objects.filter(city=city).order_by('monitor_date')[-30:] data = { 'dates': [r.monitor_date.strftime('%Y-%m-%d') for r in rows], 'pm25': [r.pm25 for r in rows], 'aqi': [r.aqi for r in rows], } return JsonResponse(data, json_dumps_params={'ensure_ascii': False})这段代码里,values_list('city', flat=True).distinct()取出去重后的城市列表,给下拉框用。[-30:]这种切片写在order_by()之后,取的是按日期排序后的最近30条记录;如果order_by写反了,取的就不是最近30天而是最早30天。strftime('%Y-%m-%d')把日期对象格式化成字符串,因为datetime.date类型不能被json.dumps直接序列化。
第二段api_pm25是为第6章的局部刷新准备的接口,这里提前定义好。注意JsonResponse(data, json_dumps_params={'ensure_ascii': False}),不加这个参数,中文城市名会变成\u5317\u4eac这种转义序列,前端拿到后显示成乱码。
4.2 URL路由:path()函数写法和include的用法
视图函数写好后,把URL映射配好。项目根URL文件config/urls.py负责挂载app路由,app内core/urls.py负责具体路径:
# config/urls.py from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('core.urls')), ]# core/urls.py from django.urls import path from core import views urlpatterns = [ path('', views.dashboard, name='dashboard'), path('api/pm25/', views.api_pm25, name='api_pm25'), ]path()函数里空字符串''表示站点根路径,'api/pm25/'是JSON接口路径。这里不推荐url()正则写法,这是Django 4.x删掉的老语法,课程设计阶段全部用path()就够了。include('core.urls')把带core/前缀的请求转发给app内路由表,这样即使以后加更多业务模块,根URL文件也不会膨胀。
4.3 模板页面:ECharts折线图从0到1的完整代码
模板文件templates/dashboard.html是整套系统最终呈现的地方。引入ECharts、定义容器、初始化图表、塞数据,四步缺一不可:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>{{ current_city }} PM2.5 近30天趋势</title> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> <style> #pm25Chart { width: 100%; height: 400px; } </style> </head> <body> <select id="citySelect"> {% for city in cities %} <option value="{{ city }}" {% if city == current_city %}selected{% endif %}>{{ city }}</option> {% endfor %} </select> <div id="pm25Chart"></div> <script> const chart = echarts.init(document.getElementById('pm25Chart')); const raw = {{ chart_data|safe }}; chart.setOption({ tooltip: { trigger: 'axis' }, title: { text: '{{ current_city }} PM2.5 浓度趋势' }, xAxis: { type: 'category', data: raw.dates }, yAxis: { type: 'value', name: 'PM2.5 (μg/m³)' }, series: [{ name: 'PM2.5', type: 'line', data: raw.pm25, smooth: true, areaStyle: { opacity: 0.2 } }] }); </script> </body> </html>这里最容易被忽略的是容器高度。ECharts的容器div如果没设置高度,默认高度为0,图表画出来就是一片空白。所以模板里#pm25Chart必须显式写height: 400px。{{ chart_data|safe }}中的safe过滤器告诉Django这段JSON不要转义,否则引号会被变成",JavaScript直接语法报错。这个用法有个前提:chart_data是视图里自己json.dumps出来的受控数据,不是用户输入,所以安全可控。
echarts.init(document.getElementById('pm25Chart'))一行完成图表实例化。tooltip配trigger: 'axis'后,鼠标悬停能看到每一天的具体数值,这是答辩演示时的核心交互点。smooth: true让折线更圆滑,areaStyle加了一层半透明填充,视觉上比裸折线更有“数据可视化”的感觉——这两个参数是观感上的加分项,成本几乎为零。
4.4 把统计结论写进页面:数据可视化不等于数据分析
这一章最后做一件很多课程设计都漏掉的事:在图表旁边加一段统计描述。老师看到折线图的直观反应是“好看”,但真正让他点头的是你能说出来“为什么好看”。
# 在dashboard视图函数中补充统计逻辑 vals = [r.pm25 for r in rows] peak_val = max(vals) if vals else 0 peak_date = rows[vals.index(peak_val)].monitor_date.strftime('%Y-%m-%d') if vals else '无数据'把peak_val和peak_date一起传给模板,页面底部显示“近30天PM2.5峰值出现在{peak_date},浓度为{peak_val}μg/m³”。这句话就是“分析”的最小单元:数据可视化是手段,结论才是目的。没有这句,你的页面是“图表展示”;有了这句,它才是“可视化分析”。整篇文章的标题里“分析”两个字,靠的就是这一小段代码坐实。
5. Django可视化项目避坑手册:中文乱码、空白图、新版Django报错的排查路径
5.1 ECharts画出来是空白:先查容器高度,不要怀疑代码
现象:页面能打开,图表区域一片空白,控制台没有报错,chart.setOption也执行了。
原因:ECharts容器div没有显式高度,或者高度是0。ECharts初始化时读取容器宽高,高度为0时图表画不出来,但不会抛异常,因为初始化本身是成功的。
解决:给容器加CSS高度,如#pm25Chart { height: 400px; }。如果容器是响应式布局,用height: 60vh也可以。这是一个非常像“玄学”的问题,因为页面看起来一切正常,实际上卡在最基础的地方。我做过一个项目,三个页面用了同一个模板,只有第三个页面白屏,排查了半天发现是那个页面的CSS里把容器写成了height: auto。
5.2 CSV中文城市名变成乱码:encoding参数要选对
现象:导入完成后,页面上城市名显示为“鍖椾含”,或者导入时直接报UnicodeDecodeError。
原因:Windows下用Excel编辑的CSV默认是GBK编码,而Windows下Excel保存CSV时会带上BOM头,utf-8编码读取GBK字节流必然解码失败;即使读进来了,字节流被错误解码后就是乱码。
解决:读取CSV时用encoding='utf-8-sig',这个编码会自动吃掉BOM头,同时正常解析UTF-8内容。如果数据源确实是GBK,就改用encoding='gbk'。更稳妥的做法是一开始就把CSV统一转成UTF-8:用VS Code打开后右下角编码选择“通过编码保存”,选UTF-8保存一次,之后所有脚本都用utf-8-sig读。数据文件编码问题在数据可视化项目里排在前三名,处理不好会直接污染数据库。
5.3 Django新版删了url()函数:老教程一抄就报错
现象:按网上老教程写url(r'^index/$', views.index),启动服务或访问页面时抛TypeError: url() got an unexpected keyword argument 'regex',或者直接提示模块没有url属性。
原因:Django 2.0起推荐path(),4.0以后彻底移除了url()函数。网上大量教程和博客还是多年前的写法,抄下来必然报错。
解决:全部改用path()路由。path('index/', views.index)就能实现同样的效果。如果你确实需要正则匹配,用re_path()代替,它是url()的正则版后继者。另外要留意版本相关的settings.py配置:Django 3.2的DEFAULT_AUTO_FIELD需要设置,很多老教程没写这个,迁移时会收到一条警告,不影响运行但有提示。这类问题不是你的代码错了,是教程版本和运行环境不匹配,装上Django 3.2 LTS后大部分老教程都能直接用。
5.4 json.dumps中文变成\uXXXX:页面显示一串反斜杠
现象:页面上折线图的横轴日期显示正常,但下拉框里或者图例中的城市名显示成\u5317\u4eac,甚至直接把整个setOption参数变成了无效的JavaScript。
原因:json.dumps默认把非ASCII字符转成\uXXXX转义序列,模板变量通过safe输出后,JavaScript拿到的是字面量反斜杠而不是汉字。如果转义序列被插进带引号的字符串里,就会破坏JS语法。
解决:json.dumps(data, ensure_ascii=False)。在API接口里如果用JsonResponse,则传参json_dumps_params={'ensure_ascii': False}。两个地方一个都不能漏:模板渲染用的chart_data走的是json.dumps,AJAX接口走的是JsonResponse。这条排查路线的现象特别容易和编码问题混淆,实际是序列化参数的问题。
5.5 导入数据重复翻倍:unique_together是后悔药
现象:导入脚本跑了两遍,数据库里城市和日期的组合出现了两行相同记录,图表里某一天有两个数值点,折线图看上去像“重影”。这会让平均值计算和峰值统计全部失真。
原因:导入脚本没有去重逻辑,第二次执行时又批量插入了全部数据。课程设计阶段重复导入很常见,尤其是调试导入脚本时反复执行。
解决:模型上建unique_together = ('city', 'monitor_date'),导入时用bulk_create(objs, ignore_conflicts=True)。unique_together在数据库层面建了联合唯一索引,重复数据插入时数据库会拒绝;ignore_conflicts=True告诉Django“冲突就跳过,不报错”。这样导入脚本跑一百遍都不会污染数据。如果已经翻倍了,先清空表再重新导入,Django shell里执行CityAirQuality.objects.all().delete()即可。
6. 进阶技巧:用AJAX把城市切换做成局部刷新,答辩时多一个亮点
基础版本里切换城市要刷新整个页面,用户体验一般,答辩演示时还会让老师觉得“这跟Excel画图区别不大”。进阶做法是通过fetch()请求JSON接口,拿回数据后调用ECharts的setOption()更新图表,这就是局部刷新。核心代码在4.1的api_pm25接口基础上,往模板里加一段监听:
const citySelect = document.getElementById('citySelect'); citySelect.addEventListener('change', function () { const city = this.value; fetch('/api/pm25/?city=' + encodeURIComponent(city)) .then(res => res.json()) .then(data => { chart.setOption({ title: { text: city + ' PM2.5 浓度趋势' }, xAxis: { data: data.dates }, series: [{ data: data.pm25 }] }); }); });这里两个关键设计:encodeURIComponent(city)把中文城市名转成URL安全编码,否则fetch请求里的中文可能变成非法字符;chart.setOption是增量更新,只需要传入要变的部分,ECharts会自动合并新旧配置,不需要重新init创建新图表。如果调用chart.clear()再重新setOption,页面会闪一下,答辩时观感不好——增量更新是官方推荐做法,也是和“刷页面”思路最不一样的地方。
最后补一个小技巧:在setOption之后加一行chart.resize(),解决容器尺寸变化时图表不跟随的问题。有些答辩现场把浏览器窗口从笔记本投到投影仪,分辨率一变,图表要么溢出要么缩成一团,resize()就是为这个场景准备的“后悔药”。
做这类课程设计项目,我最后悔的永远是贪多嚼不烂:又是地图又是瀑布图,最后哪个都没讲清楚。这次这个方案只用了折线图加局部刷新,数据链路短、逻辑能自洽、答辩时每一行代码都讲得出来龙去脉,反而拿到了不错的评价。城市PM2.5空气质量数据可视化分析做到这个程度,已经远超“交作业”的下限。如果你时间更充裕,可以再补一个按周聚合的柱状图,或者用Django的annotate()算一个城市年均值对比表——方向对了,加功能只是时间问题。希望这些排过坑的思路能帮你少走几段弯路,祝答辩顺利。
本文还有配套的精品资源,点击获取