🧩 교차 검증이란?
교차검증이 필요한 이유
교차 검증(Cross-Validation)은 데이터를 여러 번 나누어 학습과 평가를 반복하고, 그 결과의 평균을 내어 모델의 성능을 정확하게 확인하는 방법이다.
이전에 train/test 데이터를 분할할 때 단순하게 80% 훈련 + 20% 테스트 이런 식으로 나누었다. 하지만 데이터를 어떻게 나누느냐에 따라 정확도가 달라질 수 있다. 예를 들어 우연히 테스트 데이터에 쉬운 경기만 많이 포함되면 성능이 높게 나올 수 있다. 교차 검증은 데이터를 여러 번 나눠 학습/검증하여 성능을 더 안정적으로 평가하는 방법이다.
K-Fold Cross-Validation
K-겹 교차 검증(K-fold cross validation)은 전체 데이터를 K개의 부분으로 나눈 뒤, K-1개는 학습용으로 쓰고 나머지 1개는 검증용으로 사용하여 이 과정을 K번 반복하고 성능을 평균 내는 모델 평가 방법이다.

- 데이터를 먼저 섞은 뒤, 총 N개의 데이터를 겹치지 않는 K개 그룹으로 분할한다.
- 각 그룹이 번갈아 검증셋이 되고, 나머지는 훈련셋이 된다.
- K-1개의 훈련셋으로 데이터를 학습한 뒤, 남은 1개의 검증셋으로 검증한다.
- K개의 MSE를 평균 내서 테스트 오류를 추정한다.
Stratified K-Fold
계층적 K-겹 교차 검증(Stratified K-Fold)는 분류 모델의 성능을 정확하게 평가하기 위해, 원본 데이터의 레이블(클래스) 비율을 각 Fold에서도 동일하게 유지하도록 데이터를 나누는 교차 검증 방법이다
일반적인 K-Fold는 데이터를 무작위로 나누기 때문에 특정 폴드에 정답 데이터가 빠지는 문제가 생길 수 있다. 이러한 문제를 해결하기 위해 등장한 것이 Stratified K-Fold로, 전체 데이터이 정답 비율과 각 분할된 폴드의 정답 비율을 같게 만든다. 데이터의 특정 값이 너무 많거나 적을 때 사용하며, 주로 정답이 범주형인 분류 문제에 쓰인다.
from sklearn.model_selection import StratifiedKFold
cv = StratifiedKFold(
n_splits=5,
shuffle=True,
random_state=42
)
교차 검증 결과 확인 및 해석하기
여기서 만든 clf Pipeline을 그대로 사용한다고 가정한다.
from sklearn.model_selection import cross_validate
scoring = {
"accuracy": "accuracy",
"f1": "f1",
"roc_auc": "roc_auc"
}
cv_results = cross_validate(
estimator=clf,
X=X_train,
y=y_train,
cv=cv,
scoring=scoring,
return_train_score=True,
n_jobs=-1
)
# 교차 검증 결과 확인
for metric in ["accuracy", "f1", "roc_auc"]:
scores = cv_results[f"test_{metric}"]
print(f"\n[{metric}]")
print("각 Fold 점수:", np.round(scores, 4))
print(f"평균: {scores.mean():.4f}")
print(f"표준편차: {scores.std():.4f}")
'''
[accuracy]
각 Fold 점수: [0.5299 0.5281 0.5336 0.5361 0.528 ]
평균: 0.5311
표준편차: 0.0032
[f1]
각 Fold 점수: [0.5374 0.539 0.5433 0.5508 0.5369]
평균: 0.5415
표준편차: 0.0052
[roc_auc]
각 Fold 점수: [0.5488 0.5377 0.543 0.5436 0.5366]
평균: 0.5420
표준편차: 0.0044
'''
평균 정확도가 0.5311이고 표준편차가 0.0032가 나왔다.
- 훈련 데이터를 서로 다르게 나눠 5번 검증했을 때, 평균적으로 약 53.11% 정도의 정확도를 보였다.
- 표준편차가 비교적 작아(< 0.05) 모델 성능이 비교적 안정적이라고 볼 수 있다.
Train 점수와 Validation 점수 비교하기
# Train 점수와 Validation 점수 비교
for metric in ["accuracy", "f1", "roc_auc"]:
train_scores = cv_results[f"train_{metric}"]
valid_scores = cv_results[f"test_{metric}"]
print(f"\n[{metric}]")
print(f"Train 평균: {train_scores.mean():.4f}")
print(f"Validation 평균: {valid_scores.mean():.4f}")
'''
[accuracy]
Train 평균: 0.6361
Validation 평균: 0.5311
[f1]
Train 평균: 0.6445
Validation 평균: 0.5415
[roc_auc]
Train 평균: 0.6887
Validation 평균: 0.5420
'''
훈련 데이터에서는 63~68% 정도를 맞췄지만, 검증 데이터에서는 53~54% 정도를 맞췄다. 약 10% 정도 차이가 나므로 정상이라고 하기에도, 과적합이라고 하기에도 애매하다.
최종 테스트 데이터 평가
교차 검증으로 모델 설정을 확인한 뒤에는 훈련 데이터 전체를 사용해서 모델을 학습한다.
# 최종 테스트 데이터 평가
clf.fit(X_train, y_train)
from sklearn.metrics import accuracy_score, roc_auc_score
y_pred = clf.predict(X_test)
y_prob = clf.predict_proba(X_test)[:, 1]
test_accuracy = accuracy_score(y_test, y_pred)
test_auc = roc_auc_score(y_test, y_prob)
print(f"최종 Test Accuracy: {test_accuracy:.4f}")
print(f"최종 Test ROC-AUC: {test_auc:.4f}")
# 최종 Test Accuracy: 0.5316
# 최종 Test ROC-AUC: 0.5409
하이퍼파라미터 찾기
LogisticRegression()에는 C라는 파라미터가 있다. C는 규제의 강도를 조절하며, C가 작을수록 규제가 강해져 모델이 단순해진다.
# 하이퍼파라미터 찾기
from sklearn.model_selection import GridSearchCV
param_grid = {
"model__C": [0.01, 0.1, 1, 10, 100] # "단계이름__파라미터이름"
}
grid_search = GridSearchCV(
estimator=clf,
param_grid=param_grid,
cv=cv,
scoring="roc_auc",
n_jobs=-1,
verbose=1
)
grid_search.fit(X_train, y_train)
print("최적 파라미터:", grid_search.best_params_)
print(f"교차 검증 최고 ROC-AUC: {grid_search.best_score_:.4f}")
# 최적 모델로 테스트 데이터 평가
best_model = grid_search.best_estimator_
y_pred = best_model.predict(X_test)
y_prob = best_model.predict_proba(X_test)[:, 1]
print("Test Accuracy:", accuracy_score(y_test, y_pred))
print("Test ROC-AUC:", roc_auc_score(y_test, y_prob))
'''
Fitting 5 folds for each of 5 candidates, totalling 25 fits
최적 파라미터: {'model__C': 0.1}
교차 검증 최고 ROC-AUC: 0.5446
Test Accuracy: 0.533210332103321
Test ROC-AUC: 0.5447767632563115
'''
🧩 모델 해석하기
로지스틱 회귀 계수로 모델 해석하기
- 계수가 양수: 팀1 승리 확률을 높이는 방향
- 계수가 음수: 팀1 승리 확률을 낮추는 방향
- 절댓값이 클수록 모델이 더 크게 반응하는 특성
우선 One-Hot Encoding 후 특성 이름을 가져온다.
best_model = grid_search.best_estimator_
preprocessor = best_model.named_steps["preprocessor"]
logistic_model = best_model.named_steps["model"]
feature_names = preprocessor.get_feature_names_out()
coefficients = logistic_model.coef_[0]
coef_df = pd.DataFrame({
"feature": feature_names,
"coefficient": coefficients
})
coef_df["abs_coefficient"] = coef_df["coefficient"].abs()
팀1 승리에 긍정적인 영향을 준 특성은 다음과 같다.
# 팀1 승리에 긍정적인 영향을 준 특성
top_positive = coef_df.sort_values(
"coefficient",
ascending=False
).head(15)
print(top_positive)
'''
feature coefficient abs_coefficient
1681 categorical__t1_ban1_222 0.468949 0.468949
2530 categorical__t2_ban2_432 0.450617 0.450617
47 categorical__t1_champ1id_50 0.406596 0.406596
604 categorical__t1_champ5id_56 0.399429 0.399429
178 categorical__t1_champ2id_40 0.395065 0.395065
2694 categorical__t2_ban4_21 0.382102 0.382102
228 categorical__t1_champ2id_106 0.377775 0.377775
1105 categorical__t2_champ4id_1 0.373558 0.373558
316 categorical__t1_champ3id_40 0.372024 0.372024
2387 categorical__t2_ban1_420 0.368459 0.368459
2617 categorical__t2_ban3_98 0.352809 0.352809
1834 categorical__t1_ban2_429 0.348614 0.348614
589 categorical__t1_champ5id_37 0.342789 0.342789
2870 categorical__t2_ban5_62 0.342428 0.342428
434 categorical__t1_champ4id_20 0.342226 0.342226
'''
categorical__t1_ban1_222: 팀1의 첫 번째 밴 카드로 ID 222가 등장했을 때, 다른 조건이 같다면 팀1 승리 확률이 높아지는 방향으로 모델이 학습했다.categorical__t1_champ1id_50: 팀1의 첫 번째 픽으로 ID 50이 등장했을 때, 다른 조건이 같다면 팀1 승리 확률이 높아지는 방향으로 모델이 학습했다.
팀1 승리에 부정적인 영향을 준 특성은 다음과 같다.
# 팀1 승리에 부정적인 영향을 준 특성
top_negative = coef_df.sort_values(
"coefficient",
ascending=True
).head(15)
print(top_negative)
'''
feature coefficient abs_coefficient
151 categorical__t1_champ2id_13 -0.598514 0.598514
552 categorical__t1_champ4id_516 -0.496437 0.496437
414 categorical__t1_champ3id_516 -0.483014 0.483014
1624 categorical__t1_ban1_69 -0.461521 0.461521
617 categorical__t1_champ5id_74 -0.446998 0.446998
1031 categorical__t2_champ3id_74 -0.415994 0.415994
2874 categorical__t2_ban5_68 -0.403847 0.403847
1925 categorical__t1_ban3_102 -0.388269 0.388269
1613 categorical__t1_ban1_56 -0.386127 0.386127
530 categorical__t1_champ4id_164 -0.382047 0.382047
168 categorical__t1_champ2id_30 -0.381820 0.381820
892 categorical__t2_champ2id_72 -0.380050 0.380050
2217 categorical__t1_ban5_120 -0.376469 0.376469
278 categorical__t1_champ3id_2 -0.375515 0.375515
1965 categorical__t1_ban3_254 -0.371410 0.371410
'''
categorical__t1_champ2id_13: 팀1의 두 번째 픽으로 ID 92가 등장했을 때, 팀1 입장에서는 불리한 방향으로 학습되었다.
# 가장 영향력이 큰 계수 시각화
top_features = coef_df.nlargest(15, "abs_coefficient")
plt.figure(figsize=(10, 7))
sns.barplot(
data=top_features,
x="coefficient",
y="feature",
hue="coefficient",
palette="coolwarm",
legend=False
)
plt.axvline(0, color="black", linestyle="--")
plt.title("로지스틱 회귀의 영향력이 큰 특성 Top 15")
plt.xlabel("계수")
plt.ylabel("One-Hot Encoding 특성")
plt.tight_layout()
plt.show()

Permutation Importance로 해석하기
계수 해석은 One-Hot Encoding된 개별 특성을 중심으로 본다 하지만 원래 컬럼 단위로 "어떤 입력 변수가 중요한가?"를 보고 싶다면 permutation_importance를 사용할 수 있다.
- 원래 모델 성능을 측정
- 특정 컬럼의 값을 무작위로 섞음
- 성능이 얼마나 떨어지는지 측정
- 성능이 많이 떨어질수록 중요한 변수라고 판단
# Permutation Importance로 해석하기
from sklearn.inspection import permutation_importance
perm_result = permutation_importance(
estimator=best_model,
X=X_test,
y=y_test,
scoring="roc_auc",
n_repeats=10,
random_state=42,
n_jobs=-1
)
importance_df = pd.DataFrame({
"feature": X_test.columns,
"importance_mean": perm_result.importances_mean,
"importance_std": perm_result.importances_std
})
importance_df = importance_df.sort_values(
"importance_mean",
ascending=False
)
importance_df.head(15)
top_importance = importance_df.head(15)
plt.figure(figsize=(10, 6))
sns.barplot(
data=top_importance,
x="importance_mean",
y="feature",
hue="feature",
legend=False,
palette="viridis"
)
plt.title("Permutation Importance Top 15")
plt.xlabel("ROC-AUC 감소량")
plt.ylabel("원본 특성")
plt.show()

importance_mean이 크다면 "해당 컬럼을 섞었더니 모델의 ROC-AUC가 크게 떨어졌다. 즉, 모델이 그 컬럼 정보를 예측에 유의미하게 사용하고 있다"는 뜻이다.
모델 해석 시 주의할 점
- 계수가 인과관계를 의미하지 않는다:
categorical__t1_champ1id_50의 계수가 높게 나왔지만, 그것이 "스웨인(ID 50)을 픽하면 반드시 승리한다"는 의미는 아니다. - 챔피언 슬롯의 의미를 확인해야 한다:
t1_champ1id가 항상 탑라이너인지, 첫번째 픽을 하는 사람인지, 아니면 랜덤인지 확인해야 한다. (여기서는 '첫번째 픽을 하는 사람'으로 보고 해석했다.) - 챔피언 조합 시너지는 로지스틱 회귀로 잘 표현하지 못할 수 있다: "챔피언 A를 상대로 챔피언 B가 카운터다"와 같은 복잡한 상호작용은 신경망 모델 등을 사용해 학습해야 한다.
🧩 전체 검증 흐름 요약
- 전체 데이터를 train / test로 분리
- train 데이터만 사용하여 5-Fold 교차 검증
- 평균 Accuracy, F1, ROC-AUC 확인
- GridSearchCV로 최적 C 값 찾기
- 최적 모델을 train 전체 데이터로 학습
- test 데이터로 최종 성능 평가
- 계수와 Permutation Importance로 모델 해석
'CS > Python' 카테고리의 다른 글
| 파이썬으로 MLP 찍먹하기 (0) | 2026.08.11 |
|---|---|
| 파이썬의 PyTorch 라이브러리에 대해 알아보기 (0) | 2026.08.10 |
| 파이썬으로 머신러닝 찍먹하기 (0) | 2026.08.07 |
| 파이썬으로 데이터 전처리 찍먹하기 (0) | 2026.08.07 |
| 파이썬으로 데이터 시각화 찍먹하기 (0) | 2026.08.06 |