0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

練習も兼ねてSpaceShip Titanicでロジスティック回帰をやってみる

Posted at

はじめに

※本記事ではKaggle提供の公開データを使用しています
Kaggle の初心者向けコンペ「Spaceship Titanic」に挑戦中です。
本記事はあくまで 練習と途中経過の記録です。優しい目で見ていただけると嬉しいです

練習も兼ねてロジスティック回帰を試してみて、「どのくらいの精度が出るのか」「どんな特徴量が効いていそうか」を確認していきます。

コンペ概要

HPは↑から
宇宙船事故で乗客が転送されたかどうかを予測する二値分類問題になります。
特徴量はこんな感じ
HomePlanet(出発地)
CryoSleep(コールドスリープ中か)
Cabin(船室情報)
Age(年齢)
Destination(目的地)
生活費や娯楽費の金額 など

ターゲット変数は Transported(転送されたかどうか)です。
Titanicと同じ温度感で取り組めそうだったのでこちらに取り組んでいます。
それではコード書いていきます。

train_df.info()

<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

データが8693個に対して、いくつか欠損しているデータがあるようです。
for文で回して割合を見ます。

for col in train_df.columns:
    if train_df[col].isnull().any(): 
        count = train_df[col].isnull().sum()
        total_rows = len(train_df) 
        percentage = (count / total_rows) * 100 
        print(f"{col}:{percentage:.2f}%")
HomePlanet:2.31%
CryoSleep:2.50%
Cabin:2.29%
Destination:2.09%
Age:2.06%
VIP:2.34%
RoomService:2.08%
FoodCourt:2.11%
ShoppingMall:2.39%
Spa:2.11%
VRDeck:2.16%
Name:2.30%

データの割合に対してそこまで欠損していない様子です。
初めにAgeに関して、データの状態を見て埋めてみたいと思います。
Cabinとの関係性を見てみます。
扱いにくいのでCabinは3分割します。

train_df[['Deck', 'CabinNum', 'Side']] = train_df['Cabin'].str.split('/', expand=True)
Deck:デッキ
CabinNum:客室番号
Side:客室の向き

以上にCabinは分割しました。

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

image.png

若い世代はTransportedされているような傾向があるようです。
年齢を見ても30代辺りまでが多く、以降は減少傾向にあります。
次に、DeckとAgeの関係を見たいので棒グラフで示してみます。

sns.boxplot(x="Deck", y="Age", data=train_df,order=train_df['Deck'].value_counts().index)

image.png

Tだけ極端に年齢のばらつきが少ないようです。
なので他デッキと同じ補完ではなくTだけの中央値を利用してみます。

train_df.loc[(train_df["Deck"] == "F") & (train_df["Age"].isna()), "Age"] = train_df.loc[(train_df["Deck"] == "F") & (train_df["Age"]), "Age"].median()

train_df["Age"] = train_df["Age"].fillna(train_df["Age"].mean())

次にCryoSleepを見ていきます。
KaggleのHPではCryoSleepが精度に大きく関わっていそうとなっていたのでちょっと工夫したいと思います。
CryoSleepに関係ありそうなカラムをまとめて相関関係を見てみます。

image.png

TransportedとCyrosleepの相関係数が0.46と他より高めなのでやはり何かしらありそうです。
若干ChatGPTさんの支援もありつつもしかするとCyrosleepはコールドスリープしているかどうかなので、RoomServiceやFoodCourtなど船内の施設の利用はしていないのでは?ということが発見できました。

相関関係からも分かるようにCyrosleepしている人はRoomServiceやFoodCourtなどの係数が低いです。
なのでCyrosleepが欠損していて、サービス額がゼロの場合は0として埋めてみます。

cols_service = ["RoomService", "FoodCourt", "ShoppingMall", "Spa", "VRDeck"]

#CryoSleep=1 (True) の場合 → サービス利用額を0で補完
train_df.loc[train_df["CryoSleep"] == 1, cols_service] = \
    train_df.loc[train_df["CryoSleep"] == 1, cols_service].fillna(0)

#CryoSleep=0 (False) の場合 → サービス利用額を0で補完
train_df.loc[train_df["CryoSleep"] == 0, cols_service] = \
    train_df.loc[train_df["CryoSleep"] == 0, cols_service].fillna(0)

#CryoSleep が欠損 かつ サービス額がすべてゼロ → CryoSleep=1 と推定
mask = train_df["CryoSleep"].isna() & (train_df[cols_service].fillna(0).sum(axis=1) == 0)
train_df.loc[mask, "CryoSleep"] = 1

#CryoSleep が欠損 かつ サービス額に非ゼロがある → CryoSleep=0 と推定
mask = train_df["CryoSleep"].isna() & (train_df[cols_service].fillna(0).sum(axis=1) > 0)
train_df.loc[mask, "CryoSleep"] = 0

train_df["CryoSleep"] = train_df["CryoSleep"].astype(int)

CryoSleepが0でもサービスを利用していない人もいる可能性があるのでCryoSleep=0 (False) の場合 → サービス利用額を0で補完としています。

その他欠損値は最頻値で埋めました。
PassengerIdは関係ないと思ったのでdrop、nameは扱いが難しいのでdropします。次回はこのあたりもこだわってみます。

train_df['Destination'] = train_df['Destination'].fillna(train_df['Destination'].mode()[0])
train_df['HomePlanet'] = train_df['HomePlanet'].fillna(train_df['HomePlanet'].mode()[0])

今回はロジスティック回帰の練習をするのでOne-Hot-Encodingして0,1の値に変更します。

categorical_cols = ["HomePlanet", "Destination", "Deck", "Side", "VIP"]
train_df = pd.get_dummies(train_df, columns=categorical_cols, drop_first=True,dtype=int)

一旦下準備はできたので。ロジスティック回帰をやってみます。
まずは特にいじらずにやってみます。

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
from sklearn.model_selection import GridSearchCV

# 説明変数と目的変数に分ける
X = train_df.drop("Transported", axis=1)
y = train_df["Transported"]

# 学習用と検証用に分割
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)

# スケーリング
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_val_scaled = scaler.transform(X_val)

# ロジスティック回帰モデル
model = LogisticRegression(max_iter=1000, random_state=42)
model.fit(X_train_scaled, y_train)

# 予測
y_pred = model.predict(X_val_scaled)

# 精度評価
print("精度:", accuracy_score(y_val, y_pred))
精度: 0.7924094307073031

ロジスティック回帰は特に何もいじらずとも高い精度が出るのはいいところかなと思います。
せっかくなのでパラメータをちょっと弄ります。

from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV

param_grid = {"C": [0.01, 0.1, 1, 10, 100]}

log_reg_l1 = LogisticRegression(
    penalty="l1",
    solver="liblinear",  # L1対応
    max_iter=1000,
    random_state=42
)

grid_l1 = GridSearchCV(log_reg_l1, param_grid, cv=5, scoring="accuracy", n_jobs=-1)
grid_l1.fit(X_train_scaled, y_train)

print("ベストスコア:", grid_l1.best_score_)
print("ベストパラメータ:", grid_l1.best_params_)

# 最適モデルで検証
best_model_l1 = grid_l1.best_estimator_
y_pred_l1 = best_model_l1.predict(X_val_scaled)

print("検証精度:", accuracy_score(y_val, y_pred_l1))
# スケーリング前のカラム名を保存
feature_names = X_train.columns  

coefficients = best_model_l1.coef_[0]

coef_df = pd.DataFrame({
    "Feature": feature_names,
    "Coefficient": coefficients
})

print(coef_df.sort_values(by="Coefficient", key=abs, ascending=False))

image.png

Cと、L1正規化を試してみました。
ちょっと精度が上がりました。
いくつか正負の相関が強く出ているのがあって興味深いですね。。。

これで一旦アップロードしてみます。

image.png

0.78という結果でした。
お試しも兼ねてやってみてロジスティック回帰の手軽さがわかりました。ですがまだ土台程度のものなので、ここから改良していけばもっと改善できそうです。

前処理(現時点でやったこと)

欠損値はシンプルに中央値/最頻値で補完
Cabin を分割して「デッキ・番号・サイド」に分けた
カテゴリ変数はダミー変数化(One-Hot Encoding)
CryoSleepの扱いを考えてみた。また別解釈もあると思うので検討

簡単な内容ですが前処理は上記のような形で実施してみました。

Nameは扱いにくいので今回はDropしましたが、家族は一緒にTransportedされそうな傾向にあるのではと思うのでFamilynameで分割してAgeに欠損があり、10代未満であれば若い年齢で欠損値埋めもありかなと思いました。

さいごに

若干ChatGPTの力を借りてしまいましたが、自分で考えて実施する難しさも理解しました。
前処理を筋道を立てて実施すればもっと良い結果がでそうです。
ありがとうございました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?