Django毕业设计:PM2.5数据可视化项目全栈实现与数据分析
2026/9/16 9:23:05 网站建设 项目流程

简介:完整可运行的Python基于Django城市PM2.5空气质量数据可视化分析毕业设计项目,面向计算机相关专业毕业生、课程设计与期末大作业学习者。项目基于北京、上海、广州、成都、沈阳五城市六年PM2.5观测数据,用Python完成清洗与分析,研究PM2.5浓度随月份、季度、时间段的趋势变化,以及与温度、露点、气压、风向、相对湿度等气象因子的关系,并将结果导出CSV;后端采用Django,前端使用ECharts将多维度分析结果以图表形式直观呈现。压缩包共65个文件,以CSV数据结果、Python源码、HTML模板、SQL数据库脚本为主,另有Pyc编译文件与XML配置文件,整体12.37MB;目录按数据、后端应用、前端模板等模块分层,便于直接运行和二次开发。项目包含全部源码、数据库及说明文档,说明文档覆盖开发环境配置与部署要点,经严格调试可确保运行,适合作为毕业设计或课设的完整参考方案。目前已有131人学习下载。

1. Django毕业设计中的PM2.5可视化项目到底在做什么

先看结论:这个项目能高分通过,不是因为网站页面做得花哨,而是因为它把“数据获取—清洗—分析—存储—可视化”整条链路走完了。拿到源码后你会发现,真正吃功夫的部分在 get_data 目录下的通用脚本与数据导入.py,后面的 Django 和 Echarts 只是把分析结果换个形式展示出来。对于正在做毕业设计的学生,这套五城市 PM2.5 数据分析项目可以直接当模板使用,换一套城市数据、调整几个分析维度,就能变成另一个题目。它覆盖的知识点包括 pandas 数据聚合、Django ORM、MySQL 存储、Echarts 数据可视化,基本对应数据库课程设计、软件工程类课程和企业级数据可视化项目的核心要求。适合计算机相关专业学生借鉴,也适合想补全 Django 全栈链路的新手。

2. 五城市PM2.5数据清洗与聚合:那些CSV结果是怎么算出来的

2.1 原始数据字段与清洗起点

源码包里 get_data 下放着北京、上海、广州、成都、沈阳五个城市的原始 CSV,以及“一年中各个月份变化”“一天中不同时段”“pm2.5与温度之间的关系”等一批分析结果。这些结果不是手工统计的,而是通用脚本.py 用 pandas 算完导出。要复现,得先处理一个实际问题:五个 CSV 的字段命名并不统一,有的叫PM2.5,有的叫质量浓度,时间字段有的是字符串,有的已经是 datetime 对象。真实数据分析里最耗时间的就是这一步。

常见做法是写一个统一的加载函数,把字段名规范化后再合并:

import pandas as pd def load_city(path): df = pd.read_csv(path, encoding='utf-8') df.columns = [c.strip().lower() for c in df.columns] df = df.rename(columns={ 'pm2.5': 'pm25', '质量浓度': 'pm25', '时间': 'datetime', 'date': 'datetime' }) # 统一时间字段 df['datetime'] = pd.to_datetime(df['datetime']) # 浓度转数值,非数字变成 NaN df['pm25'] = pd.to_numeric(df['pm25'], errors='coerce') return df.dropna(subset=['pm25', 'datetime'])

pd.to_numericerrors='coerce'会把类似"1,234"或空字符串转成 NaN,避免后续groupby时数据类型报错。注意最后dropna只删浓度和时间为空的行,气象字段缺失可以保留,因为同一行的气象列不一定参与所有分析。加载五个城市后,还需要给每个 DataFrame 加一列城市名,再pd.concat合并成一张总表,后续所有分析都基于这张总表,否则统计口径对不上。

2.2 月份和时段聚合:季节性与昼夜规律怎么算

合并后的总表先提取月份和小时两个分组键。项目里的“一年中各个月份变化.csv”描述的是多年同月均值,不是某一年的值,所以分组键是month,聚合方式是mean

df['month'] = df['datetime'].dt.month month_stat = df.groupby(['city', 'month'])['pm25'].mean().reset_index() month_stat.to_csv('一年中各个月份变化.csv', index=False, encoding='utf-8-sig')

这里用utf-8-sig而不是utf-8,是因为 Windows 上的 Excel 打开无 BOM 的 UTF-8 文件会出现中文乱码,Django 后续再读这个 CSV 时依然可以按utf-8-sig读取,不影响解析。按城市分组是为了让前端能做多城市对比图,如果只按月份不分城市,北京和广州的年变化曲线就会被平均掉,看不出南北差异。

“一天中不同时段.csv”完全同理,把dt.month换成dt.hour即可。需要注意的是,如果原始数据是小时粒度,直接算均值没有问题;如果原始数据是每日一条,那么“一天中不同时段”这一列就没有意义。项目文件里明确有这个输出,说明原始五城市 CSV 中至少有一部分是小时级记录。我自己做的时候会把datetime的粒度先打印出来确认,不要凭感觉认为是小时级。

2.3 气象要素相关性:分箱而不是直接算相关系数

项目里输出了一组“pm2.5与温度/湿度/气压/露点之间的关系.csv”,很多人看到的第一反应是直接df[['pm25', 'temperature']].corr()。这个做法在这个场景下不推荐,原因是 PM2.5 浓度和气象要素之间不是简单线性关系,温度零上和零下时影响方向完全不同,直接算相关系数会得到接近 0 的结果,画出来也只是一团散点。

常见做法是把温度(或湿度、气压、露点)做分箱处理,按箱计算 PM2.5 均值,得到一条趋势线:

bins = [-20, -10, 0, 10, 20, 30, 40] labels = ['< -10', '-10~0', '0~10', '10~20', '20~30', '> 30'] df['temp_bin'] = pd.cut(df['temperature'], bins=bins, labels=labels, right=False) temp_stat = (df.groupby('temp_bin', observed=True)['pm25'] .agg(['mean', 'count']) .reset_index()) temp_stat.to_csv('pm2.5与温度之间的关系.csv', index=False, encoding='utf-8-sig')

pd.cut是分箱的核心函数,bins定义了分界点,right=False表示区间是左闭右开,-10~0这个区间只包含 -10 度但不包含 0 度。observed=True在 pandas 2.x 中是可选参数,在分组时避免未来警告。这样得到的 CSV 就是前端 Echarts 画折线图的数据源,横轴是温度区间,纵轴是该区间内的平均浓度。

湿度、气压、露点这三组字段做同样的处理,只是修改分箱边界。露点与温度高度相关,处理时最好用露点差或者直接沿用同一套区间,不要单独发明一种分箱逻辑。项目文件里同时存在“pm2.5与大气压之间的关系.csv”和“pm2.5与露点之间的关系.csv”,说明作者为每个气象要素都单独算了一份,而不是在一张表里塞四列。保持这种“一个主题一张表”的结构,对后续 Django 读取和前端渲染更友好。

2.4 季度逐年与时间序列:给历史趋势留出数据源

“不同季度逐年数据.csv”和“时间序列逐年数据.csv”这两张表服务于两类图:季度柱状图和年度趋势折线图。分组键要提取两个字段,季度是year + quarter,时间序列是year + month

df['year'] = df['datetime'].dt.year df['quarter'] = df['datetime'].dt.quarter quarter_stat = df.groupby(['city', 'year', 'quarter'])['pm25'].mean().reset_index() quarter_stat['period'] = quarter_stat['year'].astype(str) + 'Q' + quarter_stat['quarter'].astype(str) monthly_stat = df.groupby(['city', 'year', 'month'])['pm25'].mean().reset_index() monthly_stat['period'] = monthly_stat['year'].astype(str) + '-' + monthly_stat['month'].astype(str).str.zfill(2)

注意这里把 city 加进了分组键,因为前端需要按城市切换或叠加多城市曲线。period这一列是在拼接时间标签,str.zfill(2)7补成07,保证字符串排序时和真实时间顺序一致。如果直接用整数2020-7这种格式,Echarts 的类目轴会按字典序排列,7 月会排在 10 月后面,图表错位。这里的年份字符串和月份补零,是保证 x 轴顺序正确的关键,项目原代码里大概率也做了类似处理。

3. Django 2.x 与 MySQL:模型设计、数据导入与JSON接口

3.1 为什么不用 SQLite 而选 MySQL

源码包里带着 db129.sql,说明原项目最终数据落在 MySQL 里。对毕业设计来说,这个选择本身就是一个加分项:SQLite 是文件型数据库,不需要安装服务端,演示时看不出对数据库设计能力的掌握;MySQL 需要先建库、导入 SQL、配置连接,整条链路更贴近企业开发。而且题目里强调了“数据库”,用 MySQL 完成数据存储,答辩时能讲的内容也更多。

进入代码后,先在 models.py 里定义两张表。一张存五城市逐条原始记录,另一张存分析结果。分两张表的原因:原始记录适合做条件筛选和图表下钻,分析结果适合直接返回给 Echarts,避免每次页面请求都现场计算:

from django.db import models class Pm25Record(models.Model): city = models.CharField(max_length=16, db_index=True) collect_time = models.DateTimeField(db_index=True) pm25 = models.FloatField() temperature = models.FloatField(null=True, blank=True) humidity = models.FloatField(null=True, blank=True) pressure = models.FloatField(null=True, blank=True) dew_point = models.FloatField(null=True, blank=True) wind_direction = models.CharField(max_length=16, blank=True) class Meta: db_table = 'pm25_record' ordering = ['collect_time'] class AnalysisResult(models.Model): module = models.CharField(max_length=32, db_index=True) city = models.CharField(max_length=16, blank=True, default='') x_label = models.CharField(max_length=32) y_value = models.FloatField() class Meta: db_table = 'analysis_result'

db_index=True加在城市和时间字段上,是因为页面的多城市对比查询会频繁以city作为过滤条件,MySQL 在数据量上来后没有索引会走全表扫描。AnalysisResult.module存的是图表的业务标识,比如一年中各个月份变化,前端请求时直接filter(module=...),后端的查询逻辑可以保持通用。

3.2 settings.py 数据库配置与 mysqlclient 安装

用 MySQL 就必须在 settings.py 里改数据库配置。Django 的默认配置是 SQLite,改成 MySQL 后还需要安装驱动。Python 3.7 环境下最常用的是 mysqlclient:

DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'db129', 'USER': 'root', 'PASSWORD': '你的密码', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': {'charset': 'utf8mb4'}, } }

NAME对应 db129.sql 导入后创建的数据库名,不是表名。utf8mb4utf8多支持 emoji 和生僻字,空气质量数据里不会出现 emoji,但建议保留这个配置,避免后续导入其他数据时编码报错。Windows 上pip install mysqlclient经常失败,常见解决办法是下载与 Python 3.7 对应的.whl文件离线安装,或者把ENGINE换成django.db.backends.mysql后搭配 PyMySQL,在__init__.py里加上pymysql.install_as_MySQLdb()。Linux 下装 mysqlclient 则要先装libmysqlclient-dev,这也是项目说明文档里应该写但没有写清的一步。

3.3 导入 db129.sql 与分析结果 CSV

数据库建好后,先把原有 SQL 导入:

mysql -u root -p db129 < db129.sql

注意命令里的db129是目标数据库名,前提是已经提前执行过CREATE DATABASE db129 CHARACTER SET utf8mb4。如果直接导入报 “Unknown database”,先建库再导入。导入成功后可以用mysql -u root -p -e "SHOW TABLES FROM db129"验证表是否存在。

导入完成后,还需要把 get_data 目录下的分析结果 CSV 写入AnalysisResult表。这步可以用 Django shell 执行一段独立脚本,也可以写成 get_data/data_import.py:

import os import django os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'untitled.settings') django.setup() import pandas as pd from app01.models import AnalysisResult AnalysisResult.objects.all().delete() csv_files = [ '一年中各个月份变化.csv', '一天中不同时段.csv', 'pm2.5与温度之间的关系.csv', 'pm2.5与相对湿度之间的关系.csv', ] for fname in csv_files: df = pd.read_csv(fname, encoding='utf-8-sig') module = fname.replace('.csv', '') rows = [] for _, row in df.iterrows(): rows.append(AnalysisResult( module=module, city=row.get('city', ''), x_label=str(row.iloc[1]), y_value=row.iloc[2] )) AnalysisResult.objects.bulk_create(rows)

AnalysisResult.objects.all().delete()清空旧数据,防止重复执行脚本导致数据翻倍。bulk_create批量写入比create()循环快很多,五张表的数据量不大,但养成批量写入的习惯能避免以后处理几十万条记录时卡死。row.iloc[1]row.iloc[2]分别取第二列和第三列,对应分析 CSV 中的标签列和数值列。如果 CSV 列顺序不对,这里要按实际表头调整,不能写死。

3.4 views 层输出 JSON:一个接口处理所有图表

前端要画图,后端就要提供数据接口。最直接的方式是写一个通用接口,接收modulecity两个 GET 参数,查表后返回 JSON:

from django.http import JsonResponse from django.views.decorators.http import require_GET from .models import AnalysisResult @require_GET def chart_data(request): module = request.GET.get('module', '') city = request.GET.get('city', '') qs = AnalysisResult.objects.filter(module=module) if city: qs = qs.filter(city=city) data = [{'x': r.x_label, 'y': r.y_value} for r in qs] return JsonResponse({'code': 0, 'data': data})

require_GET限制这个方法只接受 GET 请求。如果模块名不存在,查询会返回空列表,前端不会报错但会画出一张空图,所以接口里最好再做一层校验,返回code=1和错误提示。注意这里没有用 Django REST Framework,因为项目只需要只读查询,原生 JsonResponse 足够,减少依赖也能降低部署出错的概率。URL 配置里把路径绑定到chart_data即可,不用设计复杂路由。

4. Echarts数据可视化前端:Django模板里的图表渲染

4.1 Django模板引入Echarts静态资源

前端页面在 templates/index.html 中,Echarts 的库文件需要放到静态文件目录。Django 默认的静态文件查找路径是各 app 下的 static 目录,app01/static/echarts.min.js。模板里的引入方式要使用 Django 模板引擎提供的 static 标签:

{% load static %} <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>PM2.5 数据分析</title> <script src="{% static 'echarts.min.js' %}"></script> </head> <body> <div id="month-chart" style="width: 90%; height: 420px"></div> <script src="{% static 'main.js' %}"></script> </body> </html>

{% static %}标签而不是硬编码/static/echarts.min.js,是因为 Django 项目部署到子路径或者使用 CDN 时,静态文件 URL 会变。开发阶段static标签可以正常工作,部署时再执行collectstatic收集所有静态文件即可。main.js里写图表初始化逻辑,不建议在 HTML 里写大量 script,不利于调试和维护。

4.2 fetch 请求接口并渲染折线图

前端拿到后端 JSON 后,用 Echarts 初始化图表。这里用原生 fetch 而不是 jQuery 的$.ajax,因为 Echarts 5.x 和现代浏览器已经完全支持 fetch,多一个 jQuery 依赖没有意义:

async function loadTrendChart() { const resp = await fetch('/chart_data/?module=一年中各个月份变化'); const result = await resp.json(); const chart = echarts.init(document.getElementById('month-chart')); const option = { title: { text: 'PM2.5 各月份均值变化' }, tooltip: { trigger: 'axis' }, legend: { data: ['北京', '上海', '广州'] }, grid: { left: '10%', right: '4%', bottom: '10%', containLabel: true }, xAxis: { type: 'category', name: '月份' }, yAxis: { type: 'value', name: 'μg/m³' }, series: [] }; chart.setOption(option); } loadTrendChart();

这里的 xAxis 和 series 还没有数据。因为后端返回的数据是单城市,而目标页面的图表通常是五城市对比,所以更好的做法是先按城市维度拉取五组 JSON,再动态构造 series。下面这个写法把 fetch 循环和 series 构造结合到一起:

const cities = ['北京', '上海', '广州', '成都', '沈阳']; async function buildSeries(module) { const series = []; for (const city of cities) { const resp = await fetch(`/chart_data/?module=${encodeURIComponent(module)}&city=${encodeURIComponent(city)}`); const result = await resp.json(); if (result.code !== 0) continue; series.push({ name: city, type: 'line', smooth: true, data: result.data.map(row => [row.x, row.y]) }); } return series; }

encodeURIComponent用来编码中文参数,直接拼接 URL 时中文参数在某些浏览器中会出错。data这里用[row.x, row.y]这种二维数组格式,Echarts 会自动识别为坐标对,比传两个数组更直观。接口返回的数据是按表里的顺序排的,但前端不能依赖这个顺序,尤其是 x 轴是类目轴时,更稳妥的做法是在后端查询里明确order_by('id')order_by('x_label')

4.3 图表配置项与常见坑

Echarts 图表不出图,七成问题出在数据格式,两成出在容器高度,一成出在配置项名称。Django 模板里的图表容器如果高度为 0,初始化时不会报错,但图表就是不显示。样式表里要确保容器有明确高度,比如height: 420px,不能只给宽度。

下面是本项目几类图表推荐用的配置:

图表类型核心配置说明
月度趋势折线图xAxis.type: 'category'+series[i].type: 'line'x 轴是月份字符串,适合用类目轴
时段变化折线图smooth: true平滑曲线展示昼夜变化,避免锯齿
气象要素关系图tooltip.trigger: 'axis'鼠标悬停时显示温度区间与浓度
季度柱状图series[i].type: 'bar'柱状图更适合季度对比,视觉差异明显
时间序列总图dataZoom组件6 年数据较长,加 dataZoom 拖动查看

Echarts 5 中散点图、折线图、柱状图的 data 数组格式不统一,传错后会出空白。比如折线图支持[value, value]二维数组,柱状图则常直接传[value1, value2]。把 PM2.5 浓度做成二维坐标对时,一定要确认 series 类型,否则会出现“有数据但画不出来”的假死状态。遇到这种情况,先在浏览器控制台打印result.data,看看每一行的字段名是不是xy,再检查row.x是否为空字符串。

4.4 动态切换模块的前端组织方式

页面如果只有一张图不符合“可视化分析”的定位,项目里应该有多个图表或一个联动切换的图表面板。常见做法是给模块按钮绑定点击事件,每次点击时重新调用buildSeries并更新图表:

document.querySelectorAll('.module-btn').forEach(btn => { btn.addEventListener('click', async () => { const module = btn.dataset.module; const series = await buildSeries(module); chart.setOption({ series: series }); }); });

dataset.module对应按钮上的>import pymysql pymysql.install_as_MySQLdb()

这样 Django 的 MySQL 后端会调用 PyMySQL 作为数据库驱动,功能上完全兼容。项目文件里带了__init__.py,建议把这段写进工程主目录的__init__.py而不是 app 的__init__.py,因为启动时加载顺序更早,不会出现 DSN 未初始化的问题。

5.3 从数据结构上扩展:给项目加一个污染等级模块

原项目只做均值分析,答辩时要体现扩展能力,可以在不改变现有框架的情况下增加一列“污染等级”。PM2.5 浓度分级有一个通用标准:35μg/m³ 以下为优,35-75 为良,75-115 为轻度污染,115-150 中度,150-250 重度,250 以上严重。利用 pandas 的pd.cut把浓度分箱,统计每个等级的天数占比,再走一遍“CSV 导出—数据导入—Django 查询—Echarts 柱状图”的链路,等于把整个开发流程又演示了一遍。这个模块不算复杂,但能体现你理解了整个前后端数据流,比单纯多画一张图更有说服力。

再加一步,把五个城市的等级占比数据合并输出成一张汇总表,前端用堆叠柱状图展示,就能看到北京、沈阳的冬季重污染天数明显高于广州、成都,这个结论和公开研究一致,答辩时能形成一个小亮点。顺便可以让图表支持点击柱子下钻到具体污染事件,也就是先加载月度数据,点击月份后再请求该月的逐日数据,复用现有接口多加一个level参数就能实现。整个页面仍然保持一个模块一张图,不会破坏现有设计。

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

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

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

立即咨询