Интероперабельность медданных: почему один биомаркер — это не один код. healthcare.. healthcare. LOINC.. healthcare. LOINC. OMOP.. healthcare. LOINC. OMOP. reference architecture.. healthcare. LOINC. OMOP. reference architecture. RWE.. healthcare. LOINC. OMOP. reference architecture. RWE. UCUM.. healthcare. LOINC. OMOP. reference architecture. RWE. UCUM. интероперабельность.. healthcare. LOINC. OMOP. reference architecture. RWE. UCUM. интероперабельность. медицинские данные.. healthcare. LOINC. OMOP. reference architecture. RWE. UCUM. интероперабельность. медицинские данные. нормализация данных.. healthcare. LOINC. OMOP. reference architecture. RWE. UCUM. интероперабельность. медицинские данные. нормализация данных. стандартизация.

Эта статья — про то, как я проектировал референсную архитектуру для приведения лабораторных данных к LOINC (Logical Observation Identifiers Names and Codes — международный справочник кодов лабораторных и клинических показателей) и UCUM (Unified Code for Units of Measure — стандарт машиночитаемого кодирования единиц измерения), и что понял по дороге.

Сразу оговорюсь: это архитектура на бумаге — ADR, схемы и разобранные примеры в репозитории, а не развёрнутый в проде сервис. Боевой нагрузкой пока не проверена.

Все решают одну проблему заново

В каждой лаборатории один и тот же биомаркер пишут по-своему. Вот реальная цепочка названий для одного-единственного фермента — аспартатаминотрансферазы:

АсАТ · AST · АСТ · аспартатаминотрансфераза · аспартат-аминотрансфераза · глутамат-оксалоацетат-трансаминаза · SGOT · СГОТ

Всё это — один и тот же аналит (то, что измеряют в пробе, — конкретное вещество или показатель). Пока данные живут внутри одной системы, это не проблема: справочник знает свои синонимы. Проблема появляется на границе — когда данные нужно обменять между лабораториями, свести в регистр, отдать в аналитику. Тогда выясняется, что «свой справочник» каждого не стыкуется ни с чьим другим.

Естественная реакция инженера — построить граф синонимов и схлопнуть весь этот список в один код. Я сам так думал. И это работает ровно наполовину.

Почему «список синонимов → один код» — это ловушка

LOINC-код — это не «название анализа». Это полностью специфицированная комбинация шести осей:

Ось

Что задаёт

Пример

Component

Что измеряем (аналит)

Glucose, AST

Property

В чём измеряем (размерность)

масс-конц. mg/dL или молярная mmol/L

Time

Момент или интервал

разовая проба или суточная моча

System

Биоматериал

сыворотка, плазма, моча

Scale

Тип шкалы

количественная, порядковая, номинальная

Method

Техника измерения

с P-5’-P / без P-5’-P

И вот ключевой момент: список синонимов выше схлопывается только по одной оси — Component (что за аналит). Но тот же AST в сыворотке и в плазме — это разные коды (ось System). С пиридоксальфосфатом и без — разные коды (ось Method). Количественный и качественный результат — разные коды (ось Scale).

То есть граф синонимов решает необходимую, но недостаточную часть задачи. Он канонизирует имя. А код требует ещё и обстоятельств: чем измеряли, в каком биоматериале, в какой размерности.

LOINC отвечает на «что», UCUM — на «сколько»

Здесь меня долго сбивала одна неточность: казалось, что раз есть LOINC, то единица измерения уже зашита в код, и UCUM избыточен. Это не так.

LOINC задаёт размерность (через ось Property): «это масс-концентрация» или «это молярная концентрация». Но он не говорит, отчиталась ли лаборатория в mg/dL, g/L или mcg/mL — а это всё одна размерность, но разные числа. Реальная единица приходит вместе с данными, не из кода.

LOINC

UCUM

Отвечает на

Что измерено

Сколько и в каких единицах

Нормализует

Понятие измерения

Единицу внутри размерности

Объект

Аналит + обстоятельства

Величина значения

Они ортогональны, и нужны оба. Даже внутри одного LOINC-кода единицы могут различаться: лаборатория А шлёт глюкозу в mg/dL, лаборатория Б — ту же самую в g/L. Понятие совпало, числа — нет. Усреднишь по коду без приведения единиц — получишь мусор. Вот тут и работает UCUM: приводит mg/dL → g/L детерминированно, потому что это одна размерность и коэффициент не зависит от вещества.

А вот mg/dL → mmol/L (масса → молярность) — это смена размерности, и она требует молярной массы конкретного вещества. UCUM этого не делает и делать не должен — он ничего не знает про вещества. Это та же самая стена, что ось Property в LOINC, просто с другой стороны: масс-код и молярный код — это разные коды, и переход между ними — отдельная задача с учётом вещества.

Зачем такая дотошность: RWE и OMOP

В этот момент возникает законный вопрос: не избыточное ли это усложнение? Ответ зависит от того, зачем вы нормализуете данные.

Если цель — просто зарегистрировать факт («тест на глюкозу назначался»), посчитать частоты или обменяться понятием, то хватит и одной LOINC-нормализации. UCUM здесь ничего не добавляет. То же для качественных результатов («положительно / отрицательно») — там числовой единицы нет вовсе.

Но если цель — аналитика на уровне значений, то есть считать по самим значениям (динамика, сравнение с референсными интервалами, когорты для RWE, фичи для ML), то картина другая. Real-world evidence (RWE — доказательства из данных реальной клинической практики, а не из клинических испытаний) живёт на объединении данных из десятков источников, и здесь критичны две вещи:

  • Сопоставимость — глюкоза из лаборатории А и из лаборатории Б должны быть одной и той же сущностью, иначе их нельзя складывать. LOINC даёт стабильный ключ.

  • Защита от ложной агрегации — нельзя тихо смешать сывороточную глюкозу с мочевой или активность AST «с P-5’-P» и «без». Оси не дают этому случиться молча.

И это не абстракция. OMOP CDM (Common Data Model — стандартная модель данных для наблюдательных исследований), на которой стоит весь инструментарий OHDSI (Observational Health Data Sciences and Informatics — международное сообщество и набор инструментов с открытым исходным кодом поверх OMOP), спроектирована буквально под эту пару. В таблице MEASUREMENT:

  • measurement_concept_id ← LOINC (что измерено, со всеми осями),

  • unit_concept_id + value_as_number ← UCUM (единица + число),

  • value_as_concept_id ← качественный результат (ось Scale у LOINC).

То есть OMOP-таблица — это почти один-в-один LOINC (оси) + UCUM (единица). Это не совпадение: модель строилась вокруг этих стандартов. А значит, слой нормализации сырое имя + контекст → LOINC + канон UCUM — это, по сути, ETL-дверь (extract-transform-load — извлечение, преобразование и загрузка данных) в OMOP. Без него таблица MEASUREMENT заполняется мусором.

Поток данных: сырое имя + значение + контекст → резолвер → LOINC-код и канон UCUM → таблица OMOP MEASUREMENT → RWE-аналитика

Поток данных: сырое имя + значение + контекст → резолвер → LOINC-код и канон UCUM → таблица OMOP MEASUREMENT → RWE-аналитика

Как это внедрить, не переписывая всё

Самый частый страх при слове «стандартизация»: «значит, надо снести наш справочник и собрать всё заново». Нет. Это классический паттерн strangler fig («удушающий плющ») — новый слой обрастает старую систему по краям, ничего не ломая.

Ключевой сдвиг: LOINC/UCUM не заменяет ваш внутренний справочник. Он садится слоем выше как канонический интерлингва (промежуточный язык-посредник, через который сопоставляются разные системы). Ваш справочник остаётся локальным словарём, а LOINC — тем, с чем он сопоставляется для обмена наружу. Те самые кропотливо собранные синонимы не выбрасываются — они становятся входным слоем, который питает нормализацию. Граф синонимов ALT — это не конкурент резолверу, это его поставщик.

Отсюда же модель уровней зрелости: можно войти на минимальном уровне (ключевые биомаркеры, только новые записи, базовый код), а старые данные оставить в старой форме и пересопоставить позже — по желанию, не принудительно. Внедрение становится постепенным: сопоставление только новых записей, по желанию — дозаполнение старых данных, подъём по уровням точности по мере готовности.

Почему не просто отдать всё языковой модели

Здесь возникает резонное возражение: зачем весь этот аппарат осей, версий и правил, если современная языковая модель и так «поймёт», что АлАТ — это ALT? Ответ — не «модель плохая», а «у неё в этой архитектуре точное и узкое место, и это не место того, кто выдаёт итоговый код».

Где модель незаменима — это распознавание имени аналита: схлопнуть «АсАТ / AST / SGOT / глутамат-оксалоацетат-трансаминаза» в одну сущность. Здесь доменная модель — или биомедицинские эмбеддинги (числовые векторы, кодирующие смысл: близкие по смыслу названия близки и в пространстве) класса BioLORD / SapBERT — бьёт любой рукописный словарь синонимов. То есть модель нормализует имя (ось Component) и поднимает кандидатов-кодов с оценкой. Остальные оси, от которых и зависит конкретный код (System, Method, Scale, Property), приходят отдельно и явно из контекста и применяются детерминированными правилами приоритета — а не угадываются. Поэтому важные обстоятельства не теряются: модель за них просто не отвечает. И итоговый код она тоже не выдаёт — его выбирает детерминированный слой: правила приоритета, проверка кода по снимку справочника, аудит.

Причина такого разделения — два свойства, особенно опасные в медицине:

  • Модель угадывает, а не гарантирует. Один и тот же вход даёт разные коды в разных прогонах, а в придачу модель охотно выдаст правдоподобный, но несуществующий код. Для RWE это тихий яд: дрейф сопоставления превращается в дрейф данных. Поэтому код проверяется на формат и на существование по снимку справочника, а не принимается на веру.

  • Нет воспроизводимости и учёта версий из коробки. Модель не объяснит, почему выбран именно этот код, и не знает, что он устарел и заменён в текущей версии. Это держат слой аудита и слой версионирования — на них и стоят воспроизводимость и разбор.

А какая именно модель стоит в резолвере — деталь реализации: архитектура держит его как заменяемый слой и от конкретной модели не зависит.

Честно про путь

И последнее, личное.

Я спроектировал эту архитектуру раньше, чем разобрался в LOINC и UCUM до уровня шести осей. Сначала появились решения и компромиссы, и только потом, докапываясь до деталей стандарта, я заметил, что многие из них совпали с тем, как устроены сами стандарты. Выглядит как «надо было сначала изучить, потом делать» — но, кажется, порядок был правильный.

Для меня проектирование было способом разобраться, а не итогом: грубая модель, столкновение с реальностью, уточнение. Каждый новый факт про LOINC находил, куда лечь, потому что под него уже был готов слот — и запоминался лучше, чем если бы я просто прочитал спецификацию.

Поэтому статья и репозиторий — скорее попытка показать ход мысли, чем готовый рецепт. Залезут пара человек в репозиторий, покрутят сами — хорошо. Нет — мне это всё равно уже помогло разобраться.

Ссылки

Репозиторий: https://github.com/alarent/loinc-ucum-reference-architecture

Разбор решений (ADR): каталог docs/adr/ в репозитории

Автор: alarent

Источник