3. 金融数据与特征工程
在金融模型验证与Optuna调优的完整链路中,特征工程决定了模型性能的上限,而数据清洗决定了这个上限是否可信。金融数据与通用机器学习数据集有本质差异:它同时具有时序依赖性、截面相关性、低信噪比、非平稳性四大特征。本节围绕数据清洗、缺失值处理、异常值检测、时序特征、截面特征与金融因子构建六个环节,建立一套可复现、可验证的特征工程流程。
一、核心原理:金融数据的四类"污染源"
金融原始数据在进入模型前,通常携带四类污染:缺失(停牌、未上市、数据源断档)、异常(涨跌停、乌龙指、复权错误)、不一致(不同数据源口径差异)、前视偏差(使用了未来信息)。其中前视偏差最隐蔽也最致命——例如用当日收盘价计算的特征去预测当日收益,回测曲线会异常漂亮,实盘却完全失效。
二、方法流程:六步特征工程流水线
下图展示了从原始行情到因子矩阵的完整数据流,注意清洗与特征构建是交替迭代的,而非一次性线性过程。
三、关键环节与代码实践
1. 缺失值处理
金融时序缺失不能简单用均值填充。停牌导致的缺失应前向填充(保留最后有效价格),而财务指标缺失需区分"未披露"与"不适用"。推荐策略:
- 价格类:前向填充 + 标记停牌标志位
- 财务类:按公告日前向填充,缺失超过两期则剔除
- 截面因子:行业中位数填充,避免均值被极端值拉偏
2. 异常值检测
金融数据中异常值可能是噪声,也可能是真实信号(如涨停)。推荐使用MAD(中位数绝对偏差)而非3σ,因为收益率分布厚尾,σ本身就被异常值污染。
import numpy as np
import pandas as pd
def mad_winsorize(s, n=5):
"""按MAD对截面因子做缩尾处理"""
med = s.median()
mad = (s - med).abs().median()
if mad == 0:
return s
upper = med + n * 1.4826 * mad
lower = med - n * 1.4826 * mad
return s.clip(lower, upper)
# 对每个交易日的截面因子分组处理
factor_df = factor_df.groupby('date')['value'].transform(mad_winsorize)
3. 时序特征与截面特征
时序特征刻画单只标的的历史演化,如动量、波动率、换手率变化;截面特征刻画同一时刻不同标的的相对位置,如排名、分位数、行业中性化后的暴露。二者结合才能构建稳健因子。
| 特征类型 | 典型构造 | 窗口/维度 | 常见陷阱 |
|---|---|---|---|
| 时序动量 | 过去20日收益率 | 时间窗口 | 未复权导致跳空 |
| 时序波动 | 收益率滚动标准差 | 20/60日 | 未年化、未对齐 |
| 截面排名 | 因子值在当日全市场分位 | 截面 | 未剔除ST/新股 |
| 行业中性 | 因子减去行业均值 | 行业×日期 | 行业分类变更未处理 |
| 财务因子 | ROE、营收增速 | 季度 | 公告日对齐错误 |
4. 金融因子构建
因子构建的核心是经济逻辑 + 统计验证。以动量因子为例,需先做截面标准化与中性化,再检验IC、IR与换手率。Optuna调优时,因子的窗口参数(如动量回看期)本身就是重要超参数。
# 因子标准化与中性化流程
def build_factor(df):
df['rank'] = df.groupby('date')['raw'].rank(pct=True)
df['zscore'] = df.groupby('date')['rank'].transform(
lambda x: (x - x.mean()) / x.std())
# 行业中性化:减去行业均值
df['neutral'] = df['zscore'] - df.groupby(
['date', 'industry'])['zscore'].transform('mean')
return df
四、误区与复盘
复盘一个真实场景:某策略在回测中夏普比率高达3.2,实盘却亏损。排查发现,因子构建时使用了df.fillna(method='bfill'),导致停牌期间的价格被未来复牌价填充,回测中"提前"获得了复牌信息。修正为前向填充后,夏普降至1.1,与实盘一致。这说明数据清洗的每一个选择都必须通过前视偏差检查。
另一个常见问题是异常值处理过度。将涨停板全部缩尾会抹掉真实的动量信号。建议对价格类异常保留但标记,对财务类异常做缩尾,并在Optuna调优中将缩尾阈值作为可搜索参数。