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

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

PR

【Kaggle挑戦記】Spaceship Titanic 攻略 #5:ドメイン知識の投入。支出額から「冷凍睡眠」を逆算せよ

1. 今回の作戦:欠損値を「推理」で埋めるハイブリッド戦略

前回、項目を削ることで 0.79120 まで到達しました。さらなる高みを目指すため、今回は「データの裏側にある事実」をコードに落とし込みます。ターゲットは、最重要項目である「支出額」と「CryoSleep(冷凍睡眠)」の連動性です。

宇宙船の物理法則(ドメイン知識)

  • 事実1: 施設利用(Spa等)のデータが欠損しているのは、単に「使っていない(0円)」からではないか?(中央値で埋めるのは不自然)
  • 事実2: 冷凍睡眠(CryoSleep)中の乗客はカプセル内で眠っており、物理的に1円も使えないはず。
  • 結論: まず支出額の欠損を「0」で埋め、その合計額が1円でもあれば「起きていた(False)」、0円なら「寝ていた(True)」と推論して、CryoSleepの欠損を埋める。

2. 【実装】支出と睡眠をセットで補完する「論理補完」コード全文

「とりあえず埋める」から「根拠を持って埋める」へ。Macのターミナルで実行した、論理的整合性を重視したコードです。

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

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

# 2. 支出系とCryoSleepの同時補完ロジック
for df in [train, test]:
    spend_cols = ["RoomService", "FoodCourt", "ShoppingMall", "Spa", "VRDeck"]
    
    # 【処理A】支出の欠損は「使っていない(0円)」とみなす
    df[spend_cols] = df[spend_cols].fillna(0)
    
    # 支出の合計を計算
    total_spend = df[spend_cols].sum(axis=1)
    
    # 【処理B】支出額からCryoSleep(冷凍睡眠)を逆算
    # 1円でも使っていれば、寝ているはずがない(False)
    df.loc[(df['CryoSleep'].isnull()) & (total_spend > 0), 'CryoSleep'] = False
    # 合計0円なら、寝ていた可能性が極めて高い(True)
    df.loc[(df['CryoSleep'].isnull()) & (total_spend == 0), 'CryoSleep'] = True

    # その他の項目(Ageなど)は中央値で補完
    df['Age'] = df['Age'].fillna(df['Age'].median())

# 3. 特徴量の選択(筋肉質な7項目)
features = ["CryoSleep", "Age", "RoomService", "FoodCourt", "ShoppingMall", "Spa", "VRDeck"]

# 学習データの準備(CryoSleepをTrue/Falseの2値に絞り込む)
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)

# 4. 学習
model = RandomForestClassifier(n_estimators=100, random_state=1)
model.fit(X, y)

# 5. 提出用ファイル出力
predictions = model.predict(X_test)
pd.DataFrame({'PassengerId': test['PassengerId'], 'Transported': predictions}).to_csv('sub_logic_hybrid.csv', index=False)

3. 結果:論理の正しさが生んだ「微増」の意味

Macのターミナルを叩き、運命の結果を確認しました。

Public Score: 0.79214(前回比 +0.00094)

数値としての伸びはわずかですが、エンジニアとしてこの結果には大きな意味を感じています。

  • 確信に変わった「0円」の意味: これまで適当に埋めていた欠損値を論理的に埋めた上でスコアが上がったということは、この宇宙船のルール(ドメイン知識)を正しく捉え始めている証拠です。
  • 逃げ道の封鎖: 「Unknown」という不純物を消してなお精度が向上したことは、モデルがより普遍的なパターンを学習できていることを意味します。

4. 次のフェーズ:限界の先へ

「引き算」も「欠損値の論理補完」もやり遂げました。Baselineは今、極限まで磨かれています。これ以上の精度向上を狙うには、いよいよ新しい情報の創造、つまり「特徴量エンジニアリング(足し算)」が必要です。

「個別の支出」ではなく「合計支出」はどう効くのか?あるいは「家族」の存在は? 0.8の壁を突破するための、本質的なクリエイティビティの戦いが始まります。


データの向こう側にある真実を一つずつ拾い集め、コードに落とし込む。地道な作業ですが、これが確実な勝利への唯一の道。さあ、頑張ろう!

【Kaggle挑戦記】Spaceship Titanic 攻略 #4:引き算の勝利!項目を削って 0.79120 へスコアアップ

【Kaggle挑戦記】Spaceship Titanic 攻略 #4:引き算の勝利!項目を削って 0.79120 へスコアアップ

1. 今回の作戦:ノイズを削り、モデルを研ぎ澄ませる

前回の初提出で、0.78723というBaselineを記録しました。しかし、重要度を分析すると「HomePlanet」や「Destination」といった項目が足を引っ張っている(ノイズになっている)可能性が浮上しました。

そこで今回、あえて「新しく何も足さず、効いていない項目を削るだけ」というリストラ作戦を敢行。不純物を排除し、モデルが真に重要なデータだけに集中できる環境を作りました。

2. 【実装】一切の無駄を省いた「リストラ実行コード」全文

重要度の低かった HomePlanet, Destination, VIP を削除し、予測精度に寄与する「主力メンバー」だけに絞り込んだコードです。

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

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

# 2. 最小限の前処理(欠損値補完)
# 数値型は中央値で、文字列(object)型は"Unknown"で一律補完
for df in [train, test]:
    # 数値列の抽出と補完
    num_cols = df.select_dtypes(include=['float64']).columns
    df[num_cols] = df[num_cols].fillna(df[num_cols].median())
    
    # 文字列列の抽出と補完
    obj_cols = df.select_dtypes(include=['object']).columns
    df[obj_cols] = df[obj_cols].fillna('Unknown')

# 3. 特徴量の選択(重要度の低かった3つをリストラ)
# 予測のノイズになっていた HomePlanet, Destination, VIP を排除
features = ["CryoSleep", "Age", "RoomService", "FoodCourt", "ShoppingMall", "Spa", "VRDeck"]

# 学習用データの準備(ダミー変数化で数値へ変換)
X = pd.get_dummies(train[features])
y = train["Transported"]
X_test = pd.get_dummies(test[features])

# 学習データとテストデータの列(次元)を揃える
X, X_test = X.align(X_test, join='left', axis=1, fill_value=0)

# 4. ランダムフォレストで学習(100本の木で構築)
model = RandomForestClassifier(n_estimators=100, random_state=1)
model.fit(X, y)

# 5. 特徴量の重要度(Feature Importances)を表示
importances = pd.DataFrame({
    'feature': X.columns,
    'importance': model.feature_importances_
}).sort_values('importance', ascending=False)

print("\n New Feature Importances:")
print(importances)

# 6. 提出用ファイルの作成(CSV出力)
predictions = model.predict(X_test)
output = pd.DataFrame({
    'PassengerId': test['PassengerId'],
    'Transported': predictions
})
output.to_csv('sub_reduced_features.csv', index=False)
print("\n✅ Submission file 'sub_reduced_features.csv' saved.")

3. 【実行結果】研ぎ澄まされた重要度とスコア

結果は予想を上回るポジティブなものでした。MacのターミナルからKaggleへ提出した結果です。

Public Score: 0.79120(前回比 +0.00397)

そして、ターミナルに出力された「主力メンバー」たちの重要度がこちら。

 New Feature Importances:
              feature  importance
4                 Spa    0.148013
0                 Age    0.139252
2           FoodCourt    0.133334
1         RoomService    0.127555
5              VRDeck    0.124744
7      CryoSleep_True    0.112372
6     CryoSleep_False    0.105997
3        ShoppingMall    0.105874
8   CryoSleep_Unknown    0.002859

4. 考察:引き算がモデルを「覚醒」させた

今回の実験で、「情報の質」が「量」を凌駕することが証明されました。項目を減らしたにも関わらずスコアが上がったのは、モデルがノイズに惑わされなくなり、真に影響力のある「支出額」や「年齢」の境界線をより正確に引けるようになったためです。

特にSpaやVRDeckなどの支出項目の重要度が軒並み上がり、モデルの武器が鋭利になったことがわかります。何も足さずに 0.791 を突破。いよいよ 0.8 の大台が射程圏内に入りました。


「まず削る」。このシンプルなエンジニアリングが、宇宙船のデータに潜む真実を一つ暴きました。次回、筋肉質になったこのモデルに、「新たな筋肉(特徴量)」を足すフェーズへ。攻略は続きます。


【Kaggle挑戦記】Spaceship Titanic 攻略 #3:丸腰で初提出 0.78723。重要度から見えた「金」と「運命」の相関

Titanicでの反省を活かし、今回のSpaceship Titanicでは「最初から作り込まない」Baseline構築作戦を敢行しました。Macのターミナルで最小限の前処理コードを走らせ、いきなり提出まで完走。その結果、驚くべき事実が見えてきました。

1. 【実装】一切の細工を排した初手コード

まずは「機械学習モデルがエラーを吐かずに動くこと」だけを目的とした最短コードです。数値の欠損は中央値で、文字列は"Unknown"で埋め、一気にダミー変数化(数値化)しました。

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

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

# 2. 最小限の前処理(欠損値補完)
for df in [train, test]:
    num_cols = df.select_dtypes(include=['float64']).columns
    df[num_cols] = df[num_cols].fillna(df[num_cols].median())
    obj_cols = df.select_dtypes(include=['object']).columns
    df[obj_cols] = df[obj_cols].fillna('Unknown')

# 3. 特徴量の選択と数値化
features = ["HomePlanet", "CryoSleep", "Destination", "Age", "VIP", "RoomService", "FoodCourt", "ShoppingMall", "Spa", "VRDeck"]
X = pd.get_dummies(train[features])
y = train["Transported"]
X_test = pd.get_dummies(test[features])
X, X_test = X.align(X_test, join='left', axis=1, fill_value=0)

# 4. 学習・重要度の算出・予測
model = RandomForestClassifier(n_estimators=100, random_state=1)
model.fit(X, y)

# 重要度の出力
importances = pd.DataFrame({'feature': X.columns, 'importance': model.feature_importances_}).sort_values('importance', ascending=False)
print(importances.head(10))

# 5. 提出用ファイル出力
predictions = model.predict(X_test)
pd.DataFrame({'PassengerId': test['PassengerId'], 'Transported': predictions}).to_csv('baseline_submission.csv', index=False)

2. 【実況】初提出スコアとターミナル出力

Macのターミナルを叩き、生成されたファイルをKaggleへ。返ってきたスコアは驚きの数値でした。

Public Score: 0.78723

そして、ターミナルが表示した「予測に貢献した項目(Feature Importances)」がこちらです。

 Feature Importances (Top 10):
              feature  importance
0                 Age    0.170933
4                 Spa    0.128971
5              VRDeck    0.117302
1         RoomService    0.112501
2           FoodCourt    0.110814
3        ShoppingMall    0.101754
10    CryoSleep_False    0.077724
11     CryoSleep_True    0.074114
6    HomePlanet_Earth    0.032070
7   HomePlanet_Europa    0.019907

3. 分析:この宇宙船は「金」と「眠り」が支配している

このランキング結果は、Titanicとは全く異なる宇宙船のルールを教えてくれました。

  • 支出額が運命を分ける: 2位から6位まで、船内の娯楽施設(Spa, VRDeck等)への支払額が独占しています。豪華なサービスを受けていたかどうかが、転送(Transported)に直結しているという生々しい現実が見えます。
  • 年齢(Age)はやはり最強: 最も影響力があるのは年齢でした。これは世代間の何らかの傾向を示唆しています。
  • 冷凍睡眠(CryoSleep)の重要性: 文字通り、寝ていたかどうかも生存に大きく関わっています。
  • 出身星(HomePlanet)の影の薄さ: EarthやEuropaといった出身地の項目は、支出額に比べれば重要度が1/5〜1/10程度しかありません。

4. 次の作戦:貢献度の低い特徴量を「リストラ」する

Baselineが 0.787 という高い位置にセットされた今、次の一手は「引き算の美学」です。 今回の結果で重要度が低かった HomePlanetDestination(目的地)などをあえて外し、モデルから「ノイズ」を排除します。

さらに、バラバラになっている「支出額」を合計した「TotalSpend」という新項目を作ることで、モデルに本質的な判断材料を与える。過学習を避けつつ、スコアを 0.8 の先へ押し上げる戦いが始まります。さあ、頑張ろう!


一切の細工をしないからこそ、データの真実が浮かび上がる。これがKaggleの醍醐味です。次回、特徴量のリストラ編へ。


【Kaggle挑戦記】Spaceship Titanic 攻略 #3:一切の細工なし。最小限の前処理で初提出 0.78723 をマーク!

1. 今回のミッション:最速で「Baseline」を構築せよ

前回、Macのターミナルでデータ構造をスキャンし、敵の正体(大量の欠損値と文字列データ)を把握しました。今回のミッションはシンプルです。「こねくり回さず、とにかくモデルを走らせる」こと。AIが計算できない「文字列」と「空欄(欠損値)」だけを力技で解消し、初提出まで一気に駆け抜けます。

2. 【実装】「動けばいい」の精神で行う最小限の前処理と予測

複雑な分析は後回し。まずは機械学習モデルがエラーを吐かないための最低条件である「欠損値補完」と「数値化(ダミー変数化)」だけを行い、CSV出力まで一気に進めます。

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

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

# 2. 最小限の前処理
# 数値型は中央値で、文字列(object)型は"Unknown"で埋める
for df in [train, test]:
    num_cols = df.select_dtypes(include=['float64']).columns
    df[num_cols] = df[num_cols].fillna(df[num_cols].median())
    
    obj_cols = df.select_dtypes(include=['object']).columns
    df[obj_cols] = df[obj_cols].fillna('Unknown')

# 3. 特徴量の選択と数値化
features = ["HomePlanet", "CryoSleep", "Destination", "Age", "VIP", "RoomService", "FoodCourt", "ShoppingMall", "Spa", "VRDeck"]
X = pd.get_dummies(train[features])
y = train["Transported"]
X_test = pd.get_dummies(test[features])

X, X_test = X.align(X_test, join='left', axis=1, fill_value=0)

# 4. ランダムフォレストで学習
model = RandomForestClassifier(n_estimators=100, random_state=1)
model.fit(X, y)

# 5. 特徴量の重要度を表示
importances = pd.DataFrame({
    'feature': X.columns,
    'importance': model.feature_importances_
}).sort_values('importance', ascending=False)

print("\n Feature Importances (Top 10):")
print(importances.head(10))

# 6. 提出用ファイルの作成
predictions = model.predict(X_test)
output = pd.DataFrame({'PassengerId': test['PassengerId'], 'Transported': predictions})
output.to_csv('baseline_submission.csv', index=False)
print("\n✅ submission file 'baseline_submission.csv' has been saved!")

3. 結果:丸腰で叩き出した驚きのスコア

Macのターミナルで生成されたCSVをKaggleへアップロードした結果、驚くべき数字が返ってきました。

Public Score: 0.78723

Titanicの時にあれほど苦労して到達した 0.78 という壁を、初手の「丸腰状態」であっさりと超えてしまいました。データ量が多いことで、細かな前処理に頼らずともモデルがデータの傾向を正しく掴めている証拠です。

4. 考察:重要度(Importance)が示す次へのヒント

エンジニア的な視点:
今回の前処理は「Object型を数字に変える」「欠損値を埋める」の2点のみ。それだけでこれほどのスコアが出るなら、今後の伸び代は計り知れません。
ターミナルに出力された「Feature Importances」を見ると、どの項目が生存(転送)を分けたのかが明白になります。もし特定の娯楽施設の利用額が強烈に効いているなら、そこを深掘りし、逆に全く効いていない項目があれば、それはノイズとして排除すべき「リストラ候補」です。


基準点(Baseline)が 0.78723 という高い位置にセットされました。次回の攻略では、この重要度ランキングを元に、不要な特徴量を削ぎ落とす「引き算」のプロセスに入ります。宇宙船の謎を解く戦いは、最高の滑り出しを記録しました。さあ、頑張ろう!