機械学習で時間帯を説明変数にする

機械学習や多変量解析で「時間帯」(何時ごろに発生したイベントであるか)を説明変数として使いたい場合どのようにするのがよいか。

何時に発生したかというのは生データの中では0時から24時までの数値として与えられるだろう。最初に思いつくのはこれをそのまま間隔尺度として利用する方法だ。

まずは実験のための適当なデータを作る。3時、12時、24時を中心に正規分布する3つのグループのデータを生成する。

%matplotlib inline
from numpy.random import normal

num_samples = 1000

hours = [3, 12, 24]
groups =[Series(normal(size=num_samples)).add(hour).mod(24) for hour in hours]
bins = (range(0, 24))
plt.hist(groups[0], bins=bins)
plt.hist(groups[1], bins=bins)
plt.hist(groups[2], bins=bins)
(array([ 313.,  136.,   21.,    2.,    0.,    0.,    0.,    0.,    0.,
           0.,    0.,    0.,    0.,    0.,    0.,    0.,    0.,    0.,
           0.,    0.,    3.,   26.,  145.]),
 array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16,
        17, 18, 19, 20, 21, 22, 23]),
 <a list of 23 Patch objects>)

このように24時付近のグループは0時過ぎと24時前の2つに分かれる。

このデータを機械学習アルゴリズムを使って分類してみよう。 まずはデータと分類ラベルを訓練データと検証データに分ける。

from sklearn import cross_validation

X = pd.concat(groups).reshape(-1, 1)
y = pd.concat([Series([hour for _ in range(0, num_samples)]) for hour in hours])

X_train, X_test, y_train, y_test = cross_validation.train_test_split(X, y)

まずは線形分類器を使用する。

from sklearn.linear_model.perceptron import Perceptron
clf = Perceptron()
clf.fit(X_train, y_train)
clf.score(X_test, y_test)
0.22666666666666666
from sklearn.svm import LinearSVC
clf = LinearSVC().fit(X_train, y_train)
clf.score(X_test, y_test)
0.67200000000000004

線形分類器の正答率はあまり高くない。理由は比較的明白で、24時付近のグループを分類する問題が線形分離(ここでは変数が1つなので点分離というべきか)可能でないからだ。これはXOR問題と呼ばれる。

次のようにカーネル法を用いたSVMや決定木では高い正答率が出る。

from sklearn.svm import SVC
clf = SVC().fit(X_train, y_train)
clf.score(X_test, y_test)
0.95333333333333337
from sklearn.tree import DecisionTreeClassifier
clf = DecisionTreeClassifier().fit(X_train, y_train)
clf.score(X_test, y_test)
0.93333333333333335

決定木のグラフも見ておこう。

from sklearn.externals.six import StringIO
from sklearn.tree import export_graphviz
from IPython.display import Image  
import pydot_ng

def draw_dt(clf, feature_names, class_names):
    dot_data = StringIO()
    export_graphviz(clf, out_file=dot_data,
                    feature_names=feature_names,
                    class_names=class_names,
                    filled=True, rounded=True,
                    special_characters=True,
                    max_depth=3)
    graph = pydot_ng.graph_from_dot_data(dot_data.getvalue())
    return Image(graph.create_png())

draw_dt(clf, feature_names=['hour'], class_names=[str(hour) for hour in hours])

f:id:tokb:20160409132513p:plain

まず3時付近のグループを識別するために7時半で分けている。7時半より後であれば3時半ではない。 右のブランチでさらに18時で分割し、18時より前であれば12時付近のグループ、後であれば24時のグループとしている。 左のブランチでは1時半の前後で24時付近のグループと3時付近のグループの判別を行っている。こちらのブランチはさらに先があるが、訓練データに対するオーバーフィッティングになっているとみなせるのでここで打ち切るべきだろう。

ここまででわかったことは、時間帯を間隔尺度にすると線形分離不可能な問題になってしまうということだ。

他に思いつくアプローチとしては時間帯を適当に区切ってカテゴリーデータ(名義尺度)として扱うという方法だ。0時台、1時台…のように。しかしこの方法では0時台と1時台の近さと、1時台と12時台の近さが異なるという情報が失われてしまう。

そこでいろいろ考えてみたのだが、24時間時計の円周上に配置し、1次元でなく2次元の変数とする方法がある。

import math
coss = [s.map(lambda x: math.cos(x / 24.0 * math.pi * 2)) for s in groups]
sins = [s.map(lambda x: math.sin(x / 24.0 * math.pi * 2)) for s in groups]

plt.scatter(coss[0], sins[0], color='b')
plt.scatter(coss[1], sins[1], color='g')
plt.scatter(coss[2], sins[2], color='r')
<matplotlib.collections.PathCollection at 0x16c4f588>

このようにすると24時付近のグループも1つの領域にまとまる。また、いかにも線形分離可能そうに見える。

再び機械学習を行ってみよう。

from sklearn import cross_validation

X = DataFrame({'cos': pd.concat(coss), 'sin': pd.concat(sins)})
y = pd.concat([Series([hour for _ in range(0, num_samples)]) for hour in hours])

X_train, X_test, y_train, y_test = cross_validation.train_test_split(X, y)
from sklearn.linear_model.perceptron import Perceptron
clf = Perceptron()
clf.fit(X_train, y_train)
clf.score(X_test, y_test)
0.94266666666666665
from sklearn.svm import LinearSVC
clf = LinearSVC().fit(X_train, y_train)
clf.score(X_test, y_test)   
0.93733333333333335
from sklearn.svm import SVC
clf = SVC().fit(X_train, y_train)
clf.score(X_test, y_test)
0.93733333333333335
from sklearn.tree import DecisionTreeClassifier
clf = DecisionTreeClassifier().fit(X_train, y_train)
clf.score(X_test, y_test)   
0.93066666666666664

線形分類器でも高い正答率を出すことができた。