简介:这份资源面向环境科学、数据挖掘与机器学习方向的学习者和研究者,提供一套基于Python的空气质量数据可视化分析系统源码及配套数据。项目采用BS架构,前端整合HTML、CSS、JavaScript与D3、ECharts、Mapbox等可视化库,后端基于Python与Flask构建,核心思路是先挖掘空气质量数据的时空特征,通过降维聚类完成城市群划分,再借助粒子输运与相关分析构建大气污染传输网络,并利用多维空间变换的渐进式探索方法分析污染传播过程。压缩包共2000个文件,以1295个json数据文件和697个py脚本为主,另含少量txt、xml与md说明文档,整体约76.43MB,其中files目录存放原始数据与挖掘结果,templates与static分别承载前端入口和静态资源。目前已有252人学习,适合希望掌握空气质量分析全流程、复用可视化方案或开展相关课题研究的读者参考。
1. 空气质量数据挖掘系统:从原始 CSV 到可解释预测模型
很多人第一次拿到空气质量数据集,第一反应是直接df.describe()然后画个折线图交差。但真正做过这类项目的人都知道,空气质量数据是典型的"脏数据重灾区"——传感器漂移、缺失值成片、PM2.5 和 PM10 高度共线、季节性和城市差异交织在一起。如果跳过数据挖掘直接上机器学习模型,R² 看着漂亮,一到真实预测就翻车。
这个标题讲的事情很具体:用 Python 把一份空气质量原始数据,经过数据挖掘的清洗与特征工程,再用机器学习建模,最后用可视化把结论讲清楚。它适合两类人:一类是正在做课程设计或毕业设计的学生,需要一套能跑通、能解释、能答辩的完整链路;另一类是刚转数据分析的工程师,想找一个真实场景把 pandas、sklearn、ECharts 串起来。核心不是模型多复杂,而是每一步为什么这么做、参数怎么定、哪里容易踩坑。
2. 数据挖掘前的准备:环境、数据字段与清洗策略
2.1 环境搭建与依赖版本选择
做空气质量分析,环境不需要多花哨,但版本冲突是新手第一个坑。我一般用 conda 建独立环境,避免和系统 Python 打架。核心依赖就几个:pandas 做数据挖掘、scikit-learn 做机器学习、matplotlib 和 pyecharts 做可视化、missingno 看缺失分布。
# 创建独立环境,Python 3.10 兼容性最稳 conda create -n air_quality python=3.10 -y conda activate air_quality # 核心依赖,指定版本区间避免 API 变动 pip install pandas==2.0.3 numpy==1.24.3 pip install scikit-learn==1.3.0 pip install matplotlib==3.7.2 pyecharts==2.0.3 pip install missingno==0.5.2 openpyxl==3.1.2这里解释几个参数选择。pandas 锁 2.0.x 是因为 2.1 之后fillna的method参数被移除,很多老教程代码会直接报错。scikit-learn 1.3 的HistGradientBoostingRegressor已经支持原生缺失值,这对空气质量数据很关键。pyecharts 2.x 和 1.x 的 API 差异很大,网上大量 ECharts 可视化代码是 1.x 写法,混用会出问题。
提示:如果你用 VSCode 或 PyCharm,记得把解释器切到刚建的
air_quality环境,否则 pip 装完 import 还是找不到。
2.2 空气质量数据的典型字段与质量问题
一份标准的空气质量数据集,通常包含这些字段:城市、监测站点、日期时间、AQI、PM2.5、PM10、SO2、NO2、CO、O3,以及温度、湿度、风速、气压等气象列。数据挖掘阶段要盯住四类问题。
第一类是缺失值。传感器故障会导致某个污染物整段缺失,气象数据则常见单点缺失。第二类是异常值,比如 PM2.5 出现 9999 这种标记值,或者 CO 浓度突然为负。第三类是量纲不统一,AQI 是 0-500 的指数,PM2.5 是 μg/m³,直接放一起建模会放大数值大的特征。第四类是时间粒度不一致,有的数据是小时级,有的是日级,合并前必须对齐。
import pandas as pd import numpy as np import missingno as msno # 读取数据,注意编码,国内数据常见 gbk df = pd.read_csv("air_quality.csv", encoding="gbk", parse_dates=["datetime"]) # 统一列名,去掉空格和大小写差异 df.columns = [c.strip().lower().replace(" ", "_") for c in df.columns] # 把明显是标记值的异常替换成 NaN df.replace([9999, -999, -1], np.nan, inplace=True) # 看缺失分布,missingno 的矩阵图比 isnull().sum() 直观 print(df.isnull().sum().sort_values(ascending=False)) msno.matrix(df)这段代码的逻辑是:先统一列名,避免后面df['PM2.5']和df['pm2.5']混用;再把传感器标记值转成 NaN,让它们进入统一的缺失处理流程;最后用 missingno 看缺失是随机的还是成片的。如果某个污染物列缺失超过 40%,我一般直接考虑丢弃该列而不是硬填,因为填出来的数据会污染模型。
2.3 缺失值填充与异常值处理的具体参数
缺失值填充没有万能方案,要看缺失比例和业务含义。缺失小于 5%,用前后向填充或均值填充都行;缺失在 5% 到 30% 之间,我倾向用同城市同时段的中位数填充,因为空气质量有强时段规律;缺失超过 30%,要么丢列,要么用模型预测填充,但后者容易引入偏差。
# 按城市分组,用同时段中位数填充污染物列 pollutants = ["pm2.5", "pm10", "so2", "no2", "co", "o3"] df[pollutants] = df.groupby("city")[pollutants].transform( lambda x: x.fillna(x.median()) ) # 气象列用线性插值,因为温度湿度是连续变化的 weather = ["temperature", "humidity", "wind_speed", "pressure"] df[weather] = df[weather].interpolate(method="linear", limit=3) # 异常值用 IQR 法识别,超过 3 倍四分位距的截断 for col in pollutants: q1, q3 = df[col].quantile([0.25, 0.75]) iqr = q3 - q1 lower, upper = q1 - 3 * iqr, q3 + 3 * iqr df[col] = df[col].clip(lower, upper)groupby("city")是关键,因为不同城市的污染基线差异巨大,用全国中位数填北京的数据会失真。interpolate的limit=3表示最多连续插 3 个点,超过就留 NaN,防止长段缺失被线性外推成假数据。IQR 用 3 倍而不是常见的 1.5 倍,是因为空气质量本身波动大,1.5 倍会把真实的重污染事件当异常删掉,这属于业务判断,不是纯统计问题。
3. 特征工程与机器学习建模:从时间特征到模型选型
3.1 时间特征与滞后特征的构造方法
空气质量预测的核心特征是时间。原始数据只有一列 datetime,但里面藏着大量信息:小时反映早晚高峰,星期反映工作日和周末差异,月份反映季节供暖。把这些拆出来,模型效果通常能提升一截。
# 时间特征拆解 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) # 滞后特征:用前 1、3、6、12 小时的 PM2.5 预测当前值 for lag in [1, 3, 6, 12]: df[f"pm25_lag_{lag}"] = df.groupby("city")["pm2.5"].shift(lag) # 滑动窗口统计 df["pm25_roll_mean_6"] = df.groupby("city")["pm2.5"].transform( lambda x: x.rolling(6, min_periods=1).mean() ) df["pm25_roll_std_6"] = df.groupby("city")["pm2.5"].transform( lambda x: x.rolling(6, min_periods=1).std() )滞后特征必须按城市分组做shift,否则会把 A 城市的数据错位到 B 城市。rolling(6)表示 6 小时窗口,min_periods=1保证序列开头不会因为窗口不满而全变 NaN。滑动标准差反映的是污染波动剧烈程度,重污染过程往往标准差先升高,这个特征对预测突变很有用。
3.2 模型选型:为什么用梯度提升而不是线性回归
空气质量预测是典型的非线性回归问题。PM2.5 和气象因子之间不是线性关系,比如风速对污染物的稀释效应在低风速区间很弱,高风速区间很强。线性回归在这种场景下 R² 通常只有 0.5 左右,而梯度提升树能到 0.85 以上。
我一般先用HistGradientBoostingRegressor做基线,它训练快、支持缺失值、不需要独热编码。如果数据量超过 10 万行,再考虑 LightGBM。随机森林也能用,但预测精度通常比梯度提升低 3 到 5 个百分点,而且模型文件更大。
from sklearn.model_selection import train_test_split from sklearn.ensemble import HistGradientBoostingRegressor from sklearn.metrics import mean_absolute_error, r2_score # 特征列,去掉目标泄漏列和原始时间列 feature_cols = [c for c in df.columns if c not in ["pm2.5", "datetime", "aqi"]] X = df[feature_cols] y = df["pm2.5"] # 按时间顺序切分,不能随机切,否则未来数据泄漏到训练集 split_idx = int(len(df) * 0.8) X_train, X_test = X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_test = y.iloc[:split_idx], y.iloc[split_idx:] model = HistGradientBoostingRegressor( max_iter=300, learning_rate=0.05, max_depth=6, early_stopping=True, validation_fraction=0.1, random_state=42 ) model.fit(X_train, y_train) pred = model.predict(X_test) print("MAE:", mean_absolute_error(y_test, pred)) print("R2:", r2_score(y_test, pred))max_iter=300配合early_stopping=True,模型会在验证集不再提升时自动停,避免过拟合。learning_rate=0.05是偏保守的学习率,配合 300 轮迭代通常够用,如果 R² 不理想可以调到 0.1 但要把 max_iter 降到 150 左右。按时间顺序切分是血泪经验,随机切分会让模型"看到未来",测试集 R² 虚高,上线就崩。
3.3 特征重要性与模型可解释性
模型跑通只是第一步,能解释才敢用。梯度提升树自带feature_importances_,但更可靠的是 permutation importance,它不依赖模型内部结构。
from sklearn.inspection import permutation_importance result = permutation_importance( model, X_test, y_test, n_repeats=10, random_state=42, n_jobs=-1 ) importance_df = pd.DataFrame({ "feature": feature_cols, "importance": result.importances_mean }).sort_values("importance", ascending=False) print(importance_df.head(10))n_repeats=10表示每个特征打乱 10 次取平均,次数越多越稳但越慢。通常滞后 1 小时的 PM2.5 会排第一,其次是滑动均值,然后才是气象因子。如果发现某个气象特征重要性异常高,要检查是不是数据泄漏,比如用了未来时刻的温度。
4. 可视化落地:用 ECharts 把分析结论讲清楚
4.1 时间序列趋势图与污染日历热力图
可视化不是把数据画出来就完事,而是要让不看代码的人也能秒懂结论。时间序列趋势图适合展示长期变化,日历热力图适合展示"哪天污染重"这种模式。
from pyecharts.charts import Line, Calendar, Grid from pyecharts import options as opts # 按日聚合,避免小时级数据太密看不清趋势 daily = df.groupby(df["datetime"].dt.date)["pm2.5"].mean().reset_index() daily.columns = ["date", "pm25"] line = ( Line() .add_xaxis([str(d) for d in daily["date"]]) .add_yaxis("PM2.5 日均值", daily["pm25"].round(1).tolist(), is_smooth=True, is_symbol_show=False) .set_global_opts( title_opts=opts.TitleOpts(title="PM2.5 日均浓度趋势"), xaxis_opts=opts.AxisOpts(name="日期"), yaxis_opts=opts.AxisOpts(name="μg/m³"), datazoom_opts=[opts.DataZoomOpts(range_start=0, range_end=100)] ) ) line.render("pm25_trend.html")is_smooth=True让折线平滑,但要注意平滑会掩盖极端值,如果重点是看峰值就别开。DataZoomOpts加上后可以在浏览器里拖动缩放,这对长时间序列很实用。日历热力图用Calendar组件,把每天的值映射成颜色深浅,一眼就能看出冬季供暖期的污染聚集。
4.2 相关性热力图与模型预测对比图
相关性热力图用来验证特征工程是否合理,预测对比图用来展示模型效果。这两张图放在报告里,比一堆数字有说服力。
from pyecharts.charts import HeatMap import numpy as np # 计算污染物和气象因子的相关系数 corr_cols = ["pm2.5", "pm10", "so2", "no2", "co", "o3", "temperature", "humidity", "wind_speed", "pressure"] corr_matrix = df[corr_cols].corr().round(2) data = [] for i, row in enumerate(corr_cols): for j, col in enumerate(corr_cols): data.append([i, j, corr_matrix.loc[row, col]]) heatmap = ( HeatMap() .add_xaxis(corr_cols) .add_yaxis("", corr_cols, data, label_opts=opts.LabelOpts(is_show=True, position="inside")) .set_global_opts( title_opts=opts.TitleOpts(title="污染物与气象因子相关性"), visualmap_opts=opts.VisualMapOpts(min_=-1, max_=1, is_calculable=True, pos_bottom="5%") ) ) heatmap.render("correlation.html")visualmap_opts的min_=-1, max_=1是相关系数的固定范围,不要根据数据自动调整,否则不同图之间没法对比。如果发现 PM2.5 和 PM10 相关系数超过 0.9,说明两者高度共线,建模时可以只保留一个,或者用 PCA 降维。
5. 避坑与排查:空气质量数据挖掘最常见的 5 个翻车点
5.1 缺失值填充后模型指标虚高
现象:填充完缺失值,模型 R² 从 0.7 跳到 0.95,但预测新数据时误差巨大。
原因:用了全局均值或未来数据填充,导致训练集和测试集信息泄漏。比如用整列均值填充,测试集的缺失值被训练集的统计量"污染"。
解决:填充必须按时间顺序或分组进行,测试集的填充参数只能来自训练集。我一般先切分再填充,训练集算中位数,测试集用训练集的中位数。
5.2 时间特征构造时忘记按城市分组
现象:滞后特征和滑动窗口算出来,模型效果反而比不加还差。
原因:shift和rolling没有groupby("city"),导致 A 城市的最后一条数据滑到了 B 城市的第一条,特征完全错位。
解决:所有涉及时间顺序的操作,只要数据里有多个城市或多个站点,必须分组。检查方法是看分组后每个城市的第一行滞后特征是否为 NaN。
5.3 随机切分导致测试集泄漏
现象:交叉验证 R² 很高,但按时间顺序留出的测试集 R² 低很多。
原因:train_test_split默认随机打乱,未来时刻的数据进了训练集,模型"偷看"了答案。
解决:时间序列必须按时间切分,用iloc[:split_idx]和iloc[split_idx:]。如果数据有多个城市,还要保证每个城市的切分点一致。
5.4 异常值处理把重污染事件删了
现象:清洗后数据很"干净",但模型完全预测不出重污染过程。
原因:IQR 用 1.5 倍系数,把 PM2.5 超过 150 的真实重污染值当异常截断了。
解决:空气质量数据的异常值判断要结合业务,用 3 倍 IQR 或直接保留,只处理负值和 9999 这类标记值。重污染事件恰恰是预测的重点,不能删。
5.5 ECharts 图表在浏览器打不开或空白
现象:render生成的 HTML 打开后一片空白,或者图表不显示。
原因:pyecharts 2.x 默认从 CDN 加载 echarts.min.js,内网或断网环境加载失败。另外,如果数据里有 NaN,JSON 序列化会失败。
解决:用CurrentConfig.ONLINE_HOST改成离线资源,或者把 echarts.min.js 下载到本地。数据里的 NaN 在传给图表前用fillna(0)或dropna()处理掉。
6. 把模型装进 Flask 接口:一个能演示的最小闭环
做到这一步,分析和可视化都有了,但答辩或演示时,评委更想看到"输入一组数据,系统给出预测"的交互。用 Flask 包一个预测接口,前端用 ECharts 展示结果,整个项目就从脚本升级成了系统。
from flask import Flask, request, jsonify import joblib import pandas as pd app = Flask(__name__) model = joblib.load("air_quality_model.pkl") feature_cols = joblib.load("feature_cols.pkl") @app.route("/predict", methods=["POST"]) def predict(): data = request.get_json() # 构造单行 DataFrame,列顺序必须和训练时一致 row = pd.DataFrame([data])[feature_cols] pred = model.predict(row)[0] return jsonify({ "pm25_pred": round(float(pred), 2), "level": "优" if pred <= 35 else "良" if pred <= 75 else "污染" }) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=False)这里的关键是feature_cols要单独保存,预测时按同样顺序取列,否则 sklearn 会报特征数量不匹配。debug=False在生产环境必须关,否则会暴露源码。前端用fetch调这个接口,把返回的 PM2.5 预测值和等级用 ECharts 仪表盘展示,整个演示闭环就完成了。
模型保存用joblib.dump(model, "air_quality_model.pkl"),比 pickle 快且对 numpy 数组更友好。如果要部署到服务器,用 gunicorn 起多进程:gunicorn -w 4 -b 0.0.0.0:5000 app:app,-w 4表示 4 个 worker,按 CPU 核数调整。
我自己的习惯是,每次改完特征工程或模型参数,先把 MAE 和 R² 记在一个表格里,对比着看。有次调了半天参数,R² 只涨了 0.01,回头一看是某个滞后特征分组写错了,修正后直接涨了 0.08。这种教训比任何教程都深刻。希望帮到你。
本文还有配套的精品资源,点击获取