データ分析コンペで使う思考停止初手LightGBM


# はじめに
最近KaggleやSignateなどのデータ分析コンペにはまっており、いくつかのコンペに少しづつ参加しながら日々勉強中。
毎回はじめにデータに向き合う前に、コンペの難易度やデータの傾向を知るために行っているLightGBMのテンプレートがあるので、それを公開します。

もっとこうした方がいいとかあったら教えてください！

## 環境
- Ubuntu18.04
- Python3.8.0
- 利用したデータはKaggleのTitanic  
[Titanic: Machine Learning from Disaster \| Kaggle](https://www.kaggle.com/c/titanic)


# 全体像

## データの読み込み
データの読み込みと必要なライブラリのインポートを行う。学習データをよく確認しずに始めると、意外にデータ量が膨大だった、、、みたいなことがあるのでデータ数量くらいは確認しておく。

```python
from datetime import datetime
import numpy as np
import matplotlib.pyplot as plt
import os
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder

# データの読み込み
train_df = pd.read_csv("./train.csv")
test_df = pd.read_csv("./test.csv")

print(train_df.shape, test_df.shape)
```

```
(891, 12) (418, 11)
```


## 特徴量加工

### はじめにデータを眺める
```python
train_df
```

```
PassengerId	Survived	Pclass	Name	Sex	Age	SibSp	Parch	Ticket	Fare	Cabin	Embarked
0	1	0	3	Braund, Mr. Owen Harris	male	22.0	1	0	A/5 21171	7.2500	NaN	S
1	2	1	1	Cumings, Mrs. John Bradley (Florence Briggs Th...	female	38.0	1	0	PC 17599	71.2833	C85	C
2	3	1	3	Heikkinen, Miss. Laina	female	26.0	0	0	STON/O2. 3101282	7.9250	NaN	S
3	4	1	1	Futrelle, Mrs. Jacques Heath (Lily May Peel)	female	35.0	1	0	113803	53.1000	C123	S
4	5	0	3	Allen, Mr. William Henry	male	35.0	0	0	373450	8.0500	NaN	S
5	6	0	3	Moran, Mr. James	male	NaN	0	0	330877	8.4583	NaN	Q
6	7	0	1	McCarthy, Mr. Timothy J	male	54.0	0	0	17463	51.8625	E46	S
7	8	0	3	Palsson, Master. Gosta Leonard	male	2.0	3	1	349909	21.0750	NaN	S
8	9	1	3	Johnson, Mrs. Oscar W (Elisabeth Vilhelmina Berg)	female	27.0	0	2	347742	11.1333	NaN	S
9	10	1	2	Nasser, Mrs. Nicholas (Adele Achem)	female	14.0	1	0	237736	30.0708	NaN	C
10	11	1	3	Sandstrom, Miss. Marguerite Rut	female	4.0	1	1	PP 9549	16.7000	G6	S
11	12	1	1	Bonnell, Miss. Elizabeth	female	58.0	0	0	113783	26.5500	C103	S
12	13	0	3	Saundercock, Mr. William Henry	male	20.0	0	0	A/5. 2151	8.0500	NaN	S
13	14	0	3	Andersson, Mr. Anders Johan	male	39.0	1	5	347082	31.2750	NaN	S
14	15	0	3	Vestrom, Miss. Hulda Amanda Adolfina	female	14.0	0	
```

どんなコンペでもデータを見るのは大事。
今回の目的変数が`Suvived`、`PassengerId`と`Name`はユニークな特徴量なので使わないなど、最低限のデータの確認を行う。


### 説明変数と目的変数に分割
説明変数と目的変数に分割を行う。

```python
train_x, train_y = train_df.drop("Survived", axis=1), train_df["Survived"]
```


### 特徴量加工
特徴量加工も行うが最低限。以下の３つ観点でしか行わない。

- null埋め
- 質的変数->量的変数(ラベルエンコーディング)
- 不要カラムの削除(`PassengerId`と`Name`)

```python
def label_encording(data_col):
    '''
    ラベルエンコーディング
    data_col     : 対象のデータフレームの１つの列
    '''
    le = LabelEncoder()
    le = le.fit(data_col)
    #ラベルを整数に変換
    data_col = le.transform(data_col)

    return data_col
    
def preprocess(df):
    '''
    前処理を行う
    df : padnas.Dataframe
      対象のデータフレーム
    '''
    df = df.drop("PassengerId", axis=1)
    df = df.drop("Name", axis=1)
    
    # 質的変数を数値に変換
    for column_name in df:
        if df[column_name][0].dtypes == object: # 欠損値に関してはNULLを代入する
            df[column_name] = df[column_name].fillna("NULL")
            df[column_name] = label_encording(df[column_name])   
        elif df[column_name][0].dtypes == ( "int64"  or  "float64") : # 欠損値に関しては-999を代入する
            df[column_name] = df[column_name].fillna(-999)   
            
    return df
```

ラベルエンコーディングを行う際に、学習データとテストデータでラベルの対応関係が崩れるとよくないので、学習データとテストデータを同時に特徴量加工をかける。

```python
all_x = pd.concat([train_x, test_df])
preprocessed_all_x = preprocess(all_x)

# 前処理を行なったデータを，学習データとテストデータに再分割
preprocessed_train_x, preprocessed_test_x = preprocessed_all_x[:train_x.shape[0]], preprocessed_all_x[train_x.shape[0]:]
print(preprocessed_train_x.head(5))
```


## モデル作成
LightGBMを学習するクラスを作成する。細かいパラメータの説明は以下の公式サイトを参照。
* [Parameters — LightGBM 3.0.0 documentation](https://lightgbm.readthedocs.io/en/latest/Parameters.html)

`objective`や`metrics`は学習データやコンペに応じて変更する。

```python
# LightGBM
import lightgbm as lgb

class lightGBM:
    def __init__(self, params=None):
        self.model = None
        if params is not None:
            self.params = params
        else:
            self.params = {'objective':'binary',
                            'seed': 0,
                            'verbose':10, 
                            'boosting_type': 'gbdt',
                            'metrics':'auc',
                            'reg_alpha': 0.0,
                            'reg_lambda': 0.0,
                            'learning_rate':0.01, 
                            'drop_rate':0.5
                         }
        self.num_round = 20000
        self.early_stopping_rounds = self.num_round/100
        

    def fit(self, tr_x, tr_y, va_x, va_y):
        self.target_columms = tr_x.columns
        print(self.target_columms)
        # データセットを変換
        lgb_train = lgb.Dataset(tr_x, tr_y)
        lgb_eval = lgb.Dataset(va_x, va_y)
        self.model = lgb.train(self.params, 
                            lgb_train, 
                            num_boost_round=self.num_round,
                            early_stopping_rounds=self.early_stopping_rounds,
                            valid_names=['train', 'valid'],
                            valid_sets=[lgb_train, lgb_eval],
                            verbose_eval=self.num_round/100
                            )
        return self.model
         
    
    def predict(self, x):
        data = lgb.Dataset(x)
        pred = self.model.predict(x, num_iteration=self.model.best_iteration)
        return pred
    
    
    def get_feature_importance(self, target_columms=None):
        '''
        特徴量の出力
        '''
        if target_columms is not None:
            self.target_columms = target_columms
        feature_imp = pd.DataFrame(sorted(zip(self.model.feature_importance(), self.target_columms)), columns=['Value','Feature'])
        return feature_imp
```

学習器の定義

```python
def model_learning(model, x, y):
    '''
    モデルの学習を行う。
    '''
    tr_x, va_x, tr_y, va_y = train_test_split(x, train_y, test_size=0.2, random_state=0)    
    return model.fit(tr_x, tr_y, va_x, va_y)
```
モデルをクラスで定義しておき学習器に渡す構成にすることで、異なるモデルを利用する時にソースコードの変更を最小限にすることができる。

例えばXGBoostを使いたい時、以下のように書き換えることですぐに学習するモデルを差し替えることができる。

```python

class XGBoost:
    def __init__(self, params=None):
        # 初期化処理~~~

    def fit(self, tr_x, tr_y, va_x, va_y):
        # 学習の処理~~~
    
    def predict(self, x):
        # 評価の処理~~~

xgboost_model = XGBoost()
model_learning(xgboost_model, preprocessed_train_x, train_y)
```


## 学習

```python
lightgbm_model = lightGBM()
model_learning(lightgbm_model, preprocessed_train_x, train_y)
```


```
Index(['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Ticket', 'Fare', 'Cabin',
       'Embarked'],
      dtype='object')
Training until validation scores don't improve for 200.0 rounds
Early stopping, best iteration is:
[172]	train's auc: 0.945026	valid's auc: 0.915613
```

学習が完了！すぐに終わった。

### 特徴量の重要度の評価
LightGBMでは学習した特徴量の内、どれをよく使ったかを確認することができる。これにより次のステップでおこなうEDAのヒントを得ることができる。
なんとなく`Age`や`Ticket`、`Fare`が上位にあることから、年齢とか座席の位置が重要そうだなあ、`Age`と`Survived`の相関見るか、などなんとなく次やることが見えてくる。

```python
lightgbm_model.get_feature_importance()
```

```
  Value	Feature
0	32	Parch
1	58	SibSp
2	158	Embarked
3	165	Cabin
4	172	Sex
5	206	Pclass
6	1218	Fare
7	1261	Ticket
8	1398	Age
```


## 評価&提出ファイル作成

モデルの評価。出力結果は確率であるが、今回は`0,1`のどちらでなければならないので、それに合わせて整形する。


```python
# テスト用のモデルの評価
proba_ = lightgbm_model.predict(preprocessed_test_x)
proba = list(map(lambda x: 0 if x < 0.5 else 1, proba_))
```

予測値を提出データに合わせて整形する。何気にここが一番詰まりやすい....

```python
# テストデータの作成
submit_df = pd.DataFrame({"Survived": proba})
submit_df.index.name = "PassengerId"
submit_df.index = submit_df.index + len(train_df) + 1
```

ファイル名は`submit_{%Y-%m-%d-%H%M%S}`形式で保存する。
そうすることで不意の上書きを防ぐことができるし、ファイル名を毎回考えなくていいので地味に便利。

```python
# 保存
save_folder = "results"
if not os.path.exists(save_folder):
    os.makedirs(save_folder)

submit_df.to_csv("{}/submit_{}.csv".format(save_folder, datetime.now().strftime("%Y-%m-%d-%H%M%S")),index=True)
```

# 終わりに
この結果を提出したところPublicScoreが`0.77033`で`6610位/20114人`だった。(2020/08/25時点)
とりあえず回してみてコンペの難易度や感覚を掴む、という目的では悪くないテンプレートかと思う。

EDAが甘いなあと毎回思うので今後はEDAをもっとしっかりやっていきたところ。