数据清洗与预处理:处理缺失值、异常值检测、日期对齐、插值方法的选择与实现
做利率曲线建模,有一件事我吃了不少亏才明白——数据预处理比模型本身更决定成败。你想想看,再漂亮的样条函数,喂进去一堆脏数据,出来的曲线能看吗?
这一章,我就把我在实际项目中踩过的坑、总结的经验,掰开揉碎了讲给你听。
4.1 缺失值处理:别让空值毁了你的曲线
利率数据里,缺失值太常见了。节假日、数据源中断、交易不活跃……原因五花八门。我个人习惯,拿到数据第一件事就是做缺失值扫描。
核心原则:先理解缺失原因,再选择处理方法。不要上来就插值。
4.1.1 缺失值检测
我一般用 pandas 的 isnull() 配合 sum() 快速定位。但要注意,有些数据看起来是数字,实际上是占位符,比如 9999、-1 这种。我在项目中遇到过某家数据商把缺失值填成 -999,差点没发现。
import pandas as pd
import numpy as np
# 加载利率数据
df = pd.read_csv('yield_curve_data.csv', parse_dates=['date'])
print(df.isnull().sum())
# 检查异常占位符
placeholder_values = [-999, -99, 9999]
for col in df.columns:
for val in placeholder_values:
if (df[col] == val).any():
print(f"警告:{col} 列存在占位符 {val}")
df[col].replace(val, np.nan, inplace=True)
4.1.2 缺失值处理策略
处理缺失值,没有银弹。我根据场景总结了几种做法:
| 场景 | 推荐方法 | 我的经验 |
|---|---|---|
| 短期利率(1M-1Y) | 线性插值 | 流动性好,插值误差小 |
| 长期利率(10Y-30Y) | 样条插值 | 曲线更平滑,符合经济直觉 |
| 节假日缺失 | 前向填充 | 用上一个交易日数据,合理 |
| 连续缺失超过5天 | 模型估计或剔除 | 插值会引入太多人为偏差 |
小技巧:对于关键期限点(如2Y、5Y、10Y),我建议优先使用市场报价,实在没有才用插值。因为插值会掩盖市场真实信息。
4.2 异常值检测:揪出那些离谱的数据点
异常值比缺失值更危险。缺失值你至少知道它不在,异常值会悄悄扭曲你的曲线。我曾经因为一个异常值,导致整个Nelson-Siegel模型的参数估计偏了20%。
4.2.1 统计方法检测
我常用的方法有三种:
- Z-score 方法:适合正态分布的数据。一般取 |Z| > 3 作为异常。
- IQR 方法:适合有厚尾的利率数据。取 Q1 - 1.5*IQR 和 Q3 + 1.5*IQR 之外的点。
- 滚动窗口法:我最推荐。用过去20个交易日的均值和标准差,判断当前点是否异常。
def detect_outliers_rolling(df, column, window=20, threshold=3):
"""
滚动窗口异常检测
"""
rolling_mean = df[column].rolling(window=window).mean()
rolling_std = df[column].rolling(window=window).std()
z_scores = (df[column] - rolling_mean) / rolling_std
outliers = np.abs(z_scores) > threshold
return outliers
# 使用示例
df['outlier_10Y'] = detect_outliers_rolling(df, '10Y_rate')
print(f"检测到 {df['outlier_10Y'].sum()} 个异常点")
4.2.2 业务规则检测
统计方法会漏掉一些明显的业务异常。比如:
- 负利率(在正常市场环境下)
- 相邻期限利差超过历史最大值
- 日内波动超过50个基点
嗯,这里要注意:不要机械地剔除异常值。我建议先标记,然后人工复核。有时候异常值恰恰是市场变盘的信号。
避坑指南:我曾经在2019年某次国债期货交割日,把正常的基差波动误判为异常值剔除了。后来复盘才发现,那是市场结构变化导致的。所以,异常值检测一定要结合市场事件日历。
4.3 日期对齐:让不同期限的数据站在同一起跑线
利率数据最大的麻烦是什么?不同期限的交易日不一样。国债期货有交割日,IRS有重置日,债券有付息日……日期不对齐,你没法做曲线。
4.3.1 统一日期基准
我一般选择 T+0 工作日日历 作为基准。把所有数据都映射到同一个日期序列上。
# 生成统一的工作日序列
import pandas_market_calendars as mcal
# 使用中国银行间市场日历
cal = mcal.get_calendar('SSE')
schedule = cal.schedule(start_date='2023-01-01', end_date='2023-12-31')
trading_days = schedule.index
# 对齐所有数据到交易日
df_aligned = df.set_index('date').reindex(trading_days)
4.3.2 处理不同频率的数据
实际工作中,你会遇到:
- 日频数据(大部分利率)
- 周频数据(某些信用债指数)
- 月频数据(宏观经济指标)
我的做法是:高频向低频对齐时用平均,低频向高频对齐时用插值。举个例子,要把月度CPI数据对齐到日频,我会用样条插值,因为CPI变化是平滑的。
4.4 插值方法的选择与实现
插值是数据预处理的核心环节。说白了,就是用已知点去猜未知点。但怎么猜,差别很大。
4.4.1 线性插值
最简单,也最稳定。适合短期利率,因为短期利率变化近似线性。
from scipy import interpolate
# 线性插值
x = [1, 3, 6, 12] # 期限(月)
y = [2.1, 2.3, 2.5, 2.8] # 利率(%)
f_linear = interpolate.interp1d(x, y, kind='linear')
x_new = [2, 4, 9] # 需要插值的点
y_new = f_linear(x_new)
print(f"线性插值结果:{y_new}")
4.4.2 样条插值
样条插值生成的曲线更平滑。我偏好用 三次样条,因为它能保证一阶和二阶导数连续,曲线看起来更自然。
# 三次样条插值
f_spline = interpolate.CubicSpline(x, y, bc_type='natural')
y_spline = f_spline(x_new)
print(f"样条插值结果:{y_spline}")
重要提醒:样条插值在边界处容易产生振荡(Runge现象)。我建议在两端各加一个虚拟点来控制边界行为。比如,在1个月之前加一个0.5个月的虚拟点,利率设为1个月利率减去一个合理利差。
4.4.3 前向填充
前向填充(Forward Fill)适用于:
- 节假日数据(用上一个交易日)
- 非活跃交易日的报价
- 某些场外衍生品的参考利率
它的逻辑很简单:用最近的有效值填充后面的缺失值。但要注意,如果连续缺失太多,前向填充会引入滞后偏差。
# 前向填充
df_filled = df.fillna(method='ffill')
# 限制最大填充天数
df_filled = df.fillna(method='ffill', limit=5)
4.4.4 插值方法对比
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 线性插值 | 简单、稳定、无振荡 | 不平滑,导数不连续 | 短期利率、流动性好的期限 |
| 样条插值 | 平滑、自然、导数连续 | 边界可能振荡,计算稍复杂 | 长期利率、完整曲线构建 |
| 前向填充 | 保留最新信息、简单 | 引入滞后、不适合趋势数据 | 节假日、非活跃日 |
4.5 知识体系框架
下面这张图,是我自己梳理的数据预处理流程。每次做新项目,我都会先过一遍这个框架。
你看,整个流程是串行的。每一步的输出,都是下一步的输入。我建议你在实际项目中,每一步都做一次可视化检查——把处理前后的数据画出来,一眼就能看出问题。
我的习惯:每次做完数据预处理,我都会生成一份数据质量报告,包含缺失率、异常值数量、插值误差等指标。这份报告既是工作记录,也是后续模型调试的重要参考。
好了,数据预处理这部分就讲到这里。记住一句话:干净的数据是利率曲线建模的基石。花在预处理上的时间,会在后续建模阶段加倍回报给你。