☰
Python随机森林气温预测实战:从特征工程到模型调优
2026/9/30 15:11:41 网站建设 项目流程

气象站里报出来的“晴 26°C”,背后其实是一套复杂的物理方程加无数传感器修正。但如果你手头只有一份历史气象表格,又想在几行代码内搭个能用的气温预测模型,那随机森林绝对是性价比最高的起点。去年我做本地站点高温趋势预测时,第一版基线就是用 Python 加随机森林回归算法完成的,从数据清洗到参数调优只花了一个下午,精度已经能控制在平均误差 1.5°C 以内。这篇文章就把完整链路拆开讲:为什么选随机森林、特征怎么做、代码怎么跑、以及我在实际项目里踩过的几个大坑。适合刚接触机器学习回归任务、想用真实气象数据练手的同学,也欢迎做气候数据分析的朋友来交流。

1. 项目概述与模型选型思路

1.1 为什么选中随机森林做气温预测

气温预测本质上是个回归问题,输入一批历史气象观测值,输出一个连续温度值。但这个回归关系很不“线性”:湿度高不一定降温,但配合大风和阴雨天,体感温度会骤降;气压、前一日温差、季节周期都在同时起作用。传统线性回归在这种多因子非线性的场景下,往往只能拟合一个大趋势,很难捕捉局部突变。

随机森林的做法则完全不同。它训练多棵决策树,每棵树在样本和特征上做随机采样(bootstrap 抽样和特征随机抽样),最终把多棵树的预测结果取平均作为输出。这种 bagging 策略最直接的收益是方差降低:单棵决策树很容易学到诡异的极端噪声,但几十棵上百棵树平均下来,个别的误判就被淹没了。集成之后模型对异常值也不敏感,某个传感器记录了一个离谱的瞬时风速,最多影响一棵子树的切分,整体预测不会被打偏。

有人问为什么不用支持向量机或者 XGBoost。SVR 非线性核的选择和惩罚系数 C 调整起来很看经验,网格搜索稍不留意就陷入局部最优;XGBoost 精度上限确实更高,但需要处理学习率、树深度、正则化、早停一大堆超参数,对刚入门的人来说包袱太重。随机森林在中等规模的气象历史数据上(几千到几万条样本),不需要特征归一化,不用考虑缺失值编码,默认参数跑出来已经能当 baseline,后续想替换成更强的模型也有充足的对比参照。

当然,它有一个硬伤:模型无法外推。因为预测值是训练集中样本的加权平均,如果测试期出现了百年一遇的极端高温,训练集里的最高温只有 38°C,模型永远不可能预测出 40°C。这一点在后面的问题排查章节我会再展开。

1.2 模型适用场景与数据需求

这个模型不是什么天气场景都能搞定,它的舒适区是短期趋势预测,尤其是几小时到三天的逐时或逐日气温。我在项目中主要用它做两件事:一是给农业大棚的通风决策提供温度参考,二是对城市热岛效应做一个基于历史观测的统计估算,不追求物理可解释性,只求预测趋势稳。

数据需求方面,最低要求是一份包含“日期 + 气温”的两列数据,这样只能提取日期本身的季节周期,预测精度相对有限。理想情况下,希望历史数据至少覆盖连续 2 到 3 年,并且包含这些字段:日期、气温(或最高最低温)、相对湿度、气压、风速、降水量。年份越完整,模型能学到的季节周期和年际差异越可靠。样本量上并不夸张,每天一条记录,三年也就一千条左右,随机森林完全吃得消;如果是逐小时数据,几万条也跑得动。

数据来源方面,有公开气象数据集,也有从天气接口导出的历史记录,但需要注意授权和合规问题。自己用小型气象站采集的数据最稳妥,注意传感器校准即可。

1.3 项目技术栈与整体流程

整个项目我用的就是最常见的 Python 数据科学生态:pandas 做数据整理,numpy 做数值运算,scikit-learn 中的 RandomForestRegressor 做模型训练,matplotlib 画特征重要性和预测对比图。不需要深度学习框架,也不需要 GPU,普通笔记本就能流畅跑完。

流程上大致分五步:数据获取与清洗、特征工程、训练集测试集划分、随机森林训练与调参、模型评估和特征分析。下面两章就是按这个顺序来的,先解决数据问题,再进入建模。

2. 数据准备与特征工程

2.1 数据获取与清洗实操

我用的示例数据是一份模拟气象站观测表,字段包括 date、temp、humidity、pressure、wind_speed、precipitation,其中 temp 就是我们要预测的目标变量。拿到手第一步是解析日期并排序,这是所有时间序列任务的前提。

import pandas as pd import numpy as np df = pd.read_csv("weather.csv", parse_dates=["date"]) df = df.sort_values("date").reset_index(drop=True) print(df.head())

接着做缺失值检查。气象观测数据经常有传感器掉线导致空值。我习惯的做法是:如果缺失比例低于 5%,用前后两天均值做线性插值;如果连续缺失超过 3 天,则直接删掉这一段,避免插值制造出人工数据。对异常值,我用了最简单的业务规则:气温在 -40 到 45°C 之外的视为异常;湿度必须在 0 到 100%;气压在 850 到 1080 hPa。超出范围的记录不是删除,而是标记为缺失再插值,因为删除会使时间不连续。

这里容易忽略的一点是:日期索引必须连续,否则后面生成滞后特征时会出现“前一天”错位。我通常用pd.date_range重排一个完整日期列表,再左连接原始数据,缺失日期记 NaN。

2.2 特征工程:把时间变成模型能理解的语言

随机森林虽然不需要归一化,但特征设计直接影响模型上限。我在这类项目中重点构造三组特征。

第一组是时间特征。直接把月份作为一个整数传给模型,模型会把它当作数值对待,但 1 月和 12 月之间的“距离”并不是 11,一个月更像类别变量。因此我同时拆出 year、month、day、dayofyear、weekday。dayofyear(一年中的第几天)配合正弦余弦变换后,能连续地表示季节变化,因为 1 月 1 日和 12 月 31 日的季节应该很接近,但直接数值上差了 364。用下面的代码把周期编码成二维坐标:

df["dayofyear"] = df["date"].dt.dayofyear df["season_sin"] = np.sin(2 * np.pi * df["dayofyear"] / 365.25) df["season_cos"] = np.cos(2 * np.pi * df["dayofyear"] / 365.25)

第二组是滞后特征,这是时间序列预测的灵魂。气温具有很强的自相关性,今天的温度大概率跟前一天甚至前两天的温度接近。所以我构造了目标变量的一阶滞后和二阶滞后,以及一个七日滑动平均表示近期冷热基调:

df["temp_lag1"] = df["temp"].shift(1) df["temp_lag2"] = df["temp"].shift(2) df["temp_rolling7"] = df["temp"].rolling(7).mean()

第三组是其他气象因子的滞后值。注意是滞后值,不是同时刻值。预测当天气温时,当天湿度是拿不到的未来信息,只能用前一天的湿度、气压、风速作为特征。很多新手在这里栽跟头,我会在第四章重点展开。

2.3 训练集与测试集划分的特殊性

气温预测严格说是时间序列任务,直接调用 sklearn 的train_test_split并设置随机种子是不可取的,因为随机划分会把未来的数据混进训练集,模型等于“偷看”了答案,测试指标虚高,上线后立刻翻车。

正确做法是按时间顺序切分:用前 80% 的数据训练,后 20% 的数据测试。在样本量约 2000 条时,我选择前 1600 条训练,后 400 条测试。如果需要交叉验证,也要使用TimeSeriesSplit,它保证了训练集永远是测试集之前的样本。

from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5) # tscv.split(X) 会按时间顺序生成连续递增的训练集和测试块

这个细节决定了模型的真正泛化能力,也和后面调参的网格搜索方式直接挂钩。

3. 基于sklearn构建随机森林气温预测模型

3.1 主流程代码实现与评估指标

我把特征和目标变量整理成矩阵。需要提醒的是,构造滞后特征后要丢掉前几行,因为这些行的滞后值是 NaN。数据准备好后,模型训练本身非常简洁:

# 假设 df 已经包含特征列和目标列 feature_cols = ["season_sin", "season_cos", "humidity_lag1", "pressure_lag1", "wind_speed_lag1", "temp_lag1", "temp_lag2", "temp_rolling7"] X = df[feature_cols].dropna() y = df.loc[X.index, "temp"] train_size = int(len(X) * 0.8) X_train, X_test = X.iloc[:train_size], X.iloc[train_size:] y_train, y_test = y.iloc[:train_size], y.iloc[train_size:] from sklearn.ensemble import RandomForestRegressor model = RandomForestRegressor(n_estimators=200, max_depth=10, min_samples_leaf=3, n_jobs=-1, random_state=42) model.fit(X_train, y_train) y_pred = model.predict(X_test)

评估阶段我一口气看三个指标:MAE、RMSE 和 R²。MAE 是平均绝对误差,直观反映了预测温差;RMSE 对大误差更敏感,如果某天预测差 5°C,RMSE 会比 MAE 高不少;R² 表示模型解释了测试集多少方差,越接近 1 越好。

from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score mae = mean_absolute_error(y_test, y_pred) rmse = mean_squared_error(y_test, y_pred, squared=False) r2 = r2_score(y_test, y_pred) print(f"MAE: {mae:.2f}°C, RMSE: {rmse:.2f}°C, R2: {r2:.3f}")

我当时的基线结果大约是 MAE 1.4°C,R² 0.91。对一个只基于历史统计、不考虑物理方程的模型来说,这个精度已经足够支撑日常温度参考了。

3.2 参数调优:用网格搜索找到合理超参数

随机森林默认参数能跑,但距离最优还有空间。主要调四个参数:n_estimators、max_depth、min_samples_split 和 min_samples_leaf。n_estimators 控制树的数量,太少则方差大,太多则训练变慢且后期收益极低;max_depth 限制每棵树能连续切多深,防止学到过于复杂的噪声;min_samples_leaf 要求叶子节点至少包含多少样本,相当于给预测值做平滑。

调参时最关键的一个坑是交叉验证方式。如果直接GridSearchCV(cv=5),默认会做分层 K 折,这对分类问题尚可,但在时间序列场景中会让模型用未来的数据验证过去,导致参数偏好失真。所以必须传一个 TimeSeriesSplit 实例给cv:

from sklearn.model_selection import GridSearchCV param_grid = { "n_estimators": [100, 200, 300], "max_depth": [8, 10, 12], "min_samples_leaf": [2, 3, 5], "max_features": ["sqrt", 0.5] } grid = GridSearchCV( RandomForestRegressor(random_state=42), param_grid, cv=tscv, scoring="neg_mean_absolute_error", n_jobs=-1, verbose=1 ) grid.fit(X_train, y_train) print(grid.best_params_)

这里 scoring 我选了 neg_mean_absolute_error,它更贴近业务关注的平均温差,而不是像 R² 那样容易被极端样本主导。GridSearchCV 在 TimeSeriesSplit 上大约会跑几十组参数,在几万条样本内也就十几分钟,完全可以接受。

运行完毕后,最好的三个参数组合我通常会对比它们的测试集 MAE。印象中 max_depth 从默认的无限制降到 8 到 10 后,验证误差反而下降,说明决策树深度过大确实存在过拟合。min_samples_leaf 也不宜过小,否则树会记住个别特殊天气。

3.3 特征重要性解释与应用

sklearn 的随机森林训练后有一个feature_importances_属性,可以直接查看每个特征对预测的贡献。我用它做初步筛选:

importance = pd.Series(model.feature_importances_, index=feature_cols) print(importance.sort_values(ascending=False))

在我跑的模型里,排在前几名的通常是 temp_lag1、temp_rolling7、season_cos 和 pressure_lag1。这很符合气象直觉:短期气温有很强的惯性,近期冷暖趋势比遥远的历史平均值更起作用,气压场变化又是冷暖空气活动的直接信号。

不过要注意,默认的基于基尼系数的特征重要性有一个偏差:它偏向数值型高基数特征,而且对特征之间的共线性很敏感。所以当我想要更严谨的特征排序时,会用permutation_importance做一次验证。它的原理是把某个特征的值随机打乱,观察模型误差下降多少,下降越多说明该特征越重要。这样得到的排序更可靠,只是计算成本略高。

from sklearn.inspection import permutation_importance perm_imp = permutation_importance(model, X_test, y_test, n_repeats=10, random_state=42, scoring="neg_mean_absolute_error")

如果两个方法的结果出现巨大差异,我会检查特征之间是否有强相关性,比如“前一天温度”和“七日滑动平均”本质上高度相关,保留其一即可,否则既增加计算量又让解释变得复杂。

4. 常见问题与排查技巧

4.1 数据泄漏:指标漂亮却没法落地

我在复现别人代码时见过最典型的错误:用当天的湿度、当天风速、当天气压去预测当天气温。训练时模型发现温度跟同刻气压高度相关,R² 能飞到 0.98,测试集也表现完美。可一旦需要做真正的预测,这些所谓的特征在“当天”还没发生时根本拿不到,模型瞬间失去了常用特征,预测值退化成历史平均,直接没法用。

排查方法很简单:列出模型用到的所有特征,逐一问自己“在预测目标时间点之前,我是否已经知道这个值?”如果答案是“预测当天上午需要当天下午的风速”,那就是泄漏。正确的做法是统一把特征全部替换成滞后版本,比如前一天湿度、前一天风速、前三日平均气压。

4.2 极端气温捕捉不到

随机森林没有外推能力,这是它在气象预测中最明显的短板。如果测试集包含一段热浪,期间温度超过了训练集中出现过的最大值,模型预测值就会被压缩在训练集的历史极值附近,误差会瞬间扩大。

我遇到的一次情况是,训练集最高温 37°C,测试期某天实际 39°C,模型只预测了 35.8°C。后来我做了两个调整:一是在训练阶段加入一个“极端天气标记”,比如气压变化率异常和气温超过历史 95 分位的标记,帮模型提前识别异常情景;二是将预测目标从原始气温改为距平值(气温减去同期历史平均),这样模型预测的是“偏离通常态的幅度”,在一定程度上缓解了对极端值记忆不足的问题。当然,要彻底解决还是得引入物理模型或分位数随机森林,后者能给出预测区间而不是单点值。

4.3 模型训练慢且内存占用高

随机森林虽然比深度学习轻量,但盲目加大参数也会跑不动。有人一上来就设 n_estimators=1000,而且不限制 max_depth,结果一棵树就长出几千个节点,内存直接耗尽。

实操上,树的数量在 100 到 300 之间已经是收益递减区。我用 200 棵和 500 棵做过对比,MAE 差距不到 0.05°C,但训练时间翻了近三倍。同时一定设置max_depth和min_samples_leaf,这能显著减小树体积。别忘了n_jobs=-1让所有 CPU 核一起工作。如果数据量超过十万条,我会先把样本做一次降采样,或者改用 HistGradientBoosting,但这是后话。

4.4 问题速查表

现象可能原因排查与解决
训练集 R² 极高但测试集很差过拟合或特征泄漏限制 max_depth、增大 min_samples_leaf;检查特征是否包含未来信息
预测值总是接近历史平均随机森林外推能力弱或滞后特征不足增加趋势特征、改用距平预测、尝试分位数随机森林
交叉验证结果波动很大时间序列分段不合理或样本量太少改用 TimeSeriesSplit,检查训练与测试期的天气系统差异
特征重要性排序不合理特征共线性或基尼重要性偏差计算特征相关性矩阵,用 permutation_importance 交叉验证
首次预测几天后就明显漂移递归预测导致误差累积改用直接多输出策略,或每个预测步单独训练一个模型

我个人在实际操作中最大的体会是:随机森林在气温预测里最大的价值不是“精度碾压”,而是它作为基线模型,稳定、易用、可解释。调半天参数,其实 1°C 的平均误差已经被固定在那里,再往下压需要换更强的模型。但在那之前,先用随机森林把特征思路和数据链路理顺,能帮你省掉大量返工时间。最后再分享一个小技巧:训练完成后一定要把模型用 pickle 或 joblib 保存下来,同时把“特征列顺序”一起保存,因为模型在预测时会按照训练时的列顺序解释输入,列顺序一变,预测结果就全乱了。这个细节,我替你们踩过坑。

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

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

立即咨询