ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Python+Django城市PM2.5空气质量数据可视化分析实战指南

Python+Django城市PM2.5空气质量数据可视化分析实战指南 简介一份基于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. PythonDjango的MVT骨架为什么课程设计首选Django而不是Flask2.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原生JavaScriptWeb页面图表强setOption增量更新中需要写一点JS低数据传递逻辑自己可控pyechartsPython生成图表页面弱改配置要重新生成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动手之前先把环境隔离好。很多PythonDjango项目跑不起来不是代码的问题是全局环境里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_length64, db_indexTrue, verbose_name城市) monitor_date models.DateField(db_indexTrue, verbose_name监测日期) pm25 models.FloatField(verbose_namePM2.5浓度) aqi models.IntegerField(verbose_nameAQI指数) quality models.CharField(max_length16, blankTrue, 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_indexTrue给城市和日期加了数据库索引按城市筛选、按时间范围排序会明显变快课程设计数据量虽然小但养成建索引的习惯没有坏处。unique_together (city, monitor_date)是去重约束保证同一个城市同一天的记录只有一条这比在导入脚本里手动if判断可靠得多。ordering [monitor_date]让所有查询默认按日期升序后面写视图时不用每次都想起来加order_by。FloatField用来存PM2.5浓度因为监测值经常出现78.5这样的带小数数据AQI是整数用IntegerField。quality字段设置blankTrue, default是因为有些数据源可能没给等级硬性nullFalse会导致导入失败。模型定义完执行迁移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, encodingutf-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( citystr(row[city]).strip(), monitor_datepd.to_datetime(row[date]).date(), pm25float(row[pm25]), aqiint(row[aqi]), qualitystr(row[quality]).strip(), )) if len(objs) batch_size: CityAirQuality.objects.bulk_create(objs, ignore_conflictsTrue) objs.clear() if objs: CityAirQuality.objects.bulk_create(objs, ignore_conflictsTrue) print(f导入完成共 {CityAirQuality.objects.count()} 条记录)脚本前三行是Django独立运行脚本的标准写法设置环境变量、调用django.setup()否则模型无法访问数据库。encodingutf-8-sig是专门针对Windows下编辑的CSV文件带BOM头用utf-8直接读会把\ufeff混进第一列列名。pd.to_datetime()统一日期格式float()和int()做类型转换避免字符串入库。这段代码的核心是bulk_create批量写入一次性提交1000条比在循环里逐条save()快一个数量级。ignore_conflictsTrue配合模型里的unique_together约束重复导入时不会报错重复记录会被自动跳过。最后一行打印总记录数这就是第一条验证命令——看到它说明数据真正落库了。3.5 三条验证命令确认数据真的导对了数据导入完别急着写页面先用三条命令确认数据质量。打开Django shellpython manage.py shellfrom core.models import CityAirQuality CityAirQuality.objects.count() CityAirQuality.objects.filter(city北京).count() CityAirQuality.objects.values(quality).annotate(cntmodels.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(citycity).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, flatTrue).distinct() return render(request, dashboard.html, { chart_data: json.dumps(data, ensure_asciiFalse), current_city: city, cities: cities, }) def api_pm25(request): city request.GET.get(city, ) if not city: return JsonResponse({error: 缺少city参数}, status400) rows CityAirQuality.objects.filter(citycity).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, flatTrue).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, namedashboard), path(api/pm25/, views.api_pm25, nameapi_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 langzh-CN head meta charsetUTF-8 title{{ current_city }} PM2.5 近30天趋势/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script style #pm25Chart { width: 100%; height: 400px; } /style /head body select idcitySelect {% for city in cities %} option value{{ city }} {% if city current_city %}selected{% endif %}{{ city }}/option {% endfor %} /select div idpm25Chart/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不要转义否则引号会被变成quot;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时用encodingutf-8-sig这个编码会自动吃掉BOM头同时正常解析UTF-8内容。如果数据源确实是GBK就改用encodinggbk。更稳妥的做法是一开始就把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_asciiFalse)。在API接口里如果用JsonResponse则传参json_dumps_params{ensure_ascii: False}。两个地方一个都不能漏模板渲染用的chart_data走的是json.dumpsAJAX接口走的是JsonResponse。这条排查路线的现象特别容易和编码问题混淆实际是序列化参数的问题。5.5 导入数据重复翻倍unique_together是后悔药现象导入脚本跑了两遍数据库里城市和日期的组合出现了两行相同记录图表里某一天有两个数值点折线图看上去像“重影”。这会让平均值计算和峰值统计全部失真。原因导入脚本没有去重逻辑第二次执行时又批量插入了全部数据。课程设计阶段重复导入很常见尤其是调试导入脚本时反复执行。解决模型上建unique_together (city, monitor_date)导入时用bulk_create(objs, ignore_conflictsTrue)。unique_together在数据库层面建了联合唯一索引重复数据插入时数据库会拒绝ignore_conflictsTrue告诉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()算一个城市年均值对比表——方向对了加功能只是时间问题。希望这些排过坑的思路能帮你少走几段弯路祝答辩顺利。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表