Monday, April 23, 2012

Standardized Output

If you are programming clinical tables, listings, and graphs (TLG's), then there is a high probability that you are either working for a company or consulting for one.  I have yet to meet someone who programs these things for fun.

I take that back.  Maybe my friend Karl from grad school would do this kind of thing.

When creating output, SAS can be quite helpful in changing things as needed, but the original default output is pretty ugly to look at.  Here is what I have put together to create a standardized output.


Output Features


My output consists of 4 main sections: headers, footers, titles, and footnotes.  Some may think that a header is the same as a title and that a footer is the same as a footnote.  This is because when a TITLE statement or FOOTNOTE statement is defined in SAS, they are placed in the header and footer sections of a Word document, respectively.  Read on to find how you can split them out.

The following features are pretty basic across all TLG's:

  • Headers
    • Company Name - Most companies or clients like to have their name attached to each TLG.  It seems to give greater authority and ownership to the output.
    • Study Identifier - A TLG is usually the result of a specific study run by the company/client.  This is a good place to display that protocol identifier, drug name, or any other way of identifying the study. 
    • Page Counter  - Not all TLGs fit on one page.  This is especially true when it comes to listings, even for  Phase I studies.  Knowing how many pages are in the output and which page is which keeps things organized.
  • Footers
    • Program Identifier  - It is not rare that your manager will be digging through a pile of papers, find a table that shows an interesting analysis, and want that reproduced for a new study.  To be sure that it is reproduced using the same methodology, it is nice to be able to refer to the original program.  The program identifier shows the programmer either the location or the name of the program used.
    • Production Date  - Things change.  Data gets cleaned.  New data comes in.  Suppose your output shows that your drug increases survival by 8 weeks.  You get new data and find that the rate has changed to 7 weeks, but your boss still has the output that says 8 weeks.  Having the date stamped on the table will allow you to defend why you report 7 weeks while your manager claims 8 weeks.  You simply need to explain that his/her output is three months old and that the data has since been updated.
  • Titles
    • Main Title - This is a quick description of the output.
    • Subtitle - I like to use this to specify the population being used, but it can be used (or omitted) for you specific purposes.
  • Footnotes
    • Superscripted Footnote Text  - Many people like to make the body of the table look cleaner by placing minimal information.  But the information may not make sense without explanations.  Place a superscripted number in the output and reference it to the footnote where more detailed information is given.
    • Notes  - Any additional notes that are not specific to a place in the output can be detailed here.  Perhaps the methods used, the type of regression used, the type of p-value reported, etc.
    • Keys - You may know that CI means confidence interval, but not everyone does.  This is a good place to lay out the meaning of any abbreviations that you use in your output.

SAS has many output styles.  Here are several examples.  I like the look of the Journal style the best, so this is what I will use throughout these posts (until I find something better).  It seems more professional than any other SAS style that I have come across.  Keep in mind that if you really want to customize your own style, you may use PROC TEMPLATE to create your own style.  I have not taken the time to get to know PROC TEMPLATE, but I am sure it can come in handy if your company/client demands a specific look.

Let me now show the code that I use when creating standardized output loaded with comments.  Following all the commented code will be a cleaner repeat in an example.



/*****************************************************************************************************************/ 

/***  Specify some options.  These are mostly for HTML output, but some are used for RTF or PDF.  I like to have my tables landscape and centered, but you are welcome to change the orientation to portrait and adjust the justification of the overall table as you wish.  ***/
options formchar="|____|+|___+=|_/\<>*" pageno=1 nonumber nodate orientation=landscape center;

/***  Close ODS listing so that output will go to your preferred output rather than to the SAS output screen.  Specify which ODS you want to use (I have RTF specified here).  Assign an output file path and name.  &outfile. is a macro that I specify at the top of my programs so that all output goes to the same place.  XXX is the name of the file.  Again, I like the journal style, so I specify style=journal here.  The ODS ESCAPECHAR='~'; statement will come into play later.  ***/
ods listing close;
ods rtf file="&outfile.\XXX.rtf" style=journal;
ods escapechar='~';
/***
SAS has several automatic variables.  One that I like o use is SYSUSERID.  This is the user ID of the person logged into the computer that produces the output.  I use this to attach to the program identifier so people know that it was me who ran the program.  I format this and store it in a macro variable (USER) for later.  If you do not want the user ID formatted, then simply ignore this line and replace &user. with &sysuserid. in the FOOTNOTE1 statement.
The production date can simply by saved into a macro variable (PRODDT) by caling the date() function within the %sysfunc() macro function and assigning it the date format of your choice.
***/
%let user=%sysfunc(upcase(&sysuserid.));
%let proddt=%sysfunc(date(),worddate.);

/************************************/
/***  DEFINE HEADERS AND FOOTERS  ***/
/************************************/

/***
The first line will have the company name and the page counter.  This is where the ODS ESCAPECHAR='~'; statement from above comes into play.  The tilde (~) is used to specify that what follows in brackets ({}) is an inline formatting function.  You may use any character that you want, but it should be one that is most likely not going to appear anywhere else in your code except where you want to specify these inline formatting characters.  ~n specifies a new line.

Here, I use TITLE1 to put the company name in the upper left corner and the page in the upper right corner.  I use TITLE2 to place the protocol identifier directly below the company name.
Next, you can see in the FOOTNOTE1 statement where I specify to put the program name (and my user ID) in the bottom left corner and the production date in the bottom right corner.
***/
title1 j=left font='Arial' 'ABC Company, Inc.' j=right "Page ~{pageof}";
title2 j=left font='Arial' "Protocol: AZ1234";
footnote1 j=left font='Arial' "Program: OUTPUT.sas by &user." j=right "Date Produced: &proddt.";

 
/***
PROC REPORT is my choice procedure for outputting tables.  You may prefer PROC TABULATE or simply use the raw output from PROC MEANS, PROC FREQ, PROC UNIVARIATE, or any other PROC.  It doesn't matter.

data=XXX is the dataset that I want to output.

nowindows tells SAS not to open a new window for displaying output.

split='^' tells SAS that when a carrot (^) is found in a quoted string, then start a new line within that cell.  This comes in handy when putting group counts in the table headers.

style(column)={asis=on} will keep indentations as you specify them.  I like to have indented headers on occasion.  Without this, SAS will ignore leading spaces and make everything line up.

style(report)=... is my way of tweeking the style=journal parameters just a bit for my own liking.

pretext="..." is where I specify my main title and subtitle.  This will make the title part of the table rather than putting it in the document header.  If you use the TITLE statement, then the title will be placed up in the header.

style=[CellWidth=%] is what I use to make it easier to keep footnotes within the width of the table.  Otherwise, I may have a skinny table with very long footnotes that do not wrap within the table bounds.

For the rest of the syntax, please see "REPORT Procedure" in the SAS help files.
***/
proc report data=XXX nowindows split='^' style(column)={asis=on}
            style(report)={font=('Arial',9.5pt,bold italic) pretext="&maintitle. \line\ &subtitle."};
      column XXX YYY ZZZ;
      define XXX / display left ' ' style=[CellWidth=%];
      define YYY / display center ' ' style=[CellWidth=%];
      define ZZZ / display center ' ' style=[CellWidth=%];
run;


/***
This is where I specify footnotes.  You will need to replace the text, "SUM OF CellWidth FROM PROC REPORT DEFINE STYLES" with the sum of the CellWidth percents discussed above.  This will create a macro variable that will be used to keep the footnotes within the bounds of the table.


~(super 1) will create a supersripted 1.  You can replace the 1 with any other value or character of your choice.  Follow the ~(super 1) with any related text.


The second ods rft line will output any notes that you type here.


The third ods rtf line will output any abbreviations that you type here.
***/
%let margin=%sysfunc(cats(%eval((100-SUM OF CellWidth FROM PROC REPORT DEFINE STYLES)/2),%));
ods rtf text="~S={leftmargin=&margin. rightmargin=&margin. just=l font=('Arial', 9pt)}~{super 1} PLACE FOOTNOTE TEXT HERE";
ods rtf text="~S={leftmargin=&margin. rightmargin=&margin. just=l font=('Arial', 9pt)}NOTES:~nPLACE SPECIAL NOTES HERE";
ods rtf text="~S={leftmargin=&margin. rightmargin=&margin. just=l font=('Arial', 9pt)}~nKEY:  PLACE ANY ABBREVIATIONS HERE";


/***  Close the RTF ouput.  Reopen the listing so that output can be displayed in the SAS output window.  Clean up all titles and footnotes.  ***/
ods rtf close;
ods listing;
title; footnote;

/*****************************************************************************************************************/

 
Example

I am going to use the data and code from my last post to prep this example.  Please refer to that post if you want to see how the NUMSTATS macro works.


/* Call the summary macros */
%inc 'D:\Desktop\Reusable Macros\Summary Statistic Macros.sas';

%NUMSTATS(dsin=sashelp.class, var=age, dsout=age, srt1=1, byvar=sex, lbl=%str(Age~{super 1}));
%NUMSTATS(dsin=sashelp.class, var=height, dsout=height, srt1=2, byvar=sex, lbl=%str(Height~{super 2}));
%NUMSTATS(dsin=sashelp.class, var=weight, dsout=weight, srt1=3, byvar=sex, lbl=%str(Weight~{super 3}));
data demog;
    set age height weight;
run;

/* Insert blank lines if wanted */
proc sql;
    insert into demog (srt1)
        values (1.5)
        values (2.5);
quit;

/* sort the data so the inserted blank lines are in the correct place */
proc sort data=demog;
    by srt1 srt2;
run;


/* output the table */
options formchar="|____|+|___+=|_/\<>*" pageno=1 nonumber nodate orientation=landscape center;
ods listing close;
ods rtf file="D:\Desktop\Class Summary by Sex.rtf" style=journal;
ods escapechar='~';

/* Create macro variables for footer */
%let user=%sysfunc(upcase(&sysuserid.));
%let proddt=%sysfunc(date(),worddate.);

/* Create headers and footers */
title1 j=left font='Arial' 'ABC Company, Inc.' j=right "Page ~{pageof}";
title2 j=left font='Arial' "Study: AZ1234";
footnote1 j=left font='Arial' "Program: Output_Example.sas by &user." j=right "Date Produced: &proddt.";

/* Create the report with a title/subtitle */
proc report data=demog nowindows split='^' style(column)={asis=on}
        style(report)={font=('Arial',9.5pt,bold italic) pretext="Class Demographics \line\ (Grouped by Gender)"};
    column label m f;
    define label / display left ' ' style=[CellWidth=20%];
    define m / display center 'Male' style=[CellWidth=15%];
    define f / display center 'Female' style=[CellWidth=15%];
run;

/* Create a macro variable to keep the footnotes within the bounds of the table.
LABEL is 20%, M & F are each 15%.  The sum is 50, which is placed in the line below */
%let margin=%sysfunc(cats(%eval((100-50)/2),%));

/* Now we will create the footnotes: */
ods rtf text="~S={leftmargin=&margin. rightmargin=&margin. just=l font=('Arial', 9pt)}~{super 1} Age is defined in years.
                                                                                                                           ~n~{super 2} Height is defined in inches.
                                                                                                                             ~n~{super 3} Weight is defined in pounds.";
ods rtf text="~S={leftmargin=&margin. rightmargin=&margin. just=l font=('Arial', 9pt)}~nNOTES:~nThis is a very long footnote that is supposed to demonstrate how the footnote will wrap within the bounds of the table rather than with the bounds of the document when the margin macro variable is used on a very long footnote.";
ods rtf text="~S={leftmargin=&margin. rightmargin=&margin. just=l font=('Arial', 9pt)}~nKEY:~nSD = Standard Deviation~nMin = Minimum~nMax = Maximum";


ods rtf close;
ods listing;
title; footnote;


Here is the output:



No comments: