Sklearn의 혼동 행렬(Confusion Matrix): 분류 모델을 평가하는 방법
Updated on
분류 모델이 정확도 95%를 보고하길래 배포했다고 해봅시다. 그런데 막상 운영해 보니, 실제로 중요한 양성(positive) 사례—사기 거래, 질병 진단, 불량 제품—의 80%를 놓치고 있었다는 사실을 알게 됩니다. 정확도(accuracy) 하나만으로는 모델이 어디에서, 어떻게 실패하는지를 가려버립니다.
정확도라는 단일 숫자는 모든 유형의 오류를 한 지표로 뭉뚱그립니다. 예를 들어 스팸 메일이 전체의 5%뿐이라면, 스팸 필터가 모든 스팸을 그냥 통과시키고 정상 메일만 정확히 분류하더라도 여전히 높은 정확도를 얻을 수 있습니다. 필요한 것은 전체 그림입니다. 모델이 양성을 얼마나 잡아내는지, 음성을 얼마나 잘 구분하는지, 그리고 오류가 정확히 어디에서 발생하는지 확인해야 합니다.
혼동 행렬(confusion matrix)은 모델 성능을 네 가지 구성요소—진양성(true positives), 진음성(true negatives), 위양성(false positives), 위음성(false negatives)—로 분해합니다. 여기에 precision, recall, F1-score 같은 파생 지표를 함께 보면, 모델이 무엇을 잘하고 무엇을 잘못하는지에 대한 실행 가능한 인사이트를 얻을 수 있습니다. Scikit-learn은 confusion_matrix, classification_report, ConfusionMatrixDisplay를 제공하여 이 분석을 쉽게 해줍니다.
혼동 행렬(Confusion Matrix)이란?
혼동 행렬은 분류 모델의 예측 라벨과 실제 라벨을 비교하는 표입니다. 이진 분류(binary classification)의 경우 2x2 그리드로 표현됩니다:
| Predicted Positive | Predicted Negative | |
|---|---|---|
| Actual Positive | True Positive (TP) | False Negative (FN) |
| Actual Negative | False Positive (FP) | True Negative (TN) |
각 셀은 해당 범주에 속하는 샘플 개수를 셉니다:
- True Positive (TP): 모델이 양성이라고 예측했고, 실제로도 양성. 정답.
- True Negative (TN): 모델이 음성이라고 예측했고, 실제로도 음성. 정답.
- False Positive (FP): 모델이 양성이라고 예측했지만, 실제는 음성. 제1종 오류(Type I error).
- False Negative (FN): 모델이 음성이라고 예측했지만, 실제는 양성. 제2종 오류(Type II error).
Sklearn로 기본 혼동 행렬 만들기
from sklearn.metrics import confusion_matrix
import numpy as np
# Actual and predicted labels
y_true = [1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1]
y_pred = [1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1]
cm = confusion_matrix(y_true, y_pred)
print(cm)
# [[5 1]
# [2 7]]이 출력은 이렇게 읽습니다: sklearn은 행 0 = 실제 음성(actual negative), 행 1 = 실제 양성(actual positive) 순서로 행렬을 배치합니다.
| Predicted 0 | Predicted 1 | |
|---|---|---|
| Actual 0 | TN = 5 | FP = 1 |
| Actual 1 | FN = 2 | TP = 7 |
즉, 모델은 음성 5개와 양성 7개를 정확히 맞췄고, 위양성 1개와 위음성 2개의 오류를 냈습니다.
개별 값 추출하기
from sklearn.metrics import confusion_matrix
y_true = [1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1]
y_pred = [1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1]
cm = confusion_matrix(y_true, y_pred)
tn, fp, fn, tp = cm.ravel()
print(f"True Negatives: {tn}")
print(f"False Positives: {fp}")
print(f"False Negatives: {fn}")
print(f"True Positives: {tp}")
# True Negatives: 5
# False Positives: 1
# False Negatives: 2
# True Positives: 7Precision, Recall, F1-Score, Accuracy
이 지표들은 혼동 행렬에서 직접 계산됩니다:
| Metric | Formula | What It Answers |
|---|---|---|
| Accuracy | (TP + TN) / (TP + TN + FP + FN) | 전체 예측 중 얼마나 맞았나? |
| Precision | TP / (TP + FP) | 양성이라고 예측한 것 중 실제 양성은 얼마나 되나? |
| Recall (Sensitivity) | TP / (TP + FN) | 실제 양성 중에서 얼마나 잡아냈나? |
| Specificity | TN / (TN + FP) | 실제 음성 중에서 얼마나 정확히 걸러냈나? |
| F1-Score | 2 * (Precision * Recall) / (Precision + Recall) | precision과 recall의 조화평균 |
from sklearn.metrics import (
accuracy_score, precision_score, recall_score, f1_score
)
y_true = [1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1]
y_pred = [1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1]
print(f"Accuracy: {accuracy_score(y_true, y_pred):.4f}")
print(f"Precision: {precision_score(y_true, y_pred):.4f}")
print(f"Recall: {recall_score(y_true, y_pred):.4f}")
print(f"F1-Score: {f1_score(y_true, y_pred):.4f}")
# Accuracy: 0.8000
# Precision: 0.8750
# Recall: 0.7778
# F1-Score: 0.8235Precision과 Recall 중 무엇을 우선해야 할까?
| Scenario | Prioritize | Why |
|---|---|---|
| 스팸 탐지 | Precision | 위양성(정상 메일을 스팸으로 분류)이 사용자에게 불편을 줌 |
| 질병 스크리닝 | Recall | 위음성(질병을 놓침)은 위험함 |
| 사기 탐지 | Recall | 사기를 놓치는 비용이 오탐을 조사하는 비용보다 큼 |
| 검색 결과 | Precision | 관련 없는 결과는 사용자 경험을 악화시킴 |
| 제조 불량 탐지 | Recall | 불량품이 고객에게 전달되면 비용이 큼 |
| 콘텐츠 추천 | Precision | 무관한 추천은 참여도를 떨어뜨림 |
Classification Report
Sklearn의 classification_report는 클래스별 precision, recall, F1-score, support(실제 발생 수)를 한 번에 계산합니다:
from sklearn.metrics import classification_report
y_true = [1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1]
y_pred = [1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1]
print(classification_report(y_true, y_pred, target_names=['Negative', 'Positive']))Output:
precision recall f1-score support
Negative 0.71 0.83 0.77 6
Positive 0.88 0.78 0.82 9
accuracy 0.80 15
macro avg 0.80 0.81 0.80 15
weighted avg 0.81 0.80 0.80 15- macro avg: 클래스별 평균을 가중치 없이 계산. 모든 클래스를 동일하게 취급.
- weighted avg: support로 가중치를 둔 평균. 클래스 불균형을 반영.
- support: 각 클래스의 실제 샘플 수.
혼동 행렬 시각화하기
ConfusionMatrixDisplay 사용
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_breast_cancer
import matplotlib.pyplot as plt
# Load and split data
data = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
data.data, data.target, test_size=0.2, random_state=42
)
# Train model
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
# Plot confusion matrix
cm = confusion_matrix(y_test, y_pred)
disp = ConfusionMatrixDisplay(
confusion_matrix=cm,
display_labels=data.target_names
)
disp.plot(cmap='Blues')
plt.title('Breast Cancer Classification')
plt.tight_layout()
plt.savefig('confusion_matrix.png', dpi=150)
plt.show()Seaborn Heatmap 사용
더 많은 커스터마이징이 필요하다면 seaborn을 직접 사용합니다:
from sklearn.metrics import confusion_matrix
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_breast_cancer
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
# Load, split, train
data = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
data.data, data.target, test_size=0.2, random_state=42
)
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
# Create confusion matrix
cm = confusion_matrix(y_test, y_pred)
# Plot with seaborn
plt.figure(figsize=(8, 6))
sns.heatmap(
cm,
annot=True,
fmt='d',
cmap='Blues',
xticklabels=data.target_names,
yticklabels=data.target_names,
square=True,
linewidths=0.5
)
plt.xlabel('Predicted Label')
plt.ylabel('True Label')
plt.title('Confusion Matrix - Breast Cancer Classification')
plt.tight_layout()
plt.savefig('confusion_matrix_seaborn.png', dpi=150)
plt.show()정규화된(Normalized) 혼동 행렬
원시 카운트(raw counts)는 클래스 크기가 다를 때 오해를 불러올 수 있습니다. 정규화하면 비율(proportion)을 보여줍니다:
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_breast_cancer
import matplotlib.pyplot as plt
data = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
data.data, data.target, test_size=0.2, random_state=42
)
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
# Normalized confusion matrix (by true labels)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Raw counts
ConfusionMatrixDisplay.from_predictions(
y_test, y_pred,
display_labels=data.target_names,
cmap='Blues',
ax=axes[0]
)
axes[0].set_title('Raw Counts')
# Normalized (rows sum to 1)
ConfusionMatrixDisplay.from_predictions(
y_test, y_pred,
display_labels=data.target_names,
normalize='true',
cmap='Blues',
values_format='.2%',
ax=axes[1]
)
axes[1].set_title('Normalized by True Label')
plt.tight_layout()
plt.savefig('confusion_matrix_normalized.png', dpi=150)
plt.show()normalize 파라미터는 세 가지 옵션을 받습니다:
| Value | Normalization | Use Case |
|---|---|---|
'true' | 각 행의 합이 1(실제 클래스 개수로 나눔) | 클래스별 recall을 보기 |
'pred' | 각 열의 합이 1(예측 클래스 개수로 나눔) | 클래스별 precision을 보기 |
'all' | 전체 셀의 합이 1(전체 개수로 나눔) | 전체 분포를 보기 |
다중 클래스(Multi-Class) 혼동 행렬
혼동 행렬은 두 클래스보다 많은 경우에도 자연스럽게 확장됩니다. 각 행은 실제 클래스, 각 열은 예측 클래스를 의미합니다:
from sklearn.metrics import confusion_matrix, classification_report, ConfusionMatrixDisplay
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt
# Load iris dataset (3 classes)
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
iris.data, iris.target, test_size=0.3, random_state=42
)
# Train and predict
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
# Confusion matrix
cm = confusion_matrix(y_test, y_pred)
print("Confusion Matrix:")
print(cm)
# Classification report
print("\nClassification Report:")
print(classification_report(
y_test, y_pred,
target_names=iris.target_names
))
# Visualize
disp = ConfusionMatrixDisplay(
confusion_matrix=cm,
display_labels=iris.target_names
)
disp.plot(cmap='Blues')
plt.title('Iris Classification - 3 Classes')
plt.tight_layout()
plt.savefig('multi_class_confusion_matrix.png', dpi=150)
plt.show()다중 클래스 평균(averaging) 전략
다중 클래스 문제에서 precision/recall/F1을 계산할 때는 averaging 방법을 선택해야 합니다:
from sklearn.metrics import precision_score, recall_score, f1_score
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
iris.data, iris.target, test_size=0.3, random_state=42
)
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
for avg in ['micro', 'macro', 'weighted']:
p = precision_score(y_test, y_pred, average=avg)
r = recall_score(y_test, y_pred, average=avg)
f1 = f1_score(y_test, y_pred, average=avg)
print(f"{avg:8s} -- Precision: {p:.4f}, Recall: {r:.4f}, F1: {f1:.4f}")| Average | Method | Best For |
|---|---|---|
micro | 모든 클래스의 TP/FP/FN을 합산 | 클래스 불균형의 영향이 중요할 때 |
macro | 클래스별 지표의 단순 평균 | 모든 클래스가 equally important일 때 |
weighted | support로 가중 평균 | 불균형 데이터에서 기본 선택지 |
전체 예시: End-to-End 분류 평가
from sklearn.metrics import (
confusion_matrix, classification_report, ConfusionMatrixDisplay,
accuracy_score, precision_score, recall_score, f1_score, roc_auc_score
)
from sklearn.model_selection import train_test_split
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.datasets import load_breast_cancer
import matplotlib.pyplot as plt
import numpy as np
# Load data
data = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
data.data, data.target, test_size=0.2, random_state=42, stratify=data.target
)
# Build pipeline
pipeline = Pipeline([
('scaler', StandardScaler()),
('model', GradientBoostingClassifier(
n_estimators=200, max_depth=3, random_state=42
))
])
# Train
pipeline.fit(X_train, y_train)
y_pred = pipeline.predict(X_test)
y_prob = pipeline.predict_proba(X_test)[:, 1]
# Confusion matrix
cm = confusion_matrix(y_test, y_pred)
tn, fp, fn, tp = cm.ravel()
print("=" * 50)
print("MODEL EVALUATION REPORT")
print("=" * 50)
print(f"\nConfusion Matrix:")
print(f" TP={tp}, FP={fp}")
print(f" FN={fn}, TN={tn}")
print(f"\nAccuracy: {accuracy_score(y_test, y_pred):.4f}")
print(f"Precision: {precision_score(y_test, y_pred):.4f}")
print(f"Recall: {recall_score(y_test, y_pred):.4f}")
print(f"F1-Score: {f1_score(y_test, y_pred):.4f}")
print(f"ROC-AUC: {roc_auc_score(y_test, y_prob):.4f}")
print(f"\nDetailed Report:")
print(classification_report(y_test, y_pred, target_names=data.target_names))
# Visualize
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
ConfusionMatrixDisplay.from_predictions(
y_test, y_pred, display_labels=data.target_names,
cmap='Blues', ax=axes[0]
)
axes[0].set_title('Raw Counts')
ConfusionMatrixDisplay.from_predictions(
y_test, y_pred, display_labels=data.target_names,
normalize='true', values_format='.1%', cmap='Blues', ax=axes[1]
)
axes[1].set_title('Normalized')
plt.tight_layout()
plt.savefig('full_evaluation.png', dpi=150)
plt.show()PyGWalker로 분류 결과 더 깊게 탐색하기
혼동 행렬을 만든 다음에는, 원시 데이터를 인터랙티브하게 탐색하며 오분류(misclassification)를 더 깊게 파고들 수 있습니다. PyGWalker (opens in a new tab)는 예측 결과를 Jupyter에서 드래그 앤 드롭 기반의 시각 분석 인터페이스로 바꿔줍니다:
import pandas as pd
import pygwalker as pyg
# Build results DataFrame with features and predictions
results = pd.DataFrame(X_test, columns=data.feature_names)
results['actual'] = y_test
results['predicted'] = y_pred
results['correct'] = y_test == y_pred
results['confidence'] = y_prob
# Launch interactive exploration
walker = pyg.walk(results)오분류된 샘플만 필터링하고, TP/FP/FN/TN 그룹 간 feature 분포를 비교하여, 모델이 어려워하는 구간을 설명하는 패턴을 찾아낼 수 있습니다.
또한 Jupyter에서 분류 실험을 반복하면서—threshold를 조정하거나, 다른 모델을 테스트하거나, feature 조합을 탐색하는 등—실험 루프를 빠르게 돌리고 싶다면, RunCell (opens in a new tab)은 이를 가속하는 AI agent를 제공합니다.
FAQ
What is a confusion matrix in sklearn?
혼동 행렬은 각 클래스에 대해 올바른 예측과 잘못된 예측의 개수를 보여주는 표입니다. sklearn에서 confusion_matrix(y_true, y_pred)는 “행=실제 클래스, 열=예측 클래스”인 2D numpy array를 반환합니다. 이진 분류에서는 TP, TN, FP, FN을 보여줍니다.
How do I read a confusion matrix?
sklearn의 혼동 행렬에서 행은 실제 라벨, 열은 예측 라벨입니다. 이진 분류의 경우: 좌상단은 진음성(TN), 우상단은 위양성(FP), 좌하단은 위음성(FN), 우하단은 진양성(TP)입니다. 대각선 요소는 올바른 예측입니다.
What is the difference between precision and recall?
Precision은 예측을 양성이라고 한 것 중 실제 양성의 비율(TP / (TP + FP))입니다. Recall은 실제 양성 중 모델이 찾아낸 비율(TP / (TP + FN))입니다. Precision은 “모델이 양성이라고 말했을 때 얼마나 맞나?”를, recall은 “전체 양성 중에서 얼마나 찾았나?”를 답합니다.
When should I use F1-score instead of accuracy?
클래스가 불균형할 때 F1-score를 사용하세요. 샘플의 95%가 음성이라면, 항상 음성만 예측하는 모델도 정확도 95%를 얻지만 양성 recall은 0%입니다. F1-score는 precision과 recall의 조화평균이므로 한쪽을 희생하는 모델을 불리하게 평가합니다.
How do I plot a confusion matrix in Python?
가장 빠른 방법은 ConfusionMatrixDisplay.from_predictions(y_true, y_pred)를 사용하는 것입니다. 더 많은 커스터마이징이 필요하면 confusion_matrix()로 행렬을 만든 뒤 seaborn.heatmap()으로 시각화하세요. 두 방식 모두 정규화, 컬러맵, 클래스 라벨 등을 지원합니다.
What does normalize='true' do in ConfusionMatrixDisplay?
normalize='true'는 각 행을 해당 실제 클래스의 전체 샘플 수로 나누어, 각 행의 합이 1이 되게 만듭니다. 이는 클래스별 recall을 퍼센트로 보여줍니다. normalize='pred'는 클래스별 precision을, normalize='all'은 전체 비율을 보여줍니다.
결론
혼동 행렬은 분류 모델 평가의 기반입니다. 정확도만으로는 충분하지 않으며, 모델이 어떤 유형의 오류를 내는지 구체적으로 봐야 합니다. sklearn의 confusion_matrix와 classification_report로 전체 상황을 파악하고, ConfusionMatrixDisplay 또는 seaborn heatmap으로 발표/리포트용 시각화를 만들 수 있습니다. 클래스 크기가 다르면 정규화를 활용하세요. 마지막으로 각 오류 유형의 비즈니스 비용에 따라 핵심 지표를 선택해야 합니다. 위양성이 비싸면 precision, 위음성이 위험하면 recall, 둘의 균형이 필요하면 F1-score가 적합합니다.