你跑完一个多因子策略,夏普 2.3,美滋滋。第二天有人告诉你:上周三那只股票有个乌龙指,成交价瞬间掉了 22%,你的收益序列里有一个 -22% 的日收益。
你心里咯噔一下——把那个点删掉再算,夏普 变成 1.4。
问题不在于某个数据点”坏”。问题在于:你的整套统计地基(均值、方差、协方差、相关)都是脆的。它们对每一个极端值都敏感,一个离群点就能把结论整个掀翻。而金融市场恰恰是离群值的老家:闪崩、流动性枯竭、数据录入错误、除权除息没复权、停牌后补跌……
本文告诉你一套不删数据、却不怕离群值的工具箱——稳健统计(robust statistics),核心就两样:用中位数替代均值、用 MAD 替代标准差,并把它推广到相关与协方差。所有结论都用真实合成数据跑出来,不吹不黑。
一、为什么均值-方差是”纸糊的地基”#
经典统计量有一个温柔的假设:数据来自一个光滑的钟形分布,极端值概率趋近于零。 在这个假设下,样本均值 和样本标准差 是”最优”估计。
但一旦混进哪怕一个离群值,它们就崩了。我们用一个最小例子直观感受:
import numpy as np
rng = np.random.default_rng(42)
base = rng.normal(0.0006, 0.012, 500) # 250 个交易日的"正常"日收益
# 注入 5 个单向闪崩(踩雷 / 数据错误),全部为负
out_idx = rng.choice(500, 5, replace=False)
shocks = -rng.uniform(0.15, 0.25, 5)
r = base.copy()
r[out_idx] += shocks
mean_cls = r.mean() # 样本均值
std_cls = r.std(ddof=1) # 样本标准差
med = np.median(r) # 中位数
mad = np.median(np.abs(r - med))
std_rob = 1.4826 * mad # 稳健波动率(见第三节)
print(f"样本均值 = {mean_cls:.6f}")
print(f"中位数 = {med:.6f}")
print(f"样本标准差 = {std_cls:.6f} (真实基底波动率是 0.012)")
print(f"稳健波动率 = {std_rob:.6f}")python跑出来结果是:
样本均值 = -0.001802
中位数 = 0.000468
样本标准差 = 0.024905
稳健波动率 = 0.011450plaintext注意三件事:
- 均值被拉偏了:样本均值 −0.0018,中位数 +0.0005,差了 0.0023——五个负向闪崩把”平均日收益”往下拽了将近一个标准差的量级,策略看起来像在亏钱,其实中枢是正的。
- 标准差被撑大了一倍多:样本标准差 0.0249,是真实基底波动率 0.012 的 2.08 倍。一个离群点让”风险”凭空翻倍,任何以此计算的仓位(凯利、风险平价)都会严重欠仓。
- 稳健波动率几乎命中:MAD 算出的 0.01145,正好是真实 0.012 的 0.95 倍。

图 1:红色点是注入的闪崩。均值(红虚线)被拉到深色区,中位数(绿虚线)几乎不动。
二、稳健性的数学定义:崩溃点#
严谨地说,一个估计量的**崩溃点(breakdown point)**是:要让这个估计量跑到无穷远,至少需要污染多少比例的数据。
- 样本均值:崩溃点 = 。只要一个点取到 ,均值就跑到 。完全不稳健。
- 样本中位数:崩溃点 = 。你即使把一半的数据改成任意值,中位数最多滑到”剩下那一半”的边界,不会爆炸。
这就是为什么中位数天然比均值稳。标准差同理:它建立在均值和平方之上,崩溃点也是 ;而 MAD 建立在中位数和绝对值之上,崩溃点约 。
直觉:平方(方差)给大偏差指数级权重,绝对值(MAD)只给线性权重。 离群值的”大”被平方放大、被绝对值压缩——这就是全部差别。
三、MAD:中位数的标准差#
中位数解决”中心”,但”离散程度”怎么办?答案是 MAD(Median Absolute Deviation,中位数绝对偏差):
然后乘一个常数把它校准到正态分布下的标准差:
那个 1.4826 怎么来的?在标准正态分布下 ,所以 。只有当你假设数据近似正态时才乘;若数据有厚尾,这步只是”把尺度对齐到标准差的语义”,不改变稳健性。
完整实现:
def robust_scale(x):
"""返回稳健位置(中位数)与稳健尺度(MAD 校准波动率)。"""
med = np.median(x)
mad = np.median(np.abs(x - med))
return med, 1.4826 * mad
# 滚动窗口:每天用过去 60 日重算,看离群值冲击如何随时间衰减
W = 60
roll_std = np.full(len(r), np.nan)
roll_rob = np.full(len(r), np.nan)
for t in range(W, len(r)):
seg = r[t - W:t]
roll_std[t] = seg.std(ddof=1)
m2 = np.median(seg)
roll_rob[t] = 1.4826 * np.median(np.abs(seg - m2))
print(f"滚动经典标准差峰值 = {np.nanmax(roll_std):.5f}")
print(f"滚动稳健波动率峰值 = {np.nanmax(roll_rob):.5f}")python输出:
滚动经典标准差峰值 = 0.04363
滚动稳健波动率峰值 = 0.01411plaintext
图 2:经典滚动标准差(红)在闪崩窗口飙到 0.044,而稳健波动率(绿)几乎贴着真实的 0.012 基线。用经典估计做风险预算,会在闪崩后误以为波动翻倍、疯狂降仓;稳健估计告诉你”那只是个数据点”。
四、稳健相关:离群值如何”假阴性”地毁掉你的因子#
均值方差之外,相关/协方差是最常被离群值坑的地方。皮尔逊相关的崩溃点也是 :一个 fat-finger 离群点能把相关从 0.6 打没。
我们造两个真实相关约 0.60 的资产,再注入 6 个独立极端离群点:
rng2 = np.random.default_rng(7)
n = 200
a = rng2.normal(0, 1, n)
b = 0.6 * a + rng2.normal(0, 0.8, n) # 真实皮尔逊 ≈ 0.60
oi = rng2.choice(n, 6, replace=False)
a[oi] += rng2.choice([-1, 1], 6) * rng2.uniform(7, 9, 6) # 极端录入错误
b[oi] += rng2.choice([-1, 1], 6) * rng2.uniform(7, 9, 6)
pear = np.corrcoef(a, b)[0, 1]
spear = np.corrcoef(np.argsort(a), np.argsort(b))[0, 1]
def winsorize(x, k=2.5):
med = np.median(x)
sigma = 1.4826 * np.median(np.abs(x - med))
return np.clip(x, med - k * sigma, med + k * sigma)
aw, bw = winsorize(a), winsorize(b)
pear_w = np.corrcoef(aw, bw)[0, 1]
print(f"皮尔逊(经典) = {pear:.3f}")
print(f"Spearman 秩相关 = {spear:.3f}")
print(f"Winsorized 皮尔逊 = {pear_w:.3f}")python结果:
皮尔逊(经典) = 0.238
Spearman 秩相关 = -0.081
Winsorized 皮尔逊 = 0.465plaintext
图 3:6 个红色离群点(数据错误)把皮尔逊从真实 0.60 砸到 0.24,甚至 Spearman 都翻成 −0.08。Winsorized(按 MAD 裁剪边际)后回到 0.47,方向对了、量级接近了。
两个稳健替代:
| 方法 | 思路 | 适用 |
|---|---|---|
| Spearman 秩相关 | 先把数据转成排名,再算皮尔逊 | 只关心”单调性”,对量级不敏感 |
| Winsorized 皮尔逊 | 用 MAD 尺度把边际裁剪到中位数 ±kσ,再算皮尔逊 | 想保留”线性相关”语义、只去掉极端尾巴 |
推到协方差矩阵:对每个资产先用 Winsorize 裁剪边际,再算样本协方差,得到稳健协方差 。用在马科维茨优化里,能避免离群值把组合权重甩到莫名其妙的角落。
五、落地:一个稳健风险预算片段#
把上面三样拼起来,就是你日常因子的稳健版”风险体检”:
import numpy as np
def robust_cov(returns: np.ndarray, k: float = 2.5) -> np.ndarray:
"""returns: (T, N) 资产收益矩阵,返回稳健协方差。"""
T, N = returns.shape
wins = np.empty_like(returns)
for j in range(N):
x = returns[:, j]
med = np.median(x)
sigma = 1.4826 * np.median(np.abs(x - med))
wins[:, j] = np.clip(x, med - k * sigma, med + k * sigma)
return np.cov(wins, rowvar=False, ddof=1)
def robust_vol(returns: np.ndarray) -> np.ndarray:
"""每列资产的稳健波动率。"""
return np.array([1.4826 * np.median(np.abs(c - np.median(c)))
for c in returns.T])
# 用法:
# vols = robust_vol(factor_returns) # 稳健风险
# Sigma = robust_cov(factor_returns) # 稳健协方差(喂风险平价)python六、五类真实陷阱#
- MAD 常数别乱用:1.4826 只在”假设近似正态”时把尺度对齐到 σ。厚尾数据(加密货币、小盘股)你仍该用 MAD,但别把它当”真实 σ”——它就是个稳健尺度,语义不同。
- Winsorize 的 k 是旋钮也是偏见:k=2.5 砍掉约 ±4σ 以外的点。k 太小会吃掉真实的大波动(把危机当错误);k 太大又退化成经典。建议对高频数据用 k=3、日频用 k=2.5,并做敏感性检查。
- 稳健≠无视离群:MAD 不报错,但那个 −22% 可能是真踩雷也可能是真信号。稳健统计的目的是”别让一个点到处污染”,而不是”替你做异常检测”。离群点要单独进异常检测流程,而不是默默吸收。
- 小样本 MAD 偏低:T<20 时 MAD 估计噪声很大,1.4826 校准会系统性偏低。短窗口滚动要用更厚的样本或 shrinkage 混合经典估计。
- 相关结构会被裁剪改变方向:Winsorize 是逐边际裁剪,不保证裁剪后相关仍非负定。若直接喂优化器,建议对稳健协方差再做一次 eigen-shrinkage(把负特征值夹到 0)保证半正定。
七、结论#
均值-方差模型不是错,是脆。在”数据干净”的世界里它最优,在”离群值家常便饭”的金融市场里它会悄无声息地把你的风险、相关、仓位全部算偏。
本文用真实合成数据给出三个可记的数字:
- 一个离群点把波动率从 0.012 撑到 0.0249(2.08×),MAD 只到 0.0115(0.95×);
- 滚动峰值经典 0.044 vs 稳健 0.014;
- 离群点把相关从真实 0.60 砸到 0.24,稳健处理回到 0.47。
换成中位数 + MAD,你不是”丢掉了信息”,而是让那一个极端点不再替所有点做决定。这是量化里性价比最高的一次”地基加固”。
本文数据均为自洽合成(正态基底 + 注入离群),仅用于演示方法。真实落地时:收益用复权价计算、协方差估计叠加 Ledoit-Wolf shrinkage、离群点先过一遍异常检测再决定裁剪还是保留。代码已全部跑通,数字即输出。