En résumé
Construire un ETL OMOP est souvent perçu comme un chantier de plusieurs mois. La démarche peut être découpée progressivement et intégrée au flux de travail habituel. La plus grande partie du travail est l’alignement sémantique — produire le fichier SOURCE_TO_CONCEPT_MAP.csv. Une fois cet alignement en place, le reste est largement mécanique : on route chaque ligne source vers la bonne table OMOP en fonction du domain_id du concept standard. Cet article décrit cette démarche, présente le module d’alignement de Linkr, puis vous fait construire pas à pas un mini-ETL MIMIC-IV → OMOP en exécutant le SQL dans votre navigateur.
L’ETL OMOP : une démarche progressive
Quand on découvre OMOP, on peut se représenter l’ETL comme un chantier de plusieurs années visant à standardiser l’intégralité du dictionnaire local avant de pouvoir exploiter quoi que ce soit. Cette vision est légitime — elle reflète d’ailleurs ce qu’ont effectivement vécu plusieurs équipes pionnières — mais elle n’est pas la seule.
Une autre approche, plus incrémentale, permet de démarrer un premier ETL OMOP utile en quelques semaines, puis d’enrichir progressivement la couverture au fil des projets. Cette démarche s’intègre dans le flux de travail habituel d’une équipe d’entrepôt de données, sans bouleverser les habitudes. Elle repose sur deux principes :
- Procéder projet par projet. Pour une étude donnée, vous avez besoin de quelques dizaines de variables. Vous n’alignez que celles-ci ; les autres concepts attendront un autre projet, qui réutilisera ce qui aura déjà été aligné.
- L’alignement sémantique d’abord, le reste suit mécaniquement. Une fois la table
SOURCE_TO_CONCEPT_MAPproduite, l’ETL devient surtout du routage : on regarde ledomain_idde chaque concept standard, et il indique directement dans quelle table OMOP atterrit la donnée.
Une conséquence directe de cette approche : la base OMOP que vous produisez n’aura pas forcément toutes les tables du modèle. Vous remplirez celles qui correspondent aux concepts effectivement alignés pour le projet — typiquement PERSON, VISIT_OCCURRENCE, plus quelques tables d’événements (CONDITION_OCCURRENCE, MEASUREMENT, DRUG_EXPOSURE…). Les autres (DEVICE_EXPOSURE, SPECIMEN, NOTE…) resteront vides tant qu’un projet ne les requiert pas. Ce périmètre réduit est suffisant pour le projet en cours : les analyses ne s’appuieront que sur les tables effectivement peuplées, et c’est ce qui compte pour aboutir.
L’ETL complet reste l’objectif
Si les ressources de votre équipe le permettent, l’ETL complet reste l’objectif : aligner l’ensemble du dictionnaire local, peupler toutes les tables OMOP, viser dès la première version un haut niveau de couverture. Ce qui est présenté ici est une alternative pragmatique pour les équipes qui n’ont pas ces ressources d’emblée, et qui veulent tout de même commencer à produire — quitte à étendre projet après projet.
Cet article suit cette démarche progressive. On présente d’abord la partie sémantique (la plus exigeante, qui mobilise les experts du domaine), puis le routage par domain_id, et enfin on déroule un exemple immersif sur MIMIC-IV : vous exécuterez vous-même le SQL dans votre navigateur, étape par étape, pour bâtir une mini-base OMOP.
Pré-requis
Cet article suppose la lecture des articles précédents de la série OMOP : le modèle OMOP CDM, les vocabulaires OMOP et l’écosystème d’outils OHDSI. On y emploie sans les redéfinir les notions de concept_id, domain_id, standard_concept, Maps to et concept_id = 0.
L’alignement sémantique, cœur du travail
L’alignement sémantique consiste à relier chaque code local (un itemid MIMIC, un identifiant de laboratoire interne, un libellé propre à un établissement) à un concept standard OMOP : un identifiant LOINC pour une mesure biologique, un SNOMED CT pour un diagnostic ou une observation, un RxNorm pour un médicament. Le résultat tient dans un fichier CSV : la fameuse SOURCE_TO_CONCEPT_MAP.
Cette étape est la plus chronophage de tout l’ETL. Elle s’appuie sur les experts du domaine : biologistes pour les paramètres de laboratoire, pharmaciens pour les médicaments, cliniciens (réanimateurs, internistes, urgentistes…) pour les diagnostics, observations et procédures. La qualité de l’alignement détermine la qualité de tout ce qui suivra — c’est là qu’il faut prendre le temps nécessaire.
Le module d’alignement de Linkr
Pour accompagner ce travail, Linkr propose un module dédié — l’Alignement de concepts. La création d’un projet d’alignement se fait en quelques clics, en important un fichier source (CSV, Parquet…) listant les concepts à aligner. Lors de cet import, Linkr propose une étape de paramétrage où vous choisissez les colonnes du fichier qui correspondent au code local, au libellé, à la catégorie, à la fréquence et aux métadonnées JSON :
Paramètres d'import
Configurez le parsing du fichier et associez les colonnes aux champs de concepts.
Associez les colonnes du fichier aux champs concept. Au minimum le nom ou le code du concept est requis.
| # | itemid | label | fluid | category |
|---|---|---|---|---|
| 1 | 50808 | Free Calcium | Blood | Blood Gas |
| 2 | 50826 | Tidal Volume | Blood | Blood Gas |
| 3 | 50813 | Lactate | Blood | Blood Gas |
| 4 | 52029 | % Ionized Calcium | Blood | Blood Gas |
| 5 | 50801 | Alveolar-arterial Gradient | Blood | Blood Gas |
| 6 | 50810 | Hematocrit, Calculated | Blood | Blood Gas |
| 7 | 50820 | pH | Blood | Blood Gas |
| 8 | 50912 | Creatinine | Blood | Chemistry |
| 9 | 51221 | Hematocrit | Blood | Hematology |
| 10 | 51301 | White Blood Cells | Blood | Hematology |
Une fois le projet créé, l’éditeur d’alignements s’ouvre en deux panneaux : à gauche les concepts source, à droite les candidats standards :
| Vocab | Nom du concept | Code concept | Patients | Lignes | ||
|---|---|---|---|---|---|---|
| ICCA | creatinine serum | L0021 | 1,842 | 9,614 | ||
| ICCA | lactate blood | L0102 | 1,203 | 4,871 | ||
| ICCA | hemoglobin | L0301 | 1,688 | 7,203 | ||
| ICCA | platelet count | L0411 | 974 | 3,120 | ||
| ICCA | WBC count | L0512 | 881 | 2,954 |
| Nom | Code | Vocab. | Domaine |
|---|---|---|---|
| Creatinine [Mass/volume] in Serum or Plasma | 2160-0 | LOINC | Measurement |
| Creatinine [Mass/volume] in Blood | 38483-4 | LOINC | Measurement |
| Creatinine [Moles/volume] in Serum or Plasma | 14682-9 | LOINC | Measurement |
L’alignement assisté par IA agentique
L’alignement à la main reste long. Pour gagner du temps sur la première passe, Linkr s’intègre avec Claude Code via un skill dédié concept-mapping. Un agent traite les concepts source par lots, propose pour chacun les meilleurs candidats standards, et produit un score combiné :
| Score | Méthodes | Vocab | Nom du concept | Code | Std | |
|---|---|---|---|---|---|---|
92% | 95%88%91% | LOINC | Creatinine [Mass/volume] in Serum or Plasma | 2160-0 | S | |
81% | 83%79%82% | LOINC | Creatinine [Mass/volume] in Blood | 38483-4 | S | |
76% | 80%71%77% | LOINC | Creatinine [Moles/volume] in Serum or Plasma | 14682-9 | S | |
68% | 72%64% | SNOMED | Serum creatinine measurement | 113075003 | S | |
54% | 58%51% | LOINC | Creatinine renal clearance | 2164-2 | S |
Quatre familles de méthodes contribuent à ce score :
- Syntaxique — similarité de chaînes entre le libellé local et les libellés standards (et leurs synonymes).
- Sémantique — distance entre embeddings biomédicaux (BioLORD-2023-M), calculée hors ligne sur l’ensemble du vocabulaire OMOP.
- Statistique (à venir) — comparaison des distributions de valeurs source/cible pour les concepts numériques.
- Agentique — un agent LLM examine les meilleurs candidats, applique du raisonnement clinique (unités cohérentes ? bonne granularité ? bon domaine ?) et propose une équivalence (
exactMatch,closeMatch,broadMatch,narrowMatch) avec une justification écrite.
Chaque suggestion reste à valider ou corriger explicitement dans l’éditeur — l’IA prépare le terrain, la décision finale revient toujours à l’évaluateur. La page Suggestions détaille tout ce mécanisme.
Métadonnées et traçabilité
Un alignement de concepts n’est pas qu’une table de correspondance. C’est aussi un dossier de traçabilité : qui a aligné quoi, quand, sur la base de quel raisonnement, avec quelle confiance, et quels commentaires ont été échangés au cours de la revue. Ces métadonnées sont précieuses pour l’équipe qui exploitera ensuite les données. Linkr les conserve avec chaque alignement et peut les exposer côté concept source — distributions, statistiques, propriétés, graphiques temporels :
Frequence_cardiaque
ICCA · M0042 · 12 450 lignes · 1 842 patients
Statistiques
| Min | 0 |
| P25 | 65 |
| Médiane | 78 |
| P75 | 92 |
| Max | 245 |
| Moyenne | 82,3 |
| Écart-type | 18,7 |
Boxplot
Distribution
Distribution par sexe
Évolution par année
Unités hospitalières
Propriétés
| Unité | bpm |
| Fréquence | Horaire |
| Complétude | 98 % |
| Source | Monitorage |
La structure JSON attendue est documentée sur la page Éditeur d’alignements. Elle accepte statistics (min/médiane/max/moyenne…), distributions (graphiques en barres, secteurs ou courbes), properties (paires clé/valeur libres) et metadata (métadonnées scalaires). Le tout sert à l’évaluateur pour valider un alignement en connaissance de cause.
Côté traçabilité, chaque alignement porte sa fiche de détail : libellés source et cible, statistiques, équivalence SKOS, statut, méthode, dates, et la liste des avis posés par les relecteurs. C’est cette fiche qui constitue le dossier complet d’un alignement, consultable à tout moment depuis la page Évaluation.
Concept source
| Vocabulaire source | ICCA |
| Nom concept source | TA_systolique_invasive |
| Code concept source | M0118 |
| Patients | 1 842 |
| Lignes | 24 380 |
Statistiques
| Min | 60 |
| P25 | 108 |
| Médiane | 122 |
| P75 | 138 |
| Max | 220 |
Boxplot (mmHg)
Concept cible
| Vocabulaire cible | SNOMED |
| ID concept cible | 4353843 |
| Nom concept cible | Invasive systolic arterial pressure |
| Code concept cible | 251071003 |
| Domaine | Measurement |
| Classe de concept | Observable Entity |
| Standard | S |
Métadonnées de l'alignement
| Équivalence | Exact |
| Status | Approuvé |
| Score de correspondance | — |
| Aligné par | John Doe |
| Créé le | 14/05/2026 14:32 |
| Dernière modification | 14/05/2026 14:32 |
Avis (1)
14/05/2026 15:08
Réseaux multicentriques
Dans un réseau de recherche où plusieurs hôpitaux collaborent sur des projets multicentriques, chaque établissement peut prendre en charge l’alignement d’une partie des concepts et contribuer son travail à un registre commun. C’est le principe du portail Linkr (Linkr portal) : un portail statique (déployable sur GitLab Pages, GitHub Pages ou tout hébergement statique) qui regroupe plusieurs workspaces d’alignement et projets, partagés entre équipes. La Vue d’ensemble attribue les source_concept_id custom (plage ≥ 2 milliards) par hôpital, en plages disjointes, pour éviter les collisions lors de la centralisation.
Outils OHDSI — l'alternative classique
La communauté OHDSI propose son propre outil pour cette étape : Usagi, une application de bureau Java. Vous chargez votre liste de codes locaux, Usagi propose des concepts standards par similarité textuelle, et vous validez à la main. Le format de sortie est compatible avec SOURCE_TO_CONCEPT_MAP. Usagi est stable, éprouvé, gratuit, hors ligne — la voie historique, qui reste tout à fait valable.
Le routage par domain_id
Une fois SOURCE_TO_CONCEPT_MAP produite, le reste de l’ETL devient surtout mécanique. Pour chaque ligne de donnée source, on regarde :
- Quel est le concept standard OMOP associé (via la jointure
SOURCE_TO_CONCEPT_MAP) ? - Quel est son
domain_id?
Et le domain_id détermine directement la table OMOP de destination :
domain_id | Table OMOP cible | Exemples |
|---|---|---|
Condition | CONDITION_OCCURRENCE | Diabète, sepsis, hypertension |
Drug | DRUG_EXPOSURE | Aspirine, héparine, furosémide |
Procedure | PROCEDURE_OCCURRENCE | Coloscopie, intubation, cathétérisme |
Measurement | MEASUREMENT | Créatinine, fréquence cardiaque, hémoglobine |
Device | DEVICE_EXPOSURE | Cathéter, sonde, ventilateur |
Observation | OBSERVATION | Statut, mode d’assurance, langue |
Visit | VISIT_OCCURRENCE / VISIT_DETAIL | Hospitalisation, urgences, séjour ICU |
Tout ce qui ne tombe dans aucune de ces tables atterrit par convention dans OBSERVATION — la table générique d’accueil prévue par OMOP pour ces cas. La logique d’implémentation devient alors la même pour toutes les tables d’événements : extraire les lignes source, joindre sur SOURCE_TO_CONCEPT_MAP, filtrer par domain_id du target, insérer dans la table cible avec les _concept_id standards, les _source_concept_id custom et les _source_value pour la traçabilité.
Construire un ETL MIMIC-IV → OMOP, pas à pas
Place à la pratique. On va construire ensemble une mini-base OMOP à partir de MIMIC-IV, en exécutant le SQL dans votre navigateur (via DuckDB-Wasm). Les données source sont celles de la démo publique MIMIC-IV — 100 patients, leurs séjours, leurs mesures, leurs prescriptions.
Avant d’attaquer le SQL, voici les cinq étapes de la démarche :
Extraire les concepts source
Inventorier ce qui est à aligner : une ligne par concept, avec ses fréquences et un info_json de métadonnées (distributions, unités, histogrammes…).
Récupérer la table d’alignement
L’alignement (manuel ou assisté par IA dans Linkr) produit le fichier source_to_concept_map.csv qui mappe chaque code source vers son concept standard.
Charger les vocabulaires de référence
Importer CONCEPT, CONCEPT_RELATIONSHIP et compagnie depuis ATHENA, plus le SOURCE_TO_CONCEPT_MAP produit à l’étape B.
Peupler les tables OMOP
D’abord les tables d’ancrage (PERSON, VISIT_OCCURRENCE, VISIT_DETAIL), puis une CTE de routage par domain_id qui peuple MEASUREMENT, CONDITION_OCCURRENCE et OBSERVATION. DRUG_EXPOSURE est traitée séparément.
Contrôler la qualité
Lancer ACHILLES et le Data Quality Dashboard, repérer les anomalies, corriger, recommencer.
Étape A — Extraire les concepts source
La toute première étape de tout ETL OMOP n’est pas un INSERT : c’est l’inventaire de ce qu’il y a à aligner. Pour notre périmètre (démographie, types de visite, parcours, diagnostics, mesures et médicaments), on extrait depuis les tables MIMIC les concepts source distincts, avec leurs fréquences (nombre de lignes, nombre de patients distincts) et quelques métadonnées (unités, catégories…) qui aideront ensuite l’évaluateur. La logique d’extraction tient en une requête SQL par catégorie :
Voici un extrait représentatif du SQL d’extraction pour les paramètres biologiques de labevents. La requête calcule en une passe les statistiques descriptives, l’histogramme, la distribution par sexe, la fréquence typique des mesures et la distribution temporelle — toutes assemblées dans un objet JSON unique qui suit le format attendu par Linkr (les autres catégories — chartevents, diagnoses_icd, transfers, admissions, prescriptions — suivent un patron très proche, ajusté selon que le concept est numérique ou catégoriel) :
-- Paramètres biologiques (labevents) avec info_json riche
WITH lab_stats AS (
SELECT
le.itemid,
COUNT(*) AS record_count,
COUNT(DISTINCT le.subject_id) AS patient_count,
MIN(le.valuenum) AS v_min,
MAX(le.valuenum) AS v_max,
AVG(le.valuenum) AS v_mean,
STDDEV(le.valuenum) AS v_sd,
QUANTILE_CONT(le.valuenum, 0.05) AS p5,
QUANTILE_CONT(le.valuenum, 0.25) AS p25,
QUANTILE_CONT(le.valuenum, 0.50) AS p50,
QUANTILE_CONT(le.valuenum, 0.75) AS p75,
QUANTILE_CONT(le.valuenum, 0.95) AS p95
FROM labevents le
WHERE le.itemid IN (50971, 50983, 50912, 50882, 51265, 51222, 51301, 50931)
GROUP BY le.itemid
)
SELECT
'MIMIC_LABITEMS' AS vocabulary,
'Biologie' AS category,
CAST(le.itemid AS VARCHAR) AS concept_code,
ANY_VALUE(dli.label) AS concept_name,
ANY_VALUE(s.record_count) AS record_count,
ANY_VALUE(s.patient_count) AS patient_count,
-- Compose the JSON metadata expected by Linkr's mapping editor
json_object(
'full_name', 'Lab test / ' || ANY_VALUE(dli.label),
'data_source', 'MIMIC-IV',
'data_types', 'numeric',
'unit', ANY_VALUE(le.valueuom),
'numeric_data', json_object(
'min', ANY_VALUE(s.v_min),
'p5', ANY_VALUE(s.p5),
'p25', ANY_VALUE(s.p25),
'median', ANY_VALUE(s.p50),
'mean', ANY_VALUE(s.v_mean),
'p75', ANY_VALUE(s.p75),
'p95', ANY_VALUE(s.p95),
'max', ANY_VALUE(s.v_max),
'sd', ANY_VALUE(s.v_sd)
)
-- + histogram, categorical_data (sex), temporal_distribution
-- (assemblés via des sous-requêtes du même genre)
) AS info_json
FROM labevents le
JOIN d_labitems dli ON dli.itemid = le.itemid
JOIN lab_stats s ON s.itemid = le.itemid
WHERE le.itemid IN (50971, 50983, 50912, 50882, 51265, 51222, 51301, 50931)
GROUP BY le.itemid; La colonne info_json qui en résulte suit la structure documentée dans la page Éditeur d’alignements, que Linkr lit pour afficher la fiche détaillée du concept source. Pour un signe vital comme la fréquence cardiaque, le résultat ressemble à :
{
"full_name": "Vital sign / Routine Vital Signs / Heart Rate",
"data_source": "MIMIC-IV",
"data_types": "numeric",
"unit": "bpm",
"missing_rate": 0.0,
"numeric_data": {
"min": 0.0, "p5": 62.0, "p25": 78.0, "median": 90.0, "mean": 91.12,
"p75": 104.0, "p95": 122.0, "max": 200.0, "sd": 18.69
},
"histogram": [
{ "x": 10.0, "count": 3 }, { "x": 30.0, "count": 12 },
{ "x": 50.0, "count": 454 }, { "x": 70.0, "count": 3313 },
{ "x": 90.0, "count": 5555 }, { "x": 110.0, "count": 3685 },
{ "x": 130.0, "count": 764 }, { "x": 150.0, "count": 117 }
],
"categorical_data": [
{ "category": "Female", "count": 6284, "percentage": 45.2 },
{ "category": "Male", "count": 7629, "percentage": 54.8 }
],
"temporal_distribution": {
"start_date": "2110-04-11", "end_date": "2201-12-13",
"by_year": [ { "year": 2110, "percentage": 6.5 }, … ]
},
"measurement_frequency": "every 1 hours"
} Le résultat final est un CSV avec une ligne par concept source — pour notre périmètre, 46 concepts au total (2 sexes + 7 types d’admission + 8 unités + 10 diagnostics + 8 vitaux + 8 paramètres bio + 3 médicaments).
Pour aller plus loin, ouvrez la démo Linkr et :
- Créez un nouveau projet d’alignement (voir la doc Projets d’alignement).
- Importez le fichier
source-concepts.csvci-dessus, en mappant les colonnes selon les paramètres d’import présentés plus haut. - Procédez à l’alignement concept par concept dans l’éditeur — manuel, ou avec l’assistance IA via la page Suggestions.
Pour les besoins de la suite, on saute cette étape et on récupère directement la table d’alignement déjà produite.
Étape B — Récupérer la table d’alignement
On suppose ici l’alignement bouclé : voici le fichier final, prêt à être chargé dans la base OMOP.
46 lignes, 9 colonnes au total (le détail complet est dans la référence de la table source_to_concept_map). Trois lignes représentatives, dans l’ordre des colonnes du schéma :
| source_code | source_concept_id | source_vocabulary_id | source_code_description | target_concept_id | target_vocabulary_id | valid_start_date | valid_end_date | invalid_reason |
|---|---|---|---|---|---|---|---|---|
| F | 2000000001 | MIMIC_GENDER | Female | 8532 | Gender | 1970-01-01 | 2099-12-31 | NULL |
| 220045 | 2000000028 | MIMIC_CHARTEVENTS | Heart Rate | 3027018 | LOINC | 1970-01-01 | 2099-12-31 | NULL |
| Aspirin | 2000000044 | MIMIC_DRUG | Aspirin | 1112807 | RxNorm | 1970-01-01 | 2099-12-31 | NULL |
Notez les source_concept_id dans la plage custom OMOP (≥ 2 milliards) et les target_concept_id qui pointent vers les concepts standards.
Étape C — Charger les vocabulaires de référence dans la base
Avant d’écrire les INSERT métier, la base OMOP cible doit contenir le dictionnaire des concepts standards — typiquement les tables CONCEPT, CONCEPT_RELATIONSHIP, CONCEPT_SYNONYM, CONCEPT_ANCESTOR, VOCABULARY, DOMAIN. On les récupère depuis ATHENA (compte gratuit nécessaire) en cochant les vocabulaires utiles à notre périmètre (SNOMED CT, LOINC, RxNorm, ICD10CM, UCUM…). Le téléchargement renvoie un ZIP de fichiers CSV à charger via un COPY ou un INSERT INTO ... SELECT FROM read_csv(...).
Une fois ces vocabulaires en place, on charge également notre fichier source_to_concept_map.csv dans la table éponyme, et on ajoute les concepts custom (les source_concept_id ≥ 2 milliards de notre STCM) dans la table CONCEPT — c’est ce qui permettra aux _source_concept_id de nos lignes d’événements de pointer vers des entrées valides de la base.
Pré-requis du tutoriel interactif
Pour simplifier la démo qui suit, le composant interactif ne charge pas la table CONCEPT entière : il prépare uniquement la table source_to_concept_map avec nos 46 alignements et crée les tables OMOP cibles vides. Sur une vraie base OMOP, l’étape C est obligatoire — sans elle, ni ACHILLES ni le DQD ne peuvent fonctionner correctement, et les requêtes utilisant le vocabulaire pour explorer les hiérarchies (par exemple CONCEPT_ANCESTOR) ne renverront aucun résultat.
Étape D — Peupler les tables OMOP
Place à l’ETL proprement dit. Les données MIMIC raw et la table d’alignement sont déjà chargées dans une base DuckDB qui tourne dans votre navigateur. Les tables OMOP cibles (PERSON, VISIT_OCCURRENCE, VISIT_DETAIL, MEASUREMENT, CONDITION_OCCURRENCE, OBSERVATION, DRUG_EXPOSURE) sont créées vides au schéma v5.4.
Tables d’ancrage
Trois tables sont peuplées en premier — chacune avec son propre INSERT :
PERSONdepuispatients(gender, year of birth dérivé deanchor_year - anchor_age).VISIT_OCCURRENCEdepuisadmissions(un séjour = unhadm_id).VISIT_DETAILdepuistransfers(un passage en unité de soins = untransfer_id).
Routage par domain_id
Sur MIMIC, les vocabulaires sources sont cloisonnés par domaine (MIMIC_CHARTEVENTS → Measurement, MIMIC_DRUG → Drug, MIMIC_ICD10 → Condition…) — on pourrait théoriquement écrire un INSERT par vocabulaire en se reposant sur ce cloisonnement.
Cependant, pour rester généraliste et coller à l’approche décrite plus haut, on prépare une CTE matérialisée mapped_events qui unifie les sources d’événements (diagnostics, signes vitaux, biologies) et résout le domain_id du concept cible via CONCEPT. On filtre ensuite cette CTE par target_domain_id pour dispatcher vers la bonne table OMOP :
target_domain_id = 'Measurement'→MEASUREMENTtarget_domain_id = 'Condition'→CONDITION_OCCURRENCE- Tout le reste →
OBSERVATION(table de repli par défaut)
PROCEDURE_OCCURRENCE, DEVICE_EXPOSURE, SPECIMEN, NOTE… suivent exactement la même logique — il suffit d’ajouter une branche WHERE target_domain_id = '…' ; on les laisse de côté ici, on n’a pas de concept correspondant dans notre périmètre.
DRUG_EXPOSURE est traitée à part : ses sources et ses spécificités (dose, route, unité) ne se prêtent pas au même routeur générique.
Cas d'un vocabulaire source hétérogène
Sur des bases moins propres — typiquement un vocabulaire interne d’hôpital qui mélange dans un même tableau de codes des biologies, des observations cliniques, des scores, voire des médicaments — le cloisonnement par vocabulaire ne marche plus. La CTE mapped_events décrite ici, qui résout le domain_id via CONCEPT, devient alors indispensable : c’est la seule manière de router correctement chaque ligne. C’est précisément pour cette raison qu’on adopte cette approche dès le départ — au prix d’une jointure supplémentaire — plutôt qu’un INSERT par vocabulaire.
Chaque table OMOP a sa propre section ci-dessous, avec sa requête INSERT INTO prête à l’emploi suivie d’un SELECT qui affiche un aperçu du résultat. Pour exécuter une requête, cliquez sur Exécuter (ou utilisez le raccourci ⌘ / Ctrl + Entrée). Le bouton Réinitialiser la requête au-dessus de chaque éditeur remet la version d’origine en un clic. Lancez les requêtes dans l’ordre : mapped_events doit exister avant les INSERT de routage, et PERSON / VISIT_OCCURRENCE doivent exister avant DRUG_EXPOSURE.
Le pattern est toujours le même : JOIN sur source_to_concept_map, INSERT dans la table cible, COALESCE(..., 0) pour les codes non alignés afin de ne pas perdre les lignes, et les colonnes _source_value et _source_concept_id conservent la traçabilité vers les données d’origine.
DRUG_EXPOSURE — version pédagogique simplifiée
L’INSERT dans DRUG_EXPOSURE ci-dessus est volontairement minimaliste : on extrait la quantité de form_val_disp par regex, on remplit route_source_value, dose_unit_source_value et drug_source_value, et c’est tout. Une vraie implémentation calcule la dose réellement reçue à partir de amount_value/amount_unit (quantité unitaire), numerator_value/denominator_value (concentration des solutions), de la durée d’administration et de la fréquence — et s’appuie sur DRUG_STRENGTH côté vocabulaire pour résoudre les présentations des spécialités. Les formules sont détaillées dans la documentation OMOP « drug dose ». À calculer dans un vrai projet, on ne le fait pas ici pour ne pas alourdir le tutoriel.
Étape E — Valider la qualité de la base produite
Une fois les INSERT bouclés, la base OMOP existe. Mais elle peut contenir des incohérences silencieuses : dates aberrantes, concepts non standards là où on attend un standard, champs obligatoires nuls. Avant toute utilisation pour la recherche, il faut mesurer la qualité.
Deux outils OHDSI sont conçus pour ça :
- ACHILLES — un package R qui produit un rapport synthétique de caractérisation : nombre de patients, distributions d’âge, prévalences des conditions, fréquences des médicaments, distributions des mesures biologiques. Excellent pour prendre la mesure de ce que contient la base — et repérer rapidement les anomalies grossières.
- Data Quality Dashboard (DQD) — environ 4 000 contrôles automatisés répartis en trois axes : conformité du schéma, complétude des champs obligatoires, plausibilité des valeurs. Le rapport HTML interactif permet de prioriser les corrections.
À l’usage, le DQD remonte presque toujours des centaines d’anomalies sur une base fraîchement produite. La plupart sont mineures (concepts non standards, dates manquantes sur des événements optionnels…), quelques-unes pointent de vrais bugs dans l’ETL. C’est un cycle itératif : on lance, on corrige, on relance, jusqu’à un niveau de qualité jugé suffisant pour la diffusion.
Tables vides et DQD
Le DQD gère explicitement le cas des tables non peuplées — c’est précisément le scénario d’un ETL projet par projet. Si une table existe mais reste vide, tous les contrôles de niveau champ et concept la concernant sont marqués notApplicable (les détails sont dans la documentation officielle des statuts). Ces contrôles ne comptent pas comme des anomalies. Vous pouvez en plus exclure proactivement certaines tables via le paramètre tablesToExclude d’executeDqChecks() — par défaut, le DQD exclut déjà les tables de vocabulaire (CONCEPT, VOCABULARY, etc.).
Et après ?
Une fois votre première base OMOP en place, le projet suivant réutilisera les alignements existants et n’ajoutera que quelques nouveaux concepts. Au fil des projets, votre couverture du dictionnaire local devient conséquente — sans avoir eu à tout aligner en une seule fois.
- L'ETL OMOP n'a pas besoin d'être réalisé d'un seul bloc : on procède projet par projet, en alignant uniquement les concepts dont le projet a réellement besoin.
- L'alignement sémantique est la partie la plus chronophage. Elle mobilise les experts du domaine (biologistes, pharmaciens, cliniciens) et produit la table
SOURCE_TO_CONCEPT_MAP. - L'IA agentique (Claude Code intégré à Linkr, scores syntaxiques + sémantiques + LLM avec équivalences justifiées) accélère considérablement la première passe — chaque suggestion reste validée explicitement par un évaluateur.
- Une fois l'alignement en place, le routage suit le
domain_iddu concept cible : Condition →CONDITION_OCCURRENCE, Drug →DRUG_EXPOSURE, Measurement →MEASUREMENT, etc. Tout ce qui ne tombe pas ailleurs atterrit dansOBSERVATION, la table de repli par défaut prévue par OMOP. - Toutes les tables d'événements suivent la même structure-type : extraction + JOIN sur STCM + INSERT avec les colonnes
_concept_id(standard),_source_concept_id(custom, plage ≥ 2 milliards) et_source_value(libellé d'origine, pour traçabilité). - Les réseaux multicentriques centralisent les alignements via linkr-portal : un portail statique partagé entre établissements, avec plages d'IDs custom disjointes pour éviter les collisions.
- La validation finale par ACHILLES et le Data Quality Dashboard est un cycle itératif : on lance, on corrige, on relance — jusqu'à un niveau de qualité diffusable.