【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に頼らない別の道を探すか」。この試行錯誤こそが、データサイエンスの醍醐味です。次こそ、真の汎化性能を求めて!
PR