
做量化研究这几年我最大的感受是很多刚入门的朋友把精力全砸在模型选型和参数调优上却对最前置的数据预处理环节一笔带过。实际上因子数据从数据库里拉出来是什么样直接决定了你后面做的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.0B是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(pctTrue)它跟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(mean0.2, sigma0.5, sizen) * 100 # 人工注入10个极端值 extreme_idx np.random.choice(n, size10, replaceFalse) raw_factor[extreme_idx] np.random.uniform(200, 500, size10) 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_pct0.02, upper_pct0.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, methodquantile, lower0.02, upper0.98, standardzscore): factor_df: DataFrameindex为股票代码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, axis1)这里最关键的参数是axis1即沿着交易日逐截面处理。因为因子研究中的去极值和标准化几乎都是在横截面上做的不是沿着时间序列做。具体原因下一段落会展开。4.4 一定要按截面处理别按时间序列处理很多新手在处理面板数据时有个经典错误对某一只股票的整个历史时序计算均值和标准差做标准化。这个做法有严重的逻辑问题。因子研究关心的核心命题是在同一时刻不同股票之间因子截面取值的高低能否区分后续收益的高低。所以去极值和标准化的参照系必须来自横截面——同一天所有股票分布的均值和标准差才有意义。如果按时间序列标准化你看到的只是“这只股票自己跟自己比”丢失了横向可比性这等于把截面因子硬生生做成了时序动量因子业务含义完全变了。数据缺失量较大的日期处理顺序也要注意先按日期分组在每组内剔除缺失值再计算分位数或者均值标准差。如果直接把全样本混合计算会因为不同日期的样本结构不同导致每期阈值漂移严重。5. 常见问题与排查技巧实录预处理这块看起来不起眼但实际跑数据时坑一个接一个。我把这几年遇到频次最高的问题整理出来做成一张速查表再逐个补充说明。问题现象解决方案先标准化还是先去极值标准化后仍有极端值永远是先去极值再标准化MAD法计算结果全是NaN因子序列中位数附近样本过多MAD0使用分位数法兜底按时间序列做了标准化因子失去截面可比性调整为按截面处理去极值后因子仍偏态阈值设太宽极值没压住收紧分位数阈值或改用MAD法分组处理还是全样本处理同行业内极值与全市场口径不同优先全市场全样本稳健性检验再分行业5.1 问题一因子波动率在不同日期之间差异悬殊同一个因子某一段时期的截面标准差是1另一段时期的截面标准差是10。这种情况在A股很常见尤其是市场剧烈波动时期。如果你每期都用当期的均值和标准差做Z-score那因子数值在不同时期之间就不能直接比较了历史回测的因子值分布也会忽宽忽窄给后续截面上统计指标的计算带来信号失真。一种处理思路是滚动计算用过去一段时间比如60个交易日的均值和标准差作为标准化参数而不是只依赖当期截面。这个方法能保留因子值的纵向可比性同时对参数更新频率和窗口长度要做稳健性测试。另一种思路是所有标准化参数都用训练集的统计量固定下来避免了跨度期间信号分布尺度的漂移。就我自己的习惯纯截面排序类因子用前者真正进入机器学习模型的特征才考虑固定参数的方式。5.2 问题二要不要分行业、分市值分组处理在做剔极值的时候很多人会纠结要不要在行业内部做。比如银行股市盈率普遍在个位数科技股市盈率动辄五六十倍。如果你在全市场口径下做分位数缩尾会把行业间本身的估值风格差异当成异常值削掉导致银行股和科技股的区别被过度抹平。我的处理原则是优先做全市场的去极值和标准化先把明确异常的数据清理掉然后在做因子有效性分析和中性化的时候再去剥离行业和市值的影响。这两件事的职责不同混在一起处理反而会把结构性差异和异常噪声搅成一锅粥。当然如果你想验证某因子在细分领域内部的选股能力分行业单独处理不是不行但回测时要注意分行业处理往往意味着行业内排序组合构建逻辑和全市场排序会有本质差别千万不要回测时一套、实盘时另一套。5.3 问题三去极值到底该用“剔除”还是“缩尾”很多初学者会把超出阈值的样本直接删掉这个动作在截面数据分析里极度危险。删掉一个样本等于你在横截面上人为留下一个空洞后续排序、分位统计、回归都会因为这个洞产生系统性偏差。尤其是小市值股票样本量本来就不多的时候删掉几个极端值等于把某些风格特征的股票整体排除在外样本选择性偏差会很严重。所以截面数据的极值处理几乎只用缩尾不删除样本。把极端值压缩到边界上既消除了它对数理统计的干扰又保住了样本的完整性。5.4 问题四标准化之后数据出现NaN或无穷值这个问题出现的原因主要是某个截面内股票样本量太少或者所有样本的因子值都完全相同导致标准差为0标准化时出现除零错误某个因子的去极值下界算出来还是负值在业务逻辑上不合理比如换手率不可能为负但因为前期数据质量问题出现了。日常处理时我会分两层排查先检查原始因子缺失情况和常量截面如果整个截面因子都一样直接放弃该期因子值不要硬算再检查去极值之后的上下界是否在业务合理范围内不合理则修正数据源。这个坎过不去的因子不管后面模型收益看着多漂亮都不值得信任。写在最后的一点体会数据去极值和标准化处理在整个量化研究链路里确实不起眼但它属于那种“做了不显功、不做必闯祸”的活儿。我自己踩过的最大一个坑是早期做因子合成时偷懒把去极值参数设得很宽结果因子值被三个极端股票牢牢把持分层回测出来的收益曲线底下藏的全是这几只股票的独角戏等换了个市场环境立刻崩塌。后来老老实实把每一步处理逻辑参数拆分出来做敏感性测试才彻底稳住了。所以我的建议很简单第一去极值和标准化的默认管线一定要固定下来最好形成一个标准函数库每次建模都走同一套管线不要今天用3σ明天用分位数第二任何参数都要做敏感性分析回测时顺便跑几个上下浮动版本看看结果是不是依赖特定阈值第三处理完的数据一定要存一份副本方便后续审计问题排查某个奇特结果时你就能顺着数据管线逐层溯源。数据干净了模型才有资格谈好坏。这一点做研究做得越久体会越深。