Skip to content

ADTTE, time to event

A time-to-event analysis asks how long until something happens. Some subjects have the event. The others leave the study first. For them, you know only that the event did not happen up to a date. They are censored at that date. ADTTE stores both in one record per subject and parameter:

VariableMeaning
STARTDTthe start of the clock, here the first dose
ADTthe date of the event, or the censoring date
AVALthe time in days: adt - startdt + 1
CNSR0 = the event happened, 1 = censored
EVNTDESC, SRCDOM, SRCVAR, SRCSEQwhich record gave the date, for traceability

The pilot study has one parameter: TTDE, the time to the first dermatologic event. The event is the first treatment emergent adverse event in the customized query CQ01. A customized query is a list of terms that the specification defines; here, the dermatologic terms. ADAE carries the query name in CQ01NAM. A subject without such an event is censored at the end of the study, RFENDT.

The program reads ADSL and ADAE from the reference library refadm, not from your work of Chapters 9 and 11: your ADAE has no CQ01NAM.

12_1_adtte.sas
/* Chapter 12: ADTTE by hand: time to the first dermatologic event */
/* Step 010: the event: the first treatment emergent AE of the dermatologic query (CQ01) */
PROC SORT DATA=refadm.adae(WHERE=(trtemfl = "Y" AND NOT MISSING(cq01nam))
KEEP=usubjid aeseq astdt trtemfl cq01nam)
OUT=derm;
BY usubjid astdt aeseq;
RUN;
DATA first_derm;
SET derm;
BY usubjid;
IF FIRST.usubjid;
KEEP usubjid astdt aeseq;
RUN;
/* Step 020: one record per subject: the event, or censoring at the end of study */
DATA adtte;
LENGTH paramcd $8 param $40 evntdesc $40 srcdom $8 srcvar $8;
MERGE refadm.adsl(IN=inadsl KEEP=usubjid trtsdt rfendt saffl WHERE=(saffl = "Y"))
first_derm(IN=inev);
BY usubjid;
IF inadsl;
paramcd = "TTDE";
param = "Time to First Dermatologic Event";
startdt = trtsdt;
IF inev THEN DO;
adt = astdt;
cnsr = 0;
evntdesc = "Dermatologic event";
srcdom = "ADAE";
srcvar = "ASTDT";
srcseq = aeseq;
END;
ELSE DO;
adt = rfendt;
cnsr = 1;
evntdesc = "Study completion date";
srcdom = "ADSL";
srcvar = "RFENDT";
END;
/* the first day counts: an event on the day of the first dose is day 1 */
aval = adt - startdt + 1;
FORMAT startdt adt DATE9.;
KEEP usubjid paramcd param startdt adt aval cnsr evntdesc srcdom srcvar srcseq;
RUN;
PROC FREQ DATA=adtte; TABLES cnsr*evntdesc / LIST NOCUM; RUN;
/* Compare with the reference ADTTE */
PROC SORT DATA=refadm.adtte OUT=ref_tte; BY usubjid; RUN;
PROC COMPARE BASE=ref_tte COMPARE=adtte CRITERION=1E-8 NOSUMMARY;
ID usubjid;
VAR startdt adt aval cnsr srcseq;
RUN;
%PUT NOTE: PROC COMPARE result SYSINFO=&SYSINFO. (0 means equal);

What you should see:

cnsrevntdescFrequencyPercent
0Dermatologic event15259.84
1Study completion date10240.16

and NOTE: PROC COMPARE result SYSINFO=0 (0 means equal): for all 254 subjects of the safety population, STARTDT, ADT, AVAL, CNSR and SRCSEQ are the same as in the reference ADTTE.

Three rules carry this program:

  • Every subject gets exactly one record. The MERGE starts from ADSL (IF inadsl;), not from the events. A subject without an event must not disappear: that subject is the censored one.
  • The first event wins. first_derm sorts the events by date and keeps FIRST.usubjid. A second rash later does not change the time to the first one.
  • The first day counts. An event on the day of the first dose has AVAL = 1, not 0. Subject 01-701-1015 started on 02JAN2014 and had the event on 03JAN2014: AVAL = 2.

The reference ADTTE writes “Dematologic Event Occured” in EVNTDESC, with two typing errors. This program does not compare EVNTDESC, and it writes its own text. A reference data set is a check, not a template to copy, typos included.

Stopped in the ELSE branch for subject 01-701-1033 (no dermatologic event)

Stopped in the ELSE branch for subject 01-701-1033 (no dermatologic event): cnsr = 1 and adt = 14APR2014, the end of the study. With %pry(when=usubjid = “01-701-1033”) in the same place you see the same values in the Debug Console.

Exercise 12-1. What happens to AVAL for a subject whose event date is missing (a partial date that was not imputed)? Write a check that finds such subjects in the event source. How many does the pilot have?

12.3 The next parameter is one call: %m_tte

Section titled “12.3 The next parameter is one call: %m_tte”

A real study has more than one time-to-event parameter: time to death, time to the first serious event, time to discontinuation. Each one is the program above with other sources. The rules are always the same: one record per subject, the first event wins, a subject without an event is censored, the first day counts. Write the rules once, in a macro, and describe each parameter by its sources.

The design is the important part. The macro does not know what a dermatologic event is. Each source is a small data set that you build with a plain DATA step, with the same six variables: usubjid, adt, evntdesc, srcdom, srcvar, srcseq. You can read in the program what counts as an event; the macro only combines.

12_2_adtte_m_tte.sas
/* Chapter 12: a study macro for time-to-event parameters, %m_tte (needs ADTTE from 12_1) */
/*******************************************************************************
Macro : %m_tte
Version : 1.0
Purpose : One time-to-event record per subject: the first event, or
censoring at the latest censoring date.
Parameters :
data= the subjects, one record each: usubjid and the start date (required)
out= output data set (required)
paramcd= the parameter code, e.g. TTDE (required)
param= the parameter name (required)
startdt= the start of the clock in &data. (default trtsdt)
events= one or more event source data sets (required)
censors= one or more censoring source data sets (required)
Every source has usubjid adt evntdesc srcdom srcvar srcseq.
Preconditions : adt is a SAS date in every source
Postconditions : &out. has one record per subject of &data. with paramcd
param startdt adt aval cnsr evntdesc srcdom srcvar srcseq
Change log :
2026-11-01 1.0 first version
*******************************************************************************/
%MACRO m_tte(data=, out=, paramcd=, param=, startdt=trtsdt, events=, censors=);
%LOCAL macro p par i ds;
%LET macro = M_TTE;
%DO p = 1 %TO 6;
%LET par = %SCAN(data out paramcd param events censors, &p.);
%IF %SUPERQ(&par.) = %THEN %DO;
%PUT %STR(ERR)OR: [&macro.] the parameter &par.= is required.;
%RETURN;
%END;
%END;
%DO i = 1 %TO %SYSFUNC(COUNTW(&data. &events. &censors., %STR( )));
%LET ds = %SCAN(&data. &events. &censors., &i., %STR( ));
%IF NOT %SYSFUNC(EXIST(&ds.)) %THEN %DO;
%PUT %STR(ERR)OR: [&macro.] the data set &ds. does not exist.;
%RETURN;
%END;
%END;
/* Step 010: all events; the first one per subject wins */
DATA _mtt_010;
SET &events.;
KEEP usubjid adt evntdesc srcdom srcvar srcseq;
RUN;
PROC SORT DATA=_mtt_010; BY usubjid adt srcseq; RUN;
DATA _mtt_011;
SET _mtt_010;
BY usubjid;
IF FIRST.usubjid;
cnsr = 0;
RUN;
/* Step 020: all censoring dates; the latest one per subject wins */
DATA _mtt_020;
SET &censors.;
KEEP usubjid adt evntdesc srcdom srcvar srcseq;
RUN;
PROC SORT DATA=_mtt_020; BY usubjid adt; RUN;
DATA _mtt_021;
SET _mtt_020;
BY usubjid;
IF LAST.usubjid;
cnsr = 1;
RUN;
/* Step 030: one record per subject: an event if there is one, else censored */
PROC SORT DATA=&data. OUT=_mtt_030(KEEP=usubjid &startdt.); BY usubjid; RUN;
DATA &out.;
LENGTH paramcd $8 param $40;
MERGE _mtt_030(IN=insubj) _mtt_021 _mtt_011;
BY usubjid;
IF insubj;
paramcd = "&paramcd.";
param = "&param.";
startdt = &startdt.;
/* the first day counts: an event on the day of the first dose is day 1 */
IF NOT MISSING(adt) THEN aval = adt - startdt + 1;
FORMAT startdt adt DATE9.;
KEEP usubjid paramcd param startdt adt aval cnsr evntdesc srcdom srcvar srcseq;
RUN;
PROC DATASETS LIBRARY=work NOLIST NOWARN;
DELETE _mtt_:;
RUN; QUIT;
%MEND m_tte;
/* Step 010: what an event and a censoring are, in plain DATA steps */
DATA subj;
SET refadm.adsl(KEEP=usubjid trtsdt rfendt saffl WHERE=(saffl = "Y"));
RUN;
DATA ev_derm;
LENGTH evntdesc $40 srcdom srcvar $8;
SET refadm.adae(KEEP=usubjid aeseq astdt trtemfl cq01nam
WHERE=(trtemfl = "Y" AND NOT MISSING(cq01nam)));
adt = astdt; evntdesc = "Dermatologic event"; srcdom = "ADAE"; srcvar = "ASTDT"; srcseq = aeseq;
RUN;
DATA cn_eos;
LENGTH evntdesc $40 srcdom srcvar $8;
SET subj;
adt = rfendt; evntdesc = "Study completion date"; srcdom = "ADSL"; srcvar = "RFENDT"; srcseq = .;
RUN;
/* Step 020: the parameter of 12_1, now one call */
%m_tte(data=subj, out=tte_derm, paramcd=TTDE, param=Time to First Dermatologic Event,
events=ev_derm, censors=cn_eos)
/* Test: the macro and the hand-written program of 12_1 must agree */
PROC COMPARE BASE=adtte COMPARE=tte_derm CRITERION=1E-8 NOSUMMARY;
ID usubjid;
VAR startdt adt aval cnsr evntdesc srcseq;
RUN;
%PUT NOTE: macro against 12_1: SYSINFO=&SYSINFO. (0 means equal);
/* Step 030: a second parameter is one more source and one more call */
DATA ev_ser;
LENGTH evntdesc $40 srcdom srcvar $8;
SET refadm.adae(KEEP=usubjid aeseq astdt trtemfl aeser
WHERE=(trtemfl = "Y" AND aeser = "Y"));
adt = astdt; evntdesc = "Serious adverse event"; srcdom = "ADAE"; srcvar = "ASTDT"; srcseq = aeseq;
RUN;
%m_tte(data=subj, out=tte_ser, paramcd=TTSAE, param=Time to First Serious Adverse Event,
events=ev_ser, censors=cn_eos)
DATA adtte_all;
SET tte_derm tte_ser;
RUN;
PROC FREQ DATA=adtte_all;
TABLES paramcd*cnsr / NOCOL NOPERCENT;
RUN;

Read it from the top:

  1. events= and censors= take a list of source data sets: SET &events.; reads all of them. A second source of events is one more name in the list, not a new parameter.
  2. The first event wins, the latest censoring wins. Step 010 sorts the events by date and keeps FIRST.usubjid; step 020 keeps the last censoring date. A subject is censored at the last date we know the subject was free of the event.
  3. The subjects come first. Step 030 merges the subjects with the censoring and then with the event records. When two data sets in a MERGE have the same variable, MERGE keeps the value from the data set named last. So a subject with an event gets the event’s date, CNSR = 0 and its description.

What you should see:

NOTE: macro against 12_1: SYSINFO=0 (0 means equal)

%m_tte gives the same STARTDT, ADT, AVAL, CNSR, EVNTDESC and SRCSEQ as the hand-written program, for all 254 subjects. Then the second parameter, time to the first serious adverse event, costs one source and one call:

paramcdcnsr = 0 (event)cnsr = 1 (censored)Total
TTDE152102254
TTSAE3251254

Only 3 subjects of the pilot had a treatment emergent serious adverse event. That is the moment to look at the data before you trust a number: Exercise 11-1 counts the same subjects from ADAE.

%pry in %m_tte, in the TTSAE call

%pry in %m_tte, in the TTSAE call: vt _mtt_011 and show list the 3 serious adverse events, of 01-709-1424, 01-718-1170 and 01-718-1371.

Exercise 12-2. Add death as a second event source of TTSAE. The pilot ADSL has DTHFL but no death date: take the date from DTHDTC in SDTM DM. How many events and censored subjects do you get now?

Exercise 12-3. %m_tte does not check that an event comes before the censoring date. Write a check after the call that finds records with cnsr = 0 and an event after RFENDT. How many does TTDE have?

Exercise 12-4. 12_1_adtte.sas reads the reference refadm.adae, because your ADAE of chapter 11 has no CQ01NAM. Close the chain: add CQ01NAM to your ADAE. Its value is DERMATOLOGIC EVENTS in two cases. The first case is a preferred term that contains APPLICATION, DERMATITIS, ERYTHEMA or BLISTER. The second case is the system organ class SKIN AND SUBCUTANEOUS TISSUE DISORDERS, except COLD SWEAT, HYPERHIDROSIS and ALOPECIA. Compare it with the reference, then run step 010 of 12_1_adtte.sas on your data. How many subjects have a dermatologic event?

The exercises are next to the program they change: 12-1 uses 12_1_adtte.sas, 12-2 and 12-3 use 12_2_adtte_m_tte.sas, 12-4 changes 12_1_adtte.sas and your ADAE of chapter 11. The answers are in Answers, Part II.