正しいはずのない値
大型株の日次リターンが +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%点より下の値は1%点に、99%点より上の値は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点