はじめに
※本記事では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)
若い世代はTransportedされているような傾向があるようです。
年齢を見ても30代辺りまでが多く、以降は減少傾向にあります。
次に、DeckとAgeの関係を見たいので棒グラフで示してみます。
sns.boxplot(x="Deck", y="Age", data=train_df,order=train_df['Deck'].value_counts().index)
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に関係ありそうなカラムをまとめて相関関係を見てみます。
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))
Cと、L1正規化を試してみました。
ちょっと精度が上がりました。
いくつか正負の相関が強く出ているのがあって興味深いですね。。。
これで一旦アップロードしてみます。
0.78という結果でした。
お試しも兼ねてやってみてロジスティック回帰の手軽さがわかりました。ですがまだ土台程度のものなので、ここから改良していけばもっと改善できそうです。
前処理(現時点でやったこと)
欠損値はシンプルに中央値/最頻値で補完
Cabin を分割して「デッキ・番号・サイド」に分けた
カテゴリ変数はダミー変数化(One-Hot Encoding)
CryoSleepの扱いを考えてみた。また別解釈もあると思うので検討
簡単な内容ですが前処理は上記のような形で実施してみました。
Nameは扱いにくいので今回はDropしましたが、家族は一緒にTransportedされそうな傾向にあるのではと思うのでFamilynameで分割してAgeに欠損があり、10代未満であれば若い年齢で欠損値埋めもありかなと思いました。
さいごに
若干ChatGPTの力を借りてしまいましたが、自分で考えて実施する難しさも理解しました。
前処理を筋道を立てて実施すればもっと良い結果がでそうです。
ありがとうございました。