机器学习选股:特征工程、滚动验证与过拟合控制
本课目标:明确机器学习在选股中的正确定位——截面排序问题,掌握特征工程、标签设计与滚动验证的关键细节,并建立一套对抗过拟合的系统性防线。
ML 在选股中的定位:排序问题,不是预测涨跌
把机器学习用于选股,最常见的误解是把它当成”预测某只股票明天涨还是跌”的分类器。金融数据信噪比极低,单只股票的绝对涨跌几乎不可预测。可行的问题定义是截面排序:在同一时点,判断哪些股票未来一段时间的相对表现更好。
这和多因子模型的目标完全一致——ML 模型本质上是一个非线性的因子合成器:输入是你熟悉的各类因子暴露,输出是一个综合打分,然后按打分构建多头(或多空)组合。你在因子课里学的一切——中性化、IC 检验、分组回测——依然是评估这个”超级因子”的工具。换句话说,ML 不是替代因子研究,而是替代”因子加权”这一步。
特征工程:模型的上限在这里决定
树模型再强,也只能在你给的特征空间里找规律。特征工程的几个要点:
- 以因子逻辑为起点:估值、质量、动量、流动性、波动率、分析师预期等成熟因子族是基础输入。每个特征最好能讲出经济含义,纯数据挖掘出来的特征要打折看待。
- 截面标准化:每个交易日内对特征做排序百分位或 zscore 处理,让模型学”相对关系”而非受市场整体状态干扰。
- 处理缺失与极值:缺失值的处理方式本身要放进流水线,避免用未来信息填充;极值按截面分位数缩尾。
- 控制特征数量:几十到一两百个有逻辑的特征,通常好过几千个自动生成的特征。特征越多,虚假规律越多。
def cs_rank(df: pd.DataFrame, cols: list) -> pd.DataFrame: """按交易日做截面百分位排序, 输出 0~1 之间的特征""" out = df.copy() out[cols] = df.groupby('date')[cols].rank(pct=True) return out标签设计:你让模型学什么,它就学什么
标签常被忽视,但它直接定义了模型的目标。常见选择:
| 标签设计 | 说明 | 适用场景 |
|---|---|---|
| 未来 N 日原始收益 | 最直接,但混入市场与行业波动 | 择时与选股混合的目标 |
| 未来 N 日超额收益 | 减去基准或行业均值,聚焦相对强弱 | 指数增强、市场中性 |
| 截面排序百分位 | 只保留相对名次,压制离群样本影响 | 排序模型的天然搭配 |
| 二分类(是否进前 30%) | 简化问题,牺牲部分信息 | 样本噪声极大时的稳健选择 |
两个关键细节:其一,标签周期要与调仓周期匹配,用未来 20 日收益训练的模型拿去做日频调仓,行为会失配;其二,标签计算必须严格用”当时可知”的价格,涨跌停无法成交的样本要处理,否则回测好看、实盘落空。
滚动验证:时间序列上不能随机切分
随机 K 折交叉验证在金融数据上是灾难——同一时期的样本被拆到训练集和验证集里,信息泄漏让验证分数虚高。正确的做法是滚动训练(walk-forward):
def walk_forward_splits(dates, train_years=3, test_months=6, gap_days=20): """滚动切分: 训练窗 -> 隔离带 -> 测试窗, 逐段向前推进""" splits = [] start = dates.min() while True: train_end = start + pd.DateOffset(years=train_years) test_start = train_end + pd.Timedelta(days=gap_days) # 隔离带防泄漏 test_end = test_start + pd.DateOffset(months=test_months) if test_end > dates.max(): break splits.append((start, train_end, test_start, test_end)) start = start + pd.DateOffset(months=test_months) return splits要点:训练窗与测试窗之间留出不短于标签周期的隔离带(purge/embargo),防止标签重叠造成的泄漏;模型的所有预处理(标准化参数、缺失值填充)只能在训练窗内拟合;每段测试结果拼接起来,才是接近实盘的样本外表现。
树模型 vs 线性模型:不是越复杂越好
梯度提升树(GBDT 类)是表格数据上的主力:能自动捕捉非线性和特征交互、对特征尺度不敏感、对缺失值宽容。线性模型(带正则的回归)则胜在稳定、可解释、样本效率高、不易过拟合。
务实的取舍:先用线性模型建立基线——它的样本外 IC 就是你的”及格线”;树模型如果不能稳定超过这条线,多出来的复杂度就是纯风险。金融数据的信噪比决定了模型复杂度的边际收益递减得很快,深度模型在日频截面选股上未必优于调好参的树模型。很多成熟团队的做法是线性与树模型集成,各取稳定性与非线性捕捉能力。
过拟合的系统性防线
过拟合不是某个参数没调好,而是研究流程的病。防线要建在流程上:
- 一开始就锁死最终测试集:留出最近一到两年数据,全程不看,所有模型选择只用滚动验证分数,最终测试集只用一次。
- 限制尝试次数并记录全部实验:跑了一百个配置只报告最好的一个,等于没有验证。样本外表现要按”尝试次数”打心理折扣。
- 偏好简单与稳定:两个模型分数接近时选简单的;对随机种子、窗口长度敏感的结果不可信。
- 用成本压力测试:加入现实的交易费用与冲击成本后,很多”显著”的模型信号会消失——消失的那部分本来就不属于你。
- 可解释性检查:用特征重要性、部分依赖等工具看模型在依赖什么。如果收益主要来自某个你无法解释的冷门特征,默认它是数据问题而非阿尔法。
模型上线不是终点:预测 IC、换手、特征重要性的漂移都要持续监控,衰减到基线以下就该退役重训。市场结构在变,因子拥挤度在变,一个模型的有效期是有限的——把”何时重训、何时下线”写成事前规则,好过在回撤中临场纠结。本文讨论研究方法,不构成投资建议。
本课小结
- ML 选股是截面排序问题,模型是非线性的因子合成器,评估工具与因子研究一脉相承。
- 特征要有经济逻辑并做截面标准化,标签周期须与调仓周期匹配且只用当时可知信息。
- 验证必须用带隔离带的滚动训练,随机 K 折在时间序列上会导致信息泄漏。
- 线性基线是及格线,复杂模型必须证明稳定超越它才值得采用。
- 反过拟合靠流程:锁死测试集、记录全部实验、成本压力测试、可解释性审查。
动手练习:用你已有的因子库构造 20 个左右截面标准化特征,标签取未来 20 日相对行业的超额收益排序百分位。分别训练岭回归与梯度提升树,用”3 年训练 + 20 日隔离 + 6 个月测试”的滚动切分,对比两者拼接后的样本外 RankIC 均值与标准差,并回答:树模型的优势(如果有)主要来自哪几个特征的非线性?