
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.
- 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,VSDTCetVSSEQle 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, etassign_datetime()construit une date ISO 8601. oak_id_varsest 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 :
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 :VSTESTest toujours"Systolic Blood Pressure",VSORRESUest toujours"mmHg".assign_no_ct()— copier une valeur collectée sans terminologie :VSORRESest le nombre brutSYS_BP, repris tel qu’enregistré.assign_ct()— recoder une valeur collectée via la CT :VSPOSprovient duSUBPOSbrut, 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.
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.
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
- Le flux de données des essais cliniques : du CRF au SDTM, ADaM, TLF — où le SDTM se place dans le pipeline que cette leçon alimente. · Les standards CDISC : CDASH, SDTM, ADaM, Define-XML — comment le SDTM se rapporte aux standards de collecte et d’analyse de données qui l’entourent. · Créer ADSL en R avec admiral — l’étape suivante, la construction du jeu de données d’analyse au niveau sujet à partir du SDTM. · Le passage de SAS à R pour la programmation clinique — pourquoi sdtm.oak joue le rôle d’une bibliothèque de macros de mapping validée.
- Où cela s’inscrit : les fondations réglementaires et CDISC → le mapping des données brutes vers le SDTM (vous êtes ici) → la construction d’ADaM avec admiral → la production des tables, listings et figures que livre chaque soumission. Le SDTM est la source standardisée que lit chaque étape ultérieure.
Réutilisation
Citation
@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}
}