Mapping SDTM en R avec sdtm.oak : des données EDC brutes à un domaine conforme

Mappez un extrait EDC brut de signes vitaux vers un domaine SDTM VS conforme à CDISC en R avec sdtm.oak — le modèle de tabulation, la Controlled Terminology, les algorithmes de mapping oak, et un contrôle de conformité

Un tutoriel complet et exécutable pour le mapping SDTM en R. Découvrez ce qu’est le modèle de tabulation SDTM de CDISC (classes d’observation, domaines, la variable topic, la structure –SEQ/–DTC, la Controlled Terminology), puis mappez un extrait EDC brut de signes vitaux vers un domaine SDTM VS conforme avec sdtm.oak : generate_oak_id_vars() pour la traçabilité des enregistrements, hardcode_ct() et assign_ct() pour les qualificateurs de Controlled Terminology, assign_no_ct() pour les résultats, assign_datetime() pour les dates ISO 8601, et derive_seq() pour –SEQ — puis vérifiez la conformité avec sdtmchecks, le tout sur des données d’exemple publiques pour que chaque ligne s’exécute.

Date de publication

30 juin 2026

Modifié

7 juillet 2026

AstucePoints clés
  • Le SDTM est un modèle de tabulation. Il réorganise les données EDC brutes en domaines standard (DM, AE, EX, VS…) selon une structure fixe, de sorte qu’un évaluateur — et un outil de validation de la FDA — puisse lire n’importe quelle étude de la même façon.
  • Chaque enregistrement Findings est une variable topic accompagnée de qualificateurs. Pour les signes vitaux, le topic est VSTESTCD (ce qui a été mesuré) ; VSORRES/VSORRESU (le résultat et l’unité), VSPOS, VSDTC et VSSEQ le qualifient. Le mapping consiste à remplir ces colonnes à partir des champs bruts.
  • sdtm.oak mappe un champ brut à la fois à l’aide d’algorithmes nommés. hardcode_ct() fixe une valeur codée constante, assign_no_ct() copie une valeur collectée, assign_ct() recode une valeur via la Controlled Terminology, et assign_datetime() construit une date ISO 8601.
  • oak_id_vars est la traçabilité qui relie le tout. generate_oak_id_vars() estampille chaque ligne brute d’un identifiant ; chaque mapping de qualificateur se joint dessus, de sorte que des mappings partiels se combinent en un enregistrement correct unique sans duplication de ligne.
  • La conformité est une étape obligatoire, pas une réflexion après coup. derive_seq() numérote les enregistrements, et sdtmchecks exécute les mêmes contrôles d’intégrité des données qu’un régulateur — ici, en confirmant qu’aucun enregistrement n’a une pression artérielle systolique inférieure à la diastolique.

Introduction

Une base de données clinique n’arrive jamais sous une forme prête pour la soumission. Ce qui sort du système EDC (saisie électronique des données) est un extrait brut de signes vitaux : une ligne large par évaluation, avec des noms de colonnes choisis par le promoteur (SYS_BP, DIA_BP, PULSE), des positions en texte libre, et des dates dans le format qu’utilisait le formulaire. Avant que quiconque puisse l’analyser — ou avant qu’il puisse être soumis à un régulateur — il doit devenir du SDTM : la tabulation standardisée, à une observation par ligne, qu’un évaluateur peut ouvrir et comprendre immédiatement.

Cette leçon réalise ce mapping de bout en bout en R. Nous prenons l’extrait brut de signes vitaux livré avec sdtm.oak — le package pharmaverse conçu pour la transformation des données brutes en SDTM — et le transformons en un domaine VS (Vital Signs) conforme, puis nous le vérifions avec sdtmchecks. Le SDTM est l’amont du flux de données clinique : brut → SDTM → ADaM → tables, listings et figures. Réussissez-le et chaque jeu de données en aval hérite d’entrées propres et standard. Nous travaillons sur des données d’exemple publiques, de sorte que chaque ligne s’exécute telle quelle et suit la vignette officielle du domaine Findings de sdtm.oak.

Voici où nous allons — le domaine VS analysable que nous allons construire, lu directement à partir des colonnes de résultat et d’unité standardisées :

Strip plot of the mapped SDTM VS domain. Each point is one standardized vital-signs result (VSSTRESN) positioned by value on the x axis, grouped on the y axis by the three test labels Systolic Blood Pressure, Diastolic Blood Pressure, and Pulse Rate, and coloured by subject (two subjects, azure and orange). Systolic readings cluster between 85 and 158, diastolic between 62 and 92, and pulse between 56 and 76 — every reading now carries a test code, a unit, and a subject, so the domain is queryable. The point is that a wide six-row raw extract has become a tidy, standardized set of observations.

Chaque point ici est une observation SDTM — un code de test, une valeur, une unité, un sujet. À la fin de cette leçon, vous les aurez tous produits à partir de six lignes brutes. Nouveau dans le pipeline plus large ? Commencez par le flux de données des essais cliniques, qui situe où se place le SDTM, et les standards CDISC, qui placent le SDTM aux côtés de CDASH et ADaM.

Ce qu’est le SDTM

Le SDTM — le Study Data Tabulation Model — est le standard CDISC (Clinical Data Interchange Standards Consortium) pour organiser les données collectées dans un essai clinique. C’est le format que la FDA et la PMDA exigent pour les jeux de données de tabulation bruts d’une soumission. Le modèle et son guide d’implémentation sont publiés par CDISC : le standard fondamental SDTM et le SDTM Implementation Guide (SDTMIG), qui spécifie les domaines et variables concrets.

Le SDTM regroupe chaque observation collectée dans l’une des trois classes générales d’observation, plus quelques domaines à usage spécial :

Classe d’observation Ce qu’elle contient Exemples de domaines Variable topic
Interventions Ce qui est administré ou fait au sujet CM (médicaments concomitants), EX (exposition) --TRT
Events Ce qui est arrivé au sujet AE (événements indésirables), MH (antécédents médicaux) --TERM / --DECOD
Findings Mesures et évaluations VS (signes vitaux), LB (analyses biologiques), EG (ECG) --TESTCD
Usage spécial Au niveau sujet / plan de l’étude DM (démographie), DS (disposition)

Les signes vitaux constituent un domaine Findings, donc chaque enregistrement répond à « qu’a-t-on mesuré, et quel était le résultat ». Cette forme est la clé de tout le modèle :

Rôle de la variable Exemple VS Ce qu’elle porte
Identifiants STUDYID, USUBJID, DOMAIN, VSSEQ L’étude, le sujet, le code de domaine, et un numéro de séquence unique par enregistrement.
Topic VSTESTCD, VSTEST Ce qui a été mesuré — le code court (SYSBP) et son libellé décodé.
Qualificateurs de résultat VSORRES, VSORRESU, VSSTRESN, VSPOS Le résultat original, son unité, le résultat numérique standardisé, et la position.
Temporalité VSDTC, VISIT, VISITNUM Quand — une date/heure ISO 8601 et la visite.

Deux conventions font l’essentiel du travail. D’abord, la variable topic (VSTESTCD) nomme l’observation, et tout le reste la qualifie — de sorte qu’une seule ligne brute large contenant systolique, diastolique et pouls devient trois lignes SDTM, une par test. Ensuite, de nombreuses colonnes sont restreintes par la Controlled Terminology (CT) : VSTESTCD doit valoir SYSBP, pas « Systolic » ni « sbp ». CDISC publie les codelists sous le nom de SDTM Controlled Terminology, et la CT de l’étude est la table de correspondance qui recode les valeurs collectées vers ces termes standard. Mapper le brut vers le SDTM consiste, en substance, à remplir les bonnes colonnes de qualificateurs avec les bonnes valeurs valides au regard de la CT.

Les données brutes et la Controlled Terminology

Deux entrées pilotent un mapping : l’extrait brut et la Controlled Terminology de l’étude. Les deux sont livrées avec sdtm.oak, nous pouvons donc les lire directement depuis le package.

library(sdtm.oak)
library(dplyr, warn.conflicts = FALSE)

# Raw vital-signs extract: one wide row per assessment, sponsor column names
vs_raw <- read.csv(system.file("raw_data/vitals_raw_data.csv", package = "sdtm.oak"))
# Study controlled terminology: collected value -> standard coded term
study_ct <- read.csv(system.file("raw_data/sdtm_ct.csv", package = "sdtm.oak"))

vs_raw |>
  select(PATNUM, INSTANCE, VTLD, SYS_BP, DIA_BP, PULSE, SUBPOS) |>
  head()
  PATNUM  INSTANCE      VTLD SYS_BP DIA_BP PULSE         SUBPOS
1    375    VISIT1 16-May-15    158     92    63          PRONE
2    375    VISIT1 16-May-15     94     78    76 SEMI-RECUMBENT
3    375 Screening  6-May-18    117     62    66          PRONE
4    376 Screening               NA     NA    NA               
5    376    VISIT1 23-Oct-08     85     68    73          PRONE
6    376    VISIT1 23-Oct-08    126     81    56          PRONE

Chaque ligne brute contient plusieurs mesures côte à côte — SYS_BP, DIA_BP, PULSE — collectées lors d’une visite (INSTANCE) à une date (VTLD). La Controlled Terminology fait correspondre chaque valeur collectée à son terme standard au sein d’un codelist :

study_ct |>
  filter(codelist_code == "C71148") |>     # VSPOS — subject position
  select(codelist_code, term_value, collected_value)
  codelist_code     term_value collected_value
1        C71148 SEMI-RECUMBENT     Semi-Supine
2        C71148        SITTING         Sitting
3        C71148          PRONE           Prone
4        C71148       STANDING        Standing
5        C71148         SUPINE          Supine

collected_value est ce que le CRF a enregistré ; term_value est le terme standard CDISC que sdtm.oak substituera. L’argument ct_clst de chaque fonction sensible à la CT pointe vers l’une de ces valeurs codelist_code.

Estampiller la traçabilité des enregistrements avec oak_id_vars

Parce que nous allons mapper un champ brut à la fois puis recoudre les morceaux ensemble, sdtm.oak a besoin d’une clé stable sur chaque ligne brute. generate_oak_id_vars() en ajoute trois : oak_id (un numéro de ligne), raw_source (de quel extrait elle provient) et patient_number.

vs_raw <- vs_raw |>
  generate_oak_id_vars(pat_var = "PATNUM", raw_src = "vitals")

vs_raw |>
  select(oak_id, raw_source, patient_number, SYS_BP, DIA_BP) |>
  head()
  oak_id raw_source patient_number SYS_BP DIA_BP
1      1     vitals            375    158     92
2      2     vitals            375     94     78
3      3     vitals            375    117     62
4      4     vitals            376     NA     NA
5      5     vitals            376     85     68
6      6     vitals            376    126     81

Ces trois colonnes constituent les oak_id_vars. Chaque mapping de qualificateur ci-dessous passe id_vars = oak_id_vars(), ce qui indique à sdtm.oak de faire la jointure de la nouvelle colonne sur le bon enregistrement par cette clé — le mécanisme qui permet aux mappings partiels de se combiner sans dupliquer de lignes.

Mapper la variable topic

Un enregistrement Findings commence par son topic. Nous mappons VSTESTCD = "SYSBP" à partir du champ brut SYS_BP avec hardcode_ct() — « hardcode » parce que la valeur est fixe (chaque lecture SYS_BP est une mesure systolique), et _ct parce que VSTESTCD relève de la Controlled Terminology (codelist C66741). Nous supprimons ensuite les lignes où le topic est manquant — seules les mesures réelles ont besoin de colonnes de qualificateurs.

vs_sysbp <- hardcode_ct(
  raw_dat = vs_raw,
  raw_var = "SYS_BP",
  tgt_var = "VSTESTCD",
  tgt_val = "SYSBP",
  ct_spec = study_ct,
  ct_clst = "C66741"
) |>
  filter(!is.na(VSTESTCD))

vs_sysbp |>
  select(oak_id, patient_number, VSTESTCD) |>
  head()
# A tibble: 5 × 3
  oak_id patient_number VSTESTCD
   <int>          <int> <chr>   
1      1            375 SYSBP   
2      2            375 SYSBP   
3      3            375 SYSBP   
4      5            376 SYSBP   
5      6            376 SYSBP   

Une seule colonne SDTM existe pour l’instant. Chaque qualificateur restant est ajouté de la même façon, en se rejoignant sur oak_id_vars.

Mapper les qualificateurs

Remplissez maintenant les colonnes qui qualifient une lecture systolique. Chaque appel ajoute une variable cible avec l’algorithme adapté à son mapping :

  • hardcode_ct() — une valeur CT fixe : VSTEST est toujours "Systolic Blood Pressure", VSORRESU est toujours "mmHg".
  • assign_no_ct() — copier une valeur collectée sans terminologie : VSORRES est le nombre brut SYS_BP, repris tel qu’enregistré.
  • assign_ct() — recoder une valeur collectée via la CT : VSPOS provient du SUBPOS brut, mappé vers le codelist de position standard (C71148).
vs_sysbp <- vs_sysbp |>
  hardcode_ct(
    raw_dat = vs_raw, raw_var = "SYS_BP", tgt_var = "VSTEST",
    tgt_val = "Systolic Blood Pressure",
    ct_spec = study_ct, ct_clst = "C67153", id_vars = oak_id_vars()
  ) |>
  assign_no_ct(
    raw_dat = vs_raw, raw_var = "SYS_BP", tgt_var = "VSORRES",
    id_vars = oak_id_vars()
  ) |>
  hardcode_ct(
    raw_dat = vs_raw, raw_var = "SYS_BP", tgt_var = "VSORRESU",
    tgt_val = "mmHg",
    ct_spec = study_ct, ct_clst = "C66770", id_vars = oak_id_vars()
  ) |>
  assign_ct(
    raw_dat = vs_raw, raw_var = "SUBPOS", tgt_var = "VSPOS",
    ct_spec = study_ct, ct_clst = "C71148", id_vars = oak_id_vars()
  )

vs_sysbp |>
  select(patient_number, VSTESTCD, VSTEST, VSORRES, VSORRESU, VSPOS) |>
  head()
# A tibble: 5 × 6
  patient_number VSTESTCD VSTEST                  VSORRES VSORRESU VSPOS        
           <int> <chr>    <chr>                     <int> <chr>    <chr>        
1            375 SYSBP    Systolic Blood Pressure     158 mmHg     PRONE        
2            375 SYSBP    Systolic Blood Pressure      94 mmHg     SEMI-RECUMBE…
3            375 SYSBP    Systolic Blood Pressure     117 mmHg     PRONE        
4            376 SYSBP    Systolic Blood Pressure      85 mmHg     PRONE        
5            376 SYSBP    Systolic Blood Pressure     126 mmHg     PRONE        

Voilà un ensemble complet d’enregistrements systoliques : le topic, son libellé décodé, le résultat et l’unité, et la position (standardisée par la CT). Remarquez que VSPOS vaut désormais PRONE/SEMI-RECUMBENT — les termes standard — même si le CRF les a peut-être collectés différemment.

Répéter pour les autres signes vitaux

La pression artérielle diastolique et le pouls suivent le schéma identique — seuls changent le champ brut, le code de test, le libellé et l’unité. Nous écrivons chacun comme son propre bloc, puis nous les empilons avec bind_rows(). (Un vrai domaine VS répète cela aussi pour la température, la fréquence respiratoire et la saturation en oxygène ; trois tests suffisent à montrer le schéma.)

vs_diabp <- hardcode_ct(raw_dat = vs_raw, raw_var = "DIA_BP", tgt_var = "VSTESTCD",
    tgt_val = "DIABP", ct_spec = study_ct, ct_clst = "C66741") |>
  filter(!is.na(VSTESTCD)) |>
  hardcode_ct(raw_dat = vs_raw, raw_var = "DIA_BP", tgt_var = "VSTEST",
    tgt_val = "Diastolic Blood Pressure",
    ct_spec = study_ct, ct_clst = "C67153", id_vars = oak_id_vars()) |>
  assign_no_ct(raw_dat = vs_raw, raw_var = "DIA_BP", tgt_var = "VSORRES",
    id_vars = oak_id_vars()) |>
  hardcode_ct(raw_dat = vs_raw, raw_var = "DIA_BP", tgt_var = "VSORRESU",
    tgt_val = "mmHg", ct_spec = study_ct, ct_clst = "C66770", id_vars = oak_id_vars()) |>
  assign_ct(raw_dat = vs_raw, raw_var = "SUBPOS", tgt_var = "VSPOS",
    ct_spec = study_ct, ct_clst = "C71148", id_vars = oak_id_vars())

vs_pulse <- hardcode_ct(raw_dat = vs_raw, raw_var = "PULSE", tgt_var = "VSTESTCD",
    tgt_val = "PULSE", ct_spec = study_ct, ct_clst = "C66741") |>
  filter(!is.na(VSTESTCD)) |>
  hardcode_ct(raw_dat = vs_raw, raw_var = "PULSE", tgt_var = "VSTEST",
    tgt_val = "Pulse Rate",
    ct_spec = study_ct, ct_clst = "C67153", id_vars = oak_id_vars()) |>
  assign_no_ct(raw_dat = vs_raw, raw_var = "PULSE", tgt_var = "VSORRES",
    id_vars = oak_id_vars()) |>
  hardcode_ct(raw_dat = vs_raw, raw_var = "PULSE", tgt_var = "VSORRESU",
    tgt_val = "beats/min", ct_spec = study_ct, ct_clst = "C66770", id_vars = oak_id_vars())

# Stack the per-test frames into one long VS frame
vs <- bind_rows(vs_sysbp, vs_diabp, vs_pulse)

vs |> count(VSTESTCD)
# A tibble: 3 × 2
  VSTESTCD     n
  <chr>    <int>
1 DIABP        5
2 PULSE        5
3 SYSBP        5

Une ligne large de trois mesures est devenue trois enregistrements SDTM, chacun étant une observation auto-descriptive. Ce passage du format large au format long est exactement l’idée de la tabulation SDTM : une ligne par observation, et non une ligne par évaluation.

Mapper la variable de temporalité

Les enregistrements Findings ont besoin d’un --DTC : une date/heure d’observation au format ISO 8601. La date et l’heure brutes se trouvent dans deux champs (VTLD, VTLTM) dans un format non standard. assign_datetime() les analyse et écrit un VSDTC conforme, appliqué d’un coup à l’ensemble de la trame empilée (il est commun à chaque test).

vs <- vs |>
  assign_datetime(
    raw_dat = vs_raw,
    raw_var = c("VTLD", "VTLTM"),
    tgt_var = "VSDTC",
    raw_fmt = c(list(c("d-m-y", "dd-mmm-yyyy")), "H:M")
  )

vs |>
  select(patient_number, VSTESTCD, VSORRES, VSDTC) |>
  head()
# A tibble: 6 × 4
  patient_number VSTESTCD VSORRES VSDTC           
           <int> <chr>      <int> <iso8601>       
1            375 SYSBP        158 2015-05-16T07:25
2            375 DIABP         92 2015-05-16T07:25
3            375 PULSE         63 2015-05-16T07:25
4            375 SYSBP         94 2015-05-16T10:25
5            375 DIABP         78 2015-05-16T10:25
6            375 PULSE         76 2015-05-16T10:25

raw_fmt indique à sdtm.oak comment la date et l’heure brutes ont été écrites (16-May-15, 7:25) ; il renvoie la forme standard 2015-05-16T07:25 qu’utilise chaque date SDTM. L’ISO 8601 est ce qui rend les dates comparables entre études et outils — voir la vignette sur la gestion des dates de sdtm.oak pour l’ensemble complet des formats pris en charge.

Ajouter les identifiants et le résultat standardisé

Un enregistrement conforme a besoin de ses identifiants et d’un résultat standardisé. STUDYID, DOMAIN et USUBJID sont de simples affectations ; VSSTRESC/VSSTRESN portent le résultat sous forme caractère et numérique standard (ici les mêmes nombres, puisque les unités sont déjà standard) ; et derive_seq() affecte VSSEQ — un numéro de séquence unique par enregistrement au sein d’un sujet, une exigence SDTM.

vs <- vs |>
  mutate(
    STUDYID = "CDISC01",
    DOMAIN = "VS",
    USUBJID = paste0(STUDYID, "-", patient_number),
    VSSTRESC = as.character(VSORRES),
    VSSTRESN = as.numeric(VSORRES)
  ) |>
  derive_seq(
    tgt_var = "VSSEQ",
    rec_vars = c("USUBJID", "VSTESTCD", "VSDTC")
  ) |>
  select(STUDYID, DOMAIN, USUBJID, VSSEQ, VSTESTCD, VSTEST,
         VSORRES, VSORRESU, VSSTRESN, VSPOS, VSDTC)

head(vs)
# A tibble: 6 × 11
  STUDYID DOMAIN USUBJID   VSSEQ VSTESTCD VSTEST VSORRES VSORRESU VSSTRESN VSPOS
  <chr>   <chr>  <chr>     <int> <chr>    <chr>    <int> <chr>       <dbl> <chr>
1 CDISC01 VS     CDISC01-…     1 DIABP    Diast…      92 mmHg           92 PRONE
2 CDISC01 VS     CDISC01-…     2 DIABP    Diast…      78 mmHg           78 SEMI…
3 CDISC01 VS     CDISC01-…     3 DIABP    Diast…      62 mmHg           62 PRONE
4 CDISC01 VS     CDISC01-…     4 PULSE    Pulse…      63 beats/m…       63 <NA> 
5 CDISC01 VS     CDISC01-…     5 PULSE    Pulse…      76 beats/m…       76 <NA> 
6 CDISC01 VS     CDISC01-…     6 PULSE    Pulse…      66 beats/m…       66 <NA> 
# ℹ 1 more variable: VSDTC <iso8601>

Voilà un domaine VS conforme : d’abord les colonnes d’identifiants SDTM, puis le topic, les qualificateurs et la temporalité. derive_seq() ordonne les enregistrements par rec_vars et les numérote 1, 2, 3… au sein de chaque sujet, de sorte que VSSEQ soit unique par USUBJID — la clé sur laquelle repose chaque jointure en aval.

Vérifier la conformité

Avant qu’un domaine ne passe en aval, il est contrôlé au regard des règles SDTM. En production, cela se fait avec des outils de validation (Pinnacle 21) et, en R, le package sdtmchecks, qui regroupe le type de contrôles d’intégrité des données qu’exécute l’outillage d’un régulateur. Un contrôle naturel pour les signes vitaux : aucun enregistrement ne devrait avoir une pression artérielle systolique inférieure à la diastolique — une valeur physiologiquement impossible qui signale une erreur de données.

library(sdtmchecks)

# sdtmchecks pairs SYSBP/DIABP by subject and visit, then flags any SBP < DBP.
# It needs VISIT and VSSPID present. In a full mapping VISIT/VISITNUM map from the raw
# INSTANCE via controlled terminology; this single-visit example just adds a placeholder
# VISIT to satisfy the check, then runs it.
vs_check <- vs |>
  mutate(VISIT = "VISIT 1", VSSPID = paste0("VS-", USUBJID, "-", VSDTC))

check_vs_sbp_lt_dbp(vs_check)
[1] TRUE

TRUE signifie que le contrôle a réussi — aucun enregistrement systolique-sous-diastolique. À côté d’un contrôle de package, un rapide contrôle structurel confirme le contrat SDTM : les identifiants requis existent, il y a exactement un DOMAIN, et VSSEQ est unique au sein de chaque sujet.

required <- c("STUDYID", "DOMAIN", "USUBJID", "VSSEQ", "VSTESTCD", "VSORRES")
all(required %in% names(vs))          # required identifiers + topic present?
[1] TRUE
length(unique(vs$DOMAIN)) == 1        # one domain code?
[1] TRUE
!any(duplicated(vs[c("USUBJID", "VSSEQ")]))  # VSSEQ unique within subject?
[1] TRUE

Trois TRUE : le domaine est structurellement sain et prêt à être transmis à la couche ADaM. Une vraie étude exécute la suite complète sdtmchecks et résout chaque signalement avant que les données ne soient considérées comme conformes — mais le principe est le même : mapper, puis prouver que c’est conforme.

🟢 Avec un agent IA

Demandez à Prova « comment mapper les signes vitaux de température et de saturation en oxygène dans ce domaine VS, y compris les qualificateurs VSLOC et VSLAT ? » — elle répond en s’appuyant sur les leçons de ce pilier, avec du code sdtm.oak exécutable que vous pouvez essayer sur les données d’exemple. The runtime is the judge. Demandez à Prova →

Problèmes fréquents

Une colonne de qualificateur revient entièrement à NA après un mapping. Le mapping s’est exécuté sur la mauvaise trame, ou la clé de jointure a été perdue. Chaque appel de qualificateur après le topic doit passer id_vars = oak_id_vars() pour que sdtm.oak sache comment joindre la nouvelle valeur sur les enregistrements existants. Omettez-le et les valeurs n’ont rien auquel se rattacher. Gardez raw_dat = vs_raw pointant vers la trame brute originale, et non vers celle partiellement mappée.

assign_ct() avertit que certaines valeurs « could not be mapped ». La valeur collectée n’est pas dans le codelist que vous avez indiqué. sdtm.oak laisse passer la valeur brute inchangée et vous le signale — c’est un vrai constat, pas un bug : soit le ct_clst est incorrect, soit la CT de l’étude manque un terme. Vérifiez le codelist avec filter(study_ct, codelist_code == "<code>") et confirmez que la colonne collected_value contient bien la valeur brute que vous mappez.

VSSEQ n’est pas unique, ou derive_seq() génère une erreur. Les rec_vars que vous avez donnés n’ordonnent pas de façon unique les enregistrements au sein d’un sujet, donc deux lignes sont à égalité. Ajoutez assez de variables à rec_vars (généralement le code de test plus la date, et la visite si un sujet est mesuré plus d’une fois par jour) pour que chaque enregistrement au sein d’un sujet se trie à une position distincte.

Questions fréquentes

Le mapping SDTM est le processus de transformation des données EDC/eCRF brutes en domaines CDISC SDTM — les jeux de données de tabulation standardisés, à une observation par ligne, requis dans une soumission réglementaire. Cela consiste à remodeler les lignes brutes larges en enregistrements par observation et à remplir chaque variable SDTM (le topic, ses qualificateurs, la temporalité et les identifiants) avec des valeurs valides au regard de la Controlled Terminology, en suivant le SDTM Implementation Guide.

sdtm.oak est un package R open source du pharmaverse pour créer des domaines SDTM à partir de données brutes. Il fournit un petit ensemble d’algorithmes de mapping réutilisables et indépendants du système EDC — assign_no_ct(), assign_ct(), hardcode_ct(), hardcode_no_ct(), assign_datetime() et condition_add() — que vous composez pour mapper un champ brut à la fois vers les variables SDTM cibles.

Utilisez hardcode_ct() lorsque la valeur cible est fixe et relève de la Controlled Terminology (chaque unité de pression artérielle est "mmHg"). Utilisez assign_ct() lorsque vous copiez une valeur collectée mais la recodez via un codelist (une position en texte libre vers son terme standard). Utilisez assign_no_ct() lorsque vous copiez une valeur collectée telle quelle sans terminologie (le résultat numérique VSORRES). La variante hardcode_no_ct() code en dur une valeur fixe qui n’a pas de codelist.

La Controlled Terminology (CT) est l’ensemble des codelists publiés par CDISC et gérés en version qui restreignent les valeurs qu’une variable SDTM peut prendre — par exemple VSTESTCD doit valoir SYSBP, pas « Systolic ». CDISC la maintient sous le nom de SDTM Controlled Terminology. Dans sdtm.oak, vous passez la CT de l’étude comme ct_spec et le code de codelist pertinent comme ct_clst, et les algorithmes sensibles à la CT recodent les valeurs collectées vers les termes standard.

Le SDTM tabule les données collectées — il standardise ce qui a été observé, avec un minimum de dérivation, de sorte qu’un évaluateur puisse retracer chaque valeur jusqu’au CRF. ADaM (Analysis Data Model) est prêt pour l’analyse : il ajoute des variables dérivées, des populations et une structure une-proc-un-output conçue pour l’analyse statistique. Le flux est brut → SDTM → ADaM → tables et figures ; cette leçon couvre l’étape brut → SDTM, et la construction d’ADSL avec admiral couvre la suivante.

Testez vos connaissances

L’extrait brut porte aussi un champ TEMP (température corporelle en Celsius). En suivant le même schéma que les mappings de pression artérielle, construisez une trame vs_temp avec VSTESTCD = "TEMP", VSTEST = "Temperature", le résultat dans VSORRES, et VSORRESU = "C". Comptez ensuite ses lignes.

La température n’a pas de qualificateur de position, c’est donc le schéma à trois qualificateurs : hardcode_ct() pour VSTESTCD, puis filter(!is.na(VSTESTCD)), puis hardcode_ct() pour VSTEST, assign_no_ct() pour VSORRES, et hardcode_ct() pour VSORRESU. Réutilisez les codelists C66741 (VSTESTCD), C67153 (VSTEST) et C66770 (VSORRESU), et passez id_vars = oak_id_vars() à chaque appel de qualificateur.

vs_temp <- hardcode_ct(raw_dat = vs_raw, raw_var = "TEMP", tgt_var = "VSTESTCD",
    tgt_val = "TEMP", ct_spec = study_ct, ct_clst = "C66741") |>
  filter(!is.na(VSTESTCD)) |>
  hardcode_ct(raw_dat = vs_raw, raw_var = "TEMP", tgt_var = "VSTEST",
    tgt_val = "Temperature",
    ct_spec = study_ct, ct_clst = "C67153", id_vars = oak_id_vars()) |>
  assign_no_ct(raw_dat = vs_raw, raw_var = "TEMP", tgt_var = "VSORRES",
    id_vars = oak_id_vars()) |>
  hardcode_ct(raw_dat = vs_raw, raw_var = "TEMP", tgt_var = "VSORRESU",
    tgt_val = "C", ct_spec = study_ct, ct_clst = "C66770", id_vars = oak_id_vars())

nrow(vs_temp)

vs_temp compte six enregistrements — un par ligne d’évaluation brute. C’est la chaîne d’algorithmes identique à celle des mappings de pression artérielle, avec le champ brut, le code de test, le libellé et l’unité échangés, ce qui explique précisément pourquoi les algorithmes de sdtm.oak sont réutilisables : chaque topic Findings se mappe de la même façon.

A. Parce que le SDTM stocke une ligne par sujet B. Parce que le SDTM stocke une ligne par observation, indexée par la variable topic C. Parce que sdtm.oak duplique les lignes pour la validation

B. Le SDTM est un modèle de tabulation avec une ligne par observation. La variable topic (VSTESTCD) nomme chaque observation, donc une seule évaluation brute contenant systolique, diastolique et pouls devient trois enregistrements SDTM — un par code de test — chacun entièrement qualifié par son propre résultat, unité, position et temporalité. A décrit le domaine DM au niveau sujet, pas les Findings, et C n’est pas ce qui se passe — les lignes sont des observations distinctes, pas des doublons.

Conclusion

Le mapping SDTM ressemble à une longue chaîne d’appels de fonctions, mais c’est une seule idée répétée : remplir la bonne colonne SDTM à partir du bon champ brut avec le bon algorithme. La variable topic (VSTESTCD) nomme chaque observation ; hardcode_ct(), assign_ct() et assign_no_ct() remplissent ses qualificateurs — codés en dur, recodés via la CT, ou copiés tels quels ; assign_datetime() construit la temporalité ISO 8601 ; et derive_seq() numérote les enregistrements. oak_id_vars recoud les mappings champ par champ en un enregistrement correct unique, et sdtmchecks prouve que le résultat est conforme. Maîtrisez cette boucle sur un domaine Findings et tous les autres domaines — analyses biologiques, ECG, interventions, événements — sont la même boucle avec des champs différents. Une fois un SDTM conforme en main, l’étape suivante est la couche d’analyse : la construction du jeu de données ADSL au niveau sujet avec admiral.

Note

Cette leçon est reproductible : chaque résultat de cette page a été produit par le code montré — copiez n’importe quel bloc et exécutez-le pour les reproduire. The runtime is the judge.

Leçons connexes

Cette page vous a-t-elle été utile ?

Recevez les nouvelles leçons R & Python par e-mail

Pratique, reproductible, sans spam. Désinscription à tout moment.

Double opt-in. Nous ne partageons jamais votre e-mail.

Partager cette pageXLinkedInRedditHN

Réutilisation

Citation

BibTeX
@online{2026,
  author = {},
  title = {Mapping SDTM en R avec sdtm.oak : des données EDC brutes à un
    domaine conforme},
  date = {2026-06-30},
  url = {https://www.datanovia.com/learn/pharma-clinical/02-sdtm-programming/sdtm-mapping-sdtm-oak},
  langid = {fr}
}
Veuillez citer ce travail comme suit :
“Mapping SDTM en R avec sdtm.oak : des données EDC brutes à un domaine conforme.” 2026. June 30. https://www.datanovia.com/learn/pharma-clinical/02-sdtm-programming/sdtm-mapping-sdtm-oak.