忍者ブログ
統計、機械学習、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

【DS検定対策】名前のワナを攻略!分類の王道「ロジスティック回帰」

統計学や機械学習を学び始めると最初にぶつかる「名前の矛盾」。それが「ロジスティック回帰」です。なぜ回帰なのに分類なのか、その仕組みをスッキリ整理しましょう。

1. 【 問題 】

「ロジスティック回帰」に関する説明として、最も適切なものはどれでしょうか?

① 数値を予測する「回帰」の手法であり、住宅価格の予想などに用いられる。
② データを2つのクラスに分ける「分類」の手法であり、ある事象が起こる確率を予測する。
③ データのグループ化を行う「非教師あり学習」の手法である。
④ 決定木をたくさん組み合わせた「アンサンブル学習」の手法である。


2. 【 解答 】

正解: ② データを2つのクラスに分ける「分類」の手法

3. 整理:確率を計算して「境界線」を引く世界

ロジスティック回帰は、入力データから「あるクラスに属する確率」を計算し、その値が0.5(50%)を超えたら「合格(1)」、そうでなければ「不合格(0)」のように判定します。

【 ロジスティック回帰の仕組みイメージ 】

[ 1. 入力データの計算 ]
年齢や購入履歴などのデータを、線形式で計算する。

[ 2. シグモイド関数を通す ]
★ ここが核心!
どんな大きな値(または小さな値)も、0 から 1 の範囲にギュッと押し込める。

[ 3. 確率の出力 ]
「この客が購入する確率は 0.82 (82%) です」と出力。

[ 4. 分類 ]
しきい値(0.5など)で区切り、「購入するクラス」に分類する。

--------------------------

ポイント: 計算過程で「数値を予測(回帰)」しているため名前に「回帰」と付きますが、目的は「分類」です。

4. 覚えておくべき重要キーワード

1. シグモイド関数: 出力値を0〜1の間に収めるための関数。S字型のカーブを描きます。
2. オッズ比: ある事象が起こる確率と起こらない確率の比率。ロジスティック回帰の解釈に不可欠です。
3. 二値分類: 「Yes/No」「合格/不合格」など、2つのクラスに分けるのが基本です。


5. DS検定形式:実戦4択クイズ

問:ロジスティック回帰において、出力(確率 $p$)を求める際に用いられる、以下の数式で表される関数を何と呼ぶか。
$$f(x) = \frac{1}{1 + e^{-x}}$$

① ソフトマックス関数   ② シグモイド関数   ③ 恒等関数   ④ 階段関数

【 正解: ② 】

解説: この数式は「標準シグモイド関数」と呼ばれ、ロジスティック回帰の心臓部です。$x$ がどんな値でも $f(x)$ は必ず 0 より大きく 1 より小さい値になるため、「確率」として扱うのに非常に適しています。


6. まとめ

DS検定において「回帰という名前だが分類に使われる」「シグモイド関数で確率を出力する」という特徴が出たら、迷わずロジスティック回帰を選びましょう。シンプルながら解釈性が高く、今でもビジネス現場の第一線で使われている強力な手法です!




【Kaggle挑戦記】Titanic 攻略 #11:血縁データの統合。「家族サイズ」が0.78の壁を打ち破る

前回(攻略 #10)のデバッグでは、自作の「子供フラグ」がモデルにとって不純なノイズとなっていた現実を直視しました。今回はその反省を活かし、バラバラに存在していた「血縁データ」をロジカルに統合。ついに 0.78 の壁を突破する強力なヒント FamilySize(家族人数) を導入します。

1. 今回の着眼点:孤立か、連帯か

タイタニックのデータには、家族に関する項目が2つ用意されています。しかし、これらを別々に見ていては「その人がどれほどの集団で行動していたか」という実態が見えてきません。

  • SibSp: 兄弟、配偶者の数
  • Parch: 両親、子供の数

避難の際、1人で行動していたのか、あるいは守るべき家族がいたのか。この「群れ」の大きさを一つの指標として統合し、モデルに新たな視点を与えます。自分自身(+1)を足すことで、「1 = 単独客」「2以上 = 家族連れ」という、生存率を分ける明確な境界線が生まれます。

2. 【実装】FamilySize導入とモデル構築の全コード

属性別Age補完を継承しつつ、FamilySizeを計算してランダムフォレストに投入するまでの全工程です。

import pandas as pd
from sklearn.ensemble import RandomForestClassifier

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

# 2. Age(年齢)を Pclass×Sex の中央値で補完
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'))

# 3. 家族サイズ(FamilySize)の算出
train_data['FamilySize'] = train_data['SibSp'] + train_data['Parch'] + 1
test_data['FamilySize'] = test_data['SibSp'] + test_data['Parch'] + 1

# テストデータの Fare(運賃)の欠損値を中央値で補完
test_data['Fare'] = test_data['Fare'].fillna(test_data['Fare'].median())

# 4. 学習に使用する特徴量の定義
features = ["Pclass", "Sex", "Age", "SibSp", "Parch", "Fare", "FamilySize"]

# カテゴリ変数のダミー変数化
X = pd.get_dummies(train_data[features])
y = train_data["Survived"]
X_test = pd.get_dummies(test_data[features])

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

# 6. 予測と提出ファイルの出力
predictions = model.predict(X_test)
output = pd.DataFrame({'PassengerId': test_data.PassengerId, 'Survived': predictions})
output.to_csv('submission_family_size.csv', index=False)

print("Score 0.78229 達成用の提出ファイルを保存しました。")

3. 実験結果:0.77990 → 0.78229 へ上昇!

家族サイズを導入した結果、ついに停滞していたスコアが大きく動きました。

  • 前回(Age補完のみ): Score 0.77990
  • 今回(FamilySize追加): Score 0.78229

エンジニア的な考察:
ついに 0.78 の大台に乗りました。単なる数値だった「SibSp」と「Parch」を合計して『家族というひとつの単位』として捉え直したことで、モデルは「独身男性(死亡率高)」や「小家族(生存率高)」という構造を、より正確に切り分けられるようになったと言えます。アルゴリズムをいじるのではなく、データの「見方」を変えることが勝利の鍵であることを証明しました。

4. 次なる一手:名前の「敬称」から社会的地位を暴く

0.78229 というスコアは一つの通過点に過ぎません。まだ伸び代は十分にあります。次は、これまで手をつけていなかった巨大な情報源 Name(名前) の解析に挑みます。ここから「Mr.」「Miss.」「Master.」といった敬称を抜き出せば、年齢だけでは判別できない「既婚・未婚」や「家柄(社会的地位)」という決定的なヒントが手に入るはずです。


「質の高い燃料(特徴量)」を投入することで、ランダムフォレストは見違えるような精度を見せ始めました。この勢いのまま、次はテキストデータの解析に踏み込み、さらなる高みを目指します。


【Kaggle挑戦記】Titanic 攻略 #10:モデルの検証。自作フラグは「ノイズ」だったのか?

前回(攻略 #9)では、生存予測の精度を上げるため、以下のデータ加工とモデル構築を行いました。

1. 前回の復習:投入データの構成と加工

  • Age(年齢)の精密補完: 単純な平均値ではなく、Pclass(客室階級)と Sex(性別)を掛け合わせたグループごとの中央値で欠損値を補完。
  • IsChild_Final(自作フラグ): 補完した「Age(年齢)が12歳以下」という条件に、さらに「Name(名前)に Master を含む」という確実性の高い敬称データを組み合わせて作成。
  • 投入した特徴量: Pclass, Sex, Age, IsChild_Final, SibSp(兄弟・配偶者数), Parch(親子数), Fare(運賃)。
  • モデル: ランダムフォレスト(n_estimators=500)。

しかし、結果は 0.77751 → 0.77511 へと下落。「子供と女性は優先される」という強力なドメイン知識を反映したはずが、なぜ逆効果になるのか。学習済みモデルの内部数値を直接取り出してデバッグします。

2. 思考のステップ:なぜ「子供フラグ」は失敗したのか

  1. 「Age」と「Name」から導いた子供フラグはダメだった。
    実際に精度が下がった以上、このフラグの設定には致命的な「何か」が足りない。
  2. 「子供と女性は優先されていた」というドメイン知識は揺るがない。
    歴史的事実として、避難優先順位の筆頭は「Women and children」である。
  3. モデルには「子供フラグ」と「Sex(女性)」という2つの軸を与えてある。
    理論上、これで優先グループを網羅できているはずだ。
  4. 【核心】ならば、モデル内部で、この2つの軸が「生存を切り分けるための重要度」としてどう扱われているかを確認すべきではないか?

3. 検証:ランダムフォレストが算出する「重要度」の仕組み

今回使用しているランダムフォレストは、学習(fit)のプロセスで500本の独立した決定木を作成します。その際、各項目の「重要度(Feature Importance)」は以下のステップで計算されています。

  • 不純度の減少量を累積: 各決定木がデータを分割する際、「性別」や「子供フラグ」などの軸を使います。その軸で分けた結果、グループ内の「生存・死亡」がきれいに分かれるほど(=不純度が下がるほど)、その軸に高いスコアが与えられ、500本分累積されます。
  • 比率の算出: 全項目が稼いだ累積スコアの総和を 1.0 (100%) としたとき、各項目が占める割合を算出します。

4. 【実装】モデルが各項目をどれだけ「信頼」したかを確認する

import pandas as pd
from sklearn.ensemble import RandomForestClassifier

# 1. 前回のデータ準備(攻略 #9の状態を再現)
train_data = pd.read_csv('train.csv')
group_cols = ['Pclass', 'Sex']

# Ageの補完:PclassとSexごとの中央値
train_data['Age'] = train_data['Age'].fillna(train_data.groupby(group_cols)['Age'].transform('median'))

# Age(年齢)とName(敬称Master)から導いた子供フラグ
train_data['IsChild_Final'] = ((train_data['Age'] <= 12) | (train_data['Name'].str.contains('Master'))).astype(int)

# 学習に使用する特徴量の定義
features = ["Pclass", "Sex", "Age", "IsChild_Final", "SibSp", "Parch", "Fare"]
X = pd.get_dummies(train_data[features])
y = train_data["Survived"]

# 2. モデルの学習(500本の決定木を生成し、それぞれの分割性能を記録)
model = RandomForestClassifier(n_estimators=500, max_depth=5, random_state=1)
model.fit(X, y)

# 3. 各項目の重要度(Feature Importance)を抽出
importances = pd.Series(model.feature_importances_, index=X.columns).sort_values(ascending=False)

print("--- [Feature Importance: 500本の木による信頼度の集計結果] ---")
print(importances)

# 4. 補足検証:子供フラグ(IsChild_Final)の実際の生存率
print("\n--- [Actual Survival Rate: 子供フラグ別の生存率(学習データ)] ---")
print(train_data.groupby('IsChild_Final')['Survived'].mean())

5. 実行結果:残酷な現実

検証コードを走らせた結果、以下の数値が返ってきました。

■ 500本の木による信頼度の集計結果(Feature Importance)

Sex_female       0.273813  (27.4%)
Sex_male         0.251328  (25.1%)
Fare             0.146034  (14.6%)
Pclass           0.125932  (12.6%)
Age              0.090933  (9.1%)
SibSp            0.053964  (5.4%)
IsChild_Final    0.029075  (2.9%)  ← ★ここ
Parch            0.028921  (2.9%)

■ 子供フラグ別の実際の生存率(学習データ)

IsChild_Final
0 (大人など)  : 0.366 (36.6%)
1 (子供)      : 0.575 (57.5%)

6. 考察:数字が示す「子供フラグ」の敗北原因

  • 【データ:累積貢献度 2.9%】子供フラグは「無視」された:
    500本の決定木が生存予測の軸として信頼した割合(Feature Importance)は、わずか 0.029 (2.9%) でした。これは、運賃(14.6%)や客室階級(12.6%)、さらには生の年齢(9.1%)よりも遥かに低く、モデルが「このフラグは生存を占う上で、ほとんど信用に値しない」と公式に判定したことを示しています。
  • 【データ:生存率 57.5%】「6割・4割」では使えない:
    モデルがこのフラグを信頼しなかった直接の根拠は、実行結果の IsChild_Final = 1(子供グループ)の生存率が 57.5% に留まったことです。これは「約6割が生き、42.5%(4割以上)が死亡している」という極めて不純な状態です。モデルが求めているのは生存か死亡かを明確に切り分けられる軸ですが、このフラグで分けても4割以上も死亡者が混ざってしまうため、生存予測の条件(if文)としては不適格と見なされました。
  • 【結論】不純な境界線がスコアを落とした:
    Age(年齢)とName(敬称Master)から導き出した「12歳以下」という人為的な境界線では、モデルが生存を確信できるほどの「純度」を生み出せませんでした。この「6割生存・4割死亡」という分離性能の低さこそが、累積貢献度を押し下げ、結果として全体の予測精度(スコア)を下落させた正体です。

【Kaggle挑戦記】Titanic 攻略 #9:名前(Name)による救出作戦と、裏目に出た結果

前回(攻略 #8)では、「12歳以下の子供フラグ(IsChild)」を導入した結果、スコアが 0.77990 → 0.77751 へと下落しました。この原因を「中央値補完によって大人扱いされた子供がノイズになった」と仮説を立て、今回は名前(Name)に含まれる確定情報を用いてその矛盾の解消を試みました。

1. 今回のロジック:名前と年齢のハイブリッド判定

Ageが欠損していた乗客に対し、一律の中央値で補完したことで生じた「誤判定」を上書きするのが今回の狙いです。以下の条件のいずれかを満たす場合に「真の子供(IsChild_Final)」と定義しました。

  • 条件A: 年齢(Age)が12歳以下である。
  • 条件B: 名前(Name)の中に、男の子を示す敬称 "Master." が含まれている。

名前に "Master" とあれば、たとえAgeが空欄で中央値(例:25歳)が割り当てられていても、確実に子供として救出できる計算です。論理的には、前回よりも情報の精度は高まっているはずでした。

2. 【実装】コード全文

前回(#8)からの変更点(OR条件によるフラグ作成)を反映した全文です。

import pandas as pd
from sklearn.ensemble import RandomForestClassifier

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

# 2. Age の中央値補完(攻略 #7 継承)
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())

# 3. 【攻略 #9 での変更点】真の子供フラグ(IsChild_Final)の作成
# ---------------------------------------------------------
# 年齢(Age)が12歳以下、または名前(Name)に "Master" を含む場合にフラグを立てる
# ---------------------------------------------------------
train_data['IsChild_Final'] = ((train_data['Age'] <= 12) | (train_data['Name'].str.contains('Master'))).astype(int)
test_data['IsChild_Final'] = ((test_data['Age'] <= 12) | (test_data['Name'].str.contains('Master'))).astype(int)

# 4. 特徴量の選択
features = ["Pclass", "Sex", "Age", "IsChild_Final", "SibSp", "Parch", "Fare"]

X = pd.get_dummies(train_data[features])
y = train_data["Survived"]
X_test = pd.get_dummies(test_data[features])

# 5. モデルの学習(500本維持)
model = RandomForestClassifier(n_estimators=500, max_depth=5, random_state=1)
model.fit(X, y)

predictions = model.predict(X_test)
output = pd.DataFrame({'PassengerId': test_data.PassengerId, 'Survived': predictions})
output.to_csv('submission_master_rescue.csv', index=False)

3. 実験結果:さらなるスコア下落(0.77511)

期待に反し、スコアは前回の 0.77751 からさらに下がり、0.77511 となりました。精度を高めるための「Master救出作戦」でしたが、結果としては裏目に出た形です。

4. 考察:なぜ「正しいはずの情報」でスコアが落ちるのか

論理的に正しいはずのフラグを追加してスコアが落ちる場合、以下の可能性が考えられます。

  • 過学習(Overfitting): 「Master = 生存」という強いルールを学習しすぎた結果、テストデータに含まれる「不幸にも助からなかった Master」を正しく予測できなくなった。
  • 情報の競合: すでに Age や Parch(親子数)から「子供」であることをモデルが把握していた場合、新しいフラグの追加が判断のノイズになり、木構造が不安定になった。

「フラグを立てれば立てるほど良くなる」というわけではないのが、機械学習の難しさです。これまでは「子供」という一点にのみ執着してきましたが、その偏った視点自体が現在の停滞を招いているのかもしれません。


次は、子供(Master)だけでなく、未婚女性(Miss)や既婚女性(Mrs)、成人男性(Mr)といった「すべての敬称」を網羅的に数値化し、よりフラットな視点で特徴量エンジニアリングを進めていきます。特定の属性に固執せず、全体像を捉え直すことが 0.78 突破への道かもしれません。