ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于Python的郑州空气质量预测:从数据清洗到XGBoost/LSTM模型实战

基于Python的郑州空气质量预测:从数据清洗到XGBoost/LSTM模型实战 简介这是一份针对郑州地区空气质量预测场景的Python源码项目适合环境数据分析、机器学习初学者及研究者参考。项目基于历史空气质量数据构建预测模型可帮助读者掌握数据预处理、特征工程、模型训练与评估的完整链路并可用于城市大气环境监测和污染趋势研判。压缩包共20个文件大小约652KB其中包含4个Python源码文件覆盖数据处理、模型定义、训练与预测等环节另有XML配置、文本说明、PNG可视化结果图以及HDF5数据文件等结构划分清晰。已有395人学习下载适合想通过实际案例学习时序预测或需要搭建空气质量预测系统的人员。资源提供了可运行的模型代码、训练好的权重文件、数据文件和结果图并附有注释与说明便于快速复现实验以及在此基础上进行二次开发和改进。1. 为什么郑州需要一套独立训练的空气质量预测模型你在网上能找到的空气质量预测代码十有八九是用北京或某个通用站点的数据训练的。把那个模型直接拿来做郑州的预测你会发现一个很明显的现象训练集上误差漂亮一换到郑州的监测数据上就翻车尤其是秋冬季的PM2.5突然抬升和春季的沙尘过境模型完全反应不过来。原因不复杂空气质量预测的精度很大程度上依赖本地气象条件和排放源的规律郑州作为中原城市群的中心既有冬季静稳天气下的污染物累积特征又有明显的交通扬尘和工业排放周期通用模型根本学不到这些本地规律。这个标题指向的东西就是一套面向郑州地区、用Python完整实现从数据准备到模型训练再到预测输出的源码方案。你需要理解的不是某个单一算法而是整条链路怎么做。本文会从数据怎么拿、特征怎么造、模型怎么选、参数怎么调一路写到郑州本地化落地时最容易踩的坑。适合谁看准备做毕业设计、课程设计或者单位里要搭一套城市级空气质量预测demo的工程师。新手能照着跑通熟手能直接拿走做参数调整和生产化改造。2. 数据是建模的起点郑州空气质量数据获取与预处理方案2.1 数据源选型公开监测站点与气象数据的配合方式做郑州地区的空气质量预测第一件事不是选模型而是先解决数据问题。常见做法是两部分数据配合污染物浓度数据和气象数据。污染物浓度的来源一般是空气质量监测站点的历史发布数据包含SO2、NO2、PM10、PM2.5、O3、CO这六项常规污染物的小时浓度值气象数据则包括温度、湿度、气压、风速、风向、降水量等。郑州有多个国控监测站点常见做法是取多个站点的均值作为城市整体浓度或者按你预测的目标站点单独取数。这里有一个关键决策如果你想做的是“郑州市整体空气质量预测”建议把多个站点的污染物浓度做算术平均如果你想做的是“某个具体站点比如郑东新区某站的预测”那就直接用该站点的数据这样模型更容易学到站点周边的局部规律。import pandas as pd df_pollutant pd.read_csv(zhengzhou_air_history.csv, parse_dates[time]) df_weather pd.read_csv(zhengzhou_weather_history.csv, parse_dates[time]) # 按小时对齐污染物和气象数据都取整点时刻 df_pollutant df_pollutant.set_index(time).sort_index() df_weather df_weather.set_index(time).sort_index() # 多站点平均假设数据中有 station 字段 df_city df_pollutant.groupby(time)[[PM2.5, PM10, SO2, NO2, O3, CO]].mean() # 合并气象数据取与污染物同时刻的观测值 df df_city.join(df_weather, howinner) df df.dropna()这里用groupby(time)对多站点做平均是为了先拿到城市尺度的浓度序列再做建模。join用howinner指定内连接意思是只保留污染物和气象数据都有记录的时刻避免模型被缺失值干扰。这一步看起来基础但直接影响后续所有工作——如果气象数据是3小时间隔而污染物是小时间隔直接join就会产生大量NaN所以先检查两条序列的时间频率是否一致不一致就做重采样对齐。2.2 缺失值处理和时间特征构造的具体操作拿到原始数据后缺失值处理是第一个绕不开的环节。空气质量监测站偶尔会因设备校准或通信故障产生小时级缺失气象站也可能有类似问题。我一般这样处理连续缺失不超过3小时的用线性插值补超过3小时的用前一天同时刻的值回填因为空气污染浓度有明显的日周期性前一天同一时刻的浓度有参考意义如果某个特征整体缺失超过30%直接丢弃这个特征。# 线性插值 日同期回填 df_filled df.interpolate(methodlinear, limit3) # 对仍然存在的缺失值用前一天同时刻的值回填 df_filled df_filled.fillna(df_filled.shift(24))interpolate的limit3参数表示只对连续缺失不超过3个点的位置做插值超过的部分保留为NaN交给下一步处理。shift(24)是取24小时前的值做回填前提是你的数据确实是小时粒度如果数据粒度不是小时这个数字要按实际周期换算。再往后就是特征工程。预测空气质量不能只用当前时刻的污染物浓度还要构造滞后特征和滑动窗口统计量。常用的几个滞后点过去1小时、3小时、6小时、12小时、24小时的PM2.5浓度气象特征同样做滞后尤其注意风速和风向的滞后效应——污染物浓度对风速变化的响应通常有1到3小时的延迟。for lag in [1, 3, 6, 12, 24]: df[fPM2.5_lag{lag}] df[PM2.5].shift(lag) df[PM2.5_rolling3] df[PM2.5].rolling(window3).mean() df[PM2.5_rolling6] df[PM2.5].rolling(window6).mean() df[hour_sin] np.sin(2 * np.pi * df.index.hour / 24) df[hour_cos] np.cos(2 * np.pi * df.index.hour / 24)把小时特征编码成sin和cos是两个分量而不是一个整数是因为 hour 本身是循环变量23点和0点虽然相邻但数值上差了23直接塞给模型会让模型误以为它们差异巨大。用sin/cos编码后23点和0点在两个维度上都接近模型才能学到正确的周期性。这是时间序列预测里很基础但容易被忽视的细节。3. 模型选型与对比从Prophet到XGBoost再到LSTM的取舍逻辑3.1 不同模型的适用边界为什么单变量时间序列预测模型不够用很多初学者拿到空气质量数据第一反应是套用一个“时间序列预测模型”。Prophet确实在趋势和季节性提取上很方便几行代码就能出一个相对合理的基线预测适合快速判断数据的周期性规律。但空气质量预测本质上不只是时间序列问题它同时受气象条件的强影响——同样是上午9点晴天和雾天的PM2.5可以差出两倍。Prophet这类单变量时序模型只能从历史浓度本身去外推完全吃不到风速、降水这些外部变量的信息所以它的预测上限很有限只能当基线用。from prophet import Prophet df_prophet df.reset_index().rename(columns{time: ds, PM2.5: y}) df_prophet df_prophet[[ds, y]].dropna() model Prophet(yearly_seasonalityFalse, daily_seasonalityTrue) model.add_country_holidays(country_nameCN) model.fit(df_prophet) future model.make_future_dataframe(periods24, freqH) forecast model.predict(future)以上是把Prophet当基线模型的典型写法。yearly_seasonalityFalse是因为空气质量的“年周期性”更多来自气候带和采暖政策不是纯粹的季节波动一旦数据长度只有一两年拟合年周期性很容易过拟合。daily_seasonalityTrue保留日周期因为交通早晚高峰对污染物浓度的影响是显著的。add_country_holidays加上中国法定节假日春节期间工厂停工和机动车减少会在浓度序列里造成明显的凹陷。用Prophet得到基线后你会立刻发现它的短板它把天气因素当作随机噪声处理了而实际上天气变化是空气质量短期波动的最大驱动因素。这就是为什么还需要引入能同时吃进多维度特征的模型。3.2 XGBoost与随机森林的对比回归预测模型的特征灵活性XGBoost和随机森林在空气质量预测里扮演的角色是“监督学习回归器”把PM2.5浓度预测当作回归问题特征就是上一节构造的滞后污染物浓度、气象特征和时间编码。两者的核心区别在于建模逻辑——随机森林通过多棵树的平均降低方差XGBoost通过梯度提升逐步减小残差后者在结构化表格数据上通常精度更高也更容易通过正则化参数抑制过拟合。import xgboost as xgb from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split feature_cols [c for c in df.columns if c ! PM2.5] X df[feature_cols].dropna() y df.loc[X.index, PM2.5] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, shuffleFalse ) xgb_model xgb.XGBRegressor( n_estimators500, max_depth6, learning_rate0.05, subsample0.8, colsample_bytree0.8, reg_alpha0.1, reg_lambda1.0, random_state42 ) xgb_model.fit(X_train, y_train)shuffleFalse这一行是时间序列建模的关键——打乱样本会破坏时间顺序让模型“偷看”未来信息评估结果虚高。n_estimators500配合learning_rate0.05是XGBoost常见的保守配置树多但步长小不容易过早过拟合。reg_alpha和reg_lambda分别是L1和L2正则化系数样本量不大时建议给一点压制树模型的方差。用同样的特征集跑随机森林做对比你会看到XGBoost在测试集上通常有更低的RMSE和MAE。但随机森林也不是没有优势它几乎没有需要精细调节的超参数训练速度更快在特征维度不高的时候表现稳定适合快速验证特征工程做得好不好。实际项目中我习惯先用随机森林跑一版确认特征有效、方向正确再切换XGBoost精调。3.3 LSTM的适用条件什么时候值得用深度学习模型LSTM在空气质量预测中属于进阶选择它理论上能捕捉更长的时间依赖但实际表现高度依赖数据量。郑州一个站点的逐小时数据一年大约8760条去掉缺失值可能只剩下7000多条这点数据量喂给LSTM效果通常不如XGBoost。我用LSTM的经验是至少要有3年以上的小时级数据否则模型学不到稳定的长期依赖波动大的时段很容易预测成“均值回归”。如果你确实想尝试LSTM需要注意它不是像XGBoost那样输入一行特征就输出一个预测而是需要把过去N个时间步组织成一个三维张量(样本数, 时间步长, 特征数)。import numpy as np from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout def make_sequences(data, target, seq_len24): X_seq, y_seq [], [] for i in range(len(data) - seq_len): X_seq.append(data.iloc[i:i seq_len].values) y_seq.append(target.iloc[i seq_len]) return np.array(X_seq), np.array(y_seq) X_lstm, y_lstm make_sequences(X, y, seq_len24) model Sequential([ LSTM(64, return_sequencesTrue, input_shape(X_lstm.shape[1], X_lstm.shape[2])), Dropout(0.2), LSTM(32), Dense(1) ]) model.compile(optimizeradam, lossmse)seq_len24表示用过去24小时的数据预测下一小时。两个LSTM层中间夹Dropout(0.2)是防止小数据量下过拟合的常用手段。用LSTM前务必对特征做标准化否则浓度和气象量纲差异会让训练过程极其不稳定。lossmse是回归任务的默认选择和XGBoost的评估口径保持一致。4. 搭建郑州空气质量预测模型训练流程、参数调节与效果评估4.1 最小可运行方案从数据到未来24小时预测的完整代码这个部分直接给一套能跑通的最小方案以XGBoost为主模型因为它综合效果稳定、训练速度快、调参空间明确。整条链路包括数据准备、特征构造、模型训练、未来24小时预测、结果保存。import pandas as pd import numpy as np import xgboost as xgb import joblib from sklearn.metrics import mean_absolute_error, mean_squared_error # 1. 加载数据假设已经做好清洗和特征工程 df pd.read_csv(zhengzhou_features.csv, parse_dates[time], index_coltime) feature_cols [c for c in df.columns if c ! PM2.5] X df[feature_cols].dropna() y df.loc[X.index, PM2.5] # 2. 按时间顺序划分训练集和测试集最近72小时做测试 split_time X.index[-72] X_train, X_test X[X.index split_time], X[X.index split_time] y_train, y_test y.loc[X_train.index], y.loc[X_test.index] # 3. 训练XGBoost并调参先用一组baseline参数 model xgb.XGBRegressor( n_estimators300, max_depth5, learning_rate0.05, subsample0.8, colsample_bytree0.8, reg_alpha0.1, reg_lambda1.0, random_state42 ) model.fit(X_train, y_train) # 4. 在测试集上评估 y_pred model.predict(X_test) print(fMAE: {mean_absolute_error(y_test, y_pred):.2f}) print(fRMSE: {mean_squared_error(y_test, y_pred, squaredFalse):.2f}) # 5. 未来24小时递归预测 future_df df.copy() for step in range(24): last future_df[feature_cols].iloc[-1:].fillna(0) next_pred model.predict(last)[0] next_time future_df.index[-1] pd.Timedelta(hours1) new_row {PM2.5: next_pred, hour_sin: np.sin(next_time.hour * 2 * np.pi / 24), hour_cos: np.cos(next_time.hour * 2 * np.pi / 24)} # 其他特征用最后已知值近似生产环境应接入气象预报数据 for col in feature_cols: if col not in new_row: new_row[col] future_df[col].iloc[-1] new_row pd.DataFrame([new_row], index[next_time]) future_df pd.concat([future_df, new_row]) future_forecast future_df[PM2.5].iloc[-24:]这段代码的关键在于递归预测部分预测下一小时需要用到滞后特征而滞后特征又依赖前一小时的预测值。如果不做递归、只用最后一行的已知数据预测24小时滞后特征的来源会断掉。这里用future_df.iloc[-1:]作为滚动窗口的输入每次预测完把结果追加回去让模型在每一步都能拿到自己上一步的输出作为特征。这是一种常见做法但要注意误差会随着递归步数增加而累积所以真实场景中12小时以内的预测可信度更高24小时参考价值要打折。4.2 XGBoost必调参数三个直接影响郑州秋冬季预测效果的关键选项XGBoost的参数多但不是每个都需要动。结合郑州秋冬季PM2.5浓度大幅波动的特点以下三个参数是你应该优先关注的max_depth控制树的深度。深度越大模型越能捕捉特征间的复杂交互但也越容易过拟合。对空气质量这种特征维度中等、数据量不大的场景max_depth设在4到6之间通常是安全的。郑州冬季的静稳天气会导致PM2.5浓度在几天内持续攀升这种长周期趋势依赖滞后特征和历史统计量max_depth太小学不到这种慢变量。subsample控制每轮迭代的样本采样比例。时间序列数据有自相关性相邻时刻的样本高度相似如果每轮都用全部样本模型容易对近期模式过拟合。subsample0.8表示每轮随机抽80%的样本相当于给训练过程加噪声缓解过拟合。从实际效果看这个参数对空气质量数据的稳定性帮助比colsample_bytree更明显。reg_alpha和reg_lambda是正则化系数。很多初学者容易忽略这两个参数但它们在郑州这种强季节性、强波动性的数据上作用很大。冬夏季的污染机制完全不同如果不加正则化树模型倾向于把全部精力放在学习冬季的极端高值上夏季的预测会明显偏高。给reg_alpha0.1、reg_lambda1.0可以有效压缩叶子节点的权重让模型不过度追求对高值样本的完美拟合。4.3 用GridSearchCV找参数组合时间序列交叉验证的正确姿势用GridSearchCV调参时有个大坑默认的KFold交叉验证是随机划分的用在时间序列上会引入未来信息得到的参数组合在实践中往往表现不佳。正确做法是使用TimeSeriesSplit它按时间顺序做前向划分每次训练集都在测试集之前。from sklearn.model_selection import TimeSeriesSplit, GridSearchCV param_grid { max_depth: [4, 5, 6], learning_rate: [0.03, 0.05, 0.1], subsample: [0.7, 0.8, 0.9], reg_alpha: [0.0, 0.1, 0.5] } tscv TimeSeriesSplit(n_splits4) grid GridSearchCV( estimatorxgb.XGBRegressor(n_estimators300, random_state42), param_gridparam_grid, cvtscv, scoringneg_mean_absolute_error, n_jobs-1 ) grid.fit(X_train, y_train) print(fBest params: {grid.best_params_})scoringneg_mean_absolute_error的选择有讲究MAE对异常值不敏感更能反映模型在大多数时段的典型误差而RMSE会被极端污染事件拉高。如果你更关心重污染时段的预报精度可以换成neg_mean_squared_error但要接受在普通时段的精度可能略有下降。n_jobs-1用满所有CPU核心XGBoost的网格搜索可以并行跑不用等得太痛苦。5. 郑州本地化落地避坑清单数据漂移、站点代表性与预测时效性5.1 坑一模型在测试集上表现好跨季度预测就翻车现象是用今年1到6月的数据训练在7月的测试集上预测效果不错但直接拿去预测12月的空气质量误差明显放大。原因是空气质量的时间分布不是平稳的郑州冬季PM2.5均值远高于夏季而且高值样本和低值样本的分布差异巨大模型没见过冬季的极端静稳过程自然推不准。解决思路是保证训练集覆盖至少一个完整的年度循环。如果数据只有半年就不要硬做跨季度预测对外输出时标注模型适用范围。训练时还要注意样本的时间排序不要把未来数据混进训练集这是时间序列建模里最基础也最致命的错误。5.2 坑二站点均值和具体站点数值差异过大现象是你按郑州市六个国控站点的平均值建模模型评估指标良好但拿去和某个具体站点的实时数据对比发现误差很大。原因是个别站点受局部污染源影响明显——比如站点靠近主干道早晚高峰的NO2和PM2.5会系统性偏高而城市均值把这个信息平均掉了。解决的办法是明确预测目标要么做城市整体预测评估时就用城市均值要么做站点预测建模时单独用目标站点的数据。不要混用。如果做站点预测建议把周围站点的浓度也作为特征加进去因为污染气团是区域性的周边站点的浓度变化能提供污染物传输方向的信息。5.3 坑三气象数据用的是历史观测值预测时拿不到“未来”的气象现象是训练时用的气象特征是从观测站拿到的实际数据准确完整但预测未来24小时时你手头并没有未来24小时的气象观测值只有气象预报值。这里的偏差来源不只是预报本身的误差还有两个数据集在时间粒度、更新频率上的不一致。解决思路是把这个问题拆成两段预测未来12小时可以直接用最近一次气象观测值作为近似因为气象变化在几小时内的连续性较强预测12到24小时建议接入气象预报数据或者直接降低对远端预测精度的预期。很多毕业设计在这一点上选择“作弊”——用未来时刻的真实气象数据去预测未来的空气质量这在论文里说得通但生产环境完全不可行。如果你的目标是做一个能真正跑起来的系统一定要在代码里给气象特征留出外部接口而不是硬编码历史值。5.4 坑四模型输出的负值、异常值和突变现象是XGBoost和LSTM这种回归模型在低浓度时段偶尔会给出负的PM2.5预测值物理上不成立导致后续计算AQI时报错。解决思路是在预测输出上加一个后处理层小于0的置为0超过一定阈值的做平滑处理。突变问题一般是递归预测中上一步误差被放大导致的可以用滑动窗口对输出做一次轻量平滑但要注意平滑不能太强否则会抹掉真实的污染过程起伏。def post_process(forecast_series, window3): forecast_series forecast_series.clip(lower0) return forecast_series.rolling(window, centerTrue, min_periods1).mean() smoothed_forecast post_process(future_forecast)clip(lower0)把所有负值修正为0保证物理合理性。rolling(window3, centerTrue)对连续三个预测值取平均能有效抑制单点突变又不会过度平滑。这个后处理方法在XGBoost和LSTM上都适用。5.5 坑五模型上线后的数据漂移现象是模型训练完成时效果很好过了几个月精度逐渐下降尤其在春末夏初转换季节表现最明显。原因是空气质量模型学的不仅是污染物与气象的关系还隐式学习了当时的生产活动、排放政策甚至交通模式。郑州的机动车保有量在增长、工业企业的排放标准在收紧这意味着同一套特征和浓度之间的关系一直在缓慢变化。解决思路是建立模型月度重训机制。可以每天记录新到的监测数据每月底用最近12个月的数据重新训练一次模型验证集用最近一周的数据如果MAE比当前模型高超过10%就触发模型替换。这个机制不必做成自动化但要有——哪怕一个月手动跑一次训练脚本也比让模型一直带病运行强得多。6. 把预测结果做成可交付的东西模型持久化、定时任务与结果解释到这一步你已经有了一个能用的预测模型但还差把它变成“别人能用”的形态。这节讲三个具体技巧模型持久化和热加载、定时执行预测的脚本写法、以及让预测结果对非技术用户可读的输出方式。模型持久化用joblib把训练好的模型和特征列顺序一并保存。需要特别注意的是特征列表feature_cols必须和模型绑定保存否则加载模型后很容易出现特征顺序不一致的情况——模型输入的特征是按位置解析的顺序错了预测结果没有任何意义。import joblib # 训练完成后保存模型和特征清单 joblib.dump(model, zhengzhou_pm25_model.pkl) joblib.dump(feature_cols, zhengzhou_feature_cols.pkl) # 预测服务启动时加载 model joblib.load(zhengzhou_pm25_model.pkl) feature_cols joblib.load(zhengzhou_feature_cols.pkl) def predict_next_hours(input_df, hours24): input_df input_df[feature_cols] predictions [] for _ in range(hours): pred model.predict(input_df.iloc[-1:].fillna(0))[0] predictions.append(pred) # 更新滞后特征其他特征保持不变 input_df.loc[input_df.index[-1] pd.Timedelta(hours1)] input_df.iloc[-1] input_df.iloc[-1, input_df.columns.get_loc(PM2.5_lag1)] pred return predictions定时执行用操作系统的定时任务最简单。在 Linux 服务器上写进 cron在 Windows 上用任务计划程序每天固定时间拉取最新监测数据、重新生成特征、跑一遍预测、把结果写成CSV或者推送到内部看板。Python 代码本身不需要常驻按需执行更省资源。结果解释对非技术用户很重要。不要只给一个数字“PM2.5预测值85”而是转换成可读的结论例如“未来24小时郑州市PM2.5浓度预计在70到100微克每立方米之间波动整体处于良到轻度污染级别早晚高峰时段浓度较高敏感人群建议减少户外活动”。这种输出方式的实现不算复杂def generate_advice(forecast_series): mean_val forecast_series.mean() max_val forecast_series.max() if max_val 75: level 优到良 advice 空气质量较好适合户外活动 elif max_val 115: level 轻度污染 advice 敏感人群减少长时间户外运动 else: level 中度及以上污染 advice 建议佩戴口罩减少开窗通风 return f未来24小时郑州PM2.5均值约{mean_val:.0f}峰值约{max_val:.0f}等级{level}。{advice}。最后说一个我自己的习惯每次模型训练完我会把当次的划分区间、数据量、MAE、RMSE和特征列表一起保存到一个训练日志里方便回溯哪个版本的模型为什么改参数、为什么效果变差。这个习惯帮我省过不少排查时间——空气质量的季节性变化本来就大如果没有记录两个月的模型效果差异你根本说不清是季节导致的还是特征改动导致的。希望这些流程和坑位整理能帮你在郑州的空气质量预测上少走弯路。本文还有配套的精品资源点击获取
返回列表