第四节:数据预处理——让数据“干干净净”上战场

说实话,做量化选股这么多年,我踩过最大的坑,不是策略选错了,而是数据没洗干净。

你想想看,你辛辛苦苦写了一个漂亮的选股模型,结果跑出来的结果一塌糊涂。查了半天,发现是某只股票某天的收盘价是0——这明显是数据缺失嘛。嗯,这种坑我至少掉进去过三次。

所以今天这一节,咱们就聊聊数据预处理。说白了,就是让数据变得“可用”。

核心观点:数据预处理占整个量化策略开发工作量的60%以上。别不信,这是真的。

数据预处理 量化选股基石 缺失值处理 删除/填充/插值 异常值处理 3σ/IQR/截尾 数据标准化 Z-score/MinMax 对齐与重采样 时间对齐/频率转换 合并与拼接 concat/merge/join

一、缺失值处理——别让“空”坑了你

先说说缺失值。A股市场里,停牌、新股上市、退市整理期,都会产生缺失值。我见过最离谱的是某只股票连续停牌半年,中间所有字段全是NaN。

处理缺失值,常用的就三种方法:

  1. 直接删除——简单粗暴,但会损失样本量。如果缺失比例小于5%,可以这么干。
  2. 填充——用均值、中位数、前值、后值来填。我个人习惯用前值填充,因为金融数据有很强的时序连续性。
  3. 插值——线性插值、多项式插值。适合缺失值不多但又不舍得删的情况。

我的经验:对于日频数据,我一般先用前值填充,如果连续缺失超过5天,直接删除该股票当期的数据。为什么?因为连续停牌超过5天,复牌后大概率有补涨补跌,这种数据会严重干扰模型。

import pandas as pd
import numpy as np

# 模拟缺失数据
df = pd.DataFrame({
    'date': pd.date_range('2024-01-01', periods=10, freq='D'),
    'close': [10, 11, np.nan, np.nan, 14, 15, np.nan, 17, 18, 19]
})
df.set_index('date', inplace=True)

# 前值填充
df['close_ffill'] = df['close'].ffill()

# 线性插值
df['close_interp'] = df['close'].interpolate(method='linear')

print(df)

二、异常值处理——把“野马”驯服

异常值,说白了就是那些明显不合理的数据。比如某只股票一天涨了1000%,或者市盈率突然变成负数的一万倍。这些数据如果不处理,模型会被带偏。

我常用的方法:

  • 3σ原则:超过均值±3倍标准差的数据,视为异常。适合正态分布的数据。
  • IQR方法:低于Q1-1.5×IQR或高于Q3+1.5×IQR的数据,视为异常。这个方法更稳健,不受极端值影响。
  • 截尾处理:直接把异常值替换成上下限的值。比如把超过99%分位数的值,全部设为99%分位数。

避坑指南:我曾经在回测中没处理异常值,结果某只股票因为一次“乌龙指”交易,被模型判定为“超级强势股”,重仓买入后第二天就跌停了。嗯,从那以后我再也不敢忽视异常值了。

def handle_outliers_iqr(series):
    Q1 = series.quantile(0.25)
    Q3 = series.quantile(0.75)
    IQR = Q3 - Q1
    lower = Q1 - 1.5 * IQR
    upper = Q3 + 1.5 * IQR
    return series.clip(lower, upper)

# 示例
df['close_clean'] = handle_outliers_iqr(df['close'])

三、数据标准化——让不同量纲的数据“平起平坐”

你想想看,股价从几块到几百块都有,市盈率从负数到几百倍也有。如果不做标准化,模型会天然偏向数值大的特征。这显然不合理。

两种最常用的标准化方法:

方法 公式 适用场景
Z-score标准化 (x - μ) / σ 数据近似正态分布,有异常值
Min-Max标准化 (x - min) / (max - min) 数据有明确边界,如0-1之间

我个人习惯用Z-score。为什么?因为金融数据经常有极端值,Min-Max会被极端值压缩得很难看。而Z-score天然对异常值有“容忍度”。

from sklearn.preprocessing import StandardScaler, MinMaxScaler

# Z-score标准化
scaler_z = StandardScaler()
df['close_z'] = scaler_z.fit_transform(df[['close_clean']])

# Min-Max标准化
scaler_mm = MinMaxScaler()
df['close_mm'] = scaler_mm.fit_transform(df[['close_clean']])

四、数据对齐与重采样——让时间“对齐”

做量化选股,最头疼的就是时间对齐。不同股票的交易日期不一样,有的因为节假日停牌,有的因为临时停牌。如果不做对齐,你算出来的相关性、协方差全是错的。

重采样也很关键。比如你拿到的数据是日频的,但你的策略是周频调仓。那就需要把日频数据重采样成周频。

核心要点:对齐时用“内连接”还是“外连接”?我个人建议用内连接。因为外连接会产生大量缺失值,处理起来很麻烦。除非你有特殊需求,否则内连接最安全。

# 数据对齐示例
dates1 = pd.date_range('2024-01-01', periods=5, freq='D')
dates2 = pd.date_range('2024-01-02', periods=5, freq='D')

df1 = pd.DataFrame({'close': [10, 11, 12, 13, 14]}, index=dates1)
df2 = pd.DataFrame({'close': [20, 21, 22, 23, 24]}, index=dates2)

# 内连接对齐
aligned = pd.merge(df1, df2, left_index=True, right_index=True, how='inner')
print(aligned)

# 重采样:日频转周频
weekly = df1.resample('W').last()
print(weekly)

五、数据合并与拼接——把碎片拼成拼图

最后一步,就是把处理好的数据合并起来。你可能有多个数据源:行情数据、财务数据、因子数据。怎么把它们拼到一起?

  • 纵向拼接(concat):把不同时间段的同一只股票数据拼起来。比如2023年的数据和2024年的数据。
  • 横向合并(merge/join):把同一时间点的不同数据拼起来。比如把行情数据和财务数据按日期合并。

这里有个坑:合并的时候一定要注意索引对齐。我见过有人直接用pd.concat,结果因为索引没对齐,数据全乱了。

# 纵向拼接
df_2023 = pd.DataFrame({'close': [10, 11, 12]}, index=pd.date_range('2023-01-01', periods=3))
df_2024 = pd.DataFrame({'close': [13, 14, 15]}, index=pd.date_range('2024-01-01', periods=3))
df_all = pd.concat([df_2023, df_2024])

# 横向合并
df_price = pd.DataFrame({'close': [10, 11, 12]}, index=pd.date_range('2024-01-01', periods=3))
df_factor = pd.DataFrame({'pe': [15, 16, 14]}, index=pd.date_range('2024-01-01', periods=3))
df_merged = pd.merge(df_price, df_factor, left_index=True, right_index=True)

我的习惯:每次合并完数据,我都会用df.isnull().sum()检查一下缺失值。如果合并后突然多了很多NaN,那肯定是哪里出了问题。别问我怎么知道的——都是血泪教训。

好了,数据预处理这部分就聊到这儿。记住一句话:数据预处理做得好,策略开发事半功倍;数据预处理做得差,后面全是白忙活。


公众号:蓝海数据掘金营,微信deep3321