ds4th studyKnowledge Graphs & LLMs
Study Report · Chapter 03

온톨로지로 첫
지식 그래프 만들기

임상 질문에서 HPO 그래프·질의·추론까지

발표자
태영
범위
Chapter 3 · pp. 37–64
발행
2026-08-01
Section 00 · Executive Summary

0. 핵심 요약

Chapter 03 · One Sentence 쓸모 있는 지식 그래프는 데이터부터 시작하지 않는다. 사용자의 질문을 먼저 고정하고, 온톨로지로 의미를 정렬하고, 관계에 근거를 보존한 뒤, 직접 연결과 계층 추론을 구분해 검증한다.

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의 주제다.

표 1Chapter 3의 전체 논리
단계핵심 질문장의 답검증 산출물
01 · 문제누가 어떤 판단을 하는가?임상의가 관찰한 표현형으로 희귀질환 후보와 근거를 찾는다.대표 질문·실패 비용·사용자 역할
02 · 의미서로 다른 표현을 어떻게 맞추는가?HPO를 참조 어휘와 계층 스키마로 사용한다.ID 매핑·동의어·계층 방향
03 · 모델관계별 근거를 어디에 두는가?OWL 계층을 LPG로 가져오고 주석 메타데이터를 관계에 둔다.RDF/LPG 선택 근거·그래프 계약
04 · 사용무엇을 질의하고 추론하는가?직접 연관을 세고, 계층을 따라 암시적 포함을 확장한다.재현 질의·근거 경로·결과 스냅샷
05 · 통제어디까지 믿을 수 있는가?버전·적재·추론 범위와 임상 판단의 경계를 분리한다.품질 게이트·사람의 최종 검토
Section 01 · Problem & Domain

1. 그래프보다 먼저 임상 질문을 고정한다

섹션 요약
  • 의미 이질성 — 같은 개념의 여러 이름, 같은 약어의 여러 뜻, 서로 다른 세분성이 통합을 어렵게 한다.
  • 사용자 요구 — 임상의는 표현형 조합에서 희귀질환 후보와 그 연결 근거를 보고 싶다.
  • 온톨로지 역할 — 원천을 대신하는 데이터베이스가 아니라 로컬 표현을 공통 개념에 매핑하는 참조 계층이다.

1.1 형식보다 어려운 것은 ‘같은 뜻인가’이다

XML, CSV, JSON 같은 형식 차이는 파서로 처리할 수 있다. 더 어려운 문제는 의미다. 제2형 당뇨병과 ketosis-resistant diabetes처럼 같은 개념이 다른 이름으로 나타나고, PE처럼 physical examination과 pulmonary embolism이라는 서로 다른 뜻을 가질 수 있다. necrosis와 lobular necrosis처럼 표현의 세분성도 다르다. 문자열을 정규화하는 것만으로는 이 차이를 안전하게 합칠 수 없다.

온톨로지는 표준 이름·식별자·동의어·속성·범주·관계를 정의한다. 각 원천의 로컬 필드를 온톨로지 개념에 매핑하면 서로 다른 원천이 같은 의미 좌표를 공유한다. 중요한 경계는 매핑도 하나의 주장이라는 점이다. 어떤 로컬 값이 어느 표준 개념과 같은지, 누가 언제 어떤 근거로 판단했는지를 추적해야 한다.

그림 1의미 이질성을 공통 질의 공간으로 바꾸는 온톨로지
동의어, 중의적 약어, 서로 다른 세분성이 매핑을 거쳐 HPO의 표준 식별자와 계층에 정렬되고 통합 지식 그래프 질의로 이어지는 구조 왼쪽 세 상자는 서로 다른 종류의 의미 불일치이고, 셋 다 같은 mapping 화살표를 거쳐 하나의 표준 개념으로 모인다 — 이 화살표 자체가 누가 언제 어떤 근거로 판단했는지 남겨야 할 주장이다. 교재 §3.1의 의미 통합 설명을 문제–매핑–통합 질의의 흐름으로 발표자가 재구성했다.

1.2 목표는 자동 진단이 아니라 검토 가능한 후보 탐색이다

이 장의 대상 사용자는 질병을 진단하고 치료하는 임상의다. 특히 희귀질환은 한두 증상으로 식별하기 어렵고, 여러 표현형 특징의 조합과 문헌 근거를 함께 살펴야 한다. 따라서 지식 기반에는 두 가지가 필요하다. 첫째, 같은 기관·계통에 속하는 표현형 이상을 연결하는 맥락적 계층. 둘째, 질병과 표현형의 연관뿐 아니라 출처·증거·빈도·작성자를 추적할 수 있는 주석이다.

도메인에는 경계 사례도 있다. 제1형 당뇨병은 하나의 질병으로 식별될 수 있고, 다른 희귀 증후군에서 관찰되는 표현형 특징으로도 식별될 수 있다. 한 문자열에 역할을 억지로 합치지 않고 OMIM:222100과 HP:0100651처럼 서로 다른 표준 ID를 사용하면, 질문의 문맥에 따라 질병 노드와 표현형 노드를 구분할 수 있다.

표 2희귀질환 지원 KG의 도메인 계약
항목이 장의 정의그래프 표현오해하면 생기는 문제
표현형 특징개인에게 나타나는 관찰 가능한 물리·생화학적 특성HpoPhenotype 노드와 계층질병명과 섞이면 역할별 질의가 흐려짐
질병원인·시간 경과·표현형 묶음·치료 반응으로 특징지어지는 실체HpoDisease 노드표현형 하나를 곧바로 질병으로 확정
주석특정 질병과 표현형이 연결된다는 근거 있는 주장HAS_PHENOTYPIC_FEATURE 관계출처가 노드 속성에 섞여 어느 연결의 근거인지 잃음
임상 결과관찰 특징과 연결된 검토 대상 질병 목록경로·겹침 수·근거 속성후보 목록을 진단 확률이나 확정 진단으로 해석
설계 질문

그래프 모델을 그리기 전에 “임상의가 결과 화면에서 어떤 연결과 근거를 확인해야 하는가?”를 묻는다. 이 질문이 노드·관계·속성과 기술 선택을 결정한다.

Section 02 · Data & Technology

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 증거 코드, 발현 시점, 빈도, 성별, 수정자, 큐레이터와 날짜를 함께 담는다. 이 정보는 질병이나 표현형 자체의 속성이 아니라 둘 사이 특정 연결의 속성이다.

그림 2정의·계층과 연관·근거를 한 그래프에 결합하기
hp.owl의 표현형 정의와 계층, phenotype.hpoa의 질병 표현형 연관과 근거가 표준 ID로 결합되어 질병 노드, 표현형 노드, 속성 있는 관계를 만드는 구조 정의·계층은 표현형 노드에, 한 건의 연관을 뒷받침하는 근거는 질병–표현형 관계에 둔다. 교재 §3.1.2, 그림 3.5–3.6과 Listing 3.1–3.4의 의미를 원본 도형 복제 없이 새로 배치했다.
표 3HPO 두 원천의 입력 계약
관점hp.owlphenotype.hpoa결합 시 확인
형식RDF/XML · OWLTSV · 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보다 낫다”가 아니라 표준 온톨로지를 입력으로 사용하면서 애플리케이션 질의는 관계 중심으로 최적화한 하이브리드 결정이다.

그림 3같은 주석 메타데이터를 표현하는 네 가지 패턴
RDF n항 관계, RDF 명명 그래프, RDF-star, LPG 관계 속성이 질병 표현형 연결의 출처 작성자 날짜를 각기 다른 구조로 표현하는 비교 n항 관계는 Annotation 자원에서 질병·표현형으로 나가는 관계를, 명명 그래프는 문장 묶음의 맥락을, RDF-star는 인용된 트리플의 메타데이터를, LPG는 개별 관계의 키–값을 보여준다. 교재 §3.2.2, Listing 3.5–3.12의 구조를 관계 메타데이터라는 동일 질문으로 재구성했다.

같은 사용자 의도를 네 가지 데이터 모델로 표현해 보기

네 방식의 사용자 수준 의도는 같다. “질병 OMIM:222100과 표현형 HP:0410050의 연관을 기록하고, 그 근거가 논문 PMID:9357814이며 Nicole Vasilevsky가 2018-02-23에 기록했다는 정보까지 남긴다.” 그러나 네 코드가 RDF 의미론상 완전히 같은 트리플 집합을 만든다는 뜻은 아니다. 차이는 연관 기록을 무엇으로 취급하고 근거를 어디에 붙이느냐다.

먼저 알아둘 전제

아래 네 예시는 비교에 필요한 본문만 잘라 놓은 코드 조각이다. 실제 Turtle·TriG·SPARQL 문서에는 rdf:, xsd:, OMIM:, HP:, PMID:, 기본 접두사 :가 어느 전체 IRI를 줄인 것인지 선언해야 한다. 접두사는 휴대전화 연락처의 단축 이름과 비슷하다. 엔진은 단축 이름을 선언된 전체 주소로 펼쳐서 읽는다.

표 4같은 기호도 언어에 따라 뜻이 달라진다
기호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 .
표 5공백 노드 한 개에 연관의 다섯 역할을 연결한다
코드 조각엔진에게 전달되는 뜻
_: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이라고 쓰나요?

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 .
표 6중괄호 안의 그래프와 밖의 기본 그래프를 구분한다
코드 조각엔진에게 전달되는 뜻
: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는 같은 것으로 바꾸어 읽으면 안 됩니다.

초보자 질문 · 주석 28만 건이면 봉투도 반드시 28만 개인가요?

그렇지 않습니다. 출처 파일·수집 시점·기관 단위로 여러 트리플을 한 그래프에 묶을 수도 있습니다. 다만 각 질병–표현형 연관의 근거를 서로 완전히 분리하려고 “연관 한 건당 그래프 하나”를 택하면 그래프 수도 주석 수만큼 늘어납니다. 이것은 명명 그래프의 필수 규칙이 아니라 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는 생략할 수 있기 때문입니다. 안쪽의 << … >>는 세 토큰을 단순 문자열로 묶는 것이 아니라 트리플 구조를 유지한 채 인용합니다. 그래서 조회할 때도 같은 구조를 써서 그 문장에 연결된 메타데이터를 찾습니다.

중요 · 인용했다고 그 내용이 자동으로 사실이 되지는 않습니다

트리플을 인용하고 그 인용에 대해 말하는 것과, 원래 질병–표현형 트리플을 그래프의 사실로 단언하는 것은 구분해야 합니다. “누군가 이렇게 말했다”와 “그 말이 사실이다”가 다른 것과 같습니다. 연결 자체도 사실로 넣으려면 원 트리플을 별도로 단언하거나, 단언과 주석을 함께 만드는 문법을 사용해야 합니다.

2026년 7월 사양 주의

교재는 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
표 7관계를 변수에 담아 속성을 직접 읽는다
줄엔진에게 전달되는 뜻
MATCH (d)-[r:…]->(p)지정한 타입의 관계를 찾고 양쪽 노드를 d·p, 관계를 r에 담는다.
WHERE … AND …두 ID 조건을 동시에 만족하는 연결만 남긴다.
RETURN r.source, …관계 변수 r의 세 속성을 결과 열로 반환한다.
초보자 질문 · match를 소문자로 써도 되나요?

MATCH·WHERE·RETURN 같은 키워드는 대소문자를 바꿔 쓸 수 있지만 대문자 표기가 읽기 좋은 관례입니다. 반면 변수 d와 D, 레이블 :HpoDisease와 :HPODISEASE, 속성 createdBy와 createdby는 서로 다른 이름입니다.

쉽게 말하면 — LPG는 관계 메타데이터를 조회하는 코드 경로를 짧게 만든다. 하지만 “같은 질병–표현형 사이의 여러 주석을 관계 몇 개로 볼 것인가”라는 식별·중복 설계까지 없애 주지는 않는다. §3.2에서 볼 수 있듯 관계를 질병–표현형 쌍만으로 MERGE하면 서로 다른 근거가 한 관계에 덮일 수 있다. 이 장이 LPG를 고른 이유는 문법의 절대 우월성이 아니라, 선택한 주석 단위를 보존한다는 전제에서 관계 속성을 바로 탐색할 수 있기 때문이다.

교재 Listing 3.5–3.12의 비교 의도를 보존하면서 작성·조회 코드를 분리했다. 원서 조판 과정에서 깨진 닫는 괄호·구두점(예: Listing 3.11의 미종료 문자열, Listing 3.10의 ? date 띄어쓰기)은 바로잡았다. 접두사 선언이 생략된 RDF 코드는 독립 실행문이 아닌 교재 조각이며, LPG에는 교재의 패턴 조각과 실제 MATCH·CREATE 예시를 구분해 제시했다. RDF 1.2 주의는 W3C 2026년 문서를 기준으로 보완했다.
표 8RDF와 LPG의 선택 기준
관점RDF 계열LPG이 사례의 판단
기본 표현전역 식별 가능한 트리플레이블 노드·고유 관계·속성HPO 원천은 RDF/OWL, 앱 모델은 LPG
관계 메타데이터n항 관계·명명 그래프·RDF-star/RDF 1.2 reification 등개별 관계의 키–값접근을 단순화하되 주석 식별 단위는 별도 설계
주요 강점표준 교환·연합·의미 표현·추론경로 탐색·관계 속성·운영 질의Neosemantics로 두 강점을 일부 결합
선택 위험주석 모델과 엔진 지원이 복잡할 수 있음OWL 의미를 전부 자동 보존하지 않음필요한 계층·규칙만 명시적으로 검증
Section 03 · Build & Operate

3. 적재는 파일 복사가 아니라 의미 계약의 구현이다

섹션 요약
  • 제약과 인덱스를 먼저 만들고, Neosemantics 설정 뒤 HPO 온톨로지를 적재한다.
  • 표현형 ID를 정렬한 다음 질병 노드와 관계를 만들고, 주석 필드를 관계 속성으로 풍부화한다.
  • 적재 성공 로그만으로 끝내지 않고 원천 행·노드·관계·근거 속성의 보존성을 확인한다.
  • 노트북 재실행 실측으로 책 값과의 차이(트리플 926,450 대 899,558, 3.22 단계 약 8분)를 확인한다.

3.1 CRISP-DM을 일곱 개의 산출물로 바꾼다

이 장은 Chapter 2의 CRISP-DM 변형을 구체적인 KG 구축 절차로 바꾼다. 비즈니스 이해에서는 대표 임상 질문을, 데이터 이해에서는 두 HPO 원천의 의미와 식별자를, 설계에서는 RDF/LPG 선택 근거를 남긴다. 구현은 온톨로지 적재와 주석 적재로 나뉘며, 정제 뒤 직접 질의와 계층 추론으로 사용 가치를 확인한다.

그림 4임상 질문에서 검증된 그래프 질의까지
임상 질문, HPO 두 원천 이해, RDF LPG 선택, 온톨로지 적재, 주석 적재, 정제와 점검, 직접 질의와 계층 추론이 검증 게이트를 거쳐 이어지는 파이프라인 각 단계가 남겨야 할 산출물은 대표 질문, 원천 계약, 모델 선택 근거, 적재 로그, 품질 점검과 재현 가능한 질의다. 교재 그림 3.1–3.2, 3.7, 3.9–3.10의 전체 흐름을 산출물과 검증 지점 중심으로 통합 재구성했다.
표 9설계 이후 여섯 구축 단계의 입력·출력·확인 질문
단계주요 작업출력품질 확인
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가 대신하고, 우리가 하는 일은 규칙을 미리 정해 두는 것뿐이다.

Cypher 그림 문법을 먼저 읽어 볼게요
  • (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");
네 단계가 필요한 이유
  1. CREATE DATABASE는 데이터를 담을 공간을 준비합니다. IF NOT EXISTS는 이미 있으면 오류 대신 그대로 두라는 안전장치입니다.
  2. CONSTRAINT는 중복을 거부하는 규칙이고, INDEX는 ID로 빨리 찾기 위한 색인입니다. 둘은 목적이 다릅니다.
  3. n10s.graphconfig는 RDF 이름을 Neo4j 이름으로 어떻게 번역할지 먼저 고정합니다. 번역 규칙을 적재 뒤에 바꾸면 같은 원문이 다른 그래프 모양이 될 수 있습니다.
  4. 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)
TSV 한 행이 Cypher 변수로 바뀌는 과정
  • 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를 별도 노드로 승격하거나, 관계의 고유 키를 모델에 포함하고 행 수 보존 테스트를 두어야 한다.

그림 5HPOA 한 행이 그래프 주장이 되는 생애주기
HPOA 행을 파싱하고 표준 ID로 노드를 매칭해 관계를 만든 뒤 증거 설명과 URL로 풍부화하고 건수와 근거 보존을 확인하는 단계 같은 질병–표현형 쌍의 주석이 여러 건이면 관계 식별자 없이 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]가 정본이다.

표 10원자료·코드 감사에서 확인한 재현 경계
경계관찰영향권장 통제
데이터 버전책은 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 기준이라는 점을 모르면 첫 줄에서 막힌다.

표 11책 실행값과 노트북 재실행 실측값
항목교재 (HPO 2025-02)노트북 (HPOA 2026-06-23)읽는 법
적재된 트리플899,558926,45016개월 사이 약 2.7만 건 증가. 숫자가 다른 것이 정상이다
HpoPhenotype 노드—20,413표현형 어휘 규모
HpoDisease 노드—12,956HPOA 헤더의 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에서 여는 링크로 대체했다.
Section 04 · Query & Case

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
세 줄을 그래프 그림으로 읽기
  1. MATCH path=…는 질병 노드에서 표현형 노드로 가는 전체 패턴을 찾고, 노드와 관계를 포함한 경로를 path 변수에 담습니다.
  2. WHERE dis.id = …는 찾은 모든 질병 중 제1형 당뇨병 ID 하나만 남깁니다.
  3. 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
여덟 절을 데이터 파이프라인으로 읽기
  1. 첫 MATCH와 WHERE … IN [ … ]은 관찰한 다섯 이름과 같은 표현형만 고릅니다. IN은 “이 리스트 안에 포함되는가?”를 검사합니다.
  2. WITH phe는 선택된 표현형을 다음 질의 절로 넘깁니다. SQL의 서브쿼리 경계와 비슷하지만, Cypher에서는 행이 절에서 절로 흐르는 파이프 역할을 합니다.
  3. 두 번째 MATCH는 각 표현형으로 들어오는 질병 관계를 찾아 질병–표현형 일치 한 건마다 행을 만듭니다.
  4. dis는 리스트가 아니라 질병 노드 하나인데도, 교재는 UNWIND dis AS nodes로 그 노드를 한 번 흘려보낸 뒤 count(nodes)로 일치 행 수를 셉니다. 학습 노트북에서 count(nodes)와 count(phe)의 상위 5행이 같음을 확인했으므로, 이 예제에서는 count(phe)가 의도를 더 직접 보여 줍니다.
  5. collect(phe.label)은 같은 질병에 속한 표현형 이름을 리스트 하나로 모으고, count(nodes)는 그 질병이 몇 특징과 일치했는지 셉니다.
  6. AS는 결과 열에 사람이 읽을 별칭을 붙입니다. ORDER BY … DESC는 일치 수가 큰 순서, LIMIT 5는 상위 다섯 행만 반환한다는 뜻입니다.

교재의 2025년 2월 스냅샷에서는 다섯 특징을 모두 공유하는 Odontochondrodysplasia 2 with hearing loss and diabetes(OMIM:619269)가 첫 후보로 나타난다. 그다음 후보는 세 개 또는 두 개의 특징을 공유한다. 그래프의 가치는 질병 이름만 반환하는 데 있지 않다. 어떤 표현형 경로가 점수를 만들었는지, 각 관계에 어떤 출처·증거·빈도가 있는지 다시 열어볼 수 있다.

그림 6관찰한 다섯 표현형으로 질병 후보를 순위화하기
제1형 당뇨병과 성장 지연, 큰 무릎, 감각신경성 청각 장애, 소양증이 연결된 질병 후보들을 공유 특징 수 5개, 3개, 2개 순으로 정렬하는 구조 선은 후보 랭킹에서 공유 주석의 존재만 나타내며 관계 방향은 그림 2의 질병→표현형을 따른다. 질병명과 수치는 HPO 2025-02 교재 스냅샷이다. 표기 주의 — 교재와 현재 HPOA 데이터는 모두 Ondontochondrodysplasia로 적혀 있고, 이 리포트 본문은 OMIM:619269의 정식 표기 Odontochondrodysplasia를 쓴다. 노트북을 직접 돌리면 데이터 원문 표기가 그대로 출력된다. 교재 §3.4, Listing 3.25–3.26과 표 3.2를 경로와 겹침 수 중심으로 재구성했다.
표 12교재 스냅샷의 상위 후보와 해석 경계
후보공유 특징이 결과가 말하는 것말하지 않는 것
Odontochondrodysplasia 2 with hearing loss and diabetes5입력 다섯 표현형 모두와 주석 경로가 있음환자가 이 질병일 확률 또는 확정 진단
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위)은 그대로지만 그 아래는 상당히 달라졌다.

표 13같은 질의, 다른 스냅샷 — 후보 순위 대조
순위교재 (HPO 2025-02)노트북 재실행 (HPOA 2026-06-23)
1OMIM:619269 Ondontochondrodysplasia 2 … (5)OMIM:619269 동일 (5) — 변화 없음
2OMIM:618500 Holoprosencephaly 12 … (3)OMIM:618500 동일 (3) — 변화 없음
3OMIM:614700 3-methylglutaconic aciduria VIII (2)OMIM:621585 Mendez-Johnson immunoneurologic syndrome (3) — 신규
4OMIM:616192 Alobar holoprosencephaly (2)OMIM:256500 Netherton syndrome (3) — 신규
5OMIM: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위”라는 자리 자체가 데이터 갱신에 흔들린다.

Section 05 · Ontology Reasoning

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) — 모아 둔 표현형 리스트의 길이를 계산해 정렬에 사용합니다.
그림 7하위 표현형 주석을 상위 범주 질의에 포함하기
갑상선 생리 이상, 갑상선기능저하증, 갑상선종에서 내분비계 이상 상위 범주로 향하는 SUBCLASSOF 관계가 있고, 하위 표현형에 직접 주석된 질병이 상위 범주 질의에 추론으로 포함되는 구조 화살표는 HPO 저장 방향과 같은 하위→상위 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이 프로시저를 쓰는 이유가 바로 이것이다.

표 14추론 범위와 온톨로지 변경의 실측 영향
관찰실측무엇을 뜻하는가
깊이 *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 릴리스를 썼는지를 함께 표시해야 한다. 임상의가 추론 경로를 검토할 수 없다면 추론의 설명 가능성은 저장소 내부에만 머문다.

표 15직접 질의와 계층 추론의 역할·위험 비교
관점직접 연결 질의계층 추론화면에 남길 근거
범위명시된 질병–표현형 주석하위 표현형을 상위 범주에 포함direct / inferred 상태
가치정확한 주석과 provenance 검토계층 때문에 숨은 후보 회수원 주석과 전체 계층 경로
주요 위험계층적 의미를 놓쳐 누락범위가 넓어져 관련성 약화깊이·규칙·온톨로지 버전
부재 해석주석이 없을 수 있음하위 개념이 불완전할 수 있음“근거 없음”과 “거짓”을 구분
Section 06 · Evidence & Limits

6. 신뢰도는 가장 짧은 미검증 구간에서 무너진다

섹션 요약
  • 버전이 다른 두 HPO 원천을 섞으면 재현성과 의미 정합성이 흔들린다.
  • 식별자 매핑, 주석 다중성, 파싱, 정제, 추론 범위를 단계별로 검증해야 한다.
  • 기술적으로 재현된 후보도 임상적 타당성을 자동 증명하지 않는다.
  • 근거 화면은 개념이 아니라 이미 만들 수 있는 질의다 — 관계 속성 하나를 더 반환하면 된다.

6.1 네 게이트가 후보 결과의 감사 경로를 만든다

첫째, 원천 게이트에서 릴리스·컬럼·식별자·체크섬을 고정한다. 둘째, 의미 게이트에서 질병과 표현형 역할, HPO ID, 동의어와 계층 방향을 표본 감사한다. 셋째, 적재 게이트에서 원천 행 수가 노드·관계·주석으로 어떻게 변했는지, 중복과 결측이 어떻게 처리됐는지 비교한다. 넷째, 사용 게이트에서 대표 질의를 재현하고 직접·추론 결과와 임상 판단의 경계를 확인한다.

그림 8온톨로지 기반 임상 지원 KG의 네 검증 게이트
원천, 의미, 적재, 사용의 네 검증 게이트를 차례로 통과하고 실패하면 이전 단계로 돌아가며 최종 결과는 사람 검토로 이어지는 구조 게이트 통과는 의료적 타당성의 자동 증명이 아니라 임상의가 검토할 후보와 근거를 만들었다는 뜻이다. 교재의 데이터 품질 강조와 Chapter 3 구현 감사를 운영 통제 관점으로 재구성했다.
표 16주장–조건–실패 신호 추적표
핵심 주장근거적용 조건실패 신호
온톨로지가 의미 통합을 돕는다표준 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의 실제 수집·동의·권한·비식별화, 부정 표현형, 발현 시점, 환자군별 빈도, 임상 검증, 규제와 책임 배분은 다루지 않는다. 따라서 결과를 “진단”이라고 표현하기보다 검토 가능한 후보 생성으로 제한해야 한다.

Safety Boundary그래프가 반환한 질병명은 정답이 아니라 조사 순서다. 원 주석·추론 경로·데이터 버전을 함께 보여주고, 최종 판단은 임상의와 검증 절차에 남긴다.
Section 07 · Decision & Takeaways

7. 온톨로지·LPG·추론은 질문에 필요한 만큼만 쓴다

섹션 요약
  • 여러 원천의 의미 정렬과 계층 질의가 핵심이면 온톨로지 우선 접근의 가치가 크다.
  • 관계별 provenance와 경로 탐색이 중심이면 LPG가 단순할 수 있지만 RDF 대안도 요구별로 비교한다.
  • 작은 대표 질문으로 데이터·모델·추론·검토 비용을 함께 시험한다.

7.1 기술 이름이 아니라 지식 질문으로 선택한다

표 17온톨로지 기반 KG 도입 판단표
질문도입 가치가 커지는 신호다른 접근이 나은 신호작은 실험
의미 정렬이 핵심인가?동의어·중의어·세분성 때문에 통합 오류가 큼단일 시스템의 고정 코드만 사용상위 100개 값의 표준 ID 매핑 감사
관계 경로가 답인가?여러 개체의 연결과 provenance가 판단 근거단일 테이블 필터·집계로 충분대표 질문 3개를 RDB와 그래프로 비교
계층 추론이 필요한가?상·하위 개념을 따라 누락을 줄여야 함정확 일치만 요구하고 분류체계가 불안정직접/추론 결과의 정밀 검토 표본
갱신을 책임질 수 있는가?릴리스·매핑·품질 소유자와 롤백이 있음latest 적재만 하고 결과 드리프트를 추적하지 않음두 릴리스 diff와 회귀 질의 실행
결과를 검토할 수 있는가?경로·출처·증거를 사용자에게 보여줄 수 있음질병명만 반환하고 검증 책임자가 없음임상의 리뷰용 설명 화면 프로토타입

7.2 최소 파일럿은 질문–스냅샷–그래프–근거 화면이다

첫 파일럿은 거대한 의료 온톨로지 플랫폼이 아니다. 대표 임상 질문 3개와 기대 경로를 정의하고, 같은 날짜의 hp.owl과 phenotype.hpoa를 고정한다. 필요한 HPO 하위 범위만 적재하고, 주석 다중성을 보존하는 모델을 선택한다. 직접 질의와 계층 추론의 결과를 분리해 표시하고, 원 출처와 경로를 한 번에 열 수 있는 검토 화면을 만든다.

중단 기준도 함께 둔다. 표준 ID 매핑 오류가 허용 범위를 넘거나, 주석 건수 보존이 설명되지 않거나, 추론 후보가 임상의 검토 시간을 줄이지 못하거나, 릴리스 갱신 때 회귀 결과를 설명할 수 없다면 범위를 줄이거나 모델을 바꾼다.

Three Takeaways질문이 모델을 고르고, 온톨로지가 의미를 고정하며, 근거 경로가 결과의 신뢰 범위를 정한다. 그래프의 완성은 적재 성공이 아니라 사용자가 후보와 이유를 함께 검토할 수 있을 때다.

7.3 토론 질문

표 18책의 개념을 우리 설계로 옮기는 질문
질문드러나는 설계 결정좋은 답의 증거
우리 데이터의 “같은 개념”은 누가 승인하는가?매핑 소유권·변경 이력·충돌 정책표본 감사와 되돌릴 수 있는 매핑 버전
관계 하나에 여러 근거가 있을 때 어떻게 보존할까?관계 속성 vs Annotation 노드원천 주석 수와 그래프 assertion 수의 대응
추론으로 포함된 후보를 사용자에게 어떻게 설명할까?경로 표시·깊이·규칙·불확실성 UI원 주석과 전체 계층 경로를 한 번에 검토
Appendix A · Glossary

핵심 용어

표 19Chapter 3 용어 사전
용어이 리포트에서의 의미
온톨로지도메인의 표준 개념·식별자·속성·범주·관계를 명시한 참조 어휘와 스키마.
HPO인간 표현형 이상을 표준화하고 계층화하며 질병 연관 주석을 제공하는 Human Phenotype Ontology.
표현형개인에게 관찰되는 물리적·생화학적 특징. 이 사례에서는 HPO 식별자로 코딩한다.
주석특정 질병–표현형 연관과 그 출처·증거·빈도·큐레이션 정보를 묶은 주장.
RDF주어–술어–목적어 트리플로 지식을 표현·교환하는 W3C 표준 데이터 모델.
LPG레이블이 있는 노드와 개별 관계에 키–값 속성을 둘 수 있는 그래프 모델.
Neosemantics(n10s)Neo4j에서 RDF/OWL 어휘를 가져오고 일부 의미 질의를 지원하는 플러그인.
추론저장된 주석과 온톨로지 규칙·계층에서 명시되지 않은 포함 관계를 도출하는 과정.
provenance어떤 주장이 어느 출처·작성자·시점·처리 과정에서 왔는지를 추적하는 정보.
Appendix B · References

참고 자료와 재구성 범위

  1. [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. [2]Human Phenotype Ontology 공식 포털과 hp.owl 배포 경로. 교재와 로컬 실습이 참조한 온톨로지 원천.
  3. [3]HPOA release data. 이 URL은 latest이므로 재현 가능한 운영에서는 고정 릴리스와 체크섬을 기록해야 한다.
  4. [4]저장소의 Chapter 3 원문·한국어 번역·해설판, src/ch03/importer/import_hpo.py, study.toml 리스팅 대응표를 원자료 감사에 사용했다. 업스트림 리스팅 원문은 저자 저장소 alenegro81/…/chapters/ch03에 있다.
  5. [5]그림 1–8은 교재 도형을 복제하지 않고 Chapter 3의 논리 관계를 발표자가 새로 구성한 SVG다. 임상 결과 그림의 질병명과 공유 특징 수만 교재의 HPO 2025-02 결과 스냅샷을 따른다.
  6. [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. [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. [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차 자료다.