はじめに
最近KaggleやSignateなどのデータ分析コンペにはまっており、いくつかのコンペに少しづつ参加しながら日々勉強中。
毎回はじめにデータに向き合う前に、コンペの難易度やデータの傾向を知るために行っているLightGBMのテンプレートがあるので、それを公開します。
もっとこうした方がいいとかあったら教えてください!
環境
- Ubuntu18.04
- Python3.8.0
- 利用したデータはKaggleのTitanic
Titanic: Machine Learning from Disaster | Kaggle
全体像
データの読み込み
データの読み込みと必要なライブラリのインポートを行う。学習データをよく確認しずに始めると、意外にデータ量が膨大だった、、、みたいなことがあるのでデータ数量くらいは確認しておく。
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)
特徴量加工
はじめにデータを眺める
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
はユニークな特徴量なので使わないなど、最低限のデータの確認を行う。
説明変数と目的変数に分割
説明変数と目的変数に分割を行う。
train_x, train_y = train_df.drop("Survived", axis=1), train_df["Survived"]
特徴量加工
特徴量加工も行うが最低限。以下の3つ観点でしか行わない。
- null埋め
- 質的変数->量的変数(ラベルエンコーディング)
- 不要カラムの削除(
PassengerId
とName
)
def label_encording(data_col):
'''
ラベルエンコーディング
data_col : 対象のデータフレームの1つの列
'''
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
ラベルエンコーディングを行う際に、学習データとテストデータでラベルの対応関係が崩れるとよくないので、学習データとテストデータを同時に特徴量加工をかける。
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を学習するクラスを作成する。細かいパラメータの説明は以下の公式サイトを参照。
objective
やmetrics
は学習データやコンペに応じて変更する。
# 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
学習器の定義
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を使いたい時、以下のように書き換えることですぐに学習するモデルを差し替えることができる。
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)
学習
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
の相関見るか、などなんとなく次やることが見えてくる。
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
のどちらでなければならないので、それに合わせて整形する。
# テスト用のモデルの評価
proba_ = lightgbm_model.predict(preprocessed_test_x)
proba = list(map(lambda x: 0 if x < 0.5 else 1, proba_))
予測値を提出データに合わせて整形する。何気にここが一番詰まりやすい....
# テストデータの作成
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}
形式で保存する。
そうすることで不意の上書きを防ぐことができるし、ファイル名を毎回考えなくていいので地味に便利。
# 保存
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をもっとしっかりやっていきたところ。