☰
空气质量监测与预测系统实践:从数据清洗到LightGBM模型上线避坑指南
2026/10/22 7:39:14 网站建设 项目流程

简介:一份基于机器学习的空气质量监测与预测系统技术文档,面向环保科技从业者、空气污染研究人员及数据分析师。文档系统梳理了从多种数据源采集、清洗到预处理的完整链路,并围绕Django框架的B/S架构与Mysql数据库设计展开,详细描述了系统从数据获取、模型训练到部署应用的实施步骤;重点讲解斯皮尔曼相关性分析、贝叶斯决策以及随机森林、支持向量机等算法在AQI预测中的实际应用,同时给出模型性能评估与改进思路。压缩包内包含1个docx文档,整体大小约595KB,虽为单文件却结构完整,适合作为类似项目开题报告与系统设计的参考模板。目前已有163人学习下载。文档结合北京、西藏等具体城市数据,展示了数据清洗、特征提取、模型训练与可视化展示的完整流程,能帮助读者快速理解环境数据分析项目的落地路径,并提醒根据地区实际情况调整模型参数,对开展空气质量研究或制定环保策略具有实用参考价值。

1. 空气质量预测不是“拿个模型拟合”就能上线:先搞清楚监测和预测的边界

一看到“基于机器学习的空气质量监测与预测系统”这个标题,很多人的第一反应是“又要训练一个时间序列模型”。但真正做过这类系统的开发者都会告诉你,模型只是整条链路里最好解决的一环。监测和预测是两件事:监测解决“数据从哪来、可不可信”,预测解决“未来几小时浓度会怎么走”。如果传感器读数本身带着漂移,时间戳没有对齐,标签定义成了当天的平均值,那再先进的模型也只会输出一个看起来很合理的数字。这套系统适合园区、工地、网格化监测这类场景,核心价值是把采集、清洗、建模、告警做成一条可维护的流水线。

2. 系统架构与数据链路:从传感器原始读数到可训练样本

先说系统怎么搭。常见做法是拆成四层:传感器节点负责颗粒物和气态污染物的原始测量,边缘网关负责数据汇聚和上云,后端数据库存原始分钟级数据,最后才是训练和预测服务。我一般不用那些号称“开箱即用”的采集盒子,原因是校准、替换、冗余都不好做。独立传感器加 4G 网关虽然要自己写驱动,但出问题能单点替换,成本反而更低。

2.1 监测端选型:为什么我不用“万能采集盒子”而拆成传感器+网关

一体式采集盒子的主要问题是“黑匣子”:你不知道内部传感器型号,也没办法单独替换某一个探头,一旦某个参数不准,整个通道都要返厂。拆成独立传感器之后,每个参数都可以单独校准,现场运维能直接换探头,不用停机。

具体选型我一般这么定:颗粒物用激光散射传感器,气态污染物用电化学传感器,气象参数用一体式气象百叶箱。采样频率统一设成 1 分钟,空气质量变化相对缓慢,秒级数据只会带来更多噪声和存储压力。

监测参数常见传感器类型采样频率输出协议
PM2.5 / PM10激光散射60sRS485 / Modbus
SO2 / NO2 / O3 / CO电化学60sRS485 / 4-20mA
温度 / 湿度数字温湿度60sRS485 / I2C
风速 / 风向超声波或机械式60sRS485 / Modbus

网关端用支持 MQTT 的 4G DTU,数据上报使用 QoS1,并在本地 SD 卡做断网缓存。QoS1 能保证消息至少送达一次,但可能重复,所以下游必须做去重。这一层如果偷懒,后面所有时间序列分析都会踩重复时间戳的坑。

2.2 数据清洗与时间对齐:把分钟级原始数据变成小时级稳定样本

原始分钟级数据进到数据库之后,第一步不是建模型,而是清洗。最常见的三个问题:网关断点续传导致重复记录、传感器瞬时尖峰超过量程、设备重启导致的时间戳乱序。下面这段代码是清洗的骨架。

import pandas as pd df = pd.read_csv("raw_sensor.csv", parse_dates=["ts"]) df = df.drop_duplicates(subset=["ts", "device_id"], keep="last") df = df.sort_values("ts") # 浓度超过量程上限的直接置为缺失,而不是删除 df.loc[df["pm25"] > 1000, "pm25"] = pd.NA # 按小时聚合,要求至少 80% 的原始点位才保留 hourly = df.set_index("ts").groupby("device_id").resample("H").mean(min_count=45) hourly = hourly.reset_index()

drop_duplicates 处理网关重传导致的重复时间戳,sort_values 保证时间顺序。超过量程的值置为缺失而不是删除,是为了保留时间轴完整性,后续可以用插值补。resample("H") 把分钟数据降到小时,tshift 的时间窗更稳定,也更容易对齐气象数据。

min_count=45 是一个关键参数:如果一小时 60 分钟内只有不到 45 个有效数据点,说明这段时间设备可能处于离线或异常状态,均值不可信。这个值要根据实际采集频率调整,如果你采集频率是 30 秒,那就是 1 小时 120 个点,min_count 可以设到 80 左右。

短时缺失的插值也要控制长度。我一般用线性插值补连续 6 小时以内的缺失,超过 6 小时不补,直接留空让模型自己处理缺失模式。

hourly["pm25"] = hourly.groupby("device_id")["pm25"].transform( lambda s: s.interpolate(method="linear", limit=6) )

注意:插值只对特征做,不对目标变量做。如果对目标变量插值,相当于把未来信息混进了标签,验证指标会虚高,上线后立刻现原形。

2.3 标签与特征怎么定:预测“未来1小时浓度”而不是“当天均值”

很多人第一次做这个系统时,把目标定义成“预测当天平均浓度”。这在业务上几乎没有意义,因为告警需要的是提前量,而不是事后统计。我一般把任务定义为回归:用 T 时刻及之前的特征,预测 T+1 时刻的 PM2.5 浓度。预测未来 1 小时是比较合理的起点,跑通后再扩展到 3 小时、6 小时。

生成标签和滞后特征时需要特别注意 shift 的方向。

df_feat = hourly.copy() df_feat["target"] = df_feat.groupby("device_id")["pm25"].shift(-1) for h in [1, 2, 3, 24]: df_feat[f"pm25_lag{h}"] = df_feat.groupby("device_id")["pm25"].shift(h)

shift(-1) 表示把下一条记录的当前浓度作为当前时刻的预测目标,也就是 T+1。pm25_lag1 是 1 小时前的浓度,pm25_lag24 是前一天同一小时的浓度,后者用来捕捉日周期性。

这里有一个非常容易犯的错:如果预测未来 3 小时,target 应该是 shift(-3),但 lag 特征不能包含 T 到 T+3 之间的任何数据。有些人直接把未来 3 小时的浓度也放进特征里,训练时 RMSE 低得离谱,上线后一塌糊涂。这就是典型的时间泄漏。

3. 预测模型选型与训练:从线性基线到树模型再到时序模型

数据维度准备好之后,真正进入模型环节。模型选型不要一上来就上 LSTM,而是要先建立几个参照系。

3.1 先跑一个持久化基线,再谈机器学习

空气质量在短时间内的自相关性很强,也就是说“当前浓度”本身就是最好的预测特征。持久化基线把当前浓度当作未来 1 小时的预测值,这个基线不能跳过,因为它决定了机器学习模型的价值底线。

from sklearn.metrics import mean_squared_error, mean_absolute_error import numpy as np y_true = df_feat["target"].dropna() baseline_pred = df_feat["pm25"].loc[y_true.index] rmse = np.sqrt(mean_squared_error(y_true, baseline_pred)) mae = mean_absolute_error(y_true, baseline_pred) print(f"Persistence RMSE: {rmse:.2f}, MAE: {mae:.2f}")

如果后面训练的机器学习模型连这个 RMSE 都打不过,那问题不在模型,而在特征或数据质量。我见过不少项目在这个基线上翻车,最后发现是时间对齐错了,特征里混进了“未来数据”。还有一点要注意,这个基线不是用来发布用的,它只是给树模型、神经网络一个衡量尺子。

3.2 特征工程:气象、交通、日历、滞后值

有了基线之后,再逐步往模型里加特征。我一般把特征分成四组,按优先级从高到低排列:

特征分组具体特征说明
历史浓度pm25_lag1、lag2、lag3、lag24自相关是预测的主要来源
气象温度、湿度、风速、风向、气压风速大扩散好,湿度大容易二次生成颗粒物
时间小时、星期、是否节假日交通排放的周期性
空间周边站点浓度、区域污染指数传输影响,适合多点位系统

风向不能直接用角度数值喂给模型,因为 359° 和 1° 在地理上只差 2°,但数值差 358。常见做法是用 sin 和 cos 编码,把角度映射到单位圆上的两个坐标。

df_feat["wind_sin"] = np.sin(np.deg2rad(df_feat["wind_deg"])) df_feat["wind_cos"] = np.cos(np.deg2rad(df_feat["wind_deg"]))

气象数据也要对齐到小时级。如果你拿到的是第三方气象预报,务必确认它的“有效时间”是哪个时刻,很多预报源的时间语义是乱的,这在第 5 章会专门讲。

3.3 模型对比与参数:LightGBM / XGBoost / 随机森林?还是 LSTM?

我们先看一组对比,再决定选哪个:

模型数据需求训练速度可解释性适用场景
线性回归 / 岭回归小快好基线、趋势分析
随机森林中中中中等数据量
LightGBM中快中表格数据首选
LSTM / GRU大慢差长序列依赖非常明显时

我大多数情况下会选 LightGBM,原因是这类任务的特征基本是表格形态,树模型对非线性关系和特征交互的拟合能力足够,训练速度快,调参路径成熟。LSTM 不是不能用,而是它对数据量和预处理要求高,而且要处理时间步长、滑窗、归一化等一系列问题,很容易把简单问题复杂化。

下面是用 LightGBM 训练的第一版模型,带早停和基础参数。

import lightgbm as lgb from sklearn.model_selection import train_test_split features = [ "pm25_lag1", "pm25_lag2", "pm25_lag3", "pm25_lag24", "temp", "hum", "wind_sin", "wind_cos", "hour", "dayofweek", "is_holiday" ] df_train = df_feat.dropna(subset=features + ["target"]) X = df_train[features] y = df_train["target"] # 按时间顺序切分,前 80% 训练,后 20% 验证 split_idx = int(len(X) * 0.8) X_train, X_val = X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_val = y.iloc[:split_idx], y.iloc[split_idx:] model = lgb.LGBMRegressor( n_estimators=300, learning_rate=0.05, num_leaves=31, max_depth=7, min_child_samples=20, subsample=0.8, colsample_bytree=0.8, random_state=42, ) model.fit( X_train, y_train, eval_set=[(X_val, y_val)], callbacks=[lgb.early_stopping(50), lgb.log_evaluation(50)] )

初版参数不用太纠结。n_estimators=300 配合 early_stopping(50),实际训练会在验证集 50 轮不提升后自动停止。learning_rate=0.05 是步长,越小越稳但训练轮数越多。num_leaves=31 控制树复杂度,数据量不大时超过 100 很容易过拟合。subsample 和 colsample_bytree 分别是行采样和列采样,用来降低方差。

4. 模型评估与调参:别用 R2 骗自己,要盯住高浓度误报

模型训练完之后,评估环节最容易出现“指标好看但业务不可用”的情况。空气质量预测的特殊性在于:普通时段的误差不影响决策,真正要紧的是高浓度污染的捕获能力。

4.1 评估指标选择:RMSE、MAE、以及“污染事件召回率”

RMSE 对大误差敏感,MAE 更直观,但这两个指标都没有回答一个关键问题:真正超标的时刻,模型有没有提前预报出来?所以我每次训练完都额外算一个“超标召回率”。

from sklearn.metrics import classification_report threshold = 75 # PM2.5 超标阈值,单位 μg/m³ y_true_alert = y_val >= threshold y_pred_alert = model.predict(X_val) >= threshold print(classification_report(y_true_alert, y_pred_alert))

把回归输出在阈值处离散成二分类,只看“超标事件”这个子集。分类报告里的召回率表示真实超标中有多少被模型提前抓到,精确率表示模型每次报超标时有多大概率是准的。如果召回率低于 60%,哪怕 RMSE 再低,这个模型对告警场景也是废的。

这里阈值不是固定的,要对应你所在地区的空气质量指数分级。比如 PM2.5 日均 75 μg/m³ 是国标二级,但小时级告警阈值可以定到 80 或更高,取决于业务忍不忍受误报。

4.2 时间序列交叉验证与随机打散的区别

训练代码里我用了简单的 80/20 时间切分,但做参数调优时,这种单一切分不够稳定。很多人直接上 sklearn 默认的 KFold 交叉验证,这是时间序列任务里的大坑:随机打散会把相邻时间点同时分进训练集和验证集,造成时间泄漏,指标虚高。

正确做法是用 TimeSeriesSplit,并加一个 gap 参数。

from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5, gap=24) for train_idx, val_idx in tscv.split(X): X_train, X_val = X.iloc[train_idx], X.iloc[val_idx] y_train, y_val = y.iloc[train_idx], y.iloc[val_idx] model = lgb.LGBMRegressor( n_estimators=500, learning_rate=0.05, num_leaves=31, random_state=42, ) model.fit( X_train, y_train, eval_set=[(X_val, y_val)], callbacks=[lgb.early_stopping(50), lgb.log_evaluation(0)] )

gap=24 表示训练集末尾和验证集开头之间留出 24 小时间隔。为什么要留?因为滞后 24 小时的特征会用到前一天同一时刻的浓度,如果训练集和验证集紧挨着,验证集的第 1 条样本会用到训练集最后 1 条样本的浓度作为 lag1,这实际上也属于一种轻量泄漏。gap 的数值要大于最大滞后阶数,一般设成最大 lag 的 1 到 2 倍。

4.3 LightGBM 的 5 个关键参数

调参不需要网格搜索全空间,抓住 5 个参数就能解决大部分问题。

参数作用调参建议
learning_rate每步迭代的收缩幅度0.03~0.1,配合 n_estimators
num_leaves单棵树最大叶子数30~255,小数据用 31 以下
max_depth树的最大深度5~15,防止过拟合
min_child_samples叶子节点最少样本数20~100,越大越保守
subsample / colsample_bytree行采样 / 列采样比例0.7~0.9,降低方差

调参顺序我一般是:先固定 learning_rate=0.05,然后调 num_leaves 和 max_depth,接着调 min_child_samples,最后调两个采样参数。不要一开始就把 n_estimators 调到几千,那是往过拟合里钻。用 early_stopping 让模型自己决定树数量,比手工硬设更可靠。

5. 落地避坑:从离线预测到在线监测服务的 5 个踩坑记录

这章写的都是实际部署中我见过或踩过的坑,每条都按“现象 → 原因 → 解决”整理,希望你不用走一遍。

5.1 传感器漂移让模型“越训越不准”的翻车现场

现象:某园区部署三个月后,训练时的验证 RMSE 一直在下降,但现场告警越来越不准,白天频繁误报,夜间又漏报。原因:电化学传感器在持续高湿环境下零点漂移,现场浓度读数被整体抬高,模型学到的是“虚假的高浓度”,真实浓度并没有那么高。解决:每两周用标准气或标准膜校准一次;在数据链路里加入漂移检测,比如比较传感器读数和附近参考站点的差值,超过阈值时自动标记并触发校准工单。血泪经验是:传感器漂移不是运维问题,而是数据质量问题,必须在进入模型之前解决,模型没有能力纠正输入数据的系统偏差。

5.2 气象预报和实测数据的时间错位

现象:使用第三方气象预报作为特征时,预测未来 1 小时效果好,预测未来 3 小时效果崩盘。原因:气象预报的“有效时间”和本地实际时刻没有对齐。比如上午 8 点发布的一条预报,时效 T+1 对应的是上午 9 点,如果把它当成 8 点的特征输入,就相当于用了未来数据。解决:解析预报的时效字段,把特征按“有效时间”对齐;在验证阶段强制检查所有特征时间都早于预测目标时间。黑匣子往往不是模型,而是外部数据的时间语义。

5.3 缺失值填错方向:把夜间高浓度当成噪声抹掉

现象:夜间预测值总是偏低,而且从凌晨 2 点开始掉,白天恢复正常。原因:夜间数据缺失率较高,运维同学用了前后均值填充,而夜间浓度原本呈上升趋势,线性插值把波峰削平了,模型学不到夜间的污染积累过程。解决:对缺失值分段处理——短时缺失(小于 3 小时)用线性插值;长时缺失(大于 6 小时)用同一时刻的历史中位数回填,或者直接置空让模型自动学习缺失模式。另外,绝对不要对目标变量做插值,否则标签被平滑,验证 RMSE 会虚低。

5.4 模型上线后预测值“平滑得像条直线”,告警全失灵

现象:上线第一天预测值非常稳定,曲线几乎是一条直线,但一次真实污染过程完全没有报出来。原因:滞后特征权重太高,模型基本在复制当前浓度,而当前浓度还没有反映出污染过程,新增的决策信息被淹没了。解决:在特征集里强制加入气象变化率特征,比如 3 小时风速差、2 小时气压差;同时调低 lag1 的权重。告警判定不要只看预测值,要结合观测值的变化斜率做综合判断。模型输出的是“延续趋势”而不是“未来真相”,两者要分开使用。

5.5 模型更新策略:每天全量重训导致凌晨“变傻”

现象:每天凌晨自动重训模型,白天预测波动很大,甚至某一天所有预测整体偏低。原因:训练数据窗口里混入了不同季节、不同气象条件的样本,最近一周的极端样本权重过大;每天自动替换模型版本,没有做质量门禁。解决:采用滚动窗口训练,比如只保留近 30 天数据;训练完成后先跑一遍影子模型对比,连续 3 天偏差不超过阈值才允许替换。后悔药就是保留上一版模型文件,新模型至少并行观察一周再切流量。

6. 最后一道工序:用“影子模式”验证模型值不值得上线

离线指标再漂亮,都不代表在线环境能直接切流量。我习惯先跑一段时间的“影子模式”——新模型不参与告警,只和现有模型并行计算,每天对比一次输出差异。

具体做法是写一个定时任务,每天凌晨对前一天所有时刻同时输入两个模型,生成影子预测文件,然后做差异分析。

import pandas as pd shadow = pd.read_csv("shadow_predictions.csv", parse_dates=["ts"]) shadow["dev"] = shadow["new_pred"] - shadow["old_pred"] shadow["abs_dev"] = shadow["dev"].abs() # 输出每日超标事件数量差异和最大偏差 daily = shadow.set_index("ts").resample("D").agg( old_alert=("old_pred", lambda x: (x >= 75).sum()), new_alert=("new_pred", lambda x: (x >= 75).sum()), max_dev=("abs_dev", "max"), ) print(daily[daily["max_dev"] > 15])

max_dev 超过 15 μg/m³ 的日子要重点关注。如果新模型总是比旧模型低,说明它“学懒了”,只学会了复制当前浓度;如果超标告警次数差异超过 30%,也要先查时间泄漏,再探参数问题。

除了输出差异,我还会看特征分布漂移。最简单的方法是每个月用 PSI 检查关键特征在当前月份的分布和训练时期的分布是否一致。如果 PSI 高于 0.25,说明传感器状态或环境结构发生了变化,需要重新收集训练数据或做模型迁移。

有一回我就是嫌影子模式麻烦,直接切了新模型,结果连续三天误报,运维差点把系统停掉。后来我把“新模型上线前至少并行 7 天”写进了项目规范,每次切流量都先看 diff 报告。这个习惯帮我避开了很多次“离线好、在线崩”的尴尬。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询