K_DM Book
用語集
対象者 標準
対象者別の入口 →

対象者の設定はこのブラウザの中にだけ保存されます。

言語 JA

Zスコアで異常値を検出

入門

04.4.2

Zスコアで異常値を検出

最終更新 2020-03-31 読了時間 2 分
まとめ
Z スコアは各観測値が平均から標準偏差いくつ分はなれているかを表す数で、\(z = (x - \mu)/\sigma\) で計算します。このページでは、水準 100 に周期 20 日・振幅 10 の波と標準偏差 2 のノイズを足した 180 日の系列に、60 日目に +25、140 日目に −20 の異常を仕込み、(values - values.mean()) / values.std() で Z スコアを求めて zscore.abs() > 2.5 の点だけを ax.scatter で赤く重ねます。読み終えると、閾値 2.5 を 2 や 3 に変えると赤い点が何個に変わるか、季節性の強い系列でなぜ先に季節調整が必要かを説明できるようになります。

平均と標準偏差から Z スコアを計算し、\(|z| > 2.5\) の観測点だけを赤くすると、180 日分の折れ線から「ふだんの波」ではない 2 日が浮かび上がります。

直感 #

毎日の歩数が 8,000〜12,000 の人の 25,000 歩は異常ですが、毎日 20,000 歩歩く人の 25,000 歩はふだんの範囲です。「どれだけ離れたら異常か」は、そのデータ自身の散らばりを単位にして測るしかありません。Z スコアは平均を 0、標準偏差を 1 とする物差しに値を置き直したもので、\(|z| = 2.5\) は「標準偏差 2.5 個分」のはなれ方を意味します。正規分布なら \(|z| > 2.5\) になる確率は約 1.2 % なので、180 日のうち偶然そこまで外れる日は 2 日程度、それより多く赤い点が出れば仕込みかデータの問題を疑います。図でいえば、平均の高さに中心線を引き、上下に \(2.5\sigma\) の帯をかぶせて、帯から飛び出した点だけを赤く塗る操作です。

何が起きているか(ステップで見る) #

コードを読む前に、この手法が図の上で何をしているかを追ってみましょう。「再生」を押すか、スライダーを動かすと 1 手順ずつ進みます。

見るポイント:帯の幅を σ 何個分にするかで外れ値の数が変わる。3σ の外はかなり珍しい値。

Python で描く #

次のコードは、周期 20 日の波とノイズを持つ 180 日の系列に 2 点の異常を仕込み、Z スコアの絶対値が 2.5 を超えた日を赤い点で折れ線に重ねます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

rng = np.random.default_rng(17)
dates = pd.date_range("2022-01-01", periods=180, freq="D")
series = 100 + np.sin(2 * np.pi * np.arange(len(dates)) / 20) * 10 + rng.normal(0, 2, len(dates))
series[60] += 25
series[140] -= 20
values = pd.Series(series, index=dates)

zscore = (values - values.mean()) / values.std()
anomalies = zscore.abs() > 2.5

fig, ax = plt.subplots(figsize=(6.5, 3.5))
ax.plot(values.index, values, color="#2563eb", label="系列")
ax.scatter(values.index[anomalies], values[anomalies], color="#ef4444", s=60, label="異常値")
ax.set_title("Zスコアによる異常検知")
ax.set_xlabel("日付")
ax.set_ylabel("値")
ax.legend()
ax.grid(alpha=0.3)

fig.tight_layout()
fig.savefig("static/images/timeseries/anomaly_zscore.svg")

Zスコアで異常値を検出の図

実行結果の読み方 #

青い線は 100 を中心に 90〜110 の間で 20 日周期の波を描き、60 日目付近で 125 近くまで跳ね、140 日目付近で 80 近くまで落ちます。赤い点はちょうどその 2 日だけに付き、波の山や谷には付きません。これは系列の標準偏差が約 7.3 で、閾値 2.5σ ≈ 18 の帯が波の振幅 10 を十分に包み込んでいるためです。anomalies.sum() を表示すると 2 が返り、zscore[anomalies] を見ると +3.4 と −2.7 前後の値が確認できます。実データで赤い点が波の山ごとに並ぶなら、閾値が低すぎるか、季節性を先に取り除く必要があるサインです。

使いどころと落とし穴 #

Z スコアによる検出は、センサーの値や日次 KPI に「ふだんと違う日」のアラートを付ける最初の一手として使います。閾値は業務で決めるもので、見逃しが困るなら 2.0、誤報が困るなら 3.0 に寄せます。落とし穴は 2 つあります。季節性が強い系列では波のぶんだけ標準偏差がふくらんで小さな異常が帯に隠れるので、seasonal_decompose の残差や values - values.rolling(20, center=True).mean() に対して Z スコアを計算すると誤検知も見逃しも減ります。異常が多いデータでは平均と標準偏差そのものが引っ張られるので、中央値と四分位範囲を使う修正 Z スコア(0.6745 * (x - median) / MAD)や Isolation Forest のほうが安定します。