두 검토자가 100건 중 90건에 같은 판정을 내렸다면 관측 일치율은 90%다. 하지만 두 사람이 거의 모든 문서를 ‘검토 필요’로 표시했는지, 두 범주를 비슷하게 사용했는지에 따라 그 숫자의 의미는 달라진다. Cohen의 kappa는 각 검토자의 범주 사용 비율에서 계산한 기대 일치와 실제 일치를 비교한다.
아래 표는 두 검토자가 같은 문서마다 ‘검토 필요’ 또는 ‘검토 불필요’를 하나씩 부여한 합성 자료다. 실제 사람을 모집해 실시한 평가가 아니며 정답 라벨도 없다. 따라서 검토자 간 일치를 계산할 수 있어도 정답 정확도나 OCR 품질을 측정한 결과는 아니다.
관측 일치와 주변분포로 계산한 기대 일치를 구별한다
scikit-learn의 Cohen kappa 문서는 κ=(Po−Pe)/(1−Pe)로 정의한다. Po는 같은 범주를 고른 비율이다. Pe는 각 검토자의 범주별 비율을 이용해 계산한 기대 일치다. 여기서는 두 범주와 가중치 없는 kappa만 다룬다.
검토자 1의 필요·불필요 비율을 r1, r0, 검토자 2의 비율을 c1, c0라 두면 Pe=r1c1+r0c0다. 각자의 범주 사용 비율을 유지하면서 독립적으로 고른다는 기준 모형의 기대값이다. 두 사람이 실제로 무작위 판정했다는 관측이나 ‘우연히 맞힌 문서 수’를 직접 센 값은 아니다.
일치율 90%인 두 표를 직접 비교한다
| 검토자 1 | 검토자 2: 필요 | 검토자 2: 불필요 | 합계 |
|---|---|---|---|
| 필요 | 45 | 5 | 50 |
| 불필요 | 5 | 45 | 50 |
| 합계 | 50 | 50 | 100 |
관측 일치는 (45+45)/100=0.9다. 각 검토자의 두 범주 비율은 0.5와 0.5이므로 기대 일치는 0.5×0.5+0.5×0.5=0.5다. 따라서 κ=(0.9−0.5)/(1−0.5)=0.8이다.
| 검토자 1 | 검토자 2: 필요 | 검토자 2: 불필요 | 합계 |
|---|---|---|---|
| 필요 | 90 | 5 | 95 |
| 불필요 | 5 | 0 | 5 |
| 합계 | 95 | 5 | 100 |
B의 관측 일치도 (90+0)/100=0.9다. 그러나 기대 일치는 0.95²+0.05²=0.905이므로 κ=(0.9−0.905)/(1−0.905)≈−0.052632다. 관측 일치가 이 기준 모형의 기대값보다 조금 낮다. 이 음수가 ‘두 사람이 정답과 반대로 판정한다’는 뜻은 아니다. 정답은 주어지지 않았다.
두 표의 불일치 수는 똑같이 10건이다. 달라진 것은 범주 사용 비율과 그에 따른 비교 기준이다. kappa 하나를 떼어 보편적인 등급이나 자동 합격선으로 만들면 자료 구성의 차이를 놓칠 수 있다. 일치율·원래 네 칸·각자의 주변분포를 함께 보고 판단할 업무를 명시한다.
본문 코드로 분모가 0인 경우까지 확인한다
다음 Python 함수는 위와 같은 행·열 순서의 2×2 정수 건수 표를 받는다. 기대 일치의 분자는 행 합과 열 합의 곱으로 계산한다. 분모가 정확히 0인 경우에는 kappa를 null로 남긴다. Windows의 Python 3.14.3에서 이 HTML의 코드 텍스트를 추출·entity 해제해 실행했다. scikit-learn 패키지를 실행하거나 공식 문서의 최신 선택 인자를 시험한 것은 아니다.
import json
def agreement(table):
if len(table) != 2 or any(len(row) != 2 for row in table):
raise ValueError("A 2 by 2 table is required")
if any(type(v) is not int or v < 0 for row in table for v in row):
raise ValueError("Counts must be nonnegative integers")
(a, b), (c, d) = table
n = a + b + c + d
if not n:
return {"n": 0, "observed": None, "expected": None, "kappa": None}
chance_numerator = (a + b) * (a + c) + (c + d) * (b + d)
denominator = n * n - chance_numerator
kappa = ((n * (a + d) - chance_numerator) / denominator
if denominator else None)
return {"n": n, "observed": round((a + d) / n, 6),
"expected": round(chance_numerator / (n * n), 6),
"kappa": round(kappa, 6) if kappa is not None else None}
cases = {"balanced": [[45, 5], [5, 45]],
"skewed": [[90, 5], [5, 0]],
"all_one_label": [[100, 0], [0, 0]],
"empty": [[0, 0], [0, 0]]}
for name, table in cases.items():
print(json.dumps({"name": name, **agreement(table)}))
{"name": "balanced", "n": 100, "observed": 0.9, "expected": 0.5, "kappa": 0.8}
{"name": "skewed", "n": 100, "observed": 0.9, "expected": 0.905, "kappa": -0.052632}
{"name": "all_one_label", "n": 100, "observed": 1.0, "expected": 1.0, "kappa": null}
{"name": "empty", "n": 0, "observed": null, "expected": null, "kappa": null}
세 번째 표에서는 두 사람이 전부 같은 한 범주만 사용했다. 관측 일치와 기대 일치가 모두 1이므로 kappa 식이 0/0으로 정의되지 않는다. 이를 임의로 1이나 0으로 바꾸지 않는다. 마지막 빈 표는 일치율 자체도 계산할 수 없다. 자료가 없거나 지표가 정의되지 않는 상황을 낮은 성능으로 관측한 상황과 구별해야 한다.
같은 판정이 곧 올바른 판정은 아니다
두 사람이 같은 오독을 하거나 잘못된 기준을 공유하면 일치는 높아도 결과는 틀릴 수 있다. 정확도를 평가하려면 별도의 신뢰할 수 있는 정답과 근거가 필요하다. 혼동행렬과 precision·recall은 정답과 예측을 비교하는 질문이며, 이 글의 검토자 두 명 사이의 일치와 역할이 다르다.
실무에서는 문서 ID로 두 판정을 연결하고 같은 원문·같은 기준 판본을 제공한다. 원래 판정은 합의 전에 각각 보존하고, 상의 뒤 만든 최종 라벨을 독립 평가의 원자료로 보고하지 않는다. 누락·보류·판정 불명은 별도 건수로 남긴다. 사례를 제외했다면 제외 전 전체 수와 이유를 함께 적고, 불명 결과를 조용히 ‘불필요’로 바꾸지 않는다.
불일치 사례를 읽으면 기준의 모호함, 원문 정보 부족, 항목 연결 오류를 구별할 수 있다. OCR 사람 검토 기준을 수정했다면 이전과 같은 사례만 반복해 보는 결과와 새 사례의 결과를 나누어 살핀다. 이 글은 kappa의 신뢰구간, 여러 평가자, 반복 평가의 의존성이나 기준 변경의 효과를 추정하지 않았다.
자주 묻는 질문
일치율과 kappa 중 하나만 보고해도 되나요?
두 값과 네 칸의 건수·주변분포를 함께 제시하면 해석을 확인하기 쉽다. 높은 일치율과 낮은 kappa가 동시에 나오는지 살피고, 드문 범주에서의 불일치가 업무에 어떤 영향을 주는지 따로 읽는다.
라벨이 세 단계라면 같은 계산을 써도 되나요?
이 코드는 두 범주만 지원한다. 명목 범주가 늘어난 일반 kappa와, 단계 사이 차이를 다르게 취급하는 가중 kappa는 별도 설계다. 범주를 숫자로 썼다는 이유만으로 순서·간격을 가정하거나 두 범주로 임의 통합하지 않는다.
kappa가 올라가면 검토 품질 개선이 입증되나요?
문서 구성·범주 비율·기준·평가자가 달라졌는지 먼저 확인한다. 같은 조건에서 일치가 늘었다 해도 정답 정확도와 후속 업무의 성공은 별도로 검증해야 한다. 이 합성 계산은 실제 검토 품질의 개선을 측정하지 않았다.