カバン検定を自分で実装してみる

前の記事で、statsmodels に実装されている acorr_ljungbox 関数を使って、カバン検定を行ってみました。
statsmodelsでかばん検定 (自己相関の検定)

今回は学習のため、numpyで実装しました。
実装にあったって数式から説明します。
検定の帰無仮説や対立仮説については前の記事を参照してください。

$\{y_t\}_{0}^{T-1}$を時系列データとします。 (沖本本の定義とインデックスが一つずれているので注意。)

標本平均$\bar{y}$, 標本自己共分散$\hat{\gamma}_k$, 標本自己相関係数$\hat{\rho}_k$を次のように定義します。
\begin{eqnarray}
\bar{y} & = & \frac{1}{T}\sum_{t=0}^{T-1}y_t\\
\hat{\gamma}_k & = & \frac{1}{T}\sum_{t=k}^{T-1}(y_t-\bar{y})(y_{t-k}-\bar{y}) , \hspace{1em} k = 0, 1, 2, \cdots\\
\hat{\rho}_k & = & \frac{\hat{\gamma}_k}{\hat{\gamma}_0},\hspace{1em} k = 1, 2, 3, \cdots
\end{eqnarray}

この時、Ljung と Box が考案した次の統計量$Q(m)$は漸近的に自由度$m$のカイ2乗分布に従います。
$$Q(m) = T(T+2)\sum_{k=1}^{m}\frac{\hat{\rho}_k^2}{T-k}$$

今回も7点周期のデータを準備します。
m = 7 で検定を行いますので、帰無仮説が棄却されれば、
LAG1〜7のいずれかの自己相関係数が0でないことが主張されます。
(p値は5%を基準に判断します。)


import numpy as np
import pandas as pd
from scipy.stats import chi2
# 答え合わせ用
from statsmodels.stats.diagnostic import acorr_ljungbox

# 7点ごとに周期性のあるデータを準備
series = pd.Series([1, 1, 1, 1, 1, 1, 5]*10)
# 乱数追加
series += np.random.randn(70)

# データの個数
T = len(series)
# 検定対象のm
m = 7
# 標本平均
y_ = series.mean()
# 標本自己共分散の列
gamma_list = np.array([
                np.sum([
                    (series[t]-y_)*(series[t-k]-y_) for t in range(k, T)
                ])/T for k in range(m+1)
            ])
# 標本自己相関係数の列
ro_list = gamma_list/gamma_list[0]

# Q(m)の計算
Q = T*(T+2)*np.sum([ro_list[k]**2 / (T-k) for k in range(1, m+1)])
print("lb値:", Q)
print("p値:", chi2.sf(Q, m))

# ライブラリを使った計算結果
lbvalues, pvalues = acorr_ljungbox(series, lags=7)
print(lbvalues[6])
print(pvalues[6])

# 出力結果
lb値: 43.78077348272421
p値: 2.3564368405873674e-07
43.780773482724186
2.3564368405873896e-07

p値が非常に小さな値であることがわかりました。
また、ライブラリを使った結果と四捨五入による誤差がありますが、それ以外は同じ値なのでミスも無さそうです。

この計算の注意点としては、
標本自己相関係数を使うところでしょうか。
pandasのautocorrは自己相関係数を出力してくれますが、
これが標本自己相関係数とは微妙に定義が異なり、autocorrを使うと少し値がずれてしまいます。

statsmodelsでかばん検定 (自己相関の検定)

時系列データを分析をするとき、そのデータが自己相関を持つかどうかはとても重要です。
データが自己相関を持っていたらその構造を記述できるモデルを構築して、予測等に使えるからです。
逆に自己相関を持っていないと、時系列分析でできることはかなり限られます。
過去のデータが将来のデータと関係ないわけですから当然ですね。
(どちらも沖本本の 1.4 自己相関の検定から)

ということで今回行うのは自己相関の検定です。

自己相関が全てゼロという帰無仮説、つまり
$H_0:\rho_1=\rho_2=\cdots=\rho_m=0$を、
$H_1:$少なくとも1つの$k\in[1,m]$において、$\rho_k\neq0$
という対立仮説に対して検定します。

この検定はかばん検定(portmanteau test)と呼ばれているそうです。
検定量は色々考案されているそうですが、 Ljung and Box が考案されたものがメジャーとのこと。

具体的な数式や、numpyでの計算例は次回に譲るとして、とりあえずpythonのライブラリでやってみましょう。
statsmodels に acorr_ljungbox という関数が用意されています。

statsmodels.stats.diagnostic.acorr_ljungbox
(完全に余談ですが、statsmodelsのドキュメントで portmanteau という関数を探していたので、これを見つけるのに結構苦労しました)

あからさまに7点周期を持つデータを準備し、1から10までのmに対して検定を実施したコードがこちらです、
acorr_ljungbox は lb値と、p値をそれぞれのmに対して返します。


import pandas as pd
import numpy as np
from statsmodels.stats.diagnostic import acorr_ljungbox

# 7点ごとに周期性のあるデータを準備
series = pd.Series([1, 1, 1, 1, 1, 1, 5]*10)
# 乱数追加
series += np.random.randn(70)

lbvalues, pvalues = acorr_ljungbox(series, lags=10)
lag = 1
for lb, p in zip(lbvalues, pvalues):
    print(lag, lb, p)
    lag += 1

# 出力
1 4.095089120802025 0.04300796255246615
2 4.223295881654548 0.1210383379718042
3 6.047027807671336 0.10934450247698609
4 6.312955422660912 0.1769638685520115
5 6.457291061922424 0.26422887039323306
6 9.36446186985462 0.15409458108816818
7 40.47102807634717 1.0226763047711032e-06
8 45.2406378234939 3.312995830103468e-07
9 45.24892593869829 8.298135787344997e-07
10 46.35530787049628 1.2365386593885822e-06

lag が7未満の時は、p値が0.05を超えているので、帰無仮説$H_0$を棄却できず、
7以上の時は棄却できていることがわかります。

念の為ですが、lag が 8,9,10の時に棄却できているのは、
どれもデータが7点周期を持っていることが理由であり、
8,9,10点の周期性を持っていること意味するものではありません。

定常過程の例としてのiid過程とホワイトノイズ

前回の記事で定義した定常過程の例を紹介します。
(今回も沖本本からの引用です。)

iid系列

iid系列は、強定常過程の例です。

定義:
各時点のデータが互いに独立でかつ同一の分布に従う系列はiid系列と呼ばれる。

iidとは、 independent and identically distributedの略です。
Wikipedia : 同時独立分布

$y_t$が期待値$\mu$,分散$\sigma^2$のiid系列であることを $y_t\sim iid(\mu,\sigma^2)$と書きます。

ホワイトノイズ

独立性や同一分布性は非常に強い仮定なのですが、
実用上、モデルの錯乱項などとして使うのであればもう少し弱い仮定で十分です。
そこで次のホワイトノイズというものが使われます。
ホワイトノイズは弱定常過程です。

定義:
すべての時点$t$において
$$
\begin{align}
E(\varepsilon _t) &=0\\
\gamma _k &= E(\varepsilon _t \varepsilon _{t-k}) =\left\{
\begin{array}{ll}
\sigma^2, & (k=0)\\
0, & (k\neq 0)
\end{array}\right.
\end{align}
$$
が成立するとき, $\varepsilon _t$はホワイトノイズ(white noise)と呼ばれる.

$\varepsilon _t$が分散$\sigma^2$のホワイトノイズであることを $\varepsilon _t\sim W.N.(\sigma^2)$と書きます。
次のようにホワイトノイズを使って、基礎的な弱定常過程の例を作れます。
$$
y_t = \mu + \varepsilon _t,\ \ \ \varepsilon _t\sim W.N.(\sigma^2)
$$

自分だけかもしれないのですが、ホワイトノイズと聞くと無意識に$(\varepsilon _t,\cdots,\varepsilon _{t+k})$の同時分布が期待値0の多変量正規分布であるかのようにイメージしてしまいます。
しかし定義の通り、ホワイトノイズにはそこまでの強い仮定はないので注意です。

ちなみに $(y_t,\cdots,y_{t+k})$の同時分布が多変量正規分布の場合、それは正規過程と呼ばれ、
正規過程に従うホワイトノイズは正規ホワイトノイズと呼ばれます。(沖本本のp9,p12)
正規ホワイトノイズはiid過程なので、強定常過程になります。

確率過程の弱定常性と強定常性

時系列データの分析において重要な定常性の定義のメモです。
用語は沖本先生の経済・ファイナンスデータの計量時系列分析の定義に従っています。

確率過程の定常性とは、その過程の同時分布や基本統計量の時間不変性に関するものです。
何を不変とするかによって、弱定常性 (weak stationarity)と強定常性(strict stationarity)に分類されます。

以下、$y_t$を確率過程とします。

弱定常性

弱定常性は、過程の期待値と自己共分散が一定であることです。

定義:
任意の$t$と$k$に対して、
$$
\begin{align}
E(y_t) & = \mu\\
Cov(y_t, y_{t-k}) & = E[(y_t-\mu)(y_{t-k}-\mu)] = \gamma_k
\end{align}
$$
が成立する場合、過程は弱定常(weak stationary)と言われます。

$y_t$の期待値は任意の$t$に対して一定で、$y_t$と$y_{t-k}$の共分散は$k$の値によってのみ決まります。
また、この二つが時点に依存しないので、自己相関も時点に依存しません。

経済・ファイナンスの分野では単に定常性というと、弱定常性をさすことが多いそうです。(沖本本 P10)

強定常性

強定常性は、同時分布が不変であることを要求します。

定義:
任意の$t$と$k$に対して、$(y_t,y_{t+1},\cdots,y_{t+k})$の同時分布が同一となる場合、
過程は強定常(strict stationary)と言われます。

弱定常は期待値と共分散だけに対する仮定だったので、強定常のほうが名前の通り強い仮定になります。

ただし、強定常ならば絶対に弱定常というわけではなく、
“過程の分散が有限であるならば、”強定常過程は弱定常過程である、
という風に一つ条件が必要です。

pandasで時系列データの自己相関係数算出

時系列データとそれをn時間分シフトさせたデータ間の相関係数を自己相関係数と呼びます。
データに周期性があるかどうかを確認するときなどに調査します。

単に配列をスライドさせてnumpyか何かで相関係数を出してもいいのですが、
pandasに専用の関数が定義されているのでそれを使って求めることもできます。

pandas.Series.autocorr

試しにやってみましょう。


import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# 7点ごとに周期性のあるデータを準備
series = pd.Series([5, 4, 5, 6, 5, 3, 1]*10)
# 乱数追加
series += np.random.randn(70) * 0.5

# lag=0から29までの自己相関係数
auto_series = [series.autocorr(lag=i) for i in range(30)]

# 可視化
fig = plt.figure(figsize=(10, 10))
ax = fig.add_subplot(2, 1, 1, title="周期性のあるデータ")
ax.plot(series.index, series)
ax = fig.add_subplot(2, 1, 2, title="自己相関係数")
ax.bar(range(30), auto_series)
plt.show()

出力されるグラフがこちら。

7点間隔の周期が綺麗に現れているのがわかります。

LaTeXで行列の成分を省略したときのドットを書く

大学院生のときは頻繁に書いていたのに、すっかり忘れてしまっていたので$\LaTeX$の復習です。

まず、要素を省略せずに3*3行列を書く例。

$$
\textbf{A} =
\left(
\begin{array}{ccc}
a_{11} & a_{12} & a_{13} \\
a_{21} & a_{22} & a_{23} \\
a_{31} & a_{32} & a_{33}
\end{array}\right)
$$

結果。
$$
\textbf{A} =
\left(
\begin{array}{ccc}
a_{11} & a_{12} & a_{13} \\
a_{21} & a_{22} & a_{23} \\
a_{31} & a_{32} & a_{33}
\end{array}\right)
$$

次に、ドットを使って書く例。
横並びは\cdots, 縦並びは\vdots, 斜めは\ddots を使います。


$$
\textbf{W} =
\left(
\begin{array}{ccc}
w_{11} & \cdots & w_{1n} \\
\vdots & \ddots & \vdots \\
w_{n1} & \cdots & w_{nn}
\end{array}\right)
$$

結果。
$$
\textbf{W} =
\left(
\begin{array}{ccc}
w_{11} & \cdots & w_{1n} \\
\vdots & \ddots & \vdots \\
w_{n1} & \cdots & w_{nn}
\end{array}\right)
$$

scipyで手軽にカイ二乗検定

業務でカイ二乗検定を行う場面は、割と多くあります。
自分の場合は、ABテストの評価で使うことが多いです。

Excelでも自分でコードを書いてでもさっとできるのですが、
scipyに専用の関数が用意されているのでその紹介です。

scipy.stats.chi2_contingency

これを使うと、引数にデータのクロス表を放り込むだけで、
カイ二乗値、p値、自由度、期待度数表を一発で算出してくれます。
3つの値と、期待度数がarray形式ではいったタプルが戻るので、引数を4つ用意して受け取ると便利です。


import pandas as pd
from scipy.stats import chi2_contingency

# 検定対象のデータ(サンプルなので値はダミー)
df = pd.DataFrame([[300, 100], [800, 200]])

chi2, p, dof, expected = chi2_contingency(df, correction=False)

print("カイ二乗値:", chi2)
print("p値:", p)
print("自由度:", dof)
print("期待度数:", expected)

# 以下出力
カイ二乗値: 4.242424242424244
p値: 0.03942583262944296
自由度: 1
期待度数: [[314.28571429  85.71428571]
 [785.71428571 214.28571429]]

このようにめんどな手間がなく一瞬で検定ができました。
ちなみに、
correction=False
はイエーツの修正を”行わない”設定です。
デフォルトがTrueなので注意してください。

The Zen of Python

Pythonプログラマーならどこかでみたことがあると思われる「The Zen of Python」が、
pep 20 で定義されたものであることを知りました。

PEP 20 — The Zen of Python

ちなみに、読みたくなったら import thisでいつでも読むことができます。
thisって何だろうとずっと思っていたのですが、Easter Eggとして作られたものだったようです。


>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

とても良いことが書かれていると思いますので、
定期的に読み返したいですね。

hyperoptで整数値の探索

今回もhyperoptの話題です。
この前の記事では、2変数関数の最小値を探すとき、
それぞれの変数を、一定区間の一様分布の中から探索しました。
そのときは、hp.uniformを使いました。

ただ、場合によっては、変数は実数ではなく整数を取ることもあります。
多くの例があると思いますが、機械学習で言えばDNNの中間層のユニット数などがそうですね。

この場合、 hyperoptで探索する空間を定義するときは、hp.randint(label, upper)というのを使うことで実現できます。
ドキュメントはこちら。
2.1 Parameter Expressions
ただ、これよく見ると最小値の指定ができません。
(ドキュメントに書かれてないだけで実は動くのではと思い試しましたが、本当にできません)

そのような時にどうしようかというのが今回の記事です。
0からではなく、1以上の数で探したいケースや、負の数も扱いたい時などありますからね。

今回使う関数はこちら。
$$f(x, y) = (x-5)^2 + (y+7)^2$$
この関数の$-10 \leq x\leq 10, -10 \leq y\leq 10$ の範囲で最小値を取る点を探します。
正解は明らかに$(x, y) = (5, -7)$ です。

それを実行するコードがこちら。


from hyperopt import hp, fmin, tpe, Trials


# 最小値を探したい関数
def f(x, y):
    return (x-5)**2 + (y+7)**2


# 探索する関数を定義する
def objective(args):
    x = args["x"]
    y = args["y"]

    return f(x, y)


# 探索する空間を定義する
space = {
        'x': -10 + hp.randint('x_', 21),
        'y': -10 + hp.randint('y_', 21),
    }

trials = Trials()

best = fmin(
            fn=objective,
            space=space,
            algo=tpe.suggest,
            max_evals=100,
            trials=trials,
        )

print(best)

# 以下出力
100%|██████████| 100/100 [00:00<00:00, 427.25it/s, best loss: 2.0]
{'x_': 16, 'y_': 2}

ちょっと惜しいですが、 f(x, y)が 2になる点を見つけたようです。

ポイントはここ


space = {
        'x': -10 + hp.randint('x_', 21),
        'y': -10 + hp.randint('y_', 21),
    }

このように定義することで、x_y_ はそれぞれ 0〜20の値を取り、
-10することで、xyは-10から10の値を取ります。
21にはならないので注意。(trials.vals["x_"]の値を確認するとx_=21は探していないことがわかります。)

出力の、{'x_': 16, 'y_': 2}から、
$x=16-10=6, y=2-10=8$ を見つけていることがわかります。

ただ、本当はx,yの値が欲しいのに、x_,y_を戻されると少しだけ不便です。
このようなときは、space_evalという関数が使えるようです。
ドキュメントが見つからないのですが、このページのサンプルコードで使われているのを参考にやってみます。


from hyperopt import space_eval
print(space_eval(space, best))

# 出力
{'x': 6, 'y': -8}

これで必要だった(x, y)の値が取れました。
正解から1ずつずれているのがちょっと惜しいですね。

Data Driven Developer Meetup #5 に参加しました

先日、Data Driven Developer Meetup #5 に参加してきました。
#4にも参加しているので2回目です。

今回の会場はログリーさんのオフィス。職場から近かったので徒歩で行けて便利でした。

メインセッション

データドリブンな小売店舗経営を支えるプロダクト開発の裏側』 株式会社ABEJA 大田黒 (@xecus) さん

まずこの発表資料は ABEJA SIX 2019で使われたものだそうです。
ABEJA SIXは参加したかったのですが業務的に行けなかったので、ラッキーでした。

動画の顔認識や、店内の導線の可視化など技術力が凄まじく高いのはもちろんですが、
「リアル版のグーグルアナリティクス」といった非常にイメージをつかみやすい説明が印象的でした。
これはビジネス的に強そうです。
上のスライド資料だと動画が静止画になってしまいっていましたが、
店舗内入り口のカメラの動画で、性別と年齢が推定されているデモなどは驚きでした。

プロジェクト進行における問題のところは自分も似たような問題に直面したのでよくわかります。
ゴール設定の難しさも、不確実性の多さも、本当にウォーターフォールとは相性が悪いです。

まだ自分の職場では、「研究開発」と言えるほどモデルの開発に専念できる体制も作れておらず、
機械学習のプロジェクトはもっとごちゃごちゃしているのですが、
チームで進める土壌づくりは力を入れないといけないと思いました。

機械学習で動くシステムを実運用に組み込むということ』 株式会社ブレインパッド アナリティクスサービス部 吉田勇太 (@yutatatatata ) さん

これもあるあるのオンパレードで苦笑いする場面の多い発表でした。
画像とテキストという違いはありますが、目視でやっていたことを機械学習で行いたいというニーズは自分も経験しています。
その対象が不均衡データで、
PrecisionとRecallがトレードオフになる厄介さや、Recallが重要だからと言って、Precisionはどこまでも犠牲にしていいわけじゃない苦労など
経験してきたので非常によくわかりました。

また、良いモデルを作るだけではなく業務フローをしっかり考えないといけないこと、
それにあたって、他のメンバーの協力を得ることの重要性など自分の失敗経験もあるので本当に納得です。

このあと、LTはお酒も入って気楽な雰囲気で聞いていました。

LT

学習行動データを蓄積するLearnig Record Storeの開発事例』by @yukinagae さん

まだまだ取り組みが始まったばかりという印象が大きかったのですが、
「Learnig Record Store」というコンセプトをしっかり立てて進められているのが素晴らしいと思いました。
GCPUG覚えておきます。

データドリブンを提供するサービスBrand Officialのアーキテクチャについて』 by @YasutakaYamamoto

久々に Airflowではなくdigdagの方が登場しました。
まだdigdagは使ったことがないのですが一度くらいは調べておきたいですね。
embulkは使っているのですが、よく理解していないので調べなければ。

今回も非常に参考になること、励みになることが多く参加してよかったです。
運営の皆様、登壇者の皆様、ありがとうございました。