콘텐츠로 이동

P7-2.1 비교표와 오류 사례 읽기

Section ID: P7-2.1 Version: v2026.08.01

비교표를 읽을 때는 baseline_result, candidate_result, metric_delta, error_sample_id, error_pattern, review_note를 함께 봅니다. 점수 하나보다 어떤 오류 사례가 남았는지가 다음 반복의 출발점입니다.

예측 모델을 하나 붙여 보고, 그 결과를 비교표와 오류 사례로 읽기 시작합니다.

하지만 여기서도 출발점은 모델 이름이 아닙니다. 먼저 문제 정의, 데이터 분리, 기준점(baseline), 비교 가능한 출력이라는 네 가지를 갖춰야 합니다. 예측 프로젝트에서 먼저 중요한 일은 높은 점수를 내는 것이 아니라, 비교 가능한 방식으로 기준점과 모델을 나란히 놓는 데 있습니다.

비교 가능한 예측 기록

  • 예측 모델 프로젝트는 어떤 구조로 시작하면 좋은가?
  • 학습(train)과 평가(test)를 분리해 기록하는 이유는 무엇인가?
  • baseline 없이 모델 정확도(accuracy)만 보면 왜 위험한가?
  • NumPy만으로도 실제 운영형 분류 실습 흐름을 어떻게 확인할 수 있는가?

분류 프로젝트의 최소 기록 구조를 잡는 데 집중합니다. 즉, 문제 -> 데이터 분리 -> baseline -> 모델 -> 비교로 프로젝트 문서를 어떻게 시작할지까지를 먼저 닫습니다.

예측 모델 프로젝트는 여기서 문제 정의와 baseline 문서부터 출발합니다. train/test 분리와 비교 가능한 기록 뼈대를 먼저 고정합니다.

Part 7에서 기준점(baseline)을 단순한 임시 숫자가 아니라 비교 기준선으로 다시 잡아야 할 때는, 이 절과 개념사전의 기준선(baseline) 항목으로 돌아와 뜻을 함께 다시 확인하면 됩니다.

점수와 오류를 함께 읽는 기준

  • 예측 프로젝트를 문제 -> 데이터 분리 -> baseline -> 모델 -> 비교 흐름으로 설명할 수 있습니다.
  • baseline과 모델 성능을 함께 기록해야 하는 이유를 말할 수 있습니다.
  • 운영형 예측 사례에서 샘플별 예측과 실패 목록을 직접 읽을 수 있습니다.

프로젝트 질문 설정

이번 실습은 구독 고객의 이탈 위험(churn risk)을 예측할 수 있는가?라는 질문에서 시작합니다. 이 질문은 실제 서비스 운영과 가깝고, 최근 문의 수, 최근 미접속 일수, 최근 사용량처럼 서로 다른 스케일의 특징(feature)을 함께 다루게 해 주기 때문에 비교표와 오류 사례를 같이 읽기 좋습니다.

예측 프로젝트를 시작할 때 바로 필요한 판단 기준을 먼저 표로 고정하면 다음과 같습니다.

질문 짧은 답
이 프로젝트에서 먼저 정할 것은 무엇인가? 입력 특징, 라벨, 분리 방식
왜 baseline이 먼저인가? 모델 점수의 의미를 비교하기 위해
최소 산출물은 무엇인가? baseline 예측, 모델 예측, test 결과 비교

프로젝트 흐름

flowchart TD
  A["질문 정리<br/>고객 이탈 위험 예측"]
  B["데이터 분리<br/>학습과 평가"]
  C["기준점 준비<br/>다수 라벨 예측"]
  D["모델 적용<br/>가장 가까운 이웃"]
  E["결과 비교<br/>정확도와 실패 사례"]

  A --> B --> C --> D --> E

이 흐름은 Part 3에서 반복해 본 머신러닝 공통 구조를 프로젝트 문서로 다시 옮긴 것입니다.

예제 데이터

구독 서비스 운영팀이 매일 보는 요약 지표처럼 데이터를 구성합니다.

  • 특징 1: 최근 30일 미해결 문의 수
  • 특징 2: 최근 접속 후 경과 일수
  • 특징 3: 최근 30일 사용 시간(분)
  • 라벨(label): 유지(0) / 이탈 위험(1)

학습용 데이터:

샘플 미해결 문의 수 최근 접속 후 경과 일수 최근 30일 사용 시간(분) 정답
학습-01 2 3 4200 0
학습-02 1 5 3900 0
학습-03 2 7 3600 0
학습-04 3 9 3400 0
학습-05 1 4 3000 0
학습-06 2 6 2800 0
학습-07 3 8 2500 0
학습-08 6 20 3500 1
학습-09 7 24 2600 1
학습-10 8 28 2200 1
학습-11 5 18 3300 1
학습-12 6 22 2900 1

평가용 데이터:

샘플 미해결 문의 수 최근 접속 후 경과 일수 최근 30일 사용 시간(분) 정답
평가-01 2 6 3700 0
평가-02 6 21 4050 1
평가-03 7 25 2550 1
평가-04 3 9 3200 0
평가-05 5 19 3350 1
평가-06 2 5 2950 0

이 데이터는 아주 작게 축약했지만, 실제 운영팀이 자주 보는 문의, 미접속, 사용량 축을 함께 둬서 무슨 특징이 예측에 기여했는가를 더 현실적으로 읽게 합니다.

baseline은 왜 먼저 두는가

baseline은 아주 단순합니다.

학습 데이터에서 더 자주 나온 라벨을 평가 데이터에도 그대로 예측한다.

이 baseline은 똑똑하지 않지만, 다음 질문에 답하게 해 줍니다.

내가 만든 모델이 최소한 이 단순한 기준보다 나은가?

이 기준이 없으면 모델 정확도가 0.67이든 0.90이든 그것이 얼마나 의미 있는지 판단하기 어렵습니다.

여기서 baseline은 모델 없는 기준선 역할을 합니다. 이 기준선이 있어야 개선이라는 말을 써도 과장이 줄어듭니다.

여기서 baseline은 꼭 가장 단순한 분류기 이름으로만 읽을 필요는 없습니다. 더 넓게 보면 baseline은 새 결과를 해석하기 위한 비교 기준선입니다. 즉, 지금 절에서는 다수 라벨 예측이 그 역할을 맡지만, 다른 프로젝트에서는 이전 버전 규칙, 최근 평균, 비슷한 조건의 평소 기준선처럼 비교를 가능하게 만드는 출발점이 baseline 자리에 올 수도 있습니다.

예를 들어 1-NN 정확도 0.667만 먼저 보면, 빠르게는 baseline 0.500보다 높으니 바로 쓸 만하다고 적고 싶어질 수 있습니다. 하지만 더 안전한 다음 판단은 점수 한 줄로 닫는 것이 아니라, baseline이 놓친 사례를 모델이 실제로 얼마나 회수했는가, 새로 만든 오답은 무엇인가, 가장 가까운 학습 샘플이 왜 그렇게 붙었는가를 함께 적는 것입니다. 그렇게 적어야 숫자가 조금 올랐다실제로 더 나은 판단 구조가 생겼다를 구분할 수 있습니다.

flowchart TD
  A["문제 장면<br/>baseline 0.500, 1-NN 0.667"]
  B["빠른 판단<br/>점수가 올랐으니 바로 사용"]
  C["회수 사례 확인<br/>baseline 실패를 얼마나 잡았는가"]
  D["새 오답 확인<br/>모델이 새로 틀린 샘플은 무엇인가"]
  E["최근접 샘플 확인<br/>왜 그렇게 붙었는가"]
  F["더 안전한 판단<br/>점수와 오류 사례를 같이 남긴다"]

  A --> B
  A --> C --> D --> E --> F

baseline과 1-NN의 오류 전환

예제는 train/test 분리, baseline, 간단한 분류 모델 비교를 한 화면에서 보는 것입니다. 예제에서는 단순히 정확도 숫자만 출력하지 않고, 실제 프로젝트 메모처럼 샘플별 비교 결과틀린 사례 목록까지 함께 남겨 보겠습니다.

이 기록 종류 지금 남기는 이유 뒤에서 다시 쓰는 곳
실행 요약 실행 결과에서 baseline과 모델을 비교한 최소 결과를 한 번에 남기기 위해 이후 재실험 판단과 개선 여부 확인의 기준점이 된다
샘플별 비교 행 어떤 샘플에서 예측이 갈렸는지 숫자 뒤의 실제 사례를 남기기 위해 오류 사례 묶음, 회고 메모, 재평가 후보를 정리할 때 근거가 된다
  • 문제 상황: 구독 고객의 이탈 위험을 예측한다.
  • 입력: 미해결 문의 수, 최근 접속 후 경과 일수, 최근 30일 사용 시간
  • 정답: 유지(0) / 이탈 위험(1)
  • 확인할 개념:
  • baseline과 모델을 나란히 비교해야 한다
  • 평가 데이터는 따로 두어야 한다
  • 작은 모델이어도 예측 결과를 직접 읽을 수 있어야 한다
  • 나중에 회고할 수 있도록 샘플별 결과를 기록해야 한다

실습용 분류 데이터를 코드 안에 직접 쓰지 않고 p7-2-churn-dataset.csv에 둡니다. Python 코드는 split 열을 기준으로 학습(train)과 평가(test)를 나누는 단계부터 시작합니다. 아래 코드는 docs/ 폴더가 보이는 책 저장소 루트에서 실행합니다.

입력 파일

  • 파일 경로: p7-2-churn-dataset.csv
  • 한 행의 의미: 한 명의 구독 고객에 대한 관측 기록
  • 핵심 열: split, sample_id, unresolved_tickets, days_since_login, usage_minutes_30d, label

입력 파일의 앞부분만 짧게 다시 보면 다음과 같습니다.

split sample_id unresolved_tickets days_since_login usage_minutes_30d label
train 학습-01 2 3 4200 0
train 학습-02 1 5 3900 0
train 학습-03 2 7 3600 0
train 학습-04 3 9 3400 0

점수와 오류를 함께 읽는 기록

  • 입력 특징과 라벨을 먼저 적고, train/test가 어떤 행으로 나뉘는지 확인합니다.
  • baseline이 어떤 라벨을 계속 예측하는지 표시합니다.
  • baseline과 1-NN의 정확도를 같은 평가 셋에서 나란히 둡니다.
  • 샘플별 비교에서 baseline이 놓친 사례, 1-NN이 회수한 사례, 1-NN이 새로 만든 오답을 따로 표시합니다.
# 구독 이탈 데이터에서 다수 라벨 baseline과 1-NN 예측을 비교해 샘플별 오류 사례를 읽는 예제입니다.
import csv
import numpy as np
from pathlib import Path

data_path = Path("docs/assets/part-07/chapter-02/p7-2-churn-dataset.csv")
rows = list(csv.DictReader(data_path.open(encoding="utf-8")))

for row in rows:
    row["미해결 문의 수"] = int(row["unresolved_tickets"])
    row["최근 접속 후 경과 일수"] = int(row["days_since_login"])
    row["최근 30일 사용 시간"] = int(row["usage_minutes_30d"])
    row["정답"] = int(row["label"])
    row["샘플"] = row["sample_id"]

train_rows = [row for row in rows if row["split"] == "train"]
test_rows = [row for row in rows if row["split"] == "test"]

feature_columns = ["미해결 문의 수", "최근 접속 후 경과 일수", "최근 30일 사용 시간"]
# 조작 변수: 사용 시간을 60으로 나누면 거리 계산에서 큰 숫자 축의 영향이 줄어듭니다.
feature_scales = np.array([1.0, 1.0, 1.0])

X_train = np.array([
    [row[column] for column in feature_columns]
    for row in train_rows
], dtype=float) / feature_scales
y_train = np.array([row["정답"] for row in train_rows])

X_test = np.array([
    [row[column] for column in feature_columns]
    for row in test_rows
], dtype=float) / feature_scales
y_test = np.array([row["정답"] for row in test_rows])

# baseline: train에서 가장 많은 라벨 하나만 계속 예측
baseline_class = int(np.bincount(y_train).argmax())
baseline_pred = np.full_like(y_test, baseline_class)

# 1-NN: 가장 가까운 학습 샘플의 정답을 사용
knn_pred = []
nearest_train_ids = []
for x in X_test:
    distances = np.linalg.norm(X_train - x, axis=1)
    nearest_index = int(np.argmin(distances))
    knn_pred.append(int(y_train[nearest_index]))
    nearest_train_ids.append(train_rows[nearest_index]["샘플"])

knn_pred = np.array(knn_pred)

comparison_rows = []
for index, row in enumerate(test_rows):
    baseline_correct = baseline_pred[index] == y_test[index]
    knn_correct = knn_pred[index] == y_test[index]
    if not baseline_correct and knn_correct:
        outcome = "1-NN 회수"
    elif baseline_correct and not knn_correct:
        outcome = "1-NN 새 오답"
    elif baseline_correct:
        outcome = "둘 다 정답"
    else:
        outcome = "둘 다 오류"
    comparison_rows.append({
        "평가 샘플": row["샘플"],
        "실제 정답": row["정답"],
        "기준점 예측": int(baseline_pred[index]),
        "기준점 정답 여부": "예" if baseline_correct else "아니오",
        "1-NN 예측": int(knn_pred[index]),
        "1-NN 정답 여부": "예" if knn_correct else "아니오",
        "가장 가까운 학습 샘플": nearest_train_ids[index],
        "결과 변화": outcome,
    })

baseline_errors = [
    row["평가 샘플"] for row in comparison_rows if row["기준점 정답 여부"] == "아니오"
]
knn_errors = [
    row["평가 샘플"] for row in comparison_rows if row["1-NN 정답 여부"] == "아니오"
]

project_run = {
    "질문": "구독 고객의 이탈 위험을 예측할 수 있는가?",
    "학습 샘플 수": len(train_rows),
    "평가 샘플 수": len(test_rows),
    "기준점 라벨": baseline_class,
    "기준점 정확도": round(float((baseline_pred == y_test).mean()), 3),
    "1-NN 정확도": round(float((knn_pred == y_test).mean()), 3),
    "기준점 실패 샘플": baseline_errors,
    "1-NN 실패 샘플": knn_errors,
}

print("실행 요약")
for key, value in project_run.items():
    print(f"  {key} = {value}")
print("읽은 파일 =", str(data_path))
print("샘플별 비교 =")
print("  샘플     정답  기준점  1-NN  결과 변화       최근접 학습 샘플")
for row in comparison_rows:
    print(
        f"  {row['평가 샘플']:<7}  {row['실제 정답']:^4}  "
        f"{row['기준점 예측']:^6}  {row['1-NN 예측']:^4}  "
        f"{row['결과 변화']:<13}  {row['가장 가까운 학습 샘플']}"
    )

실행 결과 예시는 다음과 같습니다.

실행 요약
  질문 = 구독 고객의 이탈 위험을 예측할 수 있는가?
  학습 샘플 수 = 12
  평가 샘플 수 = 6
  기준점 라벨 = 0
  기준점 정확도 = 0.5
  1-NN 정확도 = 0.667
  기준점 실패 샘플 = ['평가-02', '평가-03', '평가-05']
  1-NN 실패 샘플 = ['평가-02', '평가-04']
읽은 파일 = docs/assets/part-07/chapter-02/p7-2-churn-dataset.csv
샘플별 비교 =
  샘플     정답  기준점  1-NN  결과 변화       최근접 학습 샘플
  평가-01    0      0      0    둘 다 정답      학습-03
  평가-02    1      0      0    둘 다 오류      학습-02
  평가-03    1      0      1    1-NN 회수       학습-09
  평가-04    0      0      1    1-NN 새 오답    학습-11
  평가-05    1      0      1    1-NN 회수       학습-11
  평가-06    0      0      0    둘 다 정답      학습-05

0.167이 가리는 세 샘플 전환

정확도는 0.500에서 0.667로 0.167만 올랐지만, 그 변화는 세 샘플의 서로 다른 움직임에서 나왔습니다.

결과 변화 샘플 점수에 남긴 영향 다음에 읽을 것
1-NN 회수 평가-03, 평가-05 baseline이 놓친 이탈 위험 고객 두 건을 맞혀 점수가 올라간다 어떤 학습 샘플과 가까워졌는가?
1-NN 새 오답 평가-04 baseline이 맞힌 유지 고객 한 건을 새로 틀린다 왜 이탈 위험 학습 샘플에 붙었는가?
둘 다 오류 평가-02 모델을 써도 아직 회수되지 않은 오류가 남는다 입력 스케일이나 경계 사례가 부족한가?

아래 차트는 여섯 평가 샘플을 네 결과 변화로 묶어 점수 차이를 다시 읽게 합니다. 막대 높이는 샘플 수이고, 막대 아래에는 해당 샘플을 적었습니다.

baseline과 raw 1-NN 사이의 평가 샘플 결과 전환

이 차트에서 평가-03, 평가-05의 두 번의 회수와 평가-04의 한 번의 새 오답을 함께 세면, 정확도가 한 건만 늘어난 이유가 보입니다. 따라서 1-NN이 더 좋다는 기록은 맞지만, 어떤 고객에게나 더 안전하다는 결론은 아직 쓸 수 없습니다.

비교 항목 baseline raw 1-NN 읽어야 할 점
무엇을 보나 항상 유지로 예측 세 특징의 raw 거리를 그대로 사용 모델이 특징을 보기는 하지만, 스케일 차이에 약할 수 있다
정확도 0.500 0.667 baseline보다는 낫지만 아직 불안정하다
대표 실패 이탈 위험 고객을 거의 못 잡음 평가-02, 평가-04에서 잘못된 최근접 이웃 선택 입력 스케일과 거리 기준을 다시 점검해야 한다

하지만 동시에 조심해야 할 점도 있습니다.

  • 평가 샘플 수가 6개뿐이라 아직 작습니다.
  • 우연히 쉬운 혹은 어려운 분할이 섞였을 수 있습니다.
  • 다른 고객 구간에서는 같은 결과가 나오지 않을 수 있습니다.

즉, 프로젝트 문서에는 성과와 한계를 함께 남겨야 합니다.

실행 결과는 다음 세 줄로 요약할 수 있습니다.

  • baseline은 모든 고객을 유지로 보는 단순 기준선이다.
  • raw 1-NN은 baseline보다 낫지만, 사용 시간 스케일에 끌려 오판한 샘플이 남아 있다.
  • 샘플별 비교와 실패 목록이 있어야 다음 전처리 검토로 자연스럽게 이어질 수 있다.

회수와 새 오답이 가르는 다음 질문

관찰 읽어야 할 뜻
baseline 정확도는 0.500이다 단순 다수 라벨 기준선만으로는 이탈 위험 고객을 놓친다
raw 1-NN 정확도는 0.667이다 특징을 쓰면 일부 이탈 위험 고객을 회수한다
평가-03, 평가-05는 모델이 맞힌다 baseline 대비 개선이 실제 사례에서 보인다
평가-02는 둘 다 틀린다 모델이 아직 회수하지 못한 이탈 위험 고객이므로 거리와 학습 사례를 다시 읽는다
평가-04는 1-NN 새 오답이다 baseline이 맞힌 유지 고객을 모델이 새로 틀렸으므로, 개선 점수만으로 모델을 채택하지 않는다

회고에 남길 비교 기록

1
2
3
4
5
6
7
8
9
예측 질문:
입력 특징:
라벨:
train/test 분리:
baseline 결과:
모델 결과:
baseline보다 나아진 사례:
모델이 새로 만든 오답:
다음 수정 질문:

입력 스케일을 바꾸면 오류는 어떻게 움직이나

전처리 필요성을 실제 오류 사례로 느끼는 것이 중요하므로, 실행 뒤에는 입력 스케일을 일부러 흔들어 보는 편이 좋습니다. 아래 두 실험은 서로 독립적으로 실행합니다. 두 번째 실험을 시작하기 전에는 feature_scales를 기본값으로 되돌립니다.

  1. feature_scalesnp.array([1.0, 1.0, 60.0])으로 바꿔 usage_minutes_30d를 시간 단위처럼 줄여 봅니다. 관찰할 점: 평가-02의 최근접 이웃은 학습-02에서 학습-08로, 평가-04학습-11에서 학습-04로 바뀝니다. 두 행 모두 맞게 예측되어 1-NN 정확도는 1.000, 실패 샘플 목록은 빈 목록이 됩니다. 사용 시간의 큰 숫자 차이를 줄이자 다른 두 특징도 최근접 이웃 선택에 반영된 결과입니다.

  2. feature_scalesnp.array([1.0, 1.0, 1.0])으로 되돌린 뒤, test 행이 아니라 학습 행 마지막에 다음 한 줄을 추가해 봅니다. train,학습-13,6,21,4000,1 관찰할 점: 학습 라벨 수는 유지(0) 7건, 이탈 위험(1) 6건이므로 baseline은 여전히 0을 예측합니다. 반면 평가-02(6, 21, 4050)는 새 학습 행과 가장 가까워져 1-NN이 회수하고, 1-NN 정확도는 0.833, 실패 목록에는 평가-04만 남습니다. split=test로 넣으면 평가 대상만 늘어나므로 이 관찰과 다른 실험이 됩니다.

이 두 실험은 모델을 바꾸지 않아도 입력 스케일과 샘플 구성이 결과를 크게 흔든다는 점을 직접 보여 줍니다.

점수보다 먼저 확인할 운영 질문

이 작은 실습은 현실의 다음 질문과 연결됩니다.

  • 단순 규칙보다 실제 특징을 쓰는 모델이 나은가?
  • 그 개선이 어떤 샘플에서 나왔는가?
  • 어떤 오답은 모델 구조보다 입력 스케일 문제에서 나온 것 아닌가?
  • 전처리를 하면 그 오답이 줄어드는가?

이 감각은 이후 회고와 비교 문서를 쓸 때도 그대로 중요합니다.

즉, 핵심은 알고리즘 소개보다 프로젝트에서 baseline을 문서화하고 다음 비교 질문을 만드는 일에 있습니다.

체크리스트

확인할 것 스스로 답할 질문
문제 예측하려는 라벨과 입력 특징을 한 문장으로 고정했는가?
분리 train/test를 나누고, 평가 결과가 어떤 데이터에서 나온 것인지 적었는가?
baseline 모델 점수를 baseline 대비 차이로 읽었는가?
샘플별 비교 어떤 평가 샘플에서 baseline과 모델 예측이 갈렸는지 남겼는가?
실패 목록 오답 샘플이 다음 전처리 검토 질문으로 이어지는가?

이 다섯 칸이 비어 있으면 모델 종류를 더 늘려도 비교 기록은 강해지지 않습니다. 먼저 문제 -> 분리 -> baseline -> 샘플별 비교 -> 실패 목록 순서를 닫아야 다음 실험이 같은 기준 위에서 이어집니다.

출처와 참고 자료

이 절의 데이터는 개인정보가 없는 실습용 구독 고객 요약 예시를 위해 직접 구성한 synthetic 데이터입니다.