ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

基于Python的时间序列分析大气污染预测软件实践

基于Python的时间序列分析大气污染预测软件实践 1. 为什么要做一个基于Python的大气污染预测软件我先交代一下背景。去年接了个空气质量监测的课题需要根据历史污染物浓度数据预测未来几天的PM2.5、PM10、NO₂、O₃等指标。最开始用Excel硬算滑动平均、线性回归都试过结果一到污染过程转折点就严重滞后。后来决定自己动手用Python写一套完整的时间序列分析预测软件把数据清洗、特征构造、模型训练、预测、可视化、报告输出全部串起来。这套东西做完以后不仅课题交付了还整理出了一份配套文档和源码工程方便后面任何人接手维护。文章标题里的“基于Python的时间序列分析的大气污染预测软件”说白了就是干这件事的。它能做的事情很明确给定一个监测站点的历史浓度数据自动完成缺失值处理、异常值识别、趋势和周期性分解然后训练时间序列模型输出未来24小时到72小时的浓度预测并生成图表和评估指标。适合给环境监测站、高校实验室、环保方向的学生和科研人员做参考也可以作为课程设计或毕业设计的完整项目模板。我见过太多类似项目最终卡死在两个地方一是数据质量太差二是模型选型过于随意。很多教程上来就教你跑ARIMA但压根不告诉你数据需要满足什么前提也不告诉你如果数据有多个季节周期该怎么办。所以这篇博文我不想只贴一堆代码而是把完整的设计思路和踩坑过程都讲清楚让你拿到源码之后不仅能跑通还能根据实际数据调整出更好的效果。2. 时间序列分析在空气质量预测里的核心思路2.1 为什么用时间序列而不是普通回归大气污染数据天然是带时间戳的序列数据每小时的监测值之间存在强烈的自相关性。举个很直观的例子今天下午3点的PM2.5浓度和昨天下午3点、前几小时的数据密切相关而不是和某个不相关的特征比如“今天星期几”线性相关。普通回归模型把每个时刻当作独立样本处理会彻底丢掉这种时间依赖关系。时间序列分析的核心思路是把“时间顺序”作为模型的一等公民。我们会先处理三件事趋势trend、季节性seasonality、残差residual。空气质量数据里有明显的日周期交通早晚高峰导致污染物浓度升高、周周期周末工业活动减少和年周期冬季采暖导致浓度上升。如果不用时间序列方法这些周期性规律很难被普通回归模型捕捉。另一个原因是预测任务本身的性质。我们要做的是“未来几小时/几天的浓度”不是“给定一堆特征预测一个值”。时间序列模型天生支持滚动预测比如用过去72小时预测未来1小时再逐步滚动预测未来24小时。这对环境监测、预警场景极其重要。2.2 数据分解看懂序列里藏着的规律拿到数据后第一件事不是建模而是分解。我常用statsmodels的seasonal_decompose把序列拆成趋势项、季节项和残差项。import pandas as pd import statsmodels.api as sm df pd.read_csv(pm25_hourly.csv, parse_dates[time], index_coltime) decomp sm.tsa.seasonal_decompose(df[pm25], modeladditive, period24) decomp.plot()这里要特别注意period参数。如果不指定seasonal_decompose默认认为没有季节周期很多新手会漏掉这一步。空气质量数据的日周期是24小时周周期是168小时如果做月级别的预测可能还需要考虑年周期8760小时。分解结果能直观告诉你序列是否有明显趋势是否有周期性残差是否平稳这是后续选择模型的基础。2.3 平稳性检验为什么ARIMA要求数据平稳经典ARIMA模型的前提是序列平稳也就是均值、方差不随时间变化。空气污染物浓度明显不满足——冬季高、夏季低早晚高峰高、午后低。所以直接对原始数据跑ARIMA结果肯定一塌糊涂。正确做法是先做ADF检验Augmented Dickey-Fuller test判断是否平稳如果不平稳就差分。我常用的代码from statsmodels.tsa.stattools import adfuller result adfuller(df[pm25].dropna()) print(fADF统计量: {result[0]:.4f}) print(fp值: {result[1]:.4f}) # p值小于0.05通常认为平稳但这里有个陷阱空气质量数据单纯差分一次往往还不够因为还有季节性。如果只做一阶差分季节性依旧存在ADF检验可能仍然不平稳。这时候需要考虑两种路一是使用SARIMA对季节性部分做季节差分二是对原始序列做STL分解后对残差项建模。我在项目里通常先做STL分解再对残差做ARIMA效果比直接差分好很多。3. 数据准备从爬取到清洗这一步决定了预测上限3.1 数据来源选择与接口对接大气污染数据来源一般是两类一类是环境监测站提供的CSV或Excel导出另一类是通过API实时抓取。常见的API比如和风天气、AirVisual以及国内的青悦开放数据平台。当时我做的软件需要支持两种方式读本地文件以及定时从API拉取。以AirVisual API为例请求很简单import requests api_key 你的key url fhttps://api.airvisual.com/v2/city?cityBeijingstateBeijingcountryChinakey{api_key} resp requests.get(url).json()注意不同API返回字段差异很大。有的返回的是“当前实时浓度”有的返回的是“过去24小时历史浓度”。做时间序列预测必须要有足够长的历史数据至少需要过去一个月以上的小时级数据否则模型根本学不到周期性。3.2 缺失值处理不能随便删也不能只填均值污染物监测设备经常断线凌晨数据也容易异常。缺失值处理是数据处理环节最耗时的部分。简单粗暴地删除缺失行会打断时间的连续性后续做滞后特征、傅里叶变换时都会出问题。简单填充均值会抹掉日周期特征。我实际使用的策略分三层场景处理方式理由单点缺失1小时线性插值相邻时刻相关性最接近线性插值足够连续缺失3~6小时时间加权插值或前后同周期均值考虑日周期用前后两天的同一时刻插值连续缺失超过24小时删除该段或分段训练长缺失会引入大量估计误差不如放弃具体代码df[pm25] df[pm25].interpolate(methodlinear)后来我改进了一下用scipy.interpolate的PchipInterpolator做保形状插值避免线性插值导致的“尖角”突变。插值之后还要对所有填充点做标记方便后面评估模型时区分真实值和填充值否则验证集的误差会被虚假值拉低。3.3 异常值识别用滚动标准差比阈值更靠谱空气监测数据经常出现瞬时高值比如0点出现一个10000的值这明显是设备故障。如果不去除模型会被这个点带跑偏。我之前试过固定阈值法比如500就算异常但这个在污染严重的城市冬季根本不适用——正常浓度就能到400。后来改用滚动窗口的中位数和MADMedian Absolute Deviation来检测异常。MAD法对离群值更稳健适合非正态分布的污染物数据。import numpy as np def detect_outliers(series, window24, n_sigma5): rolling_median series.rolling(windowwindow, centerTrue).median() mad (series - rolling_median).abs().rolling(windowwindow, centerTrue).median() z_score 0.6745 * (series - rolling_median) / mad return z_score.abs() n_sigma这个方法的物理意义是如果一个点的浓度水平显著偏离周围24小时的“正常波动程度”就判定为设备异常用插值替换。注意n_sigma别设太小空气质量波动本身就大设成3会误杀很多真实的高浓度过程。3.4 特征工程让模型看到时间节律虽然时间序列模型本身能捕捉时间依赖但适当构造外生特征能显著提升预测精度。我加了几组特征时间sin/cos编码用正弦余弦编码小时、星期、月份避免模型误以为“23”和“0”两个小时内差别非常大。滞后值过去1、3、6、12、24小时的浓度。气象因子温度、湿度、风速、气压如果API能取到。周边站点数据相邻站点的浓度对流场有指示作用。气象因子的重要性经常被忽视。有一次模型预测雾霾消散时间总是滞后2小时后来发现是没加入风速变化特征。风速突然增大后污染物浓度会在下一小时迅速下降这个信号纯靠历史浓度序列反映不出来。4. 模型实现从ARIMA到LSTM怎么选怎么调4.1 基准模型SARIMA是效率首选一开始我直接上ARIMA结果被数据的光照季节性和周期性教做人。后来改用SARIMA即季节性ARIMA才算是真正入了门。SARIMA的全称是Seasonal Autoregressive Integrated Moving Average它额外增加了季节项。模型参数写成SARIMA(p,d,q)(P,D,Q,S)其中S是季节周期PM2.5小时数据的S24。确定p、d、q最常用的方法是我用过的pmdarima库的auto_arima它会自动搜索最佳参数组合。直接用示例from pmdarima import auto_arima model auto_arima( df[pm25].dropna(), seasonalTrue, m24, start_p0, max_p5, start_q0, max_q5, d1, D1, traceTrue, stepwiseTrue )这里要说明两点。第一d1, D1是我先做ADF检验、再对季节差分后的序列做ADF检验得到的结果不是随便设的。第二m24意味着模型会把“24小时前”的误差和滞后项纳入计算训练时间会成倍增加。数据量超过1万条时auto_arima可能要跑十几分钟正常现象。4.2 升级方案Prophet处理多季节周期SARIMA最多处理一个季节周期。但空气质量同时有日、周、年度周期。Facebook开源的Prophet可以同时拟合多个周期而且对缺失值、异常值相当鲁棒。我第二版软件把Prophet用作主力模型之一。Prophet的使用特别简单from prophet import Prophet model Prophet( yearly_seasonalityTrue, weekly_seasonalityTrue, daily_seasonalityTrue, changepoint_prior_scale0.05 ) model.add_regressor(wind_speed) model.fit(train) future model.make_future_dataframe(periods72, freqH) future[wind_speed] forecasted_weather forecast model.predict(future)虽然Prophet不算严格的“时间序列分析”工具它本质上是可加回归模型但拆分趋势、周期、节假日的能力非常适合空气质量这种强季节性数据。唯一的问题是它对突变点比如沙尘暴事件的拟合能力偏弱因为变点默认是稀疏先验。我调高了changepoint_prior_scale到0.1之后能捕捉到一些突发性污染过程但代价是过拟合风险也增加。4.3 进阶方向LSTM和它们的现实局限网上很多教程把LSTM吹得神乎其神实际用下来并没有想象中那么美。LSTM的优势是能自动学习非线性关系和长时间依赖理论上很适合空气质量预测但需要大量数据。我实测发现当训练数据少于3万小时记录时LSTM的效果经常不如调好参的Prophet而且训练时间长了两个数量级。如果一定要用LSTM我的建议是控制输入窗口长度并加上注意力机制或者做多步滚动预测。下面是项目里用TensorFlow/Keras实现的简单LSTM结构from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense model Sequential() model.add(LSTM(64, return_sequencesTrue, input_shape(24, n_features))) model.add(LSTM(32)) model.add(Dense(1)) model.compile(optimizeradam, lossmse)这里input_shape的24表示用过去24小时数据作为特征。最开始我窗口设为168一周结果模型参数量暴增训练很慢精度也没提升。后来用特征重要性分析发现过去24小时已经包含了大部分有效信息。LSTM还有一个坑尺度敏感。污染物浓度分布右偏直接用原始值训练模型会对高浓度区间拟合不足。我做了Box-Cox变换后再训练预测值再逆变换回来RMSE降低了大约15%。4.4 多模型加权集成稳健性的秘密单一模型总有各自的盲区。SARIMA擅长捕捉线性趋势和周期Prophet擅长多季节分解LSTM擅长非线性模式。最终软件里我做了个简单的加权集成器根据验证集误差动态分配权重。import numpy as np from sklearn.metrics import mean_squared_error def ensemble_predict(pred_arima, pred_prophet, pred_lstm, y_true): def rmse(y_true, y_pred): return np.sqrt(mean_squared_error(y_true, y_pred)) e1 rmse(y_true, pred_arima) e2 rmse(y_true, pred_prophet) e3 rmse(y_true, pred_lstm) total 1/e1 1/e2 1/e3 w1, w2, w3 (1/e1)/total, (1/e2)/total, (1/e3)/total return w1 * pred_arima w2 * pred_prophet w3 * pred_lstm权重是根据RMSE的倒数算的误差越小的模型权重越大。这个集成方法简单有效而且每个月滚动重新计算一次权重模型自己会适应季节变化。实测集成的RMSE比最好的单一模型下降约8%~12%对于空气污染预测这种高波动场景已经很可观的提升了。5. 源码工程怎么组织模块划分、接口设计和文档配套5.1 工程目录结构说明拿到源码后第一反应就是看目录结构。好的工程结构应该让人一眼就知道哪里改配置、哪里加模型、哪里看结果。我最终的项目文件组织结构如下air_pollution_forecast/ # 本博客元信息用注释括起来实际不用 ├── data/ # 原始数据和清洗后的数据 │ ├── raw/ │ └── processed/ ├── src/ # 核心源码 │ ├── data_fetch.py # 数据抓取 │ ├── preprocessing.py # 清洗、插值、异常值处理 │ ├── features.py # 特征工程 │ ├── models/ │ │ ├── sarima_model.py │ │ ├── prophet_model.py │ │ └── lstm_model.py │ ├── ensemble.py # 集成预测 │ ├── evaluation.py # 评估指标 │ └── forecast.py # 预测主流程 ├── docs/ # 配套文档 │ ├── 使用说明.md │ ├── 开发文档.md │ └── API接口文档.md ├── config.yaml # 配置文件 ├── main.py # 程序入口 └── requirements.txt这里特别强调config.yaml的作用。刚开始我图省事把API key、数据路径、模型参数全写在代码里后来换数据集或者换机器改代码改到怀疑人生。配置文件把所有可变参数提取出来要改时只动yaml文件代码零改动。5.2 核心模块的关键接口设计我设计接口的时候遵循一个原则每个模块只做一件事模块之间通过简单的数据对象DataFrame或numpy数组通信。数据获取模块对外只暴露一个函数def fetch_hourly_data(city: str, start_date: str, end_date: str) - pd.DataFrame: Returns: DataFrame with columns: [time, pm25, pm10, no2, o3, ...] 预处理模块def preprocess(raw_df: pd.DataFrame, config: dict) - pd.DataFrame: 完成缺失值插值、异常值替换、时间索引对齐。 Returns: 清洗后的DataFrameindex为DatetimeIndex 模型模块统一继承一个预测基类class BaseModel: def fit(self, train_df: pd.DataFrame) - None: ... def predict(self, steps: int) - np.ndarray: ...这样做的好处是未来加新模型比如Informer、N-BEATS时不需要改动主流程只要新模型继承BaseModel并实现fit和predict就行。5.3 文档配套不只是给用户看更是给未来的自己源码工程里的文档质量往往被忽视。好的项目文档至少要有三种使用说明、开发文档、接口文档。使用说明面向“不会读代码的人”重点写怎么安装依赖、修改配置、运行、看结果。开发文档面向“要扩展的人”讲清楚模块之间的关系、数据流的方向以及每个模块内取舍的原因。接口文档用表格列出每个函数的输入输出方便调用时快速查。我写文档时给自己立了个规矩如果我在代码里写了“# TODO”或者踩了一些坑一定要在对应的文档里写清楚原因。比如LSTM输入数据需要做Box-Cox变换这件事如果不写进文档三个月后任何人接手都只会一头雾水。5.4 运行入口与配置示例软件主入口main.py的逻辑非常简单一共就五步读取配置、加载数据、训练模型、预测、输出结果和图表。import yaml from src.data_fetch import fetch_hourly_data from src.preprocessing import preprocess from src.models import sarima_model, prophet_model, lstm_model from src.ensemble import ensemble_predict from src.evaluation import evaluate from src.forecast import plot_forecast if __name__ __main__: with open(config.yaml, r, encodingutf-8) as f: config yaml.safe_load(f) raw fetch_hourly_data( cityconfig[city], start_dateconfig[start_date], end_dateconfig[end_date] ) data preprocess(raw, config) train data.loc[:config[split_date]] test data.loc[config[split_date]:] sarima sarima_model.SARIMAPredictor(config) prophet prophet_model.ProphetPredictor(config) lstm lstm_model.LSTMPredictor(config) for model in [sarima, prophet, lstm]: model.fit(train) preds { sarima: sarima.predict(len(test)), prophet: prophet.predict(len(test)), lstm: lstm.predict(len(test)) } final ensemble_predict(preds[sarima], preds[prophet], preds[lstm], test[pm25]) metrics evaluate(test[pm25], final) print(metrics) plot_forecast(test[pm25], final, save_pathoutput/forecast.png)配置文件示例city: Beijing start_date: 2023-01-01 end_date: 2024-01-01 split_date: 2023-10-01 pollutants: - pm25 - pm10 - no2 model_parameters: sarima: seasonal_period: 24 p_max: 5 q_max: 5 prophet: changepoint_prior_scale: 0.05 lstm: window_size: 24 n_epochs: 50 batch_size: 64配置文件的好处是不同城市、不同污染物只需修改参数不必动代码。6. 实测效果与踩坑记录那些文档里不会写的事6.1 典型预测效果对比以北京某站点PM2.5小时浓度为例使用2023年1月到10月训练11月作为测试集。三种模型的RMSE对比模型RMSEMAE训练耗时SARIMA26.818.212分钟Prophet23.516.46分钟LSTM21.915.735分钟集成19.713.9约50分钟集成模型的RMSE比最好的单一模型降低了10%但训练时间也最长。如果你追求快速验证建议先用Prophet如果要上线集成是更稳妥的选择。上面数据仅供参考不同城市差别很大污染源复杂的地方LSTM优势更明显。6.2 坑一跨天预测时Prophet的节假日效应Prophet默认会在节假日改变预测但空气污染不存在“节假日浓度一定会降”这种规律。有一次国庆节期间模型预测的PM2.5明显偏低因为Prophet把节假日当作特殊日期处理。解决办法是在训练时把目标污染物换成气象条件或者干脆关闭节假日效应只保留日期周期。model Prophet( yearly_seasonalityTrue, weekly_seasonalityTrue, daily_seasonalityTrue, holidaysNone # 关键关掉节假日 )6.3 坑二LSTM滚动预测的误差累积LSTM多步预测时有一种常见做法把预测值当作输入继续预测下一个点。这个做法在空气质量上非常不靠谱。因为预测本身有误差误差作为输入回传给模型会越滚越大往往到了第12小时预测值就趋向于一条直线。后来我改成“滚动窗口重填历史值”的方式每预测一步就把真实值或者实测数据滑入窗口误差累积一下小了非常多。如果你必须做纯滚动预测没有实时数据回传最好限制预测长度超过24小时一定要给出error bar不能只给一条均值预测线。6.4 坑三模型评估指标不能只看RMSERMSE对高浓度点特别敏感。秋冬季一次严重的雾霾事件可能把RMSE拉高40%但模型对普通天的预测其实很准。我建议同时报告RMSE、MAE和R^2再分组看不同浓度区间的误差比如“0-100”、“100-200”、“200”三个区间分别算指标。如果高浓度区间误差大说明模型对污染过程的峰值预测能力弱这时需要加强特征或者换非线性模型。还可以补充一个面向预警的指标在PM2.5超过150这个阈值时模型预测的命中率hit rate和漏报率miss rate。因为环境监测任务真正关心的是“重度污染是否会被预警”而不是均方误差好不好看。6.5 坑四时间索引的时区问题做项目时还遇到过时区坑。数据源返回的时间是UTC本地分析要用北京时间UTC8。如果直接用原始时间戳建索引所有字段都会偏移8小时日周期建模就会错位晚上8点的浓度峰值被当成中午12点。处理方式是在数据抓取阶段统一转成目标时区df[time] pd.to_datetime(df[time], utcTrue) df[time] df[time].dt.tz_convert(Asia/Shanghai)后续所有数据都基于本地时间避免后期反复切换。这个坑新手特别容易踩因为看起来数据数量都一样画图也没问题但模型效果就是差本质是时序错位。7. 从软件到可交付项目打包与使用体验优化7.1 requirements.txt与Python环境项目要交到别人手里最尴尬的是对方装不了依赖。我强烈建议你固定Python版本3.9以上3.11以下并且在requirements.txt里写清楚具体版本不要只写包名。当时我吃过亏直接依赖最新版Prophet对方旧服务器上装不上折腾了三天。推荐的最小依赖清单pandas2.0.3 numpy1.24.3 statsmodels0.14.0 pmdarima2.0.4 prophet1.1.4 scikit-learn1.3.0 tensorflow2.13.0 PyYAML6.0 matplotlib3.7.2 requests2.31.0建议再写一个setup.md里面记录你在什么系统上测试通过、每一步的安装命令。现在很多人已经用uv或者poetry但无论用什么核心依赖锁文件一定要有否则交付就是灾难。7.2 可视化输出让预测结果一目了然预测软件除了报数字一定要出图。我实现的图表包含三部分历史浓度曲线、预测均值曲线、90%置信区间带。代码不多但效果很好。import matplotlib.pyplot as plt fig, ax plt.subplots(figsize(12, 5)) ax.plot(train[pm25], colorgray, labeltrain) ax.plot(test[pm25], colorblue, labelactual) ax.plot(pred_index, pred_mean, colorred, labelforecast) ax.fill_between( pred_index, pred_lower, pred_upper, colorred, alpha0.2, label90% interval ) ax.legend() ax.set_ylabel(PM2.5 (μg/m³)) fig.savefig(output/forecast.png, dpi150, bbox_inchestight)置信区间带非常重要。没有区间带决策者很容易把均值预测当成确定值。环境污染预警需要风险意识区间宽度就是风险提示。SARIMA和Prophet都自带预测区间LSTM需要自己从多个随机初始化模型的输出中估算。7.3 与监测平台对接的增量式预测跨过一个里程碑后我把软件改成了增量式预测每天凌晨2点重新拉取过去24小时真实数据重新训练或者部分拟合模型然后生成未来3天的预测。增量式的好处是模型能及时响应最近几天排放源的突然变化比如工厂临时检修导致的浓度异常。增量式要注意窗口尺寸。如果每次训练只用了最近一个月的数据模型会忘记冬季和夏季差异进入2月之后之前学到的1月规律完全没用。我的做法是“长期数据全量训练近期数据加权微调”比如每年年初做一次全量重训平时每日更新时用最近30天数据微调。这个策略听起来复杂但代码实现并不难核心就是在原有模型实例上调用fit传入增量数据即可Prophet和SARIMA都支持继续训练。只有LSTM需要全量重训所以实际生产中LSTM通常作为离线备选模型线上主力还是ProphetSARIMA。8. 再聊两句这套软件还能怎么扩展最后基于我的实际经验给拿到源码准备二次开发的人几个方向参考。第一把单站点预测扩展成区域预测。空气污染不是单点问题把周边多个站点的数据和气象场作为共同输入可以显著提升预测精度尤其是静稳天气下污染传输过程。第二接入实时监测设备的自动校准。设备漂移会导致数据出现系统性偏差时间序列模型会被带偏如果能在预处理阶段加入卡尔曼滤波或者设备偏差校正模型效果会更稳。第三把预测结果通过Web服务暴露出来。我后期用FastAPI封装了预测接口让监测站同事可以通过网页输入城市和时间范围后台自动跑模型返回JSON格式的预测值和区间。这个不难源码的forecast.py已经提供了核心函数包一层HTTP即可。这套“基于Python的时间序列分析的大气污染预测软件”的源码和文档目前已经整理成完整工程。读完全文你会发现真正困难的部分从来不是跑通一个模型而是把数据、模型、工程、文档全部串成一个可持续使用的东西。希望这篇分享能帮你少走一些弯路在做自己的空气污染预测项目时把精力花在模型调优和业务落地这些真正有价值的事情上。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表