4、数据预处理:缺失值处理(插值法)、异常值检测、平稳性检验(ADF检验)

收益率曲线数据,说白了就是不同期限的利率点连成的一条线。但现实中的数据,从来不会像教科书那么干净。我做了这么多年量化,几乎每套收益率数据拿过来,第一件事就是「洗数据」。今天咱们就聊聊这个环节——缺失值、异常值、还有平稳性检验。

4.1 缺失值处理:插值法

先说说缺失值。你想想看,国债收益率数据经常会出现某个期限的利率没报价,尤其是长期限或者某些流动性差的券种。我遇到过最夸张的一次,30年期国债收益率连续三天没数据,整个曲线中间断了一截。

处理缺失值,我个人习惯首选插值法。为什么?因为收益率曲线本质上是连续的,相邻期限的利率之间有很强的相关性。直接删掉那行数据太浪费了,尤其是时间序列分析中,样本本来就珍贵。

4.1.1 线性插值

最简单的办法,线性插值。假设你缺了5年期收益率,前后有4年期和6年期的数据,那就按时间权重算一个中间值。

import pandas as pd
import numpy as np

# 假设df是收益率数据,index是期限
df = pd.DataFrame({
    'yield': [2.1, 2.3, None, 2.7, 2.9]
}, index=[1, 2, 3, 4, 5])

# 线性插值
df['yield_interp'] = df['yield'].interpolate(method='linear')
print(df)

嗯,这里要注意:线性插值只适用于缺失值前后都有数据的情况。如果缺在开头或者结尾,那就得用别的方法了。

4.1.2 样条插值

我个人更推荐样条插值,尤其是处理收益率曲线这种光滑曲线。样条插值能保证曲线的一阶、二阶导数连续,说白了就是曲线更平滑,更符合市场实际。

from scipy.interpolate import CubicSpline
import numpy as np

# 已知期限和收益率
maturities = np.array([1, 2, 3, 5, 7, 10])
yields = np.array([2.1, 2.3, 2.5, 2.7, 2.9, 3.1])

# 创建三次样条插值函数
cs = CubicSpline(maturities, yields)

# 插值4年期收益率
yield_4y = cs(4)
print(f'4年期插值收益率: {yield_4y:.4f}')
我的经验: 样条插值在期限结构完整时效果很好。但如果你缺失的数据太多(比如连续缺了3个以上期限),插值结果可能失真。这时候我建议先检查数据源,而不是强行插值。

4.2 异常值检测

异常值,说白了就是那些明显「不对劲」的数据点。比如某天1年期收益率突然跳了50个基点,而其他期限纹丝不动。这种情况我见过太多次了——要么是数据录入错误,要么是某个特殊交易导致的异常报价。

怎么检测?我常用的方法有两种:

4.2.1 基于统计的方法

最简单的,用Z-score。假设收益率服从正态分布,超过3倍标准差的数据点就标记为异常。

from scipy import stats

# 计算Z-score
z_scores = np.abs(stats.zscore(df['yield'].dropna()))
threshold = 3
outliers = np.where(z_scores > threshold)[0]
print(f'异常值索引: {outliers}')

但说实话,这个方法有个坑:如果异常值本身影响了均值和标准差的计算,那检测效果会大打折扣。我曾经踩过这个坑,后来改用MAD(中位数绝对偏差)方法,更稳健一些。

4.2.2 基于IQR的方法

另一种常用方法是四分位距法。把数据分成四份,超出Q1-1.5*IQR或Q3+1.5*IQR范围的,就算异常。

Q1 = df['yield'].quantile(0.25)
Q3 = df['yield'].quantile(0.75)
IQR = Q3 - Q1

lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

outliers = df[(df['yield'] < lower_bound) | (df['yield'] > upper_bound)]
print(f'异常值数量: {len(outliers)}')
避坑指南: 我曾经在国债期货套利策略中,因为没处理好异常值,导致回测结果虚高。后来发现是某天数据里混入了一个错误报价。从那以后,我每次做预处理都会先跑一遍异常值检测,绝不偷懒。

4.3 平稳性检验:ADF检验

好了,缺失值和异常值处理完了,接下来是关键一步——平稳性检验。为什么重要?因为大多数时间序列模型(比如ARIMA、VAR)都要求数据是平稳的。如果数据不平稳,模型结果就是「伪回归」,说白了就是看起来相关,其实毫无意义。

ADF检验(Augmented Dickey-Fuller test)是最常用的单位根检验方法。它的原假设是「存在单位根,数据不平稳」。如果p值小于0.05,就拒绝原假设,认为数据平稳。

4.3.1 实战代码

from statsmodels.tsa.stattools import adfuller

# 假设yield_series是收益率时间序列
result = adfuller(yield_series.dropna())

print(f'ADF统计量: {result[0]:.4f}')
print(f'p值: {result[1]:.4f}')
print(f'临界值:')
for key, value in result[4].items():
    print(f'  {key}: {value:.4f}')

if result[1] < 0.05:
    print('结论:拒绝原假设,序列平稳')
else:
    print('结论:无法拒绝原假设,序列非平稳')

4.3.2 差分处理

如果ADF检验发现数据不平稳怎么办?别慌,这是常态。收益率曲线数据经常是非平稳的,尤其是长期利率。我一般会做一阶差分,然后再检验。

# 一阶差分
yield_diff = yield_series.diff().dropna()

# 再次检验
result_diff = adfuller(yield_diff)
print(f'差分后p值: {result_diff[1]:.4f}')

嗯,这里要注意:差分次数不是越多越好。差分一次通常就够了,过度差分会丢失信息。我个人习惯最多差分两次,如果还不平稳,那就要考虑数据本身的问题了。

核心要点: 收益率曲线的主成分分析,通常是对收益率的一阶差分(即利率变动)进行的,而不是对收益率本身。因为收益率本身往往非平稳,但变动量是平稳的。这一点很多新手容易搞混。

4.4 本章知识体系

为了让你更直观地理解整个数据预处理的流程,我画了一张图:

收益率曲线数据预处理流程 原始收益率数据 步骤1:缺失值处理(插值法) 线性插值 样条插值 前向/后向填充 步骤2:异常值检测 Z-score方法 IQR四分位距法 步骤3:ADF平稳性检验

这张图把整个流程串起来了。从原始数据开始,先处理缺失值,再检测异常值,最后做平稳性检验。每一步都有多种方法可选,具体用哪个,取决于你的数据特点和分析目标。

我的建议: 实际项目中,这三个步骤不是完全线性的。有时候你做完异常值检测,发现某个异常点其实是缺失值导致的,那就得回头重新插值。所以,预处理是一个迭代的过程,别指望一次搞定。

好了,数据预处理这块就聊到这儿。记住一句话:好的分析,从干净的数据开始。收益率曲线的主成分分析也不例外。下一节咱们就正式进入PCA的实战环节了。


公众号:蓝海数据掘金营,微信deep3321