제8장
어떤 시스템이 “WIA-BIO-004를 구현했다”는 주장은 다른 사람이 확인할 수 있을 때에만 가치가 있다. 이 장은 표준의 요구사항을 점검표로 정리하고, 기대값이 정확히 정해진 정상·부정 시험 벡터를 만들어 v1.0 SDK와 CLI에 실제로 돌려 본 뒤, 통과한 것과 실패한 것, 그리고 다음 개정판이 고쳐야 할 것을 기록한다.
앞의 일곱 장은 WIA-BIO-004가 무엇을 요구하는지를 설명하였다. 바이오마커 유형과 임상적 역할의 어휘(제1장), 고정된 진단 통계 집합(제2장), 측정값·참고구간·검증 연구를 담는 JSON 레코드(제3장), SDK와 CLI(제4장), 분석적·임상적 검증 프로토콜(제5장·제6장), 그리고 프라이버시를 고려한 통합 아키텍처(제7장)가 그것이다. 그러나 특정 제품이 실제로 표준대로 동작하는지 판단해야 하는 검사실 책임자, 임상시험 의뢰자의 데이터 관리자, 규제 담당자에게는 이것만으로 부족하다. 필요한 것은 적합성 주장, 즉 시스템이 어떤 요구사항을 충족하는지에 대한 진술과 제3자가 다시 실행해 볼 수 있는 증거이다.
이런 종류의 데이터 표준에 대한 적합성 시험에는 독특한 성질이 있다. WIA-BIO-004가 규정하는 내용의 대부분은 산술이고, 산술에는 정답이 있다. 주어진 절단값에서의 민감도는 견해의 문제가 아니며, ROC 점 집합 아래의 사다리꼴 면적도 마찬가지이다. 따라서 기대 출력을 소수점 넷째 자리까지 확정한 시험 벡터를 쓸 수 있고, 그것으로 실제 결함을 찾을 수 있다. 이 장은 바로 그 작업을 하며, 결과 가운데 몇 가지는 불편하다. v1.0 참조 SDK는 배포된 상태로는 타입 검사를 통과하지 못하고, 동점이 있거나 성긴 임곗값 격자에서 AUC를 낮게 보고하며, v1.0 CLI는 입력과 무관하게 고정된 시연용 숫자를 출력한다. 이런 발견을 정직하게 기록하는 것이 적합성 작업의 목적이지, 감추어야 할 흠이 아니다.
아래의 SDK 관찰은 모두 v1.0 TypeScript 소스(api/typescript/src/index.ts, types.ts)의 작업용 사본을 TypeScript 5.9로 컴파일하고 Node.js 22에서 실행하여 얻었다. 컴파일을 위해 소스를 고쳐야 했던 부분은 해당 위치에서 밝힌다.
WIA-BIO-004 v1.0에는 “적합성”이라는 제목의 절이 따로 없다. 적합성 주장을 시험할 근거가 되는 요구사항은 네 곳에 흩어져 있으며, 점검표는 이를 한데 모아야 한다.
"standard": "WIA-BIO-004" 봉투와 명시된 version과 함께 내보내고, 적합한 소비자는 이를 받아들인다.B001(표본 크기 부족)부터 B006(통계적 검정력 부족)까지 여섯 개 코드를 정의하고, SDK는 이를 B010까지 확장한다. 실패를 보고하는 시스템은 이 코드를 정해진 의미대로 써야 한다.앞의 세 가지는 소프트웨어와 데이터의 속성이고, 네 번째는 바이오마커 연구의 속성이다. 적합성 주장에서는 이 둘을 분리해야 한다. 공급사의 통계 엔진이 완전히 적합하더라도, 고객이 그 엔진으로 검증한 바이오마커는 코호트를 하나밖에 모집하지 못해 요구사항 3을 충족하지 못할 수 있다. 반대로 잘 설계된 연구를 부적합한 소프트웨어로 분석해도 숫자는 맞을 수 있다. 다만 맞았다는 것을 입증하지 못할 뿐이다.
문서가 말하지 않는 내용도 그만큼 중요하다. v1.0은 번호가 붙은 적합성 등급, 단계, 통과율 기준을 정의하지 않는다. 다른 인증 프로그램에서 흔히 보는 “브론즈·실버·골드” 식의 단계 체계는 v1.0에 근거가 없으며, 이 책도 그런 체계를 만들어 내지 않는다. README는 인증 사이트로 연결되는 링크를 두고 있지만, v1.0 문서에는 인증 절차, 공인 평가자, 시험 도구 모음에 대한 기술이 없다. 단계별 부속서(annex)는 증거 패키지와 프로파일 매니페스트라는 비규범적 개념을 소개하고(아래 §8.4), 저장소에 실제로는 없는 벡터 디렉터리를 가리킨다.
실무적 결론은 v1.0에서의 적합성 주장이 증거를 갖춘 자기 선언이라는 것이다. 가장 강한 형태의 자기 선언은 모든 요구사항을 나열하고, 각각을 지원·부분 지원·제외로 표시하며, 재현 가능한 시험 결과를 첨부한다. 이 장의 나머지 부분은 그것을 만드는 방법을 보여 준다.
다음 표는 이 장 전체에서 사용하는 점검표이다. 식별자(C-01 …)는 시험 결과와 매니페스트가 특정 행을 가리킬 수 있도록 이 책이 붙인 것이며, 표준에 번호가 매겨져 있는 것은 아니다.
| ID | 요구사항(출처) | 증거 | 시험 방법 |
|---|---|---|---|
| C-01 | 표준화된 측정값 수집(§8.1-1, §7.1) | 시스템이 생성한 §7.1 레코드 표본 | 스키마 검증, 알려진 입력과 필드 단위 비교 |
| C-02 | 자동 QC 점검(§8.1-2) | QC 규칙 설정, 플래그 검체 감사 기록 | qualityFlags.hemolyzed = true 레코드를 넣어 제외 또는 주석 처리 확인 |
| C-03 | ROC·민감도·특이도(§8.1-3, §4) | 통계 엔진 출력 | 정상 벡터 G-01~G-07(§8.2) |
| C-04 | 결과와의 임상 상관(§8.1-4, §6) | 상관·생존 분석 출력 | 독립 패키지로 계산해 둔 Spearman ρ 또는 위험비가 있는 기준 데이터 |
| C-05 | 표준화된 성능 보고(§8.1-5, §5.3.1) | 생성된 보고서 | 내부 일관성: PPV/NPV가 민감도·특이도·명시된 유병률에서 도출되는지 확인 |
| C-06 | 레코드 봉투와 버전(§7.1, §7.2) | 내보낸 모든 레코드 | standard가 "WIA-BIO-004"이고 version이 시맨틱 버전인지 확인 |
| C-07 | 단위 일관성(§7.1, 제3장) | 검증기 동작 | 부정 벡터 N-05(값의 단위와 참고구간 단위가 다름) |
| C-08 | 시간 일관성(제3장) | 검증기 동작 | 부정 벡터 N-06(dateMeasured가 dateCollected보다 이름) |
| C-09 | 오류 코드 B001–B006(§8.3) | 오류 응답 | 부정 벡터 N-01~N-04가 문서화된 코드를 반환하는지 확인 |
| C-10 | 표본 크기 요구(README 1) | 연구 레코드의 population.cases, controls | 명시된 최솟값과 대조, README와 SDK의 차이에 유의(§8.3) |
| C-11 | 독립 코호트(README 3) | validation.validationCohorts[] | 독립된 모집단이 둘 이상인지, 각 검증 코호트가 자체 AUC와 함께 기재되었는지 확인(6.8절 참조) |
| C-12 | 눈가림과 참조 표준(README 4–5) | referenceStandard.method, 프로토콜 문서 | 문서 검토. 데이터만으로는 확인할 수 없음 |
| C-13 | 재현성과 안정성(README 6–7) | 정밀도·안정성 보고서(제5장) | 원시 반복 측정값에서 CV를 다시 계산하고, 주장이 명시한 허용 기준과 비교 |
| C-14 | 프라이버시 통제(§9) | 비식별화·암호화 설정 | 설정 점검, 재식별 표본 점검(제7장) |
C-12부터 C-14까지는 C-03과 같은 의미로 자동화할 수 없다. 그래도 점검표에 남겨 두어야 한다. 이를 조용히 빼 버린 주장은 실제로 확인한 범위보다 더 많이 확인한 것처럼 보이기 때문이다.
시험 벡터는 입력과 기대 출력, 그리고 허용 오차의 묶음이다. 정상(golden) 벡터는 정상 동작을 시험하며 특정한 숫자를 내야 한다. 부정(negative) 벡터는 적합한 시스템이 특정 오류와 함께 거부해야 하는 입력이다. 두 가지 모두 필요하다. 잘못된 입력을 포함한 모든 입력에 그럴듯한 숫자를 돌려주는 통계 엔진은 정상 벡터를 전부 통과하면서도 여전히 위험하다.
가장 손쉬운 정상 벡터는 명세에 이미 실려 있는 계산 예제이다. 누구나 손으로 확인할 수 있기 때문이다. 다만 두 가지는 주의가 필요하다. §5.3.1 보고서 예제는 PPV 86.0 %, NPV 84.5 %를 제시하지만, 이는 같은 예제의 민감도(82 %), 특이도(88 %), 유병률(0.50)에서 나오지 않는 값이므로 아래 벡터는 다시 계산한 값을 쓴다. 또 부록 A.1은 한 임곗값에서의 개수만 제시하고 개별 측정값은 주지 않으므로, ROC 동작을 시험하려면 완전한 데이터 집합이 필요하다. 이는 §8.2.2에서 제공한다.
| 벡터 | 입력 | 기대 출력 | 출처 |
|---|---|---|---|
| G-01 | TP 75, FN 10 | 민감도 = 88.2 % | §4.1.1 |
| G-02 | TN 95, FP 5 | 특이도 = 95.0 % | §4.1.2 |
| G-03 | TP 42, FN 8, TN 45, FP 5 | 민감도 84.0 %, 특이도 90.0 %, PPV 89.4 %, NPV 84.9 %, 정확도 87.0 %, LR+ 8.40, LR− 0.178, J 0.74 | 부록 A.1 |
| G-04 | 민감도 84 %, 특이도 90 %, 유병률 0.10 | PPV 48.3 %, NPV 98.1 % | §4.1.3(베이즈 형태) |
| G-05 | 민감도 82 %, 특이도 88 %, 유병률 0.50 | PPV 87.2 %, NPV 83.0 %, LR+ 6.83, LR− 0.20 | §5.3.1, 정정값 |
| G-06 | AUC 0.87, n₁ = n₀ = 150 | Hanley–McNeil SE 0.0210, 95 % CI 0.829–0.911 | §10.3 |
| G-07 | 합성 집합 S-100(§8.2.2), 절단값 75 | 혼동행렬 42 / 8 / 45 / 5, 정확한 AUC 0.9358, 최적점 J = 0.74 | 이 책 |
G-06에는 설명이 필요하다. §7.2 검증 연구 예제는 환자 150명·대조군 150명에 대해 AUC 0.87과 95 % 신뢰구간 0.82–0.92를 보고한다. 같은 입력에 §10.3의 Hanley–McNeil 공식을 적용하면 0.829–0.911이 나온다. 부트스트랩이나 DeLong 구간은 정당하게 더 넓을 수 있으므로 이 차이가 곧 예제의 오류를 뜻하지는 않는다. 그러나 검증기가 어떤 한 공식과 다르다는 이유만으로 신뢰구간을 거부해서는 안 된다는 뜻은 된다. 검증기가 요구해야 할 것은 레코드가 구간을 어떻게 구했는지 밝히는 것인데, v1.0 레코드에는 그런 필드가 없다. 이 공백은 §8.7에서 다시 다룬다.
ROC 코드를 시험하려면 개수가 아니라 개별 값이 필요하다. 여기서 S-100이라 부르는 다음 집합은 “값 ≥ 75이면 양성”이라는 절단값에서 부록 A.1의 개수가 정확히 재현되도록 만든 것이다. 전적으로 합성된 데이터로 실제 검사법을 나타내지 않으며, 어떤 언어로든 한 줄로 다시 만들 수 있다는 것이 장점이다.
S-100 (정수, 양 끝 포함 범위; 라벨 1 = 환자, 0 = 대조군)
환자 (50): 76, 77, …, 117 → 42개, 모두 ≥ 75 (절단값 75에서 TP)
60, 61, …, 67 → 8개, 모두 < 75 (FN)
대조군 (50): 30, 31, …, 74 → 45개, 모두 < 75 (TN)
80, 81, …, 84 → 5개, 모두 ≥ 75 (FP)
절단값 75 : TP 42 FN 8 TN 45 FP 5 (= 부록 A.1)
동점 : 환자 값 80–84와 대조군 값 80–84가 같음 (동점 쌍 5개)
정확한 AUC : 2,500개 쌍에 대한 P(환자 > 대조군) + ½·P(동점) = 0.9358
유든 최적점 : J = 0.84 + 0.90 − 1 = 0.74, 관측값 76에서 처음 도달
Hanley–McNeil : SE(0.9358; 50, 50) = 0.0258 → 95 % CI 0.885–0.986
S-100이 G-03보다 쓸모 있는 이유는 두 가지이다. 첫째, 집단 간 동점이 있는데, ROC 구현이 가장 자주 틀리는 곳이 바로 동점이다. 둘째, 절단값 75가 관측값에 없으므로 구현이 최적 임곗값에 어떤 라벨을 붙이는지도 시험한다. 최적점을 76(같은 분류를 내는 가장 작은 관측값)으로 보고하는 엔진과 75로 보고하는 엔진은 보고된 민감도와 특이도가 0.84와 0.90이기만 하면 둘 다 옳다. 따라서 시험은 임곗값 라벨이 아니라 작동점(operating point)을 기준으로 단정해야 한다.
부정 벡터는 §8.3에서 약속한 오류 동작과 제3장의 레코드 수준 규칙을 고정한다. “기대 동작” 열은 적합한 구현이 해야 할 일이며, v1.0이 실제로 무엇을 하는지는 §8.3에서 보고한다.
| 벡터 | 입력 | 기대 동작 |
|---|---|---|
| N-01 | S-100에서 predictions의 마지막 값을 제거(라벨과 값의 길이 불일치) | B002(잘못된 측정값)로 거부 |
| N-02 | S-100의 모든 라벨을 1로 설정 | B002로 거부. 두 집단이 모두 필요함 |
| N-03 | S-100의 처음 99개 값 | B001(표본 크기 부족)로 거부 |
| N-04 | referenceStandard가 없는 검증 연구 | B003(참조 표준 누락)로 거부 |
| N-05 | unit: "mg/dL"이지만 referenceRange.unit: "mg/L"인 §7.1 CRP 레코드 | 거부하거나 UCUM으로 명시적 정규화. 원시 숫자끼리 비교해서는 안 됨 |
| N-06 | dateMeasured가 dateCollected보다 이른 §7.1 레코드 | 데이터 품질 오류로 거부 |
| N-07 | diseaseStatus가 불리언이 아니라 문자열 "healthy"인 측정값 | 거부. 문자열을 “질환 있음”으로 강제 변환해서는 안 됨 |
| N-08 | 90 % 신뢰구간 요청 | z = 1.645를 쓰거나, 지원하지 않는 수준이면 거부. 다른 z를 몰래 쓰면 안 됨 |
N-04는 입력에 참조 표준 필드가 없는 SDK에는 대응하는 시험이 없으며, §7.2 레코드를 받는 시스템에 적용된다. N-05와 N-06은 통계 엔진 자체보다 그 앞단의 레코드 검증기에 두어야 할 점검이다.
기대값에는 허용 오차가 필요하며, 허용 오차는 시험자의 관대함이 아니라 숫자가 표기되는 방식을 반영해야 한다. 표준은 백분율을 소수 첫째 자리, 우도비를 둘째 자리까지 표기하므로 합리적인 규칙은 다음과 같다. 개수는 정확히 일치해야 하고, 백분율은 반올림 전 값 기준 ±0.05 %p, 비율과 AUC는 ±0.0005 이내여야 한다. 허용 오차는 다른 공식을 봐주는 장치가 아니다. 구현이 다른 신뢰구간 방법을 쓴다면 벡터가 통과할 때까지 허용 오차를 넓힐 것이 아니라 그 방법을 기록해야 한다.
위의 벡터를 v1.0 SDK에 돌렸다. 결과는 세 부류로 나뉜다. 통과하는 것, 동작은 옳지만 잘못 쓰기 쉬워 모든 주장에 문서화해야 하는 것, 그리고 명백한 결함이다.
SDK의 첫 번째 시험은 빌드되는지 여부이다. v1.0 소스는 TypeScript 5.9에서 타입 검사를 통과하지 못한다. types.ts는 선언 시점에 이미 export된 이름을 다시 내보내는 export type { … } 블록과 export { BIOMARKER_CONSTANTS, BiomarkerErrorCode, BiomarkerError } 줄로 끝나고, index.ts는 export class로 선언한 클래스에 대해 export { BiomarkerDataSDK }를 반복한다. 컴파일러는 TS2323(“내보낸 변수를 다시 선언할 수 없음”)과 TS2484(“export 선언이 기존 export 선언과 충돌함”)를 보고한다. 동작을 전혀 바꾸지 않는 이 중복 블록을 지우면 패키지가 컴파일되며, 이하의 결과는 모두 그렇게 고친 사본에서 측정하였다. 또 패키지는 Jest 설정을 선언하고 있지만 시험 파일은 하나도 포함하지 않으므로, 이 장이 만드는 종류의 증거가 저장소 안에는 없다.
절단값 75와 기본 유병률 0.5로 S-100에 validateBiomarker를 호출하면 혼동행렬 42 / 45 / 5 / 8, 민감도 84, 특이도 90, 정확도 87, PPV 89.36, NPV 84.91, LR+ 8.40, LR− 0.178이 반환되어 G-03과 일치한다. N-01과 N-02는 코드 B002의 BiomarkerError를, N-03은 B001을 던진다. Hanley–McNeil 표준오차는 §10.3에 적힌 그대로 구현되어 있다. 자동 격자에서의 유든 최적점은 값 76, 민감도 0.84, 특이도 0.90, J = 0.74로 보고되며, 이는 G-07이 기대하는 작동점이다.
두 가지 척도. DiagnosticPerformance는 민감도·특이도·PPV·NPV·정확도를 백분율로 보고하지만, 모든 ROCPoint와 optimalThreshold는 민감도와 특이도를 0에서 1 사이의 비율로 보고한다. optimalThreshold.sensitivity에 대해 84를 단정하는 시험은 실패하고, 둘을 섞은 대시보드는 민감도를 0.84 %로 보여 준다. 적합성 시험은 척도를 명시적으로 단정해야 한다.
유병률 기반 예측도. SDK는 PPV를 TP/(TP + FP)로 계산하는 일이 없다. 언제나 prevalence 인수(기본값 0.5)를 넣은 베이즈 형태를 쓴다. S-100에서 두 값이 같은 것은 표본이 균형을 이루고 있기 때문일 뿐이다. prevalence: 0.1을 주면 같은 데이터에서 PPV 48.3 %, NPV 98.1 %가 나오고(G-04), 개수 기반 값은 여전히 89.4 %와 84.9 %이다. 둘 다 서로 다른 질문에 답하는 정당한 숫자이므로, 보고서는 어느 쪽을 담았는지와 어떤 유병률을 썼는지 밝혀야 한다.
100개 표본 하한. README는 “환자 100명과 대조군 100명”을 요구하지만 SDK 상수 MIN_VALIDATION_SAMPLES는 전체 100개이다. 환자 60명, 대조군 40명인 연구는 SDK는 통과하고 README 기준에는 미달한다. 또한 validateBiomarker는 내부에서 항상 calculateROC를 호출하므로, 99개 표본을 넣으면 errors[]가 채워진 결과를 돌려주는 대신 B001을 던진다. errors 배열을 기대하는 호출자는 그것을 보지 못한다.
거짓양성률 동점에서의 AUC 과소 추정. calculateAUCTrapezoidal은 ROC 점을 FPR로만 정렬한다. 여러 점의 FPR이 같으면 임곗값 순서, 즉 TPR 내림차순으로 남게 되고, 사다리꼴이 각 수직 구간의 엉뚱한 모서리를 잇는다. S-100에서 SDK는 AUC 0.9294를 반환하지만 정확한 값은 0.9358이다. TPR을 두 번째 정렬 키(오름차순)로 추가하면 0.9358이 회복된다. 이 오차는 동점 값 때문만이 아니다. 동점을 없앤 뒤에도(대조군 값 다섯 개를 80.5–84.5로 옮김) SDK는 정확한 값 0.9348 대신 0.9284를 보고한다.
끝점 없는 사용자 지정 임곗값. 호출자가 thresholds를 주면 SDK는 그 값만 평가하고 (0, 0)과 (1, 1) 모서리를 추가하지 않는다. S-100에 thresholds: [70, 75, 80]을 주면 보고되는 AUC는 0.08이다. 0.94의 근삿값이 아니라 무의미한 숫자인데, 적분되는 면적이 세 점 사이의 가느다란 띠뿐이기 때문이다. README의 예제는 바로 이런 짧은 임곗값 목록을 넘긴다. 적합한 엔진이라면 모서리를 항상 추가하거나, 부분 격자에서는 AUC 보고를 거부해야 한다.
신뢰수준. confidenceLevel이 0.95가 아니면 무엇이든 조용히 z = 2.576을 쓴다. 그래서 S-100에서 90 % 구간을 요청하면 0.860–0.999가 반환되는데, 이는 99 % 구간이며 SDK 자신의 AUC에서 계산한 올바른 90 % 구간 0.885–0.974보다 넓다. 소스 주석은 이 방법을 “DeLong 근사”라고 부르지만 실제로는 Hanley–McNeil 공식이므로, 적합성 주장에서 이를 DeLong이라고 기술해서는 안 된다.
단위 점검 없음. validateBiomarker는 측정값의 절반은 ug/L, 나머지 절반은 ng/mL로 표시한 S-100을 경고 없이 받아들였다. 이 두 단위는 우연히 같은 양이지만, mg/L이었어도 SDK는 똑같이 동작했을 것이다. 따라서 단위 일관성(C-07)은 데이터가 SDK에 닿기 전에 레코드 검증기가 강제해야 한다.
문자열 질환 상태. SDK는 두 집단을 엄격한 비교(=== true, === false)로 나누면서도 라벨은 참/거짓 판정(truthiness)으로 만든다. 그래서 비어 있지 않은 문자열은 모두 질환으로 계산된다. 상태가 전부 문자열이면 두 집단이 모두 비어 호출이 B002로 끝나는데, 안전하긴 하지만 이유가 틀렸다. 입력에 불리언과 문자열이 섞여 있으면 결과가 조용히 틀린다. S-100의 대조군 열 명을 false 대신 "healthy"로 표시하면 이들이 환자로 바뀌어 혼동행렬이 42 / 35 / 5 / 18이 되고 민감도는 70 %로 떨어지는데, 호출은 여전히 빈 errors 배열과 함께 isValid: true를 반환한다.
v1.0 CLI는 옵션을 해석한 다음(calc-roc만은 데이터 파일이 있는지 확인하고 행 수를 센다) 고정된 시연용 출력을 찍는다. 한 줄짜리 CSV 파일을 주면 calc-roc는 “Total samples: 1”이라고 보고한 뒤 혼동행렬 75 / 85 / 15 / 25와 AUC 0.85를 출력한다. README의 빠른 시작은 --version 플래그를 쓰지만 CLI에는 그런 플래그가 없고, 명령은 wia-bio-004 version이다. CLI가 입력을 읽게 되기 전까지는 적합성 주장의 일부가 될 수 없으며, CLI가 출력한 어떤 숫자도 증거에 들어가서는 안 된다.
| 발견 | 벡터 | 기대 | v1.0 결과 | 판정 |
|---|---|---|---|---|
| F-01 빌드 | — | 컴파일됨 | TS2323 / TS2484 | 실패(작업 사본에서 수정) |
| F-02 혼동행렬 지표 | G-03 | 84 / 90 / 89.4 / 84.9 | 동일 | 통과 |
| F-03 정확한 AUC | G-07 | 0.9358 | 0.9294 | 실패 |
| F-04 사용자 격자 AUC | G-07, [70, 75, 80] | 모서리 추가 또는 거부 | 0.08 | 실패 |
| F-05 90 % 신뢰구간 | N-08 | z = 1.645 | z = 2.576 | 실패 |
| F-06 오류 코드 | N-01~N-03 | B002 / B002 / B001 | 동일(예외로 던짐) | 통과 |
| F-07 단위 점검 | N-05 | 거부 | 조용히 수용 | 실패(엔진), 검증기 필요 |
| F-08 문자열 상태 | N-07 | 명시적 거부 | 전부 문자열: B002, 혼합: 수용 후 잘못 집계 | 실패 |
| F-09 CLI 출력 | 모두 | 입력에서 도출 | 고정 숫자 | 실패 |
이 발견들을 담는 시험 모음은 짧게 쓸 수 있다. 아래 예시는 패키지에 이미 설정되어 있는 Jest를 쓴다. 보조 함수 codeOf는 호출이 던진 오류 코드를 돌려주므로, 단정은 메시지 문구가 아니라 코드를 대상으로 한다.
import { BiomarkerDataSDK, BiomarkerErrorCode } from '@wia/bio-004';
const range = (a: number, b: number) =>
Array.from({ length: b - a + 1 }, (_, i) => a + i);
// 합성 집합 S-100 (8.2.2절)
const cases = [...range(76, 117), ...range(60, 67)];
const controls = [...range(30, 74), ...range(80, 84)];
const values = [...cases, ...controls];
const labels = [...cases.map(() => 1), ...controls.map(() => 0)];
const codeOf = (fn: () => unknown): string | undefined => {
try { fn(); return undefined; } catch (e: any) { return e.code; }
};
const sdk = new BiomarkerDataSDK();
const measurements = values.map((v, i) => ({
value: v, unit: 'ng/mL', diseaseStatus: labels[i] === 1,
}));
describe('golden vector G-03 / G-07', () => {
const res = sdk.validateBiomarker({
biomarkerType: 'protein',
measurements,
referenceRange: { min: 0, max: 75, unit: 'ng/mL' },
cutoffThreshold: 75,
prevalence: 0.5,
});
test('confusion matrix', () => {
expect(res.performance.confusionMatrix).toEqual({ tp: 42, tn: 45, fp: 5, fn: 8 });
});
test('percent scale', () => {
expect(res.performance.sensitivity).toBeCloseTo(84.0, 1);
expect(res.performance.npv).toBeCloseTo(84.9, 1);
});
test('operating point uses fractions', () => {
expect(res.rocAnalysis.optimalThreshold.sensitivity).toBeCloseTo(0.84, 4);
expect(res.rocAnalysis.optimalThreshold.youdenIndex).toBeCloseTo(0.74, 4);
});
test('exact AUC (v1.0에서는 0.9294로 실패)', () => {
expect(res.rocAnalysis.auc).toBeCloseTo(0.9358, 3);
});
});
describe('negative vectors', () => {
test('N-01 length mismatch', () => {
expect(codeOf(() => sdk.calculateROC({ trueLabels: labels, predictions: values.slice(1) })))
.toBe(BiomarkerErrorCode.INVALID_MEASUREMENTS);
});
test('N-03 fewer than 100 samples', () => {
expect(codeOf(() => sdk.calculateROC({
trueLabels: labels.slice(0, 99), predictions: values.slice(0, 99),
}))).toBe(BiomarkerErrorCode.INSUFFICIENT_SAMPLES);
});
});
F-03 시험은 올바른 값을 적어 두고 v1.0에서 실패하도록 작성했다는 점에 유의해야 한다. “빌드를 초록색으로 유지하려고” 현재의 틀린 값을 시험에 박아 넣으면 결함이 명세로 둔갑한다.
단계별 부속서는 적합성 주장을 어떤 형태로 전달해야 하는지를 비규범적으로 스케치한다. 증거 패키지와 프로파일 매니페스트가 그것이다. 이 발상은 부속서의 상투적인 세부 내용과 무관하게 타당하므로, 여기서는 일반적인 형태로 설명한다.
증거 패키지는 평가자가 공급사에 연락하지 않고도 주장을 다시 실행해 볼 수 있게 하는 하나의 아카이브이다. 정확한 의존성 버전을 알 수 있도록 SPDX나 CycloneDX 같은 공인된 형식의 소프트웨어 자재명세서(SBOM), 네트워크 인터페이스가 있다면 그 API 기술서, 사용한 시험 벡터와 기대값, 이를 실행해 얻은 통과/실패 행렬(건너뛴 시험과 각각을 건너뛴 이유 포함), 그리고 모든 파일과 해시를 나열하고 공급사가 서명한 매니페스트가 들어간다. 부정적 결과도 긍정적 결과와 같은 비중으로 패키지에 들어가야 한다. 이유가 기록되지 않은 채 건너뛴 시험은 실패로 읽어야 한다.
프로파일 매니페스트는 어떤 요구사항을 주장하는지 밝힌다. 점검표의 각 행에 supported, partial(이유 포함), excluded(정당화 포함) 가운데 하나의 상태를 붙인다. 이렇게 하면 적합성 주장이 오해를 부르는 가장 흔한 방식, 즉 쉬운 부분만 시험해 놓고 전부를 다룬 것처럼 암시하는 일을 막을 수 있다. 아래 예시는 §8.1.3의 점검표 식별자와 §8.3의 발견을 사용한다. 필드 이름은 이 책의 제안이며 v1.0이 정의한 스키마가 아니다.
{
"standard": "WIA-BIO-004",
"standardVersion": "1.0.0",
"implementation": { "name": "example-biomarker-engine", "version": "2.3.1" },
"requirements": [
{ "id": "C-01", "status": "supported" },
{ "id": "C-03", "status": "partial",
"reason": "AUC uses a corrected trapezoid sort; CI method is Hanley-McNeil only" },
{ "id": "C-07", "status": "supported",
"reason": "Enforced by record validator ahead of statistics engine" },
{ "id": "C-12", "status": "excluded",
"justification": "Study-level requirement; outside software scope" }
],
"evidence": {
"sbom": "sbom.spdx.json",
"vectors": "vectors/",
"results": "results/matrix.json"
},
"signature": "<detached signature over the manifest hash>"
}
이 예시의 모든 내용은 설명을 위한 것이다. 구현 이름, 버전, 상태는 F-03을 고친 가상의 엔진을 나타낼 뿐 실제 제품과는 관계가 없다.
모든 WIA-BIO-004 레코드는 "version": "1.0.0"을 담고 있으며, 단계별 부속서는 표준이 Semantic Versioning 2.0.0을 따른다고 밝힌다. 규범적 요구사항을 하위 호환되지 않게 바꾸면 메이저, 기존 구현을 깨지 않는 새 요구사항을 더하면 마이너, 편집상의 변경이면 패치 번호를 올린다. 이 규칙을 바이오마커 레코드에 적용하면 다음 표와 같은 구체적 결과가 나온다. 예시는 제3장에서 다룬 불일치에서 가져온 가상의 향후 변경이며, 발표된 변경이 아니다.
| 변경 | 예시 | 올리는 번호 |
|---|---|---|
| 새 선택 필드 추가 | rocAnalysis에 aucCIMethod 추가 | 마이너 |
| 열린 어휘에 새 값 추가 | 건조 혈반 같은 sampleType 추가 | 마이너 |
| 필드 이름·타입 변경 | min/max와 lowerLimit/upperLimit 통일 | 메이저 |
| 열거값 삭제 | diagnostic_validation을 폐지하고 SDK의 studyType 집합으로 통일 | 메이저 |
| 결과를 바꾸는 기본값 변경 | PPV 기본 계산을 유병률 기반에서 개수 기반으로 변경 | 메이저 |
| 예제·오탈자 정정 | §5.3.1의 PPV/NPV 예제 값 수정 | 패치 |
여기에는 세 가지 버전 번호가 관여하며, 이를 혼동해서는 안 된다. 레코드가 따르는 표준의 버전(레코드의 version 필드), 결과를 만든 SDK의 버전(getVersion(), BiomarkerReport.metadata.version에도 복사됨), 그리고 주장을 하는 구현의 버전이다. v1.0에서는 셋 모두 “1.0.0”이어서 구분이 드러나지 않는다. 소비자는 SDK 버전이 아니라 레코드의 version을 기준으로 해석 방식을 정해야 하고, 매니페스트는 세 가지를 모두 적어야 한다.
§10.2는 규제 및 검사실 표준 문서 다섯 가지를 나열한다. WIA-BIO-004에 적합하다고 해서 그중 어느 것을 준수하는 것도 아니며, 그중 어느 문서도 WIA-BIO-004를 인증하지 않는다. 표준이 할 수 있는 일은 그 문서들이 요구하는 증거를 모으고 감사하기 쉽게 만드는 레코드를 생산하는 것이다. 아래 표는 §10.2의 두 가지 세부 사항을 바로잡았다. EMA 지침은 ICH M10으로 대체되었고, C28-A3은 2008년에 발행되었으며 2010년은 정정 재발행본인 C28-A3c의 연도이다.
| 문서 | 범위 | 연결되는 WIA-BIO-004 증거 | 주의점 |
|---|---|---|---|
| FDA, Bioanalytical Method Validation(2018) | 시험 검체 중 약물·대사체를 측정하는 방법의 검증 | 정밀도·정확도·LOD/LOQ·안정성 레코드(제5장) | 약물 분석을 위한 문서이므로, 바이오마커 분석에는 사용 맥락에 맞춘(fit-for-purpose) 접근이 필요함 |
| EMA, Guideline on bioanalytical method validation(2011) | 위와 같음(유럽연합) | 위와 같음 | ICH M10(2022)으로 대체됨. M10의 범위는 약물·대사체 농도이며 바이오마커가 아님 |
| CLSI EP09c(2018) | 환자 검체를 이용한 측정 절차 비교와 편향 추정 | 방법 비교 데이터, measurementMethod와 편향 결과 | 요약 통계만이 아니라 짝지은 원시 결과가 필요함 |
| CLSI EP15-A3(2014) | 사용자의 정밀도 확인과 편향 추정 | 반복 측정 데이터와 CV 레코드 | 사용자 검사실에서의 확인(verification)이지 완전한 검증이 아님 |
| CLSI C28-A3(2008, C28-A3c 2010) | 참고구간의 정의·설정·확인 | §3.3.2 참고구간 레코드와 분할 | 참조 개인 수와 방법(모수·비모수)을 기록해야 함 |
§10.2에는 없지만 평가자가 흔히 묻는 틀이 두 가지 더 있다. 진단 정확도 연구 자체에 대해서는 STARD 보고 지침이 논문이 공개해야 할 사항을 나열한다. 참여자 흐름, 참조 표준, 눈가림, 판정 불가 결과의 처리 등이다. 대부분은 §7.2 필드에 대응하지만, 판정 불가 결과처럼 v1.0에 필드가 없는 항목도 있다. 검사를 수행하는 검사실에 대해서는 ISO 15189가 품질과 역량에 관한 요구사항을 정하며, 여기에는 내부 정도관리와 외부 정도관리가 포함된다. 이는 §8.1의 “품질관리” 구성 요소와 제5장의 Westgard 규칙이 구현하는 과정이다. 적용되는 체외진단 규제, 예컨대 EU IVDR이나 미국 CLIA 체계는 관할권과 사용 목적에 따라 달라지며, 어떤 데이터 표준도 결정할 수 없는 영역이다.
이 책을 쓰는 동안 여러 내부 불일치가 드러났다. 고치기 어려운 것은 없지만, 고쳐지기 전까지는 하나하나가 적합성 시험을 어렵게 만든다.
studyType에는 서로 호환되지 않는 값 집합이 세 가지 있고, 참고구간은 두 가지 필드 이름 규칙을 쓴다. 제3장은 SDK 열거형을 권장하는데, 표준이 이를 명시해야 한다.수리를 넘어, 몇 가지 흐름이 “분석물 하나, 값 하나, 절단값 하나”라는 표준의 모델을 시험대에 올릴 것이다.
멀티오믹스 복합 점수. 최근의 많은 바이오마커는 단백질·전사체·대사체의 가중 결합이다. 제3장의 다중 분석물 정의에는 이미 analytes[]와 algorithm 블록이 있지만, 적합성 시험에는 더 많은 것이 필요하다. 가중치, 점수를 계산한 소프트웨어 버전, 그리고 검증된 점수가 곧 배포된 점수임을 보장하는 고정(lock)된 모형이다. 과적합에 관한 제6장의 경고는 여기서 더욱 강하게 적용된다.
액체생검과 순환 종양 DNA. 무세포 DNA에서 얻는 변이 대립유전자 분율 같은 결과는 유전 바이오마커와 정량 바이오마커의 중간에 있다. 채혈관 종류와 처리 지연이 결과에 영향을 주므로 제3장의 VCF 수준 출처 정보와 제5장의 검체 전 단계 통제가 함께 필요하며, 검출한계도 면역분석과는 다른 방식으로 표현된다.
디지털 바이오마커. 웨어러블, 스마트폰, 음성 녹음에서 도출한 지표는 알고리즘이 처리하는 연속 데이터 흐름이다. 표준의 measurementMethod와 laboratoryID 필드로는 센서, 펌웨어 버전, 특징 추출 파이프라인을 기술할 수 없고, LOD 같은 분석적 검증 개념도 부분적으로만 옮겨 온다.
기계학습 기반 바이오마커. “값”이 모형의 출력이라면 판별력(AUC)만으로는 부족하다. 예측 위험 30 %가 실제 사건 발생률 30 %에 대응하는지를 뜻하는 보정(calibration)을 훈련 집단 정보, 성능 변화(drift) 감시 계획과 함께 보고해야 한다. 임상 예측 모형의 보고 지침인 TRIPOD와 그 AI 확장판이 자연스러운 짝이 된다.
FHIR Genomics. 제3장은 정량 결과를 FHIR Observation에 대응시켰다. 유전 바이오마커에 대해서는 HL7 FHIR Genomics Reporting 구현 가이드가 변이와 그 해석을 위한 더 풍부한 프로파일을 정의한다. WIA-BIO-004의 유전 레코드를 이 프로파일에 맞추면 통합 담당자는 두 번째 대응 작업을 하지 않아도 된다.
이 방향들은 모두 이 장이 처음에 던진 질문으로 돌아온다. 다른 사람이 그것이 제대로 되었는지 어떻게 확인할 것인가? 공유된 벡터, 공개된 방법, 정직한 매니페스트로 이 질문에 답하는 표준은 v1.0의 구체적인 숫자들이 개정된 뒤에도 오래 쓸모를 유지할 것이다.
WIA-BIO-004 v1.0에 대한 적합성은 네 가지 출처, 즉 다섯 가지 필수 구성 요소, 데이터 레코드, 오류 코드, 일곱 가지 검증 요구사항에서 조립된다. 앞의 세 가지는 기계적으로 시험할 수 있으며, 표준의 핵심이 산술이라는 점 덕분에 정확한 시험 벡터를 만들 수 있다. 그런 벡터를 참조 SDK에 돌려 보면 혼동행렬 산술과 오류 코드는 옳다는 것이 확인되고, ROC 적분, 부분 임곗값 격자, 신뢰수준 처리, 단위와 질환 상태 점검의 결함, 그리고 빌드 오류와 아직 데이터를 읽지 않는 CLI가 드러난다. 서명된 프로파일 매니페스트를 갖춘 증거 패키지는 이런 결과를 다른 사람이 검증할 수 있는 주장으로 바꾼다. 시맨틱 버전 관리, 나열된 규제·검사실 지침과의 정합, 그리고 짧은 정리 과제 목록이 다음 개정판으로 가는 길을 완성한다. 복합 점수, 순환 DNA, 디지털 지표, 학습된 모형 같은 미래의 바이오마커는 표준의 데이터 모델을 넓히라고 요구하겠지만, 확인 가능한 주장이라는 원칙은 그대로 이어진다.
B003에 대한 부정 벡터를 §7.2 레코드로 작성하고, 그것을 v1.0 SDK에 직접 시험할 수 없는 이유를 설명하라.aucCIMethod 추가, referenceRange.min 이름 변경, §5.3.1 예제 정정.