제3장
바이오마커 결과를 분석기에서 검증 연구까지 실어 나르는 레코드들 — 측정 봉투, 참고구간, 다중 분석물 정의, 연구·보고 레코드, 이들이 가리키는 오믹스 파일 형식, 그리고 결과가 거쳐 가야 하는 임상 교환 형식을 다룬다.
제2장은 바이오마커 결과를 절단값보다 크거나 작은 하나의 숫자로 다루었다. 그러나 실제로 그 숫자는 혼자 이동하지 않는다. 단위, 참고구간, 검체 종류, 채취 시각, 분석법, 검사실, 그리고 검체 상태에 관한 경고가 함께 따라온다. 기기에서 분석 단계까지 가는 동안 이 동반 정보 가운데 하나라도 사라지거나 조용히 바뀌면, 나중에 계산한 민감도와 특이도는 실제로 수행한 실험과 다른 실험을 설명하게 된다. WIA-BIO-004 데이터 형식의 목적은 숫자와 그 맥락을 한 몸으로 유지하는 데 있다.
이 장은 표준이 정의하는 레코드를 하나씩 살펴보고, 각 레코드를 @wia/bio-004 TypeScript SDK의 대응 타입과 비교한 뒤, 같은 결과를 병원과 임상시험 의뢰자가 이미 쓰고 있는 형식 — LOINC와 UCUM, HL7 FHIR, CDISC SDTM, GA4GH Phenopackets — 으로 표현하는 방법을 보인다. 본 명세, 단계별 부속서(phase annex), SDK가 서로 어긋나는 곳은 어긋남을 명시하고 실무 관례를 권고한다. 그 권고는 이 책의 편집상 선택이며 규범 문구가 아니다.
명세 §7의 모든 교환 레코드는 두 개의 봉투 필드, 즉 "standard": "WIA-BIO-004"와 "version": "1.0.0"으로 시작한다. 사소해 보이지만 수신 시스템이 가장 먼저 확인해야 하는 값이다. 표준 식별자는 여러 WIA 형식이 하나의 메시지 큐를 공유할 때 어느 파서를 쓸지 라우터에 알려 주고, 시맨틱 버전은 파서가 어떤 필드 집합을 기대해야 하는지 알려 준다. 알 수 없는 주(major) 버전을 만난 수신자는 추측하지 말고 레코드를 거부해야 한다. 알 수 없는 부(minor) 버전은 모르는 필드를 보존하고 무시한다는 조건으로 받아들일 수 있다(버전 규칙은 제8장에서 다룬다).
봉투 아래의 측정 레코드(§7.1)는 수명이 의도적으로 다른 세 블록으로 이루어진다. measurement는 하나의 분석 결과를 기술하며 일단 보고되면 바뀌지 않는다. patient는 채취 시점의 사람을 기술하며 가명 처리되어 있다. clinicalContext는 검사를 의뢰한 이유와 그 사람의 질병 상태에 관해 알려진 내용을 기술한다. 예컨대 진단이 확정되었을 때처럼 나중에 갱신될 가능성이 가장 큰 블록이다.
명세가 직접 제시하는 예는 고감도 C-반응성 단백(CRP) 결과이다. 이 장의 이후 모든 매핑이 이 레코드에서 출발하므로 그대로 옮긴다.
{
"standard": "WIA-BIO-004",
"version": "1.0.0",
"measurement": {
"biomarkerID": "BIO-12345",
"biomarkerName": "C-Reactive Protein",
"biomarkerType": "protein",
"value": 12.5,
"unit": "mg/L",
"referenceRange": { "min": 0, "max": 10, "unit": "mg/L" },
"measurementMethod": "High-sensitivity immunoturbidimetric assay",
"laboratoryID": "LAB-789",
"dateCollected": "2025-01-15T08:30:00Z",
"dateMeasured": "2025-01-15T10:45:00Z",
"sampleType": "blood_serum",
"qualityFlags": { "hemolyzed": false, "lipemic": false, "icteric": false }
},
"patient": {
"patientID": "PATIENT-456",
"age": 55,
"sex": "male",
"fastingStatus": true,
"medications": ["aspirin", "metformin"]
},
"clinicalContext": {
"indication": "Cardiovascular risk assessment",
"diseaseStatus": null,
"priorHistory": "Type 2 diabetes"
}
}
이 짧은 레코드에서 몇 가지 설계 결정이 드러난다. 값은 문자열이 아니라 JSON 숫자이므로 소비자가 "12,5"나 ">10" 같은 문자열을 해석할 일이 없다. 단위는 값 바로 옆에 있고 referenceRange 안에서 한 번 더 반복된다. 이 중복은 의도된 것이다. 다른 검사실에서 가져온 참고구간은 단위가 다를 수 있으므로, 레코드는 그 차이를 숨기지 말고 드러내야 한다. 바이오마커는 두 번 식별된다. biomarkerID는 소프트웨어가 비교하는 레지스트리 키이고, biomarkerName은 사람을 위한 표시 이름이므로 결합(join) 키로 써서는 안 된다. 끝으로 biomarkerType은 제1장에서 소개한 모달리티 축(단백질, 유전, 대사, 영상, 세포)이다. 진단 바이오마커인지 예후 바이오마커인지 같은 임상적 역할은 결과를 사용하는 연구의 속성이며, 개별 측정값에 속하지 않는다.
두 타임스탬프는 모두 Z 지정자를 붙인 UTC 기준 ISO 8601 날짜·시각이다. 오프셋 없이 현지 시각을 저장하는 것은 "처리까지 걸린 시간"을 사후에 재구성하지 못하게 되는 가장 흔한 원인이며, 처리 지연은 제5장에서 통제하도록 요구하는 검체 전 단계(전분석) 변수 가운데 하나이다. dateCollected는 검체가 환자에게서 채취된 시각이고, dateMeasured는 분석기가 값을 산출한 시각이다. 예시에서 둘의 간격은 2시간 15분이며, 품질 규칙은 이를 검사실이 채택한 처리 한도와 비교할 수 있다.
patientID는 가명처리된 연구 또는 검사실 식별자이며, 의무기록번호나 국가 식별자(주민등록번호 등)여서는 안 된다. 이 식별자를 생성하고 보호하는 방법은 제7장의 주제이다. 나이는 만 나이 정수로 기록한다. 참고구간 분할에는 이것으로 충분하며 생년월일보다 식별 위험이 낮다.
알 수 없음은 음성과 같지 않다. §7.1에서 clinicalContext.diseaseStatus는 null인데, 이는 검사 시점에 이 사람이 관심 질환을 가졌는지 아무도 몰랐다는 뜻이다. 변환기는 이 null을 결코 false로 바꿔서는 안 된다. 그렇게 하면 분류되지 않은 검체가 이후 검증 연구의 대조군으로 들어가 특이도를 왜곡한다. SDK의 DiseaseStatus 타입에는 같은 목적을 위한 'unknown' 값이 명시되어 있으며, 이 책은 JSON null과 'unknown'을 서로 대응시킨다.
자유 텍스트는 통합 분석의 적이다. 네 기관에서 "serum", "Serum", "SST", "blood (serum)"을 받은 연구는 누군가 매핑 표를 만들기 전까지 검체 종류가 네 가지인 셈이다. 그래서 WIA-BIO-004는 소수의 열거형을 고정한다. 아래 값은 명세 §3.1.1과 SDK의 types.ts에 정의된 것이며, 검체 종류는 두 출처가 동일하다.
| 필드 | 허용 값 | 비고 |
|---|---|---|
sampleType | blood_serum, blood_plasma, whole_blood, urine, tissue_biopsy, cerebrospinal_fluid, saliva, stool | 혈청과 혈장은 많은 분석물에서 값이 다르므로 별도 값이다. 혈장의 항응고제는 부호화되지 않으므로 채취 세부 정보에 기록한다. |
MeasurementMethod (SDK) | ELISA, mass_spectrometry, PCR, sequencing, immunoassay, clinical_chemistry, flow_cytometry, imaging, other | 개별 분석법이 아니라 방법 계열이다. 구체적인 키트나 플랫폼은 자유 텍스트로 남긴다. |
DiseaseStatus (SDK) | diseased, healthy, at_risk, unknown | §7.1은 알 수 없음을 null로 쓴다. unknown으로 취급한다. |
BiomarkerType | protein, genetic, metabolic, imaging, cellular | 모달리티만 나타낸다. 진단·예후 같은 역할이 아니다. |
한 가지 불일치는 주의할 만하다. §7.1에서는 필드 이름이 measurementMethod이고 서술형 문자열("High-sensitivity immunoturbidimetric assay")을 담는 반면, SDK의 BiomarkerMeasurement는 이를 method라 부르고 9개 값의 열거형으로 제한한다. 두 정보는 모두 쓸모가 있다. 열거형은 기관 간 묶음을 가능하게 하고, 서술은 예컨대 고감도 CRP 분석법과 일반 CRP 분석법을 구별해 준다. 실무 관례로는 교환 레코드에 서술 문자열을 유지하고, 가져오기 단계에서 방법 계열(여기서는 immunoassay)을 도출하며, 원문은 metadata에 보관하여 정보가 사라지지 않게 한다.
qualityFlags 객체는 분석의 정확성이 아니라 검체의 상태를 기록한다. §7.1은 고전적인 혈청 지수 간섭 세 가지를 불리언으로 정의하고, SDK의 QualityFlags 인터페이스는 네 항목을 더한다.
| 플래그 | 정의 위치 | 의미 | 대표적 영향 |
|---|---|---|---|
hemolyzed | §7.1, SDK | 적혈구 용해로 헤모글로빈과 세포 내 성분이 유출됨 | 칼륨·LDH 등의 거짓 상승, 광도 측정 분석법의 분광 간섭 |
lipemic | §7.1, SDK | 지단백 과다로 인한 혼탁 | 비탁·광도 측정법의 광산란, 부피 치환 효과 |
icteric | §7.1, SDK | 빌리루빈 고농도 | 일부 분석법의 분광·화학적 간섭 |
clotted | SDK에만 있음 | 항응고되어야 할 검체에 응고가 생김 | 혈소판·응고인자 소모, 흡인 오류 |
diluted | SDK에만 있음 | 분석 전에 희석함 | 결과에 희석 배수를 다시 곱해야 하며, 측정 범위를 넘은 값임을 알려 줌 |
outOfRange | SDK에만 있음 | 분석법의 보고 가능 범위 밖의 값 | 정확한 값이 아니라 중도절단된 값 |
other | SDK에만 있음 | 자유 텍스트 문제 목록(배열) | 포함 전에 사람이 검토 |
false로 설정된 플래그는 "확인했고 해당 없음"을, 플래그가 없는 것은 "확인하지 않음"을 뜻한다. 검사실의 혈청 지수 측정이 꺼져 있을 때 이 차이가 중요해진다. 없는 플래그를 false로 취급하는 분석은 아무도 확인하지 않은 검체를 조용히 포함하게 된다. 플래그가 붙은 검체를 제외할지 주석만 달지는 연구 차원의 결정이며, 3.8절에서 이를 명시적인 규칙으로 만든다.
참고구간은 그 자체로 모집단을 가진 데이터셋이다(설정 방법은 제5장에서 설명한다). §3.3.2는 연령군과 성별로 분할한 구간을 나열하는 독립 레코드를 정의한다. 명세의 예는 연령별 PSA 구간이다.
{
"biomarker": "PSA",
"unit": "ng/mL",
"referenceRanges": [
{ "ageGroup": "40-49", "sex": "male", "lowerLimit": 0.0, "upperLimit": 2.5, "median": 0.7 },
{ "ageGroup": "50-59", "sex": "male", "lowerLimit": 0.0, "upperLimit": 3.5, "median": 1.0 }
]
}
SDK는 분할 목록을 모델링하지 않는다. SDK의 ReferenceRange 인터페이스는 구간 하나를 기술하며, 필드 이름이 다르고 속성이 하나 더 있다.
| §3.3.2 레코드 | SDK ReferenceRange | §7.1 인라인 구간 | 설명 |
|---|---|---|---|
lowerLimit | min | min | 의미가 같으므로 가져올 때 이름만 바꾼다 |
upperLimit | max | max | 의미가 같다 |
unit(레코드 수준) | unit(구간마다) | unit | 레코드 수준 단위를 모든 구간에 복사한다 |
median | median? | — | SDK에서는 선택 항목 |
ageGroup | ageGroup? | — | "40-49" 같은 문자열이므로 경계를 명시적으로 해석한다 |
sex | sex?: male | female | all | — | 분할하지 않은 구간은 all |
| — | population? | — | 참고 모집단 기술, 기재를 권장 |
여기서 두 가지 실무 원칙이 나온다. 첫째, 측정 레코드를 만들 때 가져오기 모듈은 환자의 나이와 성별에 맞는 분할 구간을 골라 측정값의 인라인 referenceRange에 복사해야 한다. 그래야 나중에 참고구간 표가 개정되더라도 측정값을 해석할 수 있다. 둘째, ageGroup 문자열은 경계에서 모호하다("40-49"는 아마 49세 364일까지 포함하겠지만 형식이 그렇게 말하지는 않는다). 이 책은 이를 양 끝을 포함하는 만 나이 정수로 해석하고, 적재 시점에 서로 겹치는 분할을 거부할 것을 권한다. 성별 어휘도 다르다. PatientSample.sex는 other를 허용하고 ReferenceRange.sex는 all을 허용한다. other로 기록된 환자는 all 구간이 있으면 그것에 대응시키고, 없으면 성별 구간에 억지로 넣지 말고 검토 대상으로 표시한다.
단일 분석물은 biomarkerID, 이름, 모달리티로 기술할 수 있다. 그러나 오늘날 많은 검사는 여러 분석물을 결합해 하나의 점수를 만든다. 데이터 형식 부속서(PHASE-1)는 이런 경우를 위해 적응증, 분석물, 점수 산출 알고리즘, 검증 근거, 검체, 규제 상태 블록을 갖춘 더 풍부한 바이오마커 정의를 둔다. 부속서는 특정 상용 유방암 검사를 예로 들지만, 그 예에 나온 가중치·공식·성능 수치는 검증할 수 없어 여기에 옮기지 않는다. 대신 의도적으로 가상으로 만든 3개 분석물 점수로 구조를 보인다.
{
"biomarkerId": "BM-EXAMPLE-CS3",
"name": "Example Composite Score 3 (fictional)",
"version": "1.0",
"type": "diagnostic",
"indication": {
"disease": "Colon cancer (illustrative use)",
"icd10": "C18",
"population": "Adults referred for colonoscopy"
},
"analytes": [
{ "id": "PROT-A", "type": "protein_concentration", "method": "immunoassay", "unit": "ng/mL", "weight": 0.6 },
{ "id": "PROT-B", "type": "protein_concentration", "method": "immunoassay", "unit": "U/mL", "weight": 0.3 },
{ "id": "MET-C", "type": "metabolite_level", "method": "mass_spectrometry", "unit": "umol/L", "weight": 0.1 }
],
"algorithm": {
"type": "weighted_sum",
"formula": "score = 0.6*z(PROT-A) + 0.3*z(PROT-B) + 0.1*z(MET-C), rescaled to 0-100",
"scoreRange": [0, 100],
"interpretation": {
"low": { "range": [0, 39] },
"intermediate": { "range": [40, 69] },
"high": { "range": [70, 100] }
}
},
"validation": {
"analyticalValidation": { "reference": "see validation-study records" },
"clinicalValidation": { "studyIDs": ["VALID-EXAMPLE-001"] }
},
"specimen": { "type": "blood_plasma", "storage": "-80 C until analysis" },
"regulatoryStatus": { "intendedUse": "research use only" }
}
이 레코드의 모든 숫자는 읽기 쉽도록 고른 예시 값이다. 실제 정의에 옮겨 쓸 만한 구조적 특징은 세 가지이다.
부속서의 type 필드는 임상적 역할(부속서에서는 prognostic, 위 예에서는 diagnostic)을 담는 반면, §7.1의 biomarkerType은 모달리티를 담는다는 점도 유의해야 한다. 이름이 헷갈릴 만큼 비슷하다. 이 책은 PHASE-1의 type을 엄격히 역할로 읽고, 단일 모달리티 패널이라면 모달리티를 별도로 함께 기록할 것을 권한다. 위 예처럼 혼합 패널에는 단일 모달리티가 없다. 해석 구간에는 검증 근거가 뒷받침하는 절단값만 넣어야 한다. 예시에서 "위험도" 백분율을 일부러 뺀 것은, 그런 값은 보정(calibration)된 결과 연구에서만 나올 수 있기 때문이다.
§7.2는 완료된 검증 연구를 교환하는 방법을 정의한다. 최상위 구조는 다음과 같다.
{
"standard": "WIA-BIO-004",
"version": "1.0.0",
"studyID": "...",
"studyType": "...",
"biomarker": { "id": "...", "name": "...", "type": "protein" },
"population": { "totalParticipants": 0, "cases": 0, "controls": 0,
"ageRange": [0, 0], "sexDistribution": { }, "ethnicity": { } },
"referenceStandard": { "method": "...", "sensitivity": 100, "specificity": 100 },
"results": {
"rocAnalysis": { "auc": 0, "aucCI95": [0, 0], "optimalThreshold": { } },
"performanceMetrics": { "sensitivity": 0, "specificity": 0, "ppv": 0, "npv": 0, ... },
"subgroupAnalysis": [ { "subgroup": "...", "n": 0, "auc": 0 } ]
},
"validation": {
"crossValidation": "10-fold",
"externalValidation": true,
"validationCohorts": [ { "cohort": "...", "n": 0, "auc": 0 } ]
}
}
이 레코드는 검토자가 진단 정확도 연구에 던지는 질문에 순서대로 답한다. 누구를 연구했는가, 어떤 참조 표준에 대해 비교했는가, 어느 절단값에서 어떤 판별력을 보였는가, 하위군에서도 결과가 유지되는가, 개발 데이터 밖에서 재현되었는가. 제6장에서 이 골격을 완전한 예로 채우고 산술을 검산한다. 여기서의 관심은 형태, 그리고 거기서 나오는 세 가지 관례이다.
DiagnosticPerformance도 백분율을 쓰지만 ROC 점(ROCPoint.sensitivity)은 0~1 사이의 분수이다. 따라서 필드 이름만으로는 척도를 알 수 없고 레코드 종류가 척도를 결정한다. 변환기는 경계에서 정규화하고 한 객체 안에서 두 척도를 섞어서는 안 된다.optimalThreshold 안에 unit을 둔다. 단위 없는 절단값은 재사용할 수 없다.§5.3.1은 바이오마커의 대표 성능을 공표하기 위한 더 짧은 보고 형식을 정의한다. 명세의 예는 유병률 0.50, 민감도 82.0 %, 특이도 88.0 %를 제시하면서 PPV 86.0, NPV 84.5를 적고 있는데, 이 값은 주어진 입력에서 나오지 않는다. 유병률 50 %에서 PPV = 0.82 / (0.82 + 0.12) = 87.2 %, NPV = 0.88 / (0.88 + 0.18) = 83.0 %이다. 정정한 예는 다음과 같다.
{
"biomarker": "Example Biomarker",
"studyPopulation": { "cases": 150, "controls": 150, "prevalence": 0.50 },
"diagnosticPerformance": {
"auc": 0.85,
"aucCI95": [0.80, 0.90],
"optimalThreshold": 125.5,
"sensitivity": 82.0,
"sensitivityCI95": [75.0, 88.0],
"specificity": 88.0,
"specificityCI95": [82.0, 93.0],
"ppv": 87.2,
"npv": 83.0,
"lrPositive": 6.83,
"lrNegative": 0.20,
"accuracy": 85.0
}
}
PPV와 NPV 외의 값은 명세 그대로 두었으며 예시 값이다. 여기서 얻는 교훈은 일반적이다. 입력(민감도, 특이도, 유병률)과 파생량(PPV, NPV, 우도비, 정확도)을 함께 저장하는 레코드는 자동으로 검산할 수 있으며, 검증기는 반올림 한 단계의 허용오차로 이를 수행해야 한다. 또한 여기서 optimalThreshold는 단위 없는 숫자이지만 §7.2는 이를 단위를 포함한 객체로 감싼다. 예시에는 없더라도 보고 레코드에 단위를 덧붙여야 한다.
SDK는 관련된 두 타입을 제공하지만 어느 쪽도 §7.2를 일대일로 옮긴 것은 아니다.
ValidationStudy는 연구의 원자료를 담는다. population은 PatientSample 객체의 배열이고, measurements는 BiomarkerMeasurement의 배열이며, results는 validateBiomarker가 반환하는 온전한 ValidationResult이다. 교차검증은 문자열 "10-fold"가 아니라 method, folds, averageAUC, aucStdDev를 가진 객체이다. 외부 코호트는 코호트별 AUC·민감도·특이도를 가진 externalValidation.cohorts[]로 나타난다.BiomarkerReport는 요약을 담는다. 그 population은 집계 객체(totalParticipants, cases, controls, 선택적 인구학 정보)로, §7.2의 population과 같은 형태이다.
따라서 §7.2 문서는 직렬화한 BiomarkerReport에 ValidationStudy의 연구 설계 필드를 더하고 환자 수준 배열을 뺀 것으로 이해하는 것이 가장 정확하다. 교환 용도로는 합리적인 구성이다. 환자 수준 데이터는 별도의 접근 통제 아래에서 이동하고(제7장), 요약은 널리 공유할 수 있기 때문이다.
studyType 어휘| 출처 | 나타나는 값 | 이 책의 SDK 열거형 대응 |
|---|---|---|
| 본 명세 §7.2 | diagnostic_validation | validation |
| API 부속서(PHASE-2) | clinical_validation | validation |
SDK ValidationStudy | discovery, validation, clinical_utility, multi_center | 그대로 사용 |
이 책은 기계적으로 검사되는 유일한 어휘인 SDK 열거형을 권한다. 다만 약점이 하나 있다. multi_center는 제1장에서 본 생애주기의 단계가 아니라 연구 설계를 나타내므로, 다기관 임상적 검증 연구는 두 가지 옳은 이름 가운데 하나를 골라야 한다. 어휘가 통일될 때까지(제8장에서 미해결 과제로 다룬다) studyType에는 단계를 기록하고 참여 기관 수는 모집단 기술에 기록한다.
biomarkerID는 WIA 배포 환경 안의 레지스트리 키일 뿐, 병원 정보 시스템에는 아무 의미가 없다. 교환을 위해서는 측정 가능한 각 바이오마커가 LOINC 코드도 함께 가져야 한다. LOINC 코드는 관찰 항목, 즉 무엇을 어떤 검체에서 어떤 척도로, 때로는 어떤 방법으로 측정했는지를 식별한다. 각 단위는 UCUM 표현이어야 한다. LOINC 코드는 구체적이다. 혈청·혈장의 질량 농도로 측정한 CRP와 고감도 CRP는 코드가 다르며, 잘못 고르면 합쳐서는 안 될 결과가 합쳐진다. 이 책이 확인하고 예제에 쓰는 코드는 PSA 2857-1, 심장 트로포닌 I 10839-9, HbA1c 4548-4, 고감도 CRP 30522-7이다. 그 밖의 코드는 부속서에 나열된 것까지 포함해 예제에서 복사하지 말고 최신 LOINC 배포본에서 확인해야 한다.
UCUM이 중요한 이유는 사람 눈에 같아 보이는 단위 문자열이 소프트웨어에는 다르기 때문이다. UCUM은 대소문자를 구분하고 고유한 표기법을 가진다. 그리스 문자 뮤로 쓴 "μg/mL"은 ug/mL로 표현해야 하고, "IU/L"은 UCUM에서 [IU]/L이다. WIA 가져오기 모듈은 UCUM 코드를 정확히 저장해야 하며, 같은 LOINC 코드의 결과가 서로 변환 가능한 단위로 들어오면 어떤 통계를 돌리기 전에 하나의 정규 단위로 변환해야 한다.
유전 바이오마커는 변이로 교환되며, PHASE-1은 이를 위해 VCF 4.3 버전을 지정한다. 부속서의 예는 제시된 대립유전자와 맞지 않는 실제 변이 식별자를 쓰고 있으므로, 여기서는 좌표를 지어내고 rsID를 뺀 중립적인 예를 쓴다.
##fileformat=VCFv4.3
##INFO=<ID=AF,Number=A,Type=Float,Description="Allele frequency in a reference population">
##INFO=<ID=CLINSIG,Number=.,Type=String,Description="Clinical significance (source stated in header)">
##INFO=<ID=GENE,Number=1,Type=String,Description="Gene symbol">
#CHROM POS ID REF ALT QUAL FILTER INFO
chr1 1000001 . C T 99 PASS AF=0.0002;CLINSIG=Pathogenic;GENE=GENE_X
chr2 2000002 . A AG 99 PASS AF=0.0100;CLINSIG=Uncertain_significance;GENE=GENE_Y
AF는 VCF 명세의 예약 INFO 키이지만 CLINSIG와 GENE은 예약 키가 아니므로 위와 같이 헤더에 선언해야 한다. WIA 측정 레코드는 VCF를 복제하지 않는다. 대신 유전 바이오마커 결과는 변이(파일, 유전체 빌드, 염색체, 위치, 참조·대체 대립유전자)를 참조하고, 판정된 유전형이나 그로부터 도출한 바이오마커 상태를 기록한다. 유전체 빌드는 선택 사항이 아니다. 같은 위치라도 조립판(assembly)이 다르면 다른 염기를 가리킨다.
질량분석 데이터는 HUPO-PSI의 mzML 형식으로 교환되며, 스펙트럼의 모든 속성은 PSI-MS 통제 어휘의 cvParam으로 표현된다. 부속서 예에 나오는 세 개의 수탁 번호만 알아도 대부분의 파일을 읽을 수 있다. MS:1000511(ms level), MS:1000514(m/z array), MS:1000515(intensity array)이다. 머무름 시간은 단위 온톨로지(Unit Ontology)의 단위와 함께 MS:1000016(scan start time)으로 나타난다. 이진 배열은 base64로 인코딩되고 대개 압축되어 있으므로 WIA JSON에 복사하지 않는다. WIA 레코드는 정량 결과(UCUM 단위를 가진 농도)와, 그 결과를 도출한 mzML 파일 및 스펙트럼 식별자에 대한 포인터를 저장한다.
대사체 연구는 흔히 탭 구분 파일 묶음인 ISA-Tab을 쓴다. 온톨로지 출처를 선언하는 조사(investigation) 파일(i_), 검체당 한 행인 연구(study) 파일(s_), 측정당 한 행인 분석(assay) 파일(a_)로 구성된다. 한 행짜리 스케치는 다음과 같다.
a_assay.txt (illustrative values)
Sample Name Protocol REF Metabolite Identifier Concentration Unit
SAMPLE_001 LC-MS/MS HMDB0000122 5.4 mmol/L
HMDB0000122는 인간 대사체 데이터베이스(HMDB)의 D-포도당 항목이며, ChEBI 식별자도 같은 방식으로 쓸 수 있다. 가져올 때 ISA-Tab의 각 행은 biomarkerType: "metabolic", method: "mass_spectrometry"인 WIA 측정 하나가 되고, 대사체 식별자는 교차 참조로 보존되며, 검체 이름은 가명 patientID로 치환된다.
부속서의 형식 우선순위 표는 역할 분담을 요약한다. 바이오마커 정의에는 JSON 또는 FHIR, 유전체 데이터에는 VCF/BAM, 단백체에는 mzML/mzIdentML, 대사체에는 mzML/ISA-Tab, 임상 데이터에는 FHIR 또는 CDISC를 쓴다. 그 바탕에 있는 원칙은 WIA JSON이 결과와 그 맥락을 싣고, 도메인 형식이 원시 근거를 싣되, 둘은 참조로 연결된다는 것이다.
연구 데이터베이스 안에만 머무는 바이오마커 결과는 쓰임새가 제한적이다. 결과는 전자건강기록, 임상시험 제출 자료, 희귀질환 레지스트리에 도달해야 하며, 각각은 고유한 모델을 가진다. 아래 표는 §7.1 레코드의 모든 필드를 SDK, HL7 FHIR R4, CDISC SDTM에 대응시킨다. SDTM 열의 --는 두 글자 도메인 접두어를 뜻한다. 검사실형 결과는 보통 LB를 쓰며, 부속서 예는 사용자 정의 접두어 BM을 쓴다(아래 참조).
| WIA JSON (§7.1) | SDK 속성 | FHIR R4 | SDTM |
|---|---|---|---|
measurement.biomarkerID | BiomarkerMeasurement.biomarkerID | Observation.code(로컬 코드 + LOINC) | --TESTCD; IG 버전이 정의하는 경우 LOINC는 --LOINC |
biomarkerName | Biomarker.name | code.text | --TEST |
biomarkerType | Biomarker.type | 직접 대응 요소 없음(프로파일 또는 category) | --CAT(의뢰자 정의) |
value | value | valueQuantity.value | --ORRES, --STRESN |
unit | unit | valueQuantity.unit/code(UCUM system) | --ORRESU, --STRESU |
referenceRange.min/max | ReferenceRange.min/max | referenceRange.low/high | --ORNRLO, --ORNRHI |
measurementMethod | method(열거형) | Observation.method | --METHOD |
laboratoryID | laboratoryID | performer(Organization) | --NAM |
dateCollected | dateCollected | effectiveDateTime | --DTC |
dateMeasured | dateMeasured | 정확한 대응 요소 없음. issued는 보고 시각 | 보충 한정자(supplemental qualifier) |
sampleType | sampleType | specimen → Specimen.type | --SPEC |
qualityFlags | qualityFlags | Specimen.condition 또는 Observation.note | --SPCCND |
patient.patientID | patientID | subject(Patient) | USUBJID |
patient.age, sex | PatientSample.age, sex | Patient.gender, 나이는 파생 | DM.AGE, DM.SEX |
patient.fastingStatus | PatientSample.fastingStatus | 프로파일별(예: component) | --FAST |
clinicalContext.diseaseStatus | PatientSample.diseaseStatus | 별도의 Condition 리소스 | 별도 도메인(예: MH) |
두 행은 근사 대응이므로 인터페이스 명세서에 반드시 문서화해야 한다. FHIR의 issued는 결과가 제공된 시각으로, 분석 시각보다 늦은 경우가 많다. 연구에서 그 차이가 중요하다면 dateMeasured를 확장(extension)으로 실어 보낸다. 그리고 질병 상태는 FHIR에서도 SDTM에서도 관찰 결과의 속성이 아니다. 다른 곳에 기록된 진단이며 환자와 시점으로 연결된다.
§7.1 레코드에 매핑을 적용하면 다음 리소스가 된다. 값 12.5 mg/L가 인라인 상한 10 mg/L보다 크므로 변환기는 해석 코드 H를 도출한다. WIA 레코드 자체에는 해석 필드가 없으므로, "값을 인라인 구간과 비교한다"는 규칙은 수신 시스템에 맡기지 말고 변환기의 일부로 구현해야 한다.
{
"resourceType": "Observation",
"id": "bio-12345-patient-456-20250115",
"status": "final",
"category": [{
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/observation-category",
"code": "laboratory"
}]
}],
"code": {
"coding": [{
"system": "http://loinc.org",
"code": "30522-7",
"display": "C reactive protein [Mass/volume] in Serum or Plasma by High sensitivity method"
}],
"text": "C-Reactive Protein"
},
"subject": { "reference": "Patient/PATIENT-456" },
"effectiveDateTime": "2025-01-15T08:30:00Z",
"issued": "2025-01-15T10:45:00Z",
"performer": [{ "reference": "Organization/LAB-789" }],
"valueQuantity": {
"value": 12.5, "unit": "mg/L",
"system": "http://unitsofmeasure.org", "code": "mg/L"
},
"interpretation": [{
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation",
"code": "H", "display": "High"
}]
}],
"method": { "text": "High-sensitivity immunoturbidimetric assay" },
"specimen": { "reference": "Specimen/serum-PATIENT-456-20250115" },
"referenceRange": [{
"low": { "value": 0, "unit": "mg/L", "system": "http://unitsofmeasure.org", "code": "mg/L" },
"high": { "value": 10, "unit": "mg/L", "system": "http://unitsofmeasure.org", "code": "mg/L" }
}]
}
이 예에서는 검사실이 결과를 즉시 보고했다고 가정하고 issued를 dateMeasured에서 가져왔다. 실제 인터페이스라면 보고 이벤트에서 이 값을 취해야 한다. 리소스 id는 바이오마커, 가명 환자, 채취일로부터 결정론적으로 만든다. 그래야 같은 결과를 다시 보내도 중복되지 않고 갱신된다. 제7장은 같은 멱등성을 안정적인 업무 식별자와 조건부 생성으로 얻는다. 음성인 품질 플래그는 보내지 않는다. FHIR에서 검체가 용혈되지 않았다고 굳이 적을 필요는 없지만, 양성 플래그는 Specimen.condition 항목이나 주석으로 변환해 임상의가 볼 수 있게 해야 한다.
임상시험 제출 자료에서 같은 결과는 소견(findings) 도메인의 한 행이 된다. 부속서 예는 BM 접두어를 쓰며 BMTESTCD, BMORRES, BMSTRESN, BMSTRESU, VISIT, BMDTC 같은 변수를 보인다. 사용자 정의 두 글자 코드를 채택하기 전에는 도메인 코드가 예약되어 있으므로 최신 SDTM 구현 지침(IG)과 통제 용어에서 확인해야 한다. 많은 제출 자료에서 중앙 검사실이 측정한 가용성 단백질은 그냥 LB에 속하고, 유전체 소견은 별도 도메인을 가진다. 아래 변수는 LB 접두어로 표시했다.
STUDYID USUBJID LBTESTCD LBTEST LBORRES LBORRESU LBORNRLO LBORNRHI LBSTRESN LBSTRESU LBNRIND LBSPEC LBFAST LBDTC LBLOINC
EX-001 EX-001-PAT-456 CRP C-Reactive Protein 12.5 mg/L 0 10 12.5 mg/L HIGH SERUM Y 2025-01-15T08:30:00 30522-7
SDTM이 원래 보고된 결과(--ORRES, 단위를 동반한 문자 값)와 표준화된 수치 결과(--STRESN, 의뢰자의 표준 단위)를 분리한다는 점에 주목해야 한다. WIA 레코드는 값과 단위를 하나씩만 가지므로, 표준 단위로 변환하는 단계가 있다면 그 단계가 --STRESN을 채운다. SDTM 날짜·시각은 ISO 8601이지만 보통 UTC 지정자 없이 쓴다. 변환기는 연구 현지 시각으로 바꿀지 결정하고 그 선택을 문서화해야 한다.
Phenopackets(버전 2)는 희귀질환·유전체 연구를 위해 개인을 기술하는 형식이며, 바이오마커 결과는 measurements[]의 한 항목으로 나타난다.
{
"assay": { "id": "LOINC:30522-7", "label": "C reactive protein [Mass/volume] in Serum or Plasma by High sensitivity method" },
"value": {
"quantity": {
"unit": { "id": "UCUM:mg/L", "label": "milligram per liter" },
"value": 12.5,
"referenceRange": {
"unit": { "id": "UCUM:mg/L", "label": "milligram per liter" },
"low": 0, "high": 10
}
}
},
"timeObserved": { "timestamp": "2025-01-15T08:30:00Z" }
}
Phenopacket은 CURIE 형식의 식별자(LOINC:, UCUM:)를 쓰므로, FHIR용으로 고른 LOINC 코드와 UCUM 단위를 그대로 재사용한다. 이것이 3.6절이 주는 실질적인 이득이다. WIA 레코드에서 검사와 단위를 한 번 올바르게 부호화해 두면, 이후의 모든 형식은 기계적인 변환이 된다.
형식의 품질은 경계에서 적용하는 검사의 품질을 넘지 못한다. 본 명세는 레코드를 기술하지만 필수 필드 목록을 담은 기계 판독용 스키마를 공개하지 않으며, SDK의 validateBiomarker는 표본 수와 두 군의 존재를 확인할 뿐 단위, 참고구간, 품질 플래그는 보지 않는다. 따라서 그 일은 수집(ingestion) 계층이 맡아야 한다. 아래 규칙은 이 책이 권하는 최소 집합이다. 오류 코드는 SDK의 BiomarkerErrorCode에서 가져왔으며, 이를 레코드 수준 검증에 쓰는 것은 v1.0의 요구사항이 아니라 제안이다.
| 규칙 | 검사 내용 | 실패 시 | 제안 코드 |
|---|---|---|---|
| R1 봉투 | standard가 WIA-BIO-004이고 version의 주 버전을 지원함 | 거부 | — |
| R2 필수 필드 | biomarkerID, value(유한한 숫자), unit, sampleType, dateCollected가 있음 | 거부 | B002 |
| R3 어휘 | sampleType, biomarkerType, 도출한 방법 계열이 열거형의 원소임 | 거부 또는 매핑 | B002 |
| R4 단위 일관성 | referenceRange.unit이 unit과 같거나 알려진 UCUM 변환이 존재함 | 변환, 불가하면 거부 | B003 |
| R5 구간 타당성 | min ≤ max | 거부 | B003 |
| R6 시간 순서 | dateCollected ≤ dateMeasured, 둘 다 UTC | 거부 | B002 |
| R7 검체 품질 | qualityFlags에 양성 플래그가 있거나 other가 비어 있지 않음 | 연구 규칙에 따라 제외 또는 주석 | B008 |
| R8 알 수 없는 상태 | diseaseStatus가 null 또는 unknown | 보존하되 환자군·대조군 집합에서 제외 | 연구에 필요하면 B009 |
| R9 파생 지표 | PPV/NPV/LR/정확도가 명시된 입력에서 반올림 범위 안으로 재계산됨 | 보고 레코드 거부 | B010 |
다음 스케치는 §7.1 레코드에 대해 R2, R4, R6, R7 규칙을 구현한다. 의도적으로 작게 만들었으며 UCUM 변환은 시도하지 않는다. 운영 환경에서는 UCUM 라이브러리에 맡기면 된다.
type Issue = { rule: string; code: string; message: string };
export function checkMeasurement(rec: any, excludeFlagged = true): Issue[] {
const issues: Issue[] = [];
const m = rec?.measurement ?? {};
// R2: required fields
for (const f of ['biomarkerID', 'unit', 'sampleType', 'dateCollected']) {
if (m[f] === undefined || m[f] === '') {
issues.push({ rule: 'R2', code: 'B002', message: `missing ${f}` });
}
}
if (typeof m.value !== 'number' || !Number.isFinite(m.value)) {
issues.push({ rule: 'R2', code: 'B002', message: 'value must be a finite number' });
}
// R4: unit consistency (exact match; conversion handled elsewhere)
const rr = m.referenceRange;
if (rr && rr.unit !== m.unit) {
issues.push({ rule: 'R4', code: 'B003',
message: `range unit ${rr.unit} differs from value unit ${m.unit}` });
}
// R6: collection must not be after measurement
const tc = Date.parse(m.dateCollected);
const tm = Date.parse(m.dateMeasured);
if (!Number.isNaN(tc) && !Number.isNaN(tm) && tc > tm) {
issues.push({ rule: 'R6', code: 'B002', message: 'dateMeasured precedes dateCollected' });
}
// R7: specimen quality flags
const q = m.qualityFlags ?? {};
const raised = Object.entries(q)
.filter(([k, v]) => v === true || (k === 'other' && Array.isArray(v) && v.length > 0))
.map(([k]) => k);
if (raised.length > 0 && excludeFlagged) {
issues.push({ rule: 'R7', code: 'B008', message: `flagged specimen: ${raised.join(', ')}` });
}
return issues;
}
3.1절의 CRP 레코드에 이 함수를 실행하면 빈 목록이 반환된다. 필수 필드가 모두 있고, 단위가 일치하며, 채취가 측정보다 2시간 15분 앞서고, 세 플래그가 모두 false이기 때문이다. referenceRange.unit을 "mg/dL"로 바꾸면 R4 문제가 하나 생긴다. 10 mg/dL은 100 mg/L이므로, 이것이 바로 단순한 숫자 비교가 결과를 잘못 분류하는 상황이다. 제8장에서는 이런 변형을 음성 시험 벡터로 만든다.
두 가지를 덧붙인다. R7 규칙에 매개변수를 둔 것은 올바른 조치가 분석물에 따라 다르기 때문이다. 용혈은 칼륨 결과를 무효로 만들지만, 같은 채혈로 수행한 유전 검사에는 무관할 수 있다. 연구 프로토콜은 바이오마커별로 어떤 플래그가 제외 사유이고 어떤 플래그가 주석 대상인지 밝혀야 하며, 분석을 반복할 수 있도록 그 결정을 결과와 함께 기록해야 한다. 그리고 R9는 비용이 가장 적게 드는데도 가장 자주 생략되는 규칙이다. 3.5절의 §5.3.1 정정도 바로 이 재계산으로 발견한 것이다.
sampleType, 방법 계열, DiseaseStatus, BiomarkerType)가 통합 분석을 가능하게 한다. null 질병 상태는 알 수 없음을 뜻하며 결코 false가 되어서는 안 된다.lowerLimit/upperLimit 분할을 쓰고, SDK는 단일 min/max 구간을 쓴다. 가져올 때 해당 분할을 골라 측정값에 복사한다.studyType은 SDK 열거형을 우선한다.