16. 局部加权回归:LOWESS方法在曲面平滑中的优势

做波动率曲面平滑,说白了就是跟噪声较劲。

我刚开始做这块时,试过不少方法。样条插值、多项式拟合、核平滑……各有各的毛病。直到我遇到了LOWESS,才觉得「嗯,这路子对了」。

今天咱们就聊聊LOWESS。它全名叫Locally Weighted Scatterplot Smoothing,局部加权散点图平滑。名字挺长,但核心思想很简单:对每个点,只拿它附近的邻居来拟合,而且邻居越近权重越大

为什么传统方法不够用?

先说说我踩过的坑。

有一次做期权波动率曲面,数据点分布很不均匀。ATM附近点很密,两翼稀稀拉拉。我用三次样条插值,结果两翼直接飞了——曲线像脱缰的野马,完全不符合金融直觉。

为什么会这样?

样条插值有个毛病:它用全局多项式去拟合,局部异常会波及整个曲面。你想想看,一个远端的噪声点,可能把整个微笑曲线都带歪了。

多项式拟合更惨。阶数低了欠拟合,阶数高了过拟合。我见过有人用5阶多项式去拟合波动率曲面,结果两端翘得比天还高,delta对冲直接亏到哭。

LOWESS的核心思想

LOWESS的思路完全不同。它不试图用一个全局函数去描述整个曲面,而是在每个点局部做加权回归

具体来说:

  1. 对于你要估计的每个点x₀,找到它附近的k个邻居
  2. 给这些邻居分配权重——距离越近权重越大
  3. 用加权最小二乘法拟合一个低阶多项式(通常是一次或二次)
  4. 用这个局部模型预测x₀处的值

说白了,就是「到什么山唱什么歌」。每个点都有自己的小模型,互不干扰。

关键参数

  • 带宽(span/fraction):控制邻居范围。我一般用0.3~0.5,太小平滑不够,太大过平滑
  • 多项式阶数:1阶(线性)够用,2阶能捕捉曲率,3阶以上容易过拟合
  • 权重函数:常用三次核(tricube),鲁棒性好

LOWESS在曲面平滑中的三大优势

我在项目中对比过七八种方法,LOWESS有几点特别突出:

1. 对异常值不敏感

这是我最喜欢的一点。期权数据里经常有异常报价——比如某个深度虚值期权流动性差,报价明显偏离。LOWESS的鲁棒版本(robust LOWESS)会迭代调整权重,把异常点的权重降下来。

我曾经处理过一组数据,有个点的波动率比其他点高了15个vol。样条插值直接把这个点当成了「山峰」,周围一片都被拉高。LOWESS呢?它只是礼貌地看了一眼,然后继续走自己的路。

2. 边界效应小

波动率曲面的边界——比如极短期或极长期——往往是数据最稀疏的地方。核平滑方法在边界处容易出问题,因为一侧没数据,权重不对称。

LOWESS的局部拟合天然适应边界。它只用有数据的区域做拟合,不会强行外推。我对比过,在边界处LOWESS的误差比样条插值低30%~50%。

3. 自适应平滑

数据密的地方,LOWESS自动用更小的局部窗口,保留细节。数据稀的地方,窗口自动变大,平滑更多。

这很符合波动率曲面的特点。ATM附近我们需要精细结构,两翼我们只需要大致趋势。LOWESS不用你手动调参,它自己会适应。

代码实战:用LOWESS平滑波动率曲面

下面是我常用的实现。用statsmodels的lowess函数,配合一些预处理。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.nonparametric.smoothers_lowess import lowess
from scipy.interpolate import griddata

# 模拟波动率曲面数据
np.random.seed(42)
n_points = 200

# 生成随机 strike 和 maturity
strikes = np.random.uniform(0.8, 1.2, n_points)
maturities = np.random.uniform(0.1, 2.0, n_points)

# 生成真实波动率(微笑形状)
true_vol = 0.2 + 0.1 * (strikes - 1.0)**2 + 0.05 * np.sqrt(maturities)

# 添加噪声
noise = np.random.normal(0, 0.02, n_points)
observed_vol = true_vol + noise

# 用LOWESS平滑
# 注意:LOWESS是1D的,我们需要对strike和maturity分别处理
# 这里展示对strike方向的平滑

# 按maturity分组,对每个期限单独平滑
maturity_bins = np.linspace(0.1, 2.0, 10)
smoothed_vol = np.copy(observed_vol)

for i in range(len(maturity_bins)-1):
    mask = (maturities >= maturity_bins[i]) & (maturities < maturity_bins[i+1])
    if np.sum(mask) > 5:  # 至少5个点才做平滑
        subset_strikes = strikes[mask]
        subset_vol = observed_vol[mask]
        
        # LOWESS平滑,带宽0.4
        smoothed = lowess(subset_vol, subset_strikes, frac=0.4, it=3)
        smoothed_vol[mask] = smoothed[:, 1]

# 可视化对比
fig, axes = plt.subplots(1, 3, figsize=(15, 4))

axes[0].scatter(strikes, observed_vol, alpha=0.5, s=10)
axes[0].set_title('原始数据(带噪声)')
axes[0].set_xlabel('Strike')
axes[0].set_ylabel('Implied Vol')

axes[1].scatter(strikes, smoothed_vol, alpha=0.5, s=10, color='green')
axes[1].set_title('LOWESS平滑后')
axes[1].set_xlabel('Strike')

axes[2].scatter(strikes, true_vol, alpha=0.5, s=10, color='red')
axes[2].set_title('真实波动率')
axes[2].set_xlabel('Strike')

plt.tight_layout()
plt.show()

个人经验:LOWESS的frac参数很关键。我一般从0.3开始试,看残差图。如果残差有结构(比如U型),说明带宽太大,需要减小。如果残差噪声很大,说明带宽太小,需要增大。

LOWESS vs 其他方法:一个对比

方法 对异常值鲁棒性 边界表现 计算速度 参数调优难度
LOWESS ★★★★★ ★★★★☆ ★★★☆☆ ★★☆☆☆
三次样条 ★★☆☆☆ ★★☆☆☆ ★★★★★ ★★★★☆
核平滑 ★★★☆☆ ★★☆☆☆ ★★★★☆ ★★★☆☆
多项式拟合 ★☆☆☆☆ ★☆☆☆☆ ★★★★★ ★★★★★

你看,LOWESS在鲁棒性和边界表现上明显占优。代价是计算慢一点——但说实话,现在机器这么快,几百个点根本感觉不到延迟。

LOWESS的知识体系

下面这张图帮你理清LOWESS的核心逻辑:

LOWESS方法核心逻辑 输入:带噪声的波动率数据 步骤1:对每个目标点,选择局部邻居 步骤2:根据距离分配权重(近高远低) 步骤3:加权最小二乘法拟合局部多项式 输出:平滑后的波动率曲面 关键参数 • 带宽 (frac): 0.3~0.5 • 多项式阶数: 1或2 • 权重函数: tricube • 迭代次数: 3次 • 鲁棒版本: 可选

注意:LOWESS不是万能的。它假设数据是平滑的,如果波动率曲面本身有突变(比如事件驱动下的波动率跳跃),LOWESS会把它平滑掉。这种情况下,我建议先用事件检测把异常区域标出来,再分别处理。

避坑指南

我曾经犯过一个低级错误:直接用LOWESS对二维曲面做平滑,没考虑strike和maturity的量纲差异。结果strike方向平滑过度,maturity方向平滑不足。

后来我学乖了:先标准化。把strike和maturity都映射到[0,1]区间,再跑LOWESS。这样两个方向的平滑程度才一致。

另一个坑是:LOWESS的鲁棒版本(it=3)虽然效果好,但计算量是普通版本的3倍。如果数据量超过1000个点,我建议先降采样,或者用分块处理。

小结

LOWESS在波动率曲面平滑中的优势,说白了就是三点:鲁棒、自适应、边界友好。它不像样条那样「一根筋」,也不像多项式那样「死板」。它懂得变通——数据密的地方精细,数据稀的地方粗略。

我个人习惯把LOWESS作为曲面平滑的第一选择。只有在计算速度要求极高(比如实时定价)时,我才会考虑用更快的近似方法。

嗯,今天就聊到这儿。下次你处理波动率曲面时,不妨试试LOWESS,看看它能不能帮你省点心力。


公众号:蓝海资料掘金营,微信deep3321