Case study · HL7 v2 · Public-health lab reporting

Caso · HL7 v2 · Reporte de laboratorio en salud pública

HL7 v2.x Infectious Disease Parser

Analizador HL7 v2.x de enfermedades infecciosas

Turns raw ORU^R01 lab messages into a validated, human-readable summary across nine infectious-disease categories — with a tested Python engine and a bilingual browser demo that apply the same rules.

Convierte mensajes de laboratorio ORU^R01 en un resumen validado y legible para nueve categorías de enfermedades infecciosas — con un motor en Python probado y una demo bilingüe que aplican las mismas reglas.

  • Python
  • Pydantic v2
  • pytest
  • LOINC
  • SNOMED CT
  • JavaScript
RoleRol
DeveloperDesarrollador
StandardsEstándares
HL7 v2.5.1 · LOINC · SNOMED CTHL7 v2.5.1 · LOINC · SNOMED CT
DataDatos
Synthetic messages onlySolo mensajes sintéticos
QualityCalidad
81 tests · 99% coverage · CI81 pruebas · 99% cobertura · CI

The problem

El problema

HL7 v2 is still the dominant standard for electronic lab reporting, yet a line like OBX|1|CWE|94500-6^SARS-CoV-2 RNA^LN||260373001^Detected^SCT is close to unreadable for non-technical staff. That creates four real risks:

  • Unreadable data slows triage when every hour counts during an outbreak.
  • A wrong LOINC mapping silently misclassifies a result and corrupts surveillance counts.
  • Malformed messages (missing IDs, impossible dates, bad codes) either crash a parser or slip through unnoticed.
  • Language barriers add cognitive load for Spanish-speaking staff reading English-only technical output.

HL7 v2 sigue siendo el estándar dominante para el reporte electrónico de laboratorio, pero una línea como OBX|1|CWE|94500-6^SARS-CoV-2 RNA^LN||260373001^Detected^SCT es casi ilegible para personal no técnico. Eso crea cuatro riesgos reales:

  • Datos ilegibles retrasan el triaje cuando cada hora cuenta durante un brote.
  • Un mapeo LOINC incorrecto clasifica mal un resultado sin avisar y corrompe los conteos de vigilancia.
  • Mensajes mal formados (IDs faltantes, fechas imposibles, códigos inválidos) hacen fallar al analizador o pasan sin ser detectados.
  • Las barreras de idioma añaden carga cognitiva al personal hispanohablante que lee resultados técnicos solo en inglés.

How it works

Cómo funciona

Parser summarizing a respiratory panel into disease badges
A four-analyte respiratory panel (COVID-19, Flu A, Flu B, RSV): RSV is detected and flagged abnormal.
Un panel respiratorio de cuatro analitos (COVID-19, influenza A y B, VSR): el VSR se detecta y se marca como anormal.

From pipes to a clinical summary

De barras verticales a un resumen clínico

Paste a message or upload a .txt file. The parser reads the delimiters each message declares in MSH-1/MSH-2, handles CR, LF, and CRLF line endings, and extracts the patient (PID) and every result (OBX).

Results map to nine categories — COVID-19, Influenza A, Influenza B, RSV, HIV-1 viral load, TB (IGRA), and Hepatitis A, B, and C — through one LOINC lookup. Coded answers keep their SNOMED CT code but display the text (Detected), and abnormal flags follow HL7 table 0078 (H, L, A, AA…), not just A.

Pega un mensaje o carga un archivo .txt. El analizador lee los delimitadores que cada mensaje declara en MSH-1/MSH-2, maneja finales de línea CR, LF y CRLF, y extrae al paciente (PID) y cada resultado (OBX).

Los resultados se asignan a nueve categorías — COVID-19, influenza A, influenza B, VSR, carga viral de VIH-1, TB (IGRA) y hepatitis A, B y C — con una sola búsqueda LOINC. Las respuestas codificadas conservan su código SNOMED CT pero muestran el texto (Detectado), y los indicadores anormales siguen la tabla HL7 0078 (H, L, A, AA…), no solo A.

Parsed output listing validation errors for a malformed message
A deliberately broken message: every defect is named, none crashes the parser.
Un mensaje roto a propósito: cada defecto se identifica y ninguno detiene el analizador.

Every defect, with its exact location

Cada defecto, con su ubicación exacta

Each record is built through a Pydantic v2 model. When a field fails, the error becomes an issue pointing at the HL7 location — PID-3, PID-7, OBX[2]-5 — instead of an exception.

  • LOINC codes are checked for format and Mod-10 check digit
  • NM results must be numeric; result status must be in HL7 table 0085
  • Impossible or future birth dates are rejected; ORU messages without PID or OBX are flagged
  • A record with one bad optional field is salvaged, so a bad DOB doesn't throw away the patient

Cada registro se construye con un modelo Pydantic v2. Cuando un campo falla, el error se convierte en un hallazgo que apunta a la ubicación HL7 — PID-3, PID-7, OBX[2]-5 — en lugar de una excepción.

  • Los códigos LOINC se verifican en formato y dígito de control Mod-10
  • Los resultados NM deben ser numéricos; el estado debe estar en la tabla HL7 0085
  • Se rechazan fechas de nacimiento imposibles o futuras; se marcan mensajes ORU sin PID u OBX
  • Un registro con un campo opcional inválido se rescata, para que una fecha errónea no descarte al paciente
Parser interface in Spanish in the light theme
Spanish interface in the light theme, on a hepatitis and TB panel.
Interfaz en español con el tema claro, en un panel de hepatitis y TB.

Bilingual, high-contrast, and safe to paste into

Bilingüe, de alto contraste y seguro para pegar

The interface switches between English and Spanish without a reload, and light and dark themes match the rest of this portfolio. Built-in sample messages load with one click. All message content is HTML-escaped before display, so a pasted message can never inject code into the page, and processing happens entirely in the browser.

La interfaz cambia entre inglés y español sin recargar, y los temas claro y oscuro coinciden con el resto de este portafolio. Los mensajes de ejemplo se cargan con un clic. Todo el contenido del mensaje se escapa antes de mostrarse, así que un mensaje pegado nunca puede inyectar código en la página, y el procesamiento ocurre por completo en el navegador.

Getting the codes right

Usar los códigos correctos

I verified every code against loinc.org and found three mapping errors in my own first version. A surveillance tool is only as good as its terminology.

Verifiqué cada código en loinc.org y encontré tres errores de mapeo en mi propia primera versión. Una herramienta de vigilancia es tan buena como su terminología.

LOINCMeasuresMideReported asSe reporta comoChangeCambio
94500-6SARS-CoV-2 RNA, NAA with probeCOVID-19
92142-9Influenza A RNA, respiratory specimenInfluenza Aaddedagregado
76078-5Influenza A RNA, nasopharynxInfluenza Afixed — was labeled RSVcorregido — figuraba como VSR
92141-1Influenza B RNA, respiratory specimenInfluenza Bfixed — was used for Flu A and Bcorregido — se usaba para influenza A y B
85479-4RSV RNA, upper respiratory specimenRSVaddedagregado
25836-8HIV-1 RNA viral loadHIV-1
45323-3 · 64084-7M. tuberculosis IFN-γ (IGRA)TB
71772-8Mitogen-stimulated IFN-γQC control — not reportedcontrol de calidad — no se reportafixed — was counted as a TB resultcorregido — se contaba como resultado de TB
13950-1 · 5195-3 · 24113-3 · 13955-0HAV IgM · HBsAg · HBc IgM · HCV AbHepatitis A / B / C

Architecture

Arquitectura

The Python package separates the pipeline into three layers, each with its own tests:

  • Ingestion — normalizes line endings, reads declared delimiters, resolves HL7 escape sequences, and splits batch feeds
  • Validation — builds typed models and turns every problem into a located issue
  • Transformation — one LOINC lookup per result, grouped by disease, keeping every result instead of overwriting earlier ones

A command-line tool prints a summary or JSON and exits non-zero when a message has errors, so it can sit in a data pipeline.

El paquete de Python separa el pipeline en tres capas, cada una con sus propias pruebas:

  • Ingesta — normaliza finales de línea, lee los delimitadores declarados, resuelve secuencias de escape HL7 y divide lotes
  • Validación — construye modelos tipados y convierte cada problema en un hallazgo con ubicación
  • Transformación — una búsqueda LOINC por resultado, agrupada por enfermedad, conservando cada resultado en lugar de sobrescribir

Una herramienta de línea de comandos imprime un resumen o JSON y termina con error cuando un mensaje tiene problemas, para usarse dentro de un pipeline de datos.

from hl7_infectious import parse_message, Disease

report = parse_message(raw_hl7)            # ingest → validate → summarize
report.latest(Disease.COVID19).value       # "Detected"
report.has_errors                          # False
[i.location for i in report.issues]   # e.g. ["PID-7", "OBX[2]-5"]
report.model_dump_json()                   # typed JSON for the next system

Quality & privacy

Calidad y privacidad

81automated testspruebas automatizadas
99%line coveragecobertura de líneas
8synthetic sample messagesmensajes sintéticos de ejemplo
13LOINC codes verifiedcódigos LOINC verificados

Tests cover line-ending variants, custom delimiters, Latin-1 input, batch envelopes, escape sequences, impossible dates, bad check digits, and JSON round-trips, and run against every sample file. All messages are synthetic; no real patient data is used anywhere in the project.

Las pruebas cubren variantes de fin de línea, delimitadores personalizados, texto Latin-1, lotes, secuencias de escape, fechas imposibles, dígitos de control inválidos y conversiones JSON, y se ejecutan contra cada archivo de ejemplo. Todos los mensajes son sintéticos; no se usa ningún dato real de pacientes.