Эта статья — про то, как я проектировал референсную архитектуру для приведения лабораторных данных к 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 заполняется мусором.

Как это внедрить, не переписывая всё
Самый частый страх при слове «стандартизация»: «значит, надо снести наш справочник и собрать всё заново». Нет. Это классический паттерн 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/ в репозитории






















