数据清洗与预处理:处理缺失值、复权处理、去极值、标准化

各位同学,欢迎来到第四章。说实话,这一章才是真正决定你模型成败的关键。

很多人喜欢把时间花在选股因子、调参优化上,但数据没洗干净,后面全是白搭。我见过太多人,模型跑出来回测曲线漂亮得不行,一上实盘就崩——查到最后,原来是复权数据没处理好。嗯,这种坑我踩过不止一次。

4.1 缺失值处理:别让空值毁了你的策略

金融数据里,缺失值太常见了。停牌、节假日、数据源故障,都会导致空值。我个人习惯,拿到数据第一件事就是检查缺失率。

核心原则:缺失率低于5%,可以填充;超过20%,建议直接剔除该特征。

常用的填充方法有这么几种:

  • 前向填充(ffill):用上一个有效值填充。适合日频数据,比如停牌后复牌,用停牌前价格。
  • 后向填充(bfill):用下一个有效值填充。适合数据开头有缺失的情况。
  • 插值法:线性插值或时间插值。适合少量缺失,且数据趋势明显。
  • 行业均值填充:用同行业股票的均值填充。适合基本面因子。
import pandas as pd
import numpy as np

# 示例:日频收益率数据
df = pd.DataFrame({
    'date': pd.date_range('2024-01-01', periods=10, freq='D'),
    'stock_a': [1.0, np.nan, 1.02, 1.03, np.nan, 1.05, 1.06, np.nan, 1.08, 1.09],
    'stock_b': [0.98, 0.99, 1.0, np.nan, 1.02, 1.03, np.nan, 1.05, 1.06, 1.07]
})

# 前向填充
df_ffill = df.fillna(method='ffill')

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

print("原始数据:\n", df)
print("\n前向填充后:\n", df_ffill)

注意:千万不要用全局均值填充金融时间序列!我曾经在回测里这么干过,结果把停牌期间的异常值当成了正常信号,策略直接失效。金融数据有很强的时序依赖性,均值填充会破坏这种结构。

4.2 复权处理:还原真实的收益率

复权处理,说白了就是消除分红、送股、配股对价格的影响。你想想看,一只股票今天除权除息,价格跳空低开,但你的账户资产其实没变。如果不复权,技术指标全乱套。

我个人习惯用后复权数据做回测。为什么?因为后复权能真实反映持有期的总收益,包括分红再投资。

复权方式 特点 适用场景
前复权 调整历史价格,保持最新价格不变 技术分析、画图
后复权 调整历史价格,保持上市首日价格不变 收益率计算、回测
不复权 真实成交价,有跳空缺口 实盘交易、订单执行
# 模拟复权处理逻辑
def adjust_price(df, factor_col='adj_factor'):
    """
    简单复权处理
    factor_col: 复权因子列
    """
    df['adj_close'] = df['close'] * df[factor_col]
    df['adj_open'] = df['open'] * df[factor_col]
    return df

# 实际项目中,我会用akshare或tushare直接获取复权数据
# 比如:ak.stock_zh_a_hist(symbol="000001", adjust="qfq")

小技巧:如果你用的是日频数据,建议同时保留复权价和原始价。复权价用于计算因子,原始价用于模拟交易成本。我有个策略就是因为没区分这两者,回测收益虚高了20%。

4.3 去极值:别让异常值绑架你的模型

金融数据里,极端值太多了。比如某天突然涨停,或者财报里出现一个异常高的市盈率。这些极端值会严重扭曲均值和标准差,影响后续标准化。

常用的去极值方法有三种:

  1. MAD法(中位数绝对偏差):稳健性最好,适合有大量异常值的数据。
  2. 百分位法:直接截断上下1%或5%的数据。简单粗暴,但有效。
  3. 标准差法:超过3倍标准差的值替换为边界值。适合正态分布的数据。
def winsorize_mad(series, n=5):
    """
    MAD法去极值
    n: 中位数上下n倍MAD
    """
    median = series.median()
    mad = np.median(np.abs(series - median))
    upper = median + n * mad
    lower = median - n * mad
    return series.clip(lower, upper)

def winsorize_percentile(series, lower=0.01, upper=0.99):
    """
    百分位法去极值
    """
    q_low = series.quantile(lower)
    q_high = series.quantile(upper)
    return series.clip(q_low, q_high)

# 示例
np.random.seed(42)
data = pd.Series(np.random.randn(1000) * 10 + 50)
data.iloc[0] = 500  # 人为加入极端值

cleaned_mad = winsorize_mad(data)
cleaned_pct = winsorize_percentile(data)

print(f"原始数据均值: {data.mean():.2f}, 标准差: {data.std():.2f}")
print(f"MAD法均值: {cleaned_mad.mean():.2f}, 标准差: {cleaned_mad.std():.2f}")
print(f"百分位法均值: {cleaned_pct.mean():.2f}, 标准差: {cleaned_pct.std():.2f}")

我的经验:对于市值因子、换手率这类偏态分布的数据,MAD法比标准差法更靠谱。我曾经用标准差法处理换手率,结果把正常的活跃股都截断了,策略选出来的全是僵尸股。

4.4 标准化:让不同量纲的因子站在同一起跑线

标准化,说白了就是把不同量纲的数据拉到同一个尺度。市盈率可能是几十倍,换手率可能是百分之几,不标准化的话,模型会天然偏向数值大的特征。

常用的标准化方法:

  • Z-score标准化:(x - mean) / std。假设数据服从正态分布,处理后均值为0,标准差为1。
  • Min-Max标准化:(x - min) / (max - min)。将数据映射到[0,1]区间,适合有边界的数据。
  • Rank标准化:将数据排序后转为百分位数。最稳健,不受极端值影响。
from sklearn.preprocessing import StandardScaler, MinMaxScaler
from scipy.stats import rankdata

def zscore_normalize(series):
    """Z-score标准化"""
    return (series - series.mean()) / series.std()

def minmax_normalize(series):
    """Min-Max标准化"""
    return (series - series.min()) / (series.max() - series.min())

def rank_normalize(series):
    """Rank标准化:转为0-1之间的百分位数"""
    ranks = rankdata(series)
    return (ranks - 1) / (len(series) - 1)

# 示例:不同量纲的因子
factors = pd.DataFrame({
    'pe': [15, 30, 50, 100, 200],
    'turnover': [0.01, 0.05, 0.10, 0.20, 0.50],
    'volume': [1e6, 5e6, 1e7, 5e7, 1e8]
})

print("原始数据:\n", factors)
print("\nZ-score标准化后:\n", factors.apply(zscore_normalize))
print("\nRank标准化后:\n", factors.apply(rank_normalize))

建议:在量化选股中,我强烈推荐Rank标准化。为什么?因为金融数据很少服从正态分布,Z-score会受极端值影响。Rank标准化只关心排序,不受量纲和分布影响,稳健性最好。

4.5 完整的数据清洗流水线

在实际项目中,我会把上述步骤封装成一个流水线。你想想看,每次手动操作多麻烦,还容易出错。

class DataCleaner:
    """数据清洗流水线"""
    
    def __init__(self, method_missing='ffill', method_outlier='mad', 
                 method_norm='rank'):
        self.method_missing = method_missing
        self.method_outlier = method_outlier
        self.method_norm = method_norm
        
    def clean(self, df):
        """执行完整清洗流程"""
        # 1. 缺失值处理
        if self.method_missing == 'ffill':
            df = df.fillna(method='ffill')
        elif self.method_missing == 'interp':
            df = df.interpolate()
            
        # 2. 去极值
        if self.method_outlier == 'mad':
            df = df.apply(winsorize_mad)
        elif self.method_outlier == 'percentile':
            df = df.apply(winsorize_percentile)
            
        # 3. 标准化
        if self.method_norm == 'rank':
            df = df.apply(rank_normalize)
        elif self.method_norm == 'zscore':
            df = df.apply(zscore_normalize)
            
        return df

# 使用示例
cleaner = DataCleaner()
clean_data = cleaner.clean(factors)
print("清洗完成:\n", clean_data)

重要提醒:清洗参数(比如MAD的n值、百分位的阈值)必须在训练集上确定,然后应用到测试集。我见过有人在整个数据集上做标准化,结果回测过拟合得一塌糊涂。记住:测试集不能偷看未来的信息。

4.6 本章知识体系

下面这张图,是我梳理的数据清洗与预处理核心流程。你可以把它当作一个检查清单,每次处理数据时对照着来。

数据清洗与预处理核心流程 原始数据 步骤1:缺失值处理 步骤2:复权处理 步骤3:去极值 步骤4:标准化 常用方法 • 前向填充 (ffill) • 后向填充 (bfill) • 线性插值 • 行业均值填充 • 前复权 / 后复权 • MAD法 • 百分位法 • 标准差法 • Z-score / Min-Max • Rank标准化 ★

好了,这一章的内容就到这里。数据清洗看起来琐碎,但它是整个量化投资体系的基石。我建议你每次拿到新数据,都按照这个流程走一遍,养成习惯。下一章我们会讨论因子构建,到时候这些清洗好的数据就能派上大用场了。


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