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

【Kaggle挑戦記】Titanic 攻略 #12:0.77751の呪縛。精緻なクレンジングでも動かぬ壁

前回(攻略 #11)、家族サイズ(FamilySize)の導入で 0.78229 という自己ベストを叩き出した勢いのまま、今回は「最強の特徴量」と名高い Title(敬称) を投入しました。しかし、返ってきた結果は 0.77751 への後退。さらに、表記ゆれを疑い徹底したクレンジングを施すも、スコアは1ミリも動きませんでした。

1. 仮説と検証:なぜ「完璧なはずのコード」でスコアが動かないのか

抽出ミスや空白の混入を疑い、strip() や徹底した表記統合を行いました。論理的には、これでデータは綺麗になり、モデルはより正確に「Master(子供)」や「Rare(特権階級)」を識別できるはずでした。しかし、結果は非情な現状維持です。

  • 仮説1:情報の重複(多重共線性)
    既に「Sex」や「Age(精密補完済み)」、「Pclass」の中に、敬称が持つ情報の大部分が含まれてしまっている可能性。
  • 仮説2:ランダムフォレストの限界
    現在のパラメータ設定(深さ5など)では、これ以上細かい特徴量を読み取っても、汎化性能(未知のデータへの対応力)に繋がっていない可能性。

2. 【実装】クレンジングを徹底した Title 導入コード

不備のない、現時点で最も「綺麗」な状態の全コードです。結果としてスコアは改善しませんでしたが、データ処理の型としては正攻法と言えます。

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

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

# 2. 敬称抽出関数の定義(徹底した空白除去)
def get_title(name):
    if '.' in name:
        return name.split(',')[1].split('.')[0].strip()
    return 'Unknown'

# 3. データのクレンジングと正規化
for df in [train_data, test_data]:
    df['Title'] = df['Name'].map(get_title)
    
    # 希少な敬称の統合
    rare_titles = ['Lady', 'Countess','Capt', 'Col','Don', 'Dr', 'Major', 'Rev', 'Sir', 'Jonkheer', 'Dona']
    df['Title'] = df['Title'].replace(rare_titles, 'Rare')
    
    # 表記ゆれの統一
    df['Title'] = df['Title'].replace(['Mlle', 'Ms'], 'Miss')
    df['Title'] = df['Title'].replace('Mme', 'Mrs')

# 4. 精密なAge補完(Pclass × Sex × Title)
group_cols = ['Pclass', 'Sex', 'Title']
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'))

# 5. 家族サイズと運賃の処理
for df in [train_data, test_data]:
    df['FamilySize'] = df['SibSp'] + df['Parch'] + 1
test_data['Fare'] = test_data['Fare'].fillna(test_data['Fare'].median())

# 6. 特徴量のダミー変数化
features = ["Pclass", "Sex", "Age", "SibSp", "Parch", "Fare", "FamilySize", "Title"]
X = pd.get_dummies(train_data[features])
y = train_data["Survived"]
X_test = pd.get_dummies(test_data[features])

# 学習とテストで列を完全に一致させる
X, X_test = X.align(X_test, join='left', axis=1, fill_value=0)

# 7. モデル学習(500本の木、深さ5)
model = RandomForestClassifier(n_estimators=500, max_depth=5, random_state=1)
model.fit(X, y)

# 8. 予測と出力
predictions = model.predict(X_test)
pd.DataFrame({'PassengerId': test_data.PassengerId, 'Survived': predictions}).to_csv('submission_cleansed.csv', index=False)

3. 実験結果:0.77751(変化なし)

クレンジング前と後で、スコアは一桁も変わりませんでした。この結果が意味するのは、「現在の私のモデルにとって、Title情報は恩恵よりもノイズ、あるいは過学習の要因になっている」という不都合な真実です。

  • 前回(FamilySize追加): Score 0.78229
  • 今回(Title追加+徹底洗浄): Score 0.77751

4. 考察:引き算の勇気が必要か

エンジニア的な視点:
「良かれと思って追加した機能が、システム全体のパフォーマンスを落とす」。これは開発現場でもよくある話です。今回の Title 導入は、一見すると情報の追加でしたが、実際にはモデルの焦点をぼかしてしまった可能性があります。0.78229 という自己ベストに戻るには、一度「Titleを捨てる」という引き算の決断、あるいは全く別の次元の特徴量(チケット番号や客室番号)を検討する必要がありそうです。


成功体験よりも、こうした停滞こそが思考を深くしてくれます。次回の攻略では、この「0.77751の壁」を突破するために、特徴量の取捨選択を根本から見直す「モデルのスリム化」に挑みます。

PR