Null Importanceを用いた特徴量選定

# 本記事の対象
本記事はRandomForestやXGBoost、LightGBMなどの学習器を使う人向けの内容になっています。

# 概要
自身が作成した数ある特徴量の中にはノイズとなりうるものが含まれます。実際にRandomForestやXGBoost、LightGBMなどの学習器を使って特徴量の重要度を表示した際、重要度の高い特徴量にそういったノイズが含まれている可能性があります。そこで今回は、**偽の教師データを用いて重要度を測る**ことによって、ノイズに埋もれている特徴量をあぶり出し、真に重要な特徴量のみを選定する方法を紹介します。参考にした資料は[こちら](https://www.kaggle.com/ogrellier/feature-selection-with-null-importances)です。

# 準備
今回はscikit-learnのデータセットにある[Breast Cancer](https://scikit-learn.org/stable/modules/generated/sklearn.datasets.load_breast_cancer.html)を使いました（特に理由はありません）。


```python
# 必要なモジュールをインポート
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.datasets import load_breast_cancer
from sklearn.ensemble import RandomForestClassifier


# データの取得
cancer = load_breast_cancer()
X = pd.DataFrame(cancer.data, columns=cancer.feature_names)
y = cancer.target
```

# 特徴量の重要度を確認
そのままのデータでモデルを学習し、特徴量の重要度を確認します。


```python
# モデルの学習
clf = RandomForestClassifier(random_state=42)
clf.fit(X, y)

# 特徴量の重要度を含むデータフレームを作成
imp_df = pd.DataFrame()
imp_df["feature"] = X.columns
imp_df["importance"] = clf.feature_importances_
imp_df = imp_df.sort_values("importance")

# 可視化
plt.figure(figsize=(7, 10))
plt.barh(imp_df.feature, imp_df.importance)
plt.xlabel("Feature Importance")
plt.show()
```


![output_4_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/400179/eb257de8-d0f1-5ed0-ccbe-febd948c1366.png)


# 実データと虚データで特徴量重要度を取得
ここからが本題です。先ほどの特徴量重要度が本当に正しいのかを検証します。目的変数をシャッフルしたデータを用いてモデルを学習し、そのモデルにおける特徴量重要度 (**Null Importtance**) を取得します。


```python
def get_feature_importances(X, y, shuffle=False):
    # 必要ならば目的変数をシャッフル
    if shuffle:
        y = np.random.permutation(y)
    
    # モデルの学習
    clf = RandomForestClassifier(random_state=42)
    clf.fit(X, y)
    
    # 特徴量の重要度を含むデータフレームを作成
    imp_df = pd.DataFrame()
    imp_df["feature"] = X.columns
    imp_df["importance"] = clf.feature_importances_
    return imp_df.sort_values("importance", ascending=False)

# 実際の目的変数でモデルを学習し、特徴量の重要度を含むデータフレームを作成
actual_imp_df = get_feature_importances(X, y, shuffle=False)

# 目的変数をシャッフルした状態でモデルを学習し、特徴量の重要度を含むデータフレームを作成
N_RUNS = 100
null_imp_df = pd.DataFrame()
for i in range(N_RUNS):
    imp_df = get_feature_importances(X, y, shuffle=True)
    imp_df["run"] = i + 1
    null_imp_df = pd.concat([null_imp_df, imp_df])
```

# 各データから得られた重要度を比較

続いて、上記で得られたそれぞれの重要度を比較します。


```python
def display_distributions(actual_imp_df, null_imp_df, feature):
    # ある特徴量に対する重要度を取得
    actual_imp = actual_imp_df.query(f"feature == '{feature}'")["importance"].mean()
    null_imp = null_imp_df.query(f"feature == '{feature}'")["importance"]
    
    # 可視化
    fig, ax = plt.subplots(1, 1, figsize=(6, 4))
    a = ax.hist(null_imp, label="Null importances")
    ax.vlines(x=actual_imp, ymin=0, ymax=np.max(a[0]), color='r', linewidth=10, label='Real Target')
    ax.legend(loc="upper right")
    ax.set_title(f"Importance of {feature.upper()}", fontweight='bold')
    plt.xlabel(f"Null Importance Distribution for {feature.upper()}")
    plt.ylabel("Importance")
    plt.show()
    
# 実データにおいて特徴量の重要度が高かった上位5位を表示
for feature in actual_imp_df["feature"][:5]:
    display_distributions(actual_imp_df, null_imp_df, feature)
```


![output_9_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/400179/ecc6479f-4ad1-bc7c-eef5-333df3a6c99e.png)![output_9_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/400179/e35b4f99-02ee-40ed-2638-97fd541454d1.png)
![output_9_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/400179/d87f4efb-0096-76e4-484d-e34842f6e2ee.png)![output_9_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/400179/c1a521f4-a9b2-ac90-688e-8a08f1646df7.png)
![output_9_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/400179/c7a4cc9f-4918-141d-cf63-cd9ba8b188c4.png)


本記事では重要度が高いとされた上位5つの特徴量について見ていきます。図は横軸に各特徴量の重要度、縦軸にNull Importanceの頻度を取っています（青色がNull Importanceのヒストグラム分布）。ノイズに埋もれた特徴量の場合、赤色の縦線（実際の重要度）が青色の塊の中にあるか、それよりも重要度が低くなっています。今回使用したデータには、重要度の高い特徴量においてノイズとなり得るものはなさそうでした（他のデータではある場合があります）。

# 特徴量の選定
仮に重要度の高い特徴量の中にノイズが含まれる場合を想定し、その特徴量を除くために、設定した閾値を超える特徴量のみを取得する方法を記します。


```python
# 閾値を設定
THRESHOLD = 80

# 閾値を超える特徴量を取得
imp_features = []
for feature in actual_imp_df["feature"]:
    actual_value = actual_imp_df.query(f"feature=='{feature}'")["importance"].values
    null_value = null_imp_df.query(f"feature=='{feature}'")["importance"].values
    percentage = (null_value < actual_value).sum() / null_value.size * 100
    if percentage >= THRESHOLD:
        imp_features.append(feature)

imp_features
# ['worst concave points', 'worst perimeter', 'worst radius', 'mean perimeter', 'worst concavity', 'mean radius', 'mean concavity', 'mean concave points', 'area error', 'worst compactness']
```


今回は上記のグラフで示したノイズの分布を基準に、80%以上のNull Importanceを超えた特徴量を取得するようにしました。その結果、最初に求めた特徴量重要度のグラフにおいて、`worst texture`以下のものがノイズに埋もれていると判断されました。

# まとめ
特徴量の中にはノイズとなりうるものが含まれている可能性があるため、何かしらの確認、および選定が必要です。その中でも、今回のNull Importanceを用いた方法は、比較的容易に、かつ図を使っての確認もできるため、個人的にはおすすめの手法です。

他にもっと良い方法があるよ、という方がいらっしゃったらコメントをいただけると幸いです。