跳到主要内容

特征工程与标签构造

直觉

机器学习的第一步不是「选模型」,而是把原始价格变成「特征 → 未来收益」的监督学习表。这一步做得对不对,决定了后面一切是否可信——而最致命、也最常见的错误叫数据泄漏(look-ahead):用了「当时根本不可能知道」的信息去预测。特征工程本身没有高深数学,但纪律性极强:每一个特征都必须只用决策时刻之前的数据

监督学习的面板布局

把数据排成「每行一个 (时刻,股票)(\text{时刻},\text{股票}) 样本」的面板表

Xt,i=[动量t,i波动率t,i质量t,i],yt,i=Ri,tt+1X_{t,i} = \begin{bmatrix}\text{动量}_{t,i}\\ \text{波动率}_{t,i}\\ \text{质量}_{t,i}\\ \vdots\end{bmatrix},\qquad y_{t,i} = R_{i,\,t\to t+1}
  • Xt,iX_{t,i}:在月末 tt 用截至 tt 的信息算出的特征;
  • yt,iy_{t,i}:从 ttt+1t+1未来收益(标签)。
「人话」解释:什么叫「泄漏」?

你在月末 tt 做决策,只能看 tt 及以前的数据。如果某个特征偷偷用了 t+1t+1 的价格(比如「这个月的收益」算进了特征),模型就会「偷看答案」——训练时神准,实盘时崩盘。 铁律:标签 yty_tshift(-1)(未来),特征一律用 shift(≥0)(过去或当下),两者对齐到同一个决策日 tt

Rank IC:特征有没有预测力

衡量特征预测力的常用指标是 Rank IC(Spearman 信息系数)——每个时刻算特征与未来收益的截面秩相关,再对时间取均值:

ICt=Spearman ⁣(Xt,,  yt,),IC=1TtICt\text{IC}_t = \text{Spearman}\!\left(X_{t,\cdot},\; y_{t,\cdot}\right),\qquad \overline{\text{IC}} = \frac{1}{T}\sum_t \text{IC}_t

用「秩」而非原始值,是为了抗异常值。年化有效 IC 般在 0.03~0.08 就算不错的因子。

可运行案例:构造横截面特征并算 IC

用 50 只股票按月构造常见特征(多周期动量、波动率、反转)+ 元数据里的持久特征 quality,计算每个特征的月度 Rank IC 序列。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

prices = pd.read_csv('/data/sp500_universe_daily.csv', parse_dates=['date']).set_index('date')
meta = pd.read_csv('/data/sp500_metadata.csv').set_index('ticker').loc[prices.columns]
dret = np.log(prices / prices.shift(1))
monthly = prices.resample('ME').last()

# 特征: 每个都是 (date × ticker) 面板, .stack() 成 (date, ticker) 序列
mom = lambda n: np.log(monthly / monthly.shift(n))          # 截至 t 的过去 n 月动量
feats = {
  'mom_1':  mom(1).stack(),
  'mom_3':  mom(3).stack(),
  'mom_6':  mom(6).stack(),
  'mom_12': mom(12).stack(),
  'reversal': (-mom(1)).stack(),
  'vol_3': (dret.rolling(63).std() * np.sqrt(252)).resample('ME').last().stack(),
}
F = pd.DataFrame(feats).join(meta['quality'].rename('quality'), on='ticker')   # 加持久特征
y = monthly.pct_change().shift(-1).stack()                 # 标签: 下月收益(未来)
panel = F.join(y.rename('y')).dropna()

def rank_ic(col):
  return panel.groupby('date').apply(
      lambda g: g[col].rank().corr(g['y'].rank()) if len(g) > 5 else np.nan).mean()

ic = pd.Series({c: rank_ic(c) for c in F.columns}).sort_values()
print("各特征平均 Rank IC:")
print(ic.round(3))

plt.figure(figsize=(9, 3.5)); ic.plot.barh(color='steelblue')
plt.axvline(0, color='k', lw=0.6)
plt.title('横截面特征的平均 Rank IC(越靠右预测力越强)'); plt.tight_layout(); plt.show()
print("\n→ quality 为强因子; mom_12 间接捕获它; 短期动量/反转噪声大。")

动手改一改

  • mom(12) 改成「动量减波动」mom(12)/vol_3(风险调整动量),看 IC 是否更稳;
  • 删掉 quality 那一行,观察 mom_12 的 IC 是否明显下降——说明它的预测力大半来自质量因子。

小结

  • 把行情重排成 (时刻,股票)(\text{时刻},\text{股票}) 面板,特征对齐到决策日 tt,标签用 shift(-1)
  • 防泄漏:特征只用过去、标签用未来,绝不混用;
  • Rank IC:截面秩相关对时间取均值,是衡量因子/特征预测力的标准指标。