성공률을 비교하기 전에 두 실행이 같은 작업을 평가했는지부터 확인해야 합니다. 양쪽 모두 세 건이라고 해서 같은 세 건은 아닙니다. 한쪽에서 빠진 작업을 새 작업으로 바꾸면 성공률 차이에 모델 변화와 작업 구성 변화가 함께 섞입니다.
Python의 목록·사전·함수를 읽을 수 있는 독자를 위한 실습입니다. AI 재시도 로그 검산기와 Python 실습에서 만든 작업 단위 결과를 두 실행 사이에서 대조합니다. 아래의 가상 자료로 빠진 작업과 추가된 작업을 찾고, 같은 작업 목록에 대한 비교를 시작할 수 있는지 확인합니다.
2/3에서 3/3으로 올랐는데 비교를 멈추는 이유
원래 평가하기로 정한 목록은 A·B·C입니다. 이전 실행은 A 통과, B 실패, C 통과로 2/3입니다. 새 실행에는 A 통과, B 통과, D 통과가 있어 3/3입니다. 하지만 C는 없어지고 D가 추가됐습니다. 새 실행의 세 행만 보고 “같은 과제에서 성공률이 올랐다”고 말할 수 없습니다.
| 작업 | 이전 실행 | 새 실행 | 대조 결과 |
|---|---|---|---|
| A | pass | pass | 양쪽 존재 |
| B | fail | pass | 양쪽 존재 |
| C | pass | 없음 | 새 실행 누락 |
| D | 원래 목록 밖 | pass | 새 실행 추가 |
교집합 A·B만 비교하면 이전 1/2, 새 실행 2/2입니다. 이 계산은 “두 파일에 공통으로 남은 두 작업”의 결과입니다. 원래 맡긴 세 작업의 비교가 아닙니다. C가 빠진 이유를 모른 채 제외하면 비교 대상이 결과에 따라 선택됐을 가능성도 남습니다. 그래서 아래 코드는 교집합 ID만 진단 정보로 보여 주고, 전체 비교 결과는 보류합니다.

원래 목록과 비교 조건을 따로 보존합니다
EXPECTED는 결과 파일에서 추출하지 않고 실행 전에 확정한 작업 목록입니다. 두 결과 파일에서 C가 동시에 빠져도 원래 목록과 대조하면 누락을 찾을 수 있습니다. 반대로 EXPECTED 자체가 불완전하다면 이 코드가 그것을 알아내지는 못합니다. 목록의 출처와 버전을 함께 남겨야 합니다.
이 실습의 비교 조건은 rubric_version, snapshot_cutoff, dataset_version 세 문자열입니다. 같은 판정 기준·관측 마감·자료 버전을 사용했는지 먼저 봅니다. snapshot_cutoff는 같은 형식으로 정규화해 넣었다고 가정하며 이 코드는 시각을 해석하거나 오프셋을 변환하지 않습니다. 같은 순간이라도 표기가 다르면 불일치로 보류하므로 입력 단계에서 기준을 맞추세요.
모델이나 프롬프트 버전은 의도적으로 비교하는 대상일 수 있어 이 세 조건에 넣지 않았습니다. 실제 보고서에는 양쪽 버전을 따로 기록해야 합니다. 버전 이름이 같다는 것만으로 파일 내용과 판정 코드가 같다는 보장은 없습니다. 입력 사본과 기준 문서도 대조해야 합니다.
복사해서 실행하는 비교 전 점검
아래 코드를 cohort_audit.py라는 UTF-8 파일로 저장하고 python3 cohort_audit.py로 실행하세요. 환경에 따라 python 명령을 사용합니다. 외부 라이브러리는 필요 없습니다. 한 행은 재시도 한 번이 아니라 한 작업의 최종 판정입니다. pass·fail·unknown만 받고, unknown이 있으면 이 실습의 완전한 이진 비교는 보류합니다. unknown을 실패로 바꾸거나 분모에서 빼지 않습니다.
REQUIRED = ('rubric_version', 'snapshot_cutoff', 'dataset_version')
META = {'rubric_version': 'rubric-v1', 'snapshot_cutoff': '2026-11-04T09:00:00+00:00',
'dataset_version': 'tasks-v1'}
EXPECTED = ['A', 'B', 'C']
OLD = [('A', 'pass'), ('B', 'fail'), ('C', 'pass')]
NEW = [('A', 'pass'), ('B', 'pass'), ('D', 'pass')]
def index(rows):
out = {}
for row in rows:
if not isinstance(row, (tuple, list)) or len(row) != 2:
raise ValueError('행은 작업 ID와 판정 두 필드여야 합니다')
task, verdict = row
if type(task) is not str or not task or task.strip() != task:
raise ValueError('작업 ID 오류')
if task in out:
raise ValueError('중복 작업 ID')
if type(verdict) is not str or verdict not in {'pass', 'fail', 'unknown'}:
raise ValueError('판정 오류')
out[task] = verdict
return out
def compare(expected, old_rows, new_rows, old_meta, new_meta):
if (not expected or any(type(t) is not str or not t or t.strip() != t for t in expected)
or len(set(expected)) != len(expected)):
raise ValueError('원래 작업 목록 오류')
for meta in (old_meta, new_meta):
if type(meta) is not dict or any(
type(meta.get(k)) is not str or not meta[k].strip() for k in REQUIRED):
raise ValueError('비교 조건 누락 또는 형식 오류')
old, new = index(old_rows), index(new_rows)
target = set(expected)
issues = {
'old_missing': sorted(target - old.keys()),
'old_extra': sorted(old.keys() - target),
'new_missing': sorted(target - new.keys()),
'new_extra': sorted(new.keys() - target),
'condition_mismatch': [k for k in REQUIRED if old_meta[k] != new_meta[k]],
'unknown_old': sorted(t for t, v in old.items() if v == 'unknown'),
'unknown_new': sorted(t for t, v in new.items() if v == 'unknown'),
}
ready = not any(issues.values())
return {
'ready': ready, 'issues': issues,
'common_ids': sorted(old.keys() & new.keys()),
'full_comparison': {
'tasks': len(expected),
'old_pass': sum(v == 'pass' for v in old.values()),
'new_pass': sum(v == 'pass' for v in new.values()),
'improved_ids': [t for t in expected if old[t] == 'fail' and new[t] == 'pass'],
'regressed_ids': [t for t in expected if old[t] == 'pass' and new[t] == 'fail'],
} if ready else None,
}
if __name__ == '__main__':
print(compare(EXPECTED, OLD, NEW, META, META))
repaired = [('A', 'pass'), ('B', 'pass'), ('C', 'pass')]
print(compare(EXPECTED, OLD, repaired, META, META))
첫 출력과 두 번째 출력은 다른 입력을 설명합니다
첫 출력은 ready가 False, full_comparison이 None입니다. new_missing에는 C, new_extra에는 D가 표시됩니다. common_ids는 A·B이지만 이 값만으로 전체 성공률을 계산하지 않습니다. 필요한 조치는 C의 기록을 찾고 D가 들어온 이유를 확인하는 것입니다. D의 이름만 C로 바꾸거나 누락 판정을 만들어 넣어서는 안 됩니다.
두 번째 출력은 설명을 위한 별도의 가상 입력입니다. 실제로 C의 통과 기록을 확인했다고 가정한 repaired 목록을 사용합니다. ready가 True이고 전체 세 작업에서 이전 통과 2건, 새 통과 3건, fail에서 pass로 바뀐 작업은 B로 표시됩니다. 실제 분석에서는 확인한 원본 기록으로만 목록을 보완해야 합니다.
ready는 입력 대조를 통과했다는 뜻이지 “새 모델이 우수하다”는 결론이 아닙니다. 세 작업은 합성 예제이며 일반적인 성공률 추정치가 아닙니다. 반복 실행의 의존성, 표본 수, 평가 환경과 통계적 불확실성은 별도 문제입니다.
세 가지 변형으로 보류 기준을 확인합니다
새 결과에 같은 A 행을 한 번 더 넣으면 중복 작업 ID 오류가 납니다. 사전으로 곧바로 변환해 중복을 덮어쓰지 않고, 넣기 전에 검사하기 때문입니다. 새 결과의 B를 unknown으로 바꾸면 형식 오류는 아니지만 비교는 보류됩니다. 판정을 아직 확정하지 못한 상태와 실패 판정은 구분합니다.
새 실행의 비교 조건에서 rubric_version만 바꾸면 condition_mismatch에 해당 이름이 나타납니다. 이 예제는 조건이 다른 두 결과를 자동으로 섞지 않습니다. 기준 변경 자체를 평가하려는 실험이라면 별도의 질문과 설계를 세워야 합니다. 행 순서만 바꾼 경우는 작업 ID로 대조하므로 결과가 같습니다.
이 코드가 확인하지 않는 것
ID가 같아도 질문 내용이나 정답표가 바뀌었다면 같은 작업이 아닐 수 있습니다. 같은 dataset_version 문자열을 반복 사용하면 대조를 통과하므로 자료 버전의 의미를 지키는 운영이 필요합니다. 한 작업에 여러 최종 판정이 정당하게 존재하는 설계라면 실행 ID·판정 개정 등으로 단위를 다시 정하세요. 원본 행을 임의로 버려 이 코드에 맞추지 않습니다.
이 실습은 한 번의 같은 작업 집합 비교를 위한 보수적인 입력 점검입니다. 관측 마감이 달라도 비교할 수 있는 연구 설계나 서로 다른 집단의 비교가 모두 잘못됐다는 뜻은 아닙니다. 다만 그런 질문을 이 코드의 ready로 승인하지 않습니다. 비교 범위와 허용 조건을 바꾸면 보고서에도 그 차이를 드러내야 합니다.
보고서에 남길 비교 범위
원래 작업 목록의 출처와 버전: 이전 / 새 실행 ID와 모델·프롬프트 버전: 판정 기준 / 자료 버전 / 관측 마감: 양쪽 누락·추가 작업 / 중복·unknown: 전체 비교 가능 여부와 보류 이유: 비교한 작업 수 / 개선·악화 작업: 표본·환경·판정 근거의 남은 한계:
검증한 범위와 출처
2026년 11월 7일 Python 3.12.14에서 본문 코드와 합성 자료를 실행했습니다. 누락·추가·중복 작업, 조건 불일치, unknown, 행 순서 변경과 잘못된 입력을 포함한 19개 점검을 통과했습니다. 실제 모델·고객 자료·통계 검정·성능 개선의 인과성은 시험하지 않았습니다.
공식 문서 확인일은 2026년 11월 7일입니다. Python 3.12 집합 문서의 차집합·교집합과 dict 문서의 키 조회를 참고했습니다. 원래 작업 목록과 비교 조건, unknown 보류 규칙은 이 글의 학습용 설계입니다. 숫자가 좋아졌는지 보기 전에, 그 숫자가 같은 대상을 세고 있는지 확인하는 데 사용하세요.