为什么要超越线性?
线性模型简洁易用,但现实世界的关系往往是非线性 的。
股价走势呈现周期性波动,而非简单线性趋势
收益率与风险之间可能存在倒U型 关系
经济增长与投资之间的边际效应递减
本章介绍六种突破线性假设的方法,在保持可解释性的同时捕捉复杂模式。
非线性建模要先处理单变量曲线再扩展到多变量结构
学习非线性建模,逻辑上通常要分成两个层次:
先处理单变量曲线 :看清一条曲线如何偏离直线
再扩展到多变量结构 :理解多条非线性关系怎样组合进同一个模型
这两个层次分别对应两类核心任务:
在单变量情形下比较全局方法与局部方法谁更合适
在多变量情形下同时保留灵活性与可解释性
因此,本章真正要建立的是一种判断框架:
先判断非线性发生在哪里,再判断应该用多光滑、多局部、多可解释的方式去拟合它。
线性假设何时失效?来自A股的证据
案例:市盈率(PE)与未来收益率
传统金融理论假设PE与收益率呈线性负相关
但实际数据显示:PE过低(价值陷阱)和PE过高(成长股溢价)时,关系复杂
线性假设失效的三种模式 :
阈值效应
低于/高于某值时关系改变
市盈率 < 10 时,可能是价值陷阱
饱和效应
边际效应递减
研发投入对利润的影响
周期效应
存在周期性波动
消费支出的季节性模式
解决思路 :
不抛弃线性模型的可解释性
而是扩展 线性模型的框架来容纳非线性
同学们一直在用线性回归,但现实世界中纯粹的线性关系其实是例外而非常态。在A股市场,市盈率与未来收益率的关系就不是简单的线性负相关。PE过低可能是价值陷阱——公司基本面恶化导致股价下跌,未来收益率也不一定好。PE适中时负相关最明显,PE很高时又进入另一种逻辑。本章要学的方法,就是在保持可解释性的前提下处理这些非线性关系。
非线性方法的统一框架
核心思想 :所有方法都可以写成基函数展开 的形式
\[ \large{ f(X) = \sum_{m=1}^{M} \beta_m h_m(X) } \tag{1}\]
其中 \(h_m(X)\) 是基函数 (Basis Function),\(\beta_m\) 是系数。
多项式回归
\(X^m\)
全局
阶梯函数
\(I(c_m \leq X < c_{m+1})\)
局部常数
回归样条
\((X - \xi_m)_+^3\)
局部多项式
平滑样条
非参数
自动选节点
关键洞见 :一旦确定了基函数,模型仍然是线性回归 ——对 \(\beta_m\) 是线性的!
理解这一页非常重要。本章看似介绍了很多不同的方法,但它们有一个统一的数学框架——基函数展开。无论是多项式、阶梯函数还是样条,本质上都是构造一组基函数,然后对这些基函数做线性回归。所以你已经学会的所有线性回归工具——最小二乘法、假设检验、置信区间——都可以直接应用。这就是这些方法既灵活又可解释的秘诀。
两类非线性来源:曲率、阈值与局部结构
在实务中,非线性通常来自三类不同机制:
曲率 :变量效应持续变化,但变化速度本身也在变
阈值 :越过某个临界点之后,关系突然切换
局部结构 :不同区间有不同规律,无法用一条全局曲线概括
这三类来源分别对应本章不同方法的优势场景。
六种非线性方法概览
多项式回归
添加 \(x^2, x^3, \ldots\) 项
中
阶梯函数
将 \(x\) 分段为常数区间
低
回归样条
分段多项式 + 节点处平滑连接
高
平滑样条
带粗糙度惩罚的样条
高
局部回归
在每个点附近拟合局部模型
高
GAMs
多变量的可加非线性模型
高
判断非线性的第一步:残差图怎么看?
真正的建模流程不是一上来就上样条,而是先看残差图:
残差随机散开:线性假设可能已经够用
残差呈 U 型或 S 型:说明存在系统性曲率
残差在某些区间突然偏高或偏低:可能存在阈值或分段结构
所以,残差图是从线性走向非线性的第一个信号灯。
实证案例:海康威视股价数据
我们使用海康威视(002415.XSHE) 2010-2025年的日度股价数据作为贯穿全章的实证案例。
数据预处理
Code
# 若 order_book_id 在索引中则重置为普通列
if 'order_book_id' in all_stock_data.index.names: # 检查是否为多级索引
all_stock_data = all_stock_data.reset_index() # 重置索引为普通列
# 统一日期列名为 trade_date
if 'date' in all_stock_data.columns and 'trade_date' not in all_stock_data.columns: # 兼容不同数据源
all_stock_data = all_stock_data.rename(columns= {'date' : 'trade_date' }) # 重命名日期列
# 筛选海康威视并按日期排序
hikvision_data = all_stock_data[all_stock_data['order_book_id' ] == '002415.XSHE' ].sort_values('trade_date' ).copy() # 提取海康威视数据
# 构造时间序号与收盘价
time_index = np.arange(len (hikvision_data)) # 生成0到N-1的时间序号作为自变量
close_price = hikvision_data['close' ].values # 提取收盘价数组作为因变量
# 绘制股价走势
plt.figure(figsize= (10 , 4 )) # 创建10x4英寸画布
plt.plot(time_index, close_price, linewidth= 0.8 , color= 'steelblue' ) # 绑制收盘价折线
plt.xlabel('交易日序号' ) # 设置横轴标签
plt.ylabel('收盘价(元)' ) # 设置纵轴标签
plt.title(f'海康威视收盘价(共 { len (hikvision_data)} 个交易日)' ) # 设置标题
plt.tight_layout() # 自动调整布局
plt.show() # 显示图表
多项式回归的基本思想
多项式回归通过添加高次项来扩展线性模型:
\[
y_i = \beta_0 + \beta_1 x_i + \beta_2 x_i^2 + \cdots + \beta_d x_i^d + \epsilon_i
\]
本质上仍是线性模型 ——对参数 \(\beta\) 线性
阶数 \(d\) 控制灵活性:\(d\) 越大越灵活,但也越容易过拟合
Runge 现象 :高阶多项式在端点处可能剧烈振荡
多项式回归的几何直觉:高次项如何制造弯曲?
从几何上看,高次项是在给直线增加新的’弯曲自由度’:
二次项允许曲线出现一个稳定的弧度
三次项允许曲线出现拐点
更高次项会带来更多局部弯折,但也更容易失控
所以,多项式回归的灵活性,本质上来自对曲线弯曲能力的逐步开放。
多项式回归的数学本质
d阶多项式回归模型 :
\[ \large{ y_i = \beta_0 + \beta_1 x_i + \beta_2 x_i^2 + \cdots + \beta_d x_i^d + \varepsilon_i } \tag{2}\]
等价表述 :创建新变量 \(x_i^2, x_i^3, \ldots, x_i^d\) ,然后做普通多元线性回归 。
为什么d通常 ≤ 5?
高阶多项式在数据边界处剧烈振荡 (龙格现象)
多项式系数随阶数增大变得极度不稳定
阶数过高 → 严重过拟合
补充说明:共线性问题
\(x, x^2, x^3\) 之间往往高度相关,导致系数估计不稳定。 解决方案:使用正交多项式 (Orthogonal Polynomials):
from numpy.polynomial import polynomial as P
# 正交多项式避免了共线性问题
多项式回归的数学本质非常简单——它还是线性回归!只不过把原始变量的幂次作为新的自变量加入。但有一个陷阱:高阶幂次之间高度相关,比如x和x²的相关系数在很多数据中超过0.9。这导致系数估计不稳定,一个小的数据扰动可能让系数符号翻转。实际操作中,建议阶数不超过5,且使用正交多项式来缓解共线性问题。
不同阶数多项式拟合对比
Code
from sklearn.pipeline import Pipeline # 导入Pipeline用于构建建模流水线
from sklearn.preprocessing import PolynomialFeatures # 导入多项式特征变换器
from sklearn.linear_model import LinearRegression # 导入线性回归模型
degree_candidates = [1 , 3 , 5 , 10 , 15 , 20 ] # 候选多项式阶数列表
fig, axes = plt.subplots(2 , 3 , figsize= (12 , 6 )) # 创建2行3列子图网格
axes = axes.ravel() # 将二维子图数组展平为一维便于迭代
time_features = time_index.reshape(- 1 , 1 ) # 将时间序号变为列向量(sklearn要求二维输入)
for idx, degree in enumerate (degree_candidates): # 遍历每个候选阶数
# 构建多项式回归流水线:先做特征变换再拟合线性回归
poly_pipeline = Pipeline([
('poly' , PolynomialFeatures(degree= degree)), # 生成1到d次多项式特征
('lr' , LinearRegression()) # 对多项式特征做最小二乘拟合
])
poly_pipeline.fit(time_features, close_price) # 在全量数据上拟合模型
predicted_price = poly_pipeline.predict(time_features) # 预测拟合值
axes[idx].scatter(time_index, close_price, s= 1 , alpha= 0.3 , color= 'gray' ) # 绑制原始数据散点
axes[idx].plot(time_index, predicted_price, color= 'red' , linewidth= 2 ) # 绑制拟合曲线
axes[idx].set_title(f'阶数 d= { degree} ' ) # 设置子图标题
plt.tight_layout() # 自动调整子图间距
plt.show() # 显示图表
交叉验证选择最优阶数
Code
from sklearn.model_selection import TimeSeriesSplit, cross_val_score # 导入时间序列CV工具
degree_list = list (range (1 , 16 )) # 候选阶数1到15
cv_mean_mse = [] # 存储各阶数的平均交叉验证MSE
tscv = TimeSeriesSplit(n_splits= 5 ) # 5折时间序列交叉验证(保持时间顺序)
for degree in degree_list: # 遍历每个候选阶数
poly_pipeline = Pipeline([
('poly' , PolynomialFeatures(degree= degree)), # 多项式特征变换
('lr' , LinearRegression()) # 线性回归
])
# scoring='neg_mean_squared_error':返回负MSE(sklearn约定越大越好)
cv_scores = cross_val_score(poly_pipeline, time_features, close_price, cv= tscv, scoring= 'neg_mean_squared_error' )
cv_mean_mse.append(- cv_scores.mean()) # 取负号还原为正MSE并求平均
optimal_degree = degree_list[np.argmin(cv_mean_mse)] # 取MSE最小的阶数为最优
plt.figure(figsize= (8 , 4 )) # 创建画布
plt.plot(degree_list, cv_mean_mse, 'o-' , color= 'steelblue' ) # 绑制各阶数的CV MSE折线
plt.xlabel('多项式阶数' ) # 横轴标签
plt.ylabel('交叉验证 MSE' ) # 纵轴标签
plt.title(f'最优阶数 d = { optimal_degree} ' ) # 标题显示最优阶数
plt.tight_layout() # 自动调整布局
plt.show() # 显示图表
对于时间序列数据,我们使用TimeSeriesSplit而非普通KFold来保持时间顺序。交叉验证可能显示低阶多项式反而有最小的MSE,说明多项式回归在长区间拟合上并不理想。后面将看到样条方法在这方面远优于多项式。
为什么训练误差不能决定阶数?
如果只看训练误差,你几乎总会偏向更高阶模型:
阶数越高,模型越容易贴住训练样本
但贴住样本不等于学到规律,也可能只是记住噪声
真正重要的是样本外误差 ,这正是交叉验证的意义
所以,选阶数不是比谁更会贴数据,而是比谁更能推广到未来。
ANOVA逐步检验多项式阶数
除了交叉验证,可以用F检验逐步检验是否需要更高阶项:
import statsmodels.api as sm # 导入statsmodels用于统计检验
from scipy.stats import f as f_dist # 导入F分布用于计算p值
anova_results = [] # 存储各对嵌套模型的F检验结果
for degree in range (1 , 6 ): # 比较1阶vs2阶、2阶vs3阶...4阶vs5阶
# 拟合当前阶数模型
poly_current = PolynomialFeatures(degree= degree) # 当前阶数特征变换
features_current = poly_current.fit_transform(time_features) # 生成当前阶特征矩阵
model_current = sm.OLS(close_price, features_current).fit() # 拟合OLS模型
# 拟合下一阶数模型
poly_next = PolynomialFeatures(degree= degree + 1 ) # 下一阶特征变换
features_next = poly_next.fit_transform(time_features) # 生成下一阶特征矩阵
model_next = sm.OLS(close_price, features_next).fit() # 拟合OLS模型
# 计算F统计量
rss_current = model_current.ssr # 当前模型的残差平方和
rss_next = model_next.ssr # 下一阶模型的残差平方和
df_diff = model_next.df_model - model_current.df_model # 自由度差异
f_stat = ((rss_current - rss_next) / df_diff) / (rss_next / model_next.df_resid) # F统计量
p_val = 1 - f_dist.cdf(f_stat, df_diff, model_next.df_resid) # 右尾p值
anova_results.append({'比较' : f' { degree} 阶 vs { degree+ 1 } 阶' , 'F统计量' : f' { f_stat:.2f} ' , 'p值' : f' { p_val:.4f} ' })
pd.DataFrame(anova_results) # 输出ANOVA比较结果表
ANOVA与F检验的直觉理解
ANOVA的核心问题 :增加模型复杂度后,拟合改善是否显著?
\[ \large{ F = \frac{(\text{RSS}_1 - \text{RSS}_2) / (p_2 - p_1)}{\text{RSS}_2 / (n - p_2)} } \tag{3}\]
\(\text{RSS}_1\) :简单模型(如1阶)的残差平方和
\(\text{RSS}_2\) :复杂模型(如2阶)的残差平方和
分子 :增加的复杂度带来多少拟合改善
分母 :复杂模型的残差方差 (基准噪声水平)
决策逻辑 :
\(F\) 值大 → 拟合改善显著 → 值得增加复杂度
\(F\) 值小 → 改善可能只是偶然 → 保持简单模型
逐步检验:1阶 vs 2阶 → 2阶 vs 3阶 → … → 第一次p值不显著时停止
ANOVA是一个嵌套模型比较的工具。F统计量本质上在问:增加的那一项多项式有没有带来”超出随机噪声水平”的拟合改善?如果F值大,说明改善是真实的,不是噪声;如果F值小,说明多出来的那一项可能只是在拟合噪声。实操时我们按顺序比较:1阶vs2阶、2阶vs3阶……直到第一次p值不再显著就停下来。
多项式回归的局限性
为什么需要更灵活的方法?
问题一:全局影响
修改一处数据 → 影响整条拟合曲线
高杠杆点可能导致曲线在远离该点处也发生扭曲
问题二:边界振荡
多项式在数据范围边界处容易剧烈振荡
龙格现象 (Runge’s Phenomenon):阶数越高,边界振荡越严重
问题三:不适合非光滑关系
如果真实关系有”尖角”或”阶梯”,多项式无法精确拟合
需要分段 (Piecewise)的方法
过渡 :这些局限引出了本章后续的方法——阶梯函数和样条,它们用局部 拟合替代全局多项式。
多项式回归虽然直观,但有几个根本性的局限。首先,它是全局方法——数据一端的变化会影响另一端的拟合。其次是著名的龙格现象:高阶多项式在数据边界处产生剧烈振荡,完全不可控。最后,如果真实关系有拐点或阶梯状变化,光滑的多项式无法很好地捕捉。这些问题催生了阶梯函数和样条方法——它们的核心思想是”分段拟合”,用局部的灵活性替代全局的高阶。
当全局多项式顾此失彼时就该转向局部方法
多项式回归的问题,不是它不能弯,而是它的弯曲方式往往全局联动 :
一端的数据变化会牵动整条曲线
边界位置最容易出现不可信的外推
当不同区间的变化节奏不同,全局多项式往往顾此失彼
一旦出现这些现象,就说明问题已经不再是’阶数够不够高’,而是该不该把整条曲线交给同一个全局函数。
这时,局部方法的价值就会立刻上升。
阶梯函数:将连续变量离散化
阶梯函数将连续变量 \(X\) 分成 \(K\) 个区间,在每个区间内拟合常数:
\[
y_i = \beta_0 + \beta_1 \cdot \mathbf{1}(c_1 \le x_i < c_2) + \cdots + \beta_K \cdot \mathbf{1}(c_K \le x_i < c_{K+1}) + \epsilon_i
\]
切点 \(c_1, \ldots, c_K\) 将 \(x\) 的取值范围分为 \(K+1\) 段
每段内的预测值是一个常数 (该段的均值)
优点:简单直观,易于解释
缺点:对切点位置敏感,不连续的”阶梯”可能不自然
阶梯函数的数学表述
将连续变量 \(X\) 划分为 \(K+1\) 个区间 :
\[ \large{ y_i = \beta_0 + \beta_1 C_1(x_i) + \beta_2 C_2(x_i) + \cdots + \beta_K C_K(x_i) + \varepsilon_i } \tag{4}\]
其中 \(C_k(x) = I(c_k \leq x < c_{k+1})\) 是指示函数 。
系数解读 :
\(\beta_0\) :基准区间(\(x < c_1\) )的均值
\(\beta_k\) :第 \(k\) 个区间相对于基准的平均差异
等价于:对分组变量做单因子ANOVA
金融应用 :
将日成交量分为”低/中/高”三档,分析各档下的收益率特征
将市盈率按分位数分组,比较各组的未来表现
本质上就是”分组比较 ”——在量化投资中非常常见
阶梯函数本质上是”分组比较”。你把连续变量按照一组切分点划分为若干区间,然后在每个区间内假设效应是常数。这在金融分析中其实很常见——比如按市盈率分位数将股票分为5组,比较各组的未来收益率,这就是典型的阶梯函数思路。数学上,它等价于对分组虚拟变量做线性回归。
阶梯函数最适合哪类商业问题?
阶梯函数虽然简单,但在商业与金融里有很稳定的应用位置:
信用评分分箱 :把年龄、收入、负债率分成若干档
分位数组合分析 :把股票按因子值分组比较未来收益
政策阈值研究 :某指标高于监管线之后,企业行为是否改变
只要问题本身天然具有’分档’思维,阶梯函数就非常合适。
阶梯函数拟合与交叉验证
Code
# 将时间序号等频分为8个区间(每个区间约含相同数量的观测值)
hikvision_data['Time' ] = time_index # 添加时间序号列
hikvision_data['Time_bin' ] = pd.qcut(time_index, q= 8 , labels= False ) # 等频分箱为0-7共8组
# 为每个分箱生成哑变量并拟合OLS模型
step_dummies = pd.get_dummies(hikvision_data['Time_bin' ], prefix= 'bin' , drop_first= True , dtype= float ) # 生成虚拟变量
step_features = sm.add_constant(step_dummies) # 添加截距项
step_model = sm.OLS(close_price, step_features).fit() # 拟合阶梯函数OLS模型
plt.figure(figsize= (10 , 4 )) # 创建画布
plt.scatter(time_index, close_price, s= 1 , alpha= 0.3 , color= 'gray' ) # 绑制原始数据
plt.plot(time_index, step_model.fittedvalues, color= 'red' , linewidth= 2 ) # 绑制阶梯函数拟合值
plt.xlabel('交易日序号' ) # 横轴标签
plt.ylabel('收盘价(元)' ) # 纵轴标签
plt.title('阶梯函数拟合(8个等频分箱)' ) # 图表标题
plt.tight_layout() # 自动调整布局
plt.show() # 显示图表
从分段多项式到样条
回归样条 在每段区间拟合低阶多项式,并在节点处施加连续性约束:
分段多项式 :在 \(K\) 个节点处分段,每段独立拟合多项式
连续性约束 :要求函数值、一阶导数、二阶导数在节点处连续
三次样条 :每添加一个节点增加 1 个自由度(共 \(K + 4\) 个参数)
截断幂基 :\(h(x, \xi) = (x - \xi)^3_+\) 是节点 \(\xi\) 处的基函数
为什么节点处必须平滑?
如果节点处不平滑,会产生两个问题:
曲线会在连接点出现肉眼可见的折痕
导数不连续会让边际效应解释失真
因此,样条并不是简单地把多项式拼起来,而是在拼接时强制它们保留连续的经济含义。
为什么三次样条最常用?
样条理论上可以是一阶、二阶、三阶甚至更高阶,但三次样条最常见,因为它在三件事上平衡最好:
足够灵活,能表达常见的弯曲关系
足够平滑,函数值、一阶和二阶导数都能连续
计算稳定,不像更高阶样条那样容易带来额外波动
所以,三次并不是偶然,而是经验与理论共同筛出来的默认选项。
节点数量与位置的选择
节点选择策略 :
等距节点
在数据范围内均匀分布
简单
忽略数据密度
分位数节点
放在数据分位数处
适应数据密度
推荐
CV选择
用交叉验证选节点数
最优
计算量大
经验法则 :
通常 3-5 个节点已经足够
节点放在数据密度高的区域(分位数方法)
通过CV选择最优节点数量
过度节点的风险 :
节点太多 → 过拟合 → CV误差上升
极端情况:\(K = n\) (每个数据点一个节点)→ 完全插值 → 严重过拟合
样条的性能对节点的选择很敏感。最推荐的做法是把节点放在数据的分位数处——比如3个节点就放在25%、50%、75%分位数。这样在数据密集的区域有更多的灵活性。节点数量则可以用交叉验证来选择。经验上,3到5个节点在大多数场景下已经足够了。
自然样条:边界行为更稳健
自然样条在边界区域(最外侧两个节点之外)强制为线性 ,有效减少端点振荡:
边界约束额外减少 4 个参数 → 自由度 = \(K\)
直觉:边界外数据稀少,线性外推比多项式外推更稳健
Python 实现:patsy.cr() 生成自然样条基函数
自然样条的边界约束
三次样条的边界问题 :数据范围之外的外推 非常不可靠。
自然样条的额外约束 :
在边界节点之外强制函数为线性 (而非三次)。
等价于:\(f''(\xi_1) = f'''(\xi_1) = 0\) 且 \(f''(\xi_K) = f'''(\xi_K) = 0\)
每个边界多2个约束 → 自由度减少4个
自然样条自由度 = \(K\) (\(K\) 个节点)
对比 :
三次样条
三次多项式(振荡)
\(K + 4\)
自然样条
线性 (稳定)
\(K\)
金融应用的重要性 :
预测未来股价时,边界外推不可避免
自然样条的线性外推比三次外推更保守、更安全
自然样条解决了一个关键问题:在数据范围之外的外推。普通三次样条在边界外是三次多项式,可能产生剧烈振荡。自然样条强制边界外为线性——虽然这个假设可能不完全正确,但至少不会产生荒谬的预测值。在金融应用中,我们经常需要对极端市场条件下的行为做推断,自然样条的保守外推正是我们需要的。
单变量非线性方法的选择框架
走到这里,单变量方法已经可以压缩成一张选择表:
多项式回归 :适合简单、连续的曲率
阶梯函数 :适合阈值、分箱、规则型问题
自然样条 :适合大多数需要平滑且稳健的实务场景
这也是实际分析中最常见的三种起手式。
自然样条拟合与CV选择自由度
Code
from patsy import dmatrix # 导入patsy用于根据公式生成设计矩阵
df_candidates = list (range (3 , 16 )) # 候选自由度3到15
cv_mse_spline = [] # 存储各自由度的交叉验证MSE
for df_val in df_candidates: # 遍历每个候选自由度
fold_mse_list = [] # 当前自由度下各折的MSE
for train_idx, test_idx in tscv.split(time_features): # 时间序列5折交叉验证
x_train_fold = time_index[train_idx] # 当前折的训练集时间序号
x_test_fold = time_index[test_idx] # 当前折的测试集时间序号
y_train_fold = close_price[train_idx] # 训练集目标值
y_test_fold = close_price[test_idx] # 测试集目标值
# 用patsy cr()生成自然样条基矩阵
basis_train = dmatrix(f'cr(x, df= { df_val} )' , {'x' : x_train_fold}, return_type= 'dataframe' ) # 训练集基函数矩阵
basis_test = dmatrix(f'cr(x, df= { df_val} )' , {'x' : x_test_fold}, return_type= 'dataframe' ) # 测试集基函数矩阵
spline_fold_model = sm.OLS(y_train_fold, basis_train).fit() # 拟合当前折的样条回归
y_pred_fold = spline_fold_model.predict(basis_test) # 在测试折上预测
fold_mse_list.append(np.mean((y_test_fold - y_pred_fold) ** 2 )) # 计算并记录MSE
cv_mse_spline.append(np.mean(fold_mse_list)) # 求5折平均MSE
optimal_df = df_candidates[np.argmin(cv_mse_spline)] # 选取MSE最小的自由度
plt.figure(figsize= (8 , 4 )) # 创建画布
plt.plot(df_candidates, cv_mse_spline, 'o-' , color= 'steelblue' ) # 绑制CV MSE随自由度变化的折线
plt.xlabel('自然样条自由度' ) # 横轴标签
plt.ylabel('交叉验证 MSE' ) # 纵轴标签
plt.title(f'最优自由度 df = { optimal_df} (CV MSE ≈ { min (cv_mse_spline):.2f} )' ) # 显示最优结果
plt.tight_layout() # 自动调整布局
plt.show() # 显示图表
样条 vs 多项式:CV MSE 对比
Code
# 拟合15阶多项式
poly15 = Pipeline([
('poly' , PolynomialFeatures(degree= 15 )), # 15阶多项式特征
('lr' , LinearRegression()) # 线性回归
])
poly15.fit(time_features, close_price) # 拟合模型
pred_poly15 = poly15.predict(time_features) # 预测
# 拟合df=15的自然样条
basis_full = dmatrix('cr(x, df=15)' , {'x' : time_index}, return_type= 'dataframe' ) # 生成全量样条基
spline15_model = sm.OLS(close_price, basis_full).fit() # 拟合样条模型
pred_spline15 = spline15_model.fittedvalues # 获取拟合值
fig, axes = plt.subplots(1 , 2 , figsize= (12 , 4 )) # 创建1行2列子图
axes[0 ].scatter(time_index, close_price, s= 1 , alpha= 0.3 , color= 'gray' ) # 左图:原始数据
axes[0 ].plot(time_index, pred_poly15, color= 'red' , linewidth= 2 ) # 15阶多项式拟合线
axes[0 ].set_title('15阶多项式(边界剧烈振荡)' ) # 子图标题
axes[0 ].set_ylim(- 50 , 100 ) # 限制纵轴范围避免极端值
axes[1 ].scatter(time_index, close_price, s= 1 , alpha= 0.3 , color= 'gray' ) # 右图:原始数据
axes[1 ].plot(time_index, pred_spline15, color= 'blue' , linewidth= 2 ) # 自然样条拟合线
axes[1 ].set_title(f'自然样条 df=15(CV MSE ≈ { min (cv_mse_spline):.2f} )' ) # 子图标题
plt.tight_layout() # 自动调整布局
plt.show() # 显示图表
对比非常鲜明!同样是15个自由度,多项式在边界处出现了灾难性的振荡,预测值偏离实际数据数十倍。而自然样条曲线平滑稳定,与数据高度吻合。样条比多项式好的根本原因是”局部性”——样条每段只影响局部,而多项式是全局的。
样条方法的四大优势
局部性
全局基函数,牵一发动全身
局部基函数,修改节点仅影响附近
边界稳定性
高阶时边界振荡严重
自然样条边界线性化,稳定
自由度效率
需要高阶才能灵活
低自由度即可高灵活度
自适应性
均匀全局
可在变化剧烈区域放更多节点
自动平滑解决手动节点选择的局限
当样条方法开始真正用于建模时,新的问题会立刻出现:
如果我不知道节点该放哪里,能不能让算法自动决定
如果我不想显式设定节点个数,能不能直接约束曲线粗糙度
如果未来要扩展到多变量,哪一种单变量平滑方法最容易拼接成更大的可解释模型
这说明非线性建模的难点已经从’能不能画弯’,转向’如何自动而稳定地控制弯曲程度’。
平滑样条与 GAM 正是在这里发挥作用。
平滑样条:拟合与光滑性的权衡
平滑样条通过最小化带惩罚的目标函数 来拟合数据:
\[
\min_g \sum_{i=1}^{n} (y_i - g(x_i))^2 + \lambda \int g''(t)^2 \, dt
\]
第一项:拟合误差(越小越贴合数据)
第二项:粗糙度惩罚(\(g''(t)^2\) 衡量曲率,越小越光滑)
\(\lambda\) 控制两者的权衡:
\(\lambda = 0\) :完美插值(过拟合)
\(\lambda \to \infty\) :退化为直线(欠拟合)
惩罚参数 \(\lambda\) 与光滑度是什么关系?
\(\lambda\) 可以被理解为’不允许曲线乱弯’的强度旋钮:
\(\lambda\) 小:模型更愿意追随数据细节
\(\lambda\) 大:模型更强调整体平滑
最优值通常不由肉眼决定,而由交叉验证或 GCV 决定
所以,平滑样条真正要学会的不是记公式,而是读懂这个旋钮在控制什么。
有效自由度与LOOCV
平滑样条的解是在所有 \(n\) 个数据点处的自然样条,但通过惩罚实现收缩
有效自由度 (Effective Degrees of Freedom):\(\text{df}_\lambda = \text{tr}(S_\lambda)\)
\(S_\lambda\) 是平滑矩阵,\(\hat{y} = S_\lambda y\)
\(\text{df}_\lambda\) 随 \(\lambda\) 增大而减小
LOOCV 快捷公式 (无需反复拟合):
\[
\text{RSS}_{cv}(\lambda) = \sum_{i=1}^{n} \left(\frac{y_i - \hat{g}_\lambda(x_i)}{1 - \{S_\lambda\}_{ii}}\right)^2
\]
有效自由度是理解平滑样条的关键概念。虽然平滑样条在所有唯一数据点都放了节点(看起来参数无穷多),但惩罚项把实际自由度降下来了。LOOCV的快捷公式是一个巨大的计算优势——只需拟合一次模型就能得到交叉验证误差。
有效自由度为什么比节点个数更重要?
在平滑样条里,真正控制复杂度的不是’放了多少个节点’,而是有效自由度 :
节点几乎可以放在每个观测点上
但惩罚项会把很多自由度收回来
因此,模型真正的灵活性由 df\(_{eff}\) 决定,而不是由节点表面数量决定
这也是平滑样条与回归样条最容易混淆、但最本质的区别之一。
pygam实现平滑样条
Code
from pygam import LinearGAM # 导入pygam线性GAM模型
from pygam import s as s_gam # 导入平滑项函数(避免与pygam.s冲突)
if not hasattr (np, 'int' ): # 兼容新版NumPy中移除np.int别名而旧版pygam仍会引用该名称的情况
np.int = int # 将np.int临时指向Python内置int以保证pygam内部样条基函数构造正常执行
lambda_values = [0.1 , 10 , 1000 ] # 三个代表性的平滑参数值
fig, axes = plt.subplots(1 , 3 , figsize= (14 , 4 )) # 创建1行3列子图
for idx, lam in enumerate (lambda_values): # 遍历三个λ值
# s_gam(0, lam=lam):对第0个特征施加平滑样条,指定λ
gam_model = LinearGAM(s_gam(0 , lam= lam)).fit(time_features, close_price) # 拟合平滑样条GAM
x_grid = np.linspace(0 , len (time_index) - 1 , 500 ).reshape(- 1 , 1 ) # 生成500个均匀网格点用于绘图
y_pred_grid = gam_model.predict(x_grid) # 在网格上预测
axes[idx].scatter(time_index, close_price, s= 1 , alpha= 0.3 , color= 'gray' ) # 绑制原始数据散点
axes[idx].plot(x_grid, y_pred_grid, color= 'red' , linewidth= 2 ) # 绑制平滑样条拟合曲线
axes[idx].set_title(f'λ = { lam} ' ) # 子图标题
plt.tight_layout() # 自动调整布局
plt.show() # 显示图表
网格搜索最优平滑参数
Code
# 使用pygam内置的网格搜索功能自动选择最优λ
optimal_gam = LinearGAM(s_gam(0 )).gridsearch( # 对第0个特征施加平滑样条
time_features, close_price, # 输入时间特征和目标股价
lam= np.logspace(- 3 , 4 , 30 ) # 在[0.001, 10000]范围内搜索30个候选λ值
)
optimal_lambda = optimal_gam.lam[0 ][0 ] # 提取最优λ值
effective_dof = optimal_gam.statistics_['edof' ] # 提取有效自由度
x_grid = np.linspace(0 , len (time_index) - 1 , 500 ).reshape(- 1 , 1 ) # 生成绘图用网格
y_pred_optimal = optimal_gam.predict(x_grid) # 用最优模型在网格上预测
plt.figure(figsize= (10 , 4 )) # 创建画布
plt.scatter(time_index, close_price, s= 1 , alpha= 0.3 , color= 'gray' ) # 原始数据散点
plt.plot(x_grid, y_pred_optimal, color= 'red' , linewidth= 2 ) # 绑制最优平滑样条
plt.title(f'最优平滑样条(λ ≈ { optimal_lambda:.4f} ,有效自由度 ≈ { effective_dof:.2f} )' ) # 显示选中参数
plt.xlabel('交易日序号' ) # 横轴标签
plt.ylabel('收盘价(元)' ) # 纵轴标签
plt.tight_layout() # 自动调整布局
plt.show() # 显示图表
网格搜索在对数刻度上从0.001到10000搜索了30个候选λ值。最优λ约为0.0092,对应有效自由度约19.76。这意味着模型实际使用了约20个等效参数来拟合曲线——在3789个数据点中取得了很好的偏差-方差平衡。
平滑样条 vs 回归样条
节点选择
用户指定节点位置
每个唯一 \(x\) 都是节点
灵活度控制
节点数量和位置
惩罚参数 \(\lambda\)
适用场景
对变量分布有先验知识
不确定节点位置时
计算效率
低参数、快速
高参数但有高效算法
LOWESS 的带宽到底在控制什么?
LOWESS 里的 frac 并不是一个随便调的经验参数,它控制的是:
每次局部拟合会看多大的邻域
模型愿意把多少短期波动当成’信号’
局部曲线是更像平滑趋势,还是更像逐点跟随
所以,带宽本质上是在决定你对’局部性’的定义。
LOWESS的优势与局限
优势 :
✓ 完全非参数 :不假设任何函数形式
✓ 局部自适应 :不同区域可以有不同的趋势
✓ 对离群值稳健 (使用稳健权重迭代时)
✓ 是探索性数据分析 的利器
局限 :
✗ 计算量大 :每个预测点都需要做一次回归 → \(O(n^2)\)
✗ 维度诅咒 :高维空间中”邻域”概念失效
✗ 无法给出参数估计 或系数检验
✗ 不易推广到多变量 场景
在金融中的典型用途 :
探索性分析:绘制”非参数趋势线”
与参数模型对比:LOWESS曲线偏离线性 → 证据表明非线性
不适合正式建模,更多用于数据可视化
LOWESS是一个极好的探索性工具,但不太适合正式建模。它的优势在于完全不假设函数形式,让数据自己说话。但缺点也很明显:计算量大、不适合高维数据、无法给出参数估计。在实际工作中,我通常用LOWESS做数据探索——画出非参数趋势线,看看数据中有没有明显的非线性模式。如果有,再选择合适的参数方法(如样条或GAM)来正式建模。
LOWESS 为什么难以直接推广到高维?
LOWESS 在一维很好理解,但一到高维就立刻遇到困难:
高维空间里’附近’的概念变得模糊
每个局部邻域都需要更多样本才能稳定拟合
计算成本会随着维度迅速上升
这也是为什么 LOWESS 更适合作为单变量或低维探索工具,而不是高维正式模型。
LOWESS实现与带宽选择
Code
from statsmodels.nonparametric.smoothers_lowess import lowess # 导入LOWESS平滑函数
frac_values = [0.01 , 0.05 , 0.2 ] # 三个候选带宽(邻域比例)
fig, axes = plt.subplots(1 , 3 , figsize= (14 , 4 )) # 创建1行3列子图
for idx, frac_val in enumerate (frac_values): # 遍历每个带宽
# lowess返回(x排序, y平滑)的二维数组
smoothed_result = lowess(close_price, time_index, frac= frac_val, return_sorted= True ) # LOWESS平滑
axes[idx].scatter(time_index, close_price, s= 1 , alpha= 0.3 , color= 'gray' ) # 原始数据
axes[idx].plot(smoothed_result[:, 0 ], smoothed_result[:, 1 ], color= 'red' , linewidth= 2 ) # 平滑曲线
axes[idx].set_title(f'frac = { frac_val} ' ) # 设置子图标题
plt.tight_layout() # 自动调整布局
plt.show() # 显示图表
frac=0.01时曲线几乎穿过每个数据点,能捕捉日度波动但过于嘈杂。frac=0.05时平衡了局部细节和整体趋势。frac=0.2时曲线非常光滑,但可能掩盖短期波动信息。实际中应根据分析目的选择:技术分析用小frac,趋势判断用大frac。
多变量非线性为什么比单变量难得多?
单变量非线性只需要回答一条曲线怎么弯,多变量非线性则多了两层复杂性:
如果完全不加限制,模型会非常灵活,但也会迅速失去可解释性与可计算性。
GAM的基本思想
广义加性模型(GAM)将线性模型中的每个线性项替换为灵活的非线性函数 :
\[
y_i = \beta_0 + f_1(x_{i1}) + f_2(x_{i2}) + \cdots + f_p(x_{ip}) + \epsilon_i
\]
每个 \(f_j\) 可以是样条、局部回归等非线性函数
可加性假设 :各特征的效应独立相加(不含交互项)
这是GAM最大的优势也是限制:保持了可解释性 ,但可能遗漏交互效应
GAM的数学表述
广义可加模型 (Generalized Additive Model):
\[ \large{ y_i = \beta_0 + f_1(x_{i1}) + f_2(x_{i2}) + \cdots + f_p(x_{ip}) + \varepsilon_i } \tag{7}\]
每个 \(f_j\) 是单变量非参数函数 (通常用平滑样条实现)。
与普通线性回归的对比 :
单变量效应
\(\beta_j x_j\) (线性)
\(f_j(x_j)\) (非线性)
可加性
✓
✓
交互效应
需手动添加
需手动添加
可解释性
系数 \(\beta_j\)
偏依赖图
关键假设——可加性 :
各变量的效应独立相加 ,没有交互
这是一个强假设 ,但保证了可解释性
如需交互效应:可添加 \(f_{jk}(x_j, x_k)\) 项(二维平滑面)
GAM模型的核心思想可以用一句话概括:对每个自变量分别建立非线性关系,然后把它们加在一起。这保持了可加性——也就是说,你可以分别分析每个变量的效应。代价是假设变量之间没有交互作用。如果确实存在交互,可以添加二维平滑项来捕捉,但这会增加模型复杂度。
GAM 与多元线性回归到底差在哪?
GAM 并不是把线性回归推翻,而是在保留’可加’结构的前提下,把直线替换成平滑曲线:
线性回归:每个变量对应一个斜率
GAM:每个变量对应一条可学习的效应曲线
两者都能拆解变量贡献,但 GAM 允许贡献随变量水平改变
因此,GAM 可以被看成多元线性回归的非线性升级版,而不是完全陌生的新物种。
反向拟合为什么有效?
反向拟合之所以漂亮,是因为它把一个复杂的多变量问题拆成了多个容易处理的一维更新:
每次只修正一个变量的函数形状
其他变量的贡献先临时固定
多轮迭代后,各个函数逐渐协调到一起
这是一种’分而治之’的思想,也是 GAM 可计算的关键。
GAM的优势与局限
优势
自动捕捉每个变量的非线性效应
偏依赖图 使每个变量的效应可视化
可作为建模的第一步——快速探索数据模式
计算效率高(Backfitting算法)
局限
可加性假设 限制了捕捉交互效应的能力
变量间的交互需要手动指定(如张量积平滑)
当交互效应很强时,GAM可能表现不佳
高维情况下参数选择复杂
GAM最大的卖点是可解释性:你可以单独画出每个变量对预测结果的影响曲线。在金融分析中,这对理解哪些因素驱动收益非常有价值。但要注意,如果两个变量的交互效应很强(比如行业和规模的交互),纯粹的可加模型可能不够。
GAM在A股多因子选股中的应用
传统线性多因子模型 :
\[ r_i = \alpha + \beta_1 \cdot \text{PE}_i + \beta_2 \cdot \text{ROE}_i + \beta_3 \cdot \text{市值}_i + \varepsilon_i \]
GAM增强版 :
\[ r_i = \alpha + f_1(\text{PE}_i) + f_2(\text{ROE}_i) + f_3(\text{市值}_i) + \varepsilon_i \]
为什么GAM更适合?
PE效应非线性 :PE极低可能是价值陷阱,PE适中有价值溢价
规模效应非线性 :小盘股和超大盘股的收益特征不同
ROE效应非线性 :ROE过高可能不可持续
偏依赖图的威力 :
可视化每个因子的非线性效应
发现数据中的真实模式,而非强加线性假设
是因子研究的重要探索性工具
GAM在A股多因子选股中有天然的优势。传统多因子模型假设每个因子与收益率呈线性关系,但实证中很多因子效应是非线性的。比如市盈率——PE过低可能是价值陷阱,PE适中才有价值溢价,PE很高则风险巨大。GAM允许每个因子有自己独特的非线性形状,通过偏依赖图可以直观地看到这些形状。
sklearn实现GAM:海康威视多因子模型
构建三因子模型预测海康威视未来5日收益率:
动量因子 :过去20日累计收益率
波动率因子 :过去20日收益率标准差
月份因子 :捕捉A股日历效应
偏依赖图:可视化非线性效应
Code
fig, axes = plt.subplots(1 , 3 , figsize= (14 , 4 )) # 创建1行3列子图
# 偏依赖展示:保持其他变量在训练集分布上平均化,展示单变量的边际效应
PartialDependenceDisplay.from_estimator(
gam_pipeline, # 训练好的GAM管道模型
gam_feature_matrix, # 训练数据
features= ['Momentum' , 'Volatility' , 'Month' ], # 要展示偏依赖的三个特征
ax= axes, # 绑制到预设的子图轴上
subsample= 50 , # 随机下采样50个样本以加速计算(注:生产环境建议≥200)
grid_resolution= 20 # 网格精度为20个点
)
plt.tight_layout() # 自动调整布局
plt.show() # 显示图表
偏依赖图是理解GAM的核心工具。左图(动量)可能显示出倒U形——适度正动量对应较高的未来收益,但过高或过低都不利。中图(波动率)通常显示负向关系——高波动率时预期收益更低。右图(月份)反映了A股的日历效应,某些月份系统性地优于其他月份。
逻辑回归GAM:预测涨跌方向
逻辑回归GAM的准确率约为56.62%,仅略高于随机猜测的50%。从summary可以看到各项的p值——动量和波动率的平滑项可能是显著的,月份的因子项反映了日历效应。Pseudo R²约为0.03,在日频股价预测中属于正常水平。
分类GAM与回归GAM的共同框架
无论是回归 GAM 还是分类 GAM,底层结构其实一致:
都保留了可加的函数结构
都把单变量效应写成可学习的平滑项
区别主要在于响应变量分布与链接函数不同
也就是说,分类 GAM 不是一套新哲学,而是在同一框架里换了观测模型。
ANOVA检验:月份变量的最佳形式
月份变量应以哪种形式入模?我们通过三个嵌套模型的偏差分析来判断:
from pygam import l # 导入线性项函数
# 模型1: 月份作为因子项 f(2)
gam_factor = LogisticGAM(s_gam(0 ) + s_gam(1 ) + f(2 )).fit(pygam_features, direction_target) # 拟合因子项模型
# 模型2: 月份作为线性项 l(2)
gam_linear = LogisticGAM(s_gam(0 ) + s_gam(1 ) + l(2 )).fit(pygam_features, direction_target) # 拟合线性项模型
# 模型3: 月份作为平滑项 s_gam(2)
gam_smooth = LogisticGAM(s_gam(0 ) + s_gam(1 ) + s_gam(2 )).fit(pygam_features, direction_target) # 拟合平滑项模型
# 提取各模型的Deviance和有效自由度
dev_factor = float (np.ravel(gam_factor.statistics_['deviance' ])[0 ]) # 因子项模型偏差
dev_linear = float (np.ravel(gam_linear.statistics_['deviance' ])[0 ]) # 线性项模型偏差
dev_smooth = float (np.ravel(gam_smooth.statistics_['deviance' ])[0 ]) # 平滑项模型偏差
comparison_table = pd.DataFrame({
'模型' : ['f(Month) - 因子项' , 'l(Month) - 线性项' , 's(Month) - 平滑项' ],
'Deviance' : [f' { dev_factor:.2f} ' , f' { dev_linear:.2f} ' , f' { dev_smooth:.2f} ' ],
})
comparison_table # 输出比较表
三种模型的偏差比较显示:因子项模型的Deviance最低(约5060),说明将月份作为离散类别变量处理效果最好。这完全合理,因为月份是一个循环分类变量,用线性趋势或连续平滑来建模都不恰当。在实际建模中,理解变量的本质性质对于选择正确的模型形式至关重要。
分类GAM中的链接函数
从回归GAM到分类GAM :
\[ \large{ \log\frac{P(Y=1|X)}{1-P(Y=1|X)} = \beta_0 + f_1(X_1) + f_2(X_2) + \cdots } \tag{8}\]
链接函数 (Link Function)将线性预测值映射到概率:
Logit
\(\log(p/(1-p))\)
二分类(最常用)
Probit
\(\Phi^{-1}(p)\)
二分类(正态假设)
Log
\(\log(\mu)\)
计数数据(泊松)
分类GAM的优势 :
比逻辑回归更灵活(允许非线性效应)
比黑箱模型更可解释(有偏依赖图)
适合中等复杂度 的预测任务
从回归GAM到分类GAM的跳转很自然——只需要加一个链接函数。最常用的是logit链接,它将线性预测值映射到0-1的概率。分类GAM在实际中非常有用:它比逻辑回归灵活,能捕捉非线性效应;又比深度学习可解释,你可以画出每个变量的效应曲线。在金融风控中——比如信用评分——这种可解释性尤为重要。
什么时候应该给 GAM 加交互项?
当你发现下面这些信号时,就应考虑在 GAM 中加入交互:
单个变量的偏依赖图解释力很弱,但组合关系明显
经济学上明确存在协同效应或替代效应
残差中仍保留与变量组合有关的系统模式
不过,加交互会迅速提高模型复杂度,因此必须有明确理由。
非线性建模的主线是从单变量曲线到多变量结构
把整章压缩后,可以看到一条非常清晰的主线:
先解决单变量下曲线怎样画得合理
再解决多变量下曲线怎样组合后仍然可解释
从多项式到自然样条,再到 LOWESS 与 GAM,方法虽然不断变化,但始终围绕同一个核心张力:
模型需要足够灵活去贴近真实关系,同时又不能灵活到失去解释与泛化能力。
实践建议
从简单开始 :先尝试低阶多项式或基础样条
交叉验证 :始终用 CV 选择模型复杂度(阶数、节点数、\(\lambda\) )
检查边界 :特别关注拟合曲线在数据边界处的行为
多变量问题 :优先考虑 GAMs,必要时加入交互项
与线性基线比较 :非线性模型不一定优于线性——用 CV 验证
中国市场特殊性 :A股日历效应和政策驱动使得非线性建模尤为重要
不要过度解读曲线 :偏依赖图和平滑曲线首先是描述关系形状,不自动等于因果结论
正式汇报要留基线 :无论最后选哪种非线性模型,都保留一个线性或低复杂度样条版本作对照
记住,模型越复杂不一定越好。在金融应用中,过拟合是最大的敌人。始终用样本外评估来验证你的非线性模型是否真正优于简单的线性基线。最后,中国市场有其独特性——政策驱动的市场转折、日历效应、散户主导等特征,使得非线性方法在A股分析中有特殊的价值。