콘텐츠로 이동

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

Section ID: P7-2.1 Version: v2026.07.22

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

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

비교 가능한 예측 기록

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

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

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

Part 7에서 기준점(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

Python 예제

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

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

실습용 분류 데이터를 코드 안에 직접 쓰지 않고 p7-2-churn-dataset.csv에 둡니다. Python 코드는 split 열을 기준으로 학습(train)과 평가(test)를 나누는 단계부터 시작합니다.

입력 파일

  • 파일 경로: 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):
    comparison_rows.append({
        "평가 샘플": row["샘플"],
        "실제 정답": row["정답"],
        "기준점 예측": int(baseline_pred[index]),
        "기준점 정답 여부": "예" if baseline_pred[index] == y_test[index] else "아니오",
        "1-NN 예측": int(knn_pred[index]),
        "1-NN 정답 여부": "예" if knn_pred[index] == y_test[index] else "아니오",
        "가장 가까운 학습 샘플": nearest_train_ids[index],
    })

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("실행 요약 =", project_run)
print("읽은 파일 =", str(data_path))
print("샘플별 비교 =")
for row in comparison_rows:
    print(row)

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

1
2
3
4
5
6
7
8
9
실행 요약 = {'질문': '구독 고객의 이탈 위험을 예측할 수 있는가?', '학습 샘플 수': 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
샘플별 비교 =
{'평가 샘플': '평가-01', '실제 정답': 0, '기준점 예측': 0, '기준점 정답 여부': '예', '1-NN 예측': 0, '1-NN 정답 여부': '예', '가장 가까운 학습 샘플': '학습-03'}
{'평가 샘플': '평가-02', '실제 정답': 1, '기준점 예측': 0, '기준점 정답 여부': '아니오', '1-NN 예측': 0, '1-NN 정답 여부': '아니오', '가장 가까운 학습 샘플': '학습-02'}
{'평가 샘플': '평가-03', '실제 정답': 1, '기준점 예측': 0, '기준점 정답 여부': '아니오', '1-NN 예측': 1, '1-NN 정답 여부': '예', '가장 가까운 학습 샘플': '학습-09'}
{'평가 샘플': '평가-04', '실제 정답': 0, '기준점 예측': 0, '기준점 정답 여부': '예', '1-NN 예측': 1, '1-NN 정답 여부': '아니오', '가장 가까운 학습 샘플': '학습-11'}
{'평가 샘플': '평가-05', '실제 정답': 1, '기준점 예측': 0, '기준점 정답 여부': '아니오', '1-NN 예측': 1, '1-NN 정답 여부': '예', '가장 가까운 학습 샘플': '학습-11'}
{'평가 샘플': '평가-06', '실제 정답': 0, '기준점 예측': 0, '기준점 정답 여부': '예', '1-NN 예측': 0, '1-NN 정답 여부': '예', '가장 가까운 학습 샘플': '학습-05'}

결과를 어떻게 읽는가

이 결과에서 핵심은 0.667이라는 숫자 자체보다 비교 구조입니다.

  • baseline은 모든 고객을 유지로만 봤기 때문에 이탈 위험 고객 세 건을 놓쳤습니다.
  • 1-NN 모델은 특징을 실제로 사용해 평가-03, 평가-05 같은 이탈 위험 고객을 잡아냈습니다.
  • 하지만 평가-02, 평가-04에서는 raw 거리 계산이 사용 시간(분) 축에 크게 끌려 잘못된 최근접 이웃을 고른 흔적이 보입니다.

즉, 이번 실습은 모델이 baseline보다 낫다는 것과 그래도 입력 스케일 때문에 틀릴 수 있다는 것을 동시에 보여 줍니다.

비교 항목 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
2
3
4
5
6
7
8
9
예측 질문:
입력 특징:
라벨:
train/test 분리:
baseline 결과:
모델 결과:
baseline보다 나아진 사례:
모델이 새로 만든 오답:
다음 수정 질문:

직접 바꿔 보며 확인할 것

전처리 필요성을 실제 오류 사례로 느끼는 것이 중요하므로, 실행 뒤에는 입력 스케일을 일부러 흔들어 보는 편이 좋습니다.

  1. feature_scalesnp.array([1.0, 1.0, 60.0])으로 바꿔 usage_minutes_30d를 시간 단위처럼 줄여 봅니다. 관찰할 점: 평가-02, 평가-04의 최근접 이웃이 바뀌는가? 정확도와 실패 샘플이 줄어드는가?

  2. 평가-02와 비슷한 이탈 위험 고객 한 행을 CSV에 하나 더 추가해 봅니다. 관찰할 점: baseline은 그대로인데 1-NN 쪽 실패 패턴은 어떻게 달라지는가?

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

실무 감각으로 번역하면

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

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

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

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

체크리스트

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

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

출처와 참고 자료

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