☰
随机森林分位数回归实战:从点预测到区间预测
2026/9/26 7:44:10 网站建设 项目流程

简介:面向具备一定 Python 与机器学习基础、需要为多输入单输出回归任务提供区间预测的开发者和数据分析师,该资源围绕随机森林与分位数回归结合的 QRFR 模型,给出模型描述、项目背景、设计思路及示例代码。与传统仅输出点预测的回归方法不同,QRFR 可同时输出预测值与置信区间,尤其适合数据存在异方差、非线性或需要量化不确定性的金融分析、气候预测、医疗健康等场景;内容也覆盖了训练复杂性、超参数调节和模型可解释性等难点。压缩包内共 1 个 docx 文档,大小约 33KB,文档以项目背景、模型架构、代码示例等方式组织,便于直接阅读和实操参考;已有 1115 人学习下载。读者可据此掌握 QRFR 的核心原理、多分位数训练思路、关键超参数调节方法以及数据预处理要点,并通过文末代码示例快速搭建自己的区间预测模型。

1. 先看懂 QRFR:它输出的不是一个点,而是一个区间

回归模型只输出一个点时,你真的敢直接拿它做决策吗?股票明天的收盘价、台风登陆的降水量、患者未来一年内的生存概率——这些场景里,预测区间往往比点预测更值钱。随机森林分位数回归(QRFR)解决的就是这个问题:一次训练同时拿到点预测和上下界,用区间告诉决策者“这个数到底有多不准”。这套 Python 实现的每个环节我都会拆开讲:分位数回归与随机森林的原理、数据预处理、模型训练、区间计算和可视化,以及复现时踩过的几个坑。适合正被预测不确定性困扰的开发者和数据分析师。源码包里已整理好模型描述与示例代码,拿到即可按步骤跑通。

2. 为什么是随机森林 + 分位数回归:从点预测到区间预测的选型逻辑

QRFR 这个名字看起来像是把两个成熟方法拼在一起,但真正动手做之前,得先搞清楚一个问题:为什么单独用随机森林或单独用分位数回归都不够?这一章把选型逻辑和背后的原理讲清楚,后面实现时才不会改着改着就不知道自己在算什么。

2.1 分位数回归:从均值得不到的信息

线性回归(OLS)拟合的是条件均值 E(y|x),所有样本共用一条回归线,也共用同一个方差估计。这个假设在房价、股价、气温这类数据上基本站不住——方差会随着特征取值变化,专业说法叫异方差。一旦异方差存在,OLS 给出的“均值 ± 固定标准差”区间就不准:有些样本被高估风险,有些被低估。

分位数回归不做均值假设,它直接拟合条件分位数。第 q 分位对应最小化 pinball loss,q 取 0.1 就是拟合“在 x 条件下,10% 的样本落在下方”的边界。把 q=0.1 和 q=0.9 的两条边界都拟合出来,你就有了一个 80% 预测区间,而且这个区间会随特征变化自动伸缩——这才是它比 OLS 强的地方。普通分位数回归的问题是:模型形式通常还是线性的,或者要手动设计特征交叉,面对几十个特征的非线性关系,拟合能力跟不上。所以要把“非线性拟合”这部分外包给随机森林。

2.2 随机森林回归:让非线性关系有处安放

随机森林是决策树的集成。单棵决策树容易过拟合,深度一上去,训练集分得极细、测试集表现很差;随机森林通过两件事把它拉回来:一是每棵树只用约 63% 的样本(bootstrap 抽样),二是每次分裂只看随机抽出的部分特征。多棵树投票后,方差被摊薄,泛化能力就稳了。这就是随机森林回归算法在表格数据上能长期站稳脚跟的原因,也是随机森林和决策树区别的关键点:不是“一棵更聪明”,而是“一群笨树投票,噪声更小”。

对分位数回归来说,RF 还有一个额外的好处:每棵树在分裂时都在做“样本分组”,叶子里的样本天然构成一个与当前输入“相似”的邻域。预测时新样本落到哪片叶子,那片叶子里的历史样本就是条件分布 F(y|x) 的估计依据——这为区间估计打下了基础。如果只把随机森林当成一个点预测器,这个叶子分组的价值就完全浪费了。

2.3 选型对比:GBM、神经网络还是高斯过程

QRFR 并不是唯一能做区间预测的方案,但在表格数据场景里,它是性价比最高的一条路。下面这张表可以直观看到各方案的差异:

模型点预测精度区间能力数据量需求调参复杂度可解释性
线性回归 + OLS线性场景好弱(等方差假设)小低高
随机森林分位数(QRFR)强(表格数据)强(分位区间)中中中(特征重要性)
GBM / XGBoost极强需另改损失函数中高中
神经网络强但吃数据需专门的不确定性建模大高低
高斯过程小样本强天然概率输出小(难上大数据)中中

GBM 其实也能做分位数,把目标函数换成 pinball loss 就行,但工程上要自己改训练逻辑;神经网络要自己设计 Dropout 或变分推断,对大多数分析任务来说太折腾;高斯过程对非线性表格数据核函数不好选,几千样本以上计算量就开始失控。相比之下,QRFR 用 sklearn 就能直接搭出来,调参路径清晰,还能给出区间——对金融、气候、医疗这类对不确定性和精度都有要求的回归任务来说,这是最务实的组合。

3. 把数据喂进去之前:预处理流程与标准化边界

工程上跑回归,数据清洗花的时间经常比建模还多。这个资源把预处理做成了两个函数——load_data 和 preprocess_data,结构简单,但有几个边界要讲清楚。直接套用没问题,但如果你只复制代码不理解背后的选择,换一份数据就可能翻车。

3.1 一套能跑的预处理流水线

import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.impute import SimpleImputer def load_data(file_path): # 通用入口:CSV 第一行当列名,其余全部读进来 data = pd.read_csv(file_path) return data def preprocess_data(data, target_column): # 特征与标签分离:target_column 是要预测的那一列列名 X = data.drop(columns=[target_column]) y = data[target_column] # 缺失值填充:数值列用均值填充,比直接 drop 行稳健 imp = SimpleImputer(strategy='mean') X = pd.DataFrame(imp.fit_transform(X), columns=X.columns) # 异常值用 IQR 规则做修剪(可选,谨慎使用) # 这里只处理特征,不动标签;树模型对标签异常值有一定容忍度 for col in X.columns: q1, q3 = X[col].quantile([0.25, 0.75]) iqr = q3 - q1 X[col] = X[col].clip(lower=q1 - 1.5 * iqr, upper=q3 + 1.5 * iqr) # 标准化:均值 0、方差 1,对残差分位法更友好 scaler = StandardScaler() X_scaled = scaler.fit_transform(X) return X_scaled, y

逻辑说明:这个流程把数据加载、缺失值填充、异常值修剪、标准化一次走完。入口只接受 CSV,target_column 必须在列名里;异常值修剪用的是 clip 而不是删除,目的是不让异常样本彻底消失——分位数回归本来就要保留尾部信息,clip 只是一个温和兜底。标准化用 StandardScaler,输出是 numpy 数组,后面直接喂给 RandomForestRegressor 没问题。

参数说明:SimpleImputer 的 strategy 参数可选 mean、median、most_frequent,特征分布偏态严重时用 median 更稳;clip 的 1.5 倍 IQR 是箱线图标准,想保留更多尾部就放宽到 3 倍。如果你不确定自己的数据适不适合均值填充,可以先跑一下X.isna().sum()看缺失比例,超过 30% 的列建议直接删掉,填了反而引入噪声。

3.2 标准化最容易翻车的位置:数据泄漏

正文示例里直接fit_transform全量 X,对小项目够用,但放到真实流程里会踩一个数据泄漏问题:scaler 是在整个数据集上拟合的,均值/方差里已经包含了测试集信息。比赛和线上预测场景下,这是硬伤——训练集和测试集应该完全隔离,scaler 只用训练集 fit,测试集只调用 transform。

# 正确切分:先切分,后只对训练集 fit scaler X_train, X_test, y_train, y_test = train_test_split( X_scaled, y, test_size=0.2, random_state=42 ) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) # 注意!这里不能再 fit

逻辑说明:fit_transform 是 fit 加 transform 的合并,测试集如果也 fit_transform,等于把测试集的均值方差“教”给了模型,测试集就失去了独立验证的意义。正确姿势是 fit 一次、transform 多次。

另一个和标准化相关的点:随机森林这类树模型本身不依赖量纲,特征 0~1 还是 0~10000 都不影响分裂效果;但这个资源的区间计算用的是“残差 = 真实值 - 预测值”,残差的量级直接决定区间宽度。如果标签 y 本身跨度极大(比如从 0.5 到 5000),先对 y 做 log1p 变换、预测完再指数还原,区间往往比直接用原始尺度更合理。这一步资源原文没写,但只要是强偏态标签,我一般都会提前做。

3.3 缺失值和异常值:先想清楚为什么填、为什么剪

缺失数据处理有两条路线:一是像上面代码那样用 SimpleImputer 填均值或中位数,二是直接 dropna。样本量足够大、缺失比例低于 5% 时,drop 是可接受的;样本本来就少,一条数据都别浪费,填均值更划算。注意填均值会压缩方差,对分位数区间有影响——区间会略窄,因为尾部信息被平均掉了。所以更严格的做法是给填充项打一个“是否缺失”的标记列,让树模型自己决定怎么用。

异常值同理。正文强调“异常值检测与修复”,但分位数回归恰恰是少数对异常值有容忍度的回归方法:它拟合的是条件分位而不是均值,个别离谱的 y 只会把高分位残差推远一点,不会像 OLS 那样把整条回归线拉偏。所以异常值策略要分情况:特征列异常可以 clip 或 winsorize;标签列的异常值建议保留,顶多检查是不是录入错误。你把异常 y 删了,区间尾部就会失真,后续的覆盖率评估会给你一个虚高的假象。

4. 残差分位怎么变成区间:训练与区间计算细节

模型训练本身不复杂,复杂的是理解“区间从哪来”。这一章先讲随机森林训练时的超参数选择,再讲两种分位数区间的实现思路,最后逐行拆解资源里的示例代码。跑通代码不难,但看懂每一步在算什么,才是你改参数时不心虚的前提。

4.1 随机森林训练:先定这几个超参数

from sklearn.ensemble import RandomForestRegressor def train_rf_model(X_train, y_train, n_estimators=100, max_depth=None, min_samples_leaf=1, random_state=42): # n_estimators:树的数量,决定稳定性和耗时 # max_depth:树深,None 表示不限制,配合 min_samples_leaf 一起用 # min_samples_leaf:叶子最少样本数,调大能显著压过拟合 rf = RandomForestRegressor( n_estimators=n_estimators, max_depth=max_depth, min_samples_leaf=min_samples_leaf, random_state=random_state, n_jobs=-1 ) rf.fit(X_train, y_train) return rf

逻辑说明:训练函数把随机森林回归算法最重要的几个超参数暴露出来,random_state 固定成整数是为了保证每次运行结果一致——不固定的话,同一份数据两次训练结果都会漂移,调试时根本分不清是代码改了还是随机性导致的。n_jobs=-1 表示用所有 CPU 核心,样本量几十万时可以显著加速;但如果在小数据集上跑,并行开销反而让训练变慢,这时候去掉 n_jobs 就好。

参数说明:max_features 没有写进函数,默认回归是 1.0(即全部特征参与分裂),高维小样本时可以手动调低到 sqrt,分裂速度会快很多。min_samples_leaf 是最容易忽略但又最影响过拟合的参数,先记住这个规律:数据量几千时调 1~5,几万时调 5~20。

参数取值范围作用常见误用
n_estimators100~500树越多预测越稳超过 200 后收益递减,纯耗时间
max_depth10~30 或 None限制单棵树复杂度None 时极易过拟合,先试 15
min_samples_leaf1~20叶子最小样本数数据量小时调 1,量大调 5~20
max_features'sqrt' 或默认 1.0每次分裂的特征数高维小样本时调低到 sqrt 更快
random_state固定整数保证可复现不固定则每次结果漂移

这里有个经验:RF 回归里最常见的翻车组合是 n_estimators=500 + max_depth=None + min_samples_leaf=1,训练集得分高得吓人,测试集一跑就露馅。树太深、叶子太纯,等于每个样本都被记住了。随机森林的“防过拟合”是有边界的,边界就是叶子纯度控制。我一般先固定 min_samples_leaf=5,再看 max_depth 10/15/20 的表现,效果比盲目堆树数明显。

4.2 分位数回归:两种实现思路

分位数回归进入随机森林有两条路线,先分清再选。

路线 A(本资源采用):残差分位法。先让随机森林输出点预测 pred,计算训练残差 residual = y - pred,再对残差取分位数,最后把分位残差加回到 pred 上,得到区间的下界和上界。这个思路实现极简,一棵 RF 就能同时给出多个分位区间,训练成本低。它的前提假设是:残差分布在不同特征区间大致稳定。如果数据存在明显的异方差(比如小值时误差小、大值时误差大),这个区间会失真——这正是第 5 章要讲的一个坑。

路线 B(严格 QRFR):Meinshausen 2006 年提出的原始方法。每棵树的叶子不再只存预测均值,而是存落入该叶子的所有训练样本的 y 值;预测时,新样本落到一组叶子里,统计这些 y 值的经验分布,再把分位数取出来。这个做法能自适应异方差——输入落在变化剧烈的区域,叶子里的 y 分布自然更宽,区间也自动变宽。代价是实现更复杂,sklearn 的 RandomForestRegressor 不直接支持,需要自己改写或借助第三方实现。

对这个资源来说,路线 A 足够让使用者理解“区间从哪来”,也是示例代码能跑通的关键;等你想上生产、数据异方差明显时,再把路线 B 当升级方向。两边的核心差异一句话总结:A 是在全局残差上取分位,B 是在局部叶子样本上取分位。

4.3 示例代码逐行拆解

import numpy as np def quantile_regression(X_train, y_train, rf, quantile=0.5): # 第一步:用已训练好的随机森林给出训练集点预测 predictions = rf.predict(X_train) # 第二步:残差 = 真实值 - 预测值,反映模型在每个样本上的误差 residuals = y_train - predictions # 第三步:取残差的指定分位数(q=0.5 -> 中位数,残差接近 0) quantile_residuals = np.percentile(residuals, quantile * 100) # 第四步:用预测值加/减分位残差,构成区间 lower_bound = predictions + quantile_residuals upper_bound = predictions - quantile_residuals # 注意符号,见 5.1 return lower_bound, upper_bound

逻辑说明:predictions 是随机森林对每个样本的期望预测;residuals 是模型在训练集上的误差集合;np.percentile 的第二个参数 q 必须是 0~100 的数,所以分位数 0.5 要乘以 100 变成 50。当 quantile=0.5 时,取的是残差中位数,因为随机森林预测的通常是条件均值附近,残差中位数也会接近 0,所以 lower 和 upper 会对称分布在预测值两侧。

参数说明:quantile 传 0.1 时对应 10% 分位残差,通常是负值,lower = pred + (-值) 就是往下探;quantile 传 0.9 时对应 90% 分位残差,通常是正值,此时想拿到高分位边界应该用 upper = predictions + quantile_residuals。原始示例里 upper_bound 用的是减号,这个写法只在残差中位数恰好为 0 时近似成立,换成 0.9 分位会把上界算到预测值下面去——具体翻车现场放在第 5 章第一条。正确区间姿态是同时取低分位和高分位两个方向的残差。

4.4 评估指标与预测区间可视化

from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score def evaluate_model(y_true, y_pred): mse = mean_squared_error(y_true, y_pred) rmse = np.sqrt(mse) mae = mean_absolute_error(y_true, y_pred) r2 = r2_score(y_true, y_pred) print(f'MSE: {mse:.4f}') print(f'RMSE: {rmse:.4f}') print(f'MAE: {mae:.4f}') print(f'R2: {r2:.4f}') return {'mse': mse, 'rmse': rmse, 'mae': mae, 'r2': r2}

逻辑说明:RMSE 对大误差敏感,MAE 反映平均偏差,R² 是拟合优度。区间预测光看这几个还不够,还要单独看一个指标:区间覆盖率 PICP——测试集里真实值落在预测区间内的比例。这个指标 sklearn 不直接提供,得自己算,放在最后一章讲。

可视化部分,python 数据分析与可视化的常规操作是画折线加半透明区间带:

import matplotlib.pyplot as plt # 只取前 100 个测试样本画,不然图会糊成一团 plt.figure(figsize=(10, 5)) order = np.argsort(y_test[:100]) plt.plot(y_test[order], label='true', marker='o', linewidth=1) plt.plot(y_pred[order], label='pred', linewidth=1.5) plt.fill_between(np.arange(100), lower[order], upper[order], alpha=0.3, label='interval') plt.legend() plt.show()

逻辑说明:先用 argsort 对真实值排序,避免折线来回交叉;fill_between 的 alpha=0.3 把区间画成半透明带。数据量大时只画前 100 个点就够看趋势,重点观察两条线之间的距离是否平稳——如果区间带在某一段明显变宽或变窄,往往就是异方差的信号。

5. 避坑指南:五个让区间预测翻车的细节

这一章全是血泪经验。QRFR 结构不复杂,坑都在细节里。以下五条我都实际跑过,按“现象 → 原因 → 解决”写清楚,每条基本都是可以复现的翻车现场。

5.1 区间上下界倒挂:符号写反的经典现场

现象:算出来的上界小于下界,画图时 fill_between 直接报错,或者区间带穿过所有真实值,怎么看怎么别扭。

原因:示例代码里 upper_bound = predictions - quantile_residuals 只在残差中位数恰好为 0 时成立。你想要 90% 分位区间时,quantile=0.9 对应的是正残差,upper 应该加上它而不是减掉它。减号一放,上界直接跑到预测值下面去了。

解决:同时取两个分位残差,低分位做下界、高分位做上界:

lower = predictions + np.percentile(residuals, 5) upper = predictions + np.percentile(residuals, 95)

这样下界是“预测值加上一个负的残差分位”,上界是“预测值加上一个正的残差分位”,逻辑才完整。

5.2 覆盖率永远达不到 90%

现象:训练集上区间覆盖率接近 100%,测试集上实测不到 70%,差得离谱,还以为公式算错了。

原因:拿训练集残差当误差分布,而随机森林对训练集天然拟合得更好,训练残差偏窄,区间自然太紧。测试集的误差分布通常比训练集更胖,同一个分位数套上去必然漏掉更多点。

解决:改用验证集或 OOB(袋外样本)算残差分布。RandomForestRegressor 有现成的oob_prediction_属性,可以直接拿它计算 OOB 残差再取分位,覆盖率会明显向预期靠拢。注意训练时要设oob_score=True才能启用这个属性。

5.3 训练集 0.98,测试集直接滑到 0.6

现象:R² 训练集高得离谱,测试集掉一半,模型的泛化能力完全不存在。

原因:max_depth=None + min_samples_leaf=1,树完全记住训练样本。随机森林的防过拟合是相对的,不是无限度的,叶子越纯,记忆越深。

解决:min_samples_leaf 从 1 调到 5~20,max_depth 限制在 15 左右,再看测试集表现。树的数量不用加,200 棵和 500 棵在这个问题上差别很小。调完再跑一次 OOB 分数,如果 OOB 和测试集分数接近,说明泛化基本稳了。

5.4 区间宽度和实际误差水平对不上:异方差失效

现象:真实值偏差大的样本,区间反而窄;偏差小的样本,区间很宽。区间带了,但和实际误差完全对不上。

原因:残差分位法的前提是残差分布齐性,即各个特征区域的误差幅度相同。金融、气候这类数据普遍异方差,这个假设不成立,全局残差分位自然无法反映局部误差。

解决:升级到严格 QRFR(叶子存样本 y 值再取分位),或者先对 y 做 log、log1p 变换缩小方差差异,预测完再还原成原始尺度。如果换完变换后区间形态明显改善,基本可以确认是异方差在作祟。

5.5 时间序列被随机切分:泄漏得最隐蔽

现象:代码跑得通、指标也好看,但上线后预测和实际情况对不上,尤其金融和气候数据,回测成绩越好,实盘越心虚。

原因:train_test_split 默认随机洗牌,把未来的数据混进了训练集。随机森林会学到未来信息,测试集里出现的其实是已经见过的样本区域,这叫“时间泄漏”。

解决:按时间顺序切分,前 80% 做训练、后 20% 做测试,不要 shuffle。直接df.iloc[index_cut:]手动切,别再用 train_test_split 的默认行为。拿到带时间戳的数据先看一眼,是不是按时间排序的,这一步能省掉后面所有解释不通的问题。

6. 把区间调到可信:超参调优与覆盖率验证

模型能跑通只是第一步,区间能不能信是另一回事。最后一章讲两个我最常用的验证手段:网格搜索锁定关键超参,以及用 PICP 和 MPIW 两个指标给区间打分。这两件事做完,你才算真正掌握了这份资源的用法。

6.1 网格搜索把关键超参一次扫完

随机森林分位数的超参不多,真正影响区间质量的是 min_samples_leaf、max_depth 和 n_estimators。用 GridSearchCV 一次扫完比较省事:

from sklearn.model_selection import GridSearchCV param_grid = { 'n_estimators': [100, 200], 'max_depth': [10, 15, 20], 'min_samples_leaf': [5, 10, 15], } grid = GridSearchCV(RandomForestRegressor(random_state=42), param_grid, cv=5, scoring='r2', n_jobs=-1) grid.fit(X_train, y_train) print(grid.best_params_)

逻辑说明:cv=5 做五折交叉验证,scoring 用 r2 先锁定点预测;确定后再在同一组参数上检查区间覆盖率。树的数量不用扫 500——边际收益小,时间成本翻倍。注意 GridSearchCV 内部会自己切分数据,所以传入的 X_train 应该是只做了 transform 的测试集之外的数据,别再交叉验证里混入测试集信息。

6.2 区间可信度两个指标:PICP 和 MPIW

PICP(区间覆盖率)是实测落在区间内的比例,MPIW 是区间平均宽度。只看 PICP 会踩“越宽越安全”的坑,所以两项一起看:

lower = predictions + np.percentile(oob_resid, 5) upper = predictions + np.percentile(oob_resid, 95) picp = np.mean((y_test >= lower) & (y_test <= upper)) mpiw = np.mean(upper - lower) print(f'PICP={picp:.2%}, MPIW={mpiw:.4f}')

逻辑说明:oob_resid 是袋外样本的残差,用它算分位比训练残差更接近真实误差分布;picp 表示 90% 分位区间里实际覆盖了多少比例的真实值,mpiw 表示区间平均多宽。一般习惯是 90% 区间对应 PICP ≥ 85% 就算可用,具体看业务对风险容忍度。如果 PICP 不足,先把 max_depth 调低;如果 PICP 够了但 MPIW 特别大,说明树还不够深或叶子样本太杂,反过来微调 min_samples_leaf。

这套方法我前后复现过三次,前两次都在覆盖率上栽了跟头。第一次是没做 OOB 残差,训练集上好看、测试集上露馅;第二次是时间序列数据忘了关 shuffle,模型偷看了未来,指标越好越心虚。从那以后,我每次跑分位数回归前都会强制走一遍:先画残差分布图确认形态,再决定用残差分位还是严格 QRFR;测完点预测必须补上 PICP 和 MPIW,否则再高的 R² 都当它没跑。资源里的模型描述和示例代码是整理好的可直接复现版本,下载后按第 3 章的流水线过一遍数据,再按第 5 章避开那几个坑,基本一次能跑通。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询