カテゴリ値を含むデータでのLightGBMを使った分析（２本目：One-hot Encoding編）

# はじめに

[前回の投稿](https://qiita.com/Blaster36/items/d0a99f9d4f6b12a2dd83)ではKaggleのデータセット[^1]を使って二値分類問題にチャレンジしました。
その際、カテゴリ値の取扱い方法としては、**Label Encoding**を採用しました。

https://qiita.com/Blaster36/items/d0a99f9d4f6b12a2dd83

が、他の方法もあるので、後学のために本稿では**One-hot Encoding**にチャレンジします（LightGBMを利用するなど問題設定は同じ）。

## 本稿で紹介すること

- データの収集および整形
- 予測モデルの作成
- 予測精度の確認
- 特徴量重要度の確認（可視化）

前回の投稿記事からの**変更部分を中心**に、説明します。

## 本稿で紹介しないこと

- 特徴量設計
- ハイパーパラメータのチューニング

# 分析環境の各種情報

:::note info
筆者は、いつもながらDockerコンテナとしてJupyterを起動しています。
:::

- Python 3.8.8
- pip 21.3.1
- pandas 1.4.0
- numpy 1.22.1
- scikit-learn 1.0.2
- lightgbm 3.3.2 

# データの収集

前回に同じです。

# データの整形

Nullレコードを除去するところまでは前回に同じです。

前回からの変更部分は、今回の主題となるカテゴリ値の処理です。以下を参考に、カテゴリ値はOne-hot化しました。

https://qiita.com/Hyperion13fleet/items/afa49a84bd5db65ffc31

https://qiita.com/FukuharaYohei/items/06cc7b1c5eb124db6cdf

元データが15カラム（含む目的変数）だったのに対し、処理後は105カラム（含む目的変数）になりました。

各カラム内の1つのカテゴリ値が、1カラムとして独立するので、カラム数が大きく増えるのは納得でした。
（対象データは、母国語をカラムが含まれており、これが40カテゴリだったことが一因。）

例えば、整形処理はこんな感じ。説明変数の計8カラムを整形しました。

```python
columns_cat = [
    'workclass',
    'education',
    'marital-status',
    'occupation',
    'relationship',
    'race',
    'gender',
    'native-country',
    #'income',
]
:
#!pip install category_encoders
import category_encoders as ce

# One-hot Encodingしたい列をリストで指定
ce_ohe = ce.OneHotEncoder(cols=columns_cat, handle_unknown='impute')

# DataFrameを渡してOne-hot Encodingを適用
df = ce_ohe.fit_transform(df)
df.head(10)
```

後々の特徴量重要度を確認する際に必要になるので、変換処理での対応関係も確認しました。
変換前のカラム名に対し、変換後のカラム名_${Index値}のIndex値を教えてくれました。

```python
# 変換前：文字列と変換後：数値の対応関係を確認
ce_ohe.category_mapping
```

```shell
:
 {'col': 'marital-status',
  'mapping': Never-married            1
  Married-civ-spouse       2
  Widowed                  3
  Separated                4
  Divorced                 5
  Married-spouse-absent    6
  Married-AF-spouse        7
  NaN                     -2
  dtype: int64,
  'data_type': dtype('O')},
:
```

# 予測モデルの作成

こちらも、前回に同じです。
こちらのサイトで公開されているPythonコード[^2]を参考にしました。

```python
# LightGBM のハイパーパラメータ
params = {
    # 二値分類問題
    'objective': 'binary',
    # AUC の最大化を目指す
    'metric': 'auc',
    # Fatal の場合出力
    'verbosity': -1,
    # 乱数シード
    'seed': 31,
    # 学習率
    'learning_rate': 0.02,
}

# 上記のパラメータでモデルを学習する
model = lgb.train(params, lgb_train, valid_sets=lgb_valid,
                  verbose_eval=50,  # 50イテレーション毎に学習結果出力
                  num_boost_round=10000,  # 最大イテレーション回数指定
                  early_stopping_rounds=100
                 )
```

```shell
Training until validation scores don't improve for 100 rounds
[50]	valid_0's auc: 0.913351
[100]	valid_0's auc: 0.917335
/opt/conda/lib/python3.8/site-packages/lightgbm/engine.py:181: UserWarning: 'early_stopping_rounds' argument is deprecated and will be removed in a future release of LightGBM. Pass 'early_stopping()' callback via 'callbacks' argument instead.
  _log_warning("'early_stopping_rounds' argument is deprecated and will be removed in a future release of LightGBM. "
/opt/conda/lib/python3.8/site-packages/lightgbm/engine.py:239: UserWarning: 'verbose_eval' argument is deprecated and will be removed in a future release of LightGBM. Pass 'log_evaluation()' callback via 'callbacks' argument instead.
  _log_warning("'verbose_eval' argument is deprecated and will be removed in a future release of LightGBM. "
[150]	valid_0's auc: 0.922076
[200]	valid_0's auc: 0.924902
[250]	valid_0's auc: 0.926122
[300]	valid_0's auc: 0.92682
[350]	valid_0's auc: 0.92731
[400]	valid_0's auc: 0.9276
[450]	valid_0's auc: 0.927851
[500]	valid_0's auc: 0.928042
[550]	valid_0's auc: 0.928216
[600]	valid_0's auc: 0.928317
[650]	valid_0's auc: 0.928426
[700]	valid_0's auc: 0.928446
[750]	valid_0's auc: 0.928452
[800]	valid_0's auc: 0.928403
Early stopping, best iteration is:
[710]	valid_0's auc: 0.928479
```

# 予測精度の確認

こちらも、前回に同じです。
こちらのサイトで公開されているPythonコード[^3]を参考にしました。

実行すると、こんな感じ。想像以上に精度が良かった。
前回に比較すると、精度自体はほんの少し下がりました。
が、有意な差ではなく、乱数シードの差異かなというレベルでした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685193/5dbfb3f3-bb7d-5279-671a-5f37016e5637.png)

# 特徴量重要度の確認（可視化）

こちらも、前回に同じです。
特徴量重要度の確認ですが、lightgbm.plot_importanceを使いました。
こちらのサイトで公開されているPythonコード[^4]を参考にしました。

実行すると、こんな感じ。特徴量重要度の上位銘柄はあまり変わらず、順位が変動した印象でした。
が、One-hot化によりカテゴリ値を扱うカラムに対して、予測精度に貢献した具体的なカテゴリ値を知ることができ、より予測結果に対して理解を深められました。

importance_type='split'として可視化した結果。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685193/5e39a4da-92e4-520b-1e74-d6b68981b99c.png)

importance_type='gain'として可視化した結果。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685193/bf761abf-3cd5-7273-34fa-816eade2ec26.png)

特に、2つ目の方式、gainの方をもう少し詳しく確認しました[^4]。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685193/876317a0-9498-1ed7-fd20-44d13e1396f9.png)

# 結果について少し考察

特徴量重要度についてです。
特徴量重要度のTopは「**Married-civ-spouse**」であり、個人の婚姻歴として現在進行形（既婚で配偶者あり）だったことが判明しました。つまり、収入が高いから家庭（≒配偶者）をもてたのだろうな、、、という意味付けですね。
> relationship: Wife, Own-child, Husband, Not-in-family, Other-relative, Unmarried.
> marital-status: Married-civ-spouse, Divorced, Never-married, Separated, Widowed, Married-spouse-absent, Married-AF-spouse.

# Notebook（Pythonコード）

githubで公開しました。このタイミングですが、参考サイトの方々に感謝申し上げます。

https://github.com/Blaster36/Script/blob/main/Kaggle/pred50k-3.ipynb

# まとめ

カテゴリ値を含むデータをOne-hot EncodingしてLightGBMを使った分析の例をご紹介。

category_encodersというPythonライブラリ、非常に便利でした。
One-hot Encodingにより対象データのカラム数こそ増えはしますが、Label Encodingでの分析結果に対して更に理解を深められた、というのが1つの成果だと思いました。

[^1]: Adult Data Set | https://archive.ics.uci.edu/ml/datasets/adult
[^2]: コード | https://qiita.com/d_desuyon/items/807e01311ad08570ee78#%E3%82%B3%E3%83%BC%E3%83%89
[^3]: モデルの評価 | https://qiita.com/kt38k/items/3c0ee4251475b6407007#%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AE%E8%A9%95%E4%BE%A1
[^4]: 特徴量重要度の算出 (2値分類編) | https://mathmatical22.xyz/2020/04/12/%E3%80%90%E5%88%9D%E5%BF%83%E8%80%85%E5%90%91%E3%81%91%E3%80%91%E7%89%B9%E5%BE%B4%E9%87%8F%E9%87%8D%E8%A6%81%E5%BA%A6%E3%81%AE%E7%AE%97%E5%87%BA-lightgbm-%E3%80%90python%E3%80%91%E3%80%90%E6%A9%9F/