资讯动态

Django+Pandas+ECharts构建PM2.5数据可视化系统

发布时间:2026/9/11 13:47:24 来源:尧图企业网站定制
简介面向高校毕业设计、数据分析及Web开发学习者的Python与Django空气质量可视化系统依托北京、上海、广州、成都、沈阳五城市监测数据覆盖数据清洗、Pandas时序分析、ECharts交互图表展示完整链路是环境监测方向可运行的参考范例。压缩包共78个文件以24个CSV数据集、17个Python源文件、数据库SQL备份和README说明为主整体13.11MB另有HTML页面和配置文件辅助理解。已有52人学习下载。除Django项目完整代码外还附各城市PM2.5原始与分析CSV、MySQL数据库备份和通用数据导入脚本可直接运行体验系统按预处理、统计分析和Web展示三层组织模块化设计便于二次开发Pandas分析脚本也能帮助初学者理解时间序列与相关性分析是毕业设计选题或答辩的有力参考资料。1. 当PM2.5可视化遇到Django难点不在ECharts而在数据对齐拿到这套源码时让我意外的是真正核心的代码不在views.py而在get_data目录里那一堆CSV生成脚本。五个城市六年的PM2.5数据被拆成“一年中各个月份变化”“pm2.5与相对湿度之间的关系”等十多个维度。很多开发者做数据可视化第一反应是找一个像ECharts这样的图表库但实际查询需求却是先要把零散的监测记录按时间、气象因子对齐如果这一步不做前端再怎么画都是噪声。这套基于Python与Django的PM2.5数据可视化系统正是把这条链路完整串了起来用Pandas做统计分析用Django做Web服务用ECharts做交互展示最后用一个SQL文件落地所有结果。适合正在做毕业设计、数据库课程设计或者想仿照企业级数据可视化流程跑通一个小而完整的项目的人。你拿到的不仅是源码更是一套“数据加工厂”的样例。2. Pandas数据预处理从五城市原始CSV到多维分析表2.1 原始数据结构和清洗策略先从项目里的get_data目录说起里面按城市放了北京.csv、上海.csv、广州.csv、成都.csv、沈阳.csv还有一个“北上广成沈五城市六年PM2.5数据汇总 1.csv”。我实际打开后字段大致是date, hour, PM2.5, T, HUMI, PRES, DEWP, WSPM, RAIN个别列是中文名。这些原始文件往往存在缺失值、异常值比如PM2.5为负或者露点温度缺失。拿到手的第一步是把数据洗干净。常见做法是先用Pandas读入检查基本统计量。如果做完毕业设计答辩这里很容易被问到“缺失值怎么处理”所以脚本里最好保留处理痕迹。我一般会这样写import pandas as pd import numpy as np # 读取某城市原始数据 df pd.read_csv(get_data/北京.csv, encodingutf-8) # 查看基础信息 print(df.info()) print(df.describe()) # 删除全空行PM2.5为负或大于500的视作异常直接过滤 df df.dropna(howall) df df[(df[PM2.5] 0) (df[PM2.5] 500)] # 关键把时间字段统一成pandas datetime df[datetime] pd.to_datetime(df[[year, month, day, hour]]) df df.sort_values(datetime) # 每个时间点的PM2.5数据做平滑防止单点毛刺 df[PM2.5_smoothed] df[PM2.5].rolling(window3, min_periods1).mean()这段代码里dropna(howall)会删除所有列都为空的记录避免一整行空数据影响聚合rolling(window3, min_periods1)是三小时滑动平均因为空气质量存在短时突变绘图时平滑曲线比原始折线更容易看出趋势。你后面做数据可视化时尽量保留原始字段但图表数据用平滑值这样既不失真又好看。另外describe()输出的count、min、max能帮你快速定位异常如果PM2.5的min为负数说明原始监测记录了无效值如果max超过500就要确认是否包含沙尘暴之类的极端事件。实际处理中我会单独把负值标记为NaN而不是直接删除因为后续分析时你可以选择插值也可以用中位数填充直接丢弃会造成时间轴上缺洞。2.2 时间维度的拆分与聚合原始CSV是每小时一条记录按年月日小时排列。项目中导出了“一年中各个月份变化.csv”“时间序列逐年数据.csv”“不同季度逐年数据.csv”等其实就是对时间字段做分组聚合。这里关键点是groupby之后要reset_index否则Django读不到列名。# 按月份聚合每年每月PM2.5均值 monthly df.groupby([year, month])[PM2.5_smoothed].mean().reset_index() monthly.to_csv(data/一年中各个月份变化.csv, indexFalse, encodingutf-8-sig) # 按季度聚合使用pandas自带quarter属性 quarterly df.groupby([year, df[datetime].dt.quarter])[PM2.5_smoothed].mean().reset_index() quarterly.columns [year, quarter, avg_pm25] quarterly.to_csv(data/不同季度逐年数据.csv, indexFalse, encodingutf-8-sig) # 按一天中不同时段聚合只看hour字段得出一天24小时的污染变化曲线 hourly df.groupby(df[datetime].dt.hour)[PM2.5_smoothed].mean().reset_index() hourly.to_csv(data/一天中不同时段.csv, indexFalse, encodingutf-8-sig)dt.hour是DatetimeIndex的访问器dt.quarter是季度。做数据分析时把时间字段拆成年、月、日、时、季度是为了让前端展示时能按不同粒度切换。encodingutf-8-sig非常重要因为最终要交给Django或数据库Excel打开不会乱码。这里我生成的文件名和系统预置的csv文件保持一致比如“一年中各个月份变化.csv”“不同季度逐年数据.csv”。你可能会疑惑为什么按年聚合只生成一个“时间序列逐年数据.csv”因为六年数据点较少直接画一条趋势线比多表更直观。下面梳理一下系统里这些CSV文件对应的分析维度方便你对照源码输出文件维度聚合粒度主要字段时间序列逐年数据.csv年6个点year, avg_pm25一年中各个月份变化.csv年月72个点year, month, avg_pm25一天中不同时段.csv小时24个点hour, avg_pm25不同季度逐年数据.csv季度4个季度year, quarter, avg_pm25pm2.5与温度之间的关系.csv温度分箱温度区间temp_bin, mean, countpm2.5与相对湿度之间的关系.csv湿度分箱湿度区间humi_bin, mean, count表格里最后两列的输出实际是下面要说的气象因子统计。2.3 气象因子与PM2.5关系分析项目里“pm2.5与温度之间的关系.csv”“pm2.5与大气压之间的关系.csv”这类文件属于相关性分析。具体做法是把温度、大气压、相对湿度、露点、风速分箱然后计算每个箱体里的PM2.5均值。分箱用pd.cut边界参数需要结合业务设定。我习惯把温度设为0~5℃一个箱6~10℃一个箱以此类推避免噪声。# 以温度为例子分箱统计 bins [0, 5, 10, 15, 20, 25, 30, 35, 40] labels [0-5, 5-10, 10-15, 15-20, 20-25, 25-30, 30-35, 35-40] df[temp_bin] pd.cut(df[T], binsbins, labelslabels, rightFalse) temp_relation df.groupby(temp_bin)[PM2.5_smoothed].agg([mean, count]).reset_index() temp_relation.to_csv(data/pm2.5与温度之间的关系.csv, indexFalse, encodingutf-8-sig)rightFalse表示左闭右开区间这样5℃只会进到5-10箱不会重复计数。汇总字段我用agg([mean, count])count用于观察样本量如果某个箱体样本太少前端热力图上就不该显示明显色块。这个逻辑在论文里可以用“样本置信度”来描述。同样方法可以套到大气压、露点、相对湿度上只要把df[T]换成对应列分箱边界参考字段的四分位数。需要注意相对湿度是百分比我一般用5%一个箱大气压则用1010hPa为中心左右各5hPa一个箱。最重要的是生成的关系表里不要只保留平均值务必把样本数带出来否则答辩时评委一句“这个相关性显著吗”就答不上来。2.4 预处理脚本的工程化项目根目录的“通用脚本.py”和“数据导入.py”就是干这个的。我的建议是把清洗、聚合、导出拆成三个函数避免每增加一个维度就改主流程。下面给一个简化但完整的结构def load_and_clean(city): # ... 读取、清洗、平滑 return df def build_dimensions(df, city): monthly ... hourly ... return {monthly: monthly, hourly: hourly} def export_all(city): df load_and_clean(city) result build_dimensions(df, city) for name, out_df in result.items(): out_df.to_csv(fdata/{city}_{name}.csv, indexFalse, encodingutf-8-sig)工程化之后新增第五个城市时只要执行一次export_all(沈阳)就能生成所有维度文件。这也是源码里留有“通用的脚本”的原因。我见过不少项目把五个城市的读取代码粘五遍后面城市多了根本维护不了。另外写聚合脚本时一定要考虑时区问题原始数据如果用的是北京时间直接pd.to_datetime是没问题的但如果数据源混入了UTC就必须在读取后dt.tz_localize(UTC).dt.tz_convert(Asia/Shanghai)。这套资源里五个城市都是国内城市不涉及跨时区但你在扩展其他地区数据时要注意。3. Django模型与MySQL导入把CSV变成可查询的ORM表3.1 为什么用Django ORM而不是直接查CSVCSV导出后前端如果要按条件筛选比如只查北京6月的数据再逐个读取文件会很痛苦而且并发访问时文件IO容易崩。所以项目里会有一个mysql数据库/db129.sql这就是把之前生成的所有统计结果灌进MySQL。Django的ORM可以屏蔽原始SQL直接以AirStat这样的模型操作数据表。这里顺便涉及一个常见误区数据库课程设计里很多人习惯只建一张大宽表把所有城市、所有维度堆在一起这套项目用的是轻量分层每张表对应一个分析维度查询时有三个明显好处一是可以只获取前端需要的字段二是JOIN可以避免三是Django admin能直接按表浏览方便调试。以这个系统的实际体量来看数据量大概只有几百到几千行用ORM和原生SQL性能差异可以忽略。真正值得关心的是字段类型设计如果平均值用FloatField城市名用CharField索引怎么设计。你如果直接用db129.sql导入表结构是别人定死的后期要加字段就必须改SQL用Django的migrations管理反而更容易演进。3.2 models.py 的建模方式我拆解源码时看app01/models.py它没有把十几个CSV对应成十几张表而是设计了几张核心表。一张存原始汇总数据一张存城市维度统计一张存气象关系。下面的代码是典型的建模写法from django.db import models class CityStat(models.Model): city models.CharField(max_length50, verbose_name城市) year models.IntegerField(verbose_name年份) month models.IntegerField(verbose_name月份) pm25_avg models.FloatField(verbose_namePM2.5均值) sample_count models.IntegerField(default1, verbose_name样本数) class Meta: db_table city_stat unique_together (city, year, month) verbose_name 城市月度统计 def __str__(self): return f{self.city} {self.year}-{self.month}关键点是unique_together给三联字段加联合唯一约束防止重复执行导入脚本时产生脏数据db_table显式指定表名方便与db129.sql里的表对齐。我在实际做的时候会额外加一个updated_at models.DateTimeField(auto_nowTrue)用于记录最近一次导入时间便于判断数据是否过期。字段表如下字段类型约束含义cityCharField(50)联合唯一约束城市名yearIntegerField联合唯一年份monthIntegerField联合唯一月份pm25_avgFloatField可空PM2.5均值sample_countIntegerField默认1样本数量3.3 数据库连接配置与初始化由于资源包里有db129.sql你可以有两种选择一种是直接使用该SQL文件导入MySQL另一种是先用Django的makemigrations migrate创建表再运行数据导入脚本。我更推荐第二种因为这样模型和表结构可以保持一致。settings.py里的DATABASES要改成连接本地MySQLDATABASES { default: { ENGINE: django.db.backends.mysql, NAME: pm25_system, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, } }注意Django 3.2以后还需要在app01/init.py里加import pymysql; pymysql.install_as_MySQLdb()或者在requirements.txt里用mysqlclient。我在部署时踩过坑用PyMySQL连接时不要在项目里的init文件写这句话却安装了mysqlclient两者会冲突。如果你本地是Python 3.7资源里的cpython-37.pyc显然是为Python 3.7准备的那么PyMySQL兼容性最好。连接参数里NAME是数据库名要先在MySQL中创建比如CREATE DATABASE pm25_system DEFAULT CHARACTER SET utf8mb4否则迁移时Django会报“Unknown database”。字符集建议用utf8mb4因为城市名和中文标题都需要它。3.4 数据导入脚本把CSV灌入MySQL项目里的“数据导入.py”核心逻辑是遍历前面生成的CSV逐行update_or_create。之所以用update_or_create而不是create是防止重复执行导致主键冲突。请看一个简化版import pandas as pd from app01.models import CityStat def import_monthly_csv(csv_path, city): df pd.read_csv(csv_path) for _, row in df.iterrows(): CityStat.objects.update_or_create( citycity, yearint(row[year]), monthint(row[month]), defaults{pm25_avg: float(row[avg_pm25]), sample_count: int(row.get(count, 1))} ) print(f{city} 月度数据共 {len(df)} 条导入完成)这里defaults只更新第二参数中未指定的字段第一个参数中的字段用于检索记录。如果一条记录不存在update_or_create会直接创建。迭代DataFrame.iterrows()虽然慢但对于十万行数据也够用。如果想快一点可以改用cursor.executemany但会增加不少代码对于毕业设计而言iterrows可读性更友好。导入前记得执行python manage.py migrate把表建出来。如果你选择直接导入db129.sql那就不需要执行migrate但要先确认SQL文件里的表名和models.py中的db_table一致否则后面ORM查询时找不到表。这里我补充一个排错技巧导入完成后用CityStat.objects.count()看总数再和CSV的行数对比不一致多半是主键冲突被跳过或者CSV中有重复行。4. Django视图与ECharts渲染从ORM查询到交互图表4.1 视图函数的数据封装当数据库表建立好后展示层的工作就是把数据查出来按前端需要的JSON格式返回。这套项目并没有使用DRF而是直接在views.py里把QuerySet转成list交给模板。对于这种小系统这是一个非常务实的选择。下面是我在重写这个逻辑时的偏好import json from django.shortcuts import render from app01.models import CityStat def monthly_chart(request): city request.GET.get(city, 北京) year request.GET.get(year, 2018) qs CityStat.objects.filter(citycity, yearint(year)).order_by(month) labels [item.month for item in qs] values [item.pm25_avg for item in qs] context { city: city, year: year, labels_json: json.dumps(labels), values_json: json.dumps(values), } return render(request, index.html, context)这里order_by(month)确保图表横轴顺序正确很多坑都来自没有排序。json.dumps在服务端把Python列表序列化为JSON字符串模板中直接当作eval或放进JSON.parse即可。request.GET用于前端切换城市和年份配合下拉框实现交互。如果你接收到的year不是数字int(year)会抛异常所以更稳妥的方式是加一个try/except或者用request.GET.get(year, default)后做校验。视图里的参数命名要和模板里保持一致比如labels和values否则前端取不到值。实际项目中我还会把查询结果转为字典列表而不是两个平行数组因为后续做多城市对比时嵌套字典结构更好扩展。4.2 URL路由与反向解析urls.py需要把URL映射到该视图from django.urls import path from app01 import views urlpatterns [ path(monthly/, views.monthly_chart, namemonthly_chart), ]namemonthly_chart在模板里可以用{% url monthly_chart %}动态生成URL避免硬编码。如果你的前端需要更细粒度比如只取某一年的季度数据可以用path(quarter/int:year/, views.quarter_chart)同时视图函数里用year参数接收。推荐用int转换器否则非数字请求会直接404省去格式判断。服务部署到宝塔后URL路径要和ALLOWED_HOSTS配套如果出现404先检查根URL配置是否正确。下面是我整理的一个最小路由表URL视图函数用途/monthly/monthly_chart月度趋势折线图/hourly/hourly_chart一天24小时趋势/relation/temp/temp_relation_chart温度与PM2.5散点图/city/compare/city_compare多城市季度对比4.3 模板中接入EChartstemplates/index.html里直接在script标签中用ECharts的init和setOption。下面是我实际用过的写法包含折线图和热力图两种图形!DOCTYPE html html langzh-CN head meta charsetUTF-8 titlePM2.5月度趋势/title script srchttps://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js/script /head body select idcitySelect option value北京北京/option option value上海上海/option option value广州广州/option option value成都成都/option option value沈阳沈阳/option /select div idchart stylewidth:100%;height:500px;/div script const labels JSON.parse({{ labels_json|escapejs }}); const values JSON.parse({{ values_json|escapejs }}); const myChart echarts.init(document.getElementById(chart)); myChart.setOption({ title: { text: {{ city }} {{ year }}年月均PM2.5 }, tooltip: { trigger: axis }, xAxis: { type: category, data: labels }, yAxis: { type: value, name: μg/m³ }, series: [{ name: PM2.5, type: line, smooth: true, data: values, markPoint: { data: [{ type: max, name: 峰值 }, { type: min, name: 谷值 }] } }] }); /script /body /html注意escapejs过滤器它会把JSON字符串中的特殊字符转义防止单引号破坏JavaScript上下文。smooth: true让折线更圆滑但会掩盖尖峰如果你要展示污染事件的突变建议改为false或者同时保留原始线与平滑线。markPoint可以自动标识最大最小值答辩时很加分。ECharts的CDN地址可以换成你本地静态文件避免离线演示时白屏我一般会在项目目录下建一个static/echarts.min.js再在settings.py里配置STATICFILES_DIRS这样没有网也能跑通。4.4 多图表联动和数据缓存项目中还涉及“不同季度逐年数据”多城市对比以及“pm2.5与相对湿度之间的关系”这类散点图。多城市对比时我习惯把series作为数组动态生成而不是写死三个城市。另一个实践是使用Django的cache框架缓存查询结果比如每五分钟刷新一次from django.core.cache import cache def monthly_chart(request): cache_key fmonthly_{city}_{year} result cache.get(cache_key) if result is None: # 执行数据库查询并组装 result cache.set(cache_key, result, 300)cache.set的timeout参数300表示300秒因为PM2.5是小时级数据没必要每次请求都打数据库。但要注意如果后续导入脚本改了数据你必须手动清除旧缓存否则实际展示的还是旧值。可以给导入脚本加上cache.clear()调用。如果你用的是本地开发缓存默认是进程内缓存多进程部署时不一致生产环境换成Redis会更稳妥。多图表联动的意思是页面左侧选择城市右侧所有图表都切换成该城市数据。实现思路是给每个图表对应一个div容器视图函数返回每个图表的JSON数据前端用一个全局的loadChart(city)函数统一刷新。ECharts的dispose方法可以在刷新前销毁旧实例避免内存泄漏。5. 本地运行、验证技巧与二次开发扩展5.1 启动前的环境检查资源备份文件里有requirements.txt一般包含Django、pandas、pymysql等。我建议用Python 3.7或3.8来跑因为项目内残留.pyc的编译缓存是基于CPython 3.7的虽然不影响运行但如果是Python 3.10以上部分老版本pandas可能不兼容。创建一个虚拟环境后执行python -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate pip install -r requirements.txt python manage.py makemigrations app01 python manage.py migrate python manage.py runserver 127.0.0.1:8000启动前务必确认settings.py里的ALLOWED_HOSTS至少包含[127.0.0.1, localhost]否则浏览器访问会报DisallowedHost。如果要把系统部署到服务器我一般会再安装uwsgi或gunicorn或者直接使用宝塔面板里Python项目管理器把启动命令设置为gunicorn untitled.wsgi:application --bind 0.0.0.0:8000。宝塔的好处是能自动帮你管理环境变量和重载服务但对Django的路径处理有时不智能需要手工检查sys.path是否包含项目目录。5.2 验证数据一致性的技巧一个容易被忽略的检查点是读入CSV后的平均值和图里看到的不一定一致。因为数据库里的数据可能来自一次不完全导入。验证方式是写一条查询对比CSV和数据库的结果python manage.py shellimport pandas as pd from app01.models import CityStat df pd.read_csv(data/时间序列逐年数据.csv) db_year_avg {r[year]: r[avg_pm25] for _, r in df.iterrows()} for stat in CityStat.objects.filter(city北京): print(stat.year, stat.month, stat.pm25_avg)如果发现数据库里的monthly均值和CSV对不上常见原因是导入脚本没有清空旧表而模型又未设置唯一约束导致重复行覆盖了部分结果。这时候删除表记录重新导入即可CityStat.objects.filter(city北京).delete()5.3 用Django Admin管理城市数据给模型注册Django Admin是你体现系统完整度很讨巧的一招。进入admin.py写入以下代码from django.contrib import admin from .models import CityStat admin.register(CityStat) class CityStatAdmin(admin.ModelAdmin): list_display (city, year, month, pm25_avg, sample_count) list_filter (city, year) search_fields (city,)这样在后台就能直接按城市筛选、按年份搜索甚至点击pm25_avg排序。如果你觉得默认后台丑可以参考django admin界面美化方案比如使用django-simpleui或django-grappelli但引入外部依赖时需要保持和原项目版本兼容。整个系统跑通后把它当作一个数据中台也很不错前端可以换成Vue后端只返回JSON接口五个城市数据再扩充到更多城市时只要数据导入脚本保持update_or_create模式模型和视图都不需要大改。这比临时写十个脚本维护起来轻松得多。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价