【维克】一个极端值,毁掉了整个因子

那是我刚学会造因子的时候。

第77篇讲完数据存储之后,我花了一个月,疯狂造因子。

动量因子、波动率因子、流动性因子、估值因子......

一口气造了三十多个。每个都跑了IC分析,看起来都挺显著。

我信心爆棚。

心想:这次模型总该起飞了吧?

结果------

夏普0.5。

还不如之前PCA压缩成5个主成分时的1.4。

我懵了。

"不可能啊。每个因子的IC都显著,怎么合在一起就不行了?"

导师看了一眼我的因子分布。

"你做过箱线图没有?"

"什么箱线图?"

"就是看每个因子的数据分布。你把那个'换手率偏离度'因子的分布画出来给我看看。"

我画了。

屏幕上出现了一个奇怪的分布------

大部分数据集中在-2到2之间。

但是------

有一个值,是487。

"看到了吗?"

"看到了。这是一个极端值。"

"你知道这个极端值对你因子的影响有多大吗?"

我摇头。

他打开Excel,算了一下:

"你这个因子的均值是0.3。去掉这一个极端值,均值变成0.01。"

"......差了30倍?"

"对。你整个因子的'有效性',被这一个值撑起来了。模型看到的'显著IC',不是因为它真的能预测------是因为这一个异常值碰巧和收益率高度相关。"

"那去掉这个值呢?"

"去掉之后,你这个因子的IC直接从0.08掉到0.003。完全不显著。"

我盯着那个487看了很久。

后来查出来------是数据源的问题。某天的成交量数据少了一个零,导致换手率计算出来是正常值的十倍。

一个数据错误。一个极端值。

让我以为发现了一个好因子,实际上------

整个因子都是假的。

WHY:为什么一个极端值能毁掉一切?

一个生活中的比喻

想象你在统计一个小区住户的收入水平。

小区有100户人家。

大部分家庭年收入在10万到50万之间。

但是------

有一户,是做直播的,年收入2个亿。

你算一下平均值:

100户平均年收入 = (99户总和 + 2亿) / 100 ≈ 250万

250万?

你觉得这个小区是"富人区"吗?

不是。99户人家 average 才30万出头。

是那一个2亿把整个平均值拉高了20倍。

这就是极端值的威力。

它不需要多。只需要一个。就能把你所有统计量------均值、方差、相关系数、回归系数------全部带偏。

量化因子为什么特别容易受影响?

金融数据天然有"肥尾"特征------极端值出现的概率远高于正态分布的预期。

一只股票某天成交量突然放大100倍------停牌复牌、重大公告、乌龙指------都有可能。

一只股票的日收益率突然涨停或跌停------A股10%的限制还算温和的,港股、美股一天能跌50%。

一个财务数据出现录入错误------小数点点错一位,PE从15变成150。

这些极端值------

如果不处理,它们会像病毒一样感染你的因子计算、模型训练、回测结果。

你算均值------被带偏。

你算标准差------被放大。

你算相关系数------被一个点拉高或拉低。

你训练模型------模型拼命去拟合那个异常点,忽略了99%的正常数据。

一个极端值,毁掉一个因子。

一个因子,毁掉一个模型。

一个模型,毁掉你的实盘。

WHAT:三种主流去极值方法

方法一:3σ法------最简单,也最粗糙

核心思想:

假设数据服从正态分布。超过均值±3个标准差的数据,视为异常值,做截断处理。

// python
import numpy as np

def winsorize_3sigma(series):
mean = series.mean()
std = series.std()
upper = mean + 3 * std
lower = mean - 3 * std
return series.clip(lower, upper)

使用

df'factor_clean' = winsorize_3sigma(df'factor_raw')

优点: 简单,计算快。

缺点: 金融数据不服从正态分布。金融数据的尾部远厚于正态分布。用3σ法------要么处理得太少(因为标准差本身就被极端值放大了),要么处理得太多。

而且------均值和标准差本身就会被极端值影响。

这就像------你用一个被污染的尺子去测量污染程度。

尺子本身就不准。

方法二:MAD法------稳健之王

MAD = Median Absolute Deviation(中位数绝对偏差)。

核心思想:

用中位数代替均值------中位数不受极端值影响。

用绝对偏差的中位数代替标准差------同样不受极端值影响。

// python
def winsorize_mad(series, n=5):
median = series.median()

计算每个值到中位数的绝对偏差

mad = (series - median).abs().median()

MAD换算成标准差的等价量

mad_e = 1.4826 * mad
upper = median + n * mad_e
lower = median - n * mad_e
return series.clip(lower, upper)

使用

df'factor_clean' = winsorize_mad(df'factor_raw', n=5)

为什么乘1.4826?

因为对于正态分布,MAD ≈ 0.6745 × 标准差。所以 1/0.6745 ≈ 1.4826,用来把MAD换算成和标准差可比较的尺度。

优点:

• 中位数天然抗极端值------哪怕数据里有一个值是487,中位数完全不受影响

• 比3σ法稳健得多------金融数据肥尾也没关系

• 业界主流方法------大多数量化私募用这个

缺点:

• 计算比3σ法略慢(但差异可忽略)

• 阈值n的选择有一定主观性------通常取3到5

方法三:百分位法------最直觉

核心思想:

直接看数据的分布。把最极端的1%(或5%)截断到百分位数上。

// python
def winsorize_percentile(series, lower_pct=0.01, upper_pct=0.99):
lower = series.quantile(lower_pct)
upper = series.quantile(upper_pct)
return series.clip(lower, upper)

使用

df'factor_clean' = winsorize_percentile(df'factor_raw', lower_pct=0.01, upper_pct=0.99)

优点:

• 直觉清晰------"去掉最极端的1%"

• 不依赖任何分布假设

• 计算简单

缺点:

• 截断比例是固定的------不管数据分布什么样,都切1%

• 可能切掉有用信息------如果数据确实有合理的厚尾

• 也可能切不干净------如果极端值太多

HOW:完整的去极值流水线

把三种方法串起来,做一个完整的因子去极值+标准化流水线:

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

============ 第一步:加载因子数据 ============

假设有30个因子的原始数据

df = pd.read_parquet("raw_factors.parquet")

factor_cols = col for col in df.columns if col not in \['date', 'code']

============ 第二步:去极值(MAD法) ============

def winsorize_mad(series, n=5):
median = series.median()
mad = (series - median).abs().median()
mad_e = 1.4826 * mad
upper = median + n * mad_e
lower = median - n * mad_e
return series.clip(lower, upper)

对每个因子、每个截面(每天)做去极值

def cross_sectional_winsorize(df, factor_cols, date_col='date', method='mad', n=5):
df_clean = df.copy()

for col in factor_cols:
if method == 'mad':
df_cleancol = df.groupby(date_col)col.transform(
lambda x: winsorize_mad(x, n=n)
)
elif method == '3sigma':
df_cleancol = df.groupby(date_col)col.transform(
lambda x: x.clip(x.mean() - n*x.std(), x.mean() + n*x.std())
)
elif method == 'percentile':
df_cleancol = df.groupby(date_col)col.transform(
lambda x: x.clip(x.quantile(0.01), x.quantile(0.99))
)

return df_clean

去极值

df_clean = cross_sectional_winsorize(df, factor_cols, method='mad', n=5)

============ 第三步:标准化 ============

def cross_sectional_standardize(df, factor_cols, date_col='date'):
df_std = df.copy()
for col in factor_cols:
df_stdcol = df.groupby(date_col)col.transform(
lambda x: (x - x.mean()) / x.std()
)
return df_std

标准化

df_std = cross_sectional_standardize(df_clean, factor_cols)

============ 第四步:对比检查 ============

对比去极值前后的分布

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

原始分布

axes0.hist(df'换手率偏离度'.dropna(), bins=100, color='red', alpha=0.7)
axes0.set_title('去极值前:换手率偏离度分布')
axes0.set_xlabel('因子值')
axes0.axvline(df'换手率偏离度'.mean(), color='black', linestyle='--', label=f'均值={df"换手率偏离度".mean():.2f}')
axes0.legend()

处理后分布

axes1.hist(df_clean'换手率偏离度'.dropna(), bins=100, color='green', alpha=0.7)
axes1.set_title('去极值后:换手率偏离度分布')
axes1.set_xlabel('因子值')
axes1.axvline(df_clean'换手率偏离度'.mean(), color='black', linestyle='--', label=f'均值={df_clean"换手率偏离度".mean():.2f}')
axes1.legend()

plt.tight_layout()
plt.savefig("winsorize_comparison.png", dpi=150)
plt.show()

============ 第五步:检查IC变化 ============

def calc_ic(df, factor_col, return_col='next_return', date_col='date'):
"""计算因子的时间序列IC"""
ic_series = df.groupby(date_col).apply(
lambda x: xfactor_col.corr(xreturn_col)
)
return ic_series

去极值前的IC

ic_before = calc_ic(df, '换手率偏离度')
print(f"去极值前 IC均值: {ic_before.mean():.4f}, IC_IR: {ic_before.mean()/ic_before.std():.4f}")

去极值后的IC

ic_after = calc_ic(df_clean, '换手率偏离度')
print(f"去极值后 IC均值: {ic_after.mean():.4f}, IC_IR: {ic_after.mean()/ic_after.std():.4f}")

逐行讲解

第一步:加载因子数据。 把你的原始因子数据读进来。

第二步:去极值。 对每个因子、每个截面(每天)独立做MAD去极值。注意------一定要在截面上做,不是在整个时间序列上做。因为每天的因子分布不同,需要用当天数据的中位数和MAD。

第三步:标准化。 去极值之后,做截面标准化。均值=0,标准差=1。这样不同因子之间可以直接比较和合成。

第四步:对比检查。 画图看分布变化。去极值前------可能有一个长长的尾巴,尾巴末端就是极端值。去极值后------尾巴被截断,分布变得紧凑对称。

第五步:检查IC变化。 这是最关键的一步。去极值之后,因子的IC可能变低(因为之前被极端值"虚撑"的显著性没了),也可能变高(因为噪声被过滤,真实信号更清晰了)。

通常------如果去极值后IC下降了很多,说明原来的因子"显著"是假的,是被极端值带出来的。如果IC基本不变或上升,说明因子确实有预测力。

实战经验:什么时候用什么方法

MAD法------日常标配

90%的场景,用MAD就够了。

• 截面去极值,n取5(保守)或3(激进)

• 行业惯例,大多数量化私募的因子处理流水线都用这个

• 稳健、快速、不需要调参

3σ法------数据质量好的时候用

如果你的数据已经很干净(比如经过数据源清洗、去重、校验),极端值很少------用3σ也差不多。

但在A股数据里,3σ法通常不如MAD。因为金融数据肥尾严重,3σ的"标准差"本身就被极端值撑大了,反而处理不干净。

百分位法------快速验证的时候用

想快速看一眼"去掉最极端的几个点之后,因子还显不显著"------百分位法最直觉。

但它不如MAD精确------MAD是根据数据本身的离散程度动态确定阈值的,而百分位法是固定切1%。

踩坑清单

坑一:在时间序列上做去极值

去极值必须在截面 上做------每天独立处理。

如果你在整个时间序列上算均值和标准差------不同日期之间的差异(比如牛市和熊市成交量差异巨大)会干扰去极值效果。

// python

错误做法

mean = df'factor'.mean() # 所有日期的均值
std = df'factor'.std()
df'factor_clean' = df'factor'.clip(mean - 3*std, mean + 3*std)

正确做法

df'factor_clean' = df.groupby('date')'factor'.transform(
lambda x: x.clip(x.median() - 5*1.4826*(x-x.median()).abs().median(),
x.median() + 5*1.4826*(x-x.median()).abs().median())
)

坑二:去极值之后不做标准化

去极值只是把极端值截断。截断之后,因子的量纲可能还是不一致。

比如一个因子范围是0到1,另一个是0到100。去极值之后还是这样。

必须再做标准化,让所有因子在同一尺度上。

坑三:去极值太激进

n取3------意味着你会截掉很多数据。如果因子本身就有合理的厚尾分布(比如小盘股的换手率),过于激进的截断会把有用信息也切掉。

建议先用n=5保守处理,看IC变化。如果IC变化不大,可以逐步收紧到n=3。

坑四:只看去极值后的IC提升

去极值的目的是"还原因子的真实表现",不是"提升IC"。

如果去极值后IC下降了------恭喜你,你发现了一个"假因子"。这是好事,不是坏事。

因为至少你不会拿着一个假因子上实盘。

坑五:忘了检查数据源

去极值只能处理"已经出现在数据里的极端值"。

但有些极端值的根源------在数据源。

比如:

• 复权方式不对(前复权 vs 后复权 vs 不复权)

• 停牌数据没处理好

• 财务数据的单位不统一(万元 vs 元)

• 数据源的时间戳错误

去极值是最后一道防线。但最好的防线------是从数据源头就保证质量。

回到故事

那天之后,我把30个因子全部做了MAD去极值。

结果------

有5个因子,去极值后IC直接归零。

它们就是被极端值"虚撑"出来的假因子。

剩下25个因子里,有8个IC明显下降(极端值贡献了一部分"虚假显著性"),17个IC基本不变甚至上升。

最终------我用这17个因子重新建模。

夏普从0.5------涨到1.6。

不是因为用了更复杂的模型。

是因为------去掉了5个假因子,纠正了8个被污染的因子。

导师后来说:

"量化这行,最贵的不是模型。是数据。"

"而数据最贵的部分------不是获取,是清洗。"

"你花80%的时间在数据清洗上,不丢人。那20%才花在策略上------这才是正确的比例。"

总结

这一篇讲了去极值处理。

核心要点:

  1. 为什么需要 :一个极端值就能毁掉整个因子------均值、方差、IC全部被带偏

  2. 三种方法 :3σ法(简单但粗糙)、MAD法(稳健之王,业界标配)、百分位法(直觉但粗糙)

  3. 怎么做 :截面去极值 → 截面标准化 → 检查IC变化

  4. 常见坑 :不在时间序列上做、不去标准化、太激进、误解IC变化、忽略数据源

去极值是数据清洗的关键一步。但清洗不止于此------接下来还要处理缺失值、行业中性化、因子正交化......

下一篇,我们聊聊缺失值处理------你的因子数据里,那些"空值"到底该怎么填?直接扔掉?用均值填充?还是用更聪明的方法?

相关推荐
小羊没烦恼!1 小时前
Office文件的奥秘——.NET平台下不借助Office实现Word、Powerpoint等文件的解析(完)
java·大数据·前端·网络·word·powerpoint·.net
找方案1 小时前
AI+电商:AI导购和AIGC商品图如何改变在线购物
人工智能·aigc
m0_547486661 小时前
《大数据技术与应用:Hadoop和PySpark实现》全套PPT课件2026
大数据·hadoop
代码方舟1 小时前
零信任架构实战:基于天远企业四要素验证构建自动化B2B供应链金融网关
人工智能·金融·架构·自动化
船厂电气自动化ai大模型1 小时前
AI大模型与数学 第75课 基、维度、向量空间坐标
数据结构·线性代数·算法·机器学习·推荐算法
博界IT精灵1 小时前
专题1:线性表
考研·算法
深圳雨林凯AI1 小时前
图案裂变的品类适配:画面密度、构图焦点与开孔避让的量化拆解
人工智能
海盗12341 小时前
AI 新闻日报 2026-09-11:SWE-2 逼近前沿、宇树开源具身基座、中国自动驾驶欧洲载客
人工智能·开源·自动驾驶
新时代牛马1 小时前
ATF / TEE / REE 与 EL 完整篇:TrustZone 边界→软件栈→ 异常级→ 切换路径(一条主线讲透)
python