Summarise: PROC FREQ, PROC MEANS and PROC SQL
Every program in this book looks at data, counts and summarises. Four procedures do most of that work: PROC PRINT, PROC FREQ, PROC MEANS and PROC SQL. This chapter teaches them on ADSL. In one real study, 135 of 276 programs used PROC SQL. Use the procedures to count and summarise. Use the DATA step for logic that depends on the order of records.
6.1 Look at a data set: CONTENTS, PRINT, TITLE
Section titled “6.1 Look at a data set: CONTENTS, PRINT, TITLE”/* Chapter 6: look at a data set with PROC CONTENTS and PROC PRINT */
/* the variables: name, type, length, format and label, in data set order */TITLE1 "ADSL: the first ten variables";/* ODS SELECT keeps two of the tables: the data set facts and the variables */ODS SELECT Attributes Position;PROC CONTENTS DATA=refadm.adsl(KEEP=studyid--trt01an) VARNUM;RUN;
/* five records, with labels as column headers */TITLE1 "ADSL: the first five subjects of the safety population";FOOTNOTE1 "Source: refadm.adsl";PROC PRINT DATA=refadm.adsl(OBS=5) LABEL NOOBS; WHERE saffl = "Y"; ID usubjid; VAR trt01a age sex race;RUN;
/* switch the titles and footnotes off again */TITLE;FOOTNOTE;What you should see: first the facts of the data set (254 observations, 10 variables, because KEEP= kept ten), then the variables in data set order:
# Variable Type Len Label 1 STUDYID Char 12 Study Identifier 2 USUBJID Char 11 Unique Subject Identifier ... 9 TRT01A Char 20 Actual Treatment for Period 0110 TRT01AN Num 8 Actual Treatment for Period 01 (N)Then five subjects, with the labels as column headers:
Unique Subject Actual TreatmentIdentifier for Period 01 Age Sex Race
01-701-1015 Placebo 63 F WHITE01-701-1023 Placebo 64 M WHITE01-701-1028 Xanomeline High Dose 71 M WHITE01-701-1033 Xanomeline Low Dose 74 M WHITE01-701-1034 Xanomeline High Dose 77 F WHITEstudyid--trt01anis a variable range: every variable fromSTUDYIDtoTRT01ANin data set order.VARNUMlists the variables in data set order. Without it, the list is alphabetical.- ODS, the Output Delivery System, is the part of SAS that sends the output of a procedure to the window, to a file or to a data set.
ODS SELECTkeeps only the output tables that you name. PROC CONTENTS also prints a table with the path of the file. You do not need it. OBS=5stops after five records. With aWHERE, they are the first five records that meet the condition.ID usubjidprints the subject first, on the left.VARgives the other columns in the order you want.LABELuses the labels as headers.NOOBSdrops the observation number.TITLE1andFOOTNOTE1stay until you change them.TITLE;andFOOTNOTE;with nothing after them switch them off. Always switch them off at the end, or the next output has the wrong title.
Exercise 6-1. Print the first five subjects of the placebo arm only. Which WHERE do you need, and who is the fifth subject?
6.2 Count: PROC FREQ
Section titled “6.2 Count: PROC FREQ”/* Chapter 6: count with PROC FREQ */TITLE1 "Age group (safety population)";PROC FREQ DATA=refadm.adsl; WHERE saffl = "Y"; TABLES agegr1 / NOCUM; TABLES trt01a * agegr1 / NOROW NOPERCENT;RUN;
/* LIST gives one row per combination; OUT= keeps the counts as data */TITLE1 "Treatment and sex, one row per combination";PROC FREQ DATA=refadm.adsl; WHERE saffl = "Y"; TABLES trt01a * sex / LIST NOCUM OUT=trt_sex;RUN;
/* a missing value is not counted, unless you ask for it with MISSING *//* VSTPT (the position) is blank for weight and temperature */TITLE1 "Position of the BASELINE vital signs: without and with MISSING";PROC FREQ DATA=sdtm.vs; WHERE visit = "BASELINE"; TABLES vstpt / NOCUM; TABLES vstpt / NOCUM MISSING;RUN;TITLE;What you should see: age groups 65-80 144, <65 33, >80 77. Then the same groups by treatment:
Frequency |Col Pct |65-80 |<65 |>80 | Total-----------------+--------+--------+--------+Placebo | 42 | 14 | 30 | 86 | 29.17 | 42.42 | 38.96 |-----------------+--------+--------+--------+Xanomeline High | 55 | 11 | 18 | 84Dose | 38.19 | 33.33 | 23.38 |-----------------+--------+--------+--------+Xanomeline Low D | 47 | 8 | 29 | 84ose | 32.64 | 24.24 | 37.66 |-----------------+--------+--------+--------+Total 144 33 77 254Then one row per treatment and sex (Placebo F 53, M 33; High Dose F 40, M 44; Low Dose F 50, M 34), and at the end the positions of the BASELINE vital signs, twice.
TABLES a * bmakes a cross table.NOROWandNOPERCENTremove the row and total percents, so each cell shows the count and the column percent.LISTwrites one row per combination instead of a cross table. It is easier to read, andOUT=keeps the same counts as a data set for the next step.- A missing value is not counted. Without
MISSING, the last table shows three positions with 759 records each, and a lineFrequency Missing = 506: weight and temperature have no position. WithMISSING, the 506 records get their own row, and the percents change from 33.33 to 27.27. Decide on purpose which one your table needs. - The text categories sort as text:
65-80comes before<65. To get another order, sort by a number, as chapter 13 does withAGEGR1N.
Exercise 6-2. Count RACE by treatment in the safety population with LIST. How many White subjects are in the placebo arm?
6.3 Statistics: PROC MEANS, PROC SUMMARY, PROC UNIVARIATE
Section titled “6.3 Statistics: PROC MEANS, PROC SUMMARY, PROC UNIVARIATE”/* Chapter 6: statistics with PROC MEANS, PROC SUMMARY and PROC UNIVARIATE */TITLE1 "Age by treatment (safety population)";PROC MEANS DATA=refadm.adsl N MEAN STD MEDIAN MIN MAX MAXDEC=1; WHERE saffl = "Y"; CLASS trt01a; VAR age;RUN;
/* the same statistics as data: PROC SUMMARY prints nothing, it writes OUTPUT OUT= */PROC SUMMARY DATA=refadm.adsl NWAY; WHERE saffl = "Y"; CLASS trt01an trt01a; VAR age; OUTPUT OUT=age_stats(DROP=_type_ _freq_) N=n MEAN=mean STD=sd MEDIAN=median MIN=min MAX=max;RUN;TITLE1 "AGE_STATS: one record per treatment";PROC PRINT DATA=age_stats NOOBS; FORMAT mean sd median 5.1;RUN;
/* BY instead of CLASS: the data must be sorted, and SAS prints one table per group */PROC SORT DATA=refadm.adsl(WHERE=(saffl = "Y")) OUT=adsl_saf; BY trt01an;RUN;TITLE1 "Age with BY: one table per treatment";PROC MEANS DATA=adsl_saf N MEAN MAXDEC=1; BY trt01an; VAR age;RUN;
/* a quick look at one variable: the basic measures and the extreme values */TITLE1 "Baseline weight (kg): a quick look";ODS SELECT BasicMeasures ExtremeObs;PROC UNIVARIATE DATA=refadm.adsl NEXTROBS=3; WHERE saffl = "Y"; VAR weightbl; ID usubjid;RUN;TITLE;What you should see:
Actual Treatment Nfor Period 01 Obs N Mean Std Dev Median Minimum MaximumPlacebo 86 86 75.2 8.6 76.0 52.0 89.0Xanomeline High Dose 84 84 74.4 7.9 76.0 56.0 88.0Xanomeline Low Dose 84 84 75.7 8.3 77.5 51.0 88.0Then the data set AGE_STATS, with the same numbers as variables, three small tables made with BY, and the quick look at the baseline weight: mean 66.6 kg, median 66.7 kg, the three lightest subjects (34.0, 39.9 and 41.1 kg) and the three heaviest (102.1, 106.1 and 108.0 kg).
CLASSagainstBY.CLASSneeds no sort and gives one table with a row per group.BYneeds data sorted by the BY variables and gives one table per group. UseCLASSfor summaries. UseBYwhen each group needs its own page.PROC SUMMARYis PROC MEANS without the printed table.OUTPUT OUT=writes the statistics that you name (N=n MEAN=mean ...) as variables.NWAYkeeps only the rows for the fullCLASScombination. WithoutNWAY, you get every combination of theCLASSvariables: one row for all subjects together (_TYPE_ = 0), rows for eachCLASSvariable alone, and the full combinations.PROC UNIVARIATEis the quick look at one variable. The extreme values are the first place where data errors show: a weight of 34 kg, is it real?ID usubjidtells you whom to check.MAXDEC=1rounds the printed numbers only. The data setAGE_STATSkeeps every decimal.
Exercise 6-3. Remove NWAY from the PROC SUMMARY step. How many records does AGE_STATS have now, and what is the mean age of all 254 subjects?
6.4 The shape of a query
Section titled “6.4 The shape of a query”Every query has the same skeleton. The table lists the clauses in a good reading order: from the data to the result. In the program you write them in a fixed order: SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY.
| Clause | Question it answers |
|---|---|
FROM | Which table, or which tables joined? |
WHERE | Which rows go in? |
GROUP BY | Which rows belong together? |
SELECT | Which columns and summaries come out? |
HAVING | Which groups stay? |
ORDER BY | In which order? |
/* Chapter 6: the shape of a query */PROC SQL; SELECT trt01a, COUNT(*) AS n FROM refadm.adsl WHERE saffl = "Y" GROUP BY trt01a;QUIT;What you should see: Placebo 86, Xanomeline High Dose 84, Xanomeline Low Dose 84.
PROC SQL ends with QUIT;, not RUN;. Commas separate the items of a list.
Exercise 6-4. Add HAVING COUNT(*) > 85 to 06_skeleton.sas. Which rows stay?
6.5 Big N into macro variables
Section titled “6.5 Big N into macro variables”Every table header shows the number of subjects per treatment: the big N. Compute it once, and keep it in macro variables:
/* Chapter 6: big N per treatment into macro variables */PROC SQL NOPRINT; SELECT trt01an, COUNT(DISTINCT usubjid) INTO :trt1-:trt3, :n1-:n3 FROM refadm.adsl WHERE saffl = "Y" GROUP BY trt01an;QUIT;
%PUT NOTE: &=trt1. &=n1. &=trt2. &=n2. &=trt3. &=n3.;What you should see in the log:
NOTE: TRT1=0 N1=86 TRT2=54 N2=84 TRT3=81 N3=84INTO :n1-:n3writes one value per row inton1,n2andn3.- Select the treatment code too, into
trt1-trt3, so that you can prove which count belongs to which arm: 0 is placebo, 54 the low dose and 81 the high dose. COUNT(DISTINCT usubjid)counts subjects;COUNT(*)counts rows.
6.6 Subjects with at least one TEAE
Section titled “6.6 Subjects with at least one TEAE”This is the first row of every safety table: how many subjects had at least one treatment-emergent adverse event (TEAE), as a percent of the big N.
/* Chapter 6: subjects with at least one TEAE, by treatment */PROC SQL; CREATE TABLE teae_n AS SELECT a.trt01an, a.trt01a, COUNT(DISTINCT a.usubjid) AS bign, COUNT(DISTINCT b.usubjid) AS n_teae, CALCULATED n_teae / CALCULATED bign * 100 AS pct FORMAT=5.1 FROM refadm.adsl(WHERE=(saffl = "Y")) AS a LEFT JOIN refadm.adae(WHERE=(trtemfl = "Y")) AS b ON a.usubjid = b.usubjid GROUP BY a.trt01an, a.trt01a ORDER BY a.trt01an;QUIT;
PROC PRINT DATA=teae_n NOOBS; RUN;What you should see (press F12 to open TEAE_N):
| TRT01AN | TRT01A | bign | n_teae | pct |
|---|---|---|---|---|
| 0 | Placebo | 86 | 65 | 75.6 |
| 54 | Xanomeline Low Dose | 84 | 77 | 91.7 |
| 81 | Xanomeline High Dose | 84 | 76 | 90.5 |
The LEFT JOIN keeps every subject of the safety population, also the subjects without an AE. The big N and the count then come from the same query. So the percent cannot use a wrong denominator. With an inner join, the subjects without an AE are lost. Then bign becomes the number of subjects with an AE, and every percent is 100.
CALCULATED lets you use a column that the same SELECT makes.
Exercise 6-5. Change 06_teae.sas to count subjects with at least one serious TEAE (aeser = "Y").
Exercise 6-6. Rewrite 06_teae.sas with an inner join. Predict bign before you run it.
6.7 CASE and the set operators
Section titled “6.7 CASE and the set operators”CASE is the IF-THEN-ELSE of SQL. The set operators combine the rows of two queries: UNION keeps the rows of either, INTERSECT the rows of both, EXCEPT the rows of the first that are not in the second.
/* Chapter 6: CASE, and the set operators UNION, INTERSECT and EXCEPT */PROC SQL; /* CASE: a new column from a rule, as IF-THEN-ELSE in a DATA step */ CREATE TABLE agecat AS SELECT usubjid, age, CASE WHEN age < 65 THEN "<65" WHEN age <= 80 THEN "65-80" ELSE ">80" END AS agecat LENGTH=5 FROM refadm.adsl WHERE saffl = "Y"; TITLE1 "Age groups made with CASE"; SELECT agecat, COUNT(*) AS n FROM agecat GROUP BY agecat;
/* one row per subject in each list */ CREATE TABLE ae_subj AS SELECT DISTINCT usubjid FROM sdtm.ae; CREATE TABLE cm_subj AS SELECT DISTINCT usubjid FROM sdtm.cm;
TITLE1 "Subjects with an AE, a concomitant medication, or both"; SELECT "AE or CM (UNION)" AS question LENGTH=30, COUNT(*) AS subjects FROM (SELECT usubjid FROM ae_subj UNION SELECT usubjid FROM cm_subj) OUTER UNION CORRESPONDING SELECT "AE and CM (INTERSECT)" AS question, COUNT(*) AS subjects FROM (SELECT usubjid FROM ae_subj INTERSECT SELECT usubjid FROM cm_subj) OUTER UNION CORRESPONDING SELECT "AE but no CM (EXCEPT)" AS question, COUNT(*) AS subjects FROM (SELECT usubjid FROM ae_subj EXCEPT SELECT usubjid FROM cm_subj);QUIT;TITLE;What you should see: the age groups again (65-80 144, <65 33, >80 77, the same as AGEGR1 from ADSL), then:
question subjectsAE or CM (UNION) 244AE and CM (INTERSECT) 210AE but no CM (EXCEPT) 15225 subjects have an AE (210 + 15) and 229 have a concomitant medication (244 − 15).
CASEtests theWHENconditions in order and stops at the first true one. GiveEND ASa name and aLENGTH.- A missing number is smaller than every number. So
WHEN age < 65would also catch a missing age. The pilot has no missing age; your study may have one. TestNOT MISSING(age)first. - The set operators remove duplicate rows.
UNION ALLkeeps them. OUTER UNION CORRESPONDINGstacks the three one-row queries into one result, by column name.
Exercise 6-7. How many subjects have a concomitant medication but no AE?
6.8 The remerge NOTE
Section titled “6.8 The remerge NOTE”/* EXPECT: NOTE The query requires remerging summary statistics back with the original data *//* Chapter 6: a summary next to detail rows makes SAS remerge */PROC SQL; CREATE TABLE remerge AS SELECT usubjid, age, MEAN(age) AS mean_age FROM refadm.adsl;QUIT;What you should see:
NOTE: The query requires remerging summary statistics back with the original data.SAS computed one mean and copied it to all 254 rows. Sometimes you want that. Often it is a missing GROUP BY. A QC log check flags this NOTE, and so does the Suspicious Log. If you want the remerge, compute the summary in a separate step. Then the log stays clean, and the intent is clear.
6.9 Ask SAS about your data: dictionary tables
Section titled “6.9 Ask SAS about your data: dictionary tables”SAS keeps metadata about every library, data set and variable in dictionary tables. You can query them like any data.
/* Chapter 6: ask SAS about your data */PROC SQL; SELECT name, type, length, label FROM dictionary.columns WHERE libname = "REFADM" AND memname = "ADSL" AND name LIKE "TRT%";QUIT;
PROC SQL NOPRINT; CREATE TABLE flags AS SELECT name, varnum FROM dictionary.columns WHERE libname = "REFADM" AND memname = "ADSL" AND name LIKE '%FL' ORDER BY varnum; SELECT name INTO :flvars SEPARATED BY " " FROM flags;QUIT;
%PUT NOTE: &=flvars.;What you should see: seven variables whose name starts with TRT, among them TRT01P (char, 20, “Planned Treatment for Period 01”) and TRTSDT (num, 8, “Date of First Exposure to Treatment”). Then in the log:
NOTE: FLVARS=SAFFL ITTFL EFFFL COMP8FL COMP16FL COMP24FL DISCONFL DSRAEFL DTHFLLibrary and member names are stored in uppercase. So compare them with uppercase values. The first query writes the names to a table. A second query sorts them. A query that sorts by a column that it does not select gives another NOTE that a log check flags.
Exercise 6-8. Put the names of all SDTM data sets into one macro variable &domains. from dictionary.tables.
6.10 The percent sign trap
Section titled “6.10 The percent sign trap”/* EXPECT: WARNING Apparent invocation of macro FL not resolved *//* Chapter 6: a percent sign in double quotes calls the macro processor */PROC SQL NOPRINT; SELECT name INTO :flvars SEPARATED BY " " FROM dictionary.columns WHERE libname = "REFADM" AND memname = "ADSL" AND name LIKE "%FL";QUIT;What you should see:
WARNING: Apparent invocation of macro FL not resolved.Inside double quotes, the macro processor reads %FL as a call of a macro named FL. Single quotes stop the macro processor. That is why 06_dictionary.sas writes '%FL'. Use single quotes for any text with % or & that is not for the macro processor. Part V explains how the macro processor reads your code.
Answers
Section titled “Answers”The answers to Exercises 6-1 to 6-8 are in Answers, Part I.