"F1, ROC-AUC, PR-AUC 중에 뭘 대표 지표로 써야 하나요?"라는 질문 자체가 잘못된 질문이라면? 불균형 데이터 다루기 4-5에서 다양한 지표를 배웠습니다. 이번 글에서는 그 지표들 중 무엇을 선택할지 결정하는 더 근본적인 질문을 던집니다. 정답은 통계 공식이 아니라, "어떤 오류가 더 비싼가?"라는 업무적 질문에서 시작됩니다.
두 종류의 실수, 다른 무게
집에 불이 났는데 경보기가 울리지 않는 것(거짓 음성)과, 아무 일도 없는데 경보기가 울려 소방차가 출동하는 것(거짓 양성) 중 어느 쪽이 더 큰 문제일까요? 대부분의 사람은 첫 번째가 훨씬 심각하다고 답할 것입니다. 하지만 만약 경보기가 100번 중 99번을 헛경보로 울린다면, 사람들은 결국 경보기를 무시하게 되고, 이는 다시 첫 번째 문제(진짜 화재를 놓침)로 이어집니다. 두 오류는 종류도, 무게도 다르며, 그 무게는 통계가 아니라 상황이 결정합니다. 이 번 글은 이 무게를 명시적으로 정의하는 방법을 다룹니다.
"어떤 오류가 더 비싼가?"를 출발점으로 삼기
이 시리즈에서 반복해서 강조하는 원칙입니다. 지표를 고르기 전에 먼저 다음 질문에 답해야 합니다.
- 거짓 음성(FN, 놓침)의 비용은 얼마인가? (예: 사기 거래 금액, 놓친 질병의 치료 지연 비용)
- 거짓 양성(FP, 오탐)의 비용은 얼마인가? (예: 불필요한 조사 인력 비용, 불필요한 재검사 비용)
- 두 비용의 비율은 대략 어느 정도인가?
이 질문에 대한 답이 나오기 전까지는, F1이든 ROC-AUC든 어떤 지표도 "정답"이 될 수 없습니다.
비용 행렬과 기대 비용
비용 행렬(Cost Matrix)은 혼동행렬의 네 칸 각각에 실제 비용을 대응시킨 표입니다.
| 예측: 양성 | 예측: 음성 | |
|---|---|---|
| 실제: 양성 | 비용 0 (올바른 조치) | FN 비용 (놓침의 대가) |
| 실제: 음성 | FP 비용 (오탐의 대가) | 비용 0 (올바른 판단) |
이를 이용해 기대 비용(Expected Cost)을 계산할 수 있습니다.
기대 비용 = (FP 개수 × FP 단위 비용) + (FN 개수 × FN 단위 비용)
Recall@Precision과 Precision@Recall
업무 요구사항이 "Precision은 최소 50% 이상 유지하면서 Recall을 최대화하라"처럼 한쪽 지표에 최소 기준을 걸고 다른 쪽을 최적화하는 형태로 주어지는 경우가 많습니다.
- Recall@Precision(x): Precision이 x 이상인 조건을 만족하는 임곗값들 중, Recall이 가장 높은 값.
- Precision@Recall(x): Recall이 x 이상인 조건을 만족하는 임곗값들 중, Precision이 가장 높은 값.
이 두 지표는 F1처럼 두 값을 뭉뚱그리지 않고, 업무에서 정말로 지켜야 할 제약 조건을 명시적으로 반영한다는 장점이 있습니다.
Top-k 지표: 운영 용량을 반영하는 평가
나중에 실제 사례로 다시 다루겠지만, 실무에는 종종 "하루에 조사할 수 있는 건수는 몇 건까지"와 같은 물리적 용량 제약이 있습니다. 이런 상황에서는 임곗값이 아니라 "위험도 순으로 상위 k건"을 기준으로 평가하는 것이 훨씬 현실적입니다.
Precision@k = 상위 k건 중 실제 양성 개수 / k
Recall@k = 상위 k건 중 실제 양성 개수 / 전체 양성 개수

작은 숫자 예제
FN 비용을 100(단위), FP 비용을 5(단위)로 가정한 사기 탐지 상황에서, 임곗값 0.5로 예측했을 때 FP=45건, FN=62건이 나왔다고 합시다.
기대 비용(0.5 임곗값) = 45×5 + 62×100 = 225 + 6,200 = 6,425
만약 임곗값을 낮춰 FP=680건, FN=17건이 되었다면,
기대 비용(낮춘 임곗값) = 680×5 + 17×100 = 3,400 + 1,700 = 5,100
F1만 보면 오탐이 크게 늘어난 두 번째 경우가 "더 나쁜 모델"처럼 보일 수 있지만, 실제 비용은 오히려 더 낮습니다. 비용을 명시적으로 계산하지 않으면 이런 차이를 놓칠 수 있습니다.
Python 실습: 사기 탐지의 조사 인력 제한을 반영한 top-k 평가
# 설치가 필요한 환경에서 한 번만 실행
# !pip install -q scikit-learn numpy
import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import precision_recall_curve
RANDOM_STATE = 42
X, y = make_classification(
n_samples=5000, n_features=10, n_informative=5, n_redundant=2,
weights=[0.95, 0.05], class_sep=1.0, random_state=RANDOM_STATE,
)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, stratify=y, random_state=RANDOM_STATE
)
model = LogisticRegression(max_iter=2000, random_state=RANDOM_STATE).fit(X_train, y_train)
probs = model.predict_proba(X_test)[:, 1]
# 1. 비용 기반 임곗값 비교
FN_COST, FP_COST = 100, 5
precisions, recalls, thresholds = precision_recall_curve(y_test, probs)
costs = []
for t in thresholds:
preds = (probs >= t).astype(int)
fp = np.sum((preds == 1) & (y_test == 0))
fn = np.sum((preds == 0) & (y_test == 1))
costs.append(fp * FP_COST + fn * FN_COST)
costs = np.array(costs)
best_idx = np.argmin(costs)
th_at_05_idx = np.argmin(np.abs(thresholds - 0.5))
print(f"임곗값 0.5의 기대 비용: {costs[th_at_05_idx]:,}")
print(f"비용 최소화 임곗값({thresholds[best_idx]:.4f})의 기대 비용: {costs[best_idx]:,}")
# 2. Recall@Precision, Precision@Recall
target_precision = 0.5
mask = precisions[:-1] >= target_precision
if mask.any():
idxs = np.where(mask)[0]
best_recall_idx = idxs[np.argmax(recalls[:-1][idxs])]
print(f"\nRecall@Precision>={target_precision}: {recalls[best_recall_idx]:.4f} "
f"(임곗값={thresholds[best_recall_idx]:.4f})")
target_recall = 0.7
mask2 = recalls[:-1] >= target_recall
if mask2.any():
idxs2 = np.where(mask2)[0]
best_prec_idx = idxs2[np.argmax(precisions[:-1][idxs2])]
print(f"Precision@Recall>={target_recall}: {precisions[best_prec_idx]:.4f} "
f"(임곗값={thresholds[best_prec_idx]:.4f})")
# 3. Top-k 지표: 조사 인력이 하루 30건으로 제한된 경우
K = 30
top_k_idx = np.argsort(-probs)[:K]
precision_at_k = y_test[top_k_idx].sum() / K
recall_at_k = y_test[top_k_idx].sum() / y_test.sum()
print(f"\nPrecision@{K}: {precision_at_k:.4f}")
print(f"Recall@{K}: {recall_at_k:.4f}")
실행 결과:
임곗값 0.5의 기대 비용: 7,415
비용 최소화 임곗값(0.0739)의 기대 비용: 3,405
Recall@Precision>=0.5: 0.3415 (임곗값=0.2579)
Precision@Recall>=0.7: 0.1850 (임곗값=0.0802)
Precision@30: 0.6667
Recall@30: 0.2439
비용 기준으로 임곗값을 재조정하자, 기대 비용이 7,415에서 3,405로 절반 이하로 줄었습니다. FN 비용(100)이 FP 비용(5)보다 20배 크기 때문에, 더 낮은 임곗값(더 민감한 설정)을 선택하는 것이 합리적이었습니다.
Recall@Precision과 Precision@Recall은 서로 다른 질문에 답합니다. "Precision을 50% 이상 유지하면서 최대 Recall은?"이라는 질문에는 34.15%라는 답이, "Recall을 70% 이상 확보하면서 최대 Precision은?"이라는 질문에는 18.50%라는 답이 나옵니다. 어느 쪽 질문이 우리 업무에 맞는 질문인지가, 어느 지표를 봐야 하는지를 결정합니다.
Top-30 지표는 또 다른 그림을 보여줍니다. 상위 30건 중 66.67%가 실제 사기이며, 이는 전체 사기의 24.39%를 커버합니다. 이 숫자는 "조사 인력이 하루 30건만 처리할 수 있다"는 현실적 제약 아래에서, 우리가 기대할 수 있는 가장 직관적인 성과 지표입니다.

흔한 실수
- 비용을 정의하지 않고 임의로 F1을 대표 지표로 정하는 것. F1은 Precision과 Recall에 동일한 가중치를 부여한다는 암묵적 가정을 담고 있으며, 이것이 업무 현실과 다를 수 있다.
- Recall@Precision과 Precision@Recall을 혼동하는 것. 어느 지표를 "고정 조건"으로 사용하고 어느 지표를 "최적화 대상"으로 사용하는지 명확히 구분해야 한다.
- Top-k 지표에서 k 값을 실제 운영 용량과 무관하게 임의로 정하는 것. k는 반드시 실제 조사 인력, 예산 등 현실적 제약을 반영해야 의미가 있다.
- FN/FP 비용을 한 번 정하고 다시는 검토하지 않는 것. 비용은 시간이 지나며 바뀔 수 있으므로(예: 조사 인력 증원, 사기 수법의 고도화), 주기적으로 재검토해야 한다.
한 걸음 더: 지표 선택은 통계가 아니라 협상이다
이번 글의 결론은 다소 도발적으로 들릴 수 있습니다. "어떤 지표가 최선인가"는 순수한 통계적 질문이 아니라, 이해관계자들과의 협상을 통해 정해지는 업무적 결정입니다. 조사 인력 담당자는 Precision을, 리스크 관리 담당자는 Recall을 중시할 수 있고, 경영진은 기대 비용이라는 하나의 숫자로 통합된 지표를 원할 수 있습니다. 데이터 과학자의 역할은 "정답 지표"를 찾아내는 것이 아니라, 이런 서로 다른 우선순위를 비용 행렬이나 Recall@Precision 같은 명시적인 형태로 번역하여, 모두가 같은 기준으로 대화할 수 있게 만드는 것입니다.
'머신러닝' 카테고리의 다른 글
| 불균형 데이터 다루기 8 - 다수 데이터를 줄이는 언더샘플링 (0) | 2026.09.12 |
|---|---|
| 불균형 데이터 다루기 7 - 분할·교차검증·데이터 누수 방지 (0) | 2026.09.12 |
| 불균형 데이터 다루기 5 - ROC 곡선과 PR 곡선을 제대로 읽는 법 (0) | 2026.09.11 |
| 불균형 데이터 다루기 4 - 혼동행렬에서 시작하는 분류 지표 (1) | 2026.09.11 |
| 불균형 데이터 다루기 3 - 불균형 문제를 발견하고 진단하는 법 (0) | 2026.09.11 |