온톨로지로 첫
지식 그래프 만들기
임상 질문에서 HPO 그래프·질의·추론까지
0. 핵심 요약
Chapter 3은 Human Phenotype Ontology(HPO)를 이용해 희귀질환 진단을 지원하는 첫 지식 그래프를 만드는 구현 장이다. 겉으로는 Neo4j와 Cypher 실습처럼 보이지만, 실제 핵심은 문제 정의 → 데이터 의미 이해 → 표현 모델 선택 → 적재와 품질 통제 → 질문과 추론의 설계 순서다. 임상의에게 필요한 것은 “그래프가 있다”는 사실이 아니라, 관찰한 표현형 조합에서 후보 질병을 찾고 그 연결의 출처·증거·작성일을 검토할 수 있는가이다.
이를 위해 장은 성격이 다른 두 HPO 원천을 결합한다. hp.owl은 표현형의 표준 정의와 subClassOf 계층을 제공하고, phenotype.hpoa는 질병–표현형 연관과 그 근거를 제공한다. 온톨로지의 표현력은 보존하되 관계별 메타데이터를 쉽게 탐색하려고 LPG와 Neo4j를 선택하고, Neosemantics로 RDF/OWL을 가져온다. 완성된 그래프는 직접 연결 질의뿐 아니라 상위 표현형 범주 아래의 하위 개념을 따라 암시적 후보까지 확장한다.
- 문제 — 왜 희귀질환 후보 탐색에는 문자열 통합보다 표준 의미와 계층이 필요한가?
- 선택 — RDF 원천을 쓰면서도 왜 LPG를 애플리케이션 모델로 선택했는가?
- 구축 — 두 원천을 어떤 식별자와 품질 게이트로 하나의 그래프로 만드는가?
- 경계 — 후보 랭킹과 계층 추론을 어디까지 믿고, 무엇을 임상의에게 남겨야 하는가?
이 장은 개념 장이 아니라 구현 장이다. 그래서 이 리포트는 교재 Listing 3.5–3.28의 코드를 본문에 함께 싣고 한 줄씩 쉬운 말로 풀었다. §2.2는 같은 사실 하나를 RDF 세 방식과 LPG로 각각 적어 비교하고, §3은 온톨로지 적재부터 정제까지의 파이프라인을, §5–6은 추론과 근거 추적 질의를 다룬다.
아울러 이 회차의 학습 노트북 03_chapter_guide.ipynb가 모든 리스팅을 실제 Neo4j에서 끝까지 실행한 기록을 남겼다. 그 실측치를 책 값과 나란히 놓으면 재현에 관한 사실이 드러난다 — 적재 트리플은 899,558에서 926,450으로 늘었고, 진단 질의의 1위 후보는 그대로지만 2위 이하는 바뀌었으며, 책 표 3.3이 강조한 Hyperglycemia는 더 이상 내분비계 이상의 하위가 아니다. 코드는 그대로인데 결과가 달라지는 이 세 가지가 §3.4·4.2·5.2의 주제다.
| 단계 | 핵심 질문 | 장의 답 | 검증 산출물 |
|---|---|---|---|
| 01 · 문제 | 누가 어떤 판단을 하는가? | 임상의가 관찰한 표현형으로 희귀질환 후보와 근거를 찾는다. | 대표 질문·실패 비용·사용자 역할 |
| 02 · 의미 | 서로 다른 표현을 어떻게 맞추는가? | HPO를 참조 어휘와 계층 스키마로 사용한다. | ID 매핑·동의어·계층 방향 |
| 03 · 모델 | 관계별 근거를 어디에 두는가? | OWL 계층을 LPG로 가져오고 주석 메타데이터를 관계에 둔다. | RDF/LPG 선택 근거·그래프 계약 |
| 04 · 사용 | 무엇을 질의하고 추론하는가? | 직접 연관을 세고, 계층을 따라 암시적 포함을 확장한다. | 재현 질의·근거 경로·결과 스냅샷 |
| 05 · 통제 | 어디까지 믿을 수 있는가? | 버전·적재·추론 범위와 임상 판단의 경계를 분리한다. | 품질 게이트·사람의 최종 검토 |
1. 그래프보다 먼저 임상 질문을 고정한다
- 의미 이질성 — 같은 개념의 여러 이름, 같은 약어의 여러 뜻, 서로 다른 세분성이 통합을 어렵게 한다.
- 사용자 요구 — 임상의는 표현형 조합에서 희귀질환 후보와 그 연결 근거를 보고 싶다.
- 온톨로지 역할 — 원천을 대신하는 데이터베이스가 아니라 로컬 표현을 공통 개념에 매핑하는 참조 계층이다.
1.1 형식보다 어려운 것은 ‘같은 뜻인가’이다
XML, CSV, JSON 같은 형식 차이는 파서로 처리할 수 있다. 더 어려운 문제는 의미다. 제2형 당뇨병과 ketosis-resistant diabetes처럼 같은 개념이 다른 이름으로 나타나고, PE처럼 physical examination과 pulmonary embolism이라는 서로 다른 뜻을 가질 수 있다. necrosis와 lobular necrosis처럼 표현의 세분성도 다르다. 문자열을 정규화하는 것만으로는 이 차이를 안전하게 합칠 수 없다.
온톨로지는 표준 이름·식별자·동의어·속성·범주·관계를 정의한다. 각 원천의 로컬 필드를 온톨로지 개념에 매핑하면 서로 다른 원천이 같은 의미 좌표를 공유한다. 중요한 경계는 매핑도 하나의 주장이라는 점이다. 어떤 로컬 값이 어느 표준 개념과 같은지, 누가 언제 어떤 근거로 판단했는지를 추적해야 한다.
1.2 목표는 자동 진단이 아니라 검토 가능한 후보 탐색이다
이 장의 대상 사용자는 질병을 진단하고 치료하는 임상의다. 특히 희귀질환은 한두 증상으로 식별하기 어렵고, 여러 표현형 특징의 조합과 문헌 근거를 함께 살펴야 한다. 따라서 지식 기반에는 두 가지가 필요하다. 첫째, 같은 기관·계통에 속하는 표현형 이상을 연결하는 맥락적 계층. 둘째, 질병과 표현형의 연관뿐 아니라 출처·증거·빈도·작성자를 추적할 수 있는 주석이다.
도메인에는 경계 사례도 있다. 제1형 당뇨병은 하나의 질병으로 식별될 수 있고, 다른 희귀 증후군에서 관찰되는 표현형 특징으로도 식별될 수 있다. 한 문자열에 역할을 억지로 합치지 않고 OMIM:222100과 HP:0100651처럼 서로 다른 표준 ID를 사용하면, 질문의 문맥에 따라 질병 노드와 표현형 노드를 구분할 수 있다.
| 항목 | 이 장의 정의 | 그래프 표현 | 오해하면 생기는 문제 |
|---|---|---|---|
| 표현형 특징 | 개인에게 나타나는 관찰 가능한 물리·생화학적 특성 | HpoPhenotype 노드와 계층 | 질병명과 섞이면 역할별 질의가 흐려짐 |
| 질병 | 원인·시간 경과·표현형 묶음·치료 반응으로 특징지어지는 실체 | HpoDisease 노드 | 표현형 하나를 곧바로 질병으로 확정 |
| 주석 | 특정 질병과 표현형이 연결된다는 근거 있는 주장 | HAS_PHENOTYPIC_FEATURE 관계 | 출처가 노드 속성에 섞여 어느 연결의 근거인지 잃음 |
| 임상 결과 | 관찰 특징과 연결된 검토 대상 질병 목록 | 경로·겹침 수·근거 속성 | 후보 목록을 진단 확률이나 확정 진단으로 해석 |
그래프 모델을 그리기 전에 “임상의가 결과 화면에서 어떤 연결과 근거를 확인해야 하는가?”를 묻는다. 이 질문이 노드·관계·속성과 기술 선택을 결정한다.
2. 두 데이터 원천이 모델 선택을 결정한다
hp.owl은 표현형의 정의·동의어·외부 참조·계층을 제공한다.phenotype.hpoa는 질병–표현형 연관과 출처·증거·빈도·큐레이션 정보를 제공한다.- 이 사례의 LPG 선택은 일반 우열이 아니라 관계별 메타데이터를 직접 탐색하려는 목표에서 나온다.
2.1 HPO는 ‘어휘’와 ‘주석’을 따로 준다
hp.owl은 RDF/XML로 직렬화된 OWL 온톨로지다. 각 표현형을 owl:Class로 정의하고 URI, 자연어 정의, 작성자·작성일, 외부 데이터베이스 상호참조, 동의어, rdfs:subClassOf를 제공한다. 이 가운데 계층은 나중에 “내분비계 이상” 같은 넓은 질문을 더 구체적인 표현형까지 확장하는 추론의 재료가 된다.
phenotype.hpoa는 TSV 형식의 주석 파일이다. 각 행은 질병 ID와 표현형 ID의 연관을 주장하며, PubMed나 OMIM 출처, IEA·PCS·TAS 증거 코드, 발현 시점, 빈도, 성별, 수정자, 큐레이터와 날짜를 함께 담는다. 이 정보는 질병이나 표현형 자체의 속성이 아니라 둘 사이 특정 연결의 속성이다.
| 관점 | hp.owl | phenotype.hpoa | 결합 시 확인 |
|---|---|---|---|
| 형식 | RDF/XML · OWL | TSV · HPOA | 파서와 헤더/메타데이터 행 수 |
| 정본 정보 | 표현형 ID·정의·동의어·계층 | 질병 ID와 표현형 ID의 주석 | hpo_id가 온톨로지 노드와 일치하는가 |
| 근거 | 개념 자체의 작성·상호참조 | 연관의 reference·evidence·frequency | 근거가 해당 관계에 보존되는가 |
| 변경 영향 | 계층·용어의 추가·폐기 | 주석·증거·질병 목록의 갱신 | 같은 스냅샷 조합과 적재 로그를 재현하는가 |
2.2 RDF와 LPG는 요구를 기준으로 비교한다
RDF는 주어–술어–목적어 트리플을 기본 단위로 하며, W3C 표준과 웹 식별자, 상호운용, 풍부한 온톨로지 표현에 강하다. LPG는 노드와 개별 관계에 키–값 속성을 둘 수 있어 경로 순회와 관계 메타데이터 접근이 직관적이다. RDF가 “관계에 속성을 붙일 수 없다”는 단순 비교는 정확하지 않다. n항 관계, 명명 그래프, RDF-star처럼 문장에 대한 메타데이터를 표현하는 방법이 있으며, 대신 모델·질의·엔진 지원의 복잡성이 달라진다.
이 장의 사용자 질문은 특정 질병–표현형 연관의 출처·작성자·날짜를 비교하는 데 집중한다. 그래서 저자는 관계 속성을 직접 다루는 LPG와 Cypher를 선택하고, Neosemantics로 RDF/OWL 어휘와 일부 추론 기능을 Neo4j에 가져온다. 이는 “LPG가 RDF보다 낫다”가 아니라 표준 온톨로지를 입력으로 사용하면서 애플리케이션 질의는 관계 중심으로 최적화한 하이브리드 결정이다.
같은 한 문장을 네 가지로 적어 보기
네 방식 모두 똑같은 사실 하나를 적는다. “질병 OMIM:222100과 표현형 HP:0410050이 연결되며, 그 근거는 논문 PMID:9357814이고 Nicole Vasilevsky가 2018-02-23에 기록했다.” 차이는 근거를 어디에 붙이느냐 하나뿐이다. 아래 네 블록은 각각 적는 법과 꺼내는 법을 한 쌍으로 묶었다. 마지막 LPG만 유일하게 우회 없이 끝난다.
① N항 관계 — 근거를 담을 노드를 하나 새로 만든다 (책 Listing 3.5 · 3.6)
# 적는 법 — 이름 없는 노드(_:Annotation)를 만들어 질병·표현형·근거를 모두 매단다
_:Annotation rdf:type :PhenotypicAnnotation ;
:forDisease OMIM:222100 ;
:phenotypicFeature HP:0410050 ;
:source PMID:9357814 ;
:createdBy "Nicole Vasilevsky" ;
:creationDate "2018-02-23"^^xsd:date .
# 꺼내는 법 (SPARQL) — 질병·표현형으로 그 노드를 먼저 찾고 나서 근거를 읽는다
SELECT ?source ?author ?date WHERE {
?annotation a :PhenotypicAnnotation ;
:forDisease OMIM:222100 ;
:phenotypicFeature HP:0410050 ;
:source ?source ; :createdBy ?author ; :creationDate ?date .
}
쉽게 말하면 — “A와 B가 연결됨”이라는 한 줄을 적을 수 없어서, 연결 그 자체를 대신할 노드를 하나 발명한다. 질병과 표현형은 이제 직접 이어지지 않고 이 중간 노드를 거친다. 모델이 한 단계 깊어지고, 질의도 “중간 노드부터 찾기”로 길어진다. _:Annotation의 _:는 공백 노드(blank node), 즉 이름을 붙일 필요가 없는 익명 자원이라는 표시다.
② 명명 그래프 — 문장을 묶어 이름표를 붙인다 (책 Listing 3.7 · 3.8)
# 적는 법 (TriG) — 문장을 :Graph1 이라는 상자에 넣고, 상자에 근거를 붙인다
:Graph1 {
OMIM:222100 :hasPhenotypicFeature HP:0410050
}
:Graph1
:source PMID:9357814 ;
:createdBy "Nicole Vasilevsky" ;
:creationDate "2018-02-23"^^xsd:date .
# 꺼내는 법 (SPARQL) — 상자 안을 확인하고, 상자 바깥의 근거를 읽는다
SELECT ?source ?author ?date WHERE {
GRAPH :Graph1 { OMIM:222100 :hasPhenotypicFeature HP:0410050 . }
:Graph1 :source ?source ; :createdBy ?author ; :creationDate ?date .
}
쉽게 말하면 — 문장은 그대로 두고, 문장을 담은 봉투에 근거를 적는다. 트리플(3요소)에 “어느 봉투 소속인가”라는 네 번째 요소가 붙는 셈이다. 맥락을 담기엔 강력하지만, 주석이 28만 건이면 봉투도 28만 개가 필요해 저장·교환이 무거워지고 봉투 안 문장 하나만 고치기도 까다롭다.
③ RDF-star — 문장 자체를 주어로 삼는다 (책 Listing 3.9 · 3.10)
# 적는 법 — << >> 로 문장 하나를 통째로 감싸 그것을 다시 주어로 쓴다
<<OMIM:222100 :hasPhenotypicFeature HP:0410050>>
:source PMID:9357814 ;
:createdBy "Nicole Vasilevsky" ;
:creationDate "2018-02-23"^^xsd:date .
# 꺼내는 법 (SPARQL-star) — 같은 << >> 표기로 바로 읽는다
SELECT ?source ?author ?date {
<<OMIM:222100 :hasPhenotypicFeature HP:0410050>>
:source ?source ; :createdBy ?author ; :creationDate ?date .
}
쉽게 말하면 — 우회를 없애고 “이 문장에 대해 말하기”를 문법으로 직접 지원한다. 셋 중 가장 읽기 쉽다. 다만 표준 확장이라 엔진이 지원해야만 쓸 수 있고, 질의 성능도 아직 개선 중이다. 교재는 Orlandi 등을 인용해 “새 구문 확장을 쓰려면 RDF 엔진의 특정 구현이 필요하고, 그래서 채택이 제한된다”고 짚는다.
④ LPG — 관계 안에 그냥 적는다 (책 Listing 3.11 · 3.12)
// 적는 법 — 화살표 안 중괄호에 키–값을 그대로 넣는다
(d {id: "OMIM:222100"})
-[:HAS_PHENOTYPIC_FEATURE {
source: "PMID:9357814",
createdBy: "Nicole Vasilevsky",
creationDate: "2018-02-23"
}]->
(p {id: "HP:0410050"})
// 꺼내는 법 (Cypher) — 관계에 변수 r 을 붙이고 r 의 속성을 읽는다
MATCH (d)-[r:HAS_PHENOTYPIC_FEATURE]->(p)
WHERE d.id = "OMIM:222100" AND p.id = "HP:0410050"
RETURN r.source, r.createdBy, r.creationDate
쉽게 말하면 — 새 노드도, 봉투도, 확장 문법도 필요 없다. 관계에 이름(r)을 붙이고 속성을 읽으면 끝이다. 앞의 세 방식이 “근거를 어디 둘까”를 먼저 설계해야 했다면, LPG는 그 질문 자체가 생기지 않는다. 이 3줄짜리 질의가 이 장이 LPG를 고른 이유의 전부다.
? date 띄어쓰기)은 실행 가능한 형태로 바로잡았다. 주석 줄과 들여쓰기는 읽기 편하도록 발표자가 붙였다.
| 관점 | RDF 계열 | LPG | 이 사례의 판단 |
|---|---|---|---|
| 기본 표현 | 전역 식별 가능한 트리플 | 레이블 노드·고유 관계·속성 | HPO 원천은 RDF/OWL, 앱 모델은 LPG |
| 관계 메타데이터 | n항 관계·명명 그래프·RDF-star 등 | 개별 관계의 키–값 | 출처·증거·날짜 접근을 단순화 |
| 주요 강점 | 표준 교환·연합·의미 표현·추론 | 경로 탐색·관계 속성·운영 질의 | Neosemantics로 두 강점을 일부 결합 |
| 선택 위험 | 주석 모델과 엔진 지원이 복잡할 수 있음 | OWL 의미를 전부 자동 보존하지 않음 | 필요한 계층·규칙만 명시적으로 검증 |
3. 적재는 파일 복사가 아니라 의미 계약의 구현이다
- 제약과 인덱스를 먼저 만들고, Neosemantics 설정 뒤 HPO 온톨로지를 적재한다.
- 표현형 ID를 정렬한 다음 질병 노드와 관계를 만들고, 주석 필드를 관계 속성으로 풍부화한다.
- 적재 성공 로그만으로 끝내지 않고 원천 행·노드·관계·근거 속성의 보존성을 확인한다.
- 노트북 재실행 실측으로 책 값과의 차이(트리플 926,450 대 899,558, 3.22 단계 약 8분)를 확인한다.
3.1 CRISP-DM을 일곱 개의 산출물로 바꾼다
이 장은 Chapter 2의 CRISP-DM 변형을 구체적인 KG 구축 절차로 바꾼다. 비즈니스 이해에서는 대표 임상 질문을, 데이터 이해에서는 두 HPO 원천의 의미와 식별자를, 설계에서는 RDF/LPG 선택 근거를 남긴다. 구현은 온톨로지 적재와 주석 적재로 나뉘며, 정제 뒤 직접 질의와 계층 추론으로 사용 가치를 확인한다.
| 단계 | 주요 작업 | 출력 | 품질 확인 |
|---|---|---|---|
| DB 준비 | DB 생성, URI/ID 유일성 제약, ID 인덱스 | 중복을 거부하는 저장 공간 | 제약 충돌과 기존 데이터 상태 |
| n10s 설정 | 그래프 설정, URI 처리와 Neo4j 명명 규칙 지정 | RDF→LPG 변환 규칙 | namespace를 무시해도 충돌하지 않는가 |
| 온톨로지 적재 | n10s.rdf.import.fetch, phenotype label·ID 부여 | 표현형 정의와 계층 | 교재 기준 899,558 statements와 URI·ID·SUBCLASSOF 건수 비교* |
| 주석 적재 | 질병 노드와 HAS_PHENOTYPIC_FEATURE 생성 | 두 원천의 연결 | 매칭 실패·중복·누락 건수 |
| 풍부화 | evidence·frequency·biocuration 파싱과 URL 생성 | 검토 가능한 관계 속성 | 결측·복수값·정규식 실패 |
| 정제 | 사용하지 않는 Resource 노드 제거 | 질의 중심 그래프 | 추론·감사에 필요한 정보까지 지우지 않았는가 |
* 899,558은 Neo4j/APOC/n10s 5.20.0과 HPO 2025-02를 사용한 교재 실행값이다. 현재 릴리스의 정답 건수가 아니라 스냅샷 재현 기준선으로 사용한다.
전반부 코드 — 온톨로지가 들어오기까지 네 줄
표 5의 앞 세 단계는 코드로 보면 놀랄 만큼 짧다. 무거운 일은 전부 Neosemantics가 대신하고, 우리가 하는 일은 규칙을 미리 정해 두는 것뿐이다.
// ① 그릇 만들기 (책 3.13) — Enterprise 전용 명령이다
CREATE DATABASE hpo IF NOT EXISTS
// ② 중복을 막는 장치 (책 3.14) — 단순 무결성 장치가 아니다.
// uri 유일성이 있어야 n10s 가 온톨로지를 MERGE 로 '몇 번 돌려도 같게' 적재한다
CREATE CONSTRAINT n10s_unique_uri IF NOT EXISTS FOR (r:Resource) REQUIRE r.uri IS UNIQUE;
CREATE CONSTRAINT IF NOT EXISTS FOR (n:Resource) REQUIRE (n.id) IS UNIQUE;
CREATE INDEX disease_id IF NOT EXISTS FOR (n:HpoDisease) ON (n.id);
CREATE INDEX phenotype_id IF NOT EXISTS FOR (n:HpoPhenotype) ON (n.id);
// ③ 변환 규칙 정하기 (책 3.15) — 이 두 줄이 이후 모든 Cypher 의 생김새를 결정한다
CALL n10s.graphconfig.init();
CALL n10s.graphconfig.set({ handleVocabUris: "IGNORE" }); // 네임스페이스를 버린다
CALL n10s.graphconfig.set({ applyNeo4jNaming: True }); // 관계 타입을 대문자로
// ④ 적재 (책 3.16) — 딱 한 줄이다
CALL n10s.rdf.import.fetch("http://purl.obolibrary.org/obo/hp.owl", "RDF/XML");
③이 왜 중요한가 — handleVocabUris: "IGNORE"는 임포트할 때 네임스페이스를 떼어 낸다. 그래서 속성 이름이 obo:IAO_0000115가 아니라 iAO_0000115가 된다. applyNeo4jNaming: True는 관계 타입을 대문자로 바꾼다. 그래서 OWL의 rdfs:subClassOf가 LPG 관용 표기인 :SUBCLASSOF로 들어온다. §5의 추론이 SUBCLASSOF라는 이름으로 계층을 훑을 수 있는 건 이 한 줄 덕분이다.
// ⑤ 접합면 만들기 (책 3.17) — 이 장에서 가장 중요한 다섯 줄
MATCH (n:Resource)
WHERE n.uri STARTS WITH "http://purl.obolibrary.org/obo/HP"
SET n:HpoPhenotype,
n.id = coalesce(n.id,
replace(apoc.text.replace(n.uri, '(.*)obo/', ''), '_', ':'))
쉽게 말하면 — 온톨로지에서 온 노드의 주소는 http://purl.obolibrary.org/obo/HP_0410050이고, TSV 주석 파일이 쓰는 표기는 HP:0410050이다. 생김새가 달라서 그냥 두면 두 파일은 영영 만나지 못한다. 이 질의는 URI 앞부분을 잘라내고 _를 :로 바꿔 두 파일이 같은 언어를 쓰게 만든다. coalesce는 “이미 id가 있으면 건드리지 말라”는 뜻이라 여러 번 실행해도 안전하다. 그림 2가 말한 “표준 ID로 결합”이 실제로 일어나는 곳이 바로 이 다섯 줄이다.
3.2 한 행을 한 건의 감사 가능한 주장으로 보존한다
교재의 Cypher는 TSV를 읽어 질병과 표현형을 MERGE하고, 관계가 만들어진 뒤 원시 필드와 사람이 읽을 설명을 순차적으로 추가한다. FOREACH(CASE …) 패턴은 값이 있는 필드만 설정해 null로 기존 값을 덮지 않는다. APOC 정규식은 biocuration에서 큐레이터와 날짜를 뽑고, 증거 코드 IEA·PCS·TAS를 설명으로 바꾼다.
후반부 코드 — TSV 한 행이 관계 하나가 되기까지
같은 파일을 세 번 읽는다. 첫 번째는 노드를 만들고, 두 번째는 선을 잇고, 세 번째는 그 선에 근거를 붙인다. 한 번에 못 하는 이유는 관계를 만들려면 양쪽 노드가 이미 있어야 하기 때문이다.
// ① 질병 노드 만들기 (책 3.19)
LOAD CSV FROM 'https://mng.bz/qRyr' AS row
FIELDTERMINATOR '\t'
WITH row
SKIP 5 // 메타데이터 4줄 + 헤더 1줄을 건너뛴다
MERGE (dis:Resource:HpoDisease {id: row[0]}) // 같은 질병이 여러 행에 나오므로 MERGE
ON CREATE SET dis.label = row[1]; // 이름은 처음 만들 때만 채운다
// ② 질병 ↔ 표현형 잇기 (책 3.20)
LOAD CSV FROM 'https://mng.bz/qRyr' AS row
FIELDTERMINATOR '\t'
WITH row
SKIP 5
MATCH (dis:HpoDisease) WHERE dis.id = row[0] // TSV 0번 열 = OMIM:222100
MATCH (phe:HpoPhenotype) WHERE phe.id = row[3] // TSV 3번 열 = HP:0410050
MERGE (dis)-[:HAS_PHENOTYPIC_FEATURE]->(phe)
여기가 두 파일이 만나는 지점이다. phe.id는 ①에서 hp.owl의 URI를 가공해 만든 값이고, row[3]은 TSV가 원래 갖고 있던 값이다. 형식이 전혀 다른 두 파일이 온톨로지의 표준 식별자를 공통 언어로 삼아 하나의 그래프가 된다. §1에서 말한 “의미 통합”의 구체적 실체가 이 두 줄이다.
// ③ 관계에 근거 붙이기 (책 3.22) — 열 8개를 그대로 관계 속성으로
LOAD CSV FROM 'https://mng.bz/qRyr' AS row
FIELDTERMINATOR '\t'
WITH row
SKIP 5
MATCH (dis:HpoDisease)-[rel:HAS_PHENOTYPIC_FEATURE]->(phe:HpoPhenotype)
WHERE phe.id = row[3] AND dis.id = row[0]
FOREACH(_ IN CASE WHEN row[4] IS NOT NULL THEN [1] ELSE [] END | SET rel.source = row[4])
FOREACH(_ IN CASE WHEN row[5] IS NOT NULL THEN [1] ELSE [] END | SET rel.evidence = row[5])
FOREACH(_ IN CASE WHEN row[6] IS NOT NULL THEN [1] ELSE [] END | SET rel.onset = row[6])
FOREACH(_ IN CASE WHEN row[7] IS NOT NULL THEN [1] ELSE [] END | SET rel.frequency = row[7])
FOREACH(_ IN CASE WHEN row[8] IS NOT NULL THEN [1] ELSE [] END | SET rel.sex = row[8])
FOREACH(_ IN CASE WHEN row[9] IS NOT NULL THEN [1] ELSE [] END | SET rel.modifier = row[9])
FOREACH(_ IN CASE WHEN row[10] IS NOT NULL THEN [1] ELSE [] END | SET rel.aspect = row[10])
FOREACH(_ IN CASE WHEN row[11] IS NOT NULL THEN [1] ELSE [] END | SET rel.biocuration = row[11])
FOREACH(CASE …)가 왜 이렇게 생겼나 — Cypher에는 “조건이 맞을 때만 SET”이라는 문법이 없다. 그래서 원소가 1개인 리스트를 도는 반복문과 빈 리스트를 도는 반복문(=아무것도 안 함)으로 if를 흉내 낸다. 목적은 단순하다. TSV에서 빈 칸인 열이 기존 값을 null로 덮어쓰지 못하게 막는 것이다. 여덟 번 반복되는 건 열이 여덟 개이기 때문이지 다른 뜻은 없다.
// ④ 사람이 읽을 수 있게 바꾸기 (책 3.23) — 약어를 문장으로, 출처를 링크로
CALL apoc.periodic.iterate(
"MATCH (dis:HpoDisease)-[rel:HAS_PHENOTYPIC_FEATURE]->(phe:HpoPhenotype) RETURN rel",
"SET rel.createdBy = apoc.text.regexGroups(rel.biocuration, 'HPO:(\\w+)\\[')[0][1],
rel.creationDate = apoc.text.regexGroups(rel.biocuration, '\\[(\\d{4}-\\d{2}-\\d{2})\\]')[0][1],
rel.aspectName = CASE
WHEN rel.aspect = 'P' THEN 'Phenotypic abnormality'
WHEN rel.aspect = 'I' THEN 'Inheritance' END,
rel.evidenceName = CASE
WHEN rel.evidence = 'IEA' THEN 'Inferred from electronic annotation'
WHEN rel.evidence = 'PCS' THEN 'Published clinical study'
WHEN rel.evidence = 'TAS' THEN 'Traceable author statement' END,
rel.url = CASE
WHEN rel.source STARTS WITH 'PMID:' THEN
'https://pubmed.ncbi.nlm.nih.gov/' + apoc.text.replace(rel.source, '(.*)PMID:', '')
WHEN rel.source STARTS WITH 'OMIM:' THEN
'https://omim.org/entry/' + apoc.text.replace(rel.source, '(.*)OMIM:', '') END",
{batchSize: 1000})
// ⑤ 안 쓰는 것 치우기 (책 3.24)
CALL apoc.periodic.iterate(
"MATCH (n:Resource) RETURN id(n) AS id",
"MATCH (n) WHERE id(n) = id
AND NOT 'HpoPhenotype' IN labels(n)
AND NOT 'HpoDisease' IN labels(n)
DETACH DELETE n",
{batchSize: 10000})
YIELD batches, total RETURN batches, total
④는 “친절함”을 데이터에 새겨 넣는 단계다. biocuration 칸에 HPO:NicoleVasilevsky[2018-02-23]이라고만 적혀 있으면 사람이 읽기 불편하다. 정규식으로 이름과 날짜를 뽑아 별도 속성으로 만들고, PCS 같은 세 글자 약어를 Published clinical study라는 문장으로 풀고, PMID:9357814를 클릭 가능한 PubMed 주소로 바꾼다. §6이 요구하는 “임상의가 근거를 열어볼 수 있어야 한다”가 여기서 준비된다. apoc.periodic.iterate는 28만 건을 1,000건씩 나눠 처리해 메모리를 아끼는 장치다.
⑤는 되돌릴 수 없는 단계다. 표현형도 질병도 아닌 Resource 노드를 전부 지운다. 온톨로지의 동의어 타입·주석 출처 같은 메타데이터가 여기서 사라지고, 그래서 책 3.18의 중간 점검 질의는 이 단계 이후 빈 결과를 낸다(§3.3 참조).
하지만 이 구현에는 중요한 보존성 경계가 있다. 관계를 MERGE (dis)-[:HAS_PHENOTYPIC_FEATURE]->(phe)처럼 질병–표현형 쌍만으로 합치면, 같은 쌍에 서로 다른 출처·작성일의 주석이 여러 건 있어도 하나의 관계가 된다. 이후 모든 일치 관계에 속성을 설정하면 마지막 처리 행의 값이 앞선 근거를 덮을 수 있다. 장의 개념 목표인 “주석별 provenance”를 구현하려면 주석 ID를 별도 노드로 승격하거나, 관계의 고유 키를 모델에 포함하고 행 수 보존 테스트를 두어야 한다.
import_hpo.py 감사를 바탕으로 재구성. 같은 질병–표현형 쌍의 주석이 여러 건이면 관계 식별자 없이 MERGE한 한 관계에 마지막 값이 덮일 수 있다는 구현 위험은 발표자의 코드 감사 결과다.
3.3 책의 결과와 현재 실행 결과는 분리한다
교재 결과는 Neo4j 5.20.0, APOC 5.20.0, Neosemantics 5.20.0과 2025년 2월 HPO를 기준으로 한다. 저장소의 학습 노트북은 2026-06-23 HPOA 메타데이터를 확인했으며, “latest” URL을 쓰면 데이터가 계속 바뀐다. 따라서 노드·관계 수와 상위 질병 목록이 책과 다른 것은 곧바로 오류가 아니라 데이터 드리프트일 수 있다. 재현하려면 온톨로지·주석의 릴리스 URL과 체크섬을 함께 고정해야 한다.
또한 교재 Listing 3.18의 중간 상태 질의는 최종 정제 후 실행하면 실패한다. 이는 동일 코드가 어떤 시점에 실행됐는지가 결과의 일부라는 뜻이다. 실습 저장소의 파일 번호는 최종 교재보다 +3 오프셋이 있고, 일부 업스트림 파일은 중복 또는 0바이트라 study.toml의 [mapping.listings.ch03]가 정본이다.
| 경계 | 관찰 | 영향 | 권장 통제 |
|---|---|---|---|
| 데이터 버전 | 책은 2025-02, 노트북 확인본은 2026-06-23 | 건수·후보 순위·주석 내용 변경 | 릴리스 URL·체크섬·적재일 기록 |
| 주석 다중성 | 질병–표현형 쌍만으로 관계 MERGE | 여러 provenance가 한 관계에서 덮일 수 있음 | 주석 식별자 또는 Annotation 노드 |
| 파싱 가정 | 큐레이터·날짜 정규식이 첫 패턴을 기대 | 결측·복수값에서 오류 또는 정보 손실 | 파싱 실패율과 원문 보존 |
| 단계 의존 | 중간 조회는 최종 정제 뒤 실패 | 실행 순서에 따라 데모 결과가 달라짐 | 단계별 스냅샷 또는 독립 검증 질의 |
| 조회 부작용 | Listing 3.21이 조회 목적에 미바인딩 MERGE 사용 | 노드·관계가 새로 생겨 그래프 오염 | MATCH로 교정하고 실행 전 스냅샷 보존 |
| 추론 결과 고정 | 교재 3.28은 5개 질병 화이트리스트, 저장소 3.31은 SKIP 100 LIMIT 5 | 같은 추론도 표시되는 부분집합이 다름 | 필터·정렬·페이지 기준을 결과와 함께 기록 |
| 리스팅 번호 | 업스트림 ch03 파일은 교재 번호와 +3 | 잘못된 코드 참조 위험 | study.toml 전수 대응표 사용 |
3.4 직접 돌려 보면 숫자가 이만큼 달라진다
이 회차의 학습 노트북 src/ch03/03_chapter_guide.ipynb는 책의 리스팅을 실제 Neo4j에 처음부터 끝까지 실행한 기록이다. 앞 절의 “드리프트가 있을 수 있다”는 말을 추상으로 두지 않고, 실측치를 책 값과 나란히 두면 무엇이 얼마나 움직이는지 눈으로 보인다.
노트북은 Docker로 띄운 Neo4j 5.26.28 Community에서 돌렸다(플러그인 n10s·apoc·gds). 그런데 책의 첫 명령 CREATE DATABASE hpo가 Community 에디션에서는 실패한다 — 멀티 데이터베이스는 Enterprise 전용 기능이라 UnsupportedAdministrationCommand 오류가 난다. 노트북은 기본 neo4j 데이터베이스를 그대로 쓰는 것으로 우회했고, 학습 내용에는 영향이 없다. 교재가 Enterprise 5.20 기준이라는 점을 모르면 첫 줄에서 막힌다.
| 항목 | 교재 (HPO 2025-02) | 노트북 (HPOA 2026-06-23) | 읽는 법 |
|---|---|---|---|
| 적재된 트리플 | 899,558 | 926,450 | 16개월 사이 약 2.7만 건 증가. 숫자가 다른 것이 정상이다 |
HpoPhenotype 노드 | — | 20,413 | 표현형 어휘 규모 |
HpoDisease 노드 | — | 12,956 | HPOA 헤더의 8574 OMIM · 47 DECIPHER · 4337 ORPHANET과 대조 가능 |
HAS_PHENOTYPIC_FEATURE 관계 | — | 284,994 | 주석 행 수와 이 값을 비교하는 것이 §3.2가 지적한 보존성 테스트다 |
| 정제 단계 삭제 노드 | — | 207,754 | 적재한 것의 상당수가 최종 그래프에 남지 않는다 |
SUBCLASSOF 관계 | — | 24,378 | §5 추론이 훑는 계층의 크기 |
소요 시간도 실습 준비에 직결된다. 대부분의 단계는 1초 안팎이고 온톨로지 적재(3.16)가 9.5초, 질병 노드·관계 생성(3.19·3.20)이 각 3초 남짓이다. 그런데 관계에 속성을 붙이는 3.22 한 단계만 약 8분(496초) 걸린다. 28만 5천 개 관계에 FOREACH 여덟 개를 배치 처리 없이 돌리기 때문이다. 전체를 처음부터 다시 만들면 9분 남짓 걸리므로, 시연 중에 이 셀을 실행하면 안 된다.
- 업스트림 파일 2건이 망가져 있다 — 책 3.21에 해당하는 저장소 파일은 다음 파일과 내용이 완전히 중복돼 엉뚱한 질의가 들어 있고, 책 3.25에 해당하는 파일은 0바이트 빈 파일이다. 두 건 모두 해설판 본문으로 대체했고,
study.toml이 각각upstream-wrong·upstream-missing으로 선언한다. 정정 내역은errata.toml에 기록돼 있다. - 해설판 본문의 3.21도 그대로 쓰면 안 된다 —
MERGE (dis:HpoDisease)-[…]->(phe:HpoPhenotype)로 시작하는데, 바인딩되지 않은 변수에MERGE를 쓰면 조회가 아니라 노드와 관계를 새로 만들어 그래프를 오염시킨다. 의도가 조회이므로MATCH로 고쳐 실행했다. - 교재의 HPO 용어 링크가 지금은 404다 —
hpo.jax.org/app/browse/term/…체계가 바뀌었다. 노트북은 같은 용어를 EBI OLS4에서 여는 링크로 대체했다.
4. 그래프는 표현형 조합을 후보 경로로 바꾼다
- 직접 질의는 특정 질병과 연결된 표현형을 먼저 펼쳐 기준 패턴을 확인한다.
- 새로 관찰한 표현형들을 공유하는 질병을 찾고 겹치는 특징 수로 후보를 정렬한다.
- 결과는 후보 생성과 근거 탐색을 지원할 뿐, 진단 확정이나 확률 추정이 아니다.
- 같은 질의를 최신 스냅샷에서 다시 돌리면 1위는 유지되지만 2위 이하는 바뀐다.
4.1 직접 연결은 답보다 검토 경로를 준다
첫 질의는 OMIM:222100 제1형 당뇨병 노드에 직접 연결된 표현형을 가져온다. 질문은 “이 질병의 증상은 무엇인가”다.
// 책 3.25 — 한 질병에서 뻗어 나가는 모든 표현형
MATCH path=(dis:HpoDisease)-[:HAS_PHENOTYPIC_FEATURE]->(phe:HpoPhenotype)
WHERE dis.id = "OMIM:222100"
RETURN path
노트북 재실행 결과 OMIM:222100에는 표현형 8개가 달려 있다 — Autoimmunity, Decreased circulating 1,5 anhydroglucitol concentration, Diabetes mellitus, Hyperglycemia, Ketoacidosis, Polydipsia, Polyphagia, Polyuria. 다음(그림 6)에서 중심 노드를 둘러싼 별 모양이 이것이다.
이어 임상의가 환자에게서 성장 지연(HP:0001510), 큰 무릎(HP:0030866), 감각신경성 청각 장애(HP:0000407), 소양증(HP:0000989)을 추가로 관찰했다고 가정한다. 위 목록에 없는 증상들이다. 이제 질문이 뒤집힌다 — “이 질병의 증상은?”이 아니라 “이 증상 조합을 가진 질병은?”이다. 그래프에서는 화살표를 반대로 따라가기만 하면 되는 같은 질의다. 두 번째 질의는 다섯 표현형과 연결된 질병을 모으고, 겹치는 특징 수를 내림차순으로 정렬한다.
// 책 3.26 — 증상 조합에서 질병 후보로
MATCH (phe:HpoPhenotype)
WHERE phe.label IN [
"Growth delay", "Large knee",
"Sensorineural hearing impairment", "Pruritus",
"Type I diabetes mellitus"
]
WITH phe
MATCH path=(dis:HpoDisease)-[:HAS_PHENOTYPIC_FEATURE]->(phe)
UNWIND dis AS nodes
RETURN dis.id AS disease_id,
dis.label AS disease_name,
collect(phe.label) AS features,
count(nodes) AS num_of_features
ORDER BY num_of_features DESC, disease_name
LIMIT 5
교재의 2025년 2월 스냅샷에서는 다섯 특징을 모두 공유하는 Odontochondrodysplasia 2 with hearing loss and diabetes(OMIM:619269)가 첫 후보로 나타난다. 그다음 후보는 세 개 또는 두 개의 특징을 공유한다. 그래프의 가치는 질병 이름만 반환하는 데 있지 않다. 어떤 표현형 경로가 점수를 만들었는지, 각 관계에 어떤 출처·증거·빈도가 있는지 다시 열어볼 수 있다.
| 후보 | 공유 특징 | 이 결과가 말하는 것 | 말하지 않는 것 |
|---|---|---|---|
| Odontochondrodysplasia 2 with hearing loss and diabetes | 5 | 입력 다섯 표현형 모두와 주석 경로가 있음 | 환자가 이 질병일 확률 또는 확정 진단 |
| Holoprosencephaly 12 with or without pancreatic agenesis (OMIM:618500) | 3 | 세 표현형을 공유하는 비교 후보 | 세 특징의 특이도·중요도가 같다는 뜻 |
| 3-methylglutaconic aciduria VIII (OMIM:614700), Alobar holoprosencephaly (OMIM:616192), Alpha-thalassemia/mental retardation syndrome, X-linked (OMIM:602782) | 각 2 | 교재 스냅샷의 동률 3건 | 빈도·부정 표현형·환자 맥락을 반영한 랭킹 |
4.2 같은 질의를 지금 돌리면 2위 이하가 바뀐다
§3.4의 노트북으로 똑같은 책 3.26 질의를 2026-06-23 HPOA에서 다시 돌린 결과다. 결론(1위)은 그대로지만 그 아래는 상당히 달라졌다.
| 순위 | 교재 (HPO 2025-02) | 노트북 재실행 (HPOA 2026-06-23) |
|---|---|---|
| 1 | OMIM:619269 Ondontochondrodysplasia 2 … (5) | OMIM:619269 동일 (5) — 변화 없음 |
| 2 | OMIM:618500 Holoprosencephaly 12 … (3) | OMIM:618500 동일 (3) — 변화 없음 |
| 3 | OMIM:614700 3-methylglutaconic aciduria VIII (2) | OMIM:621585 Mendez-Johnson immunoneurologic syndrome (3) — 신규 |
| 4 | OMIM:616192 Alobar holoprosencephaly (2) | OMIM:256500 Netherton syndrome (3) — 신규 |
| 5 | OMIM:602782 Alpha-thalassemia/mental retardation, X-linked (2) | OMIM:617248 3-methylglutaconic aciduria, type VIII (2) — 같은 병, 다른 OMIM 번호 |
세 가지를 읽어야 한다. 첫째, 진단 결론은 견고하다 — 다섯 특징을 모두 공유하는 1위는 16개월이 지나도 그대로다. 둘째, 3개 공유 후보가 두 건 새로 생겼다. 교재에서는 3개 공유가 1건뿐이라 2위가 도드라져 보였지만, 지금은 3개 공유가 3건이라 “2위와 3위의 격차”라는 인상 자체가 사라진다. 셋째, 3-methylglutaconic aciduria VIII의 OMIM 번호가 614700에서 617248로 바뀌었다 — 질병 식별자조차 고정된 상수가 아니다.
이것이 §3.3이 말한 드리프트의 실물이다. 슬라이드에 표 8만 띄우고 “이 목록이 나옵니다”라고 말하면, 청중이 노트북을 돌렸을 때 다른 화면을 보고 코드가 틀렸다고 생각하게 된다. 결과를 보여줄 때는 반드시 어느 릴리스에서 나온 값인지 함께 적어야 한다.
단순 겹침 수 count(nodes)는 표현형의 희귀도, 부정 소견, 발현 시점, 증거 질, 환자 인구집단을 가중하지 않는다. 결과 화면은 후보·경로·근거를 함께 보여주고 임상의의 다음 검토를 지원해야 한다. 위의 순위 변동이 보여주듯 “2위”라는 자리 자체가 데이터 갱신에 흔들린다.
5. 계층은 저장되지 않은 포함 관계를 질의한다
- 직접 연결만 찾으면 상위 범주 아래의 더 구체적인 표현형 주석을 놓친다.
SUBCLASSOF경로 또는 Neosemantics 추론은 하위 표현형을 상위 범주 질의에 포함한다.- 추론은 새 임상 사실을 발명하지 않으며, 계층·깊이·방향·주석 품질의 영향을 그대로 받는다.
- 깊이
*1..3은 실제 계층의 절반만 보고, 온톨로지 재분류는 코드 없이도 결과를 바꾼다.
5.1 직접 사실과 암시적 포함을 분리한다
“내분비계 이상(Abnormality of the endocrine system, HP:0000818)과 관련된 질병은?”이라는 질문을 생각해 보자. 어떤 질병은 이 상위 범주에 직접 주석되어 있지만, 더 많은 질병은 갑상선 생리 이상(HP:0002926), 갑상선기능저하증(HP:0000821), 갑상선종(HP:0000853)처럼 검증된 하위 표현형에 연결되어 있다. 직접 관계만 조회하면 후자를 놓친다.
교재는 먼저 자식에서 부모로 향하는 [:SUBCLASSOF*1..3] 경로를 역방향으로 탐색해 상위 범주 아래의 하위 표현형을 찾고, 이어 n10s.inference.nodesInCategory로 이 계층을 따라 질병을 포함한다. 반환된 질병–표현형 주석은 원래 그래프에 명시적으로 존재하지만, 상위 범주와 질병의 포함 관계는 계층 규칙에서 도출된다. 교재 표 3.3은 재현성을 위해 5개 질병 화이트리스트로 제한한 부분집합이고, 저장소 Listing 3.31은 화이트리스트 대신 SKIP 100 LIMIT 5를 사용하므로 표시 결과가 같지 않다.
코드로 보면 두 줄 차이다
// 책 3.27 — 계층을 손으로 훑는다
MATCH (p:HpoPhenotype)<-[:SUBCLASSOF*1..3]-(n:HpoPhenotype)
WHERE p.id = "HP:0000818"
RETURN p, n
// 책 3.28 — 같은 일을 Neosemantics 프로시저에 맡기고, 그 결과로 질병까지 끌어온다
MATCH (cat:HpoPhenotype {label: "Abnormality of the endocrine system"})
CALL n10s.inference.nodesInCategory(cat, {
inCatRel: "HAS_PHENOTYPIC_FEATURE", // 질병을 표현형에 잇는 관계
subCatRel: "SUBCLASSOF"}) // 하위 개념을 잇는 관계
YIELD node AS dis
MATCH (dis)-[:HAS_PHENOTYPIC_FEATURE]->(phe:HpoPhenotype)
RETURN dis.label AS disease, collect(DISTINCT phe.label) AS features
ORDER BY size(features) ASC, disease
쉽게 말하면 — *1..3은 “하위 클래스 관계를 1단계에서 3단계까지 따라가라”는 뜻이다. 화살표 방향(<-)에 주의해야 한다. HPO는 자식 → 부모 방향으로 SUBCLASSOF를 저장하므로, 부모에서 자식을 찾으려면 화살표를 거꾸로 읽어야 한다. 3.28의 nodesInCategory는 이 순회를 대신 해 주면서 깊이 제한 없이 훑고, 찾은 표현형에 주석된 질병까지 한 번에 돌려준다.
SUBCLASSOF이며, 세 예시는 HPO Ontology Service 2026-06-23 기준 HP:0000818의 2–3단계 하위임을 확인했다. 추론은 새 임상 사실을 만들지 않고 기존 주석의 질의 범위를 넓힌다.
5.2 더 넓은 결과가 더 정확한 결과는 아니다
계층 추론은 검색 누락을 줄이지만 품질을 자동 보장하지 않는다. 1..3 깊이는 구현자가 정한 범위이고, 더 깊은 하위 개념을 놓칠 수 있다. 반대로 범위를 무제한으로 넓히면 너무 일반적인 상위 개념을 통해 관련성이 약한 후보가 늘 수 있다. 또한 HPO와 주석은 불완전하게 갱신되므로 “연결 없음”을 “관련 없음”으로 해석해서는 안 된다.
노트북이 이 두 문장을 숫자로 확인했다. HP:0000818의 하위 표현형은 깊이 1\~3으로 세면 256개, 깊이 제한을 풀면 474개다. 즉 책 3.27의 *1..3은 실제 계층의 절반가량만 본다. 무엇을 놓치는지도 구체적이다. 책 표 3.3에 나오는 Transient neonatal diabetes mellitus는 HP:0000818에서 4단계 아래라 *1..3으로는 잡히지 않는다. 3.28이 프로시저를 쓰는 이유가 바로 이것이다.
| 관찰 | 실측 | 무엇을 뜻하는가 |
|---|---|---|
깊이 *1..3 하위 표현형 | 256개 | 책 3.27이 실제로 보는 범위 |
| 깊이 무제한 하위 표현형 | 474개 | 3.28의 nodesInCategory가 보는 범위. 임의로 정한 깊이는 절반을 가린다 |
| Transient neonatal diabetes mellitus | 깊이 4 | 책 표 3.3에 실제로 등장하는 항목인데 *1..3으로는 못 찾는다 |
| Hyperglycemia의 소속 | 더 이상 내분비계 이상의 하위가 아님 | 책 표 3.3은 굵게 표시했지만 현재 HPO는 대사 이상 쪽으로 재분류했다 |
마지막 행이 이 절에서 가장 중요한 관찰이다. 책 표 3.3은 Diabetes mellitus, transient neonatal, 1의 특징 가운데 Hyperglycemia를 “내분비계 이상의 하위”로 굵게 강조한다. 그런데 지금 HPO에서 이 용어는 내분비계가 아니라 대사 이상 쪽으로 옮겨졌다. 코드는 한 글자도 바뀌지 않았고 데이터도 그대로인데, 온톨로지의 분류가 바뀌었다는 이유만으로 추론 결과가 달라진 것이다.
§4.2의 순위 변동이 “데이터가 늘면 결과가 바뀐다”였다면, 이것은 “의미 구조가 바뀌면 결과가 바뀐다”다. 추론은 그래프에 저장된 사실뿐 아니라 계층 자체를 재료로 쓰기 때문에, 온톨로지 버전이 곧 결과의 일부다. 재현 가능한 운영이라면 주석 릴리스와 함께 온톨로지 릴리스도 고정해야 한다.
따라서 결과에는 직접 연결인지 계층으로 포함됐는지, 어떤 SUBCLASSOF 경로를 따라왔는지, 어느 HPO 릴리스를 썼는지를 함께 표시해야 한다. 임상의가 추론 경로를 검토할 수 없다면 추론의 설명 가능성은 저장소 내부에만 머문다.
| 관점 | 직접 연결 질의 | 계층 추론 | 화면에 남길 근거 |
|---|---|---|---|
| 범위 | 명시된 질병–표현형 주석 | 하위 표현형을 상위 범주에 포함 | direct / inferred 상태 |
| 가치 | 정확한 주석과 provenance 검토 | 계층 때문에 숨은 후보 회수 | 원 주석과 전체 계층 경로 |
| 주요 위험 | 계층적 의미를 놓쳐 누락 | 범위가 넓어져 관련성 약화 | 깊이·규칙·온톨로지 버전 |
| 부재 해석 | 주석이 없을 수 있음 | 하위 개념이 불완전할 수 있음 | “근거 없음”과 “거짓”을 구분 |
6. 신뢰도는 가장 짧은 미검증 구간에서 무너진다
- 버전이 다른 두 HPO 원천을 섞으면 재현성과 의미 정합성이 흔들린다.
- 식별자 매핑, 주석 다중성, 파싱, 정제, 추론 범위를 단계별로 검증해야 한다.
- 기술적으로 재현된 후보도 임상적 타당성을 자동 증명하지 않는다.
- 근거 화면은 개념이 아니라 이미 만들 수 있는 질의다 — 관계 속성 하나를 더 반환하면 된다.
6.1 네 게이트가 후보 결과의 감사 경로를 만든다
첫째, 원천 게이트에서 릴리스·컬럼·식별자·체크섬을 고정한다. 둘째, 의미 게이트에서 질병과 표현형 역할, HPO ID, 동의어와 계층 방향을 표본 감사한다. 셋째, 적재 게이트에서 원천 행 수가 노드·관계·주석으로 어떻게 변했는지, 중복과 결측이 어떻게 처리됐는지 비교한다. 넷째, 사용 게이트에서 대표 질의를 재현하고 직접·추론 결과와 임상 판단의 경계를 확인한다.
| 핵심 주장 | 근거 | 적용 조건 | 실패 신호 |
|---|---|---|---|
| 온톨로지가 의미 통합을 돕는다 | 표준 ID·동의어·계층으로 원천을 정렬 | 매핑 규칙과 온톨로지 범위가 맞음 | 동일 개념의 복수 ID·잘못된 계층 |
| LPG가 이 사례의 주석 탐색을 단순화한다 | 관계 속성으로 출처·증거에 직접 접근 | 개별 주석 다중성을 모델이 보존 | 관계 MERGE로 근거가 덮임 |
| 표현형 조합이 후보를 좁힌다 | 공유 특징 수 기준의 재현 가능한 질의 | 입력 코딩·주석·버전이 적절 | 결과가 드리프트하거나 근거가 빈약 |
| 계층 추론이 숨은 연관을 찾는다 | SUBCLASSOF 경로의 암시적 포함 | 계층 방향·깊이·규칙이 타당 | 너무 넓은 후보·설명할 수 없는 경로 |
6.2 근거 화면은 이미 만들 수 있다
지금까지 “임상의가 근거를 열어볼 수 있어야 한다”를 반복했다. 그것이 실제로 어떤 모습인지 보여주는 것이 교재 §3.4의 연습문제다 — “Listing 3.26의 질의를 확장해 evidence_name, evidence_description, source, url을 포함한 관계 속성까지 조회하라.” 노트북이 이 문제를 풀어 두었다.
// 책 3.26 + 관계 속성 — 후보와 '왜 그렇게 판단했는지'를 함께 반환한다
MATCH (phe:HpoPhenotype) WHERE phe.label IN $features
MATCH (dis:HpoDisease)-[rel:HAS_PHENOTYPIC_FEATURE]->(phe)
WITH dis, count(DISTINCT phe) AS matched,
collect({feature: phe.label, evidence: rel.evidenceName,
description: rel.evidenceDescription, source: rel.source,
url: rel.url, curator: rel.createdBy, date: rel.creationDate}) AS grounds
RETURN dis.id AS disease_id, dis.label AS disease, matched, grounds
ORDER BY matched DESC, disease
달라진 것은 한 군데뿐이다. 관계에 rel이라는 이름을 붙이고, rel.source·rel.url·rel.createdBy를 함께 담았다. 실행 결과는 이렇게 나온다.
OMIM:619269 Ondontochondrodysplasia 2 with hearing loss and diabetes — 특징 5개 일치
· Pruritus
증거 : Published clinical study
출처 : PMID:32101163 -> https://pubmed.ncbi.nlm.nih.gov/32101163
큐레이션: probinson (2021-06-20)
· Growth delay (이하 3건 동일 근거)
...
OMIM:618500 Holoprosencephaly 12 with or without pancreatic agenesis — 특징 3개 일치
· Growth delay
증거 : Published clinical study
출처 : PMID:31006513 -> https://pubmed.ncbi.nlm.nih.gov/31006513
큐레이션: probinson (2023-03-16)
· Sensorineural hearing impairment
증거 : Inferred from electronic annotation
출처 : OMIM:618500 -> https://omim.org/entry/618500
큐레이션: skoehler (2019-09-07)
여기서 두 후보의 성격이 갈린다. 1위 후보는 다섯 특징이 전부 같은 논문 하나(PMID:32101163)에서 왔다. 즉 이 증후군을 기술한 단일 임상 연구가 근거다. 2위 후보는 근거가 섞여 있다 — 하나는 Published clinical study(PMID:31006513 — 사람이 논문을 읽고 기록), 둘은 Inferred from electronic annotation(OMIM:618500 텍스트를 기계가 파싱). 같은 “3개 일치”라도 근거의 무게가 다르다.
겹침 수만 보면 이 차이가 보이지 않는다. §4.2의 “단순 겹침 수는 증거 질을 가중하지 않는다”는 경고가 추상이 아닌 이유가 이것이고, §2.2에서 LPG를 고른 결정이 최종적으로 값을 치르는 지점이기도 하다. RDF였다면 이 화면을 만들기 위해 n항 관계·명명 그래프·RDF-star 중 하나로 우회해야 했다.
6.3 이 장은 임상 의사결정 지원의 개념 증명이다
교재 사례는 HPO 데이터 위에서 질의와 추론이 어떻게 작동하는지 보여주는 교육용 예시다. 환자 EHR의 실제 수집·동의·권한·비식별화, 부정 표현형, 발현 시점, 환자군별 빈도, 임상 검증, 규제와 책임 배분은 다루지 않는다. 따라서 결과를 “진단”이라고 표현하기보다 검토 가능한 후보 생성으로 제한해야 한다.
7. 온톨로지·LPG·추론은 질문에 필요한 만큼만 쓴다
- 여러 원천의 의미 정렬과 계층 질의가 핵심이면 온톨로지 우선 접근의 가치가 크다.
- 관계별 provenance와 경로 탐색이 중심이면 LPG가 단순할 수 있지만 RDF 대안도 요구별로 비교한다.
- 작은 대표 질문으로 데이터·모델·추론·검토 비용을 함께 시험한다.
7.1 기술 이름이 아니라 지식 질문으로 선택한다
| 질문 | 도입 가치가 커지는 신호 | 다른 접근이 나은 신호 | 작은 실험 |
|---|---|---|---|
| 의미 정렬이 핵심인가? | 동의어·중의어·세분성 때문에 통합 오류가 큼 | 단일 시스템의 고정 코드만 사용 | 상위 100개 값의 표준 ID 매핑 감사 |
| 관계 경로가 답인가? | 여러 개체의 연결과 provenance가 판단 근거 | 단일 테이블 필터·집계로 충분 | 대표 질문 3개를 RDB와 그래프로 비교 |
| 계층 추론이 필요한가? | 상·하위 개념을 따라 누락을 줄여야 함 | 정확 일치만 요구하고 분류체계가 불안정 | 직접/추론 결과의 정밀 검토 표본 |
| 갱신을 책임질 수 있는가? | 릴리스·매핑·품질 소유자와 롤백이 있음 | latest 적재만 하고 결과 드리프트를 추적하지 않음 | 두 릴리스 diff와 회귀 질의 실행 |
| 결과를 검토할 수 있는가? | 경로·출처·증거를 사용자에게 보여줄 수 있음 | 질병명만 반환하고 검증 책임자가 없음 | 임상의 리뷰용 설명 화면 프로토타입 |
7.2 최소 파일럿은 질문–스냅샷–그래프–근거 화면이다
첫 파일럿은 거대한 의료 온톨로지 플랫폼이 아니다. 대표 임상 질문 3개와 기대 경로를 정의하고, 같은 날짜의 hp.owl과 phenotype.hpoa를 고정한다. 필요한 HPO 하위 범위만 적재하고, 주석 다중성을 보존하는 모델을 선택한다. 직접 질의와 계층 추론의 결과를 분리해 표시하고, 원 출처와 경로를 한 번에 열 수 있는 검토 화면을 만든다.
중단 기준도 함께 둔다. 표준 ID 매핑 오류가 허용 범위를 넘거나, 주석 건수 보존이 설명되지 않거나, 추론 후보가 임상의 검토 시간을 줄이지 못하거나, 릴리스 갱신 때 회귀 결과를 설명할 수 없다면 범위를 줄이거나 모델을 바꾼다.
7.3 토론 질문
| 질문 | 드러나는 설계 결정 | 좋은 답의 증거 |
|---|---|---|
| 우리 데이터의 “같은 개념”은 누가 승인하는가? | 매핑 소유권·변경 이력·충돌 정책 | 표본 감사와 되돌릴 수 있는 매핑 버전 |
| 관계 하나에 여러 근거가 있을 때 어떻게 보존할까? | 관계 속성 vs Annotation 노드 | 원천 주석 수와 그래프 assertion 수의 대응 |
| 추론으로 포함된 후보를 사용자에게 어떻게 설명할까? | 경로 표시·깊이·규칙·불확실성 UI | 원 주석과 전체 계층 경로를 한 번에 검토 |
핵심 용어
| 용어 | 이 리포트에서의 의미 |
|---|---|
| 온톨로지 | 도메인의 표준 개념·식별자·속성·범주·관계를 명시한 참조 어휘와 스키마. |
| HPO | 인간 표현형 이상을 표준화하고 계층화하며 질병 연관 주석을 제공하는 Human Phenotype Ontology. |
| 표현형 | 개인에게 관찰되는 물리적·생화학적 특징. 이 사례에서는 HPO 식별자로 코딩한다. |
| 주석 | 특정 질병–표현형 연관과 그 출처·증거·빈도·큐레이션 정보를 묶은 주장. |
| RDF | 주어–술어–목적어 트리플로 지식을 표현·교환하는 W3C 표준 데이터 모델. |
| LPG | 레이블이 있는 노드와 개별 관계에 키–값 속성을 둘 수 있는 그래프 모델. |
| Neosemantics(n10s) | Neo4j에서 RDF/OWL 어휘를 가져오고 일부 의미 질의를 지원하는 플러그인. |
| 추론 | 저장된 주석과 온톨로지 규칙·계층에서 명시되지 않은 포함 관계를 도출하는 과정. |
| provenance | 어떤 주장이 어느 출처·작성자·시점·처리 과정에서 왔는지를 추적하는 정보. |
참고 자료와 재구성 범위
- [1]Alessandro Negro et al., Knowledge Graphs and LLMs in Action, Chapter 3 “Create Your First Knowledge Graph from Ontologies,” Manning, pp. 37–64. 본문의 개념·코드·예제·수치는 이 장을 자체 해설로 재구성했다. 저장소의 한국어 해설판과 원문 마크다운에서 대조할 수 있다.
- [2]Human Phenotype Ontology 공식 포털과 hp.owl 배포 경로. 교재와 로컬 실습이 참조한 온톨로지 원천.
- [3]HPOA release data. 이 URL은 latest이므로 재현 가능한 운영에서는 고정 릴리스와 체크섬을 기록해야 한다.
- [4]저장소의 Chapter 3 원문·한국어 번역·해설판,
src/ch03/importer/import_hpo.py,study.toml리스팅 대응표를 원자료 감사에 사용했다. 업스트림 리스팅 원문은 저자 저장소 alenegro81/…/chapters/ch03에 있다. - [5]그림 1–8은 교재 도형을 복제하지 않고 Chapter 3의 논리 관계를 발표자가 새로 구성한 SVG다. 임상 결과 그림의 질병명과 공유 특징 수만 교재의 HPO 2025-02 결과 스냅샷을 따른다.
- [6]학습 노트북
03_chapter_guide.ipynb. 표 7·9·10과 §3.4·4.2·5.2·6.2의 모든 실측 수치·실행 결과·소요 시간은 이 노트북을 Neo4j 5.26.28 Community(플러그인n10s·apoc·gds)에서 HPOA 2026-06-23 스냅샷으로 끝까지 실행한 기록에서 가져왔다. 교재 값과 다른 숫자는 모두 이 재실행 결과다. - [7]본문의 Cypher·Turtle·TriG·SPARQL 코드는 교재 Listing 3.5–3.28을 옮긴 것이다. 실행 가능한 리스팅은 저장소
src/ch03/listings/의 원문을 따랐고(파일 번호는 교재 번호 +3), 원서 조판 과정에서 깨진 구두점만 바로잡았다. 주석 줄과 들여쓰기는 읽기 편하도록 발표자가 붙인 것이며 교재 원문에는 없다.