Skip to content

Commit 3ff6a91

Browse files
author
alex-omophub
committed
Enhance README and example scripts for Mappings API
- Updated README to clarify the mapping process from ICD-10 to SNOMED, including code lookup and mapping retrieval. - Improved example script to demonstrate fetching mappings, handling pagination, and filtering valid mappings only. - Added new functions to illustrate specific vocabulary mapping and the handling of composite concepts.
1 parent 6fff39f commit 3ff6a91

2 files changed

Lines changed: 139 additions & 19 deletions

File tree

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,11 @@ results = client.search.basic("metformin", vocabulary_ids=["RxNorm"], domain_ids
5555
for c in results["concepts"]:
5656
print(f"{c['concept_id']}: {c['concept_name']}")
5757

58-
# Map ICD-10 code to SNOMED
59-
mappings = client.mappings.get_by_code("ICD10CM", "E11.9", target_vocabulary="SNOMED")
58+
# Map an ICD-10 code to SNOMED: look the code up, then map its concept.
59+
# (`Maps to` points at *standard* concepts, so SNOMED is a valid target here
60+
# while the reverse, SNOMED -> ICD10CM, would return nothing.)
61+
icd = client.concepts.get_by_code("ICD10CM", "E11.9")
62+
mappings = client.mappings.get(icd["concept_id"], target_vocabulary="SNOMED")
6063

6164
# Navigate concept hierarchy
6265
ancestors = client.hierarchy.ancestors(201826, max_levels=3)
@@ -323,7 +326,7 @@ suggestions = client.concepts.suggest("diab", vocabulary_ids=["SNOMED"], page_si
323326
| `concepts` | Concept lookup and batch operations | `get()`, `get_by_code()`, `batch()`, `suggest()` |
324327
| `search` | Full-text and semantic search | `basic()`, `advanced()`, `semantic()`, `similar()`, `bulk_basic()`, `bulk_semantic()` |
325328
| `hierarchy` | Navigate concept relationships | `ancestors()`, `descendants()` |
326-
| `mappings` | Cross-vocabulary mappings | `get()`, `map()` |
329+
| `mappings` | Cross-vocabulary mappings | `get()`, `get_iter()`, `map()` |
327330
| `vocabularies` | Vocabulary metadata | `list()`, `get()`, `stats()` |
328331
| `domains` | Domain information | `list()`, `get()`, `concepts()` |
329332
| `fhir` | FHIR-to-OMOP resolution | `resolve()`, `resolve_batch()`, `resolve_codeable_concept()` |

examples/map_between_vocabularies.py

Lines changed: 133 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,147 @@
55

66

77
def get_mappings() -> None:
8-
"""Get mappings for a concept to other vocabularies."""
8+
"""Get the mappings defined for a concept."""
99
print("=== Concept Mappings ===")
1010

1111
client = omophub.OMOPHub()
1212

1313
try:
14-
# Type 2 diabetes mellitus (SNOMED)
14+
# Type 2 diabetes mellitus (SNOMED, standard)
1515
concept_id = 201826
1616

17+
# `get()` returns ONE page, and it returns the response's `data` field
18+
# only -- the `meta.pagination` that would say whether more pages exist
19+
# is not part of what you get back. See get_every_mapping() below.
20+
result = client.mappings.get(concept_id)
21+
mappings = result.get("mappings", [])
22+
23+
print(f"Mappings for concept {concept_id} (this page: {len(mappings)}):")
24+
for m in mappings[:10]:
25+
# A mapping row carries only these fields. Vocabulary id and
26+
# concept code are NOT part of it -- fetch the target concept if
27+
# you need them.
28+
print(
29+
f" {m.get('relationship_id')}: "
30+
f"{m.get('target_concept_id')} {m.get('target_concept_name')}"
31+
)
32+
except omophub.OMOPHubError as e:
33+
print(f"API error: {e.message}")
34+
finally:
35+
client.close()
36+
37+
38+
def map_to_a_specific_vocabulary() -> None:
39+
"""Find which ICD-10-CM codes correspond to a SNOMED concept.
40+
41+
Note the DIRECTION. `Maps to` always points at a *standard* concept, and
42+
ICD-10-CM is non-standard, so `target_vocabulary="ICD10CM"` on the default
43+
relationship matches nothing -- it returns an empty list rather than an
44+
error. The codes that roll up INTO a standard concept are reached with
45+
`Mapped from`.
46+
"""
47+
print("\n=== Mapping to a Specific Vocabulary ===")
48+
49+
client = omophub.OMOPHub()
50+
51+
try:
52+
concept_id = 201826
53+
54+
empty = client.mappings.get(concept_id, target_vocabulary="ICD10CM")
55+
print(
56+
f" 'Maps to' + ICD10CM: {len(empty.get('mappings', []))} rows (as expected)"
57+
)
58+
59+
icd_codes = list(
60+
client.mappings.get_iter(
61+
concept_id,
62+
relationship_ids=["Mapped from"],
63+
target_vocabulary="ICD10CM",
64+
)
65+
)
66+
print(f" 'Mapped from' + ICD10CM: {len(icd_codes)} rows")
67+
for m in icd_codes[:5]:
68+
print(f" <- {m.get('target_concept_id')} {m.get('target_concept_name')}")
69+
except omophub.OMOPHubError as e:
70+
print(f"API error: {e.message}")
71+
finally:
72+
client.close()
73+
74+
75+
def get_every_mapping() -> None:
76+
"""Walk every page instead of trusting the first one.
77+
78+
This is the one to copy when building a code list: a partial code list is
79+
wrong in a way nothing in the result reveals.
80+
"""
81+
print("\n=== Every Mapping (all pages) ===")
82+
83+
client = omophub.OMOPHub()
84+
85+
try:
86+
concept_id = 201826
87+
88+
# get_iter() follows has_next to the end; it never has to guess from
89+
# the page length.
90+
all_mappings = list(client.mappings.get_iter(concept_id))
91+
print(f" {len(all_mappings)} mappings in total")
92+
93+
# Streaming, if you would rather not hold them all at once.
94+
for m in client.mappings.get_iter(concept_id):
95+
_ = m["target_concept_name"]
96+
except omophub.OMOPHubError as e:
97+
print(f"API error: {e.message}")
98+
finally:
99+
client.close()
100+
101+
102+
def value_as_concept() -> None:
103+
"""Composite concepts decompose across TWO relationships.
104+
105+
The default returns only the first, so you learn the patient is allergic
106+
to *a drug* but not *which* drug.
107+
"""
108+
print("\n=== Value-as-Concept ===")
109+
110+
client = omophub.OMOPHub()
111+
112+
try:
113+
# Allergy to penicillin G
17114
result = client.mappings.get(
18-
concept_id,
19-
target_vocabulary="ICD10CM",
115+
4167462,
116+
relationship_ids=["Maps to", "Maps to value"],
20117
)
21118

22-
source = result.get("source_concept", {})
23-
mappings = result.get("mappings", [])
24-
summary = result.get("mapping_summary", {})
119+
for m in result.get("mappings", []):
120+
# `Maps to` -> the OMOP concept column;
121+
# `Maps to value` -> value_as_concept_id.
122+
column = (
123+
"value_as_concept_id"
124+
if m.get("relationship_id") == "Maps to value"
125+
else "concept_id"
126+
)
127+
print(
128+
f" {m.get('relationship_id')}: {m.get('target_concept_name')} -> {column}"
129+
)
130+
except omophub.OMOPHubError as e:
131+
print(f"API error: {e.message}")
132+
finally:
133+
client.close()
25134

26-
source_name = source.get("concept_name", "Unknown") if source else "Unknown"
27-
print(f"Mappings for '{source_name}':")
28-
print(f" Total mappings: {summary.get('total_mappings', len(mappings))}")
29135

30-
for m in mappings[:10]:
31-
target_vocab = m.get("target_vocabulary_id", "?")
32-
target_code = m.get("target_concept_code", "?")
33-
target_name = m.get("target_concept_name", "?")
34-
print(f"\n [{target_vocab}] {target_code}")
35-
print(f" Name: {target_name}")
136+
def exclude_invalid() -> None:
137+
"""Deprecated mappings come back by default; pass False to drop them."""
138+
print("\n=== Valid Mappings Only ===")
139+
140+
client = omophub.OMOPHub()
141+
142+
try:
143+
concept_id = 201826
144+
with_invalid = list(client.mappings.get_iter(concept_id))
145+
valid_only = list(client.mappings.get_iter(concept_id, include_invalid=False))
146+
147+
print(f" default (includes deprecated): {len(with_invalid)}")
148+
print(f" include_invalid=False: {len(valid_only)}")
36149
except omophub.OMOPHubError as e:
37150
print(f"API error: {e.message}")
38151
finally:
@@ -99,5 +212,9 @@ def lookup_by_code() -> None:
99212

100213
if __name__ == "__main__":
101214
get_mappings()
215+
map_to_a_specific_vocabulary()
216+
get_every_mapping()
217+
value_as_concept()
218+
exclude_invalid()
102219
map_concepts()
103220
lookup_by_code()

0 commit comments

Comments
 (0)