機械学習　k-NN で 不均衡データ  sklearn

(2020/02/25　追記)　TODO: k-NNのweightsの計算を距離の逆数を使わず、距離の和で計算してしまっている。距離の逆数に修正する予定。(KNeighborsClassifierの計算方法が間違っているわけではなく、私の自作関数の計算方法が間違っている状態)


# 結論
- sklearnのKNeighborsClassifierで不均衡データの少ないサイプル側に重いweightを設定をすることができた。

- その結果：少ないサンプル側のrecallを上げることができた。
    - before confusion matrix <br>
 [[2641,   67]<br>
 [ 167,  125]]
    - after: confusion matrix <br>
 [[2252  456]<br>
 [  80  212]]

<br>
イメージ図　before
<img width="903" alt="スクリーンショット 2020-02-24 12.39.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418485/b50f5858-deef-c6ee-1bc7-ebd4bd17d5ce.png">

イメージ図　after
<img width="900" alt="スクリーンショット 2020-02-24 12.38.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418485/9e2892dc-854a-96b0-3064-299286163c69.png">



# 背景・課題
- sklearn.neighbors.KNeighborsClassifierのweightsという引数の挙動が意味不明だったので調べてみた。

# 方法
## 不均衡データへの設定なし

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

import sklearn
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix, accuracy_score

%matplotlib inline
```
```python
from sklearn.datasets import make_classification
data_base = make_classification(
    n_samples = 10000, n_features = 2, n_informative = 2, n_redundant = 0, 
    n_repeated = 0, n_classes = 2, n_clusters_per_class = 2, weights = [0.9, 0.1], 
    flip_y = 0, class_sep = 0.5, hypercube = True, shift = 0.0, 
    scale = 1.0, shuffle = True, random_state =5)

df = pd.DataFrame(data_base[0], columns = ['f1', 'f2'])
df['class'] = data_base[1]

fig = plt.figure()
ax = fig.add_subplot()
for i in df.groupby('class'):
    cls = i[1]
    ax.plot(cls['f1'],
              cls['f2'],
               'o',
            ms=2)

plt.show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418485/5c47306e-4530-14f6-db6d-7ba1ad1d56fb.png)

```python
X = df[["f1","f2"]]
y = df["class"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)

X_train = X_train.reset_index(drop=True)
X_test = X_test.reset_index(drop=True)
y_train = y_train.reset_index(drop=True)
y_test = y_test.reset_index(drop=True)

print("train", X_train.shape, y_train.shape)
print("test", X_test.shape, y_test.shape)
```
train (7000, 2) (7000,)
test (3000, 2) (3000,)

```python

from sklearn.neighbors import KNeighborsClassifier
clf = KNeighborsClassifier(n_neighbors=5)
clf.fit(X_train, y_train)

pred = clf.predict(X_test)
    
result   = confusion_matrix(y_test, pred)
result2 = accuracy_score(y_test, pred)

print("confusion matrix \n",result) 
print("accuracy \n", result2 )
```
confusion matrix 
 [[2641   67]
 [ 167  125]]
accuracy 
 0.922

## 不均衡データへの設定あり
- まずweightをサンプルサイズの割合の逆数を算出する。

```python
size_and_weight = pd.DataFrame({
                'class0': [sum(clf._y == 0),1/ (sum(clf._y == 0)/ len(clf._y))],
                'class1': [sum(clf._y == 1),1/ (sum(clf._y == 1)/ len(clf._y))]}).T
size_and_weight.columns = ['sample_size', 'weight']
size_and_weight
```
<table border="1" class="dataframe">  <thead>    <tr style="text-align: right;">      <th></th>      <th>sample_size</th>      <th>weight</th>    </tr>  </thead>  <tbody>    <tr>      <th>class0</th>      <td>6292.0</td>      <td>1.112524</td>    </tr>    <tr>      <th>class1</th>      <td>708.0</td>      <td>9.887006</td>    </tr>  </tbody></table>


- trainデータに対して学習を済ませておき、その上で、testデータに対しても距離計算を実施しておく。


```python


weights_array = pd.Categorical(clf._y)
weights_array.categories = [size_and_weight.loc[('class0'),'weight'],
                            size_and_weight.loc[('class1'),'weight']]

clf = KNeighborsClassifier(n_neighbors=5)
clf.fit(X_train, y_train)
neigh_dist, neigh_ind = clf.kneighbors(X_test) # この部分のデータフレームについては後述

weights_array = np.array(weights_array).reshape((-1, 1))[neigh_ind,0]
pd.DataFrame(weights_array).head()
```

<table border="1" class="dataframe">  <thead>    <tr style="text-align: right;">      <th></th>      <th>0</th>      <th>1</th>      <th>2</th>      <th>3</th>      <th>4</th>    </tr>  </thead>  <tbody>    <tr>      <th>0</th>      <td>1.112524</td>      <td>1.112524</td>      <td>1.112524</td>      <td>1.112524</td>      <td>1.112524</td>    </tr>    <tr>      <th>1</th>      <td>1.112524</td>      <td>1.112524</td>      <td>1.112524</td>      <td>1.112524</td>      <td>1.112524</td>    </tr>    <tr>      <th>2</th>      <td>1.112524</td>      <td>9.887006</td>      <td>1.112524</td>      <td>1.112524</td>      <td>1.112524</td>    </tr>    <tr>      <th>3</th>      <td>1.112524</td>      <td>1.112524</td>      <td>1.112524</td>      <td>1.112524</td>      <td>1.112524</td>    </tr>    <tr>      <th>4</th>      <td>1.112524</td>      <td>1.112524</td>      <td>1.112524</td>      <td>1.112524</td>      <td>1.112524</td>    </tr>  </tbody></table>

- ↑　不均衡データに対応するためのweightが完成



- weightを考慮するために、引数weightsを設定し予測まで実行

```python
def tmp(array_):
    global weights_array
    array_ = array_ * weights_array
    return array_

clf = KNeighborsClassifier(n_neighbors=5,weights=tmp)
clf.fit(X_train, y_train)
pred = clf.predict(X_test)

pred = clf.predict(X_test)
result   = confusion_matrix(y_test, pred)
result2 = accuracy_score(y_test, pred)

print("confusion matrix \n",result) 
print("accuracy \n", result2 )
```

confusion matrix 
 [[2252  456]
 [  80  212]]
accuracy 
 0.8213333333333334

# 結論
- KNeighborsClassifierでは、testデータを距離の総和のもっとも大きいclassへ分類する。 (推定結果が遠いクラスになるというやや直感と反するアルゴリズムだが、もっとも近しいn個のデータに対してのみ距離の計算は適用されるのでweightをかけない場合は、距離の総和というより多数決に近い推定となる。)
- 前記総和を "総和とweights_arrayの要素積"と置き換えることで、不均衡データに対応する方法へカスタマイズした。

# 詳細： カスタマイズ引数について
> 前記総和を "総和とweights_arrayの要素積"と置き換える

にあたり、以下のデータフレームの対応関係を理解する必要がある。
- X_train
- X_test
- y_train
- neigh_dist <- X_trainとX_testの距離計算の距離
- neigh_ind  <- X_trainとX_testの距離計算のindex
- neigh_indのclass

<br>

## neigh_dist, neigh_ind, y_train(clf._y) の対応について表から理解する。

```python
neigh_dist, neigh_ind = clf.kneighbors(X_test)
pd.DataFrame(neigh_dist).tail(5)
pd.DataFrame(neigh_ind).tail(5)
pd.DataFrame(clf._y.reshape((-1, 1))[neigh_ind,0]).tail(5)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418485/959390ed-06a6-e7f4-96c1-283869986cc8.png)

↑の左から順に、neigh_dist, neigh_ind, "neigh_indのclass"となる

- 考察1：上記3つのテーブルのshapeは同じである。
- 考察2: [上記3つの行数] = [テストデータの行数] 
- 考察3: カラム数 = [n_neighbors=5]
- 考察4: neigh_distに関して、右に進むと大きくなる。　つまりテストデータに対して、もっとも距離の近い５つの点を抽出したと考えられる。

## neigh_dist の算出から、neigh_dist、X_train、X_testの対応を理解する。
- DataFrame: neigh_dist の以下の数値を算出する
    - index = 2998   # 2998番目のテストデータ
    - values = 0.015318 # [もっとも近い距離と判定されたX_trainの1374番目のデータ] と [前記indexのテストデータ] との距離

```python
test_index = 2998
tmp1 = pd.DataFrame(X_test.iloc[test_index])
display(tmp1.T)

train_index = 1374
tmp2 = pd.DataFrame(X_train.iloc[train_index])
display(tmp2.T)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418485/a092e07e-4bce-f1b4-bfd6-ff1c7d70e63b.png)

```python
# ユークリッド距離を算出
(
sum(   (tmp1.values - tmp2.values)  **2    )
**(1/2)
)
```
array([0.01531811])

- neigh_dist.iloc[2998,0]について:を学習データとテストデータから算出できた。

## y_train と clf_knn._yの対応

```python
sum(clf_knn._y == y_train) == len(y_train)
```
True

- y_trainとclf_knn._yが一致することがわかった。

## neigh_ind と "neigh_indのclass"の対応

```python
index_ = neigh_ind[2998,:]
pd.DataFrame(clf._y[index_]).T
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418485/edf904fb-5d2d-e82e-32a8-4fbfb3adfb8a.png)

- "neigh_indのclass" 2998行目について：neight_indとy_trainから"neigh_indのclass"を作成することができた。

#　最後に
- 章[詳細： カスタマイズ引数について]では、KNeighborsClassifier.predict部分のソースコードを解説しているだけなので、gitのソースコードを見た方が早いかもしれません。　参考[git sklearn](https://github.com/scikit-learn/scikit-learn/blob/b194674c4/sklearn/neighbors/_classification.py#L25)