Null importances による特徴量選択

## はじめに
null importance を利用した特徴量選択について調べたのでその覚書です。何かおかしなとこがあればご指摘ください。
参考：[Feature Selection with Null Importances](https://www.kaggle.com/ogrellier/feature-selection-with-null-importances)

## 概要
特徴量選択をするときにノイズとなる特徴量を取り除き、本当に重要な特徴量を取り出すために行う。目的変数をランダムにシャッフルした訓練データを使って各特徴量の重要度をはかる。

## 手順
1. 目的変数をシャッフルした訓練データでモデルを何度も訓練し、null importanceの分布を作成
2. オリジナルの訓練データでモデルを訓練し、各特徴量の重要度を取得
3. null importanceの分布に対する実際の重要度のスコアを計算
4. 適当な閾値を定めて特徴量を選択

## 0. 準備
必要なライブラリのインポート

```python
import pandas as pd
import numpy as np
np.random.seed(123)

from sklearn.metrics import mean_squared_error
from sklearn.model_selection import KFold
import time
import lightgbm as lgb

import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import seaborn as sns

import warnings
warnings.simplefilter('ignore', UserWarning)

import gc
gc.enable()

import time
```

データの準備. 今回はkaggleのHouse Priceチュートリアルのデータを使用します.
[House Prices: Advanced Regression Techniques](https://www.kaggle.com/c/house-prices-advanced-regression-techniques/overview)

```python
# データ読み込み
data = pd.read_csv("./House_Price/train.csv")
target = data['SalePrice']

# カテゴリ変数を取得
cat_features = [
    f for f in data.columns if data[f].dtype == 'object'
]

for feature in cat_features:
    # カテゴリ変数を数値に変換
    data[feature], _ = pd.factorize(data[feature])
    # タイプをcategoryに変換
    data[feature] = data[feature].astype('category')

# とりあえず欠損値を含む特徴量は削除
drop_cols = [f for f in data.columns if data[f].isnull().any(axis=0) == True]
# drop_cols.append('SalePrice') # 目的変数も削除
data = data.drop(drop_cols, axis=1)
```

## 1. null importanceの分布を作成
特徴量の重要度を返す関数を用意.
今回は参考にした記事と同じでLightGBMを用いました.

```python
def get_feature_importances(data, cat_features, shuffle, seed=None):
    # 特徴量を取得
    train_features = [f for f in data if f not in 'SalePrice']
    
    # 必要なら目的変数をシャッフル
    y = data['SalePrice'].copy()
    if shuffle:
        y = data['SalePrice'].copy().sample(frac=1.0)
    
    # LightGBMで訓練
    dtrain = lgb.Dataset(data[train_features], y, free_raw_data=False, silent=True)
    params = {
        'task': 'train',
        'boosting_type': 'gbdt',
        'objective': 'regression',
        'metric': {'l2'},
        'num_leaves': 128,
        'learning_rate': 0.01,
        'num_iterations':100,
        'feature_fraction': 0.38,
        'bagging_fraction': 0.68,
        'bagging_freq': 5,
        'verbose': 0
    }
    clf = lgb.train(params=params, train_set=dtrain, num_boost_round=200, categorical_feature=cat_features)

    # 特徴量の重要度を取得
    imp_df = pd.DataFrame()
    imp_df["feature"] = list(train_features)
    imp_df["importance"] = clf.feature_importance()
    
    return imp_df
```

Null Importance の分布を作成.

```python
null_imp_df = pd.DataFrame()
nb_runs = 80
start = time.time()
for i in range(nb_runs):
    imp_df = get_feature_importances(data=data, cat_features=cat_features, shuffle=True)
    imp_df['run'] = i + 1 
    null_imp_df = pd.concat([null_imp_df, imp_df], axis=0)
```

## 2. 通常の特徴量の重要度を取得

```python
actual_imp_df = get_feature_importances(data=data, cat_features=cat_features, shuffle=False)
```

## 3. 重要度のスコアを計算
通常の特徴量の重要度をnull importance分布の75パーセンタイルで割ったものを使用

```python
feature_scores = []
for _f in actual_imp_df['feature'].unique():
    f_null_imps = null_imp_df.loc[null_imp_df['feature'] == _f, 'importance'].values
    f_act_imps = actual_imp_df.loc[actual_imp_df['feature'] == _f, 'importance'].mean()
    imp_score = np.log(1e-10 + f_act_imps / (1 + np.percentile(f_null_imps, 75)))
    feature_scores.append((_f, imp_score))

scores_df = pd.DataFrame(feature_scores, columns=['feature', 'imp_score'])
```

## 4. 特徴量を選定
適当な閾値を定めて特徴量を選定します. 今回はスコアが0.5以上のものを使用することにしました.

```python
sorted_features = scores_df.sort_values(by=['imp_score'], ascending=False).reset_index(drop=True)
new_features = sorted_features.loc[sorted_features.imp_score >= 0.5, 'feature'].values
print(new_features)

# ['CentralAir' 'GarageCars' 'OverallQual' 'HalfBath' 'OverallCond' 'BsmtFullBath']
```

## 最後に
先日参加したコンペで上位入賞者の方々が使っていたので調べてみました。
他にも特徴量選択の手法は色々あると思うので調べて行きたいと思います。