【Python】相関を一定以下・最大の特徴量数

# 1. 要約
卒論やってるときに、「２つの特徴量間の相関係数を一定以下に抑えないといけない、でも特徴量の候補が100もある」みたいなことに直面した。たぶん技術的に何でもない基本的なことなせいか、ネットに同じようなものが見当たらなかった。とりあえず記事にしておく。内容の詳細は「2. コードの構成」以降に書いた。
### 1.1 特徴量間の相関が高いときの問題
統計や機械学習のモデルで分析を行うとき相関が高い特徴量の組み合わせが含まれていると、

- 分析結果の説得力が望めない
- 学習ごとに重みづけが安定しない

などの弊害（多重線形性）が生じてしまう。手っ取り早いものに「相関が高い特徴量のそれぞれの組み合わせのうち片方を削除」という手法がある。

### 1.2 「特徴量の数」と「相関の低さ」のトレードオフ
とはいえ、なにも考えずに特徴量を削除していくと極端に特徴量の数が減ってしまう可能性がある。例えば、相関が高い組み合わせが複数含まれているケースで、他の特徴量とよりおおくの相関をもつ特徴量を残してしまい、必要以上に特徴量を減らさないといけない可能性がある。短く言えば「『相関係数を一定以下』かつ『特徴量の数がもっとも多い組み合わせ』を見つけたい、ということ。

### 1.3 コードと実用例
コードの例と簡単に実用例を上げておく。
#### コードの一例
最初の二つの関数をつかって三つ目の関数を定義する流れになっていて、処理を実行するときは三つ目の関数を使う。３つ目の関数no_high_corr(df, threshold)の概要は以下。

|関数|引数|説明|
|---------|----|------|
|no_high_corr( )|df, threshold|df：特徴量のデータ、pandas.DataFrame <br>threshold：相関係数の水準、0以上1以下<br>返り値：2特徴量間の相関係数がthreshold以下のときの特徴量数が最大の組み合わせ, pandas.DataFrame

最初の二つの関数については「4. メイン」の(2.)でまとめてある。


```Python:main_in
import pandas as pd
import numpy as np

def corr_loc(corr_df,threshold):
    count_sums = pd.Series(np.zeros(corr_df.shape[1]))
    for i in range(corr_df.shape[1]):
        count_j = 0
        for j in range(corr_df.shape[1]):
            if corr_df.iloc[j,i] > threshold:
                count_j += 1
            else:
                pass
        count_sums.iloc[i] = count_j
    print(count_sums)
    return count_sums.idxmax()


def corr_max(corr_df,threshold):
    count_sums = pd.Series(np.zeros(corr_df.shape[1]))
    for i in range(corr_df.shape[1]):
        count_j = 0
        for j in range(corr_df.shape[1]):
            if corr_df.iloc[j,i] > threshold:
                count_j += 1
            else:
                pass
        count_sums.iloc[i] = count_j
    print(count_sums)
    return count_sums.max()


def no_high_corr(df, threshold):
    corrmat = df.corr()
    a = corrmat.abs()
    b = corr_loc(a,threshold)
    c = corr_max(a,threshold)
    while c > 1:
        A = a.drop(a.columns[b],axis=1)
        B = A.drop(A.index[b])
        a = B
        b = corr_loc(B,threshold)
        c = corr_max(B, threshold)
    return df.loc[:,a.columns]
```


####実用例
sklearnのデータセット"ボストン住宅価格"の特徴量を対象にした実装例。各特徴量同士の相関係数が0.5以下、かつ特徴量数が最大になる組み合わせを出力する。

```python:main_in
import pandas as pd
import numpy as np
from sklearn.datasets import load_boston

boston = load_boston()
df_features = pd.DataFrame(boston.data, columns = boston.feature_names)
print('==============Oridinary data==============')
print(df_features.head())

low_corr_features = no_high_corr(df_features,0.5)
print('==============Corrected data==============')
print(low_corr_features)
```
以下が出力。もともと13あった特徴量（Ordinary data）から6の特徴量（Corrected data）が選択された。

```python:main_out
==============Oridinary data==============
      CRIM    ZN  INDUS  CHAS    NOX     RM   AGE     DIS  RAD    TAX  \
0  0.00632  18.0   2.31   0.0  0.538  6.575  65.2  4.0900  1.0  296.0   
1  0.02731   0.0   7.07   0.0  0.469  6.421  78.9  4.9671  2.0  242.0   
2  0.02729   0.0   7.07   0.0  0.469  7.185  61.1  4.9671  2.0  242.0   
3  0.03237   0.0   2.18   0.0  0.458  6.998  45.8  6.0622  3.0  222.0   
4  0.06905   0.0   2.18   0.0  0.458  7.147  54.2  6.0622  3.0  222.0   

   PTRATIO       B  LSTAT  
0     15.3  396.90   4.98  
1     17.8  396.90   9.14  
2     17.8  392.83   4.03  
3     18.7  394.63   2.94  
4     18.7  396.90   5.33  

==============Corrected data==============
     CHAS     DIS  RAD  PTRATIO       B  LSTAT
0     0.0  4.0900  1.0     15.3  396.90   4.98
1     0.0  4.9671  2.0     17.8  396.90   9.14
2     0.0  4.9671  2.0     17.8  392.83   4.03
3     0.0  6.0622  3.0     18.7  394.63   2.94
4     0.0  6.0622  3.0     18.7  396.90   5.33
..    ...     ...  ...      ...     ...    ...
501   0.0  2.4786  1.0     21.0  391.99   9.67
502   0.0  2.2875  1.0     21.0  396.90   9.08
503   0.0  2.1675  1.0     21.0  396.90   5.64
504   0.0  2.3889  1.0     21.0  393.45   6.48
505   0.0  2.5050  1.0     21.0  396.90   7.88
```


# 2. コードの構成
### 2.1 全体の流れ
相関が一定以下かつ、最大数の特徴量の組み合わせを以下の流れで取得。
1. 相関行列を取得
2. 各列で一定以上の相関係数をカウント
3. 相関係数の数が一番多い特徴量を相関行列の行・列から削除
4. 相関係数が一定以上が、全ての列で０になるまで2., 3., 4.を繰り返す
5. 残った特徴量で新しいデータテーブルを出力

### 2.2 その他
- 基本的にPandasを中心にしてにして構成。



# 3. データの準備
構成にむけて、5つの特徴量（A,B,C,D,E）$\times$ 10個のデータをpd.DataFrameで用意。

```Python:script_1_in
demo_data = pd.DataFrame(np.random.randint(-1000, 1000,(10,5)),columns=['A','B','C','D','E'])
print(demo_data)
```

```Python:script_1_out
     A    B    C    D    E
0 -644  225    8  509 -980
1  809  993  882 -144 -462
2 -501 -505  972 -657  194
3 -980  862  886 -163 -444
4 -757 -254  186 -506 -178
5 -171 -317  973 -237  760
6  831  265  461    0  214
7  814 -466  610 -668  112
8 -281  832 -753  963  306
9  578 -557 -962    3  435
```

# 4. メイン
「2.1 全体の流れ」の1.〜5.にしたがって処理していく。
### (1.) 相関行列を取得
pd.DataFrame.corr()で相関行列を計算すると以下。

```Python:Script_2_in
corrmat = demo_data.corr()
print(corrmat)
```

```Python:Script_2_out
          A         B         C         D         E
A  1.000000 -0.093300 -0.070089 -0.112714  0.312305
B -0.093300  1.000000  0.048836  0.559999 -0.472975
C -0.070089  0.048836  1.000000 -0.638188 -0.117121
D -0.112714  0.559999 -0.638188  1.000000 -0.153192
E  0.312305 -0.472975 -0.117121 -0.153192  1.000000
```

### (2.) 各列で一定以上の相関係数をカウント
相関係数を各列でカウントし、最大値の列番号を返す関数（corr_loc(corr_df,threshold)）と、最大値を返す関数（corr_max(corr_df,threshold)）を定義する。\*1～\*4は共通で返り値の*5と*6が異なる。

|関数|引数|説明|
|---------|----|------|
|corr_loc( )|corr_df, threshold|corr_df：各成分の絶対値をとった相関行列、pandas.DataFrame <br>threshold：相関係数の水準、0以上1以下<br>返り値：thresholdより大きい相関係数を最ももつ特徴量の列番号
|corr_max( )|corr_df, threshold|corr_df：各成分の絶対値をとった相関行列、pandas.DataFrame <br>threshold：相関係数の水準、0以上1以下<br>返り値：thresholdより大きい相関係数を最ももつときの個数|

```Python:script_3_in
# 最大値の列番号を返す関数
def corr_loc(corr_df,threshold):
    # *1 i列の相関係数一定以上をカウントを記録する空のテーブル
    count_sums = pd.Series(np.zeros(corr_df.shape[1]))
    # *2 i列ごとに相関係数一定以上の個数をカウントするためのループ
    for i in range(corr_df.shape[1]):
        # *3 i列のうちj行が相関係数一定以上ならカウント、一定以下ならパスする
        count_j = 0
        for j in range(corr_df.shape[1]):
            if corr_df.iloc[j,i] > threshold:
                count_j += 1
            else:
                pass
        # *4 i列での相関係数一定以上の個数を記録
        count_sums.iloc[i] = count_j
    print(count_sums)
    # *5 相関係数一定以上のカウントが最大の列番号を返す
    return count_sums.idxmax()

# 最大値を返す関数
def corr_max(corr_df,threshold):
    # *1
    count_sums = pd.Series(np.zeros(corr_df.shape[1]))
    # *2
    for i in range(corr_df.shape[1]):
        # *3
        count_j = 0
        for j in range(corr_df.shape[1]):
            if corr_df.iloc[j,i] > threshold:
                count_j += 1
            else:
                pass
        # *4
        count_sums.iloc[i] = count_j
    print(count_sums)
    # *6 相関係数一定以上のカウントの最大値を返す
    return count_sums.max()
```
|番号| 注釈|
|-----|------|
|*1|i列ごとに相関係数一定以上をカウントを記録する空のテーブル|
|*2|i列ごとに相関係数一定以上の個数をカウントするためのループ|
|*3|i列のうちj行が相関係数一定以上ならカウント、一定以下ならパス|
|*4|i列の個数を記録|
|*5|相関係数一定以上のカウントが最大の列番号を返す
|*6|相関係数一定以上のカウントの最大値を返す|


### (3.) 相関係数の数が一番多い特徴量を行・列から削除 <br>&emsp;＆ (4.) 一定以上の相関係数が無くなるまで繰り返し
pd.DataFrame.drop()をつかって、一定水準より大きい相関係数の数が一番多い特徴量を、相関行列の行・列それぞれから削除するのを繰り返す。すべての列から一定水準より大きい相関係数がなくなったらループ終了。以下の例では相関係数の絶対値が0.2を一定の水準としている。ただし、相関行列では、各列に自らとの相関（$=1$）が含まれるのでループの終了条件に注意。

```Python:script_3_in
# *7 正・負の高い相関係数を削除したいので、相関行列の各成分を絶対値にしておく
a = corrmat.abs()
# *8 今回は相関係数の絶対値が0.2より大きいものを削除
b = corr_loc(a,0.2)
c = corr_max(a,0.2)
# *9 相関係数が一定以上の組み合わせが1個より大きいかぎり繰り返す
while c > 1:
    A = a.drop(a.columns[b],axis=1)
    B = A.drop(A.index[b])
    a = B
    b = corr_loc(B,0.2)
    c = corr_max(B, 0.2)
print(a)
```
```python:script_3_out
          D         E
D  1.000000  0.153192
E  0.153192  1.000000
```

|番号| 注釈|
|-----|------|
|*7|正・負の高い相関係数を削除したいので、相関行列の各成分を絶対値にしておく|
|*8|今回は相関係数の絶対値が0.2より大きいものを削除|
|*9|相関係数が一定以上の組み合わせが1個より大きいかぎり繰り返す|


### (5.) 残った特徴量で新しいデータテーブルを出力
元のデータ（demo_data）から相関係数0.2以下の組み合わせを抽出する。

```python:script_4_in
# *10 列名のリスト（a.coulumns）を渡して抽出
no_high_corr = demo_data.loc[:,a.columns]
print(no_high_corr)
```
```python:script_4_out
     D    E
0  509 -980
1 -144 -462
2 -657  194
3 -163 -444
4 -506 -178
5 -237  760
6    0  214
7 -668  112
8  963  306
9    3  435
```
|番号| 注釈|
|-----|------|
|*10|列名のリスト（a.coulumns）を渡して抽出|