多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

OpenMed OMOP 队列导出校验器:零网络的关系、词汇与溯源不变量本地校验实战

OpenMed OMOP 队列导出校验器:零网络的关系、词汇与溯源不变量本地校验实战 OpenMed OMOP 队列导出校验器零网络的关系、词汇与溯源不变量本地校验实战【免费下载链接】openmedLocal-first healthcare AI: clinical NER HIPAA PII de-identification that runs 100% on-device. 2,200 medical models, 21 languages, Apple MLX Python, no cloud, no patient data leaving your network. Apache-2.0项目地址: https://gitcode.com/GitHub_Trending/ope/openmedOpenMed 为临床文本到 OMOP CDM 的本地队列导出链路提供了一个确定性的、完全离线的结构校验器omop_cohort_check。本文以 docs/integrations/omop-cohort-check.md 为核心结合 实现源码、OMOP 加载器 与 单元测试 展开读完你可以掌握校验器的输入输出形态、它验证的三类不变量、16 种违规原因的完整语义、PHI-free 报告的读取方式以及如何在加载-校验-持久化管线中把它当作 fail-closed 的质量门禁使用。一、为什么需要一个独立的队列导出校验器OpenMed 的本地互操作链路会把“grounded clinical note spans”通过 Athena/Usagi 风格的词汇路由转换成 OMOP CDM 表完整流程见 FHIR 与 OMOP 互操作指南 中的grounded spans - ... - Athena/Usagi routing - OMOP CDM tables。在这一步之后队列研究者通常需要把 CDM 表导出给下游消费方而此时任何一条破损的关系、错误的词汇映射或断裂的溯源链都会静默污染后续的队列分析、统计建模或合规审计。openmed.interop.omop_cohort_check正是为此设计的本地校验器它只操作内存中的 OMOP 行从模块 docstring 开始就明确声明“intentionally has no network, database, or vocabulary-service dependency”。也就是说不发起任何网络调用不访问数据库不依赖外部词汇服务校验行为完全确定deterministic对相同输入无论映射的迭代顺序如何产出完全相同校验输出的报告永远不含源标识符、笔记文本或任何行值只含表名、列名、聚合计数与稳定的行指纹。它要回答的问题非常聚焦这份即将导出的 OMOP 队列在**关系relationships、词汇vocabulary、溯源provenance**三个维度上是否自洽、是否可被下游安全消费。二、快速上手从加载器到校验报告校验器直接对接加载器返回的OmopCdmTables文档给出的最小用法如下from openmed.interop.omop import load_grounded_notes from openmed.interop.omop_cohort_check import validate_omop_cohort_export tables load_grounded_notes(synthetic_notes) report validate_omop_cohort_export(tables) if not report.is_valid: print(report.to_dict())其中load_grounded_notes(notes, *, vocabulary_versionNone, vocabulary_routerNone, modeappend)是 cdm_loader.py 提供的 OMOP CDM v5.4 内存加载器接收带start/end偏移与概念编码的实体 span产出按主键排序的内存 CDM 表校验器拿到OmopCdmTables后即可直接校验。三种可接受的输入形态从validate_omop_cohort_export的签名源码 L397-L420可以看到除OmopCdmTables实例外它还接受两种等价形态输入形态说明OmopCdmTables加载器返回的内存表容器tables字段 聚合summaryMapping[str, Iterable[Mapping[str, Any]]]表名 → 行可迭代对象的普通映射如{person: [{person_id: 1, ...}], ...}OmopCdmTables.to_dict()返回的嵌套映射含tables键的映射内部会自动解包缺失的表按空表处理不影响其它表的校验但传入的表名若不在受支持的 11 张表清单内会直接抛错。为兼容不同调用习惯模块还提供了check_omop_cohort_export与validate_cohort_export两个等价的别名函数源码 L438-L451以及报告类型的别名OmopCohortCheckReport/OmopCohortExportReport。一个完整的端到端示例结合测试中的合成数据形态test_omop_cohort_check.py L22-L83可以构造一份可直接运行的最小导出并完成校验from openmed.interop.omop_cohort_check import validate_omop_cohort_export export { concept: [ {concept_id: 0, vocabulary_id: UNMAPPED, standard_concept: }, {concept_id: 10, vocabulary_id: SYNTHETIC, standard_concept: S}, {concept_id: 20, vocabulary_id: SYNTHETIC, standard_concept: }, ], person: [{person_id: 1, person_source_value: synthetic-person}], visit_occurrence: [{visit_occurrence_id: 2, person_id: 1}], note: [ { note_id: 3, person_id: 1, visit_occurrence_id: 2, source_note_hash: a * 64, } ], note_nlp: [{note_nlp_id: 4, note_id: 3, note_nlp_event_id: 5}], condition_occurrence: [ { condition_occurrence_id: 5, person_id: 1, condition_concept_id: 10, condition_source_concept_id: 20, visit_occurrence_id: 2, note_id: 3, note_nlp_id: 4, source_note_hash: a * 64, } ], source_to_concept_map: [ { source_to_concept_map_id: 6, source_code: SYN-CODE, source_concept_id: 20, source_vocabulary_id: SYNTHETIC, target_concept_id: 10, target_vocabulary_id: SYNTHETIC, note_nlp_id: 4, source_note_hash: a * 64, } ], } report validate_omop_cohort_export(export) print(report.is_valid) # True print(report.row_counts[condition_occurrence]) # 1这段合成数据覆盖了校验器关心的核心链条note_nlp_event_id指向condition_occurrence的事件行、source_to_concept_map同时挂到note_nlp与溯源哈希、standard_conceptS的标准概念引用。对应测试test_validates_relationship_vocabulary_and_provenance_invariants断言其is_valid is True且by_table/by_reason均为空。三、校验范围三类不变量校验器验证的不变量并非任意规则而是围绕 OMOP CDM v5.4 表结构加载器 DDL 中 11 张 loader-owned 表的_SQL_DDL设计的三组约束。1. 关系不变量主键唯一 外键可达 person/visit 一致受支持的 11 张表及主键源码_PRIMARY_KEYS为表主键conceptconcept_idpersonperson_idvisit_occurrencevisit_occurrence_idnotenote_idnote_nlpnote_nlp_idcondition_occurrencecondition_occurrence_iddrug_exposuredrug_exposure_idmeasurementmeasurement_idprocedure_occurrenceprocedure_occurrence_idobservationobservation_idsource_to_concept_mapsource_to_concept_map_id校验器先建立全表主键索引_build_primary_indexes再对以下外键关系做可达性检查_validate_relationships必填外键visit_occurrence.person_id → person、note.person_id → person、note_nlp.note_id → note以及五个域表condition/drug/measurement/procedure/observation的person_id → person条件必填外键仅当该列出现在任何一行时启用由_field_enabled判断visit_occurrence.visit_concept_id → concept、note.visit_occurrence_id → visit_occurrence、域表与source_to_concept_map的note_id/note_nlp_id链接概念引用五个域表的*_concept_id与*_source_concept_id必须指向存在的concept行note、note_nlp、source_to_concept_map各自的*_concept_id列同理person/visit 一致性_validate_person_visit_consistencynote与五个域表如果同时给出visit_occurrence_id和person_id则该 visit 行所属的 person 必须与行内 person 一致否则记为person_visit_mismatch。重复主键有一个关键语义被判定重复的主键全部计入违规且不会进入主键索引——即重复键所在行不能作为任何外键关系的目标。对应测试test_duplicate_primary_keys_are_all_flagged_and_not_referenceable验证两行person_id1的 person 记录各记一次duplicate_primary_key而引用它的visit_occurrence行因目标不可达再记一次missing_reference。2. 标识符与主键的严格规则主键与关系标识符必须无损地落在有符号 64 位整数范围内_MIN_BIGINT -(2**63)到_MAX_BIGINT 2**63 - 1见 源码_identifier。合法形态只有两种原生整数或可无损解析的整数字符串解析前会 strip 空白。以下情况一律视为非法标识符缺失列不存在→missing_primary_keynull / 空字符串→ 按缺失处理浮点数如1.5→ 非法布尔值True/False会被特殊拦截避免与1/0混淆→ 非法超出 ±2^63 范围如2**63→ 非法非数字字符串→ 非法。对应测试test_missing_and_lossy_primary_keys_are_rejected验证None、1.5、2**63三种输入分别产出missing_primary_key× 1 与invalid_primary_key× 2。3. 词汇不变量标准概念、词表一致性与映射唯一性词汇校验_validate_vocabulary围绕concept.standard_concept、域表的概念引用与source_to_concept_map展开standard_concept枚举允许值仅、C、N、S源码_ALLOWED_STANDARD_CONCEPTS其它值记invalid_standard_concept标准概念引用约束域表行引用的 concept 若声明了standard_concept只允许或S即标准概念引用CClassification或NNon-standard记nonstandard_concept_reference词表一致性source_to_concept_map中声明source_vocabulary_id/target_vocabulary_id时必须与 concept 行的vocabulary_id一致否则记vocabulary_mismatch目标概念必须标准target_concept_id ! 0时目标 concept 的standard_concept只能为或S否则记nonstandard_target_concept映射唯一性同一(source_vocabulary_id, source_code)对只允许映射到唯一目标概念多个不同目标记conflicting_vocabulary_mapping。4. 溯源不变量NOTE/NOTE_NLP 溯源链OpenMed 的 OMOP 导出带有一条独有的溯源链provenance chain每篇临床笔记对应一个source_note_hash64 位小写十六进制 SHA-256域表行与source_to_concept_map行通过note_id、note_nlp_id和source_note_hash回连到note/note_nlp。校验器_validate_provenance会检查哈希格式source_note_hash必须匹配[0-9a-f]{64}否则记invalid_provenance必填性当某表任一行的溯源字段启用时所有行都必须补齐note_id/note_nlp_id/source_note_hash缺失记missing_provenance哈希一致性域表行的source_note_hash必须与所挂note行的哈希一致否则记provenance_mismatch事件链接note_nlp.note_nlp_event_id必须恰好解析到一个域表事件行——解析不到记unreachable_event解析到多个例如同一事件同时挂 condition 与 drug 行记ambiguous_event且该域表行的note_nlp_id必须与note_nlp行自身、以及note_nlp.note_id与域表行的note_id全部对齐。四、报告结构只有计数、表名列名与行指纹校验产物是OmopCohortValidationReport冻结 dataclass其to_dict()输出结构源码 L296-L305为{ count: 违规总行数, row_counts: {concept: ..., person: ..., ...}, # 11 张表各自行数 by_table: {condition_occurrence: 2, ...}, # 按表聚合的失败数 by_reason: {missing_reference: 2, ...}, # 按原因聚合的失败数 violations: [ { table: condition_occurrence, column: visit_occurrence_id, # 可能缺失 reason: missing_reference, count: 2, row_fingerprints: [sha256:..., ...], }, # ... ], }报告对象还暴露is_valid与兼容别名valid、violation_count与failure_count/failures同义、by_table、by_reason等只读属性。每条OmopCohortViolation是按 (表, 列, 原因) 分组聚合的count是精确的失败行数row_fingerprints是被影响的行的去重指纹集合。行指纹确定性且可关联omop_row_fingerprint(table, row)源码 L365-L394对每行计算sha256:前缀的确定性指纹序列化载荷带固定 schema 标记openmed.omop.cohort-check.v1与表名同名行在不同表不会共享指纹JSON 序列化使用sort_keysTrueseparators(,, :)因此行内键顺序不影响指纹——对应测试验证{b: 2, a: 1}与{a: 1, b: 2}指纹相同规范化_canonicalize会区分类型1与1是不同的映射键非有限浮点数被转换为{type: float, value: ...}形式集合类型排序后参与序列化。重要边界由于指纹是内容的确定性摘要内容完全相同的两行会得到相同指纹——指纹可被用来跨批次关联同一行例如定位“上次也失败的那条记录”因此官方文档明确要求把它当作诊断元数据diagnostic metadata而非匿名化数据对待不能因为“只见指纹不见原文”就视为已匿名。报告与异常中永不出现源标识符person_source_value之类、note_text笔记文本、lexical_variant原文片段或任何行值。对应测试test_reports_deterministic_counts_and_fingerprints_without_source_values断言序列化后的报告不包含synthetic-note corpus value与synthetic-person。五、违规原因目录16 种可枚举的失败类型源码_VIOLATION_REASONSL97-L116将失败原因收敛为封闭集合报告与异常中的reason只会取以下值原因所属维度含义missing_primary_key标识符主键列缺失或为 nullinvalid_primary_key标识符主键非无损 64 位整数浮点/越界/布尔/非数字duplicate_primary_key标识符主键重复所有重复行均计入且不可作引用目标missing_reference关系必填外键缺失或指向不存在的目标invalid_reference关系外键值本身非法person_visit_mismatch关系行内 person 与其 visit 所属 person 不一致invalid_standard_concept词汇standard_concept取值不在/C/N/Snonstandard_concept_reference词汇域表概念引用了非标准概念nonstandard_target_concept词汇映射目标概念非标准vocabulary_mismatch词汇声明的词表与 concept 行vocabulary_id不一致conflicting_vocabulary_mapping词汇同一 (词表, 源编码) 映射到多个目标missing_provenance溯源溯源字段启用但行内缺失invalid_provenance溯源source_note_hash不是 64 位小写 SHA-256provenance_mismatch溯源域表/映射行的哈希或事件链接与 note/note_nlp 不一致unreachable_event溯源note_nlp_event_id解析不到任何域表事件行ambiguous_event溯源note_nlp_event_id解析到多个域表事件行这条封闭枚举既是契约也是安全机制OmopCohortViolation/OmopCohortValidationReport在__post_init__中会拒绝任何不在此集合内的表名、列名或原因对应测试test_public_report_types_reject_untrusted_diagnostic_labels从类型层面杜绝任意字符串混入诊断输出。六、Fail-closed断言辅助函数与异常语义如果只想在导出管线中做“不合格即中止”的硬门禁用断言辅助函数文档示例from openmed.interop.omop_cohort_check import assert_valid_omop_cohort_export assert_valid_omop_cohort_export(tables)行为源码 L454-L462校验通过静默返回OmopCohortValidationReport校验失败抛出OmopCohortExportValidationErrorValueError子类该异常携带完整聚合报告exc.report但异常消息只有一行聚合信息——OMOP cohort export validation failed with N row-level failure(s)其中N是失败的行级不变量总数各违规count之和绝不内嵌任何行值。对应测试test_row_fingerprints_are_canonical_and_validation_errors_are_phi_free验证异常字符串中不包含任何合成数据值synthetic不出现且exc.report.is_valid is False。这种设计保证异常可以安全地进入日志、监控与 CI 输出而不泄露 PHI。七、内部实现管线四步确定式校验从源码_validate_omop_cohort_exportL423-L435可以看到完整管线_normalise_tables ── _build_primary_indexes ── _validate_relationships ── _validate_vocabulary ── _validate_provenance ── collector.report(row_counts)规范化_normalise_tables把OmopCdmTables/ 普通映射 /to_dict()输出统一成{表名: (行, ...)}表名转小写并校验合法性行必须是映射、列名必须是字符串非法表名、非法行、不可迭代行都会抛错缺失表补为空元组。主键索引_build_primary_indexes逐表解析主键标识符把缺失/非法/重复三类问题直接送入_FailureCollector仅把“唯一且合法”的主键放入索引供后续外键查找。三组校验按关系 → 词汇 → 溯源的固定顺序执行所有失败都经由_FailureCollector.add(table, row, reason, column)聚合——它只保留(表, 列, 原因) → 指纹计数器三元组从源头保证报告无行值。产出报告_FailureCollector.report()按表顺序_TABLE_ORDER与 (表, 列, 原因) 字典序稳定排序生成冻结的OmopCohortValidationReport。错误边界面对敌意输入也不泄漏校验器对“无法安全处理”的输入采取有界错误策略模块 docstring 明确errors are bounded and never copy the source value无法迭代的行容器如__iter__直接抛异常的类→ 归一化阶段捕获并转为ValueError(cohort export table rows must be iterable)异常__cause__被清除原始异常消息不会透出test_hostile_row_iterators_fail_without_echoing_source_values断言消息长度 100 字符循环引用行row[nested] row→ 指纹阶段通过id()标记检测环并拒绝_canonicalize的_active集合深度过深嵌套层级超过_MAX_CANONICAL_DEPTH 64→ 拒绝并报告有界错误__str__抛异常的敌意值、非有限浮点数 → 规范化阶段安全降级指纹计算整体被 try/except 包裹任何异常统一转为ValueError(OMOP row cannot be fingerprinted safely)且指纹只是计算时的中间量从不回传或嵌入异常。对应测试test_hostile_and_cyclic_rows_fail_without_echoing_source_values用RAW-SYNTHETIC-PATIENT哨兵值验证循环行、65 层嵌套、抛异常的__str__值在异常消息中均不出现。八、与加载器及持久化组合完整质量门禁校验器不是孤立组件它可以与加载器/写入器的其它质量工具组合成完整链路加载load_grounded_notes(...)或load_grounded_jsonl(path, ...)产出OmopCdmTables加载器内部还会拒绝缺失 offset、无效 offset、不支持域名的实体 span见 cdm_loader.py内存校验validate_omop_tables(tables)严格的行形状 主键 概念引用 NOTE_NLPoffset 边界 事件双向可达性校验或本文主角validate_omop_cohort_export(tables)持久化write_omop_duckdb/write_omop_sqlite/write_omop_parquet按主键幂等合并写入modereplace_by_note时先删除对应笔记哈希的历史行库级复核对已落库的 DuckDB/SQLite 连接执行validate_omop_database(con)或validate_omop_database_report(con)用同一套 PHI-free 违规形状复核持久化结果源码validate_omop_databaseL1261-L1285。推荐的 CI/批处理模式from openmed.interop.omop import ( load_grounded_jsonl, validate_omop_database_report, write_omop_duckdb, ) from openmed.interop.omop_cohort_check import assert_valid_omop_cohort_export tables load_grounded_jsonl(grounded_notes.jsonl) assert_valid_omop_cohort_export(tables) # fail-closed 内存门禁 con write_omop_duckdb(tables, cohort.duckdb) report validate_omop_database_report(con) # 落库后复核 assert report.is_valid, report.to_dict()OmopCdmTables.load_event()还能产出仅含哈希与计数的OmopLoadEvent供下游OmopDownstreamConsumer在持久化成功后安全订阅事件里只有changed_note_hashes与row_counts永远没有笔记文本。九、定位与边界结构校验 ≠ 合规认证最后必须明确校验器的定位文档结尾与模块 docstring 反复强调它验证的是结构质量structural quality主键、外键、词汇标注、溯源链是否自洽它不是合规认证也不能替代临床决策保障——OpenMed 不内置 Athena、UMLS、SNOMED CT、CPT 等受限词表内容见 FHIR/OMOP 互操作指南概念编码的临床正确性由上游 grounded spans 与用户自带的词汇路由负责报告中的指纹是诊断元数据而非匿名数据相同内容行可被关联处理时需按受控诊断信息对待校验器只检查当前内存导出的自洽性跨批次增量合并append / replace_by_note的最终一致性由 加载器写入逻辑 与库级validate_omop_database_report共同兜底与 FHIR 侧的关系可参考 FHIR-OMOP 互操作矩阵 了解双向映射的覆盖情况。在本地优先local-first的 OpenMed 设计里omop_cohort_check的价值正在于它把“导出前最后一公里”的质量检查变成了一次无网络、无依赖、无 PHI 泄漏的纯函数调用——任何一个批处理作业、Airflow DAG 或 CI 管道都可以在几行代码内把它变成一张确定性的、可审计的队列质量门禁。【免费下载链接】openmedLocal-first healthcare AI: clinical NER HIPAA PII de-identification that runs 100% on-device. 2,200 medical models, 21 languages, Apple MLX Python, no cloud, no patient data leaving your network. Apache-2.0项目地址: https://gitcode.com/GitHub_Trending/ope/openmed创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表