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

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


PR

【Kaggle挑戦記】Spaceship Titanic 攻略 #2:まずは「丸腰」で挑む。Baseline構築作戦

1. 今回の作戦:なぜ「何もしない」から始めるのか?

Titanicでの試行錯誤を経て、今回のSpaceship Titanicでは「最初から作り込まない」という戦略をとります。名付けて『Baseline(基準点)構築作戦』です。

あえて高度な前処理や欠損値の精査をスキップし、まずは最小限の手間で予測モデル(ランダムフォレスト)を走らせます。その狙いは以下の通りです。

  • 基準(Baseline)の測定: 何もしていない状態のスコアを把握し、後で行う加工に「どれだけ意味があったか」を数値で比較可能にする。
  • 不要な特徴量の早期リストラ: ランダムフォレストの「特徴量の重要度(Feature Importance)」を算出し、予測に寄与していない項目をあらかじめ特定して排除する。
  • 過学習の抑制: Titanicでは項目を増やしすぎて自爆した反省を活かし、今回は「引き算」の思考で必要最低限のデータを見極める。

まずは現状把握のため、Macのターミナルから乗客名簿のデータ構造を確認します。

2. 【実装】データ構造の基本スキャン

作業はすべてMacのターミナル上で行い、Pythonでデータの概要を抽出しました。

import pandas as pd

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

print("\n Spaceship Titanic Data Scan Started...\n")

# 2. データ項目(カラム)の一覧と型を確認
print("--- [1] Data Columns & Types ---")
print(train.info())

print("\n" + "="*50 + "\n")

# 3. 欠損値の有無を確認
print("--- [2] Missing Values Scan ---")
null_counts = train.isnull().sum()
print(null_counts[null_counts > 0]) # 欠損がある項目だけを表示

print("\n" + "="*50 + "\n")

# 4. ターゲットの分布を確認
print("--- [3] Target Distribution (Transported) ---")
print(train['Transported'].value_counts())

3. 【実行結果】ターミナルから得られたログ

Macのターミナルで実行した結果、以下の情報が得られました。これが今回の分析のスタート地点となります。

 Spaceship Titanic Data Scan Started...

--- [1] Data Columns & Types ---
<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)

--- [2] Missing Values Scan ---
HomePlanet      201
CryoSleep       217
Cabin           199
Destination     182
Age             179
VIP             203
RoomService     181
FoodCourt       183
ShoppingMall    208
Spa             183
VRDeck          188
Name            200

--- [3] Target Distribution (Transported) ---
Transported
True     4378
False    4315

4. データの「素顔」をどう読み解くか?

出力されたログの注目すべきポイントを整理します。

① 謎の「Object型」の正体

Dtype 列にある object。これはデータが「文字列(テキスト)」であることを意味します。 AIモデルは数値しか扱えないため、この object 項目はそのままでは学習に使用できません。「Earth→1、Europa→2」のように数値に置き換えるか、ダミー変数化する処理が必須となります。全14項目のうち半分を占めるこれら7項目の扱いが、最初の関門です。

② 欠損値(Missing Values)の特異性

--- [2] Missing Values Scan --- の数値が示す通り、今回は非常に特徴的な欠落をしています。

  • 「均一な欠損」: 特定の項目に穴が集中しているのではなく、ほぼすべての項目において 200件前後(約2.3%)が欠落しています。これはシステム的な要因で一律にデータが欠落した可能性を示唆しています。
  • 連動性の仮説: 欠損数が全項目で近いことから、ある項目が欠けている乗客は、他の項目も同時に欠けているといった「欠損の連動」があるかもしれません。

③ ターゲット(Transported)のバランス

「飛ばされた(True)」のが 4,378人、「残った(False)」のが 4,315人。比率はほぼ 50%:50%。非常にバランスの良いデータであり、純粋にモデルの識別能力がスコアに反映されやすい環境です。

5. 今後のステップ:丸腰での初予測へ

約8,700件というデータ量は、Titanicの10倍近いです。まずはこれらの欠損を中央値や定数で一括補完し、文字列(object)をダミー変数化して、ランダムフォレストによる「Baseline」を叩き出します。最短ルートで予測を完了させ、次の一手を見極めます。


ターミナルに表示されたログを元に、離陸の準備を整えました。次はいよいよ、一切の細工を排除した「丸腰予測」へと向かいます。さあ、頑張ろう!

【Kaggle挑戦記】Spaceship Titanic 攻略 #1:新章開幕。宇宙船へのチェックインと環境構築

Titanic号での生存予測では、0.8の壁と「過学習」という巨大な怪物に翻弄されました。手元のスコアに一喜一憂し、本番で叩き落とされる過酷な経験……。しかし、その失敗こそが次なる戦いの最大の武器になります。私は次なる挑戦の舞台として、「Spaceship Titanic」を選びました。

1. なぜ「宇宙版タイタニック」への転戦なのか?

舞台は29世紀。太陽系から別の惑星へ向かう宇宙船が亜空間の異常に巻き込まれ、乗客の半分が異次元に飛ばされてしまった……というSF設定です。前作(Titanic)との決定的な違いはデータ量にあります。

  • Titanic: 約900件(データが少なすぎて過学習の罠にハマりやすい)
  • Spaceship Titanic: 約13,000件!

データ量が多いということは、Titanicで学んだ「モデルの制御」や「特徴量エンジニアリング」の結果が、より素直にスコアに反映されることを意味します。いわば、本当の「実力」が試されるフィールド。Titanicで溜まったフラストレーションを、この広大な宇宙で解き放ちます。

2. ターミナルでのセットアップ:戦いの準備

Macのターミナルを叩き、専用の作業ディレクトリを構築。zipファイルを解凍するこの瞬間、新しいコンペ特有のワクワク感が込み上げます。

# ディレクトリ作成と移動
mkdir spaceship_titanic
cd spaceship_titanic

# データの解凍(Kaggleからダウンロードしたzipを展開)
unzip spaceship-titanic.zip

# ファイルの確認
ls
# sample_submission.csv  spaceship-titanic.zip  test.csv  train.csv

途中で ;s と打ち間違えて「command not found」と怒られるのも、集中して作業しているエンジニアの「あるある」です。準備は整いました。

3. 次なるミッション:データの正体を見極める

今回のターゲット変数は Transported(異次元に飛ばされたかどうか)。
さらに HomePlanet(出身星)、CryoSleep(冷凍睡眠)、Cabin(客室番号)など、Titanicとは一味違う、しかしどこか似た匂いのする特徴量が並んでいます。Titanicで培った「欠損値補正」や「ダミー変数化」のテクニックが、この広大な宇宙でどう機能するのか。今から楽しみでなりません。


「0.8の壁」は、この宇宙で超える。
Titanicでの悔しさを燃料に変えて、Spaceship Titanic号、いよいよ離陸です。さあ、頑張ろう!



【Kaggle挑戦記】Titanic 攻略 #18:Title再投入の罠。CV 0.8440 からの「急転下落」

前回、LightGBMに Title(敬称) を再投入し、手元の検証(CV)では 0.8440 という過去最高レベルの数字を叩き出しました。「今度こそ0.8突破か?」と震える手で提出。しかし、返ってきた結果は 0.76076。期待とは裏腹に、前回(0.77511)よりもスコアを下げる「急転直下」の結末となりました。

1. 敗因分析:Titleという「劇薬」

CVスコアが上がり、本番スコアが下がる。これは典型的な 過学習(オーバーフィッティング) の再発です。なぜあんなに制限をかけたのに失敗したのか、エンジニア的に振り返ります。

  • 「敬称」のヒントが強すぎた: Titleには生存率に直結する情報(性別や年齢層)が凝縮されています。LightGBMがこれに依存しすぎてしまい、他の特徴量との組み合わせによる「本質的な予測」を疎かにしてしまった可能性があります。
  • CVスコアの「甘い誘惑」: CV 0.8440 という数字は、訓練データ内の特定のパターンを覚え込むことで得られた「虚像」でした。データの少ないTitanicでは、特徴量を1つ増やすだけで、モデルは簡単に「カンニング(暗記)」を始めてしまいます。
  • 汎化性能の喪失: パラメータ探索で min_child_samples: 20 が選ばれ、制約が緩んだことも、ノイズを拾う原因になったと考えられます。

2. 【教訓】光と影を記録した実装コード

CVスコアに一喜一憂し、過学習を招いてしまった「失敗の記録」としてのコードです。

import pandas as pd
import numpy as np
import lightgbm as lgb
from sklearn.model_selection import GridSearchCV

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

# 2. Title(敬称)の抽出(SyntaxWarning対策済み)
for df in [train_data, test_data]:
    df['Title'] = df['Name'].str.extract(r' ([A-Za-z]+)\.', expand=False)
    df['Title'] = df['Title'].replace(['Lady', 'Countess','Capt', 'Col','Don', 'Dr', 'Major', 'Rev', 'Sir', 'Jonkheer', 'Dona'], 'Rare')
    df['Title'] = df['Title'].replace('Mlle', 'Miss')
    df['Title'] = df['Title'].replace('Ms', 'Miss')
    df['Title'] = df['Title'].replace('Mme', 'Mrs')

# 3. 前処理(ベスト構成を維持)
for df in [train_data, test_data]:
    df['Embarked'] = df['Embarked'].fillna('S')
    df['FamilySize'] = df['SibSp'] + df['Parch'] + 1

group_cols = ['Pclass', 'Sex']
train_data['Age'] = train_data['Age'].fillna(train_data.groupby(group_cols)['Age'].transform('median'))
test_data['Age'] = test_data['Age'].fillna(test_data.groupby(group_cols)['Age'].transform('median'))
test_data['Fare'] = test_data['Fare'].fillna(test_data['Fare'].median())

# 4. 特徴量選択(ここでTitleを追加したのが裏目に…)
features = ["Pclass", "Sex", "Age", "SibSp", "Parch", "Fare", "FamilySize", "Embarked", "Title"]
X = pd.get_dummies(train_data[features], drop_first=True)
y = train_data["Survived"]
X_test = pd.get_dummies(test_data[features], drop_first=True)
X, X_test = X.align(X_test, join='left', axis=1, fill_value=0)

# 5. パラメータ設定
param_grid = {
    'num_leaves': [7, 10, 15],
    'learning_rate': [0.01, 0.05],
    'n_estimators': [100, 200, 300],
    'max_depth': [3, 4], 
    'min_child_samples': [20, 40],
    'random_state': [1]
}

gbm = lgb.LGBMClassifier(verbosity=-1)
grid_search = GridSearchCV(gbm, param_grid, cv=5, n_jobs=-1, verbose=0)
grid_search.fit(X, y)

print(f"Best CV Score: {grid_search.best_score_:.4f}") # 0.8440

# 6. 予測・出力
best_model = grid_search.best_estimator_
predictions = best_model.predict(X_test)
pd.DataFrame({'PassengerId': test_data.PassengerId, 'Survived': predictions}).to_csv('submission_lgbm_title.csv', index=False)

3. 実験結果:0.77511 → 0.76076 への後退

アルゴリズムや特徴量を変えるたびに、スコアは大きく変動します。今回はその「負の側面」が強く出てしまいました。

  • 前回: Score 0.77511 (LightGBM 制限付き)
  • 今回: Score 0.76076 (Title追加)
  • 分析: CVスコア 0.844 との乖離は 0.08 以上。明らかな過学習です。

4. 考察:0.8の壁は「暗記」では越えられない

エンジニア的な視点:
Titleという強力な特徴量を入れたことで、モデルは「考える」ことをやめ、「覚える」ことに走ってしまいました。Titanic攻略において、0.8という数字がなぜ高い壁なのか。それは、強力なモデルや特徴量を「いかに使わずに、本質だけを捉えさせるか」という、引き算の思考が求められるからだと痛感しました。


スコアが下がったことは敗北ではありません。この強力なTitleを「どう手なずけるか」、あるいは「Titleに頼らない別の道を探すか」。この試行錯誤こそが、データサイエンスの醍醐味です。次こそ、真の汎化性能を求めて!

【Kaggle挑戦記】Titanic 攻略 #17:0.77511への浮上。LightGBMを「制御」する手応え

前回、最強モデル LightGBM を投入するも、過学習により 0.76315 という惨敗を喫しました。今回はその反省を活かし、モデルにあえて「制約」を課すリベンジマッチ。結果は 0.77511。自己ベストには届きませんでしたが、確かな改善の兆しが見えてきました。

1. 戦略の検証:過学習の抑制は成功したか?

今回の修正の肝は、モデルに「深追いさせない」ことでした。その結果、スコアには以下のような変化が現れました。

  • 本番スコアの向上: 0.76315 → 0.77511(+0.012の改善)
  • CVスコアの適正化: 0.8418 → 0.8373(手元の数字が下がり、本番が上がった)

この「手元の数字を下げて、本番を上げる」という現象こそ、過学習が解消に向かっている決定的な証拠です。F1マシンを路地裏に合わせて減速させたことで、壁にぶつからずにコーナーを曲がれるようになったのです。

2. 【実装】「制約」を刻んだリベンジ・コード

過学習した前回(負け)の設定をコメントで残し、今回(改善)の設定を対比させました。

import pandas as pd
import numpy as np
import lightgbm as lgb
from sklearn.model_selection import GridSearchCV

# 1. 前処理(自己ベスト 0.78468 時の最強布陣を維持)
train_data = pd.read_csv('train.csv')
test_data = pd.read_csv('test.csv')

for df in [train_data, test_data]:
    df['Embarked'] = df['Embarked'].fillna('S')
    df['FamilySize'] = df['SibSp'] + df['Parch'] + 1

group_cols = ['Pclass', 'Sex']
train_data['Age'] = train_data['Age'].fillna(train_data.groupby(group_cols)['Age'].transform('median'))
test_data['Age'] = test_data['Age'].fillna(test_data.groupby(group_cols)['Age'].transform('median'))
test_data['Fare'] = test_data['Fare'].fillna(test_data['Fare'].median())

# 2. 特徴量のダミー変数化
features = ["Pclass", "Sex", "Age", "SibSp", "Parch", "Fare", "FamilySize", "Embarked"]
X = pd.get_dummies(train_data[features], drop_first=True)
y = train_data["Survived"]
X_test = pd.get_dummies(test_data[features], drop_first=True)
X, X_test = X.align(X_test, join='left', axis=1, fill_value=0)

# 3. ライトGBM:過学習を防ぐための「抑制」チューニング
param_grid = {
    # 'num_leaves': [31],        # 前回:多すぎて細かく分けすぎていた
    'num_leaves': [7, 10, 15],   # 修正:モデルをシンプルに保つ

    'learning_rate': [0.01, 0.05],
    'n_estimators': [100, 200],

    # 'max_depth': [-1],         # 前回:無制限が過学習の主因
    'max_depth': [3, 4],         # 修正:あえて「浅い木」に限定する

    'min_child_samples': [20, 40], # 修正:1つの枝に40人以上のデータを要求(ノイズ対策)

    'random_state': [1]
}

# 4. グリッドサーチ実行
gbm = lgb.LGBMClassifier(verbosity=-1)
grid_search = GridSearchCV(gbm, param_grid, cv=5, n_jobs=-1, verbose=0)
grid_search.fit(X, y)

# 5. 結果の記録
print(f"Best Params: {grid_search.best_params_}")
print(f"Best Score (CV): {grid_search.best_score_:.4f}") # 0.8373 を記録

# 6. 予測
best_model = grid_search.best_estimator_
predictions = best_model.predict(X_test)
pd.DataFrame({'PassengerId': test_data.PassengerId, 'Survived': predictions}).to_csv('submission_lgbm_v2.csv', index=False)

3. 考察:0.8の壁を越えるために必要なこと

エンジニア的な視点:
今回の実験で、「強力なアルゴリズムを使うなら、強力なブレーキが必要である」という教訓が実証されました。0.77511 という数字は、まだ自己ベストには届きませんが、「モデルの使いこなし」という点では過去最高のレベルに達しています。

ここからさらに 0.03 スコアを伸ばし、0.8の大台に乗るには、モデルの微調整(チューニング)だけでは限界があるかもしれません。次は、データの背後に隠れた「家族の運命」や「チケット番号の繋がり」など、人間らしい洞察(特徴量エンジニアリング)を LightGBM に教え込むフェーズに来ていると感じます。


数字は嘘をつきません。改善した 0.012 は、私たちが正しい方向に進んでいる証拠。この調子で、次なる一手「特徴量の深化」へ進みます!