简介本资源面向环境科学、数据挖掘与机器学习方向的学习者与研究者提供一套基于Python的空气质量数据可视化分析系统源码及配套数据可用于城市群划分、污染传输网络构建与传播过程探索等课题实践。压缩包共约2000个文件以1295个json数据文件和697个py脚本为主另含少量txt、xml与md说明文档整体约76.43MBjson文件承载原始及挖掘后的空气质量数据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 把结构摸清楚。典型的空气质量数据集通常包含date、hour、city、station、PM2.5、PM10、SO2、NO2、CO、O3、AQI、quality_level这些列。不同来源的数据列名可能带中文、带空格、带括号单位比如PM2.5(ug/m3)这种直接按列名索引会翻车。import pandas as pd import numpy as np # 读取时统一处理列名去掉空格和单位括号 df pd.read_csv(air_quality.csv, encodingutf-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显式指定格式解析失败的行用errorscoerce标记出来再处理。# 解析时间列兼容多种格式 df[datetime] pd.to_datetime(df[date], format%Y/%m/%d %H:%M, errorscoerce) # 检查解析失败的行 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_onlyTrue) # 对重采样后的缺失值做线性插值限制最大连续插值长度为3 df_hourly df_hourly.interpolate(methodlinear, limit3)resample(1h)把不规则时间戳对齐到整点mean(numeric_onlyTrue)避免对非数值列求均值报错。interpolate的limit3是关键参数——如果连续缺失超过 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[fPM25_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 行是 NaNdropna()会一并清掉。如果数据量本身不大丢弃这些行可能损失 10% 到 15% 的样本这时候可以考虑用min_periods1让窗口不满也计算但统计量的稳定性会下降。我一般会在数据量充足时直接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 以上。常见做法是用RandomForestRegressor或XGBRegressor做基线再用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_estimators200, max_depth12, random_state42, n_jobs-1) rf.fit(X_train, y_train) y_pred rf.predict(X_test) print(fMAE: {mean_absolute_error(y_test, y_pred):.2f}) print(fR2: {r2_score(y_test, y_pred):.4f})时间序列数据必须按时间切分不能train_test_split(shuffleTrue)否则未来信息会泄漏到训练集。n_estimators200是树的数量太少欠拟合太多训练慢且收益递减max_depth12控制单棵树深度空气质量数据一般 8 到 15 之间比较合适。n_jobs-1用满所有 CPU 核心加速训练。3.2 XGBoost 的关键参数怎么设XGBoost 在空气质量预测上通常比随机森林好 3 到 5 个百分点但参数更多。核心参数就四个learning_rate、max_depth、subsample、colsample_bytree。import xgboost as xgb xgb_model xgb.XGBRegressor( n_estimators500, learning_rate0.05, max_depth6, subsample0.8, colsample_bytree0.8, reg_alpha0.1, reg_lambda1.0, random_state42, early_stopping_rounds50, eval_metricmae ) xgb_model.fit( X_train, y_train, eval_set[(X_test, y_test)], verboseFalse ) y_pred_xgb xgb_model.predict(X_test) print(fXGBoost MAE: {mean_absolute_error(y_test, y_pred_xgb):.2f}) print(fXGBoost R2: {r2_score(y_test, y_pred_xgb):.4f})learning_rate0.05配合n_estimators500是经典组合学习率低需要更多树来补偿。max_depth6比随机森林浅因为 boosting 是串行叠加单棵树太深容易过拟合。subsample0.8和colsample_bytree0.8引入随机性防止过拟合。early_stopping_rounds50表示验证集指标 50 轮不提升就停避免无效训练。reg_alpha和reg_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_typebar, showFalse) import matplotlib.pyplot as plt plt.tight_layout() plt.savefig(shap_importance.png, dpi150)SHAP 值反映每个特征对单次预测的贡献。如果PM25_lag1的 SHAP 值最大说明前一小时浓度是预测当前浓度的最强信号这符合空气污染的累积效应。如果hour的 SHAP 值呈现早晚高峰模式说明交通排放对 PM2.5 有显著影响。这些结论比单纯看 R² 更有业务价值也是课程设计答辩时能讲出东西的地方。4. 可视化呈现用 Pyecharts 做可交互的空气质量大屏4.1 为什么选 Pyecharts 而不是 MatplotlibMatplotlib 适合静态图但空气质量分析系统通常需要交互——鼠标悬停看具体数值、时间轴缩放、地图联动。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_smoothTrue, linestyle_optsopts.LineStyleOpts(width2), label_optsopts.LabelOpts(is_showFalse) ) .add_yaxis( PM10, df_plot[PM10].round(1).tolist(), is_smoothTrue, label_optsopts.LabelOpts(is_showFalse) ) .set_global_opts( title_optsopts.TitleOpts(title近7天PM2.5与PM10趋势), xaxis_optsopts.AxisOpts(name时间), yaxis_optsopts.AxisOpts(name浓度 ug/m3), datazoom_optsopts.DataZoomOpts(range_start0, range_end100), tooltip_optsopts.TooltipOpts(triggeraxis) ) ) line.render(pm_trend.html)is_smoothTrue让折线平滑datazoom_opts开启底部缩放条tooltip_opts(triggeraxis)让鼠标悬停时显示该时间点所有系列的值。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_optsopts.TitleOpts(title各城市平均AQI分布), visualmap_optsopts.VisualMapOpts( min_0, max_300, range_color[#50a3ba, #eac763, #d94e5d] ) ) ) map_chart.render(aqi_map.html)visualmap_opts的range_color定义颜色梯度从蓝到黄到红对应空气质量从优到重度污染。min_和max_根据实际数据范围调整如果最大值只有 200设 300 会导致颜色偏淡。城市名要和 Pyecharts 内置的 GeoJSON 匹配比如「北京市」要写成「北京」否则地图上不显示。4.3 把多个图表拼成一张大屏单独一张折线图或地图撑不起「可视化分析系统」的场面需要把趋势图、地图、分类饼图、特征重要性图拼到一个页面。Pyecharts 提供Page和Grid两种布局方式。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_optsopts.TitleOpts(title空气质量等级分布)) .set_series_opts(label_optsopts.LabelOpts(formatter{b}: {d}%)) ) # 用 Page 组合所有图表 page Page(layoutPage.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(methodtime)其次用前向填充实在不行再考虑均值填充。填充后一定要在验证集上评估不能只看训练集指标。如果缺失比例超过 20%考虑把「是否缺失」作为一个二值特征加入模型。5.2 时间切分时用了随机打乱现象交叉验证 R² 很高但按时间顺序预测未来数据时效果很差。原因train_test_split(shuffleTrue)把未来数据混入了训练集模型「偷看」了未来信息。空气质量数据有强自相关性打乱后相邻时间点的特征和标签同时出现在训练集和测试集造成信息泄漏。解决始终按时间顺序切分训练集在前、测试集在后。如果要做交叉验证用TimeSeriesSplit而不是KFold。TimeSeriesSplit(n_splits5)保证每次验证集都在训练集之后。5.3 Pyecharts 地图城市名不匹配现象地图渲染出来是空白的或者只有底图没有数据着色。原因Pyecharts 内置的 GeoJSON 对城市名有固定格式比如「北京」「上海」「广州」不能带「市」字。如果数据里是「北京市」地图上找不到对应区域。解决在聚合前统一去掉「市」「省」「自治区」等后缀。用df[city] df[city].str.replace(市$, , regexTrue)处理。如果还是不行检查 Pyecharts 版本旧版本的地图数据可能不完整升级到最新版。5.4 XGBoost 早停不生效现象设置了early_stopping_rounds50但模型还是跑满了n_estimators500轮。原因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默认只显示一部分。如果必须展示全量数据考虑用Line的sampling参数或者改用Scatter配合largeTrue模式。我一般会在可视化前先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_seasonalityTrue, weekly_seasonalityTrue, daily_seasonalityTrue, changepoint_prior_scale0.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_scale0.05控制趋势变化的灵活度值越大趋势越容易突变空气质量数据一般设 0.01 到 0.1 之间。yearly_seasonality和weekly_seasonality分别捕捉年度和周的周期模式daily_seasonality捕捉日内变化。残差阈值用 2 倍标准差是经验值如果数据噪声大可以放宽到 3 倍。检测出的异常点可以叠加到 Pyecharts 折线图上用markpoint标红一眼就能看出哪天出了问题。这个方法的局限在于 Prophet 假设周期模式稳定如果数据只有几个月年周期学不出来需要关掉yearly_seasonality。另外 Prophet 对缺失值敏感输入前要确保ds列没有重复时间戳和空值。我一般会先用 Prophet 做趋势分解和异常检测再把异常标记作为特征喂给 XGBoost两者结合比单用任何一个效果都好。希望帮到你。本文还有配套的精品资源点击获取