第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 本章知识体系

下面这张图,帮你把本章的核心逻辑串起来。数据清洗不是孤立的三步,而是一个闭环流程。

数据清洗实战流程 原始数据 步骤1:缺失值处理 前向填充 | 插值法 步骤2:异常值处理 3-Sigma | IQR法 步骤3:去重与时间戳对齐 去重 | 重采样 | 对齐 清洗后数据 检查质量,不合格则返回

看到没?数据清洗不是一次性的。清洗完后,我建议你做个简单的统计检查:缺失率是否降到0?异常值是否被合理处理?时间戳是否严格对齐?如果不过关,就再走一遍流程。

最后说一句:数据清洗没有银弹。每个数据集都有自己的脾气。我的做法是:先自动化处理80%的脏数据,剩下20%靠人工判断。别想着一个函数解决所有问题,那是不可能的。


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