ステップ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"])
✅ こう出れば成功
| FamilySize | mean(生存率) | count(人数) |
|---|---|---|
| 1(一人) | 0.30 | 537 |
| 2 | 0.55 | 161 |
| 3 | 0.58 | 102 |
| 4 | 0.72 | 29 |
| 5 | 0.20 | 15 |
| 6 | 0.14 | 22 |
| 7 | 0.33 | 12 |
| 8 | 0.00 | 6 |
| 11 | 0.00 | 7 |
🎯 はっきりした傾向が出ました。
- 一人客(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 などは数人ずつしかいません。そのまま使うと、モデルがその数人を丸暗記してしまい、過学習します。少数のカテゴリはまとめるのが定石です。
✅ ここまでできたら
おつかれさまでした。残りは参考資料です。