ステップ11:自分で特徴量を作って伸ばす

🎯 このステップでやることデータにない「新しい列」を自分で作り、CVが上がるか確かめる。所要時間:約10〜15分。この章のクライマックスです。


考え方

いまモデルが持っている材料は、主催者がくれた列そのままです。

Pclass, Sex, Age, SibSp, Parch, Fare, Embarked

ここに、人間が考えた新しい列を足します。これを 特徴量エンジニアリング と呼びます。ステップ10で見た通り、ここがスコアに最も効きます。

今回は、2つの仮説を立てて試します。

仮説作る列
① 大家族は逃げ遅れ、一人客は身軽だったのでは?家族の人数
② 名前の敬称(Mr/Mrs/Miss/Master)には年齢と性別と身分の情報が詰まっているのでは?敬称

11-1. 仮説①:家族の人数

まず、作る前に 仮説が正しいか確認します。

📋 コード

tmp = train.copy()
tmp["FamilySize"] = tmp["SibSp"] + tmp["Parch"] + 1   # 本人を含めた家族の人数

tmp.groupby("FamilySize")["Survived"].agg(["mean", "count"])

✅ こう出れば成功

FamilySizemean(生存率)count(人数)
1(一人)0.30537
20.55161
30.58102
40.7229
50.2015
60.1422
70.3312
80.006
110.007

🎯 はっきりした傾向が出ました。

  • 一人客(537人)の生存率は30%と低い
  • 2〜4人の家族は55〜72%と高い
  • 5人以上の大家族は急落する

「SibSp」と「Parch」を足すだけで、こんな構造が見えました。これが特徴量エンジニアリングです。難しい数学は一切使っていません。

11-2. 仮説②:名前の敬称

名前は Braund, Mr. Owen Harris という形をしています。この Mr. の部分を取り出します。

📋 コード

tmp["Title"] = tmp["Name"].str.extract(r" ([A-Za-z]+)\.")

print(tmp["Title"].value_counts())
print()
print(tmp.groupby("Title")["Survived"].agg(["mean", "count"]).sort_values("count", ascending=False).head(8))

✅ こう出れば成功

Mr        517
Miss      182
Mrs       125
Master     40
Dr          7
Rev         6
...

生存率はだいたいこうなります。

Title意味生存率
Mr成人男性約16%
Miss未婚女性約70%
Mrs既婚女性約79%
Master少年(当時の男児への敬称)約57%
Rev牧師0%

🎯 注目は Master です。男性全体の生存率は19%なのに、少年(Master)は57%。つまり「女性と子どもを優先」の"子ども"の部分が、名前から取り出せたのです。

しかも、Ageが空欄の人でも敬称はわかります。177人の年齢不明者にも効きます。

11-3. 新しい特徴量を入れて作り直す

仮説が確認できたので、本番のコードに組み込みます。

📋 コード(ステップ7の prepare を強化した版。そのまま貼り付けてください)

age_median  = train["Age"].median()
fare_median = train["Fare"].median()

def prepare2(df):
    df = df.copy()

    # --- もとからある列の処理 ---
    df["Sex"] = df["Sex"].map({"male": 0, "female": 1})
    df["Embarked"] = df["Embarked"].fillna("S").map({"S": 0, "C": 1, "Q": 2})
    df["Age"]  = df["Age"].fillna(age_median)
    df["Fare"] = df["Fare"].fillna(fare_median)

    # --- 仮説①:家族の人数 ---
    df["FamilySize"] = df["SibSp"] + df["Parch"] + 1
    df["IsAlone"] = (df["FamilySize"] == 1).astype(int)

    # --- 仮説②:敬称 ---
    t = df["Name"].str.extract(r" ([A-Za-z]+)\.")[0]
    t = t.replace(["Mlle", "Ms"], "Miss").replace(["Mme"], "Mrs")
    t = t.where(t.isin(["Mr", "Miss", "Mrs", "Master"]), "Rare")
    df["Title"] = t.map({"Mr": 0, "Miss": 1, "Mrs": 2, "Master": 3, "Rare": 4})

    return df


features2 = ["Pclass", "Sex", "Age", "SibSp", "Parch", "Fare", "Embarked",
             "FamilySize", "IsAlone", "Title"]

X2      = prepare2(train)[features2]
X_test2 = prepare2(test)[features2]

print("空欄:", X2.isnull().sum().sum(), X_test2.isnull().sum().sum())
X2.head()

✅ こう出れば成功空欄が 0 0 と表示され、FamilySize / IsAlone / Title の3列が増えた表が出ます。

⚠️ うまくいかないときは

  • prepare2(train) に渡しているのは train 全体 です(train[features] ではありません)。Name列が必要なためです。
  • 空欄が0にならない → Title の .where(...) の行が抜けていないか確認してください。

11-4. CVで効果を測る

📋 コード(before / after を並べて比較)

from sklearn.model_selection import cross_val_score

model = RandomForestClassifier(n_estimators=300, max_depth=6, random_state=0)

before = cross_val_score(model, X,  y, cv=5, scoring="accuracy").mean()
after  = cross_val_score(model, X2, y, cv=5, scoring="accuracy").mean()

print(f"追加前のCV: {before:.4f}")
print(f"追加後のCV: {after:.4f}")
print(f"差       : {after - before:+.4f}")

✅ こう出れば成功

追加前のCV: 0.8238
追加後のCV: 0.8350 前後
差       : +0.011 前後

🎉 上がりました。 数値は環境で変わりますが、+0.005〜+0.02 程度の改善が出るはずです。

たった3列足しただけです。モデルは何も変えていません。これが「特徴量 ≫ モデル」の意味です。

⚠️ もし下がった場合も、それは失敗ではありません。「この特徴量は効かなかった」という情報が得られたのです。記録して次に行きます。

11-5. 重要度を見る

📋 コード

model.fit(X2, y)

pd.DataFrame({
    "特徴量": X2.columns,
    "重要度": model.feature_importances_
}).sort_values("重要度", ascending=False)

✅ こう出れば成功 Title が Sex に並ぶくらい上位に来ているはずです。自分で作った列が、モデルに最も使われている。気持ちのいい瞬間です。

11-6. 提出する(3回目)

📋 コード

model = RandomForestClassifier(n_estimators=300, max_depth=6, random_state=0)
model.fit(X2, y)
pred = model.predict(X_test2)

submission = pd.DataFrame({
    "PassengerId": test["PassengerId"],
    "Survived": pred
})
submission.to_csv("submission.csv", index=False)

print("CV:", cross_val_score(model, X2, y, cv=5).mean().round(4))
print(submission.shape)

そして Save Version → Save & Run All → Submit。

✅ 期待されるスコア:0.77〜0.80

11-7. 自分で次の仮説を立ててみる

ここからが本番です。あなた自身の仮説を試してください。ヒントを置いておきます。

仮説作り方のヒント
子どもかどうかが効くのではdf["IsChild"] = (df["Age"] < 10).astype(int)
運賃は歪んでいるので対数を取るdf["FareLog"] = np.log1p(df["Fare"])
客室番号の頭文字(デッキ)が効くのではdf["Deck"] = df["Cabin"].fillna("U").str[0] → 数値に変換
同じチケット番号=同行者ではdf["TicketCount"] = df.groupby("Ticket")["Ticket"].transform("count")
年齢を区切ると効くのではpd.cut(df["Age"], bins=[0,12,20,40,60,100], labels=False)

手順は毎回同じです。

① 仮説を立てる
② groupby で本当にそうか確かめる
③ prepare2() に列を足す
④ CVで before / after を比べる
⑤ 上がったら採用、下がったら捨てる + 記録する

このループを回すことが、Kaggleそのものです。


💡 何をしたのか(読み飛ばし可)

コード意味
.agg(["mean", "count"])平均と件数を同時に出す(件数の確認は重要)
.str.extract(r" ([A-Za-z]+)\.")正規表現で「スペース+英字+ピリオド」を抜き出す
.replace([...], "...")表記ゆれをまとめる(Mlle→Miss など)
.where(条件, 置換値)条件を満たさないものを置き換える(少数派を Rare に)
(条件).astype(int)True/False を 1/0 に変換

なぜ件数(count)も見るのか

生存率100%の特徴量が見つかっても、該当者が3人だけならただの偶然です。「割合」と「件数」は必ずセットで見る。これは一生使える習慣です。

なぜ少数派を「Rare」にまとめたのか

Dr Rev Col Countess などは数人ずつしかいません。そのまま使うと、モデルがその数人を丸暗記してしまい、過学習します。少数のカテゴリはまとめるのが定石です。


✅ ここまでできたら

おつかれさまでした。残りは参考資料です。