[Python] 평가 결과 일치도 확인 (1)

Connected Brain·2026년 4월 10일

Kappa-Agreement 분석 Tool

0.Summary

분석 Tool 요약

  • 목적: TTS(음성합성) 평가 데이터의 품질 지표(평균/표준편차)신뢰도(Kappa/Agreement)를 다차원으로 분석
  • 핵심 기능:
    • 다차원 분석: 전체, 모델별, 문항(ID)별, 도메인/카테고리별 자동 그룹화 분석
    • 신뢰도 검증: Fleiss' Kappa 및 Agreement를 통한 평가자 간 합의 수준 측정
    • 의사결정 지원: 분석 수치에 따른 자동 조치 가이드(Action Guide) 생성
  • 기대 효과: 평가 데이터의 객관성 확보 및 불성실 평가자/모호한 문항의 조기 선별

1. Input xlsx, csv Format

예시 이미지

입력 xlsx, csv 형식

  • 구분자 헤더 : 문항, 평가자, 모델, 도메인 등 (녹색 영역)
  • 구분자 입력값 : (노란색 영역)
  • 평가지표 헤더 : 정확성, 발음, 속도, 끊어읽기 등 (파란색 영역)
  • 평가지표 입력값 : (빨간색 영역)

2. Input Setting

입력값

  • ID_COLUMN
  • RATER_COLUMN
  • GROUP_INPUT
    • 문항, 평가자 열의 이름과 그 외 구분자로 사용할 헤더 목록을 입력
    • 해당 목록에 포함되지 않은 헤더는 평가지표로 간주
  • RESULT_FILE
    • 최종 분석 결과를 기록하여 저장할 파일명

3. Install Libraries

#@title 라이브러리 설치 및 확인
import importlib
import subprocess
import sys

def install_if_missing(package_name, import_name=None):
    if import_name is None:
        import_name = package_name

    try:
        importlib.import_module(import_name)
        # print(f"✅ {package_name} 이(가) 이미 설치되어 있습니다.")
    except ImportError:
        print(f"Installing {package_name}...")
        # Colab 환경에서 시스템 호출을 통해 설치
        subprocess.check_call([sys.executable, "-m", "pip", "install", package_name])
        print(f"✅ {package_name} 설치 완료.")

# 설치가 필요한 라이브러리 목록 (패키지명, 임포트명)
packages = [
    ("openpyxl", "openpyxl"),
    ("statsmodels", "statsmodels"),
    ("xlsxwriter", "xlsxwriter")
]

for pkg, imp in packages:
    install_if_missing(pkg, imp)

import pandas as pd
import numpy as np
import io
  • 필수 라이브러리, 패키지 openptxl, statsmodel , xlsxwriter 설치
    • openptxl : 엑셀(xlsx) 파일 불러와 Data Frame으로 변환
    • statsmodel : 빈도 행렬을 통해 Fleiss-Kappa, Agreement 계산 함수 사용
    • xlsxwriter : 엑셀(xlsx) 파일 내용 수정, 쓰기
  • 설치 확인 후 설치되지 않은 라이브러리, 패키지만 선택적으로 설치

4. Data Frame 변환

업로드 된 파일 불러오기

# 1. 파일 확장자 확인 및 데이터 로드
if FILE_PATH and os.path.exists(FILE_PATH):
    # 파일 확장자에 따라 읽기 방식 결정
    if FILE_PATH.endswith('.csv'):
        df = pd.read_csv(FILE_PATH)
    elif FILE_PATH.endswith(('.xls', '.xlsx')):
        df = pd.read_excel(FILE_PATH)
    else:
        print("❌ 지원하지 않는 파일 형식입니다. (CSV 또는 Excel 파일 필요)")
        df = pd.DataFrame() # 빈 데이터프레임 생성
else:
    print("❌ 파일을 업로드하거나 올바른 경로를 입력해주세요.")
    df = pd.DataFrame()
  • 파일 확장자에 따라 파일 불러와 Data Frame(df) 형식으로 변환해 향후 활용에 용이하게 함

입력값 전처리

if not df.empty:
    # (1) 그룹 컬럼 리스트화 (쉼표 분리 및 공백 제거)
    GROUP_COLUMNS = [x.strip() for x in GROUP_INPUT.split(',') if x.strip()]

    # (2) 구분자(식별자) 목록 통합
    # 사용자가 입력한 ID, 평가자, 그룹 컬럼들을 모두 모음
    excluded_cols = [ID_COLUMN, RATER_COLUMN] + GROUP_COLUMNS

    # (3) 평가지표(Metrics) 자동 추출
    # 전체 컬럼 중 구분자 영역을 제외한 나머지를 모두 평가지표로 간주
    metric_cols = [col for col in df.columns if col not in excluded_cols and "Unnamed" not in col]
    
    # (4) 데이터 타입 정리 (평가지표는 숫자형으로 변환)
    for col in metric_cols:
        df[col] = pd.to_numeric(df[col], errors='coerce')
  • 데이터 프레임을 정상적으로 불러온 경우 GROUP_COLUMNS 리스트화
    • 예시) GROUP_COLUMNS"모델, 도메인, 카테고리" 입력
      리스트로 변형 : GROUP_COLUMNS = ["모델", "도메인", "카테고리"]
  • ID_COLUMN, RATER_COLUMN에서 입력받은 값들도 모아 excluded_cols 구분자 리스트를 구성
    • 예시) ID_COLUMN = "문항", RATER_COLUMN = "평가자"로 입력
      리스트로 변형 및 통합 : excluded_cols = ["문항","평가자","모델", "도메인", "카테고리"]
  • 나머지 열의 요소는 모두 평가지표로 간주
    • 예시) 기존 헤더 목록 : [문항,평가자,모델,도메인,카테고리,정확성,발음,속도,끊어읽기...]
      구분자 제외 후 평가지표만 추출한 목록 : [정확성,발음,속도,끊어읽기...]
  • 평가지표 열의 요소를 전부 숫자형으로 변환

5. Fleiss-kappa & Agreement 측정

평균, 표준편차, kappa 산출 통합 함수

def get_combined_metrics(target_df, group_id_col, metric_col):
    try:
        # 데이터 정제
        valid_df = target_df[[group_id_col, metric_col]].dropna()
        if valid_df.empty: return np.nan, np.nan, np.nan, np.nan, 0

        # [통계] 평균 및 표준편차
        mean_v = round(valid_df[metric_col].mean(), 4)
        std_v = round(valid_df[metric_col].std(), 4)

        # [Kappa] 빈도 행렬 생성 (행: 평가대상, 열: 점수범주)
        count_matrix = pd.get_dummies(valid_df[metric_col]).groupby(valid_df[group_id_col]).sum().to_numpy()

        kappa = np.nan
        # 대상(Subject)이 2개 이상이고 점수 종류가 2개 이상일 때 Fleiss' Kappa 산출
        if count_matrix.shape[0] >= 2 and count_matrix.shape[1] >= 2:
            kappa = round(fleiss_kappa(count_matrix, method='fleiss'), 4)
        elif count_matrix.shape[0] >= 2 and count_matrix.shape[1] == 1:
            kappa = 1.0 # 모든 대상에 대해 모든 평가자가 동일 점수를 준 경우

        agreement = np.nan
        # 일치도(Agreement) 계산 로직
        # n_raters가 2명 이상이어야 쌍(pair)을 지어 일치도를 구할 수 있음
        if count_matrix.shape[0] >= 2:
          n_raters = count_matrix[0].sum()

          if n_raters >= 2:
            # 각 문항별 일치도 P_i 계산: (sum(n_ij^2) - n_i) / (n_i * (n_i - 1))
            numerator = np.sum(count_matrix**2, axis=1) - n_raters
            denominator = n_raters * (n_raters - 1)
            p_i = numerator / denominator
            agreement = round(np.mean(p_i), 4) # 전체 문항에 대한 평균 일치도


        return mean_v, std_v, kappa, agreement, len(valid_df)
    except Exception as e:
      print(f"Error in {metric_col} for {group_id_col}: {e}") # 에러 메시지 출력
      return np.nan, np.nan, np.nan, np.nan, 0

Fleiss-kappa 측정

# [Kappa] 빈도 행렬 생성 (행: 평가대상, 열: 점수범주)
count_matrix = pd.get_dummies(valid_df[metric_col]).groupby(valid_df[group_id_col]).sum().to_numpy()

kappa = np.nan
# 대상(Subject)이 2개 이상이고 점수 종류가 2개 이상일 때 Fleiss' Kappa 산출
if count_matrix.shape[0] >= 2 and count_matrix.shape[1] >= 2:
	kappa = round(fleiss_kappa(count_matrix, method='fleiss'), 4)
elif count_matrix.shape[0] >= 2 and count_matrix.shape[1] == 1:
	kappa = 1.0 # 모든 대상에 대해 모든 평가자가 동일 점수를 준 경우
  • Fleiss-kappa 측정을 위해서는 빈도 행렬을 구성해야 함
  • metric_col에 포함된 평가 지표에 해당하는 데이터를 순차적으로 가져옴
    • 예시) metric_col = [정확성,발음,속도,끊어읽기...]인 경우 정확성 항목에 관한 평가 데이터를 가져옴
  • 점수를 열로 가지는 표로 재구성해, 각 평가자의 점수에 해당하는 열에 1로 표기
    • 예시) 정확성에 대한 점수 구성이 [4,2,1,3,5]인 경우 아래와 같이 행렬 구성
      이후 묶고자 하는 구분자를 기준으로 변수들을 한 그룹으로 묶음
평가자12345
test_0100010
test_0201000
test_0310000
test_0400100
test_0500001
...
  • 구분자를 기준으로 묶은 변수 그룹을 합쳐 해당 점수의 빈도 행렬로 변환
    • 예시) 문항을 기준으로 위의 행렬을 빈도 행렬로 변환한 결과
문항12345
1번11111
...
  • statsmodels이 계산할 수 있도록 문자열 영역을 제거하고 숫자 값만 행렬로 전달
  • 어떤 평가자가 몇 점을 주었는지는 중요하지 않고, 전체에서 각 점수가 어떤 빈도로 등장했는지가 중요함
count_matrix.shape[0] >= 2 and count_matrix.shape[1] >= 2
  • 구분자에 해당하는 평가 요소가 2개 이상인지, 발생한 점수의 종류가 2개 이상인지 확인
    구분자 : 평가요소 = 문항 : [1번, 2번, 3번...]
    • 평가 요소가 하나라면 비교할 수 있는 다른 대상이 없어 kappa를 구할 수 없음
      • Fleiss-kappa는 평가 대상이 다름에도 평가가 일관되게 이루어졌는지를 확인하기 위함이므로 단일 항목에 대한 정보로는 평가가 일관되게 이루어졌는지를 정의할 수 없음
    • 점수의 종류가 1개라면 모두 통일된 상태이므로 kappa가 1

Agreement 측정

agreement = np.nan
# 일치도(Agreement) 계산 로직
# n_raters가 2명 이상이어야 쌍(pair)을 지어 일치도를 구할 수 있음
if count_matrix.shape[0] >= 2:
n_raters = count_matrix[0].sum()

if n_raters >= 2:
	# 각 문항별 일치도 P_i 계산: (sum(n_ij^2) - n_i) / (n_i * (n_i - 1))
    numerator = np.sum(count_matrix**2, axis=1) - n_raters
    denominator = n_raters * (n_raters - 1)
    p_i = numerator / denominator
    agreement = round(np.mean(p_i), 4) # 전체 문항에 대한 평균 일치도
  • 일치도 계산도 동일하게 빈도 행렬을 사용함
    • 점수 빈도 행렬의 첫번째 열의 모든 빈도 값을 합치면 전체 평가자 수를 알 수 있음
numerator = np.sum(count_matrix**2, axis=1) - n_raters
  • 각 빈도 값을 제곱한 뒤, 다 더해서 평가자 수를 뺌
    • 같은 점수를 선택한 사람들끼리 짝을 맺을 수 있는 경우의 수를 구함
denominator = n_raters * (n_raters - 1)
  • 발생할 수 있는 모든 쌍의 개수를 구함(가능한 모든 쌍이므로 순서/방향을 고려한 개수)
agreement = round(np.mean(p_i), 4)
  • 같은 점수 짝을 맺을 경우의 수/가능한 모든 쌍의 개수 의 평균을 구함
    • 평가 결과가 보여주는 평균적인 합의 수준을 보여줌

6. 분석 시나리오 세팅

# (차원 이름, 그룹화할 컬럼, Kappa 계산 시 기준이 될 대상 컬럼)
scenarios = [('전체', [], 'id_model_pair')] # 전체는 ID+모델 조합을 대상으로 봄

# 단일 구분자별 분석 (id, 모델, 도메인, 카테고리 등)
for col in [ID_COLUMN] + GROUP_COLUMNS:
    # ID별 분석일 때는 '모델'을 대상으로 일치도를 보고, 그 외에는 'ID'를 대상으로 봄
    sub_target = '모델' if col == ID_COLUMN else ID_COLUMN
    scenarios.append((f'{col}별', [col], sub_target))

# 조합별 분석 (사용자 입력 그룹 전체 조합)
if len(GROUP_COLUMNS) > 1:
    scenarios.append(('조합별(Group)', GROUP_COLUMNS, ID_COLUMN))

# 4. 분석 실행 루프
final_report = []

# 전체 분석용 임시 식별자 생성
df['id_model_pair'] = df[ID_COLUMN].astype(str) + "_" + df['모델'].astype(str)

for sc_name, sc_cols, sub_col in scenarios:
    if not sc_cols: # 전체 분석
        for m in metric_cols:
            mean, std, kap, agr, n = get_combined_metrics(df, sub_col, m)
            final_report.append({
                '분석차원': sc_name, '대상': 'Total', '지표': m,
                '평균': mean, '표준편차': std, 'Kappa': kap, '일치도': agr, '샘플수': n
            })
    else: # 그룹/구분자별 분석
        grouped = df.groupby(sc_cols)
        for name, group_df in grouped:
            label = name if isinstance(name, str) else "-".join(map(str, name))
            for m in metric_cols:
                mean, std, kap, agr, n = get_combined_metrics(group_df, sub_col, m)
                final_report.append({
                    '분석차원': sc_name, '대상': label, '지표': m,
                    '평균': mean, '표준편차': std, 'Kappa': kap, '일치도': agr, '샘플수': n
                })

# 5. 결과 정리 및 최종 검증
report_df = pd.DataFrame(final_report)

# 지표 열에 구분자 명칭이 들어간 행 강제 제거 (순수 지표만 남김)
report_df = report_df[~report_df['지표'].isin(all_identifiers)]

print(f"✅ 분석 완료!")
display(report_df.head(20))

전체 분석

  • 전체 분석시에는 ID+모델 조합을 구분자로 사용해 지표를 구함
    • 하나의 문항에 대해 3개의 모델이 있는 조건이므로 하나의 고유한 요소는 ID+모델이 조합되어야 구분할 수 있음

단일 구분자 분석

  • 각 구분자를 기준으로 지표를 계산
    • 문항별 분석일 때는 '모델'을 대상으로 일치도를 보고, 그 외에는 '문항'을 대상으로 봄

조합 구분자 분석

  • 구분자 조합 전체를 합친 조합 구분자를 기준으로 지표를 계산

분석 시나리오 분리 이유

  • 각 구분자에 따라 Fleiss-kappa와 Agreement를 따로 계산함으로써, 어느 요소에서 평가 결과의 불일치가 발생하는지 파악하기 쉽게 하고자 함
  • 특정 구분자에서 불일치가 반복된다면, 해당 구분자에 맞춘 대처가 가능
    • 모델 : 모델 성능이 월등하여 전반적으로 점수가 높게 치우친 경우, 아주 적은 특이값으로도 kappa가 낮게 나올 수 있음
    • 평가자 : 평가자가 다른 평가자와 다른 기준을 적용하고 있거나, 가이드 숙지가 미흡할 수 있음

7. 지표에 따른 가이드

def get_action_guide(row):
    kap = row.get('Kappa')
    agr = row.get('일치도')

    # 데이터가 없는 경우 처리
    if pd.isna(kap) or pd.isna(agr):
        return "-"

    # 1. 일치도 높음 / Kappa 음수 또는 매우 낮음 (Prevalence 현상)
    if agr >= 0.8 and kap <= 0.1:
        return "[특이값 확인] 대세와 다른 소수 의견 존재. 단순 실수인지 또는 타인이 놓친 결함 발견인지 해당 평가자 면담 필요."

    # 2. 일치도 중간 / Kappa가 0에 가까움 (Bimodal 분리)
    elif 0.35 <= agr <= 0.55 and kap <= 0.1:
        return "[가이드 수정] 의견이 두 집단으로 팽팽하게 갈림. 기준 모호성 확인 및 가이드라인 구체화 필요."

    # 3. 일치도 낮음 / Kappa가 어느 정도 존재 (난이도 높음)
    elif agr < 0.4 and 0.1 < kap <= 0.4:
        return "[난이도 높음] 전반적으로 의견이 분산되었으나 일부 합의 존재."

    # 4. 일치도 매우 낮음 / Kappa 0 이하 (무작위 수준)
    elif agr < 0.3 and kap <= 0:
        return "[신뢰 불가] 무작위 평가 수준. 데이터 분석 가치 낮음."

    return "정상"

# '조치 가이드' 컬럼 생성
report_df['조치 가이드'] = report_df.apply(get_action_guide, axis=1)

# 파일 저장
output_path = f"{RESULT_FILE}_통합분석.csv"
report_df.to_csv(output_path, index=False, encoding='utf-8-sig')

print(f"✅ 분석 및 가이드 생성 완료! '{output_path}' 파일에 조치사항이 포함되었습니다.")
display(report_df.head(20))

Fleiss-kappa / Agreement에 따른 가이드

Fleiss-kappaAgreement조치 가이드
낮음(0.1 이하 혹은 음수)높음(0.8 이상)대세와 다른 소수의견 존재
낮음(0)중간(0.4~0.5)평가 결과가 두 집단으로 갈림
약간 높음(0.4 이상)낮음(0.4 이하)전반적인 의견 분산이 있으나 일부 합의 존재
낮음(0)낮음(0.3 이하)무작위 수준임
  • 각 지표에 따른 조치 가이드를 제공해 평가 결과를 반영한 의사결정을 도움

0개의 댓글