【Kaggle】学業成績データセットによる分類コンペ

# 1. 本投稿の目的
- データ分析・機械学習の学習を実ビジネスに落とし込む練習のため
- 様々なライブラリ・コードに触れ、引き出しを増やすため

# 2. 今回取り扱うKaggleコンペテーマ
## 2-1.コンペの概要  
テーマ：**Classification with an Academic Success Dataset**
和訳すると、「学業成績データセットによる分類」

2024年の Kaggle Playground シリーズの１つです。
Kaggle Playground とは、Kaggleの運営サイドが私のような機械学習を学習中の人に向けて提供している、機械学習スキルを練習するための興味深く取り組みやすいデータセットです。

このコンペの目標は、「高等教育を受ける学生の学業リスクを予測すること」です。


https://www.kaggle.com/competitions/playground-series-s4e6/overview

35個の説明変数があり、目的変数は「Target」です。
Targetには「Graduate:卒業」「Dropout:中退」「Enrolled:在籍中」の3つがあり、テストデータのTargetを予想します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3780099/036e5e1d-2af2-d5cb-cd04-de42ebfd6d29.png)


## 2-2.今回取り扱うノートブック

今回は以下のノートブックの要点をまとめます。
ユーザーからの投票数で1位、スコアも金メダルの素晴らしいコードです。

https://www.kaggle.com/code/michaelxyjonathan/s04e06-academic-success-eda-and-classification/notebook

書いたのは中国の学生の方のようですが、機械学習の初心者とのこと。
本当にすごい。見習わねば。


# 3.学び３選

先に今回の学び（結論）をまとめておきます。具体的なコードは項目４にて。

## 3-1.学びその１「カーネル密度推定プロットによるデータ可視化」

探索的データ分析の段階で、使われている「カーネル密度推定プロット」について、初見でしたが、数値変数を非常にわかりやすく可視化してくれました。ぜひ今後の分析に使おうと思います。

## 3-2.学びその２「比較するモデルの引き出し増」

今回6つのモデルを比較してスコアを検証していますが、GXGBoost,CatBoostは初めて使いました。時間とリソースに限りはあるので比較できるモデル数にも限りがありますが、今回CatBoostが高スコアを出したこともあり、どのモデルで比較をするかは大切にしようと思います。
特に今回のように、モデルごとの強み・弱みを整理した上で、根拠を持ってモデル比較できたら強いなと思いました。

## 3-3.学びその３「特徴量ごとの重要度の可視化」

本コードではスコアの算出の後に、特徴量ごとに重要度を可視化しています。これにより、データを根拠を持って説明できるため、実生活やビジネスにも適用できることが想像できました。


# 4.コードの要所を解説

ここからは、私が特に勉強になったコードを解説していきます。


## 4-1.探索的データ分析(EDA)のわかりやすさ

前回のQiita投稿で、EDAの重要性とその数々の手法を学びました。

https://qiita.com/setowatson/items/d90fcc9478061d923481

今回のノートブックでも、モデリング前のデータ可視化がわかりやすく、非常に参考になりました。特に以下２つの手法です。

### 4-1-1.カーネル密度推定(KDE)プロット

本notebookでは、数値変数をカーネル密度推定(EDA)プロットによって、可視化しています。

カーネル密度推定プロットとは、「数値変数の分布を視覚化するため。各変数の分布を個別のサブプロットとして描画するもの」です。横軸（x軸）に各数値変数の値、縦軸（y軸）に確率密度を示しています。特にy軸は、特定の値の周りのデータの密度を表しており、データがどの範囲に集中しているかを示します。ただのヒストグラムより、正確な分布がわかることと直感的にもわかりやすいため、今回知ることができてよかったです。

以下コードです。

```ruby:
# Distribution of numeric variables
plt.figure(figsize=(18, 40))
plotnumber = 1

for column in num_cols:
    if plotnumber <= len(num_cols): # セーフガード
        ax = plt.subplot(9, 3, plotnumber)
        #Seabornライブラリ内のKDEプロット 
        sns.kdeplot(train[column], color='deepskyblue', fill=True)
        for spine in ax.spines.values():
            spine.set_visible(True)
            spine.set_color('black')
            spine.set_linewidth(0.5)
        plt.xlabel(column)
        ax.grid(False)
        
    plotnumber += 1

plt.suptitle('Distribution of Numeric Variables', fontsize=40, y=1)
plt.tight_layout()
plt.show()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3780099/48fd4382-0512-20b1-b4f8-76c857a1fa58.png)

例えば、一番左上の「Application mode(おそらく試験かプログラムのコース)」だと、0付近のデータが一番多く、続いて10後半と40前後がほとんど同等の量で分布しています。データ自体はあまりばらついておらず、また対称性もありません。本投稿では省略していますがKaggel記事内のデータ探索と照らしあわせると、Application modeについては22個のint型のキーがあるが、一定のキーに集中しており、密度は高いと言えそうです。

EDAプロットを使用すれば、こうしたデータの概要や仮説を視覚的に得ることができます。


### 4-1-2.相関行列Correlation Matrix

もう1つは、相関行列の可視化です。
今回のコンペには目的変数も合わせて全部で36の変数がありますが、それらを表にしています。
相関行列はいろんな可視化方法がありますが、今回はヒートマップにしてあるため、こちらも視覚的にどの要素が関わり合いを持っているか一瞬で仮説を立てることができます。
以下コードです。


```ruby:
from sklearn.preprocessing import LabelEncoder
# カテゴリ変数であるTargetのラベルエンコーディング
categories = ['dropout', 'enrolled', 'graduate']
label_encoder = LabelEncoder()

train['Target'] = label_encoder.fit_transform(train['Target'])
plt.figure(figsize=(21, 18))
# Seabornライブラリを使用してヒートマップを作成
sns.heatmap(train.corr(), annot=True, cmap='coolwarm', fmt='.1f', linewidths=2, linecolor='lightgrey')
plt.suptitle('Correlation Matrix', fontsize=40, y=1)
plt.show()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3780099/5db9bd7b-e667-1b11-3415-3ac69e6315a0.png)

x軸一番右のTargetを確認すると、
 'Curricular units 1st sem (approved)',
 'Curricular units 1st sem (grade)',
 'Curricular units 2nd sem (approved)',
 'Curricular units 2nd sem (grade)',
などが特に強い相関を持っていることがわかります。
当たり前のように思えますが、卒業可否には学期末の成績・到達度が大きく関わっているとデータからもわかります。


## 4-2.K-Fold交差検証の利便性

続いてはモデル評価のための下準備です。

本notebookでは、K-Fold交差検証をモデル評価に利用しています。K-fold法は私も以前使用したことがあるため、復習要素が大きいですが、モデル評価の基盤となる大事なフェーズのため、解説を入れておきます。

K-Foldのメリットはいくつかありますが、代表的なものは「モデルの汎化性能を高め、過学習を防ぐこと」です。今回はデフォルトのまま、データを10分割しており、1つをテスト用、残りを訓練用という役割を順繰りに10回行っています。これにより、特定のデータだけに特化したモデルにならないよう評価をすることができます。ただ、単純なモデル評価(ホールドアウト法など)より時間を要することがデメリットです。

どちらの方法を選択するかは、データセットのサイズや利用可能な計算リソースに依存しますが、一般的に小さなデータセットの場合、ホールドアウト法が適しているかもしれません。今回のように大規模なデータセットの場合は交差検証法がより適していると思われます。

以下コードです。

```ruby:
# 特徴量と目的変数の分割
X_train = train[initial_features]
y_train = train['Target']
X_test = test[initial_features]

from sklearn.model_selection import KFold
from sklearn.metrics import accuracy_score

# k-fold法を実施する関数の定義　
def cross_validate_model(model, X_train, y_train, params, n_splits=10):
    # KFoldインスタンスの作成。データを10個のフォールドに分割。
    cv = KFold(n_splits=n_splits, shuffle=True, random_state=0)
    val_scores = [] # 各フォールドの検証精度を格納するリスト

    # 各フォールドで訓練と検証を実行
    for fold, (train_ind, valid_ind) in enumerate(cv.split(X_train)):
        X_fold_train = X_train.iloc[train_ind]
        y_fold_train = y_train.iloc[train_ind]
        X_val = X_train.iloc[valid_ind]
        y_val = y_train.iloc[valid_ind]
        
        clf = model(**params)
        clf.fit(X_fold_train, y_fold_train)
        
        y_pred_trn = clf.predict(X_fold_train)
        y_pred_val = clf.predict(X_val)
        train_acc = accuracy_score(y_fold_train, y_pred_trn)
        val_acc = accuracy_score(y_val, y_pred_val)
        print(f"Fold: {fold}, Train Accuracy: {train_acc:.5f}, Val Accuracy: {val_acc:.5f}")
        print("-" * 50)
        
        val_scores.append(val_acc)

    average_val_accuracy = np.mean(val_scores)
    print("Average Validation Accuracy:", average_val_accuracy)

    return clf, average_val_accuracy
```

本noteboookでは、いくつかのモデルでスコアを比較していますが、どのモデルもすべてこのk-fold法で評価しています。これから行うモデルの学習とスコアの算出について、時間はかかるもののコーティング自体はすぐ終わるため、データの下処理とモデル評価方法の選定がいかに重要かがわかります。


## 4-3.複数モデルでのスコア比較

ここからはモデル学習、スコア評価に入ります。
本コードでは、合計6つのモデルを比較しています。
本投稿で詳しく取り扱うのは、1番高いスコアを示したCatBoostだけですが、ほかのモデルの概要を記しておきます。

モデル|特徴|強み|弱み
---|---|---|--- 
Random Forest|多数の決定木を使ったバギング|過学習を防ぐ、高い精度、不均衡データに強い|訓練に時間がかかる、大規模モデルはメモリを多く消費する|
AdaBoost|誤分類されたサンプルに重点|過学習を防ぐ、高い精度|ノイズに敏感、多くの弱い学習器を必要とする|
Gradient Boosting|誤差修正のために新しいモデルを追加するブースティング|高い精度、柔軟性が高い|訓練に時間がかかる、過学習しやすい|
XGBoost|Gradient Boostingの高速化および性能向上|高速な訓練と予測、高い精度、過学習の抑制|ハイパーパラメータの調整が複雑|
CatBoost|カテゴリデータの処理に特化したGradient Boosting|カテゴリデータを効果的に処理、高い精度|適用範囲が限られる場合がある|
LightGBM|Gradient Boostingの軽量化および高速化|高速な訓練と予測、高い精度、大規模データに強い|小さなデータセットでは過学習しやすい|


メリット・デメリットはありますが、どのモデルがいいかは一概には言えません。データセットによって、どのモデルがふさわしいかを確認するため、こうして同時に複数モデルを比較する場合が多いです。
個人的にはLightGBMで数回高いスコアが出た経験があるため、なんだか思い入れがあります。


### 4-3-1. Categorical Boosting (CatBoost)

それでは、今回1番精度の高かったCatBoostについて、解説していきます。

https://docs.kanaries.net/ja/topics/Python/catboost

CatBoostの大きな特徴として、カテゴリデータの処理に特化している点があります。

それはCatBoostが、「カテゴリデータを直接処理できる機能を持っているから」です。これにより、カテゴリデータを数値に変換するための前処理（エンコーディング）を行う必要がありません。

通常のエンコーディング手法ではカテゴリデータをone-hotエンコーディングやラベルエンコーディングなどの手法で数値に変換しますが、これらの手法は情報の損失や次元爆発が発生することがあります。

CatBoostは、独自のエンコーディング（ordered boostingと呼ばれるエンコード）で解決します。

以下コードです。
```ruby:
from catboost import CatBoostClassifier
cat_params = {'verbose': 0}  
print('CatBoost Cross-Validation Results:\n')
cat_model, cat_mean_accuracy = cross_validate_model(CatBoostClassifier, X_train, y_train, cat_params)


# テストデータの予測とラベルエンコーディングの逆変換
cat_preds = cat_model.predict(X_test)
cat_preds_labels = label_encoder.inverse_transform(cat_preds)

# 予測結果をCSVファイルに保存
cat_result = pd.DataFrame(X_test.index)
cat_result['Target'] = cat_preds_labels
cat_result.to_csv('result_cat.csv', index=False)
cat_result
```

CatBoostの特徴「カテゴリ変数を数値ラベルにエンコーディングしている点」があるので、最後は逆の実行「予測結果を数値ラベルから元のカテゴリラベルに変換」をする必要があります。

最後に、6つの学習モデルの精度スコアを比較・精度順にソートした結果の表示したものだけ記載しておきます。

```ruby:
accuracy = pd.DataFrame({
    'Model': ['Random Forest', 'AdaBoost', 'Gradient Boosting', 'XGBoost', 'CatBoost', 'LightGBM'],
    'Score': [rf_mean_accuracy, ada_mean_accuracy, gb_mean_accuracy,
              xgb_mean_accuracy, cat_mean_accuracy, lgb_mean_accuracy]
})

accuracy_sorted = accuracy.sort_values(by='Score', ascending=False)
accuracy_sorted
```
　|Model|Score|
---|---|---|
4|CatBoost|0.831765| 
5|LightGBM|0.831713|
3|XGBoost|0.831569|
2|Gradient Boosting|0.826917|
0|Random Forest|0.826054|
1|AdaBoost|0.818971|

## 4-4.特徴量ごとの重要度の可視化

本コードでは、最後に「特徴量の重要度」を可視化しています。
この部分はかなり勉強になりました。ただ高いスコアが出た！というだけでなく、「なぜ高く出たのか？」「何が要因だったのか？」とスコアが出た後に言える考察にまでしっかりと目を向けているんですね。こうしたアクションがビジネスでは必須だと感じています。


```ruby:
def plot_feature_importances(model, model_name, color_scale='Reds', dataframe=None):
    if dataframe is None:　#念の為dataframeがNoneでないことを確認
        raise ValueError("Dataframe cannot be None and must contain the feature names.")　
        
    # モデルの特徴量重要度を抽出
    importances = model.feature_importances_
    indices = np.argsort(importances)[::-1]
    feature_names = dataframe.columns　 # 特徴量の名前を取得

    feature_importances = pd.DataFrame({
        'Feature': feature_names[indices],
        'Importance': importances[indices]
    })

    fig = px.bar(feature_importances.sort_values('Importance', ascending=True), 
                 x='Importance', 
                 y='Feature',
                 title=f"Feature Importances in {model_name}",
                 labels={'Importance': 'Importance', 'Feature': 'Feature'},
                 height=1400,
                 color='Importance',
                 color_continuous_scale=color_scale)

    fig.update_layout(xaxis_title='Importance', yaxis_title='Feature')

    return fig
    
# CatBoostの特徴量分析
model_name = 'CatBoost'
fig = plot_feature_importances(cat_model, model_name, 'Temps', X_train)
fig.show()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3780099/27eeab3c-aabc-5049-7ad5-d0d8f45b4949.png)

これにより、どの特徴量が重要だったかが一目でわかります。
重要だったのは、相関行列で予想できていたCurricular units（単位や成績）だけでなく、
Course(おそらく科目や文理等のコース)やTuition fees up to date （最新の授業料）など、カテゴリ変数も含まれていることから、CatBoostのスコアが高く出たのかと思われます。
（とは言っても、どのモデルも高いスコアが出ていますが…）

以上がコードの要点の解説です。
ビジネスでの活用・再現性を意識して、コードの写経・解説に取り組みました。