3、数据清洗基础:缺失值处理、异常值检测、重复数据去重、数据类型统一

数据清洗,说白了就是给原始数据「洗澡」。

我在债券做市这行干了快十年,见过太多人拿着脏数据直接跑模型。结果呢?模型跑出来漂亮得很,一上实盘就崩。为什么?因为垃圾进,垃圾出。你想想看,债券数据本身就杂——交易所的、银行间的、经纪商的,格式五花八门。不洗干净,后面全是白费功夫。

核心观点:数据清洗不是体力活,是建模的第一道防线。80%的建模问题,根源都在数据质量上。

3.1 缺失值处理——别急着删

债券数据里,缺失值太常见了。比如某只信用债的「最新成交价」字段是空的,或者「久期」没算出来。很多人上来就 fillna(0),我看了直摇头。

我个人习惯分三步走:

  1. 先诊断缺失模式——是随机缺失,还是系统性缺失?
  2. 再判断缺失比例——低于5%可以填充,高于30%建议直接丢弃该字段。
  3. 最后选填充方法——别只会用均值。

实战技巧:我在处理银行间债券数据时,经常遇到「估值收益率」缺失。这时候我会用同期限、同评级的国债收益率做线性插值。比直接填均值靠谱得多。

# 我个人常用的缺失值处理流程
import pandas as pd
import numpy as np

# 1. 查看缺失情况
print(df.isnull().sum())

# 2. 时间序列数据用前向填充
df['yield'].fillna(method='ffill', inplace=True)

# 3. 数值型字段用中位数(比均值更抗异常值)
df['volume'].fillna(df['volume'].median(), inplace=True)

# 4. 分类字段用众数
df['bond_type'].fillna(df['bond_type'].mode()[0], inplace=True)

⚠️ 注意:千万别对「时间戳」字段做填充。我曾经犯过这个错,把缺失的时间戳填成了前一天的,结果回测时所有交易信号都偏移了。血的教训。

3.2 异常值检测——别让一个坏数据毁了整个模型

债券市场里,异常值往往意味着「错误报价」或者「非理性成交」。比如一只AAA级国债,突然出现5%的收益率——这明显是输入错误。

我常用的检测方法有三种:

方法 适用场景 阈值设置
Z-score 法 正态分布的数据 |Z| > 3 视为异常
IQR 四分位法 偏态分布的数据 Q1 - 1.5*IQR 或 Q3 + 1.5*IQR
滚动窗口法 时间序列数据 超过均值±3倍标准差

嗯,这里要注意。债券收益率数据通常不是正态分布,所以我更推荐 IQR 法。举个例子:

# 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

# 标记异常值
df['is_outlier'] = (df['yield'] < lower_bound) | (df['yield'] > upper_bound)

# 处理方式:我一般用上下限截断,而不是直接删除
df['yield'] = df['yield'].clip(lower=lower_bound, upper=upper_bound)

避坑指南:我曾经在国债期货数据上直接用 Z-score,结果把正常的「跳空缺口」全当异常值删了。后来改用滚动窗口法,结合市场事件日历,才真正解决问题。记住:异常值不一定是错误,可能是市场异动。

3.3 重复数据去重——别让数据「胖」了

债券数据源经常重复推送。比如同一个报价,交易所推了一次,经纪商又推了一次。如果不做去重,你的模型会「学」到这些重复模式,导致过拟合。

我的去重策略:

  • 精确去重:所有字段完全一致,直接 drop_duplicates()
  • 模糊去重:时间戳相差1秒以内,价格相同,视为重复
  • 业务去重:同一只债券、同一方向、同一价格,只保留第一条
# 精确去重
df.drop_duplicates(inplace=True)

# 模糊去重:按债券代码和时间戳分组,保留第一条
df.sort_values('timestamp', inplace=True)
df.drop_duplicates(subset=['bond_code', 'timestamp'], keep='first', inplace=True)

# 业务去重:同一只债券的同一报价方向,只保留最新一条
df.drop_duplicates(subset=['bond_code', 'side', 'price'], keep='last', inplace=True)

⚠️ 注意:去重前一定要先排序。否则保留的是随机顺序,结果不可复现。我见过有人没排序就去重,每次跑出来的数据量都不一样——这还怎么回测?

3.4 数据类型统一——别让字符串坑了你

这个坑我踩过太多次了。债券代码有时候是字符串,有时候是数字。收益率有时候是百分比,有时候是小数。不统一,模型直接报错。

标准化清单:

字段 原始类型 统一类型 说明
bond_code int / str str 防止前导零丢失
timestamp str / int datetime64 统一时间格式
yield float / str float 去掉%符号
volume int / float float 防止溢出
# 数据类型统一
df['bond_code'] = df['bond_code'].astype(str)
df['timestamp'] = pd.to_datetime(df['timestamp'])
df['yield'] = df['yield'].str.replace('%', '').astype(float) / 100
df['volume'] = df['volume'].astype(float)

实战经验:我建议你写一个 clean_dtypes() 函数,把类型转换封装起来。每次接入新数据源,直接调用。省时省力,还不会漏掉字段。

知识体系总览

下面这张图,是我自己总结的数据清洗核心流程。你可以把它贴在工位上:

债券数据清洗核心流程 原始债券数据 缺失值处理 填充/删除/插值 异常值检测 IQR/Z-score/滚动 重复数据去重 精确/模糊/业务 数据类型统一 str/datetime/float 清洗后的标准数据集 建模输入 → 回测 → 实盘交易

这张图的核心逻辑很简单:原始数据进来,经过四个清洗步骤,变成标准数据集。然后才能放心地拿去建模。少一步都不行。

最后说一句:数据清洗没有银弹。每只债券、每个市场、每个数据源都有自己的「脾气」。我的建议是——先写一个通用的清洗流水线,然后针对具体数据源做微调。别想着一步到位,迭代才是王道。


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