
简介面向毕业设计场景的城市PM2.5空气质量数据可视化分析项目基于Python与Django框架开发。资源内含完整可运行的Web应用源码以及配套数据库文件覆盖登录注册、数据查询、图表展示等核心功能模块适合计算机相关专业学生直接参考学习或在此基础上进行功能扩展。压缩包共64个文件以Python源码文件、CSV数据文件、HTML页面、数据库脚本及配置文件为主其中包含北京、上海、广州、成都、沈阳五个城市六年PM2.5数据汇总以及各月份、季度、时间段的空气质量变化分析数据。项目还提供通用数据处理脚本与数据导入脚本便于复用和二次开发。资源整体大小约为12.38MB目录结构清晰已有406人学习浏览。通过该项目可以掌握Django框架的项目搭建方法、MySQL数据库设计与使用、ECharts等前端可视化组件的实际应用是一份内容完整、可直接运行的毕业设计参考资源。1. 城市PM2.5可视化分析Django不是只能做增删改查如果你的毕业设计选题卡在Web应用数据分析这个交叉点上那这份基于PythonDjango的城市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清洗好再导入MySQLDjango只负责从数据库读数据渲染页面。好处是分析逻辑和应用逻辑解耦写论文时能清楚说明每一层的数据流转。2.2 数据入库通用脚本与分城市CSV的配合get_data/通用脚本.py是理解整套数据流程的关键入口。这个脚本做的事其实很朴素遍历指定目录下的CSV文件把每个文件的数据清洗后批量插入MySQL表。核心逻辑大致是这样的import pymysql import pandas as pd import os # 数据库连接配置 conn pymysql.connect( hostlocalhost, userroot, passwordyour_password, databasepm25_db, charsetutf8mb4 ) 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 fINSERT 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监测数据经常会有设备离线导致的空值直接空着不管会让后续统计分析失真。参数上要注意charsetutf8mb4因为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_length50, verbose_name城市) date models.DateField(verbose_name日期) pm25 models.FloatField(verbose_namePM2.5浓度) temperature models.FloatField(nullTrue, blankTrue, verbose_name温度) humidity models.FloatField(nullTrue, blankTrue, verbose_name相对湿度) dew_point models.FloatField(nullTrue, blankTrue, verbose_name露点) wind_direction models.CharField(max_length50, nullTrue, blankTrue, verbose_name风向) pressure models.FloatField(nullTrue, blankTrue, 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(citycity) .annotate(monthExtractMonth(date)) .values(month) .annotate(avg_pm25Avg(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( cityrequest.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, nameindex), path(login/, views.login_view, namelogin), path(register/, views.register_view, nameregister), # 数据接口 path(api/monthly_trend/, views.monthly_trend_chart, namemonthly_trend), path(api/correlation/, views.correlation_scatter, namecorrelation), path(api/daily_trend/, views.daily_trend_chart, namedaily_trend), path(api/seasonal_compare/, views.seasonal_compare, nameseasonal_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, usernameusername, passwordpassword) 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(usernameusername).exists(): error_msg 用户名已存在 else: User.objects.create_user(usernameusername, passwordpassword) 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 srchttps://cdn.jsdelivr.net/npm/echarts5/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() factorhumidity) .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版本有兼容问题用文本编辑器打开检查ENGINEInnoDB后面的字符集设置是否一致。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 Django3.2.*Django 3.2是LTS版本支持Python 3.6到3.10覆盖面广。不要装Django 5.xurls.py和settings.py的写法会有破坏性变更项目是基于旧版写的直接用新版大概率报错。5.4 静态文件和模板路径404现象浏览器控制台报GET /static/js/echarts.min.js 404 (Not Found)图表全部不渲染。原因settings.py里的STATIC_URL和STATICFILES_DIRS没有配置好或者引用了网络CDN但离线环境访问不了。解决检查settings.pySTATIC_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.pyimport 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(subsetfeatures [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_size0.2, random_state42) model RandomForestRegressor(n_estimators200, max_depth10, random_state42) model.fit(X_train, y_train) print(fR² {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时的报错堆栈最后才考虑是不是代码本身有坑。从那以后每次接手这类毕业设计源码我都会强制走一遍先看目录、再配环境、后测数据的流程不急着跑服务器省的启动一堆报错让人头大。希望帮到你。本文还有配套的精品资源点击获取