1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Titanicで機械学習入門①:EDAからロジスティック回帰でモデル作成

1
Last updated at Posted at 2026-09-01

はじめに

機械学習の勉強のために,Kaggleの代表的なデータセットのTitanicを使って今回はLogistic回帰により生存予測をしたいと思います.
AIに教えてもらいながら進めました.

Titanicデータセットとは

Kaggleが提供する入門用コンペティションのデータセットで、1912年のタイタニック号沈没事故における乗客の情報から生存者を予測する分類問題です。

ライブラリのインポート,データの読み込み

まずはライブラリをインポートします.

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
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report

データを読み込みこんで,データフレームをつくります.
まずはKaggleからデータをダウンロードします。コンペページの「Data」タブから手動でダウンロードします.
作業ディレクトリにtrain.csvtest.csvが配置されていることを確認したら、ライブラリをインポートしてデータを読み込みます。

train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')

データの確認

データがどんな感じなのかを確認します.

train.head()

image.png

test.head()

image.png

print(f"train: {train.shape}")
print(f"test: {test.shape}")

image.png

Survived列の0,1が生存の有無だとわかります.trainの方にだけtargetとなるSurvived列があると言うことがわかりました.
他のカラムの意味は以下の通りです。

カラム名 説明
PassengerId 乗客ID
Survived 生存(1)/ 死亡(0)※trainのみ
Pclass 客室の等級(1=上級, 2=中級, 3=下級)
Name 氏名
Sex 性別
Age 年齢
SibSp 同乗している兄弟・配偶者の数
Parch 同乗している親・子供の数
Ticket チケット番号
Fare 運賃
Cabin 客室番号
Embarked 乗船港(C=Cherbourg, Q=Queenstown, S=Southampton)

次に欠損値を確認します.

missing_train = train.isnull().sum()
missing_test = test.isnull().sum()
missing_df = pd.DataFrame({
    'train': missing_train,
    'test': missing_test
})
print(missing_df[missing_df.sum(axis=1) > 0])  # 欠損がある列だけ表示

image.png

trainはCabinがかなりの割合で欠損なので,この変数は使用しないでいいかなと言う判断ができます.
(Cabinに数値があること自体が情報になることを活かしたアプローチもあるみたいですが,今回は削る方向で行きます.)
AgeとEmbarkedは後ほど欠損値を埋める操作を行います.

.describe()で,数値列の統計を確認します.
また,数値だけで判断するのがまだ慣れていないので,特徴量の分布を図示してみます.

print(train.describe())

image.png

#特徴量のヒストグラム作成
train.hist(figsize=(12, 12), bins=30, layout=(4, 4))
plt.tight_layout()
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3756897/9f0fe172-9425-479a-a4bb-37474de94f01.png)

image.png

ここから以下のようなことが考えられます.

  • Passengerid:ただの数字なので無視
  • survived:0(死亡)の方が多い
  • Pclass:3等客が多い
  • Age:割と全体的に分布.0-5歳に塊があるので,そこに着目するのもありか.
  • SibSp/Parchは0が多い.連続値として扱うよりカテゴリ化するか,他と合算する方がいいかも
  • Fare:極端に分布.左が一番高くて,右に裾がある.→log変換が有効と考えられますが,今回はしません

次に,TargetであるSurvive列の構成を見てみました.

print(train['Survived'].value_counts())
print(f"Survival rate: {train['Survived'].mean():.3f}")

先ほどのヒストグラムでもわかりましたが,targetは0(死亡)549,1(生存)342とより正確な値がわかりました.

次に,targetとのカテゴリ変数の分布の一覧プロットを作成します.
各特徴量との関係性を見れば,データの全体像が把握しやすく,アプローチも思いつくきっかけになるかもしれません.

cat_cols = ['Pclass', 'Sex', 'Embarked', 'SibSp', 'Parch']

fig, axes = plt.subplots(1, len(cat_cols), figsize=(4 * len(cat_cols), 4))

for ax, col in zip(axes, cat_cols):
    sns.barplot(x=col, y='Survived', data=train, ax=ax, hue=col)
    ax.set_title(f'Survival by {col}')

plt.tight_layout()
plt.show()

image.png

  • Sex:女性が生存する確率が高いことがわかり,単純に「女性なら生存」というベースラインが作れそうです.
  • Pclass:1 > 2 > 3となるので,客室の位が高いほど,生き残る可能性がありそう

targetとの連続変数の生存率分布についてもプロットしてみます.KDEプロットという,面積の合計が1になるように正規化されたものを作成し,相対的にどの区間にデータが集中しているかがわかります.

# 連続変数:生存/死亡別KDE
cont_cols = ['Age', 'Fare']
fig, axes = plt.subplots(1, len(cont_cols), figsize=(6 * len(cont_cols), 4))
for ax, col in zip(axes, cont_cols):
    for val in [0, 1]:
        train[train['Survived'] == val][col].dropna().plot.kde(ax=ax, label=f'Survived={val}')
    ax.set_title(f'{col} by Survival')
    ax.legend()
plt.tight_layout()
plt.show()

image.png
Ageを見ると,0-10歳付近で明確に1が上回っています.子供が優先救助という背景が反映されていることがわかります.年齢で特徴量を分けるアプローチが有効かもしれない,と判断できます.

次に,特徴量のヒートマップ(ピアソン相関)を作成して,特徴量間の共線性(多重共線性)と目的変数との線形関係の強さを確認します.
目的変数との相関が極端に低い特徴量は予測に寄与しにくいため削る候補とし,特徴量同士の相関が高い組(例:|r| > 0.5)があれば,片方を削るか統合する,という判断をしたいです.

numeric_cols = train.select_dtypes(include=[np.number]).columns
sns.heatmap(train[numeric_cols].corr(), annot=True, cmap='coolwarm',center=0, fmt='.2f')
plt.title('Correlation Matrix')
plt.tight_layout()

image.png

  • Survived との相関は Pclass(-0.34) と Fare(0.26) が最も強く、この2変数が線形モデルにおける主要な説明変数となりそう.
  • Pclass と Fare の相関が -0.55 と強く、共線性があるので片方を削ってもいいかもしれません.
  • SibSp と Parch の相関が 0.41 とやや高い。家族構成という同じ概念を二重に持っているため統合してもいいかも

前処理,モデル作成(ロジスティック回帰)

全体像が把握できたので,一回モデルを作ってみたいと思います.
.copy()としたのは,独立したコピーとして作成して元のtestを壊さないための安全策.

# 目的変数と特徴量を分離
y_train = train['Survived']
X_train = train.drop('Survived', axis=1)
X_test = test.copy()

# test の PassengerId を保存(提出用)
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')

# --- 欠損値処理(AgeとFareを中央値でうめる) ---
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())

# --- カテゴリ変数を数値化 (Sex→0,1に変換)---
X_train = pd.get_dummies(X_train, columns=['Sex'], drop_first=True)
X_test = pd.get_dummies(X_test, columns=['Sex'], drop_first=True)

print("=== After Preprocessing ===")
print(f"Features: {list(X_train.columns)}")
print(f"Shape: {X_train.shape}")
print(f"Missing: {X_train.isnull().sum().sum()}")  # 0 であるべき

特徴量を絞り込んで,欠損値がないことがわかりました.

  • NameのMr, Miss, Masterなどが良い特徴量となるらしいのですが,そんなアイデアは最初に思い浮かばないので,一旦単純に落とします.

  • 欠損値処理に関しては,全体のmedianで埋めると言う一番単純な操作を行いました.
    (今後の回で,SexとPclassでgroupbyしてからmedianで埋める,と言うちょっと工夫を加えて処理する方法もあります)

  • Sexは,ロジスティック回帰だとそのまま扱えないので,0,1に変換します.
    drop_first=Trueとすることで、Sex_female(またはSex_male)の1列だけが作られます。


スケーリングを行います.各特徴量の値を平均0・分散1に標準化(StandardScaler) します.
ロジスティック回帰だと勾配の計算や正則化がスケールに依存するので必須です.
tree系だと不要ですが,別に害はないので,基本的には行うようにします.

# スケーリング
scaler = StandardScaler()
scaler.fit(X_train)
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)

データを分割して,モデルを作成して,結果を見てみます.

# データ分割,モデル作成
X_tr, X_val, y_tr, y_val = train_test_split(X_train_scaled, y_train, test_size=0.3, random_state=42
)

model = LogisticRegression(random_state=42, max_iter=1000)
model.fit(X_tr, y_tr)

y_pred_val = model.predict(X_val)
accuracy = accuracy_score(y_val, y_pred_val)

print("\n=== First Model (All Features) ===")
print(f"Validation Accuracy: {accuracy:.4f}")
print(classification_report(y_val, y_pred_val))

image.png

accuracy(生存→生存,死亡→死亡と正しく予想できた割合)は0.81でした.
単純に死亡者の方が多かったので,モデルでは死亡と判断しやすく,生存者(1)のrecallが低かった(見逃しが多かった)と考えられます.

交差検証

次に5分割交差検証(5-fold Cross Validation)を行います. データを5つに分割し、検証に使う部分を順番に入れ替えながら5回学習・評価を繰り返し,モデルの汎化性能を平均と分散から評価します. 評価項目としては,accuracyを用います(scoring="accuracy").

# 交差検証
cv_scores = cross_val_score(
    LogisticRegression(random_state=42, max_iter=1000),
    X_train_scaled, y_train, cv=5, scoring='accuracy'
)

print(f"Scores: {cv_scores}")
print(f"Mean:   {cv_scores.mean():.4f}")
print(f"Std:    {cv_scores.std():.4f}")

image.png

こちらの値は0.786でした.単一の分割データセットより若干下がりました.
単純に「女性=生存,男性=死亡」と予想するベースラインではaccuracyは約0.78なので,
これほぼ同じで今回のもでるはめっちゃいい訳ではない,と言うことがわかりました.

結果の提出,まとめ

ここまでで一旦今回は終わらせて,提出してみます.

y_pred_test = model.predict(X_test_scaled)
submission = pd.DataFrame({
    'PassengerId': test_ids,
    'Survived': y_pred_test
})
submission.to_csv('submission.csv', index=False)

Kaggleに実際に提出した際のスコアは0.76315でした.
性別で判断するベースライン(約0.78)よりも低かったですね...

次回は特徴量エンジニアリングを行い,モデルが改善できればと思います.

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?