技能 编程开发 临床实体至FHIR R4导出

临床实体至FHIR R4导出

v20260803
exporting-to-fhir
该技能用于将OpenMed识别出的临床文本片段(NER输出)转换为结构化、符合FHIR R4标准的电子健康记录(EHR)资源。它提供构建CodeableConcept的机械帮助函数,支持SNOMED、LOINC、ICD-10等主流编码体系,确保数据具备高度的互操作性。
获取技能
329 次下载
概览

Exporting to FHIR

OpenMed's NER (openmed.analyze_text) returns spans — text, label, offsets, confidence. To make those spans interoperable you wrap each clinically relevant span in a FHIR R4 resource (Condition, MedicationStatement, Observation, ...) carrying a coded CodeableConcept. OpenMed ships the mechanical R4 export helpers for this in openmed.clinical.exporters; you own the small amount of clinical mapping (which span becomes which resource).

When to use

Use this after NER, when the consumer is a FHIR system (an EHR, a registry, a data lake on FHIR). Reach for it when the user says "export to FHIR", "make a Condition/Observation", "build a CodeableConcept", or needs RxNorm/LOINC/ICD-10/ SNOMED-coded resources. For packaging many resources into one transaction Bundle, hand off to assembling-fhir-bundles. To check the result against US Core, hand off to validating-us-core.

What OpenMed gives you (verified API)

OpenMed deliberately ships the purely mechanical pieces and leaves clinical judgement to you. The verified entry points:

# CodeableConcept builder — openmed/clinical/exporters/codeable_concept_simple.py
from openmed.clinical.exporters.codeable_concept_simple import (
    system_uri,        # vocab id -> canonical HL7 system URI
    coding,            # (system, code, display) -> Coding dict
    codeable_concept,  # [Coding, ...] -> CodeableConcept dict (deterministic order)
)

# Bundle + reference + OperationOutcome — openmed/clinical/exporters/fhir/
from openmed.clinical.exporters.fhir import (
    to_bundle,                 # [resource, ...] -> R4 transaction Bundle
    deterministic_fullurl,     # (doc_id, index) -> stable urn:uuid
    OperationOutcomeIssue,     # issue dataclass
    to_operation_outcome,      # [issue, ...] -> OperationOutcome
    from_validation_result,    # validator result -> OperationOutcome
)

system_uri knows these vocabularies out of the box: rxnorm, icd-10-cm, loinc, snomed, hpo, mesh (and passes through any http(s):// URI unchanged). It is the single source of truth for vocab-id → system-URI mapping.

There is no to_condition() / to_observation() magic function. You build the resource shell (a small dict) and drop a codeable_concept(...) into its coded slot. That is by design: the resource type and clinical status are decisions OpenMed will not make for you.

Quick start: entity → Condition

import openmed
from openmed.clinical.exporters.codeable_concept_simple import coding, codeable_concept

# 1) NER (synthetic note — no real PHI)
result = openmed.analyze_text(
    "Assessment: type 2 diabetes mellitus, stable.",
    model_name="disease_detection_superclinical",
)
# result.entities -> EntityPrediction(text, label, confidence, start, end)
span = result.entities[0]            # e.g. text="type 2 diabetes mellitus"

# 2) Ground to a code OUT OF PROCESS (your terminology server / mapping table).
#    OpenMed bundles no restricted vocab — see querying-terminology-service.
icd_code, snomed_code = "E11.9", "44054006"

# 3) Build the CodeableConcept with OpenMed's builder
cc = codeable_concept(
    [
        coding("snomed", snomed_code, "Diabetes mellitus type 2"),
        coding("icd-10-cm", icd_code, "Type 2 diabetes mellitus without complications"),
    ],
    text=span.text,
)

# 4) Assemble the resource shell yourself
condition = {
    "resourceType": "Condition",
    "id": "cond-1",
    "clinicalStatus": {
        "coding": [{
            "system": "http://terminology.hl7.org/CodeSystem/condition-clinical",
            "code": "active",
        }]
    },
    "verificationStatus": {
        "coding": [{
            "system": "http://terminology.hl7.org/CodeSystem/condition-ver-status",
            "code": "confirmed",
        }]
    },
    "category": [{
        "coding": [{
            "system": "http://terminology.hl7.org/CodeSystem/condition-category",
            "code": "encounter-diagnosis",
        }]
    }],
    "code": cc,                                  # OpenMed-built CodeableConcept
    "subject": {"reference": "Patient/patient-1"},
    "recordedDate": "2024-03-02",
}

codeable_concept sorts codings deterministically (SNOMED, LOINC, RxNorm, ICD-10-CM, HPO, MeSH first; everything else alphabetical), so the JSON is byte-stable across runs — important for diffable pipelines and golden tests.

Worked: the resulting Condition.code

{
  "code": {
    "coding": [
      { "system": "http://snomed.info/sct", "code": "44054006",
        "display": "Diabetes mellitus type 2" },
      { "system": "http://hl7.org/fhir/sid/icd-10-cm", "code": "E11.9",
        "display": "Type 2 diabetes mellitus without complications" }
    ],
    "text": "type 2 diabetes mellitus"
  }
}

Workflow

  1. NERopenmed.analyze_text(note, model_name=...)result.entities.
  2. Classify each span: a disease label → Condition; a drug → MedicationStatement; a lab/vital/measurement → Observation.
  3. Ground the surface text to a code out of process (terminology server or your own map). Never invent codes; if you cannot ground a span, emit a CodeableConcept with only text and no coding.
  4. Build the CodeableConcept with coding(...) + codeable_concept(...).
  5. Wrap it in the resource shell (set clinicalStatus/status, subject, dates). Use the cheat-sheet below.
  6. Reference the Patient/Encounter via {"reference": "Patient/<id>"}.
  7. Pass the list to to_bundle(...) (assembling-fhir-bundles) and validate (validating-us-core).

Resource cheat-sheet (where the CodeableConcept goes)

OpenMed entity kind FHIR resource Coded slot Required status field
Disease / diagnosis Condition code clinicalStatus, verificationStatus
Drug / medication MedicationStatement medicationCodeableConcept status (e.g. active)
Lab / vital / finding Observation code (+ valueQuantity/valueCodeableConcept) status (e.g. final)
Procedure Procedure code status
Allergy AllergyIntolerance code clinicalStatus

MedicationStatement (drug span)

med = {
    "resourceType": "MedicationStatement",
    "id": "med-1",
    "status": "active",
    "medicationCodeableConcept": codeable_concept(
        [coding("rxnorm", "860975", "metformin hydrochloride 500 MG Oral Tablet")],
        text="metformin 500 mg",
    ),
    "subject": {"reference": "Patient/patient-1"},
}

Observation (lab/vital span)

obs = {
    "resourceType": "Observation",
    "id": "obs-1",
    "status": "final",
    "category": [{"coding": [{
        "system": "http://terminology.hl7.org/CodeSystem/observation-category",
        "code": "laboratory",
    }]}],
    "code": codeable_concept(
        [coding("loinc", "4548-4", "Hemoglobin A1c/Hemoglobin.total in Blood")],
        text="HbA1c",
    ),
    "valueQuantity": {
        "value": 7.4, "unit": "%",
        "system": "http://unitsofmeasure.org", "code": "%",
    },
    "subject": {"reference": "Patient/patient-1"},
}

Hand-off to / from OpenMed

  • From OpenMed: result.entities (EntityPrediction.text/.label/.confidence /.start/.end) is the input. Keep confidence and the offsets in an extension or a side log so the resource is auditable back to the source span.
  • To OpenMed: before exporting a note that still contains PHI, run openmed.deidentify(...); or de-identify a built resource/Bundle with openmed.interop.fhir_operations.de_identify_resource / de_identify_bundle (see that module — it walks free-text + narrative and never touches codes, references, systems, or temporal values).
  • OperationOutcome: report any spans you could not map as OperationOutcomeIssue(severity="warning", code="incomplete", diagnostics=..., expression="Condition.code")to_operation_outcome([...]). Keep diagnostics PHI-free (offsets/labels, never raw identifiers).

Edge cases & gotchas

  • No code? Still valid. A CodeableConcept with only text and no coding is legal R4. Emit it rather than inventing a code, and flag it via OperationOutcome. US Core may still require a code — see validating-us-core.
  • system_uri raises on an unknown short id that is not a URL. Pass a known short id (rxnorm/loinc/snomed/icd-10-cm/hpo/mesh) or a full http(s):// system URI.
  • Negation / temporality. OpenMed NER finds the mention, not its assertion. A negated ("no diabetes") or historical span should change verificationStatus/clinicalStatus or be dropped. Resolve assertion first (resolving-clinical-context, openmed.clinical).
  • Stable ids. Give each resource a unique id; to_bundle rejects duplicate ResourceType/id pairs because they corrupt cross-references.
  • Local-first. Grounding to RxNorm/LOINC/SNOMED is out-of-process with the user's own credentials. OpenMed bundles no restricted terminology.

Standards & references

信息
Category 编程开发
Name exporting-to-fhir
版本 v20260803
大小 10.2KB
更新时间 2026-08-04
语言