Skip to content

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 PositivePredicted Negative
Actual PositiveTrue Positive (TP)False Negative (FN)
Actual NegativeFalse 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 0Predicted 1
Actual 0TN = 5FP = 1
Actual 1FN = 2TP = 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:  7

Precision, Recall, F1-Score, Accuracy

이 지표들은 혼동 행렬에서 직접 계산됩니다:

MetricFormulaWhat It Answers
Accuracy(TP + TN) / (TP + TN + FP + FN)전체 예측 중 얼마나 맞았나?
PrecisionTP / (TP + FP)양성이라고 예측한 것 중 실제 양성은 얼마나 되나?
Recall (Sensitivity)TP / (TP + FN)실제 양성 중에서 얼마나 잡아냈나?
SpecificityTN / (TN + FP)실제 음성 중에서 얼마나 정확히 걸러냈나?
F1-Score2 * (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.8235

Precision과 Recall 중 무엇을 우선해야 할까?

ScenarioPrioritizeWhy
스팸 탐지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 파라미터는 세 가지 옵션을 받습니다:

ValueNormalizationUse 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}")
AverageMethodBest For
micro모든 클래스의 TP/FP/FN을 합산클래스 불균형의 영향이 중요할 때
macro클래스별 지표의 단순 평균모든 클래스가 equally important일 때
weightedsupport로 가중 평균불균형 데이터에서 기본 선택지

전체 예시: 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_matrixclassification_report로 전체 상황을 파악하고, ConfusionMatrixDisplay 또는 seaborn heatmap으로 발표/리포트용 시각화를 만들 수 있습니다. 클래스 크기가 다르면 정규화를 활용하세요. 마지막으로 각 오류 유형의 비즈니스 비용에 따라 핵심 지표를 선택해야 합니다. 위양성이 비싸면 precision, 위음성이 위험하면 recall, 둘의 균형이 필요하면 F1-score가 적합합니다.

📚