第四章 数据工程(下):特征工程基础
好,咱们接着聊。上一章我们把数据清洗和拼接讲透了,这一章进入真正让模型「吃饱吃好」的关键环节——特征工程。
说实话,我入行头两年,一直觉得特征工程就是「标准化一下、归一化一下」就完事了。直到我在实盘策略里栽了个大跟头——一个看起来完美的因子,回测夏普3.5,实盘直接变成0.8。后来排查了三天,发现是截面数据的时间戳对齐出了问题。嗯,从那以后,我再也不敢小看特征工程里的任何一个细节。
4.1 标准化:让所有特征站在同一起跑线
标准化,说白了就是把不同量纲的数据拉到同一个尺度上。比如市盈率可能是几十倍,换手率是百分之几,成交额是几亿——你让模型直接学,它肯定优先关注数值大的那个。
我个人习惯用Z-score标准化:
import pandas as pd
import numpy as np
def zscore_standardize(df, columns):
"""
Z-score标准化
公式: (x - mean) / std
"""
result = df.copy()
for col in columns:
mean = result[col].mean()
std = result[col].std()
result[col] = (result[col] - mean) / std
return result
# 示例:对因子数据做标准化
factor_data = pd.DataFrame({
'pe_ratio': [15.2, 28.6, 9.8, 45.3, 12.1],
'turnover': [0.03, 0.08, 0.01, 0.12, 0.05]
})
standardized = zscore_standardize(factor_data, ['pe_ratio', 'turnover'])
print(standardized)
4.2 中性化:剔除你不想要的「杂质」
中性化这个概念,很多初学者容易搞混。它不是为了消除量纲,而是为了剔除某个已知因素的影响。
举个例子:你发现「市值越小,收益越高」——但这不是你想要的因子,你真正想研究的是「盈利能力」这个因子。那你就需要把市值的影响从中性化掉。
怎么做?回归法是最常用的:
from sklearn.linear_model import LinearRegression
def neutralize_factor(factor, exposure):
"""
对因子做中性化处理
factor: 原始因子值
exposure: 需要中性化的暴露变量(如市值)
"""
# 第一步:用exposure回归factor
model = LinearRegression()
model.fit(exposure.values.reshape(-1, 1), factor)
# 第二步:取残差作为中性化后的因子
predicted = model.predict(exposure.values.reshape(-1, 1))
neutralized = factor - predicted
return neutralized
# 示例:对盈利因子做市值中性化
profit_factor = np.array([0.05, 0.12, -0.03, 0.08, 0.15])
market_cap = np.array([100, 500, 50, 200, 800]) # 单位:亿
neutralized_profit = neutralize_factor(profit_factor, market_cap)
print(f"原始因子: {profit_factor}")
print(f"中性化后: {neutralized_profit}")
4.3 哑变量处理:把「类别」变成「数字」
机器学习模型只认数字,不认「行业」「板块」「地区」这些文字。哑变量(也叫独热编码)就是干这个的。
但这里有个坑——哑变量陷阱。如果你有K个类别,只需要K-1个哑变量,否则会引入完全多重共线性。
import pandas as pd
# 原始数据:行业分类
data = pd.DataFrame({
'stock': ['A', 'B', 'C', 'D', 'E'],
'industry': ['金融', '科技', '消费', '金融', '科技']
})
# 方法一:pandas的get_dummies(自动处理K-1)
dummies = pd.get_dummies(data['industry'], prefix='industry', drop_first=True)
print("哑变量处理结果:")
print(pd.concat([data, dummies], axis=1))
# 方法二:手动编码(适合有序类别)
# 比如:小盘=0,中盘=1,大盘=2
data['size_level'] = [0, 1, 2, 1, 0]
print("\n有序编码示例:")
print(data[['stock', 'size_level']])
4.4 时序 vs 截面:处理方式的本质差异
这是本章最核心的部分。我见过太多人把时序数据和截面数据混在一起处理,结果模型一塌糊涂。
你想想看:
- 时序数据:同一只股票,不同时间点的数据。比如贵州茅台过去500天的日收益率。
- 截面数据:同一时间点,不同股票的数据。比如2024年1月1日,全市场5000只股票的市盈率。
处理方式完全不同:
| 操作 | 时序处理 | 截面处理 |
|---|---|---|
| 标准化 | 按时间窗口滚动计算均值和标准差 | 每个时间点单独计算 |
| 中性化 | 通常不做,或做时间序列去趋势 | 按截面回归取残差 |
| 哑变量 | 时间哑变量(月份、星期几) | 截面哑变量(行业、板块) |
| 缺失值处理 | 前向填充或插值 | 截面均值填充或删除 |
举个例子,你就明白了:
# 时序标准化:滚动窗口
def time_series_standardize(series, window=60):
"""滚动窗口标准化,避免未来信息"""
rolling_mean = series.rolling(window=window).mean()
rolling_std = series.rolling(window=window).std()
return (series - rolling_mean) / rolling_std
# 截面标准化:每个时间点单独做
def cross_section_standardize(df, date_col, factor_col):
"""按日期分组,每组内做Z-score"""
def zscore_group(group):
mean = group[factor_col].mean()
std = group[factor_col].std()
group[factor_col + '_zscore'] = (group[factor_col] - mean) / std
return group
return df.groupby(date_col).apply(zscore_group)
# 示例数据
import pandas as pd
import numpy as np
# 模拟数据:3只股票,5个交易日
dates = pd.date_range('2024-01-01', periods=5, freq='D')
stocks = ['A', 'B', 'C']
data = []
for date in dates:
for stock in stocks:
data.append({
'date': date,
'stock': stock,
'factor': np.random.randn() * 0.1 + 0.05
})
df = pd.DataFrame(data)
# 截面标准化
df_standardized = cross_section_standardize(df, 'date', 'factor')
print(df_standardized)
4.5 本章知识体系总览
下面这张图,是我自己梳理的特征工程核心流程。你照着这个框架走,基本不会出错:
这张图的核心逻辑就是:拿到原始因子后,先判断数据类型。时序数据走左边,截面数据走右边。两边处理完,最终汇合成一个统一格式的特征矩阵,再喂给模型。
好了,这一章的内容就到这里。特征工程是因子挖掘的「地基」,地基打不牢,模型再花哨也是空中楼阁。下一章我们进入真正的「因子挖掘」环节,到时候会用到今天讲的所有技巧。