같은 과제로 결과 비교
75분 안팎
학습 목표
고정 평가 사례에서 요구 충족률·실패 유형·시간·토큰을 집계하고 평균에 가려진 실패를 설명합니다.
개념
총점이 올라도 이전 계약이 깨질 수 있습니다
할 일 앱의 새 수정안이 생성과 완료 검사에서는 좋아졌지만 조회 순서를 바꾸었다고 가정합니다. 전체 통과 수가 늘었다는 보고만 받으면 회귀를 놓치기 쉽습니다. 이번 레슨은 두 버전을 같은 고정 사례 ID로 비교하고 요구 충족률, 실패 유형, 사용 토큰과 시간 합계를 따로 출력합니다. 평균이나 합계가 좋은 이유와 남아 있는 실패를 함께 설명하는 것이 목표입니다. 코드 실행 성공과 요구 충족은 결과의 서로 다른 칸에 남깁니다.
평가 세트는 변경 전에 고정합니다
사례 ID는 create, blank-title, complete, list-order처럼 계약 하나를 가리키는 안정된 이름으로 만듭니다. 각 사례는 입력과 기대값과 판정 기준을 갖고 버전 비교에서 같은 의미를 유지합니다. 이번 브라우저 과제는 검사 자체를 실행하는 대신 이미 관찰한 결과 행을 집계합니다. 미션의 단위와 통합 검사 근거와 이 집계 모형을 구분합니다. 평가 행이 pass라고 주장하는 것만으로 실제 테스트가 수행되었다고 확인한 것은 아닙니다. 집계에 들어갈 근거의 수집은 앞 레슨 관문이 담당합니다.
ID로 연결하고 배열 위치는 믿지 않습니다
before와 after의 행 순서가 달라도 같은 ID끼리 비교합니다. 사전으로 바꾸기 전에 중복 ID를 검사합니다. 중복을 마지막 행으로 덮어쓰면 실패한 첫 결과가 사라질 수 있습니다. ids와 결과의 ID 집합이 정확히 같고 행 개수도 같아야 유효한 비교입니다. 빠진 사례와 추가 사례가 있으면 CASE_SET 오류로 INVALID를 출력합니다. 사례가 바뀐 비교를 정상 점수인 것처럼 내놓지 않습니다. 별도 평가 세트를 만들었다면 새 평가라는 사실부터 기록합니다.
통과·실패·미확인을 분리합니다
status는 pass, fail, unknown 중 하나입니다. unknown은 실행하지 못했거나 결과 근거가 부족한 상태이며 실패 원인을 확정했다는 뜻은 아닙니다. 충족률의 분모는 고정 ids의 전체 개수이고 분자는 pass 수입니다. unknown을 분모에서 빼면 실행한 쉬운 사례만으로 점수가 올라갈 수 있습니다. 빈 평가 세트는 rate를 0.0으로 출력하지만 평가 근거가 없다는 사실을 total 0으로 드러냅니다. 이것을 모든 요구를 만족한 100퍼센트로 바꾸지 않습니다.
실패 유형을 사례별로 남깁니다
failure 필드는 fail 행에서만 집계하며 expected-order, empty-accepted 같은 작성된 원인 표식을 보존합니다. 같은 유형의 실패 두 건도 각각의 ID로 남겨 어느 입력을 재현할지 찾을 수 있게 합니다. 전체 실패 유형별 개수만 두면 다른 사례의 실패로 바뀐 것을 놓칠 수 있습니다. 정렬은 보고서의 ID 표시 순서에만 사용합니다. 앱 목록의 정렬 계약을 바꾸는 코드와 집계 표를 안정되게 정렬하는 코드는 다른 목적입니다. unknown은 실패 사전에 넣지 않고 별도의 ID 목록으로 표시합니다.
회귀와 개선과 근거 소실을 따로 셉니다
이전의 pass가 새 버전에서 fail이면 regressed이고 이전에 pass가 아니던 사례가 새 pass이면 improved입니다. 이전 pass가 unknown이 되면 lost입니다. lost는 새 코드의 결함이 증명되었다는 뜻이 아니라 이전에 있던 통과 근거를 이번에는 확보하지 못했다는 뜻입니다. 그 경우도 전체 채택 판단에서 숨기지 않습니다. 계속 fail인 사례는 개선도 회귀도 아니지만 after.failures에 남습니다. 새 버전이 어디서 나아졌고 무엇이 여전히 남았는지 각 목록의 역할을 설명합니다.
자원 합계에는 실패 비용도 포함합니다
tokens와 seconds는 평가 행마다 기록된 음이 아닌 정수 모의 사용량입니다. 통과한 행만 합치지 않고 모든 행을 더합니다. unknown 행도 이미 소비한 자료가 있다면 그 값은 합계에 들어갑니다. 충족률이 높은 버전이 더 많은 토큰을 쓰는 경우에는 품질과 자원을 함께 보고 결정합니다. 이 과제는 실제 모델 비용이나 제품 가격을 비교하지 않습니다. 작은 고정 fixture에서 집계가 정확한지 확인할 뿐 특정 도구나 모델이 우수하다는 결론을 내리지 않습니다.
평균을 계산하기 전에 분포를 읽습니다
네 사례의 총 시간이 12초면 평균은 3.0초입니다. 그러나 각 사례가 1, 1, 1, 9초라면 한 사례가 대부분의 시간을 씁니다. 이번 출력은 합계만 제공하므로 원래 행의 seconds를 ID별로 읽어 느린 사례를 설명해야 합니다. 요구 충족률도 비슷합니다. 쉬운 세 사례가 통과하고 중요한 목록 순서가 실패하면 75.0이라는 값만으로 완료를 선언하지 않습니다. 제출 설명에는 after.failures와 unknown 및 lost를 먼저 읽고 어떤 추가 검사가 필요한지 적습니다.
기준을 바꾸어 점수를 올리지 않습니다
목록 순서를 비교하던 평가를 집합 비교로 바꾸면 정렬 회귀가 사라질 수 있습니다. 하지만 요구가 입력순을 유지하는 것이라면 이는 개선이 아니라 검사 약화입니다. 같은 평가 사례와 기준을 유지한 비교를 먼저 만들고 기준 변경이 필요하면 별도 검토 자료로 둡니다. 모델이 작성한 평가 결과도 입력과 실제 반환값을 직접 대조합니다. 이번 집계 함수는 채점 기준의 타당성을 판단하지 않습니다. 통계 처리의 정확성과 평가 사례의 품질을 각각 설명해야 합니다.
INVALID는 새 버전 점수가 아닙니다
CASE_SET 또는 STATUS 오류는 평가 자료를 다시 준비해야 한다는 뜻입니다. 이 오류를 실패율 100퍼센트로 바꾸면 앱의 결함과 집계 자료 결함이 섞입니다. try와 except로 ValueError를 잡아 INVALID만 출력하고 정상 비교 객체는 내놓지 않습니다. 잘못된 JSON 문법과 음수 비용은 입력 범위 밖입니다. 유효한 결과에서 소수점 표시는 rate 0.0처럼 한 자리로 남습니다. JSON 직렬화는 목록 순서를 바꾸지 않고 키 정렬만 사용하므로 결과 표현과 의미를 구분합니다.
사례 하나를 끝까지 설명해 봅니다
list-order가 이전 pass에서 새 fail이 되었으면 동일 ID의 입력, 기대 순서, 실제 순서를 모읍니다. 변경 diff에서 정렬이나 집합 변환이 추가되었는지 살펴봅니다. 이 자료로 회귀 가설을 세우고 같은 입력으로 재검사합니다. 전체 점수 상승이나 토큰 절약을 이유로 실패를 삭제하지 않습니다. 수정한 버전은 평가 전체를 다시 실행하여 다른 계약을 보호합니다. 각 평가의 ID와 수정 후보 식별자를 함께 보관하면 뒤 세션도 같은 반례에서 작업을 이어갈 수 있습니다.
모듈 미션의 근거 묶음으로 연결합니다
미션 starter는 앞 모듈의 solution을 이어받고 gate.py에만 종료 코드 결함을 제공합니다. 이를 고쳐 기존 앱 회귀와 새 루프 검사를 통과시키고 python3 loop_runner.py를 실행해 loop-report.json을 만듭니다. 이 보고서는 고정 시도마다 unit과 integration 사례의 실제 검사 기록을 보존합니다. 교육용 승인 fixture와 실제 사람 검토를 구분하여 인계합니다. 중단 이유와 다음 행동을 보고서에서 설명하고, 모델 호출과 자동 코드 수정은 미구현이라고 적습니다. 더 읽기에서는 추적 기록과 평가 운영을 넓게 살펴봅니다.
따라하기
총점과 회귀 동시 확인
같은 두 ID를 순서가 다른 결과로 비교합니다.
def evaluate(ids, rows):
if len(ids) != len(set(ids)) or len(rows) != len(ids):
raise ValueError('CASE_SET')
by_id = {r['id']: r for r in rows}
if len(by_id) != len(rows) or set(by_id) != set(ids):
raise ValueError('CASE_SET')
if any(r['status'] not in ('pass', 'fail', 'unknown') for r in rows):
raise ValueError('STATUS')
passed = sum(r['status'] == 'pass' for r in rows)
failures = {r['id']: r['failure'] for r in rows if r['status'] == 'fail'}
unknown = sorted(r['id'] for r in rows if r['status'] == 'unknown')
return {'passed': passed, 'total': len(ids),
'rate': round(passed / len(ids) * 100, 1) if ids else 0.0,
'failures': dict(sorted(failures.items())), 'unknown': unknown,
'tokens': sum(r['tokens'] for r in rows),
'seconds': sum(r['seconds'] for r in rows)}
def compare(ids, before, after):
left, right = evaluate(ids, before), evaluate(ids, after)
old = {r['id']: r['status'] for r in before}
new = {r['id']: r['status'] for r in after}
return {'before': left, 'after': right,
'improved': sorted(i for i in ids if old[i] != 'pass' and new[i] == 'pass'),
'regressed': sorted(i for i in ids if old[i] == 'pass' and new[i] == 'fail'),
'lost': sorted(i for i in ids if old[i] == 'pass' and new[i] == 'unknown')}
import json
def row(i,s): return dict(id=i,status=s,failure='order',tokens=4,seconds=1)
print(json.dumps(compare(['create','order'],[row('create','fail'),row('order','pass')],[row('order','fail'),row('create','pass')]),sort_keys=True))실행 결과
{"after": {"failures": {"order": "order"}, "passed": 1, "rate": 50.0, "seconds": 2, "tokens": 8, "total": 2, "unknown": []}, "before": {"failures": {"create": "order"}, "passed": 1, "rate": 50.0, "seconds": 2, "tokens": 8, "total": 2, "unknown": []}, "improved": ["create"], "lost": [], "regressed": ["order"]}
미확인과 빈 평가
미확인 사례는 분모에 남으며 빈 평가도 완료 근거가 아닙니다.
def evaluate(ids, rows):
if len(ids) != len(set(ids)) or len(rows) != len(ids):
raise ValueError('CASE_SET')
by_id = {r['id']: r for r in rows}
if len(by_id) != len(rows) or set(by_id) != set(ids):
raise ValueError('CASE_SET')
if any(r['status'] not in ('pass', 'fail', 'unknown') for r in rows):
raise ValueError('STATUS')
passed = sum(r['status'] == 'pass' for r in rows)
failures = {r['id']: r['failure'] for r in rows if r['status'] == 'fail'}
unknown = sorted(r['id'] for r in rows if r['status'] == 'unknown')
return {'passed': passed, 'total': len(ids),
'rate': round(passed / len(ids) * 100, 1) if ids else 0.0,
'failures': dict(sorted(failures.items())), 'unknown': unknown,
'tokens': sum(r['tokens'] for r in rows),
'seconds': sum(r['seconds'] for r in rows)}
def compare(ids, before, after):
left, right = evaluate(ids, before), evaluate(ids, after)
old = {r['id']: r['status'] for r in before}
new = {r['id']: r['status'] for r in after}
return {'before': left, 'after': right,
'improved': sorted(i for i in ids if old[i] != 'pass' and new[i] == 'pass'),
'regressed': sorted(i for i in ids if old[i] == 'pass' and new[i] == 'fail'),
'lost': sorted(i for i in ids if old[i] == 'pass' and new[i] == 'unknown')}
print(evaluate(['order'],[dict(id='order',status='unknown',failure='',tokens=0,seconds=0)]))
print(evaluate([],[]))실행 결과
{'passed': 0, 'total': 1, 'rate': 0.0, 'failures': {}, 'unknown': ['order'], 'tokens': 0, 'seconds': 0}
{'passed': 0, 'total': 0, 'rate': 0.0, 'failures': {}, 'unknown': [], 'tokens': 0, 'seconds': 0}
평균에 가려진 느린 사례
평균뿐 아니라 가장 느린 사례의 시간을 확인합니다.
seconds = {'create':1,'blank-title':1,'complete':1,'list-order':9}
print(f'평균 {sum(seconds.values()) / len(seconds):.1f}')
print(max(seconds, key=seconds.get), max(seconds.values()))실행 결과
평균 3.0 list-order 9
확인 문제
실습
JSON 객체 ids·before·after를 읽습니다. ids는 문자열 사례 ID 배열이고 각 결과 행은 id·status·failure·tokens·seconds입니다. status는 pass/fail/unknown, failure는 문자열, 비용은 음이 아닌 정수입니다. 각 결과의 ID가 ids와 정확히 같고 중복이 없어야 하며 status가 틀리거나 세트가 다르면 INVALID를 출력합니다. 정상 출력은 before와 after의 passed·total·rate(한 자리 반올림 백분율, 빈 세트 0.0)·failures(fail만 ID→failure)·unknown(정렬 ID)·tokens·seconds, improved(비pass→pass), regressed(pass→fail), lost(pass→unknown)를 가진 JSON입니다. 세 ID 목록은 정렬합니다. 결과 객체 키도 sort_keys=True로 직렬화합니다. unknown을 분모에서 빼지 않습니다.
모범 답안
def evaluate(ids, rows):
if len(ids) != len(set(ids)) or len(rows) != len(ids):
raise ValueError('CASE_SET')
by_id = {r['id']: r for r in rows}
if len(by_id) != len(rows) or set(by_id) != set(ids):
raise ValueError('CASE_SET')
if any(r['status'] not in ('pass', 'fail', 'unknown') for r in rows):
raise ValueError('STATUS')
passed = sum(r['status'] == 'pass' for r in rows)
failures = {r['id']: r['failure'] for r in rows if r['status'] == 'fail'}
unknown = sorted(r['id'] for r in rows if r['status'] == 'unknown')
return {'passed': passed, 'total': len(ids),
'rate': round(passed / len(ids) * 100, 1) if ids else 0.0,
'failures': dict(sorted(failures.items())), 'unknown': unknown,
'tokens': sum(r['tokens'] for r in rows),
'seconds': sum(r['seconds'] for r in rows)}
def compare(ids, before, after):
left, right = evaluate(ids, before), evaluate(ids, after)
old = {r['id']: r['status'] for r in before}
new = {r['id']: r['status'] for r in after}
return {'before': left, 'after': right,
'improved': sorted(i for i in ids if old[i] != 'pass' and new[i] == 'pass'),
'regressed': sorted(i for i in ids if old[i] == 'pass' and new[i] == 'fail'),
'lost': sorted(i for i in ids if old[i] == 'pass' and new[i] == 'unknown')}
import json,sys
d=json.load(sys.stdin)
try:
print(json.dumps(compare(d['ids'],d['before'],d['after']),sort_keys=True))
except ValueError:
print('INVALID')
더 읽기
면접 질문
- AI가 만든 코드가 실행될 때 추가로 확인할 내용을 설명해 주시면 됩니다.