【Kaggle挑戦記】Spaceship Titanic 攻略 #13:IDに隠された「絆」を解く。グループ人数と転送率の意外な相関<
1. PassengerId の仕様から「集団」を定義する
これまで乗客一人ひとりのスペック(年齢や支出)に注目してきましたが、今回は視点を広げ、乗客が属する「グループ」に着目しました。PassengerId の前半4桁を抽出し、同じIDを持つメンバーの数をカウント。新特徴量 GroupSize としてモデルに投入しました。
2. 【実装】グループ解析機能付き・フルソースコード
支出の論理補完、Cabinの物理分解、そして今回のグループサイズ抽出を統合したコードです。最後に、データの裏側を暴くための分析ログを出力するように設計しています。
import pandas as pd
import numpy as np
import lightgbm as lgb
# 1. データの読み込み
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')
# グループサイズを正確に測るため全データを結合
all_df = pd.concat([train, test], axis=0)
all_groups = all_df['PassengerId'].apply(lambda x: x.split('_')[0]).value_counts()
# 2. 特徴量エンジニアリング
spend_cols = ["RoomService", "FoodCourt", "ShoppingMall", "Spa", "VRDeck"]
for df in [train, test]:
# --- A. 支出の論理補完 ---
df[spend_cols] = df[spend_cols].fillna(0)
total_spend = df[spend_cols].sum(axis=1)
df.loc[(df['CryoSleep'].isnull()) & (total_spend > 0), 'CryoSleep'] = False
df.loc[(df['CryoSleep'].isnull()) & (total_spend == 0), 'CryoSleep'] = True
df['Age'] = df['Age'].fillna(df['Age'].median())
# --- B. Cabinの物理分解 ---
df['Cabin'] = df['Cabin'].fillna('U/U/U')
df['Cabin_Deck'] = df['Cabin'].apply(lambda x: x.split('/')[0])
df['Cabin_Side'] = df['Cabin'].apply(lambda x: x.split('/')[-1])
# --- C. GroupSize(グループ人数)の抽出 ---
df['Group_ID'] = df['PassengerId'].apply(lambda x: x.split('_')[0])
df['GroupSize'] = df['Group_ID'].map(all_groups)
# 3. 学習の準備
features = ["CryoSleep", "Age", "RoomService", "FoodCourt", "ShoppingMall", "Spa", "VRDeck", "Cabin_Deck", "Cabin_Side", "GroupSize"]
X = pd.get_dummies(train[features], drop_first=True)
y = train["Transported"].astype(int)
X_test = pd.get_dummies(test[features], drop_first=True)
X, X_test = X.align(X_test, join='left', axis=1, fill_value=0)
# 4. モデル学習
model = lgb.LGBMClassifier(n_estimators=100, learning_rate=0.05, random_state=1)
model.fit(X, y)
# 5. 予測と保存
predictions = model.predict(X_test)
output = pd.DataFrame({'PassengerId': test['PassengerId'], 'Transported': predictions.astype(bool)})
output.to_csv('sub_v13_groupsize.csv', index=False)
# 6. 分析ログの出力
print("\n グループサイズ別 統計データ")
analysis = train.copy()
analysis['GroupSize'] = analysis['PassengerId'].apply(lambda x: x.split('_')[0]).map(all_groups)
summary = analysis.groupby('GroupSize')['Transported'].mean()
for size, rate in summary.items():
print(f"グループ人数 {int(size)}人 : 転送率 {rate:.2%}")
print("\n 特徴量寄与度 (Importance)")
importances = pd.DataFrame({'Feature': X.columns, 'Importance': model.feature_importances_}).sort_values(by='Importance', ascending=False)
print(importances.head(15)) # 傾向把握のため上位15件を表示
3. 結果と考察:データが語る「家族の運命」
リーダーボードの結果は 0.80243。前回のベストスコア(0.80406)には一歩届きませんでしたが、コンソールが出力した統計データには驚くべき事実が隠されていました。
グループサイズ別 統計データ グループ人数 1人 : 転送率 45.24% グループ人数 2人 : 転送率 53.80% グループ人数 3人 : 転送率 59.31% グループ人数 4人 : 転送率 64.08% グループ人数 8人 : 転送率 39.42%
1人旅の転送率が約45%なのに対し、4人家族(グループ)では64.08%と跳ね上がっています。一方で、8人の大家族になると39.42%まで急落します。「中規模な家族ほど、何らかの理由で揃って異次元へ転送されやすかった」というドラマチックな偏りが見て取れます。
4. Importanceが示す「支出データの壁」
スコアが伸び悩んだ理由は、モデルが弾き出した Importance(重要度) の数値に如実に表れていました。
特徴量寄与度 (Importance) 1. Spa : 436 2. VRDeck : 429 3. FoodCourt : 414 4. Age : 375 ... 10. Cabin_Deck_E : 89 (GroupSize は圏外)
上位を占めるのは依然として Spa, VRDeck, FoodCourt といった「個人の状態」を示す支出データです。今回投入した GroupSize は、統計的な傾向(4人組は危ない、など)こそあるものの、LightGBMが「Spaに金を使っているかどうか」以上に優先すべき判断基準とは見なさなかったようです。
5. まとめ:0.8突破のその先にある「壁」
今回の実験で、宇宙船内の「社会的な繋がり」が運命を左右している確証は得られました。しかし、単なる「人数」という数字だけでは、支出データが持つ圧倒的な情報量には勝てないことも浮き彫りになりました。
「傾向はあるが、決め手にならない」。このジレンマを解消するには、次は単なる人数だけでなく、グループ内での「全員寝ていたか?」「全員同じデッキか?」といった、より深い関係性の抽出――つまり、集団としての「文脈」をさらに深掘りする必要がありそうです。