☰
量化因子预处理:去极值与标准化的完整实战指南
2026/10/5 17:39:15 网站建设 项目流程

做量化研究这几年,我最大的感受是:很多刚入门的朋友把精力全砸在模型选型和参数调优上,却对最前置的数据预处理环节一笔带过。实际上,因子数据从数据库里拉出来是什么样,直接决定了你后面做的IC分析、分层回测、回归检验,到底是在挖真实规律还是在拟合噪声。这篇是金融学习系列第十三篇,重点聊聊数据去极值和标准化处理这两件事。它们不炫技,但属于那种“不做好就别谈后面一切”的底层功夫。文章会把这俩操作到底在解决什么问题、每种方法背后的数学逻辑和适用场景、以及我踩过的坑全部摊开讲清楚,适合刚接触多因子研究、或者被异常值坑过又不知道怎么系统处理的朋友。

1. 为什么因子数据不能“拿来就用”

先别急着上代码,我们把最基础的问题想明白:一份原始因子数据,到底脏在哪里。

1.1 一份典型的因子数据,是带着“毛病”出厂的

无论是在聚宽、Wind还是自己搭的数据库里取数,你拉出来的因子值通常包含三类问题。

第一类是明显的脏数据。比如停牌期间被填充的异常值、复权价格计算错误导致收益率突变、某一天某只股票因为除权除息没有处理干净而出现的极端数字。这类数据属于纯粹的“录入事故”,如果不处理,一个点就能毁掉整条截面上的统计结果。

第二类是真实存在但极端异常的观测值。一家公司因为一次性资产处置,单季度净利润暴增几十倍,于是对应的市盈率因子突然从30倍掉到1.5倍;或者某只次新股上市首日被爆炒,换手率因子冲到80%。这些数据在业务层面是真实的,但它不代表这家公司的正常状态,也不具备预测意义。如果直接拿去算均值、算标准差、跑回归,就会把整个模型的系数拉偏。

第三类是量纲问题。一个多因子模型里往往同时包含市盈率(数值可以从负数到几百上千)、换手率(0.01到0.3之间的浮点数)、市值(几十亿到几万亿)这几种完全不同尺度的变量。如果不做标准化,模型天然会“偏爱”数值大的因子——这就像你同时用“厘米”和“公里”两种单位去量两条路的长度,然后直接加在一起,那个数字毫无意义。

所以,数据预处理不是为了走流程,而是要解决这三类问题:剔除或压缩异常值的影响,以及把不同因子的尺度抹平,让每一个因子都站在同一条起跑线上被模型评估。

1.2 为什么顺序一定是“先去极值,再标准化”

处理流程上有两个动作,但顺序不能反。

我见过不少新手上来就把数据做Z-score标准化,做完发现结果还是被少数极端值牵着走。原因很简单:Z-score本身用的是均值和标准差,而这两个统计量对异常值极其敏感。你数据里有一个异常大的值,它会把均值拉高,把标准差拉大,最后算出来的标准化结果,绝大多数样本会挤在一个很小的区间里,而那个异常值依旧远远甩在外面。等于说,标准化之前不处理极值,标准化也白做。

正确顺序是先做去极值,把那些极端观测值压缩到合理的边界上,让数据的统计分布回归基本正常;然后再做标准化,统一量纲。标准化依赖的均值和标准差,这时候才是有代表性的。

2. 去极值三兄弟:3σ、MAD和分位数法

去极值的主流方法,业内兜兜转转就三种:3σ法、MAD法(绝对中位数法)、分位数法。我把三种方法的原理、代码、适用场景一次讲透。

2.1 3σ法:最简单,但别指望它一招鲜

3σ法在课本上很常见,原理也直白:假设数据服从正态分布,那么99.73%的观测值应该落在均值加减三个标准差的区间内,超出这个范围的就认为是极值。处理方式有两种:一种是直接剔除,另一种是“缩尾”处理,把超过上界的值强制改成上界,低于下界的改成下界。

用Python实现缩尾版的3σ法,核心代码大概长这样:

import numpy as np import pandas as pd def winsorize_3sigma(factor: pd.Series, n_sigma: float = 3.0): mean = factor.mean() std = factor.std() lower = mean - n_sigma * std upper = mean + n_sigma * std return factor.clip(lower, upper)

看起来简单干净,但问题在于它默认数据接近正态分布。实际金融因子数据,尤其是估值类、换手率类因子,分布往往尖峰厚尾,极端值特别多。这时候用3σ法,上下界会被这些极端值本身撑得非常宽,你阈值之外的样本可能没几个,缩尾效果大打折扣。

所以我的习惯是:3σ法适合用在波动比较温和、近似对称分布的因子上。但凡发现数据分布有明显的右偏或左偏,先考虑下面两种方法。

2.2 MAD法:不惧极端值的稳健派

MAD法全称是Median Absolute Deviation,绝对中位数偏差。它跟3σ法的核心区别在于,用中位数替代均值,用绝对中位差替代标准差。中位数本身只跟排序位置有关,所以天然对极端值不敏感——这就解决了3σ法“均值被极值拉走”的死穴。

计算过程分两步:

  • 求出因子序列的中位数median
  • 对每个样本求绝对偏差 |x_i - median|,再对这组绝对偏差求中位数,得到MAD

然后构造上界和下界,标准做法是用一个系数把MAD转换成与标准差可比的尺度。正态分布下,MAD约等于0.6745倍的标准差,所以要除以0.6745。更稳妥的写法是乘以常数1.4826,本质上是一回事。

def winsorize_mad(factor: pd.Series, n_medians: float = 3.0): median = factor.median() mad = (factor - median).abs().median() if mad == 0: # 如果MAD为0,说明超过一半的样本值相同,这种因子不适合用MAD return factor lower = median - n_medians * 1.4826 * mad upper = median + n_medians * 1.4826 * mad return factor.clip(lower, upper)

MAD法的稳健性我用过很多次,确实比3σ法抗造。尤其当你的股票池里混进次新股、微盘股这种群体性极端值的时候,MAD的边界不会跟着它们乱跑。但要注意:如果因子序列里超过一半的值都相同,比如某个事件因子在一半股票上都是0,那MAD会退化成0,公式直接失效。这时候需要手动判断,改成用分位数法兜底。

2.3 分位数法:A股人最亲的手艺

分位数法,说白了就是按从大到小排个序,把排名在后1%(或者2%,5%)的样本找出来,把它们的值压缩到第99百分位(或者其他阈值百分位)上。这个思路非常朴素,它不依赖分布假设,直接看图说话。

def winsorize_quantile(factor: pd.Series, lower_pct: float = 0.02, upper_pct: float = 0.98): lower = factor.quantile(lower_pct) upper = factor.quantile(upper_pct) return factor.clip(lower, upper)

在A股做因子研究,分位数法是实战里用得最多的。原因很简单:大部分财务类截面因子远非正态分布,用分位数法不管数据长什么样,都能保证每期被处理的样本比例是固定且可控的,对后续截面回归的稳定性更友好。

阈值怎么选?没有绝对标准,常见的有1%/99%、2%/98%,也有人用5%/95%这种激进参数。我的经验是:如果因子本身噪声很大,阈值设严一点(比如1%);如果你担心处理掉真实信号,就设松一点(比如5%)。回测的时候不同阈值都得做敏感性测试,不要拍脑袋定死。

2.4 三种方法怎么选:一张表说清楚各自的脾气

方法分布假设对极端值敏感度适用场景主要缺点
3σ法近似正态高(均值和标准差会被极值污染)对称性好、波动温和的时序指标厚尾分布下缩尾效果差
MAD法基本无低(核心统计量是中位数)有尖峰厚尾特征、结构稳健的因子中位数过密时可能失效
分位数法无低(固定比例缩尾)财务类截面因子、A股多因子场景阈值选择偏主观

一句话总结:截面数据、股票池里情况复杂,优先分位数法;时序数据、方法稳健性要求高,优先MAD法;只有数据质量高、近似正态分布时才推荐直接3σ法。实际项目里,我一般把分位数法设为默认,异常偏好明显的因子用MAD复核一下。

3. 标准化处理:给所有因子装上同一套度量衡

去极值解决的是“极端值”问题,标准化解决的是“量纲”问题。这一步做完,因子与因子之间才能做横向比较,模型才能公平地评估每个因子的贡献。

3.1 Z-score标准化:默认选项,但不无脑选

Z-score是最常用的标准化方法,公式是 (x - mean) / std。处理完之后,因子均值归零,标准差为1,量纲影响被消除,同时保留了原始数据的相对分布形态。

def zscore(factor: pd.Series): return (factor - factor.mean()) / factor.std()

Z-score的好处在于它保留了个体之间差异的大小程度。比如标准化后A股票的因子值是2.0,B是0.5,这不仅能看出A比B大,还能知道大了多少倍个标准差。

但它有一个前提性弱点:如果因子分布严重偏斜,Z-score之后数据仍然偏斜,只是换了个尺子量而已。因此Z-score一般搭配分位数/MAD去极值使用——先去极值把分布纠偏,再做Z-score,效果才会好。

3.2 Min-Max归一化:每期都在变区间的老实人

Min-Max公式是 (x - min) / (max - min),把数据压缩到0到1之间。看起来直观,但它有两个硬伤:

  • 对极值极其敏感,一个异常值就能把max顶到天上,其他样本全被压到贴近0的位置
  • 上线区间跟样本本身有关,新样本到来后区间可能变化,不方便对实盘信号做严格复现

所以Min-Max在因子研究里我基本不用,它更适用于某些固定区间、业务含义明确的物理量。比如舆情打分、单因子得分这类天生有严格上下界的场景。

3.3 排序百分位法:不看大小看名次

百分位标准化是把原始因子值映射为它在截面上的排名百分位,不管原始值的绝对大小,只看它在横截面上排第几。处理后的数据均匀分布在0到1(或0到100)之间,不容易受极端值影响,也天然适应非线性分布。

def rank_pct(factor: pd.Series): return factor.rank(pct=True)

它跟Z-score的本质区别在于信息维度:Z-score保留了“差距大小”的信息,百分位法只保留“先后顺序”的信息。做多因子合成时,如果某些因子的数值大小业务含义不明确,或者因子本身非线性特征明显,用百分位法往往比Z-score更稳。

但这方法也有代价:它把所有样本间距离都抹平成等差,原始分布中“头部显著优于尾部”的表达就丢了。所以它不适合用在对间距敏感的回归模型里,更适合用在排序型打分框架中。

3.4 从“选什么标准化”想到的因子合成习惯

这几种标准化方法用下来,我的习惯是:做传统IC分析、多因子回归时,用Z-score,并且统一配合分位数去极值;做打分排序模型、风格配置时,用百分位法;只有遇到业务边界清晰的指标,才考虑Min-Max。还有一个小细节:标准化参数到底是横截面滚动算还是全样本算,这个要在回测里说清楚,不然会造成未来函数或者过拟合。多因子框架中加入新因子时,我会复盘每个因子历史各期的均值和标准差波动情况,确保因子“尺度”与处理方式匹配,而不是无脑套一个模板。

4. 完整实操:用Python串起去极值与标准化的全流程

理论讲再多,不如跑一遍数据。这里我直接给出一个可复现的完整流程,从原始因子拿到手,到输出干净的、可直接进模型的标准化因子。

4.1 构造一份带极端值的示例数据

先用模拟数据制造一个典型场景:假设有1000只股票,某换手率因子大致分布在对数正态范围内,但中间混入了几只次新股的极端高换手率样本。

import numpy as np import pandas as pd np.random.seed(42) n = 1000 # 生成一个右偏的原始因子,模拟换手率类指标 raw_factor = np.random.lognormal(mean=0.2, sigma=0.5, size=n) * 100 # 人工注入10个极端值 extreme_idx = np.random.choice(n, size=10, replace=False) raw_factor[extreme_idx] = np.random.uniform(200, 500, size=10) df = pd.DataFrame({ 'stock_id': [f'{i:04d}' for i in range(n)], 'raw_factor': raw_factor }) print(df['raw_factor'].describe())

输出结果里你会发现,raw_factor的均值被那10个极端值拉升得很明显,标准差也非常大,最大最小值差距悬殊。这就是典型的未经处理的原始状态。

4.2 三个去极值函数和两个标准化函数的组合方案

我刚才已经写了三个去极值函数和Z-score、百分位法函数。组合使用时,我一般这样做:

# 第一步:分位数去极值,压缩极端值 df['factor_w'] = winsorize_quantile(df['raw_factor'], lower_pct=0.02, upper_pct=0.98) # 第二步:Z-score标准化 df['factor_z'] = zscore(df['factor_w']) # 对比处理前后的分布 print(df[['raw_factor', 'factor_w', 'factor_z']].describe())

实际运行之后,你会看到几个显著变化:

  • raw_factor的均值明显高于中位数,因子分布右偏
  • 分位数去极值后,最大值从几百以上被压缩到上界附近,均值的中位数之间的差距迅速收窄
  • Z-score标准化之后,均值接近0,标准差接近1,但数据形态不再被异常值扭曲

4.3 批量处理多个因子时的工程化写法

实际做多因子研究时不会只处理一个因子,你要面对的是几百个因子乘几百个交易日的截面数据。这时候不能每期循环手动调函数,要封装成统一的处理管道。

def factor_preprocess(factor_df: pd.DataFrame, method='quantile', lower=0.02, upper=0.98, standard='zscore'): """ factor_df: DataFrame,index为股票代码,columns为日期,值为原始因子 """ def _process_row(row): row = row.dropna() if len(row) < 10: return row if method == 'quantile': row = winsorize_quantile(row, lower, upper) elif method == 'mad': row = winsorize_mad(row, 3) elif method == '3sigma': row = winsorize_3sigma(row, 3) if standard == 'zscore': row = zscore(row) elif standard == 'rank': row = rank_pct(row) return row return factor_df.apply(_process_row, axis=1)

这里最关键的参数是axis=1,即沿着交易日逐截面处理。因为因子研究中的去极值和标准化,几乎都是在横截面上做的,不是沿着时间序列做。具体原因,下一段落会展开。

4.4 一定要按截面处理,别按时间序列处理

很多新手在处理面板数据时有个经典错误:对某一只股票的整个历史时序计算均值和标准差做标准化。这个做法有严重的逻辑问题。

因子研究关心的核心命题是:在同一时刻,不同股票之间,因子截面取值的高低能否区分后续收益的高低。所以去极值和标准化的参照系必须来自横截面——同一天所有股票分布的均值和标准差才有意义。如果按时间序列标准化,你看到的只是“这只股票自己跟自己比”,丢失了横向可比性,这等于把截面因子硬生生做成了时序动量因子,业务含义完全变了。

数据缺失量较大的日期处理顺序也要注意:先按日期分组,在每组内剔除缺失值,再计算分位数或者均值标准差。如果直接把全样本混合计算,会因为不同日期的样本结构不同导致每期阈值漂移严重。

5. 常见问题与排查技巧实录

预处理这块看起来不起眼,但实际跑数据时,坑一个接一个。我把这几年遇到频次最高的问题整理出来,做成一张速查表,再逐个补充说明。

问题现象解决方案
先标准化还是先去极值标准化后仍有极端值永远是先去极值,再标准化
MAD法计算结果全是NaN因子序列中位数附近样本过多,MAD=0使用分位数法兜底
按时间序列做了标准化因子失去截面可比性调整为按截面处理
去极值后因子仍偏态阈值设太宽,极值没压住收紧分位数阈值或改用MAD法
分组处理还是全样本处理同行业内极值与全市场口径不同优先全市场全样本,稳健性检验再分行业

5.1 问题一:因子波动率在不同日期之间差异悬殊

同一个因子,某一段时期的截面标准差是1,另一段时期的截面标准差是10。这种情况在A股很常见,尤其是市场剧烈波动时期。如果你每期都用当期的均值和标准差做Z-score,那因子数值在不同时期之间就不能直接比较了,历史回测的因子值分布也会忽宽忽窄,给后续截面上统计指标的计算带来信号失真。

一种处理思路是滚动计算:用过去一段时间(比如60个交易日)的均值和标准差作为标准化参数,而不是只依赖当期截面。这个方法能保留因子值的纵向可比性,同时对参数更新频率和窗口长度要做稳健性测试。另一种思路是,所有标准化参数都用训练集的统计量固定下来,避免了跨度期间信号分布尺度的漂移。就我自己的习惯,纯截面排序类因子用前者;真正进入机器学习模型的特征才考虑固定参数的方式。

5.2 问题二:要不要分行业、分市值分组处理

在做剔极值的时候,很多人会纠结要不要在行业内部做。比如银行股市盈率普遍在个位数,科技股市盈率动辄五六十倍。如果你在全市场口径下做分位数缩尾,会把行业间本身的估值风格差异当成异常值削掉,导致银行股和科技股的区别被过度抹平。

我的处理原则是:优先做全市场的去极值和标准化,先把明确异常的数据清理掉;然后在做因子有效性分析和中性化的时候,再去剥离行业和市值的影响。这两件事的职责不同,混在一起处理,反而会把结构性差异和异常噪声搅成一锅粥。

当然,如果你想验证某因子在细分领域内部的选股能力,分行业单独处理不是不行,但回测时要注意:分行业处理往往意味着行业内排序,组合构建逻辑和全市场排序会有本质差别,千万不要回测时一套、实盘时另一套。

5.3 问题三:去极值到底该用“剔除”还是“缩尾”

很多初学者会把超出阈值的样本直接删掉,这个动作在截面数据分析里极度危险。删掉一个样本,等于你在横截面上人为留下一个空洞,后续排序、分位统计、回归都会因为这个洞产生系统性偏差。尤其是小市值股票样本量本来就不多的时候,删掉几个极端值,等于把某些风格特征的股票整体排除在外,样本选择性偏差会很严重。

所以,截面数据的极值处理几乎只用缩尾,不删除样本。把极端值压缩到边界上,既消除了它对数理统计的干扰,又保住了样本的完整性。

5.4 问题四:标准化之后数据出现NaN或无穷值

这个问题出现的原因主要是:某个截面内股票样本量太少,或者所有样本的因子值都完全相同,导致标准差为0,标准化时出现除零错误;某个因子的去极值下界算出来还是负值,在业务逻辑上不合理(比如换手率不可能为负),但因为前期数据质量问题出现了。

日常处理时我会分两层排查:先检查原始因子缺失情况和常量截面,如果整个截面因子都一样,直接放弃该期因子值,不要硬算;再检查去极值之后的上下界是否在业务合理范围内,不合理则修正数据源。这个坎过不去的因子,不管后面模型收益看着多漂亮,都不值得信任。

写在最后的一点体会

数据去极值和标准化处理,在整个量化研究链路里确实不起眼,但它属于那种“做了不显功、不做必闯祸”的活儿。我自己踩过的最大一个坑,是早期做因子合成时偷懒把去极值参数设得很宽,结果因子值被三个极端股票牢牢把持,分层回测出来的收益曲线底下藏的全是这几只股票的独角戏,等换了个市场环境立刻崩塌。后来老老实实把每一步处理逻辑参数拆分出来做敏感性测试,才彻底稳住了。

所以我的建议很简单:第一,去极值和标准化的默认管线一定要固定下来,最好形成一个标准函数库,每次建模都走同一套管线,不要今天用3σ明天用分位数;第二,任何参数都要做敏感性分析,回测时顺便跑几个上下浮动版本,看看结果是不是依赖特定阈值;第三,处理完的数据一定要存一份副本,方便后续审计问题,排查某个奇特结果时你就能顺着数据管线逐层溯源。数据干净了,模型才有资格谈好坏。这一点,做研究做得越久,体会越深。

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

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

立即咨询