
Domaine SDTM DM en R avec sdtm.oak : démographie
Mappez un extrait brut de démographie et d’inclusion vers un domaine SDTM DM conforme à CDISC en R avec sdtm.oak — la classe Special-Purpose, une ligne par sujet, USUBJID, la date de début de référence RFSTDTC, le bras de traitement prévu vs réel, et les AGE/SEX/RACE/ETHNIC codés par CT
Un tutoriel complet et exécutable pour le domaine SDTM DM (demographics, démographie) en R. Découvrez ce qu’est la classe Special-Purpose de SDTM CDISC (une ligne par sujet, sans variable topic, sans numéro de séquence), en quoi elle diffère des classes Findings, Events et Interventions, puis mappez un extrait démographique brut vers un domaine DM conforme avec sdtm.oak : generate_oak_id_vars() pour la traçabilité des enregistrements, assign_ct() pour les SEX/RACE/ETHNIC à Controlled Terminology, assign_no_ct() pour les bras de traitement prévu et réel (ARM/ARMCD/ACTARM/ACTARMCD), assign_datetime() pour les dates de référence ISO 8601 (RFSTDTC/RFENDTC/BRTHDTC), un AGE dérivé, et derive_study_day() pour DMDY — la colonne vertébrale des sujets sur laquelle chaque autre domaine se joint par USUBJID et se cadence sur RFSTDTC.
- DM est un domaine Special-Purpose — une quatrième forme d’enregistrement, et la plus fondamentale. Les classes Findings, Events et Interventions contiennent chacune plusieurs lignes par sujet ; DM contient exactement une ligne par sujet, sans variable topic et sans numéro de séquence.
- DM est la colonne vertébrale des sujets. Chaque autre domaine s’y joint par l’identifiant unique de sujet
USUBJID, et chaque jour d’étude (VSDY,AEDY,EXDY…) est compté à partir de la date de début de référence de DMRFSTDTC. Construisez DM de travers et chaque domaine en aval est faux. - Les descripteurs démographiques sont contrôlés.
assign_ct()recode le sexe, la race et l’ethnicité collectés vers leurs termes standard CDISC (SEX,RACE,ETHNIC). - Le traitement prévu et le traitement réel sont deux colonnes distinctes.
ARM/ARMCDenregistrent le bras auquel le sujet a été randomisé ;ACTARM/ACTARMCDenregistrent le bras qu’il a réellement reçu — ils diffèrent lorsqu’un sujet est mal dosé, et les deux sont copiés avecassign_no_ct(). - Les dates de référence ancrent toute l’horloge de l’étude.
assign_datetime()construit lesRFSTDTC/RFENDTC/BRTHDTCISO 8601 ;AGEest dérivé de la date de naissance ; etderive_study_day()place la date de collecte propre à DM sur l’horloge — le jour d’étude 1 estRFSTDTClui-même, donc une date de sélection tombe sur un jour négatif, jamais le jour 0.
Introduction
Chaque chiffre d’une soumission — chaque événement indésirable, chaque valeur de laboratoire, chaque dose — dépend d’une ligne par sujet : qui il est, sur quel bras il se trouvait, et la date à laquelle l’horloge a démarré. Cette ligne, c’est le domaine SDTM (Study Data Tabulation Model) DM (demographics, démographie). Ce que le site consigne est plus lâche : un CRF (case report form, cahier d’observation) d’inclusion avec une date de naissance, un sexe coché « Female », une race et une ethnicité choisies dans une liste, un bras de randomisation, et quelques dates clés saisies comme le coordinateur les a saisies. Le travail ici consiste à transformer cet extrait brut en un domaine DM conforme — le seul jeu de données sur lequel tous les autres domaines de l’étude sont construits.
Cette leçon réalise ce mapping de bout en bout en R avec sdtm.oak, le package pharmaverse de transformation des données brutes vers SDTM. DM complète le tableau des classes SDTM : le domaine Findings (signes vitaux) enregistre ce qui a été mesuré, le domaine Events (événements indésirables) enregistre ce qui est arrivé, le domaine Interventions (exposition au médicament) enregistre ce qui a été administré — et DM est la quatrième classe, la classe Special-Purpose : non pas de nombreuses lignes d’observations, mais une unique ligne au niveau sujet. C’est la colonne vertébrale des sujets que les trois autres référencent : chacun se joint à DM par USUBJID et calcule ses jours d’étude par rapport au RFSTDTC de DM. SDTM est l’amont du flux de données cliniques — raw → SDTM → ADaM → tableaux, listings et figures (TLF) — donc un DM propre ici est ce qui permet à la couche d’analyse de construire le jeu de données ADSL au niveau sujet, qui part précisément de ces colonnes.
Voici où nous allons — le domaine DM conforme que nous allons construire, dessiné comme les caractéristiques initiales qu’il encode, directement à partir du résultat mappé :
Chaque point ici est un enregistrement SDTM — un sujet, avec un âge, un sexe et un bras de traitement. Parce que DM a exactement une ligne par sujet, le point est l’enregistrement. À la fin de cette leçon, vous les aurez tous produits, avec leurs dates de référence, à partir d’un extrait brut. Nouveau dans le pipeline global ? Commencez par le flux de données de l’essai clinique, qui situe où se place SDTM, et les standards CDISC, qui placent SDTM à côté de CDASH et ADaM.
Ce qu’est la classe Special-Purpose du SDTM
SDTM — le Study Data Tabulation Model — est le standard CDISC (Clinical Data Interchange Standards Consortium) qui organise les données d’essai collectées en domaines fixes. Il est publié sous la forme du standard fondamental SDTM et du SDTM Implementation Guide (SDTMIG). La plupart des observations relèvent de l’une des trois classes générales d’observation — mais DM appartient à un quatrième groupe, les domaines Special-Purpose, et la différence est structurelle :
| Classe d’observation | Ce qu’elle enregistre | Exemples de domaines | Variable topic | Lignes |
|---|---|---|---|---|
| Interventions | Ce qui est administré au sujet | EX (exposition), CM (médicaments concomitants) | --TRT |
une par épisode d’administration |
| Events | Ce qui est arrivé | AE (événements indésirables), MH, DS | --TERM |
une par événement |
| Findings | Mesures | VS (signes vitaux), LB, EG | --TESTCD |
une par mesure |
| Special-Purpose | Niveau sujet / référence de l’étude | DM (démographie), CO, SE, SV | (aucune) | une par sujet |
La leçon sur les signes vitaux, la leçon sur les événements indésirables et la leçon sur l’exposition au médicament mappaient chacune une classe générale d’observation, où une variable topic (un code de test, un terme rapporté, un traitement) nomme chaque enregistrement et où un sujet a plusieurs lignes. DM est différent. Il n’a aucune variable topic — un sujet n’est ni une mesure, ni un événement, ni une intervention — et il y a exactement une ligne par sujet. Cette unique ligne porte l’identité du sujet, son traitement prévu et réel, et les dates de référence par rapport auxquelles le reste de l’étude est cadencé. Ce n’est pas une observation parmi d’autres ; c’est l’ancre à laquelle les nombreuses observations se rattachent.
Un enregistrement DM est un ensemble de champs au niveau sujet regroupés par rôle :
| Rôle de la variable | Exemple DM | Ce qu’elle porte |
|---|---|---|
| Identifiants | STUDYID, USUBJID, DOMAIN, SUBJID, SITEID |
L’étude, l’identifiant unique de sujet, le code de domaine (DM), l’identifiant de sujet du promoteur, et le site. |
| Descripteurs du sujet | AGE, AGEU, SEX, RACE, ETHNIC |
Qui est le sujet — l’âge et son unité, le sexe, la race, l’ethnicité. |
| Traitement | ARMCD, ARM, ACTARMCD, ACTARM |
Le bras prévu (issu de la randomisation) et le bras réel (ce que le sujet a reçu). |
| Temporalité de référence | RFSTDTC, RFENDTC, BRTHDTC, DMDTC |
Début et fin de référence, date de naissance, et date de collecte de la démographie — tous en ISO 8601. |
Deux d’entre elles font l’essentiel du travail porteur pour toute l’étude. USUBJID — l’identifiant unique de sujet — est la clé sur laquelle chaque autre domaine se joint : un AE, une mesure VS et un épisode EX portent tous le même USUBJID, et c’est ainsi qu’un évaluateur relie l’événement d’un sujet à son bras de traitement. Et RFSTDTC — la date de début de référence du sujet (typiquement la date de première dose ou de randomisation) — est l’origine de l’horloge de l’étude : le jour d’étude --DY de chaque domaine (VSDY, AEDY, EXDY) est compté à partir de RFSTDTC. Réussissez DM et toute l’étude s’aligne ; trompez-vous sur USUBJID ou RFSTDTC et chaque jointure et chaque jour d’étude en aval sont faux. Mapper les données brutes vers SDTM ici consiste à remplir ces colonnes au niveau sujet — certaines sous Controlled Terminology (CT), des codelists publiées par CDISC qui fixent les valeurs autorisées (SDTM Controlled Terminology), certaines copiées telles quelles, certaines dérivées.
L’extrait démographique brut et sa Controlled Terminology
Deux entrées pilotent un mapping : l’extrait brut et la Controlled Terminology de l’étude. sdtm.oak livre des exemples bruts pour les médicaments concomitants et les signes vitaux, mais pas pour la démographie, alors nous construisons un petit extrait d’inclusion brut dans la forme sous laquelle un système EDC (electronic data capture) en exporte un — une large ligne par sujet, des noms de colonnes choisis par le promoteur, des dates dans le format utilisé par le formulaire. Il suit le pilote public CDISC : un médicament à l’étude transdermique (Xanomeline) titré entre une faible et une forte dose face à un bras placebo.
library(sdtm.oak)
library(dplyr, warn.conflicts = FALSE)
# A raw demographics/enrollment extract: one row per subject, as collected
dm_raw <- data.frame(
SUBJ = c("1015","1023","1028","1033","1039","1044","1050","1074","1096","1103","1115","1120"),
SITE = c("701","701","701","701","702","702","702","703","703","704","704","705"),
BRTHDAT = c("26 Dec 1950","22 Jul 1948","11 Jul 1942","10 Mar 1940","05 Sep 1955","19 Nov 1938",
"02 Feb 1947","30 Apr 1952","14 Aug 1945","21 Jan 1958","08 Jun 1943","17 Oct 1949"),
SEXR = c("Female","Male","Male","Male","Female","Female","Male","Female","Male","Female","Female","Male"),
RACER = c("White","White","White","White","Black or African American","White","Asian","White",
"White","Black or African American","White","American Indian or Alaska Native"),
ETHR = c("Hispanic or Latino","Hispanic or Latino","Not Hispanic or Latino","Not Hispanic or Latino",
"Not Hispanic or Latino","Not Hispanic or Latino","Not Hispanic or Latino","Hispanic or Latino",
"Not Hispanic or Latino","Not Hispanic or Latino","Not Hispanic or Latino","Not Hispanic or Latino"),
CNTRY = "USA",
PARM = c("Placebo","Placebo","Xanomeline High Dose","Xanomeline Low Dose","Placebo","Xanomeline High Dose",
"Xanomeline Low Dose","Placebo","Xanomeline High Dose","Xanomeline Low Dose","Placebo","Xanomeline High Dose"),
PARMCD = c("Pbo","Pbo","Xan_Hi","Xan_Lo","Pbo","Xan_Hi","Xan_Lo","Pbo","Xan_Hi","Xan_Lo","Pbo","Xan_Hi"),
AARM = c("Placebo","Placebo","Xanomeline High Dose","Xanomeline Low Dose","Placebo","Xanomeline Low Dose",
"Xanomeline Low Dose","Placebo","Xanomeline High Dose","Xanomeline Low Dose","Placebo","Xanomeline High Dose"),
AARMCD = c("Pbo","Pbo","Xan_Hi","Xan_Lo","Pbo","Xan_Lo","Xan_Lo","Pbo","Xan_Hi","Xan_Lo","Pbo","Xan_Hi"),
RFSTDAT = c("02 Jan 2014","05 Aug 2012","19 Jul 2013","18 Mar 2014","11 Mar 2013","09 Apr 2013",
"22 Sep 2013","03 Feb 2014","20 Feb 2014","28 Mar 2013","15 Jan 2014","06 Jun 2013"),
RFENDAT = c("02 Jul 2014","02 Sep 2012","14 Jan 2014","14 Apr 2014","30 May 2013","30 Sep 2013",
"18 Oct 2013","19 Jun 2014","30 Jun 2014","25 Jul 2013","15 Jul 2014","25 Nov 2013"),
DMDAT = c("26 Dec 2013","22 Jul 2012","11 Jul 2013","10 Mar 2014","04 Mar 2013","01 Apr 2013",
"13 Sep 2013","27 Jan 2014","06 Feb 2014","20 Mar 2013","08 Jan 2014","30 May 2013"),
stringsAsFactors = FALSE
)
dm_raw |>
select(SUBJ, SITE, SEXR, RACER, PARM, AARM, RFSTDAT) |>
head() SUBJ SITE SEXR RACER PARM
1 1015 701 Female White Placebo
2 1023 701 Male White Placebo
3 1028 701 Male White Xanomeline High Dose
4 1033 701 Male White Xanomeline Low Dose
5 1039 702 Female Black or African American Placebo
6 1044 702 Female White Xanomeline High Dose
AARM RFSTDAT
1 Placebo 02 Jan 2014
2 Placebo 05 Aug 2012
3 Xanomeline High Dose 19 Jul 2013
4 Xanomeline Low Dose 18 Mar 2014
5 Placebo 11 Mar 2013
6 Xanomeline Low Dose 09 Apr 2013
Chaque ligne brute est un sujet : une date de naissance, le sexe/la race/l’ethnicité collectés, le pays et le site, le bras prévu (PARM/PARMCD) et le bras réel reçu (AARM/AARMCD), et les dates clés de l’étude — le début de référence (RFSTDAT), la fin de référence (RFENDAT), et la date de collecte de la démographie (DMDAT). Remarquez le sujet 1044 : prévu Xanomeline High Dose mais réel Xanomeline Low Dose — une vraie discordance sur laquelle nous reviendrons. La Controlled Terminology fait correspondre chaque valeur collectée à son terme CDISC standard au sein d’une codelist. La CT d’une vraie étude est livrée dans le cadre de la spécification SDTM ; ici nous définissons les codelists démographiques dont nous avons besoin :
# Study controlled terminology: collected value -> CDISC standard term, by codelist
dm_ct <- data.frame(
codelist_code = c("C66731","C66731", # SEX
"C74457","C74457","C74457","C74457", # RACE
"C66790","C66790"), # ETHNIC
term_value = c("F","M",
"WHITE","BLACK OR AFRICAN AMERICAN","ASIAN","AMERICAN INDIAN OR ALASKA NATIVE",
"HISPANIC OR LATINO","NOT HISPANIC OR LATINO"),
collected_value = c("Female","Male",
"White","Black or African American","Asian","American Indian or Alaska Native",
"Hispanic or Latino","Not Hispanic or Latino"),
term_preferred_term = NA_character_, term_synonyms = NA_character_,
stringsAsFactors = FALSE
)
dm_ct |> select(codelist_code, collected_value, term_value) codelist_code collected_value
1 C66731 Female
2 C66731 Male
3 C74457 White
4 C74457 Black or African American
5 C74457 Asian
6 C74457 American Indian or Alaska Native
7 C66790 Hispanic or Latino
8 C66790 Not Hispanic or Latino
term_value
1 F
2 M
3 WHITE
4 BLACK OR AFRICAN AMERICAN
5 ASIAN
6 AMERICAN INDIAN OR ALASKA NATIVE
7 HISPANIC OR LATINO
8 NOT HISPANIC OR LATINO
collected_value est ce que le CRF a enregistré ; term_value est le terme standard CDISC que sdtm.oak substituera. Les trois codelists sont des standards CDISC, publiés dans SDTM Controlled Terminology : C66731 est la codelist Sex (SEX), C74457 la codelist Race (RACE), et C66790 la codelist Ethnic Group (ETHNIC). L’argument ct_clst de chaque fonction gérant la CT pointe vers l’une de ces valeurs codelist_code.
Estampiller la traçabilité des enregistrements avec oak_id_vars
Parce que nous mappons un champ brut à la fois et assemblons les morceaux, sdtm.oak a besoin d’une clé stable sur chaque ligne brute. generate_oak_id_vars() en ajoute trois : oak_id (le numéro de ligne), raw_source (l’extrait dont elle provient), et patient_number.
dm_raw <- dm_raw |>
generate_oak_id_vars(pat_var = "SUBJ", raw_src = "dm_raw")
dm_raw |>
select(oak_id, raw_source, patient_number, SEXR, PARM) |>
head() oak_id raw_source patient_number SEXR PARM
1 1 dm_raw 1015 Female Placebo
2 2 dm_raw 1023 Male Placebo
3 3 dm_raw 1028 Male Xanomeline High Dose
4 4 dm_raw 1033 Male Xanomeline Low Dose
5 5 dm_raw 1039 Female Placebo
6 6 dm_raw 1044 Female Xanomeline High Dose
Ces trois colonnes sont les oak_id_vars. Chaque mapping ci-dessous passe id_vars = oak_id_vars(), qui indique à sdtm.oak de rejoindre la nouvelle colonne sur le bon enregistrement grâce à cette clé — le mécanisme qui permet aux mappings partiels de se combiner sans dupliquer de lignes. Pour DM, cette garantie compte doublement : parce qu’il y a une ligne par sujet, une jointure qui dupliquerait une ligne casserait silencieusement la règle qui définit le domaine.
Mapper les descripteurs démographiques sous Controlled Terminology
Les descripteurs du sujet SEX, RACE et ETHNIC sont chacun une valeur collectée recodée via la Controlled Terminology, donc chacun utilise assign_ct() (« assign », parce qu’elle copie une valeur collectée ; « ct », parce que la valeur est recodée via une codelist) pointée vers la codelist qui la régit :
SEX— la codelist SexC66731:Female/Male→F/M.RACE— la codelist RaceC74457:White→WHITE,Black or African Americanreste sous son terme standard en majuscules, et ainsi de suite.ETHNIC— la codelist Ethnic GroupC66790:Hispanic or Latino→HISPANIC OR LATINO.
dm <- assign_ct(raw_dat = dm_raw, raw_var = "SEXR", tgt_var = "SEX",
ct_spec = dm_ct, ct_clst = "C66731") |>
assign_ct(raw_dat = dm_raw, raw_var = "RACER", tgt_var = "RACE",
ct_spec = dm_ct, ct_clst = "C74457", id_vars = oak_id_vars()) |>
assign_ct(raw_dat = dm_raw, raw_var = "ETHR", tgt_var = "ETHNIC",
ct_spec = dm_ct, ct_clst = "C66790", id_vars = oak_id_vars())
dm |>
select(oak_id, patient_number, SEX, RACE, ETHNIC) |>
head()# A tibble: 6 × 5
oak_id patient_number SEX RACE ETHNIC
<int> <chr> <chr> <chr> <chr>
1 1 1015 F WHITE HISPANIC OR LATINO
2 2 1023 M WHITE HISPANIC OR LATINO
3 3 1028 M WHITE NOT HISPANIC OR LATINO
4 4 1033 M WHITE NOT HISPANIC OR LATINO
5 5 1039 F BLACK OR AFRICAN AMERICAN NOT HISPANIC OR LATINO
6 6 1044 F WHITE NOT HISPANIC OR LATINO
Chaque valeur collectée est désormais un terme CDISC standard : SEX vaut F/M, RACE vaut WHITE, ETHNIC vaut HISPANIC OR LATINO. C’est tout l’intérêt de la Controlled Terminology — un évaluateur, ou un outil de validation de la FDA, lit les mêmes valeurs codées d’une étude à l’autre, quelle que soit la manière dont chaque site les a formulées sur le CRF. Une colonne SDTM par descripteur collecté, un enregistrement par sujet.
Mapper les bras de traitement prévu et réel
DM porte le traitement deux fois, et cette distinction est l’une des raisons mêmes de l’existence de DM. ARM/ARMCD sont le bras prévu — le traitement auquel le sujet a été randomisé. ACTARM/ACTARMCD sont le bras réel — le traitement que le sujet a réellement reçu. Ils sont identiques pour la plupart des sujets, mais divergent lorsqu’un sujet est inclus ou dosé différemment de ce qui était prévu — une erreur de randomisation ou de dosage — et l’analyse de sécurité doit savoir lequel est lequel. À la fois le libellé long (ARM) et son code court (ARMCD) proviennent de la définition Trial Arms de l’étude, donc ce sont des valeurs définies par l’étude copiées telles quelles, non recodées via la terminologie CDISC — ce qui signifie assign_no_ct() (« no_ct », parce qu’il n’y a pas de codelist par laquelle recoder) :
dm <- dm |>
assign_no_ct(raw_dat = dm_raw, raw_var = "PARM", tgt_var = "ARM", id_vars = oak_id_vars()) |>
assign_no_ct(raw_dat = dm_raw, raw_var = "PARMCD", tgt_var = "ARMCD", id_vars = oak_id_vars()) |>
assign_no_ct(raw_dat = dm_raw, raw_var = "AARM", tgt_var = "ACTARM", id_vars = oak_id_vars()) |>
assign_no_ct(raw_dat = dm_raw, raw_var = "AARMCD", tgt_var = "ACTARMCD", id_vars = oak_id_vars()) |>
assign_no_ct(raw_dat = dm_raw, raw_var = "CNTRY", tgt_var = "COUNTRY", id_vars = oak_id_vars())
dm |>
select(patient_number, ARMCD, ARM, ACTARMCD, ACTARM) |>
filter(ARM != ACTARM)# A tibble: 1 × 5
patient_number ARMCD ARM ACTARMCD ACTARM
<chr> <chr> <chr> <chr> <chr>
1 1044 Xan_Hi Xanomeline High Dose Xan_Lo Xanomeline Low Dose
Une ligne revient — le sujet 1044, prévu Xanomeline High Dose mais réellement sous Xanomeline Low Dose. C’est exactement le cas pour lequel la conception à deux colonnes existe : une analyse d’efficacité est habituellement menée par bras prévu (intention de traiter), une analyse de sécurité par bras réel (ce à quoi le sujet a été exposé), et DM est le seul endroit où les deux sont enregistrés pour que l’une ou l’autre analyse puisse choisir le bras dont elle a besoin.
Mapper les dates de référence et dériver l’âge
Les champs temporels de DM sont les points de référence de l’étude, chacun une date ISO 8601. ISO 8601 est le standard international de date (2014-01-02) qui rend les dates triables et comparables d’une étude à l’autre. assign_datetime() analyse les dates brutes et écrit les colonnes conformes : la date de naissance BRTHDTC, le début de référence RFSTDTC, la fin de référence RFENDTC, et la date de collecte de la démographie DMDTC.
dm <- dm |>
assign_datetime(raw_dat = dm_raw, raw_var = "BRTHDAT", tgt_var = "BRTHDTC",
raw_fmt = list(c("d-m-y", "dd mmm yyyy")), id_vars = oak_id_vars()) |>
assign_datetime(raw_dat = dm_raw, raw_var = "RFSTDAT", tgt_var = "RFSTDTC",
raw_fmt = list(c("d-m-y", "dd mmm yyyy")), id_vars = oak_id_vars()) |>
assign_datetime(raw_dat = dm_raw, raw_var = "RFENDAT", tgt_var = "RFENDTC",
raw_fmt = list(c("d-m-y", "dd mmm yyyy")), id_vars = oak_id_vars()) |>
assign_datetime(raw_dat = dm_raw, raw_var = "DMDAT", tgt_var = "DMDTC",
raw_fmt = list(c("d-m-y", "dd mmm yyyy")), id_vars = oak_id_vars())
dm |>
select(patient_number, BRTHDTC, RFSTDTC, RFENDTC) |>
head()# A tibble: 6 × 4
patient_number BRTHDTC RFSTDTC RFENDTC
<chr> <iso8601> <iso8601> <iso8601>
1 1015 1950-12-26 2014-01-02 2014-07-02
2 1023 1948-07-22 2012-08-05 2012-09-02
3 1028 1942-07-11 2013-07-19 2014-01-14
4 1033 1940-03-10 2014-03-18 2014-04-14
5 1039 1955-09-05 2013-03-11 2013-05-30
6 1044 1938-11-19 2013-04-09 2013-09-30
raw_fmt indique à sdtm.oak comment la date brute a été écrite (02 Jan 2014) ; il renvoie la forme standard 2014-01-02. AGE n’est pas collecté directement ici — il est dérivé de la date de naissance. Le SDTMIG définit AGE comme l’âge du sujet à un point de référence de l’étude ; nous le calculons comme le nombre d’années révolues entre BRTHDTC et le début de référence RFSTDTC, et fixons l’unité AGEU à YEARS :
dm <- dm |>
mutate(
AGE = as.integer(floor(
as.numeric(difftime(as.Date(as.character(RFSTDTC)),
as.Date(as.character(BRTHDTC)), units = "days")) / 365.25)),
AGEU = "YEARS"
)
dm |>
select(patient_number, BRTHDTC, RFSTDTC, AGE, AGEU) |>
head()# A tibble: 6 × 5
patient_number BRTHDTC RFSTDTC AGE AGEU
<chr> <iso8601> <iso8601> <int> <chr>
1 1015 1950-12-26 2014-01-02 63 YEARS
2 1023 1948-07-22 2012-08-05 64 YEARS
3 1028 1942-07-11 2013-07-19 71 YEARS
4 1033 1940-03-10 2014-03-18 74 YEARS
5 1039 1955-09-05 2013-03-11 57 YEARS
6 1044 1938-11-19 2013-04-09 74 YEARS
AGE est un simple calcul, pas un algorithme sdtm.oak — le package mappe et dérive les variables SDTM structurelles, et une arithmétique d’âge comme celle-ci est du R ordinaire. (Une étude fixe quelle date de référence régit la dérivation — souvent RFSTDTC ou la date de consentement éclairé — selon ses conventions SDTMIG ; nous utilisons RFSTDTC ici.) C’est la première fois que RFSTDTC justifie son rôle d’ancre — et pas la dernière.
Ajouter les identifiants et le jour d’étude
Un enregistrement conforme a besoin de ses identifiants et de sa place sur l’horloge de l’étude. STUDYID et DOMAIN sont fixes ; SITEID et SUBJID proviennent des champs bruts ; et USUBJID — la clé sur laquelle chaque autre domaine se joint — est construit pour être unique sur toute l’étude, conventionnellement STUDYID-SITEID-SUBJID. Enfin, derive_study_day() calcule DMDY, le jour d’étude de la date de collecte de la démographie DMDTC par rapport à RFSTDTC — la même fonction que la leçon sur l’exposition utilisait pour EXSTDY/EXENDY, sauf qu’ici DM fournit RFSTDTC lui-même.
dm <- dm |>
left_join(dm_raw |> select(oak_id, SITE, SUBJ), by = "oak_id") |>
mutate(
STUDYID = "CDISCPILOT01",
DOMAIN = "DM",
SITEID = SITE,
SUBJID = SUBJ,
USUBJID = paste(STUDYID, SITEID, SUBJID, sep = "-")
)
# derive_study_day reads RFSTDTC from DM itself, so USUBJID must already be built above
dm <- dm |>
derive_study_day(dm_domain = dm, tgdt = "DMDTC", refdt = "RFSTDTC", study_day_var = "DMDY") |>
select(STUDYID, DOMAIN, USUBJID, SUBJID, SITEID, BRTHDTC, AGE, AGEU, SEX, RACE, ETHNIC,
ARMCD, ARM, ACTARMCD, ACTARM, COUNTRY, RFSTDTC, RFENDTC, DMDTC, DMDY)
head(dm)# A tibble: 6 × 20
STUDYID DOMAIN USUBJID SUBJID SITEID BRTHDTC AGE AGEU SEX RACE ETHNIC
<chr> <chr> <chr> <chr> <chr> <iso86> <int> <chr> <chr> <chr> <chr>
1 CDISCPILO… DM CDISCP… 1015 701 1950-1… 63 YEARS F WHITE HISPA…
2 CDISCPILO… DM CDISCP… 1023 701 1948-0… 64 YEARS M WHITE HISPA…
3 CDISCPILO… DM CDISCP… 1028 701 1942-0… 71 YEARS M WHITE NOT H…
4 CDISCPILO… DM CDISCP… 1033 701 1940-0… 74 YEARS M WHITE NOT H…
5 CDISCPILO… DM CDISCP… 1039 702 1955-0… 57 YEARS F BLAC… NOT H…
6 CDISCPILO… DM CDISCP… 1044 702 1938-1… 74 YEARS F WHITE NOT H…
# ℹ 9 more variables: ARMCD <chr>, ARM <chr>, ACTARMCD <chr>, ACTARM <chr>,
# COUNTRY <chr>, RFSTDTC <date>, RFENDTC <iso8601>, DMDTC <date>, DMDY <int>
Voilà un domaine DM conforme : d’abord les identifiants, puis les descripteurs du sujet, les bras prévu et réel, et la temporalité de référence. Regardez DMDY : la démographie est collectée à la sélection, avant le début de référence, donc chaque DMDY est négatif — le jour d’étude 1 est RFSTDTC lui-même, le jour qui le précède est le jour −1, et il n’y a pas de jour 0. Cette convention sans décalage est le bug de jour d’étude le plus courant, et il vit ici, dans le domaine qui définit l’horloge. Une vérification structurelle rapide confirme le contrat SDTM qui rend DM spécial :
required <- c("STUDYID", "DOMAIN", "USUBJID", "SUBJID", "RFSTDTC")
all(required %in% names(dm)) # required subject-level variables present?[1] TRUE
length(unique(dm$DOMAIN)) == 1 # one domain code?[1] TRUE
nrow(dm) == length(unique(dm$USUBJID)) # EXACTLY one row per subject?[1] TRUE
Trois TRUE — et le troisième est celui qui compte le plus : exactement une ligne par USUBJID. Contrairement à VS, AE ou EX, DM n’a aucune variable --SEQ, précisément parce qu’un numéro de séquence n’a de sens que lorsqu’un sujet a plusieurs enregistrements ; ici chaque sujet est une unique ligne, donc USUBJID à lui seul est la clé. En production, la vérification complète relève d’un outil de conformité (Pinnacle 21, ou le package pharmaverse sdtmchecks, présenté dans la leçon sur les signes vitaux) qui vérifie aussi que chaque USUBJID de tout autre domaine existe bien ici dans DM. Les colonnes que nous avons construites sont les variables démographiques essentielles que porte le jeu de données de référence dm publié de pharmaversesdtm (il ajoute plusieurs autres variables de dates de référence et de motif d’absence de bras que vous pouvez superposer de la même manière). Avec un DM conforme en main, chaque autre domaine peut s’y joindre et la couche d’analyse peut construire le jeu de données ADSL au niveau sujet par-dessus.
Demandez à Prova « comment ajouter les variables de traitement et de période de référence — RFXSTDTC/RFXENDTC et RFICDTC — à ce domaine DM, et comment ADSL dérive-t-il TRT01P et TRT01A à partir de ARM et ACTARM ? » — elle répond en s’appuyant sur les leçons de ce pilier, avec du code exécutable que vous pouvez essayer sur les données d’exemple. The runtime is the judge. Demandez à Prova →
Problèmes fréquents
DM a plus d’une ligne pour un sujet — la règle qui définit le domaine est cassée. Une ligne en double provient généralement d’un extrait brut qui portait un sujet deux fois (une re-sélection, un doublon de saisie) ou d’une jointure qui a essaimé. Parce que DM n’a pas de --SEQ pour distinguer les lignes, deux lignes pour un même USUBJID sont toujours une erreur. Vérifiez avec dm[duplicated(dm$USUBJID), ] ; si les données brutes ont légitimement un enregistrement de re-sélection, conservez l’unique ligne d’inclusion que le protocole spécifie et supprimez l’autre — DM, c’est une ligne par sujet, un point c’est tout.
RFSTDTC est manquant, et chaque jour d’étude en aval ressort NA. RFSTDTC est l’ancre que derive_study_day() lit pour chaque domaine — un champ vide ici se propage silencieusement à VSDY, AEDY, EXDY, et à chaque date d’analyse. Il manque pour une vraie raison (un sujet en échec de sélection qui n’a jamais été dosé n’a pas de début de référence), alors confirmez que le sujet n’a réellement pas de début de référence plutôt que de combler ce vide avec une valeur ; un RFSTDTC légitimement nul est conforme, et le code en aval devrait traiter le NA délibérément, plutôt que de recevoir une date fabriquée.
Vous confondez ARM et ACTARM, ou n’en remplissez qu’un seul. ARM est prévu (randomisé), ACTARM est réel (reçu) ; une analyse d’efficacité lit ARM, une analyse de sécurité lit ACTARM. Remplir ACTARM à partir des données de randomisation — ou le laisser vide en supposant qu’il est égal à ARM — masque précisément les sujets mal dosés que la revue de sécurité doit débusquer. Mappez-les à partir de champs bruts distincts, et lorsqu’ils diffèrent (le sujet 1044 ici), faites confiance à la différence plutôt que de la « corriger ».
Questions fréquentes
Le domaine SDTM DM (démographie) est le jeu de données CDISC SDTM qui contient un enregistrement au niveau sujet par participant à l’essai — la classe Special-Purpose. Chaque ligne porte l’identifiant unique de sujet USUBJID, les descripteurs du sujet (AGE, AGEU, SEX, RACE, ETHNIC), les bras de traitement prévu et réel (ARM/ARMCD, ACTARM/ACTARMCD), et les dates de référence (RFSTDTC, RFENDTC, BRTHDTC). C’est la colonne vertébrale des sujets de toute l’étude : chaque autre domaine s’y joint par USUBJID, et c’est la source des variables au niveau sujet dans ADSL.
RFSTDTC est la date de début de référence du sujet sur le domaine DM — typiquement la date du premier traitement à l’étude ou de la randomisation. C’est l’origine de l’horloge de l’étude : la variable de jour d’étude de chaque domaine (VSDY, AEDY, EXDY, DMDY) est dérivée comme le nombre de jours entre RFSTDTC et la date d’observation, où le jour d’étude 1 est RFSTDTC lui-même et le jour qui le précède est le jour −1 — il n’y a pas de jour 0. Parce qu’il ancre chaque jour d’étude, un RFSTDTC manquant ou erroné propage des erreurs dans chaque domaine en aval, et c’est pourquoi il vit sur l’unique enregistrement DM au niveau sujet.
ARM (avec son code court ARMCD) est le bras de traitement prévu — le bras auquel le sujet a été randomisé. ACTARM (ACTARMCD) est le bras réel — le traitement que le sujet a réellement reçu. Ils sont identiques pour la plupart des sujets mais diffèrent lorsqu’un sujet est dosé ou inclus différemment de ce qui était prévu (une erreur de randomisation ou de dosage). Les deux existent parce qu’une analyse d’efficacité (en intention de traiter) est menée par bras prévu tandis qu’une analyse de sécurité est menée par bras réel, et DM est le seul endroit où les deux sont enregistrés. Les deux sont des valeurs définies par l’étude issues de la définition Trial Arms, copiées verbatim avec assign_no_ct().
Les trois classes générales d’observation enregistrent chacune plusieurs lignes par sujet indexées par une variable topic : Findings (une mesure, indexée par --TESTCD), Events (quelque chose qui est arrivé, indexé par --TERM), Interventions (un traitement administré, indexé par --TRT). La classe Special-Purpose — à laquelle DM appartient — n’a aucune variable topic et contient des informations au niveau sujet ou de référence de l’étude ; DM en particulier a exactement une ligne par sujet et aucun numéro de séquence --SEQ. Ce n’est pas une observation parmi d’autres, mais l’ancre à laquelle les observations se rattachent.
Utilisez assign_ct() pour les descripteurs à Controlled Terminology recodés via une codelist (SEX via C66731, RACE via C74457, ETHNIC via C66790) ; assign_no_ct() pour les valeurs définies par l’étude copiées telles quelles (les bras prévu et réel ARM/ARMCD/ACTARM/ACTARMCD, et COUNTRY) ; assign_datetime() pour les dates de référence ISO 8601 (RFSTDTC, RFENDTC, BRTHDTC, DMDTC) ; et derive_study_day() pour DMDY. AGE est un simple calcul R de BRTHDTC à RFSTDTC, et generate_oak_id_vars() estampille d’abord la clé de traçabilité sur laquelle chaque mapping se rejoint. DM n’a aucune étape derive_seq() — un domaine Special-Purpose à une ligne par sujet ne porte pas de --SEQ. La vignette sur les algorithmes documente les algorithmes de mapping, et l’index de référence du package liste chaque fonction.
Testez vos connaissances
Supposons que l’extrait brut gagne un sujet en échec de sélection — inclus et évalué, mais jamais randomisé ni dosé. Par convention CDISC, un tel sujet a ARMCD = "SCRNFAIL" et ARM = "Screen Failure", aucun bras réel, et aucune date de début de référence. Esquissez ce qui change : que contiennent ARM, ACTARM et RFSTDTC, et qu’advient-il du DMDY de ce sujet et des jours d’étude dans chaque autre domaine ?
Un échec de sélection n’a jamais été traité, donc il n’y a pas de traitement réel ni de début de référence. Réfléchissez à ce que renvoie derive_study_day() lorsque son refdt (RFSTDTC) est manquant — et rappelez-vous qu’un RFSTDTC légitimement nul est conforme, et non un bug à corriger.
La ligne d’échec de sélection existe toujours dans DM (DM a une ligne par sujet inclus, traité ou non), mais :
# for the screen-failure subject:
# ARMCD = "SCRNFAIL" ARM = "Screen Failure" (planned "arm" = the not-assigned category)
# ACTARMCD = "" ACTARM = "" (never treated -> actual arm is blank)
# RFSTDTC = NA (no reference start -> no study clock)Avec RFSTDTC manquant, derive_study_day() renvoie NA pour le DMDY de ce sujet — et, surtout, pour chaque jour d’étude de tout autre domaine, parce qu’ils s’ancrent tous sur le RFSTDTC de ce sujet. C’est le comportement correct : un sujet sans traitement n’a pas d’horloge d’étude, donc ses jours d’étude sont véritablement indéfinis. Vous n’inventez jamais une date de référence pour faire disparaître le NA ; l’analyse en aval traite le nul délibérément. C’est la démonstration la plus claire de pourquoi DM est la colonne vertébrale — une seule date manquante ici change ce que chaque autre domaine peut calculer pour ce sujet.
A. Parce que sdtm.oak ne peut pas générer de numéro de séquence pour la démographie B. Parce que DM est un domaine Special-Purpose avec exactement une ligne par sujet, donc USUBJID à lui seul est la clé C. Parce que le numéro de séquence de DM est stocké dans le jeu de données ADSL à la place
B. Une variable --SEQ existe pour distinguer plusieurs enregistrements d’un même sujet au sein d’un domaine (un sujet a de nombreuses lignes AE, VS ou EX, donc AESEQ/VSSEQ/EXSEQ les numérotent). DM est un domaine Special-Purpose à une ligne par sujet, donc il n’y a rien à séquencer — USUBJID à lui seul identifie de façon unique l’enregistrement. A est faux (l’absence est intentionnelle, pas une limitation de l’outil), et C est faux — ADSL est un jeu de données d’analyse en aval, lui aussi à une ligne par sujet, et n’a de même aucune telle séquence.
Conclusion
Mapper DM est la même boucle champ par champ que pour n’importe quel domaine SDTM, appliquée à la classe Special-Purpose : remplir la bonne colonne SDTM à partir du bon champ brut avec le bon algorithme. assign_ct() recode le sexe, la race et l’ethnicité via la Controlled Terminology ; assign_no_ct() copie les bras prévu et réel définis par l’étude ; assign_datetime() construit les dates de référence ISO 8601 ; AGE est une simple dérivation à partir de la date de naissance ; et derive_study_day() place la date de démographie sur l’horloge. Mais DM n’est pas un domaine comme les autres — c’est la colonne vertébrale des sujets : une ligne par sujet, indexée par USUBJID, ancrée sur RFSTDTC, avec le traitement prévu et le traitement réel côte à côte. Chaque autre domaine s’y joint et se cadence sur lui, donc réussir DM est la condition préalable de chaque jeu de données en aval. Avec un DM conforme en main, l’étape suivante est la couche d’analyse, où ces colonnes au niveau sujet deviennent l’ossature du jeu de données ADSL construit 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
- Mapper des données EDC brutes vers un domaine SDTM Findings avec sdtm.oak — la contrepartie « signes vitaux » de la classe Findings. · Mapper les événements indésirables vers le domaine SDTM AE avec sdtm.oak — la contrepartie Events. · Mapper l’exposition au médicament vers le domaine SDTM EX avec sdtm.oak — la contrepartie Interventions, dont les jours d’étude sont cadencés sur le
RFSTDTCde ce domaine. · Créer ADSL en R avec admiral — l’étape suivante, où les colonnes de niveau sujet de DM ancrent le jeu de données d’analyse. - Où cela s’inscrit : les fondations réglementaires et CDISC → la construction de la colonne vertébrale des sujets DM (vous êtes ici), à laquelle les domaines Findings, Events et Interventions se joignent tous → la construction d’ADSL avec admiral → les tables, listings et figures que livre chaque soumission. Le SDTM DM est l’ancre à une ligne par sujet autour de laquelle toute l’étude est assemblée.
Réutilisation
Citation
@online{2026,
author = {},
title = {Domaine SDTM DM en R avec sdtm.oak : démographie},
date = {2026-07-01},
url = {https://www.datanovia.com/learn/pharma-clinical/02-sdtm-programming/sdtm-dm-demographics-domain},
langid = {fr}
}