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

【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割死亡」という分離性能の低さこそが、累積貢献度を押し下げ、結果として全体の予測精度(スコア)を下落させた正体です。
PR

【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 突破への道かもしれません。


【Kaggle挑戦記】Titanic 攻略 #8:フラグ追加の試みと、見えてきた「欠損値補完」の罠

前回(攻略 #7)では、性別と客室クラスを掛け合わせた精緻な Age(年齢)補完を行い、スコアは 0.77511 → 0.77990 と微増しました。今回は、さらに「子供」という属性を強調し、0.78 の大台を突破するための特徴量エンジニアリングに挑戦しました。

1. これまでの振り返りと今回の仮説

現在の私たちの戦績と構成は以下の通りです。

  • 攻略 #6: パラメータ調整(木の数)ではスコアに変化なし
  • 攻略 #7: Age(年齢)を補完して投入。スコアが 0.00479 上昇

現在、Age は連続値としてモデルに渡っています。しかし、生存の鍵は「子供か大人か」という二値的な境界線にあるはず。そこで、「12歳以下(Child)」か「それ以外(Adult)」かというフラグ(IsChild)を明示的に作ることで、モデルの判断を助けるという仮説を立てました。

2. 【実装】子供フラグ(IsChild)の導入とコード全文

Age が 12 以下の場合は 1、それ以外を 0 とする新しいカラム IsChild を作成し、特徴量に加えました。前回からの変更・追加箇所がわかるように実装したコードの全文です。

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. 【攻略 #7からの継承】Age の欠損値を「性別×客室クラス」ごとの中央値で補完
# ※ここでの中央値補完が、後のフラグ判定に影響を与えた可能性あり
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. 【攻略 #8での追加】子供フラグ(IsChild)の作成
# ---------------------------------------------------------
# 12歳以下を子供(1)、それ以外を大人(0)として新規カラム作成
# ---------------------------------------------------------
train_data['IsChild'] = (train_data['Age'] <= 12).astype(int)
test_data['IsChild'] = (test_data['Age'] <= 12).astype(int)

# 4. 【攻略 #8での変更】特徴量の選択
# ---------------------------------------------------------
# 新たに作成した "IsChild" をリストに追加
# ---------------------------------------------------------
features = ["Pclass", "Sex", "Age", "IsChild", "SibSp", "Parch", "Fare"]

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

# 5. モデルの構築(攻略 #6 で決めた 500本 を維持)
model = RandomForestClassifier(n_estimators=500, max_depth=5, random_state=1)
model.fit(X, y)

# 6. 予測の実行
predictions = model.predict(X_test)

# 7. 提出用ファイルの作成
output = pd.DataFrame({'PassengerId': test_data.PassengerId, 'Survived': predictions})
output.to_csv('submission_ischild_added.csv', index=False)

3. 実験結果:無情にもスコアは 0.77511 へ下落

期待に反し、スコアは前回の Age 投入前と同じ 0.77511 まで落ち込んでしまいました。良かれと思って追加した「子供フラグ」が、なぜ足を引っ張ったのでしょうか。

4. 考察:欠損値補完が「真の子供」を消した可能性

ここで非常に重要な仮説が浮かび上がりました。「Ageが空欄で、適当な中央値で埋められた人たち」の中に、実は助かったはずの子供(Master)が混ざっていたのではないか? という点です。

攻略 #7 で行った「クラス別の中央値補完」では、たとえば3等客のAge欠損値に対し、一律で「20代半ば」といった大人の数値を割り当ててしまいました。その結果、本来は子供であったはずの乗客が、この IsChild フラグによって「確定的な大人(0)」としてモデルに誤学習されてしまった恐れがあります。

つまり、現在の「Ageベースのフラグ」は、補完データの不正確さを増幅させるノイズになってしまったと言えます。


次の一手は、この矛盾を解消するために「名前(Name)」に含まれる情報を使います。年齢が不明でも、名前に "Master." とあればその乗客は確実に男の子です。次回、「名前 + 年齢」による真の子供特定に挑み、0.78 の壁を叩き割ります。



【Kaggle挑戦記】Titanic 攻略 #7:データクレンジングの深化(Age の欠損値補完)

前回(攻略 #6)の実験では、アルゴリズムのパラメータ(木の数)を増やすという「計算側の工夫」を行いましたが、結果は無情にもスコアに変化なし。エンジニアとして、現在の材料(特徴量)だけでは限界に達したことを確認しました。

1. これまでの振り返り

現在の私たちの立ち位置を整理します。これまでは、極めてシンプルなデータ処理に留まっていました。

  • 利用した特徴量: Pclass, Sex, SibSp, Parch, Fare(計5項目)
  • 利用したアルゴリズム: ランダムフォレスト(Random Forest)
  • 前回の実験: 木の数を 100 → 500 へ増強したが、Score 0.77511(変化なし)

この結果から導き出されるロジカルな結論は、「計算式をいじくり回すよりも、モデルに与える『新しいヒント(特徴量)』を増やすべきだ」ということです。

2. 今回のターゲット:Age(年齢)の救出

生存率に大きく関与しているはずなのに、これまで無視してきたデータがあります。それが Age(年齢) です。無視していた理由は、データの約20%が空欄(欠損値)であり、そのままでは機械学習にかけられないからです。

今回は、この欠損値を「単なる全体平均」で埋めるのではなく、データの背景を洞察した「属性別の層別補完」という手法で救い出します。

3. 【戦略】性別 × 客室クラス別の中央値で補完

「1等客の男性」と「3等客の女性」では、明らかに年齢層が異なります。そこで、Pclass(客室クラス)と Sex(性別)を掛け合わせたグループごとに中央値を算出し、より実態に近い値を穴埋めします。

# 【実装】性別×客室クラスごとの中央値で Age の欠損値を埋める
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'))

4. 実験結果:0.77511 → 0.77990 へ上昇!

Ageを特徴量に加えて再学習させた結果、ついにスコアが動きました。

  • 前回(Ageなし): Score 0.77511
  • 今回(Ageあり): Score 0.77990

エンジニア的な考察:
「少ししか上がらないな」というのが正直な感想ですが、前回のパラメータ調整ではピクリとも動かなかったスコアが、データのクレンジングによって確実に前進しました。これは、モデルが「Women and Children First(婦女子優先)」という歴史的背景を、より正しく理解し始めた証拠と言えます。


「質の高いヒント」を投入することで、停滞していた壁をわずかながら突破しました。しかし、まだ 0.78 の大台には届いていません。次はさらに踏み込み、名前(Name)に含まれる敬称から「社会的地位」や「未婚・既婚」を抽出する、より高度な特徴量エンジニアリングが必要かもしれません。



【Kaggle挑戦記】Titanic 攻略 #6:モデルの「合議制」を強化し、精度向上の実験へ

前回(攻略 #5)では、生存率と相関の強かった「Fare(運賃)」を特徴量に加え、スコアを 0.76749 → 0.77511 へと上昇させることに成功しました。今回は、さらに精度を上げるための「次の一手」を、エンジニアらしく最小の工数で検証します。

1. これまでの復習

まずは、現在の私たちの武器(モデル構成)を整理しておきます。

  • 利用した特徴量(5項目): Pclass, Sex, SibSp, Parch, Fare
    (生存に直結する納得感のあるヒントを投入済みです)
  • 利用したアルゴリズム: ランダムフォレスト(Random Forest)
    (攻略 #4 で中身を覗いた、100本の決定木による「合議制」モデルです)

2. 今回の戦略:精度向上の実験

さらなる精度向上を狙うにあたり、今回は「木の数(n_estimators)」を変更する実験を行います。検討の理由は以下の通りです。

  1. 木の深さ(max_depth)の限界:
    現在は特徴量が5項目と少なく、これ以上深く掘り下げても学習データへの過剰適合(過学習)を招くリスクが高いと判断しました。
  2. 多数決(合議制)の強化:
    判断材料が限られている現状では、個々の木の判断をより多くの「仲間」で補完し、予測のブレを最小限に抑える(多数決の精度を上げる)ほうが、スコア向上への確実な一手になると仮説を立てました。

3. 【実験】木の数を 100 → 500 へ増強

具体的には、`RandomForestClassifier` を定義している一行にある n_estimators という引数を書き換えます。これは「アンサンブル(合議制)に参加させる木の数」を指定するパラメータです。

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. 特徴量の選択(前回と同じ5項目)
features = ["Pclass", "Sex", "SibSp", "Parch", "Fare"]
X = pd.get_dummies(train_data[features])
y = train_data["Survived"]

X_test = pd.get_dummies(test_data[features])
X_test['Fare'] = X_test['Fare'].fillna(X_test['Fare'].median())

# 3. モデルの構築(★ここを修正!)
# 【変更前】 model = RandomForestClassifier(n_estimators=100, max_depth=5, random_state=1)
model = RandomForestClassifier(n_estimators=500, max_depth=5, random_state=1)

# 4. モデルの学習(書き換えた設定で再計算)
model.fit(X, y)

# 5. 予測の実行
predictions = model.predict(X_test)

# 6. 提出用ファイルの作成
output = pd.DataFrame({'PassengerId': test_data.PassengerId, 'Survived': predictions})
output.to_csv('submission_n500.csv', index=False)
print("Submission with 500 trees saved!")

修正のポイント:
`n_estimators=100` を `n_estimators=500` に変更しました。これにより、100本の多数決から500本の多数決へと「合議制」がパワーアップします。修正はこの1箇所のみ。極めて手間の少ない「次の一手」です。

4. 考察と実験結果

ここでエンジニアとして一つの疑問が湧きます。「教師データが約800件しかないのに、木の数を500本にするのは多すぎる(リソースの無駄)ではないか?」という点です。

結論から言うと、アンサンブル学習において「合議の数(木の数)」を増やすことは、計算コスト以外のデメリット(精度低下のリスク)はありません。それぞれの木が重複を許して抽出された異なるデータセットを学習するため、数を増やすほど予測は安定する方向に向かいます。

さて、注目の実験結果は以下の通りでした。

  • 前回(100本): Score 0.77511
  • 今回(500本): Score 0.77511(変化なし)

エンジニア的な結論:
残念ながら、今回のパラメータ変更だけではスコアはピクリとも動きませんでした。これは、現在の5つの特徴量から引き出せるパターンは、すでに100本の多数決で「出し尽くされている」ということを意味します。計算式をいじっても限界があることが証明されました。


仮説と検証の結果、次なる道が明確になりました。これ以上「既存のヒントの計算」を強化しても意味がありません。次は、いよいよ「Age(年齢)」という新しいヒント(特徴量)の投入、そしてそのための「欠損値処理」という、より高度な前処理へと進みます。