Overview
Laboratory result standardization is essential for healthcare interoperability. This guide demonstrates how to map local laboratory tests to LOINC (Logical Observation Identifiers Names and Codes) using the OMOPHub API, enabling consistent interpretation of lab results across different healthcare systems.Use Case: Automatically map laboratory test codes and results to LOINC standards for data exchange, clinical decision support, and population health analytics.
Business Problem
Healthcare organizations face significant challenges with laboratory data:- Data Silos: Different labs use proprietary codes and naming conventions
- Interoperability Issues: Unable to aggregate lab data across systems
- Clinical Decision Support: Inconsistent reference ranges and units
- Quality Reporting: Difficulty in population health analytics
- Regulatory Compliance: HL7 FHIR and CMS requirements for standardized codes
Solution Architecture
Implementation Guide
Step 1: Set Up Laboratory Mapping Service
Copy
from omophub import OMOPHubClient
from typing import List, Dict, Any, Optional, Tuple
from dataclasses import dataclass
from decimal import Decimal
import re
import logging
@dataclass
class LabResult:
local_code: str
local_name: str
result_value: str
result_unit: str
reference_range: str
specimen_type: str
result_status: str
id: Optional[str] = None
collection_datetime: Optional[str] = None
result_datetime: Optional[str] = None
@dataclass
class StandardizedLabResult:
loinc_code: str
loinc_name: str
local_code: str
local_name: str
standardized_value: float
standardized_unit: str
standardized_reference_range: str
specimen_type: str
result_status: str
conversion_applied: bool
mapping_confidence: float
class LabResultMapper:
def __init__(self, api_key: str):
self.client = OMOPHubClient(api_key=api_key)
self.logger = logging.getLogger(__name__)
# Common unit conversions
self.unit_conversions = {
# Glucose: mg/dL to mmol/L
("glucose", "mg/dl", "mmol/l"): lambda x: x * 0.0555,
("glucose", "mmol/l", "mg/dl"): lambda x: x / 0.0555,
# Cholesterol: mg/dL to mmol/L
("cholesterol", "mg/dl", "mmol/l"): lambda x: x * 0.02586,
("cholesterol", "mmol/l", "mg/dl"): lambda x: x / 0.02586,
# Hemoglobin: g/dL to g/L
("hemoglobin", "g/dl", "g/l"): lambda x: x * 10,
("hemoglobin", "g/l", "g/dl"): lambda x: x / 10,
# Creatinine: mg/dL to μmol/L (clinical factor: 1 mg/dL = 88.4 μmol/L)
("creatinine", "mg/dl", "umol/l"): lambda x: x * 88.4,
("creatinine", "umol/l", "mg/dl"): lambda x: x * 0.0113,
}
# Common specimen types mapping
self.specimen_mapping = {
"serum": "SER",
"plasma": "PLAS",
"whole blood": "BLD",
"urine": "UR",
"cerebrospinal fluid": "CSF",
"saliva": "SAL"
}
def map_lab_result(self, lab_result: LabResult) -> Optional[StandardizedLabResult]:
"""Map a single lab result to LOINC standard"""
try:
# Step 1: Find LOINC code
loinc_mapping = self.find_loinc_code(lab_result)
if not loinc_mapping:
self.logger.warning(f"No LOINC mapping found for {lab_result.local_name}")
return None
# Step 2: Standardize units and values
standardized_value, standardized_unit = self.standardize_units(
lab_result.result_value,
lab_result.result_unit,
loinc_mapping["preferred_unit"],
lab_result.local_name # Use descriptive name for analyte identification
)
# Step 3: Standardize reference range
standardized_ref_range = self.standardize_reference_range(
lab_result.reference_range,
lab_result.result_unit,
standardized_unit,
loinc_mapping["loinc_name"]
)
return StandardizedLabResult(
loinc_code=loinc_mapping["loinc_code"],
loinc_name=loinc_mapping["loinc_name"],
local_code=lab_result.local_code,
local_name=lab_result.local_name,
standardized_value=standardized_value,
standardized_unit=standardized_unit,
standardized_reference_range=standardized_ref_range,
specimen_type=self.standardize_specimen_type(lab_result.specimen_type),
result_status=lab_result.result_status,
conversion_applied=lab_result.result_unit.lower() != standardized_unit.lower(),
mapping_confidence=loinc_mapping["confidence"]
)
except Exception as e:
self.logger.error(f"Error mapping lab result {lab_result.local_name}: {e}")
return None
def find_loinc_code(self, lab_result: LabResult) -> Optional[Dict[str, Any]]:
"""Find appropriate LOINC code for lab test"""
# Search strategies in order of preference
search_strategies = [
# Strategy 1: Exact local code lookup
{
"query": lab_result.local_code,
"vocabularies": ["LOINC"],
"domains": ["Measurement"],
"search_type": "code"
},
# Strategy 2: Test name search
{
"query": lab_result.local_name,
"vocabularies": ["LOINC"],
"domains": ["Measurement"],
"search_type": "name"
},
# Strategy 3: Broader search with specimen context
{
"query": f"{lab_result.local_name} {lab_result.specimen_type}",
"vocabularies": ["LOINC"],
"domains": ["Measurement"],
"search_type": "contextual"
}
]
for strategy in search_strategies:
try:
if strategy["search_type"] == "code":
# Direct code lookup
result = self.client.get_concept_by_code("LOINC", lab_result.local_code)
if result:
return {
"loinc_code": result["concept_code"],
"loinc_name": result["concept_name"],
"preferred_unit": self.extract_preferred_unit(result),
"confidence": 1.0
}
else:
# Text search
search_results = self.client.search_concepts({
"query": strategy["query"],
"vocabularies": strategy["vocabularies"],
"domains": strategy["domains"],
"standard_concepts_only": True,
"limit": 10
})
if search_results["concepts"]:
# Find best match considering specimen type
best_match = self.find_best_loinc_match(
search_results["concepts"],
lab_result
)
if best_match:
return {
"loinc_code": best_match["concept_code"],
"loinc_name": best_match["concept_name"],
"preferred_unit": self.extract_preferred_unit(best_match),
"confidence": best_match.get("relevance_score", 0.8)
}
except Exception as e:
self.logger.debug(f"Search strategy {strategy['search_type']} failed: {e}")
continue
return None
def find_best_loinc_match(self, loinc_concepts: List[Dict[str, Any]], lab_result: LabResult) -> Optional[Dict[str, Any]]:
"""Find best LOINC match considering context"""
scored_matches = []
for concept in loinc_concepts:
score = 0
concept_name = concept["concept_name"].lower()
local_name = lab_result.local_name.lower()
# Base relevance score
score += concept.get("relevance_score", 0.5)
# Specimen type matching bonus
specimen_keywords = {
"serum": ["ser", "serum"],
"plasma": ["plas", "plasma"],
"whole blood": ["bld", "blood", "whole"],
"urine": ["ur", "urine"],
"csf": ["csf", "cerebrospinal"]
}
specimen_type = lab_result.specimen_type.lower()
if specimen_type in specimen_keywords:
keywords = specimen_keywords[specimen_type]
if any(keyword in concept_name for keyword in keywords):
score += 0.2
# Method matching bonus
if any(method in concept_name for method in ["enzymatic", "immunoassay", "chromatography"]):
score += 0.1
scored_matches.append((score, concept))
if scored_matches:
# Sort by score and return best match
scored_matches.sort(key=lambda x: x[0], reverse=True)
return scored_matches[0][1]
return None
def extract_preferred_unit(self, loinc_concept: Dict[str, Any]) -> str:
"""Extract preferred unit from LOINC concept"""
concept_name = loinc_concept.get("concept_name", "")
# Common unit patterns in LOINC names
unit_patterns = {
r"mg/dL": "mg/dL",
r"mmol/L": "mmol/L",
r"g/dL": "g/dL",
r"g/L": "g/L",
r"μmol/L": "μmol/L",
r"umol/L": "μmol/L",
r"mEq/L": "mEq/L",
r"IU/L": "IU/L",
r"U/L": "U/L",
r"10\^9/L": "10^9/L",
r"10\^6/μL": "10^6/μL"
}
for pattern, unit in unit_patterns.items():
if re.search(pattern, concept_name, re.IGNORECASE):
return unit
# Default fallback
return "units"
Step 2: Unit Standardization and Conversion
Copy
def standardize_units(self, result_value: str, current_unit: str, preferred_unit: str, analyte_name: str) -> Tuple[float, str]:
"""Standardize lab result units
Args:
result_value: Numeric result value as string
current_unit: Original unit of measurement
preferred_unit: Target unit for standardization
analyte_name: Descriptive name/hint for the analyte (e.g., "glucose", "cholesterol")
"""
try:
# Parse numeric value
numeric_value = self.parse_numeric_value(result_value)
if numeric_value is None:
return float('nan'), current_unit
# Normalize unit strings
current_unit_norm = self.normalize_unit_string(current_unit)
preferred_unit_norm = self.normalize_unit_string(preferred_unit)
# If units are the same, no conversion needed
if current_unit_norm == preferred_unit_norm:
return numeric_value, preferred_unit
# Find conversion factor using analyte name
conversion_factor = self.find_unit_conversion(
analyte_name, current_unit_norm, preferred_unit_norm
)
if conversion_factor:
converted_value = conversion_factor(numeric_value)
return converted_value, preferred_unit
else:
# No conversion available, keep original
self.logger.warning(f"No conversion available from {current_unit} to {preferred_unit}")
return numeric_value, current_unit
except Exception as e:
self.logger.error(f"Error in unit standardization: {e}")
return float('nan'), current_unit
def parse_numeric_value(self, result_value: str) -> Optional[float]:
"""Extract numeric value from result string"""
# Handle common result formats
result_value = result_value.strip()
# Handle qualitative results
qualitative_mappings = {
"positive": 1.0,
"negative": 0.0,
"detected": 1.0,
"not detected": 0.0,
"reactive": 1.0,
"non-reactive": 0.0
}
if result_value.lower() in qualitative_mappings:
return qualitative_mappings[result_value.lower()]
# Handle numeric ranges (e.g., "5.0-10.0")
range_match = re.match(r'(\d+\.?\d*)\s*-\s*(\d+\.?\d*)', result_value)
if range_match:
# Take the midpoint of the range
lower = float(range_match.group(1))
upper = float(range_match.group(2))
return (lower + upper) / 2
# Handle comparison operators (e.g., ">100", "<0.5")
comparison_match = re.match(r'[<>=]+\s*(\d+\.?\d*)', result_value)
if comparison_match:
return float(comparison_match.group(1))
# Handle standard numeric values
numeric_match = re.search(r'(\d+\.?\d*)', result_value)
if numeric_match:
return float(numeric_match.group(1))
return None
def normalize_unit_string(self, unit: str) -> str:
"""Normalize unit string for comparison and handle micro symbols"""
if not unit:
return ""
# First handle micro symbols
unit_cleaned = unit.replace("μ", "u").replace("µ", "u")
normalized = unit_cleaned.strip().lower()
# Common unit normalizations
normalizations = {
"mg/dl": "mg/dl",
"mg/dL": "mg/dl",
"MG/DL": "mg/dl",
"mmol/l": "mmol/l",
"mmol/L": "mmol/l",
"MMOL/L": "mmol/l",
"g/dl": "g/dl",
"g/dL": "g/dl",
"G/DL": "g/dl",
"g/l": "g/l",
"g/L": "g/l",
"G/L": "g/l",
# Micro symbol variants (all converted to u)
"umol/l": "umol/l",
"umol/L": "umol/l",
"UMOL/L": "umol/l",
"μmol/l": "umol/l",
"μmol/L": "umol/l",
"µmol/l": "umol/l",
"µmol/L": "umol/l",
# Microgram variants
"mcg": "ug",
"μg": "ug",
"µg": "ug",
"ug": "ug"
}
# Apply direct mappings
for original, standard in normalizations.items():
if normalized == original.lower():
return standard
# Handle special characters
normalized = normalized.replace("μ", "u").replace("µ", "u")
return normalized
def find_unit_conversion(self, result_name: str, from_unit: str, to_unit: str) -> Optional[callable]:
"""Find appropriate unit conversion function"""
# Determine analyte type from result name
analyte_type = self.determine_analyte_type(result_name)
# Look up conversion
conversion_key = (analyte_type, from_unit, to_unit)
return self.unit_conversions.get(conversion_key)
def determine_analyte_type(self, result_name: str) -> str:
"""Determine analyte type for unit conversion"""
name_lower = result_name.lower()
# Glucose variants
if any(term in name_lower for term in ["glucose", "blood sugar", "bg"]):
return "glucose"
# Cholesterol variants
if any(term in name_lower for term in ["cholesterol", "chol", "ldl", "hdl"]):
return "cholesterol"
# Hemoglobin variants
if any(term in name_lower for term in ["hemoglobin", "hgb", "hb"]):
return "hemoglobin"
# Creatinine variants
if any(term in name_lower for term in ["creatinine", "creat", "cr"]):
return "creatinine"
# Default
return "unknown"
def standardize_reference_range(self, reference_range: str, original_unit: str,
new_unit: str, analyte_name: str) -> str:
"""Standardize reference range with unit conversion"""
if not reference_range or original_unit.lower() == new_unit.lower():
return reference_range
try:
# Parse reference range (e.g., "3.5-5.0", "< 100", "> 10")
range_patterns = [
r'(\d+\.?\d*)\s*-\s*(\d+\.?\d*)', # Range: "3.5-5.0"
r'<\s*(\d+\.?\d*)', # Less than: "< 100"
r'>\s*(\d+\.?\d*)', # Greater than: "> 10"
r'(\d+\.?\d*)' # Single value: "100"
]
for pattern in range_patterns:
match = re.search(pattern, reference_range)
if match:
# Get conversion function
analyte_type = self.determine_analyte_type(analyte_name)
original_unit_norm = self.normalize_unit_string(original_unit)
new_unit_norm = self.normalize_unit_string(new_unit)
conversion_func = self.unit_conversions.get(
(analyte_type, original_unit_norm, new_unit_norm)
)
if conversion_func:
if len(match.groups()) == 2: # Range
lower = conversion_func(float(match.group(1)))
upper = conversion_func(float(match.group(2)))
return f"{lower:.2f}-{upper:.2f}"
else: # Single value or comparison
converted = conversion_func(float(match.group(1)))
if '<' in reference_range:
return f"< {converted:.2f}"
elif '>' in reference_range:
return f"> {converted:.2f}"
else:
return f"{converted:.2f}"
break
return reference_range # Return original if no conversion possible
except Exception as e:
self.logger.error(f"Error standardizing reference range: {e}")
return reference_range
def standardize_specimen_type(self, specimen_type: str) -> str:
"""Standardize specimen type to common abbreviations"""
if not specimen_type:
return "Unknown"
specimen_lower = specimen_type.lower().strip()
return self.specimen_mapping.get(specimen_lower, specimen_type)
Step 3: Batch Lab Processing and FHIR Generation
Copy
import math
from datetime import datetime
def process_lab_batch(self, lab_results: List[LabResult], patient_id: str) -> Dict[str, Any]:
"""Process multiple lab results for a patient"""
processed_results = []
mapping_summary = {
"total_results": len(lab_results),
"successful_mappings": 0,
"failed_mappings": 0,
"unit_conversions": 0,
"confidence_scores": []
}
for lab_result in lab_results:
try:
standardized = self.map_lab_result(lab_result)
if standardized:
processed_results.append(standardized)
mapping_summary["successful_mappings"] += 1
mapping_summary["confidence_scores"].append(standardized.mapping_confidence)
if standardized.conversion_applied:
mapping_summary["unit_conversions"] += 1
else:
mapping_summary["failed_mappings"] += 1
except Exception as e:
self.logger.error(f"Error processing lab result {lab_result.local_name}: {e}")
mapping_summary["failed_mappings"] += 1
# Generate FHIR resources
fhir_resources = self.generate_fhir_resources(processed_results, patient_id)
# Calculate quality metrics
quality_metrics = self.calculate_quality_metrics(processed_results, mapping_summary)
return {
"patient_id": patient_id,
"processed_results": processed_results,
"fhir_resources": fhir_resources,
"mapping_summary": mapping_summary,
"quality_metrics": quality_metrics,
"processed_at": datetime.now().isoformat()
}
def generate_fhir_resources(self, standardized_results: List[StandardizedLabResult],
patient_id: str) -> List[Dict[str, Any]]:
"""Generate FHIR Specimen and Observation resources for lab results"""
fhir_resources = []
for result in standardized_results:
# Generate deterministic specimen ID
if hasattr(result, 'id') and result.id:
specimen_id = f"{result.id}-specimen"
else:
# Create deterministic ID from patient + local_code + timestamp
specimen_id = f"spec-{patient_id}-{result.local_code}-{int(datetime.now().timestamp())}"
# Create FHIR Specimen resource first
specimen = {
"resourceType": "Specimen",
"id": specimen_id,
"subject": {
"reference": f"Patient/{patient_id}"
},
"type": {
"coding": [{
"system": "http://snomed.info/sct",
"code": self.get_snomed_specimen_code(result.specimen_type),
"display": result.specimen_type
}]
},
"collection": {
"collectedDateTime": (
getattr(result, 'collection_datetime', None) or
getattr(result, 'result_datetime', None) or
datetime.now().isoformat()
)
}
}
# Add specimen to resources
fhir_resources.append(specimen)
# Determine value type
if isinstance(result.standardized_value, (int, float)) and not math.isnan(result.standardized_value):
value_element = {
"valueQuantity": {
"value": result.standardized_value,
"unit": result.standardized_unit,
"system": "http://unitsofmeasure.org",
"code": self.get_ucum_code(result.standardized_unit)
}
}
else:
# Handle qualitative results
value_element = {
"valueString": str(result.standardized_value)
}
# Create FHIR Observation resource
observation = {
"resourceType": "Observation",
"id": f"lab-{result.local_code}-{int(datetime.now().timestamp())}",
"status": self.map_result_status(result.result_status),
"category": [{
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/observation-category",
"code": "laboratory",
"display": "Laboratory"
}]
}],
"code": {
"coding": [
{
"system": "http://loinc.org",
"code": result.loinc_code,
"display": result.loinc_name
},
{
"system": "http://example.org/local-codes",
"code": result.local_code,
"display": result.local_name
}
]
},
"subject": {
"reference": f"Patient/{patient_id}"
},
"specimen": {
"reference": f"Specimen/{specimen_id}",
"display": result.specimen_type
},
**value_element
}
# Add reference range if available
if result.standardized_reference_range and result.standardized_reference_range != "":
ref_range = self.parse_reference_range(result.standardized_reference_range)
if ref_range:
observation["referenceRange"] = [{
"low": {"value": ref_range["low"], "unit": result.standardized_unit} if ref_range.get("low") else None,
"high": {"value": ref_range["high"], "unit": result.standardized_unit} if ref_range.get("high") else None,
"type": {
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/referencerange-meaning",
"code": "normal",
"display": "Normal Range"
}]
}
}]
fhir_resources.append(observation)
return fhir_resources
def get_snomed_specimen_code(self, specimen_type: str) -> str:
"""Get SNOMED code for specimen type"""
specimen_mappings = {
"serum": "119364003",
"plasma": "119361006",
"whole blood": "119297000",
"urine": "122575003",
"cerebrospinal fluid": "258450006",
"saliva": "119342007",
"tissue": "119376003"
}
return specimen_mappings.get(specimen_type.lower(), "123038009") # Default: specimen
def parse_reference_range(self, range_str: str) -> Optional[Dict[str, float]]:
"""Parse reference range string into low/high values"""
if not range_str:
return None
import re
# Handle various reference range formats
patterns = [
(r'^([\d.]+)\s*-\s*([\d.]+)$', 'range'), # "70-100"
(r'^<\s*([\d.]+)$', 'upper'), # "<200"
(r'^>\s*([\d.]+)$', 'lower'), # ">5"
(r'^([\d.]+)\s*-\s*$', 'lower_only'), # "70-"
(r'^\s*-\s*([\d.]+)$', 'upper_only') # "-100"
]
for pattern, range_type in patterns:
match = re.match(pattern, range_str.strip())
if match:
if range_type == 'range':
return {
'low': float(match.group(1)),
'high': float(match.group(2))
}
elif range_type == 'upper':
return {'low': None, 'high': float(match.group(1))}
elif range_type == 'lower':
return {'low': float(match.group(1)), 'high': None}
elif range_type == 'lower_only':
return {'low': float(match.group(1)), 'high': None}
elif range_type == 'upper_only':
return {'low': None, 'high': float(match.group(1))}
return None
def get_ucum_code(self, unit: str) -> str:
"""Get UCUM code for unit"""
ucum_mappings = {
"mg/dL": "mg/dL",
"mmol/L": "mmol/L",
"g/dL": "g/dL",
"g/L": "g/L",
"μmol/L": "umol/L",
"mEq/L": "meq/L",
"IU/L": "[IU]/L",
"U/L": "U/L"
}
return ucum_mappings.get(unit, unit)
def map_result_status(self, local_status: str) -> str:
"""Map local result status to FHIR status"""
status_mappings = {
"final": "final",
"complete": "final",
"resulted": "final",
"preliminary": "preliminary",
"pending": "preliminary",
"corrected": "amended",
"amended": "amended",
"cancelled": "cancelled"
}
return status_mappings.get(local_status.lower(), "final")
def calculate_quality_metrics(self, results: List[StandardizedLabResult],
mapping_summary: Dict[str, Any]) -> Dict[str, Any]:
"""Calculate mapping quality metrics"""
if not results:
return {"overall_quality": 0.0}
# Calculate average confidence
confidence_scores = mapping_summary.get("confidence_scores", [])
avg_confidence = sum(confidence_scores) / len(confidence_scores) if confidence_scores else 0.0
# Calculate mapping success rate
total_results = mapping_summary["total_results"]
success_rate = mapping_summary["successful_mappings"] / total_results if total_results > 0 else 0.0
# Calculate unit standardization rate
unit_conversion_rate = mapping_summary["unit_conversions"] / len(results) if results else 0.0
# Overall quality score (weighted average)
overall_quality = (
avg_confidence * 0.4 +
success_rate * 0.4 +
unit_conversion_rate * 0.2
)
return {
"overall_quality": overall_quality,
"average_confidence": avg_confidence,
"mapping_success_rate": success_rate,
"unit_conversion_rate": unit_conversion_rate,
"total_processed": len(results),
"quality_grade": self.get_quality_grade(overall_quality)
}
def get_quality_grade(self, quality_score: float) -> str:
"""Convert quality score to letter grade"""
if quality_score >= 0.9:
return "A"
elif quality_score >= 0.8:
return "B"
elif quality_score >= 0.7:
return "C"
elif quality_score >= 0.6:
return "D"
else:
return "F"
Example Implementation
Sample Lab Results
Copy
Lab Results for Patient ID: PT12345
1. Glucose: 126 mg/dL (Ref: 70-100 mg/dL) - Serum
2. Total Cholesterol: 245 mg/dL (Ref: <200 mg/dL) - Serum
3. Hemoglobin: 12.5 g/dL (Ref: 12.0-15.5 g/dL) - Whole Blood
4. Creatinine: 1.2 mg/dL (Ref: 0.7-1.3 mg/dL) - Serum
5. Urinalysis Protein: Trace (Ref: Negative) - Urine
Processing Report
Copy
# Initialize the mapper
mapper = LabResultMapper("your_api_key")
# Sample lab results
lab_results = [
LabResult(
local_code="GLUC",
local_name="Glucose",
result_value="126",
result_unit="mg/dL",
reference_range="70-100",
specimen_type="serum",
result_status="final"
),
LabResult(
local_code="CHOL",
local_name="Total Cholesterol",
result_value="245",
result_unit="mg/dL",
reference_range="<200",
specimen_type="serum",
result_status="final"
),
LabResult(
local_code="HGB",
local_name="Hemoglobin",
result_value="12.5",
result_unit="g/dL",
reference_range="12.0-15.5",
specimen_type="whole blood",
result_status="final"
),
LabResult(
local_code="CREAT",
local_name="Creatinine",
result_value="1.2",
result_unit="mg/dL",
reference_range="0.7-1.3",
specimen_type="serum",
result_status="final"
)
]
# Process batch
report = mapper.process_lab_batch(lab_results, "PT12345")
# Print results
print("=== LABORATORY RESULT MAPPING REPORT ===")
print(f"Patient ID: {report['patient_id']}")
print(f"Quality Grade: {report['quality_metrics']['quality_grade']}")
print(f"Mapping Success Rate: {report['quality_metrics']['mapping_success_rate']:.1%}")
print("\n=== STANDARDIZED RESULTS ===")
for result in report['processed_results']:
print(f"✅ {result.local_name}")
print(f" LOINC: {result.loinc_code} - {result.loinc_name}")
print(f" Value: {result.standardized_value} {result.standardized_unit}")
print(f" Conversion Applied: {'Yes' if result.conversion_applied else 'No'}")
print(f" Confidence: {result.mapping_confidence:.1%}")
print()
print("=== FHIR RESOURCES GENERATED ===")
print(f"Generated {len(report['fhir_resources'])} FHIR Observation resources")
for resource in report['fhir_resources'][:2]: # Show first 2
print(f" Resource ID: {resource['id']}")
print(f" LOINC Code: {resource['code']['coding'][0]['code']}")
print(f" Value: {resource.get('valueQuantity', resource.get('valueString', 'N/A'))}")
print()
Expected Output
Copy
=== LABORATORY RESULT MAPPING REPORT ===
Patient ID: PT12345
Quality Grade: A
Mapping Success Rate: 100.0%
=== STANDARDIZED RESULTS ===
✅ Glucose
LOINC: 2345-7 - Glucose [Mass/volume] in Serum or Plasma
Value: 126.0 mg/dL
Conversion Applied: No
Confidence: 95.0%
✅ Total Cholesterol
LOINC: 2093-3 - Cholesterol [Mass/volume] in Serum or Plasma
Value: 245.0 mg/dL
Conversion Applied: No
Confidence: 92.0%
✅ Hemoglobin
LOINC: 718-7 - Hemoglobin [Mass/volume] in Blood
Value: 12.5 g/dL
Conversion Applied: No
Confidence: 98.0%
✅ Creatinine
LOINC: 2160-0 - Creatinine [Mass/volume] in Serum or Plasma
Value: 1.2 mg/dL
Conversion Applied: No
Confidence: 96.0%
=== FHIR RESOURCES GENERATED ===
Generated 4 FHIR Observation resources
Resource ID: lab-GLUC-1703123456789
LOINC Code: 2345-7
Value: {value: 126.0, unit: 'mg/dL'}
Resource ID: lab-CHOL-1703123456790
LOINC Code: 2093-3
Value: {value: 245.0, unit: 'mg/dL'}
Integration Patterns
1. EHR Integration with HL7 FHIR
Copy
import requests
class FHIRLabIntegration:
def __init__(self, omophub_api_key: str, fhir_server_url: str):
self.mapper = LabResultMapper(omophub_api_key)
self.fhir_server = fhir_server_url
def sync_lab_results(self, patient_id: str, local_results: List[LabResult]) -> Dict[str, Any]:
"""Sync local lab results to FHIR server"""
# Process and standardize results
report = self.mapper.process_lab_batch(local_results, patient_id)
# Upload FHIR resources
upload_results = []
for fhir_resource in report['fhir_resources']:
try:
response = self.upload_fhir_observation(fhir_resource)
upload_results.append({
"resource_id": fhir_resource['id'],
"status": "success",
"fhir_id": response.get('id'),
"server_url": f"{self.fhir_server}/Observation/{response.get('id')}"
})
except Exception as e:
upload_results.append({
"resource_id": fhir_resource['id'],
"status": "failed",
"error": str(e)
})
return {
"processing_report": report,
"fhir_upload_results": upload_results,
"summary": {
"total_resources": len(report['fhir_resources']),
"successful_uploads": len([r for r in upload_results if r['status'] == 'success']),
"failed_uploads": len([r for r in upload_results if r['status'] == 'failed'])
}
}
def upload_fhir_observation(self, observation: Dict[str, Any]) -> Dict[str, Any]:
"""Upload FHIR Observation to server"""
headers = {
'Content-Type': 'application/fhir+json',
'Accept': 'application/fhir+json'
}
response = requests.post(
f"{self.fhir_server}/Observation",
json=observation,
headers=headers
)
if response.status_code in [200, 201]:
return response.json()
else:
raise Exception(f"FHIR upload failed: {response.status_code} - {response.text}")
2. Population Health Analytics
Copy
import math
import numpy as np
class PopulationHealthAnalytics:
def __init__(self, lab_mapper: LabResultMapper):
self.mapper = lab_mapper
def analyze_population_trends(self, lab_data: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Analyze population health trends from standardized lab data"""
# Group results by LOINC code
grouped_results = {}
for patient_data in lab_data:
patient_id = patient_data['patient_id']
for result in patient_data['standardized_results']:
loinc_code = result['loinc_code']
if loinc_code not in grouped_results:
grouped_results[loinc_code] = {
'test_name': result['loinc_name'],
'values': [],
'patients': set()
}
if not math.isnan(result['standardized_value']):
grouped_results[loinc_code]['values'].append(result['standardized_value'])
grouped_results[loinc_code]['patients'].add(patient_id)
# Calculate statistics for each test
analytics_report = {
'test_analytics': {},
'population_summary': {},
'risk_indicators': []
}
for loinc_code, data in grouped_results.items():
if len(data['values']) < 10: # Skip tests with insufficient data
continue
values = np.array(data['values'])
test_stats = {
'loinc_code': loinc_code,
'test_name': data['test_name'],
'patient_count': len(data['patients']),
'result_count': len(values),
'mean': np.mean(values),
'median': np.median(values),
'std_dev': np.std(values),
'percentiles': {
'p25': np.percentile(values, 25),
'p75': np.percentile(values, 75),
'p95': np.percentile(values, 95)
},
'abnormal_percentage': self.calculate_abnormal_percentage(loinc_code, values)
}
analytics_report['test_analytics'][loinc_code] = test_stats
# Identify risk indicators
if test_stats['abnormal_percentage'] > 20: # >20% abnormal results
analytics_report['risk_indicators'].append({
'test_name': data['test_name'],
'loinc_code': loinc_code,
'abnormal_percentage': test_stats['abnormal_percentage'],
'risk_level': 'HIGH' if test_stats['abnormal_percentage'] > 40 else 'MEDIUM'
})
# Population summary
analytics_report['population_summary'] = {
'total_patients': len(set().union(*[data['patients'] for data in grouped_results.values()])),
'total_tests': len(grouped_results),
'total_results': sum(len(data['values']) for data in grouped_results.values()),
'high_risk_indicators': len([r for r in analytics_report['risk_indicators'] if r['risk_level'] == 'HIGH'])
}
return analytics_report
def calculate_abnormal_percentage(self, loinc_code: str, values: np.ndarray) -> float:
"""Calculate percentage of abnormal results based on reference ranges"""
# This would typically use established reference ranges for each LOINC code
# For demonstration, using simplified thresholds
reference_ranges = {
'2345-7': (70, 100), # Glucose mg/dL
'2093-3': (0, 200), # Total Cholesterol mg/dL
'718-7': (12.0, 15.5), # Hemoglobin g/dL
'2160-0': (0.7, 1.3) # Creatinine mg/dL
}
if loinc_code not in reference_ranges:
return 0.0
low, high = reference_ranges[loinc_code]
abnormal_count = np.sum((values < low) | (values > high))
return (abnormal_count / len(values)) * 100 if len(values) > 0 else 0.0
Best Practices
1. Quality Assurance and Validation
Copy
import math
class LabMappingValidator:
def __init__(self):
self.validation_rules = {
'glucose': {
'reasonable_range': (20, 800), # mg/dL
'critical_values': {'low': 40, 'high': 400}
},
'cholesterol': {
'reasonable_range': (50, 1000), # mg/dL
'critical_values': {'low': 100, 'high': 500}
},
'hemoglobin': {
'reasonable_range': (3.0, 25.0), # g/dL
'critical_values': {'low': 7.0, 'high': 20.0}
}
}
def validate_mapping_result(self, result: StandardizedLabResult) -> Dict[str, Any]:
"""Validate a standardized lab result"""
validation_result = {
'is_valid': True,
'warnings': [],
'errors': [],
'quality_score': 1.0
}
# Check mapping confidence
if result.mapping_confidence < 0.7:
validation_result['warnings'].append(
f"Low mapping confidence: {result.mapping_confidence:.1%}"
)
validation_result['quality_score'] -= 0.2
# Check value reasonableness
if not math.isnan(result.standardized_value):
analyte_type = self.determine_analyte_type(result.loinc_name)
if analyte_type in self.validation_rules:
rules = self.validation_rules[analyte_type]
low, high = rules['reasonable_range']
if not (low <= result.standardized_value <= high):
validation_result['errors'].append(
f"Value {result.standardized_value} outside reasonable range ({low}-{high})"
)
validation_result['is_valid'] = False
# Check critical values
critical = rules.get('critical_values', {})
if 'low' in critical and result.standardized_value < critical['low']:
validation_result['warnings'].append(
f"Critical low value: {result.standardized_value} < {critical['low']}"
)
elif 'high' in critical and result.standardized_value > critical['high']:
validation_result['warnings'].append(
f"Critical high value: {result.standardized_value} > {critical['high']}"
)
# Check unit consistency
expected_units = self.get_expected_units(result.loinc_code)
if expected_units and result.standardized_unit not in expected_units:
validation_result['warnings'].append(
f"Unexpected unit: {result.standardized_unit}, expected one of {expected_units}"
)
validation_result['quality_score'] -= 0.1
# Calculate final quality score
if validation_result['errors']:
validation_result['quality_score'] = 0.0
elif len(validation_result['warnings']) > 2:
validation_result['quality_score'] -= 0.3
return validation_result
def get_expected_units(self, loinc_code: str) -> Optional[List[str]]:
"""Get expected units for a LOINC code"""
unit_mappings = {
'2345-7': ['mg/dL', 'mmol/L'], # Glucose
'2093-3': ['mg/dL', 'mmol/L'], # Cholesterol
'718-7': ['g/dL', 'g/L'], # Hemoglobin
'2160-0': ['mg/dL', 'μmol/L'] # Creatinine
}
return unit_mappings.get(loinc_code)
Next Steps
Clinical Trial Eligibility
Screen patients for clinical trial eligibility using lab criteria
Population Health Analytics
Analyze population health trends using standardized lab data
Drug Interaction Checking
Check drug interactions based on lab values
FHIR Integration
Complete FHIR integration patterns for labs