EDA 실습 — Titanic 생존 데이터
Titanic 데이터셋을 대상으로 EDA 10단계를 직접 적용한 실습 노트. 코드보다 각 단계에서 무엇을 판단했는지에 집중.
실습 파일: ~/Desktop/SKN/SKN_33/05_data_analysis/04_eda/02_eda_titanic.ipynb
핵심 분석 질문
EDA를 시작하기 전에 질문을 먼저 정해야 함.
- Titanic 승객의 생존 여부는 어떤 변수와 관련이 있어 보이는가?
- 결측치나 이상값 때문에 전처리가 필요한 컬럼은 무엇인가?
- 모델링을 한다면 어떤 파생변수를 만들 수 있는가?
- 최종적으로 어떤 인사이트를 보고서에 쓸 수 있는가?
01. 데이터 로드
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import seaborn as sns
import platform
# 한글 폰트 설정 (Windows: Malgun Gothic)
font_candidates = {
"Windows": ["Malgun Gothic", "NanumGothic"],
"Darwin": ["AppleGothic", "NanumGothic"],
"Linux": ["NanumGothic"],
}
available_fonts = {font.name for font in fm.fontManager.ttflist}
for font_name in font_candidates.get(platform.system(), []):
if font_name in available_fonts:
plt.rcParams["font.family"] = font_name
break
plt.rcParams["axes.unicode_minus"] = False # 마이너스 기호 깨짐 방지
sns.set_theme(style="whitegrid")
raw_df = pd.read_csv("data/titanic.csv")
df = raw_df.copy() # 원본 유지, 복사본으로 작업
02. 데이터 구조
- 크기: 891행 × 12열
- 한 행 의미: Titanic 탑승 승객 1명
- 타깃: Survived (0=사망, 1=생존)
컬럼 타입 의미
| PassengerId | int | 승객 고유 번호 |
| Survived | int | 생존 여부 (타깃) |
| Pclass | int | 객실 등급 (1·2·3, 숫자이지만 범주형) |
| Name | str | 이름 |
| Sex | str | 성별 |
| Age | float | 나이 (결측치 있음) |
| SibSp | int | 형제·배우자 수 |
| Parch | int | 부모·자녀 수 |
| Ticket | str | 티켓 번호 |
| Fare | float | 요금 |
| Cabin | str | 객실 번호 (결측치 많음) |
| Embarked | str | 탑승 항구 (S·C·Q) |
print(df.shape) # (891, 12)
print(df.columns.tolist())
df.info()
03. 기술 통계
수치형 요약 (describe().T)
컬럼 평균 중앙값 주목할 점
| Survived | 0.38 | 0 | 생존율 약 38% |
| Age | 29.7 | 28.0 | 결측치로 714개만 집계 |
| Fare | 32.2 | 14.5 | 평균 > 중앙값 → 오른쪽 치우침 |
범주형 요약
컬럼 최빈값 고유값 수
| Sex | male (577) | 2 |
| Embarked | S (644) | 3 |
| Name | — | 891 (모두 고유) |
| Cabin | — | 147 |
df.describe().T
df.describe(include=["object"]).T
# 타깃 클래스 비율
df["Survived"].value_counts(normalize=True) * 100
# → 사망 61.6%, 생존 38.4%
04. 결측치·중복·이상값
결측치 요약
컬럼 결측 수 결측률 처리 전략
| Cabin | 687 | 77.1% | 원본 제거 → HasCabin 파생변수 생성 |
| Age | 177 | 19.9% | 성별·등급별 중앙값으로 대체 |
| Embarked | 2 | 0.2% | 최빈값(S)으로 대체 |
missing_summary = pd.DataFrame({
"missing_count": df.isna().sum(),
"missing_rate": (df.isna().sum() / df.shape[0]) * 100,
}).sort_values("missing_count", ascending=False)
중복
- 전체 중복 행: 0개
- PassengerId 중복: 0개 → 식별자로 사용 가능
이상값 (IQR 기준)
컬럼 이상값 후보 수 해석
| Age | 11 | 매우 나이 많은 승객. 오류보다 실제값 가능성 높음 |
| Fare | 116 | 고가 티켓. 1등급 특성. 로그 변환 고려 |
for col in ["Age", "Fare"]:
q1 = df[col].quantile(0.25)
q3 = df[col].quantile(0.75)
iqr = q3 - q1
lower, upper = q1 - 1.5 * iqr, q3 + 1.5 * iqr
outlier_count = ((df[col] < lower) | (df[col] > upper)).sum()
print(f"{col}: 이상값 후보 {outlier_count}개")
05 & 06. 분포 시각화 + 변수 간 관계
# 범주형 4개 분포 한 번에
fig, axes = plt.subplots(2, 2, figsize=(12, 8), constrained_layout=True)
axes = axes.ravel() # 2D → 1D
for ax, (col, title) in zip(axes, [
("Survived", "생존 여부"), ("Pclass", "객실 등급"),
("Sex", "성별"), ("Embarked", "탑승 항구")
]):
sns.countplot(data=df, x=col, ax=ax, hue=col, palette="Set2", legend=False)
ax.set_title(title)
plt.show()
# 성별·등급·항구별 생존율
fig, axes = plt.subplots(1, 3, figsize=(15, 4), constrained_layout=True)
for ax, col in zip(axes, ["Sex", "Pclass", "Embarked"]):
sns.barplot(data=df, x=col, y="Survived", errorbar=None, ax=ax)
ax.set_ylim(0, 1)
ax.set_title(f"{col}별 생존율")
plt.show()
07. 상관관계 및 교차분석
수치형 상관계수 (주목할 관계)
변수 조합 상관계수 해석
| Pclass ↔ Fare | -0.55 | 등급 낮을수록 요금 낮음 |
| Survived ↔ Pclass | -0.34 | 등급 높을수록 생존율 높음 |
| Survived ↔ Fare | 0.26 | 요금 높을수록 생존율 약간 높음 |
corr_cols = ["Survived", "Pclass", "Age", "SibSp", "Parch", "Fare"]
corr_mat = df[corr_cols].corr(numeric_only=True)
sns.heatmap(corr_mat, annot=True, fmt=".2f", cmap="coolwarm", center=0)
plt.show()
범주형 교차표
# normalize='index': 행 기준 비율 (각 그룹 내 생존율)
pd.crosstab(df["Sex"], df["Survived"], normalize="index").round(3)
pd.crosstab(df["Pclass"], df["Survived"], normalize="index").round(3)
성별 생존율
| female | 74.2% |
| male | 18.9% |
객실 등급 생존율
| 1등급 | 63.0% |
| 2등급 | 47.3% |
| 3등급 | 24.2% |
08. 전처리 실습
eda_df = raw_df.copy() # 원본 유지!
# Cabin: 결측 여부 파생변수
eda_df["HasCabin"] = eda_df["Cabin"].notna().astype(int)
# Age: 성별·등급별 중앙값으로 대체 (transform: 그룹 크기 유지)
age_median = eda_df.groupby(["Sex", "Pclass"])["Age"].transform("median")
eda_df["AgeFilled"] = eda_df["Age"].fillna(age_median)
# Embarked: 최빈값으로 대체
embarked_mode = eda_df["Embarked"].mode()[0] # mode()[0]: 최빈값 첫 번째
eda_df["EmbarkedFilled"] = eda_df["Embarked"].fillna(embarked_mode)
groupby().transform(): 그룹별 계산 후 원본 DataFrame과 같은 인덱스로 반환. fillna()와 함께 쓰기 좋음.
09. Feature Engineering 실습
# 가족 규모
eda_df["FamilySize"] = eda_df["SibSp"] + eda_df["Parch"] + 1
# 단독 탑승 여부
eda_df["IsAlone"] = (eda_df["FamilySize"] == 1).astype(int)
# 나이 구간화
eda_df["AgeGroup"] = pd.cut(
eda_df["AgeFilled"],
bins=[0, 12, 18, 35, 60, eda_df["AgeFilled"].max()],
labels=["Child", "Teen", "YoungAdult", "Adult", "Senior"],
include_lowest=True # 가장 왼쪽 경계값 포함
)
# 로그 변환 (Fare 치우침 완화)
eda_df["LogFare"] = np.log1p(eda_df["Fare"]) # log(1+x)
# 원-핫 인코딩
model_ready_df = pd.get_dummies(
eda_df[["Survived", "Pclass", "AgeFilled", "FamilySize",
"IsAlone", "LogFare", "HasCabin", "Sex", "EmbarkedFilled"]],
columns=["Sex", "EmbarkedFilled"],
dtype=int
)
# 결과: Sex_female, Sex_male, EmbarkedFilled_C, EmbarkedFilled_Q, EmbarkedFilled_S 생성
10. 최종 인사이트
- 전체 생존율은 약 38.4%로 사망자 비율이 더 높음
- 여성 생존율(74.2%)이 남성(18.9%)보다 훨씬 높음 → Sex는 가장 관련 큰 변수
- 객실 등급이 높을수록 생존율이 높음 (1등급 63%, 3등급 24%)
- Cabin 결측률이 77%로 원본 컬럼 사용 어려움 → HasCabin 파생변수 활용
- Age 결측률 20% → 성별·등급별 중앙값으로 대체 전략 선택
- Fare는 오른쪽 치우침 + 이상값 후보 116개 → LogFare 변환 고려
다음 단계: 결측치 처리 → 인코딩 → Feature/Target 분리 → 머신러닝 모델 학습
자주 쓴 코드 패턴
# 결측치 요약표
pd.DataFrame({
"missing_count": df.isna().sum(),
"missing_rate": (df.isna().sum() / len(df)) * 100,
}).sort_values("missing_count", ascending=False)
# 그룹별 교차표 (비율)
pd.crosstab(df["Sex"], df["Survived"], normalize="index").round(3)
# 여러 subplot + ravel
fig, axes = plt.subplots(2, 2, figsize=(12, 8), constrained_layout=True)
axes = axes.ravel()
for ax, col in zip(axes, col_list):
sns.countplot(data=df, x=col, ax=ax)
# 그룹별 변환 후 fillna
age_fill = df.groupby(["Sex", "Pclass"])["Age"].transform("median")
df["AgeFilled"] = df["Age"].fillna(age_fill)
'SKN_AI_33 > Data_Analysis' 카테고리의 다른 글
| 04_EDA (0) | 2026.07.06 |
|---|---|
| 03_Seaborn (0) | 2026.07.06 |
| 02_Pandas (0) | 2026.07.06 |
| 01. NumPy (0) | 2026.07.06 |
