简介:本资源面向环境科学、数据挖掘与机器学习方向的学习者与研究者,提供一套基于Python的空气质量数据可视化分析系统源码及配套数据,可用于城市群划分、污染传输网络构建与传播过程探索等课题实践。项目采用BS架构,前端整合HTML、CSS、JavaScript及D3、ECharts、Mapbox等可视化库,后端基于Python与Flask搭建,main.py为服务入口,dataManager.py负责数据模块,files目录存放原始数据与挖掘结果,templates下index.html为前端入口。压缩包共2000个文件,以1295个json数据文件和697个py脚本为主,另含少量txt、xml与md说明文档,整体约76.43MB,结构清晰便于按模块查阅。目前已有252人学习下载,适合希望掌握降维聚类、粒子输运与相关分析、多维空间变换渐进式探索等方法,并需要完整可运行案例的读者参考。
1. 空气质量数据挖掘系统:从一份 CSV 到能跑通的分析闭环
空气质量数据本身不稀缺,各地环保部门每天都会公开 AQI、PM2.5、PM10、SO2、NO2、CO、O3 这些指标,真正卡住人的是拿到数据之后那一步:字段对不上、时间戳乱、缺失值成片、模型跑出来 R² 只有 0.3,可视化图表画出来自己都不想看。这套「Python 基于数据挖掘与机器学习的空气质量数据可视化分析系统」要解决的正是这条链路——把原始监测数据清洗成可建模的结构,用机器学习做污染物浓度预测或空气质量等级分类,再用可视化把结论讲清楚。它适合两类人:一类是正在做课程设计或毕业设计的学生,需要一套能跑通、能改、能写进论文的完整流程;另一类是想把数据分析能力落到环保、城市治理场景的工程师,需要知道特征怎么选、模型怎么调、图表怎么组织。下面按数据获取、清洗、特征工程、建模、可视化、避坑的顺序,把每个环节的参数和代码都摊开讲。
2. 数据获取与字段理解:先把数据源和字段含义吃透
2.1 空气质量数据的常见来源与字段结构
做这个系统,第一步不是写代码,是搞清楚数据长什么样。常见的空气质量数据来源有三类:一是各地环境监测总站公开的逐小时或逐日数据,通常以 CSV 或 Excel 形式提供;二是通过公开 API 获取的实时数据,返回 JSON 格式;三是竞赛平台或教学数据集,字段已经整理好但需要自己理解含义。不管哪种来源,核心字段基本一致:城市名、监测站点、日期时间、AQI 值、空气质量等级、PM2.5、PM10、SO2、NO2、CO、O3 这几项污染物浓度。其中 AQI 是综合指数,由六项污染物分指数取最大值得到,PM2.5 和 PM10 通常是影响 AQI 的主要因子。
理解字段之后要确认两件事:一是单位,PM2.5、PM10、SO2、NO2、CO、O3 的浓度单位一般是 μg/m³,CO 有时用 mg/m³,混用会导致数值差三个数量级;二是时间粒度,逐小时数据和逐日数据的建模策略完全不同,逐小时数据要考虑昼夜周期和滞后效应,逐日数据更适合做趋势分析和等级分类。我一般会先写一段探查代码,把字段类型、缺失比例、取值范围一次性打出来,避免后面反复返工。
import pandas as pd import numpy as np # 读取原始数据,注意编码,国内公开数据常见 gbk 或 utf-8-sig df = pd.read_csv('air_quality_raw.csv', encoding='utf-8-sig') # 基础探查:字段类型、缺失率、描述统计 print(df.dtypes) print(df.isnull().mean().sort_values(ascending=False)) print(df.describe().T[['mean', 'std', 'min', 'max']]) # 检查时间字段是否能正确解析 df['datetime'] = pd.to_datetime(df['datetime'], errors='coerce') print('时间解析失败条数:', df['datetime'].isnull().sum())这段代码做三件事:dtypes看字段类型,判断数值列有没有被读成字符串;isnull().mean()算每列缺失比例,超过 30% 的列要重点处理;describe()看均值和极值,如果 PM2.5 最大值出现 9999 这种数,基本是异常标记值,需要替换成 NaN。时间字段用errors='coerce'强制转换,解析失败的会变成 NaT,方便后续统计。
2.2 用 Pandas 做缺失值处理与异常值识别
空气质量数据缺失是常态,传感器故障、通信中断、维护校准都会导致整段缺失。处理方式取决于缺失比例和缺失模式:缺失比例低于 5% 且随机分布,直接删掉或均值填充影响不大;缺失比例在 5% 到 30% 之间,建议用前后值插值或按小时均值填充;超过 30% 的列,要么放弃该字段,要么用模型预测填补,但后者容易引入偏差。异常值识别常用 3σ 原则和 IQR 四分位距法,空气质量数据更适合 IQR,因为浓度分布本身右偏,3σ 会把大量真实高值误判为异常。
# 缺失值处理:按时间排序后做线性插值,限制最大连续填充长度 df = df.sort_values('datetime').reset_index(drop=True) df['PM2.5'] = df['PM2.5'].interpolate(method='linear', limit=6) df['PM10'] = df['PM10'].interpolate(method='linear', limit=6) # 异常值处理:IQR 法,超出上下界的替换为边界值 def cap_outliers(series): q1, q3 = series.quantile(0.25), series.quantile(0.75) iqr = q3 - q1 lower, upper = q1 - 1.5 * iqr, q3 + 1.5 * iqr return series.clip(lower, upper) for col in ['PM2.5', 'PM10', 'SO2', 'NO2', 'CO', 'O3']: df[col] = cap_outliers(df[col]) print('处理后缺失情况:', df[['PM2.5', 'PM10']].isnull().sum().to_dict())interpolate的limit=6表示最多连续填充 6 个点,超过就保留 NaN,避免用插值掩盖长时间缺失。clip把超出 IQR 上下界的值压到边界,而不是直接删除,这样保留样本量的同时降低极端值对模型的干扰。如果数据里存在 9999 这类标记值,要在插值前先用replace(9999, np.nan)替换掉,否则插值结果会被污染。
3. 特征工程与模型选型:让机器学习真正学到东西
3.1 时间特征与污染物交互特征的构造
原始数据只有时间戳和浓度值,直接丢给模型效果通常一般,因为空气质量有强时间规律:早晚高峰 PM2.5 和 NO2 升高,午后 O3 升高,冬季供暖期 PM2.5 整体偏高。把这些规律显式构造成特征,模型更容易学到。常用做法是从时间戳拆出小时、星期、月份、是否周末、是否供暖季,再对 PM2.5 和 PM10 做滞后特征,因为当前时刻的浓度和前一小时高度相关。
# 时间特征 df['hour'] = df['datetime'].dt.hour df['weekday'] = df['datetime'].dt.weekday df['month'] = df['datetime'].dt.month df['is_weekend'] = (df['weekday'] >= 5).astype(int) # 滞后特征:前一小时和前三小时的 PM2.5 df['PM2.5_lag1'] = df['PM2.5'].shift(1) df['PM2.5_lag3'] = df['PM2.5'].shift(3) # 交互特征:PM2.5 与 PM10 的比值,反映颗粒物构成 df['pm_ratio'] = df['PM2.5'] / (df['PM10'] + 1e-6) # 删除因滞后产生的空值行 df = df.dropna().reset_index(drop=True) print('特征构造后样本量:', len(df))shift(1)生成前一小时值,注意这会在第一行产生 NaN,所以最后要dropna。pm_ratio加1e-6是防止 PM10 为 0 时除零。如果做的是逐日数据,滞后特征可以改成前一天和前三天。构造完特征后建议用热力图看相关性,PM2.5 和 PM10 相关性通常超过 0.8,如果同时放进线性模型要考虑共线性,树模型则不太敏感。
3.2 回归与分类模型的选型对比
这个系统通常做两类任务:一是预测 PM2.5 或 AQI 的具体数值,属于回归;二是预测空气质量等级(优、良、轻度污染等),属于分类。回归常用线性回归、随机森林、XGBoost、LightGBM,分类常用逻辑回归、随机森林、SVM、XGBoost。选型时不要一上来就上深度学习,空气质量数据样本量通常几千到几万条,特征维度几十维,树模型在这个规模上表现稳定且调参成本低。我一般先用随机森林跑基线,再用 XGBoost 或 LightGBM 对比,如果提升不明显就保留随机森林,因为可解释性更好。
from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, r2_score import xgboost as xgb features = ['PM10', 'SO2', 'NO2', 'CO', 'O3', 'hour', 'weekday', 'month', 'is_weekend', 'PM2.5_lag1', 'PM2.5_lag3', 'pm_ratio'] 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) rf = RandomForestRegressor(n_estimators=200, max_depth=12, min_samples_leaf=3, random_state=42) rf.fit(X_train, y_train) pred_rf = rf.predict(X_test) print('RF MAE:', mean_absolute_error(y_test, pred_rf), 'R2:', r2_score(y_test, pred_rf)) xgb_model = xgb.XGBRegressor(n_estimators=300, learning_rate=0.05, max_depth=6, subsample=0.8, colsample_bytree=0.8, random_state=42) xgb_model.fit(X_train, y_train) pred_xgb = xgb_model.predict(X_test) print('XGB MAE:', mean_absolute_error(y_test, pred_xgb), 'R2:', r2_score(y_test, pred_xgb))随机森林的n_estimators=200是树的数量,太少欠拟合,太多训练慢且收益递减;max_depth=12控制单棵树深度,空气质量数据一般 8 到 15 之间比较合适;min_samples_leaf=3防止叶子节点样本过少导致过拟合。XGBoost 的learning_rate=0.05配合n_estimators=300是常见组合,学习率低就需要更多树,subsample和colsample_bytree都是 0.8,表示每棵树随机用 80% 样本和 80% 特征,增强泛化。如果 R² 低于 0.7,优先检查滞后特征是否构造正确、时间字段有没有排序错。
4. 可视化落地:用 ECharts 和 Matplotlib 把结论讲清楚
4.1 趋势图、热力图与污染物相关性矩阵
可视化不是把数据画出来就完事,是要让看图的人三秒内抓到重点。空气质量场景最常用的四类图:时间趋势折线图看 AQI 和 PM2.5 随时间变化;日历热力图看全年污染分布,一眼能看出冬季高发;相关性热力图看六项污染物之间的关联;城市或站点对比柱状图看区域差异。Python 端用 Matplotlib 或 Pyecharts 生成,前端用 ECharts 渲染,Pyecharts 的优势是直接输出 HTML,方便嵌入 Web 系统。
from pyecharts.charts import Line, HeatMap from pyecharts import options as opts import pandas as pd # 按天聚合,画 AQI 和 PM2.5 趋势 daily = df.set_index('datetime').resample('D').agg( {'AQI': 'mean', 'PM2.5': 'mean'}).dropna().reset_index() daily['date_str'] = daily['datetime'].dt.strftime('%Y-%m-%d') line = (Line() .add_xaxis(daily['date_str'].tolist()) .add_yaxis('AQI', daily['AQI'].round(1).tolist(), is_smooth=True) .add_yaxis('PM2.5', daily['PM2.5'].round(1).tolist(), is_smooth=True) .set_global_opts( title_opts=opts.TitleOpts(title='AQI 与 PM2.5 日趋势'), datazoom_opts=opts.DataZoomOpts(range_start=0, range_end=30), yaxis_opts=opts.AxisOpts(name='浓度 μg/m³'))) line.render('aqi_trend.html')resample('D')按天聚合,如果原始是逐小时数据,这一步把 24 条压成 1 条,趋势更清晰。is_smooth=True让折线平滑,但要注意平滑会掩盖极端值,做污染过程分析时建议关掉。DataZoomOpts默认只显示前 30% 数据,用户可拖动查看全时段,避免 x 轴标签挤成一团。相关性矩阵用df[['PM2.5','PM10','SO2','NO2','CO','O3']].corr()算出来,再用 HeatMap 渲染,重点看 PM2.5 和 PM10 是否高度相关,如果超过 0.9,建模时可以考虑只保留一个。
4.2 把可视化结果接进 Web 系统的三种方式
如果这套系统要交付成 Web 应用,可视化结果有三种接法:一是 Pyecharts 生成独立 HTML,用 iframe 嵌入;二是后端返回 JSON,前端 ECharts 直接渲染;三是用 Flask 或 FastAPI 做接口,前端按需请求。第一种最省事,适合课程设计快速出效果;第二种最灵活,适合需要交互筛选的场景;第三种最接近生产环境,但要多写接口和前端逻辑。我一般先用第一种把图表跑通,确认数据没问题,再改成第二种。
from flask import Flask, jsonify import pandas as pd app = Flask(__name__) @app.route('/api/trend') def trend(): daily = df.set_index('datetime').resample('D').agg( {'AQI': 'mean', 'PM2.5': 'mean'}).dropna().reset_index() result = { 'dates': daily['datetime'].dt.strftime('%Y-%m-%d').tolist(), 'aqi': daily['AQI'].round(1).tolist(), 'pm25': daily['PM2.5'].round(1).tolist() } return jsonify(result) if __name__ == '__main__': app.run(debug=True, port=5000)这个接口返回 JSON,前端拿到后传给 ECharts 的setOption。debug=True只在开发时开,部署时要关掉。如果数据量大,接口里加缓存或分页,避免每次请求都重新聚合。前端 ECharts 配置里xAxis.type设为category,yAxis.type设为value,series里两个对象分别对应 AQI 和 PM2.5,tooltip.trigger设为axis让鼠标悬停显示同一天两个值。
5. 避坑与排查:那些跑通之后才会暴露的问题
5.1 时间字段排序错误导致滞后特征失效
现象:模型 R² 看起来还行,但预测值和真实值错位,滞后特征和当前值对不上。原因:读取 CSV 后没有按时间排序就直接shift,数据行顺序是乱的,shift(1)取到的不是前一小时而是上一行。解决:在任何特征构造之前先df.sort_values('datetime').reset_index(drop=True),并且检查时间字段有没有重复值,重复时间戳会让滞后关系错乱。
5.2 缺失值插值把整段缺失填成直线
现象:趋势图上某段时间 PM2.5 是一条完美直线,明显不真实。原因:interpolate没有限制limit,连续缺失几十个点被线性填充,两端值一拉就成直线。解决:设置limit=6或更小,超过限制的保留 NaN,后续建模时要么删掉这些行,要么用模型预测填补。如果缺失段正好是污染过程,插值会严重低估峰值。
5.3 训练集和测试集随机划分导致时间泄漏
现象:测试集 R² 高得离谱,换一批数据就崩。原因:用train_test_split随机划分,测试集里的时间点可能夹在训练集中间,滞后特征把未来信息泄漏给了训练。解决:时间序列数据要按时间切分,比如前 80% 做训练、后 20% 做测试,或者用TimeSeriesSplit做交叉验证。这个坑在课程设计里特别常见,答辩时被问到很难解释。
5.4 可视化图表中文乱码
现象:Matplotlib 生成的图表标题和坐标轴中文显示成方框。原因:默认字体不支持中文。解决:在绘图前设置plt.rcParams['font.sans-serif'] = ['SimHei']和plt.rcParams['axes.unicode_minus'] = False。Pyecharts 和 ECharts 一般没这个问题,因为渲染在浏览器里,字体由系统提供。如果服务器端生成图片,要确认服务器装了中文字体。
5.5 模型特征重要性高但业务上说不通
现象:XGBoost 输出特征重要性,排第一的是一个看起来无关的字段。原因:可能是该字段和标签存在间接泄漏,比如用 AQI 的某个分项去预测 AQI,或者用了未来才会知道的字段。解决:逐个检查高重要性特征的业务含义,确认它在预测时刻是否真的可得。空气质量预测里,当前时刻的 PM2.5 对预测下一时刻有用,但当前时刻的 AQI 等级如果是由 PM2.5 算出来的,拿它预测 PM2.5 就是循环论证。
6. 进阶技巧:用 SHAP 解释模型并做特征筛选
模型跑通之后,真正拉开差距的是能不能解释「为什么预测这个值」。SHAP 是目前解释树模型最实用的工具,它能给出每个样本每个特征的贡献值,既能做全局特征重要性,也能看单个预测的归因。我一般会先用 SHAP 做一轮特征筛选,把贡献接近零的特征去掉,再重新训练,往往能在保持精度的同时减少特征数量,推理速度也更快。
import shap # 用训练好的 XGBoost 模型做 SHAP 解释 explainer = shap.TreeExplainer(xgb_model) shap_values = explainer.shap_values(X_test) # 全局特征重要性 shap.summary_plot(shap_values, X_test, plot_type='bar') # 单个样本归因,看第 0 个测试样本各特征贡献 shap.force_plot(explainer.expected_value, shap_values[0], X_test.iloc[0], matplotlib=True)TreeExplainer对 XGBoost、LightGBM、随机森林都适用,计算速度比 KernelExplainer 快很多。summary_plot的 bar 图按平均绝对 SHAP 值排序,能直接看出哪些特征重要。force_plot看单个样本,红色推高预测值,蓝色拉低,能解释为什么这个时刻预测 PM2.5 偏高。如果发现滞后特征 SHAP 值远高于其他特征,说明模型主要靠自相关预测,这时候要考虑加入更多气象特征(温度、湿度、风速、气压)来提升泛化能力,因为纯靠滞后特征在污染过程突变时容易失效。
特征筛选的具体做法:跑完 SHAP 后,把平均绝对 SHAP 值低于阈值的特征列出来,逐个删掉再训练,观察验证集 MAE 变化。如果删掉后 MAE 上升不超过 2%,就保留删除,模型更简洁。我自己的习惯是每轮只删一个特征,避免一次性删太多导致性能骤降后无法定位原因。这套流程走下来,一个能跑通、能解释、能交付的空气质量分析系统基本就成型了。希望帮到你。
本文还有配套的精品资源,点击获取