数据清洗实战:缺失值处理、异常值检测与时间戳对齐

各位同学,欢迎来到第三章。前两章我们聊了数据获取和存储,今天要动真格的了——处理那些「脏数据」。

说实话,我在刚入行那会儿,觉得数据清洗就是个体力活。直到有一次,因为一个毫秒级的缺失值没处理好,导致整个回测结果偏差了3个点。嗯,从那以后我再也不敢小看这一步了。

今天的内容分三块:缺失值处理、异常值检测、时间戳对齐与重采样。这三件事,说白了就是让你的数据「干净、合理、整齐」。

核心观点:数据清洗不是「擦掉脏东西」,而是「让数据说话」。你清洗得越干净,后面的特征工程就越省心。

数据清洗实战知识体系 缺失值处理 异常值检测 时间戳对齐 前向填充 插值法 Z-score IQR 对齐 重采样 目标:干净、合理、整齐的数据

一、缺失值处理:别让「空」毁了你的策略

期货高频数据里,缺失值太常见了。比如某个tick因为网络抖动没收到,或者交易所休市期间的数据断层。我个人习惯,拿到数据第一件事就是检查缺失率。

import pandas as pd
import numpy as np

# 模拟一个5分钟K线数据,故意制造缺失
dates = pd.date_range('2024-01-01 09:30:00', periods=10, freq='5min')
data = {
    'close': [4120, 4125, np.nan, 4130, 4128, np.nan, 4135, 4140, 4138, 4142],
    'volume': [1200, 1350, np.nan, 1100, 980, np.nan, 1500, 1600, 1450, 1700]
}
df = pd.DataFrame(data, index=dates)
print(df.isnull().sum())  # 看看哪些列有缺失

处理缺失值,我常用的就两种:前向填充插值

1. 前向填充(Forward Fill)

说白了就是用上一个有效值填充当前缺失。这在期货数据里很实用——价格在短时间内不会突变,用前一个tick的价格填充,误差可控。

# 前向填充
df_ffill = df.ffill()
print(df_ffill)

我的经验:前向填充适合连续缺失不超过3个的情况。如果连续缺失超过5个,我建议直接删除那段数据,否则会引入偏差。

2. 插值法(Interpolation)

插值更「聪明」一点。它会根据前后数据点,用线性或多项式方式估算缺失值。我个人偏爱线性插值,简单且有效。

# 线性插值
df_interp = df.interpolate(method='linear')
print(df_interp)

你想想看,如果价格在4120和4130之间缺失,线性插值会给出4125,这比前向填充的4120更合理。但要注意,插值在价格剧烈波动时可能失真。

避坑指南:我曾经在股指期货数据上用多项式插值,结果插出了一个「尖峰」,导致后续特征全部偏离。后来我学乖了——高频数据里,线性插值就够了,别整花活。

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

异常值,就是那些明显偏离正常范围的数据点。比如螺纹钢价格突然从4000跳到10000,这大概率是数据录入错误,而不是市场行为。

我常用的两种方法:Z-scoreIQR

1. Z-score 方法

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

from scipy import stats

# 计算Z-score
z_scores = np.abs(stats.zscore(df['close'].dropna()))
threshold = 3
outliers = np.where(z_scores > threshold)[0]
print(f"异常值索引: {outliers}")

Z-score 适合数据近似正态分布的情况。但期货价格往往有偏态,所以我会先检查分布。

2. IQR 方法

IQR(四分位距)不依赖正态假设,更稳健。它用 Q1 - 1.5*IQR 和 Q3 + 1.5*IQR 作为边界。

Q1 = df['close'].quantile(0.25)
Q3 = df['close'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

outliers_iqr = df[(df['close'] < lower_bound) | (df['close'] > upper_bound)]
print(f"IQR方法检测到的异常值:\n{outliers_iqr}")

我的建议:对于期货高频数据,我优先用IQR。因为它对极端值不敏感,更适合金融数据的「肥尾」特征。Z-score 我一般用在预处理阶段做快速筛查。

检测到异常值后怎么办?我通常的做法是:先确认是不是数据录入错误,如果是,直接删除或替换为前一个有效值。如果是真实的市场异动(比如闪崩),我会保留并打上标签,后续特征工程里单独处理。

三、时间戳对齐与重采样:让不同频率的数据「同步」

这是高频数据清洗里最头疼的一环。不同交易所、不同品种的数据,时间戳可能不一致。有的精确到毫秒,有的到秒,还有的到分钟。你想想看,如果不对齐,你的策略信号可能早了几秒或晚了几秒,结果天差地别。

1. 时间戳对齐

对齐的核心是找到一个「基准时间戳」,然后把所有数据都映射到这个基准上。我习惯用 pandas 的 reindex 方法。

# 创建基准时间戳(每5分钟)
base_index = pd.date_range('2024-01-01 09:30:00', '2024-01-01 10:30:00', freq='5min')

# 假设有两个不同频率的数据
df1 = pd.DataFrame({'price': [4120, 4125, 4130]}, 
                   index=pd.to_datetime(['2024-01-01 09:30:00', '2024-01-01 09:35:00', '2024-01-01 09:40:00']))
df2 = pd.DataFrame({'price': [4122, 4128]}, 
                   index=pd.to_datetime(['2024-01-01 09:32:00', '2024-01-01 09:38:00']))

# 对齐到基准
df1_aligned = df1.reindex(base_index, method='ffill')
df2_aligned = df2.reindex(base_index, method='ffill')

print(df1_aligned)
print(df2_aligned)

小技巧:对齐时我常用 method='ffill',因为高频数据里,用最近的价格填充最合理。但如果你做的是回测,要注意「未来数据」的问题——别用未来的价格填充过去。

2. 重采样

重采样就是把高频数据降频到低频。比如把tick数据合成1分钟K线。pandas 的 resample 方法非常强大。

# 模拟tick数据
tick_dates = pd.date_range('2024-01-01 09:30:00', periods=100, freq='3s')
tick_data = pd.DataFrame({'price': np.random.normal(4120, 5, 100)}, index=tick_dates)

# 重采样到1分钟
minute_data = tick_data.resample('1min').agg({
    'price': ['first', 'max', 'min', 'last', 'mean']
})
minute_data.columns = ['open', 'high', 'low', 'close', 'avg']
print(minute_data.head())

重采样时要注意:聚合函数的选择。OHLC(开盘、最高、最低、收盘)是标准做法,但如果你做的是均值回归策略,用 mean 可能更合适。

我曾经踩过的坑:有一次做重采样,我忘了处理空值,结果某个时间窗口内没有数据,聚合后直接变成了NaN。后来我加上了 min_count 参数,确保窗口内有足够数据才计算。

实战总结:一个完整的清洗流程

好了,理论讲完了。我给你们一个实战中常用的清洗流程,直接拿去用。

def clean_futures_data(df, freq='1min'):
    """
    期货高频数据清洗函数
    """
    # 1. 缺失值处理:先前向填充,再线性插值
    df = df.ffill(limit=3)
    df = df.interpolate(method='linear')
    
    # 2. 异常值检测:IQR方法
    for col in ['close', 'volume']:
        Q1 = df[col].quantile(0.25)
        Q3 = df[col].quantile(0.75)
        IQR = Q3 - Q1
        lower = Q1 - 1.5 * IQR
        upper = Q3 + 1.5 * IQR
        df[col] = df[col].clip(lower, upper)  # 用边界值替换异常
    
    # 3. 重采样到指定频率
    df_resampled = df.resample(freq).agg({
        'close': 'last',
        'volume': 'sum'
    })
    
    return df_resampled

# 使用示例
cleaned_df = clean_futures_data(df, freq='5min')
print(cleaned_df.head())

这个函数我用了好几年,在多个品种上验证过。当然,具体参数要根据你的数据特点调整。比如股指期货的波动大,IQR的倍数可以放宽到2.0;而商品期货波动小,1.5就够。

最后说一句:数据清洗没有「银弹」。每个品种、每个频率都有它的脾气。多动手、多观察,慢慢你就能摸清门道了。


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