가늠 › 계산 검증
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절)
AHP 결과는 "이 항목의 가중치가 0.42"라는 숫자 하나로 요약되고, 그 숫자가 예산 배분이나 심사 배점의 근거가 된다. 그래서 계산이 맞는지를 쓰는 사람이 직접 확인할 수 있어야 한다.
가늠은 두 가지를 약속한다.
구현이 따르는 정의다. 프로토타입(ganum_prototype_v2.html)에서 검증된 구현을 그대로 옮겼다.
응답자는 두 항목 사이를 슬라이더 -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]
슬라이더를 손대지 않았거나 「동등으로 두고 넘기기」로 넘긴 비교는 동등(1)으로 집계한다. 그리고 그 건수를 결과의 집계 기준에 「미조작 동등 N건」 으로 적는다 (0이면 적지 않는다). 화면의 집계 기준 줄·결과 CSV·원자료 CSV·회의록이 같은 문구를 쓴다.
왜 받는가. 그전에는 손대지 않은 비교를 「무응답」으로 두었고, 무응답이 하나라도 있으면 그 응답 전체가 집계에서 빠졌다. 스무 쌍 중 하나를 지나친 분의 답 열아홉 개가 통째로 버려진다.
왜 적는가. 같은 a = 1 이라도 「동등이라 판단한 1」과 「손대지 않아 남은 1」은
다른 자료다. 계산은 둘을 구별하지 않지만(값이 같으므로 가중치도 CR 도 같다),
숫자를 읽는 사람은 구별할 수 있어야 한다. 그래서 막지 않고 적는다 — 응답자에게는
제출 직전에 한 번 알리고, 진행자에게는 집계 기준에 함께 내놓는다.
원자료 CSV 의 「조작 여부」 열에 비교마다 조작 / 미조작(동등) / 빈칸 이 적힌다.
이 규칙 이전(2026-09-27 이전) 조사의 무응답 처리는 종전대로다. 옛 「무응답」 비교를 가진 응답은 그대로 미완으로 제외된다. 이미 나간 결과는 바뀌지 않는다. 옛 자료의 「조작 여부」는 빈칸(
NULL)이며 「미조작 동등」으로 세지 않는다 — 알 수 없는 것을 「안 했다」로 세면 거짓이다. 배포 전에 프로덕션 자료 사본으로 조사 13건 전부를 옛 코드·새 코드로 각각 집계해 12자리까지 대조했다 (보고서 참조).
거듭제곱법(power iteration)으로 주고유벡터를 구한다. 200회 반복하며 매회 합으로 정규화한다. 결과 벡터의 합은 1이다.
λmax = (1/n) · Σ (A·w)ᵢ / wᵢ
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 이다.
여러 응답자를 하나로 합칠 때 행렬 원소별 기하평균으로 통합 행렬을 만든 뒤, 그 행렬에서 다시 고유벡터를 구한다.
Ā[i][j] = (∏ₖ A⁽ᵏ⁾[i][j])^(1/K)
개인 가중치를 평균 내지 않는다. 두 방식은 서로 다른 값을 낸다 (검증 C5).
그룹 통합에는 크게 두 길이 있다. 이 서비스는 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.
글로벌 = 상위 계층 가중치 × 하위 계층 지역 가중치
CR 이 기준을 넘으면, 응답값과 가중치 역산값의 괴리가 큰 비교쌍을 상위 1~3개 짚는다. 괴리는 로그 거리로 잰다.
거리 = | ln a_ij − ln(wᵢ / wⱼ) |
비율 척도이므로 단순 차이로 재면 큰 값 쪽이 과대평가된다. 예를 들어 응답 9 대 역산 5(차이 4)가 응답 3 대 역산 1(차이 2)보다 항상 앞서지만, 실제로 더 어긋난 것은 3배 차이가 나는 후자다. 두 방식의 순위가 실제로 다름을 검증했다 (D4).
쌍대비교가 아니라 채점이다. 항목마다 두 기준(효과·실현가능성)을 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.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 |
R2 동형 — 기준 2개·동일 가중치 후보 평가 = 2×2 매트릭스. 같은 응답(세 사람 × 후보 넷 × 두 기준)을 2×2 의 «두 축 평균의 합» 과 후보 평가(w = ½, ½)로 각각 셈해 순위가 같은지 본다. 2×2 는 이 일반형의 특수형이므로 달라지면 둘 중 하나가 틀린 것이다.
두 축으로 대조한다.
서비스는 거듭제곱법으로 푼다. 검증은 NumPy 고유값 분해(numpy.linalg.eig)라는
다른 알고리즘으로 같은 행렬을 푼다. 두 결과가 소수점 4자리까지 일치하는지 본다.
같은 코드를 두 번 돌려 같은 값이 나오는 것은 검증이 아니다. 다른 방법으로 풀어 같은 답이 나와야 구현이 정의를 따랐다고 말할 수 있다.
값이 공개된 표준 예제와 맞춘다. Saaty 의 3×3 예제는 가중치 (0.6370, 0.2583, 0.1047), CR 0.0332 로 널리 인용된다.
수치 대조로 잡히지 않는 것은 성질로 확인한다 — 가중치 합이 1인가, 통합 행렬이 역수 성질을 유지하는가, 같은 응답만 모으면 그 행렬이 그대로 나오는가, 짚어준 쌍을 고치면 CR 이 실제로 내려가는가.
전체 22건, 실패 0건. 차이는 최대 절대 오차이며 1e-16 수준은 배정밀도 부동소수점의
반올림 한계로, 두 방법이 사실상 같은 값에 도달했음을 뜻한다.
| # | 케이스 | 가중치 차이 | λ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)에서의 동작이다.
| # | 케이스 | 결과 | 기대 | 판정 |
|---|---|---|---|---|
| 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 | 개념 없음 | 통과 |
응답자 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 를 개인 가중치 평균으로 잘못 구현하지 않았음"을 보이는 검사다. 두 값이 같게 나오면 구현이 틀린 것이다.
순환 모순을 넣은 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 | 통과 |
계산이 맞아도 엉뚱한 조사를 가리키면 결과는 틀린 것이 된다. 2026-09-26 에 실제로 그런 일이 있었다 — 워크샵의 전환 기록이 «이어진 우선순위 조사» 라며 남의 워크샵을 가리켰다(원인: 조사 id 가 삭제 후 재사용됨. 보고서). 그래서 검증의 범위를 자료 무결성까지 넓혔다.
실행: .venv/bin/python scripts/verify_links.py — 임시 DB 에 실제 스키마를 세우고
실제 코드(convert.py)를 불러 확인한다. 프로덕션 자료는 건드리지 않는다.
| # | 케이스 | 결과 | 판정 |
|---|---|---|---|
| E1 | 지운 id 를 새 조사가 받지 않는다 | 지운 #1 → 새 #2 | 통과 |
| E2 | 번호는 계속 늘어난다 | #2 → #3 | 통과 |
| E3 | 조사를 지우면 딸린 행(항목·응답자·카드)도 함께 지워진다 | 남은 행 0 | 통과 |
E3 이 통과해야 E1·E2 가 뜻을 갖는다 — 고아 행이 남으면 번호를 새로 줘도 옛 자료가 뒤섞인다.
| # | 케이스 | 결과 | 판정 |
|---|---|---|---|
| F1 | 정상 연결은 이어진다 | #5 → #6 | 통과 |
| F2 | 반대 방향도 찾는다 | #6 → #5 | 통과 |
| F3 | 남의 워크샵을 가리키면 연결을 내지 않는다 | 표시 없음 | 통과 |
| F4 | 상대가 다른 워크샵을 원천이라 하면 내지 않는다 | 표시 없음 | 통과 |
| F5 | 역방향 표시가 없는 옛 조사는 유형으로 거른다(AHP 는 통과) | 이어짐 | 통과 |
| F6 | 전환으로 만든 조사는 원본의 원천 표시를 물려받지 않는다 | 물려받지 않음 | 통과 |
F3 이 이번 사고의 회귀 검사다. F5 는 옛 조사를 버리지 않기 위한 것 — 역방향 표시가 없는 조사는 유형으로만 거르고, 맞으면 예전처럼 이어 준다(기존 기록은 고치지 않았다).
| # | 케이스 | 결과 | 판정 |
|---|---|---|---|
| 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 를 마감한 뒤 도는 라운드이므로, 물어야 할 것은 «조사가 닫혔는가» 가
아니라 «지금 이 사람이 낼 것을 받고 있는가» 다.
| # | 케이스 | 결과 | 판정 |
|---|---|---|---|
| H1 | 열린 단계는 받는다 | 통과 | 통과 |
| H2 | 닫은 단계는 받지 않는다 | 통과 | 통과 |
| H3 | 닫은 사실이 누가·언제로 남는다 | 「카드 모으기 — … 마감 (…)」 | 통과 |
| H4 | 지금 단계가 아니면 받지 않는다 | 통과 | 통과 |
| H5 | 닫지 않은 단계는 넘어가지 못한다 | 「「카드 마감」을 먼저…」 | 통과 |
| H6 | 닫으면 넘어간다 | live_phase=group |
통과 |
| H7 | 되돌리면 다시 받는다 | 통과 | 통과 |
| H8 | 되돌린 사실은 지워지지 않는다 | 되돌림 1회 | 통과 |
| H9 | 단계 되돌림은 진행 기록의 묶음을 만들지 않는다 | 경계 0 | 통과 |
| H10 | 열린 워크샵은 전환 화면을 열지 않는다 | 400 | 통과 |
| 절 | 무엇 | 건수 |
|---|---|---|
| J | 입구도 «들어와 낼 것이 있는 단계인가» 를 묻는다 — 마감이어도 2×2 가 도는 중이면 들어온다 | 6 |
| K | 점 투표를 연 적이 있는가 — 생략 기록, 옛 조사는 지나간 흔적으로 판정(추정하지 않는다) | 8 |
J3 이 D1 의 회귀 검사다 — A7 에서 «저장하는 길» 만 고치고 입구를 남겨 두었다.
H5 가 A3 의 회귀 검사다 — 「다음 단계로 간다」 와 「이 단계를 닫는다」 가 같은 버튼이었다.
H9 는 places.py 가 적어 둔 반례(지각자 한 명이 가짜 회차를 만든다)를 코드로 못박은 것이다.
G5 가 이 묶음의 균형점이다 — 막는 것은 입구와 저장하는 길뿐이고, 폰을 들고 있던 분의 화면은 끊지 않는다(«마감되었습니다» 를 그 자리에서 읽게 한다). G3 의 문구는 이유를 설명하지 않는다 — 코드를 가진 사람이면 누구나 보는 문장이다.
계산이 맞고 자료가 옳아도 그림이 다른 말을 하면 회의는 그림을 따라간다. 2026-09-26 리허설에서 발표 화면의 수렴 막대가 5.0 을 4.0 보다 짧게 그렸다(조사 #28). 숫자는 옳았고 막대만 틀렸으므로 계산 검증으로는 잡히지 않는 자리였다 (보고서).
실행: .venv/bin/python scripts/verify_screens.py — 임시 DB 에 실제 스키마를 세우고
실제 라우터를 불러 HTML 을 받아 확인한다. 재현 자료는 #28 과 같은 모양(5·5·4)이다 —
같은 값이 둘 있어야 격자의 열 밀림이 드러난다.
| # | 케이스 | 결과 | 판정 |
|---|---|---|---|
| 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 | 일곱 단계 전부에 검증 각주·워드마크·공통 꼬리말·링크·버튼이 없다 | 깨끗함 | 통과 (7건) |
방이 함께 보는 화면에 남는 것은 그 방의 결론뿐이다. 빔에서 링크는 기능이 0이고, 만든 쪽의 이름이 결론 옆에 나란히 설 일이 아니다.
| # | 케이스 | 결과 | 판정 |
|---|---|---|---|
| J1 | 카드 단계에는 QR·접속 코드가 있다 | 통과 | 통과 |
| J2 | 묶기가 시작되면 내린다 | 통과 | 통과 |
| J3 | 그 자리에 묶음이 크게 선다 | 통과 | 통과 |
| # | 케이스 | 결과 | 판정 |
|---|---|---|---|
| K1 | 마감 전 여섯 단계 어디에도 내지 않는다 | cards~vote | 통과 |
| K2 | 투표를 마감하면 낸다 | 통과 | 통과 |
| K3 | 투표 없이 워크샵을 마감한 자리에도 낸다 | 통과 | 통과 |
요약 한 장에는 득표 순위가 들어 있다. 마감 전에 그 창을 열면 남은 사람의 표가 거기에 끌린다.
| # | 케이스 | 결과 | 판정 |
|---|---|---|---|
| L1 | 카드 단계에서는 지도도 «카드» 다(그룹이 있어도) | 통과 | 통과 |
| L2 | 화면의 두 줄이 어긋나지 않는다 | 지도 「카드」 · 알약 「카드 모으기」 | 통과 |
| L | 지도는 live_phase 한 값에서만 나온다 |
rate1·closed·vote·voted | 통과 (4건) |
L2 가 회귀 검사다 — 지도는 «그룹이 있으면 묶기» 라는 파생 규칙을, 알약은 live_phase
를 읽어 같은 화면의 두 줄이 서로 다른 말을 했다. 두 표시가 어긋나면 진행자는 둘 다 믿지
않는다.
| 절 | 무엇 | 건수 |
|---|---|---|
| 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 에서 세어 적게 고쳤는데 그룹·사람 쪽이 남아 있었다.
정직하게 밝힌다. 아래는 현재 판본이 보증하지 않는다. 첫 항목은 예정된 작업이고, 나머지는 계산 검증의 범위 자체가 아니다.
docs/설계_IF매트릭스.md..venv/bin/python scripts/verify_ahp.py
모든 케이스와 차이값이 출력되고, 하나라도 실패하면 종료 코드 1 로 끝난다.
계산 코드(ahp.py)를 고치면 반드시 다시 돌리고 이 문서의 4절 결과표와 아래 실행 기록을
갱신한다. 이는 CLAUDE.md 5절에 작업 규칙으로 박혀 있다.
검증 스크립트가 보는 코드는 ahp.py 한 곳이다. 화면이나 저장 방식을 바꾸는 일은 이
검증에 영향을 주지 않지만, 척도·반복 횟수·RI 표·통합 방식을 건드리면 영향을 준다.
이 문서를 읽고도 «내 손으로 다시 계산해 보겠다» 는 분을 위해, 원자료 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다). 이름을 «슬라이더값(내부)» 으로 바꾸고 위 세 열을 더한 이유가 그것이다.
| 실행일 | 커밋 | 환경 | 결과 |
|---|---|---|---|
| 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건).