Skip to content

A tour of clinical SAS

You learn SAS by writing SAS programs. This chapter is a fast tour: six small programs that use the SAS you need most. Do not try to understand every detail now. Each later chapter explains one part in full.

01_1_first.sas
/* Chapter 1: a first program. Read ADSL, keep five variables, count the subjects per arm */
DATA adsl_tour;
SET refadm.adsl(KEEP=usubjid arm age sex saffl);
RUN;
PROC FREQ DATA=adsl_tour;
TABLES arm;
RUN;

Press F4 to run it.

What you should see:

ARMFrequencyPercent
Placebo8633.86
Xanomeline High Dose8433.07
Xanomeline Low Dose8433.07

The program has two steps:

  • The DATA step makes a data set. DATA adsl_tour; names the new data set. SET reads REFADM.ADSL one record at a time. KEEP= keeps only five variables.
  • The PROC step runs a procedure, a program that SAS gives you. PROC FREQ counts the values of arm.
  • Every statement ends with a semicolon. RUN; ends a step.

ADSL has one record per subject, so the table counts subjects: 254 in three arms.

Exercise 1-1. Change TABLES arm; to TABLES sex;. How many women and men are in the study?

01_2_if.sas
/* Chapter 1: IF, THEN and ELSE make a new variable from an old one */
DATA adsl_age;
LENGTH agegrp $11;
SET refadm.adsl(KEEP=usubjid arm age);
IF age >= 65 THEN agegrp = "65 or older";
ELSE agegrp = "under 65";
RUN;
PROC FREQ DATA=adsl_age;
TABLES agegrp;
RUN;

What you should see:

agegrpFrequencyPercent
65 or older22187.01
under 653312.99
  • IF ... THEN ... ELSE gives agegrp one of two values for each subject.
  • LENGTH agegrp $11; makes agegrp a text variable of 11 characters. Without it, SAS takes the length from the first value it sees. Chapter 3 shows the trap.

Exercise 1-2. Change the program so that it counts the subjects older than 80. How many are there?

01_3_sort_by.sas
/* Chapter 1: sort, then read in groups. The oldest subject of each arm */
PROC SORT DATA=refadm.adsl(KEEP=usubjid arm age) OUT=adsl_sorted;
BY arm DESCENDING age usubjid;
RUN;
DATA oldest;
SET adsl_sorted;
BY arm;
IF FIRST.arm;
RUN;
PROC PRINT DATA=oldest NOOBS;
RUN;

What you should see: the oldest subject of each arm.

USUBJIDARMAGE
01-710-1083Placebo89
01-703-1295Xanomeline High Dose88
01-710-1002Xanomeline Low Dose88
  • PROC SORT puts the records in order: by arm, and within an arm from the oldest down. usubjid breaks a tie, so the result is the same every time.
  • BY arm; in the DATA step reads the records in groups. FIRST.arm is 1 on the first record of each arm.
  • IF FIRST.arm; keeps only those records. This “subsetting IF” has no THEN.

Chapter 4 explains BY, FIRST. and LAST. step by step.

Exercise 1-3. Remove DESCENDING. What do you get now?

01_4_means.sas
/* Chapter 1: summary statistics per arm */
PROC MEANS DATA=refadm.adsl N MEAN STD MIN MAX MAXDEC=1;
CLASS arm;
VAR age;
RUN;

What you should see:

ARMNMeanStd DevMinimumMaximum
Placebo8675.28.652.089.0
Xanomeline High Dose8474.47.956.088.0
Xanomeline Low Dose8475.78.351.088.0

CLASS arm makes one row per arm. VAR age names the variable. These are the numbers of the age rows in a demographics table (chapter 13).

01_5_sql.sas
/* Chapter 1: PROC SQL counts subjects, not records */
PROC SQL;
SELECT trta, COUNT(*) AS records, COUNT(DISTINCT usubjid) AS subjects
FROM refadm.adae
WHERE trtemfl = "Y"
GROUP BY trta;
QUIT;

What you should see:

Actual Treatmentrecordssubjects
Placebo28165
Xanomeline High Dose43376
Xanomeline Low Dose41277

ADAE has one record per adverse event. A subject can have many events. COUNT(*) counts records. COUNT(DISTINCT usubjid) counts each subject once. A safety table reports subjects: 65 subjects on placebo had at least one treatment-emergent adverse event. This is the most important rule of clinical tables. Chapter 6 teaches PROC SQL.

Exercise 1-4. Add AND aesev = "SEVERE" to the WHERE. How many subjects per arm had a severe event?

01_6_let.sas
/* Chapter 1: a macro variable holds text that you write once and use many times */
%LET arm = Placebo;
TITLE "Age of the subjects on &arm.";
PROC MEANS DATA=refadm.adsl N MEAN MIN MAX MAXDEC=1;
WHERE arm = "&arm.";
VAR age;
RUN;
TITLE;

What you should see: a title “Age of the subjects on Placebo”, and N 86, mean 75.2, minimum 52.0, maximum 89.0.

%LET arm = Placebo; makes a macro variable. SAS puts its text where you write &arm., before the step runs. Change the value once, and the title and the WHERE both follow. This is the first idea of the macro language (chapter 8).

Exercise 1-5. Change the value to Xanomeline Low Dose. What are N, the mean and the oldest age?

ProgramIdeaExplained in
01_1DATA step, SET, PROC FREQChapter 2
01_2IF/THEN/ELSE, LENGTHChapter 3
01_3PROC SORT, BY, FIRST., subsetting IFChapter 4
01_4PROC MEANSChapter 6
01_5PROC SQL, count subjectsChapter 6
01_6%LET and &var.Chapter 8

That is most of the SAS in a clinical study. The rest of this chapter shows where the data comes from and what the numbers are for.

A clinical programmer turns trial data into the numbers that a regulator reads. Two rules shape every page of this book:

  • Every number must be traceable to the data it came from.
  • A second programmer must be able to reproduce it.
Every number is traced back, and built twiceProtocol + SAPADaM specTLF shellsyou implement, you never decidePRODUCTION PROGRAMMEREDC / CRFSDTMDM: 306 subjectsADaMADSL: 254 subjectsTLFN = 86 / 84 / 8452 screen failures leaveone PROCawayQC PROGRAMMER: same spec, own codeindependent programQC ADSL254 subjectsPROCCOMPARE0 differences → releasedifferences → find which program is wrongNOTE: No unequal values were found. All values compared are exactly equal.
Documents decide what to compute. The production programmer builds SDTM → ADaM → TLF; a second programmer builds the same data set from the same specification with their own code. PROC COMPARE must find no differences before an output is released.

You do not decide what to compute. Documents decide it. You implement them exactly.

DocumentWritten byWhat you take from it
ProtocolClinical teamVisits, treatments, populations, the primary endpoint
Statistical analysis plan (SAP)StatisticianPopulations, derivation rules, imputation rules, models
TLF shells (mock tables)StatisticianThe layout of each output: rows, columns, decimals, footnotes
ADaM specificationLead programmerEvery ADaM variable: name, label, type, length, derivation
define.xmlProgrammer (generated)The metadata sent to the regulator with the data

TLF means tables, listings and figures. When two documents disagree, do not choose. Ask the statistician. Write the answer where the next programmer can find it.

The study data comes in two standards. SDTM holds what was collected. ADaM holds what the analysis needs.

SDTMADaM
PurposeShow what was collectedMake the analysis easy
Organised byTopic: DM, AE, LB, VS, EX, DSAnalysis need: ADSL, ADAE, ADLB, ADTTE
DatesISO 8601 text, can be partial: 2014-03SAS dates (ADT), with imputation flags
Derived valuesFewMany: baseline, change, flags, analysis visits

The core ADaM rule is “one PROC away”. A table program only selects records with flags, counts, summarises and prints. If a table program must derive a value, that value belongs in ADaM.

ADaM has four structures:

  • ADSL, subject level: one record per subject. Treatment, populations, demography, key dates. Chapter 9.
  • BDS, basic data structure: one record per subject, parameter and time point. Labs, vital signs. Chapter 10.
  • OCCDS, occurrence data: one record per event. Adverse events, medications. Chapter 11.
  • ADTTE, time to event: a BDS with the time (AVAL) and a censor flag (CNSR). Chapter 12.

SDTM DM of the pilot study holds 306 subjects. 52 are screen failures: they signed consent, but they were never randomised. ADSL keeps the 254 randomised subjects: 86 on placebo, 84 on the low dose, 84 on the high dose. Program 01_1 counted them.

When a table says N = 86, you must be able to show the 86 records that made it. That is traceability.

The usual standard is double programming. Two programmers write the same specification independently. Then PROC COMPARE must find no differences (chapter 15).

This changes how you write code. Your program must:

  • be easy to read for the second programmer,
  • give the same result each time it runs,
  • leave a clean log.

A program that is right but cannot be checked is not finished.

Exercise 1-6. Name the SDTM domain that holds the date of first study treatment. Name the ADSL variable that holds it as a SAS date.

Exercise 1-7. A table program computes “change from baseline” itself. Which ADaM rule does it break?

Exercise 1-8. DM has 306 records and ADSL has 254. Write one sentence for a reviewer that explains the difference.

The answers are in Answers, Part I.