時系列データのテクニカル分析では、ウィンドウを移動させながら平均を取ったり、最大値や最小値を求めることが多いです。pandasだと、rollingで移動ウィンドウを指定してmean, max, minのメソッドで簡単に書けてしまいます。今回の記事は、pandasより速くできる方法はないかと探したときのメモです。
pandasのrollingを使う
まず、以下のようにnumpyのarrayとpandasのSeriesで乱数の時系列を作っておきます。
import numpy as np
import pandas as pd
a = np.random.randint(100, size=100000)
s = pd.Series(a)
移動ウィンドウでの平均(いわゆる単純移動平均)は、rollingにmeanメソッドを使って次のように書けます。
period=10 #期間
%timeit smean = s.rolling(period).mean()
実行時間は
100 loops, best of 3: 5.47 ms per loop
でした。次に移動ウィンドウでの最大値、最小値です。
%timeit smax = s.rolling(period).max()
%timeit smin = s.rolling(period).min()
100 loops, best of 3: 5.51 ms per loop
100 loops, best of 3: 5.53 ms per loop
実行時間は移動平均とほぼ同じです。
scipyのフィルタ関数を使う
移動平均はいわゆるFIRフィルタなので、scipyのlfilter関数が利用できます。
from scipy.signal import lfilter
%timeit amean = lfilter(np.ones(period)/period, 1, a)
重みをすべて1/periodにしたFIRフィルタとして計算させます。実行時間は
1000 loops, best of 3: 980 µs per loop
となりました。pandasの5倍以上速くなりました。さすがscipyです。
こんどは、最大値、最小値を求めたいのですが、それにぴったりの関数はなくて、使えそうなのが、order_filter関数でした。この関数は、指定したウィンドウ中で指定した順位の値を順次返す関数です。引数domainにウィンドウのマスク配列、引数rankに順位を指定します。ただし、時系列のサンプルを中心に対象なウィンドウとなるので、配列の前半だけに1を入れておきます。最小値の場合rank=0、最大値の場合、rank=period-1とすればOKです。
from scipy.signal import order_filter
domain = np.concatenate((np.ones(period), np.zeros(period-1)))
%timeit amax = order_filter(a, domain, period-1)
%timeit amin = order_filter(a, domain, 0)
実行結果は以下のようになりました。
10 loops, best of 3: 102 ms per loop
10 loops, best of 3: 102 ms per loop
こんどはpandasの20倍近く遅くなってしまいました。scipyの関数でもダメでした。やはり、任意の順位を出せるよう毎回ソートしているからでしょう。最大値、最小値を求めるのであれば、それ専用の関数がいいということでした。