【Kaggleチュートリアル】spaceship Titanicに参加してみた

プログラミング

機械学習とPythonの勉強がてら、kaggle常設コンテストのspaceTitanicに参加してみた。

ドメイン知識による有利不利が関係ないので簡単そう 見慣れない奇抜な題材で面白そうなので、参加してみるとことにした。

kaggle、Pythonについては初心者なので、間違い、不適切な考察があるかもしれないのでご了承を。

Spaceship Titanic | Kaggle
Predict which passengers are transported to an alternate dimension

コンテスト概要

2912年(タイタニック号が出航してから1000年後)、宇宙船タイタニック号が13,000人の乗客を乗せていた。乗客らは、太陽系から近隣の居住可能な太陽系外惑星へ移住しようとしてる。

その航海中、宇宙船タイタニック号は時空の歪みに衝突したことで、乗客のほぼ半数が異次元に転送された。

今回は、与えられるデータを基に、乗客が異次元に転送されたら否かを予測することが目的となる。

与えられるデータ

  • PassengerId
    それぞれの乗客に対して一意の番号が割り振られている。IDはgggg_ppの形式となっており、ggggはグループを示す。ppはグループの人数を示す。なお、グループのメンバーは家族であることが多いが、必ずしもそうであるとは限らない。
  • HomePlanet
    乗客の出発した惑星。基本的に彼らの居住地となっている。(Earth、Europa、Marsのどれかに属する。)
  • CryoSleep
    乗客が、航海中に冷凍睡眠をしているか。冷凍睡眠中の乗客は客室に閉じ込められる。
  • Cabin
    どの客室にいるかを示す。Cabinはdeck/num/sideの形式をとる。sideの、PはPort(進行方向に対して左側のこと)、SはStarbord(進行方向に対して右側のこと)を示す。
  • Destination
    乗客の目的地(TRAPPIST-1e、55 Cancri e、PSO J318.5-22のどれかに属する。)
  • Age
    乗客の年齢
  • VIP
    乗客が特別VIPサービスにお金を払っているかを示す
  • RoomService, FoodCourt, ShoppingMall, Spa, VRDeck
    タイタニック号の豪華施設にどれほどの金額を使ったかを表す。
  • Name
    乗客の氏名
  • Transported
    乗客が異次元に転送されたかを表す。今回、予測する目標となる項目である。

データの特徴

2912年の宇宙船事故による異次元転送の有無なんて、何が因子か見当もつかないので、とりあえず何が関係がありそうか見てみる。

とりあえず、以下のようにデータ処理に必要そうなライブラリをインポートして、各種データを見てみた。

Python
import seaborn as sns
import numpy as np
import pandas as pd
import math

import os
for dirname, _, filenames in os.walk('/kaggle/input'):
    for filename in filenames:
        print(os.path.join(dirname, filename))

dataset=pd.read_csv("/kaggle/input/spaceship-titanic/train.csv")

年齢別転送数

Python
sns.histplot(x="Age",hue="Transported",data=dataset)

どうやら10代は転送されていることが多く、20歳前後はやや転送させないことが多い。

それ以外の年代については、あまり年齢と転送数に大きな関係はなさそう。

VIP別転送数

Python
sns.countplot(x="VIP",hue="Transported",data=dataset)

VIPだろうとそうでなかろうと、あまり転送に影響を与えることはなさそう。

VIP会員は人数が少ないのでわかりづらいが、転送された人:転送されていない人は180:222となっており、これもあまり大きな関係はなさそう。

目的地別転送数

Python
sns.countplot(x="Destination",hue="Transported",data=dataset)

目的地が、55 Cancri eの乗客が転送される割合がやや高めであることが判明。

TRAPPIST-1eについては、わずかに転送されにくそう。

出発地別転送数

Python
sns.countplot(x="HomePlanet",hue="Transported",data=dataset)

出発する惑星によっても転送数に差がでることがあることが、判明。

ほむほむ( ˘ω˘)…

特徴量エンジニアリング

Cabin列は、1列にdeck/num/sideと3つの要素が入っているので、このままでは極めて扱いづらい。これをdeck、num、sideの3列に分けて、データ処理に使いやすい形に変換する。

次に、RoomService, FoodCourt, ShoppingMall, Spa, VRDeckの合計使用金額とグループ人数を追加した。最後に今回の予測に名前は使用しないので、名前列を削除する。

Cabin列について

CabinDeck,CabinPortについては、とりあえず欠損値をXXとして、CabinNumについては欠損値を-1として設定し、グラフを見てみる。

Python
df=dataset.assign(CabinDeck="XX").assign(CabinNum=-1).assign(CabinPort="XX")

for index,row in df[::].iterrows():
    cabin = str(row["Cabin"]).split("/")
    if(len(cabin)!=3):
        continue
    for i in range(3):
        df.iat[index,14+i]=cabin[i]

CabinDeck

Python
sns.countplot(x="CabinDeck",hue="Transported",data=df)

各CabinDeck間で転送率に差があることが分かった。また、TDeckの乗客数が極端に少ないことも分かった。

CabinDeckの欠損値を最頻値(つまり、F)で補完してしまうと、欠損している客室が実はBだった時に上手く予測できない。このため、欠損値を示すXXのままにしておく。

なお、Tは極端にデータ数が少なく、残しておくと不均衡データが発生する要因となることから、Tは転送率の差が小さいAに置換する。

Python
df.loc[df["CabinDeck"]=="T","CabinDeck"]="A"

CabinNum

Python
df["CabinNum"]=df["CabinNum"].astype(int)
sns.histplot(x="CabinNum",hue="Transported",data=df)

客室番号によっても差が転送率に差が生じることが判明。

欠損値は最も転送率に差がなさそうな625で補完する。

Python
df.loc[df["CabinNum"]==-1,"CabinNum"]=625

CabinPort

Python
sns.countplot(x="CabinPort",hue="Transported",data=df)

Portによって転送率に差があることが判明。欠損値XXを無理にPかSのどれかに属させると精度悪化につながる可能性があるので、これはこのままにしておく。

合計使用金額について

RoomService, FoodCourt, ShoppingMall, Spa, VRDeckの欠損値処理については、特別な処理を行った。欠損していない他のどの項目で1円も使っていない場合、どの施設も使用していないと考えられる。一方、何かしらの施設を使用している場合は、欠損している項目についても使用していると考えられる。

このため、欠損値以外の要素がすべて0ならば、欠損値を0に、それ以外は、0を除く中央値で欠損値処理を行った。

この様な欠損値処理をした上で、新規にTotalCost列を用意し、RoomService, FoodCourt, ShoppingMall, Spa, VRDeckの合計値を代入する。

Python
MedianData=dataset.iloc[:,7:12].replace(0,float("nan")).median()
for index, row in df.iterrows():
    if row["RoomService"]>0 or row["FoodCourt"]>0 or row["ShoppingMall"]>0 or row["Spa"]>0 or row["VRDeck"]>0:
        df.iloc[index,:]=row.fillna(MedianData)
    else:
        df.iloc[index,7:12]=0
        
df["TotalCost"]=df["RoomService"]+df["FoodCourt"]+df["ShoppingMall"]+df["Spa"]+df["VRDeck"]
Python
sns.histplot(x="TotalCost",hue="Transported",data=df)

船内施設を全く使わない人よりも、何か使っている人の方が転送されにくいことが判明。顕著に差が生じており、なかなかいい感じ。

グループ人数について

trainデータとtestデータを組み合わせて、乗客IDとグループ人数をまとめたデータフレームを作成する。

Python
testdata=pd.read_csv("/kaggle/input/spaceship-titanic/test.csv")
groupdf=pd.DataFrame(pd.concat([df,testdata])["PassengerId"])
groupdf["GroupSize"]=0
size=0
for index,row in groupdf[::-1].iterrows():
    if size==0:
        size=int(str(row["PassengerId"]).split("_")[1])
        groupdf.iloc[index-size+1:index+1,1]=size
    size=size-1
df=df.merge(groupdf,on="PassengerId")
Python
sns.countplot(x="GroupSize",hue="Transported",data=df)

グループ人数が多い程、転送される割合が多いことが判明。

名前列削除について

名前が転送可能性に関与について、検証をしていないので安易に名前を削除してしまうのは、よろしくないかもしれない。しかしながら、普通に考えて転送するか否かは名前と関係がなさそうなので、列を削除した。

Python
df=df.drop("Name",axis=1)
df=df.drop("Cabin",axis=1)
df=df.drop("PassengerId",axis=1)

その他、必要のない列を削除している。

欠損値処理

trainデータには欠損値が存在する。データを見る限り、転送についての決定的な要素はなかったと考えられるため、今回は深く考えず、年齢の欠損値は中央値で、VIPや目的地等カテゴリデータについては最頻値で処理を行った。

Python
df=df.fillna({"Age":dataset["Age"].median()})
df=df.fillna(df.mode().iloc[0])

ダミー変数処理

カテゴリ変数をOne-hotエンコーディングする。この際、多重共線性の発生を防ぐために、ダミー変数を1列だけ削除したい。

drop_firstを有効にすると、CabinDeck_Aが削除されてしまい、欠損値を示すCabinDeck_XXは残ってしまう。このため今回、drop_firstによる削除をせず、明示的に欠損値を示すカテゴリデータを削除する。

Python
df=df.astype({"CabinNum":int})
finaldf=pd.get_dummies(df, drop_first=False)
finaldf=finaldf.drop(["CabinPort_XX","CabinDeck_XX"],axis=1)

学習

今回は、SVMで学習させる。実行するたびに成績が変化すると困るので、パラメータについては、random_state=1としている。

Python
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.svm import SVC
from sklearn.metrics import accuracy_score
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import cross_val_score

X=df.drop(columns="Transported")
Y=df["Transported"]
X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size=0.2, random_state=1)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

# SVMモデルの設定
svm = SVC(kernel='rbf', random_state=1)
# k分割交差検証
k = 5 # 分割数
scores = cross_val_score(svm, X_train, y_train, cv=k, n_jobs=-1)
# 平均精度の表示
print(f'Average accuracy across {k} folds: {scores.mean()}')

# モデルのトレーニング
svm.fit(X_train, y_train)

# テストセットでの評価
y_pred = svm.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print(f'Test set accuracy: {accuracy}')

Average accuracy across 5 folds: 0.798100740112439
Test set accuracy: 0.8016101207590569

予測

以下のコードでtestデータを変形させ、先ほど作成した分類機で予測させた。

Python
testdata=testdata.assign(CabinDeck="XX").assign(CabinNum=625).assign(CabinPort="XX")

for index,row in testdata[::].iterrows():
    cabin = str(row["Cabin"]).split("/")
    if(len(cabin)!=3):
        continue
    for i in range(3):
        testdata.iat[index,13+i]=cabin[i]
testdata.loc[testdata["CabinDeck"]=="T","CabinDeck"]="A"
        
for index, row in testdata.iterrows():
    if row["RoomService"]>0 or row["FoodCourt"]>0 or row["ShoppingMall"]>0 or row["Spa"]>0 or row["VRDeck"]>0:
        testdata.iloc[index,:]=row.fillna(MedianData)
    else:
        testdata.iloc[index,7:12]=0
        
testdata["TotalCost"]=testdata["RoomService"]+testdata["FoodCourt"]+testdata["ShoppingMall"]+testdata["Spa"]+testdata["VRDeck"]

testdata=testdata.merge(groupdf,on="PassengerId")

testdata=testdata.drop("Name",axis=1)
testdata=testdata.drop("Cabin",axis=1)
testdata=testdata.drop("PassengerId",axis=1)

testdata=testdata.fillna({"Age":dataset["Age"].median()})
testdata=testdata.fillna(testdata.mode().iloc[0])

testdata=testdata.astype({"CabinNum":int})
testdata=pd.get_dummies(testdata, drop_first=False)
testdata=testdata.drop("CabinDeck_XX",axis=1)
testdata=testdata.drop("CabinPort_XX",axis=1)
Python
testdata1=scaler.fit_transform(testdata)
pred=rfc.predict(pd.DataFrame(testdata1))

結果

以下のコードで適当に提出ファイルとしてふさわしい形に変換し、提出した。

Python
predd=pd.DataFrame({"Transported":pred})
tmp=pd.read_csv("/kaggle/input/spaceship-titanic/test.csv")
predd.assign(PassengerId=0)
predd["PassengerId"]=tmp["PassengerId"]
predd=predd.reindex(columns=['PassengerId', 'Transported'])
predd.to_csv("submission.csv", index = False)

コンテスト参加者全体で半分よりは上で、初心者にしてはかなりいい成績を出すことができた。

次回は、更に予測精度を高めるため、SVMのハイパーパラメータの最適化を行う。

コメント