4. 数据清洗与预处理:缺失值处理、异常值检测、复权处理、数据对齐
数据清洗这步,说实话,是整条分析流水线里最枯燥、但又最绕不开的环节。我见过太多人,模型跑得飞起,结果一查原始数据,日期对不上、价格跳空、复权没做……嗯,那基本就是白干。今天咱们就把这块硬骨头啃下来。
4.1 缺失值处理:别让空值毁了你的策略
逐笔成交数据里,缺失值很常见。比如某只股票突然停牌,或者交易所数据推送延迟,都会留下空档。我个人习惯,先看缺失比例,再决定怎么补。
核心原则: 缺失比例 < 5%,用前向填充;5%-20%,用插值;> 20%,直接丢弃该时间段。
我在项目中遇到过一只小盘股,某天下午14:00-14:30的逐笔数据全部缺失。后来发现是交易所网关故障。这种情况,用前向填充(ffill)最稳妥——用14:00之前最后一笔成交价填充,至少不会引入未来信息。
import pandas as pd
import numpy as np
# 模拟逐笔数据
df = pd.DataFrame({
'time': pd.date_range('2024-01-01 09:30', periods=100, freq='1min'),
'price': np.random.randn(100).cumsum() + 10
})
# 人为制造缺失
df.loc[20:25, 'price'] = np.nan
# 前向填充
df['price_ffill'] = df['price'].ffill()
# 线性插值
df['price_interp'] = df['price'].interpolate(method='linear')
print(df.head(30))
小技巧: 对于高频逐笔数据,我建议用「时间加权插值」。因为成交间隔不均匀,单纯线性插值会扭曲微观结构。用
interpolate(method='time') 更准。
4.2 异常值检测:揪出那些离谱的成交价
逐笔数据里经常出现「乌龙指」或者数据录入错误。比如某股票正常价格10块,突然蹦出一笔100块的成交。你想想看,这要是直接拿去算成本,主力建仓成本能给你算出天际线来。
我常用的方法有两种:
- 3σ 原则: 价格偏离均值3个标准差以上,标记为异常。
- IQR 四分位法: 低于 Q1-1.5*IQR 或高于 Q3+1.5*IQR,视为异常。
对于逐笔数据,我更推荐 IQR 法。为什么?因为价格分布往往有厚尾,3σ 会漏掉一些极端但合理的成交(比如涨停板瞬间)。
def detect_outliers_iqr(series):
Q1 = series.quantile(0.25)
Q3 = series.quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
return (series < lower_bound) | (series > upper_bound)
# 应用
df['is_outlier'] = detect_outliers_iqr(df['price'])
print(f"发现 {df['is_outlier'].sum()} 个异常值")
注意: 我曾经在回测时没处理异常值,结果策略年化收益虚高了8%。后来发现是某天尾盘一笔异常大单被当成了正常成交。所以,异常值一定要先标记,再决定是剔除还是修正。
4.3 复权处理:还原真实交易成本
复权这件事,说白了就是消除分红、送股、配股对价格的影响。如果不复权,你会发现股价突然跳空,主力成本线也跟着乱跳。
我个人习惯用「后复权」来分析建仓成本。为什么?因为后复权能真实反映持有至今的总收益,而且价格序列是连续的,方便做技术分析。
| 复权类型 | 适用场景 | 缺点 |
|---|---|---|
| 前复权 | 看历史价格形态 | 最新价格被调整,失真 |
| 后复权 | 计算真实收益、建仓成本 | 历史价格会很大,不直观 |
| 不复权 | 分析除权除息缺口 | 价格不连续,无法直接分析 |
# 假设有复权因子数据
df['adj_factor'] = np.random.uniform(0.8, 1.2, len(df))
# 后复权价格 = 收盘价 * 累积复权因子
df['price_adj'] = df['price'] * df['adj_factor'].cumprod()
print(df[['time', 'price', 'price_adj']].head())
避坑指南: 我曾经直接用第三方库的复权数据,结果发现它把分红日期的复权因子算错了。后来我都是自己从交易所下载除权除息公告,手动计算复权因子。虽然麻烦,但心里踏实。
4.4 数据对齐:让不同频率的数据「对上话」
逐笔成交数据是毫秒级的,但主力建仓成本分析往往需要分钟级或日级数据。怎么对齐?说白了就是降采样。
我常用的对齐方式:
- 时间戳对齐: 把逐笔数据按分钟、小时、日进行分组聚合。
- 价格对齐: 取每个时间窗口的 VWAP(成交量加权平均价)作为代表价格。
- 成交量对齐: 累加每个窗口的总成交量。
# 将逐笔数据对齐到分钟级别
df['minute'] = df['time'].dt.floor('1min')
minute_data = df.groupby('minute').agg(
open=('price', 'first'),
high=('price', 'max'),
low=('price', 'min'),
close=('price', 'last'),
volume=('volume', 'sum'),
vwap=('price', lambda x: np.average(x, weights=df.loc[x.index, 'volume']))
).reset_index()
print(minute_data.head())
核心逻辑: 数据对齐的关键是「时间窗口」和「聚合函数」的选择。对于主力建仓成本分析,我强烈建议用 VWAP 而不是简单均价。因为大单成交对价格影响更大,VWAP 能更真实反映主力的实际成交成本。
嗯,到这里,数据清洗与预处理的四个核心步骤就讲完了。你可能会问,为什么要把这么多时间花在数据清洗上?因为我在实战中吃过太多亏了——数据没洗干净,后面所有分析都是空中楼阁。记住一句话:垃圾进,垃圾出。
总结一下: 数据清洗不是一次性工作。我每次拿到新数据,都会先跑一遍这个流程,然后可视化检查一遍。比如画个价格曲线,看看有没有突兀的跳点。肉眼扫一遍,比任何统计指标都管用。