Isolation Forest

isolation Forestを使って異常値を検出してみます。 このページでは時系列データの中にいくつか異常値を混ぜ、それが検出できるかどうか検証します。 また、最後に異常値と判定したルールを可視化します。

import japanize_matplotlib
import numpy as np
import pandas as pd
import seaborn as sns
from matplotlib import pyplot as plt
from sklearn.ensemble import IsolationForest
from sklearn.tree import plot_tree

人工データの作成

[11, 49, 149, 240, 300, 310]の日付にて異常値を混ぜておきます。 カテゴリ変数や整数の特徴も含まれるデータです。数値のみプロットしてみます。

rs = np.random.RandomState(365)
dates = pd.date_range("1 1 2016", periods=365, freq="D")

data = {}
data["月"] = [d.strftime("%m") for d in dates]
data["曜日"] = [d.strftime("%A") for d in dates]
data["特徴1"] = [np.sin(d.day / 50) + np.random.rand() for d in dates]
data["特徴2"] = [np.cos(d.day / 50) + np.random.rand() for d in dates]
data["特徴3"] = [3 * np.random.rand() + np.log(d.dayofyear) * 0.03 for d in dates]
data["特徴4"] = [np.random.choice(["☀", "☂", "☁"]) for d in dates]
column_names = list(data.keys())


anomaly_index = [11, 49, 149, 240, 300, 310]
anomaly_dates = [dates[i] for i in anomaly_index]
for i in anomaly_index:
    data["特徴1"][i] = 2.5
    data["特徴2"][i] = 0.3


data = pd.DataFrame(data, index=dates)

plt.figure(figsize=(10, 4))
sns.lineplot(data=data, palette="tab10", linewidth=2.5)
<Axes: >

png

data

曜日特徴1特徴2特徴3特徴4
2016-01-0101Friday0.4326781.6450271.118289
2016-01-0201Saturday0.0994631.6457340.289383
2016-01-0301Sunday0.2974901.6139582.499115
2016-01-0401Monday1.0430771.0299470.484240
2016-01-0501Tuesday0.1847971.3552652.795718
.....................
2016-12-2612Monday0.9142071.5803300.617349
2016-12-2712Tuesday1.2297531.5995992.605319
2016-12-2812Wednesday0.9361001.4083782.540507
2016-12-2912Thursday1.1565751.4286010.831889
2016-12-3012Friday1.3831851.5446291.416885

365 rows × 6 columns

カテゴリ変数の変換

「曜日」のような特徴をIsolation Forestで扱うためにダミー変数に変換します。

X = pd.get_dummies(data)
X

特徴1特徴2特徴3月_01月_02...曜日_Thursday曜日_Tuesday曜日_Wednesday特徴4_☀特徴4_☁
2016-01-010.4326781.6450271.1182891000000...
2016-01-020.0994631.6457340.2893831000000...
2016-01-030.2974901.6139582.4991151000000...
2016-01-041.0430771.0299470.4842401000000...
2016-01-050.1847971.3552652.7957181000000...
....................................
2016-12-260.9142071.5803300.6173490000000...
2016-12-271.2297531.5995992.6053190000000...
2016-12-280.9361001.4083782.5405070000000...
2016-12-291.1565751.4286010.8318890000000...
2016-12-301.3831851.5446291.4168850000000...

365 rows × 25 columns

Isolation Forestの作成

ilf = IsolationForest(
    n_estimators=100,
    max_samples="auto",
    contamination=0.01,
    max_features=5,
    bootstrap=False,
    random_state=np.random.RandomState(365),
)

ilf.fit(X)

data["is_anomaly"] = ilf.predict(X) < 0
data["anomaly_score"] = ilf.decision_function(X)

検出した日付と正解の比較

異常値として検出したタイミングと正解を比較します。

plt.figure(figsize=(10, 4))
plt.title("検出箇所")
sns.lineplot(data=data[column_names], palette="tab10", linewidth=2.5)
for d in data[data["is_anomaly"]].index:
    plt.axvline(x=d, color="red", linewidth=4)


plt.figure(figsize=(10, 4))
plt.title("正解")
sns.lineplot(data=data[column_names], palette="tab10", linewidth=2.5)
for d in anomaly_dates:
    plt.axvline(x=d, color="black", linewidth=4)

png

png

異常値のルール

サンプル数(samples)が1の分岐が一番右にあり、それは特徴1による分岐だと分かります。 実際、今回の異常値は特徴1が大きすぎる値のときに異常値になりやすいです。

plt.figure(figsize=(24, 8))
plot_tree(
    ilf.estimators_[0],
    feature_names=column_names,
    filled=True,
    fontsize=13,
    max_depth=3,
    precision=2,
    rounded=True,
)

plt.show()

png