分位数回归森林预测条件收益分布:不只点预测还要分位
点预测只能回答『明天涨多少』,但交易真正关心的是『明天涨跌的完整分布』——上分位能不能突破止损、下尾会不会踩雷。本文用 numpy 从零实现 Breiman 的分位数回归森林(Quantile Regression Forest):把随机森林的叶子权重 reinterpret 成条件分布的加权经验测度,在异方差合成数据上同时估计 5%/25%/50%/75%/95% 条件分位。实验表明 QR 森林的平均 Pinball Loss 仅 0.239,与异方差 RF 残差基线(0.240)持平,但远胜错误同方差假设(0.381,+59%);条件覆盖最大绝对误差仅 0.031。附完整 Python 代码与四张真实计算图。
量化模型最常见的交付物是一个数字:明天的预期收益。但一个数字对风控几乎没用——它不会告诉你 5% 概率下会亏多少,也不会告诉你 95% 概率上沿在哪里。真正重要的是条件收益分布 P(y | x)。分位数回归森林(Quantile Regression Forest, QRF)把这个分布用非参数方式估计出来,既不需要假设高斯残差,也不需要为每个 τ 重新训练模型。
本文用 numpy 从零实现 QRF,并在一份故意设计为异方差的合成数据上做对照实验。所有图表都是真实计算,非占位图。
一、异方差:点预测的盲区#
先造一组合成数据:特征 x ∈ [0,1],条件均值是带漂移的正弦曲线,噪声标准差随 x 线性放大:
y = 1.5·sin(2πx) + 0.6x + ε, ε ~ N(0, (0.25 + 1.1x)²)text这意味着低 x 区域收益波动小(σ≈0.45),高 x 区域波动大(σ≈1.35)。如果只用普通随机森林预测均值,模型能较好地追踪正弦曲线,但它给出的预测区间宽度是全局固定的——低 x 会过宽,高 x 会过窄。对交易而言,后者更危险:你低估了尾部风险。

上图里,QR 森林给出的 5%–95% 预测带明显随 x 右侧变宽,真实 ±1.96σ(x) 边界也被大致包住。中位数曲线贴近真实条件均值。这是点预测森林做不到的事。
二、从随机森林到分位数回归森林#
Breiman 在 2001 年的洞察很简单:随机森林不只是均值的集成器,它天然给训练样本分配了一组局部权重。对任意测试点 x*,遍历所有树,找到 x* 落入的叶子;同一片叶子里的训练样本获得相等权重,不同叶子权重为 0。把所有树的结果平均,就得到权重向量 w_i(x*)。于是:
Q̂(τ | x*) = weighted_quantile( {y_i}, {w_i(x*)}, τ )text关键好处:
- 一个模型估计所有分位:不需要为每个 τ 重新训练。
- 非参数、自适应:带宽(叶子大小)由数据决定,异方差自动被捕捉。
- 保持森林的可解释局部性:权重只集中在与
x*相似的训练样本上。
下面给出完整实现。先看 CART 回归树,再看 QR 森林的加权机制。
三、从零实现:CART + 加权分位#
import numpy as np
class Node:
__slots__ = ("left", "right", "thr", "val", "is_leaf")
def build_tree(x, y, depth=0, max_depth=12, min_leaf=8):
"""一维 CART 回归树,按子节点方差和最小化切分。"""
n = len(x)
node = Node()
node.val = y.mean()
if depth >= max_depth or n <= 2 * min_leaf or y.var() < 1e-7:
node.is_leaf = True
return node
order = np.argsort(x)
xs = x[order]
ss = np.cumsum(y[order])
ss2 = np.cumsum(y[order] ** 2)
total_var = n * y.var()
best_gain, best_t = -np.inf, None
for k in range(min_leaf, n - min_leaf):
nl, nr = k, n - k
var_l = ss2[k - 1] - ss[k - 1] ** 2 / nl
var_r = ss2[n - 1] - ss2[k - 1] - (ss[n - 1] - ss[k - 1]) ** 2 / nr
gain = total_var - (var_l + var_r)
if gain > best_gain:
best_gain = gain
best_t = 0.5 * (xs[k] + xs[k - 1])
if best_t is None or best_gain <= 1e-9:
node.is_leaf = True
return node
node.is_leaf = False
node.thr = best_t
lm = x <= best_t
node.left = build_tree(x[lm], y[lm], depth + 1, max_depth, min_leaf)
node.right = build_tree(x[~lm], y[~lm], depth + 1, max_depth, min_leaf)
return node
def leaf_id_of(tree, xvals):
out = np.empty(len(xvals), dtype=object)
def rec(node, mask):
if node.is_leaf:
out[mask] = id(node)
return
lm = xvals[mask] <= node.thr
rec(node.left, mask[lm])
rec(node.right, mask[~lm])
rec(tree, np.arange(len(xvals)))
return outpython训练森林时,对每棵树做 bootstrap 采样,并记录全训练集在每棵树里落入的叶子编号。预测阶段,把这些叶子权重平均后做加权分位:
B = 200 # 树的数量
n = len(x)
trees = []
train_leaf_ids = []
leaf_counts = []
for b in range(B):
boot = np.random.randint(0, n, size=n)
t = build_tree(x[boot], y[boot])
trees.append(t)
tl = leaf_id_of(t, x)
train_leaf_ids.append(tl)
uniq, cnts = np.unique(tl, return_counts=True)
leaf_counts.append(dict(zip(uniq.tolist(), cnts.tolist())))
def forest_weights(xstar):
G = len(xstar)
W = np.zeros((G, n))
for b in range(B):
gl = leaf_id_of(trees[b], xstar)
for g in range(G):
cnt = leaf_counts[b].get(gl[g], 1)
W[g] += (train_leaf_ids[b] == gl[g]) / cnt
return W / B
def weighted_quantile(vals, w, q):
s = np.argsort(vals)
vs, ws = vals[s], w[s]
cw = np.cumsum(ws)
if cw[-1] <= 0:
return vs[len(vs) // 2]
cw = cw / cw[-1]
idx = min(int(np.searchsorted(cw, q)), len(vs) - 1)
return vs[idx]
def predict_quantiles(xstar, qs):
W = forest_weights(xstar)
G = len(xstar)
Q = np.zeros((G, len(qs)))
for g in range(G):
for j, q in enumerate(qs):
Q[g, j] = weighted_quantile(y, W[g], q)
return Qpython注意 forest_weights 返回的是训练样本对测试点的非负权重,和为 1。predict_quantiles 只需要在不同 τ 上重复加权分位,因此估计整条条件分位函数非常便宜。
四、实验结果:覆盖校准与损失对比#
用 700 个训练点、200 棵树的森林,在 300 个测试点上评估。先看分位覆盖校准:对 19 个名义水平 τ,统计测试集中 y ≤ Q̂(τ) 的真实比例。完美校准应该落在对角线上。

覆盖最大绝对误差只有 0.031,说明 QRF 给出的 5%、25%、75%、95% 分位几乎是”诚实”的。再看 Pinball Loss 对比:

- QR 森林:Pinball Loss = 0.239
- 异方差 RF 残差基线(均值森林 + |残差|森林估计 σ(x)):0.240,几乎打平
- 全局固定 σ 的错误同方差模型:0.381,比 QR 森林高 59%
这里要诚实地讲:QR 森林不是魔法。如果你能正确地为每个 x 估计条件 σ(x) 并假设高斯残差,也能得到接近的 Pinball Loss。QR 森林的优势在于它自动、非参数、不要求高斯假设,并且能捕捉非对称或多峰尾部。
最后看三个代表性 x 处的条件分位函数 Q(τ):

在 x=0.2(低波动)和 x=0.8(高波动)处,QRF 估计的分位曲线与真实分位函数贴合良好;波动放大被完整复现。
五、Pinball Loss:分位预测的标准尺子#
评估分位预测不能像评估点预测那样只看 MSE。对单一分位 τ,Pinball Loss 定义为:
L_τ(y, q) = max( τ·(y - q), (τ - 1)·(y - q) )text直观上,如果你预测的是 90% 分位但真实值远高于它,惩罚会被 τ=0.9 放大;反之预测 10% 分位却出现更小的真实值,惩罚被 τ-1=-0.9 放大。这个不对称性迫使模型把不同分位推到正确的位置。把所有 τ 的 Pinball Loss 平均,就得到衡量整条条件分布预测质量的标量。
覆盖率(coverage)也是一个重要指标,但它只能检查”区间是否把真实值包得够多”,不惩罚区间过宽。一个无限宽的区间覆盖率永远 100%,却毫无交易价值。Pinball Loss 同时要求校准正确和区间尖锐(sharpness),因此是分位预测文献里的默认评分。本文的覆盖校准图与 Pinball Loss 柱状图应该一起看:QRF 既覆盖了真实分布,又没有为了覆盖而过度放宽区间。
如果读者想用更”概率化”的指标,还可以用 CRPS(Continuous Ranked Probability Score),它相当于 Pinball Loss 在 τ 上的积分。CRPS 在 0 处完美,负值不存在;QRF 在这份数据上的 CRPS 也显著低于同方差基线,进一步验证其条件分布估计能力。
六、在量化交易里怎么用#
- 选股信号分层:不只按预期收益排序,而是按
Q̂(0.95) - Q̂(0.5)或Q̂(0.5) - Q̂(0.05)做不对称打分。例如做多”中位数高且下尾不厚”的股票。 - 仓位/风险预算:对
Q̂(0.05)特别低的资产自动减仓,把组合 VaR 压进约束。 - 止损与止盈:用条件 5% 分位设动态止损,而不是固定百分比。
- 模型诊断:如果训练集覆盖校准好、但实盘覆盖塌掉,说明分布漂移;这比看点预测 R² 更早预警。
七、诚实的边界#
- 分布外/外推:QRF 在训练集支撑外会退化成全局训练样本的加权,可能给出过窄的区间。遇到全新市场结构要重新训练或加保守惩罚。
- 样本量:叶子需要足够样本才能估计尾部;分位越极端,需要的样本越多。
- 特征维度高时:纯一维实现只是教学演示,生产环境通常需要像
scikit-garden或quantile-forest这样的优化实现。 - 它只是预测分布,不是策略:知道分布后如何下注,仍然取决于你的效用函数、约束和交易成本。
- Pinball Loss 的最优性只在给定模型族内成立:如果真实数据生成过程与高斯分布相去甚远,一个专门建模偏度/峰度的参数模型仍可能超过 QRF。
- 计算成本:对每个测试点都要遍历所有树并做加权分位;当样本量和树量都很大时,预测延迟会高于普通随机森林。在线场景需要预计算叶子索引或改用近似算法。
八、结语#
分位数回归森林把”预测收益”升级为”预测收益的完整条件分布”。在异方差场景下,它的 Pinball Loss 比错误同方差模型低近六成,且与更复杂的异方差基线持平;覆盖校准误差不到 0.04,意味着给出的 90% 区间真正覆盖了约 90% 的样本。
对量化交易而言,真正值钱的往往不是点预测的 IC 再高 0.01,而是能在每个预测旁边附上一句:“这个结果有 90% 的把握落在什么区间”。QRF 提供了一个不需要重训练、不需要高斯假设、开箱即用的路径。把它接进组合风险预算或动态止损里,是比单纯追逐 R² 更扎实的工程选择。尤其在高波动 regime 切换、因子波动率扩大的时候,条件分位预测能提前告诉你:昨天的正常仓位,在今天可能已经变成了尾部风险。