Exemple d’analyse ADTTE en R : un package de survie réglementaire complet

Le livrable de bout en bout qu’un biostatisticien produit pour un rapport d’étude clinique — l’analyse primaire de temps jusqu’à l’événement sur un jeu de données CDISC ADTTE (KM, log-rank stratifié, le hazard ratio de Cox, tables et figure de CSR) plus l’ensemble d’analyses de sensibilité qui le défend : RMST, risques concurrents et une vérification en sous-groupes, encadrés par ICH E9(R1)

Un package complet et détaillé d’analyse réglementaire de temps jusqu’à l’événement sur un jeu de données CDISC ADTTE en R. Construisez l’objet Surv() prêt à l’analyse à partir du drapeau CNSR, exécutez l’analyse PRIMAIRE pré-spécifiée (Kaplan-Meier avec médiane et IC à 95 %, log-rank stratifié, le hazard ratio de Cox primaire avec une vérification des risques proportionnels, la figure de soumission et les tables de CSR gtsummary), puis ajoutez l’ensemble d’analyses de SENSIBILITÉ qui le rend défendable — le temps de survie moyen restreint (survRM2) comme effet sans hypothèse PH, une vue de l’incidence cumulée en risques concurrents (tidycmprsk), et une vérification de cohérence en sous-groupes / stratifiée. Assemblez les sorties de soumission et rédigez le paragraphe de rapport réglementaire avec l’encadrement par l’estimande ICH E9(R1).

Date de publication

26 juin 2026

Modifié

7 juillet 2026

AstucePoints clés
  • Un livrable réglementaire de temps jusqu’à l’événement n’est pas un seul chiffre — c’est une analyse primaire pré-spécifiée plus un ensemble de sensibilité qui montre que le résultat tient quand vous changez de méthode. Ce capstone construit tout le package sur un seul jeu de données CDISC ADTTE.
  • Analyse primaire (l’estimand pré-spécifié) : Kaplan-Meier avec OS médiane + IC à 95 %, le test du log-rank stratifié, le hazard ratio de Cox avec une vérification PH, plus la figure de soumission et les tables de CSR gtsummary. Sur l’essai de cancer du côlon de forme ADTTE, Lev+5FU a réduit le risque de décès d’environ 30 % (HR = 0.70, IC à 95 % 0.55–0.88, p = 0.003), les risques proportionnels tenant (p = 0.34).
  • Analyse de sensibilité (la valeur ajoutée qui défend la primaire) : le RMST (un effet interprétable et sans hypothèse PH — +111 jours sur 5 ans, p = 0.020) ; une vue de l’incidence cumulée en risques concurrents (décès vs l’événement concurrent de récidive) ; et une vérification de cohérence en sous-groupes / stratifiée (HR 0.68 chez les patients avec <4 ganglions, 0.72 chez ceux avec 4+ ; interaction p = 0.92).
  • L’intérêt d’un ensemble de sensibilité est la convergence : si l’analyse absolue (RMST), celle sans hypothèse et celle en sous-groupes pointent toutes dans le même sens que le HR primaire, la conclusion est robuste. Si l’une diverge, vous avez trouvé quelque chose — et vous le rapportez.
  • Encadrez tout le package avec ICH E9(R1) : un estimand pré-spécifié, des règles de censure documentées, des facteurs de stratification nommés, et des rôles primaire-vs-sensibilité fixés dans le SAP avant la levée de l’insu.

Introduction

Vous maîtrisez le mapping CDISC ADTTE — vous savez passer d’un critère de temps jusqu’à l’événement d’un jeu de données ADaM à un objet Surv() correct et ajuster le modèle primaire. Ce capstone est l’étape suivante : le package d’analyse complet qu’un biostatisticien remet pour un rapport d’étude clinique.

Une vraie soumission n’est jamais un hazard ratio isolé. C’est une analyse primaire pré-spécifiée qui définit l’effet du traitement, enveloppée dans un ensemble d’analyses de sensibilité dont le seul rôle est de demander : « un relecteur qui se méfierait de ma méthode aboutirait-il à la même conclusion ? » Changez la mesure de synthèse, abandonnez l’hypothèse des risques proportionnels, tenez compte d’un événement concurrent, regardez au sein des sous-groupes — si la réponse reste la même, le résultat est robuste. Cette convergence est ce qui rend une découverte défendable, et l’assembler est le travail que cette leçon parcourt de bout en bout.

Nous le construisons sur un seul jeu de données ADTTE et exécutons, dans l’ordre :

  • l’analyse primaire — KM + médiane/IC, log-rank stratifié, le HR de Cox, la vérification PH, la figure de soumission et les tables de CSR ;
  • l’ensemble de sensibilitéRMST (un effet interprétable et sans hypothèse PH), une vue de l’incidence cumulée en risques concurrents, et une vérification de cohérence en sous-groupes ;
  • l’assemblage — les sorties de soumission et le rapport encadré par ICH E9(R1).

Voici le projet qui applique tout ce qu’il y a dans la série. Si vous voulez les mécanismes d’une étape expliqués depuis le début, les liens croisés pointent vers la leçon qui l’enseigne ; ici nous les réunissons de la façon dont le travail se déroule réellement.

Le jeu de données ADTTE (construit une fois, utilisé partout)

Les vraies données ADTTE sont confidentielles, donc nous dérivons un jeu de données ADTTE de forme CDISC à partir de l’essai public colon fourni avec survival — un essai de thérapie adjuvante dans le cancer du côlon. La leçon CDISC ADTTE explique chaque variable et le mapping en détail ; ici nous le construisons une fois et passons à la suite. Deux choses portent toute l’analyse :

  • Le retournement de la censure. CDISC code CNSR = 1 comme censuré (l’inverse du Surv(time, event) de R, qui veut event = 1). Nous mappons le colon$status brut (1 = décès) en CNSR = 1 - status, puis reconstruisons l’indicateur d’événement R en event = 1 - CNSR. Inversez cela et chaque résultat est silencieusement faux.
  • Une ligne par sujet par paramètre. Nous prenons les enregistrements de décès (etype == 2) comme paramètre de survie globale et deux bras — Observation vs Lev+5FU.
library(survival)

# Overall-survival records (death endpoint), two arms
os <- subset(colon, etype == 2 & rx %in% c("Obs", "Lev+5FU"))
os$rx <- droplevels(os$rx)

# Map raw trial data to a CDISC ADaM ADTTE structure
adtte <- data.frame(
  USUBJID = sprintf("COLON-ADJ-001-%04d", os$id),
  PARAMCD = "OS",
  AVAL    = os$time,                      # time to event / censoring (days)
  CNSR    = 1 - os$status,                # CDISC: 1 = censored, 0 = event
  TRT01P  = factor(os$rx, levels = c("Obs", "Lev+5FU"),
                   labels = c("Observation", "Lev+5FU")),
  STRAT   = factor(ifelse(os$differ == 3, "Poor", "Well/Moderate")),  # stratification factor
  NODE4   = factor(ifelse(os$node4 == 1, "4+ positive nodes", "<4 positive nodes")),  # subgroup
  stringsAsFactors = FALSE
)
adtte <- adtte[!is.na(adtte$STRAT), ]      # drop unknown stratum

# Rebuild the R event indicator from the CDISC censoring flag — the load-bearing line
adtte$event <- 1 - adtte$CNSR

# Sanity check: events must equal CNSR == 0
cat("Subjects:", nrow(adtte),
    " | Events (deaths):", sum(adtte$event),
    " | Censored (CNSR == 1):", sum(adtte$CNSR == 1), "\n")
Subjects: 606  | Events (deaths): 287  | Censored (CNSR == 1): 319 
table(arm = adtte$TRT01P)
arm
Observation     Lev+5FU 
        308         298 

606 sujets (308 Observation, 298 Lev+5FU), 287 décès, le reste censuré — en vie au dernier suivi. Le nombre d’événements correspond à sum(CNSR == 0), donc le retournement de la censure est correct. Cet adtte est l’unique jeu de données sur lequel toutes les analyses primaire et de sensibilité ci-dessous s’exécutent.

Partie 1 — l’analyse primaire (l’estimand pré-spécifié)

L’analyse primaire estime l’effet pré-spécifié : la survie globale, comparée entre les bras, résumée par le hazard ratio, avec le log-rank stratifié comme test correspondant. Tout ici est fixé dans le SAP avant la levée de l’insu.

Kaplan-Meier, OS médiane et IC à 95 %

library(survival)

os <- subset(colon, etype == 2 & rx %in% c("Obs", "Lev+5FU"))
os$rx <- droplevels(os$rx)
adtte <- data.frame(
  AVAL = os$time, CNSR = 1 - os$status,
  TRT01P = factor(os$rx, levels = c("Obs", "Lev+5FU"),
                  labels = c("Observation", "Lev+5FU")),
  STRAT = factor(ifelse(os$differ == 3, "Poor", "Well/Moderate")))
adtte <- adtte[!is.na(adtte$STRAT), ]
adtte$event <- 1 - adtte$CNSR

fit <- survfit(Surv(AVAL, event) ~ TRT01P, data = adtte)
print(fit)
Call: survfit(formula = Surv(AVAL, event) ~ TRT01P, data = adtte)

                     n events median 0.95LCL 0.95UCL
TRT01P=Observation 308    165   2083    1548    2789
TRT01P=Lev+5FU     298    122     NA    2725      NA
  • Observation : OS médiane = 2083 jours (IC à 95 % 1548–2789).
  • Lev+5FU : médiane = NA (« non atteinte ») — moins de la moitié des sujets traités sont décédés pendant le suivi, donc la courbe ne franchit jamais 0.5. C’est un bon signe, pas une donnée manquante : rapportez plutôt la borne inférieure de l’IC (2725 jours) et une probabilité de survie à un instant fixe.

La figure de soumission

Le graphique de Kaplan-Meier avec une table des sujets à risque, des bandes de confiance, des lignes de médiane et la p-value du log-rank est la figure de survie standard dans un CSR. Tracez-la avec le ggsurvplot() de survminer :

library(survival)
library(survminer)

os <- subset(colon, etype == 2 & rx %in% c("Obs", "Lev+5FU"))
os$rx <- droplevels(os$rx)
adtte <- data.frame(
  AVAL = os$time, CNSR = 1 - os$status,
  TRT01P = factor(os$rx, levels = c("Obs", "Lev+5FU"),
                  labels = c("Observation", "Lev+5FU")),
  STRAT = factor(ifelse(os$differ == 3, "Poor", "Well/Moderate")))
adtte <- adtte[!is.na(adtte$STRAT), ]
adtte$event <- 1 - adtte$CNSR

fit <- survfit(Surv(AVAL, event) ~ TRT01P, data = adtte)

ggsurvplot(
  fit, data = adtte,
  risk.table = TRUE,        # number at risk under the plot (a submission requirement)
  conf.int = TRUE,          # 95% confidence bands
  pval = TRUE,              # log-rank p-value on the plot
  surv.median.line = "hv",  # dashed median-survival guides
  palette = "jco",          # colourblind-safe journal palette
  legend.labs = c("Observation", "Lev+5FU"),
  legend.title = "Treatment arm",
  xlab = "Time (days)",
  ylab = "Overall survival probability",
  ggtheme = theme_minimal()
)

A submission-style Kaplan-Meier plot of overall survival by treatment arm from the CDISC ADTTE dataset. Two step-function survival curves descend from 1.0 over time; the Lev+5FU curve stays consistently above the Observation curve, each with a shaded 95% confidence band. A dashed median-survival line marks 2083 days for the Observation arm (the Lev+5FU median is not reached). The log-rank p-value is printed on the plot, and a number-at-risk table beneath shows subjects still at risk over time.

La courbe Lev+5FU se situe au-dessus d’Observation à chaque instant — un avantage constant, les courbes étant nettement séparées et ne se croisant pas.

Log-rank stratifié et le hazard ratio de Cox primaire

Le protocole pré-spécifie une stratification par différenciation tumorale, donc le test et le modèle stratifient tous deux dessus. Le log-rank stratifié répond à « les bras diffèrent-ils au sein des strates ? » ; le Cox stratifié donne la taille d’effet — le hazard ratio, l’estimand primaire.

library(survival)

os <- subset(colon, etype == 2 & rx %in% c("Obs", "Lev+5FU"))
os$rx <- droplevels(os$rx)
adtte <- data.frame(
  AVAL = os$time, CNSR = 1 - os$status,
  TRT01P = factor(os$rx, levels = c("Obs", "Lev+5FU"),
                  labels = c("Observation", "Lev+5FU")),
  STRAT = factor(ifelse(os$differ == 3, "Poor", "Well/Moderate")))
adtte <- adtte[!is.na(adtte$STRAT), ]
adtte$event <- 1 - adtte$CNSR

# Pre-specified stratified log-rank
survdiff(Surv(AVAL, event) ~ TRT01P + strata(STRAT), data = adtte)
Call:
survdiff(formula = Surv(AVAL, event) ~ TRT01P + strata(STRAT), 
    data = adtte)

                     N Observed Expected (O-E)^2/E (O-E)^2/V
TRT01P=Observation 308      165      140      4.65      9.07
TRT01P=Lev+5FU     298      122      147      4.40      9.07

 Chisq= 9.1  on 1 degrees of freedom, p= 0.003 
# Primary estimand: stratified Cox hazard ratio
cox <- coxph(Surv(AVAL, event) ~ TRT01P + strata(STRAT), data = adtte)
summary(cox)$conf.int
              exp(coef) exp(-coef) lower .95 upper .95
TRT01PLev+5FU 0.6989835   1.430649 0.5529454 0.8835917
summary(cox)$coefficients
                    coef exp(coef)  se(coef)         z   Pr(>|z|)
TRT01PLev+5FU -0.3581281 0.6989835 0.1195776 -2.994942 0.00274497

Lisez le résultat primaire :

  • Log-rank stratifié : χ² = 9.1 à 1 df, p = 0.003 — les bras diffèrent au sein des strates de différenciation.
  • HR = 0.70 (IC à 95 % 0.55–0.88) — Lev+5FU multiplie le risque de décès par 0.70, un risque inférieur de 30 % ((1 − 0.70) × 100). Observation est la référence, donc le HR se lit « traité vs témoin ».
  • IC entièrement sous 1 et le Wald p = 0.003 — convaincant ; aucune valeur plausible où le traitement serait délétère ou nul.

C’est le chiffre phare auquel le reste du package doit survivre.

Vérifier l’hypothèse des risques proportionnels

Un hazard ratio unique n’a de sens que si l’effet est constant dans le temps. Vérifiez-le avec cox.zph() (résidus de Schoenfeld) avant de faire confiance au HR comme synthèse :

library(survival)

os <- subset(colon, etype == 2 & rx %in% c("Obs", "Lev+5FU"))
os$rx <- droplevels(os$rx)
adtte <- data.frame(
  AVAL = os$time, CNSR = 1 - os$status,
  TRT01P = factor(os$rx, levels = c("Obs", "Lev+5FU"),
                  labels = c("Observation", "Lev+5FU")),
  STRAT = factor(ifelse(os$differ == 3, "Poor", "Well/Moderate")))
adtte <- adtte[!is.na(adtte$STRAT), ]
adtte$event <- 1 - adtte$CNSR

cox <- coxph(Surv(AVAL, event) ~ TRT01P + strata(STRAT), data = adtte)
cox.zph(cox)
       chisq df    p
TRT01P 0.912  1 0.34
GLOBAL 0.912  1 0.34

χ² = 0.91, p = 0.34 — aucune preuve que le hazard ratio change dans le temps. Le HR unique est une synthèse valide. (Quand ce test est significatif, le HR constant induit en erreur — voir tester l’hypothèse PH — et la sensibilité RMST ci-dessous devient la défense principale.)

Les tables de CSR — gtsummary

Un rapport d’étude clinique présente des tables propres, pas la sortie R brute. gtsummary transforme les mêmes objets ajustés en tables de style soumission. D’abord la probabilité de survie à un instant fixe par bras :

library(survival)
library(gtsummary)

os <- subset(colon, etype == 2 & rx %in% c("Obs", "Lev+5FU"))
os$rx <- droplevels(os$rx)
adtte <- data.frame(
  AVAL = os$time, CNSR = 1 - os$status,
  TRT01P = factor(os$rx, levels = c("Obs", "Lev+5FU"),
                  labels = c("Observation", "Lev+5FU")),
  STRAT = factor(ifelse(os$differ == 3, "Poor", "Well/Moderate")))
adtte <- adtte[!is.na(adtte$STRAT), ]
adtte$event <- 1 - adtte$CNSR

fit <- survfit(Surv(AVAL, event) ~ TRT01P, data = adtte)

# 5-year (1825-day) overall-survival probability per arm
tbl_survfit(fit, times = 1825,
            label_header = "**5-year overall survival (95% CI)**")
Characteristic 5-year overall survival (95% CI)
TRT01P
    Observation 52% (47%, 58%)
    Lev+5FU 63% (58%, 69%)

À 5 ans, 52 % du bras Observation contre 63 % de Lev+5FU sont encore en vie — l’avantage sous forme d’estimation à un instant fixe (utile précisément parce que la médiane traitée était « non atteinte »). Puis l’estimand primaire sous forme de tbl_regression :

library(survival)
library(gtsummary)

os <- subset(colon, etype == 2 & rx %in% c("Obs", "Lev+5FU"))
os$rx <- droplevels(os$rx)
adtte <- data.frame(
  AVAL = os$time, CNSR = 1 - os$status,
  TRT01P = factor(os$rx, levels = c("Obs", "Lev+5FU"),
                  labels = c("Observation", "Lev+5FU")),
  STRAT = factor(ifelse(os$differ == 3, "Poor", "Well/Moderate")))
adtte <- adtte[!is.na(adtte$STRAT), ]
adtte$event <- 1 - adtte$CNSR

cox <- coxph(Surv(AVAL, event) ~ TRT01P + strata(STRAT), data = adtte)
tbl_regression(cox, exponentiate = TRUE)   # exponentiate -> hazard ratios
Characteristic HR 95% CI p-value
TRT01P


    Observation
    Lev+5FU 0.70 0.55, 0.88 0.003
Abbreviations: CI = Confidence Interval, HR = Hazard Ratio

exponentiate = TRUE rapporte les hazard ratios avec l’IC à 95 % et le p — la ligne exacte pour la table d’efficacité du CSR : HR 0.70 (IC à 95 % 0.55, 0.88), p = 0.003, Observation comme référence. C’est l’analyse primaire, complète. Maintenant nous la défendons.

Partie 2 — l’ensemble d’analyses de sensibilité (la valeur ajoutée)

Le travail d’un relecteur est de trouver l’hypothèse qui, si elle est fausse, brise votre conclusion. Une analyse de sensibilité ré-exécute la comparaison sous une autre hypothèse ou mesure de synthèse. Le livrable n’est pas trois p-values de plus — c’est une affirmation sur la robustesse : les analyses alternatives convergent-elles avec le HR primaire, ou l’une diverge-t-elle et vous dit-elle quelque chose que le HR cachait ?

NotePrimaire vs sensibilité — les rôles, fixés à l’avance
  • Analyse primaire — l’unique estimation pré-spécifiée sur laquelle repose la conclusion de l’essai (ici le HR de Cox stratifié). Elle définit l’estimand et est fixée dans le SAP avant la levée de l’insu.
  • Analyses de sensibilité — des alternatives pré-spécifiées qui mettent à l’épreuve une hypothèse précise de la primaire : une mesure de synthèse différente (RMST, sans hypothèse PH), une structure d’événement différente (risques concurrents), une tranche de population différente (sous-groupes). Elles soutiennent ou nuancent la primaire ; elles ne la remplacent pas, et vous ne choisissez pas après coup celle dont le p est le plus petit.

Sensibilité (a) — RMST, un effet interprétable et sans hypothèse PH

Le hazard ratio suppose des risques proportionnels et rapporte un effet relatif. Le temps de survie moyen restreint ne suppose ni l’un ni l’autre : c’est l’aire sous la courbe de survie jusqu’à un horizon τ — le temps de survie moyen au cours des premières τ unités — donc l’effet est un nombre absolu de jours, valide même si PH échoue. Exécutez-le à un horizon pré-spécifié de 5 ans avec survRM2::rmst2() (le bras codé 0/1) :

library(survival)
library(survRM2)

os <- subset(colon, etype == 2 & rx %in% c("Obs", "Lev+5FU"))
os$rx <- droplevels(os$rx)
adtte <- data.frame(
  AVAL = os$time, CNSR = 1 - os$status,
  TRT01P = factor(os$rx, levels = c("Obs", "Lev+5FU"),
                  labels = c("Observation", "Lev+5FU")))
adtte$event <- 1 - adtte$CNSR

arm <- ifelse(adtte$TRT01P == "Lev+5FU", 1, 0)   # rmst2 needs a 0/1 arm
res <- rmst2(adtte$AVAL, adtte$event, arm, tau = 1825)
res$unadjusted.result
                            Est.  lower .95   upper .95          p
RMST (arm=1)-(arm=0) 111.3315563 19.2504063 203.4127063 0.01780193
RMST (arm=1)/(arm=0)   1.0831733  1.0137502   1.1573507 0.01807623
RMTL (arm=1)/(arm=0)   0.7711351  0.6195828   0.9597578 0.01991902

Différence de RMST = 111 jours (IC à 95 % 18–204), p = 0.020. Sur 5 ans, les patients traités vivent en moyenne environ 111 jours — soit à peu près 3.7 mois — de plus (ratio de RMST 1.08). Cela converge avec le HR primaire : une mesure de synthèse différente et sans hypothèse pointe dans le même sens, et le bénéfice est désormais exprimé en un temps qu’un patient comprend. Comme la vérification PH a réussi, le RMST est ici une sensibilité confirmatoire ; si PH avait échoué, il aurait été l’analyse que vous auriez mise en avant.

Sensibilité (b) — une vue en risques concurrents

L’analyse OS traite tout non-décès comme une censure. Mais dans cet essai, un patient peut aussi avoir une récidive du cancer avant de décéder, et le décès sans récidive est un événement concurrent pour la récidive : un patient qui décède d’abord ne pourra jamais être observé en récidive. Lire le risque de récidive sur une naïve courbe 1 − KM le surestimerait. La synthèse honnête est la fonction d’incidence cumulée (CIF), qui tient compte de l’événement concurrent. Construisez la variable d’événement en risques concurrents (un facteur dont le niveau de censure est en premier) et estimez la CIF par bras avec tidycmprsk::cuminc() :

library(survival)
library(tidycmprsk)

# Per subject, merge the recurrence (etype==1) and death (etype==2) records
co  <- subset(colon, rx %in% c("Obs", "Lev+5FU"))
rec <- co[co$etype == 1, c("id", "time", "status", "rx", "differ")]
dth <- co[co$etype == 2, c("id", "time", "status")]
names(rec) <- c("id", "rtime", "rstat", "rx", "differ")
names(dth) <- c("id", "dtime", "dstat")
cr <- merge(rec, dth, by = "id")

# One event time and one competing-risks event code
cr$etime <- pmin(cr$rtime, cr$dtime)
ecode <- with(cr, ifelse(rstat == 1, 1, ifelse(dstat == 1, 2, 0)))   # 1 = recurrence, 2 = death
cr$event <- factor(ecode, levels = c(0, 1, 2),
                   labels = c("censor", "recurrence", "death"))       # censor level FIRST
cr$TRT01P <- factor(cr$rx, levels = c("Obs", "Lev+5FU"),
                    labels = c("Observation", "Lev+5FU"))

table(cr$event)

    censor recurrence      death 
       295        296         28 

Soit 296 récidives, 28 décès sans récidive (l’événement concurrent) et 295 censurés. Maintenant la CIF de récidive par bras, avec le test de Gray :

library(survival)
library(tidycmprsk)

co  <- subset(colon, rx %in% c("Obs", "Lev+5FU"))
rec <- co[co$etype == 1, c("id", "time", "status", "rx")]
dth <- co[co$etype == 2, c("id", "time", "status")]
names(rec) <- c("id", "rtime", "rstat", "rx")
names(dth) <- c("id", "dtime", "dstat")
cr <- merge(rec, dth, by = "id")
cr$etime <- pmin(cr$rtime, cr$dtime)
ecode <- with(cr, ifelse(rstat == 1, 1, ifelse(dstat == 1, 2, 0)))
cr$event <- factor(ecode, levels = c(0, 1, 2),
                   labels = c("censor", "recurrence", "death"))
cr$TRT01P <- factor(cr$rx, levels = c("Obs", "Lev+5FU"),
                    labels = c("Observation", "Lev+5FU"))

ci <- cuminc(Surv(etime, event) ~ TRT01P, data = cr)
ci
strata        time    n.risk   estimate   std.error   95% CI          
Observation   500     203      0.006      0.004       0.001, 0.021    
Observation   1,000   161      0.019      0.008       0.008, 0.039    
Observation   1,500   139      0.029      0.009       0.014, 0.052    
Observation   2,000   115      0.032      0.010       0.016, 0.056    
Observation   2,500   40       0.044      0.013       0.023, 0.074    
Observation   3,000   5        0.075      0.034       0.026, 0.157    
Lev+5FU       500     231      0.016      0.007       0.006, 0.036    
Lev+5FU       1,000   198      0.023      0.009       0.010, 0.045    
Lev+5FU       1,500   184      0.023      0.009       0.010, 0.045    
Lev+5FU       2,000   158      0.037      0.011       0.019, 0.062    
Lev+5FU       2,500   62       0.051      0.014       0.029, 0.083    
Lev+5FU       3,000   7        0.068      0.022       0.034, 0.118    
strata        time    n.risk   estimate   std.error   95% CI          
Observation   500     203      0.346      0.027       0.294, 0.399    
Observation   1,000   161      0.467      0.028       0.411, 0.521    
Observation   1,500   139      0.528      0.028       0.471, 0.582    
Observation   2,000   115      0.547      0.028       0.491, 0.601    
Observation   2,500   40       0.566      0.029       0.508, 0.620    
Observation   3,000   5        0.584      0.033       0.517, 0.645    
Lev+5FU       500     231      0.224      0.024       0.179, 0.272    
Lev+5FU       1,000   198      0.326      0.027       0.274, 0.379    
Lev+5FU       1,500   184      0.362      0.028       0.308, 0.416    
Lev+5FU       2,000   158      0.382      0.028       0.327, 0.437    
Lev+5FU       2,500   62       0.394      0.028       0.338, 0.449    
Lev+5FU       3,000   7        0.394      0.028       0.338, 0.449    
outcome      statistic   df     p.value    
recurrence   19.4        1.00   <0.001     
death        0.146       1.00   0.70       
tidy(ci, times = 1825) |>
  subset(outcome == "recurrence",
         select = c(strata, time, estimate, conf.low, conf.high))
# A tibble: 2 × 5
  strata       time estimate conf.low conf.high
  <fct>       <dbl>    <dbl>    <dbl>     <dbl>
1 Observation  1825    0.544    0.487     0.597
2 Lev+5FU      1825    0.379    0.324     0.433

Lisez le résultat en risques concurrents :

  • CIF de récidive à 5 ans : 55 % (Observation) vs 38 % (Lev+5FU) ; test de Gray p < 0.001. Le traitement réduit fortement l’incidence cumulée de récidive, en tenant compte du risque concurrent de décès — un bénéfice large et significatif sur le critère de récidive.
  • Décès (événement concurrent) : Gray p = 0.70 — la mortalité concurrente des deux bras est similaire, donc la comparaison de récidive n’est pas faussée par un risque concurrent différentiel.

C’est exactement ce qu’une analyse de sensibilité doit faire : elle converge en direction avec le bénéfice OS et ajoute une preuve que l’analyse OS ne peut pas apporter — que l’effet du médicament apparaît clairement aussi sur la récidive de la maladie, en tenant correctement compte de la mortalité concurrente.

Sensibilité (c) — une vérification de cohérence en sous-groupes / stratifiée

La dernière sensibilité demande si l’effet primaire est cohérent dans toute la population ou tiré par une seule tranche. Estimez le HR au sein d’un sous-groupe cliniquement pertinent (nombre de ganglions lymphatiques positifs, un fort facteur pronostique) et testez l’interaction traitement × sous-groupe — une interaction significative signifierait que l’effet diffère selon le sous-groupe :

library(survival)

os <- subset(colon, etype == 2 & rx %in% c("Obs", "Lev+5FU"))
os$rx <- droplevels(os$rx)
adtte <- data.frame(
  AVAL = os$time, CNSR = 1 - os$status,
  TRT01P = factor(os$rx, levels = c("Obs", "Lev+5FU"),
                  labels = c("Observation", "Lev+5FU")),
  STRAT  = factor(ifelse(os$differ == 3, "Poor", "Well/Moderate")),
  NODE4  = factor(ifelse(os$node4 == 1, "4+ positive nodes", "<4 positive nodes")))
adtte <- adtte[!is.na(adtte$STRAT), ]
adtte$event <- 1 - adtte$CNSR

# HR within each nodal subgroup
for (lv in levels(adtte$NODE4)) {
  sub <- adtte[adtte$NODE4 == lv, ]
  s <- summary(coxph(Surv(AVAL, event) ~ TRT01P, data = sub))
  cat(sprintf("%-18s n=%d, events=%d: HR = %.2f (95%% CI %.2f-%.2f), p = %.3f\n",
              lv, nrow(sub), sum(sub$event),
              s$conf.int[1], s$conf.int[3], s$conf.int[4], s$coefficients[5]))
}
<4 positive nodes  n=441, events=174: HR = 0.68 (95% CI 0.50-0.91), p = 0.011
4+ positive nodes  n=165, events=113: HR = 0.72 (95% CI 0.50-1.05), p = 0.086
# Treatment x subgroup interaction test (the formal check)
ix <- coxph(Surv(AVAL, event) ~ TRT01P * NODE4 + strata(STRAT), data = adtte)
anova(ix)
Analysis of Deviance Table
 Cox model: response is Surv(AVAL, event)
Terms added sequentially (first to last)

              loglik   Chisq Df Pr(>|Chi|)    
NULL         -1590.5                          
TRT01P       -1586.0  9.0837  1   0.002579 ** 
NODE4        -1563.3 45.2877  1  1.701e-11 ***
TRT01P:NODE4 -1563.3  0.0090  1   0.924500    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Lisez le résultat en sous-groupes :

  • <4 ganglions positifs : HR = 0.68 (IC à 95 % 0.50–0.91, p = 0.011) ; 4+ ganglions : HR = 0.72 (IC à 95 % 0.50–1.05, p = 0.086). Les estimations ponctuelles sont presque identiques ; l’IC plus large dans le groupe 4+ plus petit et plus à risque est une question de puissance, pas un effet différent.
  • Interaction p = 0.92 — aucune preuve que l’effet du traitement diffère selon le statut ganglionnaire. Le bénéfice est cohérent à travers les sous-groupes.

Trois sensibilités, toutes convergeant vers la primaire : un effet absolu sans hypothèse PH (RMST), une vue en risques concurrents sans hypothèse, et un effet cohérent en sous-groupes. La conclusion est robuste.

AstuceL’ensemble de sensibilité en un coup d’œil
Analyse Question qu’elle met à l’épreuve Résultat Verdict
Primaire — Cox stratifié L’effet relatif pré-spécifié HR 0.70 (0.55–0.88), p = 0.003 Bénéfice
RMST (τ = 5 ans) Abandonner l’hypothèse PH ; effet absolu +111 jours (18–204), p = 0.020 Converge
Risques concurrents (CIF de récidive) Tenir compte de l’événement concurrent 38 % vs 55 % à 5 ans, Gray p < 0.001 Converge
Sous-groupe (statut ganglionnaire) Cohérence dans la population HR 0.68 vs 0.72, interaction p = 0.92 Cohérent

Assembler le package — le rapport réglementaire

Le livrable réunit la primaire et les sensibilités en un paragraphe qui énonce l’estimand, le résultat primaire, et la cohérence de l’ensemble de sensibilité :

La survie globale a été analysée sur l’ensemble d’analyse complet du jeu de données ADTTE (PARAMCD = OS ; n = 606, 287 décès), comparant Lev+5FU à l’observation. L’analyse primaire pré-spécifiée — un modèle de Cox stratifié (facteur de stratification : différenciation tumorale) — a donné un hazard ratio de 0.70 (IC à 95 % 0.55–0.88) en faveur de Lev+5FU (log-rank stratifié p = 0.003) ; l’hypothèse des risques proportionnels était satisfaite (p = 0.34). La survie globale médiane était de 2083 jours (IC à 95 % 1548–2789) sous observation et n’a pas été atteinte sous Lev+5FU ; la survie à 5 ans était de 52 % contre 63 %. Les analyses de sensibilité pré-spécifiées étaient cohérentes avec le résultat primaire : le temps de survie moyen restreint sur un horizon de 5 ans favorisait le traitement de 111 jours (IC à 95 % 18–204, p = 0.020) ; l’incidence cumulée de récidive du cancer, en tenant compte du risque concurrent de décès, était plus faible sous traitement (38 % vs 55 % à 5 ans, Gray p < 0.001) avec une mortalité concurrente similaire entre les bras (p = 0.70) ; et l’effet du traitement était cohérent à travers les sous-groupes ganglionnaires (HR 0.68 pour <4 ganglions, 0.72 pour 4+ ; interaction p = 0.92). L’estimand, la population d’analyse, les règles de censure, le facteur de stratification, et les rôles primaire-vs-sensibilité ont été définis dans le plan d’analyse statistique, selon ICH E9(R1), avant la levée de l’insu.

NoteICH E9(R1) — ce qui rend ce package « apte à la soumission »

Les statistiques ci-dessus ne sont réglementaires que si quelques éléments ont été réglés avant l’analyse. Vous n’avez pas besoin d’un manuel de conformité — vous devez honorer ce qui a été pré-spécifié :

  • Un estimand défini. Population (le FAS/ITT), critère (PARAMCD = OS, temps de la randomisation au décès toutes causes), condition de traitement (TRT01P), stratégie pour les événements intercurrents, et mesure de synthèse (le hazard ratio). Le HR est rapporté pour cet estimand, pas comme une association générique.
  • Rôles primaire vs sensibilité, fixés à l’avance. L’estimation primaire et chaque sensibilité (RMST, risques concurrents, sous-groupe) sont pré-spécifiées dans le SAP — vous ne promouvez pas une sensibilité au rang de primaire, ni ne sélectionnez le τ / sous-groupe / la méthode au plus petit p, après avoir vu les données.
  • Règles de censure documentées. CNSR, EVNTDESC et CNSDTDESC rendent le statut de chaque sujet auditable — événement ou censure, à quelle date et pourquoi.
  • Facteurs de stratification nommés entrent dans le log-rank stratifié et le Cox, de sorte que l’effet du traitement soit estimé au sein des strates pronostiques.
  • Traçabilité. Chaque chiffre se trace SDTM → ADaM (ADTTE) → sortie d’analyse, de façon reproductible.
NoteQuand ce package doit être audité et validé — Arkon

Tout sur cette page est la statistique d’un package d’analyse réglementaire — l’estimand primaire et un ensemble de sensibilité défendable, de façon reproductible. Le transformer en un véritable livrable de soumission ajoute une couche de production : des analyses double-programmées validées par rapport au SAP, une traçabilité complète SDTM → ADaM → TLF, le contrôle de version, et une piste d’audit documentée qu’un régulateur peut suivre de bout en bout. Cette discipline de production — des pipelines d’analyse clinique reproductibles, validés et assistés par agent — est ce pour quoi Arkon est conçu. Utilisez cette leçon pour construire le package d’analyse ; tournez-vous vers Arkon quand il doit être audité et validé pour un dépôt réel.

Problèmes courants

Vous avez promu une analyse de sensibilité au rang de primaire parce que son p était plus petit. L’analyse primaire est fixée dans le SAP avant la levée de l’insu ; une sensibilité qui se trouve être plus significative ne devient pas le résultat phare. Rapportez la primaire pré-spécifiée comme conclusion et les sensibilités comme soutien. Échanger les rôles a posteriori est l’erreur d’analyse de sensibilité la plus fréquente — et la plus pénalisée.

Votre facteur d’événement en risques concurrents n’a pas été construit correctement. cuminc() et crr() ont besoin de l’événement comme facteur dont le premier niveau est le code de censure. Un événement numérique, ou un facteur dont le niveau de censure n’est pas en premier, échoue ou renvoie n’importe quoi. Construisez factor(code, levels = c(0, 1, 2), labels = c("censor", ...)) explicitement, et confirmez les comptes avec table().

Vous avez lu le risque de récidive sur 1 − KM au lieu de la CIF. Avec un vrai événement concurrent (décès avant récidive), le complément de Kaplan-Meier surestime l’incidence cumulée — il crédite les patients qui sont décédés d’une chance future de récidiver. Utilisez cuminc() pour tout critère ayant un événement concurrent ; réservez 1 − KM à la survie globale, où le décès est l’événement.

Vous avez fixé l’horizon τ du RMST après avoir vu le résultat. τ définit l’estimand et change la réponse (la leçon RMST montre les mêmes données donnant p = 0.02 à 5 ans et p = 0.18 à 3). Pré-spécifiez τ à partir de la question clinique ; rapporter le τ qui minimise le p relève de la pêche aux résultats.

Questions fréquentes

Une analyse de temps jusqu’à l’événement apte à la soumission est un package, pas une statistique unique : une analyse primaire pré-spécifiée (généralement Kaplan-Meier avec médiane et IC à 95 %, le test du log-rank stratifié, et le hazard ratio de Cox avec une vérification des risques proportionnels), les sorties de soumission (la figure KM avec une table des sujets à risque et des tables de style CSR, par ex. via gtsummary), et un ensemble d’analyses de sensibilité qui montre que le résultat est robuste (communément RMST, une vue en risques concurrents, et la cohérence en sous-groupes). Il est enveloppé dans un encadrement par l’estimand ICH E9(R1) — population définie, critère, règles de censure, facteurs de stratification — le tout fixé dans le SAP avant la levée de l’insu.

L’analyse primaire est l’unique estimation pré-spécifiée sur laquelle repose la conclusion de l’essai (par ex. le hazard ratio de Cox stratifié) ; elle définit l’estimand et est fixée avant la levée de l’insu. Une analyse de sensibilité ré-exécute la comparaison sous une autre hypothèse ou mesure de synthèse — RMST (sans hypothèse de risques proportionnels), risques concurrents (une structure d’événement différente), sous-groupes (une tranche de population différente) — pour tester si la conclusion primaire tient. Les sensibilités soutiennent ou nuancent la primaire ; elles ne la remplacent jamais, et vous ne promouvez pas celle au plus petit p après avoir vu les données.

Chacun met à l’épreuve une hypothèse précise du HR de Cox primaire. Le RMST abandonne l’hypothèse des risques proportionnels et rapporte un effet absolu (jours/mois gagnés), donc il est valide même si PH échoue et est interprétable pour les cliniciens et les patients. Les risques concurrents tiennent compte d’un événement (souvent le décès) qui empêche l’événement d’intérêt, ce qu’une analyse 1 − Kaplan-Meier naïve ignore — de sorte que l’incidence cumulée est estimée honnêtement. Si les deux convergent avec le HR primaire, la conclusion est robuste ; si l’un diverge, il a révélé quelque chose que le HR unique cachait.

La p-value d’interaction demande si l’effet du traitement diffère à travers le sous-groupe. Un grand p (ici 0.92) signifie aucune preuve que l’effet diffère — le bénéfice est cohérent, et les hazard ratios par sous-groupe (0.68 et 0.72) ne diffèrent que par hasard. Un petit p d’interaction suggérerait une véritable modification d’effet à investiguer. Les analyses en sous-groupes sont des vérifications de sensibilité/cohérence : lisez le test d’interaction, pas seulement les p-values par sous-groupe, qui sont individuellement sous-puissantes.

Non — la statistique de survie est la même sur n’importe quelle donnée de temps jusqu’à l’événement. ADTTE est le standard CDISC ADaM qui rend l’analyse prête à la soumission : un enregistrement par sujet par paramètre, des noms de variables fixes (AVAL, CNSR, TRT01P), et une censure documentée — de sorte qu’un relecteur peut la ré-exécuter et obtenir vos chiffres. Pour le mapping d’ADTTE vers Surv() (et le retournement de censure CNSR = 1), voir la leçon CDISC ADTTE ; ce capstone applique ce mapping et ajoute l’ensemble complet d’analyses primaire et de sensibilité.

Testez vos connaissances

  1. Assemblez-le. Vous avez un critère OS ADTTE avec un HR de Cox primaire significatif de 0.75 et une vérification PH réussie. Nommez trois analyses de sensibilité pré-spécifiées que vous ajouteriez, et énoncez l’hypothèse précise que chacune met à l’épreuve. Quel résultat unique à travers l’ensemble vous rendrait le moins confiant dans la primaire ?
  2. Exécutez une sensibilité. Ci-dessous, calculez la sensibilité RMST à un horizon de 5 ans sur les données ADTTE et confirmez qu’elle converge avec le HR primaire. Remplissez le blanc avec l’horizon en jours.

Pour (1) : les trois que vous avez construites ici sont le RMST (abandonne l’hypothèse PH / effet absolu), les risques concurrents (tiennent compte d’un événement concurrent), et les sous-groupes (cohérence dans la population). Le résultat le moins rassurant est une sensibilité dont la direction diverge de celle de la primaire. Pour (2) : un horizon de 5 ans vaut 1825 jours ; lisez la première ligne de res$unadjusted.resultEst. est la différence de RMST en jours et la dernière colonne est son p.

library(survival)
library(survRM2)

# Build the ADTTE OS data (death records), event = 1 - CNSR, arm coded 0/1
os <- subset(colon, etype == 2 & rx %in% c("Obs", "Lev+5FU"))
os$rx <- droplevels(os$rx)
adtte <- data.frame(
  AVAL = os$time, CNSR = 1 - os$status,
  TRT01P = factor(os$rx, levels = c("Obs", "Lev+5FU"),
                  labels = c("Observation", "Lev+5FU")))
adtte$event <- 1 - adtte$CNSR
arm <- ifelse(adtte$TRT01P == "Lev+5FU", 1, 0)

# RMST sensitivity at the prespecified 5-year horizon
res <- rmst2(adtte$AVAL, adtte$event, arm, tau = 1825)
res$unadjusted.result
#> RMST (arm=1)-(arm=0)  Est. = 111 days, 95% CI 18-204, p = 0.020 — CONVERGES with HR 0.70.

Pour la question 1 : ajoutez le RMST (met à l’épreuve l’hypothèse des risques proportionnels et donne un effet absolu), une analyse d’incidence cumulée en risques concurrents (met à l’épreuve l’hypothèse selon laquelle les événements concurrents ne sont que de la censure), et une analyse en sous-groupes / d’interaction (met à l’épreuve la cohérence dans la population). Vous seriez le moins confiant dans la primaire si l’une d’elles divergeait en direction — par ex. une différence de RMST proche de zéro ou une CIF en risques concurrents ne montrant aucun bénéfice — car cela signale que le HR unique est sensible à une hypothèse dont il dépend. La convergence à travers l’ensemble est ce qui rend la primaire défendable.

Pour la question 2 : la différence de RMST est de 111 jours (IC à 95 % 18–204, p = 0.020) en faveur du traitement — elle converge avec le HR primaire de 0.70, confirmant le bénéfice sous une synthèse absolue et sans hypothèse PH.

AstuceVérification rapide

Dans votre package d’analyse, une analyse de sensibilité RMST pré-spécifiée renvoie une p-value plus petite que le modèle de Cox primaire. Devriez-vous rapporter le résultat RMST comme conclusion primaire ?

Non. L’analyse primaire est fixée dans le SAP avant la levée de l’insu ; une analyse de sensibilité qui se trouve être plus significative ne devient pas le résultat phare. Rapportez la primaire pré-spécifiée (le HR de Cox) comme conclusion et le RMST comme preuve de soutien de la robustesse. Promouvoir une sensibilité au rang de primaire après avoir vu les résultats gonfle le taux d’erreur de type I et constitue une erreur réglementaire grave — et pénalisée. La valeur de la sensibilité est qu’elle converge avec la primaire, pas qu’elle peut la remplacer.

Conclusion

Un livrable réglementaire de temps jusqu’à l’événement est un package : une analyse primaire pré-spécifiée qui définit l’estimand, plus un ensemble de sensibilité qui prouve sa robustesse. Sur l’essai de cancer du côlon de forme CDISC, le modèle de Cox stratifié primaire a donné HR = 0.70 (IC à 95 % 0.55–0.88, p = 0.003) avec des risques proportionnels tenant, la figure de soumission et les tables de CSR gtsummary à côté. L’ensemble de sensibilité a convergé : le RMST a montré +111 jours sur 5 ans (p = 0.020) comme effet absolu et sans hypothèse PH ; la vue en risques concurrents a montré une incidence cumulée de récidive plus faible sous traitement (Gray p < 0.001) une fois le risque concurrent de décès pris en compte ; et l’effet était cohérent à travers les sous-groupes ganglionnaires (interaction p = 0.92). Cette convergence — la même conclusion sous une synthèse différente, une structure d’événement différente, et une tranche de population différente — est ce qui rend le résultat défendable. Enveloppez-le dans l’encadrement par l’estimand ICH E9(R1), fixez les rôles primaire-vs-sensibilité dans le SAP avant la levée de l’insu, et vous avez le package d’analyse qu’un rapport d’étude clinique exige.

Leçons connexes

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

Prouvez que vous savez le faire. Maîtrisez toute la série Analyse de survie en R — suivez votre parcours, construisez des projets et obtenez un certificat.

Commencer gratuitement →

Passez à Pro — Prova illimité sur vos propres données et un certificat vérifiable qui atteste la compétence.

dès 15 $/mois facturé annuellement

Passer à Pro →

✓ Vous êtes Pro — continuez. The runtime is the judge.

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
🟢 Avec un agent IA

Demandez à Prova « construis un package complet d’analyse de survie réglementaire sur mes données CDISC ADTTE — le HR de Cox stratifié primaire avec KM, médiane et une table gtsummary, puis une analyse de sensibilité RMST, en risques concurrents et en sous-groupes, encadrée par ICH E9(R1) » — elle répond avec du code survival + survRM2 + tidycmprsk + gtsummary que vous pouvez exécuter sur vos propres données. The runtime is the judge. Demander à Prova →

Note

Chaque résultat de cette page a été produit par le code montré, exécuté au moment du build dans un environnement R figé — copiez n’importe quel bloc et exécutez-le pour les reproduire.

Réutilisation

Citation

BibTeX
@online{2026,
  author = {},
  title = {Exemple d’analyse ADTTE en R : un package de survie
    réglementaire complet},
  date = {2026-06-26},
  url = {https://www.datanovia.com/learn/biostatistics/survival-analysis/capstone-adtte-regulatory},
  langid = {fr}
}
Veuillez citer ce travail comme suit :
“Exemple d’analyse ADTTE en R : un package de survie réglementaire complet.” 2026. June 26. https://www.datanovia.com/learn/biostatistics/survival-analysis/capstone-adtte-regulatory.