Box-Cox変換を理解してみる

# 概要

### 背景

最近機会学習を始め、特徴量エンジニアリングを勉強している。
そのとき、Box-Cox変換を知ったのだか、この変換が何なのか、何をしているのか理解できなかったので、自分なりにできるだけ理解しやすいようにまとめてみた。

本投稿では、Box-Cox変換が何なのか、どう変換されるのかについて扱う。
Box-Cox変換を行うために必要な、ライブラリのインストールなど環境準備は記載しないので、他の記事を参照いただきたい。

### 環境

実行環境は次の通り。

````
$sw_vers
ProductName:	Mac OS X
ProductVersion:	10.13.6
BuildVersion:	17G6030
````
````
$python3 --version
Python 3.7.3
````

# Box-Cox変換とは

## 定義

Wikipedia( https://en.m.wikipedia.org/wiki/Power_transform )によるとBox-Cox変換は下のように定義される。

```math
y^{(\lambda)} = \left\{
\begin{array}{ll}
\frac{x^{\lambda} - 1}{\lambda} & ( \lambda \neq 0)\\
\ln(x) & (\lambda = 0 )
\end{array}
\right.
```

ここで、$x$は変換したい変数、&lambda;は変換のパラメータで、&lambda;の値によって変換の関数形が異なる。

## 変換関数の概要

数式のままだと元の変数$x$がどう変換されるのか分かりにくいため、&lambda;の値を
[-2.0、-1.0、-0.5、0、0.5、1.0、2.0]のそれぞれに設定したときの関数をグラフに表した(下図)。

![transformation.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329888/df0e614b-0bfe-6a28-0700-0d709f52c992.png)

ここで分かる通り、$&lambda; \leq 0$ のとき、$x \rightarrow 0$ とすると変換後の値$y$が$ - \infty $となってしまう。
また、$x$が負の値の場合には対応していない。
この問題に対応したものが、Yeo-Johnson変換だが、本投稿では記載しない。

$x > 0$ に着目すると、&lambda;の値によっていくつか特徴がある。
例えば、$&lambda; < 0$のとき、$x \gg 1$では$y \rightarrow const$となる。
一方で、$&lambda; > 0$のとき、$x \gg 1$では$y \rightarrow \infty$となる。
スケール変換の特徴をまとめると、下表のようになる。

| パラメータ | $x$：小 | $x$：大 |
|:-------------------|:-----------:|:-------:|
| $\lambda < 0$      | $+$ | $-$ / (const) |
| $\lambda = 0$      | $+$ | $-$ |
| $0 < \lambda < 1$  | $+$ | $-$ |
| $\lambda > 1$      | $-$ | $+$ |
ここで、$+$は変換によって$x$のスケールが伸びること、$-$は$x$のスケールが縮むことを表している。

したがって、例えばある分布に対してBox-Cox変換を施すと横軸の値$x$が小さい領域のスケールを縮めて、$x$が大きい領域のスケールを伸ばすというような変化をすることが分かる。
もちろん、&lambda;の値によって、その逆もあり得る。

Box-Cox変換の関数を描画したときに使ったコードは次の通り。

```boxcox.py

df_trans = pd.DataFrame({'x':np.arange(0.1, 5.1, 0.1)})

list_lambda = [-2, -1, -0.5, 0, 0.5, 1, 2]
for i, i_lambda in enumerate(list_lambda):
    df_trans[ 'y'+str(i) ] = stats.boxcox( df_trans.x, lmbda = i_lambda )

fig, ax = plt.subplots()
ax.plot(df_trans.x, df_trans.y0, color='darkblue',  label="lambda="+str(list_lambda[0]))
ax.plot(df_trans.x, df_trans.y1, color='mediumblue',label="lambda="+str(list_lambda[1]))
ax.plot(df_trans.x, df_trans.y2, color='skyblue',   label="lambda="+str(list_lambda[2]))
ax.plot(df_trans.x, df_trans.y3, color='green',label="lambda="+str(list_lambda[3]))
ax.plot(df_trans.x, df_trans.y4, color='salmon',    label="lambda="+str(list_lambda[4]))
ax.plot(df_trans.x, df_trans.y5, color='red',       label="lambda="+str(list_lambda[5]))
ax.plot(df_trans.x, df_trans.y6, color='darkred',   label="lambda="+str(list_lambda[6]))
ax.legend()
ax.set_title('y = Box-Cox( x, labmda )')
ax.set_xlabel('x')
ax.set_ylabel('y')
plt.ylim(-10, 10)
plt.grid()
plt.subplots_adjust(left=0.15, right=0.95, bottom=0.1, top=0.9)
plt.show()
```

## パラメータ&lambda;

次に気になるのは、パラメータ&lambda;をどう決めたらいいのか。
scipyのドキュメント( https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.boxcox.html )によると、&lambda;は自分で与える必要はない。与えない場合は

>If lmbda is None, find the lambda that maximizes the log-likelihood function 

と記載されている。この記載だけでは分からないので、論文( https://www.ime.usp.br/~abe/lista/pdfQWaCMboK68.pdf )を読んだところ、要するに、
変換後の分布が最も正規分布(ガウス分布)らしくなるときの&lambda;を求めるということである。

Box-Cox変換とは、変数のスケールを変えて分布を正規分布(ガウス分布)の形に変えてくれる変換であることがわかった。

# 調査

Box-Cox変換についてなんとなく分かって気がしたところで、実際にある分布に対して変換を実行してみる。

## 変換前

今回扱う分布は下図の通り。

![original.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329888/edbeef8b-da54-f8fc-021d-c5f7e5d1a3ee.png)

横軸が変数valで、縦軸が規格化した行数(density)である。
変数valの値が大きい方に長いテールをもつような分布であることが分かる。
この分布をBox-Cox変換してみる。

分布を描画した際のコードは次の通り。

```before.py
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats

df = pd.read_csv( 'data.csv', index_col=0,  header=None, names=['val'] )

binlist=range(0, 1401, 100)
plt.hist(df['val'], bins=binlist, color='r', alpha=0.5, density=True)
plt.xlabel('val')
plt.ylabel('density')
plt.subplots_adjust(left=0.15, right=0.95, bottom=0.1, top=0.9)
plt.show()
```

## 変換後

元の分布の形状を左右対象の正規分布(ガウス分布)にするには、変換valが小さい領域のスケールを伸ばしつつ、大きい領域のスケールを縮めればよいので、&lambda;が0.5あたりになるのではと予想できる。

下図は、&lambda;を
[-2.0、-1.0、-0.5、0、0.5、1.0、2.0]と与えて変換した場合に加えて、&lambda;の値を指定しないで変換した場合、合計8個の分布を示している。

![transformed.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329888/c89abce0-dfba-2fde-a661-2ed999092850.png)

パラメータ&lambda;を指定しないで変換した場合、$&lambda; = 0.37$となり、予想とおおよそ一致し、左右対象の正規分布(ガウス分布)らしい分布に変換されたことが分かった。

上図を描画した際のコードは次の通り。

```after.py

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats

fig = plt.figure(figsize=(6.0, 6.0))

list_lambda = [-2, -1, -0.5, 0, 0.5, 1, 2]
for i, i_lambda in enumerate(list_lambda):
    df[ 'val_'+str(i) ] = stats.boxcox( df.val, lmbda = i_lambda )
    fig.add_subplot(4, 2, i+1).hist(df['val_'+str(i)], bins=20, color='r', alpha=0.5, density=True)
    plt.title("lambda="+str(list_lambda[i]))
    plt.xlabel('val')
    plt.ylabel('density')

df[ 'val_auto' ], best_lambda = stats.boxcox( df.val )
fig.add_subplot(4, 2, 8).hist(df['val_auto'], bins=20, color='r', alpha=0.5, density=True)
plt.title("lambda="+str(round(best_lambda, 2)))
plt.xlabel('val')
plt.ylabel('density')

fig.tight_layout()
fig.show()
plt.show()
```


# まとめ

Box-Cox変換が何者か理解できた。
図を描画する際に参考にした。

* https://qiita.com/waterada/items/20f2d7aa49d0c1cbef3d#%E6%8A%98%E3%82%8C%E7%B7%9A%E3%82%B0%E3%83%A9%E3%83%95
* https://qiita.com/kakiuchis/items/798c00f54c9151ab2e8b
* https://stats.biopapyrus.jp/python/subplot.html