Заранее вычисленные группы LOINC-кодов, которые сшивают семантически-эквивалентные коды на одну trend-линию. У одного analyte в LOINC может быть несколько кодов (Hb в крови 718-7, Cr в крови 14682-9 vs Cr в плазме 59826-8, глюкоза mg/dL vs mmol/L) — без группировки каждый превращается в самостоятельный график. trending_group_id — production-уровневый ID, по которому observation’ы одного биомаркера сшиваются.
Слой — надстройка над loinc: нативный LOINC выпускает свои “Flowsheet-laboratory” группы LG-codes (Flowsheet в смысле «лист наблюдения» — клиническая форма куда лаборатория собирает группы связанных тестов вместе: cholesterol panel, CBC, comprehensive metabolic panel и т.д.; в LOINC это Category в Group.csv). LOINC сам определил, какие коды относятся к одному параметру в рамках такого flowsheet’а — мы их group_id переиспользуем как L1. Остальное (Mass-Molar merge, property-pair, component+system fallback) достраиваем сами.
Это не таксономия биомаркеров. Биомаркер-уровневая таксономия (что такое «гемоглобин», какие у него clinical связи, какие follow-up) живёт в biomarker-graph — clinical knowledge-слой. А trending groups — чисто codification-слой, синонимия LOINC-кодов одного analyte.
Термины биомаркер / analyte / observation / тренд (и legacy «параметр крови») разнесены — см. biomarker.
Где живёт в данных
- CSV-словарь —
loinc_harmonization_service/data/trending_groups.csv1, 14 943 строки, 5895 уникальных групп. Колонки:loinc_code, group_id, group_level, group_display_name. Один code → ровно один group_id (deterministic). Сегодня CSV в git, регенерируется вручную; будущее хранилище — открытый вопрос, см. large-data-files-storage (общий вопрос про LOINC-словари) + biomarker-graph-storage (аналогичный для biomarker graph) + dictionary-first-paradigm (managed dictionary с admin UI вместо файла-в-репо). - FHIR extension на Observation — URL
http://bloodgpt.com/fhir/StructureDefinition/trending-group-id,valueString. Пишется один раз при write FHIR-Observation, не обновляется2. - Legacy Postgres column —
LoincParameters.TrendingGroupId(наследие из старого стека). Тот же ID; миграция перепишет в FHIR extension.
Где живёт в коде
- Generator (dev-time) —
loinc_harmonization_service/scripts/generate_trending_groups.py3. Берёт LOINC 2.81 раздачу (Loinc.csv,Group.csv,GroupLoincTerms.csv), фильтрует на ACTIVE + CLASSTYPE=1 (lab) + SCALE_TYP=Qn, прогоняет 4 уровня группировки, сериализует CSV. Регенерация — manual. - Resolver (runtime) —
loinc_harmonization_service/src/loinc_service/trending_groups.py4. Простойloinc_code → (group_id, group_level, group_display_name)lookup в памяти. Сервис отдаётtrending_group_idрядом с маппинг-результатом через LOINC service API. - Write path —
apps/analysis-worker/src/inngest/functions/utils/fhir-bundle-builder.ts:140-145ставит extension на Observation, еслиtrending_group_idпришёл из harmonization-шага. - Read path —
apps/b2c-dashboard/app/api/v0/[...path]/lib/fhir-reader.ts5: extension читается черезreadExtension(obs, TRENDING_GROUP_ID_URL). Sort-key для observations —tg:${trendingGroupId}, c fallbackfhir-${loincCode || name}когда extension отсутствует. Fallback не сшивает синонимы — два разных LOINC-кода одного analyte остаются разными линиями.
Четыре уровня группировки
Generator работает по убыванию native-LOINC покрытия. Один LOINC попадает ровно в один уровень. Грубо: L1 и L2 опираются на официальные LOINC-данные (группы и Mass-Molar relationships); L3 — наша собственная сборка по 5-кортежу defining columns; L4 — singleton, в API просто null6. Численные распределения ниже — счёт уровней в текущем trending_groups.csv (awk -F',' 'NR>1 {print $3}' | sort | uniq -c).
L1 — нативные Flowsheet-laboratory группы LOINC. 7820 кодов. group_id — LOINC LG-code (например LG10030-1). LOINC сам признал, что эти коды отчитывают одно и то же — мы просто переиспользуем его group_id.
L2 — augmentation поверх LOINC через Union-Find. 4303 кода. Решает проблему, когда один analyte измеряется в разных units / property, и LOINC даёт два независимых group’а — мы связываем их в один. Источник связей — данные LOINC, merge-логика наша:
- Mass-Molar conversion —
MCnc ↔ SCnc(mg/dL ↔ mmol/L),MRat ↔ SRat,MRto ↔ SRto. Требует знания molecular weight (MW) компонента — конверсия масса↔моль возможна только при известной молекулярной массе analyte. Generator валидирует это черезMass-Molar conversiongroup самого LOINC. - Property-pair без MW —
MFr ↔ SFr ↔ NFr(fractions). Безразмерные доли, MW-проверка не нужна.
group_id — UUID v5 от canonical key, namespace b8f0e1a2-3c4d-5e6f-7a8b-9c0d1e2f3a4b (фиксирован — без этого regenerate-CSV ломал бы все historical observations).
L3 — наша сборка по (Component + Property + Scale + Time + NormalizedSystem). 2820 кодов. Для LOINC’ов, не попавших ни в L1, ни в L2, generator группирует по 5-кортежу defining columns. Логика наша, не из LOINC; LOINC-группы и наши trending-группы концептуально немного разные сущности, но для нашей цели (один тренд per биомаркер) — equivalent. group_id — UUID v5.
L4 — singletons (всё, что не попало в L1/L2/L3). Generator пишет в CSV только L1/L2/L3; absence кода в CSV = singleton. В API output поле group_id для такого LOINC-а просто null — не emit’ится. На read-стороне (b2c-dashboard fhir-reader.ts:512) consumer подставляет synthetic ID fhir-${loincCode} как fallback, чтобы любой код имел какой-то trending-key (без него join по tg: не сработал бы). Префикс fhir- нужен только чтобы отличить synthetic-IDы от настоящих trending_group_id (UUID-формат у L2/L3, LG-код у L1); технически ничего не мешало бы использовать сам loinc_code, но тогда сложнее различать «реальный group» vs «degraded singleton». Эффект: observation’ы с одинаковым LOINC-кодом сшиваются между собой (один тренд per code), но синонимичные коды того же analyte, оба попавшие в L4, остаются параллельными графиками.
Specimen families
Конкретные множества, через которые L2 и L3 нормализуют system при сборе ключа:
- Blood family —
{Bld, Ser/Plas, Ser, Plas, BldA, BldV, BldC, BldMV, BldCV, Ser/Plas/Bld}→ANYBldSerPlas - Urine family —
{Urine, Urine sed}→ANYUrineUrineSed
Без этого Cr-серум и Cr-кровь оставались бы независимыми трендами.
Покрытие
- 14 943 LOINC-кодов в CSV из ~95K active+lab+Qn (фильтры generator’а). Для singleton’ов (L4) trending работает только в пределах одного и того же LOINC-кода — observations того же code сшиваются между собой, но cross-code merge с синонимами невозможен.
- 5895 уникальных групп на этих кодах.
- Generator привязан к LOINC 2.81. Production сервис на момент 2026-05-18 — 2.77. Версионный gap означает, что коды, появившиеся в 2.78-2.82, не получают group_id даже если concept-эквивалент уже был в 2.77. См. [[../domain/loinc|LOINC#Источник кодов]] про темп обновлений (~1100 новых кодов в 2.82).
Использование downstream
- Recognition pipeline —
normalize-and-harmonizeInngest-step запрашивает harmonization, получает(loinc_code, trending_group_id), передаёт вfhir-bundle-builderдля extension. - Patient timeline / trends — sort/join по строке
tg:${trendingGroupId}(буквальный prefix в коде, см.fhir-reader.ts:400). Сегодня единственный production-механизм, по которому два observation’а с разными LOINC-кодами объединяются в один тренд. - Канонический ключ biomarker графа — draft-предложение использовать
trending_group_idкак primary key графа (вместо display-имён). На момент 2026-05-18 production уже пишет одинtrending_group_idна observation, decision-страница описывает альтернативуloincCodes: string[]. Reconcile — open. См. Открытые вопросы ниже. - Сервис актуальности и
retrieved-join — сегодня join по analyte-имени; если граф переедет на trending_group_id, evidence-enrichment и retriever получат deterministic join.
Failure modes
- Extension отсутствует. Старые observation’ы из .NET-эпохи и из периодов, когда harmonization service был недоступен, идут без extension. Read path подставляет synthetic
fhir-${loincCode}(буквально в кодеapps/b2c-dashboard/.../lib/fhir-reader.ts:512— это не гипотеза) — каждый код становится отдельным трендом, синонимы (Mass-Molar pair) не сходятся. Backfill — retrofit-script (LOINC → trending_groups → PUT extension) или re-process через recognition. - L4 — каждый код собственный тренд. Synthetic
fhir-${loincCode}сшивает observations того же кода между собой, но не с синонимами того же analyte, которые тоже попали в L4 (см. описание L4 выше). Если у пациента в анамнезе есть два разных LOINC-кода одного analyte, и оба singleton’ы — UI рисует параллельные графики. Естественное лечение — переводом одного из кодов в L1-L3 при regen CSV (если LOINC выпустил новую группу или у кодов появились конкуренты по 5-кортежу). - CSV в git → не правится не-разработчиком. Custom group (например, врач хочет временно отделить research-метод от рутины) требует PR в
loinc_harmonization_service+ регенерации. Параллельные вопросы про хранение таких справочников — large-data-files-storage (общий: куда складывать большие LOINC-словари; draft, висит), biomarker-graph-storage (про biomarker graph; draft) и dictionary-first-paradigm (managed dictionary с admin UI вместо файла-в-репо; proposed). - Namespace UUID hardcoded. В
generate_trending_groups.pyзашита единственная константа namespace —b8f0e1a2-3c4d-5e6f-7a8b-9c0d1e2f3a4b, от которой через UUID v5 порождаются все group_id уровней L2 и L3 (L1 использует native LOINC LG-codes, namespace не нужен). Изменение этой константы перегенерирует ВСЕ L2/L3 group_id → break’нет existing FHIR observations (у них в extension хранится старый ID). Никогда не менять без миграции historical data.
Открытые вопросы
- L4 fallback в CSV vs absence. Сейчас singleton’ы в CSV отсутствуют, и synthetic ID
fhir-${loincCode}генерируется на read-стороне. Альтернатива — writing’нуть singleton’ы в сам CSV (loinc_code → fhir-${loinc_code}или→ <UUID v5>), чтобы вся generation жила в одном месте (generator), а consumer’ы только читали из CSV. Trade-off: CSV растёт ~10× (большинство LOINC’ов — singleton’ы), реальная выгода — только если код потом «вырастает» в group через regen, и нам надо отслеживать ID-переезды. Сейчас не критично. - LOINC version sync. Generator на 2.81, production service на 2.77. План: либо downgrade generator до 2.77 (synced, но теряем новые коды), либо upgrade service до 2.82+ (наоборот). Зависит от прогресса TS-port unification.
- Editable group overrides. Можно ли позволить admin-UI override-ить group ассоциацию для конкретного LOINC (medical advisor видит, что generator неправильно сгруппировал) — параллельно с override-таблицей для маппинга lab-name → LOINC.
Связано
- loinc — стандарт, поверх которого построены группы
- loinc-harmonization-pipeline — алгоритм маппинга raw
(name, units, material)→ LOINC code; этот pipeline отдаёт ID, который resolver конвертирует в group - loinc-harmonization-service — Python-сервис, host CSV + resolver
- biomarker-graph — entity, для которой trending_group_id предлагается как ключ
- biomarker-graph-key-loinc —
trending_group_idкак канонический ключ biomarker графа — draft - biomarker-graph-storage — где хранится biomarker граф (Postgres + admin-UI vs JSON-in-code) — draft
- large-data-files-storage — общий вопрос про хранение LOINC-словарей / больших data-файлов — draft
- dictionary-first-paradigm — managed dictionary как продукт vs cache — proposed
- loinc-unification-direction — TS-port LOINC service + unification — active
- override-storage-design — отдельная override-таблица vs единая с историей — contested
- fhir-modeling-ai-content — overview как AI-output (включая harmonized коды) маппится в FHIR
- fhir-meta-tagging — общая policy для extension’ов на FHIR-ресурсах
- fhir-observation — host resource для extension
- legacy-stack-migration — migration
LoincParameters.TrendingGroupId(Postgres) → FHIR extension (под-аспект общей миграции legacy-стэка)
Сноски
-
CSV артефакт —
Realai-plus/loinc_harmonization_service:data/trending_groups.csv, accessed 2026-05-18, https://github.com/Realai-plus/loinc_harmonization_service/blob/main/data/trending_groups.csv. ↩ -
FHIR write path —
Realai-plus/bloodgpt-for-business:apps/analysis-worker/src/inngest/functions/utils/fhir-bundle-builder.ts(L140-145), accessed 2026-05-18, https://github.com/Realai-plus/bloodgpt-for-business/blob/staging/apps/analysis-worker/src/inngest/functions/utils/fhir-bundle-builder.ts. ↩ -
Generator script —
Realai-plus/loinc_harmonization_service:scripts/generate_trending_groups.py, accessed 2026-05-18, https://github.com/Realai-plus/loinc_harmonization_service/blob/main/scripts/generate_trending_groups.py. ↩ -
Runtime resolver —
Realai-plus/loinc_harmonization_service:src/loinc_service/trending_groups.py, accessed 2026-05-18, https://github.com/Realai-plus/loinc_harmonization_service/blob/main/src/loinc_service/trending_groups.py. ↩ -
Read path с synthetic fallback —
Realai-plus/bloodgpt-for-business:apps/b2c-dashboard/app/api/v0/[...path]/lib/fhir-reader.ts(L512), accessed 2026-05-18, https://github.com/Realai-plus/bloodgpt-for-business/blob/staging/apps/b2c-dashboard/app/api/v0/%5B…path%5D/lib/fhir-reader.ts. ↩ -
Артур (автор LOINC harmonization-сервиса) — DM-тред с Ильдаром, 2026-05-18: «1 и 2 это официальные коды и составы групп от лоинков. А 3 это мы сами собрали коды с одинаковым component, property, scale, time, normsystem и объединили их в искусственную группу. Дали ей UUID. Потому что лоинк группы и наши трендинг группы это чутка разное, по сути, но для нас одно и то же… Если ничего не смогли объединить в группу, то группа состоит из одного кода, он singleton. Апи в таком случае код группы не создает и поле в аутпуте с айди группы будет пустым». Источник intra-team (Slack DM
D094D5Y5NGJ/p1779110250266079). ↩