首页 / 资讯中心 / 文章详情

随机森林温度预测模型优化:从特征工程到时序验证与残差建模

随机森林温度预测模型优化:从特征工程到时序验证与残差建模 ★ FEATURED ARTICLE
1. 温度预测为什么值得用随机森林它到底卡在哪儿做气象相关的数据活儿温度预测是最容易上手、也最容易翻车的一类任务。说它容易是因为温度数据本身连续、噪声相对可控、历史数据积累得厚说它容易翻车是因为很多人拿随机森林跑一遍MAE 看着挺漂亮2℃ 上下一上线发现连续三天的高温过程全错凌晨的降温幅度永远估不准遇到冷空气过境直接崩盘。我在几个温度预测的小项目里反复踩过这个坑后来才想明白一件事随机森林温度预测模型优化的难点从来不在换个更强的算法而在于你怎么定义特征、怎么切验证集、怎么看待那 2℃ 的平均误差背后藏着的长尾错误。这篇内容适合三类人看一是刚用机器学习做气象/能耗/农业相关预测想找个靠谱基线的二是已经在用随机森林但发现效果卡在某个瓶颈上不动的三是不做气象但做其他时序回归想借鉴一下思路的。我不打算讲随机森林的数学推导——那些教科书上都有我讲的是实操中真正影响结果的那几个决策点滞后特征怎么构造、验证集怎么切才不算作弊、参数搜索的空间怎么定、残差怎么二次利用。先给个结论垫底在中小规模结构化气象特征站点观测 常规要素上调好的随机森林通常能把 24 小时内的逐小时温度预测 MAE 压到 1.2~1.8℃ 区间比起线性回归和单纯的历史均值持续性预报有明显提升但如果你只喂了前一个小时温度这一个强特征模型会退化成昨天多少度今天多少度的懒惰预测器短期指标好看长期完全没用。这个陷阱后面会细讲它是这个任务里最大的坑。注意本文所有案例、站点、数据来源均为虚构代称不涉及任何真实机构与真实站点。2. 数据准备特征工程才是这类模型真正的上限2.1 原始观测数据的清洗与时间对齐温度预测的第一步不是建模是把数据对齐。真实拿到的站点观测数据通常长这样逐小时温度、湿度、气压、风速风向、云量、能见度、降水量字段名可能是缩写单位可能混着摄氏/华氏、m/s 和 km/h时间戳可能是 UTC 也可能是本地时间缺测值可能是 NaN、可能是 999、可能是空字符串、可能是 -9999。我一般会先写一段固定的体检脚本把每个字段的缺失率、取值范围、异常跳变列出来人工过一遍再往下走。具体做法上有几个细节值得说。时间戳统一所有来源都先转成同一时区、统一为整点重采样用resample(1H).mean()比interpolate更稳妥因为插值会凭空造出平滑数据掩盖真实的观测中断。缺测处理连续缺测超过 3 小时的整段丢掉单点缺测用前后各 2 个点的均值补不要用前向填充——前向填充会让温度曲线出现阶梯状平台模型会学到虚假的温度不变模式。异常值温度的单点跳变比如相邻小时差 15℃ 以上且前后都对不上基本是传感器错误直接置空再按缺测处理。import pandas as pd import numpy as np def clean_station_data(df: pd.DataFrame, tz: str Asia/Shanghai) - pd.DataFrame: df df.copy() df[time] pd.to_datetime(df[time]).dt.tz_localize(UTC).dt.tz_convert(tz) df df.set_index(time).sort_index() # 统一异常值并转为 nan df df.replace([999, 9999, -999, -9999, ], np.nan).astype(float) # 逐小时重采样同时保留观测计数用于判断可信度 cnt df[temp].resample(1H).count() df df.resample(1H).mean() # 单点缺测前后各两点均值补 df[temp] df[temp].interpolate(limit1, limit_directionboth) # 连续缺测超 3 小时整段剔除 bad df[temp].isna().rolling(3).sum() 3 df.loc[bad, :] np.nan df df.dropna(subset[temp]) return df这段代码不长但它决定了后面所有指标的可信度。我见过不止一次图省事用ffill把缺失全填了最后模型在缺失率高的月份表现异常好——因为那段时间的数据是被复制出来的验证集和训练集几乎一样等于自己给自己放水。数据清洗阶段偷的懒一定会在验证阶段以效果虚高的形式还回来。2.2 滞后特征与滑动窗口最强特征也是最大陷阱温度预测里temp_lag1前 1 小时温度几乎必然是特征重要性排第一的变量相关性通常在 0.98 以上。很多人到这一步就收工了觉得模型效果已经很不错。问题是如果模型只靠 lag1它实际上学到的是当前温度 ≈ 上一小时温度这跟持续性预报persistence forecast没有本质区别而气象业务里持续性预报是最基本的对照基线打败它才叫有价值。我踩过的坑是这样的第一版模型只用了 lag1、lag2、lag3 和几个气象要素24 小时预测 MAE 0.9℃看起来很漂亮。但仔细一看预测曲线它几乎是实测曲线整体右移一小时温度转折点永远滞后。原因就是树模型无法外推它把 lag 特征当成了主导其他特征的作用被压得极低。后来我把 lag 特征的窗口拉长、同时加入变化率特征lag1 - lag2、3 小时滑动均值 - 6 小时滑动均值模型的转折点捕捉能力才起来。我常用的特征组大致如下特征组具体构造作用注意点短时滞后lag1~lag6 温度、湿度、气压捕捉惯性不要只留 lag1长时滞后lag12、lag24、lag48 温度日周期与日际变化lag24 与时刻特征高度共线滑动统计3/6/12/24h 的均值、极差、标准差刻画趋势强度极差比标准差更能反映突变变化率diff1、diff3、diff6分离升/降温过程是抬升转折点精度的关键时刻周期hour 的 sin/cos 编码日变化节律不要直接喂原始 hour 整数日历月份 sin/cos、是否节假日如适用季节漂移节假日对气象无用对能耗有用其他要素露点差、风速、云量、日照时数辐射与平流信息露点差比湿度更稳定时刻编码这块值得展开一句。直接把hour当数值喂进去树模型会认为 23 点和 0 点差得很远实际它们只差一小时。用sin(2πh/24)和cos(2πh/24)编码后0 点和 23 点在特征空间里是相邻的模型的切分逻辑才对得上真实物理规律。月份同理sin/cos编码比 one-hot 更省维度也更适合树模型做阈值切分。def build_features(df: pd.DataFrame, target: str temp) - pd.DataFrame: out df.copy() # 短时滞后 for lag in [1, 2, 3, 6, 12, 24, 48]: out[f{target}_lag{lag}] out[target].shift(lag) # 滑动统计 for w in [3, 6, 12, 24]: out[f{target}_mean{w}] out[target].shift(1).rolling(w).mean() out[f{target}_range{w}] out[target].shift(1).rolling(w).max() - out[target].shift(1).rolling(w).min() # 变化率 out[f{target}_diff1] out[target].shift(1) - out[target].shift(2) out[f{target}_diff3] out[target].shift(1) - out[target].shift(4) # 周期编码 out[hour_sin] np.sin(2 * np.pi * out.index.hour / 24) out[hour_cos] np.cos(2 * np.pi * out.index.hour / 24) out[month_sin] np.sin(2 * np.pi * out.index.month / 12) out[month_cos] np.cos(2 * np.pi * out.index.month / 12) # 露点差 if {temp, dewpoint}.issubset(out.columns): out[dewpoint_dep] out[temp] - out[dewpoint] return out.dropna()这里所有的shift(1)都是刻意的滑动窗口如果包含了当前时刻就等于把答案塞进了特征里。这种泄漏极其隐蔽因为代码跑得通、指标还特别好只有当你把它部署到真实提前量预测的场景里才会发现全错。规则很简单任何特征都只能用 t-1 及之前的信息预测 t 时刻的值。2.3 评估指标怎么定比模型参数重要十倍温度预测如果只看一个总体 MAE你会被平均数骗得很惨。我的做法是至少拆成三档来看全时段 MAE、极端温度时段如当日最高温的 90 分位以上MAE、转折时段3 小时内变化超过 3℃ 的样本MAE。实际经验是一个总体 MAE 1.5℃ 的模型在转折时段的 MAE 可能是 3.5℃而恰恰是这段时间的业务价值最高——升降温过程的预报准不准决定了模型到底有没有用。另外要明确预测提前量。提前 1 小时和提前 24 小时是完全不同的任务前者可以重度依赖 lag 特征后者必须依赖气象要素的物理关系。我一般会做 1h、6h、24h 三个提前量的模型或者干脆用直接多步的方式为每个提前量单独训练一个模型而不是训练一个模型然后滚动递归——递归预测会把误差一步步放大24 步之后基本没法看。还有一点容易被忽略持续性基线的对照。任何温度模型都应该和用 t 时刻的观测值直接当作 th 时刻的预测比一比。如果你的模型打不过持续性基线说明你的特征工程没有提供任何新信息只是把 lag1 学了一遍。我见过不少所谓优化后的模型在 1 小时提前量上确实赢了持续性基线一小截但在 6 小时提前量上输得很难看这就是典型的过度依赖滞后特征。3. 随机森林的参数调优别调不该调的调对了就够用3.1 四个真正影响结果的参数随机森林参数不少但真正常用的就四个而且它们对温度这类平滑连续目标的影响方向很明确。n_estimators树的数量这个参数属于够用就行。从 100 加到 300验证误差通常有明显下降300 到 800 收益递减超过 800 基本是白烧算力。我的习惯是按数据量定样本 1 万以内取 3001 万到 10 万取 500再往上 800。这里有个计算上的小参考——如果单棵树训练耗时约 0.05 秒500 棵树串行需要 25 秒开n_jobs-1用 8 核并行大约 3~4 秒这个量级做 50 组参数搜索是完全可接受的。min_samples_leaf叶子最小样本数这是温度预测里最关键的正则化参数。温度是连续量如果叶子节点只放 1 个样本模型会去拟合观测噪声传感器抖动、瞬时扰动表现为训练集误差极低、验证集误差反而上升。实测下来min_samples_leaf取 2~5 通常最优数据噪声大时取到 8 甚至 10 也可能更好。它和max_depth有一定替代关系一般先固定max_depthNone只调min_samples_leaf效率更高。max_features每次分裂的候选特征数回归任务默认是1.0即全部特征但我更推荐设成 0.3~0.6 之间或者sqrt。原因是温度特征之间共线性极强lag1、lag2、mean3 彼此高度相关如果每次分裂都能看到全部特征模型会反复选那几个强特征树的多样性下降集成效果打折。把候选特征限制在 30%~50%相当于强制模型去看不同的特征组合方差下降更明显。max_depth我通常不设上限靠min_samples_leaf控制复杂度。设了固定深度反而容易出现所有树长得一样的问题因为温度任务里强特征太突出深度限制会让树在同一个位置反复提前停止。3.2 时序数据不能用随机 KFold这是硬伤这一条我单独拎出来说因为它是新手最容易犯、后果最严重的错误。cross_val_score(model, X, y, cv5)默认是随机切分对温度这种强自相关的时序数据随机切分会让训练集里包含验证集时刻的前后邻居——模型相当于偷看了答案。这种情况下交叉验证分数可能比真实效果高 30%~50%你以为调出个好模型上线一测全是水分。正确的做法是TimeSeriesSplit或者更贴近业务的做法按时间块切分。比如用 1 月到 9 月训练、10 月验证、11 到 12 月测试或者做滚动窗口验证前 6 个月训练预测下 1 个月窗口往后滑。后一种更贴近实际部署场景也能看出模型在不同季节的稳定性。from sklearn.model_selection import TimeSeriesSplit, RandomizedSearchCV from sklearn.ensemble import RandomForestRegressor from scipy.stats import randint, uniform tscv TimeSeriesSplit(n_splits5, gap24) # gap24 留出一天缓冲避免边界泄漏 param_dist { n_estimators: randint(300, 800), min_samples_leaf: randint(1, 10), max_features: uniform(0.2, 0.6), min_samples_split: randint(2, 20), } rf RandomForestRegressor(random_state42, n_jobs-1) search RandomizedSearchCV( rf, param_dist, n_iter60, cvtscv, scoringneg_mean_absolute_error, random_state42, n_jobs-1, verbose1 ) search.fit(X_train, y_train) print(search.best_params_, -search.best_score_)gap24这个参数很关键。TimeSeriesSplit默认训练集和验证集是紧邻的边界处的样本几乎完全相关容易让分数虚高。加一天24 个样本的间隔模拟真实的用历史预测未来的间隔感分数会略降但更可信。我一般会跑两遍gap0和gap24对比一下如果两者差距很大比如超过 15%说明模型对近期样本的依赖过强泛化能力存疑。3.3 特征重要性别只看不纯度要看置换重要性随机森林自带的feature_importances_是基于不纯度减少算的有个众所周知的偏差它会高估取值多的连续特征。温度里的 lag 特征、滑动均值特征全是连续值天然占便宜而云量、风向这类离散或类别特征会被系统性低估很容易误判这个特征没用然后删掉。置换重要性permutation importance更靠谱做法是把某一列特征的值随机打乱看验证集误差涨多少涨得越多说明越重要。代价是要跑多次但针对温度任务验证集不大跑 10 次重复也就几十秒。实操上我会做两件事一是按置换重要性排序删掉重要性接近 0 甚至为负的特征这些特征在随机化之后误差反而降低了说明纯粹是噪声二是按特征组看重要性而不是按单列。比如变化率组合计贡献 18%周期组贡献 12%这比单看某一列更有决策价值。4. 从基线到提升一次完整的优化实操记录4.1 基线模型先把什么都不做的水平立住我习惯先跑三个基线把地板线钉死后面所有优化都要跟它们比。第一个是气候态基线用历史同期比如过去 30 天同一时刻的均值作为预测。第二个是持续性基线直接用 t 时刻观测值预测 th。第三个是线性回归基线用同一套特征跑 Ridge 回归。这一步的意义在于校准预期。某次我做的项目里持续性基线在 1 小时提前量上的 MAE 只有 0.6℃比大部分人的随机森林模型还好。这说明在超短期温度预测上随机森林的优势空间本来就小你花大力气优化的边际收益很低但到了 6 小时、12 小时提前量持续性基线迅速崩到 2.5℃ 以上随机森林的价值才体现出来。先搞清楚任务的可优化空间再决定投入多少精力调参。我那次的项目各基线在 6 小时提前量上的表现大致是气候态 3.9℃、持续性 2.6℃、Ridge 回归带全部特征1.9℃、未调参随机森林 1.6℃、调参后随机森林 1.42℃。可以看到从 Ridge 到随机森林的增益0.48℃比从默认参数到调优参数0.18℃更大说明特征和非线性建模能力的价值高于参数精度。这也是我一直劝人不要一上来就死磕网格搜索的原因。4.2 残差分析把错误摊开看才知道往哪优化模型跑完别急着看总体指标先把残差预测 - 实测画出来按几个维度切开看。我一般会看四张图残差随时间的散点看有没有系统性偏移、残差随预测值的散点看是不是在某一段温度区间系统性偏低或偏高、残差按小时的箱线图看时刻相关的偏差、残差按天气过程分组的分布。实操中经常能看出规律性的东西。举个真实碰到的例子残差按小时看凌晨 4~6 点的残差均值是 -0.8℃预测偏低午后 14~16 点是 0.6℃预测偏高。意思是模型把日较差压小了。原因在于树模型的预测是叶子节点样本的均值而均值天生趋向中间、压缩极值所以模型对非常热和非常冷的估计都会往中间收。这是随机森林的结构性缺陷靠调参没法彻底解决得靠后处理或者换建模方式。另一个常见现象是残差和风速、云量相关。晴朗无风的夜晚降温快多云或有风的夜晚降温慢如果模型里没有明确的辐射冷却代理特征比如云量 风速的交互项、露点差夜间温度预测就会系统性偏。我后来加了cloud_cover * wind_speed的交互特征和dewpoint_dep之后凌晨时段的 MAE 从 1.3℃ 降到 1.05℃。4.3 四项针对性优化按性价比排序第一项极端样本加权。温度分布本身是近似正态的中间温度的样本多极端温度的样本少。随机森林在损失函数上是所有样本等权结果就是对中间温度拟合好、对极端温度拟合差。解决办法是给样本加权让极端温度样本的权重更高。权重的算法我用得比较多的是按目标值的经验分位数来定def extreme_weight(y, low_q0.1, high_q0.9, boost2.5): lo, hi np.quantile(y, [low_q, high_q]) w np.ones_like(y, dtypefloat) w[(y lo) | (y hi)] boost return w sample_w extreme_weight(y_train.values, boost2.5) rf.fit(X_train, y_train, sample_weightsample_w)boost这个值不要设太大2~3 之间比较稳妥。设到 5 以上整体 MAE 会明显变差因为模型为了照顾少量极端样本牺牲了大部分常规样本的精度。这种取舍要看业务需求如果高温预警是核心诉求那就值得牺牲一点平均精度如果只是做日常温度曲线展示加权带来的收益有限。第二项残差二次建模。这是性价比最高的一招。做法是先用主模型预测得到残差再用一个轻量的模型比如min_samples_leaf20的随机森林或者 GBDT去学残差和特征之间的关系最后预测值 主模型预测 残差模型预测。本质上是让模型分两阶段学第一阶段学主要规律第二阶段学系统性偏差。residual y_train - rf.predict(X_train) rf_res RandomForestRegressor(n_estimators200, min_samples_leaf20, max_features0.5, random_state42, n_jobs-1) rf_res.fit(X_train, residual) final_pred rf.predict(X_test) rf_res.predict(X_test)注意残差模型的min_samples_leaf一定要设大因为残差本身信噪比低如果让它也自由拟合会把训练集的噪声学进去等于变相过拟合。我这一招在 6 小时提前量的任务上通常能再降 0.08~0.15℃ 的 MAE在 24 小时提前量上降幅可以到 0.2℃ 左右因为提前量越长系统性偏差越明显、残差模型越有用。第三项分位数随机森林做区间预测。只给一个点预测在很多场景不够用比如农业防冻需要知道最坏情况会降到几度。RandomForestRegressor支持quantile参数需要 sklearn 1.1或者更通用的做法是用分位数损失拟合多个模型。实操上我用GradientBoostingRegressor(lossquantile, alphaq)做 0.1 和 0.9 分位得到 80% 预测区间再检查区间的覆盖率是否接近 80%。如果覆盖率只有 65%说明区间偏窄需要放宽如果覆盖率 92%说明区间太保守对决策没帮助。区间校准这件事经常被忽略但实际业务里它比点预测的 MAE 更重要。第四项预测平滑与后处理。树模型的输出是阶梯状的因为叶子节点值是离散的相邻时刻的预测可能在几个值之间跳变。我一般会做一次轻度的指数平滑pred_smooth[t] 0.8 * pred[t] 0.2 * pred[t-1]。系数 0.8 是我试出来的经验值太大会削弱转折点的响应速度太小又平滑不掉跳变。另外一个后处理是偏差校正按小时统计验证集上的平均残差然后从预测里减掉。这一招简单粗暴但对时段性系统偏差特别有效通常能降 0.05~0.1℃。优化前后的对比我整理成了一张表横向是不同提前量纵向是各版本模型版本1h MAE6h MAE24h MAE转折时段 MAE极端温度 MAE持续性基线0.612.583.923.104.21Ridge 回归0.721.902.452.622.88随机森林默认参数0.681.602.182.412.73 时序交叉验证调参0.641.422.022.202.55 极端样本加权0.651.442.052.182.21 残差二次建模0.611.331.882.052.10 分位数区间 后处理0.591.301.851.982.06表里的数字是我在某次虚构数据集上的记录具体值会随数据不同浮动但相对关系有参考价值加权在极端温度上收益最大2.73→2.21但在常规指标上甚至有轻微回退0.64→0.65这是预期内的取舍残差二次建模则是全面提升几乎没有副作用。5. 实操中的常见问题与排查实录5.1 常见问题速查表下面这些是我和同行交流、以及自己踩坑过程中反复遇到的按出现频率排序现象最可能的原因排查与解决验证集指标远好于线上随机 KFold 导致时序泄漏改用 TimeSeriesSplit加 gap训练集 MAE 0.2验证集 2.5树太深、叶子样本太少提 min_samples_leaf 到 3~8预测曲线整体滞后一小时过度依赖 lag1加变化率特征考虑剔除或衰减 lag1 权重日较差被明显压缩树模型均值化倾向极端样本加权、残差建模、分位数预测夜间温度系统性偏低缺辐射冷却相关特征加云量×风速、露点差换季时效果突然变差分布漂移模型未更新按月重训或加月份特征做分组训练特征重要性里风向排第一不纯度重要性偏差改用置换重要性重新评估不同批次预测结果不一致特征计算版本不一致特征工程固化为同一份代码与配置预测值出现重复阶梯叶子节点值离散轻度指数平滑后处理极端高温全部低估样本稀疏 回归均值加权 单独训练高温子模型5.2 特征管线一致性最容易在部署时翻车的地方模型调得再好如果训练时和推理时的特征算法不一致线上效果一定崩。这种问题在温度预测里特别常见因为特征里有大量滚动窗口和滞后项只要时间对齐错了半小时lag1就变成了lag2整个模型的输入分布就偏了。我的做法是把特征构造写成一个带状态的类训练时fit记住最后的窗口状态推理时喂新数据就能续上而不是每次重新算全量历史。另外把特征列表和顺序也固定下来加载模型时校验一下feature_names_in_对不上直接报错别让模型静默地用错列顺序——随机森林不会报错它只会给你一个莫名其妙的结果。模型体积也得留意。500 棵树、深度不限、样本几万条的随机森林用 joblib 存下来可能到几十 MB 甚至上百 MB如果要做边缘端部署比如站点本地推理得把n_estimators降下来或者做树剪枝。我一般会用max_depth限制到 20 以内配合min_samples_leaf体积能压到原来的三分之一精度损失通常在 0.03℃ 以内。5.3 模型会衰减别指望一次训练管一年温度预测模型有个特点它对季节的适应性会随时间漂移。你在春秋季训练出来的模型到了盛夏的高温期特征分布对不上效果会明显下滑。我用同一套模型做过连续 12 个月的跟踪发现夏季月份的 MAE 比全年平均高 25%~35%冬季次之春秋最好。原因不难理解夏季温度日较差大、对流活动强、突发性强而训练样本里这类极端形态占比低。对应的策略有三条按投入从低到高一是按月重训用最近 12 个月的数据滚动训练成本可控效果稳定二是分段建模按季节或按温度区间分别训练模型用月份特征做路由模型复杂度上升但精度更好三是在线更新样本权重让近期样本权重更高用sample_weight exp(-Δt / τ)衰减τ取 30~60 天。我个人更倾向第一种因为简单、可解释、好维护。分段建模听起来高级实际调起来问题不少分段边界怎么定、样本少的季节模型稳不稳、切换点会不会跳变都是麻烦。滚动重训配合固定的特征管线是我用下来最省心的方案。还有一点提醒重训之后一定要做新旧模型对比别直接覆盖。用同一段回测期把新旧模型的指标都跑一遍如果新模型在转折时段变差了哪怕总体 MAE 略好也要慎重——转折时段的业务价值更高。我给自己定的规则是总体 MAE 提升超过 3% 且转折时段没有退化退化不超过 2%才允许替换线上模型。6. 几个我认为最值钱的经验如果只能记三条我会挑这三条。第一条先打败持续性基线再谈优化。随机森林在超短期温度预测上的优势没有想象中大很多时候费半天劲调出来的模型跟上一小时多少度就报多少度打平。把基线立住你会更清楚优化的方向在哪里也不容易被虚高的指标自我感动。第二条验证集切分方式决定你看到的数字是真是假。时序数据的随机切分是最普遍的隐性作弊而且它给出的分数特别诱人。我现在的习惯是任何时序模型的第一行代码就是写TimeSeriesSplit宁可一开始分数难看也不要后面返工。第三条残差里藏着最多的信息。调参的边际收益会很快见底而按小时、按温度区间、按天气类型切开看残差几乎每次都能发现一两个能补的特征或能做的后处理。它比调参枯燥但性价比高得多。最后分享一个小技巧把每次实验的配置、特征版本、验证分数、残差图路径都记在一个固定的表格里包括失败的尝试。我一开始懒得记后来发现这个参数组合好像试过这种记忆模糊的情况太常见重复劳动浪费的时间远超记录的成本。哪怕只是在一个文本文件里按日期追加几行长期看都值。
阅读完成 · 觉得有帮助?
咨询建站