忍者ブログ
統計、機械学習、AIを学んでいきたいと思います。 お役に立てば幸いです。

【Kaggle挑戦記】Spaceship Titanic 攻略 #11:ついに0.8突破!物理情報「Cabin」の導入が運命の分かれ道となった

1. 前回の敗北から原点回帰へ

前回、統計的な分布調整による「閾値最適化」に挑みましたが、結果は 0.79003 へのダウン。 エンジニアとしての仮説「学習データとテストデータの分布は同じはず」は間違っていないと確信しつつも、AIに与える「判断材料(特徴量)」そのものを強化する必要性を痛感しました。 そこで今回、満を持して投入したのが、船内の物理的な位置を示す Cabin(客室番号) です。

2. 実装:ドメイン知識と物理情報の融合

これまでの最高得点(0.79611)を出した「支出データからの睡眠状態逆算」というドメイン知識に基づく論理補完に、Cabinからパースした「Deck(デッキ)」と「Side(右舷・左舷)」を掛け合わせました。 Macのターミナルで実行した、今回の決定版コードがこちらです。

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')

# 2. 特徴量エンジニアリング(論理補完 & Cabin分解)
spend_cols = ["RoomService", "FoodCourt", "ShoppingMall", "Spa", "VRDeck"]

for df in [train, test]:
    # --- A. 支出実績から CryoSleep を論理的に推論 ---
    df[spend_cols] = df[spend_cols].fillna(0)
    total_spend = df[spend_cols].sum(axis=1)
    
    # 支出があれば起きている(False)、なければ寝ている(True)
    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(客室)を Deck/Num/Side に分解 ---
    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])

# 3. 学習に使用する特徴量の選定
features = [
    "CryoSleep", "Age", "RoomService", "FoodCourt", 
    "ShoppingMall", "Spa", "VRDeck", "Cabin_Deck", "Cabin_Side"
]

# 4. データの整形(ダミー変数化)
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)

# 5. モデル学習(LightGBM)
model = lgb.LGBMClassifier(
    n_estimators=100, 
    learning_rate=0.05, 
    random_state=1
)
model.fit(X, y)

# 6. 予測と提出ファイルの出力
predictions = model.predict(X_test)
output = pd.DataFrame({
    'PassengerId': test['PassengerId'], 
    'Transported': predictions.astype(bool)
})
output.to_csv('submission_v11.csv', index=False)

3. リーダーボードの結果:歓喜の瞬間

Kaggleにファイルをアップロードし、リーダーボードが更新された瞬間、思わずガッツポーズが出ました。

 Previous Best : 0.79611
 New Score     : 0.80406 (0.8の壁を突破!)

4. 考察:なぜ「Side」が効いたのか

今回追加した「Cabin_Side(右舷・左舷)」は、事故の被害がどちらから来たかという物理的な衝突面をモデルに示唆しました。 「寝ていたか、起きていたか(状態)」×「船のどちら側にいたか(位置)」。 このミクロな情報の掛け合わせが、これまでのマクロな調整を凌駕し、ついに 0.80 の大台へと連れて行ってくれました。


一つ一つの仮説を積み上げ、データで検証する。エンジニアとしての地道なアプローチが報われた瞬間でした。
しかし、まだ上には上がいます。この勢いを止めることなく、さらなる高みを目指します。



PR

【Kaggle挑戦記】Spaceship Titanic 攻略 #10:統計的補正の罠。AIの「楽観」を抑えた結果、見えてきたもの

1. 独自の仮説:分布不変の原則

LightGBMで 0.796 まで到達した今、次なる一手として「学習データとテストデータの分布は同じはずだ」という統計的な仮説を立てました。 AIが一律「0.5」という閾値で判断するなら、その結果としての True(転送された)の割合は、学習データの事実(50.36%)に一致すべき。もしズレているなら、閾値を動かして矯正すべきではないか、と考えたのです。

2. 実装と、Macのターミナルが示した驚愕の数字

AIに「確率」を出させ、上位50.36%だけを True と判定するように閾値を調整したところ、コンソールには衝撃的なログが流れました。

 学習データの True 割合 (目標): 0.5036
...
[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.503624 -> initscore=0.014495
⚙️ 算出された最適な閾値: 0.5950
   (デフォルトの 0.5 から +0.0950 調整されました)
------------------------------
 調整後の予測 True 割合: 0.5036 (目標との差: 0.0000)

なんと、AI(LightGBM)をそのまま信じると、True の割合が統計的予測を大きく上回ってしまっていたのです。私は判定ラインを約10%引き上げ、0.5950 という厳しい基準で「選別」を行いました。

3. リーダーボードの結果:無情な 0.79003

結果は、自己ベスト更新ならず。

 Current Best (LightGBM) : 0.79611
 Ratio Adjusted Result    : 0.79003 (▼ 0.00608)

あえて統計に寄せた判断が、スコアを落とす結果となりました。

4. 考察:なぜ「正論」が通じなかったのか?

エンジニアとして、この結果から2つの教訓を得ました。

  • テストデータの分布差: 「学習用」と「テスト用」のデータ分布は、必ずしも完全一致するとは限らない。今回の事故では、テストデータ側の転送率は 50.36% より高かった可能性があります。
  • 確率は「相対的」なもの: AIが出す確率は「確信度」であって、絶対的な数値ではない。AIが 0.6 と言っても、それは「0.5の人よりは可能性が高い」という順位付けには有効ですが、その数値そのものを統計に当てはめるのは時期尚早だったのかもしれません。

5. それでも、方向性は間違っていない

「0.5」というデフォルト設定を疑い、マクロな視点で補正を試みたことは、今後の複雑なコンペティションにおいて必ず活きる経験です。 AIに盲従せず、エンジニアとしての仮説をぶつけ、その反応をデータで確認する。 この試行錯誤こそが、0.8 への唯一の道。次はいよいよ、物理情報である「Cabin(客室)」のパースに挑みます。



データは正直だ。そして、だからこそ面白い。

【Kaggle挑戦記】Spaceship Titanic 攻略 #9:最強の刺客 XGBoost 投入。しかし、LightGBM の牙城は崩せず

1. 今回の挑戦:勾配ブースティングの「二大巨頭」対決

LightGBMで 0.79611 という自己ベストを更新した前回。次なる一手として、Kaggleで不動の人気を誇るもう一つの最強エンジン、XGBoost を投入しました。 同じ「勾配ブースティング」という系譜に属しながら、計算の進め方(木の育て方)が異なるこの2つのアルゴリズム。どちらが今回の宇宙船タイタニックのデータと相性が良いのか、真剣勝負です。

2. 【実装】XGBoost 換装モデル

Macのターミナルで pip install xgboost を実行し、環境を構築。最高スコア時の前処理ロジックはそのままに、学習器だけを差し替えました。

import xgboost as xgb

# 特徴量と前処理は最高スコア時を継承
model = xgb.XGBClassifier(
    n_estimators=100,
    learning_rate=0.05,
    max_depth=6,
    random_state=1,
    use_label_encoder=False,
    eval_metric='logloss'
)

model.fit(X, y)
# 予測結果を sub_xgboost_v1.csv として出力

3. 実行結果:LightGBM の底力

期待を込めて提出した結果、リーダーボードに刻まれた数字は意外なものでした。

 LightGBM (Current Best) : 0.79611
 XGBoost (New Trial)    : 0.79448 (▼ 0.00163)

わずかな差ではありますが、XGBoostは前回の記録に届かず。今回のデータセットにおいては、LightGBMの方が「急所(重要な分岐)」をより的確に捉えていたようです。

4. 考察:なぜ「同じ手法」で差が出たのか?

どちらも「勾配ブースティング」ですが、その違いが結果に現れました。

  • 木の育て方の違い: LightGBMは「誤差が大きい部分をピンポイントで掘り下げる(Leaf-wise)」のに対し、XGBoostは「層ごとにバランスよく育てる(Level-wise)」。今回のデータでは、LightGBMの尖った学習スタイルが有利に働いたと考えられます。
  • アルゴリズムの相性: 特定の数値(支出額など)が重要な意味を持つこのコンペでは、LightGBMの高速で柔軟な分岐がフィットしたのでしょう。

5. 次なる一手:最強の「合体(アンサンブル)」へ

XGBoostが負けたからといって、無駄だったわけではありません。エンジニアには「アンサンブル(平均化)」という必殺技が残されています。 LightGBMの予測とXGBoostの予測。性格の違う2つの知性を組み合わせることで、単体では届かなかった 0.8 の壁を突破できるかもしれません。


失敗はデータの蓄積。次は「二人の知才」を一つに合わせて、大台を狙います。


【Kaggle挑戦記】Spaceship Titanic 攻略 #8:アルゴリズムの「換装」。LightGBM投入で自己ベストを更新せよ

1. 今回の戦略:データの加工ではなく「エンジン」を変える

前回、外れ値を削りすぎてスコアを落とすという「情報の欠損」を経験しました。そこから得た教訓は、「複雑なデータは、より強力なアルゴリズムに委ねるべき」ということ。 今回は、データのクリーニングは最高得点(0.792)時の最小限に留め、学習器を「ランダムフォレスト」から、現代のKaggle三種の神器の一つ「LightGBM」へと載せ替えました。

なぜ LightGBM なのか?

  • 勾配ブースティングの威力: 一度に学習するランダムフォレストと違い、前のミスを修正するように段階的に学習するため、より緻密な境界線を見極められます。
  • 外れ値への耐性: データの「歪み」を無理に直さなくても、アルゴリズム側で最適に処理してくれます。

2. 【実装】最高得点ロジック + LightGBM

支出の論理補完は維持しつつ、学習エンジンを最新鋭に積み替えたコードです。

import pandas as pd
import lightgbm as lgb

# データの読み込み
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')

# 支出補完とCryoSleepの推論(最高スコア時のロジックを継承)
spend_cols = ["RoomService", "FoodCourt", "ShoppingMall", "Spa", "VRDeck"]
for df in [train, test]:
    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())

# 特徴量準備
features = ["CryoSleep", "Age", "RoomService", "FoodCourt", "ShoppingMall", "Spa", "VRDeck"]
X = pd.get_dummies(train[features], drop_first=True)
y = train["Transported"].astype(int)
X_test = pd.get_dummies(test[features], drop_first=True)

# アルゴリズム変更:LightGBM
model = lgb.LGBMClassifier(n_estimators=100, learning_rate=0.05, random_state=1)
model.fit(X, y)

# 予測・提出
predictions = model.predict(X_test)
output = pd.DataFrame({'PassengerId': test['PassengerId'], 'Transported': predictions.astype(bool)})
output.to_csv('sub_lightgbm_v1.csv', index=False)

3. 実行結果:壁を突き抜ける一撃

Macのターミナルで実行し、生成されたファイルをKaggleへ。結果は、これまでの停滞を吹き飛ばすものでした。

 Random Forest (Best) : 0.79214
 LightGBM (New)       : 0.79611 (UP! )

ついに **0.796**。0.8という大台まで、あとわずか **0.004** ポイント。データの切り分け方を変えるのではなく、計算の「深さ」と「正確性」を上げたことが、この微差にして大きな前進を生みました。

4. まとめ:エンジニアとしての決断

「データの質」を追及することも大切ですが、時には「使う道具」を進化させることも重要だと痛感しました。LightGBMという新しい武器を手に入れた今、視界が開けました。 次は、この強力なエンジンに、これまで温めてきた「Cabin(客室)の分解データ」を流し込みます。物理的な位置情報が加われば、0.8突破は確実です。


道具を磨き、知識を積み、一歩ずつ。Kaggleの頂は見え始めてきた。



【Kaggle挑戦記】Spaceship Titanic 攻略 #7:良かれと思った「外れ値除外」でスコアが急降下した話

1. 今回の仮説:外れ値は「毒」である

前回までで 0.79214 という自己ベストを記録していました。しかし、支出項目のデータ分布を見ると、ごく一部の乗客が数万ドルという極端な金額を使っています。 「これほどの外れ値は、モデルの判断を狂わせるノイズ(毒)に違いない」——そう考えた私は、外れ値の基準を厳格化し、上位5%(95パーセンタイル)で数値を一律カットする強硬策に出ました。

2. 実装した「徹底排除」コード

以下が、あえて「失敗」を招くことになったコードの全文です。支出の上限をかなり低く設定しました。

import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestClassifier

# データの読み込み
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')

# 支出項目の欠損値を0で埋め、合計を算出
spend_cols = ["RoomService", "FoodCourt", "ShoppingMall", "Spa", "VRDeck"]
for df in [train, test]:
    df[spend_cols] = df[spend_cols].fillna(0)
    df['TotalSpend'] = df[spend_cols].sum(axis=1)

# 【ここが失敗の種】外れ値を95%タイルで厳格にクリッピング
for col in spend_cols:
    # 上位5%をカット。Spaなら1600ドル程度が上限に
    upper_limit = train[col].quantile(0.95)
    train[col] = train[col].clip(upper=upper_limit)
    test[col] = test[col].clip(upper=upper_limit)
    print(f"✂️ {col} の上限を {upper_limit:.1f} (95%) に設定")

# CryoSleepの論理補完とAgeの中央値補完
for df in [train, test]:
    df.loc[(df['CryoSleep'].isnull()) & (df['TotalSpend'] > 0), 'CryoSleep'] = False
    df.loc[(df['CryoSleep'].isnull()) & (df['TotalSpend'] == 0), 'CryoSleep'] = True
    df['Age'] = df['Age'].fillna(df['Age'].median())

# 特徴量選択と学習
features = ["CryoSleep", "Age", "RoomService", "FoodCourt", "ShoppingMall", "Spa", "VRDeck"]
X = pd.get_dummies(train[features], drop_first=True)
y = train["Transported"]
X_test = pd.get_dummies(test[features], drop_first=True)
X, X_test = X.align(X_test, join='left', axis=1, fill_value=0)

model = RandomForestClassifier(n_estimators=100, random_state=1)
model.fit(X, y)

# 予測・提出
predictions = model.predict(X_test)
pd.DataFrame({'PassengerId': test['PassengerId'], 'Transported': predictions}).to_csv('sub_clip_95.csv', index=False)

3. 実行結果:Macのターミナルに突きつけられた現実

意気揚々とKaggleに提出した結果、リーダーボードに表示された数字に目を疑いました。

 Previous Score : 0.79214
 New Score      : 0.78185 (▲0.01029)

なんと、自己ベストから一気に **0.01 ポイントもの急落**。これまでの積み上げを台無しにするような、手痛い敗戦となりました。

4. 考察:なぜ「外れ値」は必要だったのか?

この失敗から、このコンペティションにおける重要な真実が見えてきました。 「超高額な支出をしている乗客」というのは、単なるノイズではありませんでした。「高額な施設を頻繁に利用していた=事故の瞬間に特定のエリアにいた」 という、転送(Transported)されるか否かを決める極めて重要なシグナルだったのです。

それを95%という低いラインで丸めてしまったことで、モデルは「重要人物」と「普通の客」の区別がつかなくなってしまった。ランダムフォレストは元々外れ値に強いアルゴリズムであり、人間が余計な手出しをするべきではありませんでした。

5. 次なる一手

「外れ値=悪」という先入観は捨てました。次回は、この「数値の大きさ」という情報を残しつつ、まだ手付かずの「Cabin(客室)」データを分解し、物理的な位置関係から0.8の壁に再挑戦します。


Kaggleは、自分の思い込みをデータが粉砕してくれる場所。この敗北を糧に、次はもっと賢いコードを書こう。攻略は続く。