가늠 › 계산 검증

이 글은 저장소의 docs/계산검증.md 를 그대로 옮긴 것입니다. 검증 스크립트와 대상 케이스도 저장소에 함께 있습니다.

계산 검증 문서

이 문서가 답하는 질문: "가늠이 내놓는 가중치와 일관성 지표를 어떻게 믿을 수 있는가."

대상: 조사 결과를 논문·보고서·심사 근거로 쓰려는 연구자와 실무자. 최종 실행: 2026년 10월 1일 · 커밋 34673d0 · 24건 전부 통과 (계산 — 고유벡터·CR 22건 + 후보 평가 R1·R2, §2.8) 연결·식별자·접근 검증: 2026년 9월 27일 · 42건 전부 통과 (§5) 화면 검증(방이 보는 화면·단계의 문·연달아 넣는 입력): 2026년 9월 27일 · 148건 전부 통과 (§5-2) (개발 환경과 프로덕션 서버 양쪽. 전체 이력은 7절)


1. 왜 이 문서가 있는가

AHP 결과는 "이 항목의 가중치가 0.42"라는 숫자 하나로 요약되고, 그 숫자가 예산 배분이나 심사 배점의 근거가 된다. 그래서 계산이 맞는지를 쓰는 사람이 직접 확인할 수 있어야 한다.

가늠은 두 가지를 약속한다.

  • 계산 규칙을 문서로 고정한다 (아래 2절). 임의로 바꾸지 않는다.
  • 규칙대로 계산되는지를 독립된 방법으로 대조하고, 그 결과를 이 문서에 남긴다. 계산 코드를 고치면 검증을 다시 돌리고 이 문서를 갱신한다.

2. 계산 규칙

구현이 따르는 정의다. 프로토타입(ganum_prototype_v2.html)에서 검증된 구현을 그대로 옮겼다.

2.1 입력 척도

응답자는 두 항목 사이를 슬라이더 -8 ~ +8(정수)로 답한다. 이를 쌍대비교 행렬의 원소로 바꾼다.

a = |v| + 1        (Saaty 1~9 척도)
v < 0 이면 왼쪽 항목이 a 배 중요 → A[i][j] = a
v > 0 이면 오른쪽 항목이 a 배 중요 → A[i][j] = 1/a
v = 0 이면 동등 → A[i][j] = 1
언제나 A[j][i] = 1 / A[i][j]

2.1-1 손대지 않은 비교 — 어떻게 집계하는가 (2026-09-27 부터)

슬라이더를 손대지 않았거나 「동등으로 두고 넘기기」로 넘긴 비교는 동등(1)으로 집계한다. 그리고 그 건수를 결과의 집계 기준에 「미조작 동등 N건」 으로 적는다 (0이면 적지 않는다). 화면의 집계 기준 줄·결과 CSV·원자료 CSV·회의록이 같은 문구를 쓴다.

왜 받는가. 그전에는 손대지 않은 비교를 「무응답」으로 두었고, 무응답이 하나라도 있으면 그 응답 전체가 집계에서 빠졌다. 스무 쌍 중 하나를 지나친 분의 답 열아홉 개가 통째로 버려진다.

왜 적는가. 같은 a = 1 이라도 「동등이라 판단한 1」과 「손대지 않아 남은 1」은 다른 자료다. 계산은 둘을 구별하지 않지만(값이 같으므로 가중치도 CR 도 같다), 숫자를 읽는 사람은 구별할 수 있어야 한다. 그래서 막지 않고 적는다 — 응답자에게는 제출 직전에 한 번 알리고, 진행자에게는 집계 기준에 함께 내놓는다.

원자료 CSV 의 「조작 여부」 열에 비교마다 조작 / 미조작(동등) / 빈칸 이 적힌다.

이 규칙 이전(2026-09-27 이전) 조사의 무응답 처리는 종전대로다. 옛 「무응답」 비교를 가진 응답은 그대로 미완으로 제외된다. 이미 나간 결과는 바뀌지 않는다. 옛 자료의 「조작 여부」는 빈칸(NULL)이며 「미조작 동등」으로 세지 않는다 — 알 수 없는 것을 「안 했다」로 세면 거짓이다. 배포 전에 프로덕션 자료 사본으로 조사 13건 전부를 옛 코드·새 코드로 각각 집계해 12자리까지 대조했다 (보고서 참조).

2.2 가중치

거듭제곱법(power iteration)으로 주고유벡터를 구한다. 200회 반복하며 매회 합으로 정규화한다. 결과 벡터의 합은 1이다.

λmax = (1/n) · Σ (A·w)ᵢ / wᵢ

2.3 일관성

CI = (λmax − n) / (n − 1)
CR = CI / RI          기준: CR ≤ 0.1
RI = {1: 0, 2: 0, 3: 0.58, 4: 0.90, 5: 1.12, 6: 1.24, 7: 1.32, 8: 1.41, 9: 1.45}

CR 은 비교 세트(행렬)별로 따로 판정한다. 2계층 조사라면 대과제 세트와 각 대과제의 소과제 세트가 각각 자기 CR 을 갖는다. 항목이 2개 이하인 세트는 일관성 개념이 없어 CR = 0 이다.

2.4 그룹 통합 (AIJ)

여러 응답자를 하나로 합칠 때 행렬 원소별 기하평균으로 통합 행렬을 만든 뒤, 그 행렬에서 다시 고유벡터를 구한다.

Ā[i][j] = (∏ₖ A⁽ᵏ⁾[i][j])^(1/K)

개인 가중치를 평균 내지 않는다. 두 방식은 서로 다른 값을 낸다 (검증 C5).

2.4-1 그룹 통합 방법의 선택 근거

그룹 통합에는 크게 두 길이 있다. 이 서비스는 AIJ(판단 통합) — 행렬 원소별 기하평균을 쓴다. 왜 그쪽인지, 그리고 다른 길이 필요한 연구자는 어떻게 하는지를 적는다.

AIJ (Aggregating Individual Judgments) AIP (Aggregating Individual Priorities)
무엇을 합치나 판단(행렬 원소 a_ij)을 먼저 합쳐 행렬 하나를 만들고, 거기서 가중치를 구한다 각자의 가중치를 먼저 구하고, 그것을 평균한다
전제 그룹이 하나의 판단 주체로 행동한다 개인이 각자 독립된 주체로 남는다
이 서비스 채택 원자료로 재계산 가능 (아래)

근거 1 — 역수 성질이 보존된다 (Aczél & Saaty, 1983). 쌍대비교 행렬은 a_ij · a_ji = 1 이어야 한다. 여러 사람의 a_ij 를 산술평균하면 이 성질이 깨진다 — mean(a) · mean(1/a) ≠ 1. 기하평균은 이 성질을 지키는 유일한 통합 함수라는 것이 Aczél 과 Saaty 의 결과다(만장일치·동차성·역수 조건을 함께 요구할 때). 통합한 행렬이 여전히 «쌍대비교 행렬» 이어야 그 위에서 고유벡터법을 쓸 수 있으므로, AIJ 를 택하는 순간 기하평균은 선택이 아니라 귀결이다. 검증 C2 가 이 성질을 실제로 확인한다.

근거 2 — 우리 사용 장면은 AIJ 의 전제에 해당한다 (Forman & Peniwati, 1998). Forman 과 Peniwati 는 두 방식의 차이를 «수학» 이 아니라 «그룹을 무엇으로 보는가» 로 갈랐다. 위원회·조합·워크샵처럼 하나의 결정을 내리는 한 몸이면 AIJ, 서로 다른 이해를 가진 개인들의 선호를 각자 존중하며 합산해야 하면 AIP 다. 이 서비스가 쓰이는 자리는 앞쪽이다 — 참가자들은 «우리 조직의 우선순위» 를 함께 정하러 모였지, 각자의 선호를 등록하러 모인 것이 아니다.

파레토 공리 비판도 함께 적는다 (Ramanathan & Ganesh, 1994). AIJ 는 파레토 최적성을 만족하지 않는다 — 모든 개인이 A 를 B 보다 선호해도 통합 결과에서는 그렇지 않을 수 있다. 이것은 알려진 사실이며 숨기지 않는다. 다만 이 공리는 개인이 각자 주체로 남는 상황(AIP)에서 성립하는 잣대다. 그룹을 하나의 판단 주체로 보는 AIJ 에서는 «개인 각각의 선호가 보존되어야 한다» 는 요구 자체가 전제와 어긋난다 — 이것이 Forman & Peniwati 의 반론이고, 이 서비스는 그 입장을 따른다. AIP 상황에 이 서비스의 결과를 그대로 쓰면 안 되는 이유이기도 하다.

응답자에게 가중치를 두지 않는다. 모든 응답이 같은 무게로 통합된다. Ramanathan & Ganesh 가 제안한 «구성원 가중치» 는 두지 않는다 — 누가 더 무거운지를 정하는 일은 계산이 아니라 그 조직의 정치이고, 도구가 대신 정할 일이 아니다.

AIP 가 필요한 연구자는 원자료 CSV 로 재계산할 수 있다. 원자료 CSV 의 a_AB 열은 응답자별 행렬을 그대로 세울 수 있는 값이다(6-2절). 응답자마다 행렬을 만들어 고유벡터를 구하고 그 가중치들을 평균하면 AIP 결과가 된다 — 서비스가 낸 AIJ 값과 나란히 놓고 견줄 수 있다.

참고 문헌 - Aczél, J., & Saaty, T. L. (1983). Procedures for synthesizing ratio judgements. Journal of Mathematical Psychology, 27(1), 93–102. - Forman, E., & Peniwati, K. (1998). Aggregating individual judgments and priorities with the Analytic Hierarchy Process. European Journal of Operational Research, 108(1), 165–169. - Ramanathan, R., & Ganesh, L. S. (1994). Group preference aggregation methods employed in AHP: An evaluation and an intrinsic process for deriving members' weightages. European Journal of Operational Research, 79(2), 249–265.

2.5 글로벌 가중치

글로벌 = 상위 계층 가중치 × 하위 계층 지역 가중치

2.6 어긋난 쌍 식별 (CR 수정 루프)

CR 이 기준을 넘으면, 응답값과 가중치 역산값의 괴리가 큰 비교쌍을 상위 1~3개 짚는다. 괴리는 로그 거리로 잰다.

거리 = | ln a_ij − ln(wᵢ / wⱼ) |

비율 척도이므로 단순 차이로 재면 큰 값 쪽이 과대평가된다. 예를 들어 응답 9 대 역산 5(차이 4)가 응답 3 대 역산 1(차이 2)보다 항상 앞서지만, 실제로 더 어긋난 것은 3배 차이가 나는 후자다. 두 방식의 순위가 실제로 다름을 검증했다 (D4).


2.7 I&F 매트릭스 — 절대측정(Ratings) 2기준 특수형의 계산

쌍대비교가 아니라 채점이다. 항목마다 두 기준(효과·실현가능성)을 1~10 으로 매기고, 항목의 자리는 기준별 산술평균 두 개다. 규칙은 impact.py 한 곳에 있다.

값 정의
좌표 x = 실현가능성 평균, y = 효과 평균 (각각 그 축에 답한 사람의 산술평균)
경계 눈금의 절대 중앙 = (최소 + 최대) / 2 = 5.5. 응답 평균으로 가르지 않는다(가르면 어느 조사든 네 칸이 반드시 찬다)
사분면 y > 5.5 이면 효과 높음, x > 5.5 이면 실현 높음. 정확히 5.5 인 축이 있으면 어느 칸도 아니다 — «경계» 로 짚기만 한다
폭 최고 답 − 최저 답 (델파이 표와 같은 정의). 5 이상이면 «의견이 갈렸다» (전체 폭 9 의 절반)
몰림 경계 항목을 뺀 나머지가 둘 이상이고 모두 같은 칸이면 «몰렸다» — 오류가 아니라 결과다
평균 보조선 항목 평균들의 평균(축별). 기본 꺼짐. «이 후보들 사이에서는 여기가 가운데»

응답자 가중치·기준 가중치 모두 두지 않는다. 두 기준은 같은 무게이고(2×2 이므로), 사람도 같은 무게다(2.4-1 과 같은 이유). 기준을 N개로 열고 그 가중치를 AHP 로 재면 일반형(보류 15)이 된다 — 그때 가중합 Σ(가중치 × 등급값)이 이 절에 더해진다.

산술평균과 대소 비교뿐이라 독립 구현 대조는 하지 않았다. 검증할 것이 있다면 «경계 판정이 부동소수 오차로 흔들리지 않는가» 인데, 5 와 6 이 반반인 평균은 이진 소수로 정확히 5.5 라 abs(y − 5.5) < 1e-9 판정이 안전하다.

2.8 후보 평가(Ratings) — 절대측정 일반형의 계산 (2026-10-01 추가)

2.7 의 2×2 를 기준 k개·가중치로 넓힌 것이다(보류 15 의 일반형). 규칙은 ratings.py 한 곳에 있다.

값 정의
정규화 r' = r / 척도 최대 (1~5 면 /5, 1~10 이면 /10) — 이상(ideal) 모드
응답자 점수 S_a = Σ_c w_c · r'_ac (가중치 합 1)
종합 응답자 점수의 산술평균 — 모든 기준·후보를 다 매기고 제출한 응답만
분포·1위 후보별 최소~최대, 사람마다 가장 높은 후보의 수(동률 1위면 둘 다 센다)
동률 같은 값(1e-9 안)이면 같은 순위 — 1위를 지어내지 않는다
가중치 출처 쌍대비교에서 이어받으면 «쌍대비교 #N 상속», 직접 적으면 «진행자 지정»
민감도 각 기준 가중치를 ±5·10·15·20% 흔들고 합을 1로 다시 맞춰 1위가 바뀌는 가장 작은 흔들림을 한 문장으로

결과 화면은 아래 두 검증을 통과해야 열린다 — 앱이 결과를 그리기 전에 매번 다시 셈한다(ratings.engine_check).

R1 문헌값 대조 — Saaty, T.L. (1990) How to make a decision: The Analytic Hierarchy Process, European Journal of Operational Research 48, 9–26, 4.2절 «Absolute measurement: Employee evaluation» 표 5. 기준 6개(가중치 0.061 · 0.196 · 0.043 · 0.071 · 0.162 · 0.466)와 기준별 등급의 우선순위로 세 사람의 점수를 가중합한다. 실린 합계: Mr. X 0.623 · Ms. Y 0.369 · Mr. Z 0.478.

실린 값으로 다시 셈 문헌 차이
Mr. X 0.6219 0.623 0.0011
Ms. Y 0.3685 0.369 0.0005
Mr. Z 0.4771 0.478 0.0009
  • 허용 오차 ±0.002. 논문은 가중치·등급값을 소수 셋째 자리로 반올림해 싣고 합계는 반올림 전 값으로 셈했다 — 본문의 Mr. X 식을 실린 값 그대로 셈하면 0.6219 다. 순위(X > Z > Y)도 같아야 통과.
  • 판독 메모. 공개된 PDF 의 글자층(스캔 인식)에서 Maturity 의 «Immat.» 값이 0.181 로 읽힌다. 그러면 그 기준의 등급 합이 1.100 이 되고(다른 다섯 기준은 모두 1.000) Mr. Z 가 0.497 로 문헌 0.478 과 어긋난다. 0.081 이면 합 1.000, Mr. Z 0.477 — 인쇄값은 0.081 이고 0.181 은 인식 오류로 판단해 0.081 을 쓴다.
  • 이 예제는 등급마다 쌍대비교로 낸 우선순위를 쓴다. 가늠의 후보 평가는 등급을 1~N 숫자로 받아 r/최대로 바꾼다(등급 우선순위가 선형인 특수형). 대조하는 것은 가중합 엔진(S = Σ w·r')이다.

R2 동형 — 기준 2개·동일 가중치 후보 평가 = 2×2 매트릭스. 같은 응답(세 사람 × 후보 넷 × 두 기준)을 2×2 의 «두 축 평균의 합» 과 후보 평가(w = ½, ½)로 각각 셈해 순위가 같은지 본다. 2×2 는 이 일반형의 특수형이므로 달라지면 둘 중 하나가 틀린 것이다.

3. 검증 방법

두 축으로 대조한다.

축 1 — 독립 구현 대조

서비스는 거듭제곱법으로 푼다. 검증은 NumPy 고유값 분해(numpy.linalg.eig)라는 다른 알고리즘으로 같은 행렬을 푼다. 두 결과가 소수점 4자리까지 일치하는지 본다.

같은 코드를 두 번 돌려 같은 값이 나오는 것은 검증이 아니다. 다른 방법으로 풀어 같은 답이 나와야 구현이 정의를 따랐다고 말할 수 있다.

축 2 — 문헌값 대조

값이 공개된 표준 예제와 맞춘다. Saaty 의 3×3 예제는 가중치 (0.6370, 0.2583, 0.1047), CR 0.0332 로 널리 인용된다.

성질 검사

수치 대조로 잡히지 않는 것은 성질로 확인한다 — 가중치 합이 1인가, 통합 행렬이 역수 성질을 유지하는가, 같은 응답만 모으면 그 행렬이 그대로 나오는가, 짚어준 쌍을 고치면 CR 이 실제로 내려가는가.


4. 대상 케이스와 결과

전체 22건, 실패 0건. 차이는 최대 절대 오차이며 1e-16 수준은 배정밀도 부동소수점의 반올림 한계로, 두 방법이 사실상 같은 값에 도달했음을 뜻한다.

A. 고유벡터와 λmax — 독립 구현 대조

# 케이스 가중치 차이 λmax 차이 판정
A1 Saaty 3×3 표준 예제 2.2e-16 3.6e-15 통과
A2 완전 일관 4×4 1.1e-16 4.4e-16 통과
A3 전부 동등 5×5 2.8e-17 0.0 통과
A4 슬라이더 입력 경로 3×3 2.2e-16 3.6e-15 통과
A5 모순 응답 3×3 1.7e-16 0.0 통과
A6 최대 항목 수 7×7 1.1e-16 7.1e-15 통과

문헌값 대조 — A1 의 가중치 0.6370 / 0.2583 / 0.1047, CR 0.0332. 일치. 모든 케이스에서 가중치 합 = 1.

A4 는 슬라이더 값에서 행렬을 만드는 경로까지 거쳐 확인한 것으로, 척도 변환(2.1)이 올바른지를 함께 본다. A6 은 MVP 가 허용하는 최대 항목 수(7)에서의 동작이다.

B. 일관성

# 케이스 결과 기대 판정
B1 완전 일관 행렬 CI 0.000000 · CR 0.000000 0 통과
B2 전부 동등 CR 0.000000 0 통과
B3 모순 응답 (A>B, B>C, C>A) CR 2.7586 > 0.1 통과
B4 항목 2개 이하 CR 0 개념 없음 통과

C. 그룹 통합 (AIJ)

응답자 3인의 3×3 행렬을 통합해 확인했다.

# 케이스 결과 판정
C1 기하평균 통합 행렬 vs NumPy 최대 차이 4.4e-16 통과
C2 통합 행렬의 역수 성질 (a_ij · a_ji = 1) 성립 통과
C3 같은 응답만 모으면 그 행렬 그대로 성립 통과
C4 통합 행렬의 가중치 vs NumPy 최대 차이 1.4e-16 통과
C5 개인 가중치 평균과 다름 다름 확인 통과

C5 는 "AIJ 를 개인 가중치 평균으로 잘못 구현하지 않았음"을 보이는 검사다. 두 값이 같게 나오면 구현이 틀린 것이다.

D. CR 수정 루프

순환 모순을 넣은 4×4 행렬로 확인했다.

# 케이스 결과 판정
D1 모순 행렬의 CR 이 기준 초과 CR 1.3407 통과
D2 순환 모순에 낀 쌍이 상위로 식별 상위 (0,1) (0,2) (1,2) 통과
D3 짚어준 쌍을 고치면 CR 하락 1.3407 → 0.5313 통과
D4 로그 거리와 단순 차이의 순위가 다름 다름 확인 통과
D5 권장값 되돌리기 (비율 → 슬라이더) 3배→-2, 1/3배→+2, 동등→0 통과

5. 연결·식별자 검증 (2026-09-26 추가)

계산이 맞아도 엉뚱한 조사를 가리키면 결과는 틀린 것이 된다. 2026-09-26 에 실제로 그런 일이 있었다 — 워크샵의 전환 기록이 «이어진 우선순위 조사» 라며 남의 워크샵을 가리켰다(원인: 조사 id 가 삭제 후 재사용됨. 보고서). 그래서 검증의 범위를 자료 무결성까지 넓혔다.

실행: .venv/bin/python scripts/verify_links.py — 임시 DB 에 실제 스키마를 세우고 실제 코드(convert.py)를 불러 확인한다. 프로덕션 자료는 건드리지 않는다.

E. 조사 id 재사용

# 케이스 결과 판정
E1 지운 id 를 새 조사가 받지 않는다 지운 #1 → 새 #2 통과
E2 번호는 계속 늘어난다 #2 → #3 통과
E3 조사를 지우면 딸린 행(항목·응답자·카드)도 함께 지워진다 남은 행 0 통과

E3 이 통과해야 E1·E2 가 뜻을 갖는다 — 고아 행이 남으면 번호를 새로 줘도 옛 자료가 뒤섞인다.

F. 전환 연결 — 양쪽이 서로를 가리킬 때만

# 케이스 결과 판정
F1 정상 연결은 이어진다 #5 → #6 통과
F2 반대 방향도 찾는다 #6 → #5 통과
F3 남의 워크샵을 가리키면 연결을 내지 않는다 표시 없음 통과
F4 상대가 다른 워크샵을 원천이라 하면 내지 않는다 표시 없음 통과
F5 역방향 표시가 없는 옛 조사는 유형으로 거른다(AHP 는 통과) 이어짐 통과
F6 전환으로 만든 조사는 원본의 원천 표시를 물려받지 않는다 물려받지 않음 통과

F3 이 이번 사고의 회귀 검사다. F5 는 옛 조사를 버리지 않기 위한 것 — 역방향 표시가 없는 조사는 유형으로만 거르고, 맞으면 예전처럼 이어 준다(기존 기록은 고치지 않았다).

G. 마감된 자리의 입구와 쓰기

# 케이스 결과 판정
G1 열려 있으면 입구가 열린다 통과 통과
G2 열려 있으면 쓰기가 통과한다 통과 통과
G3 마감하면 입구가 닫힌다 — «마감된 조사입니다.» 404 통과
G4 마감하면 쓰기가 거절된다(카드·평가·투표·2×2 공통 문) 404 통과
G5 이미 들어온 사람의 보기는 끊기지 않는다 통과 통과
G6 다시 열면 입구도 쓰기도 되살아난다 통과 통과
G7 마감 + 2×2 수집 중이면 2×2 경로는 제출을 받는다 통과 통과
G8 같은 상태에서도 카드·투표 경로는 받지 않는다 통과 통과
G9 2×2 도 끝났으면 2×2 경로도 받지 않는다 통과 통과

G7 은 배포 당일 실기에서 잡힌 회귀다 — 마감을 그대로 «받지 않는다» 로 읽었더니 2×2 라운드가 통째로 막혔다(2026-09-26 21:46, 조사 #29 에서 POST /p/…/rate 가 404). 2×2 는 설계상 AHP 를 마감한 뒤 도는 라운드이므로, 물어야 할 것은 «조사가 닫혔는가» 가 아니라 «지금 이 사람이 낼 것을 받고 있는가» 다.

H. 단계의 문 — 닫은 단계는 받지 않는다 (2026-09-27 추가)

# 케이스 결과 판정
H1 열린 단계는 받는다 통과 통과
H2 닫은 단계는 받지 않는다 통과 통과
H3 닫은 사실이 누가·언제로 남는다 「카드 모으기 — … 마감 (…)」 통과
H4 지금 단계가 아니면 받지 않는다 통과 통과
H5 닫지 않은 단계는 넘어가지 못한다 「「카드 마감」을 먼저…」 통과
H6 닫으면 넘어간다 live_phase=group 통과
H7 되돌리면 다시 받는다 통과 통과
H8 되돌린 사실은 지워지지 않는다 되돌림 1회 통과
H9 단계 되돌림은 진행 기록의 묶음을 만들지 않는다 경계 0 통과
H10 열린 워크샵은 전환 화면을 열지 않는다 400 통과

J·K. 입구와 점 투표 판정 (2026-09-27 추가)

절 무엇 건수
J 입구도 «들어와 낼 것이 있는 단계인가» 를 묻는다 — 마감이어도 2×2 가 도는 중이면 들어온다 6
K 점 투표를 연 적이 있는가 — 생략 기록, 옛 조사는 지나간 흔적으로 판정(추정하지 않는다) 8

J3 이 D1 의 회귀 검사다 — A7 에서 «저장하는 길» 만 고치고 입구를 남겨 두었다.


H5 가 A3 의 회귀 검사다 — 「다음 단계로 간다」 와 「이 단계를 닫는다」 가 같은 버튼이었다. H9 는 places.py 가 적어 둔 반례(지각자 한 명이 가짜 회차를 만든다)를 코드로 못박은 것이다.


G5 가 이 묶음의 균형점이다 — 막는 것은 입구와 저장하는 길뿐이고, 폰을 들고 있던 분의 화면은 끊지 않는다(«마감되었습니다» 를 그 자리에서 읽게 한다). G3 의 문구는 이유를 설명하지 않는다 — 코드를 가진 사람이면 누구나 보는 문장이다.


5-2. 화면 검증 — 방이 보는 것 (2026-09-26 추가)

계산이 맞고 자료가 옳아도 그림이 다른 말을 하면 회의는 그림을 따라간다. 2026-09-26 리허설에서 발표 화면의 수렴 막대가 5.0 을 4.0 보다 짧게 그렸다(조사 #28). 숫자는 옳았고 막대만 틀렸으므로 계산 검증으로는 잡히지 않는 자리였다 (보고서).

실행: .venv/bin/python scripts/verify_screens.py — 임시 DB 에 실제 스키마를 세우고 실제 라우터를 불러 HTML 을 받아 확인한다. 재현 자료는 #28 과 같은 모양(5·5·4)이다 — 같은 값이 둘 있어야 격자의 열 밀림이 드러난다.

H. 수렴 막대 — 길이가 값을 따르는가

# 케이스 결과 판정
H1 항목마다 한 줄이 나온다 3줄 / 항목 3개 통과
H2 막대 길이는 척도 최댓값 기준 비례다 5.0→100% · 5.0→100% · 4.0→80% 통과
H3 값이 큰 것부터 내려 적는다 5.0 → 5.0 → 4.0 통과
H4 큰 값의 막대가 더 짧게 그려지지 않는다 통과 통과
H5 순위 없는 막대 목록은 두 열 격자를 쓴다 .showbars.norank 통과
H6 집계 기준이 막대와 같은 화면에 있다 「평가 1명 · 1~5점 평균」 통과
H7 마감을 되돌린 적이 있으면 기준 줄에 적힌다 「… · 재개방 2회」 통과

H4 가 이번 결함의 회귀 검사다. 뿌리는 «격자의 열 수와 한 줄이 내는 셀 수가 달랐던 것» 이다 — 세 열(순위·이름·막대) 격자에 셀을 둘만 내니 행마다 열이 한 칸씩 밀려, 막대가 제 값과 무관한 길이로 그려졌다. 값과 반대가 아니라 무관이었다.

I. 발표 화면에 남지 않아야 할 것

# 케이스 결과 판정
I 일곱 단계 전부에 검증 각주·워드마크·공통 꼬리말·링크·버튼이 없다 깨끗함 통과 (7건)

방이 함께 보는 화면에 남는 것은 그 방의 결론뿐이다. 빔에서 링크는 기능이 0이고, 만든 쪽의 이름이 결론 옆에 나란히 설 일이 아니다.

J. 접속 안내는 들어올 자리가 열려 있는 동안에만

# 케이스 결과 판정
J1 카드 단계에는 QR·접속 코드가 있다 통과 통과
J2 묶기가 시작되면 내린다 통과 통과
J3 그 자리에 묶음이 크게 선다 통과 통과

K. 결과 요약은 투표를 마감한 뒤에만

# 케이스 결과 판정
K1 마감 전 여섯 단계 어디에도 내지 않는다 cards~vote 통과
K2 투표를 마감하면 낸다 통과 통과
K3 투표 없이 워크샵을 마감한 자리에도 낸다 통과 통과

요약 한 장에는 득표 순위가 들어 있다. 마감 전에 그 창을 열면 남은 사람의 표가 거기에 끌린다.

L. 여정 띠와 단계 알약 — 한 값에서 나오는가

# 케이스 결과 판정
L1 카드 단계에서는 지도도 «카드» 다(그룹이 있어도) 통과 통과
L2 화면의 두 줄이 어긋나지 않는다 지도 「카드」 · 알약 「카드 모으기」 통과
L 지도는 live_phase 한 값에서만 나온다 rate1·closed·vote·voted 통과 (4건)

L2 가 회귀 검사다 — 지도는 «그룹이 있으면 묶기» 라는 파생 규칙을, 알약은 live_phase 를 읽어 같은 화면의 두 줄이 서로 다른 말을 했다. 두 표시가 어긋나면 진행자는 둘 다 믿지 않는다.

M·N·O·P·Q. 단계 동선과 지우는 길 (2026-09-27 추가)

절 무엇 건수
M 닫기 전엔 마감 버튼만, 닫은 뒤엔 다음 버튼만 — 네 단계 전수. 점 투표만 한 번(마감이 곧 공개). 받는 것이 없는 세 단계에는 마감 버튼이 없다 9
N AHP 전환은 워크샵을 마감한 뒤에만 — closed·voted 양쪽에서 4
O 참가자 화면 — 닫힌 단계에는 낼 곳이 없고, 서버도 404 가 아니라 303 으로 돌려보낸다 7
P 그룹 삭제 — 묶기 마감 전에만 · 카드는 미분류로 · 매긴 값이 있으면 거절 · 지운 사실 기록 6
Q 대상자 삭제 — 함께 사라지는 답을 세어 적고, 마감 뒤에는 막는다 4

| R | 연달아 넣는 입력 — 제출해도 입력칸이 교체되지 않는다(A1). 자바스크립트가 없으면 예전 길 | 8 | | S | 그룹 이름 변경 — 묶기 마감 전에만 · 매긴 값이 있으면 거절 · 조사 편집 화면에도 같은 조건 · 고친 사실 기록 | 6 | | U | 점 투표 생략 — 열지 않은 투표를 «0표» 로 그리지 않는다. 투표 단계에 발을 들였으면 «투표 0표» 로 사실대로 | 11 | | V | 발표 화면 — 카드 마감과 함께 QR 을 내리고, 평가 항목을 발표 크기로 | 4 | | W | AHP 결과 — 「원본 워크샵 회의록」 은 «다음으로» 에서 빼고 머리로 옮긴다 | 2 | | AA·BB | 얹힌 2×2 의 입구 셋과 서버 가드 · 이미 돌던 라운드는 끝까지 받는다 | 7 | | CC | 이어받은 조사 — 양쪽이 서로를 가리킬 때만, 항목은 옮기지 않는다 | 4 | | DD | 2×2 는 스스로 넘어간다 — 축별 현황, 축을 섞지 않는다 | 6 | | EE | 열지 않은 투표로 가는 길 — 화면 다섯 곳 비노출 + 서버가 단계 이동을 막는다 | 8 | | FF | 답하는 화면의 꼬리는 처리방침 하나 · 문의처는 그 문서 안에 | 6 | | T | 응답 뒤 항목 이름 변경 — 막지 않되 집계 기준 줄·CSV·회의록에 «N건». 응답 전 변경은 적기만 하고 보이지 않는다 | 9 |

R 은 화면의 «모양» 을 보는 검사다 — 한글 입력기가 영문으로 돌아가는지는 기계가 볼 수 없으므로, 그 원인(제출 때 화면을 새로 받는 것)이 사라졌는지를 본다. 실기 확인은 브라우저에서 따로 했다(제출 뒤에도 같은 입력 요소·값 비움·포커스 유지). S4 가 옆문 검사다 — 현장 화면만 막으면 조사 편집 화면으로 들어온다.

O4 가 A7 의 교훈을 지킨다 — 늦게 도착한 제출은 타이밍이지 오류가 아니므로 303 이다 (CLAUDE.md 9절). P4 와 Q1 은 같은 뿌리다: LiveResponse.node_id 와 comparisons.respondent_id 가 CASCADE 라 지우면 답이 조용히 함께 사라진다. 항목 쪽은 2026-09-05 감사 C-5 에서 세어 적게 고쳤는데 그룹·사람 쪽이 남아 있었다.


5. 검증 범위 밖

정직하게 밝힌다. 아래는 현재 판본이 보증하지 않는다. 첫 항목은 예정된 작업이고, 나머지는 계산 검증의 범위 자체가 아니다.

  • 상용 도구와의 화면 대 화면 비교 — 아직 하지 않았다 (베타 단계 예정). 지금은 알고리즘이 다른 독립 구현(NumPy)과 문헌값으로 확인한 상태다. 상용 도구 대조는 베타(설계서 8주차)에서 자문 연구자가 실제로 쓰는 도구와 수행한다. 임의의 도구를 골라 맞추는 것보다, 연구자가 논문·보고서에 쓰는 그 도구와 같은 값이 나오는지가 실제 신뢰 요건이기 때문이다. 그때 대조한 도구·행렬·결과를 이 문서에 한 절로 덧붙인다.
  • 2×2 매트릭스 평가(두 기준 채점) — 구현됨 (2026-09-16, 2026-09-18 축 일반화), 계산은 2.7 절. 학술적으로는 AHP 절대측정(Ratings)의 2기준 범용형이다 — 다수 후보를 쌍대비교 없이 기준별 채점으로 평가하는 정통 계열(Expert Choice 의 Ratings 모드 계열)에 속한다. 축은 효과 × 실현가능성(I&F, 기본) · 중요도 × 만족도(IPA) · 발생가능성 × 영향(위험) · 비용 × 편익 · 직접 입력 — 어느 축이든 계산은 같다(축 이름만 자료). 지원 범위는 참여자 채점형 2×2 만 — 의견이 데이터인 매트릭스(설계 §0-2). 계산은 산술평균과 경계 비교뿐이라 이 문서의 검증 스크립트(22건) 범위 밖이며, 고유벡터법과 무관하다. 설계는 docs/설계_IF매트릭스.md.
  • 후보 평가 — 2026-10-01 에 생겼다(§2.8, N기준 절대측정). 아래는 그 전에 적어 둔 우회로: 다만 대안이 4~5개 이하이고 정통 쌍대비교로 평가하고 싶다면 지금도 된다 — 2계층 조사의 소과제 자리에 대안을 넣으면 상위 계층이 기준, 하위가 대안이 되어 글로벌 가중치(2.5)가 곧 대안 점수다. 수학은 동일하다. 대안이 그보다 많으면 문항 수가 기준 수 × C(n,2) 로 늘어 응답이 무너지므로, 그 자리는 절대측정(Ratings)이 맡는다.
  • 응답의 타당성 — 계산이 정의를 따르는지는 확인했으나, 응답자가 항목을 제대로 이해했는지, 표본이 대표성을 갖는지는 계산의 문제가 아니다.
  • RI 표의 출처 차이 — 무작위 지수(RI)는 문헌마다 소수점 이하가 조금씩 다르다. 가늠은 위 2.3 의 값을 쓰며, 다른 표를 쓰면 CR 값이 미세하게 달라질 수 있다.
  • 대규모 응답에서의 수치 안정성 — 현재 검증은 MVP 범위(항목 2~7개, 응답자 수십 명)를 가정한다. 항목이 9개를 넘으면 RI 표의 마지막 값을 쓰며 이는 근사다.

6. 재실행 방법

.venv/bin/python scripts/verify_ahp.py

모든 케이스와 차이값이 출력되고, 하나라도 실패하면 종료 코드 1 로 끝난다.

계산 코드(ahp.py)를 고치면 반드시 다시 돌리고 이 문서의 4절 결과표와 아래 실행 기록을 갱신한다. 이는 CLAUDE.md 5절에 작업 규칙으로 박혀 있다.

검증 스크립트가 보는 코드는 ahp.py 한 곳이다. 화면이나 저장 방식을 바꾸는 일은 이 검증에 영향을 주지 않지만, 척도·반복 횟수·RI 표·통합 방식을 건드리면 영향을 준다.


6-2. 원자료로 직접 확인하기

이 문서를 읽고도 «내 손으로 다시 계산해 보겠다» 는 분을 위해, 원자료 CSV 는 행렬을 바로 세울 수 있는 형태로 나온다 (결과 화면 → 원자료 CSV).

열 뜻
우세 항목 A · B · 동등 — 항목 A 와 항목 B 중 어느 쪽이 더 중요하다고 답했는가
강도(Saaty) 1~9. 2.1 의 척도 그대로
a_AB 항목 A 의 항목 B 대비 중요도 비율. 이 값 하나면 행렬이 선다
슬라이더값(내부) −8~+8. 화면의 내부 표현이며 Saaty 값이 아니다

두 CSV 모두 머리글이 먼저 온다. 기계로 읽을 때는 그만큼 건너뛴다 — 원자료 CSV 는 앞 5줄(설명 4줄 + 빈 줄), 결과 CSV 는 앞 6줄(설명 5줄 + 빈 줄) 다음이 열 이름 줄이다. 결과 CSV 는 표가 둘이면 그 사이에 빈 줄이 한 번 더 있다 (1계층 조사는 표가 하나다).

# R 예시 — 원자료
raw <- read.csv("조사제목_원자료_20260905.csv", skip = 5, fileEncoding = "UTF-8-BOM")

a_AB 는 서비스가 행렬을 만들 때 쓰는 바로 그 값이다(ahp.scale_to_ratio). 그래서 아래처럼 세우면 서비스가 푼 것과 같은 행렬이 된다.

A[i][j] = a_AB          (그 행의 항목 A=i, 항목 B=j)
A[j][i] = 1 / a_AB
A[i][i] = 1

B 가 우세하면 a_AB 는 1보다 작은 역수다 — 예를 들어 B 가 강도 5 면 0.2.

a_AB 는 소수 6자리로 반올림하므로 역수 성질(a_AB · a_BA = 1)이 그 자리까지 정확하다. 반올림 없는 값이 필요하면 우세 항목 과 강도 로 세우면 된다 (A 우세면 강도, B 우세면 1/강도).

슬라이더값을 Saaty 값으로 읽지 말 것. 예전에는 이 열의 이름이 그냥 «응답값» 이었고, −4 를 강도 4 로 오독할 소지가 있었다(실제 강도는 5다). 이름을 «슬라이더값(내부)» 으로 바꾸고 위 세 열을 더한 이유가 그것이다.


7. 실행 기록

실행일 커밋 환경 결과
2026-09-04 3072a0c Python 3.13.9 · NumPy 2.5.2 22건 전부 통과
2026-09-05 144ec39 Python 3.13.9 · NumPy 2.5.2 (개발 맥) 22건 전부 통과
2026-09-05 144ec39 Python 3.12.3 · NumPy 2.5.2 (프로덕션 서버) 22건 전부 통과
2026-09-05 의존성 상향 직후 fastapi 0.141.1 · starlette 1.6.0 · python-multipart 0.0.32 22건 전부 통과
2026-09-05 ab1ac0f (2단계 배포) 프로덕션 서버 · Python 3.12.3 · NumPy 2.5.2 · fastapi 0.141.1 · starlette 1.6.0 22건 전부 통과
2026-09-26 f999aa4 (② 묶음 A6·A5·A4 배포) 프로덕션 서버 · 세 벌 전부 (verify_ahp 22 · verify_links 18 · verify_screens 26) 66건 전부 통과
2026-09-27 0c95eb8 (③ 묶음 A3·A2 배포) 프로덕션 서버 · 세 벌 전부 (verify_ahp 22 · verify_links 28 · verify_screens 57) 105건 전부 통과
2026-09-27 5b0d1a9 (④ 묶음 A1 · 이름 변경 기록 배포) 프로덕션 서버 · 세 벌 전부 (verify_ahp 22 · verify_links 28 · verify_screens 80) 130건 전부 통과
2026-09-27 227fbb7 (⑤-가 D1~D5 배포) 프로덕션 서버 · 세 벌 전부 (verify_ahp 22 · verify_links 40 · verify_screens 96) 158건 전부 통과
2026-10-01 34673d0 (3단계 D부 서식 배포) 프로덕션 서버 · 세 벌 전부 (verify_ahp 24 · verify_links 48 · verify_screens 290) 362건 전부 통과

둘째·셋째 줄은 감사 1단계 수정 7건을 배포하면서 돌린 것이다. ahp.py 는 건드리지 않았으나, 집계 쪽(results.py 의 미완 응답 제외)을 고쳤으므로 계산이 그대로인지 확인했다. 서버 줄은 배포점검표 6절이 요구하는 실행이며, 파이썬 버전이 개발 환경(3.13)과 다른데도(3.12) 같은 값이 나오는 것을 확인한 것이 이 줄의 뜻이다.

넷째·다섯째 줄은 의존성을 크게 올린 뒤(감사 ⑤) 돌린 것이다. ahp.py 는 웹 프레임워크를 쓰지 않으므로 영향이 없어야 하지만, «없어야 한다» 와 «없었다» 는 다르다. 다섯째 줄은 그 상향분이 실제로 올라간 프로덕션 서버에서 돌린 것이다.

이전 실행(고유벡터·CR 5건만 다루던 판본)은 2026-09-03 부터 통과해 왔으며, 2026-09-04 에 그룹 통합(AIJ)과 CR 수정 루프 케이스를 더해 22건으로 넓혔다. 2026-10-01 에 후보 평가의 문헌 대조(R1)와 2×2 동형(R2)을 더해 24건이 되었다(§2.8 — 4절 표는 고유벡터·CR 22건).