第3章:数据清洗实战:处理缺失值、异常值与时间戳对齐
数据清洗,说白了就是给原始数据“洗澡”。
我见过太多人,模型调得再好,数据一塌糊涂,结果全白费。量化交易里,数据就是你的命根子。脏数据进去,垃圾策略出来,这个道理我吃了不少亏才真正明白。
今天咱们就聊聊数据清洗的三个核心动作:处理缺失值、干掉异常值、对齐时间戳。嗯,这三件事做好了,你的数据才算能上桌。
核心观点:数据清洗不是可做可不做的“美容”,而是必须做的“体检”。跳过这一步,你的强化学习智能体就是在垃圾堆里找金子。
3.1 缺失值处理:别让“空”坑了你
金融数据里,缺失值太常见了。停牌、节假日、数据源故障,都会导致某根K线或某个指标为空。
我个人习惯,拿到数据第一件事就是检查缺失率。用 df.isnull().sum() 扫一眼,心里就有数了。
3.1.1 前向填充(Forward Fill)
前向填充,就是用上一个有效值填充当前缺失值。说白了,就是“假设没变”。
为什么在量化里常用?因为价格序列有很强的连续性。上一分钟收盘价是100,这一分钟缺失,用100填充,逻辑上说得通。
import pandas as pd
import numpy as np
# 模拟数据
data = {
'time': pd.date_range('2024-01-01', periods=10, freq='1min'),
'close': [100, 101, np.nan, 103, np.nan, np.nan, 106, 107, np.nan, 109]
}
df = pd.DataFrame(data)
df.set_index('time', inplace=True)
# 前向填充
df['close_ffill'] = df['close'].ffill()
print(df)
我的经验:前向填充适合处理“短暂缺失”,比如1-2根K线。如果连续缺失超过5根,我建议你直接删除这段数据,别硬填。我在做期货高频数据时遇到过连续10分钟缺失,用前向填充后策略回测曲线漂亮得不像话,实盘直接崩了——因为市场早就变了。
3.1.2 插值法(Interpolation)
插值法更聪明一些。它根据前后数据点,用数学方法估算缺失值。常用的有线性插值、多项式插值、时间插值。
# 线性插值
df['close_interp'] = df['close'].interpolate(method='linear')
print(df)
线性插值假设价格在两个点之间是直线变化。对于分钟级数据,这个假设基本靠谱。
但要注意,如果数据波动剧烈,线性插值会“削峰填谷”,把真实波动抹平了。我建议你对比一下插值前后的统计特征,看看均值、标准差变化大不大。
避坑指南:我曾经在日线数据上用多项式插值(order=3),结果插出了负价格。嗯,股票价格怎么可能为负?从那以后,我老老实实用线性插值或前向填充。金融数据,别玩太花哨的插值方法。
3.2 异常值处理:把“野马”拉回来
异常值,就是那些明显偏离正常范围的数据点。比如某只股票突然涨了1000%,或者成交量突然少了几个零。这些多半是数据录入错误或极端事件。
你想想看,强化学习智能体如果看到这种异常值,它会怎么学?它会以为市场经常这样波动,策略就会变得极其激进或保守。所以,必须处理。
3.2.1 3-Sigma 法则
3-Sigma 基于正态分布假设。数据在均值±3倍标准差范围内的,算正常;之外的,算异常。
def detect_outliers_3sigma(series):
mean = series.mean()
std = series.std()
lower = mean - 3 * std
upper = mean + 3 * std
outliers = (series < lower) | (series > upper)
return outliers
# 示例
returns = np.random.randn(1000) * 0.02 # 模拟收益率
returns[500] = 0.5 # 人为制造异常
outliers = detect_outliers_3sigma(returns)
print(f"检测到 {outliers.sum()} 个异常值")
这个方法简单粗暴,但有个前提:数据要近似正态分布。金融收益率数据虽然厚尾,但大部分情况下3-Sigma还是能用的。
我的习惯:我会先用3-Sigma标记异常值,然后人工看一眼。如果异常值出现在重大事件日(比如美联储加息),我会保留它,因为那是真实的市场反应。如果明显是数据错误,比如价格跳了10个数量级,直接删除或替换为NaN再填充。
3.2.2 IQR(四分位距)法
IQR 法不依赖正态分布假设,更稳健。它用四分位数来定义正常范围:Q1 - 1.5*IQR 到 Q3 + 1.5*IQR。
def detect_outliers_iqr(series):
Q1 = series.quantile(0.25)
Q3 = series.quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
outliers = (series < lower) | (series > upper)
return outliers
outliers_iqr = detect_outliers_iqr(returns)
print(f"IQR法检测到 {outliers_iqr.sum()} 个异常值")
IQR 法对极端值不敏感,适合处理偏态分布的数据。比如成交量数据,经常是右偏的,用3-Sigma可能会把正常的高成交量误判为异常,但IQR法就好很多。
对比总结:
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 3-Sigma | 近似正态分布的数据(如收益率) | 简单、计算快 | 对厚尾数据不敏感 |
| IQR | 偏态分布、有离群值的数据(如成交量) | 稳健、不受极端值影响 | 可能漏掉一些边缘异常 |
3.3 去重与时间戳对齐:让数据“排好队”
数据重复,在量化里是个隐形杀手。比如同一根K线被记录了两次,或者不同数据源的时间戳有偏差。
我刚开始做回测时,发现策略收益高得离谱,后来一查,原来是数据重复了,同一个信号被重复执行了两次。嗯,这种“虚假繁荣”千万别信。
3.3.1 去重
# 基于所有列去重
df.drop_duplicates(inplace=True)
# 基于时间戳去重,保留第一个
df.drop_duplicates(subset='time', keep='first', inplace=True)
去重时,我建议你保留第一个值。因为金融数据里,最早记录的那个通常是最准确的,后面的重复可能是数据源重传导致的。
3.3.2 时间戳对齐
不同数据源的时间戳可能不一致。比如行情数据是整秒的,但你的因子数据是毫秒级的。不对齐的话,你可能会用到“未来数据”。
# 将时间戳对齐到整秒
df['time_aligned'] = df['time'].dt.floor('1s')
# 按对齐后的时间戳重采样
df_aligned = df.resample('1min', on='time_aligned').last()
对齐的核心原则是:宁向后,不向前。也就是说,对齐时只能使用过去的数据,不能偷看未来的数据。这是防止未来函数的关键。
我曾经踩过的坑:在做多因子模型时,我把因子数据和收益率数据按时间戳简单合并,结果发现因子值竟然用到了当天的收盘价来计算。这就是典型的时间戳没对齐导致的未来函数。回测曲线漂亮得不行,实盘直接亏成狗。从那以后,我每次合并数据都会检查时间戳是否严格对齐到上一时刻。
3.4 本章知识体系
下面这张图,帮你把本章的核心逻辑串起来。数据清洗不是孤立的三步,而是一个闭环流程。
看到没?数据清洗不是一次性的。清洗完后,我建议你做个简单的统计检查:缺失率是否降到0?异常值是否被合理处理?时间戳是否严格对齐?如果不过关,就再走一遍流程。
最后说一句:数据清洗没有银弹。每个数据集都有自己的脾气。我的做法是:先自动化处理80%的脏数据,剩下20%靠人工判断。别想着一个函数解决所有问题,那是不可能的。
公众号:蓝海资料掘金营,微信deep3321