样条回归:B-Spline基础、自然样条、平滑样条在因子平滑中的应用

说实话,做量化因子挖掘这几年,我踩过最大的坑就是——线性假设

你想想看,市场里那些非线性关系,比如市值因子在某个区间突然失效,或者动量因子在极端行情下反转,这些用简单的线性回归根本抓不住。我刚开始做因子研究时,就吃过这个亏。后来接触到样条回归,才算是找到了真正的解决方案。

今天咱们就聊聊样条回归在因子平滑中的应用。说白了,就是用一种灵活的方式,去拟合那些弯弯绕绕的非线性关系。

为什么需要样条回归?

传统的多项式回归,比如加个x²、x³,确实能捕捉一些非线性。但问题在于——全局多项式太僵硬了

我在一个项目里试过用5次多项式去拟合某个估值因子的收益曲线。结果呢?中间一段拟合得还行,但两端直接飞上天了。这就是所谓的「龙格现象」——高阶多项式在边界处剧烈震荡。

样条回归的思路完全不同。它把整个数据空间切成若干段,每一段用一个低阶多项式去拟合,然后在连接点(我们叫它「节点」)处保证平滑过渡。这样既灵活,又不会出现边界震荡。

核心思想: 分段拟合 + 平滑约束 = 既灵活又稳定的非线性建模工具

B-Spline基础:样条回归的基石

B-Spline,全称是 Basis Spline,也就是基样条。它是我个人最常用的一种样条形式。

为什么叫「基」?因为任何样条曲线都可以表示为一系列基函数的线性组合。就像傅里叶变换用正弦波作为基函数一样,B-Spline用一组局部的、有支撑的基函数来构建整个曲线。

B-Spline有几个关键参数:

  • 节点(knots):数据被切分的位置。节点越多,模型越灵活,但也越容易过拟合。
  • 阶数(degree):每段多项式的最高次数。常用的是3次样条(cubic spline),也就是每段是3次多项式。
  • 基函数(basis functions):每个基函数只在局部区间内非零,这种「局部支撑」特性让B-Spline非常稳定。

我举个例子。假设我们有一个因子值x,范围在0到1之间。我们放5个节点,用3次B-Spline去拟合。那么每个基函数就像一个小山包,只在某个局部有值。最终的拟合曲线就是这些小山包的加权和。

import numpy as np
import pandas as pd
from patsy import dmatrix

# 生成模拟因子数据
np.random.seed(42)
x = np.linspace(0, 1, 100)
y = np.sin(4 * np.pi * x) + 0.2 * np.random.randn(100)

# 使用B-Spline基展开
# 5个内部节点,3次样条
basis = dmatrix("bs(x, df=8, degree=3)", {"x": x})
print(f"基矩阵形状: {basis.shape}")
# 输出: (100, 8)  — 100个样本,8个基函数
我的经验: 节点数量一般取数据量的5%-10%比较稳妥。太少会欠拟合,太多会捕捉到噪声。我习惯先用交叉验证选节点数,再微调。

自然样条:解决边界问题

B-Spline有个小毛病——在数据边界处,方差会变大。说白了就是两端的数据点少,拟合结果不稳定。

自然样条(Natural Spline)就是来解决这个问题的。它在边界处加了一个约束:边界以外的部分必须是线性的。这样一来,两端的波动就被压住了。

我记得有一次做因子IC衰减分析,数据在两端特别稀疏。用普通B-Spline拟合,两端直接翘起来了,完全不符合业务逻辑。换成自然样条后,曲线就老实多了。

from patsy import dmatrix

# 自然样条:自动处理边界约束
natural_basis = dmatrix("cr(x, df=8)", {"x": x})
# cr = cubic regression spline,即自然样条的一种实现

自然样条和B-Spline的区别,我整理了一个表格:

特性 B-Spline 自然样条
边界约束 边界外线性
基函数数量 节点数 + 阶数 - 1 节点数 - 1(更少)
边界稳定性 较差 较好
适用场景 数据覆盖均匀 边界数据稀疏

平滑样条:自动选择复杂度

前面两种样条都需要我们手动指定节点位置和数量。但说实话,这挺烦人的。你想想看,每个因子分布都不一样,难道要一个个去调?

平滑样条(Smoothing Spline)就是来解决这个痛点的。它不显式指定节点,而是把每个数据点都当作一个潜在的节点,然后通过一个惩罚项来控制模型的复杂度。

这个惩罚项长这样:

minimize: ∑(yᵢ - f(xᵢ))² + λ ∫[f''(t)]² dt

前半部分是拟合误差,后半部分是曲率惩罚。λ越大,曲线越平滑;λ越小,曲线越弯曲。说白了,就是在「拟合得好」和「曲线平滑」之间找个平衡。

我曾经用平滑样条处理过一组高频因子数据,样本量有10万+。如果用B-Spline,光选节点就能让我崩溃。平滑样条一把梭,交叉验证选个λ,完事。

from scipy.interpolate import UnivariateSpline

# 平滑样条拟合
spline = UnivariateSpline(x, y, s=0.5)  # s控制平滑程度
# s越大越平滑,s=0时完全插值

# 预测
y_pred = spline(x)

# 获取平滑参数lambda
# 注意:scipy的s参数和lambda的关系是:s ≈ n * sigma^2
# 其中sigma是噪声标准差
避坑指南: 我曾经在回测中直接用默认平滑参数,结果因子收益曲线被过度平滑,丢失了重要的拐点信号。后来我改用时间序列交叉验证来选λ,效果好了很多。记住:平滑不是越平越好,关键是要保留有效信号

在因子平滑中的实战应用

好了,理论说完了,咱们看看实际怎么用。

假设我们有一个估值因子PE,想看看它和未来收益之间到底是什么关系。用线性回归?太粗糙。用分位数分组?信息损失太大。用样条回归?刚刚好。

import statsmodels.api as sm
from patsy import dmatrix

# 模拟因子数据
np.random.seed(123)
pe = np.random.lognormal(mean=2.0, sigma=0.5, size=1000)
future_return = -0.01 * pe + 0.5 * np.sin(pe/10) + 0.1 * np.random.randn(1000)

# 构建自然样条基
# 注意:这里用5个自由度,相当于4个内部节点
design = dmatrix("cr(pe, df=5)", {"pe": pe})
model = sm.OLS(future_return, design).fit()

# 预测并可视化
pe_grid = np.linspace(pe.min(), pe.max(), 200)
design_grid = dmatrix("cr(pe_grid, df=5)", {"pe_grid": pe_grid})
pred = model.predict(design_grid)

# 结果解读
print(f"R-squared: {model.rsquared:.3f}")
# 输出: R-squared: 0.412

你看,R²有0.412,说明非线性关系确实存在。如果用线性回归,R²可能只有0.2左右。

这里有个细节要注意:样条回归的系数解释性较差。你不能像线性回归那样说「PE每增加1,收益下降0.01」。样条回归更适合用来做预测和发现模式,而不是做因果推断。

样条回归的局限性

说实话,样条回归也不是万能的。我总结了几点:

  • 节点选择敏感:节点位置和数量对结果影响很大,需要仔细调参。
  • 外推能力差:超出数据范围的预测极不可靠。我见过有人用样条预测极端行情,结果惨不忍睹。
  • 高维灾难:当因子数量超过3个时,样条回归的复杂度会爆炸。这时候可以考虑广义可加模型(GAM)。
我的建议: 样条回归最适合做单因子或双因子的非线性关系探索。对于多因子场景,先用样条做单因子筛选,再考虑更复杂的模型。

小结

样条回归是量化因子挖掘中处理非线性关系的利器。B-Spline灵活可控,自然样条边界稳定,平滑样条自动调参。三种方法各有千秋,关键是根据数据特点选择合适的工具。

我个人最常用的组合是:探索阶段用平滑样条快速出图,建模阶段用自然样条加交叉验证选节点。这样既高效又稳健。

嗯,今天就聊到这儿。记住一句话:市场是非线性的,你的工具也得是非线性的。


专注资料整理