pyomop sem vocabulário Athena: 60 pacientes FHIR e concept_id em zero

Carreguei 60 pacientes FHIR sintéticos no OMOP com o pyomop 6.4.0, sem o vocabulário do OHDSI. Os campos conferidos chegaram iguais, mas as 499 linhas clínicas ficaram com concept_id em 0 e nenhuma ligada à consulta.

Rogério Rodrigues

Rogério Rodrigues

Engenheiro de IA · mestrando em Informática em Saúde (UFSC)

Cartão azul-gelo com fonte monoespaçada: concept_id = 0 em 499 de 499 registros clínicos; valores de origem iguais; CPF, CNS e nome em alguma tabela 0 de 60; linhas ligadas à consulta 0 de 390. pyomop 6.4.0, FHIR para OMOP CDM 5.4.

Rodei o pyomop 6.4.0 em 60 pacientes FHIR sintéticos, no comando mínimo, sem o vocabulário do OHDSI. O README do pacote manda passar --vocab; eu não passei, de propósito, para medir o que a carga entrega sem ele. Levou cerca de 4 segundos, já com a criação das tabelas, e os campos que conferi chegaram iguais. Mas as 499 linhas clínicas (em cinco tabelas) ficaram com concept_id em 0. Sem esse número, consultas por conceito deixam essas linhas de fora.

Código, dados e saída gravada: demos/10-pyomop-fhir-omop no nursia-research-lab.

Como rodei

  • pyomop 6.4.0 (pip install pyomop==6.4.0), Python 3.11, CPU, OMOP CDM 5.4 (modelo comum de dados) em SQLite. As versões das dependências estão fixadas no requirements.txt.
  • Gerei o FHIR em NDJSON (um JSON por linha), com semente fixa: 60 Patient, 109 Encounter, 90 Condition (CID-10), 120 Observation (LOINC 8867-4 e 8480-6) e 60 MedicationRequest (RxNorm).
  • Cada paciente tem CPF e CNS no identifier, além de nome, telefone e endereço inventados. CPF e CNS passam no dígito verificador, mas são sorteados e não pertencem a ninguém.
  • Condição, observação e prescrição apontam para uma consulta no campo encounter.
  • Comando do pyomop: pyomop --create --name cdm.sqlite --input fhir_sintetico. É o fluxo do README sem o --vocab. Depois, o script lê o banco e confere cinco coisas.

Não carreguei o vocabulário do OHDSI Athena, o repositório de vocabulários do OHDSI. O download exige conta e eu não fiz. Isso explica o resultado principal: o concept_id em 0 é o esperado para essa chamada, e não um defeito do pacote. Volto nisso nos limites.

O que chegou

Recurso FHIR No FHIR Tabela OMOP (linhas)
Patient 60 person (60)
Encounter 109 visit_occurrence (109)
Condition 90 condition_occurrence (90)
Observation 120 measurement (120) e observation (120)
MedicationRequest 60 drug_exposure (60)

Conferi seis grupos de campos de origem, e todos chegaram iguais: sexo em 60 de 60, nascimento em 60 de 60, medidas (código, valor e data) em 120 de 120, condições em 90 de 90, consultas (início, fim e classe) em 109 de 109 e medicamentos (código e data) em 60 de 60. Não conferi unidade, horário, data de fim da condição nem posologia.

O que o pyomop faz certo

A parte difícil de uma carga FHIR para OMOP é não perder nem trocar dado. Aqui nenhum registro sumiu: as contagens batem recurso a recurso, e as datas e os valores dos campos que conferi batem. O sexo foi convertido para o concept_id padrão (8507 para masculino, 8532 para feminino) sem consultar a tabela concept: o loader.py usa um mapeamento fixo no código. O person_id seguiu a ordem de carga, e o id do FHIR ficou em person_source_value, o que permite voltar ao recurso original.

A carga funcionou. O que falta é o vocabulário.

O que ficou pelo caminho

concept_id em 0

No OMOP, o concept_id é o que permite perguntar “todos os pacientes com hipertensão” sem listar códigos. Na carga, ele ficou em 0 em todas estas colunas:

Coluna Em 0 ou nulo
condition_concept_id 90 de 90
measurement_concept_id 120 de 120
unit_concept_id 120 de 120
observation_concept_id 120 de 120
drug_concept_id 60 de 60
visit_concept_id 109 de 109

Qualquer consulta que filtre por conceito deixa esses registros de fora (não testei no ATLAS, a ferramenta de consulta do OHDSI). O código original ficou nas colunas *_source_value (por exemplo, I10 e 8867-4), então nada se perdeu. Raça e etnia também ficaram em 0 em 60 de 60, mas aí a entrada ajuda: meu Patient sintético não tem a extensão de raça e etnia. Só gender_concept_id veio preenchido, com 8507 e 8532. A tabela concept tinha 0 linhas.

Nenhuma linha clínica ligada à consulta

O visit_occurrence_id ficou vazio nas 390 linhas clínicas de condição, medida, observação e medicamento, apesar de o FHIR de entrada trazer o campo encounter. Abri o mapping.default.json do pacote: ele não mapeia visit_occurrence_id nem encounter. Ligar o evento à consulta exigiria um mapeamento próprio, que eu não testei, e não sei se o loader resolve: ele só tem passo de normalização de chave para person_id. No meu FHIR, todas as consultas são ambulatoriais e cada evento aponta para a primeira consulta do paciente.

Cada Observation entra em duas tabelas

As 120 Observation viraram 120 linhas em measurement e mais 120 em observation. O mapeamento padrão faz isso. Quem somar as duas tabelas conta cada observação duas vezes.

O dado pessoal não entrou

Procurei CPF, CNS, nome completo, sobrenome, telefone, rua e CEP em todas as células de todas as tabelas: 0 de 60 pacientes em cada um. O person_source_value guarda só o id do Patient (p001). O CPF não está no banco. Se você precisa ligar o registro ao paciente, mantenha a tabela de correspondência fora do OMOP. Isso vale para o mapeamento padrão com estes campos, e não é uma garantia de privacidade do pacote.

O que eu faria

  • Antes de carregar dado real, rode a carga num FHIR sintético seu e conte as linhas com concept_id = 0.
  • Carregue o vocabulário do Athena junto com a carga, como o README do pacote manda. Sem ele, todo concept_id clínico fica em 0, foi o que medi. Carregar não garante acerto: pelo código, o passo de mapeamento busca só por igualdade de concept_code, sem olhar o vocabulário de origem, e códigos iguais em vocabulários diferentes podem colidir. Não testei.
  • Se o seu caso precisa ligar evento e consulta, um mapeamento próprio é o caminho a investigar. Eu não testei.

Limites

  • Sem vocabulário Athena. O pyomop tem um passo que preenche o concept_id procurando o concept_code na tabela concept. Não rodei esse passo com vocabulário, então não sei quanto ele acerta.
  • 60 pacientes sintéticos, uma semente, cinco tipos de recurso, CDM 5.4, SQLite. Não testei PostgreSQL, CDM 6, Bundle em JSON (um arquivo com vários recursos) nem os recursos de LLM e MCP do pacote.
  • Não testei um arquivo de mapeamento próprio.
  • Medi a carga e a integridade dos valores. Não medi a qualidade clínica do mapeamento.
  • O tempo de cerca de 4 segundos vale para esta máquina e para 60 pacientes. Não medi desempenho em volume maior.

Fonte e como reproduzir

Clone https://github.com/rogeriorrodrigues/nursia-research-lab e entre em demos/10-pyomop-fhir-omop. Daí são três comandos:

pip install -r requirements.txt
python3 demo.py
python3 -m pytest tests -q

NursIA é pesquisa do PPGINFOS/UFSC, com bolsa FAPESC.

In English

I loaded 60 synthetic FHIR patients into OMOP CDM 5.4 with pyomop 6.4.0 in about 4 seconds. The source fields I checked arrived intact and no CPF, name or address landed in any table. But all 499 clinical rows (five tables, 379 FHIR resources) have concept_id = 0 without an Athena vocabulary, as expected without --vocab, none links to its visit, and each Observation is written to two tables.

Conhece alguém que precisa ler isso?

Manda pra essa pessoa. Leva dez segundos e é o que mais ajuda esse trabalho a chegar em quem usa IA com dado de verdade.

Rogério Rodrigues

Rogério Rodrigues

Engenheiro de IA há 20 anos em software, mestrando em Informática em Saúde na UFSC. Escrevo o que rodei: IA com dado de saúde, FHIR e LGPD, com código aberto.

Ficou uma dúvida? Discorda de algo? Me chama.

Ver publicações científicas