☰
机器学习多因子选股全流程:数据预处理、特征选择与模型落地
2026/10/21 6:09:30 网站建设 项目流程

多因子选股这词儿,在量化圈里快被聊烂了,但真正想把它从一篇论文落成一个能跑通的全流程,中间全是细节。尤其是"数据获取、数据预处理、特征选择"这三板斧,看着简单,做起来处处是坑。去年我重构了这套基于机器学习模型的多因子选股预测流水线,把之前跑不通、过拟合、换数据就失效的问题一个个摁下去之后,才算是摸清了里面真正的门道。这篇就把整个流程拆开讲清楚,重点是前三个环节:怎么拿数据、怎么把脏数据洗干净、怎么从几十个因子里面筛出真正有用的特征。文里所有代码片段都是可以直接拿去改的,数据口径也按我实际用下来的可靠方案给出来了。

做这套东西之前,得先明确一个概念:多因子选股不是简单地"挑几个指标然后排序打分"。传统做法是给估值、成长、质量、动量这些因子各分配一个权重,最后加权求和得到一个综合得分。但问题在于,因子和未来收益之间的关系大多不是线性的——市盈率低的股票不一定就涨,动量过高的可能反转向下,这里面有阈值效应、交互效应,单靠线性加权根本刻画不出来。机器学习擅长解决的就是这类非线性拟合问题,让模型自己去学因子和收益之间的复杂映射。所以整个项目的核心思路,就是用一套标准流程把原始数据变成干净的因子矩阵,再交给LightGBM、随机森林这类模型做预测,然后用分层回测来验证模型到底能不能产生稳定超额收益。

这个方案最大的优点是容错率高、迭代快。数据做扎实了,模型部分其实换什么框架都行;特征选对了,很多调参的功夫都可以省下来。今天我们按实操顺序过一遍,从数据开始,走完整条链路。

1. 全流程框架与整体设计思路

1.1 先把"因子"这个核心概念掰扯清楚

因子(Factor)本质上就是某个能解释股票未来收益的特征变量。基本面因子比如ROE(净资产收益率)、PE(市盈率)、营收增速;量价因子比如N日收益率动量、波动率、成交量乖离;还有行业因子、情绪因子、宏观因子……每一个都是在描述股票的不同侧面。多因子选股模型的本质,就是找到这些特征与未来收益之间的规律,再用这个规律去预测新的样本。

传统线性多因子模型在业界用了很多年,CAPM、Fama-French三因子、Carhart四因子都是这个框架的经典代表。但市场越来越复杂,因子的非线性构造、因子之间的交互作用开始变得明显。比如一个低PE股票如果同时具备高ROE和正的动量,它的预期收益通常远高于三个因子单独使用时的简单叠加。这种非线性交互关系,传统线性模型几乎无法捕捉。

1.2 为什么选择机器学习模型而不是传统打分法

用机器学习做多因子选股,最大的优势是它不需要你提前假设因子和收益之间的关系形式。树模型天然支持非线性切分,LightGBM的直方图算法在海量样本上训练速度很快,还能自动处理缺失值。神经网络更灵活,但数据量不够时容易过拟合,我在实践里反而觉得树模型更稳。

另外,收益预测本质上是信噪比极低的任务。机器学习的优势不在"精准预测每一天涨跌",而在"批量筛选出概率上更可能跑赢的股票组合"。我们要的是一种排序能力,而不是精确的回归拟合。这个定位想清楚之后,很多建模细节就不会纠结了。

1.3 完整Pipeline需要涵盖哪些环节

一个能实盘验证的完整流程至少包含六个环节:

  1. 数据获取:行情数据、财务数据、复权因子、行业分类、指数成分,缺一不可。
  2. 数据预处理:去极值、标准化、缺失值填充、中性化、数据对齐。
  3. 特征选择:单因子检验、相关性分析、重要性排序,最终形成精简有效的特征集。
  4. 样本与标签构造:确定预测目标(比如未来20日收益率),划分训练集、验证集、测试集。
  5. 模型训练与调优:按时序切分方式验证,防止数据泄漏,选择稳定模型。
  6. 回测与归因:分层回测、超额收益曲线、换手率分析、IC序列监控。

今天重点落在前三个环节,但我在讲设计时会把整个框架串起来讲,因为前端的每一个决定都会直接影响模型和回测的质量。例如数据对齐如果出了前视偏差(look-ahead bias),后面所有结果都是假的,这问题在回测里极难排查。

2. 数据获取:地基决定上层建筑

2.1 四种数据源怎么选、怎么配

做多因子选股,至少需要四类数据:

第一类是日线行情,包含OHLCV(开高低收量),必须包含复权因子,否则分红除权会导致收益率计算错误。第二类是财务报表数据,包括资产负债表、利润表、现金流量表的主要科目,用来计算ROE、毛利率、负债率等基本面因子。第三类是交易状态数据,例如停复牌标记、ST标记、上市日期、退市日期。第四类是行业与指数分类,比如申万一级行业分类、沪深300成分股调整记录。

数据源的选择上我实际用的组合是:行情数据用开源接口直接拉日线,财务数据用专业数据终端导出的标准化问卷;如果资金有限,公共数据API也能顶住,但要注意财务数据的频率和质量有一定差距。数据获取没有一个"银弹",关键是搞清楚每张表的更新频率、字段格式、口径差异。

2.2 数据对齐的正确姿势:代码表、索引时间戳和复权因子

数据对齐是多因子模型最容易出问题的地方。股票代码在不同数据源里格式可能不一样(比如"600000"还是"600000.SH"),财务报告的发布日期和报告期也不是同一个时间。必须建立一个统一的主数据表,包含证券代码、交易日期、复权因子、停牌状态、上市状态,并以此作为合并数据的主键。所有因子数据都对齐到这个表上,才能保证每个时间截面上拿到的信息都是当时真实可得的。

这里分享一个实际工作中容易踩的坑:财务数据的发布时间问题。年报的截止日期是12月31日,但真正披露很多要到次年4月。如果你在4月30日前就用"最新年报数据"计算因子,也就是用了尚未发布的财报,这在做法上属于未来数据,回测时不会暴露,实盘时却是灾难。我实际的解决方案是必须用发布公告日期而不是报告期来对齐财务数据,也就是建一张"公司公告发布日期表",把每个报告期对应的最早公告日期维护好,因子计算时以公告日期为可用点。这一步极其重要。

2.3 数据清洗的第一道工序:股票池与交易状态过滤

拿到原始数据后,第一件事不是算因子,而是先圈定股票池。通常选沪深300或中证500成分股作为研究范围,然后在这个范围内剔除两类股票:一类是上市不满60个自然日的新股,次新股波动剧烈、数据不稳定;另一类是已经被标记为ST或*ST的股票,它们受涨跌停限制影响,交易行为严重失真,且存在退市风险。

剔除的方式建议直接在主数据表里打标记列,而不是物理删除,方便后续做敏感性和稳健性检验时快速复用原始样本。同时还要处理停牌数据——A股个股经常因为重大事项停牌,停牌期间没有成交,收益率恒定为0,这些数据点对模型训练是有害的。我实盘里通常的做法是:停牌超过5个交易日的样本直接从特征矩阵中剔除,当天停牌且未成交的也剔除,只保留可正常交易时段的样本。

2.4 涨跌停和零成交量的特殊处理

处理涨跌停数据是这边一个独有的细节。股票涨停时,卖单通常被锁死,你根本买不进,但行情数据里的收益率仍然会记录一个很大的正收益。如果不加处理,模型会学习到"涨停次日继续大涨"这类虚假规律,实盘完全无法复现。所以必须把当日涨停且收盘价接近涨停价的样本标记出来,在构造标签和训练样本时按需剔除,或者将标签做截尾处理(winsorize)。

零成交量也要注意。部分股票长期成交低迷,日成交量只有几十手,价格基本是"有价无市"。这类样本即使被纳入股票池,也会严重干扰模型的排序。实测下来,在全市场选股时,剔除近20个交易日日均成交额低于某阈值(比如1000万)的股票,能显著提升模型的稳定性。这个阈值需要根据你的股票池和周期调整,不是固定的。

3. 数据预处理:从原始数据到可用特征

3.1 缺失值处理:纵向填充与横向截面的取舍

原始数据拿过来,第一反应肯定是df.fillna(method='ffill'),但多因子场景下机械地向前填充会出问题。财务数据是有公布日期的,在公布日期之前缺失是正常的——那时数据还没产生;在公布日期之后缺失则可能是停牌或数据源问题。必须区分这两种情况,分别处理。

对于行情类因子(如动量、波动率),缺失通常是因为停牌,建议不填充,直接从样本中剔除。对于财务类因子,逻辑上应该用最近一期已公告的数据填充,但要注意"最近一期"必须小于因子的计算日期。我通常的做法是:先按"公告日期"做一次性向前填充(ffill),然后检查每个时间截面上缺失率,缺失率超过20%的因子直接弃用。这个比例是经验值,你可以根据自己的样本量调节。缺失率在可接受范围内的因子,用行业横截面的中位数填充,比用全局均值填充更合理。

3.2 去极值处理:MAD法与百分位法的选择

因子值中经常出现极端异常值,比如某只股票单日涨幅超过20%(因重组复牌),或者PE因为微利而高达数千倍。这些极端值如果不处理,树模型虽然有一定耐受力,但回归类模型和标准化后的距离类特征会被严重扭曲。

去极值有两个常用方案:MAD(中位数绝对偏差)法和百分位缩尾法。MAD法先求因子值的中位数M,再求每个样本与M的绝对偏差的中位数,即MAD,然后认为因子值落在"M ± n × MAD"之外的样本都是异常值,将它们截断到边界值。n取值通常为3或5,我实测下来用3效果偏保守,5则过于宽松,一般取4。百分位法更直接,超过98%分位数或低于2%分位数的值都拉回边界。两者的差异在于MAD对偏态分布更稳健,百分位法对样本量要求更高。我的建议是先用MAD处理极端异常,再用百分位法做一个二次保险。

import numpy as np def mad_winsorize(s, n=4): median = np.nanmedian(s) mad = np.nanmedian(np.abs(s - median)) if mad == 0: return s upper = median + n * mad lower = median - n * mad return np.clip(s, lower, upper)

3.3 标准化:Z-Score还是Rank化

去极值后需要标准化,因为不同因子的量纲差异很大。ROE可能是0.15,动量因子可能是0.35,换手率可能是2.8,如果不标准化,树模型做特征分裂时只关心数值切分点,不受量纲影响;但岭回归或神经网络等模型却会因此偏向数值更大的特征。

Z-Score是常见做法(x - mean) / std,但因子分布往往不是正态的,Z-Score会被极值尾巴影响。另一个更稳健的方法是正态化排序(Rank化),将因子值从大到小排序后映射到0到1之间的均匀分布,再通过逆正态变换把均匀分布转成近似的正态分布。这样处理后每个因子都变成"标准化后的分位数排名",因子之间的可比性大幅提升。

我在实践中的经验是:如果后面用的是LightGBM这类树模型,Z-Score简单处理就够了,因为树模型不敏感于单调变换;如果后面搭配了线性模型或SVM,建议做Rank化。如果你不确定模型最终形态,那就先Rank化,它不会让任何模型变差。

3.4 行业与市值中性化:剔除你不想要的风格暴露

这一步很多人会忽略,但恰恰是多因子模型不过拟合市场的关键。行业中性化的目的是保证你选出的股票覆盖多个行业,而不是疯狂集中到某个高收益行业。市值中性化的目的是避免模型变相做一个"买小盘"或"买大盘"的策略。某些年份小盘股涨到天上,模型如果有市值暴露,回测曲线会很好看,但一旦风格切换,实盘就会剧烈回撤。这类收益属于风格因子收益,不是你的模型Alpha。

中性化的做法是回归残差法。先将因子值对行业虚拟变量和市值对数做线性回归:

import statsmodels.api as sm # df_factor 包含因子值、行业虚拟变量、log_market_cap X = df[industry_dummies + ['log_market_cap']] y = df['factor_value'] model = sm.OLS(y, sm.add_constant(X)).fit() df['neutralized_factor'] = model.resid

回归后的残差就是剔除行业和市值影响后的纯因子值。注意回归要在每个横截面上独立做,也就是说每天分别做一次,不能跨越时间混着做。做完中性化,再拿残差去做标准化,这就是实践中常说的"因子预处理三部曲":去极值、中性化、标准化。

4. 特征选择:从几十个因子到有效特征集

4.1 单因子有效性检验:IC、ICIR与分组收益

特征选择不应该是直接扔给模型去跑重要度,因为模型重要性很容易被多重共线性和噪声误导。标准做法先从单因子检验入手。IC(信息系数)是因子值与下期收益的秩相关系数(Spearman),衡量因子预测能力的单调性。IC的绝对值大于0.03就算有初步预测力,大于0.05算优秀。ICIR(IC的均值除以IC的标准差)衡量IC的稳定性,一般要求ICIR大于0.3,才说明因子不是靠运气。

分组收益测试更直观:每次按因子值从小到大排序,等分为5组或10组,计算每组在未来N日的平均收益。如果最高组的收益显著高于最低组且中间组呈现单调递增(或递减),说明因子确实具有分组能力。单调性比头尾差值更重要,很多因子头尾有差异但中间组杂乱,这种因子在实盘里可靠性很低。

一个小贴士:IC和分组收益测试需要使用中性化之前的因子值还是之后的?我习惯两个都测。如果中性化之前有高IC、中性化之后IC归零,说明这个因子赚的是行业或市值风格的钱,不是真正的Alpha。这种因子如果是作为趋势判断可以保留,但对模型的增量帮助有限。

4.2 因子相关性分析与去冗余

几十个因子算出来之后,第一个动作就是算相关性矩阵。两类常见的情况:一是高度同质化,比如动量因子(过去20日收益率)和区间涨跌幅因子(过去20日累计涨幅)相关性可能超过0.9,留一个就够了;二是因子本身是同一财务指标的不同表达式,比如ROE和ROA,它们高度相关,同时纳入存在多重共线性问题。

处理方法是按相关性聚类分组,组内相关性超过0.7的因子只保留一个代表。保留标准有三个优先级:单因子IC最高者优先;若IC接近,优先选经济逻辑更直接、计算口径更透明的因子;若仍接近,优先选行业覆盖完整、缺失更少、时间序列更长的因子。做完这一步,特征数量通常能下降50%,模型的泛化能力会有明显提升。

4.3 机器学习特征筛选:不能让模型全权代理

树模型自带的特征重要性可以直接输出特征排序,但不是所有"高重要性"特征都是好东西。重要性高只能说明这个特征在训练集上切分贡献大,如果它本身有大量的未来信息或数据泄漏,重要性再高也没用。所以我对特征重要性的使用方式,是把它和单因子检验结果比对:只有在单因子检验中有效(IC显著、分组单调)、且在模型中重要性也高的因子,才算真正的核心因子。单靠机器学习筛出来的高分特征,必须回到业务逻辑里去验证它为什么有效。

另一种筛选方法是用L1正则化的线性模型,比如Lasso回归,系数被压到零的特征直接剔除。这个方法的好处是快,且结果可解释;缺点是线性模型无法捕捉非线性关系,可能低估那些单独看非线性但实际有效的因子。所以最稳妥的方案是:先用聚类相关分析粗略去冗余,再用Lasso做一轮粗筛,最后丢进LightGBM看重要性排序,用 permutaion importance 做终审。

# 用排列重要性做最终特征筛选的思路 from sklearn.inspection import permutation_importance result = permutation_importance(model, X_val, y_val, n_repeats=10, random_state=42) # 只保留 feature_importance_mean 大于某个阈值的特征

4.4 避免"因子挖掘"变成"数据挖掘陷阱"

单因子检验跑几千个因子,挑选那些回测IC历史表现最好的再放进模型,这本身就是一种选择偏差。当你从众多因子中选出表现最好的几个时,哪怕它们原本都是随机噪声,也会因为"幸存者偏差"呈现出不错的IC值。所以特征选择时必须控制FDR(错误发现率),用Benjamini-Hochberg方法调整p值,或者对每个因子做更长时间的样本外检验。在样本外、风格完全相反的市场阶段重新测试,能通过这一关的因子,才是真正值得保留的核心特征。

5. 模型训练与评估的落地细节

5.1 样本与标签设计:预测什么比怎么预测更重要

绝大多数多因子模型把标签定义为股票未来N日的收益率。N的选择很关键:用3日太短,交易成本摩擦大,模型学到的大多是市场微观结构噪声;用60日太长,换手率低但样本重叠严重,降低了有效样本量。我实际跑下来,20日(约一个月)是常用的折中周期。在标签构造时还要把极端收益处理掉,比如未来20日收益超过100%的股票通常有关重大重组,这类收益不具备可预测性,直接剔除或截断。

样本权重的设计也容易被忽略。如果所有历史样本一视同仁,模型会过度关注那些成交稀疏、股价极低的问题股。我会根据过去20日平均成交额给样本加权重,成交越活跃的样本权重越大,这样模型更聚焦于"实际可交易的股票"。

5.2 模型选型:为什么LightGBM是主力选择

多因子预测场景下,样本量大(几十万行)、特征中等(几十维)、标签噪声高、特征中混合数值型和类别型变量,LightGBM是目前效率与效果平衡最好的方案。它的直方图算法在大样本下训练极快,原生支持类别特征,叶子节点方向的生长方式可以在高噪声场景下用更小的学习率配合更多子树来抑制过拟合。随机森林适合做特征重要性的交叉验证,XGBoost和LightGBM类似但训练速度略慢,神经网络则建议在特征数量很多、样本量百万级以上时再考虑。

树模型的参数优先级也有讲究。我调参的顺序是:先设置学习率0.05、叶子节点数16、最小样本叶子50,训练1000棵树观察验证集损失曲线;然后根据过拟合程度依次调整num_leaves、min_data_in_leaf和feature_fraction;最后才动学习率和迭代次数。默认参数直接跑,结果往往严重过拟合。

5.3 时间序列交叉验证:杜绝数据泄漏的最后防线

这是整个模型流程里最容易翻车的地方。标准K折交叉验证假设样本独立同分布,但金融数据是时间序列,用随机切分的方式会把未来信息混进训练集,导致回测性能虚高、实盘一地鸡毛。必须使用时间序列交叉验证:按时间先后依次扩展或滚动切分,训练集只使用t时刻之前的数据,验证集和测试集只使用t时刻之后的数据。

实际操作中还要额外加一道"冷却期"。因为预测标签是未来20日收益,训练集最后20天的数据标签实际上覆盖了验证集开头20天的信息,两者存在重叠。切分时应该在训练集末尾和验证集开头之间留出至少N日的间隔,避免信息泄漏。这个细节很多公开代码都不会处理,但我实测下来,忽略这一步会使验证集IC整体虚高0.01到0.02,看起来很美,实盘却完全做不出效果。

# 时序扩展窗口切分的伪代码 for fold in range(n_folds): train_end = start + fold * step val_end = train_end + horizon X_train = X_weekly[:train_end] y_train = y_weekly[:train_end] X_val = X_weekly[train_end + horizon : val_end + horizon] y_val = y_weekly[train_end + horizon : val_end + horizon]

5.4 回测与业绩归因:分层收益和真实Alpha

模型训练完成后,最常见的验证动作是把预测得分按行业、市值分层,构建多空组合或Top组合回测。但泛泛地看累计收益曲线远远不够。拆解收益来源是第二步:把组合收益分解到因子收益(Beta)、行业配置收益、个股选择收益,如果个股选择收益(Alpha)不显著,说明模型只是变相的行业或风格轮动,不是真正的预测模型。这个拆解逻辑可以参考业绩归因框架(Brinson模型),拆解完再看分层单调性。

另外一个重要的指标是模型预测得分的IC和RankIC序列。记录每个调仓日模型预测得分和未来20日实际收益的秩相关,画成IC序列走势图,观察IC均值和稳定性。IC均值模型大于0.05且IC胜率高于55%,才算建模成功。这个标准不同周期、不同市场结构下会有波动,但作为通用基准是可靠的。

6. 实操中踩过的坑与排查心得

6.1 复权方式不一致导致结果完全失真

复权分为前复权和后复权。前复权以最新价为基准调整历史价格;后复权以最早价格为基准。做因子计算时必须统一,尤其是涉及收益率和历史涨幅的因子,混用会导致数值完全错乱。我自己踩过一次:从两个数据源分别拉行情,一个用前复权、一个用后复权,模型在某只票上预测得分极值,查了很久才发现是复权因子差了一个数量级。解决办法很简单:所有价格序列统一用后复权计算因子,计算收益率时再做差分。后复权的好处是历史价格不会因新除权除息事件而整体改变,因子序列更稳定。建好一张复权因子表,入库之前必须做除权除息价格的抽样校验。

6.2 回测阶段捡了"幸存者偏差"的便宜

只用当前还在交易的股票构建回测样本,等于剔除了那些已经退市的股票,收益天然偏高。尤其是A股历史上不少退市股在最后阶段跌幅惊人,它们如果在模型打分里排前面,就会真实地拉低实盘收益。要尽可能使用"当时时点"的股票池:每期回测的股票名单和股票池成分调整应该使用该时点真实可得的列表,而不是用最新的全市场名单回溯替换。在做因子数据入库之前,务必把退市股、吸收合并股的交易数据保留完整,不要因为"反正现在交易不了"就筛选掉。这个坑修复起来数据量不大,但影响的是整条回测曲线的可信度。

6.3 过拟合信号与监控手段

模型的训练集IC和验证集IC差距过大,是最直接的过拟合警报。差距在0.01到0.02以内正常;超过0.03就要警惕。另一个监控是特征重要度的月度变化:如果每月重要度排名波动剧烈,说明模型学习的主要是噪声而不是稳定规律,应适当增加min_data_in_leaf或者削减特征数量。

还有一点要强调,回测和实盘的"换手率"差距也要监控。一个Top20的组合,如果每次调仓换手率超过50%,说明模型得分在相邻两期之间极不稳定,实盘滑点和冲击成本会吞掉大量理论收益。这时候应该延长调仓周期、增加模型组合的持仓集中度约束,或者在因子层做时序平滑。

6.4 端到端工程化:从一次性研究到可复用系统

这个项目早期我是用一堆Notebook拼起来的,每跑一遍新数据就要手动重跑全流程,既容易出错又不高效。后来我把流程改成了模块化流水线:数据层统一走数据入库脚本;预处理层每个因子做独立处理函数;特征层把单因子测试结果自动汇总成报告;模型层把训练、验证、测试划分做成标准化组件。每个环节产出的中间数据都有版本号,任何一步修改都可以追溯。强烈建议在这类项目之初就做好工程化规划,否则后期每次加上新因子,都会在一堆不可复现的流程里消耗时间。

7. 最后再分享几个让流程更顺的小技巧

如果你准备复现这个项目,有几个小事我建议先做。第一,因子库先用Excel或者数据库建一张"因子定义表",记录每个因子的名称、计算逻辑、依赖字段、更新频率、去极值参数和中性化选项,这是整个项目的数据字典,维护好它会让你后期省很多时间。第二,预处理函数全部按"横截面"维度写,而不是按"全量数据"维度写——输入是某一天的股票截面数据,输出是清洗后的同截面数据。这样保证每个时间点上处理逻辑一致,也方便并行计算。第三,模型训练前固定随机种子,每一次实验才能在相同条件下对比,否则你永远分不清改进效果来自模型还是来自随机性。

按照这套流程走下来,整个多因子建模项目的核心部分就落地了。数据、预处理、特征选择这三个环节占到全流程60%以上的工作量,也决定了项目80%的上限。把地基打牢,后面模型和回测部分反而会顺畅得多。我自己重构这版流程时最大的感受是:跑通一个模型不难,难的是让它在不同市场环境下都不露馅。希望这些经验能帮你少走一些我走过的弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询