不可能正确的数值
大盘股单日涨 30%,要么是被收购,要么是数据错误,比如漏了拆股或小数点错位。异常值与其他值相距足够远,需要做出决定:保留、修正或剔除。千万不要仅仅因为碍事就删掉它,因为真实的冲击恰恰是风险模型必须看到的。第一步始终是用明确且可重复的规则让它们显现出来。
Z 分数规则及其可能失效的原因
经典规则会标出偏离均值超过 2 或 3 个标准差的值:z = (x - mean) / std。它简单,在干净数据上有效。但异常值自己会抬高均值,尤其是标准差,于是从规则里躲了起来。下面的例子中,涨 30% 那天的 Z 分数只有 2.85,阈值取 3 就不会被标出。错误越极端,保护自己的能力越强。
import pandas as pd
returns = pd.Series([0.004, -0.002, 0.001, 0.003, -0.004, 0.002, 0.30, -0.001, 0.0, 0.002])
z = (returns - returns.mean()) / returns.std()
print(z.round(2).tolist())
# [-0.28, -0.34, -0.31, -0.29, -0.36, -0.3, 2.85, -0.33, -0.32, -0.3]
print(returns.mean().round(4), returns.std().round(4)) # 0.0305 0.0947 one day moved the mean tenfold
print(returns.drop(6).std().round(4)) # 0.0026 the std without that day稳健规则:IQR 与 MAD
稳健统计量几乎不受它们要找的异常值影响。四分位距 IQR = Q3 - Q1 衡量数据中间一半的离散程度,超出 Q1 - 1.5 * IQR 或 Q3 + 1.5 * IQR 的值就落在箱线图的边界之外。MAD(绝对中位差)用中位数取代标准差,修正 Z 分数 0.6745 * (x - median) / MAD 的解读方式与普通 Z 分数相同(3.5 是常用的阈值)。两者都能轻松标出涨 30% 的那天。
# The fences of a box plot
q1, q3 = returns.quantile(0.25), returns.quantile(0.75)
spread = q3 - q1
lower, upper = q1 - 1.5 * spread, q3 + 1.5 * spread
print(round(lower, 4), round(upper, 4)) # -0.006 0.008
print(returns[(returns < lower) | (returns > upper)].to_dict()) # {6: 0.3}
# The modified z-score, built on the median
median = returns.median()
mad = (returns - median).abs().median()
modified_z = 0.6745 * (returns - median) / mad
print(modified_z.round(1).tolist()) # [0.8, -1.2, -0.2, 0.5, -1.9, 0.2, 100.7, -0.8, -0.5, 0.2]缩尾处理:截断而不是删除
删除一行也会删掉其他列的信息,并打乱日期间隔。缩尾处理保留所有行,把极端值拉回到选定的分位数,例如 1% 和 99%:低于 1% 分位数的变成该分位数,高于 99% 分位数的变成该分位数。在 pandas 中就是用 quantile 得到上下限再 clip(lower, upper)。序列的形态得以保留,极端值对均值和回归的拉扯也随之减弱。
low, high = returns.quantile(0.1), returns.quantile(0.9)
print(round(low, 4), round(high, 4)) # -0.0022 0.0336
clipped = returns.clip(lower=low, upper=high)
print(clipped.round(4).tolist())
# [0.004, -0.002, 0.001, 0.003, -0.0022, 0.002, 0.0336, -0.001, 0.0, 0.002]
# ^ raised ^ the 30% day pulled back to the 90% quantile# Flag with a rule, then look at what is flagged before deciding anything
mask = z.abs() > 2
print(returns[mask].to_dict()) # {6: 0.3} the +30% day
print(returns[~mask].std().round(4)) # 0.0026 the volatility of the remaining days用规则做决定,并写下来异常值规则是建模选择,而不是事实。在改动任何数据之前,先查清被标出的日子是什么(新闻、拆股还是错误报价),保留原始列,并记录所用阈值。阈值不同,回测结果也不同。
- Z 分数规则
abs((x - mean) / std) > k很简单,但异常值会抬高均值与标准差而自我隐藏。 - 稳健规则使用分位数和中位数:IQR 边界(
Q1 - 1.5 IQR、Q3 + 1.5 IQR)与基于 MAD 的修正 Z 分数。 - 用分位数上下限做
clip(lower, upper)(缩尾)会保留行;没弄清冲击是什么之前绝不删除它。
下一节:公司行为,让旧价格与今天的价格无法直接比较的拆股和分红。
实验:评分内容
- 用
zscore_outliers标出偏离很远的收益率25 分 - 用
iqr_bounds计算箱线图的边界25 分 - 用
mad_outliers以稳健得分标出异常值25 分 - 用
winsorize压制极端值25 分