4、数据清洗与预处理:处理缺失值、处理异常值、数据类型转换、时间序列索引处理、重采样与对齐

数据清洗,说白了就是给原始数据「洗澡」。我做了这么多年量化,见过太多人一上来就调参、跑模型,结果数据本身一塌糊涂。嗯,这种策略上线后基本就是送钱。今天咱们就把这五个核心环节掰开揉碎讲清楚。

4.1 处理缺失值:别让「空」坑了你

金融数据里缺失值太常见了。停牌、节假日、数据源抽风,都会导致NaN。我个人习惯是,先看缺失比例,再决定怎么补。

核心原则:缺失超过30%的字段,直接扔掉。低于5%的,可以填充。中间地带,看情况。

常用的填充方法就三种:

  • 向前填充(ffill):用上一个有效值填充。适合价格数据,因为价格不会突变。
  • 向后填充(bfill):用下一个有效值填充。适合收盘后统计的数据。
  • 插值法:线性插值或时间插值。适合成交量这种连续变化的数据。
import pandas as pd
import numpy as np

# 模拟数据
df = pd.DataFrame({
    'close': [100, np.nan, 102, 103, np.nan, 105],
    'volume': [1000, 1200, np.nan, 1100, 1300, np.nan]
})

# 向前填充价格
df['close'].fillna(method='ffill', inplace=True)

# 线性插值成交量
df['volume'].interpolate(method='linear', inplace=True)

print(df)

我的经验:千万别用均值填充金融数据。股票价格均值填充后,你的策略会以为价格一直在均值附近波动,实际根本不是这样。我曾经因为这个坑,回测曲线漂亮得不行,实盘直接崩了。

4.2 处理异常值:把「妖怪」揪出来

异常值是什么?就是那些明显不合理的数据。比如某只股票一天涨了1000%,或者成交量突然变成平时的100倍。你想想看,这正常吗?

我常用的方法有两种:

方法 适用场景 阈值
3σ原则 正态分布的数据(如收益率) 均值±3倍标准差
IQR四分位法 非正态分布的数据(如成交量) Q1-1.5IQR 到 Q3+1.5IQR
# 3σ原则处理收益率异常值
returns = df['close'].pct_change()
mean = returns.mean()
std = returns.std()
df['returns_clean'] = returns.clip(mean - 3*std, mean + 3*std)

# IQR法处理成交量
Q1 = df['volume'].quantile(0.25)
Q3 = df['volume'].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
df['volume_clean'] = df['volume'].clip(lower, upper)

注意:异常值不一定要删除。有时候异常值本身就是信号,比如突然放量可能意味着有大资金进场。我建议先标记,再决定是剔除还是保留。

4.3 数据类型转换:让数据「对号入座」

这个环节看着简单,但最容易出问题。我见过有人把日期当成字符串算,结果排序全乱了。数据类型不对,后面的计算全是错的。

常见的转换场景:

  • 字符串转日期:用 pd.to_datetime()
  • 字符串转数值:用 pd.to_numeric()
  • 类别数据编码:行业分类、板块标签等
# 字符串转日期
df['date'] = pd.to_datetime(df['date'], format='%Y-%m-%d')

# 字符串转数值(处理带逗号的数字)
df['volume'] = pd.to_numeric(df['volume'].str.replace(',', ''), errors='coerce')

# 行业分类编码
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
df['industry_code'] = le.fit_transform(df['industry'])

一个小技巧:转换完后记得用 df.dtypes 检查一下。我每次做完数据清洗,第一件事就是打印数据类型,确保没出幺蛾子。

4.4 时间序列索引处理:把时间「立」起来

时间序列分析,索引就是命根子。没有正确的时间索引,你没法做对齐、没法做回测、没法做任何时序分析。

核心操作就三个:

  1. 设置索引:把日期列设为 DataFrame 的索引
  2. 排序:确保时间顺序是从早到晚
  3. 频率推断:让 pandas 知道你的数据是日频、分钟频还是 tick 级
# 设置时间索引
df.set_index('date', inplace=True)
df.sort_index(inplace=True)

# 推断频率
df = df.asfreq('D')  # 日频
# 或者用 infer_freq 自动推断
freq = pd.infer_freq(df.index)
print(f'数据频率: {freq}')

避坑指南:我曾经在处理美股数据时,没注意时区问题。结果回测时,某天的数据被算到了前一天。嗯,从那以后我每次都会检查时区,用 df.index.tz_localize() 统一时区。

4.5 重采样与对齐:让数据「步调一致」

做量化最头疼的事之一,就是不同数据源频率不一样。日线数据、分钟线数据、财报数据,频率各不相同。怎么把它们对齐?重采样就是干这个的。

重采样的核心逻辑:

  • 降采样:高频转低频(如分钟转日),用聚合函数
  • 升采样:低频转高频(如日转分钟),用插值或填充
# 分钟数据降采样为日数据
df_daily = df_minute.resample('D').agg({
    'open': 'first',
    'high': 'max',
    'low': 'min',
    'close': 'last',
    'volume': 'sum'
})

# 日数据升采样为小时数据(向前填充)
df_hourly = df_daily.resample('H').ffill()

# 多数据源对齐
df_aligned = pd.concat([df_price, df_volume, df_fundamental], axis=1).dropna()

我的习惯:对齐时用 join='inner' 还是 join='outer'?我一般用 inner,只保留所有数据源都有的时间点。这样虽然会损失一些数据,但保证了数据的一致性,回测结果更可靠。

知识体系总览

下面这张图,把数据清洗与预处理的整个流程串起来了。你可以把它当成一个检查清单,每次处理数据时对照着来一遍。

数据清洗与预处理流程 原始数据 缺失值处理 ffill/bfill/插值 异常值处理 3σ/IQR/截断 类型转换 日期/数值/编码 时间索引 排序/频率/时区 重采样 降/升采样 多数据源对齐(concat/merge) 清洗后的数据

数据清洗这件事,说白了就是「垃圾进,垃圾出」。你模型再牛,数据不行一切都是白搭。我见过太多人花90%的时间调参,却只花10%的时间处理数据。结果呢?回测漂亮,实盘惨淡。

记住一句话:数据质量决定了策略的上限,模型只是逼近这个上限的工具。把上面这五个环节走一遍,你的数据基本就干净了,后面的建模工作才能站得住脚。

专注资料整理