🐡

お馴染みのタイタニックのデータセットを使って、いろんなモデルを試してみる

2022/12/16に公開

この記事は、Do'er Advent Calender の15日目の記事です。

皆さんこんにちは!
Do'er代表のかずやんです。

25日に今年一年の振り返りはするとして、
本記事ではkaggleのチュートリアルでお馴染みのタイタニックデータセットを使って色んなモデルを構築し、精度を比較したいと思います。

また、実装を含めた内容となっていますので、気になる部分はアコーディオンを展開してみてください!

1. データの読み込み

タイタニックのデータセットには以下の3つが与えられます。

  • train.csv: 訓練データ
  • test.csv: テストデータ
  • gender_submission.csv: サンプルの提出用ファイル

各データセットを以下で読み込みます。

import pandas as pd
# 可視化用のライブラリ
import matplotlib.pyplot as plt
import seaborn as sns

INPUT_DIR = "dataset"
# データの読み込み
df_train = pd.read_csv(f"{INPUT_DIR}/train.csv")
df_test = pd.read_csv(f"{INPUT_DIR}/test.csv")

2. 簡単なEDA (探索的データ分析)

コンペに参加する上で、"データを見る"のは非常に大事だとよく言われます。
良い解法を出すためのヒントもデータに隠されていることが多いでしょう。

2-1. カラム一覧の確認

# DataFrame の情報を出力する
df_train.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   PassengerId  891 non-null    int64  
 1   Survived     891 non-null    int64  
 2   Pclass       891 non-null    int64  
 3   Name         891 non-null    object 
 4   Sex          891 non-null    object 
 5   Age          714 non-null    float64
 6   SibSp        891 non-null    int64  
 7   Parch        891 non-null    int64  
 8   Ticket       891 non-null    object 
 9   Fare         891 non-null    float64
 10  Cabin        204 non-null    object 
 11  Embarked     889 non-null    object 
dtypes: float64(2), int64(5), object(5)
memory usage: 83.7+ KB

ふむふむ、いくつかのカラムで欠損値がありそうですね。
見やすいように可視化してみたいと思います。

詳細な実装
# 欠損値を集計
_summary = df_train.isna().mean().sort_values(ascending=False)
# 描画
plt.subplots(figsize=(12, 6))
g = sns.barplot(x=_summary.index, y=_summary.values)
g.set_ylim(0, 1)
g.set_xticklabels(g.get_xticklabels(), rotation=30)
plt.show()


Cabin(客室番号),Age(年齢)、Embarked(出港地)に欠損値があるようですね。
特に、Cabinについては欠損値が半数以上あります。

ついでに、カラム内容をまとめてみます

  • PassengerId – 乗客識別ID
  • Survived – 生存フラグ(0=死亡、1=生存)
  • Pclass – チケットクラス
  • Name – 乗客の名前
  • Sex – 性別(male=男性、female=女性)
  • Age – 年齢
  • SibSp – タイタニックに同乗している兄弟/配偶者の数
  • Parch – タイタニックに同乗している親/子供の数
  • Ticket – チケット番号
  • Fare – 料金
  • Cabin – 客室番号
  • Embarked – 出港地

2-2. 目的変数に関する分析

タイタニックではSurvived(生存フラグ)を目的変数として予測します。
では、どれくらいの割合の人が生存していたのでしょうか?

詳細な実装
# 割合を計算
_summary = df_train["Survived"].value_counts(normalize=True)
print(round(_summary, 2))
# 可視化
sns.countplot(data=df_train, x="Survived")
plt.show()
0    0.62
1    0.38
Name: Survived, dtype: float64


約4割の人が生存したようですね!
ところで、生存率に男女の差はあるのでしょうか?

詳細な実装
# 集計
_summary = df_train.groupby("Sex")["Survived"].value_counts(normalize=True)
print(round(_summary, 2))
# 可視化
sns.countplot(data=df_train, x="Sex", hue="Survived")
plt.show()
Sex     Survived
female  1           0.74
        0           0.26
male    0           0.81
        1           0.19


女性の約75%が生存しているのに対して、男性の割合が約20%しか生存できていません!
女性の生存率がかなり高い傾向にあるようです。

では、チケットクラスで生存率に差はあるのでしょうか?

詳細な実装
sns.countplot(data=df_train, x="Pclass", hue="Survived")
plt.show()


チケットクラスが3になった途端、生存率が一気に低下するようです。

他にも色々集計の仕方があると思うので、気になった特徴量は可視化してみましょう!
実際に見てみることで気づきがあったりします。
今回は分量の関係上、これくらいにしておきます。

3. 特徴量エンジニアリング

3-1. BaseBlock

今回は特徴量の作成の見通しを良くするため、BaseBlock形式のclassで管理します。
入出力を常に一定にするのと、trainデータの状態をtestで適用したいときに非常に便利な書き方となります。

過去のAtmaCup #10にて詳しく動画で解説されているので、興味がある方は参照いただければと思います。(参照:18:45-)

以下の基底クラスを継承し、BaseBlockを作成していきます。
fit メソッドは学習データに対してのみ、transform メソッドは学習データとテストデータの両方に適用されます。
これにより、実装忘れなどを防ぐことができます。
また、引数としてparent_blocksを与えてあげることで、親ブロックを参照した実装できます。

class AbstractBaseBlock:
  def __init__(self, parent_blocks = None):
    self.parent_blocks = [] if parent_blocks is None else parent_blocks

  def fit(self, input_df: pd.DataFrame, y=None):        
    return self.transform(input_df)

  def transform(self, input_df: pd.DataFrame) -> pd.DataFrame:
    raise NotImplementedError()

以下は実装に使用する関数の実行時間を計測するTimerと、BaseBlockを実行するための関数です。
詳しい解説は省きますが、ログとして実行時間が出力され、特徴量を作成したデータフレームにはカラム名にブロック名のsuffixをつけることができます。

詳細な実装
# タイマー
class Timer:
  def __init__(self, logger=None, format_str="{:.3f}[s]", prefix=None, suffix=None, sep=" "):
    if prefix:
      format_str = str(prefix) + sep + format_str
    if suffix:
      format_str = format_str + sep + str(suffix)
    self.format_str = format_str
    self.logger = logger
    self.start = None
    self.end = None

  @property
  def duration(self):
    if self.end is None:
      return 0
    return self.end - self.start

  def __enter__(self):
    self.start = time()

  def __exit__(self, exc_type, exc_val, exc_tb):
    self.end = time()
    out_str = self.format_str.format(self.duration)
    if self.logger:
      self.logger.info(out_str)
    else:
      print(out_str)

# BaseBlock 実行関数
def run_blocks(input_df, blocks, y=None, test=False):
  df_out = pd.DataFrame()

  for block in blocks:
    if block.parent_blocks:
      _df = run_blocks(input_df=input_df, blocks=block.parent_blocks, y=y, test=test)
    else:
      _df = input_df

    with Timer(prefix='\t- {}'.format(str(block))):
      if not test:
        out_i = block.fit(_df, y=y)
      else:
        out_i = block.transform(_df)

    assert len(input_df) == len(out_i), block
    name = block.__class__.__name__
    df_out = pd.concat([df_out, out_i.add_suffix(f"@{name}")], axis=1)

  return df_out

3-2. 特徴量の作成

ここからは、特徴量の作成します。
主に以下の特徴量を作成しています。

  • 量的データをそのまま使用したもの
  • LabelEncoding(質的データをランダムな数値で表現したもの)
  • CountEncoding(質的データの登場回数を数値で表現したもの)
  • OneHotEncoding(質的データの種類ごとでを、0または1で表現したもの)
  • TargetEncodingの平均、標準偏差(目的変数を質的データで集計したもの)
  • 各列の交互作用
  • 搭乗者の氏名に関する敬称情報
  • 乗船位置に関する情報

また、各クラスのコメントに実装内容を簡単に記しています。

詳細な実装
class OriginalColumnBlock(AbstractBaseBlock):
    """指定したカラムをそのまま返す"""
    def __init__(self, target_col: str) -> None:
        super().__init__()
        self.target_col = target_col

    def transform(self, df_input: pd.DataFrame):
        return pd.DataFrame(df_input[self.target_col])


class DummyValiableBlock(AbstractBaseBlock):
    """ダミー変数の作成"""
    def __init__(self, target_col: str, drop_first: bool, **kwrgs) -> None:
        super().__init__(**kwrgs)
        self.target_col = target_col
        self.drop_first = drop_first
    
    def fit(self, df_input: pd.DataFrame, y=None):
        df_train_out = pd.get_dummies(df_input[self.target_col], drop_first=self.drop_first).add_prefix(f"{self.target_col}_")
        self.train_column_ = df_train_out.columns
        return df_train_out

    def transform(self, df_input: pd.DataFrame):
        return pd.get_dummies(df_input[self.target_col], drop_first=self.drop_first).add_prefix(f"{self.target_col}_").reindex(columns=self.train_column_, fill_value=0)


class LabelEncodingBlock(AbstractBaseBlock):
    """LabelEncoding"""
    def __init__(self, target_col: str) -> None:
        super().__init__()
        self.le = LabelEncoder()
        self.target_col = target_col

    def fit(self, df_input: pd.DataFrame, y=None):
        self.le.fit(df_input[self.target_col])
        return self.transform(df_input)

    def transform(self, df_input: pd.DataFrame):
        return pd.DataFrame({
            self.target_col: self.le.fit_transform(df_input[self.target_col])
        })


class CountEncodingBlock(AbstractBaseBlock):
    """CountEncoding"""
    def __init__(self, target_col: str, **kwrgs) -> None:
        super().__init__(**kwrgs)
        self.target_col = target_col

    def fit(self, df_input: pd.DataFrame, y=None):
        self.vc_ = df_input[self.target_col].value_counts()
        return self.transform(df_input)

    def transform(self, df_input: pd.DataFrame):
        return pd.DataFrame(df_input[self.target_col].map(self.vc_))


class TargetEncodeBlock(AbstractBaseBlock):
    """Target Encoding"""
    def __init__(self, target_col, agg: List, **kwrgs) -> None:
        super().__init__(**kwrgs)
        self.target = target_col
        self.agg = agg

    def fit(self, df_input: pd.DataFrame, y):
        self.dics_ = []
        if y not in df_input.columns:
            df_input = pd.concat([df_input, df_train[y]], axis=1)
        fold = StratifiedKFold(n_splits=3, shuffle=True, random_state=SEED)
        cv = fold.split(df_input[self.target], df_input[self.target])
        df_out = pd.DataFrame(index=df_input.index)
        for idx_feature, idx_valid in cv:
            df_feature = df_input[df_input.index.isin(idx_feature)]
            df_valid = df_input[df_input.index.isin(idx_valid)]
            _dic = df_feature.groupby(self.target)[y].agg(self.agg).to_dict()
            self.dics_.append(_dic)
            for agg in self.agg:
                df_out.loc[idx_valid, f"target={self.target}_agg_func={agg}"] = df_valid[self.target].map(_dic[agg])
        return df_out
	
    def transform(self, df_input: pd.DataFrame):
        df_out = pd.DataFrame()
        for agg in self.agg:
            _df = pd.DataFrame()
            for i, _dic in enumerate(self.dics_):
                _df[i] = df_input[self.target].map(_dic[agg])
            df_out[f"target={self.target}_agg_func={agg}"] = _df.mean(axis=1)
        return df_out


class AddBlock(AbstractBaseBlock):
    """指定したカラムの和を算出する"""
    def __init__(self, col1: str, col2: str, **kwrgs) -> None:
        super().__init__(**kwrgs)
        self.col1 = col1
        self.col2 = col2

    def transform(self, df_input: pd.DataFrame):
        return pd.DataFrame({
            f"{self.col1}+{self.col2}": df_input[self.col1] + df_input[self.col2]
        })

class NameTitleBlock(AbstractBaseBlock):
    """敬称に関する特徴量"""
    def __init__(self) -> None:
        super().__init__()

    def transform(self, df_input: pd.DataFrame):
        df_input['Title'] = df_input['Name'].map(lambda x: x.split(', ')[1].split('. ')[0])
        df_input['Title'].replace(['Capt', 'Col', 'Major', 'Dr', 'Rev'], 'Officer', inplace=True)
        df_input['Title'].replace(['Don', 'Sir',  'the Countess', 'Lady', 'Dona', 'Jonkheer'], 'Royalty', inplace=True)
        df_input['Title'].replace(['Mme', 'Ms'], 'Mrs', inplace=True)
        df_input['Title'].replace(['Mlle'], 'Miss', inplace=True)
        return pd.DataFrame(df_input['Title'])


class DeckBlock(AbstractBaseBlock):
    """乗船位置に関する特徴量"""
    def __init__(self) -> None:
        super().__init__()

    def transform(self, df_input: pd.DataFrame):
        return pd.DataFrame({"Deck": df_input['Cabin'].apply(lambda s: s[0] if pd.notnull(s) else 'M')})


class MultiGroupAggBlock(AbstractBaseBlock):
    """複数のカラムのグループ集計に関する特徴量"""
    def __init__(self, group_by: List) -> None:
        super().__init__()
        self.group_by = group_by

    def transform(self, df_input: pd.DataFrame):
        return pd.DataFrame({"Deck": df_input['Cabin'].apply(lambda s: s[0] if pd.notnull(s) else 'M')})

############################# 特徴量の作成
blocks = [
    *[OriginalColumnBlock(target_col) for target_col in ["Pclass", "Age", "SibSp", "Parch", "Fare"]],
    *[DummyValiableBlock(target_col, drop_first=False) for target_col in ["Sex"]],
    *[DummyValiableBlock(target_col, drop_first=False) for target_col in ["Embarked"]],
    *[LabelEncodingBlock(target_col) for target_col in ["Cabin", "Ticket"]],
    *[CountEncodingBlock(target_col) for target_col in ["Cabin", "Pclass", "Age", "SibSp", "Embarked", "Ticket"]],
    *[TargetEncodeBlock(target_col, ["mean", "std"]) for target_col in ["Sex", "SibSp", "Parch", "Ticket"]],
    TargetEncodeBlock(target_col="SibSp+Parch@AddBlock", agg=["mean", "std"], parent_blocks=[AddBlock(col1="SibSp", col2="Parch")]),
    TargetEncodeBlock(target_col="Title@NameTitleBlock", agg=["mean", "std"], parent_blocks=[NameTitleBlock()]),
    DummyValiableBlock(target_col="Title@NameTitleBlock", drop_first=False, parent_blocks=[NameTitleBlock()]),
    CountEncodingBlock(target_col="Title@NameTitleBlock", parent_blocks=[NameTitleBlock()]),
    TargetEncodeBlock(target_col="Deck@DeckBlock", agg=["mean", "std"], parent_blocks=[DeckBlock()]),
    DummyValiableBlock(target_col="Deck@DeckBlock", drop_first=False, parent_blocks=[DeckBlock()]),
    CountEncodingBlock(target_col="Deck@DeckBlock", parent_blocks=[DeckBlock()]),
]

df_train_feature = run_blocks(df_train.copy(), blocks, y="Survived")
df_test_feature = run_blocks(df_test.copy(), blocks, test=True)

実行結果
	- <__main__.OriginalColumnBlock object at 0x7fa46863ee20> 0.000[s]
	- <__main__.OriginalColumnBlock object at 0x7fa4686430a0> 0.000[s]
	- <__main__.OriginalColumnBlock object at 0x7fa468643190> 0.000[s]
	- <__main__.OriginalColumnBlock object at 0x7fa4686431f0> 0.000[s]
	- <__main__.OriginalColumnBlock object at 0x7fa4686433a0> 0.000[s]
	- <__main__.DummyValiableBlock object at 0x7fa468643400> 0.001[s]
	- <__main__.DummyValiableBlock object at 0x7fa468643460> 0.000[s]
	- <__main__.LabelEncodingBlock object at 0x7fa4686434c0> 0.000[s]
	- <__main__.LabelEncodingBlock object at 0x7fa468643550> 0.001[s]
	- <__main__.CountEncodingBlock object at 0x7fa4686435e0> 0.001[s]
	- <__main__.CountEncodingBlock object at 0x7fa468643640> 0.001[s]
	- <__main__.CountEncodingBlock object at 0x7fa4686436a0> 0.001[s]
	- <__main__.CountEncodingBlock object at 0x7fa468643700> 0.000[s]
	- <__main__.CountEncodingBlock object at 0x7fa468643760> 0.001[s]
	- <__main__.CountEncodingBlock object at 0x7fa4686437c0> 0.001[s]
	- <__main__.TargetEncodeBlock object at 0x7fa468643820> 0.009[s]
	- <__main__.TargetEncodeBlock object at 0x7fa468643880> 0.008[s]
	- <__main__.TargetEncodeBlock object at 0x7fa4686438e0> 0.008[s]
	- <__main__.TargetEncodeBlock object at 0x7fa468643940> 0.013[s]
	- <__main__.AddBlock object at 0x7fa46863ed60> 0.000[s]
	- <__main__.TargetEncodeBlock object at 0x7fa4686439d0> 0.008[s]
	- <__main__.NameTitleBlock object at 0x7fa468643a30> 0.002[s]
	- <__main__.TargetEncodeBlock object at 0x7fa468643a90> 0.009[s]
	- <__main__.NameTitleBlock object at 0x7fa468643af0> 0.002[s]
	- <__main__.DummyValiableBlock object at 0x7fa468643b50> 0.000[s]
	- <__main__.NameTitleBlock object at 0x7fa468643bb0> 0.002[s]
	- <__main__.CountEncodingBlock object at 0x7fa468643c10> 0.001[s]
	- <__main__.DeckBlock object at 0x7fa468643c70> 0.001[s]
	- <__main__.TargetEncodeBlock object at 0x7fa468643cd0> 0.009[s]
	- <__main__.DeckBlock object at 0x7fa468643d30> 0.001[s]
	- <__main__.DummyValiableBlock object at 0x7fa468643d90> 0.000[s]
	- <__main__.DeckBlock object at 0x7fa468643df0> 0.001[s]
	- <__main__.CountEncodingBlock object at 0x7fa468643e50> 0.001[s]
	- <__main__.OriginalColumnBlock object at 0x7fa46863ee20> 0.000[s]
	- <__main__.OriginalColumnBlock object at 0x7fa4686430a0> 0.000[s]
	- <__main__.OriginalColumnBlock object at 0x7fa468643190> 0.000[s]
	- <__main__.OriginalColumnBlock object at 0x7fa4686431f0> 0.000[s]
	- <__main__.OriginalColumnBlock object at 0x7fa4686433a0> 0.000[s]
	- <__main__.DummyValiableBlock object at 0x7fa468643400> 0.000[s]
	- <__main__.DummyValiableBlock object at 0x7fa468643460> 0.000[s]
	- <__main__.LabelEncodingBlock object at 0x7fa4686434c0> 0.000[s]
	- <__main__.LabelEncodingBlock object at 0x7fa468643550> 0.000[s]
	- <__main__.CountEncodingBlock object at 0x7fa4686435e0> 0.000[s]
	- <__main__.CountEncodingBlock object at 0x7fa468643640> 0.000[s]
	- <__main__.CountEncodingBlock object at 0x7fa4686436a0> 0.000[s]
	- <__main__.CountEncodingBlock object at 0x7fa468643700> 0.000[s]
	- <__main__.CountEncodingBlock object at 0x7fa468643760> 0.000[s]
	- <__main__.CountEncodingBlock object at 0x7fa4686437c0> 0.000[s]
	- <__main__.TargetEncodeBlock object at 0x7fa468643820> 0.004[s]
	- <__main__.TargetEncodeBlock object at 0x7fa468643880> 0.004[s]
	- <__main__.TargetEncodeBlock object at 0x7fa4686438e0> 0.004[s]
	- <__main__.TargetEncodeBlock object at 0x7fa468643940> 0.005[s]
	- <__main__.AddBlock object at 0x7fa46863ed60> 0.000[s]
	- <__main__.TargetEncodeBlock object at 0x7fa4686439d0> 0.004[s]
	- <__main__.NameTitleBlock object at 0x7fa468643a30> 0.002[s]
	- <__main__.TargetEncodeBlock object at 0x7fa468643a90> 0.004[s]
	- <__main__.NameTitleBlock object at 0x7fa468643af0> 0.001[s]
	- <__main__.DummyValiableBlock object at 0x7fa468643b50> 0.000[s]
	- <__main__.NameTitleBlock object at 0x7fa468643bb0> 0.002[s]
	- <__main__.CountEncodingBlock object at 0x7fa468643c10> 0.000[s]
	- <__main__.DeckBlock object at 0x7fa468643c70> 0.000[s]
	- <__main__.TargetEncodeBlock object at 0x7fa468643cd0> 0.004[s]
	- <__main__.DeckBlock object at 0x7fa468643d30> 0.000[s]
	- <__main__.DummyValiableBlock object at 0x7fa468643d90> 0.001[s]
	- <__main__.DeckBlock object at 0x7fa468643df0> 0.000[s]
	- <__main__.CountEncodingBlock object at 0x7fa468643e50> 0.000[s]

3-3. 欠損値補完

EDAでも確認した通り、年齢や出港地に欠損値がありました。

kaggleなどで一般的に広く用いられている、LightGBMなどの勾配ブースティングを用いた決定木では欠損値補完をしなくても、学習できます。
しかしながら、欠損値を別の特徴量から予測してあげることで、より高精度なモデルを作成できる可能性があります。
また、Neural Networkなど欠損値があるとうまく学習できないモデルも多数存在します。

そこで、今回はLightGBMを用いた欠損値補完します。

実装関数の詳細な説明

まず、fit() メソッドで欠損値がある列を指定して、モデルをトレーニングします。
欠損値のある行をデータセットとして、その他の行を使用してモデルをトレーニングします。
このとき、early_stopping_roundsを指定することで、指定したラウンド数で検証データの精度が改善しなくなったときにトレーニングを停止できます。

その後、transform() メソッドを呼び出すことで、指定された列の欠損値を補完したデータフレームを返します。
このとき、fit() メソッドでトレーニングされたモデルを使用して、欠損値を予測します。
_create_ds() メソッドは、トレーニングデータセットと検証データセットを作成するためのヘルパー関数です。
fit() およびtransform() メソッドで呼び出されます。

詳細な実装
import lightgbm as lgbm
from tqdm.notebook import tqdm
from sklearn.model_selection import train_test_split

class LGBMImputer:
    def __init__(
            self,
            feature_cols: List,
            target_cols: List,
            early_stopping_rounds: int = 20
        ) -> None:
        self.feature_cols = feature_cols
        self.target_cols = target_cols
        self.early_stopping_rounds = early_stopping_rounds
        self.models: Dict[str, lgbm.Booster] = {}
    
    def fit(self, df_input: pd.DataFrame):
        for target_col in tqdm(self.target_cols, desc="train lgbm models..."):
            # 欠損の行をデータセットとする
            train_ds, valid_ds = self._create_ds(df_input, target_col)
            params = self._feature_type_checker(train_ds.label)
            # 予測
            model = lgbm.train(
                {**params, "verbosity": -1},
                num_boost_round=500000,
                train_set=train_ds,
                callbacks=[
                    lgbm.early_stopping(stopping_rounds=self.early_stopping_rounds, verbose=-1)
                ],
                valid_sets=[valid_ds]
            )
            self.models[target_col] = model
        
        return self.transform(df_input)
    

    def transform(self, df_input: pd.DataFrame):
        df_input_cp = df_input.copy(deep=True)

        for target_col in tqdm(self.target_cols, desc="predict from lgbm models..."):
            if df_input[target_col].isnull().sum() > 0:
                # 欠損の行をデータセットとする
                _, valid_ds = self._create_ds(df_input, target_col)
                df_input_cp.loc[valid_ds.data.index, target_col] = self.models[target_col].predict(valid_ds.data, num_iteration=self.models[target_col].best_iteration)
        return df_input_cp
    

    def _create_ds(self, df_input: pd.DataFrame, target_col: str):
            # 欠損の行をデータセットとする / 欠損補完するカラムはテストデータとする
            idx_miss = df_input[df_input[target_col].isnull()].index
            if len(idx_miss) != 0:
                train_ds = lgbm.Dataset(
                    df_input[~df_input.index.isin(idx_miss)][self.feature_cols],
                    df_input[~df_input.index.isin(idx_miss)][target_col],
                )
                valid_ds = lgbm.Dataset(
                    df_input[df_input.index.isin(idx_miss)][self.feature_cols],
                    df_input[df_input.index.isin(idx_miss)][target_col],
                )
            else:
                # 訓練データに欠損値がないとき
                df_train, df_valid = train_test_split(df_input, test_size=.2, random_state=712)
                train_ds = lgbm.Dataset(
                    df_input[df_input.index.isin(df_train.index)][self.feature_cols],
                    df_input[df_input.index.isin(df_train.index)][target_col],
                )
                valid_ds = lgbm.Dataset(
                    df_input[df_input.index.isin(df_valid.index)][self.feature_cols],
                    df_input[df_input.index.isin(df_valid.index)][target_col],
                )
            return train_ds, valid_ds
    
    def _feature_type_checker(self, series: pd.Series) -> dict:
        # 自動で型を判定してパラメータを返す
        if pd.api.types.is_numeric_dtype(series):
            # 連続変数のとき
            return {
                'objective': 'regression'
            }
        else:
            # カテゴリカル変数のとき
            nuni = series.dropna().nunique()
            if nuni == 2:
                return {
                    'objective': 'binary'
                }
            elif nuni > 2:
                return {
                    'objective': 'multiclass',
                    'num_class': nuni + 1
                }
# trainの欠損値の列を取得
_summary_train = df_train_feature.isnull().sum()
# testの欠損値の列を取得
_summary_test = df_test_feature.isnull().sum()
# trainとtestの欠損値の列を和集合として取る
target_cols = set(_summary_train[_summary_train > 0].index) | set(_summary_test[_summary_test > 0].index)
# trainとtestの欠損値ではない列を取る
feature_cols = set(df_test_feature.columns) - target_cols

# 欠損値補完
imputer = LGBMImputer(feature_cols, target_cols)
df_train_feature = imputer.fit(df_train_feature)
df_test_feature = imputer.transform(df_test_feature)

# 目的変数を付与
df_train_feature["Survived"] = df_train["Survived"]

# 保存
df_train_feature.to_csv(f"{OUTPUT_DIR}/002_train.csv", index=False)
df_test_feature.to_csv(f"{OUTPUT_DIR}/002_test.csv", index=False)

df_train_feature.head(5)

4. いざ、モデルに学習させる!

ここまで、データの把握と特徴量の作しました。
ようやく本題の精度の比較に入ります!
比較するモデルたちは以下の5つになります。

  • LightGBM
  • XGBoost
  • NN (Neural Network)
  • SVM (Support Vector Machines)
  • k-NN (k-Nearest Neighbor)

すべてのモデルの目的変数に対してStratifiedKFold, k=5(5分割層化抽出)を行っています。
※今回はkNN以外、パラメータのチューニングまでは実装していないので、精度に多少の誤差を生む可能性があります。

4-1. LightGBM

LightGBMは、勾配ブースティング決定木(GBDT)アルゴリズムを使用したモデルです。
GBDTは、複数の決定木を組み合わせることで、与えられたタスクを解決する手法です。
LightGBMは、GBDTをさらに高速化するための様々な改善を施しており、大規模なデータセットでも高速に学習できます。
また、回帰や分類問題など多くの異なるタスクに対して有効であることが示されており、特にkaggleなどの機械学習コンペでは常連のモデルとなっています。

詳細な実装(関連ライブラリの読み込み)
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

import lightgbm as lgbm

from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import accuracy_score

import shap
# 可視化のための javascript を読み込み
shap.initjs()
詳細な実装(データの読み込み)
SEED = 712

########################## データの読み込み
df_train = pd.read_csv("../features/002_train.csv")
df_test = pd.read_csv("../features/002_test.csv")

df_train_lab = df_train.pop("Survived")
詳細な実装(パラメータ)
GBM_PARAMS = {
  # 目的関数. これの意味で最小となるようなパラメータを探します. 
  "objective": "binary", 

    # 学習率. 小さいほどなめらかな決定境界が作られて性能向上に繋がる場合が多いです、
  # がそれだけ木を作るため学習に時間がかかります
  "learning_rate": .005,

  # L2 Reguralization
  "reg_lambda": .1,
  # こちらは L1 
  "reg_alpha": .1,

  # 木の深さ. 深い木を許容するほどより複雑な交互作用を考慮するようになります
  "max_depth": 7, 

  # 木の最大数. early_stopping という枠組みで木の数は制御されるようにしていますのでとても大きい値を指定しておきます.
  "n_estimators": 50000, 

  # 木を作る際に考慮する特徴量の割合. 1以下を指定すると特徴をランダムに欠落させます。小さくすることで, まんべんなく特徴を使うという効果があります.
  "colsample_bytree": .7, 

  # 最小分割でのデータ数. 小さいとより細かい粒度の分割方法を許容します.
  "min_child_samples": 20,

  # bagging の頻度と割合
  "subsample_freq": 3,
  "subsample": .9,

  # 特徴重要度計算のロジック(後述)
  "importance_type": "gain",
  
  "early_stopping_rounds": 50,
  "verbose_eval": 200,
  
  # "metrics": "auc",
  "verbose": -1,
  "seed": SEED,
  
  'device_type': 'gpu',
}
詳細な実装(学習)
def fit_lgbm(X: pd.DataFrame, y, cv, params):
    oof_pred = np.zeros(len(X), dtype=np.float32)
    models = []
    scores = []

    for i, (idx_train, idx_valid) in enumerate(cv):
        x_train, y_train = X[X.index.isin(idx_train)], y[idx_train]
        x_valid, y_valid = X[X.index.isin(idx_valid)], y[idx_valid]

        ds_train = lgbm.Dataset(x_train, y_train)
        ds_valid = lgbm.Dataset(x_valid, y_valid)

        with Timer(prefix=f"fit ========== Fold: {i + 1}"):
            model = lgbm.train(
                params,
                ds_train,
                valid_names=["train, valid"],
                valid_sets=[ds_train, ds_valid]
            )

            pred_i = model.predict(x_valid, num_iteration=model.best_iteration)
            oof_pred[idx_valid] = pred_i

            score = accuracy_score(y_valid, pred_i.round())
            print(f" - fold{i + 1} - {score:.4f}")

            scores.append(score)
            models.append(model)
        
    score = accuracy_score(y, oof_pred.round())
    print("=" * 50)
    print(f"FINISH: Whole Score: {score:.4f}")

    return oof_pred, models, score

4-1-a. Cross Validation(交差検証、以降では CV)の結果

 - fold1 - 0.8324
fit ========== Fold: 1 1.008[s]
 - fold2 - 0.8427
fit ========== Fold: 2 0.860[s]
 - fold3 - 0.8989
fit ========== Fold: 3 1.776[s]
 - fold4 - 0.7865
fit ========== Fold: 4 0.923[s]
 - fold5 - 0.8708
fit ========== Fold: 5 1.495[s]
==================================================
FINISH: Whole Score: 0.8462

4-1-b. 特徴量の重要度の可視化

チケット番号のLabelEncodingが最も効いているようです。
また、年齢も重要な特徴量になってそうですね!

詳細な実装
def visualize_importance(models, feat_train_df, top_n):
    """lightGBM の model 配列の feature importance を plot する
    CVごとのブレを boxen plot として表現

    args:
        models:
            List of lightGBM models
        feat_train_df:
            学習時に使った DataFrame
    """
    feature_importance_df = pd.DataFrame()
    for i, model in enumerate(models):
        _df = pd.DataFrame()
        _df["feature_importance"] = model.feature_importance(importance_type='split')
        _df["column"] = feat_train_df.columns
        _df["fold"] = i + 1
        feature_importance_df = pd.concat([feature_importance_df, _df], 
                                          axis=0, ignore_index=True)

    order = feature_importance_df.groupby("column")\
        .sum()[["feature_importance"]]\
        .sort_values("feature_importance", ascending=False).index[:top_n]

    fig, ax = plt.subplots(figsize=(8, max(6, len(order) * .25)))
    sns.boxenplot(data=feature_importance_df, 
                  x="feature_importance", 
                  y="column", 
                  order=order, 
                  ax=ax, 
                  palette="viridis", 
                  orient="h")
    ax.tick_params(axis="x", rotation=90)
    ax.set_title("Importance")
    ax.grid()
    fig.tight_layout()
    plt.show()
    return fig, ax

fig, ax = visualize_importance(models, df_train, 100)

4-1-c. 予測値の分布を見る

詳細な実装
plt.subplots(figsize=(8, 6))
ax = sns.distplot(oof, bins=30, label="train out of fold")
ax = sns.distplot(pred_prob, bins=30, label="test predict")
ax.legend()
plt.show()

4-1-d. Shapで特徴量の影響を見る

詳細な実装(Shapで特徴量の影響を見る)
explainer = shap.TreeExplainer(models[0])

shap_values = explainer.shap_values(df_train)
shap.summary_plot(shap_values=shap_values[0], features=df_train)

4-1-e. CV / LB

CVの精度は0.846とかなり高いスコアを予測できています。
では、submitしてみるとどうなるでしょうか?
0.797!悪くないのではないでしょうか?

4-2. XGBoost

XGBoostもLightGBMと同じく、勾配ブースティング決定木のモデルとなっています。
kaggleなどのデータ分析コンペではLightGBMとのアンサンブル(複数のモデルを組み合わせること)モデルとしてよく使われます。
XGBoostとLightGBMの両方とも、高い性能を発揮しますが、LightGBMが一般的により高速に動作することが知られています。

詳細な実装(関連ライブラリの読み込み)
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

import xgboost as xgb

from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import f1_score, accuracy_score

import shap
# 可視化のための javascript を読み込み
shap.initjs()
詳細な実装(データの読み込み)
######### 延長戦になるかどうかを予測するモデル
SEED = 712

########################## データの読み込み
df_train = pd.read_csv("../features/002_train.csv")
df_test = pd.read_csv("../features/002_test.csv")

df_train_lab = df_train.pop("Survived")
詳細な実装(パラメータ)
XGB_PARAMS = {
    "objective": "binary:logistic",
    "eta": .005,
    "max_depth": 7,
    "eval_metric": "logloss",
    "tree_method": "gpu_hist",
}
詳細な実装(学習)
def fit_xgb(X: pd.DataFrame, y, cv, params, early_stopping_rounds: int = 20):
    oof_pred = np.zeros(len(X), dtype=np.float32)
    models = []
    scores = []

    for i, (idx_train, idx_valid) in enumerate(cv):
        x_train, y_train = X[X.index.isin(idx_train)], y[idx_train]
        x_valid, y_valid = X[X.index.isin(idx_valid)], y[idx_valid]

        ds_train = xgb.DMatrix(x_train, y_train)
        ds_valid = xgb.DMatrix(x_valid, y_valid)

        with Timer(prefix=f"fit ========== Fold: {i + 1}"):
            model = xgb.train(
                params,
                dtrain=ds_train,
                evals=[(ds_train, "train"), (ds_valid, "valid")],
                early_stopping_rounds=early_stopping_rounds,
                num_boost_round=500000,
                verbose_eval=200
            )

            pred_i = model.predict(ds_valid, ntree_limit=model.best_ntree_limit)
            oof_pred[idx_valid] = pred_i

            score = accuracy_score(y_valid, pred_i.round())
            print(f" - fold{i + 1} - {score:.4f}")

            scores.append(score)
            models.append(model)
        
    score = accuracy_score(y, oof_pred.round())
    print("=" * 50)
    print(f"FINISH: Whole Score: {score:.4f}")

    return oof_pred, models, score

fold = StratifiedKFold(n_splits=5, shuffle=True, random_state=SEED)
cv = fold.split(df_train, df_train_lab)
oof, models, score = fit_xgb(df_train, df_train_lab, cv, XGB_PARAMS)

4-2-a. Cross Validation(交差検証、以降では CV)の結果

[0]	train-logloss:0.68989	valid-logloss:0.69082
[200]	train-logloss:0.35434	valid-logloss:0.47992
[400]	train-logloss:0.23235	valid-logloss:0.44055
[570]	train-logloss:0.18361	valid-logloss:0.43356
 - fold1 - 0.8156
fit ========== Fold: 1 1.983[s]
[0]	train-logloss:0.68983	valid-logloss:0.69045
[200]	train-logloss:0.35324	valid-logloss:0.46200
[390]	train-logloss:0.23367	valid-logloss:0.43595
 - fold2 - 0.8202
fit ========== Fold: 2 1.421[s]
[0]	train-logloss:0.69009	valid-logloss:0.69001
[200]	train-logloss:0.37238	valid-logloss:0.39419
[400]	train-logloss:0.25708	valid-logloss:0.31708
[600]	train-logloss:0.20065	valid-logloss:0.29060
[768]	train-logloss:0.17048	valid-logloss:0.28495
 - fold3 - 0.8933
fit ========== Fold: 3 2.520[s]
[0]	train-logloss:0.68977	valid-logloss:0.69069
[200]	train-logloss:0.34480	valid-logloss:0.49083
[354]	train-logloss:0.24156	valid-logloss:0.47992
 - fold4 - 0.7865
fit ========== Fold: 4 1.243[s]
[0]	train-logloss:0.68997	valid-logloss:0.69027
[200]	train-logloss:0.36193	valid-logloss:0.43049
[400]	train-logloss:0.24332	valid-logloss:0.36727
[553]	train-logloss:0.19982	valid-logloss:0.35849
 - fold5 - 0.8483
fit ========== Fold: 5 1.927[s]
==================================================
FINISH: Whole Score: 0.8328

4-2-b. 予測値の分布を見る

詳細な実装
plt.subplots(figsize=(8, 6))
ax = sns.distplot(oof, bins=30, label="train out of fold")
ax = sns.distplot(pred_prob, bins=30, label="test predict")
ax.legend()
plt.show()

4-2-c. Shapで特徴量の影響を見る

詳細な実装(Shapで特徴量の影響を見る)
explainer = shap.TreeExplainer(models[0])

shap_values = explainer.shap_values(df_train)
shap.summary_plot(shap_values=shap_values[0], features=df_train)

4-2-d. CV / LB

CVの精度は0.833でした。
ではSubmitしてみましょう。..0.790
LightGBMと遜色ない精度が出ていますね!

4-3. NN (Neural Network)

ニューラルネットワークは、脳の構造と機能を参考にした機械学習モデルです。
"ニューロン"と呼ばれる層からなるもので、情報を処理して伝送します。

各ニューロンは他のニューロンや外部からの入力を受け取り、その入力を使用して出力信号を他のニューロンや外部環境に出力します。
各ニューロンの入力と出力は通常数値であり、出力を計算するプロセスを「活性化関数」と呼びます。

ニューラルネットワークは非常に強力であり、画像や音声認識、自然言語処理など多様なタスクで活用できます。
大規模で複雑なデータセットを扱うのに特に優れており、データ内のパターンや関係性に基づいて判断や予測をできます。

特徴量を入力する際に標準化(平均0分散1)する処理をおこないます。
今回は入力層+出力層+隠れ層が3層のモデルを構築してみます。

詳細な実装(関連ライブラリの読み込み)
import pandas as pd
import numpy as np
from typing import List, Callable

import matplotlib.pyplot as plt
import seaborn as sns

from sklearn.model_selection import StratifiedKFold
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score

from tensorflow.keras.callbacks import EarlyStopping
from tensorflow.keras.layers import Dense, Input, BatchNormalization
from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import Adam
詳細な実装(データの読み込み)
SEED = 712

########################## データの読み込み
df_train = pd.read_csv("../features/002_train.csv")
df_test = pd.read_csv("../features/002_test.csv")

df_train_lab = df_train.pop("Survived")
詳細な実装(標準化)
class StanderdScale():
    def __init__(self) -> None:
        self.sc = StandardScaler()

    def fit(self, df_input: pd.DataFrame):
        self.sc.fit(df_input)
        return self.transform(df_input)
    
    def transform(self, df_input):
        return pd.DataFrame(data=self.sc.transform(df_input), columns=df_input.columns)

sc = StanderdScale()
df_train = sc.fit(df_train)
df_test = sc.transform(df_test)
詳細な実装(モデルを作成する関数)
class NNModel:
    """Neaural Networkモデルの構築"""
    def __init__(
            self,
            input_size: int,
            hidden_sizes: List,
            output_size: int,
            activation: str = "relu",
            output_activation: str = "relu",
            add_BN: bool = True
            ) -> None:
        self.input = Input(shape=(input_size, ), name="input")
        self.output_size = Dense(output_size, name="output")
        self.add_BN = add_BN

        self.hidden_layers = []
        for idx, hidden_size in enumerate(hidden_sizes):
            if (idx+1) == len(self.hidden_layers):
                self.hidden_layers.append(Dense(hidden_size, output_activation, name=f"hidden_{idx+1}"))
                continue
            self.hidden_layers.append(Dense(hidden_size, activation, name=f"hidden_{idx+1}"))
    
    def build(self) -> Model:
        inputs = self.input
        for idx, hidden_layer in enumerate(self.hidden_layers):
            if idx == 0:
                x = hidden_layer(inputs)
                continue

            x = BatchNormalization()(x)
            x = hidden_layer(x)
        outputs = self.output_size(x)
        return Model(inputs, outputs)
詳細な実装(学習モデルの定義)
def plot_history(hist):
    # 損失値(Loss)の遷移のプロット
    plt.plot(hist.history['loss'],label="train set")
    plt.plot(hist.history['val_loss'],label="test set")
    plt.title('model loss')
    plt.xlabel('epoch')
    plt.ylabel('loss')
    plt.legend()
    plt.show()

def fit_nn(
        X,
        y,
        cv, 
        metrics: List,
        score_func: Callable[[np.ndarray, np.ndarray], float],
        NNModel: Model, 
        NN_MODEL_PARAMS: dict,
        n_epoch: int = 500, 
        loss: str = "mse",
        optimizer = "adam",
        verbose: int = -1,
        eary_stopping_rounds: int = 50,
        ):
    oof_pred = np.zeros(len(X), dtype=np.float32)
    models = []
    scores = []

    for i, (idx_train, idx_valid) in enumerate(cv):
        x_train, y_train = X[X.index.isin(idx_train)], y[idx_train]
        x_valid, y_valid = X[X.index.isin(idx_valid)], y[idx_valid]

        model = NNModel(**NN_MODEL_PARAMS)
        model = model.build()
        model.compile(
            optimizer,
            loss,
            metrics
        )
        history = model.fit(
                x_train,
                y_train,
                epochs=n_epoch,
                verbose=verbose,
                validation_data=(x_valid, y_valid),
                callbacks=[EarlyStopping(patience=eary_stopping_rounds)]
            )

        plot_history(history)

        oof = model.predict(x_valid)
        oof_pred[idx_valid] = oof.flatten()
        models.append(model)
        
        print("-"*50)
        if score_func.__name__ == "accuracy_score":
            print(f"score {i+1}:\t {score_func(y_valid, np.round(oof).astype(int))}")
        else:
            print(f"score {i+1}:\t {score_func(y_valid, oof)}")

    print("*"*50)
    if score_func.__name__ == "accuracy_score":
        score = score_func(y, np.round(oof_pred).astype(int))
    else:
        score = score_func(y, oof_pred)
    print(f"score {i+1}:\t {score}")

    return models, oof_pred, score
詳細な実装(学習)
NN_MODEL_PARAMS = {
    "input_size": len(df_train.columns),
    "hidden_sizes": [512, 256, 128, 64, 32],
    "output_size": 1,
    "activation": "relu",
    "output_activation": "sigmoid",
    "add_BN": True
}

NN_FIT_PARAMS = {
    "NNModel": NNModel,
    "NN_MODEL_PARAMS": NN_MODEL_PARAMS,
    "metrics": ["accuracy"],
    "score_func": accuracy_score,
    "n_epoch": 1000,
    "loss": "mse",
    "optimizer": Adam(learning_rate=.001),
    "verbose": 0,
    "eary_stopping_rounds": 50
}

fold = StratifiedKFold(n_splits=5, shuffle=True, random_state=SEED)
cv = fold.split(df_train, df_train_lab)
models, oof, score = fit_nn(X=df_train, y=df_train_lab, cv=cv, **NN_FIT_PARAMS)

# k 個のモデルの予測確率 (predict_proba) を作成. shape = (k, N_test, n_classes).
pred_prob = np.array([model.predict(df_test).flatten() for model in models])
print(f"1. shape: {pred_prob.shape}")

# k 個のモデルの平均を計算
pred_prob = np.mean(pred_prob, axis=0) # axis=0 なので shape の `k` が潰れる 
print(f"2. shape: {pred_prob.shape}")

4-3-a. Cross Validation(交差検証、以降では CV)の結果

6/6 [==============================] - 0s 1ms/step
--------------------------------------------------
score 1:	 0.7877094972067039
6/6 [==============================] - 0s 1ms/step
--------------------------------------------------
score 2:	 0.7752808988764045
6/6 [==============================] - 0s 1ms/step
--------------------------------------------------
score 3:	 0.8876404494382022
6/6 [==============================] - 0s 999us/step
--------------------------------------------------
score 4:	 0.8370786516853933
6/6 [==============================] - 0s 1ms/step
--------------------------------------------------
score 5:	 0.8426966292134831
**************************************************
score 5:	 0.8260381593714927

4-3-b. 予測値の分布を見る

詳細な実装

plt.subplots(figsize=(8, 6))
ax = sns.distplot(oof, bins=30, label="train out of fold")
ax = sns.distplot(pred_prob, bins=30, label="test predict")
ax.legend()
plt.show()

4-3-c. CV / LB

CVの精度は0.826でした。
ではSubmitしてみましょう。..0.754
勾配ブースティングベースのモデルと比較すると若干精度は劣るようです。
層の数や入力サイズなど、調整すべきパラメータが多いため、最適化には時間を要しそうです。

4-4. SVM

SVMは、分類や回帰タスクに使用できる教師あり学習アルゴリズムです。
異なるクラスのデータの要素を最大限に分離するように実装されます。
また、高次元データの扱いも可能で、外れ値にも強いとされています。

詳細な実装(関連ライブラリの読み込み)
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import f1_score, accuracy_score
from sklearn.preprocessing import StandardScaler

from sklearn.svm import SVC
詳細な実装(データの読み込み)
SEED = 712

########################## データの読み込み
df_train = pd.read_csv("../features/002_train.csv")
df_test = pd.read_csv("../features/002_test.csv")

df_train_lab = df_train.pop("Survived")
詳細な実装(標準化)
class StanderdScale():
    def __init__(self) -> None:
        self.sc = StandardScaler()

    def fit(self, df_input: pd.DataFrame):
        self.sc.fit(df_input)
        return self.transform(df_input)
    
    def transform(self, df_input):
        return pd.DataFrame(data=self.sc.transform(df_input), columns=df_input.columns)

sc = StanderdScale()
df_train = sc.fit(df_train)
df_test = sc.transform(df_test)
詳細な実装(学習)
def fit_svm(X: pd.DataFrame, y, cv):
    oof_pred = np.zeros(len(X), dtype=np.float32)
    models = []
    scores = []

    for i, (idx_train, idx_valid) in enumerate(cv):
        x_train, y_train = X[X.index.isin(idx_train)], y[idx_train]
        x_valid, y_valid = X[X.index.isin(idx_valid)], y[idx_valid]

        with Timer(prefix=f"fit ========== Fold: {i + 1}"):
            model = SVC()
            model.fit(x_train, y_train)

            pred_i = model.predict(x_valid)
            oof_pred[idx_valid] = pred_i

            score = accuracy_score(y_valid, pred_i.round())
            print(f" - fold{i + 1} - {score:.4f}")

            scores.append(score)
            models.append(model)
        
    score = accuracy_score(y, oof_pred.round())
    print("=" * 50)
    print(f"FINISH: Whole Score: {score:.4f}")

    return oof_pred, models, score

fold = StratifiedKFold(n_splits=5, shuffle=True, random_state=SEED)
cv = fold.split(df_train, df_train_lab)
oof, models, score = fit_svm(df_train, df_train_lab, cv)

4-4-a. Cross Validation(交差検証、以降では CV)の結果

 - fold1 - 0.8324
fit ========== Fold: 1 0.019[s]
 - fold2 - 0.8090
fit ========== Fold: 2 0.017[s]
 - fold3 - 0.8764
fit ========== Fold: 3 0.019[s]
 - fold4 - 0.7921
fit ========== Fold: 4 0.017[s]
 - fold5 - 0.8315
fit ========== Fold: 5 0.018[s]
==================================================
FINISH: Whole Score: 0.8283

4-4-b. 予測値の分布を見る

詳細な実装
plt.subplots(figsize=(8, 6))
ax = sns.distplot(oof, bins=30, label="train out of fold")
ax = sns.distplot(pred_prob, bins=30, label="test predict")
ax.legend()
plt.show()

4-4-c. CV / LB

CVの精度は0.828でした。
ではSubmitしてみましょう。..0.766
CV / LBともにNNよりも若干高い精度が出ています。
パラメータの数も少なく、実装も簡単なので、使い勝手は良さそうです。

4-5. k-NN

k-NNはk近傍法とも呼ばれ、新しいデータが与えられたときに、その近傍のk個のデータの要素を見つけます。
k個のデータの要素の多数決に基づいて、新しいデータのクラスを予測します。

データが非常に大きい場合や、訓練データセットが膨大な場合には適しているとはいえないとされています。
また、特徴量の数が多い場合にも、予測の正確性において低下する可能性があります。
そのため、k-NNは小規模で特徴量の数が少ないデータセットで有効です。

今回は、k(近傍数)を2から50まで探索してみます。

詳細な実装(関連ライブラリの読み込み)
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import f1_score, accuracy_score
from sklearn.preprocessing import StandardScaler

from sklearn.neighbors import KNeighborsClassifier
詳細な実装(データの読み込み)
SEED = 712

########################## データの読み込み
df_train = pd.read_csv("../features/002_train.csv")
df_test = pd.read_csv("../features/002_test.csv")

df_train_lab = df_train.pop("Survived")
詳細な実装(標準化)
class StanderdScale():
    def __init__(self) -> None:
        self.sc = StandardScaler()

    def fit(self, df_input: pd.DataFrame):
        self.sc.fit(df_input)
        return self.transform(df_input)
    
    def transform(self, df_input):
        return pd.DataFrame(data=self.sc.transform(df_input), columns=df_input.columns)

sc = StanderdScale()
df_train = sc.fit(df_train)
df_test = sc.transform(df_test)
詳細な実装(モデル)
def fit_knn(X: pd.DataFrame, y, cv, k: int = 6):
    oof_pred = np.zeros(len(X), dtype=np.float32)
    models = []
    scores = []

    for i, (idx_train, idx_valid) in enumerate(cv):
        x_train, y_train = X[X.index.isin(idx_train)], y[idx_train]
        x_valid, y_valid = X[X.index.isin(idx_valid)], y[idx_valid]

        with Timer(prefix=f"fit ========== Fold: {i + 1}"):
            model = KNeighborsClassifier(n_neighbors=k)
            model.fit(x_train, y_train)

            pred_i = model.predict(x_valid)
            oof_pred[idx_valid] = pred_i

            score = accuracy_score(y_valid, pred_i.round())
            print(f" - fold{i + 1} - {score:.4f}")

            scores.append(score)
            models.append(model)
        
    score = accuracy_score(y, oof_pred.round())
    print("=" * 50)
    print(f"FINISH: Whole Score: {score:.4f}")

    return oof_pred, models, score
詳細な実装(kの探索)
scores = []
for k in range(1, 51):
    fold = StratifiedKFold(n_splits=5, shuffle=True, random_state=SEED)
    cv = fold.split(df_train, df_train_lab)
    oof, models, score = fit_knn(df_train, df_train_lab, cv, k)
    scores.append(score)

plt.subplots(figsize=(12, 6))
g = sns.lineplot(x=range(1, 51), y=scores)
g.grid()
plt.show()
print(np.argmax(scores)+1)
14

CVによる最適な近傍数を探索した結果、k=14のときが最大となりました。

詳細な実装(学習)
fold = StratifiedKFold(n_splits=5, shuffle=True, random_state=SEED)
cv = fold.split(df_train, df_train_lab)
oof, models, score = fit_knn(df_train, df_train_lab, cv, k=np.argmax(scores)+1)

4-5-a. Cross Validation(交差検証、以降では CV)の結果

 - fold1 - 0.8436
fit ========== Fold: 1 0.009[s]
 - fold2 - 0.8539
fit ========== Fold: 2 0.014[s]
 - fold3 - 0.8652
fit ========== Fold: 3 0.011[s]
 - fold4 - 0.8202
fit ========== Fold: 4 0.010[s]
 - fold5 - 0.8371
fit ========== Fold: 5 0.012[s]
==================================================
FINISH: Whole Score: 0.8440

4-5-b. 予測値の分布を見る

詳細な実装
plt.subplots(figsize=(8, 6))
ax = sns.distplot(oof, bins=30, label="train out of fold")
ax = sns.distplot(pred_prob, bins=30, label="test predict")
ax.legend()
plt.show()

4-5-c. CV / LB

CVの精度は0.844でした。
ではSubmitしてみましょう。..0.761
CVの精度はLightGBMに次いで高いですが、LBがあまり良くありませんね。
Trust CV(交差検証を信じよ)という文脈ではある程度精度が出ているという判断でも良いのかもしれません。

5. まとめ

今回は5つのモデル(LightGBM,XGBoost、NN、SVM、k-NN)に対してCVとLBを比較してみました。
まとめは以下の通りになります。

Model CV Accuracy LB Accuracy
LightGBM 0.846 0.797
XGBoost 0.833 0.790
NN 0.826 0.754
SVM 0.828 0.766
k-NN 0.844 0.761

GBDT系のモデルは安定して精度が出ていますね。
これらのモデルをアンサンブルして提出するとさらに良い精度が得られそうですね!

今回はこんなところで!

GitHubで編集を提案

Discussion