온톨로지로 첫
지식 그래프 만들기
임상 질문에서 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처럼 표현의 세분성도 다르다. 문자열을 정규화하는 것만으로는 이 차이를 안전하게 합칠 수 없다.
온톨로지는 표준 이름·식별자·동의어·속성·범주·관계를 정의한다. 각 원천의 로컬 필드를 온톨로지 개념에 매핑하면 서로 다른 원천이 같은 의미 좌표를 공유한다. 중요한 경계는 매핑도 하나의 주장이라는 점이다. 어떤 로컬 값이 어느 표준 개념과 같은지, 누가 언제 어떤 근거로 판단했는지를 추적해야 한다.
mapping 화살표를 거쳐 하나의 표준 개념으로 모인다 — 이 화살표 자체가 누가 언제 어떤 근거로 판단했는지 남겨야 할 주장이다. 교재 §3.1의 의미 통합 설명을 문제–매핑–통합 질의의 흐름으로 발표자가 재구성했다.
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에 기록했다는 정보까지 남긴다.” 그러나 네 코드가 RDF 의미론상 완전히 같은 트리플 집합을 만든다는 뜻은 아니다. 차이는 연관 기록을 무엇으로 취급하고 근거를 어디에 붙이느냐다.
아래 네 예시는 비교에 필요한 본문만 잘라 놓은 코드 조각이다. 실제 Turtle·TriG·SPARQL 문서에는 rdf:, xsd:, OMIM:, HP:, PMID:, 기본 접두사 :가 어느 전체 IRI를 줄인 것인지 선언해야 한다. 접두사는 휴대전화 연락처의 단축 이름과 비슷하다. 엔진은 단축 이름을 선언된 전체 주소로 펼쳐서 읽는다.
| 기호 | Turtle·TriG·SPARQL에서 | Cypher에서 |
|---|---|---|
: | rdf:type처럼 접두사와 로컬 이름을 나눈다. 앞이 비면 기본 접두사다. | :HpoDisease는 노드 레이블, :HAS_…는 관계 타입이다. |
? | SPARQL에서만 ?source처럼 엔진이 결과값을 담을 변수 바구니를 표시한다. | 이 예제에서는 쓰지 않는다. Cypher 변수는 d, r처럼 그냥 이름을 쓴다. |
{ } | TriG에서는 그래프의 내용, SPARQL에서는 찾아야 할 패턴의 범위를 감싼다. | {id: "…"}처럼 속성 이름과 값을 묶는 map이다. |
; · . | ;는 같은 주어를 이어 쓰고, .은 한 RDF 문장을 끝낸다. | 문장 끝의 ;는 Neo4j 실행 환경에 따라 선택적으로 쓴다. .은 n.id처럼 변수와 속성 이름을 나눈다. |
^^ | "2018-02-23"^^xsd:date를 단순 문자열이 아니라 날짜 값으로 해석하라고 지정한다. | 이 예제에서는 날짜를 문자열 속성으로 저장하므로 쓰지 않는다. |
① N항 관계 — 연관 기록을 ‘임시 포스트잇’ 노드로 만든다 (책 Listing 3.5 · 3.6)
비유부터 잡아 볼게요. 질병 카드와 표현형 카드 사이에 바로 메모를 쓰기 어려우니, 두 카드를 함께 가리키는 포스트잇 한 장을 붙이고 그 포스트잇에 출처·작성자·날짜를 적는 방식입니다. 여기서 “임시”는 이해를 돕는 비유일 뿐, 공백 노드가 메모리에서 곧 사라진다는 뜻은 아닙니다.
적는 법 · Turtle
_:Annotation rdf:type :PhenotypicAnnotation ;
:forDisease OMIM:222100 ;
:phenotypicFeature HP:0410050 ;
:source PMID:9357814 ;
:createdBy "Nicole Vasilevsky" ;
:creationDate "2018-02-23"^^xsd:date .
| 코드 조각 | 엔진에게 전달되는 뜻 |
|---|---|
_:Annotation | 밑줄+콜론 _:는 전역 IRI가 없는 공백 노드라는 표시이고, Annotation은 이 문서 안에서 같은 노드를 다시 가리키는 로컬 표찰이다. |
rdf:type :PhenotypicAnnotation | 이 포스트잇이 아무 노드가 아니라 “표현형 주석” 종류라고 선언한다. |
:forDisease · :phenotypicFeature | 포스트잇이 어느 질병과 어느 표현형을 함께 설명하는지 연결한다. |
:source · :createdBy | 주장의 출처와 작성자를 같은 포스트잇에 붙인다. 세미콜론 덕분에 주어를 반복하지 않는다. |
:creationDate … . | 날짜 자료형을 붙이고 마침표로 이 주어에 관한 서술을 끝낸다. |
꺼내는 법 · SPARQL
SELECT ?source ?author ?date
WHERE {
?annotation a :PhenotypicAnnotation ;
:forDisease OMIM:222100 ;
:phenotypicFeature HP:0410050 ;
:source ?source ;
:createdBy ?author ;
:creationDate ?date .
}
위에서 아래로 읽어 보세요. WHERE { … }는 “이 모양과 같은 그래프 조각을 찾아 주세요”라는 검색 틀입니다. ?annotation에 조건을 모두 만족하는 포스트잇 노드가 들어오면, 그 노드의 출처·작성자·날짜가 각각 ?source·?author·?date에 채워지고 SELECT가 그 세 열만 결과로 내보냅니다. a는 작성 코드의 rdf:type과 같은 뜻인 짧은 표기입니다.
Annotation은 웹 전체에서 통하는 이름이 아니라 현재 문서 안에서 줄들을 한 노드로 묶기 위한 손잡이입니다. _:memo1로 바꾸어도 모든 등장 위치를 함께 바꾸면 의미는 같습니다. 다른 문서의 _:Annotation과 같은 노드라는 보장은 없습니다.
② 명명 그래프 — 문장을 봉투에 넣고 봉투에 발신 정보를 적는다 (책 Listing 3.7 · 3.8)
명명 그래프는 RDF 데이터셋 안의 트리플 묶음에 그래프 이름을 연결합니다. 편지 한 장을 봉투에 넣고, 봉투 겉면에 출처와 작성일을 적는 모습을 떠올리면 됩니다. 봉투 안 여러 문장은 같은 맥락을 공유할 수 있습니다.
적는 법 · TriG
:Graph1 {
OMIM:222100 :hasPhenotypicFeature HP:0410050 .
}
:Graph1
:source PMID:9357814 ;
:createdBy "Nicole Vasilevsky" ;
:creationDate "2018-02-23"^^xsd:date .
| 코드 조각 | 엔진에게 전달되는 뜻 |
|---|---|
:Graph1 { | 이름이 :Graph1인 그래프의 내용이 시작된다. :Graph1은 기본 접두사를 펼친 IRI이지 변수가 아니다. |
OMIM:… :has… HP:… . | 질병–표현형 트리플을 :Graph1 안에 단언한다. |
} | 명명 그래프의 내용이 끝난다. |
:Graph1 :source … | 중괄호 밖이므로 이 메타데이터 트리플은 기본 그래프에 기록된다. 내용물이 아니라 봉투 이름을 주어로 삼는다. |
꺼내는 법 · SPARQL
SELECT ?source ?author ?date
WHERE {
GRAPH :Graph1 {
OMIM:222100 :hasPhenotypicFeature HP:0410050 .
}
:Graph1 :source ?source ;
:createdBy ?author ;
:creationDate ?date .
}
GRAPH :Graph1 { … }은 검색 대상을 그 봉투 안으로 제한합니다. 이어지는 :Graph1 :source … 패턴은 기본 그래프에서 봉투에 붙은 메타데이터를 읽습니다. SPARQL의 SELECT·WHERE·GRAPH 같은 키워드는 대소문자를 바꿔도 되지만, 읽기 쉽게 대문자로 쓰는 관례가 있습니다. 반면 ?source와 ?Source, 접두사로 펼쳐지는 IRI는 같은 것으로 바꾸어 읽으면 안 됩니다.
그렇지 않습니다. 출처 파일·수집 시점·기관 단위로 여러 트리플을 한 그래프에 묶을 수도 있습니다. 다만 각 질병–표현형 연관의 근거를 서로 완전히 분리하려고 “연관 한 건당 그래프 하나”를 택하면 그래프 수도 주석 수만큼 늘어납니다. 이것은 명명 그래프의 필수 규칙이 아니라 provenance의 세밀도를 정하는 모델링 선택입니다.
③ RDF-star — 문장을 인용해 그 문장에 관해 말한다 (책 Listing 3.9 · 3.10)
이중 꺾쇠는 대화에서 큰따옴표로 남의 문장을 인용하는 것과 비슷합니다. “질병이 이 표현형을 가진다”라는 문장을 하나의 대상으로 집어 들고, 그 인용된 문장에 출처·작성자·날짜를 연결합니다.
적는 법 · 교재 시점의 Turtle-star
<<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 .
}
SELECT 뒤에 곧바로 { … }가 와도 오류가 아닙니다. SPARQL에서 WHERE는 생략할 수 있기 때문입니다. 안쪽의 << … >>는 세 토큰을 단순 문자열로 묶는 것이 아니라 트리플 구조를 유지한 채 인용합니다. 그래서 조회할 때도 같은 구조를 써서 그 문장에 연결된 메타데이터를 찾습니다.
트리플을 인용하고 그 인용에 대해 말하는 것과, 원래 질병–표현형 트리플을 그래프의 사실로 단언하는 것은 구분해야 합니다. “누군가 이렇게 말했다”와 “그 말이 사실이다”가 다른 것과 같습니다. 연결 자체도 사실로 넣으려면 원 트리플을 별도로 단언하거나, 단언과 주석을 함께 만드는 문법을 사용해야 합니다.
교재는 RDF-star Community Group 계열의 << … >> 설명을 따른다. 2026-07-31 열람 기준 W3C RDF 1.2 Concepts는 Candidate Recommendation Snapshot(2026-04-07), Turtle 1.2는 Working Draft(2026-07-30), SPARQL 1.2 Query는 Working Draft(2026-06-25)다. Turtle 1.2는 triple term <<( … )>>, reified triple 표기 << … >>(식별자를 생략하면 새 공백 노드가 reifier가 된다), 단언과 주석을 함께 쓰는 {| … |} 문법을 구분한다. 따라서 실제 제품에서는 교재 구문을 그대로 채택하기 전에 사용하는 엔진의 RDF 1.2 지원 범위와 의미를 확인해야 한다.[8]
# Turtle 1.2 초안의 annotation 문법: 원 트리플을 단언하면서 근거도 함께 붙인다
VERSION "1.2"
OMIM:222100 :hasPhenotypicFeature HP:0410050
{| :source PMID:9357814 ;
:createdBy "Nicole Vasilevsky" ;
:creationDate "2018-02-23"^^xsd:date |} .
④ LPG — 관계 자체에 키–값 속성을 둔다 (책 Listing 3.11 · 3.12)
LPG에서는 관계가 데이터베이스 안의 개별 요소이므로, 화살표 모양의 관계 안에 속성을 바로 넣을 수 있습니다. 다만 교재 Listing 3.11은 CREATE나 MERGE가 없는 모델 모양이라 그 조각만 실행할 수는 없습니다.
모델 모양 · Cypher 패턴
(d {id: "OMIM:222100"})
-[:HAS_PHENOTYPIC_FEATURE {
source: "PMID:9357814",
createdBy: "Nicole Vasilevsky",
creationDate: "2018-02-23"
}]->
(p {id: "HP:0410050"})
실제로 적는 법 · 기존 두 노드를 연결하는 예
MATCH (d:HpoDisease {id: "OMIM:222100"})
MATCH (p:HpoPhenotype {id: "HP:0410050"})
CREATE (d)-[:HAS_PHENOTYPIC_FEATURE {
source: "PMID:9357814",
createdBy: "Nicole Vasilevsky",
creationDate: "2018-02-23"
}]->(p)
MATCH 두 줄은 이미 존재하는 질병 노드와 표현형 노드를 각각 d와 p라는 변수에 담습니다. 소괄호 ( )는 노드, 대괄호 [ ]는 관계, ->는 저장 방향입니다. CREATE는 관계를 새로 만들기 때문에 같은 코드를 반복하면 같은 내용의 관계가 더 생길 수 있습니다. 반복 실행 안전성이 필요하면 주석 식별자와 중복 정책을 먼저 설계해야 합니다.
꺼내는 법 · Cypher
MATCH (d)-[r:HAS_PHENOTYPIC_FEATURE]->(p)
WHERE d.id = "OMIM:222100" AND p.id = "HP:0410050"
RETURN r.source, r.createdBy, r.creationDate
| 줄 | 엔진에게 전달되는 뜻 |
|---|---|
MATCH (d)-[r:…]->(p) | 지정한 타입의 관계를 찾고 양쪽 노드를 d·p, 관계를 r에 담는다. |
WHERE … AND … | 두 ID 조건을 동시에 만족하는 연결만 남긴다. |
RETURN r.source, … | 관계 변수 r의 세 속성을 결과 열로 반환한다. |
MATCH·WHERE·RETURN 같은 키워드는 대소문자를 바꿔 쓸 수 있지만 대문자 표기가 읽기 좋은 관례입니다. 반면 변수 d와 D, 레이블 :HpoDisease와 :HPODISEASE, 속성 createdBy와 createdby는 서로 다른 이름입니다.
쉽게 말하면 — LPG는 관계 메타데이터를 조회하는 코드 경로를 짧게 만든다. 하지만 “같은 질병–표현형 사이의 여러 주석을 관계 몇 개로 볼 것인가”라는 식별·중복 설계까지 없애 주지는 않는다. §3.2에서 볼 수 있듯 관계를 질병–표현형 쌍만으로 MERGE하면 서로 다른 근거가 한 관계에 덮일 수 있다. 이 장이 LPG를 고른 이유는 문법의 절대 우월성이 아니라, 선택한 주석 단위를 보존한다는 전제에서 관계 속성을 바로 탐색할 수 있기 때문이다.
? date 띄어쓰기)은 바로잡았다. 접두사 선언이 생략된 RDF 코드는 독립 실행문이 아닌 교재 조각이며, LPG에는 교재의 패턴 조각과 실제 MATCH·CREATE 예시를 구분해 제시했다. RDF 1.2 주의는 W3C 2026년 문서를 기준으로 보완했다.
| 관점 | RDF 계열 | LPG | 이 사례의 판단 |
|---|---|---|---|
| 기본 표현 | 전역 식별 가능한 트리플 | 레이블 노드·고유 관계·속성 | HPO 원천은 RDF/OWL, 앱 모델은 LPG |
| 관계 메타데이터 | n항 관계·명명 그래프·RDF-star/RDF 1.2 reification 등 | 개별 관계의 키–값 | 접근을 단순화하되 주석 식별 단위는 별도 설계 |
| 주요 강점 | 표준 교환·연합·의미 표현·추론 | 경로 탐색·관계 속성·운영 질의 | 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를 사용한 교재 실행값이다. 현재 릴리스의 정답 건수가 아니라 스냅샷 재현 기준선으로 사용한다.
전반부 코드 — 온톨로지가 들어오기까지 네 줄
표 9의 앞 세 단계는 코드로 보면 놀랄 만큼 짧다. 무거운 일은 전부 Neosemantics가 대신하고, 우리가 하는 일은 규칙을 미리 정해 두는 것뿐이다.
(n)— 소괄호는 노드이고,n은 그 노드를 뒤에서 다시 부르기 위한 변수입니다.(n:HpoPhenotype)— 콜론 뒤의HpoPhenotype은 노드의 역할을 나타내는 레이블입니다.{id: "HP:…"}— 중괄호는 속성 이름과 값을 묶은 map입니다.-[r:TYPE]->— 대괄호는 관계,r은 관계 변수,TYPE은 관계 타입, 화살촉은 저장 방향입니다.n.id— 점은 변수n에 들어 있는 요소의id속성을 읽는다는 뜻입니다.
// ① 그릇 만들기 (책 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");
CREATE DATABASE는 데이터를 담을 공간을 준비합니다.IF NOT EXISTS는 이미 있으면 오류 대신 그대로 두라는 안전장치입니다.CONSTRAINT는 중복을 거부하는 규칙이고,INDEX는 ID로 빨리 찾기 위한 색인입니다. 둘은 목적이 다릅니다.n10s.graphconfig는 RDF 이름을 Neo4j 이름으로 어떻게 번역할지 먼저 고정합니다. 번역 규칙을 적재 뒤에 바꾸면 같은 원문이 다른 그래프 모양이 될 수 있습니다.n10s.rdf.import.fetch는 URL에서 RDF/XML을 받아 앞서 정한 규칙으로 노드·관계를 만듭니다. 문자열"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)
LOAD CSV … AS row는 파일을 한 행씩 읽어 현재 행을row라는 리스트에 담습니다.FIELDTERMINATOR '\t'는 쉼표가 아니라 탭으로 열을 나누라는 뜻입니다. 이름은 CSV지만 TSV도 이 설정으로 읽습니다.WITH row SKIP 5는 다음 절로row만 넘기면서 메타데이터 4행과 헤더 1행을 버립니다.row[0]은 첫 번째 열,row[3]은 네 번째 열입니다. 리스트 번호가 0부터 시작하기 때문입니다.MERGE는 패턴을 먼저 찾고 없을 때만 만듭니다.ON CREATE SET은 이번에 새로 만든 경우에만 이름을 채워 기존 값을 함부로 덮지 않습니다.
여기가 두 파일이 만나는 지점이다. 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로 덮어쓰지 못하게 막는 것이다. 여덟 번 반복되는 건 열이 여덟 개이기 때문이지 다른 뜻은 없다.
CASE WHEN … THEN [1] ELSE [] END는 값이 있으면 한 칸짜리 리스트, 없으면 빈 리스트를 만듭니다. FOREACH(_ IN … | SET …)의 밑줄 _은 리스트의 실제 값이 필요 없다는 관례적 변수 이름이고, 세로줄 | 뒤가 각 원소에 대해 실행할 갱신입니다. 즉 리스트 길이를 1 또는 0으로 바꾸어 실행 횟수를 제어합니다.
// ④ 사람이 읽을 수 있게 바꾸기 (책 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를 별도 노드로 승격하거나, 관계의 고유 키를 모델에 포함하고 행 수 보존 테스트를 두어야 한다.
MERGE한 한 관계에 마지막 값이 덮일 수 있다. 이 구현 위험은 발표자의 코드 감사 결과이며, 도형은 교재 Listing 3.19–3.24와 저장소 import_hpo.py 감사를 바탕으로 재구성했다.
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
MATCH path=…는 질병 노드에서 표현형 노드로 가는 전체 패턴을 찾고, 노드와 관계를 포함한 경로를path변수에 담습니다.WHERE dis.id = …는 찾은 모든 질병 중 제1형 당뇨병 ID 하나만 남깁니다.RETURN path는 이름 목록이 아니라 경로 자체를 반환하므로 Neo4j Browser가 중심 질병과 주변 표현형을 그래프로 그릴 수 있습니다.
노트북 재실행 결과 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
- 첫
MATCH와WHERE … IN [ … ]은 관찰한 다섯 이름과 같은 표현형만 고릅니다.IN은 “이 리스트 안에 포함되는가?”를 검사합니다. WITH phe는 선택된 표현형을 다음 질의 절로 넘깁니다. SQL의 서브쿼리 경계와 비슷하지만, Cypher에서는 행이 절에서 절로 흐르는 파이프 역할을 합니다.- 두 번째
MATCH는 각 표현형으로 들어오는 질병 관계를 찾아 질병–표현형 일치 한 건마다 행을 만듭니다. dis는 리스트가 아니라 질병 노드 하나인데도, 교재는UNWIND dis AS nodes로 그 노드를 한 번 흘려보낸 뒤count(nodes)로 일치 행 수를 셉니다. 학습 노트북에서count(nodes)와count(phe)의 상위 5행이 같음을 확인했으므로, 이 예제에서는count(phe)가 의도를 더 직접 보여 줍니다.collect(phe.label)은 같은 질병에 속한 표현형 이름을 리스트 하나로 모으고,count(nodes)는 그 질병이 몇 특징과 일치했는지 셉니다.AS는 결과 열에 사람이 읽을 별칭을 붙입니다.ORDER BY … DESC는 일치 수가 큰 순서,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이 말한 드리프트의 실물이다. 슬라이드에 표 12만 띄우고 “이 목록이 나옵니다”라고 말하면, 청중이 노트북을 돌렸을 때 다른 화면을 보고 코드가 틀렸다고 생각하게 된다. 결과를 보여줄 때는 반드시 어느 릴리스에서 나온 값인지 함께 적어야 한다.
단순 겹침 수 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로 이 계층을 따라 질병을 포함한다. 반환된 질병–표현형 주석은 원래 그래프에 명시적으로 존재하지만, 상위 범주와 질병의 포함 관계는 계층 규칙에서 도출된다.
책 Listing 3.28은 재현 가능한 출력을 위해 WHERE dis.label IN [ … ]으로 질병 다섯 개를 직접 지정하고, 표 3.3이 그 결과다. 저장소 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
WHERE dis.label IN [ … 질병 5개 … ] // 재현용 화이트리스트 (지면 생략)
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는 이 순회를 대신 해 주면서 깊이 제한 없이 훑고, 찾은 표현형에 주석된 질병까지 한 번에 돌려준다.
CALL n10s.inference.nodesInCategory(cat, { … })— 현재 노드cat과 설정 map을 외부 프로시저에 전달합니다.inCatRel은 “구성원과 범주를 어떤 관계로 연결했는가”,subCatRel은 “하위 범주와 상위 범주를 어떤 관계로 연결했는가”를 알려 줍니다.YIELD node AS dis— 프로시저가 내놓은node열을 이후 질의에서dis라는 질병 변수로 사용합니다.collect(DISTINCT phe.label)— 같은 표현형 이름이 여러 경로에서 나와도 한 번만 모읍니다.size(features)— 모아 둔 표현형 리스트의 길이를 계산해 정렬에 사용합니다.
SUBCLASSOF이며, 세 예시는 HPO Ontology Service 2026-06-23 기준 HP:0000818의 2–3단계 하위임을 확인했다. 추론은 새 임상 사실을 만들지 않고 기존 주석의 질의 범위를 넓힌다. 교재 §3.5, Listing 3.27–3.28과 표 3.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
$features— 질의 문자열 안에 환자 특징을 직접 붙이지 않고 애플리케이션이 별도로 전달하는 parameter입니다.[rel:HAS_…]— 관계에rel이라는 변수를 붙였기 때문에 출처·URL·작성자 속성을 읽을 수 있습니다.count(DISTINCT phe)— 같은 표현형이 중복 경로로 잡혀도 한 번만 세어 일치 수가 부풀지 않게 합니다.{feature: …, source: …}— 근거 한 건을 key–value map으로 만들고,collect(…)가 그 map들을grounds리스트로 모읍니다.WITH— 질병별 일치 수와 근거 목록을 먼저 계산한 뒤, 다음RETURN에 필요한 값만 넘깁니다.
핵심 변화는 관계에 rel이라는 이름을 붙이고, 그 관계의 source·url·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/RDF 1.2 reification 같은 모델로 같은 근거 화면을 만들 수 있지만, 이 사례의 LPG에서는 관계 변수 rel에서 메타데이터에 곧바로 접근하는 질의 경로가 짧다.
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. 표 11·13·14와 §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의 비교 의도와 실행 흐름을 옮긴 것이다. 실행 가능한 Cypher 리스팅은 저장소
src/ch03/listings/의 원문을 따랐고(파일 번호는 교재 번호 +3), 원서 조판 과정에서 깨진 구두점은 바로잡았다. Listing 3.5–3.12의 RDF 계열 코드는 접두사 선언이 생략된 교재 조각이고, Listing 3.11은 실행 절이 없는 패턴 표현이므로 §2.2에서 독립 실행문과 구분했다. 주석 줄과 들여쓰기는 읽기 편하도록 발표자가 붙인 것이며 교재 원문에는 없다. - [8]W3C의 RDF 1.2 Concepts and Abstract Data Model, RDF 1.2 Turtle, RDF 1.2 TriG, SPARQL 1.2 Query Language. 2026-07-31에 열람했으며, §2.2의 공백 노드·명명 그래프·triple term·reified triple·reifier·annotation 문법과 당시 문서 성숙도를 확인한 1차 자료다.