💨

[kaggle] Spaceship Titanic で学ぶ

2023/12/22に公開

この記事は mob Advent Calendar 22日目の記事です。

Kaggle を初めて開いてから2回目。 Getting Started な Spaceship Titanic をさらっと眺めてみてもちんぷんかんぷんだったので、メモしながら解説していきます。

https://www.kaggle.com/competitions/spaceship-titanic/overview

問題の内容

箇条書きにするとこのような感じです。

  • 2912年、宇宙の謎を解くためにはデータサイエンスのスキルが必要です
  • 4光年離れた場所からの通信を受信し、状況が良くないことが分かりました
  • 「スペースシップ・タイタニック」は1ヶ月前に発射された恒星間客船で、約13,000人の乗客を乗せていました
  • この船は、我々の太陽系から近くの星々を周回する3つの新しく住める系外惑星へ移民を輸送するために出発しました
  • 最初の目的地である熱い惑星「55カンクリE」へ向かう途中、アルファ・ケンタウリを回っている間に、スペースシップ・タイタニックは宇宙塵雲内に隠された時空の異常と衝突しました
  • 残念ながら、1000年前の同名の船と同じ運命をたどり、乗客のほぼ半数が別の次元に転送されてしまいました
  • この宇宙船の救助活動と失われた乗客の回収に貢献するために、宇宙船の損傷したコンピューターシステムから回収された記録を使って、どの乗客が時空の異常によって転送されたかを予測する課題に挑戦します

データセットについて

train.csvtest.csvsample_submission.csv が用意されています。

train.csvtest.csv は構造は同じで、その名の通り前者がトレーニング用のデータ、後者がテスト用のデータです。データとしては下記のものがあります。

  • PassengerId: 各乗客に割り当てられたユニークなID。形式は gggg_pp で、gggg は乗客が所属するグループを示し、pp はそのグループ内での番号です。グループにはしばしば家族が含まれますが、必ずしもそうではありません。
  • HomePlanet: 乗客が出発した惑星で、通常は永住惑星です。
  • CryoSleep: 乗客が航海の期間中、人工休眠状態に入ることを選択したかどうかを示します。人工休眠中の乗客はキャビンに閉じ込められます。
  • Cabin: 乗客が滞在しているキャビン番号。形式は deck/num/side で、side はポート(P)かスターボード(S)のいずれかです。
  • Destination: 乗客が下船する惑星。
  • Age: 乗客の年齢。
  • VIP: 乗客が航海中に特別なVIPサービスを購入したかどうか。
  • RoomService, FoodCourt, ShoppingMall, Spa, VRDeck: スペースシップ・タイタニックの各種豪華アメニティで乗客が請求した金額。
  • Name: 乗客の名前(名と姓)。
  • Transported: 乗客が別の次元に転送されたかどうか。これが予測対象の列です。

参考にするノートブック

こちらのノートブックを参考にどのようにモデルを作っていくのかをみていきます。
こちらのノートブックではTensorFlow Decision Forestsを用いた基本的なランダムフォレストモデルのトレーニング方法を説明しています。

https://www.kaggle.com/code/gusthema/spaceship-titanic-with-tfdf

ライブラリのロード

まずはライブラリのロードをしています 。

import tensorflow as tf
import tensorflow_decision_forests as tfdf
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

データセットのロードおよび確認

データセットをロードして

dataset_df = pd.read_csv('/kaggle/input/spaceship-titanic/train.csv')

データをざっとみてみます。

dataset_df.head(5)
dataset_df.describe()
dataset_df.info()

最後の info() の output は次のようになっています。

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 8693 entries, 0 to 8692
Data columns (total 14 columns):
 #   Column        Non-Null Count  Dtype  
---  ------        --------------  -----  
 0   PassengerId   8693 non-null   object 
 1   HomePlanet    8492 non-null   object 
 2   CryoSleep     8476 non-null   object 
 3   Cabin         8494 non-null   object 
 4   Destination   8511 non-null   object 
 5   Age           8514 non-null   float64
 6   VIP           8490 non-null   object 
 7   RoomService   8512 non-null   float64
 8   FoodCourt     8510 non-null   float64
 9   ShoppingMall  8485 non-null   float64
 10  Spa           8510 non-null   float64
 11  VRDeck        8505 non-null   float64
 12  Name          8493 non-null   object 
 13  Transported   8693 non-null   bool   
dtypes: bool(1), float64(6), object(7)
memory usage: 891.5+ KB

他にも Notebookでは グラフで表示したりしてました。

前処理する

まずは不要な PassengerId, Name を削除します

dataset_df = dataset_df.drop(['PassengerId', 'Name'], axis=1)

次の記述で'VIP', 'CryoSleep', 'FoodCourt', 'ShoppingMall', 'Spa', 'VRDeck' の欠損値を 0 で置き換えています。

dataset_df[['VIP', 'CryoSleep', 'FoodCourt', 'ShoppingMall', 'Spa', 'VRDeck']] = dataset_df[['VIP', 'CryoSleep', 'FoodCourt', 'ShoppingMall', 'Spa', 'VRDeck']].fillna(value=0)

TF-DFはブール型の列を扱うことができないため、整数型に変換します。

dataset_df['Transported'] = dataset_df['Transported'].astype(int)
dataset_df['VIP'] = dataset_df['VIP'].astype(int)
dataset_df['CryoSleep'] = dataset_df['CryoSleep'].astype(int)

'Cabin' には deck/num/side の形でデータが入っているため、それぞれ分割して、'Cabin'そのものはドロップします。

dataset_df[["Deck", "Cabin_num", "Side"]] = dataset_df["Cabin"].str.split("/", expand=True)
try:
    dataset_df = dataset_df.drop('Cabin', axis=1)
except KeyError:
    print("Field does not exist")

最後にデータセットをトレーニング用と検証用に分割します。

def split_dataset(dataset, test_ratio=0.20):
  test_indices = np.random.rand(len(dataset)) < test_ratio
  return dataset[~test_indices], dataset[test_indices]

train_ds_pd, valid_ds_pd = split_dataset(dataset_df)
print("{} examples in training, {} examples in testing.".format(
    len(train_ds_pd), len(valid_ds_pd)))
train_ds = tfdf.keras.pd_dataframe_to_tf_dataset(train_ds_pd, label=label)
valid_ds = tfdf.keras.pd_dataframe_to_tf_dataset(valid_ds_pd, label=label)

モデルを作る

下記でモデルを作ります。

rf = tfdf.keras.RandomForestModel()
rf.fit(x=train_ds)

ノートブックでは決定木をビジュアライズしたりしてます。

検証する

検証用データセットを用いて精度をみます。

evaluation = rf.evaluate(x=valid_ds,return_dict=True)

for name, value in evaluation.items():
  print(f"{name}: {value:.4f}")

重要な指標を見る

下記で重要指標をみることができます。

inspector = rf.make_inspector()
inspector.variable_importances()["NUM_AS_ROOT"]

NUM_AS_ROOTは決定木でどれだけ使われたかがわかります。これのアウトプットが下記です。

[("CryoSleep" (1; #2), 113.0),
 ("Spa" (1; #10), 69.0),
 ("RoomService" (1; #7), 52.0),
 ("VRDeck" (1; #12), 39.0),
 ("ShoppingMall" (1; #8), 14.0),
 ("FoodCourt" (1; #5), 6.0),
 ("Age" (1; #0), 3.0),
 ("Deck" (4; #3), 3.0),
 ("HomePlanet" (4; #6), 1.0)]

提出する

test.csv を用いて、作ったモデルをもとに rf.predict します。
testデータについても先ほどと同様の前処理をおこなっています。

# Load the test dataset
test_df = pd.read_csv('/kaggle/input/spaceship-titanic/test.csv')
submission_id = test_df.PassengerId

# Replace NaN values with zero
test_df[['VIP', 'CryoSleep']] = test_df[['VIP', 'CryoSleep']].fillna(value=0)

# Creating New Features - Deck, Cabin_num and Side from the column Cabin and remove Cabin
test_df[["Deck", "Cabin_num", "Side"]] = test_df["Cabin"].str.split("/", expand=True)
test_df = test_df.drop('Cabin', axis=1)

# Convert boolean to 1's and 0's
test_df['VIP'] = test_df['VIP'].astype(int)
test_df['CryoSleep'] = test_df['CryoSleep'].astype(int)

# Convert pd dataframe to tf dataset
test_ds = tfdf.keras.pd_dataframe_to_tf_dataset(test_df)

# Get the predictions for testdata
predictions = rf.predict(test_ds)
n_predictions = (predictions > 0.5).astype(bool)
output = pd.DataFrame({'PassengerId': submission_id,
                       'Transported': n_predictions.squeeze()})
		       
# Submission
sample_submission_df = pd.read_csv('/kaggle/input/spaceship-titanic/sample_submission.csv')
sample_submission_df['Transported'] = n_predictions
sample_submission_df.to_csv('/kaggle/working/submission.csv', index=False)
sample_submission_df.head()

これを提出したスコアは 0.77975 でした。

参考に自分の回答も出してみる

参考にしたノートブックでは PassengerId を削除してモデルをつくっていました 。

しかし、 PassengerId にはあるランダムな文字列ではなくて、意味をもった数字になっています。

各乗客に割り当てられたユニークなID。形式は gggg_pp で、gggg は乗客が所属するグループを示し、pp はそのグループ内での番号です。グループにはしばしば家族が含まれますが、必ずしもそうではありません。

ここも考慮したモデルを考えてみます。 PassengerId を drop していないようにして、次のようにPassengerIdから二つのカラムを作ります。

dataset_df[["GroupId", "NoInGroup"]] = dataset_df["PassengerId"].str.split("_", expand=True)

問題なく追加されてそうです。

モデルができたので、精度や NUM_AS_ROOT などをみていきます。


元の精度


改善後の精度

改善したつもりですが、精度は少し落ちてます。


元のNUM_AS_ROOT


改善後のNUM_AS_ROOT

追加した GroupId や NoInGroup などは出てきてません。これらはモデルに対する影響度は小さいようです。残念。

先ほどのソースコードを少しだけ編集して提出します。

# Load the test dataset
test_df = pd.read_csv('/kaggle/input/spaceship-titanic/test.csv')
submission_id = test_df.PassengerId

# Replace NaN values with zero
test_df[['VIP', 'CryoSleep']] = test_df[['VIP', 'CryoSleep']].fillna(value=0)

# Creating New Features - Deck, Cabin_num and Side from the column Cabin and remove Cabin
test_df[["Deck", "Cabin_num", "Side"]] = test_df["Cabin"].str.split("/", expand=True)
test_df = test_df.drop('Cabin', axis=1)

+# Creating New Features - GroupId and NoInGroup from the column PassengerId
+test_df[["GroupId", "NoInGroup"]] = test_df["PassengerId"].str.split("_", expand=True)

# Convert boolean to 1's and 0's
test_df['VIP'] = test_df['VIP'].astype(int)
test_df['CryoSleep'] = test_df['CryoSleep'].astype(int)

# Convert pd dataframe to tf dataset
test_ds = tfdf.keras.pd_dataframe_to_tf_dataset(test_df)

# Get the predictions for testdata
predictions = rf.predict(test_ds)
n_predictions = (predictions > 0.5).astype(bool)
output = pd.DataFrame({'PassengerId': submission_id,
                       'Transported': n_predictions.squeeze()})

output.head()

# Submission
sample_submission_df = pd.read_csv('/kaggle/input/spaceship-titanic/sample_submission.csv')
sample_submission_df['Transported'] = n_predictions
sample_submission_df.to_csv('/kaggle/working/submission.csv', index=False)
sample_submission_df.head()

スコアを出してみたら 0.7877 で本当に少しだけスコアが上昇しました。

また、ノートブックを参考に理解して、自分でも改善してさらに学んでいこうと思います。

Discussion