imbalanced-learnのRandomUnderSamplerでアンダーサンプリング

[`imbalanced-learn`パッケージ](https://imbalanced-learn.org/stable/index.html)の[`RandomUnderSampler`関数](https://imbalanced-learn.org/stable/references/generated/imblearn.under_sampling.RandomUnderSampler.html)でアンダーサンプリングの方法です。注意点を含めていくつか書いておきます。imbalanced-learnには他のアンダーサンプリングやオーバーサンプリング、両者を同時に行う関数など様々あります。まずは入門として[`RandomUnderSampler`関数](https://imbalanced-learn.org/stable/references/generated/imblearn.under_sampling.RandomUnderSampler.html)を使いました。
関連トピックとして以下の記事があります。

https://qiita.com/FukuharaYohei/items/b2f08bf95c595ecd34ae

https://qiita.com/FukuharaYohei/items/dce8dd7b867fa00381dd


# 環境
Python3.7.13でGoogle Colaboratory上で動かしています。Google Colabプリインストールされているパッケージはそのまま使っています。

|Package|Version|備考|
|:-:|:-:|:-:|
|imblearn|0.9.1|Google Colabプリインストール|
|matplotlib|3.2.2|Google Colabプリインストール|
|numpy|1.21.6|Google Colabプリインストール|
|pandas|1.3.5|Google Colabプリインストール|
|scikit-learn|1.0.2|Google Colabプリインストール|

# プログラム
## 1. Import
パッケージインポート。

```python
from imblearn.under_sampling import RandomUnderSampler
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.ticker import PercentFormatter
import numpy as np
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import ConfusionMatrixDisplay, log_loss
from sklearn.model_selection import train_test_split
```

## 2. 不均衡データ作成
`make_classification`関数を使って正例が１％の不均衡データを生成し、訓練と評価データに分割。

```python
def make_df():
    X, y = make_classification(n_samples=10_000, weights=[0.99, 0.01])
    X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.9)
    return X_train, X_test, y_train, y_test

X_train, X_test, y_train, y_test = make_df()
```

## 3. 訓練と評価実施
[`RandomUnderSampler`関数](https://imbalanced-learn.org/stable/references/generated/imblearn.under_sampling.RandomUnderSampler.html)を使ってアンダーサンプリングします。注意点としては、`sampling_strategy`に渡すパラメータは以下の値です。

```math
\frac{アンダーサンプリング後負例数}{アンダーサンプリング前負例数}
```

上記のように割合を渡す以外の方法もあるようですが、詳しくは[公式文書](https://imbalanced-learn.org/stable/references/generated/imblearn.under_sampling.RandomUnderSampler.html)を参照ください。
アンダーサンプリング率を変更し、以下の流れで実施。

1. アンダーサンプリング実施
1. ロジスティック回帰で訓練
1. 評価データで予測し、対数ロスを計算
1. 予測確率のCalibration実施
1. Calibration後の予測確率で対数ロス計算

```python
results = []
train_count = np.unique(y_train, return_counts=True)
lfunc = lambda x: x/(x+((1-x)/beta))

for i in range(1, 21):
    # サンプリング後の負例数/ サンプリング前の負例数
    beta = 1 / i

    # 正例数 / (DS前 負例数 * サンプリング率)
    ratio = train_count[1][1] / ( train_count[1][0] * beta)
    X_sampled, y_sampled = RandomUnderSampler(sampling_strategy=ratio).fit_resample(X_train, y_train)
    sample_count = np.unique(y_sampled, return_counts=True)

    # 訓練および推論
    clf = LogisticRegression()
    clf.fit(X_sampled, y_sampled)
    y_pred_proba = clf.predict_proba(X_test)
    lloss_non_calib = log_loss(y_test, y_pred_proba[:,1])

    # Calibration
    calibrated = lfunc(y_pred_proba[:,1])
    lloss_calib = log_loss(y_test, calibrated)
    results.append([i, sample_count[1][0], beta, lloss_non_calib, lloss_calib])

# 最終行のみヒストグラム表示
else:
    plt.hist(calibrated)
```

参考に最終行(サンプリング率が0.05)の予測確率ヒストグラムです。Calibrationによって自信過剰を抑えられるのですが、予測確率が0.1以下と低く集中します。詳しいシミュレーションは[別記事「不均衡データへのダウンサンプリング後のCalibration」](https://qiita.com/FukuharaYohei/items/b2f08bf95c595ecd34ae)に書きました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134273/a0e5700a-fd43-7589-a62c-45bbe775fad9.png)

## 4. 対数ロスグラフ表示
対数ロスをグラフに表示してみます。

```python
df_result = pd.DataFrame(results, columns=['i', 'sample num', 'BETA', 'lloss', 'lloss calibrated'])
df_result.index = round(df_result['BETA'], 3)

_, axes = plt.subplots(nrows=2, figsize=(10, 7), constrained_layout=True)
df_result.loc[:,['lloss', 'lloss calibrated']].plot.bar(ax=axes[0])
axes[0].grid(axis='y', linestyle='dotted')
axes[0].axhline(y=results[0][3], ls='--', color='red', alpha=0.6)

df_result['relative'] = df_result.loc[:,['lloss calibrated']] / results[0][3]
df_result.loc[:,['relative']].plot.bar(ax=axes[1])
axes[1].grid(axis='y', linestyle='dotted')
axes[1].set_ylim([0.9, 1.1])
axes[1].yaxis.set_major_formatter(PercentFormatter(1.0))
axes[1].axhline(y=1, ls='--', color='red', alpha=0.6)

plt.show()
```

上のグラフがCalibration前後の対数ロスで、Y軸の赤点線はアンダーサンプリング未実施時の対数ロスの値です。
下のグラフはCalibration後の対数ロスの値を相対表示(100%はアンダーサンプリング未実施時で赤点線で強調)。
アンダーサンプリングによって対数ロスが低下する、みたいな結果だとわかりやすいのですが、そんなこともなかったです。正直、**どういう場合に効果があるのかわからない**ですが、[Facebookの例](https://qiita.com/FukuharaYohei/items/dce8dd7b867fa00381dd)のように**効果がある場合もあります**。私が仕事で効果があったこともありました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134273/6eba5b41-ce44-3b3b-32f4-13f6867f7293.png)