文系機械学習初心者がKaggleのHouse Pricesで他人のxgboostを写経&改造しながらコードの内容をメモしていった

こんにちは
以下ではコードをブロック分けして並べています。
Kaggle内のNotebook上と同じ状態とみてもらえればと思います。

また以下のコードは[こちら](https://www.kaggle.com/kenkanayama/house-price-traning-1)と同じです。

「[Kaggleで勝つデータ分析の技術](https://www.amazon.co.jp/Kaggle%E3%81%A7%E5%8B%9D%E3%81%A4%E3%83%87%E3%83%BC%E3%82%BF%E5%88%86%E6%9E%90%E3%81%AE%E6%8A%80%E8%A1%93-%E9%96%80%E8%84%87-%E5%A4%A7%E8%BC%94-ebook/dp/B07YTDBC3Z/ref=sr_1_1?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&dchild=1&keywords=Kaggle%E3%81%A7%E5%8B%9D%E3%81%A4%E3%83%87%E3%83%BC%E3%82%BF%E5%88%86%E6%9E%90%E3%81%AE%E6%8A%80%E8%A1%93&qid=1589985776&s=books&sr=1-1)」のパラパラ読んでからタイトル通り写経＆改造をしています。

# Notebook

```.py
# 以下のnoteを参考に写経＆勉強＆特徴量を作成している。
# https://www.kaggle.com/comartel/house-price-xgboost-starter
import csv
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import itertools
import datetime
pd.set_option('display.max_columns', None) # 表示させる際に省力をさせないために
from sklearn.model_selection import KFold
from sklearn.model_selection import train_test_split
import xgboost as xgb
from operator import itemgetter
import time
from sklearn import preprocessing
```

以下のファイル読み込み元はおのおので変えてもらえればと思います。

```.py
train = pd.read_csv("/kaggle/input/house-prices-advanced-regression-techniques/train.csv") # read train data
test = pd.read_csv("/kaggle/input/house-prices-advanced-regression-techniques/test.csv") # read test data
```

上記ファイルの中身をざっくりと確認↓

```.py
print(len(train))
print(len(train.columns))
train.head()
```

```.py
print(len(test))
print(len(test.columns))
test.head()
```

## 特徴量を作っていきます

```.py
# トレーニングデータとテストデータを配列にしたものと、合体させたデータフレームを作る。それからisnullの個数も見ておく
# データフレームの合体は、同じカラムであることを確認してから行うようにする。今回は別途確認してしまったので、そのままマージさせている。
tables=[test,train]

train_sp = train['SalePrice']
train_train = train.drop('SalePrice', axis=1)
merge_df = pd.concat([train_train, test])

total_missing = merge_df.isnull().sum()
print(total_missing)
```

```.py
# でisnullでnull値ががっちゃんこdfのレコード数のうち3分の1以上あるカラムは消す。ここで3分の1以上にするか、どうかは自由に。
# テストデータとトレーニングデータから消す
to_delete = total_missing[total_missing > (len(merge_df)//3)]
for table in tables:
    table.drop(to_delete.index.tolist(),axis=1, inplace=True)
# 消えたカラムを見ておく
to_delete.index.tolist()
```

```.py
# がっちゃんこDFからも消えたカラムを消しておく
merge_df.drop(to_delete.index.tolist(),axis=1, inplace=True)
merge_df.head()
```

```.py
# カラムに対して数値データとそうじゃないデータを判別し、それぞれのカラム取得する。
numerical_features   = merge_df.select_dtypes(include=["float","int","bool"]).columns.values
categorical_features = merge_df.select_dtypes(include=["object"]).columns.values
# ↓見てみる
print(numerical_features)
print(categorical_features)
```

```.py
# 数値が入っている特徴量カラムのnull値は中央値で置き換え、
# 文字が入っている特徴量カラムのnull値は最も多く出てくる文字で置き換える
# 数値を平均ではなく中央値で置き換えたほうが外れ値の影響を受けにくくさせるためです。（「平均値 中央値 外れ値」でぐぐるとよくわかる）
# 中央値や頻出ワードを出す際にがっちゃんこDFを使わないでそれぞれのトレーニングデータDF、テストデータDFから各中央値や頻出ワードを出すほうがよいかも？この辺りを試行錯誤するのがカグラ―だそう
for table in tables:
    for feature in numerical_features: 
        table[feature].fillna(merge_df[feature].median(), inplace = True)
    for feature in categorical_features:
        table[feature].fillna(merge_df[feature].value_counts().idxmax(), inplace = True)
# がっちょんこDFも同じく。
for feature in numerical_features: 
    merge_df[feature].fillna(merge_df[feature].median(), inplace = True)
for feature in categorical_features:
    merge_df[feature].fillna(merge_df[feature].value_counts().idxmax(), inplace = True)
```

```.py
# 文字列は数値に変換するためにラベルエンコーディングにかける
# ラベルエンコーディングとワンホットエンコーディングの2つを覚えていれば問題ないと思われる。
for feature in categorical_features:
    le = preprocessing.LabelEncoder()
    le.fit(merge_df[feature])
    for table in tables:
        table[feature] = le.transform(table[feature])
```

↓表示させて見てみる

```.py
# 確認：テストデータ
tables[0].head()
```

```.py
# 確認：学習データ
tables[1].head()
```

```.py
# それぞれのカラムを取得
trainval = list(tables[1].columns.values)
testval = list(tables[0].columns.values)

# 念のため共通なのだけれど共通のカラムを取得する。Id列は削除する。
features = list(set(trainval) & set(testval))
features.remove('Id') 
```

## モデル作成

以下のURLを見ながら各パラメーターはどんな意味を持つのか調べてメモしていっている。
https://xgboost.readthedocs.io/en/latest/parameter.html#parameters-for-linear-booster-booster-gblinear
わかっていない箇所もある、、。

```.py
# シンプルな xgboost
train = tables[1]
test = tables[0]
target='SalePrice'

eta_list = [0.1,0.2,0.3] # デフォルトは0.3。次のブースティングプロセスに使用する重みをここで決定する。ブースティングプロセスについてはxgboost、決定木について調べればわかる
max_depth_list = [3,4,6,8] # 決定木の深さを3パターン用意してみる
subsample = 0.8 # デフォは1トレーニングデータの何％をサンプリングするかを決める。今回は80%。1にすると過剰適合になりがち。
colsample_bytree = 0.8 # colsample_bytree、colsample_bylevel、colsample_bynodeの3種類があり、デフォは1。列のサブサンプリング用のパラメーターのファミリー。←どういう意味でどう使われているのかはわからない、、。

num_boost_round = 400
early_stopping_rounds = 10 # 過学習を防ぐ学習方法。検証スコアの改善が止まるまでトレーニングする。今回は10回前と比較して評価指標が改善していなければ学習を停止させるようにしている。
test_size = 0.2

# start the training
array_score=np.ndarray((len(eta_list)*len(max_depth_list),3)) # この後の多くのパターンのスコアを格納するためだけのもの

i=0
# itertools.product()はかっこ内のリストを直積にして全パターンのループができる。
for eta,max_depth in list(itertools.product(eta_list, max_depth_list)):
    print('XGBoost params. ETA: {}, MAX_DEPTH: {}, SUBSAMPLE: {}, COLSAMPLE_BY_TREE: {}'.format(eta, max_depth, subsample, colsample_bytree))
    params = {
        "objective": "reg:linear", # どんな回帰を使うか。ロジスティック回帰を使うかみたいなものをここで設定する。デフォはreg：squarederrorで二乗損失を伴う回帰
        "booster" : "gbtree", # デフォはgbtree。これが決定木。他にはgblinearまたはdartがある
        "eval_metric": "rmse", # 回帰の場合はrmse、分類の場合はerror、ランキングの場合はmean average precision
        "eta": eta, # 上で説明している
        "tree_method": 'exact', # デフォはauto。auto、exact、approx、hist、gpu_histが使える。autoを選べばexact、approx、hist、gpu_histから適切だと思われるものを選んでくれる
        "max_depth": max_depth,# 決定木の深さ
        "subsample": subsample, # 上で説明している
        "colsample_bytree": colsample_bytree, # 上で説明している
        "silent": 1, #メッセージ出力について設定できるものらしい。有効な値は、0（サイレント）、1（警告）、2（情報）、3（デバッグ）
        "seed": 0, #乱数シードというものらしい。デフォは0。公式でも説明がなくて、、。
    }

    X_train, X_valid = train_test_split(train, test_size=test_size, random_state = 0)# トレーニングデータの8割を使う。2割は検証用に使う。この各割合で使うレコードはランダムに抽出している。
    y_train = np.log(X_train[target]) # 底をeとするaの対数。aは今回X_train[target] 。対数変換させると何かとよいらしい。勉強します。
    y_valid = np.log(X_valid[target]) # 底をeとするaの対数。X_valid[target]　。対数変換させると何かとよいらしい。勉強します。
    dtrain = xgb.DMatrix(X_train[features], y_train) # XGBoostがモデルに使う際の形になるようにデータを生成
    dvalid = xgb.DMatrix(X_valid[features], y_valid) # XGBoostがモデルに使う際の形になるようにデータを生成

    watchlist = [(dtrain, 'train'), (dvalid, 'eval')] # 評価とprintするときに使う。
    # ↓モデル作る
    gbm = xgb.train(params, dtrain, num_boost_round, evals=watchlist, early_stopping_rounds=early_stopping_rounds, verbose_eval=True) # トレーニングする。verbose_eval=Trueでイテレーション毎にmerror, stdを表示する。？

    print("Validating...")
    score = gbm.best_score # アーリーストッピングが発生した場合に使える「gbm.best_score」。最良のスコアを入れている
    print('Last error value: {:.6f}'.format(score))
    array_score[i][0]=eta
    array_score[i][1]=max_depth
    array_score[i][2]=score
    i+=1
```
↑の補足。
for文で各パラメータの全パターンを使ってモデルを作成し、その度に検証用のデータに当ててスコアを出している。
最後のarray_scoreではそのパターンで使ったパラメータ値とその結果のスコアを入れている。
実行するとログがプリントされるので、それを見るとわかりやすい

## 予測する

ここまででモデルに対してぶち込むパラメータで何がよいかを検証できた状態。
以下でその最も評価がよかったモデルとなったパラメータを使用したモデルで予測を行う

```.py
df_score=pd.DataFrame(array_score,columns=['eta','max_depth','Score'])
print("df_score : \n", df_score)
importance = gbm.get_fscore() # 各特徴量の重要性をゲットする
importance = sorted(importance.items(), key=itemgetter(1), reverse=True)
print('Importance array: ', importance)
np.save("features_importance",importance) # 後で使うためにセーブできるそう。np.loadでロードできる。「features_importance.npy」というファイルが出力されているはず。
print("Predict test set...")

# ↓ここで予測
test_prediction = gbm.predict(xgb.DMatrix(test[features]), ntree_limit=gbm.best_ntree_limit) # アーリーストッピングが有効な場合に「ntree_limit=bst.best_ntree_limit」が使える。検証結果（スコア）のうち最も結果が良かったモデルで予測を行える。
```

↑これを実行すると最もスコアの良かったeta、max_depthが確認できる。
自分はそれぞれ0.1と6.0だった。モデルの評価時のテストデータと検証データはランダムでそれらにある程度スコアは揺れるので再実行すると0.1と4.0がベストスコアになったりする。

```.py
output = np.exp(test_prediction) # np.logで対数変換したものを戻している。
# テストデータとアウトプットデータの数が同じだよね、っていう確認
print(len(test))
print(len(output))
```

```.py
# 出力する
last_list = []
for i , j in zip(test['Id'], output):
    last_list.append({
        'Id' : i,
        'SalePrice' : j
    })
df=pd.DataFrame(last_list)
now = datetime.datetime.now()
sub_file = 'submission_' + str(score) + '_' + str(now.strftime("%Y-%m-%d-%H-%M")) + '.csv'
df.to_csv(sub_file , index = False)
```

終わり。

# わからないことが多すぎる

colsample_bytreeの用途は公式を読んでもわからないし、その他のパラメータ値も、わからないものはある。
"booster"で使うパラメータ値はgbtreeの他gblinearやdartを使うベストなシーンはなんなのか。
またnumpyもこれまで扱ったことなかったし対数ってなんだろうと調べたし、、
調べれば調べるほど数学の知識が必須なんだなと痛感。

しかし一つずつ調べて都度printしたりした結果とても勉強になりました。
今回は回帰タスクでしたが、今後は分類タスク（二値分類・多クラス分類）、レコメンデーション、物体検出、セグメンテーションタスクも同様な取り組みを行おうと思います。

数学も勉強します。
誰かの参考になれば幸いです。