勾配ブースティング木のお気持ちを知る

将棋の初手七六歩と同じくらい使われるのが、テーブルデータの機械学習での初手勾配ブースティング木です。

最初に勾配ブースティングにかけて、どの特徴量が有効そうかも見れてしまうとさらにウレシイということで、勾配ブースティングの特徴量の重要性を表示してみます。

### 実行環境とデータ

Googlw Colaboratory で試しました。
お手元の Jupyter でも Python スクリプトでも問題なく実行できます。

ノートブックは以下です。
https://colab.research.google.com/drive/1N1gtzTHFRKsbm88NyuEKqBr9wNS3tU7K?usp=sharing

データは以下の「スピードデートテスト」を使います。
https://knowledge-ja.domo.com/Training/Self-Service_Training/Onboarding_Resources/Fun_Sample_Datasets

4分間の参加者全員とのデートの後、再びデートしたいと思ったか、またそのデートの評価を行うという実験のデータです。とても面白そうなデータなのですが、200項目近くある割に説明が無い項目があったりと、結構データを見るのが大変だったので今回はデータにはあまりこだわらず実行方法を見てみます。

### ソースコードと実行結果

まずデータを取得します。

```Python
! wget https://knowledge-ja.domo.com/@api/deki/files/5950/Speed_Dating_Data.csv
```

テキスト列のエンコード用のパッケージをインストールします。

```Python
! pip install category_encoders
```

CSV のデータをデータフレームに読み込みます。

```Python
import pandas as pd
speed_date = pd.read_csv("Speed_Dating_Data.csv", encoding='cp932')
speed_date
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323666/d9c02f54-25ed-99f2-2fb3-bfc4e836108e.png)


データの前処理を行います。少々データは荒れています。

* マッチングの成立 match 列を目的変数として、予測します。
* 13〜97列目までを説明変数とします(それ以降の列は後日の調査に基づくデータとのこと)。
* `dec_o`列はデートしようと思ったか、という列でマッチングの答えに関連しすぎると思われたので外しました(入れると実際、答えに対する関連はダントツで高く出ます)。
* 数値にカンマが入っている列があるので除去します。
* ラベルデータを数値に変換します。

```Python
import category_encoders as encoders
label_cols = ['field', 'from', 'career', 'undergra']

# id 列などを除く加工
exist_match_df = speed_date

object_col = exist_match_df['match']
object_col = object_col.values.astype(int)

feature_cols = exist_match_df.iloc[:,13:97]
feature_cols = feature_cols.drop('dec_o', axis=1)
col_names = feature_cols.columns.values
feature_cols['zipcode'] = feature_cols['zipcode'].str.replace(',', '')
feature_cols['income'] = feature_cols['income'].str.replace(',', '')
feature_cols['tuition'] = feature_cols['tuition'].str.replace(',', '')
feature_cols['mn_sat'] = feature_cols['mn_sat'].str.replace(',', '')

ordinal_encoder = encoders.OrdinalEncoder(cols=label_cols, handle_unknown='impute')
feature_cols = ordinal_encoder.fit_transform(feature_cols)

feature_cols = feature_cols.values.astype(float)
feature_cols
```

加工したデータを元に学習を行います。

* xgboost で学習を実行します。
* 5分割の交差検証を行います。
* Accuracy などを表示します。

```Python
import xgboost as xgb
from sklearn.model_selection import cross_validate, cross_val_predict, KFold

kfold = KFold(n_splits=5)
score_func = ["accuracy", "precision_macro", "recall_macro", "f1_macro"]
 
clf = xgb.XGBClassifier(objective="binary:logistic", max_depth=10, n_estimatoers=10000, early_stopping_rounds=20)
 
score = cross_validate(clf, feature_cols, object_col, cv=kfold, scoring=score_func, return_estimator=True)
 
print('acc:       ' + str(score["test_accuracy"].mean()))
print('precision: ' + str(score["test_precision_macro"].mean()))
print('recall:    ' + str(score["test_recall_macro"].mean()))
print('F1:        ' + str(score["test_f1_macro"].mean()))
```

acc:       0.8350436362341039
precision: 0.6755307380632243
recall:    0.5681596439505251
F1:        0.5779607716750095

recall などをみると、あまりよく学習できたとは言えませんが、今回の本題はこの後です。

##### 実行結果の出力

先ほどの cross_validate（） の `return_estimator=True` によって estimator が得られます。
estimator は feature_importances_ として関連の高かった説明変数を持っていますので、これを出力します。

```Python
import numpy as np

estimators = score["estimator"]
sum_score = np.zeros(len(col_names))
 
for i in range(5):
    sum_score += estimators[i].feature_importances_
 
df_score = pd.DataFrame(sum_score/5, index=col_names, columns=["score"])
df_score.sort_values("score", ascending=False)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323666/87963188-487f-c878-5578-0bb10e962425.png)

このようにして学習を行った際に重要とみなした変数を得ることができます。