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 图展示了从原始数据到可用变量的完整预处理流程,以及各步骤之间的迭代关系:
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),实时监控上述指标。当指标超出阈值时,自动触发告警并暂停因子衍生流程,避免将低质量数据引入后续建模环节。