
[#8] 모델 학습 및 평가
7편에서 전처리된 데이터가 준비됐습니다. 8편에서는 로지스틱 회귀 모델을 학습하고 성능을 평가합니다.
전처리 데이터 준비
X_train_df = pd.read_csv(OUTPUT_DIR / "21_X_train_processed.csv")
y_train = X_train_df["Survived"]
X_train = X_train_df.drop(columns=["Survived"])
Step 1. 로지스틱 회귀 모델 학습
로지스틱 회귀는 이름에 "회귀"가 붙어 있지만, 실제로는 분류 모델이에요. 내부적으로 세 단계로 작동합니다.
1단계 — 선형 결합
각 변수에 가중치를 곱해서 더합니다.
$$ z = w_1 \cdot \text{Sex\_female} + w_2 \cdot \text{Pclass} + w_3 \cdot \text{IsChild} + \cdots + b $$
z 는 −∞ 부터 +∞까지 어떤 값도 될 수 있어요. 이 상태로는 "생존 확률"로 쓸 수가 없습니다.
2단계 — 시그모이드 함수
$$ P(\text{생존}) = \frac{1}{1 + e^{-z}} $$
시그모이드 함수는 어떤 값이 들어와도 0~1 사이로 압축해줘요. 이 단계까지가 모델이 확률을 계산하는 과정이고, 이 확률을 실제 분류 결과(0 또는 1)로 변환하려면 한 단계가 더 필요합니다.
3단계 — Threshold 적용
시그모이드에서 나온 확률값을 기준값(threshold)과 비교해서 최종 분류를 결정합니다.
$$ \hat{y} = \begin{cases} 1 & \text{if } P(\text{생존}) \geq \text{threshold} \\ 0 & \text{if } P(\text{생존}) < \text{threshold} \end{cases} $$
기본값은 0.5이고, sklearn의 model.predict() 는 이 0.5를 그대로 사용합니다. 이번 분석에서도 별도로 조정하지 않아서 0.5 기준으로 분류했습니다.
model = LogisticRegression(random_state=42, max_iter=1000)
model.fit(X_train, y_train)
LogisticRegression의 파라미터들 아래와 같이 설정한 이유
random_state=42 로지스틱 회귀는 내부적으로 최적의 가중치(w)를 찾을 때 데이터를 섞는 과정이 있습니다. 이 섞는 순서가 매번 달라지면 실행할 때마다 결과가 조금씩 달라집니다. random_state는 이 섞는 순서를 고정하는 시드값입니다. 42라는 숫자 자체는 특별한 의미가 없습니다.
max_iter=1000 로지스틱 회귀는 최적의 w를 한 번에 구하는 게 아니라, 조금씩 조정하면서 반복적으로 찾아갑니다. 이 반복 횟수의 상한선이 max_iter입니다.
기본값은 100인데, 데이터가 복잡하거나 변수가 많으면 100번 안에 수렴하지 못하고 경고 메시지가 뜨는 경우가 있습니다. 넉넉하게 1000으로 설정해서 충분히 수렴할 시간을 줍니다.
.fit(X_train, y_train) X_train(입력 변수들)과 y_train(정답인 Survived)을 넣어서 모델을 학습시킵니다.
이 과정에서 각 변수의 가중치 (coefficient)가 결정됩니다. 학습이 끝나면 model 객체 안에 최적의 w와 bias값이 저장됩니다
Step 2. 교차 검증 — 모델이 진짜 잘하는 건지 확인
학습한 데이터로 바로 평가하면 "시험 문제를 미리 본 것" 과 같아서 성능이 과대평가됩니다. 교차 검증으로 더 신뢰할 수 있는 성능을 측정합니다.
교차검증은 다른 말로 “학습에 쓰지 않은 데이터로 평가”하는 것입니다.
`cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
cv_scores = cross_val_score(model, X_train, y_train, cv=cv, scoring="accuracy")
print(f"CV 정확도: {cv_scores}")
print(f"평균: {cv_scores.mean():.3f}")
print(f"표준편차: {cv_scores.std():.3f}")`
StratifiedKFold가 하는 일 (관심 데이터의 분류 비율을 일정하게 유지한다)
1회: [조각1 평가] [조각2,3,4,5 학습]
2회: [조각2 평가] [조각1,3,4,5 학습]
3회: [조각3 평가] [조각1,2,4,5 학습]
4회: [조각4 평가] [조각1,2,3,5 학습]
5회: [조각5 평가] [조각1,2,3,4 학습]
만약 k=5일 경우, 데이터를 5개 조각으로 나눠서, 4개로 학습하고 1개로 평가하는 과정을 5번 반복합니다.
Stratified가 붙은 이유는 각 조각마다 생존자/사망자 비율을 원본과 동일하게 유지하기 때문입니다. 예를 들어 원본 데이터의 생존율이 38%라면, 5개 조각 각각도 생존율이 38%에 가깝도록 나눕니다. 생존자가 한 조각에 몰리는 걸 방지합니다.
교차검증 결과
CV 정확도: [0.810 0.787 0.798 0.821 0.804]
평균: 0.804
표준편차: 0.012
평균 정확도 80.4퍼센트는 모델이 승객 10명 중 약 8명의 생존 여부를 맞춘다는 의미이며, 표준편차가 0.012로 작다는 것은 5번의 평가결과가 편차가 적어서 상당히 안정적인 결과라고 볼 수 있습니다.

Step 3. 혼동 행렬 — 어떤 실수를 하는가
y_pred = model.predict(X_train)
cm = confusion_matrix(y_train, y_pred)
cm_df = pd.DataFrame(
cm,
index=["실제 사망(0)", "실제 생존(1)"],
columns=["예측 사망(0)", "예측 생존(1)"]
)
cm_df.to_csv(OUTPUT_DIR / "22_confusion_matrix.csv")
cm_df
| 실제 사망(0) | 501 | 48 |
| 실제 생존(1) | 107 | 235 |
사망자 549명 중에서 모델이 501명은 사망으로 정확히 맞췄고, 48명은 생존으로 잘못 예측했습니다. 사망자의 91.3%를 맞추었고,
생존자 342명 중에서 모델이 235명은 생존으로 정확히 맞췄고, 107명은 사망으로 잘못 예측했어. 생존자의 68.7%만 맞추었습니다.
이에 따라 정확도를 도출해보면,
(501 + 235) / 891 = 82.6%
Step 4. 정밀도/재현율/F1 — 정확도 하나로만 보면 안 되는 이유
report = classification_report(y_train, y_pred, target_names=["사망(0)", "생존(1)"])
print(report)
| 사망(0) | 0.824 | 0.913 | 0.866 | 549 |
| 생존(1) | 0.830 | 0.687 | 0.752 | 342 |
| accuracy | 0.827 | 891 |
정확도(accuracy)가 82.7%라서 상당히 좋은 모델이라고 생각하기 쉽지만, 왜곡이 있을 수 있어 다른 지표들도 검토해야 합니다.
생존자 재현율(recall)은 68.7%밖에 안 됩니다. 생존자 10명 중 3명을 놓치고 있는 셈입니다. 또한 정밀도(Precision)은 83%로 좋은 성능을 보여줬습니다. 둘을 조화평균 낸 F1 Score으로 둘의 균형성을 평가할 수 있습니다.
F1 = 2 × (0.830 × 0.687) / (0.830 + 0.687) = 0.752
이 모델의 성능을 종합적으로 평가하면 이렇습니다.
캐글 타이타닉 기준으로 단순하게 "전원 사망"으로 찍으면 정확도가 61.6%입니다. 아무것도 학습하지 않은 baseline 대비 우리 모델은 82.7%로 약 21%p 높습니다. 의미 있는 학습이 이루어졌다고 볼 수 있습니다.
다만 생존자 재현율이 68.7%로 상대적으로 낮은 점은 아쉽습니다. 이는 데이터에서 사망자(549명)가 생존자(342명)보다 많아서 모델이 사망 쪽으로 보수적으로 예측하는 경향이 생겼기 때문입니다.
F1 Score 0.752는 준수한 수준입니다. 로지스틱 회귀라는 단순한 모델 하나에, 파생변수 4개만 추가한 것 치고는 나쁘지 않은 결과입니다. 더 복잡한 모델(랜덤 포레스트, XGBoost 등)을 쓰면 성능을 더 끌어올릴 수 있지만, 이번 분석의 목적은 로지스틱 회귀로 기본기를 다지는 것이었으므로 충분히 의미 있는 결과입니다.
Step 5. 계수 확인 — 어떤 변수가 얼마나 영향을 줬는가
coef_df = pd.DataFrame({
"feature": X_train.columns,
"coefficient": model.coef_[0].round(3)
}).sort_values("coefficient", ascending=False)
coef_df.to_csv(OUTPUT_DIR / "23_coefficients.csv", index=False)
coef_df
feature coefficient
| Sex_female | 2.631 |
| HasCabin | 0.701 |
| Pclass | -0.821 |
| IsChild | 0.543 |
| IsAlone | -0.412 |
Sex_female (2.631) — 압도적으로 가장 큰 계수입니다. 여성일 때 생존 확률이 가장 크게 올라갑니다. 3편에서 확인한 "여성 74.2% vs 남성 18.9%"와 일치하며, 이 모델에서 가장 강력한 변수입니다.
Pclass (-0.821) — 음수 계수 중 절댓값이 가장 큽니다. Pclass 숫자가 커질수록(등급이 낮아질수록) 생존 확률이 가장 크게 떨어집니다. 3편의 "1등석 63% vs 3등석 24%"와 일치합니다.
HasCabin (0.701) — 객실 기록이 있을수록 생존 확률이 올라갑니다. 5편에서 "기록 있음 66.7% vs 없음 30.0%"로 확인한 신호가 계수에도 반영됐습니다.
IsChild (0.543) — 어린이일수록 생존 확률이 올라갑니다. "어린이 먼저" 원칙이 계수에도 그대로 반영됐습니다.
IsAlone (-0.412) — 혼자 탑승할수록 생존 확률이 낮아집니다. 5편의 "혼자 30.4% vs 동반 50.5%"와 일치합니다.
[#9] 예측
8편에서 모델 학습과 평가까지 완료했습니다. 9편에서는 test 데이터에 모델을 적용해서 실제 예측을 합니다.
Step 1. 전체 파이프라인 재실행
Train.csv가 아닌 실제 Test.csv로 테스트.
8편까지는 train 데이터로만 작업했습니다. 이제 test 데이터에도 똑같은 전처리와 파생변수 생성을 적용해야 합니다. 7~8편에서 만든 코드를 그대로 가져와서 train과 test 양쪽에 동시에 적용합니다.
def make_features(df):
df = df.copy()
df["IsChild"] = (df["Age"] < 16).astype(int)
df["FamilySize"] = df["SibSp"] + df["Parch"] + 1
df["IsAlone"] = (df["FamilySize"] == 1).astype(int)
df["HasCabin"] = df["Cabin"].notna().astype(int)
return df
train = make_features(train)
test = make_features(test)
FEATURES = ["Sex", "Pclass", "IsChild", "IsAlone", "HasCabin"]
TARGET = "Survived"
X_train = train[FEATURES]
y_train = train[TARGET]
X_test = test[FEATURES]
함수 안에서 원본 DataFrame을 직접 건드리지 않기 위해 copy()를 사용하여 복사본을 만듭니다. 이게 없으면 train = make_features(train) 을 호출하는 순간 원본 train이 바뀌어버려서 나중에 디버깅할 때 원본을 확인할 수 없게 됩니다.
참고로, 8편에서 X_train_df.drop(columns=["Survived"]) 로 X_train을 만들었을 때, drop() 은 원본을 건드리지 않고 새로운 DataFrame을 반환하기 때문에 X_train_df 는 Survived 컬럼이 그대로 남아있는 상태입니다. 명시적으로 copy() 를 쓰지 않았지만, drop() 자체가 새 객체를 만들어내기 때문에 결과적으로 같은 효과입니다.
9편에서는 이와 달리 make_features() 함수 안에서 명시적으로 df.copy() 를 사용합니다. 함수는 train과 test 양쪽에 재사용되기 때문에, 원본이 오염되면 디버깅이 어려워집니다. 탐색 단계에서는 원본을 직접 건드려도 괜찮지만, 재사용되는 함수 안에서는 복사본을 만드는 것이 안전한 습관입니다.
make_features() 함수를 train과 test 양쪽에 동일하게 호출합니다. 함수로 묶어뒀기 때문에 실수 없이 동일한 파생변수가 만들어집니다. 만약 함수 없이 코드를 따로 작성했다면, train에는 적용하고 test에는 빠뜨리는 실수가 생길 수 있습니다.
X_test에는 Survived 컬럼이 없습니다. test 데이터는 정답이 없는 데이터이고, 우리가 예측해야 할 대상이기 때문입니다.
Step 2. 전처리 및 모델 학습
categorical_features = ["Sex"]
원-핫 인코딩이 필요한 컬럼 목록입니다. Sex만 문자열(male/female)이라 숫자로 바꿔야 합니다.
passthrough_features = [...]
그대로 통과시킬 컬럼 목록입니다. 이미 0/1이나 1/2/3 숫자라서 추가 전처리가 필요 없습니다.
ColumnTransformer(transformers=[...])
transformers 안에 리스트로 규칙을 넣습니다. 각 규칙은 세 가지로 구성됩니다.
("이름", 전처리방법, 적용할컬럼)
transformers=[
("cat", OneHotEncoder(drop="first", sparse_output=False), categorical_features),
("num", "passthrough", passthrough_features)
]
첫 번째 규칙 ("cat", OneHotEncoder(...), categorical_features) 으로 범주형 변수인 Sex 컬럼에 OneHotEncoder를 적용합니다.
두 번째 규칙 ("num", "passthrough", passthrough_features) 는 나머지 컬럼은 그대로 통과시킵니다.
실제 Test를 위한 Preprocessor 객체 재사용
# 1단계 — fit_transform으로 기준이 preprocessor 안에 저장됨
X_train_processed = preprocessor.fit_transform(X_train)
# 2단계 — 같은 preprocessor로 transform만 호출
X_test_processed = preprocessor.transform(X_test)
이젠 실제 test 데이터를 가지고, 모델이 잘 학습했는지 화인해야하기에, train으로 학습한 전처리 기준을 test에그대로 적용하는 과정이 추가됩니다.
fit_transform(X_train) — train 데이터로 전처리 기준을 학습하고 동시에 변환합니다. "Sex에는 male과 female이 있다", "female을 1로, male을 0으로 인코딩한다"는 기준이 preprocessor 객체 안에 저장됩니다.
transform(X_test) — 저장된 기준을 test 데이터에 그대로 적용합니다. test 데이터 기준으로 절 새로 학습하지 않습니다. 따라서 fit_transform을 쓰는 것이 아닌 그냥 transform을 사용합니다.
만약 test에 fit_transform을 쓰면 train과 다른 기준이 적용되어 컬럼 구조가 달라질 수 있습니다.
실제 모델 학습
model = LogisticRegression(random_state=42, max_iter=1000)
model.fit(X_train_processed, y_train)
model.fit(X_train_processed, y_train) — 전처리된 X_train과 정답 y_train으로 로지스틱 회귀를 학습합니다. 이 단계에서 각 변수의 가중치(coefficient)가 결정됩니다. 8편에서 확인한 Sex_female(2.631), Pclass(-0.821) 같은 값들이 이 과정에서 만들어집니다.
7편에서는 학습 후 성능 평가가 목적이었다면, 9편에서는 최종 제출을 위해 전체 train 데이터를 빠짐없이 활용해서 학습하는 것이 목적입니다. 교차 검증에서는 train 일부를 평가용으로 빼두었지만, 이제는 평가가 필요 없으니 891명 전체로 학습해서 모델의 성능을 최대한 끌어올립니다.
Step 3. test 데이터 예측
y_pred_test = model.predict(X_test_processed)
print(f"예측값 분포: {pd.Series(y_pred_test).value_counts().to_dict()}")
print(f"예측 생존율: {y_pred_test.mean():.3f}")
출력 결과
예측값 분포: {0: 266, 1: 152}
예측 생존율: 0.364`
model.predict()가 하는 일
Train.cs로 학습시킨 모델 안에는 이미 각 변수에 대한 가중치(coefficient)가 저장되어있습니다.
model.predict(X_test_processed)를 호출하면 418명 각각에 대해 아래 과정을 반복합니다.
1단계 — z값 계산
z = 2.631 * Sex_female
+ 0.701 * HasCabin
+ 0.543 * IsChild
+ (-0.412) * IsAlone
+ (-0.821) * Pclass
+ b (bias)
각 변수에 가중치를 곱해서 더합니다. 예를 들어 여성(Sex_female=1), 1등석(Pclass=1), 객실 기록 있음(HasCabin=1), 성인(IsChild=0), 동반 탑승(IsAlone=0)인 승객이라면 z값이 크게 나옵니다.
2단계 — sigmoid로 확률 변환
생존 확률 = 1 / (1 + e^(-z))
z값을 sigmoid 함수에 넣어 0~1 사이의 생존 확률로 변환합니다. z가 클수록 확률이 1에 가까워집니다.
3단계 — 0.5 기준으로 이진 분류
생존 확률 >= 0.5 → 1 (생존)
생존 확률 < 0.5 → 0 (사망)
확률을 최종적으로 0 또는 1로 변환합니다. 418명 각각에 대해 이 과정을 반복해서
418개의 0/1 배열이 y_pred_test에 담깁니다.
예측값 분포
print(f"예측값 분포: {pd.Series(y_pred_test).value_counts().to_dict()}")
print(f"예측 생존율: {y_pred_test.mean():.3f}")
예측값 분포: {0: 266, 1: 152}
예측 생존율: 0.364
value_counts().to_dict()
y_pred_test 배열에서 0이 몇 개, 1이 몇 개인지 세어서 딕셔너리로 보여줍니다. 418명 중 266명을 사망(0), 152명을 생존(1)으로 예측했다는 의미입니다.
y_pred_test.mean()
0/1 배열의 평균은 1의 비율과 같습니다.
test 데이터 418명 중 152명(36.4%)을 생존으로 예측했습니다. train 데이터의 실제 생존율은 38.4%입니다. 두 수치가 비슷한 수준입니다.
이 수치를 train 데이터의 실제 생존율과 비교합니다.
생존율
| train 실제 생존율 | 38.4% |
| test 예측 생존율 | 36.4% |
두 수치가 약 2%p 차이로 비슷합니다. 이는 모델이 train에서 학습한 패턴을 test에도 비슷하게 적용하고 있다는 신호입니다.

최종 결론
본 분석은 단순히 모델을 돌리는 것이 아니라, 왜 이 변수를 쓰는지, 왜 이 전처리를 하는지 매 단계마다 데이터로 근거를 남기는 방식으로 진행했습니다.
로지스틱 회귀라는 단순한 모델에 신중하게 선택한 5개 변수만으로 교차 검증 기준 80.4%의 성능을 달성했습니다.
test 데이터 예측 결과는 418명 중 152명(36.4%)을 생존으로 예측했습니다. train 데이터의 실제 생존율 38.4%와 약 2%p 차이로, 모델이 train에서 학습한 패턴을 test에도 안정적으로 적용했다고 판단할 수 있습니다.
계수 분석에서 확인했듯이 Sex_female(2.631)이 가장 강한 생존 신호였고, 이는 3편에서 데이터로 검증한 "여성 생존율 74.2% vs 남성 18.9%"와 완전히 일치합니다.
아쉬운 점은 생존자 재현율이 68.7%로 생존자 10명 중 3명을 놓친다는 점입니다. 이는 사망자(549명)가 생존자(342명)보다 많은 클래스 불균형에서 비롯된 한계입니다.
그럼에도 불구하고, 로지스틱 회귀 하나로 baseline(61.6%) 대비 약 21%p 높은 성능을 달성하였습니다.
'Machine Learning > Real Analysis' 카테고리의 다른 글
| 로지스틱 회귀 모델링 <전처리> (0) | 2026.04.22 |
|---|---|
| 로지스틱 회귀 모델링 <가설> (0) | 2026.04.21 |
| 로지스틱 회귀 모델링 <진단> (1) | 2026.04.20 |