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

【Kaggle挑戦記】Titanic 攻略 #12:名前の中に答えがあった。「敬称(Title)」抽出で0.80超えへ

前回(攻略 #11)では、家族サイズを導入することで 0.78229 という自己ベストを更新しました。しかし、依然として「生存」か「死亡」かをモデルが確信しきれないグレーゾーンが存在します。今回は、これまで無視してきたテキストデータ Name(名前) から、生存率を決定づける隠れた指標「敬称」を掘り起こします。

1. なぜ「名前」ではなく「敬称」なのか?

機械学習モデルにとって「個別の氏名」はただのラベルですが、名前に含まれる Mr. / Miss. / Mrs. / Master. といった敬称(Title)は情報の宝庫です。これらを抽出することで、当時の社会における立場や家族背景をより深くモデルに反映させることができます。

  • 「女性」の中のさらなる分類: 未婚(Miss)か既婚(Mrs)かを知ることで、生存傾向の差をより細かく捉えます。
  • 子供の確実な特定: 名前に Master とあれば、年齢データが欠損していても確実に「男児」であると断定でき、補完精度が劇的に向上します。
  • 社会的地位の捕捉: Dr.(医師)や Col.(大佐)といった希少な敬称は、優先的にボートへ誘導された可能性を示唆します。

2. 【実装】敬称抽出とモデル構築の全コード

正規表現的に敬称を切り出し、主要な5グループに統合した上で、これまでの「家族サイズ」「Age補完」と組み合わせて学習させます。

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. 敬称(Title)を抽出する関数の定義
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本の木)
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_title.csv', index=False)

3. 実験結果:0.78229 → 0.77751 へ下落

最強のヒントを投入したはずが、結果は無情なスコアダウンとなりました。

  • 前回(FamilySize追加): Score 0.78229
  • 今回(Title追加): Score 0.77751

エンジニア的な考察:
「論理的に正しい加工が、必ずしも精度に直結しない」という機械学習の洗礼を受けました。今回の下落の原因として考えられるのは、モデルが Title という強い情報に頼りすぎてしまい、複雑な分岐を作った結果、未知のデータに対する柔軟性を失った(過学習した)可能性です。また、SexTitle には強い相関があるため、情報が重複して判断を狂わせたのかもしれません。


敗北は、次なる改善への貴重なデータです。0.78突破の喜びから一転、モデルの繊細さを痛感する結果となりました。次は、今回追加した Title を削るべきか、あるいはパラメータを再調整して「情報の衝突」を解消すべきか、慎重な見極めが必要です。


PR