news 2026/9/24 18:03:50

Python空气质量数据挖掘与可视化分析系统实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python空气质量数据挖掘与可视化分析系统实战

简介:本资源面向环境科学、数据挖掘与机器学习方向的学习者与研究者,提供一套基于Python的空气质量数据可视化分析系统源码及配套数据,可用于城市群划分、污染传输网络构建与传播过程探索等课题实践。压缩包共约2000个文件,以1295个json数据文件和697个py脚本为主,另含少量txt、xml与md说明文档,整体约76.43MB;json文件承载原始及挖掘后的空气质量数据,py脚本覆盖后端服务、数据管理与课题测试逻辑。系统采用BS架构,前端整合HTML、CSS、JavaScript及D3、ECharts、Mapbox等可视化库,后端基于Python与Flask构建,运行main.py即可启动服务,dataManager.py负责数据模块,files与templates目录分别存放数据与前端入口页面。目前已有252人学习下载,适合希望掌握降维聚类、粒子输运与相关分析、多维空间变换渐进式探索等方法的读者参考复用。

1. 空气质量数据挖掘系统:从一份 CSV 到能跑通的可视化分析链路

手里拿到一份空气质量历史数据,字段有 PM2.5、PM10、SO2、NO2、CO、O3 以及对应的 AQI 和城市站点信息,很多人第一反应是直接df.describe()看一眼然后画个折线图交差。但真正做过这类项目的人知道,空气质量数据挖掘的难点从来不在画图,而在数据本身——缺失值成片出现、时间粒度不统一、污染物量纲差异巨大、AQI 与分指数之间的换算关系容易搞错。这套「Python 基于数据挖掘与机器学习的空气质量数据可视化分析系统」要解决的,就是把原始监测数据变成可解释、可预测、可展示的完整链路。它适合有 Python 基础、想做一个能写进简历或课程设计的完整项目的同学,也适合需要快速搭建环境监测分析原型的从业者。下面按数据清洗、特征工程、模型训练、可视化呈现的顺序,把每个环节的参数和坑讲清楚。

2. 数据清洗与特征工程:空气质量原始表的六个处理动作

2.1 先搞清楚数据长什么样再动手

拿到数据第一步不是写清洗代码,而是用 pandas 把结构摸清楚。典型的空气质量数据集通常包含datehourcitystationPM2.5PM10SO2NO2COO3AQIquality_level这些列。不同来源的数据列名可能带中文、带空格、带括号单位,比如PM2.5(ug/m3)这种,直接按列名索引会翻车。

import pandas as pd import numpy as np # 读取时统一处理列名,去掉空格和单位括号 df = pd.read_csv("air_quality.csv", encoding="utf-8") df.columns = [c.strip().replace(" ", "_") for c in df.columns] # 打印基本信息,确认行列数和类型 print(df.shape) print(df.dtypes) print(df.head(10)) print(df.isnull().sum())

这段代码做了三件事:去列名空格、看数据类型、统计每列缺失值。df.dtypes里如果PM2.5显示为object而不是float64,说明列里混了非数字字符,比如"--""NA",需要在read_csv时加na_values=["--", "NA", "null", ""]。缺失值统计是后续决策的依据——如果某列缺失超过 40%,考虑直接丢弃该列;如果缺失在 5% 到 20% 之间,用插值或前向填充;低于 5% 可以删行。

2.2 时间字段解析与重采样

空气质量数据的时间列经常是"2023/1/1 0:00"这种格式,pandas 默认解析可能失败。用pd.to_datetime显式指定格式,解析失败的行用errors="coerce"标记出来再处理。

# 解析时间列,兼容多种格式 df["datetime"] = pd.to_datetime(df["date"], format="%Y/%m/%d %H:%M", errors="coerce") # 检查解析失败的行 bad_time = df[df["datetime"].isnull()] print(f"时间解析失败行数: {len(bad_time)}") # 设为索引并按小时重采样,取均值 df = df.set_index("datetime").sort_index() df_hourly = df.resample("1h").mean(numeric_only=True) # 对重采样后的缺失值做线性插值,限制最大连续插值长度为3 df_hourly = df_hourly.interpolate(method="linear", limit=3)

resample("1h")把不规则时间戳对齐到整点,mean(numeric_only=True)避免对非数值列求均值报错。interpolatelimit=3是关键参数——如果连续缺失超过 3 小时,插值结果不可信,应该保留 NaN 让后续模型自己处理。这一步做完,数据从原始表变成了规整的时间序列。

2.3 污染物特征构造:从原始浓度到可建模特征

原始浓度值直接喂给模型效果一般,因为空气质量有明显的周期性和滞后效应。需要构造几类特征:时间特征(小时、星期、月份)、滞后特征(前 1 小时、前 3 小时、前 24 小时的浓度)、滚动统计(过去 6 小时均值、标准差)、以及 AQI 分指数换算。

# 时间特征 df_hourly["hour"] = df_hourly.index.hour df_hourly["weekday"] = df_hourly.index.weekday df_hourly["month"] = df_hourly.index.month # 滞后特征:PM2.5 的前1、3、24小时值 for lag in [1, 3, 24]: df_hourly[f"PM25_lag{lag}"] = df_hourly["PM2.5"].shift(lag) # 滚动统计:过去6小时均值和标准差 df_hourly["PM25_roll6_mean"] = df_hourly["PM2.5"].rolling(6).mean() df_hourly["PM25_roll6_std"] = df_hourly["PM2.5"].rolling(6).std() # 丢弃因 shift 和 rolling 产生的 NaN 行 df_model = df_hourly.dropna() print(f"建模数据形状: {df_model.shape}")

shift(lag)把过去的值挪到当前行,rolling(6)计算滑动窗口统计。注意rolling默认从窗口填满才开始计算,所以前 5 行是 NaN,dropna()会一并清掉。如果数据量本身不大,丢弃这些行可能损失 10% 到 15% 的样本,这时候可以考虑用min_periods=1让窗口不满也计算,但统计量的稳定性会下降。我一般会在数据量充足时直接dropna(),数据量紧张时才用min_periods

2.4 AQI 换算的常见错误

很多教程直接用AQI列作为标签训练模型,但 AQI 本身是由六项污染物分指数取最大值得到的,直接预测 AQI 会引入信息泄漏——因为输入特征里包含了计算 AQI 的原始浓度。正确做法是预测某一项具体污染物浓度(比如 PM2.5),或者预测 AQI 等级(优、良、轻度污染等分类标签)。

# 按 AQI 数值划分等级标签 def aqi_to_level(aqi): if aqi <= 50: return 0 # 优 elif aqi <= 100: return 1 # 良 elif aqi <= 150: return 2 # 轻度污染 elif aqi <= 200: return 3 # 中度污染 else: return 4 # 重度及以上 df_model["AQI_level"] = df_model["AQI"].apply(aqi_to_level) print(df_model["AQI_level"].value_counts())

这段代码把连续 AQI 转成 5 分类标签。value_counts()用来检查类别是否均衡——如果「优」占了 70%,模型会倾向于全预测「优」,需要过采样或调整类别权重。这一步是很多课程设计里被忽略的,但恰恰是模型能不能用的分水岭。

3. 机器学习建模:从线性回归到 XGBoost 的选型与调参

3.1 为什么空气质量预测首选树模型

空气质量数据有几个特点:特征之间非线性关系强(比如温度和 O3 的关系是 U 型)、存在异常值(沙尘暴、烟花燃放)、特征量纲差异大。线性回归在这类数据上 R² 通常只有 0.3 到 0.5,而梯度提升树能到 0.7 以上。常见做法是用RandomForestRegressorXGBRegressor做基线,再用GridSearchCV调参。

from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, r2_score # 特征列和标签列 feature_cols = [c for c in df_model.columns if c not in ["AQI", "AQI_level", "quality_level"]] X = df_model[feature_cols] y = df_model["PM2.5"] # 按时间顺序切分,不能随机打乱 split_idx = int(len(X) * 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:] # 随机森林基线 rf = RandomForestRegressor(n_estimators=200, max_depth=12, random_state=42, n_jobs=-1) rf.fit(X_train, y_train) y_pred = rf.predict(X_test) print(f"MAE: {mean_absolute_error(y_test, y_pred):.2f}") print(f"R2: {r2_score(y_test, y_pred):.4f}")

时间序列数据必须按时间切分,不能train_test_split(shuffle=True),否则未来信息会泄漏到训练集。n_estimators=200是树的数量,太少欠拟合,太多训练慢且收益递减;max_depth=12控制单棵树深度,空气质量数据一般 8 到 15 之间比较合适。n_jobs=-1用满所有 CPU 核心加速训练。

3.2 XGBoost 的关键参数怎么设

XGBoost 在空气质量预测上通常比随机森林好 3 到 5 个百分点,但参数更多。核心参数就四个:learning_ratemax_depthsubsamplecolsample_bytree

import xgboost as xgb xgb_model = xgb.XGBRegressor( n_estimators=500, learning_rate=0.05, max_depth=6, subsample=0.8, colsample_bytree=0.8, reg_alpha=0.1, reg_lambda=1.0, random_state=42, early_stopping_rounds=50, eval_metric="mae" ) xgb_model.fit( X_train, y_train, eval_set=[(X_test, y_test)], verbose=False ) y_pred_xgb = xgb_model.predict(X_test) print(f"XGBoost MAE: {mean_absolute_error(y_test, y_pred_xgb):.2f}") print(f"XGBoost R2: {r2_score(y_test, y_pred_xgb):.4f}")

learning_rate=0.05配合n_estimators=500是经典组合,学习率低需要更多树来补偿。max_depth=6比随机森林浅,因为 boosting 是串行叠加,单棵树太深容易过拟合。subsample=0.8colsample_bytree=0.8引入随机性防止过拟合。early_stopping_rounds=50表示验证集指标 50 轮不提升就停,避免无效训练。reg_alphareg_lambda是 L1 和 L2 正则化,数据噪声大时适当调高。

3.3 特征重要性分析与模型解释

训练完模型不能只看 R²,还要看哪些特征真正起了作用。树模型自带feature_importances_,但更可靠的是用 SHAP 值做解释。

import shap # 用 SHAP 解释 XGBoost 模型 explainer = shap.TreeExplainer(xgb_model) shap_values = explainer.shap_values(X_test) # 输出特征重要性排序 shap.summary_plot(shap_values, X_test, plot_type="bar", show=False) import matplotlib.pyplot as plt plt.tight_layout() plt.savefig("shap_importance.png", dpi=150)

SHAP 值反映每个特征对单次预测的贡献。如果PM25_lag1的 SHAP 值最大,说明前一小时浓度是预测当前浓度的最强信号,这符合空气污染的累积效应。如果hour的 SHAP 值呈现早晚高峰模式,说明交通排放对 PM2.5 有显著影响。这些结论比单纯看 R² 更有业务价值,也是课程设计答辩时能讲出东西的地方。

4. 可视化呈现:用 Pyecharts 做可交互的空气质量大屏

4.1 为什么选 Pyecharts 而不是 Matplotlib

Matplotlib 适合静态图,但空气质量分析系统通常需要交互——鼠标悬停看具体数值、时间轴缩放、地图联动。Pyecharts 基于 ECharts,生成的 HTML 文件可以直接在浏览器打开,支持缩放、筛选、导出图片。对于课程设计或内部演示,Pyecharts 的视觉效果和交互能力明显更占优势。

from pyecharts.charts import Line, Grid from pyecharts import options as opts # 取最近 7 天数据 df_plot = df_model.tail(24 * 7) line = ( Line() .add_xaxis([str(t) for t in df_plot.index]) .add_yaxis( "PM2.5", df_plot["PM2.5"].round(1).tolist(), is_smooth=True, linestyle_opts=opts.LineStyleOpts(width=2), label_opts=opts.LabelOpts(is_show=False) ) .add_yaxis( "PM10", df_plot["PM10"].round(1).tolist(), is_smooth=True, label_opts=opts.LabelOpts(is_show=False) ) .set_global_opts( title_opts=opts.TitleOpts(title="近7天PM2.5与PM10趋势"), xaxis_opts=opts.AxisOpts(name="时间"), yaxis_opts=opts.AxisOpts(name="浓度 ug/m3"), datazoom_opts=opts.DataZoomOpts(range_start=0, range_end=100), tooltip_opts=opts.TooltipOpts(trigger="axis") ) ) line.render("pm_trend.html")

is_smooth=True让折线平滑,datazoom_opts开启底部缩放条,tooltip_opts(trigger="axis")让鼠标悬停时显示该时间点所有系列的值。render生成独立 HTML 文件,不依赖服务器。如果要在 Jupyter 里直接显示,用line.render_notebook()

4.2 城市空气质量地图与热力图

如果数据包含多个城市,可以用 Geo 地图展示 AQI 分布。Pyecharts 的Map组件支持按省份或城市着色。

from pyecharts.charts import Map # 按城市聚合平均 AQI city_aqi = df_model.groupby("city")["AQI"].mean().round(0).to_dict() map_chart = ( Map() .add("平均AQI", list(city_aqi.items()), "china") .set_global_opts( title_opts=opts.TitleOpts(title="各城市平均AQI分布"), visualmap_opts=opts.VisualMapOpts( min_=0, max_=300, range_color=["#50a3ba", "#eac763", "#d94e5d"] ) ) ) map_chart.render("aqi_map.html")

visualmap_optsrange_color定义颜色梯度,从蓝到黄到红对应空气质量从优到重度污染。min_max_根据实际数据范围调整,如果最大值只有 200,设 300 会导致颜色偏淡。城市名要和 Pyecharts 内置的 GeoJSON 匹配,比如「北京市」要写成「北京」,否则地图上不显示。

4.3 把多个图表拼成一张大屏

单独一张折线图或地图撑不起「可视化分析系统」的场面,需要把趋势图、地图、分类饼图、特征重要性图拼到一个页面。Pyecharts 提供PageGrid两种布局方式。

from pyecharts.charts import Page, Pie # 空气质量等级分布饼图 level_counts = df_model["AQI_level"].value_counts().sort_index() level_names = ["优", "良", "轻度污染", "中度污染", "重度及以上"] pie = ( Pie() .add("", [list(z) for z in zip(level_names, level_counts.tolist())]) .set_global_opts(title_opts=opts.TitleOpts(title="空气质量等级分布")) .set_series_opts(label_opts=opts.LabelOpts(formatter="{b}: {d}%")) ) # 用 Page 组合所有图表 page = Page(layout=Page.DraggablePageLayout) page.add(line, map_chart, pie) page.render("air_quality_dashboard.html")

Page.DraggablePageLayout允许在浏览器里拖拽调整图表位置和大小,调整完点保存会生成一个 JSON 布局文件,下次加载时用page.save_resize_html()固定布局。这个功能在演示时很实用,不用反复改代码调位置。

5. 避坑与排查:空气质量分析项目里最容易翻车的五个地方

5.1 缺失值填充后 R² 虚高

现象:用均值填充缺失值后,模型 R² 达到 0.95 以上,但换一批数据预测效果骤降。

原因:均值填充抹平了数据的方差,模型学到的是填充后的「假模式」,不是真实规律。尤其是当缺失值集中在某些时段(比如夜间监测设备维护),均值填充会引入系统性偏差。

解决:优先用时间序列插值(interpolate(method="time")),其次用前向填充,实在不行再考虑均值填充。填充后一定要在验证集上评估,不能只看训练集指标。如果缺失比例超过 20%,考虑把「是否缺失」作为一个二值特征加入模型。

5.2 时间切分时用了随机打乱

现象:交叉验证 R² 很高,但按时间顺序预测未来数据时效果很差。

原因train_test_split(shuffle=True)把未来数据混入了训练集,模型「偷看」了未来信息。空气质量数据有强自相关性,打乱后相邻时间点的特征和标签同时出现在训练集和测试集,造成信息泄漏。

解决:始终按时间顺序切分,训练集在前、测试集在后。如果要做交叉验证,用TimeSeriesSplit而不是KFoldTimeSeriesSplit(n_splits=5)保证每次验证集都在训练集之后。

5.3 Pyecharts 地图城市名不匹配

现象:地图渲染出来是空白的,或者只有底图没有数据着色。

原因:Pyecharts 内置的 GeoJSON 对城市名有固定格式,比如「北京」「上海」「广州」,不能带「市」字。如果数据里是「北京市」,地图上找不到对应区域。

解决:在聚合前统一去掉「市」「省」「自治区」等后缀。用df["city"] = df["city"].str.replace("市$", "", regex=True)处理。如果还是不行,检查 Pyecharts 版本,旧版本的地图数据可能不完整,升级到最新版。

5.4 XGBoost 早停不生效

现象:设置了early_stopping_rounds=50,但模型还是跑满了n_estimators=500轮。

原因eval_set没有传,或者eval_metric和早停监控的指标不一致。XGBoost 的早停依赖验证集上的评估指标,没有验证集就不会触发。

解决:确保fit时传了eval_set=[(X_test, y_test)],并且eval_metric和早停监控的指标一致。如果用的是XGBRegressor的 sklearn 接口,early_stopping_rounds要放在构造函数里而不是fit里。训练完用xgb_model.best_iteration查看实际用了多少棵树。

5.5 可视化页面数据量过大导致卡顿

现象:折线图渲染了几万个点,浏览器打开后卡死或滚动卡顿。

原因:Pyecharts 生成的 HTML 把每个数据点都写进了 JavaScript 数组,数据量超过 5000 个点时渲染压力很大。

解决:对长时间序列做降采样,比如按小时聚合后只取最近 30 天,或者用datazoom_opts默认只显示一部分。如果必须展示全量数据,考虑用Linesampling参数,或者改用Scatter配合large=True模式。我一般会在可视化前先df_plot = df_model.resample("6h").mean()降采样,趋势图用 6 小时粒度完全够看。

6. 进阶技巧:用 Prophet 做空气质量趋势分解与异常检测

前面讲的树模型擅长预测,但不擅长解释趋势。Prophet 是 Facebook 开源的时间序列分解工具,能把 PM2.5 序列拆成趋势项、周期项和残差项,残差超过阈值的点就是异常。这个技巧在空气质量分析里特别实用——沙尘暴、烟花爆竹、工业事故都会在残差里留下明显尖峰。

from prophet import Prophet # Prophet 要求列名为 ds 和 y df_prophet = df_model.reset_index()[["datetime", "PM2.5"]].rename( columns={"datetime": "ds", "PM2.5": "y"} ) # 拟合模型,开启周周期和年周期 model = Prophet( yearly_seasonality=True, weekly_seasonality=True, daily_seasonality=True, changepoint_prior_scale=0.05 ) model.fit(df_prophet) # 预测并计算残差 forecast = model.predict(df_prophet) df_prophet["residual"] = df_prophet["y"] - forecast["yhat"].values # 残差超过 2 倍标准差标记为异常 threshold = df_prophet["residual"].std() * 2 anomalies = df_prophet[df_prophet["residual"].abs() > threshold] print(f"检测到异常点: {len(anomalies)} 个") print(anomalies[["ds", "y", "residual"]].head(10))

changepoint_prior_scale=0.05控制趋势变化的灵活度,值越大趋势越容易突变,空气质量数据一般设 0.01 到 0.1 之间。yearly_seasonalityweekly_seasonality分别捕捉年度和周的周期模式,daily_seasonality捕捉日内变化。残差阈值用 2 倍标准差是经验值,如果数据噪声大可以放宽到 3 倍。检测出的异常点可以叠加到 Pyecharts 折线图上,用markpoint标红,一眼就能看出哪天出了问题。

这个方法的局限在于 Prophet 假设周期模式稳定,如果数据只有几个月,年周期学不出来,需要关掉yearly_seasonality。另外 Prophet 对缺失值敏感,输入前要确保ds列没有重复时间戳和空值。我一般会先用 Prophet 做趋势分解和异常检测,再把异常标记作为特征喂给 XGBoost,两者结合比单用任何一个效果都好。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 18:03:31

广东知名的建造师培训公司推荐,靠谱机构团队实力测评

重庆仕仕通教育咨询有限公司&#xff0c;是深耕职业教育咨询领域、聚焦建筑工程行业人才职业成长的专业服务机构&#xff0c;精准定位为建筑行业全链条职业成长一站式服务提供商&#xff0c;核心围绕在职人员考证晋升的实际需求&#xff0c;打造职业资格培训、职称评审咨询两大…

作者头像 李华
网站建设 2026/9/24 18:02:49

云桌面哪家好:企业桌面云采购的评价体系

在政企数字化、办公国产化、远程协同常态化的大背景下&#xff0c;云桌面已经从早期的辅助办公工具&#xff0c;变成企业终端标准化、数据安全治理的核心基础设施。不同于传统本地电脑&#xff0c;云桌面将计算、存储、数据全部部署在云端&#xff0c;本地终端仅承担显示与输入…

作者头像 李华