数据清洗基础:缺失值、异常值与时间戳对齐
各位同学,今天咱们聊聊高频交易里最基础、也最要命的一环——数据清洗。
你可能觉得,数据清洗嘛,不就是把脏数据弄干净?嗯,对,但也不全对。在高频交易里,数据清洗直接决定了你的信号是金子还是垃圾。我见过太多团队,模型建得花里胡哨,结果数据没洗干净,回测曲线漂亮得像假的,实盘一跑就崩。
所以,这一章我们老老实实把基础打牢。我会从三个核心问题入手:缺失值怎么补?异常值怎么揪?时间戳怎么对齐?
一、缺失值处理:别让数据「断片」
高频数据里,缺失值太常见了。交易所网络抖动、撮合引擎延迟、甚至某个品种突然没成交——都会导致数据出现空洞。
我个人习惯,拿到数据第一件事就是检查缺失率。用 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
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]
三、时间戳对齐与标准化
这是高频交易里最让人头疼的问题。不同交易所、不同品种的时间戳格式五花八门。有的用毫秒,有的用微秒,有的甚至用字符串。
不统一时间戳,你的策略根本没法跑。
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
总结一下
这一章我们讲了三个核心操作:
- 缺失值处理:前向填充适合实时场景,插值适合离线分析
- 异常值检测:Z-score 适合正态分布,IQR 更稳健
- 时间戳对齐:先标准化,再重采样,最后处理新产生的缺失值
这些看起来简单,但我在实际项目中见过太多人栽跟头。记住一句话:数据清洗花的时间,会在模型效果上十倍地还给你。
好了,这一章就到这里。动手把代码跑一遍,你会对数据清洗有更深的体感。