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.

Date de publication

1 juillet 2026

Modifié

7 juillet 2026

AstucePoints clés
  • 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 DM RFSTDTC. 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/ARMCD enregistrent le bras auquel le sujet a été randomisé ; ACTARM/ACTARMCD enregistrent le bras qu’il a réellement reçu — ils diffèrent lorsqu’un sujet est mal dosé, et les deux sont copiés avec assign_no_ct().
  • Les dates de référence ancrent toute l’horloge de l’étude. assign_datetime() construit les RFSTDTC/RFENDTC/BRTHDTC ISO 8601 ; AGE est dérivé de la date de naissance ; et derive_study_day() place la date de collecte propre à DM sur l’horloge — le jour d’étude 1 est RFSTDTC lui-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é :

Dot plot of the mapped SDTM DM domain, one dot per subject. The three planned treatment arms (Placebo, Xanomeline Low Dose, Xanomeline High Dose) sit on the y axis; each subject's age is its position on the x axis, from about 55 to 74 years; dots are coloured by sex (azure for female, orange for male); a dark vertical crossbar marks each arm's mean age. Twelve dots in total — because DM has exactly one row per subject, each dot IS one DM record. The point is that a raw enrollment extract has become a tidy, subject-level demographics table, immediately readable as a baseline-characteristics figure.

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 Sex C66731 : Female/MaleF/M.
  • RACE — la codelist Race C74457 : WhiteWHITE, Black or African American reste sous son terme standard en majuscules, et ainsi de suite.
  • ETHNIC — la codelist Ethnic Group C66790 : Hispanic or LatinoHISPANIC 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.

🟢 Avec un agent IA

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.

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 = {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}
}
Veuillez citer ce travail comme suit :
“Domaine SDTM DM en R avec sdtm.oak : démographie.” 2026. July 1. https://www.datanovia.com/learn/pharma-clinical/02-sdtm-programming/sdtm-dm-demographics-domain.