決定木モデルを可視化しよう！

# はじめに
皆さんはPythonのライブラリである`dtreeviz`について知っていますか？
今回は`dtreeviz`を用いたLightGBMの決定木モデルを可視化していこうと思います。

# dtreevizとは
決定木（Decision Trees）の視覚化をサポートしたPythonライブラリです。決定木の各ノードでの決定の詳細を色分けされたノードやパス、ヒストグラムなどのリッチなグラフィクスで表現することができます。

主な機能としては
1. ツリーの各ノードでのデータの分布を示すヒストグラムやバープロット
1. クラスの分布や平均値などのツリーの各ノードの詳細情報
1. ノードの色分けによるクラスや目的変数の値の分布の視覚化
1. ターゲット変数の分布や主要な特徴量の影響を示すシャドウツリーの表示

があります。

詳細は以下のGitリポジトリを参考にしてみてください。
今回は、私がよく使用しているLightGBMのモデルを可視化していこうと思います。

https://github.com/parrt/dtreeviz

# コード内容
実行環境はvscodeのJupyterNotebookを使用します。
必要に応じてファイルを作成してください。
pythonのバージョンは3.11を使用しています。

まずは`dtreeviz`をインストールしましょう。
```
$ pip install dtreeviz
```

それではライブラリをインポートしましょう。
```python
import numpy as np
import pandas as pd
import lightgbm as lgb
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
import plotly.express as px
import dtreeviz

random_state = 0
```

シード値は今回は固定しておきます。
0でも42でもお好きな数字を選択してください。
データに再現性を持たせるために指定しているだけですので、私と同じグラフを作成して確認していくのであれば`random_state = 0`としてください。

---

ではデータを読み込みます。
今回はネット上からタイタニックのデータを引っ張ってきます。
```python
import pandas as pd
dataset_url = 'https://raw.githubusercontent.com/parrt/dtreeviz/master/data/titanic/titanic.csv'
dataset = pd.read_csv(dataset_url)

dataset.head()
```

![g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/2d541e3f-adb1-25a3-a361-61aa0682d8ab.png)
はい。お馴染みのデータですね。
では、データの前処理を行っていきましょう。
`Age`カラムの欠損値を平均値で補完し、`Sex`、`Cabin`、`Embarked`それぞれのカラムをカテゴリー変数に変換します。
```python
dataset.fillna({'Age':dataset.Age.mean()},inplace=True)
dataset['Sex_label'] = dataset.Sex.astype('category').cat.codes
dataset['Cabin_label'] = dataset.Cabin.astype('category').cat.codes
dataset['Embarked_label'] = dataset.Embarked.astype('category').cat.codes
```
今回は分類モデルと回帰モデルの2パターンで可視化していきます。


## 分類モデル
今回モデルの学習に使用する特徴量とターゲットを指定し、トレーニングデータとテストデータに分割します。
また、モデル用のトレーニングデータとバリデーションデータのデータセットを作成しておきます。
ここでもシード値を選択しています。値はお任せします。

データセットは引数として**data：特徴量**、**label：ターゲット**、**feature_name：特徴量の名前**、**categorical_feature：カテゴリカル特徴量**を設定してますが、特徴量とターゲット以外はなくてもOKです。

```python
features = ['Pclass','Sex_label','Embarked_label','Age','Cabin_label','Fare']
target = 'Survived'

X_train,X_test,y_train,y_test = train_test_split(dataset[features],dataset[target],test_size=0.2,random_state=42)

train_data = lgb.Dataset(data=X_train,label=y_train,feature_name=features,categorical_feature=['Sex_label','Pclass'])
valid_data = lgb.Dataset(data=X_test,label=y_test,feature_name=features,categorical_feature=['Sex_label','Pclass'])
```
---

次にLightGBMのハイパーパラメータの設定をし、さっそくモデルを学習させていきます。
ここでは、バリデーションデータとしてトレーニングデータとバリデーションデータの両方を指定しています。

両方指定する理由は
- モデルの評価とチューニング: バリデーションデータセットを使用することで、モデルの性能を評価し、ハイパーパラメータの調整（ハイパーパラメータチューニング）を行うための指標を持つことができます。モデルの性能が過剰適合または過小適合しているかどうかを確認し、モデルの改善ポイントを特定するのに役立ちます。
- 過学習の検出: トレーニングデータセットでの性能が高い場合、モデルが過学習している可能性があります。バリデーションデータセットを使用することで、モデルが新しいデータにどれだけ汎化できるかを評価し、過学習の兆候を検出できます。
- アンサンブル学習: バリデーションデータを使用して複数のモデルをトレーニングし、それらのモデルをアンサンブル（組み合わせ）することができます。これにより、モデルの予測性能を向上させることができます。
- モデルの信頼性向上: バリデーションデータを使用してモデルを評価することは、モデルの予測の信頼性を高めるのに役立ちます。モデルがバリデーションデータに対してうまく機能する場合、そのモデルは新しい未知のデータに対しても信頼性が高い可能性が高くなります。
- 早期停止: バリデーションデータを使用して、トレーニング中に早期停止（early stopping）を実施することができます。モデルの性能が改善しなくなったときにトレーニングを中断し、過学習を防ぎます。

私も調べながら初めて知りました(;´･ω･)
やはり気になったことは調べるべきですね！

```python
lgbm_params = {
    'boosting_type':'dart',
    'application':'binary',
    'learning_rate':0.05,
    'min_data_in_leaf':2,
    'feature_pre_filter':False,
    'feature_fraction':0.7,
    'num_leaves':41,
    'drop_rate':0.15,
    'max_depth':4,
    "seed":1212}

lgbm_model = lgb.train(lgbm_params,train_data,valid_sets=[train_data,valid_data],verbose_eval=False)
```

--- 

ここでようやく`dtreeviz`の出番です。
可視化を行います。
それぞれの引数の説明をします。
`lgbm_model`：可視化させたいモデル
`tree_index`：LightGBMはアンサンブル学習であるため、複数の決定木の中からインデックスを指定して可視化
`X_train`：特徴量
`y_train`：ターゲット
`feature_name`：特徴量の名前
`target_name`：ターゲット名
`class_names`：分類名（今回はSurvivedがターゲットのため`persih：死亡`、`survive：生存`の2つを指定）

```python
viz_model = dtreeviz.model(lgbm_model,tree_index=1,
                        X_train=dataset[features],y_train=dataset[target],
                        feature_names=features,
                        target_name=target,class_names=["perish","survive"])
```

---

では、まずはグラフを作成しましょう。
ここでは、指定したインデックスの決定木の全体像を可視化します。
```python
viz_model.view()
```

![g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/cbff33ee-c2af-6718-7c6a-e38d22716a38.png)

ちなみに横向きにしたかったら、引数に`orientation = 'LR'`を入れてください。
```python
viz_model.view(orientation = 'LR')
```
![g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/734f7128-d0c2-1ec1-ec88-e1cf69c12dcc.png)
---

引数に`fancy=False`を入れることで、視覚化のディテールや装飾をシンプルにすることができます。ツリーのノードやエッジの色、ヒストグラムやデータの分布などのリッチなグラフィクスが省略され、ツリーの主要な決定のみを強調したベーシックな決定木の視覚化が得られます。

```python
viz_model.view(fancy=False)
```
![g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/9e540662-0a42-4a4e-a25c-6babdcc2e707.png)
---

特定の深さの範囲の決定木を表示してみましょう。

```python
viz_model.view(depth_range_to_display=(1,2))
```
`depth_range_to_display=(1,2)`は表示したい決定木の深さの範囲を指定しています。
つまり、決定木の1～2の深さの部分のみを視覚化しています。
これは、非常に大きな決定木を持つときや、特定の部分のみに焦点を当てたいときに役立ちます。

![g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/96d62f43-dd81-da96-daec-f720c6cab140.png)
---

データセットから特定の行を1つ取ってきてみましょう。
```python
x = dataset[features].iloc[5]
```
今回は6行目を取ってきてみます。
では、この特定の行に対して、決定木の中でその行のデータがどのように分類または予測されるかを追跡してみましょう。
このメソッドを使用すると、指定された行が決定木を通過する際のパスを視覚的にハイライトすることができます。特定の行のデータがどのような条件や決定を経て、最終的な結果（つまり、どの葉ノードに到達するか）に至るのかを理解することができます。
```python
viz_model.view(x=x)
```
![g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/ed854a67-998a-02cc-7dd6-bf361f06069f.png)

また、特定の行のデータのみを表示することもできます。

```python
viz_model.view(x=x,show_just_path=True)
```

![g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/1c1e4e22-1727-1b50-60cb-103f68370b12.png)

また、特定の行のデータが決定木を通過する際のパスをテキスト形式で表示することもできます。
```python
print(viz_model.explain_prediction_path(x))
```
```
出力結果：
Age < 6.5
10.51 <= Fare  < 21.04
Pclass in {3}  
```
このメソッドから次のような情報が得られます。
- $x$が通過する各ノードでのテスト条件
- どのノードでどの方向（例：左の子ノードまたは右の子ノード）へ移動したか
- 最終的に$x$が到達する葉ノードとその葉ノードでの予測またはクラス分類

---


決定機のすべての葉ノードに到達するサンプル数を可視化してみましょう。
各葉ノードがどれだけのデータポイントをカバーしているかを理解できます。

```python
viz_model.leaf_sizes()
```
![g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/e3fefa26-dcfc-c6fa-f154-c44f3dc6b41f.png)
さらに各葉ノードに到達するサンプルのクラス分類をしてみましょう。
具体的には、各葉ノードでの各クラスのサンプル数を可視化することで、各葉ノードでのクラスの分布や主要なクラスを理解できます。

```python
viz_model.ctree_leaf_distributions()
```
![g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/4c49eec6-e99c-0f19-5efa-b89bd33c3077.png)
---

指定したノードIDに関する統計情報を確認することもできます。
このメソッドから次のような情報が得られる可能性があります。
- ノードでのテスト条件や分割条件。
- そのノードに到達するサンプル数。
- 分類問題の場合、ノードに到達するサンプルのクラス分布。
- 回帰問題の場合、ノードに到達するサンプルの目的変数の平均や分散などの統計情報。
- ノードの深さや位置情報。
- 子ノードや親ノードとの関連情報。

```python
viz_model.node_stats(node_id=21)
```
![g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/e1ae5acd-f9b4-ee68-6b27-33c62933b432.png)

## 回帰モデル
同様にLightGBMでモデルを学習させます。
ここではターゲットを`Age`とし、回帰モデルを作成します。
```python
features_reg = ["Pclass", "Fare", "Survived", "Sex_label", "Cabin_label", "Embarked_label"]
target_reg = "Age"

X_train_reg, X_test_reg, y_train_reg, y_test_reg = train_test_split(dataset[features_reg], dataset[target_reg], test_size=0.2, random_state=42)

categorical_feature=["Pclass", "Survived", "Sex_label"]
train_data_reg = lgb.Dataset(data=X_train_reg, label=y_train_reg)
valid_data_reg = lgb.Dataset(data=X_test_reg, label=y_test_reg)

lgbm_params = {
    'num_tree':10,
    'boosting': 'dart',          # dart (drop out trees) often performs better
    'application': 'regression_l1',     
    'learning_rate': 0.05,       # Learning rate, controls size of a gradient descent step
    'min_data_in_leaf': 20,      # Data set is quite small so reduce this a bit
    'feature_fraction': 0.7,     # Proportion of features in each boost, controls overfitting
    'num_leaves': 41,            # Controls size of tree since LGBM uses leaf wise splits
    'drop_rate': 0.15,
    'max_depth':4,
    "seed":1212}

lgbm_reg_model = lgb.train(lgbm_params, train_data_reg, valid_sets=[train_data_reg, valid_data_reg], categorical_feature=categorical_feature, verbose_eval=False)
```
グラフも作成します。
```python
viz_rmodel = dtreeviz.model(model=lgbm_reg_model, tree_index=1,
                            X_train=dataset[features_reg], 
                            y_train=dataset[target_reg], 
                            feature_names=features_reg, 
                            target_name=target_reg)
```

---
それでは可視化していきましょう。
コードは分類モデルと全く同じです。
```python
viz_rmodel.view()
```
![g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/4ad25d8e-81ef-89c2-0506-960f2f9ae594.png)

**横向き**
```python
viz_rmodel.view(orientation="LR")
```
![g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/75b2d25d-7c43-1097-cab6-9cd1b03878ee.png)

**シンプル表示**
```python
viz_rmodel.view(fancy=False)
```
![g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/640ce5e5-f9c0-88fa-52df-20fe94702287.png)

**深さ指定**
```python
viz_rmodel.view(depth_range_to_display=(0, 2))
```
![g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/9fb98f4b-f565-d3f2-f8fa-959a24897862.png)

**特定の行のデータのパスを表示**
```python
x = dataset[features_reg].iloc[10]
viz_rmodel.view(x = x)
```
![g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/a21fc828-c909-ba20-abe6-9f870d50fb6a.png)

**特定の行のデータのみ表示**
```python
viz_rmodel.view(show_just_path=True, x = x)
```
![g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/09911d1b-b19b-f586-f121-ca9caaf54891.png)

**パスをテキスト表示**
```python
print(viz_rmodel.explain_prediction_path(x))
```
```
出力：
1.5 <= Embarked_label 
Pclass in {3} not in {1}  
```

**すべての葉ノードに到達するサンプル数**
```python
viz_rmodel.leaf_sizes()
```
![g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/525b2b10-3172-1d13-0caa-03131f1da319.png)

**回帰ツリーモデルの各葉ノードにおけるターゲットの分布**
回帰ツリーでは、各葉ノードは通常、その葉に到達するトレーニングサンプルの目的変数の平均値としての予測を表します。
ツリーの分割によって定義される特徴空間の異なる領域での目的変数の分布を理解するのに役立ちます。モデルが不確実である可能性がある領域（例えば、ある葉の目的変数の範囲が広い場合）や、モデルが確信している領域（例えば、ある葉の目的変数が非常に近い値を持つ場合）についての洞察を提供することができます。

```python
viz_rmodel.rtree_leaf_distributions()
```
![g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/8bbef852-3a82-609d-8769-3c3f2e5c4255.png)

**特定のノードIDの統計情報**
```python
viz_rmodel.node_stats(node_id=19)
```
![g.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524375/e3ab765f-3b8f-4938-730e-59580c16c364.png)


分類モデルの可視化と見た目が異なりますね。
まあ、ターゲットが違うのでそりゃそうかって感じです。

今回`viz_model`と`viz_rmodel`を作成しましたが、簡単に説明しておきます


`viz_model`:
- これは分類のための決定木またはアンサンブルモデル（LightGBMの分類モデル）の視覚化オブジェクト
- ターゲット変数はカテゴリカルな値（生存 or 死亡）を持つ
- viz_modelを用いると、各ノードでのクラスの分布や決定条件、葉ノードでの最終的なクラス予測など、分類のための決定木の詳細を視覚的に確認可能

`viz_rmodel`:

- これは回帰のための決定木またはアンサンブルモデル（LightGBMの回帰モデル）の視覚化オブジェクト
- ターゲット変数は連続的な値（年齢）を持つ
- viz_rmodelを用いると、各ノードでの目的変数の平均値や分布、葉ノードでの予測値など、回帰のための決定木の詳細を視覚的に確認可能

# まとめ
ここまでお疲れさまでした！！！
お付き合いいただきありがとうございました。
今回はLightGBMの決定木の可視化を行いましたが、XGboostなど他の決定木系のモデルの可視化もできるみたいですので、興味があれば参考サイトから確認してみてください。
可視化することで、より深く考察できるようになるかもしれませんね！
皆さんのお役に立てたら幸いです。
それでは！！！

---
参考サイト
> https://github.com/parrt/dtreeviz