Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,11 @@ results = client.search.basic("metformin", vocabulary_ids=["RxNorm"], domain_ids
for c in results["concepts"]:
print(f"{c['concept_id']}: {c['concept_name']}")

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

# Navigate concept hierarchy
ancestors = client.hierarchy.ancestors(201826, max_levels=3)
Expand Down Expand Up @@ -323,7 +326,7 @@ suggestions = client.concepts.suggest("diab", vocabulary_ids=["SNOMED"], page_si
| `concepts` | Concept lookup and batch operations | `get()`, `get_by_code()`, `batch()`, `suggest()` |
| `search` | Full-text and semantic search | `basic()`, `advanced()`, `semantic()`, `similar()`, `bulk_basic()`, `bulk_semantic()` |
| `hierarchy` | Navigate concept relationships | `ancestors()`, `descendants()` |
| `mappings` | Cross-vocabulary mappings | `get()`, `map()` |
| `mappings` | Cross-vocabulary mappings | `get()`, `get_iter()`, `map()` |
| `vocabularies` | Vocabulary metadata | `list()`, `get()`, `stats()` |
| `domains` | Domain information | `list()`, `get()`, `concepts()` |
| `fhir` | FHIR-to-OMOP resolution | `resolve()`, `resolve_batch()`, `resolve_codeable_concept()` |
Expand Down
149 changes: 133 additions & 16 deletions examples/map_between_vocabularies.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,147 @@


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

client = omophub.OMOPHub()

try:
# Type 2 diabetes mellitus (SNOMED)
# Type 2 diabetes mellitus (SNOMED, standard)
concept_id = 201826

# `get()` returns ONE page, and it returns the response's `data` field
# only -- the `meta.pagination` that would say whether more pages exist
# is not part of what you get back. See get_every_mapping() below.
result = client.mappings.get(concept_id)
mappings = result.get("mappings", [])

print(f"Mappings for concept {concept_id} (this page: {len(mappings)}):")
for m in mappings[:10]:
# A mapping row carries only these fields. Vocabulary id and
# concept code are NOT part of it -- fetch the target concept if
# you need them.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The mapping row does expose vocabulary/code fields, so this comment tells users to make an unnecessary concept lookup and contradicts the SDK schema. Updating the comment to describe those optional fields would keep the example accurate.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/map_between_vocabularies.py, line 25:

<comment>The mapping row does expose vocabulary/code fields, so this comment tells users to make an unnecessary concept lookup and contradicts the SDK schema. Updating the comment to describe those optional fields would keep the example accurate.</comment>

<file context>
@@ -5,34 +5,147 @@
+
+        print(f"Mappings for concept {concept_id} (this page: {len(mappings)}):")
+        for m in mappings[:10]:
+            # A mapping row carries only these fields. Vocabulary id and
+            # concept code are NOT part of it -- fetch the target concept if
+            # you need them.
</file context>
Suggested change
# A mapping row carries only these fields. Vocabulary id and
# concept code are NOT part of it -- fetch the target concept if
# you need them.
# Mapping rows include the target concept ID/name and may also include
# target vocabulary/code fields when returned by the API.

print(
f" {m.get('relationship_id')}: "
f"{m.get('target_concept_id')} {m.get('target_concept_name')}"
)
except omophub.OMOPHubError as e:
print(f"API error: {e.message}")
finally:
client.close()


def map_to_a_specific_vocabulary() -> None:
"""Find which ICD-10-CM codes correspond to a SNOMED concept.

Note the DIRECTION. `Maps to` always points at a *standard* concept, and
ICD-10-CM is non-standard, so `target_vocabulary="ICD10CM"` on the default
relationship matches nothing -- it returns an empty list rather than an
error. The codes that roll up INTO a standard concept are reached with
`Mapped from`.
"""
print("\n=== Mapping to a Specific Vocabulary ===")

client = omophub.OMOPHub()

try:
concept_id = 201826

empty = client.mappings.get(concept_id, target_vocabulary="ICD10CM")
print(
f" 'Maps to' + ICD10CM: {len(empty.get('mappings', []))} rows (as expected)"
)

icd_codes = list(
client.mappings.get_iter(
concept_id,
relationship_ids=["Mapped from"],
target_vocabulary="ICD10CM",
)
)
print(f" 'Mapped from' + ICD10CM: {len(icd_codes)} rows")
for m in icd_codes[:5]:
print(f" <- {m.get('target_concept_id')} {m.get('target_concept_name')}")
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
except omophub.OMOPHubError as e:
print(f"API error: {e.message}")
finally:
client.close()


def get_every_mapping() -> None:
"""Walk every page instead of trusting the first one.

This is the one to copy when building a code list: a partial code list is
wrong in a way nothing in the result reveals.
"""
print("\n=== Every Mapping (all pages) ===")

client = omophub.OMOPHub()

try:
concept_id = 201826

# get_iter() follows has_next to the end; it never has to guess from
# the page length.
all_mappings = list(client.mappings.get_iter(concept_id))
print(f" {len(all_mappings)} mappings in total")

# Streaming, if you would rather not hold them all at once.
for m in client.mappings.get_iter(concept_id):
_ = m["target_concept_name"]
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
except omophub.OMOPHubError as e:
print(f"API error: {e.message}")
finally:
client.close()


def value_as_concept() -> None:
"""Composite concepts decompose across TWO relationships.

The default returns only the first, so you learn the patient is allergic
to *a drug* but not *which* drug.
"""
print("\n=== Value-as-Concept ===")

client = omophub.OMOPHub()

try:
# Allergy to penicillin G
result = client.mappings.get(
concept_id,
target_vocabulary="ICD10CM",
4167462,
relationship_ids=["Maps to", "Maps to value"],
)

source = result.get("source_concept", {})
mappings = result.get("mappings", [])
summary = result.get("mapping_summary", {})
for m in result.get("mappings", []):
# `Maps to` -> the OMOP concept column;
# `Maps to value` -> value_as_concept_id.
column = (
"value_as_concept_id"
if m.get("relationship_id") == "Maps to value"
else "concept_id"
)
print(
f" {m.get('relationship_id')}: {m.get('target_concept_name')} -> {column}"
)
except omophub.OMOPHubError as e:
print(f"API error: {e.message}")
finally:
client.close()

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

for m in mappings[:10]:
target_vocab = m.get("target_vocabulary_id", "?")
target_code = m.get("target_concept_code", "?")
target_name = m.get("target_concept_name", "?")
print(f"\n [{target_vocab}] {target_code}")
print(f" Name: {target_name}")
def exclude_invalid() -> None:
"""Deprecated mappings come back by default; pass False to drop them."""
print("\n=== Valid Mappings Only ===")

client = omophub.OMOPHub()

try:
concept_id = 201826
with_invalid = list(client.mappings.get_iter(concept_id))
valid_only = list(client.mappings.get_iter(concept_id, include_invalid=False))

print(f" default (includes deprecated): {len(with_invalid)}")
print(f" include_invalid=False: {len(valid_only)}")
except omophub.OMOPHubError as e:
print(f"API error: {e.message}")
finally:
Expand Down Expand Up @@ -99,5 +212,9 @@ def lookup_by_code() -> None:

if __name__ == "__main__":
get_mappings()
map_to_a_specific_vocabulary()
get_every_mapping()
value_as_concept()
exclude_invalid()
map_concepts()
lookup_by_code()
Loading