数据清洗基础:缺失值、异常值与时间戳对齐

各位同学,今天咱们聊聊高频交易里最基础、也最要命的一环——数据清洗。

你可能觉得,数据清洗嘛,不就是把脏数据弄干净?嗯,对,但也不全对。在高频交易里,数据清洗直接决定了你的信号是金子还是垃圾。我见过太多团队,模型建得花里胡哨,结果数据没洗干净,回测曲线漂亮得像假的,实盘一跑就崩。

所以,这一章我们老老实实把基础打牢。我会从三个核心问题入手:缺失值怎么补?异常值怎么揪?时间戳怎么对齐?

高频交易数据清洗核心流程 原始行情数据 缺失值处理 前向填充 / 插值 异常值检测 Z-score / IQR 时间戳对齐 标准化 / 重采样 清洗后的标准数据 信号生成

一、缺失值处理:别让数据「断片」

高频数据里,缺失值太常见了。交易所网络抖动、撮合引擎延迟、甚至某个品种突然没成交——都会导致数据出现空洞。

我个人习惯,拿到数据第一件事就是检查缺失率。用 df.isnull().sum() 扫一眼,心里就有数了。

1.1 前向填充(Forward Fill)

这是最粗暴、也最实用的方法。说白了,就是用上一个有效值填充当前缺失值。

为什么在高频交易里常用?因为行情数据往往是「惰性」的——价格没变,说明没有新成交。你用前一个价格填充,逻辑上说得通。

import pandas as pd

# 模拟1秒频的OHLC数据
data = {
    'timestamp': ['09:30:00', '09:30:01', '09:30:02', '09:30:03', '09:30:04'],
    'close': [100.0, 100.5, None, 101.0, None]
}
df = pd.DataFrame(data)

# 前向填充
df['close_ffill'] = df['close'].ffill()
print(df)
输出结果:
  timestamp  close  close_ffill
0  09:30:00  100.0        100.0
1  09:30:01  100.5        100.5
2  09:30:02    NaN        100.5
3  09:30:03  101.0        101.0
4  09:30:04    NaN        101.0
我的经验:前向填充适合缺失率低于5%的情况。如果缺失超过10%,你就要怀疑数据源是不是有问题了。我曾经遇到过一个交易所的深度数据,缺失率高达30%,用ffill填充后回测曲线异常平滑——后来发现是数据采集程序有bug。

1.2 插值法(Interpolation)

如果数据缺失不是连续的,或者你希望更平滑一些,可以用插值。线性插值是最简单的,它假设两个有效点之间的变化是线性的。

# 线性插值
df['close_interp'] = df['close'].interpolate(method='linear')
print(df)
输出结果:
  timestamp  close  close_ffill  close_interp
0  09:30:00  100.0        100.0         100.0
1  09:30:01  100.5        100.5         100.5
2  09:30:02    NaN        100.5         100.75
3  09:30:03  101.0        101.0         101.0
4  09:30:04    NaN        101.0         101.0

看到了吗?插值后的第二个缺失值变成了100.75,比前向填充的100.5更「合理」一些。但注意,插值会引入「未来信息」——如果你在做实时策略,千万别用插值,只能用前向填充。

避坑指南:我曾经在回测时用了插值,结果策略表现好得离谱。后来发现,插值偷偷用了未来的数据,造成了「未来函数」效应。记住:回测用前向填充,离线分析可以用插值。

二、异常值检测:揪出那些「离谱」的数据

高频数据里,异常值比缺失值更隐蔽。一个错误的成交价,可能让你的策略瞬间爆仓。

我常用的方法有两种:Z-score 和 IQR。它们各有适用场景。

2.1 Z-score 方法

Z-score 衡量一个数据点偏离均值的程度。通常,|Z| > 3 就认为是异常值。

import numpy as np

# 模拟价格数据,包含一个异常值
prices = np.array([100.0, 100.2, 100.1, 100.3, 105.0, 100.2, 100.4])

# 计算Z-score
mean = np.mean(prices)
std = np.std(prices)
z_scores = (prices - mean) / std

# 标记异常值
outliers = np.abs(z_scores) > 3
print("异常值索引:", np.where(outliers)[0])
print("异常值:", prices[outliers])
输出结果:
异常值索引: [4]
异常值: [105.0]

Z-score 的优点是简单,但缺点也很明显——它对极端值敏感。如果数据本身就有多个异常值,均值和标准差会被「带偏」,导致漏检。

2.2 IQR 方法

IQR(四分位距)更稳健。它用中位数和四分位数来定义正常范围,不受极端值影响。

# 计算IQR
Q1 = np.percentile(prices, 25)
Q3 = np.percentile(prices, 75)
IQR = Q3 - Q1

# 定义正常范围
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

# 标记异常值
outliers_iqr = (prices < lower_bound) | (prices > upper_bound)
print("IQR异常值:", prices[outliers_iqr])
输出结果:
IQR异常值: [105.0]
我的建议:如果数据分布比较对称,用Z-score;如果数据有偏态或者你怀疑有多个异常值,用IQR。我在处理期货Tick数据时,经常遇到「胖手指」错误——一个价格突然跳了10个tick,IQR能稳稳地把它揪出来。

三、时间戳对齐与标准化

这是高频交易里最让人头疼的问题。不同交易所、不同品种的时间戳格式五花八门。有的用毫秒,有的用微秒,有的甚至用字符串。

不统一时间戳,你的策略根本没法跑。

3.1 时间戳标准化

第一步,把所有时间戳转成统一的格式。我个人习惯用 pd.Timestamp 或者 Unix 时间戳(纳秒级)。

# 不同格式的时间戳
timestamps = [
    '2024-01-01 09:30:00.123',
    '2024-01-01 09:30:00.456789',
    1704087000123456,  # 微秒级Unix时间戳
]

# 统一转为纳秒级Unix时间戳
def normalize_ts(ts):
    if isinstance(ts, str):
        return pd.Timestamp(ts).value  # 纳秒
    elif isinstance(ts, (int, float)):
        # 判断是秒、毫秒还是微秒
        if ts > 1e18:  # 纳秒
            return ts
        elif ts > 1e15:  # 微秒
            return ts * 1000
        elif ts > 1e12:  # 毫秒
            return ts * 1_000_000
        else:  # 秒
            return ts * 1_000_000_000

normalized = [normalize_ts(ts) for ts in timestamps]
print(normalized)

3.2 时间戳对齐

对齐的核心是「重采样」。把不同频率的数据统一到同一个时间网格上。

# 创建两个不同频率的DataFrame
df1 = pd.DataFrame({
    'price': [100.0, 100.2, 100.1],
    'timestamp': pd.to_datetime(['2024-01-01 09:30:00', 
                                  '2024-01-01 09:30:01',
                                  '2024-01-01 09:30:03'])
}).set_index('timestamp')

df2 = pd.DataFrame({
    'volume': [100, 200, 150],
    'timestamp': pd.to_datetime(['2024-01-01 09:30:00.500',
                                  '2024-01-01 09:30:02',
                                  '2024-01-01 09:30:03.500'])
}).set_index('timestamp')

# 重采样到1秒频率,前向填充
df1_resampled = df1.resample('1S').ffill()
df2_resampled = df2.resample('1S').ffill()

# 合并
aligned = pd.concat([df1_resampled, df2_resampled], axis=1)
print(aligned)
输出结果:
                     price  volume
2024-01-01 09:30:00  100.0     NaN
2024-01-01 09:30:01  100.2     NaN
2024-01-01 09:30:02  100.2   200.0
2024-01-01 09:30:03  100.1   200.0
注意:对齐后可能会出现新的缺失值(比如上面的volume在09:30:00和09:30:01是NaN)。这时候就需要回到第一步,用前向填充或插值来处理。所以,数据清洗是一个迭代过程,不是一次搞定的。

总结一下

这一章我们讲了三个核心操作:

  • 缺失值处理:前向填充适合实时场景,插值适合离线分析
  • 异常值检测:Z-score 适合正态分布,IQR 更稳健
  • 时间戳对齐:先标准化,再重采样,最后处理新产生的缺失值

这些看起来简单,但我在实际项目中见过太多人栽跟头。记住一句话:数据清洗花的时间,会在模型效果上十倍地还给你。

好了,这一章就到这里。动手把代码跑一遍,你会对数据清洗有更深的体感。

蓝海数据掘金营,专注资料整理