はじめに
前回は,ロジスティック回帰を用いてTitanicの生存の判定を行いました.
記述子としてPclass(客室の等級),Age(年齢),SibSp(兄弟・配偶者の数),Parch(親・子の数),Fare(運賃),Sex_male(性別:男性なら1、女性なら0)の6つを用いました.
結果はaccuracyが0.76315と,性別だけによる判断(約0.78)よりも低い値でした.
今回は特徴量エンジニアリングによってその数値の改善に取り組んでいきたいと思います.
具体的には,影響の小さい特徴量を削減,新しい特徴量を作成の二つを行いました.
データ読み込み,前処理
ライブラリのインポート,不要な列の削除,欠損値処理,カテゴリ変数を数値化,を行います.(前回と一緒)また,Nameはいったん落としますが,後ほどTitle(敬称)の抽出に使います.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, classification_report
# データ読み込み
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')
# 目的変数と特徴量を分離
y_train = train['Survived']
X_train = train.drop('Survived', axis=1)
X_test = test.copy()
test_ids = test['PassengerId']
# --- 前処理(前回と同じ) ---
# 不要な列を落とす
cols_to_drop = ['PassengerId', 'Name', 'Ticket', 'Cabin', 'Embarked']
X_train = X_train.drop(cols_to_drop, axis=1, errors='ignore')
X_test = X_test.drop(cols_to_drop, axis=1, errors='ignore')
# 欠損値処理(中央値で埋める)
X_train['Age'] = X_train['Age'].fillna(X_train['Age'].median())
X_test['Age'] = X_test['Age'].fillna(X_test['Age'].median())
X_train['Fare'] = X_train['Fare'].fillna(X_train['Fare'].median())
X_test['Fare'] = X_test['Fare'].fillna(X_test['Fare'].median())
# カテゴリ変数を数値化
X_train = pd.get_dummies(X_train, columns=['Sex'], drop_first=True)
X_test = pd.get_dummies(X_test, columns=['Sex'], drop_first=True)
1. 影響の小さい特徴量を削減
まずは前回も作成した特徴量のヒートマップ(ピアソン相関)をみてみます.
また,ロジスティック回帰のモデルの係数も計算してみます.
相関とモデルの係数の違いはあまりまだしっくりきていないのですが,ざっくりとした違いは以下の通りです.
- 相関:その特徴量だけを見たとき、Survived とどれくらい連動するか
- 係数:他の特徴量の影響を差し引いた後、その特徴量が予測にどれだけ効いているか
前回,不要な特徴量を削減してSexを数値に置き換えたデータフレームX_trainにsurvivedを足してdf_corrとして,これに対してヒートマップを作成します.
df_corr = X_train.copy()
df_corr["Survived"] = y_train
sns.heatmap(df_corr.corr(), annot=True, cmap='coolwarm',center=0, fmt='.2f')
plt.title('Correlation Matrix')
plt.tight_layout()
# 係数を確認するためモデルを作成
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
model = LogisticRegression(random_state=42, max_iter=1000)
model.fit(X_train_scaled, y_train)
print("Model Coefficients:")
for name, coef in zip(X_train.columns, model.coef_[0]):
print(f" {name}: {coef:.4f}")
これらの結果から以下のことが考察できます.
- Sex_male が最大の影響(相関 -0.54、係数 -1.24)
- Pclass が2番目(相関 -0.34、係数 -0.77)→「裕福な人ほど生存率が高かった」
- PclassとFareが-0.55で共線性あり。モデル係数もPclass(-0.77) >> Fare(0.18)なので、Fareを削る候補
- SibSpとParchが0.41でやや高い相関 → FamilySizeに統合できる?
- Age, SibSp, Parchは対Survivedの相関が|r| < 0.1だが、係数ではAge(-0.44), SibSp(-0.32)とそれなりに効いている → 非線形な効果がある?
まずは,特徴量の削減を行い,モデルを作成します.
先ほど考察したように,ヒートマップより共線性があり(-0.55),かつPclassより係数が小さなFareを落とします.
# === 改善1: Fareを削除 ===
X_train_v2 = X_train.drop('Fare', axis=1)
X_test_v2 = X_test.drop('Fare', axis=1)
scaler_v2 = StandardScaler()
X_train_v2_scaled = scaler_v2.fit_transform(X_train_v2)
cv_scores_v2 = cross_val_score(
LogisticRegression(random_state=42, max_iter=1000),
X_train_v2_scaled, y_train, cv=5, scoring='accuracy'
)
print("=== v2: Drop Fare ===")
print(f"Mean: {cv_scores_v2.mean():.4f}, Std: {cv_scores_v2.std():.4f}")

前回(Mean: 0.7856, Std: 0.0223)と比較しても,改善は見られませんでした.
ですが,結果が同じなら特徴量の数は少ない方が解釈しやすいので,このまま次に進めたいと思います.
2. 新しい特徴量を作成
SibSp(同乗している兄弟・配偶者の数)とParch(同乗している親・子供の数)はヒートマップからも相関があり(0.41),かつ意味的にも統合できそうだとわかりました.
FamilySize = SibSp + Parch + 1(+1は自分):一緒に乗っている家族の合計人数。
また、IsAlone = FamilySize が 1 なら 1、それ以外は 0 という特徴量も作ります。
「一人で乗船しているかどうか」を表す二値変数です.
一人旅だと助けてくれる家族がいないので、生存率に影響すると考えられます.
# === 改善2: FamilySize + IsAlone ===
X_train_v3 = X_train_v2.copy()
X_test_v3 = X_test_v2.copy()
X_train_v3['FamilySize'] = X_train_v3['SibSp'] + X_train_v3['Parch'] + 1
X_train_v3['IsAlone'] = (X_train_v3['FamilySize'] == 1).astype(int)
X_train_v3 = X_train_v3.drop(['SibSp', 'Parch'], axis=1)
X_test_v3['FamilySize'] = X_test_v3['SibSp'] + X_test_v3['Parch'] + 1
X_test_v3['IsAlone'] = (X_test_v3['FamilySize'] == 1).astype(int)
X_test_v3 = X_test_v3.drop(['SibSp', 'Parch'], axis=1)
#標準化
scaler_v3 = StandardScaler()
X_train_v3_scaled = scaler_v3.fit_transform(X_train_v3)
#交差検証
cv_scores_v3 = cross_val_score(
LogisticRegression(random_state=42, max_iter=1000),
X_train_v3_scaled, y_train, cv=5, scoring='accuracy'
)
print("=== v3: Drop Fare + FamilySize/IsAlone ===")
print(f"Mean: {cv_scores_v3.mean():.4f}, Std: {cv_scores_v3.std():.4f}")

若干の改善が見られました.
次に,KDEで見られた0-10歳の高い生存率を拾えるような特徴量を作成します.
改めて,ageのKDEプロットを作成します.
for val in [0, 1]:
train[train['Survived'] == val]['Age'].dropna().plot.kde(label=val)
plt.title('Age by Survival')
plt.legend()
plt.show()

1(生存)では,0-10の部分に明確に山があることがわかります.
IsChildという列を作り,10歳未満なら1,それ以外は0という変数を作ります.
# === 改善3: IsChildフラグ ===
X_train_v4 = X_train_v3.copy()
X_test_v4 = X_test_v3.copy()
X_train_v4['IsChild'] = (X_train_v4['Age'] < 10).astype(int)
X_test_v4['IsChild'] = (X_test_v4['Age'] < 10).astype(int)
scaler_v4 = StandardScaler()
X_train_v4_scaled = scaler_v4.fit_transform(X_train_v4)
cv_scores_v4 = cross_val_score(
LogisticRegression(random_state=42, max_iter=1000),
X_train_v4_scaled, y_train, cv=5, scoring='accuracy'
)
print("=== v4: + IsChild ===")
print(f"Mean: {cv_scores_v4.mean():.4f}, Std: {cv_scores_v4.std():.4f}")

また少し改善が見られました.0.8を超えることができました.
次に,Name列から敬称(Title)を抽出して特徴量を作成します.
Titleは性別・年齢層・社会的地位を1語に圧縮した情報を含んでいるというドメイン知識から,生存率と強い相関があると考えられるからです.
これは正直今は自力で考えつかないなと思いますが,こういうのを考えるのが機械学習の楽しさであり腕の見せ所なんだなと思います.
方針:Mr(成人男性)、Miss(未婚女性)、Mrs(既婚女性)、Master(少年)の4つに分類し、それ以外のレアなTitle(Dr, Rev等)はOtherにまとめた上で、one-hot encodingで数値化します.
# === 改善4: Title抽出 ===
# Nameからtitleを取り出す必要があるので、元のtrainから取得
X_train_v5 = X_train_v4.copy()
X_test_v5 = X_test_v4.copy()
X_train_v5['Title'] = train['Name'].str.extract(r' ([A-Za-z]+)\.', expand=False)
X_test_v5['Title'] = test['Name'].str.extract(r' ([A-Za-z]+)\.', expand=False)
# レアなtitleをまとめる
title_map = {
'Mr': 'Mr', 'Miss': 'Miss', 'Mrs': 'Mrs', 'Master': 'Master',
}
X_train_v5['Title'] = X_train_v5['Title'].map(lambda x: title_map.get(x, 'Other'))
X_test_v5['Title'] = X_test_v5['Title'].map(lambda x: title_map.get(x, 'Other'))
X_train_v5 = pd.get_dummies(X_train_v5, columns=['Title'], drop_first=True)
X_test_v5 = pd.get_dummies(X_test_v5, columns=['Title'], drop_first=True)
scaler_v5 = StandardScaler()
X_train_v5_scaled = scaler_v5.fit_transform(X_train_v5)
cv_scores_v5 = cross_val_score(
LogisticRegression(random_state=42, max_iter=1000),
X_train_v5_scaled, y_train, cv=5, scoring='accuracy'
)
print("=== v5: + Title ===")
print(f"Mean: {cv_scores_v5.mean():.4f}, Std: {cv_scores_v5.std():.4f}")

さらに改善が見られました.とりあえずここまでで再度提出してみます.
X_test_v5_scaled = scaler_v5.transform(X_test_v5)
model.fit(X_train_v5_scaled, y_train)
y_pred_test = model.predict(X_test_v5_scaled)
submission = pd.DataFrame({
'PassengerId': test_ids,
'Survived': y_pred_test
})
submission.to_csv('submission_2.csv', index=False)
今回の結果は0.77511と,前回よりも高いですが,思った以上には改善されませんでした.
まとめ
今回やったことを以下のTableにまとめます.
| バージョン | 変更内容 | CV accuracy |
|---|---|---|
| v1(前回) | 6特徴量 + ロジスティック回帰 | 0.7856 |
| v2 | Fare を削除 | 0.7856 |
| v3 | FamilySize / IsAlone に統合 | 0.7957 |
| v4 | IsChild フラグ追加 | 0.8013 |
| v5 | Title 抽出 | 0.8114 |
効果あり
- FamilySize統合 + IsAlone
- IsChildフラグ:子供の高生存率を拾えた
- Title抽出:性別×年齢×地位を1変数に圧縮.最も改善幅が大きい
効果なし
- Fareの削除:他の処理の際にFareがあっても,効果はほぼなかったので,消したままにしました.
まだ検討できそうなことはありそうですが(自分じゃ思いつかないですがAIが提案してくれた),今回は特徴量エンジニアリングはここまでとして,次回以降はモデルの変更を行いたいと思います.

