ランダムの森

20代エンジニアです。プログラミングについて主に書いてます。

pythonのnumpyを用いて時系列データの外れ値(outlier)を取り除く方法

時系列データで外れ値を除去する方法についての備忘録。

一般的にデータで外れ値を除去するには、例えば、データ全体の標準偏差を算出し、2σの外側に位置する値を取り除くというやり方があります。分かりやすい例があったので貼っておきます。
pandasのデータフレームから外れ値を含む行を取り除く - Qiita

しかし、時間軸によって値全体が変動する時系列データの場合、データ全体を一括に処理することができないことが多いでしょう。どういうことか、実際にデータを作って見て見ましょう。

まずは必要なパッケージをインポートします。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import random
%matplotlib inline  

次に、適当に以下の数式で時系列データを作ります。
y = 0.05x^{2} + 2000

#時間軸とデータを作る。
time = np.arange(500)
data_curve = 0.05 *time**2 + 2000
#グラフ表示
plt.plot(time, data_curve, 'ro')
plt.axis([0, 510, 0, 15000])
plt.show()

>output
f:id:doreikaiho:20181223212611p:plain

この時系列データに対して外れ値を与えてやります。
簡単のため20単位時間あたりに一回外れ値があるとういう想定です。(もちろん現実のデータでは規則的に外れ値が発生しているとは限りません。)

#データに対して20単位時間あたりに一個のランダム値を与えるためarrayを作成。
make_random = []
for i in range(500):
    make_random.append(0) if i % 20 !=0 else make_random.append(random.uniform(10000, -10000))
make_random = np.array(make_random)
#ランダム値を加える。
data = data_curve + make_random
#データフレームにする。
df = pd.DataFrame(time,columns = ['TIME'])
df['data'] = data
#グラフ表示
df.plot(x = 'TIME',y = 'data',style ='ro',ylim = [-600,20000], figsize = (15,5))

>output
f:id:doreikaiho:20181223215515p:plain

図を見て分かるように外れ値が発生しています。
ここで、前述した2σを基準にした外れ値除去の関数を書いていきます。

def make_outlier_criteria(col):
    #arrayに対して標準偏差と平均を算出。
    average = np.mean(col)
    sd = np.std(col)
    #2σよりのデータ位置を指定。
    outlier_min = average - (sd) * 2
    outlier_max = average + (sd) * 2
    return outlier_max,outlier_min 

何も考えずにこの関数を適用して外れ値除去をしようとすると以下のようになってしまいます。
f:id:doreikaiho:20181223215541p:plain
ほとんど外れ値が除去されていません。
これは全データで見ると一見外れてる値が2σに収まってしまうからです。
例えば、100(単位時間)に存在する外れ値、曲線から上に飛び出して5000付近に位置していますが、300(単位時間)になると曲線自体が5000付近に達しています。つまり、データ全体で見ると、全くな正常の値と認識されてしまうということです。

ではどうするべきか?
データを細かく区切って、区切った塊ごとに外れ値を除去して最後に合体させるという作戦でいきます。
binsでデータを40分割し、先ほど作った関数make_outlier_criteriaを使って、分割されたデータの塊ごとに外れ値を除去します。(nanとする。)
最後に分割したデータの塊を合体させて、dropna()で外れ値を落とします。

def drop_outlier(df_new,bins = 40):
    #データフレームを分割
    df_new['div']  = pd.qcut(df_new['TIME'],bins,labels = False)
    df_without_outlier = pd.DataFrame([])
    for i in range(bins):
        #分割されたデータフレームの中で2σより外の外れ値を除去。
        df_temp = df_new.groupby('div').get_group(i)
        df_temp[df_temp['data'] <= make_outlier_criteria(df_temp['data'].values)[1]] = None
        df_temp[df_temp['data'] >= make_outlier_criteria(df_temp['data'].values)[0]] = None
        df_without_outlier = df_without_outlier.append(df_temp)
    #外れ値除去後の分割データを合体。
    df_without_outlier = df_without_outlier.dropna()
    return df_without_outlier

上記で作った関数を用いて以下実行します。

drop_outlier(df).plot(x = 'TIME',y = 'data',style ='ro',ylim = [-600,20000],markersize = 5,figsize = (15,5))

output>
f:id:doreikaiho:20181223215050p:plain

見事にきれいになりました。
今回が外れ値を除去する対処方について記しましたが、必ずしもこの方法が外れ値対応として最適という訳ではなく、移動平均を使うなどケースバイケースで対応するべきです。

www.randomlyforest.com