
简介这是一套基于机器学习的PM2.5预测与分析系统包含完整源代码与项目文档面向计算机、人工智能、自动化等专业的毕设、课设或项目实践人群。压缩包共299个文件约4.19MB以Python脚本为核心配合HTML/CSS/JavaScript前端页面、PNG/GIF可视化素材和Markdown说明文档涵盖数据清洗、特征工程、模型训练与结果展示等完整流程。系统基于近80万条民间空气质量数据展开特征分析构建预测模型代码经测试运行成功作者答辩平均分达96分实用性与完成度较高。目前已有494人学习下载适合正在做相关课题的学生参考也可在此基础上修改扩展用于课程作业或初期项目演示。资源附带README等说明文件便于快速上手和理解整体架构包括数据预处理与模型评估等模块。1. 为什么大家都在做基于机器学习的PM2.5预测先问能不能落地每年秋冬季环保部门、健康管理App和工厂安环科的同事都会盯着同一个问题明天的PM2.5会到多少。以前靠气象预报员经验加数值模型现在越来越多的团队改用机器学习来做这件事。所谓“基于机器学习的PM2.5预测与分析系统”本质上就是一条完整的数据流水线——从空气质量历史数据出发经过清洗、特征工程、模型训练与评估最终输出未来若干小时的浓度预测再把结果可视化或接入业务系统。它不替代空气质量数值模型但胜在训练成本低、迭代快对市级站点尺度的短期预测表现足够好。适合谁手里有一批历史监测数据、想快速搭建预测能力的数据工程师以及正在做课程设计或毕业设计、需要一个能讲清全流程的机器学习项目的同学。2. 数据准备拿到一份PM2.5历史数据后先解决“脏、乱、缺”三件事任何一个真实落地的机器学习系统数据清洗占掉的时间一定比模型训练多。PM2.5预测也不例外。常见的数据来源是各城市空气质量监测站点的逐小时记录字段一般包含时间、PM2.5浓度、PM10、SO₂、NO₂、CO、O₃以及温度、气压、湿度、风速等气象变量。注意这不是一份整理好的CSV而常常是多个站点分文件、时间戳格式不统一、缺测率超过两成的原始记录。拿到数据后不要急着建模先把“脏、乱、缺”按顺序处理掉。2.1 数据清洗的顺序缺失值、异常值、重复值谁先处理我的习惯顺序是先去重再修异常值最后插补缺失值。先看重复值——某个站点因为设备回传重试同一小时可能写了两条记录直接drop_duplicates按时间戳和站点ID去重。异常值要在插补缺失值之前处理因为一个“看起来正常”的异常点会污染插补的邻域参考值。import pandas as pd import numpy as np df pd.read_csv(pm2_5_hourly.csv, parse_dates[datetime]) df df.sort_values(datetime).drop_duplicates(subsetdatetime, keeplast) # 异常值PM2.5不可能为负不可能高于1500风速为0且浓度骤变为0一般是传感器故障 df.loc[df[PM2.5] 0, PM2.5] np.nan df.loc[df[PM2.5] 1500, PM2.5] np.nan bad_mask (df[WSPM] 0) (df[PM2.5] 0) df.loc[bad_mask, PM2.5] np.nan # 缺失值时间序列用线性插值气象连续变量不建议用均值硬填 df[PM2.5] df[PM2.5].interpolate(methodlinear, limit_directionboth) df[TEMP] df[TEMP].interpolate(methodtime) print(df.isnull().sum())逻辑说明先把物理上不可能的值置为缺失再做插补能避免“异常点带偏插补曲线”的连锁问题。interpolate(methodlinear)按等间隔时间序列做线性插值对PM2.5这种小时级缓变变量够用methodtime则适合时间间隔不完全均匀的数据按时间间隔加权。参数上要注意limit_directionboth否则首尾的缺失值不会被填补下游训练时又会丢一段样本。2.2 气象特征合并多源数据对齐的口径与时区坑PM2.5浓度受气象条件影响很大所以通常要把监测站的污染物数据和气象站数据合并。这里最大的坑是时间口径不一致污染物数据是北京时间的整点气象数据可能来自全球再分析数据集存的是UTC时间。别小看这8小时偏移——直接合并会导致特征和标签错位模型学到的全是“错误对应关系”。weather pd.read_csv(weather_hourly.csv, parse_dates[time_utc]) weather[datetime] weather[time_utc] pd.Timedelta(hours8) # UTC - 北京时间 weather weather.rename(columns{t2m: TEMP, rh2m: DEWP, wspd: WSPM}) df df.merge( weather[[datetime, TEMP, DEWP, WSPM, PRES]], ondatetime, howleft, ) df df.dropna(subset[PM2.5]).fillna(methodffill, limit3)逻辑说明howleft保证以污染物监测数据为主表气象数据缺失时不会把整行样本丢掉ffill限3小时是考虑到气象站数据偶尔回传延迟超过3小时还补不上说明那段时间两套系统都异常宁缺毋滥。合并后务必做一次抽样检查随机打印20行看PM2.5和气象字段的日期时间是否在同一小时这是很多人跳过但能提前暴露时区问题的关键动作。2.3 机器学习中的数据处理是什么把“能用”的数据加工成“好用”的数据很多人会问机器学习中的数据处理到底指什么。顺着这个项目说就三件事对齐、清洗、转换。对齐指的是时间戳统一、多表合并、去除重复清洗是上面做的缺失值插补和异常值剔除转换则是把原始观测值变成模型能直接消费的结构——包括后续要讲的时间特征、滞后特征、分类变量编码。这三步做扎实后面建模就是顺水推舟做不扎实模型调参调得再好精度也上不去。整个机器学习应用流程里数据准备通常占六成以上的工作量这不是夸张而是血泪经验。3. 特征工程把时间序列变成机器学习问题的72维特征向量PM2.5预测本质上是一个监督学习问题用过去几小时的气象和浓度数据预测未来1小时或24小时的浓度。但原始表格里只有一个时间戳和若干观测值模型看不出“昨天同时段的浓度”“过去6小时的变化趋势”这些关键信息。特征工程就是把这些时间结构显式地构造出来。我一般会把特征分成四组滞后特征、滑动窗口特征、日历特征、气象交叉特征合计构造60到80维。3.1 滞后特征与滑动窗口喂给模型“最近发生了什么”PM2.5浓度有很强的自相关性——今天的浓度和昨天同一时刻的浓度相关性极高污染过程通常是持续累积的。滞后特征lag就是把过去第1小时、第3小时、第6小时、第12小时、第24小时的浓度作为当前样本的输入。滑动窗口特征则是对过去一段时间的浓度做均值、标准差描述“近期趋势和波动幅度”这对重污染过程的起止判断很有帮助。for lag in [1, 2, 3, 6, 12, 24]: df[fpm25_lag_{lag}] df[PM2.5].shift(lag) for win in [3, 6, 12, 24]: df[fpm25_roll_mean_{win}] df[PM2.5].rolling(windowwin).mean() df[fpm25_roll_std_{win}] df[PM2.5].rolling(windowwin).std() # 用差分特征描述“最近3小时是上升还是下降” df[pm25_diff_3h] df[PM2.5] - df[pm25_lag_3] df df.dropna()逻辑说明shift(lag)把目标列向上平移让当前时刻的样本能“看到”过去的值。rolling(windowwin).mean()做的是移动平均窗口越大曲线越平滑捕捉的趋势周期越长。参数选择上滞后点覆盖1、2、3、6、12、24小时对应PM2.5的短时波动和日周期窗口选3、6、12、24小时基本覆盖一次污染累积和消散过程。dropna()必须放在最后——所有滞后和滚动操作都会在序列头部产生缺失值直接删掉前24小时的样本即可不必插补。3.2 日历特征与气象交叉特征抓住周期和累积效应PM2.5有明显的周期性一天之内早晚高峰浓度高一周之内周末和工作日不同一年之内秋冬季远高于夏季。这类周期信号模型自己学得慢直接编码成特征喂进去效率更高。气象交叉特征则是把风速、湿度组合起来——湿度高、风速低的静稳天气条件下污染物最容易累积这是重污染过程最常见的成因。df[hour] df.index.hour df[weekday] df.index.weekday df[month] df.index.month df[is_weekend] (df[weekday] 5).astype(int) # 季节春1 夏2 秋3 冬4 df[season] (df[month] % 12 // 3) 1 # 交叉特征风速与湿度交互、气压变化趋势 df[wspm_x_dewp] df[WSPM] * df[DEWP] / 100.0 df[pres_diff_6h] df[PRES] - df[PRES].shift(6)逻辑说明hour、weekday、month用整数编码即可树模型能自行处理非线性切分season的计算方式(month % 12 // 3) 1把12个月均匀映射到1到4。交叉特征wspm_x_dewp是我比较依赖的一个——单纯风速低不一定导致高浓度但“风速低且湿度高”同时出现时静稳天气的概率显著上升这个交互信息单靠模型从原始特征里学出来需要更多数据。气压6小时差分描述的是天气系统过境带来的通风条件变化。3.3 特征筛选别让30个无关特征拖垮在线推理速度特征不是越多越好。有些特征比如PM10的滞后项和PM2.5高度相关留着没问题但有些特征比如站点编号、无关的传感器读数只会增加过拟合风险。我的做法分两步先用相关性粗筛再用递归特征消除RFE做精筛。粗筛的目标是把明显无关的特征去掉减少后续交叉验证的运算量。corr df.select_dtypes(include[np.number]).corr()[PM2.5].abs().sort_values(ascendingFalse) keep_cols corr[corr 0.15].index.tolist() print(粗筛后特征数, len(keep_cols)) from sklearn.feature_selection import RFE from sklearn.ensemble import RandomForestRegressor X df[keep_cols].drop(columns[PM2.5]) y df[PM2.5] selector RFE(RandomForestRegressor(n_estimators100, random_state42), n_features_to_select30) selector.fit(X, y) selected X.columns[selector.support_].tolist() print(RFE 精选后特征数, len(selected))逻辑说明相关性粗筛用皮尔逊系数绝对值大于0.15做阈值过滤掉几乎与目标无关的列RFE 用随机森林做基模型每次迭代删除重要性最低的特征。n_features_to_select30是我的经验值——在站点级数据上30个左右的特征通常已经能覆盖滞后、窗口、日历和气象四大类信息再多对精度提升有限反而增加模型体积和推理耗时。注意RFE必须用训练集的数据做拟合不能拿全量数据筛选后再划分训练集否则同样有信息泄露。4. 模型选型与训练从线性回归基线到XGBoost的对比实验特征工程做完接下来进入建模环节。这里我强烈建议不要一上来就上XGBoost或深度学习——先跑一个线性回归基线把数据切分、评估流程走通再逐步加复杂度。这样做有两个好处一是基线模型能给后续所有实验提供对照分数线二是如果线性回归的效果已经可以接受那更复杂的模型对业务来说就是过度设计。对PM2.5预测这种中等规模表格数据线性回归、随机森林、XGBoost/LightGBM是三个最常见的候选。4.1 先跑通线性回归基线机器学习线性回归实验的完整代码线性回归对时间序列预测的意义不是精度而是可解释性和速度。它的预测系数直接告诉你“过去1小时浓度每升高10µg/m³未来1小时浓度升多少”这对环保业务侧的同事来说是友好的。代码上最需要注意的是时间序列切分——不能用随机切分必须按时间先后划分训练集和测试集否则就用未来数据预测过去了。from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error, root_mean_squared_error split_date 2019-12-01 train df[df.index split_date] test df[df.index split_date] X_cols [c for c in selected if c ! PM2.5] model LinearRegression() model.fit(train[X_cols], train[PM2.5]) pred model.predict(test[X_cols]) mae mean_absolute_error(test[PM2.5], pred) rmse root_mean_squared_error(test[PM2.5], pred) print(fLinearRegression MAE: {mae:.2f} µg/m³, RMSE: {rmse:.2f} µg/m³)逻辑说明以日期为界划分训练集和测试集模拟真实场景——用过去预测未来。mean_absolute_error衡量平均误差幅度root_mean_squared_error对高误差样本的惩罚更大重污染日预测偏掉100单位会显著拉高RMSE所以业务上两者都要看。线性回归在这个任务上的MAE通常在15到25µg/m³左右具体取决于城市和季节如果基线MAE超过30先别调模型回去检查特征对齐和数据清洗。4.2 XGBoost时序预测参数五个必调参数与早停设置线性回归拟合能力有限对特征交互和高维关系的建模不够。XGBoost是这类任务的默认升级选择。网上的教程一上来就是一堆参数实际需要调的就那么几个。我把五次实验调出来的有效参数组合固定成下面这组可以直接作为起点。import xgboost as xgb # 按时间切出训练/验证/测试训练验证用于调参测试用于最终评估 train_df df[df.index 2019-10-01] valid_df df[(2019-10-01 df.index) (df.index 2019-12-01)] test_df df[df.index 2019-12-01] model xgb.XGBRegressor( n_estimators1000, learning_rate0.03, max_depth5, min_child_weight3, subsample0.8, colsample_bytree0.7, reg_alpha0.5, reg_lambda1.0, random_state42, ) model.fit( train_df[X_cols], train_df[PM2.5], eval_set[(valid_df[X_cols], valid_df[PM2.5])], verboseFalse, )参数说明n_estimators设为1000配合早点停用让模型自己在验证集最优时停止而不是硬跑满。learning_rate0.03是一组黄金组合——学习率降一半树的数量加倍精度通常能提升一点儿训练时间也能接受。max_depth5对小时级表格数据够用。超过6容易过拟合且对重污染样本的“极端值记忆”效应会变强。subsample0.8和colsample_bytree0.7是做行采样和列采样。时间序列数据有自相关性全量训练容易让模型对近期样本过度敏感。reg_alpha和reg_lambda是L1/L2正则项。PM2.5数据噪声较大两个正则都加上能稳定系数。这里要说一个关键点早停参数没有在构造器里出现而是通过eval_set隐式起作用的。上述fit写法配合early_stopping_rounds50模型会在验证集连续50轮没有变好的时候提前停止。如果不加早停1000棵树全跑完必然会过拟合验证集——这不是玄学是树模型在噪声数据上的常见翻车方式。4.3 归一化、评估指标与预测窗口别只盯着R²线性回归和XGBoost对特征量纲的敏感度完全不同——线性回归需要归一化XGBoost不需要它是基于分裂的树模型。所以不要一上来就无脑对所有特征做MinMaxScaler。正确做法是线性回归的pipeline里单独加StandardScaler树模型保持原始量纲。评估指标上R²只能说明“和均值预测相比好了多少” PM2.5预测的实际场景更关心绝对误差和重污染过程捕获率。from sklearn.preprocessing import StandardScaler # 线性回归需要归一化XGBoost不需要 pipe Pipeline([ (scaler, StandardScaler()), (lr, LinearRegression()), ]) # 分浓度区间统计误差重污染段的表现才是核心 test_copy test_df.copy() test_copy[pred] model.predict(test_df[X_cols]) test_copy[err] (test_copy[pred] - test_copy[PM2.5]).abs() for lo, hi, label in [(0, 35, 优), (35, 75, 良), (75, 150, 轻度), (150, 999, 中重度)]: seg test_copy[(test_copy[PM2.5] lo) (test_copy[PM2.5] hi)] if len(seg) 0: print(f{label}段({lo}-{hi}): n{len(seg)}, MAE{seg[err].mean():.1f})逻辑说明分浓度区间统计误差是PM2.5预测项目里必须做的一步——如果整体MAE不错但重污染段误差平均偏高50单位那系统在污染天气下就不可信。用户在意的恰是重污染天的预报准确性。评估时我会同时打印整体MAE/RMSE和分段MAE分段结果才是决定要不要上线的依据。5. 避坑指南PM2.5预测项目里最常见的五个翻车现场这部分是全文最有价值的地方。上面那些流程跑通之后你大概率会遇到下面五个问题之一。每个我都按“现象→原因→解决”的格式拆开讲都是我实际做过和见过的问题。5.1 预测曲线整体滞后一天拐点永远慢半拍现象测试集上MAE不高但把预测值和真实值画在同一张图里发现预测曲线几乎就是真实曲线往右平移了一天。今天浓度飙升模型预测明天才升。原因滞后1小时特征pm25_lag_1与目标相关性太强模型偷懒了——直接复制上一个时刻的值就能把损失降到很低学习不到真正的因果机制。这不只是PM2.5预测的问题任何时间序列预测任务里都常见。解决要么去掉lag_1和lag_2迫使模型依赖更长时间窗口要么改变任务定义——不预测下一个小时而是预测未来6小时或24小时把目标列整体向上平移6/24行让模型无法直接复制近邻值。我会优先尝试后者因为未来6小时的预测才对业务有意义逐小时滚动预测部署起来也累。5.2 模型在重污染日预测值被严重低估现象平时预测误差20以内一到重污染天浓度超过200预测值只有真实值的一半。系统性低估。原因数据不平衡。全年大部分小时浓度在100以下重污染样本占比可能不足3%模型优化的是整体损失天然偏向预测“安全”的低值。这和信用卡欺诈检测、故障诊断里的正样本稀缺问题是同一类。解决两个手段双管齐下。第一训练时给重污染样本加权——把样本权重按浓度分段设置浓度越高的样本权重越大让模型不敢无视这部分数据。第二为重污染场景单独训练一个分类器回归器的级联结构先判断未来是否进入重污染过程再在污染过程内做浓度回归。后者的工程量大一些但精度提升非常明显。5.3 数据泄露把“当天的气象实况”当成了“明天的特征”现象模型在训练集上R²高达0.98在测试集上直接崩到0.3以下差别大得让人不知所措。原因这是时间序列项目中最隐蔽的错误——你合并进来的气象字段是“事后记录”的实况数据而不是预测时刻真正能拿到的预报数据。比如预测12月1日上午10点的浓度模型用到了当天10点的实测温度——但这个温度在预测时刻根本还没发生。模型学到的不是因果关系而是“数据报表里的事后巧合”。解决一句话特征里用到的一切数据在预测时刻必须已经存在。气象字段只能用预测时刻之前的观测值或者用气象台发布的预报值注意获取时间和预报时效要对齐。检查方法很简单——把你用于预测的特征列都看一下凡是时间戳晚于目标时刻的全部删掉或换成滞后版本。5.4 随机切分训练集把未来信息漏进了样本现象用train_test_split(random_state42)切分数据模型MAE只有8换到按时间切分之后MAE变成20。前面调参调的好像都是错觉。原因随机切分把同一条污染过程的数据打散到了训练集和测试集里模型在测试集上见过的其实是“同一天的兄弟样本”测试分数虚高。这不叫测试叫自欺欺人。解决时间序列数据的切分只能按时间顺序。推荐用TimeSeriesSplit做交叉验证——它保证每一折的训练集时间都在验证集之前。最终评估时拿最后一个连续时间段做测试集。还有一个进阶做法切分时把测试集前的缓冲期比如3天从训练集里丢掉避免训练集末尾的污染过程“余温”影响测试集初段。5.5 归一化参数记错范围上线后预测值变成负数现象离线测试一切正常部署到线上跑了一周突然连续几天预测出负数的PM2.5浓度运维那边以为模型坏了。原因训练时对整个特征矩阵做了MinMaxScaler并且用训练集的min和max保存下来。线上新数据的特征值超出了训练集范围缩放后小于0模型输出跟着异常。本质是模型上线时没考虑“分布漂移”。解决第一对树模型取消归一化从源头规避这个问题——XGBoost和LightGBM不依赖特征缩放。第二如果使用线性模型用RobustScaler替代MinMaxScaler它基于中位数和四分位距对极端值和新数据的容忍度高很多。第三无论用哪种缩放方法把训练集的数值边界存进配置文件比如JSON线上推理时如果特征值越界显式打日志告警而不是默默计算出负数。6. 从脚本到系统模型保存、定时重训与结果可视化预测模型跑通只是第一步要成为一套能用的“系统”还需要解决三个工程问题模型怎么保存、预测任务怎么定时触发、结果怎么让人看明白。我一般会用一条命令把模型落盘再用一套轻量级的调度方式实现每日滚动重训。import joblib joblib.dump(model, pm25_xgb_model.joblib) joblib.dump({X_cols: X_cols, scaler: scaler}, pm25_meta.pkl) # 加载并预测未来的写法 loaded joblib.load(pm25_xgb_model.joblib) new_df build_latest_features(api_data) pred loaded.predict(new_df[X_cols])训练频率上我建议按周重训而不是天天重训。PM2.5的污染过程随季节变化但逐小时数据量足够大一周内的新数据不会让模型发生剧烈变化每天重训反而容易引入近期的偶然波动。重训时要保留上一版模型至少一周新的那版在验证集上如果没有稳定提升就不要上线——这就是模型层面的“后悔药”。可视化方面不要只画预测和真实的折线对比加一张“未来24小时趋势带”比什么都有说服力——把过去7天的实测值和未来24小时的预测值放在同一张图上预测部分用阴影或不同颜色标记。业务方一眼就能看出“预报的拐点在哪里”这比任何RMSE指标都来得直观。我踩过最深的坑是刚上线时只给了业务方一个预测数字没有给置信区间。后来有一次重污染过程预测值偏低50单位业务方直接失去信心。从那以后我坚持输出分位数预测——用XGBoost的多个随机种子跑集成取5%和95%分位作为区间边界预测变成“有依据的范围”而不再是“一个数字”。这个改动比调任何参数都能提升系统的可信度也让我明白了落地的本质是让人敢用你的预测而不是让指标好看。希望帮到你。本文还有配套的精品资源点击获取