Skip to main content

Titanic EDA

Titanic 데이터셋은 데이터 분석과 ML의 대표적인 입문 프로젝트입니다. 1912년 타이타닉호 침몰 사건의 승객 데이터를 분석하여 생존에 영향을 미친 요인을 탐색합니다.

학습 목표

  • 실제 데이터셋으로 EDA 전체 과정을 수행할 수 있다
  • 결측치 처리, 피처 분석, 시각화를 종합적으로 적용할 수 있다
  • 분석 결과를 인사이트로 정리하고 보고할 수 있다
  • ML 모델링을 위한 데이터 준비 과정을 이해한다

왜 중요한가

Titanic 프로젝트는 구조화된 데이터 분석의 모든 기본 요소를 포함합니다. 결측치, 범주형 변수, 수치형 변수, 이진 타겟이 모두 있어 EDA의 전 과정을 연습하기에 이상적입니다.

Step 1: 데이터 로드와 이해

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# Titanic 데이터셋 로드
# seaborn 내장 데이터셋 사용
df = sns.load_dataset('titanic')
print(f"데이터 크기: {df.shape}")
print(f"\n컬럼 정보:")
print(df.dtypes)
print(f"\n처음 5행:")
print(df.head())

데이터 사전

변수설명타입
survived생존 여부 (0=사망, 1=생존)타겟
pclass객실 등급 (1=1등석, 2=2등석, 3=3등석)순서형
sex성별범주형
age나이수치형
sibsp동반한 형제/배우자 수수치형
parch동반한 부모/자녀 수수치형
fare운임수치형
embarked탑승항 (C, Q, S)범주형
class객실 등급 (문자열)범주형
deck갑판범주형
alone혼자 탑승 여부불리언

Step 2: 데이터 품질 점검

# 결측치 확인
missing = df.isnull().sum()
missing_pct = (missing / len(df) * 100).round(1)
missing_report = pd.DataFrame({
    '결측 수': missing,
    '결측 비율(%)': missing_pct
}).query('`결측 수` > 0').sort_values('결측 비율(%)', ascending=False)
print("결측치 현황:")
print(missing_report)

# 기술통계
print("\n수치형 변수 요약:")
print(df.describe().round(1))

# 범주형 변수 확인
print("\n범주형 변수:")
for col in df.select_dtypes(include=['object', 'category', 'bool']).columns:
    print(f"  {col}: {df[col].nunique()}개 — {df[col].value_counts().head(3).to_dict()}")

Step 3: 타겟 변수 분석

# 생존율 확인
survival_rate = df['survived'].value_counts(normalize=True)
print(f"생존율: {survival_rate[1]:.1%}")

fig, ax = plt.subplots(figsize=(6, 4))
df['survived'].value_counts().plot(kind='bar', ax=ax, color=['#e6a23c', '#4a9e4a'])
ax.set_xticklabels(['사망', '생존'], rotation=0)
ax.set_title(f'생존 분포 (생존율: {survival_rate[1]:.1%})')
ax.set_ylabel('인원 수')

# 비율 레이블
for i, (count, pct) in enumerate(zip(df['survived'].value_counts(), survival_rate)):
    ax.text(i, count + 5, f'{count}명 ({pct:.1%})', ha='center')

plt.tight_layout()
plt.show()

Step 4: 단변량 분석

# 수치형 변수 분포
fig, axes = plt.subplots(1, 3, figsize=(15, 4))

for ax, col in zip(axes, ['age', 'fare', 'sibsp']):
    sns.histplot(df[col].dropna(), kde=True, ax=ax, color='#4a9eca')
    ax.set_title(f'{col} 분포')

plt.tight_layout()
plt.show()

# 범주형 변수 빈도
fig, axes = plt.subplots(1, 3, figsize=(15, 4))

for ax, col in zip(axes, ['sex', 'pclass', 'embarked']):
    df[col].value_counts().plot(kind='bar', ax=ax, color='#4a9eca', alpha=0.7)
    ax.set_title(f'{col} 분포')
    ax.tick_params(axis='x', rotation=0)

plt.tight_layout()
plt.show()

Step 5: 이변량 분석 — 생존 요인 탐색

# 성별 vs 생존
fig, axes = plt.subplots(2, 3, figsize=(15, 8))

# 성별
sns.barplot(data=df, x='sex', y='survived', ax=axes[0, 0], palette=['#4a9eca', '#e6a23c'])
axes[0, 0].set_title('성별 생존율')
axes[0, 0].set_ylabel('생존율')

# 객실 등급
sns.barplot(data=df, x='pclass', y='survived', ax=axes[0, 1], palette='Set2')
axes[0, 1].set_title('객실 등급별 생존율')

# 탑승항
sns.barplot(data=df, x='embarked', y='survived', ax=axes[0, 2], palette='Set2')
axes[0, 2].set_title('탑승항별 생존율')

# 나이 분포 (생존 vs 사망)
sns.histplot(data=df, x='age', hue='survived', kde=True, ax=axes[1, 0],
             palette=['#e6a23c', '#4a9e4a'], alpha=0.5)
axes[1, 0].set_title('나이별 생존 분포')

# 운임 분포
sns.boxplot(data=df, x='survived', y='fare', ax=axes[1, 1],
            palette=['#e6a23c', '#4a9e4a'])
axes[1, 1].set_xticklabels(['사망', '생존'])
axes[1, 1].set_title('운임별 생존 분포')

# 가족 크기
df['family_size'] = df['sibsp'] + df['parch'] + 1
sns.barplot(data=df, x='family_size', y='survived', ax=axes[1, 2], color='#4a9eca')
axes[1, 2].set_title('가족 크기별 생존율')

plt.tight_layout()
plt.show()

교차 분석

# 성별 x 등급별 생존율
cross = pd.crosstab([df['sex'], df['pclass']], df['survived'], normalize='index')
cross.columns = ['사망', '생존']
print("성별 x 등급별 생존율:")
print(cross.round(3))

# 히트맵
pivot = df.pivot_table(values='survived', index='sex', columns='pclass', aggfunc='mean')
fig, ax = plt.subplots(figsize=(8, 4))
sns.heatmap(pivot, annot=True, fmt='.1%', cmap='RdYlGn', ax=ax)
ax.set_title('성별 x 등급별 생존율')
plt.tight_layout()
plt.show()

Step 6: 결측치 처리

# age: 중앙값으로 대체 (성별/등급별)
df['age_filled'] = df.groupby(['sex', 'pclass'])['age'].transform(
    lambda x: x.fillna(x.median())
)

# embarked: 최빈값으로 대체
df['embarked'].fillna(df['embarked'].mode()[0], inplace=True)

# deck: 결측 비율이 높아 삭제 또는 결측 표시
df['has_deck'] = df['deck'].notna().astype(int)

print("결측치 처리 후:")
print(df[['age_filled', 'embarked', 'has_deck']].isnull().sum())

Step 7: 인사이트 정리

insights = """
## Titanic EDA 인사이트

### 핵심 발견
1. **성별이 가장 강력한 생존 예측 변수**: 여성 생존율 74%, 남성 19%
2. **객실 등급과 생존율의 강한 양의 관계**: 1등석 63%, 2등석 47%, 3등석 24%
3. **성별과 등급의 교호작용**: 1등석 여성의 생존율이 97%로 가장 높음
4. **가족 크기 2~4명이 최적**: 혼자 탑승하거나 대가족일 때 생존율 낮음
5. **운임이 높을수록 생존율 높음**: 고가 운임 = 상위 등급 = 높은 생존율

### 결측치 처리
- age: 성별/등급별 중앙값으로 대체 (177개, 19.9%)
- embarked: 최빈값(S)으로 대체 (2개)
- deck: 결측률 77.2%로 높아 has_deck 이진 피처 생성

### ML 확장 제안
- **타겟**: survived (이진 분류)
- **주요 피처**: sex, pclass, age, fare, family_size, embarked
- **권장 모델**: Random Forest, Logistic Regression
- **전처리**: 성별/탑승항 → One-Hot, 나이/운임 → StandardScaler
"""
print(insights)

ML로 확장하기

from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer

# 피처와 타겟 정의
features = ['pclass', 'sex', 'age', 'sibsp', 'parch', 'fare', 'embarked']
X = df[features].copy()
y = df['survived']

# 전처리 파이프라인
numeric_features = ['age', 'sibsp', 'parch', 'fare']
categorical_features = ['pclass', 'sex', 'embarked']

preprocessor = ColumnTransformer([
    ('num', Pipeline([
        ('imputer', SimpleImputer(strategy='median')),
        ('scaler', StandardScaler())
    ]), numeric_features),
    ('cat', Pipeline([
        ('imputer', SimpleImputer(strategy='most_frequent')),
        ('encoder', OneHotEncoder(drop='first', sparse_output=False))
    ]), categorical_features)
])

# 모델 파이프라인
model = Pipeline([
    ('preprocessor', preprocessor),
    ('classifier', RandomForestClassifier(n_estimators=100, random_state=42))
])

# 교차검증
scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')
print(f"교차검증 정확도: {scores.mean():.3f} (+/- {scores.std():.3f})")
  1. Name에서 호칭(Mr, Mrs, Miss 등)을 추출하여 피처로 사용, 2) Cabin의 첫 글자(갑판)를 활용, 3) 가족 크기를 범주화(혼자/소가족/대가족), 4) 운임을 구간별로 비닝하면 성능이 향상됩니다.

체크리스트

  • 데이터 로드 후 구조와 품질을 점검할 수 있다
  • 타겟 변수의 분포를 확인하고 불균형을 파악할 수 있다
  • 변수별 생존율을 비교하고 핵심 요인을 도출할 수 있다
  • 결측치를 적절한 전략으로 처리할 수 있다
  • 분석 결과를 구조화된 인사이트로 정리할 수 있다

다음 문서