機械学習の評価指標とPythonでの実装【二値分類 後編＋他クラス分類】

「効果指標入門」を参考に、機械学習における評価指標をまとめました。今回は二値問題における評価指標の実装例と他クラス分類における評価指標を取り上げます。

※内容に間違いがあればご指摘いただけますと幸いです。

# 評価指標とは
機械学習モデルの性能を評価するために使用される「評価指標」とは、モデルがタスクをどれだけ効果的に実行しているかを定量的に測定するための尺度や基準です。

これらの指標は、モデルの訓練やチューニングにおいて不可欠であり、モデルの予測がどれだけ正確で信頼性があるかを評価します。

評価指標には大きく分けて二つのタイプがあります。一つは「分類タスク」のためのもので、もう一つは「回帰タスク」のためのものです。今回は分類タスクに焦点を当て、二値分類の後編としてPythonによる評価指標の実装を行います。

また、二値分類の評価指標を拡張した他クラス分類における評価指標についてもまとめます。

前半記事はこちら
https://qiita.com/pnd642/items/71369fc19101b27a91f6

回帰タスクにおける評価指標はこちら
https://qiita.com/pnd642/items/089143957de635df6444

# 分類タスクに用いられる評価指標

まずは前編の復習として、分類タスクにおける各評価指標の名称、説明、メリット・デメリットをまとめた表を再掲します。

| 評価指標 | 説明 | メリット | デメリット |
|---|---|---|---|
| 混同行列 | モデルがどの程度正確に予測したかを示す | 具体的な予測結果を視覚的に理解できる | モデル全体の性能を一つの数値で表現することはできない |
| 正解率（accuracy） | 全体の予測のうち正しく予測できた割合を示す | シンプルで直感的 | 不均衡なデータセットでは誤解を招く可能性がある |
| 適合率 (precision) | 正と予測されたインスタンスのうち実際に正であった割合を示す | 偽陽性を重視する場合に有用 | 偽陰性は考慮しないため、再現率が低い場合には問題がある |
| 再現率 (recall) | 実際に正であったインスタンスのうち正しく予測できた割合を示す | 偽陰性を重視する場合に有用 | 偽陽性は考慮しないため、適合率が低い場合には問題がある |
| F1-score | 適合率と再現率の調和平均 | 適合率と再現率のバランスを取ることができる | 不均衡なコスト設定では不適切な場合がある |
| マシューズ相関係数 (MCC) | 二値分類器の品質を評価するための指標 | 不均衡なデータセットでも有効 | 計算が複雑であり、理解するのが難しい場合がある |
| G-Mean | 真陽性率と真陰性率の幾何平均 | 不均衡なデータセットでも有効 | 特定のクラスへの重視が難しい場合がある |
| ROC-AUC | 偽陽性率が変化するときの真陽性率をプロットした曲線下面積を示す | 閾値選択に依存しないため、モデル全体の性能を評価できる | 不均衡なデータセットでは誤解を招く可能性がある |
| PR-AUC | 再現率が変化するときの適合率をプロットした曲線下面積を示す | 不均衡なデータセットでも有効 | 閾値選択に影響されやすい |
| pAUC | ROC曲線の一部分（特定のFPR範囲）の面積を示す、不均衡なデータセットに対するモデルの性能を評価する際に有用 | 特定のFPR範囲に焦点を当てられるため、部分的な性能評価が可能 | 全体的な性能評価は難しい場合がある |

# Pythonでの実装
今回はKaggleの「Stroke Prediction Dataset」を使用しました。データセットに含まれる様々な変数から脳卒中を発症するか否かを0,1で予測します。

データの詳細は以下
https://www.kaggle.com/datasets/fedesoriano/stroke-prediction-dataset/code

予測にはランダムフォレストを用い、各評価指標での結果を比較しました。実行環境はGoogle Colaboratoryです。

```python:予測モデルの作成.py
from sklearn.preprocessing import LabelEncoder
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix
import seaborn as sns
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from google.colab import drive

# データの読み込み
drive.mount('/content/drive/')

stroke_prediction_raw = pd.read_csv('/content/drive/MyDrive/Colab Notebooks/stroke_data.csv')
stroke_prediction_raw

# Nanが含まれるカラムを確認
stroke_prediction_raw.isna().any()
# データの重複とNanを削除
df = stroke_prediction_raw.drop_duplicates()
df = df.dropna()
df.isna().sum()

# 脳卒中の発症有無をカテゴリ変数ごとに確認
cols = ['gender','hypertension','heart_disease','ever_married','work_type','Residence_type','smoking_status']
plt.figure(figsize=(16,13))

for i in range(len(cols)):
    plt.subplot(3,3,i+1)
    sns.countplot(x=df[cols[i]],hue = df['stroke'],palette = 'bone')

# age, bmi, avg_glucose_levelの箱ひげ図を描画
num_cols = ['age','bmi','avg_glucose_level']
plt.figure(figsize=(15, 5))

for i in range(3) :
    plt.subplot(1,3,i+1)
    sns.boxplot(x=df[num_cols[i]],color='#6DA59D')
    plt.title(num_cols[i])
plt.show()


# 外れ値の除去
def detect_outliers(data,column):
    q1 = df[column].quantile(.25)
    q3= df[column].quantile(.75)
    IQR = q3-q1
    lower_bound = q1 - (1.5*IQR)
    upper_bound = q3 + (1.5*IQR)
    ls = df.index[(df[column] <lower_bound) | (df[column] > upper_bound)]
    return ls

index_list = []

for column in num_cols:
    index_list.extend(detect_outliers(df,column))

index_list = sorted(set(index_list))

df = df.drop(index_list)

#カテゴリ変数をダミー変数に変換
df_dummies = pd.get_dummies(data = df, columns =  ['gender','ever_married','work_type','Residence_type','smoking_status'],drop_first=True )


# 説明変数と目的変数にデータを分割
X = df_dummies.drop(columns = ['id','stroke'])
y = df_dummies.filter(items=['stroke'], axis=1)
scaler = StandardScaler()
X = scaler.fit_transform(X)


# 訓練データとテストデータに分割
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0 , stratify=y)
print('X_train', X_train.shape)
print('y_train', y_train.shape)
print('X_test', len(X_test))
print('y_test', len(y_test))
rfc = RandomForestClassifier(n_estimators=150,criterion='entropy',random_state = 123)
rfc.fit(X_train, y_train)
y_pred = rfc.predict(X_test)


# 混同行列を作成する
conf_matrix = confusion_matrix(y_test, y_pred, labels=[1, 0])
df_confusion_matrix = pd.DataFrame(conf_matrix, columns=["Positive", "Negative"], index=["Positive", "Negative"])


# 作成した混同行列を描画
sns.heatmap(df_confusion_matrix, fmt="d", annot=True, cmap="Blues", annot_kws={"fontsize": 20})
plt.xlabel("model output class")
plt.ylabel("actual class")
plt.show()
```

作成したモデルから脳卒中の発症有無について予測を行い、予測結果から作成した混合行列が以下です。

![混合行列_モデル１.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3093526/232f6281-fe78-2bf5-4f2e-ea478b9fd365.png)

この結果を見ると、Negativeという予測に偏っており、Positiveという予測は0です。これは明らかに誤った予測であり、そもそもの学習データに偏りがあったと考えられます。
（データの大部分が「Negative」の場合、すべて「Negative」と予測してしまえば当たる確率が高くなるため）

実際に目的変数となるStrokeの1,0の数を比較すると、以下のように1つまり「Positive（発症する）」のデータが、０「Negative（発症しない）」というデータよりかなり少ないことがわかります。

```python:
# 0/1の数を確認
df['stroke'].value_counts()
# 0 : 4124 , 1 : 136

```
## 評価指標の算出

各評価指標も算出してみます。

```python:評価指標の算出.py
import math
from sklearn.metrics import accuracy_score
from sklearn.metrics import precision_score
from sklearn.metrics import recall_score
from sklearn.metrics import f1_score
from sklearn.metrics import matthews_corrcoef
from sklearn.metrics import roc_auc_score
from sklearn.metrics import auc, precision_recall_curve
from sklearn.metrics import roc_auc_score

# G-Meanを算出するための関数
def g_mean_score(y_test, y_pred):
    # 混同行列を作成して各要素を変数に格納する
    tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel()
    # True Positive Rateを算出する
    tp_rate =  tp / (tp + fn)
    print("True Positive Rate:", round(tp_rate, 2))
    # True Negative Rateを算出する
    tn_rate = tn / (tn + fp)
    print("True Negative Rate:", round(tn_rate, 2))
    # G-meanはTrue Positive RateとTrue Negative Rateの幾何平均を計算
    return math.sqrt(tp_rate * tn_rate)

def evaluate(X_train, y_train, X_test, y_test):
    rfc.fit(X_train, y_train)
    y_pred = rfc.predict(X_test)
    accuracy = accuracy_score(y_test, y_pred)
    print(f"accuracy: {round(accuracy, 4)}")

    precision = precision_score(y_test, y_pred)
    print(f"precision: {round(precision, 4)}")

    recall = recall_score(y_test, y_pred)
    print(f"recall: {round(recall, 4)}")

    f1score = f1_score(y_test, y_pred)
    print(f"f1 score: {round(f1score, 4)}")

    mcc = matthews_corrcoef(y_test, y_pred)
    print(f"MCC: {round(mcc, 4)}")

    g_mean = g_mean_score(y_test, y_pred)
    print(f"G-Mean: {round(g_mean, 4)}")

    y_pred =rfc.predict_proba(X_test)[:, 1]
    roc_auc = roc_auc_score(y_test, y_pred)
    print(f"ROC-AUC: {round(roc_auc, 4)}")

    precision, recall, thresholds = precision_recall_curve(y_test, y_pred)
    pr_auc = auc(recall, precision)
    print(f"PR-AUC: {round(pr_auc, 4)}")

    p_auc = roc_auc_score(y_test, y_pred, max_fpr=0.2)
    print(f"pAUC: {round(p_auc, 4)}")

evaluate(X_train, y_train, X_test, y_test)
```

計算された各指標の数値は以下となりました。
| 指標                  | 値      |
|----------------------|---------|
| 正解率（Accuracy）     | 0.9683  |
| 適合率 (precision)    | 0.0     |
| 再現率（Recall）       | 0.0     |
| F1 Score              | 0.0     |
| MCC (Matthews Correlation Coefficient) | 0.0 |
| 真陽性率（True Positive Rate）    | 0.0     |
| 真陰性率（True Negative Rate ）  | 1.0     |
| G-Mean                | 0.0     |
| ROC-AUC               | 0.8246  |
| PR-AUC                | 0.1009  |
| pAUC (Partial AUC)    | 0.651   |

結果、正解率（全体の予測のうち正しく予測できた割合）と真陰性率（正しく陰性と判断したものの割合）、ROC-AUC（モデル全体の性能を評価する）の値は高くなっていましたが、それ以外の指標を見ると正しく予測できているとは言えません。

### データの偏りをなくしてモデル作成
データの偏りにより正しい予測ができていないことが判明したため、以下のようにリサンプリングを行うことでデータの偏りをなくし、再度予測を行いました。

（データが少ない方に合わせてサンプリングする方法もありますが、その場合、今回はデータの数がかなり少なくなってしまうため、リサンプリングをしています）

```python:均一データの作成.py
# リサンプリングで均一データを作成する
from sklearn.utils import resample

df_0 = df[df.iloc[:,-1]==0]
df_1 = df[df.iloc[:,-1]==1]

df_1 = resample(df_1,replace=True, n_samples=df_0.shape[0] , random_state=123 )
df = np.concatenate((df_0,df_1)) # 陽性データと陰性データをマージ
df = pd.DataFrame(df_adjust)
df.columns = ['id', 'gender', 'age', 'hypertension', 'heart_disease', 'ever_married','work_type', 'Residence_type', 'avg_glucose_level', 'bmi','smoking_status', 'stroke']
```

均一データで予測した結果から作成した混合行列は以下になります。
![混合行列＿モデル２.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3093526/627cf75f-c049-9d9f-8250-03455b7e48c9.png)

均一にしていない場合と比べて、Positiveなケースも予測されており、うまく分類できていることがわかります。

実際に評価指標を見てみても、以下のように全ての指標で良いスコアとなっていることがわかります。


| 指標                  | 値      |
|-----------------------|---------|
| 正解率（Accuracy）             | 0.9976  |
| 適合率（Precision）           | 0.9952  |
| 再現率（Recall）                | 1.0     |
| F1 Score              | 0.9976  |
| MCC (Matthews Correlation Coefficient) | 0.9952 |
| 真陽性率（True Positive Rate）   | 1.0     |
| 真陰性率（True Negative Rate）   | 1.0     |
| G-Mean                | 0.9976  |
| ROC-AUC               | 1.0     |
| PR-AUC                | 1.0     |
| pAUC (Partial AUC)    | 1.0     |


正解率、真陰性率、ROC-AUCといった指標は、データに偏りがあるときに誤った判断をしてしまう場合があることが、実際のデータを使って予測した結果からも明らかになりました。これらは別の評価指標と組み合わせて見る必要がありそうです。


## 多クラス分類における評価指標
ここまで、二値分類の評価指標を見てきました。多クラス分類では、これらの評価指標を拡張して使用します。

多クラス分類は、2つ以上の相互排他的なカテゴリ（例えば「赤」、「青」、「緑」）のいずれかに分類するタスクです。

多クラス分類においても二値分類と同様に混合行列を作成することができ、正解のクラス数✕予測のクラス数の行列で表現されます。このとき、行列の対角成分の値が大きいほど、モデルの予測と正解が一致しているといえます。

また、一つのクラスに注目してPositiveとし、それ以外をNegativeとしてTP、TN、FP、FNを割り当てることもできます。

例えば、A・B・C・Dというクラスに分類するタスクを考えるとき、クラスAに着目すると、各値は以下の図のようになり、TP_A、TN_A、FP_A、FN_Aと表されます。各値は注目するクラスによって変わってきます。
![スクリーンショット 2023-09-25 0.14.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3093526/7ab3e5c5-d8eb-5e99-7ce6-a090a7d6f53b.png)
（高柳 慎一,長田 怜士(2023). 『評価指標入門〜データサイエンスとビジネスをつなぐ架け橋』より）

### 多クラス分類における正解率（Accuracy）

多クラス分類における正解率は「モデルが予測したクラスと真のクラスの一致数 / 全体のデータ数」で算出されます。

例えば、先程の例と同様にクラスAに着目すると、クラスAの正解率は以下のように表されます。
```math
accuracy_A = \frac{ (TP_A + TN_A) } {(TP_A + TN_A + FP_A + FN_A)}

```

### micro平均とmacro平均

多クラス分類では、正解率（Accuracy）のほか、適合率（Precision）、再現率（Recall）、F1 Score、ROC-AUCなども算出可能です。 

ただし、多クラス分類では単純にPositive,Negativeのどちらかで分けることができないため、クラスの個数分だけPositive,Negativeのパターンが作られます。そして、それぞれのパターンごとに算出した評価指標の平均によって評価を行います。

このときの平均の取り方として、Micro平均、Macro平均、加重平均があります。


- Micro平均
全体の混合行列で見たときのTP、FP、FNを使って算出する方法です。クラスの偏りは考慮することができず、すべてのクラスが均一なモデルに対して有用です。適合率（Precision）、再現率（Recall）、F1 ScoreのMicro平均は正解率（Accuracy）と一致します。

- Macro平均
各クラスごとの評価指標の値を計算し、その後にそれらの加算平均を取ることで算出する方法です。クラスの偏りを考慮して評価することができます。すべてのクラスが同程度に重要な場合に適しています。

- 加重平均
各評価指標について加重平均を取って算出する方法です。加重平均の重みには各クラスのサンプル数を使用します。

## まとめ
分類タスクにおける評価指標をまとめました。単純に正解率だけをみる、といったことをすると、不均衡データの場合には判断を見誤る可能性があることを実例からも確認できました。

状況に合わせて適切な評価指標を選択すること、複数の評価指標で判断することが重要になりそうです。

## 参考文献・参考サイト
・評価指標入門
・Stroke Prediction| Ensemble Learning For Beginners
https://www.kaggle.com/code/abdulrahmankhaled1/stroke-prediction-ensemble-learning-for-beginners
・2値分類の不均一データ対策って実際効果あるんかい
https://toeming.hatenablog.com/entry/2021/03/09/binary_unevenness_jissai
・二値、多値、多ラベル分類タスクの評価指標
https://qiita.com/jyori112/items/110596b4f04e4e1a3c9b