2. 变量衍生基础:原始数据清洗、缺失值处理、异常值识别与截尾

原始数据是因子研究的起点,但金融数据天生不完美:缺失、异常、噪声无处不在。本章将系统讲解变量衍生前的数据预处理三步骤——清洗、缺失值处理、异常值识别与截尾,确保后续衍生变量建立在可靠的数据基础之上。

核心原则:数据预处理不是一次性任务,而是与变量衍生迭代进行的闭环过程。每次衍生新变量后,都应重新评估数据质量。

2.1 原始数据清洗:从脏数据到可用数据

数据清洗是预处理的第一步,主要解决格式不一致、重复记录、逻辑错误等问题。金融数据常见的脏数据类型包括:

脏数据类型 典型表现 清洗方法
格式不一致 日期格式混用(YYYY-MM-DD vs MM/DD/YYYY) 统一转换为标准格式
重复记录 同一交易日同一股票出现多条记录 按时间戳去重,保留最新或均值
逻辑错误 开盘价 > 最高价,成交量 < 0 标记为缺失或按规则修正
编码错误 股票代码混入特殊字符 正则表达式提取有效编码
单位不一致 部分数据以万元计,部分以元计 统一换算为基准单位
实战建议:建立数据清洗清单(Checklist),每次接入新数据源时逐项检查。对于高频数据,还需额外检查时间戳对齐和交易时段过滤。

2.2 缺失值处理:识别、分类与填充策略

金融数据中的缺失值并非随机出现,往往与停牌、退市、数据采集故障等因素相关。处理缺失值前,需先识别缺失模式:

  • 完全随机缺失(MCAR):缺失与观测数据无关,如随机传输错误。可直接删除或简单填充。
  • 随机缺失(MAR):缺失依赖于其他观测变量,如小市值股票更易缺失分析师覆盖数据。需用模型预测填充。
  • 非随机缺失(MNAR):缺失与未观测到的因素相关,如业绩差的公司主动不披露数据。需谨慎处理,避免引入偏差。

常用缺失值填充方法对比:

方法 适用场景 优点 缺点
前向填充(ffill) 时间序列数据,如日频价格 简单快速,保留趋势 无法处理连续缺失
后向填充(bfill) 时间序列尾部缺失 利用未来信息 引入未来数据偏差
均值/中位数填充 截面数据,缺失率低 计算简单,不改变分布中心 压缩方差,削弱相关性
线性插值 连续型时间序列 平滑过渡,保留局部趋势 对非线性序列效果差
模型预测填充 缺失率较高或结构复杂 利用多变量信息,精度高 计算成本高,可能过拟合
# Python 示例:缺失值处理流程
import pandas as pd
import numpy as np

# 加载日频股票数据
df = pd.read_csv('stock_data.csv', parse_dates=['date'])
df.set_index(['stock_id', 'date'], inplace=True)

# 步骤1:识别缺失模式
missing_ratio = df.isnull().mean().sort_values(ascending=False)
print("缺失率排名:\n", missing_ratio[missing_ratio > 0])

# 步骤2:按变量类型选择填充策略
# 价格类变量:前向填充 + 线性插值
df['close'] = df.groupby('stock_id')['close'].ffill().interpolate(method='linear')

# 成交量类变量:前向填充,若仍缺失则填充0
df['volume'] = df.groupby('stock_id')['volume'].ffill().fillna(0)

# 财务指标类变量:截面中位数填充
df['pe_ratio'] = df.groupby('date')['pe_ratio'].transform(
    lambda x: x.fillna(x.median())
)

# 步骤3:标记仍存在的缺失(用于后续分析)
df['close_missing_flag'] = df['close'].isnull().astype(int)
注意:切勿在因子回测中使用未来信息填充缺失值。例如,使用后向填充或未来均值填充会引入前视偏差(Look-ahead Bias),导致回测结果虚高。

2.3 异常值识别:统计方法与领域知识结合

异常值(Outlier)是偏离正常分布的数据点,可能由数据错误、极端市场事件或真实但罕见的交易行为引起。识别异常值需结合统计方法和金融领域知识。

常用异常值识别方法:

  • Z-Score 方法:假设数据服从正态分布,将 |Z| > 3 的点视为异常。适用于对称分布数据。
  • IQR(四分位距)方法:将低于 Q1 - 1.5×IQR 或高于 Q3 + 1.5×IQR 的点视为异常。对偏态分布更稳健。
  • MAD(中位数绝对偏差)方法:使用中位数替代均值,对异常值本身不敏感,适合金融数据。
  • 百分比截尾法:直接截取上下 1% 或 5% 的分位数,简单但可能丢失信息。
# Python 示例:异常值识别与可视化
from scipy import stats

# 计算日收益率
df['return'] = df.groupby('stock_id')['close'].pct_change()

# 方法1:Z-Score 识别
z_scores = np.abs(stats.zscore(df['return'].dropna()))
outlier_z = z_scores > 3
print(f"Z-Score 方法识别异常值数量:{outlier_z.sum()}")

# 方法2:IQR 方法
Q1 = df['return'].quantile(0.25)
Q3 = df['return'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outlier_iqr = (df['return'] < lower_bound) | (df['return'] > upper_bound)
print(f"IQR 方法识别异常值数量:{outlier_iqr.sum()}")

# 方法3:MAD 方法
median = df['return'].median()
mad = np.median(np.abs(df['return'] - median))
modified_z = 0.6745 * (df['return'] - median) / mad
outlier_mad = np.abs(modified_z) > 3.5
print(f"MAD 方法识别异常值数量:{outlier_mad.sum()}")
领域知识提示:金融数据中,某些“异常值”实际上是真实的市场事件(如财报发布日的跳空、除权除息日的价格调整)。建议建立白名单机制,对已知事件日的“异常”值予以保留。

2.4 异常值截尾:截断与缩尾技术

识别异常值后,需决定如何处理。截尾(Truncation)和缩尾(Winsorization)是两种主流方法:

方法 操作方式 适用场景 对分布的影响
截断(Truncation) 直接删除异常值 确认异常值由数据错误引起 减少样本量,可能引入选择偏差
缩尾(Winsorization) 将异常值替换为指定分位数的值 异常值可能包含部分真实信息 保留样本量,压缩极端值影响
分位数截尾 将超出上下百分位数的值设为边界值 因子标准化前的常规处理 控制极端值对后续建模的影响
# Python 示例:缩尾处理
def winsorize_series(s, limits=[0.01, 0.01]):
    """
    对序列进行缩尾处理
    limits: [lower_percentile, upper_percentile]
    """
    lower = s.quantile(limits[0])
    upper = s.quantile(1 - limits[1])
    return s.clip(lower, upper)

# 对日收益率进行1%缩尾
df['return_winsorized'] = df.groupby('date')['return'].transform(
    lambda x: winsorize_series(x, limits=[0.01, 0.01])
)

# 对比处理前后的统计量
print("原始收益率统计:")
print(df['return'].describe())
print("\n缩尾后收益率统计:")
print(df['return_winsorized'].describe())
截尾参数选择指南:
  • 截面因子(如市值、估值):通常使用 1% 或 5% 分位数缩尾。
  • 时间序列因子(如动量、波动率):建议使用滚动窗口的 MAD 方法,适应市场状态变化。
  • 高频数据:异常值更密集,可适当放宽截尾阈值至 0.5%。

2.5 数据预处理流程总览

以下 SVG 图展示了从原始数据到可用变量的完整预处理流程,以及各步骤之间的迭代关系:

数据预处理与变量衍生迭代流程 原始数据 数据清洗 缺失值处理 异常值识别 截尾处理 可用变量 变量衍生迭代 预处理检查清单 ✓ 格式统一 ✓ 重复记录处理 ✓ 逻辑错误修正 ✓ 缺失模式识别 异常值处理策略 • Z-Score / IQR / MAD • 结合领域知识判断 • 截断 vs 缩尾选择 • 参数敏感性测试

2.6 实战案例:A股日频数据预处理

以下案例展示完整的预处理流程在 A 股日频数据上的应用:

# 完整预处理流程示例
import pandas as pd
import numpy as np
from scipy import stats

def preprocess_stock_data(df, date_col='date', stock_col='stock_id',
                          price_cols=['open','high','low','close'],
                          vol_col='volume',
                          outlier_method='mad',
                          winsor_limits=[0.01, 0.01]):
    """
    股票数据完整预处理流程
    """
    # 1. 数据清洗
    # 去重
    df = df.drop_duplicates(subset=[stock_col, date_col])
    # 逻辑检查:价格必须为正
    for col in price_cols:
        df = df[df[col] > 0]
    # 成交量检查
    df = df[df[vol_col] >= 0]

    # 2. 缺失值处理
    # 价格类:前向填充 + 线性插值
    for col in price_cols:
        df[col] = df.groupby(stock_col)[col].ffill().interpolate(method='linear')
    # 成交量:前向填充,剩余填充0
    df[vol_col] = df.groupby(stock_col)[vol_col].ffill().fillna(0)

    # 3. 计算收益率
    df['return'] = df.groupby(stock_col)['close'].pct_change()

    # 4. 异常值识别与截尾
    if outlier_method == 'mad':
        # MAD 方法
        median = df['return'].median()
        mad = np.median(np.abs(df['return'] - median))
        modified_z = 0.6745 * (df['return'] - median) / mad
        df['return_outlier'] = np.abs(modified_z) > 3.5
    elif outlier_method == 'iqr':
        Q1 = df['return'].quantile(0.25)
        Q3 = df['return'].quantile(0.75)
        IQR = Q3 - Q1
        lower = Q1 - 1.5 * IQR
        upper = Q3 + 1.5 * IQR
        df['return_outlier'] = (df['return'] < lower) | (df['return'] > upper)

    # 5. 缩尾处理(截面维度)
    df['return_winsor'] = df.groupby(date_col)['return'].transform(
        lambda x: x.clip(
            lower=x.quantile(winsor_limits[0]),
            upper=x.quantile(1 - winsor_limits[1])
        )
    )

    # 6. 标记缺失率高的股票(可选)
    missing_threshold = 0.2  # 20% 缺失率
    stock_missing = df.groupby(stock_col)['close'].apply(
        lambda x: x.isnull().mean()
    )
    high_missing_stocks = stock_missing[stock_missing > missing_threshold].index
    df['high_missing'] = df[stock_col].isin(high_missing_stocks)

    return df

# 使用示例
# df_clean = preprocess_stock_data(raw_df)
重要提醒:预处理参数(如截尾分位数、缺失率阈值)不应在回测中动态调整。应在回测开始前基于全样本或滚动窗口固定参数,避免引入数据窥探偏差(Data Snooping Bias)。

2.7 预处理质量评估指标

完成预处理后,需通过以下指标评估数据质量改善效果:

评估维度 指标 目标值 监控频率
完整性 缺失率(整体/截面/时间序列) < 5% 每次预处理后
一致性 逻辑错误数量(如价格倒挂) 0 每日
稳定性 异常值比例(按方法统计) < 3% 每周
分布形态 偏度、峰度变化 缩尾后偏度绝对值 < 2 每月
相关性保持 预处理前后变量相关系数 > 0.95 每次参数调整后
最佳实践:建立数据质量看板(Dashboard),实时监控上述指标。当指标超出阈值时,自动触发告警并暂停因子衍生流程,避免将低质量数据引入后续建模环节。