ehr-data-parsing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

EHR Data Parsing

EHR 数据解析

Overview

概述

This skill encodes Python pipelines for parsing clinical data from two dominant formats: HL7v2 (pipe-delimited messages) and FHIR R4 (JSON resources). It covers extraction of patient demographics, lab results, diagnoses, and medications, plus format conversion and data quality checks.
Scope:
  • HL7v2 message parsing (MSH, PID, PV1, OBR, OBX segments)
  • FHIR R4 resource extraction (Patient, Observation, Condition, MedicationRequest)
  • HL7v2 → tabular (CSV/DataFrame) conversion
  • FHIR Bundle iteration and flattening
  • Data quality checks (completeness, date validation, code system verification)
Out of scope: HL7v3/CDA parsing, FHIR server deployment, SMART on FHIR authentication, real-time message routing.
本 skill 提供了 Python 流水线,用于解析两种主流格式中的临床数据:HL7v2(管道符分隔的消息)和 FHIR R4(JSON 资源)。它涵盖患者基本信息、实验室结果、诊断和用药信息的提取,以及格式转换和数据质量检查。
范围:
  • HL7v2 消息解析(MSH、PID、PV1、OBR、OBX segment)
  • FHIR R4 资源提取(Patient、Observation、Condition、MedicationRequest)
  • HL7v2 → 表格数据(CSV/DataFrame)转换
  • FHIR Bundle 迭代与扁平化
  • 数据质量检查(完整性、日期验证、代码系统验证)
不在范围:HL7v3/CDA 解析、FHIR server 部署、SMART on FHIR 认证、实时消息路由。

Usage

用法

Invoke when the user asks to:
  • Parse an HL7v2 message or batch file
  • Extract patient demographics, labs, diagnoses, or medications from FHIR
  • Convert HL7v2 messages to CSV or pandas DataFrames
  • Flatten FHIR Bundles into tabular format
  • Run quality checks on clinical data extracts
The skill emits Python code using
python-hl7
for HL7v2 and
fhir.resources
for FHIR R4. Install dependencies:
bash
pip install python-hl7 fhir.resources pandas
在用户要求执行以下操作时调用:
  • 解析 HL7v2 消息或批处理文件
  • 从 FHIR 中提取患者基本信息、实验室结果、诊断或用药信息
  • 将 HL7v2 消息转换为 CSV 或 pandas DataFrame
  • 将 FHIR Bundle 扁平化为表格格式
  • 对临床数据提取结果运行质量检查
该 skill 输出的 Python 代码使用
python-hl7
解析 HL7v2,并使用
fhir.resources
解析 FHIR R4。安装依赖:
bash
pip install python-hl7 fhir.resources pandas

Response Format

响应格式

  • Lead with the command or code the user needs — explain after
  • Structure as: confirm inputs → working code → key parameters explained → gotchas
  • One complete working example per task; do not show every alternative
  • Keep code comments minimal and functional (what, not why-it-exists)
  • Target: 50-100 lines of code with brief surrounding explanation
  • 先提供用户所需的命令或代码——随后再进行说明
  • 结构:确认输入 → 可运行的代码 → 关键参数说明 → 注意事项
  • 每项任务提供一个完整可运行示例;不要展示所有备选方案
  • 代码注释应精简且实用(说明做什么,而非说明为何存在)
  • 目标:代码 50–100 行,并附简短说明

Core Concepts

核心概念

Parser Selection Decision Tree

解析器选择决策树

What is the input format?
├─ HL7v2 (pipe-delimited, segments start with MSH|PID|OBX...)
│  ├─ Message type?
│  │  ├─ ORU^R01 (lab results) → extract OBR + OBX segments
│  │  ├─ ADT^A01/A08 (admit/update) → extract PID + PV1 segments
│  │  └─ ORM^O01 (orders) → extract ORC + OBR segments
│  └─ Library: python-hl7 (parse → segment → field access)
└─ FHIR R4 (JSON resources)
   ├─ Single resource → fhir.resources model_validate(json)
   └─ Bundle → iterate entry[], check resource_type, follow pagination (link.next)
      ├─ Extract by system URI, not array position
      └─ Check choice types (valueQuantity vs valueCodeableConcept)
What is the input format?
├─ HL7v2 (pipe-delimited, segments start with MSH|PID|OBX...)
│  ├─ Message type?
│  │  ├─ ORU^R01 (lab results) → extract OBR + OBX segments
│  │  ├─ ADT^A01/A08 (admit/update) → extract PID + PV1 segments
│  │  └─ ORM^O01 (orders) → extract ORC + OBR segments
│  └─ Library: python-hl7 (parse → segment → field access)
└─ FHIR R4 (JSON resources)
   ├─ Single resource → fhir.resources model_validate(json)
   └─ Bundle → iterate entry[], check resource_type, follow pagination (link.next)
      ├─ Extract by system URI, not array position
      └─ Check choice types (valueQuantity vs valueCodeableConcept)

HL7v2 Message Structure

HL7v2 消息结构

HL7v2 messages are pipe-delimited, segment-based. Each segment starts with a 3-character identifier followed by fields separated by
|
.
HL7v2 消息以 segment 为单位并使用管道符分隔。每个 segment 以一个 3 字符标识符开头,字段之间使用
|
分隔。

Key segments

关键 segment

SegmentNameContains
MSHMessage HeaderSending/receiving app, message type, timestamp, version
PIDPatient IdentificationMRN, name, DOB, sex, address, phone
PV1Patient VisitVisit number, patient class, attending physician
OBRObservation RequestOrder number, test ordered, ordering provider
OBXObservation ResultResult value, units, reference range, abnormal flag
DG1DiagnosisDiagnosis code, type, description
AL1AllergyAllergen, reaction, severity
Segment名称包含内容
MSH消息头发送/接收应用、消息类型、时间戳、版本
PID患者标识MRN、姓名、出生日期、性别、地址、电话
PV1患者就诊就诊编号、患者类别、主治医生
OBR观察申请申请编号、申请检查、申请医生
OBX观察结果结果值、单位、参考范围、异常标记
DG1诊断诊断代码、类型、描述
AL1过敏过敏原、反应、严重程度

Common message types

常见消息类型

TypeTriggerUse
ADT^A01AdmitPatient admission
ADT^A08UpdatePatient information update
ORU^R01ObservationLab/radiology results
ORM^O01OrderNew order placed
MDM^T02DocumentDocument notification
类型触发操作用途
ADT^A01入院患者入院
ADT^A08更新患者信息更新
ORU^R01观察实验室/放射学结果
ORM^O01医嘱创建新医嘱
MDM^T02文档文档通知

1. Parse HL7v2 Messages

1. 解析 HL7v2 消息

python
import hl7

raw = (
    "MSH|^~\\&|LAB|HOSP|EHR|HOSP|20240115120000||ORU^R01|MSG001|P|2.5\r"
    "PID|1||MRN123^^^HOSP^MR||DOE^JOHN^A||19800315|M|||"
    "123 MAIN ST^^ANYTOWN^CA^90210\r"
    "OBR|1|ORD001||CBC^Complete Blood Count^L|||20240115080000\r"
    "OBX|1|NM|WBC^White Blood Cell Count^L||7.5|10*3/uL|4.5-11.0|N|||F\r"
    "OBX|2|NM|HGB^Hemoglobin^L||14.2|g/dL|12.0-17.5|N|||F\r"
    "OBX|3|NM|PLT^Platelet Count^L||250|10*3/uL|150-400|N|||F\r"
)

msg = hl7.parse(raw)
python
import hl7

raw = (
    "MSH|^~\\&|LAB|HOSP|EHR|HOSP|20240115120000||ORU^R01|MSG001|P|2.5\r"
    "PID|1||MRN123^^^HOSP^MR||DOE^JOHN^A||19800315|M|||"
    "123 MAIN ST^^ANYTOWN^CA^90210\r"
    "OBR|1|ORD001||CBC^Complete Blood Count^L|||20240115080000\r"
    "OBX|1|NM|WBC^White Blood Cell Count^L||7.5|10*3/uL|4.5-11.0|N|||F\r"
    "OBX|2|NM|HGB^Hemoglobin^L||14.2|g/dL|12.0-17.5|N|||F\r"
    "OBX|3|NM|PLT^Platelet Count^L||250|10*3/uL|150-400|N|||F\r"
)

msg = hl7.parse(raw)

Extract patient demographics from PID

从 PID 提取患者基本信息

python
def extract_pid(msg):
    pid = msg.segment("PID")
    return {
        "mrn": str(pid(3)(0)(0)),          # PID-3.1: patient ID
        "last_name": str(pid(5)(0)(0)),     # PID-5.1: family name
        "first_name": str(pid(5)(0)(1)),    # PID-5.2: given name
        "dob": str(pid(7)),                 # PID-7: date of birth
        "sex": str(pid(8)),                 # PID-8: administrative sex
    }

demographics = extract_pid(msg)
python
def extract_pid(msg):
    pid = msg.segment("PID")
    return {
        "mrn": str(pid(3)(0)(0)),          # PID-3.1: patient ID
        "last_name": str(pid(5)(0)(0)),     # PID-5.1: family name
        "first_name": str(pid(5)(0)(1)),    # PID-5.2: given name
        "dob": str(pid(7)),                 # PID-7: date of birth
        "sex": str(pid(8)),                 # PID-8: administrative sex
    }

demographics = extract_pid(msg)

{'mrn': 'MRN123', 'last_name': 'DOE', 'first_name': 'JOHN',

{'mrn': 'MRN123', 'last_name': 'DOE', 'first_name': 'JOHN',

'dob': '19800315', 'sex': 'M'}

'dob': '19800315', 'sex': 'M'}

undefined
undefined

PID field reference

PID 字段参考

FieldPositionFormat
Patient IDPID-3CX datatype (ID^^^authority^type)
Patient NamePID-5XPN: Family^Given^Middle
Date of BirthPID-7YYYYMMDD
SexPID-8M, F, O, U
AddressPID-11XAD: Street^City^State^Zip
PhonePID-13XTN datatype
字段位置格式
患者 IDPID-3CX 数据类型(ID^^^authority^type)
患者姓名PID-5XPN:Family^Given^Middle
出生日期PID-7YYYYMMDD
性别PID-8M、F、O、U
地址PID-11XAD:Street^City^State^Zip
电话PID-13XTN 数据类型

Extract lab results from OBX segments

从 OBX segment 提取实验室结果

python
def extract_obx_results(msg):
    results = []
    for segment in msg:
        if str(segment(0)) == "OBX":
            results.append({
                "set_id": str(segment(1)),
                "value_type": str(segment(2)),       # NM=numeric, ST=string, CE=coded
                "test_code": str(segment(3)(0)(0)),   # OBX-3.1: identifier
                "test_name": str(segment(3)(0)(1)),   # OBX-3.2: text
                "value": str(segment(5)),             # OBX-5: observation value
                "units": str(segment(6)),             # OBX-6: units
                "ref_range": str(segment(7)),         # OBX-7: reference range
                "abnormal_flag": str(segment(8)),     # OBX-8: N/H/L/A
                "status": str(segment(11)),           # OBX-11: F=final, P=preliminary
            })
    return results

labs = extract_obx_results(msg)
python
def extract_obx_results(msg):
    results = []
    for segment in msg:
        if str(segment(0)) == "OBX":
            results.append({
                "set_id": str(segment(1)),
                "value_type": str(segment(2)),       # NM=numeric, ST=string, CE=coded
                "test_code": str(segment(3)(0)(0)),   # OBX-3.1: identifier
                "test_name": str(segment(3)(0)(1)),   # OBX-3.2: text
                "value": str(segment(5)),             # OBX-5: observation value
                "units": str(segment(6)),             # OBX-6: units
                "ref_range": str(segment(7)),         # OBX-7: reference range
                "abnormal_flag": str(segment(8)),     # OBX-8: N/H/L/A
                "status": str(segment(11)),           # OBX-11: F=final, P=preliminary
            })
    return results

labs = extract_obx_results(msg)

OBX value types and status codes

OBX 值类型和状态代码

OBX-2MeaningOBX-8MeaningOBX-11Meaning
NMNumericNNormalFFinal
STStringHHighPPreliminary
CECoded entryLLowCCorrected
OBX-2含义OBX-8含义OBX-11含义
NM数值N正常F最终
ST字符串H高P初步
CE编码条目L低C已更正

2. Batch HL7v2 Processing

2. 批量处理 HL7v2

python
import hl7
import pandas as pd

def parse_hl7_file(filepath):
    """Parse a file containing multiple HL7v2 messages separated by blank lines."""
    with open(filepath, "r") as f:
        content = f.read()

    raw_messages = content.strip().split("\nMSH|")
    raw_messages = [raw_messages[0]] + ["MSH|" + m for m in raw_messages[1:]]

    all_results = []
    for raw in raw_messages:
        raw = raw.strip().replace("\n", "\r")
        if not raw:
            continue
        msg = hl7.parse(raw)
        demo = extract_pid(msg)
        for lab in extract_obx_results(msg):
            all_results.append({**demo, **lab})

    return pd.DataFrame(all_results)

df = parse_hl7_file("lab_results.hl7")
python
import hl7
import pandas as pd

def parse_hl7_file(filepath):
    """Parse a file containing multiple HL7v2 messages separated by blank lines."""
    with open(filepath, "r") as f:
        content = f.read()

    raw_messages = content.strip().split("\nMSH|")
    raw_messages = [raw_messages[0]] + ["MSH|" + m for m in raw_messages[1:]]

    all_results = []
    for raw in raw_messages:
        raw = raw.strip().replace("\n", "\r")
        if not raw:
            continue
        msg = hl7.parse(raw)
        demo = extract_pid(msg)
        for lab in extract_obx_results(msg):
            all_results.append({**demo, **lab})

    return pd.DataFrame(all_results)

df = parse_hl7_file("lab_results.hl7")

3. FHIR R4 Resource Extraction

3. FHIR R4 资源提取

Parse a Patient resource

解析 Patient resource

python
from fhir.resources.patient import Patient

patient_json = {
    "resourceType": "Patient",
    "id": "example-001",
    "identifier": [
        {"system": "http://hospital.example.org/mrn", "value": "MRN123"}
    ],
    "name": [{"family": "Doe", "given": ["John", "A"]}],
    "gender": "male",
    "birthDate": "1980-03-15",
    "address": [
        {"line": ["123 Main St"], "city": "Anytown", "state": "CA", "postalCode": "90210"}
    ],
}

patient = Patient.model_validate(patient_json)

demographics = {
    "id": patient.id,
    "mrn": patient.identifier[0].value if patient.identifier else None,
    "last_name": patient.name[0].family if patient.name else None,
    "first_name": patient.name[0].given[0] if patient.name and patient.name[0].given else None,
    "gender": patient.gender,
    "birth_date": str(patient.birthDate) if patient.birthDate else None,
}
python
from fhir.resources.patient import Patient

patient_json = {
    "resourceType": "Patient",
    "id": "example-001",
    "identifier": [
        {"system": "http://hospital.example.org/mrn", "value": "MRN123"}
    ],
    "name": [{"family": "Doe", "given": ["John", "A"]}],
    "gender": "male",
    "birthDate": "1980-03-15",
    "address": [
        {"line": ["123 Main St"], "city": "Anytown", "state": "CA", "postalCode": "90210"}
    ],
}

patient = Patient.model_validate(patient_json)

demographics = {
    "id": patient.id,
    "mrn": patient.identifier[0].value if patient.identifier else None,
    "last_name": patient.name[0].family if patient.name else None,
    "first_name": patient.name[0].given[0] if patient.name and patient.name[0].given else None,
    "gender": patient.gender,
    "birth_date": str(patient.birthDate) if patient.birthDate else None,
}

Extract Observation, Condition, MedicationRequest

提取 Observation、Condition 和 MedicationRequest

Pattern:
model_validate(json)
→ extract by system URI, not array position.
python
from fhir.resources.observation import Observation
from fhir.resources.condition import Condition
from fhir.resources.medicationrequest import MedicationRequest

def extract_observation(obs_json):
    obs = Observation.model_validate(obs_json)
    return {
        "id": obs.id, "status": obs.status,
        "loinc_code": next((c.code for c in (obs.code.coding or []) if c.system == "http://loinc.org"), None),
        "display": obs.code.coding[0].display if obs.code.coding else None,
        "value": obs.valueQuantity.value if obs.valueQuantity else None,
        "unit": obs.valueQuantity.unit if obs.valueQuantity else None,
        "effective_date": str(obs.effectiveDateTime) if obs.effectiveDateTime else None,
        "patient_ref": obs.subject.reference if obs.subject else None,
    }

def extract_condition(cond_json):
    cond = Condition.model_validate(cond_json)
    return {
        "id": cond.id,
        "status": cond.clinicalStatus.coding[0].code if cond.clinicalStatus and cond.clinicalStatus.coding else None,
        "snomed_code": next((c.code for c in (cond.code.coding or []) if c.system and "snomed" in c.system), None),
        "icd10_code": next((c.code for c in (cond.code.coding or []) if c.system and "icd-10" in c.system), None),
        "display": cond.code.coding[0].display if cond.code.coding else None,
        "onset": str(cond.onsetDateTime) if cond.onsetDateTime else None,
        "patient_ref": cond.subject.reference if cond.subject else None,
    }

def extract_medication_request(med_json):
    med = MedicationRequest.model_validate(med_json)
    cc = med.medicationCodeableConcept
    return {
        "id": med.id, "status": med.status,
        "rxnorm_code": cc.coding[0].code if cc and cc.coding else None,
        "display": cc.coding[0].display if cc and cc.coding else None,
        "dosage_text": med.dosageInstruction[0].text if med.dosageInstruction else None,
        "authored_on": str(med.authoredOn) if med.authoredOn else None,
        "patient_ref": med.subject.reference if med.subject else None,
    }
模式:
model_validate(json)
→ 按 system URI 提取,而不是依赖数组位置。
python
from fhir.resources.observation import Observation
from fhir.resources.condition import Condition
from fhir.resources.medicationrequest import MedicationRequest

def extract_observation(obs_json):
    obs = Observation.model_validate(obs_json)
    return {
        "id": obs.id, "status": obs.status,
        "loinc_code": next((c.code for c in (obs.code.coding or []) if c.system == "http://loinc.org"), None),
        "display": obs.code.coding[0].display if obs.code.coding else None,
        "value": obs.valueQuantity.value if obs.valueQuantity else None,
        "unit": obs.valueQuantity.unit if obs.valueQuantity else None,
        "effective_date": str(obs.effectiveDateTime) if obs.effectiveDateTime else None,
        "patient_ref": obs.subject.reference if obs.subject else None,
    }

def extract_condition(cond_json):
    cond = Condition.model_validate(cond_json)
    return {
        "id": cond.id,
        "status": cond.clinicalStatus.coding[0].code if cond.clinicalStatus and cond.clinicalStatus.coding else None,
        "snomed_code": next((c.code for c in (cond.code.coding or []) if c.system and "snomed" in c.system), None),
        "icd10_code": next((c.code for c in (cond.code.coding or []) if c.system and "icd-10" in c.system), None),
        "display": cond.code.coding[0].display if cond.code.coding else None,
        "onset": str(cond.onsetDateTime) if cond.onsetDateTime else None,
        "patient_ref": cond.subject.reference if cond.subject else None,
    }

def extract_medication_request(med_json):
    med = MedicationRequest.model_validate(med_json)
    cc = med.medicationCodeableConcept
    return {
        "id": med.id, "status": med.status,
        "rxnorm_code": cc.coding[0].code if cc and cc.coding else None,
        "display": cc.coding[0].display if cc and cc.coding else None,
        "dosage_text": med.dosageInstruction[0].text if med.dosageInstruction else None,
        "authored_on": str(med.authoredOn) if med.authoredOn else None,
        "patient_ref": med.subject.reference if med.subject else None,
    }

4. FHIR Bundle Flattening

4. FHIR Bundle 扁平化

python
import json
import pandas as pd
from fhir.resources.bundle import Bundle

def flatten_observation_bundle(bundle_json):
    """Extract all Observation resources from a FHIR Bundle into a DataFrame."""
    bundle = Bundle.model_validate(bundle_json)
    rows = [extract_observation(entry.resource.model_dump())
            for entry in (bundle.entry or [])
            if entry.resource.resource_type == "Observation"]
    return pd.DataFrame(rows)

with open("observations_bundle.json") as f:
    df = flatten_observation_bundle(json.load(f))
python
import json
import pandas as pd
from fhir.resources.bundle import Bundle

def flatten_observation_bundle(bundle_json):
    """Extract all Observation resources from a FHIR Bundle into a DataFrame."""
    bundle = Bundle.model_validate(bundle_json)
    rows = [extract_observation(entry.resource.model_dump())
            for entry in (bundle.entry or [])
            if entry.resource.resource_type == "Observation"]
    return pd.DataFrame(rows)

with open("observations_bundle.json") as f:
    df = flatten_observation_bundle(json.load(f))

5. Data Quality Checks

5. 数据质量检查

python
import pandas as pd
from datetime import datetime

def check_completeness(df, required_columns):
    """Report missing value rates for required columns."""
    report = {}
    for col in required_columns:
        if col not in df.columns:
            report[col] = {"status": "MISSING_COLUMN", "pct_complete": 0.0}
        else:
            pct = (df[col].notna().sum() / len(df)) * 100
            report[col] = {
                "status": "OK" if pct >= 95 else "WARNING" if pct >= 80 else "FAIL",
                "pct_complete": round(pct, 1),
            }
    return report

def validate_dates(series, fmt="%Y-%m-%d", min_date="1900-01-01", max_date=None):
    """Validate date strings. Returns indices of invalid entries."""
    max_dt = datetime.strptime(max_date, fmt) if max_date else datetime.now()
    min_dt = datetime.strptime(min_date, fmt)
    invalid = []
    for idx, val in series.items():
        if pd.isna(val):
            continue
        try:
            dt = datetime.strptime(str(val)[:10], fmt)
            if dt < min_dt or dt > max_dt:
                invalid.append({"index": idx, "value": val, "reason": "out_of_range"})
        except ValueError:
            invalid.append({"index": idx, "value": val, "reason": "parse_error"})
    return invalid

def verify_code_system(df, code_col, valid_codes, system_name="unknown"):
    """Check that coded values belong to an expected set."""
    if code_col not in df.columns:
        return {"status": "MISSING_COLUMN", "invalid_count": 0}
    codes = df[code_col].dropna().unique()
    invalid = [c for c in codes if c not in valid_codes]
    return {
        "system": system_name, "total_unique": len(codes),
        "invalid_codes": invalid, "invalid_count": len(invalid),
        "status": "OK" if len(invalid) == 0 else "FAIL",
    }
python
import pandas as pd
from datetime import datetime

def check_completeness(df, required_columns):
    """Report missing value rates for required columns."""
    report = {}
    for col in required_columns:
        if col not in df.columns:
            report[col] = {"status": "MISSING_COLUMN", "pct_complete": 0.0}
        else:
            pct = (df[col].notna().sum() / len(df)) * 100
            report[col] = {
                "status": "OK" if pct >= 95 else "WARNING" if pct >= 80 else "FAIL",
                "pct_complete": round(pct, 1),
            }
    return report

def validate_dates(series, fmt="%Y-%m-%d", min_date="1900-01-01", max_date=None):
    """Validate date strings. Returns indices of invalid entries."""
    max_dt = datetime.strptime(max_date, fmt) if max_date else datetime.now()
    min_dt = datetime.strptime(min_date, fmt)
    invalid = []
    for idx, val in series.items():
        if pd.isna(val):
            continue
        try:
            dt = datetime.strptime(str(val)[:10], fmt)
            if dt < min_dt or dt > max_dt:
                invalid.append({"index": idx, "value": val, "reason": "out_of_range"})
        except ValueError:
            invalid.append({"index": idx, "value": val, "reason": "parse_error"})
    return invalid

def verify_code_system(df, code_col, valid_codes, system_name="unknown"):
    """Check that coded values belong to an expected set."""
    if code_col not in df.columns:
        return {"status": "MISSING_COLUMN", "invalid_count": 0}
    codes = df[code_col].dropna().unique()
    invalid = [c for c in codes if c not in valid_codes]
    return {
        "system": system_name, "total_unique": len(codes),
        "invalid_codes": invalid, "invalid_count": len(invalid),
        "status": "OK" if len(invalid) == 0 else "FAIL",
    }

Quality check thresholds

质量检查阈值

CheckPassWarningFail
Completeness (required field)≥ 95%80–95%< 80%
Date validity0 parse errors—Any parse error
Date rangeAll within expected range—Future dates or pre-1900
Code system membershipAll codes valid—Any unrecognized code
Duplicate patient IDs0 exact duplicates—Any duplicate MRN with conflicting demographics
检查项通过警告失败
完整性(必填字段)≥ 95%80–95%< 80%
日期有效性0 个解析错误—存在任何解析错误
日期范围全部处于预期范围内—存在未来日期或早于 1900 年的日期
代码系统成员资格所有代码均有效—存在任何无法识别的代码
重复患者 ID0 个完全重复项—存在任何 MRN 重复且基本信息冲突的情况

6. HL7v2 to FHIR Conceptual Mapping

6. HL7v2 到 FHIR 的概念映射

HL7v2 SegmentFHIR ResourceKey fields
PIDPatientidentifier, name, birthDate, gender
PV1Encounterclass, period, participant
OBR + OBXObservationcode, value, effectiveDateTime, status
DG1Conditioncode, onsetDateTime, clinicalStatus
ORC + RXEMedicationRequestmedicationCodeableConcept, dosageInstruction
AL1AllergyIntolerancecode, reaction, criticality
HL7v2 SegmentFHIR Resource关键字段
PIDPatientidentifier、name、birthDate、gender
PV1Encounterclass、period、participant
OBR + OBXObservationcode、value、effectiveDateTime、status
DG1Conditioncode、onsetDateTime、clinicalStatus
ORC + RXEMedicationRequestmedicationCodeableConcept、dosageInstruction
AL1AllergyIntolerancecode、reaction、criticality

Common Mistakes

常见错误

  • Wrong: Assuming fixed field positions across all HL7v2 versions without checking for empty segments Right: Always check for empty segments before accessing subfields — field positions are stable but optional fields may be absent Why: Accessing a subfield of an empty segment raises index errors or returns incorrect data
  • Wrong: Splitting HL7v2 messages on
    \n
    instead of
    \r
    Right: Normalize segment delimiters to
    \r
    before parsing — many files use
    \r\n
    or
    \n
    Why: The HL7v2 standard segment delimiter is carriage return (
    \r
    ); incorrect splitting produces malformed segments
  • Wrong: Parsing OBX-5 without checking the value type in OBX-2 Right: Always check OBX-2 first — numeric (NM) and coded (CE) results require different parsing logic Why: Misinterpreting a coded entry as numeric (or vice versa) produces corrupt data
  • Wrong: Treating all OBX segments as lab results Right: Filter by OBR context or OBX-3 code system to distinguish labs from vitals, imaging impressions, and clinical notes Why: OBX carries many observation types; assuming all are labs leads to mixed, unusable datasets
  • Wrong: Hardcoding FHIR resource field paths (e.g., always accessing
    effectiveDateTime
    ) Right: Check which choice type variant is present (e.g.,
    effectiveDateTime
    vs
    effectivePeriod
    ,
    valueQuantity
    vs
    valueCodeableConcept
    ) Why: FHIR allows choice types — hardcoded paths raise AttributeError or miss data when the alternate variant is used
  • Wrong: Including preliminary Observations (
    status: "preliminary"
    ) in analysis datasets Right: Filter on
    status: "final"
    for analysis — preliminary results may be superseded Why: Preliminary results can be corrected or replaced, leading to duplicate or inaccurate data in analyses
  • Wrong: Extracting coded values by array position (e.g.,
    coding[0]
    ) instead of by system URI Right: Extract by system URI (e.g., filter for
    "http://snomed.info/sct"
    ) — a Condition may carry both SNOMED and ICD-10 codings Why: Array order is not guaranteed; position-based access returns the wrong code system unpredictably
  • Wrong: Processing only the first page of a FHIR Bundle without checking for pagination Right: Check
    bundle.link
    for
    relation: "next"
    and follow all pages until exhausted Why: FHIR servers return paginated Bundles — stopping at page one misses most of the data
  • Wrong: Assuming all dates are ISO 8601 format across HL7v2 and FHIR Right: Parse HL7v2 dates as
    YYYYMMDD[HHmmss]
    and FHIR dates with variable precision (
    2024
    ,
    2024-01
    ,
    2024-01-15
    ) Why: Format mismatches cause parse errors or silent data corruption
  • Wrong: Accepting coded values without validating the code system URI Right: Verify that the
    system
    field matches the expected URI (e.g.,
    http://loinc.org
    for LOINC codes) Why: Codes without system URIs are ambiguous — a "6690-2" without system confirmation may not be LOINC
  • 错误: 在未检查空 segment 的情况下,假定所有 HL7v2 版本中的字段位置都固定 正确: 访问子字段前始终检查 segment 是否为空——字段位置虽然稳定,但可选字段可能不存在 原因: 访问空 segment 的子字段会引发索引错误或返回错误数据
  • 错误: 使用
    \n
    而不是
    \r
    拆分 HL7v2 消息 正确: 解析前将 segment 分隔符规范化为
    \r
    ——许多文件使用
    \r\n
    或
    \n
    原因: HL7v2 标准中的 segment 分隔符是回车符(
    \r
    );拆分方式不正确会生成格式错误的 segment
  • 错误: 解析 OBX-5 前不检查 OBX-2 中的值类型 正确: 始终先检查 OBX-2——数值(NM)和编码值(CE)结果需要不同的解析逻辑 原因: 将编码条目误认为数值(或反之)会破坏数据
  • 错误: 将所有 OBX segment 都视为实验室结果 正确: 按 OBR 上下文或 OBX-3 代码系统筛选,以区分实验室结果、生命体征、影像检查结论和临床记录 原因: OBX 可承载多种观察类型;将所有内容都视为实验室结果会混合出不可用的数据集
  • 错误: 硬编码 FHIR 资源字段路径(例如,始终访问
    effectiveDateTime
    ) 正确: 检查当前存在的是哪种 choice type(例如
    effectiveDateTime
    与
    effectivePeriod
    ,或
    valueQuantity
    与
    valueCodeableConcept
    ) 原因: FHIR 允许 choice type——硬编码路径会引发 AttributeError,或在使用其他变体时遗漏数据
  • 错误: 将初步 Observation(
    status: "preliminary"
    )纳入分析数据集 正确: 分析时按
    status: "final"
    筛选——初步结果可能会被替代 原因: 初步结果可能被更正或替换,从而导致分析中出现重复数据或不准确数据
  • 错误: 按数组位置提取编码值(例如使用
    coding[0]
    ),而不是按 system URI 提取 正确: 按 system URI 提取(例如筛选
    "http://snomed.info/sct"
    )——Condition 可能同时包含 SNOMED 和 ICD-10 编码 原因: 数组顺序不受保证;基于位置访问可能不可预测地返回错误的代码系统
  • 错误: 处理 FHIR Bundle 时只处理第一页,未检查分页 正确: 检查
    bundle.link
    中是否存在
    relation: "next"
    ,并持续跟踪所有页面直至遍历完毕 原因: FHIR server 返回分页 Bundle——停在第一页会遗漏大部分数据
  • 错误: 假定 HL7v2 和 FHIR 中的所有日期均为 ISO 8601 格式 正确: 将 HL7v2 日期解析为
    YYYYMMDD[HHmmss]
    ,将 FHIR 日期解析为可变精度(
    2024
    、
    2024-01
    、
    2024-01-15
    ) 原因: 格式不匹配会导致解析错误或静默数据损坏
  • 错误: 接受编码值但不验证代码系统 URI 正确: 验证
    system
    字段与预期 URI 匹配(例如 LOINC 代码使用
    http://loinc.org
    ) 原因: 没有 system URI 的代码含义不明确——未确认 system 的
    "6690-2"
    未必是 LOINC 代码

References

参考资料