基于Django与ECharts的城市PM2.5空气质量数据可视化分析

发布时间:2026/10/26 23:41:00
基于Django与ECharts的城市PM2.5空气质量数据可视化分析

简介:面向毕业设计场景的城市PM2.5空气质量数据可视化分析项目,基于Python与Django框架开发。资源内含完整可运行的Web应用源码以及配套数据库文件,覆盖登录注册、数据查询、图表展示等核心功能模块,适合计算机相关专业学生直接参考学习或在此基础上进行功能扩展。压缩包共64个文件,以Python源码文件、CSV数据文件、HTML页面、数据库脚本及配置文件为主,其中包含北京、上海、广州、成都、沈阳五个城市六年PM2.5数据汇总,以及各月份、季度、时间段的空气质量变化分析数据。项目还提供通用数据处理脚本与数据导入脚本,便于复用和二次开发。资源整体大小约为12.38MB,目录结构清晰,已有406人学习浏览。通过该项目可以掌握Django框架的项目搭建方法、MySQL数据库设计与使用、ECharts等前端可视化组件的实际应用,是一份内容完整、可直接运行的毕业设计参考资源。

1. 城市PM2.5可视化分析:Django不是只能做增删改查

如果你的毕业设计选题卡在"Web应用+数据分析"这个交叉点上,那这份基于Python+Django的城市PM2.5空气质量数据可视化分析源码,算是把整条链路都打通了。它不只是给你一个能跑的Django项目,而是把数据采集、清洗入库、模型设计、可视化展示、登录注册这一整套流程完整串起来——拿到手上,数据库、后端、前端图表都是现成的,直接就能启动。

项目覆盖北京、上海、广州、成都、沈阳五个城市六年的PM2.5监测数据,分析维度包括PM2.5与温度、相对湿度、露点、风向、大气压的相关性,以及不同季度、不同时段的分布规律。它解决的痛点是:论文里"数据来源与处理"和"实验结果展示"两章怎么写才有说服力。对正在做毕业设计的学生,以及想快速搭一个数据可视化Demo的开发者来说,这比从零开始造轮子划算太多。

2. 项目结构与数据底座:从CSV到MySQL的入库流程

2.1 目录结构:先看Django项目该有的东西在哪

拿到压缩包后,先别急着跑,花五分钟把目录结构过一遍。这个项目的Django核心代码在untitled目录下,这是一个标准的Django项目配置目录,包含settings.py、urls.py、wsgi.py、asgi.py。业务应用在app01里,models.py定义数据模型,views.py写业务逻辑,templates目录放index.html、login.html、reg.html三个页面模板。

数据层分两块:根目录下有一堆CSV文件,比如一年中各个月份变化.csv、pm2.5与露点之间的关系.csv、不同季度逐年数据.csv,这些是原始数据和中间分析结果;入库后的结构化数据在mysql数据库目录下的db129.sql里。get_data目录装的是数据导入脚本和分城市的CSV文件,requirements.txt列明了依赖。

这个结构是典型的"数据处理与分析分离"设计——先用Python脚本把CSV清洗好,再导入MySQL,Django只负责从数据库读数据渲染页面。好处是分析逻辑和应用逻辑解耦,写论文时能清楚说明每一层的数据流转。

2.2 数据入库:通用脚本与分城市CSV的配合

get_data/通用脚本.py是理解整套数据流程的关键入口。这个脚本做的事其实很朴素:遍历指定目录下的CSV文件,把每个文件的数据清洗后批量插入MySQL表。核心逻辑大致是这样的:

import pymysql import pandas as pd import os # 数据库连接配置 conn = pymysql.connect( host='localhost', user='root', password='your_password', database='pm25_db', charset='utf8mb4' ) cursor = conn.cursor() # 遍历目录下所有CSV csv_dir = './data' table_name = 'pm25_records' for filename in os.listdir(csv_dir): if not filename.endswith('.csv'): continue file_path = os.path.join(csv_dir, filename) # 用pandas读取CSV,处理缺失值 df = pd.read_csv(file_path) df = df.dropna(subset=['pm25']) # 丢弃PM2.5为空的记录 # 批量插入数据 for _, row in df.iterrows(): sql = f"INSERT INTO {table_name} (city, date, pm25, temperature, humidity, dew_point, wind_direction, pressure) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)" cursor.execute(sql, (row['城市'], row['日期'], row['PM2.5'], row['温度'], row['相对湿度'], row['露点'], row['风向'], row['大气压'])) conn.commit() print(f"{filename} 导入完成,共 {len(df)} 条记录") cursor.close() conn.close()

这里用了pandas的dropna做缺失值处理,这是数据清洗最基础也最重要的一步。PM2.5监测数据经常会有设备离线导致的空值,直接空着不管会让后续统计分析失真。参数上要注意charset='utf8mb4',因为CSV里的中文城市名和风向描述字符集超出utf8范围时会报错。

数据导入.py则更精细——它允许你指定单个CSV文件导入,还加了去重逻辑,避免重复导入同一份数据。实际使用时我一般先跑通用脚本.py全量导入,再用数据导入.py做增量修正。导入完成后可以用SELECT COUNT(*) FROM pm25_records快速核验记录数,判断导入是否完整。

2.3 数据库设计:db129.sql里到底存了什么

db129.sql是MySQL的完整导出文件,包含建表语句和全量数据。如果你不想自己跑导入脚本,直接用source db129.sql命令导入即可。表设计遵循典型的"宽表"模式,一张大表存储所有监测记录,字段包括城市、日期、PM2.5浓度、温度、相对湿度、露点、风向、大气压。

宽表设计的取舍在于:查询简单、聚合高效,写SELECT AVG(pm25) FROM pm25_records WHERE city='北京' AND date BETWEEN ...这种SQL非常直接;缺点是数据冗余度高、修改字段要重建表。对于毕业设计这个体量的数据集——五个城市六年,大概几万条记录——宽表完全够用,还能减少ORM映射的复杂度。别一上来就搞三范式拆表,查询时要跨表JOIN,反而把自己绕晕。

Django的models.py里对应该表定义如下:

from django.db import models class Pm25Record(models.Model): city = models.CharField(max_length=50, verbose_name='城市') date = models.DateField(verbose_name='日期') pm25 = models.FloatField(verbose_name='PM2.5浓度') temperature = models.FloatField(null=True, blank=True, verbose_name='温度') humidity = models.FloatField(null=True, blank=True, verbose_name='相对湿度') dew_point = models.FloatField(null=True, blank=True, verbose_name='露点') wind_direction = models.CharField(max_length=50, null=True, blank=True, verbose_name='风向') pressure = models.FloatField(null=True, blank=True, verbose_name='大气压') class Meta: db_table = 'pm25_records' verbose_name = 'PM2.5监测记录'

字段类型的选择有讲究:PM2.5浓度用FloatField而不是IntegerField,因为不同城市的日均值可能带小数;温度和气压允许为空,毕竟不是每个监测站都全量采集气象数据。外键在这里明确不设置——城市直接用字符串存,日期也直接用DateField,因为分析场景是按城市分组、按时间范围筛选,不需要范式化建模。

3. Django后端与业务逻辑:数据如何从数据库到页面

3.1 视图函数:聚合查询与JSON接口

Django在这一章节是纯工具人的角色——负责从MySQL捞数据、按前端要求的格式返回。app01/views.py里的核心模式是:把SQL聚合查询封装成视图,视图再序列化成JSON返回给前端ECharts渲染。以"一年中各个月份变化"这个分析维度为例:

from django.http import JsonResponse from django.shortcuts import render from .models import Pm25Record from django.db.models import Avg, Count from django.db.models.functions import ExtractMonth def monthly_trend_chart(request): """ 返回各城市逐月平均PM2.5数据 """ city = request.GET.get('city', '北京') result = ( Pm25Record.objects .filter(city=city) .annotate(month=ExtractMonth('date')) .values('month') .annotate(avg_pm25=Avg('pm25')) .order_by('month') ) # 把QuerySet转成前端需要的字典列表 data = { 'months': [item['month'] for item in result], 'avg_pm25': [round(item['avg_pm25'], 2) for item in result] } return JsonResponse(data) def correlation_scatter(request): """ 返回PM2.5与指定气象因素的相关性散点数据 factor: temperature / humidity / pressure / dew_point """ factor_map = { 'temperature': '温度', 'humidity': '相对湿度', 'pressure': '大气压', 'dew_point': '露点' } factor = request.GET.get('factor', 'temperature') records = Pm25Record.objects.filter( city=request.GET.get('city', '北京') ).exclude(**{factor: None}) data = { 'x': [], 'y': [] } for record in records: data['x'].append(getattr(record, factor)) data['y'].append(record.pm25) return JsonResponse(data)

monthly_trend_chart用了ExtractMonth函数从日期字段提取月份,这是Django ORM提供的数据库层函数,比在Python里循环处理字符串高效得多。avg_pm25取两位小数是为了图表展示更清爽。correlation_scatter则用exclude(**{factor: None})把该字段为空的记录剔除——做相关性分析时,样本的质量比数量重要,掺杂空值的散点会画出误导性的趋势。

视图函数返回JsonResponse而不是render,是因为前端ECharts通过fetch或axios异步拿数据,页面刷新和图表渲染解耦。这是数据可视化项目比传统Django模板渲染更现代的做法。

3.2 URL路由与请求参数设计

urls.py里的路由配置清晰对应了页面和接口两个层级。页面路由直接渲染模板,接口路由返回JSON数据。常见配置如下:

from django.urls import path from . import views urlpatterns = [ path('', views.index, name='index'), path('login/', views.login_view, name='login'), path('register/', views.register_view, name='register'), # 数据接口 path('api/monthly_trend/', views.monthly_trend_chart, name='monthly_trend'), path('api/correlation/', views.correlation_scatter, name='correlation'), path('api/daily_trend/', views.daily_trend_chart, name='daily_trend'), path('api/seasonal_compare/', views.seasonal_compare, name='seasonal_compare'), ]

参数传递走request.GET,这样前端拼URL很灵活,比如/api/monthly_trend/?city=上海。path函数里的命名参数name在模板中用{% url %}反查时很有用,改URL路径不用改模板代码。

3.3 登录注册:session认证与密码安全

login.html和reg.html的存在说明项目集成了用户体系,虽然可视化页面完全可以匿名访问,但加上认证更贴合毕设的系统完整性要求。Django的authenticate和login函数在这里派上用场:

from django.contrib.auth import authenticate, login, logout from django.contrib.auth.models import User def login_view(request): if request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') user = authenticate(request, username=username, password=password) if user is not None: login(request, user) return redirect('index') else: error_msg = '用户名或密码错误' return render(request, 'login.html', {'error': error_msg if 'error_msg' in locals() else None}) def register_view(request): if request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') password2 = request.POST.get('password2') if password != password2: error_msg = '两次输入的密码不一致' elif User.objects.filter(username=username).exists(): error_msg = '用户名已存在' else: User.objects.create_user(username=username, password=password) return redirect('login') return render(request, 'reg.html', {'error': error_msg if 'error_msg' in locals() else None})

密码存储交给Django的create_user处理,它自动做哈希加盐,绝不自己set_password裸存。很多初学者在这里翻车,直接在数据库里存明文密码,答辩时被问一句"密码安全怎么保证"就卡壳了。登录后页面可以这样控制显示:模板里用{% if user.is_authenticated %}判断。

4. 可视化层实现:ECharts图表与前端页面的配合

4.1 折线图:月度趋势与时间序列

可视化是这份源码最出彩的部分。index.html里通过<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>引入ECharts 5,然后每个图标用独立div容器承载。以月度趋势图为例,核心JS如下:

document.addEventListener('DOMContentLoaded', function () { // 初始化折线图实例 var chart = echarts.init(document.getElementById('monthlyChart')); // 请求后端数据接口 fetch('/api/monthly_trend/?city=' + getSelectedCity()) .then(response => response.json()) .then(data => { // 配置图表参数 var option = { title: { text: '各月平均PM2.5浓度变化', left: 'center' }, tooltip: { trigger: 'axis', formatter: '月份 {b}:{c} μg/m³' }, xAxis: { type: 'category', data: data.months, name: '月份' }, yAxis: { type: 'value', name: 'PM2.5浓度 (μg/m³)', scale: true }, series: [{ name: 'PM2.5', type: 'line', data: data.avg_pm25, smooth: true, lineStyle: { width: 3 }, areaStyle: { opacity: 0.2 } }] }; chart.setOption(option); }); }); function getSelectedCity() { // 读取下拉选中的城市 return document.getElementById('citySelect').value || '北京'; }

这段代码的关键参数:smooth: true让折线平滑过渡,视觉上更适合展示趋势;areaStyle.opacity: 0.2填了淡色区域,强调数值大小。scale: true配合Y轴的作用是让原点从最小值开始,避免数据波动小时折线看起来太平。如果你要展示多个城市对比,series数组里多塞几个对象就行,数据从不同接口或一次查询返回。

4.2 相关性散点图与热力图:找关系的利器

相关性分析是这份毕设的加分项。PM2.5与温度、湿度、露点、大气压的关系分别用散点图表达,数据已经按城市分好。散点图配置里要加趋势线才显得专业:

fetch('/api/correlation/?city=' + getSelectedCity() + '&factor=humidity') .then(response => response.json()) .then(data => { var scatterChart = echarts.init(document.getElementById('scatterChart')); scatterChart.setOption({ xAxis: { type: 'value', name: '相对湿度 (%)' }, yAxis: { type: 'value', name: 'PM2.5浓度 (μg/m³)' }, series: [{ type: 'scatter', data: data.x.map((val, i) => [val, data.y[i]]), symbolSize: 6, itemStyle: { opacity: 0.5 } }], visualMap: { dimension: 1, min: 0, max: 300, inRange: { color: ['#d94e5d', '#eac736', '#50a3ba'] } } }); });

data.x.map((val, i) => [val, data.y[i]])把两个平行数组转成[[x1, y1], [x2, y2]]的坐标对格式,这正是散点图需要的输入结构。visualMap按Y轴数值映射颜色,高PM2.5浓度偏红色,视觉上马上能看出湿度和污染分布的关系。

4.3 页面模板与交互

三个HTML模板用了Bootstrap的类名做样式,但没有强依赖前端工程化工具,属于"原生页面+JS库"的轻量组合。index.html的布局大致是顶部仪表盘栏、左侧城市选择器、右侧图表网格。城市切换不刷新页面,直接用citySelect的onchange事件触发表格重新加载:

document.getElementById('citySelect').addEventListener('change', function () { refreshAllCharts(); // 重新请求所有接口并setOption });

组件化的点在于把refreshAllCharts封装成了公共函数,里面依次调用月度趋势、散点、季度对比等多个图表的数据加载函数。这个设计模式叫"集中式刷新管理",比每个图表各自监听事件更易维护——你新增一个图表时,只要在refreshAllCharts里加一行调用。

5. 部署与使用避坑指南

5.1 数据库连接和字符集报错

现象:pymysql连接数据库时报OperationalError: (1049, "Unknown database 'pm25_db'"),或者导入SQL时中文变成乱码。原因:没用CREATE DATABASE pm25_db CHARACTER SET utf8mb4先建库,直接source db129.sql时MySQL按默认latin1解释字符集。解决:我一般先执行建库语句再导入:

CREATE DATABASE pm25_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE pm25_db; SOURCE db129.sql;

db129.sql里如果你的MySQL版本有兼容问题,用文本编辑器打开检查ENGINE=InnoDB后面的字符集设置是否一致。

5.2 Django migration与已导入数据库冲突

现象:python manage.py migrate时提示表已存在或字段重复。原因:数据库是通过SQL文件直接导入的,但Django的migrations记录表(django_migrations)是空的,Django认为自己从未建过表。解决:先假装执行migrations,再手动打标记:

python manage.py migrate --fake-initial

--fake-initial表示"表已经存在了,你只要把migration记录标记为已执行"。如果你改了models.py加新字段,则要makemigrations生成新的迁移文件再migrate,别偷懒直接改SQL表结构,两边一不同步,后面查数据就乱套了。

5.3 Django版本与Python版本不匹配

现象:pip install -r requirements.txt时提示Django版本不支持当前Python版本。原因:requirements.txt里指定的Django版本可能高于你环境中Python能支持的版本。解决:先看README.md里的版本要求,如果没有明确,用保守组合:

pip install Django==3.2.*

Django 3.2是LTS版本,支持Python 3.6到3.10,覆盖面广。不要装Django 5.x,urls.py和settings.py的写法会有破坏性变更,项目是基于旧版写的,直接用新版大概率报错。

5.4 静态文件和模板路径404

现象:浏览器控制台报GET /static/js/echarts.min.js 404 (Not Found),图表全部不渲染。原因:settings.py里的STATIC_URL和STATICFILES_DIRS没有配置好,或者引用了网络CDN但离线环境访问不了。解决:检查settings.py:

STATIC_URL = '/static/' STATICFILES_DIRS = [ BASE_DIR / 'static', ]

然后把ECharts的min.js文件放在static/js/目录下,HTML里改用本地路径引用。

5.5 启动后报App registry not ready

现象:python manage.py runserver直接抛出LookupError: App registry not ready。原因:models.py里写了不合法的查询,或视图里在模块顶层调用了ORM操作。解决:检查app01/models.py,确保顶层只有类定义和方法定义。如果你在models.py里写了Pm25Record.objects.all()这种代码,把它挪到视图函数内部。Django的ORM必须是在应用加载完成后才能使用,顶层执行肯定会炸。

6. 进阶用法:基于现有代码快速扩展

这份项目文件里有一堆"PM2.5与XX之间关系"的CSV,说明作者做数据预处理时把相关性分析的结果导出成表格了。你完全可以在这基础上做更深入的延展,把毕设的"亮点"再抬一个档次。

一个我常用的扩展方向是"特征重要性排序"。用一个简单的多变量线性回归或者随机森林模型,输入温度、湿度、露点、气压、风速特征,预测PM2.5浓度,输出特征重要性可视化。这只需要在你的环境里加一个train_model.py:

import pandas as pd from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split from sklearn.metrics import r2_score # 读取数据汇总文件 df = pd.read_csv('北上广成沈五城市六年PM2.5数据汇总 1.csv') features = ['温度', '相对湿度', '露点', '大气压', '风向编码'] df = df.dropna(subset=features + ['PM2.5']) # 风向是文字,需要编码成数值 df['风向编码'] = df['风向'].astype('category').cat.codes X = df[features] y = df['PM2.5'] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) model = RandomForestRegressor(n_estimators=200, max_depth=10, random_state=42) model.fit(X_train, y_train) print(f'R² = {r2_score(y_test, model.predict(X_test)):.3f}') print('特征重要性:', list(zip(features, model.feature_importances_)))

然后把这个结果接到Django里,新增一个接口返回特征重要性数据,前端用一个饼图或条形图就展示出来了。比单纯的相关性散点图更有"建模"的味道。

另一个方向是多城市对比动画。ECharts的timeline组件可以做成时间轴播放器,逐年展示五座城市的污染排名变化。数据用不同季度逐年数据.csv,通过option.timeline配置的autoPlay和loop循环播放。答辩时点一下播放键,直观展示六年污染变迁,视觉冲击力很强。

用这份源码改造成竞品分析也顺手——比如换成监测其他污染物(O₃、SO₂、NO₂),只需要改models.py的字段定义和前端图表的Y轴名称,其他逻辑基本不用动。数据库里加一列、CSV里多读一列的事情,改动量控制在半小时以内。

碰到问题时我的习惯是先查README.md,再看settings.py的DEBUG = True时的报错堆栈,最后才考虑是不是代码本身有坑。从那以后每次接手这类毕业设计源码,我都会强制走一遍"先看目录、再配环境、后测数据"的流程,不急着跑服务器,省的启动一堆报错让人头大。希望帮到你。

本文还有配套的精品资源,点击获取