不均衡データへの対応方法について

* 製造業出身のデータサイエンティストがお送りする記事
* 今回は不均衡データへの対応方法について整理します。

##はじめに
業務で分類問題を実施しなければいけない時に、不均衡データを扱う時がありましたので、対応方法を調査していたら「under sampling」と「over sampling」という方法を見つけましたので、整理します。

##不均衡データとは
不均衡データとは、目的変数の分布に大きな偏りがあるデータのことを指します。簡単に言うと、ラベル0が99％、ラベル1が1％といったデータが存在するとき、特段の工夫をせずに分類モデルを生成すると少数派の分類精度の低いモデルになることが知られています（全てラベル0と言っても99%は当たるので当たり前ですが）。
不均衡データを取り扱う場合は、少数派データの識別が目的のケースが多いので、工夫する必要があるという事です。

##under samplingとは
少数派のデータ件数に合うように多数派データからランダムに抽出する方法です。この方法は簡単ですが、学習に有用なデータをサンプリング時に捨てたり、全体のデータ数が不足したりするため、次の二つの問題を引き起こす原因になることに注意が必要です。

1. 学習した分類器の分散が大きくなる問題
2. 学習後に得られる事後分布が歪む問題

上記のような課題に関して、対策や研究はされておりますが、今回は省略させて頂きます。

##over samplingとは
少数派のデータをもとに不足分のデータを補完するというものです。新しいデータを生成するシンプルな方法は下記です。

* 数値データ：平均と分散を使ってランダム生成
* カテゴリーデータ：構成比をウェイトにランダム生成

ただ、この方法だと特徴量間の相関が考慮できません。例えば、身長と体重のように相関があるにも関わらず、身長170cm・体重60kgというデータが生成される可能性があり、モデルの精度に影響してしまう場合があります。

このような課題を解決する手法として、SMOTE (Synthetic Minority Oversampling TEchnique)があります。SMOTEは、少ない方のラベルが付いた各データ点同士を線でつなぎ、その線分上の任意の点をランダムに人工データとして生成する手法です。
拡張された手法等もありますが、今回は省略します。

##不均衡データへの対応（実装）
今回はkaggleで提供されている[Credit Card Fraud Detection](https://www.kaggle.com/mlg-ulb/creditcardfraud)のデータセットを使います。
各レコードには不正利用か否かを表す値(1ならば不正利用)を持っていますが、ほとんどが0で極めて不均衡なデータセットとなっています。

pythonのコードは下記の通りです。

```php
# 必要なライブラリーのインポート
import pandas as pd
from sklearn.datasets import make_classification

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

#　Credit Card Fraud Detectionデータを読み込み
df = pd.read_csv('creditcard.csv')

print(df.shape)
df.head()
```

![スクリーンショット 2021-01-13 9.56.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743453/21754ce0-31d1-dfe8-afc2-1f0a89028e9f.png)

次に不正利用かどうかは'Class'列に入っておりますので、データの偏りを見てみようと思います。

```php
# 分類クラスのデータ数を確認
df['Class'].value_counts()
```

![スクリーンショット 2021-01-13 9.58.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743453/e5aea6d5-8909-3664-6969-3965e9969513.png)

偏っていることが分かるかと思います。最初は何も対応せずに勾配ブースティング木で分類モデルを作成してみようと思います。

```php
# 欠損値の確認
df.isnull().sum()

# 欠損値データの削除
df = df.dropna()

# データを学習用と検証用に分ける
x = df.iloc[:, 1:30]
y = df['Class']
x_train, x_test, y_train, y_test = train_test_split(x, y, random_state = 40)

# 分類モデル作成
model = GradientBoostingClassifier()
model.fit(x_train, y_train)

# 作成したモデルで、テストデータを予測値
y_pred = model.predict(x_test)

# Accuracyと混同行列
print('Confusion matrix(test):\n{}'.format(confusion_matrix(y_test, y_pred)))
print('Accuracy(test) : %.4f' %accuracy_score(y_test, y_pred))
# Confusion matrix(test):
# [[3473    0]
#  [   2   14]]
# Accuracy(test) : 0.9994

# PrecisionとRecall
tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel()
print('precision : %.4f'%(tp / (tp + fp)))
print('recall : %.4f'%(tp / (tp + fn)))
# precision : 1.0000
# recall : 0.8750

# F値
f1_score(y_pred, y_test)
# 0.9333333333333333
```
次にUnder Samplingを実施してみようと思います。

```php
# ライブラリ
from imblearn.under_sampling import RandomUnderSampler

# 正例の数を保存
positive_count_train = int(y_train.sum())
print('positive count:{}'.format(positive_count_train))

# 正例が10％になるまで負例をダウンサンプリング
rus = RandomUnderSampler(sampling_strategy={0:positive_count_train*9, 1:positive_count_train}, random_state=40)

# 学習用データに反映
x_train_resampled, y_train_resampled = rus.fit_sample(x_train, y_train)
print('y_train_undersample:\n{}'.format(pd.Series(y_train_resampled).value_counts()))
# positive count:40
# y_train_undersample:
# 0.0    360
# 1.0     40

# 分類モデル作成
mod = GradientBoostingClassifier()
mod.fit(x_train_resampled, y_train_resampled)

# 予測値算出
y_pred = mod.predict(x_test)

# Accuracyと混同行列
print('Confusion matrix(test):\n{}'.format(confusion_matrix(y_test, y_pred)))
print('Accuracy(test) : %.4f' %accuracy_score(y_test, y_pred))
# Confusion matrix(test):
# [[3458   15]
#  [   2   14]]
# Accuracy(test) : 0.9951

# PrecisionとRecall
tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel()
print('precision : %.4f'%(tp / (tp + fp)))
print('recall : %.4f'%(tp / (tp + fn)))
# precision : 0.4828
# recall : 0.8750

# F値
f1_score(y_pred, y_test)
# 0.6222222222222222
```

F値の結果を見ると改悪されている結果になっておりますね。
今回は詳細な調査は実施しませんが、Under Samplingは良くないようですね。

次にOver Samplingを実施します。

```php
# ライブラリ
from imblearn.over_sampling import RandomOverSampler

# 正例を10％まであげる
ros = RandomOverSampler(sampling_strategy={0:x_train.shape[0], 1:x_train.shape[0]//9}, random_state = 40)

# 学習用データに反映
x_train_resampled, y_train_resampled = ros.fit_sample(x_train, y_train)

# 分類モデル作成
mod = GradientBoostingClassifier()
mod.fit(x_train_resampled, y_train_resampled)

# 予測値算出
y_pred = mod.predict(x_test)

# Accuracyと混同行列
print('Confusion matrix(test):\n{}'.format(confusion_matrix(y_test, y_pred)))
print('Accuracy(test) : %.4f' %accuracy_score(y_test, y_pred))
# Confusion matrix(test):
# [[3471    2]
#  [   2   14]]
# Accuracy(test) : 0.9989

# PrecisionとRecall
tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel()
print('precision : %.4f'%(tp / (tp + fp)))
print('recall : %.4f'%(tp / (tp + fn)))
# precision : 0.8750
# recall : 0.8750

# F値
f1_score(y_pred, y_test)
# 0.875
```

Under Samplingよりは、Over Samplingの方が良い結果にはなっておりますが、何も実施しない時の方がF値は良いですね。
最後にUnder SamplingとOver Samplingの組み合わせSOMTEを使ってみようと思います。

```php
# ライブラリ
from imblearn.over_sampling import SMOTE

# SMOTE
smote = SMOTE(sampling_strategy={0:x_train.shape[0], 1:x_train.shape[0]//9}, random_state = 40)
x_train_resampled_smoth, y_train_resampled_smoth = smote.fit_sample(x_train, y_train)

# 分類モデル作成
mod = GradientBoostingClassifier()
mod.fit(x_train_resampled_smoth, y_train_resampled_smoth)

# 予測値算出
y_pred = mod.predict(x_test)

# Accuracyと混同行列
print('Confusion matrix(test):\n{}'.format(confusion_matrix(y_test, y_pred)))
print('Accuracy(test) : %.4f' %accuracy_score(y_test, y_pred))
# Confusion matrix(test):
# [[3469    4]
#  [   2   14]]
# Accuracy(test) : 0.9983

# PrecisionとRecall
tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel()
print('precision : %.4f'%(tp / (tp + fp)))
print('recall : %.4f'%(tp / (tp + fn)))
# precision : 0.7778
# recall : 0.8750

# F値
f1_score(y_pred, y_test)
# 0.823529411764706
```

これも結果としては、微妙でした。

## さいごに
最後まで読んで頂き、ありがとうございました。
今回は不均衡データへの対応方法について整理しました。
結果としては、良い結果が得られませんでしたが、製造現場では不均衡データは頻繁に発生する課題だと思いますので、ご参考になればと思います。

訂正要望がありましたら、ご連絡頂けますと幸いです。