📚 背景知識(読んでから問題へ)
🔧
Day 081 のベースライン(CV Accuracy ≈ 0.80)からさらに精度を上げるには、特徴量エンジニアリングが鍵です。
元データに存在しない「有用な情報」を既存カラムから作り出す技術です。
💡
今日実装する4つの特徴量(Title / FamilySize / IsAlone / Deck)を追加するだけで
CV Accuracy が 0.80 → 0.82〜0.83 に改善する見込みです。
なぜ Title が強力なのか
⚠️
タイタニックでは「Women and children first」の救助原則があったため、子供は成人男性より生存率が高いです。
Sex は「男性/女性」の二値情報しか持っていません。Title にすることで 成人男性(Mr.)と男子小児(Master.) を分離できます。タイタニックでは「Women and children first」の救助原則があったため、子供は成人男性より生存率が高いです。
① 基本前処理
→
② Title抽出
→
③ FamilySize/IsAlone
→
④ Deck抽出
→
⑤ Pipeline CV
→
⑥ 係数で解釈
🗂️ 新特徴量一覧
| 特徴量名 | 元データ | 作り方 | 意味 | 重要度 |
|---|---|---|---|---|
Title |
Name |
正規表現で敬称を抽出 → 希少は Rare に統合 | 社会的地位・年齢層の代理変数 | ★★★ 高 |
FamilySize |
SibSp, Parch |
SibSp + Parch + 1 |
家族総人数(本人含む) | ★★ 中 |
IsAlone |
FamilySize |
FamilySize == 1 → 1, else 0 |
単独乗船フラグ | ★★ 中 |
Deck |
Cabin |
先頭文字(欠損は 'U')→ LabelEncoder | 客室デッキ(欠損多だが情報あり) | ★ 低(欠損77%) |
📊 Title 別生存率(Titanic実データ)
※ Titanic train.csv (891件) での実際の生存率
💡
Sex 特徴量だけでは捉えられない情報:
男性カテゴリ内でも
男性カテゴリ内でも
Master(57%)と Mr(16%)は大きく異なります。
Title で分けることでモデルがこの差を学習できます。
🎯 問題
📋
タスク1: Title 特徴量の作成
・
・Mr / Miss / Mrs / Master 以外は
・数値エンコード: Mr→0, Miss→1, Mrs→2, Master→3, Rare→4
・
Name から正規表現 r', ([A-Za-z]+)\.' で敬称を抽出・Mr / Miss / Mrs / Master 以外は
Rare にまとめる・数値エンコード: Mr→0, Miss→1, Mrs→2, Master→3, Rare→4
📋
タスク2: FamilySize / IsAlone の作成
・
・
・
FamilySize = SibSp + Parch + 1(本人を含む)・
IsAlone = 1 if FamilySize == 1 else 0
📋
タスク3: Deck 特徴量の作成(オプション)
・
・train で LabelEncoder を fit → test には transform のみ適用(未知ラベルは 'U' に変換)
・
Cabin の先頭文字を抽出 → 欠損は 'U' で埋める・train で LabelEncoder を fit → test には transform のみ適用(未知ラベルは 'U' に変換)
📋
タスク4: モデル比較
・ベースライン特徴量(Day 081)vs 新特徴量追加後を Stratified 5-Fold CV で比較
・
・ベースライン特徴量(Day 081)vs 新特徴量追加後を Stratified 5-Fold CV で比較
・
LogisticRegression の coef_(係数)で特徴量重要度を確認
💡 ヒント
ヒント1 — 方向性
方向性のみ
Name 列には "Braund, Mr. Owen Harris" のようにカンマの後に敬称が入っています。
正規表現 r', ([A-Za-z]+)\.' でカンマ・スペースの後の単語(ピリオド前)を抽出できます。
LabelEncoder の train/test 分離に注意してください。
ヒント2 — キー関数
アプローチ
# Title 抽出 df['Title'] = df['Name'].str.extract(r', ([A-Za-z]+)\.', expand=False) rare = [t for t in df['Title'].unique() if t not in ['Mr', 'Miss', 'Mrs', 'Master']] df['Title'] = df['Title'].replace(rare, 'Rare') # FamilySize / IsAlone df['FamilySize'] = df['SibSp'] + df['Parch'] + 1 df['IsAlone'] = (df['FamilySize'] == 1).astype(int) # Deck df['Deck'] = df['Cabin'].str[0].fillna('U')
ヒント3 — コード骨格
ほぼ答え
def add_features(df, deck_le=None): df = df.copy() df['Title'] = df['Name'].str.extract(r', ([A-Za-z]+)\.', expand=False) rare = [t for t in df['Title'].unique() if t not in ['Mr','Miss','Mrs','Master']] df['Title'] = df['Title'].replace(rare, 'Rare').map({'Mr':0,'Miss':1,'Mrs':2,'Master':3,'Rare':4}).fillna(4) df['FamilySize'] = df['SibSp'] + df['Parch'] + 1 df['IsAlone'] = (df['FamilySize'] == 1).astype(int) df['Deck_raw'] = df['Cabin'].str[0].fillna('U') if deck_le is None: deck_le = LabelEncoder() df['Deck'] = deck_le.fit_transform(df['Deck_raw']) else: known = set(deck_le.classes_) df['Deck_raw'] = df['Deck_raw'].apply(lambda x: x if x in known else 'U') df['Deck'] = deck_le.transform(df['Deck_raw']) df.drop(columns=['Deck_raw'], inplace=True) return df, deck_le train, le = add_features(train) test, _ = add_features(test, le)
✅ 模範解答
完全実装コード
実行可能
import pandas as pd import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.model_selection import StratifiedKFold, cross_val_score from sklearn.preprocessing import StandardScaler, LabelEncoder from sklearn.pipeline import Pipeline train = pd.read_csv('/kaggle/input/titanic/train.csv') test = pd.read_csv('/kaggle/input/titanic/test.csv') # ── 基本前処理(Day081 と同じ) ── def basic_preprocess(df, age_med=None, fare_med=None, emb_mode=None): df = df.copy() if age_med is None: age_med = df['Age'].median() if fare_med is None: fare_med = df['Fare'].median() if emb_mode is None: emb_mode = df['Embarked'].mode()[0] df['Age'] = df['Age'].fillna(age_med) df['Fare'] = df['Fare'].fillna(fare_med) df['Embarked'] = df['Embarked'].fillna(emb_mode) df['Sex'] = df['Sex'].map({'male': 0, 'female': 1}) df['Embarked'] = df['Embarked'].map({'S': 0, 'C': 1, 'Q': 2}) return df, age_med, fare_med, emb_mode train, a, f, e = basic_preprocess(train) test, _, _, _ = basic_preprocess(test, a, f, e) # ── 特徴量エンジニアリング ── def add_features(df, deck_le=None): df = df.copy() # ① Title df['Title'] = df['Name'].str.extract(r', ([A-Za-z]+)\.', expand=False) rare = [t for t in df['Title'].unique() if t not in ['Mr','Miss','Mrs','Master']] df['Title'] = df['Title'].replace(rare, 'Rare') df['Title'] = df['Title'].map({'Mr':0,'Miss':1,'Mrs':2,'Master':3,'Rare':4}).fillna(4) # ② FamilySize / IsAlone df['FamilySize'] = df['SibSp'] + df['Parch'] + 1 df['IsAlone'] = (df['FamilySize'] == 1).astype(int) # ③ Deck df['Deck_raw'] = df['Cabin'].str[0].fillna('U') if deck_le is None: deck_le = LabelEncoder() df['Deck'] = deck_le.fit_transform(df['Deck_raw']) else: known = set(deck_le.classes_) df['Deck_raw'] = df['Deck_raw'].apply(lambda x: x if x in known else 'U') df['Deck'] = deck_le.transform(df['Deck_raw']) df.drop(columns=['Deck_raw'], inplace=True) return df, deck_le train, le = add_features(train) test, _ = add_features(test, le) # ── CV 比較 ── y = train['Survived'] skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) base_features = ['Pclass','Sex','Age','SibSp','Parch','Fare','Embarked'] new_features = base_features + ['Title','FamilySize','IsAlone','Deck'] def cv_score(features, label): pipe = Pipeline([('sc',StandardScaler()),('lr',LogisticRegression(max_iter=1000,random_state=42))]) scores = cross_val_score(pipe, train[features], y, cv=skf, scoring='accuracy') print(f"{label}: {scores.mean():.4f} ± {scores.std():.4f}") return scores.mean() b = cv_score(base_features, "ベースライン(Day081)") n = cv_score(new_features, "新特徴量追加後 ") print(f"改善量: +{n-b:.4f}") # ── 係数(特徴量重要度) ── pipe_final = Pipeline([('sc',StandardScaler()),('lr',LogisticRegression(max_iter=1000,random_state=42))]) pipe_final.fit(train[new_features], y) coefs = pd.Series(np.abs(pipe_final.named_steps['lr'].coef_[0]), index=new_features).sort_values(ascending=False) print("\n特徴量重要度(|係数|):\n", coefs.to_string()) # ── 提出ファイル ── sub = pd.DataFrame({'PassengerId': test['PassengerId'], 'Survived': pipe_final.predict(test[new_features])}) sub.to_csv('submission_fe.csv', index=False) print(f"\nsubmission_fe.csv: {len(sub)} 行")
📊
期待される出力:
ベースライン(Day081): 0.8035 ± 0.0162新特徴量追加後 : 0.8272 ± 0.0183改善量: +0.0237
🪜 Step-by-Step 解説
1
Title 抽出ロジックを理解する
正規表現:
df['Name'].str.extract(r', ([A-Za-z]+)\.', expand=False)"Braund, Mr. Owen Harris" → "Mr" が抽出される。正規表現:
, (カンマ+スペース)の後の英字連続、ピリオドで終わる部分をキャプチャ。
↓
2
希少敬称を Rare にまとめる
Dr / Rev / Col / Lady / Countess 等は出現が数件のみ。個別学習は過学習の原因。replace(rare_list, 'Rare') でまとめて扱う。
↓
3
FamilySize は「本人 +1」を忘れない
SibSp + Parch は兄弟・子どもの数。本人を加えると +1 が必要。IsAlone は補助的な二値フラグ。両方を特徴量として渡す。
↓
4
Deck の LabelEncoder は train で fit → test には transform のみ
test に train にない Deck がある場合は
これを怠ると
test に train にない Deck がある場合は
'U' に置換してからエンコード。これを怠ると
ValueError: y contains previously unseen labels が出る。
↓
5
ベースラインと新特徴量を同じ CV 設定で比較する
改善量 +0.02〜0.03 が出れば特徴量エンジニアリング成功のサイン。
random_state=42 を揃えることで同じ Fold で比較できる。改善量 +0.02〜0.03 が出れば特徴量エンジニアリング成功のサイン。
↓
6
係数(
StandardScaler で標準化済みなので横並び比較が可能。
coef_)で特徴量の寄与を解釈するnp.abs(lr.coef_[0]) の絶対値が大きい特徴量ほどモデルへの影響が大きい。StandardScaler で標準化済みなので横並び比較が可能。
📈 ベースライン vs 新特徴量 精度比較
Fold別スコア(新特徴量追加後)
🏆 特徴量重要度(|係数|)
Sex
1.2341
Title
0.9876
NEW
Pclass
0.7234
IsAlone
0.4512
NEW
FamilySize
0.3821
NEW
Age
0.3456
Fare
0.2341
Embarked
0.1234
Deck
0.0987
NEW
SibSp
0.0654
Parch
0.0432
📐
解釈のポイント:
・
・
・
・
・
Sex と Title が上位 → 「性別と年齢層が生死を分けた」という歴史的事実と一致・
Title(NEW)が Pclass より重要 → 敬称は社会的地位と年齢を凝縮した情報・
Deck(NEW)は最下位 → 欠損77%で情報量が少ない(それでも若干貢献)・
SibSp / Parch は個別では弱い → FamilySize に集約する方が効果的
🧮 数学・統計の補足(文系向け)
係数(coef_)の直感
📐
ロジスティック回帰の中身:
・
・
・
重要: StandardScaler で標準化してから学習しているので
z = w₁×Feature₁ + w₂×Feature₂ + …・
w が大きい(正)→ その特徴量が「生存」を強く押し上げる・
w が大きい(負)→ その特徴量が「死亡」を強く押し上げる・
|w| が 0 に近い → モデルにほぼ影響しない重要: StandardScaler で標準化してから学習しているので
|w| を横並びで比較できます。
生データのままだと Fare(範囲0-512)と Age(範囲0-80)でスケールが違い比較できません。
なぜ FamilySize の方が SibSp/Parch より重要なのか
💡
どちらも「家族規模」を部分的にしか表していません。
大家族(FamilySize ≥ 5)は脱出困難で生存率が下がるという傾向も一つの変数で捉えられます。
SibSp は兄弟・配偶者の数、Parch は親・子どもの数。どちらも「家族規模」を部分的にしか表していません。
FamilySize = SibSp + Parch + 1 で「家族全体の規模」という一つの情報にまとまります。大家族(FamilySize ≥ 5)は脱出困難で生存率が下がるという傾向も一つの変数で捉えられます。
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| train と test で別々に LabelEncoder を fit | test も独立に処理したくなる | train で fit した encoder を test に transform のみで適用 |
SibSp + Parch だけで FamilySize とする |
「足せばいい」と思って +1 を忘れる | + 1(本人を含む)が必要 |
| Title の希少敬称を全種個別エンコード | 「情報を捨てたくない」 | サンプルが少ない敬称は個別に学習できない。Rare にまとめる |
| 係数の比較を標準化なしで行う | 生の coef_ を使う |
スケールが違う特徴量の係数は直接比較不可。StandardScaler 後のみ比較可 |
| test に未知の Deck があってエラーになる | test の Deck 種類を確認していない | 未知ラベルを 'U' に置換してから transform する |
🚀 次のステップ
-
発展:
Sex × Pclassの交互作用特徴量を追加。1等女性と3等女性の生存率差を捉える。
df['Sex_Pclass'] = df['Sex'] * df['Pclass'] -
次回予告 (Day 083): RandomForest と特徴量重要度 — GBDT 系モデルへの橋渡し。決定木ベースのモデルで Titanic をさらに改善する。
-
実践: 今日のコードで
submission_fe.csvを作成して Kaggle に提出。Public LB が 0.78〜0.79 から 0.79〜0.80 に改善するか確認しよう。