データサイエンティスト(仮)

元素粒子論博士。今はデータサイエンティスト(仮)。

Pythonでデータ分析:imbalanced-learnで不均衡データのサンプリングを行う

導入

クラス分類、例えば0:負例と1:正例の二値分類を行う際に、データが不均衡である場合がたびたびあります。例えば、クレジットカードの取引データで、一つの取引に対して不正利用かどうか(不正利用なら1、それ以外は0)といった値が付与されているカラムがあるとします。通常、不正利用というのは稀に起こる事象なので、不正利用かどうかが格納されているカラムに関してはほとんどが0で、1がほとんどない、という状況になりがちです。

上記の状況で不正利用を予測するようなモデル構築をする場合、目的変数として不正利用かどうかを用いることになりますが、0と1の比率が50%から極度に乖離します(1の比率が0.X%とかになる)。こういったデータで予測モデルを構築すると、往々にして負例だけを予測する(予測値がすべて0になる)モデルになりがちです。というのは、不均衡なデータの場合はそれでも「正解率(Accuracy)」が高くなってしまうからです。例えば、目的変数の内訳が、0が99990件、1が10件の場合に、すべて0と出力するモデルができたとすると、正解率は99990 / (99990 + 10) = 99.99%となります。このモデルは正解率は高いのですが、すべての不正利用を見逃す(偽陰性:本当は不正利用(=1)だけれども不正利用でない(=0)と誤って予測する)ことになり、不正利用を検知したいという目的には全くそぐわないモデルになっています。

不正利用を予測したい、つまり誤検出が多少増えてもから不正利用を検出したいという状況では、サンプリングによって正例と負例の割合を変える、といった方法が採られます。つまり、学習に使われる正例の割合を増やすことで偽陰性を減らし、多少の偽陽性(本当は不正利用していない(=0)けれども不正利用(=1)と誤って予測する)は出しつつも不正利用も検出できるようにします。割合を変化させるにあたって、大きく以下の3パターンがあります。

  • Under Sampling:負例を減らす
  • Over Sampling:正例を増やす
  • 上記の両方を行う

これら割合の変化は、Pythonではimbalanced-learnというライブラリを用いると簡単に行えます。今回は、このimbalanced-learnを用いてUnder/Over Samplingをどう行うかを簡単に紹介します。

ライブラリのインストール

pipが利用できるなら、以下のように簡単にインストールできます。

$ pip install -U imbalanced-learn 

開発版を使用したい場合は、githubからインストールします。

$ git clone https://github.com/scikit-learn-contrib/imbalanced-learn.git
$ cd imbalanced-learn
$ python setup.py install

サンプルデータ

不均衡データを人工的に生成します。こういった人工データは、sklearn.datasets.make_classificationを用いると簡単に作成できます。今回、10万件のデータで、正例が10件のデータを以下のようにして作成しました。

from sklearn.datasets import make_classification
df = make_classification(
    n_samples = 100000, n_features = 10, n_informative = 2, n_redundant = 0, 
    n_repeated = 0, n_classes = 2, n_clusters_per_class = 2, weights = [0.9999, 0.0001], 
    flip_y = 0, class_sep = 1.0, hypercube = True, shift = 0.0, 
    scale = 1.0, shuffle = True, random_state = 71)

パラメータの意味は以下です。今回大事なのは、n_sample, n_classes, weightsの3つで、あとはえいやっと決めています。

パラメータ名 説明
n_samples 生成するサンプルの数
n_features 生成する特徴量の数
n_informative 目的変数のラベルと相関が強い特徴量(Informative fearture)の数
n_redundant Informative featureの線形結合から作られる特徴量(Redundant fearture)の数
n_repeated Infomative、Redundant featureのコピーからなる特徴量の数(Repeated feature)
n_classes 分類するクラス数
n_clusters_per_class 1クラスあたりのクラスタ
weights クラスの比率で、例えば、2値分類問題の場合、Noneとすると0と1が50%ずつだが、[0.9, 0.1] と与えると0が90%、1が10%になる
flip_y クラスのフリップ率で、例えば0.01とすると各クラスの1%の符号がランダムに変更される
class_sep 生成アルゴリズムに関係するパラメータ(細かい話はドキュメント参照)
hypercube 生成アルゴリズムに関係するパラメータ(細かい話はドキュメント参照)
shift 全ての特徴量にshiftを加算する。Noneが指定された場合、[-class_sep, class_sep]の一様乱数を加算する
scale 全ての特徴量にscaleを乗算、Noneが指定された場合、 [1, 100]の一様乱数を乗算する
shuffle Trueにすると行と列をシャッフルする
random_state 乱数を制御するパラメータで、Noneにすると毎回違うデータが生成されが、整数をシードとして渡すと毎回同じデータが生成される

sklearn.datasets.make_classification — scikit-learn 0.19.1 documentation

あとは申し訳程度にDataFrameに変換してカラム名などをつけておきます。ここは、私がcsvにいったん保存したりする関係で行った操作なので、必須ではないです。

import numpy as np
import pandas as pd
from pandas import DataFrame, Series
df_raw = DataFrame(df[0], columns = ['var1', 'var2', 'var3', 'var4', 'var5', 'var6', 'var7', 'var8', 'var9', 'var10'])
df_raw['Class'] = df[1]

クラスの割合は、以下のようになっています。

df_raw['Class'].value_counts()

# 出力
0    99990
1       10
Name: Class, dtype: int64

圧倒的に0が多くなっています。

プロトタイプモデル作成

不均衡データをサンプリングしないまま、分類のためのロジスティック回帰モデルを作成してみます。

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

# 学習用と検証用に分割
X = df_raw.iloc[:, 0:10]
y = df_raw['Class']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.3, random_state = 71)

# モデル構築
mod = LogisticRegression()
mod.fit(X_train, y_train)

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

正解率(Accuracy)は、以下になります。

print('Accuracy(test) : %.5f' %accuracy_score(y_test, y_pred))

# 出力
Accuracy(test) : 0.99990

このように、正解率99.99%という、一見精度が良さそうなモデルができています。

しかし、混同行列を出力してみると

tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel()
(tn, fp, fn, tp)

# 出力
(29997, 0, 3, 0)

のように、TN(正でない(=0)ものを正でない(=0)と予測する)とFN(本当は正(=1)だが正でない(=0)と誤って予測する)のみに値があり、FP(本当は正でない(=0)ものを正である(=1)と誤って予測する)とTP(正である(=1)ものを正である(=1)と予測する)が0となっています。つまり、単にすべて0と予測するモデルになっています*1

Precision(正と予測したデータのうち,実際に正であるものの割合:TP / (TP + FP))とRecall(実際に正であるもののうち,正であると予測されたものの割合:TP / (TP + FN))を評価してみます。

print('precision : %.4f'%(tp / (tp + fp)))
print('recall : %.4f'%(tp / (tp + fn)))

# 出力
precision : nan
recall : 0.0000

計算が不能になっているか、0になっているという、ひどい結果です。まあ実質意味のある予測ができるモデルではありませんからね…。

Under Sampling

ここでは、負例を減らして結果がどう変わるかを見てみます。imbalanced-learnで提供されているRandomUnderSamplerで、負例サンプルをランダムに減らし、正例サンプルの割合を10%まで上げます。

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

# 正例の数を保存
positive_count_train = y_train.sum()
# print('positive count:{}'.format(positive_count_train))とすると7件

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

# 学習用データに反映
X_train_resampled, y_train_resampled = rus.fit_sample(X_train, y_train)

あとはプロトタイプモデル作成の際と同様、ロジスティック回帰モデルを構築し、性能を見てみます。

# モデル作成
mod = LogisticRegression()
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) : %.5f' %accuracy_score(y_test, y_pred))

# 出力
Accuracy(test) : 0.96907
Confusion matrix(test):
[[29070   927]
 [    1     2]]

# 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.0022
recall : 0.6667

正解率は落ちたものの、PrecisionとRecallが0でない値になりました。混同行列を見ても、TPが0でなくなっており、FNが小さくなっていることがわかります。しかし、その代償としてFPが927件と大きくなってしまい、それが小さいPrecisionとして跳ね返っています。

Over Sampling

今度は逆に正例を水増しして正例サンプルの割合を10%まで上げます。imbalanced-learnで提供されているRandomOverSamplerで行います。

# ライブラリ
from imblearn.under_sampling import RandomOverSampler

# 正例を10%まであげる
ros = RandomOverSampler(ratio = {0:X_train.shape[0], 1:X_train.shape[0]//9}, random_state = 71)

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

Under Samplingの場合と同様、モデルを作成して性能を見てみます。

# モデル作成
mod = LogisticRegression()
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) : %.5f' %accuracy_score(y_test, y_pred))

# 出力
Accuracy(test) : 0.98983
Confusion matrix(test):
[[29693   304]
 [    1     2]]

# 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.0065
recall : 0.6667

Under Samplingの場合と比較して、FPの数が若干抑えられており(304件)、Precisionが若干良くなっています。

SMOTE

上記のOver Samplingでは、正例を単に水増ししていたのですが、負例を減らし、正例を増やす、といった考えもあります。こういった方法の一つに、SMOTE(Synthetic Minority Over-sampling Technique)というアルゴリズムがあります。imbalanced-learnでは、このSMOTEも提供されているので、ここでも試してみます。

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

# SMOTE
smote = SMOTE(ratio={0:X_train.shape[0], 1:X_train.shape[0]//9}, random_state=71)
X_train_resampled, y_train_resampled = smote.fit_sample(X_train, y_train)

# モデル作成
mod = LogisticRegression()
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) : %.5f' %accuracy_score(y_test, y_pred))

# 出力
Accuracy(test) : 0.98923
Confusion matrix(test):
[[29675   322]
 [    1     2]]

# 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.0062
recall : 0.6667

Under SamplingとOver Samplingの間くらいの性能になりました。Under/Over Samplingを両方合わせ技でやっているので、直感的にはそうなるんですかね。

まとめ

今回は、Pythonのライブラリで不均衡データの取扱いについて紹介しました。今回はOver Samplingが一番有効でありましたが、データが与えられたときに有力な手法はそのデータの性質に依存する部分も大きいです。なのでどういったサンプリングがよいかは、都度色々試してみて決める必要があります。

また、今回紹介したimbalanced-learnには、上記の3つ以外にもサンプリングの方法が実装されています。今回はそのすべてを紹介できませんでしたが、どういったものがあるかは、以下のページを参照していただければと思います。
imbalanced-learn API — imbalanced-learn 0.3.0 documentation

*1:この結果を出すためにseedを調整していたり…。