같은 입력 200개에서 기존 모델의 성공이 150건, 새 모델의 성공이 160건이었다면 성공률은 75%에서 80%로 5%p 올랐다. 하지만 이 두 합계만으로 어떤 입력이 좋아지고 나빠졌는지는 알 수 없다. 같은 사례의 두 판정을 연결한 대응표가 있어야 개선과 퇴행을 따로 셀 수 있다.
이 글의 두 표와 사례 ID는 모두 설명용 합성 자료다. 실제 모델을 호출하거나 운영 트래픽을 비교하지 않았다. 각 사례에는 같은 고정 기준으로 내린 기존·신규의 성공 또는 실패 판정이 하나씩 있고, 서로 다른 사례는 독립적으로 표집됐다고 가정한다.
같은 사례 ID의 성공·실패를 네 칸으로 묶는다
행은 기존 모델, 열은 새 모델이다. 두 모델이 모두 성공한 건수를 a, 기존 성공이 새 실패로 바뀐 퇴행을 b, 기존 실패가 새 성공으로 바뀐 개선을 c, 모두 실패를 d로 둔다. 기존 성공은 a+b, 새 성공은 a+c이므로 순증은 c−b다.
| 기존 판정 | 새 모델 성공 | 새 모델 실패 | 합계 |
|---|---|---|---|
| 기존 성공 | a = 146 | b = 4: 퇴행 | 150 |
| 기존 실패 | c = 14: 개선 | d = 36 | 50 |
| 합계 | 160 | 40 | 200 |
14건의 개선에서 4건의 퇴행을 빼면 성공 10건 순증이다. ‘10건 좋아졌다’라고만 쓰면 새 실패 네 건을 가린다. 프롬프트 회귀 평가 절차에 따라 그 네 건의 내용과 중요도를 별도로 읽어야 한다.
McNemar 정확검정은 불일치 쌍의 방향을 묻는다
statsmodels의 McNemar 문서는 2×2 대응표의 주변분포가 같다는 귀무가설을 다룬다. 여기서는 기존과 새 모델의 성공 확률이 같다는 질문이다. exact 방식은 이항분포를 사용하며, 카이제곱 근사와 연속성 보정은 다른 계산 선택이다.
이진 대응에서 귀무가설 아래 불일치가 어느 방향으로 생길지의 확률은 각각 1/2다. 불일치 총수 m=b+c를 고정하면 그중 한 방향의 수를 X~Binomial(m, 0.5)로 계산한다. 이 글의 양측 정확 p값은 min(1, 2×P[X≤min(b,c)])이다. 비교 A는 m=18, 작은 방향이 4건이므로 p≈0.030884다. 실제 공식 구현의 이항 누적확률 계산과 같은 식을 표준 라이브러리의 조합 합으로 직접 계산한다.
p값은 귀무가설과 표집 가정이 맞을 때 현재처럼 또는 더 극단적으로 한쪽에 치우친 불일치가 나올 확률이다. ‘새 모델이 더 좋을 확률 96.9%’도, ‘귀무가설이 참일 확률 3.1%’도 아니다. 이 양측검정 자체가 개선 방향을 정해 주는 것도 아니므로 c와 b, 효과 크기를 함께 제시한다.
성공률 차이가 같아도 대응표와 p값은 달라진다
| 비교 | a | b: 퇴행 | c: 개선 | d | 성공률 변화 | 양측 정확 p값 |
|---|---|---|---|---|---|---|
| A | 146 | 4 | 14 | 36 | 75% → 80% | 약 0.030884 |
| B | 110 | 40 | 50 | 0 | 75% → 80% | 약 0.342833 |
B도 성공은 10건 순증이지만 바뀐 사례가 90건이며 퇴행이 40건이다. 같은 5%p라도 불일치 방향의 치우침과 업무상 변화가 다르다. 두 표는 별개의 가상 상황이며, 실제 동일 데이터에서 관측한 두 실험이 아니다. 비율 변화의 표현은 %p와 상대 변화 계산과 연결하되 대응 정보를 합계로 지워 버리지 않는다.
사례 ID를 확인하고 본문 코드로 정확 p값을 계산한다
아래 Python은 각 행을 (사례 ID, 기존 성공 0/1, 새 성공 0/1)로 받는다. 중복 ID와 이진값이 아닌 판정은 거절한다. 누락된 한쪽 판정을 실패로 자동 대체하지 않는다. 예제에서 표를 사례 행으로 펼치는 부분은 합성 자료 생성일 뿐 실제 입력 수집이 아니다. Windows의 Python 3.14.3에서 본문 HTML의 코드 텍스트를 추출·entity 해제해 실행했다. statsmodels·SciPy 패키지 실행이나 실제 모델 비교는 하지 않았다.
import json
from math import comb
def paired_report(records):
table = [[0, 0], [0, 0]]
seen = set()
for case_id, old, new in records:
if not isinstance(case_id, str) or not case_id or case_id in seen:
raise ValueError("case_id must be nonempty and unique")
if any(type(v) is not int or v not in (0, 1) for v in (old, new)):
raise ValueError("Both judgments must be integer 0 or 1")
seen.add(case_id)
table[1 - old][1 - new] += 1
(a, b), (c, d) = table
n = a + b + c + d
m = b + c
if not n:
p = None
elif not m:
p = 1.0
else:
numerator = 2 * sum(comb(m, k) for k in range(min(b, c) + 1))
p = min(1.0, numerator / (2 ** m))
return {"table": table, "n": n, "discordant": m,
"delta_pp": round(100 * (c - b) / n, 6) if n else None,
"p_exact_two_sided": round(p, 6) if p is not None else None}
def synthetic_records(table):
return [(f"cell-{i}-{j}-{k}", 1 - i, 1 - j)
for i, row in enumerate(table)
for j, count in enumerate(row) for k in range(count)]
cases = {"A": [[146, 4], [14, 36]],
"B": [[110, 40], [50, 0]],
"unchanged": [[8, 0], [0, 2]],
"empty": [[0, 0], [0, 0]]}
for name, table in cases.items():
print(json.dumps({"name": name, **paired_report(synthetic_records(table))}))
{"name": "A", "table": [[146, 4], [14, 36]], "n": 200, "discordant": 18, "delta_pp": 5.0, "p_exact_two_sided": 0.030884}
{"name": "B", "table": [[110, 40], [50, 0]], "n": 200, "discordant": 90, "delta_pp": 5.0, "p_exact_two_sided": 0.342833}
{"name": "unchanged", "table": [[8, 0], [0, 2]], "n": 10, "discordant": 0, "delta_pp": 0.0, "p_exact_two_sided": 1.0}
{"name": "empty", "table": [[0, 0], [0, 0]], "n": 0, "discordant": 0, "delta_pp": null, "p_exact_two_sided": null}
불일치가 0인 비어 있지 않은 표는 이 계산에서 p=1이다. 차이를 보여 줄 불일치 정보가 없다는 뜻이며 두 모델의 동등성을 입증한 것은 아니다. 아예 평가한 사례가 없는 표는 p=null로 구별한다. 조합을 직접 합하는 코드는 작은 예제의 검산용이며 대규모 통계 계산의 성능을 시험하지 않았다.
검정 전에 비교 단위와 판정 기준을 고정한다
서로 다른 기간의 요청 묶음은 사례별 대응이 없으므로 이 표에 임의로 나란히 넣을 수 없다. 같은 원본의 여러 페이지나 같은 입력의 반복 실행도 독립된 새 사례로 세면 불확실성을 잘못 평가할 수 있다. 공식 소스의 표집 설명도 관측 단위의 독립성을 전제로 한다. 군집·반복이 있는 설계는 그 의존성을 반영하는 분석이 필요하며 이 글에서는 계산하지 않았다.
성공 기준·평가 묶음·비교할 판본·검정 방향과 판단 기준을 결과 전에 정한다. 모델이나 지표를 많이 시험한 뒤 작은 p값만 고르는 과정에는 다중 비교 문제도 있다. 이 글의 p값은 그 문제를 보정하지 않았다. 판정 불명이나 실행 실패를 제외한다면 원래 전체 건수와 제외 이유를 함께 남겨야 한다.
자주 묻는 질문
p가 0.05보다 작으면 바로 교체하면 되나요?
자동 교체 조건으로 삼을 수 없다. 비교 A에도 퇴행 네 건이 있고 그 피해는 건수만으로 알 수 없다. 효과 크기와 중요한 새 오류, 지연·관측 비용·운영 조건을 함께 판단해야 한다. 0.05는 이 글이 모든 업무에 정한 합격선이 아니다.
p가 크면 두 모델이 같다는 뜻인가요?
현재 설계와 표본에서 차이를 뒷받침할 근거가 충분하지 않다는 해석과 동등성 입증은 다르다. ‘실무적으로 허용할 차이’와 그에 맞는 설계를 정하는 동등성 평가는 여기서 수행하지 않았다.
정확도가 올랐다는 사실과 검정 결과는 어떻게 같이 적나요?
기존·신규의 성공 수와 전체 수, 개선·퇴행 건수, %p 차이, 양측 정확검정의 p값과 가정을 함께 적는다. 성공 기준이 관심 오류를 놓치지 않는지는 혼동행렬의 분모와 오류 유형으로 다시 확인한다. 이 표만으로 변경의 인과효과나 실제 서비스 개선을 확정하지 않는다.