CDISC標準(SDTM・ADaM)とは ― 生物統計家のためのデータ標準入門とRによる実装 ―

本記事にはプロモーション(広告・アフィリエイトリンク)が含まれます。
記事の目次
Toggleこの記事でわかること
- CDISC標準の全体像:CDASH(収集)→ SDTM(観測)→ ADaM(解析)→ define.xml(定義書)という、バラバラの規格ではなく一本の流れであること
- SDTMとADaMの決定的な違い:SDTMは観測されたものを観測されたまま持ち、導出はADaM側で行うという役割分担になっていること
- ADaMの二大構造:ADSL(1被験者1行)とBDS(縦持ち)を、Rで実際に組み立てながら手を動かして理解できること
- BDSにする見返り:
PARAMCD/AVAL/BASE/CHGを持つ構造なら解析は3行で書けること(ANCOVAで治療効果 −7.7681 mmHg、95%信頼区間 −11.779〜−3.757 を実測) - 提出物まで到達する道筋:define.xml の骨格を xml2 で生成し、
haven::write_xpt(version = 5)で提出形式(.xpt)まで出力できること
はじめに
製薬企業やCRO(Contract Research Organization、開発業務受託機関)で臨床試験の解析を担当すると、遅かれ早かれ「SDTM」「ADaM」「define.xml」という言葉に出会います。ところが、世の中の解説の多くは「SDTMは観測データ、ADaMは解析用データです」という用語の定義で終わってしまい、実際にどんな形をしたデータなのかが最後まで見えてこない、というのが正直なところではないでしょうか。
本記事の立場ははっきりしています。CDISCの構造は、手を動かせば理解できます。ここではRで小さなSDTM風のデータを作り、そこからADaMを組み立て、実際に解析を走らせ、define.xmlの骨格を生成し、提出形式である.xptで書き出すところまでを一気通貫でやります。用語の暗記ではなく、データの形そのものを見てもらうのが狙いです。
想定読者は、製薬企業・CROの生物統計担当者とプログラマー、データマネジメント(DM)担当者、そしてこれから製薬業界を目指す学生・転職検討者の方です。SASしか触ったことがなく、Rでの実装を知りたいという方にも読んでいただけるように書いています。試験全体の流れの中で解析担当者が何をしているかはフェーズ3臨床試験の流れと統計解析担当者の役割で、申請時の電子データ提出そのものについては申請電子データ提出についてで扱っていますので、併せてご覧ください。
本記事のコードはすべて R 4.5.1 で実際に実行した結果を掲載しており、上から順にコピーすれば同じ数値を再現できます。ただし、ここで扱うデータは構造を理解するために乱数で作った架空のものであり、実際の臨床試験データでも、実在する医薬品の結果でもありません。この点は最初にお断りしておきます。
CDISC標準とは ― CDASH・SDTM・ADaM・define.xmlのつながり
CDISC(Clinical Data Interchange Standards Consortium)は、臨床試験データの収集から規制当局への提出までを対象とする標準を策定している非営利の標準化団体です。策定されている標準は複数ありますが、それぞれが試験データのライフサイクルの別々の段階を担当しています。
| 標準 | 正式名称 | 扱う段階 | 現行版(実装ガイド) |
|---|---|---|---|
| CDASH | Clinical Data Acquisition Standards Harmonization(臨床データ収集標準) | 収集(CRFの項目設計) | CDASHIG v2.3 |
| SDTM | Study Data Tabulation Model(試験データ表形式モデル) | 観測(集めたデータの標準化) | SDTMIG v3.4 |
| ADaM | Analysis Data Model(解析データモデル) | 解析(導出変数・フラグを付与) | ADaMIG v1.3 |
| Define-XML | Define-XML(データ定義書のXML標準) | 定義書(提出データの取扱説明書) | v2.1(最新のパッチは v2.1.11) |
| SEND | Standard for Exchange of Nonclinical Data(非臨床データ交換標準) | 非臨床試験(動物試験)向けのSDTM実装 | SENDIG v3.1.1 |
モデル本体のバージョンは SDTM が v2.1、ADaM が v2.1 です。なお SDTMIG v4.0 は現時点で未発行で、公開レビュー段階にあります。実装ガイドは頻繁に改訂されるため、「最新版はどれか」ではなく「自分の試験でどの版を使うと決めたか」を管理する意識のほうが実務では重要になります。用語の取りうる値を定めた Controlled Terminology(統制用語集)は、NCI EVS から四半期ごとに公開されます。
では、なぜここまで標準化するのでしょうか。理由は3つに整理できます。第一に、規制当局が同じ形で審査できること。標準がなければ、審査官は試験ごとにデータの読み方を一から学び直すことになり、審査そのものが成立しません。第二に、社内やCRO間でプログラムを再利用できること。変数名と構造が決まっていれば、前の試験で書いた集計プログラムがそのまま次の試験で動きます。第三に、データ移管や統合解析が容易になること。複数試験をまとめて解析する際、構造が揃っていることの価値は計り知れません。
CDISCは規格の寄せ集めではなく、収集(CDASH)→ 観測(SDTM)→ 解析(ADaM)→ 定義書(define.xml)という一本の流れです。それぞれの標準が「どの段階の、誰に向けたデータか」を分担しており、この流れが頭に入ると個々の規格の位置づけが一気に見通せるようになります。
日本のPMDA(医薬品医療機器総合機構)も、米国FDAも、承認申請にあたって電子データを標準化された形式で提出することを求めています。具体的な通知や受け入れ可能なバージョンについては、後半の「実務でのポイント」の節で詳しく扱います。試験全体の設計の中でデータがどう位置づけられるかという観点からは、【完全解説】ICH E8(R1)「臨床試験の一般指針」をわかりやすくまとめてみたも併せてお読みいただくと理解が立体的になります。
SDTMを理解する ― 観測データを標準ドメインに写し取る
SDTM(Study Data Tabulation Model)の設計思想は一言で表せます。観測されたものを、観測されたまま、決められた変数名で持つ。これに尽きます。ベースラインからの変化量のような導出値も、解析対象集団のフラグも、原則としてSDTMには置きません。SDTMの仕事は「収集された事実を、誰が見ても同じ意味に読める形に写し取る」ことであって、解析の準備をすることではないからです。
SDTMのデータはドメインという単位に分かれています。ドメインは大きく3つのクラスに分類され、これに Special Purpose(特殊目的)が加わります。
| クラス | 代表ドメイン | 何を記録するか |
|---|---|---|
| Interventions(介入) | EX, CM, SU | 被験者に与えられたもの。治験薬の曝露、併用薬、嗜好品の摂取など |
| Events(事象) | AE, MH, DS | 被験者に起きた出来事。有害事象、既往歴、試験の中止・完了など |
| Findings(所見) | VS, LB, EG, QS | 測定・評価した結果。バイタルサイン、臨床検査値、心電図、質問票など |
| Special Purpose(特殊目的) | DM, CO, SE, SV | 上の3分類に収まらないもの。人口統計学的情報、コメント、試験の要素・訪問など |
変数にも役割(Role)が定められています。Identifier(STUDYID, DOMAIN, USUBJID, --SEQ)はレコードを識別するもの、Topic(--TESTCD, --TERM)はそのレコードが何についてのものかを示す中心的な変数、Timing(VISIT, --DTC)は時点、Qualifier(--STRESN, --STRESU, --BLFL)は結果の値や単位、フラグを表します。ここで -- は各ドメインの2文字の接頭辞に置き換わることを必ず押さえてください。VSドメインなら VSTESTCD、AEドメインなら AETERM というように展開されます。この命名規則があるおかげで、初めて見るドメインでも変数の役割が推測できるわけです。
では実際に、SDTM風のデータをRで作ってみます。以下は本記事全体の共通セットアップで、これ以降のコードはすべてこの続きとして実行します。まずは DM(人口統計学ドメイン)です。
set.seed(20260819)
n_sub <- 60
usubjid <- sprintf("STUDY01-%03d", 1:n_sub)
# SDTM DM(人口統計学ドメイン:1被験者1行)
DM <- data.frame(
STUDYID = "STUDY01",
DOMAIN = "DM",
USUBJID = usubjid,
SUBJID = sprintf("%03d", 1:n_sub),
AGE = round(rnorm(n_sub, 62, 9)),
AGEU = "YEARS",
SEX = sample(c("M", "F"), n_sub, replace = TRUE),
ARMCD = rep(c("PBO", "TRT"), each = n_sub / 2),
ARM = rep(c("Placebo", "Drug X 10mg"), each = n_sub / 2),
RFSTDTC = "2026-01-15",
stringsAsFactors = FALSE
)
str(DM)
'data.frame': 60 obs. of 10 variables:
$ STUDYID: chr "STUDY01" "STUDY01" "STUDY01" "STUDY01" ...
$ DOMAIN : chr "DM" "DM" "DM" "DM" ...
$ USUBJID: chr "STUDY01-001" "STUDY01-002" "STUDY01-003" "STUDY01-004" ...
$ SUBJID : chr "001" "002" "003" "004" ...
$ AGE : num 65 67 51 64 56 67 56 58 63 66 ...
$ AGEU : chr "YEARS" "YEARS" "YEARS" "YEARS" ...
$ SEX : chr "M" "M" "F" "F" ...
$ ARMCD : chr "PBO" "PBO" "PBO" "PBO" ...
$ ARM : chr "Placebo" "Placebo" "Placebo" "Placebo" ...
$ RFSTDTC: chr "2026-01-15" "2026-01-15" "2026-01-15" "2026-01-15" ...
DMは Special Purpose クラスに属し、1被験者につき1行を持つドメインです。60行あるということは、60例が登録されたということを意味します。ここで最も重要な変数が
USUBJID(Unique Subject Identifier)で、これは試験全体を通じて被験者を一意に識別するキーです。実施医療機関単位で付番される SUBJID とは別物で、全ドメイン・全ADaMデータセットがこの USUBJID で連結されます。RFSTDTC は治験薬投与開始日など参照開始日を表す変数で、SDTMの日付が YYYY-MM-DD というISO 8601形式の文字列で持たれている点にも注目してください。中身を確認し、割付の状況を見てみます。
head(DM[, c("USUBJID", "AGE", "SEX", "ARMCD", "ARM")], 4)
table(DM$ARM, DM$SEX)
USUBJID AGE SEX ARMCD ARM
1 STUDY01-001 65 M PBO Placebo
2 STUDY01-002 67 M PBO Placebo
3 STUDY01-003 51 F PBO Placebo
4 STUDY01-004 64 F PBO Placebo
F M
Drug X 10mg 15 15
Placebo 14 16
60例が Placebo と Drug X 10mg に30例ずつ割り付けられています。性別の内訳は Drug X 10mg 群が女性15例・男性15例、Placebo 群が女性14例・男性16例です。
ARMCD(短いコード)と ARM(人が読む名称)が対になっている点は、後に出てくるADaMの PARAMCD と PARAM の関係と同じ発想です。SDTMではコードと表示名を必ずペアで持つという考え方が徹底されています。次に、Findings クラスの代表である VS(バイタルサイン)を作ります。ここでは収縮期血圧をベースライン・4週・8週の3時点で測定した設定です。
visits <- c("BASELINE", "WEEK 4", "WEEK 8")
visitn <- c(0, 4, 8)
base_bp <- rnorm(n_sub, 148, 12)
eff <- ifelse(DM$ARMCD == "TRT", -12, -3)
VS <- do.call(rbind, lapply(seq_along(visits), function(k) {
data.frame(
STUDYID = "STUDY01",
DOMAIN = "VS",
USUBJID = usubjid,
VSTESTCD = "SYSBP",
VSTEST = "Systolic Blood Pressure",
VSSTRESN = round(base_bp + (visitn[k] / 8) * eff + rnorm(n_sub, 0, 6), 1),
VSSTRESU = "mmHg",
VISIT = visits[k],
VISITNUM = visitn[k],
VSBLFL = ifelse(visits[k] == "BASELINE", "Y", ""),
stringsAsFactors = FALSE
)
}))
VS <- VS[order(VS$USUBJID, VS$VISITNUM), ]
rownames(VS) <- NULL
nrow(VS)
head(VS[, c("USUBJID", "VSTESTCD", "VISIT", "VISITNUM", "VSSTRESN", "VSBLFL")], 6)
[1] 180
USUBJID VSTESTCD VISIT VISITNUM VSSTRESN VSBLFL
1 STUDY01-001 SYSBP BASELINE 0 138.2 Y
2 STUDY01-001 SYSBP WEEK 4 4 141.6
3 STUDY01-001 SYSBP WEEK 8 8 143.5
4 STUDY01-002 SYSBP BASELINE 0 138.6 Y
5 STUDY01-002 SYSBP WEEK 4 4 141.2
6 STUDY01-002 SYSBP WEEK 8 8 133.3
60例 × 3時点で 180行になりました。SDTMのFindingsクラスは1測定につき1行の縦持ちで持つのが原則で、「BASELINE列・WEEK4列・WEEK8列」という横持ちにはしません。
VSSTRESN は Standardized Result in Numeric format(標準化された数値結果)を意味し、単位を統一したうえで数値型にしたものです。CRFに書かれた元の測定値そのものは VSORRES という別の変数に入るので、両者は別物である点に注意してください。VSBLFL(Baseline Flag)はその測定がベースラインであることを示すフラグで、ベースライン行だけが "Y"、それ以外は空になっています。最後に、Events クラスの代表である AE(有害事象)です。
n_ae <- 45
ae_sub <- sample(usubjid, n_ae, replace = TRUE)
AE <- data.frame(
STUDYID = "STUDY01",
DOMAIN = "AE",
USUBJID = ae_sub,
AETERM = sample(c("Headache", "Nausea", "Dizziness"), n_ae, replace = TRUE),
AEDECOD = NA_character_,
AESEV = sample(c("MILD", "MODERATE", "SEVERE"), n_ae,
replace = TRUE, prob = c(0.6, 0.3, 0.1)),
AESER = sample(c("N", "Y"), n_ae, replace = TRUE, prob = c(0.9, 0.1)),
stringsAsFactors = FALSE
)
AE$AEDECOD <- AE$AETERM
nrow(AE)
length(unique(AE$USUBJID))
table(AE$AESEV)
[1] 45
[1] 30
MILD MODERATE SEVERE
23 16 6
45件の有害事象が、30人の被験者から報告されています。行数(45)と人数(30)が一致しないのは、1人が複数件の事象を報告するためです。これは後の集計で決定的に効いてくるので、いま覚えておいてください。重症度
AESEV は MILD 23件、MODERATE 16件、SEVERE 6件でした。AETERM は報告されたそのままの表現(医師が記載した文言)で、AEDECOD はMedDRAの基本語(PT)に読み替えたものです。集計は原則として AEDECOD のほうで行います。上のコードでは
AE$AEDECOD <- AE$AETERM として AETERM をそのまま AEDECOD に入れていますが、これは構造を示すための便宜的な処理です。実際のMedDRAコーディングは辞書に基づいて専門の担当者が行う作業であり、報告された表現をそのまま基本語として使うことはありません。この点を混同しないでください。ここまで作ったDM・VS・AEを見返すと、SDTMは「解析用データ」ではないことがはっきりします。ベースラインからの変化量はどこにもありませんし、ITT集団・安全性解析対象集団といった解析対象集団のフラグも存在しません。年齢を「65歳以上/未満」に区切った層別変数もありません。SDTMはあくまで観測された事実の写しであり、これらの導出はすべてADaM側の仕事です。だからこそ、解析にはADaMが必要になります。
有害事象データの発現割合や曝露調整発現率(EAIR)、リスク差の信頼区間といった具体的な解析手法については、有害事象(AE)データの統計解析 ― 発現割合・曝露調整発現率(EAIR)・リスク差の信頼区間をRで実装するで詳しく扱っています。本記事では、そうした解析を可能にするデータの形のほうに集中します。
ADaMを理解する ― ADSLとBDSをRで組み立てる
ここからが本記事の中核です。SDTM(Study Data Tabulation Model、試験データ表形式モデル)が観測されたものを観測されたまま持つのに対し、そこに解析に必要な導出を明示的に足したものが ADaM(Analysis Data Model、解析データモデル)です。前節で作った DM・VS・AE の続きとして、ADSL・ADVS・ADAE を組み立てます。
ADaMが満たすべき2つの原則
ひとつめは analysis-ready(そのまま解析できる) です。必要な導出変数と解析対象集団のフラグがすべて載っており、解析プログラムは「読み込んで、絞り込んで、モデルに渡す」だけで済む状態を指します。解析プログラムの中でベースライン値を計算しなければならないなら、まだ analysis-ready ではありません。
ふたつめは traceability(トレーサビリティ) です。ADaM のすべての値が、SDTM のどの値から、どの手順で作られたかを追える状態を指します。「この解析結果の元の数値は、記録された値と本当に同じか」を審査側が確かめられなければ、結果そのものが信用できません。後述の define.xml が存在する理由も、突き詰めればここにあります。
ADaM はさらに、メタデータによって自己記述的で、審査者にも統計担当者にも理解可能であることを求めます。つまり ADaM は SDTM を解析用に作り替えたものではなく、SDTM を出発点として、解析に必要な導出を明示的に足したものです。元の値は書き換えず、導出した値は新しい変数として並べる。この発想が ADaM の構造を決めています。
ADSL ― 1被験者1行の土台
ADSL(Subject-Level Analysis Dataset、被験者レベル解析データセット)は1被験者につき1行を持ち、試験の内容がどうであれ ADaM で必ず作る唯一のデータセットです。SDTM の DM ドメインから、年齢の層別変数・計画された治療群・解析対象集団フラグを導出します。
ADSL <- data.frame(
STUDYID = DM$STUDYID,
USUBJID = DM$USUBJID,
SUBJID = DM$SUBJID,
AGE = DM$AGE,
AGEGR1 = ifelse(DM$AGE >= 65, ">=65", "<65"),
SEX = DM$SEX,
TRT01P = ifelse(DM$ARMCD == "TRT", "Drug X 10mg", "Placebo"),
TRT01PN = ifelse(DM$ARMCD == "TRT", 2, 1),
ITTFL = "Y",
SAFFL = "Y",
stringsAsFactors = FALSE
)
nrow(ADSL)
head(ADSL[, c("USUBJID", "AGE", "AGEGR1", "SEX", "TRT01P", "TRT01PN", "ITTFL")], 4)
table(ADSL$TRT01P, ADSL$AGEGR1)
[1] 60
USUBJID AGE AGEGR1 SEX TRT01P TRT01PN ITTFL
1 STUDY01-001 65 >=65 M Placebo 1 Y
2 STUDY01-002 67 >=65 M Placebo 1 Y
3 STUDY01-003 51 <65 F Placebo 1 Y
4 STUDY01-004 64 <65 F Placebo 1 Y
<65 >=65
Drug X 10mg 18 12
Placebo 18 12
行数は 60、60例に対して1被験者1行です。注目すべきは
AGEGR1。65歳以上か未満かという層別変数は SDTM の DM には存在せず、ADSL で初めて登場する導出変数です。ここでは両群とも <65 が18例、>=65 が12例に分かれました。TRT01P は Planned Treatment for Period 01、割り付けられた治療です。TRT01A は Actual、実際に投与された治療で、取り違え投与が起きれば両者は一致しません。ITT解析では TRT01P、安全性解析では TRT01A を使います。TRT01PN は数値版で、出力表の並び順の制御に使います。解析対象集団フラグ(ITTFL、SAFFL、PPROTFL など)が ADSL に置かれるのは、集団に含まれるかどうかが被験者単位で1回だけ決まるからです。時点ごとに変わらない以上、被験者レベルに1回だけ持たせるのが自然です。集団の定義そのものは 【完全理解】ICH E9「臨床試験の統計的原則」と補遺(Estimand)を徹底解説 で扱った ICH E9 が定めます。
集団フラグは “Y” か空(またはNULL)で持ち、”N” を入れないのが ADaM の慣行です。絞り込み条件が常に
== "Y" に統一され、取り違えが起きにくくなります。そして最も重要なのは、フラグの定義が統計解析計画書(SAP)と一字一句一致していなければならないことです。食い違いは審査での指摘に直結します。BDS ― 縦持ちにする理由
BDS(Basic Data Structure、基本データ構造)は ADaM の二大構造のもう一方で、粒度は 1被験者 × 1パラメータ × 1時点(必要ならさらに解析基準)で1行です。バイタルサインの解析データセット ADVS をこの構造で組み立てます。
ADVS <- data.frame(
STUDYID = VS$STUDYID,
USUBJID = VS$USUBJID,
PARAMCD = VS$VSTESTCD,
PARAM = "Systolic Blood Pressure (mmHg)",
AVISIT = VS$VISIT,
AVISITN = VS$VISITNUM,
AVAL = VS$VSSTRESN,
ABLFL = VS$VSBLFL,
stringsAsFactors = FALSE
)
# ADSL の被験者レベル変数を持ち込む
ADVS <- merge(ADVS, ADSL[, c("USUBJID", "TRT01P", "TRT01PN", "AGE", "SEX", "ITTFL")],
by = "USUBJID")
# BASE(ベースライン値)と CHG(変化量)を導出する
bl <- ADVS[ADVS$ABLFL == "Y", c("USUBJID", "PARAMCD", "AVAL")]
names(bl)[3] <- "BASE"
ADVS <- merge(ADVS, bl, by = c("USUBJID", "PARAMCD"))
ADVS$CHG <- ifelse(ADVS$ABLFL == "Y", NA, ADVS$AVAL - ADVS$BASE)
ADVS <- ADVS[order(ADVS$USUBJID, ADVS$AVISITN), ]
rownames(ADVS) <- NULL
nrow(ADVS)
head(ADVS[, c("USUBJID", "PARAMCD", "AVISIT", "AVISITN", "AVAL", "ABLFL", "BASE", "CHG", "TRT01P")], 6)
[1] 180
USUBJID PARAMCD AVISIT AVISITN AVAL ABLFL BASE CHG TRT01P
1 STUDY01-001 SYSBP BASELINE 0 138.2 Y 138.2 NA Placebo
2 STUDY01-001 SYSBP WEEK 4 4 141.6 138.2 3.4 Placebo
3 STUDY01-001 SYSBP WEEK 8 8 143.5 138.2 5.3 Placebo
4 STUDY01-002 SYSBP BASELINE 0 138.6 Y 138.6 NA Placebo
5 STUDY01-002 SYSBP WEEK 4 4 141.2 138.6 2.6 Placebo
6 STUDY01-002 SYSBP WEEK 8 8 133.3 138.6 -5.3 Placebo
60例×3時点で 180 行になりました。行数は SDTM の VS と同じですが、持っている列がまったく違います。
PARAMCD は絞り込み用の短いコード(SYSBP)、PARAM は人が読む名前で単位まで含み(Systolic Blood Pressure (mmHg))、出力表の見出しにそのまま使えます。AVAL は解析対象の値、BASE はベースライン値、CHG は変化量です。STUDY01-001 は
BASE=138.2 で、WEEK 8 の AVAL が 143.5、CHG は 5.3。STUDY01-002 は WEEK 8 の AVAL が 133.3 で CHG は −5.3 です。ベースライン行(ABLFL=”Y”)では変化量が定義できないため CHG は NA になります。変化量の定義は次の一行で尽きます。
\[ CHG=AVAL-BASE \]
\(AVAL\) は各時点の解析値、\(BASE\) はベースライン値です。この計算を解析プログラムではなくデータセット側で済ませておくのが ADaM の思想です。

なぜ横持ちではなく縦持ちなのか。 ここが ADaM の理解でいちばん引っかかる部分です。理由は3つあります。
第一に、パラメータが増えても列が増えないことです。実際の試験では収縮期血圧のほかに拡張期血圧・脈拍・体温・体重と、10種類以上のバイタルサインを取ります。横持ちなら SYSBP_BL、SYSBP_W4、SYSBP_W8、DIABP_BL……と、パラメータ数×時点数だけ列が増えます。BDS なら拡張期血圧を足しても PARAMCD が DIABP の行が180行増えるだけで、列の構成は1文字も変わりません。
第二に、同じプログラムを使い回せることです。列名が変わらなければ、参照する変数名も変わりません。収縮期血圧と拡張期血圧の解析コードの違いは PARAMCD の絞り込み条件ただ1か所です。横持ちなら変数名を書き換えたプログラムをパラメータの数だけ並べることになり、修正漏れの温床になります。
第三に、時点が増減しても構造が壊れないことです。12週の訪問が追加されても、被験者が8週で中止しても、BDS では行が増減するだけです。横持ちなら列を追加する改修が必要で、既存プログラムにも影響が波及します。
代償もあります。ひとつは行数が急増することで、実際の試験では数十万行規模の ADaM データセットも珍しくありません。もうひとつは、
PARAMCD の絞り込みを1つ間違えると、まったく別の解析になってしまうことです。しかもエラーは出ず、プログラムは何事もなかったように結果を返します。絞り込み後の行数を必ず確認してください。なお ADaM の構造は BDS だけではありません。有害事象や併用薬のような「発現」を扱うデータには OCCDS(Occurrence Data Structure、発現データ構造)、生存時間解析には ADTTE(Time-to-Event) があり、ADTTE は AVAL にイベントまでの時間、CNSR に打ち切りの有無を持ちます。詳しくは 生存時間解析の基礎:カプラン–マイヤー法と SAS・R による実装を徹底解説 をご覧ください。
BDSにすると解析は3行で書ける
ここまでの投資が何に効くのかを確かめます。8週時点の収縮期血圧の変化量を、ベースライン値で調整した ANCOVA(共分散分析)で比較します。
ana <- subset(ADVS, PARAMCD == "SYSBP" & AVISIT == "WEEK 8" & ITTFL == "Y")
nrow(ana)
ana$TRT01P <- relevel(factor(ana$TRT01P), ref = "Placebo")
fit <- lm(CHG ~ TRT01P + BASE, data = ana)
round(summary(fit)$coefficients, 4)
round(confint(fit)[2, ], 3)
[1] 60
Estimate Std. Error t value Pr(>|t|)
(Intercept) 15.3752 10.5324 1.4598 0.1498
TRT01PDrug X 10mg -7.7681 2.0029 -3.8784 0.0003
BASE -0.1294 0.0706 -1.8331 0.0720
2.5 % 97.5 %
-11.779 -3.757
絞り込みは
PARAMCD・AVISIT・ITTFL の3条件だけで、解析対象の60行が得られました。治療効果の推定値は −7.7681 mmHg(標準誤差 2.0029、t = −3.8784、p = 0.0003)、95%信頼区間は −11.779 〜 −3.757。プラセボと比べ、Drug X 10mg のほうが8週時点の収縮期血圧の低下が大きいと読めます。
ベースライン共変量
BASE の係数は −0.1294(p = 0.0720)で、ベースラインの血圧が高い被験者ほど下がり幅が大きい傾向を示します。relevel() で参照群をプラセボに揃えているため、係数は「プラセボに対する Drug X 10mg の効果」として読めます。注目していただきたいのは、解析の本体が実質3行しかないことです。ベースライン値の計算も集団の判定も変化量の導出も、すでに ADVS の中で済んでいるからです。別のパラメータを解析したくなったら PARAMCD == "SYSBP" を書き換えるだけでよく、これが BDS への投資対効果です。
ANCOVA の理論的な扱いは 共変量調整(ANCOVA)徹底解説 ― FDA/EMAガイダンスとR・SASでベースライン共変量を扱う で解説しています。全時点を反復測定として扱う場合の発展先は MMRM(反復測定混合モデル)と多重代入法の組み合わせ解析 です。
ADAE ― 発現割合を出せる形にする
安全性集計も同じ考え方です。AE ドメインに ADSL の被験者レベル変数を結合し、発現割合を計算します。
ADAE <- merge(AE, ADSL[, c("USUBJID", "TRT01P", "SAFFL")], by = "USUBJID")
ADAE$TRTEMFL <- "Y"
n_saf <- table(ADSL$TRT01P[ADSL$SAFFL == "Y"])
n_ae_sub <- table(unique(ADAE[ADAE$TRTEMFL == "Y", c("USUBJID", "TRT01P")])$TRT01P)
n_saf
n_ae_sub
round(100 * n_ae_sub / n_saf, 1)
Drug X 10mg Placebo
30 30
Drug X 10mg Placebo
14 16
Drug X 10mg Placebo
46.7 53.3
安全性解析対象集団は各群30例。少なくとも1件の有害事象を報告した被験者は Drug X 10mg で14例、Placebo で16例で、発現割合はそれぞれ 46.7%、53.3% です。
TRTEMFL は Treatment-Emergent Flag(治験薬投与後に発現したかどうかを示すフラグ)で、投与開始後に新たに発現・悪化した事象を対象とするために必要です。本記事の
TRTEMFL は構造を示すため一律 “Y” としていますが、本来は治験薬の投与開始日と有害事象の発現日を比較して導出されるべきものです。日付の欠測をどう扱うかも含め、SAP に定義を明記したうえで導出します。安全性集計で最も典型的な誤りは、件数と人数を取り違えることです。SDTMのAEドメインで見たとおり、この試験では45件の有害事象が30人から報告されています。1人が複数件報告するため件数と人数は一致せず、45を分子にすると被験者ベースの実際の発現割合とはまったく違う数字になります。上のコードが unique() で被験者と治療群の組み合わせを一意にしているのは、この取り違えを防ぐためです。詳しい計算や曝露調整発現率(EAIR)は 有害事象(AE)データの統計解析 ― 発現割合・曝露調整発現率(EAIR)・リスク差の信頼区間をRで実装する をご覧ください。
トレーサビリティを実際に確認する
最後にトレーサビリティを数値で確かめます。ADVS の AVAL が VS の VSSTRESN と一致するか、ADSL の被験者集合が DM と一致するかを照合します。
chk <- merge(ADVS[, c("USUBJID", "AVISIT", "AVAL")],
VS[, c("USUBJID", "VISIT", "VSSTRESN")],
by.x = c("USUBJID", "AVISIT"), by.y = c("USUBJID", "VISIT"))
nrow(chk)
all.equal(chk$AVAL, chk$VSSTRESN)
setequal(ADSL$USUBJID, DM$USUBJID)
[1] 180
[1] TRUE
[1] TRUE
照合できたのは 180 行、つまり ADVS の全行です。そのすべてで
all.equal(chk$AVAL, chk$VSSTRESN) が TRUE となり、「ADVS の AVAL は VS の VSSTRESN をそのまま持ってきたものである」という主張が数値レベルで裏づけられました。setequal(ADSL$USUBJID, DM$USUBJID) も TRUE で、被験者集合も過不足なく一致しています。この種の照合はプログラムとして書いて自動化しておくことが極めて有用です。データが更新されるたびに走らせれば、導出が壊れた瞬間に気づけます。特に ADSL の被験者数が DM と合わないのは審査での指摘に直結する典型例で、マージ条件を1つ間違えて数例が落ちる事故は誰にでも起こります。トレーサビリティは後から書き起こす資料ではなく、導出しながら記録し、その場で検証するものだと考えてください。
define.xmlとトレーサビリティ ― 提出物として何を作るのか
define.xml は、提出したデータセットの「取扱説明書」をXML形式で機械可読にしたものです。どの変数が何を意味し、どう導出されたのかを記述します。審査側はまずこのファイルを開いてデータの地図を把握します。
| 記述する情報 | 内容 |
|---|---|
| データセット一覧 | 名前、構造(1行が何を表すか)、キー変数 |
| 変数一覧 | 変数名、ラベル、型、長さ、並び順、必須性 |
| コードリスト | 取りうる値の一覧(例:SEX なら M / F) |
| 導出方法(Method) | 導出変数がどのルールで作られたか |
| 原資料へのリンク | アノテーション付きCRFやプログラムへの参照 |
CHG という変数があるだけでは何から作られたのかは分かりません。この「構造」と「導出方法」の記述が橋渡しをします。
define.xmlの骨格をRで組み立てる
XMLと聞くと身構えますが、構造は素直な入れ子です。xml2 で最小の骨格を組み立ててみます。
library(xml2)
doc <- xml_new_root("ODM", xmlns = "http://www.cdisc.org/ns/odm/v1.3",
`xmlns:def` = "http://www.cdisc.org/ns/def/v2.1",
FileType = "Snapshot", ODMVersion = "1.3.2")
study <- xml_add_child(doc, "Study", OID = "STUDY01")
mdv <- xml_add_child(study, "MetaDataVersion",
OID = "MDV.ADaM", Name = "ADaM Define",
`def:DefineVersion` = "2.1.0")
ig <- xml_add_child(mdv, "ItemGroupDef", OID = "IG.ADVS", Name = "ADVS",
Repeating = "Yes", Purpose = "Analysis",
`def:Structure` = "One record per subject per parameter per visit",
`def:Class` = "BASIC DATA STRUCTURE")
xml_add_child(ig, "ItemRef", ItemOID = "IT.ADVS.USUBJID", OrderNumber = "1", Mandatory = "Yes")
xml_add_child(ig, "ItemRef", ItemOID = "IT.ADVS.PARAMCD", OrderNumber = "2", Mandatory = "Yes")
xml_add_child(ig, "ItemRef", ItemOID = "IT.ADVS.AVAL", OrderNumber = "3", Mandatory = "No")
cat(as.character(doc))
<?xml version="1.0" encoding="UTF-8"?>
<ODM xmlns="http://www.cdisc.org/ns/odm/v1.3" xmlns:def="http://www.cdisc.org/ns/def/v2.1" FileType="Snapshot" ODMVersion="1.3.2">
<Study OID="STUDY01">
<MetaDataVersion OID="MDV.ADaM" Name="ADaM Define" def:DefineVersion="2.1.0">
<ItemGroupDef OID="IG.ADVS" Name="ADVS" Repeating="Yes" Purpose="Analysis" def:Structure="One record per subject per parameter per visit" def:Class="BASIC DATA STRUCTURE">
<ItemRef ItemOID="IT.ADVS.USUBJID" OrderNumber="1" Mandatory="Yes"/>
<ItemRef ItemOID="IT.ADVS.PARAMCD" OrderNumber="2" Mandatory="Yes"/>
<ItemRef ItemOID="IT.ADVS.AVAL" OrderNumber="3" Mandatory="No"/>
</ItemGroupDef>
</MetaDataVersion>
</Study>
</ODM>
ItemGroupDef が1つのデータセットに対応します。def:Structure に「1被験者×1パラメータ×1時点で1行」というBDSの粒度が文章で書き込まれている点に注目してください。粒度はコードのコメントではなく提出物のメタデータとして宣言するもの、という思想の表れです。ItemRef は並び順と必須性を記述します。これは最小の骨格です。実際の提出用 define.xml は
ItemDef・CodeList・MethodDef・def:leaf(CRFやプログラムへのリンク)などが加わり、はるかに大きくなります。実務では専用ツールで生成するのが一般的です。なお Define-XML に v2.2 は存在せず、v2.1系のパッチが継続中で最新は v2.1.11(2026年4月6日)です。提出形式に落とす前のセルフチェック
提出データは SAS V5 XPORT 形式(拡張子 .xpt)で作ります。古い規格に由来する制約があるため、書き出す前にチェックします。
# 変数名が8文字を超えていないか
long_names <- names(ADVS)[nchar(names(ADVS)) > 8]
long_names
# 文字変数の最大長(SAS V5 XPORT では200バイトが上限)
sapply(ADVS[sapply(ADVS, is.character)], function(x) max(nchar(x)))
# 主要変数に欠測がないか
colSums(is.na(ADSL[, c("USUBJID", "TRT01P", "AGE", "SEX")]))
character(0)
USUBJID PARAMCD STUDYID PARAM AVISIT ABLFL TRT01P SEX ITTFL
11 5 7 30 8 1 11 1 1
USUBJID TRT01P AGE SEX
0 0 0 0
8文字超の変数名は
character(0)、つまり1つもありません。文字型変数は PARAM の30バイトが最長で、上限の200バイトに収まっています。ADSLの主要変数の欠測も0件です。Rで作ったデータをそのまま提出形式にすると、変数名が長すぎて弾かれるのはよくある失敗です。変数名は8文字まで、変数ラベルは40文字まで、文字型変数の値は200文字まで。そしてUS-ASCIIのみで、日本語などのマルチバイト文字は入れられません。最後の点は日本の実務で強く効き、どこで英語表記に揃えるかはデータの流れを設計する時点で決めておくべき論点です。
.xptで書き出して読み戻す
haven::write_xpt() に version = 5 を指定すると、SAS V5 XPORT形式で出力できます。書き出して終わりにせず、読み戻して確認します。
library(haven)
write_xpt(ADSL, "adsl.xpt", version = 5, name = "ADSL")
write_xpt(ADVS, "advs.xpt", version = 5, name = "ADVS")
file.size("adsl.xpt")
file.size("advs.xpt")
back <- read_xpt("adsl.xpt")
dim(back)
identical(names(back), names(ADSL))
[1] 5520
[1] 25040
[1] 60 10
[1] TRUE
adsl.xpt が 5,520バイト、advs.xpt が 25,040バイトで出力されました。
version = 5 の指定は必須です。読み戻した back は 60行10列で元のADSLと一致し、変数名も identical() が TRUE、すなわち完全一致です。往復で一致を確認するこの一手間が事故を防ぎます。SASで行う場合は libname に xport エンジンを指定し、proc copy で流し込みます。
/* ADaM データセットを SAS V5 XPORT 形式で書き出す */
libname advs xport "advs.xpt";
proc copy in=work out=advs;
select advs;
run;
提出データの位置づけは申請電子データ提出についてもあわせてご覧ください。
実務でのポイント ― PMDA・FDAへの提出で押さえること
現行の通知はどれか
よく引用される「承認申請時の電子データ提出に関する基本的考え方について」(平成26年6月20日 薬食審査発0620第6号)と、実務的事項についての通知(平成27年4月27日 薬食審査発0427第1号)は、いずれも廃止されています。古い通知を参照した解説がインターネット上に残っているため注意が必要です。
現行の通知は「承認申請時等の電子データ提出に関する取扱いについて」(令和4年4月1日、薬生薬審発0401第10号)で、上記2通知を廃止したうえで取扱いを定めています。さらに令和6年4月8日 医薬薬審発0408第3号による一部改正があり、同日付で事務連絡(Q&A)と技術的ガイドの一部改正も出ています。標準やツールを論じる前に、参照している文書が現行のものかを確認する。これが第一歩です。
提出物として何が求められるか
| 提出物 | 位置づけ |
|---|---|
| SDTMデータセット | 観測データを標準ドメインに写し取ったもの |
| ADaMデータセット | 解析に用いたデータ(ADSL・BDS等) |
| Define.XML | データセットと変数の定義書(機械可読) |
| 解析用プログラム | 原則としてADaMを基データとする解析のプログラム |
| Annotated CRF | CRF項目とSDTM変数の対応を示す注釈 |
| データガイド | 作成方針や留意点を審査側に伝える資料 |
誤解が多い点です。現行通知は原則としてADaMデータセットを基データとする解析用プログラムの提出を定める一方、必ずしもPMDAでそのまま実行可能な形式・内容で提出する必要はないとしています。プログラムは「どのような処理をしたかを示す証跡」として求められている、と理解すると腹落ちします。
受け入れられるバージョンを先に確認する
PMDAのバリデーションルール v6.0(2025年3月14日公表、2025年4月1日受付分から適用)では、受け入れるバージョンが次のとおり定められています。
| 標準 | 受け入れられるバージョン |
|---|---|
| SDTMIG | v3.1.2 / v3.1.2 Amd1 / v3.1.3 / v3.2 / v3.3 / v3.4 |
| ADaMIG | v1.0 / v1.1 / v1.2 / v1.3 |
| Define | v2.0 / v2.1 |
勘所は、最新版が常に受け入れられるとは限らないという点です。順序は「最新版を採る」ではなく「当局が受け入れるバージョンを確認してから選ぶ」になります。適用範囲について通知は、令和2年4月1日以降に開始した試験をCDISC標準に準拠した形式で提出する対象としており、希少疾病用医薬品等で同日より前に開始した試験は他の形式も認められています。
非臨床データ(SEND)は現行通知の対象外で、活用は並行して検討中とされ、現時点でPMDAは提出を求めていません。バリデーションソフトウェアは、PMDAで用いるものと同等の結果を返すと公表されたものを用いてよいとされているのみで、特定の製品名が当局公認とされているわけではありません。米国FDAも電子データの標準形式での提出を求めていますが、要求される版や適用時期は随時更新されるため、実際の提出に際してはFDAの最新の公表資料をご確認ください。
・SAPとADaMの定義は一致させる。解析対象集団フラグや導出変数の定義が統計解析計画書と食い違うのは指摘の常連です。
・トレーサビリティを後付けしない。導出のたびに「どのSDTM変数から、どのルールで」を記録します。
・データセットとdefine.xmlを同時に更新する。データを直したのに定義書が古い、という不整合が最多です。
・コンフォーマンスチェックは開発中から回す。早いほど修正は安く済みます。
・SDTMマッピングは共同作業。統計担当者は「解析に必要な情報がSDTMに残っているか」を早期に確認する立場です。

Rで提出対応をする場合の現状
本記事では構造を理解するためにゼロからADaMを組み立てましたが、実務でその必要はありません。admiral は pharmaverse(RocheとGSKが主導)が提供する、CDISC ADaM準拠のデータセットをRで作成するためのツールボックスです。CRANの最新版は v1.5.0(2026年6月12日)、ライセンスは Apache-2.0、R >= 4.1 が必要です。
提出の観点では、R Consortium の R Submissions Working Group がFDAへのパイロット提出を続けています。表・図をRで提出した Pilot 1、Shinyアプリの Pilot 2、表・図に加えADaMをRで提出した Pilot 3 まではFDAのレビューが完了したと公表されています。一方、コンテナ/WebAssemblyの Pilot 4 と Dataset-JSON を用いる Pilot 5 は、提出済みではあるものの現在も進行中です。「RでのFDA提出が全面的に認められた」という段階ではありません。
実務ではSASも依然として広く使われています。使い分けは生物統計家に必要なスキルー統計解析ソフトウェアについて、SASマクロによるADaM作成の効率化はADaMデータセット自動生成とAI活用 ― SASマクロでADLB・ADQSを効率的に作成するをご覧ください。CDISCの理解は解析職で評価される実務スキルの1つであり、学習の順序は生物統計家を目指すための勉強法 ― 基礎から実務スキルまでのロードマップで整理しています。
参考書籍
CDISCそのものの和書は限られますが、周辺の土台を固める書籍は充実しています。データマネジメント・統計解析・試験デザインの3方向から本記事を補強できる3冊を紹介します。



関連記事・次のステップ
CDISCの構造が見えてくると、別々に学んできた話が1本の線でつながります。どの相でどんなデータが集まるのかは臨床試験のフェーズ(第I相〜第IV相)とは、ADSLの TRT01P に入る割付情報の決まり方はランダム化割付の方法とR実装 ― 単純・置換ブロック・層別・最小化法をシミュレーションで比較するで整理できます。
本記事のADVSは全員が3時点を完走した理想的なデータでしたが、実際にはBDSに欠測行が生じ、その扱いが論点になります。考え方は欠測値の取り扱いについて、無視できない欠測の感度分析はティッピングポイント解析(Tipping Point Analysis)とは ― MNAR欠測の感度分析をRで実装する、コードで手が止まった方はR入門 ― インストールからデータ読み込み・要約統計・t検定・グラフ作成までの最初の一歩をご覧ください。標準化の発想は治験の外にも広がっており、実診療データの活用はリアルワールドエビデンス(RWE)入門 ― 規制活用と外部対照群への展開、定型プログラムの効率化はSASマクロ自動生成とAI活用 ― ChatGPT・Claudeで製薬業務を効率化する実践ガイドが参考になります。
まとめ
本記事では、CDISC標準を用語ではなくデータの形として確認してきました。SDTM風のVSは60例×3時点で180行、ADSLは1被験者1行の60行、ADVSはBDSとして180行。AEは45件が30人からで、件数と人数の不一致がそのまま集計の落とし穴になります。BDSに整えたADVSは PARAMCD・AVISIT・ITTFL の3条件で絞るだけでANCOVAが走り、治療効果は −7.7681 mmHg、95%信頼区間は −11.779 〜 −3.757 と推定されました。トレーサビリティも all.equal() が180行すべてで TRUE となり裏づけられています。提出形式では define.xml の骨格を xml2 で組み立て、haven::write_xpt(version = 5) で adsl.xpt 5,520バイト、advs.xpt 25,040バイトを書き出し、読み戻しで変数名の完全一致を確認しました。規制面では、現行通知が令和4年4月1日 薬生薬審発0401第10号であり、平成26年の通知は廃止済みである点を押さえてください。
CDISCの本質は、規格の暗記ではなくデータの流れの理解です。SDTMに写し取り、必要な導出を足してADaMにし、その全体をdefine.xmlで説明する。この流れとADSL・BDSの粒度が腹落ちすれば、実務のコードはほとんど同じ形の繰り返しになります。まずは本記事のコードを手元のRで動かしていただければと思います。ADVSの図示は箱ひげ図をSASとRで実装する方法、指標そのものの読み方はオッズ比・相対リスク・ハザード比の違いをあわせてご覧いただければと思います。











