I was writing some new examples for the Customizing the Kaplan-Meier Survival Plot chapter of SAS/STAT software. I thought I would share some of the issues I think about as I write template modification code. We'll take a deeper dive into understanding item stores--the files in which compiled templates are stored--and ways in which you can access them. At the end, I will show you one of my new examples: displaying percentages in the failure plot.
You can do many things with ODS and ODS Graphics and yet know little or nothing about how the tables and graphs are created. As you move beyond simply running a procedure and saving the output, you might want to customize the output. That is when you need to know about templates and where they are stored. Every table and graph has a template. Developers at SAS write the templates, and we provide you with the template source code so that can use them and modify them. Templates contain instructions about formats, table headers, types of graphs, and many other things. You can customize everything.
SAS provides several item stores that contain compiled templates. SAS also provides tools in PROC TEMPLATE via the LIST and SOURCE statements to access these item stores and ensure you are always accessing the templates that SAS provides. You can write DATA step programs to modify the templates. Programmatic template modification ensures that you have a fully documented path between the templates that SAS provides and the templates that you use. These tools give you choices about how to proceed. This post explains some of those choices. Along the way you will see examples of some the interesting ways you can use PROC TEMPLATE to access templates. Every time I write about template modification, I find myself in a bit of a quandary. How much should I say about item stores, and which method of accessing those templates should I use? I usually rely on SAS default settings for item stores. My colleague Rick Wicklin recently wrote about other ways. This post explores some of the alternatives and reasons why you might choose different alternatives in different situations. Let's begin by submitting the following statement:
ods path show; |
We are asking ODS to show us the template search path--the default places that ODS looks for templates.
The results are:
Current ODS PATH list is: 1. SASUSER.TEMPLAT(UPDATE) 2. SASHELP.TMPLMST(READ) |
This says that there are two default item stores. Templates that SAS provides are stored in the SASHELP.TMPLMST item store. This is a read-access only file. This is important! Never set the access to anything else. You never want to alter templates that SAS provides in the SASHELP.TMPLMST item store.You always want to modify them somewhere else. So where? The other item store in the list is SASUSER.TEMPLAT. The access for that item store is "update." By default, when you modify a template, it is stored in SASUSER.TEMPLAT. The version in SASHELP.TMPLMST remains unchanged. The ODS template search path shows that SAS looks for templates first in SASUSER.TEMPLAT to see if there is a template there that you modified. If it does not find a template there, it searches SASHELP.TMPLMST. Templates in SASHELP.TMPLMST remain for the duration of your SAS release. Templates in SASUSER.TEMPLAT remain across SAS sessions until you delete them.
You can store templates that you modify in other places, too. The following statement adds an item store to the ODS search path:
ods path (prepend) work.templat(update); |
If you submit the following statement, you can see the change:
ods path show; |
Current ODS PATH list is: 1. WORK.TEMPLAT(UPDATE) 2. SASUSER.TEMPLAT(UPDATE) 3. SASHELP.TMPLMST(READ) |
Now when you modify a template, the modification is stored in WORK.TEMPLAT. ODS first searches for templates in the item store WORK.TEMPLAT, which you can update. If ODS does not find the template there, then it searches SASUSER.TEMPLAT followed by SASHELP.TMPLMST. This is all very analogous to SAS data sets: item stores in SASUSER persist across SAS sessions, and item stores in WORK last only for the duration of the current SAS session. Furthermore, you can make your own permanent item stores (at least until you delete them). The following statements illustrate:
libname mytpls '.'; ods path (prepend) mytpls.template(update); ods path show; |
Current ODS PATH list is: 1. MYTPLS.TEMPLATE(UPDATE) 2. WORK.TEMPLAT(UPDATE) 3. SASUSER.TEMPLAT(UPDATE) 4. SASHELP.TMPLMST(READ) |
Now the first item store is in the file template.sas7bitm, which is in my working directory. (The "template" in "template.sas7bitm" matches the "template" in "mytpls.template".) You might want to create multiple item stores and put item stores in different directories for different projects.
You can restore and display the default path by submitting these statements:
ods path reset; ods path show; |
Current ODS PATH list is: 1. SASUSER.TEMPLAT(UPDATE) 2. SASHELP.TMPLMST(READ) |
While the ODS template search path provides a simple notation for specifying the templates that SAS provides, you will find that PROC TEMPLATE provides more granular control. In the early days of ODS, SAS provided precisely one template item store, SASHELP.TMPLMST. That is the origin for the current form of the template search path. Now, SAS ships several item stores. In the context of the template search path, SASHELP.TMPLMST still means all of the templates that SAS provides. That is not true in PROC TEMPLATE.
The following step lists 555 templates that SAS provides in SASHELP.TMPLMST:
proc template; list / store=sashelp.tmplmst where=(Type ne 'Dir'); quit; |
They include some Base SAS templates such as templates for PROC CONTENTS, ODS style templates, tagsets, some templates that get shared across procedures, and a few others (some for no particular reason). This is 6.4% of the total templates that SAS provides. The following steps list the names of all of the item stores and the number of templates in each:
proc template; ods exclude stats; ods output stats=s; list / stats=store; run; proc freq data=s(where=(type ne 'Dir')); tables store; run; |
Cumulative Cumulative Store Frequency Percent Frequency Percent -------------------------------------------------------------------------------- SASHELP.TMPLACAS 667 7.70 667 7.70 SASHELP.TMPLAIR 23 0.27 690 7.96 SASHELP.TMPLBASE 159 1.83 849 9.80 SASHELP.TMPLCAS 80 0.92 929 10.72 SASHELP.TMPLCOMMON 87 1.00 1016 11.72 SASHELP.TMPLETS 1589 18.34 2605 30.06 SASHELP.TMPLHPA 81 0.93 2686 30.99 SASHELP.TMPLHPDM 54 0.62 2740 31.62 SASHELP.TMPLHPETS 117 1.35 2857 32.97 SASHELP.TMPLHPF 172 1.98 3029 34.95 SASHELP.TMPLHPHPF 6 0.07 3035 35.02 SASHELP.TMPLHPSTAT 387 4.47 3422 39.49 SASHELP.TMPLHPTM 1 0.01 3423 39.50 SASHELP.TMPLIML 43 0.50 3466 40.00 SASHELP.TMPLLASR 99 1.14 3565 41.14 SASHELP.TMPLMST 555 6.40 4120 47.54 SASHELP.TMPLNETWORK 49 0.57 4169 48.11 SASHELP.TMPLNETWORKCOMMON 33 0.38 4202 48.49 SASHELP.TMPLNETWORKOPTIMIZATION 24 0.28 4226 48.77 SASHELP.TMPLNETWORKSOCIAL 12 0.14 4238 48.90 SASHELP.TMPLOPTGRAPH 4 0.05 4242 48.95 SASHELP.TMPLOPTIMIZATION 21 0.24 4263 49.19 SASHELP.TMPLOPTMINER 17 0.20 4280 49.39 SASHELP.TMPLOPTNETWORK 57 0.66 4337 50.05 SASHELP.TMPLOR 80 0.92 4417 50.97 SASHELP.TMPLQC 357 4.12 4774 55.09 SASHELP.TMPLSTAT 3888 44.86 8662 99.95 SASHELP.TMPLTMINE 4 0.05 8666 100.00 |
All are accessible through the ODS search path SASHELP.TMPLMST. However, in other contexts such as PROC TEMPLATE, you cannot specify SASHELP.TMPLMST as a surrogate for all templates that SAS provides. You can work through the series of short examples that follow to better understand how ODS stores and accesses templates.
You can list all of the templates in SASUSER.TEMPLAT as follows:
proc template; list / store=sasuser.templat; quit; |
This produces:
WARNING: Path 'SASUSER.TEMPLAT' does not exist! |
Now define a simple template:
proc template; define table Base.Freq.Factoid; parent=Common.Factoid; end; quit; |
PROC TEMPLATE prints the note:
NOTE: TABLE 'Base.Freq.Factoid' has been saved to: SASUSER.TEMPLAT |
Now list the contents of that item store again:
proc template; list / store=sasuser.templat; quit; |
Listing of: SASUSER.TEMPLAT Path Filter is: * Sort by: PATH/ASCENDING Obs Path Type ---------------------------------- 1 Base Dir 2 Base.Freq Dir 3 Base.Freq.Factoid Table |
You can see there is now one table and two directory levels. Now list the Base.Freq.Factoid template:
proc template; list Base.Freq.Factoid; quit; |
Listing of: SASUSER.TEMPLAT Path Filter is: Base.Freq.Factoid Sort by: PATH/ASCENDING Obs Path Type ---------------------------------- 1 Base.Freq.Factoid Table Listing of: SASHELP.TMPLBASE Path Filter is: Base.Freq.Factoid Sort by: PATH/ASCENDING Obs Path Type ---------------------------------- 1 Base.Freq.Factoid Table |
It is in two item stores. Now delete and list the template:
proc template; delete Base.Freq.Factoid; list Base.Freq.Factoid; quit; |
Listing of: SASHELP.TMPLBASE Path Filter is: Base.Freq.Factoid Sort by: PATH/ASCENDING Obs Path Type ---------------------------------- 1 Base.Freq.Factoid Table |
It is in one item store. Now try to delete it again and list it:
proc template; delete Base.Freq.Factoid; list Base.Freq.Factoid; quit; |
PROC TEMPLATE prints these messages:
WARNING: Path 'Base.Freq.Factoid' does not exist! NOTE: Could not delete 'Base.Freq.Factoid' from template store! |
Here is the listing:
Listing of: SASHELP.TMPLBASE Path Filter is: Base.Freq.Factoid Sort by: PATH/ASCENDING Obs Path Type ---------------------------------- 1 Base.Freq.Factoid Table |
It shows that the template still exists in a SASHELP item store since SASHELP.TMPLMST (a surrogate in the ODS template search path for SASHELP.TMPLBASE and all of the other SASHELP item stores) is read access only. Assuming that you never change the access SASHELP.TMPLMST to anything other than read access, you can safely delete templates by specifying their name in the DELETE statement. When you are writing a program that involves template changes, you might want to begin the program by deleting the template that you have not yet modified. This way you can be certain that as you develop your code (as you submit code subsets and make mistakes along the way) that you are always modifying the templates that SAS provides. This is illustrated in the last example.
Now let's look at a graph template:
ods trace on; proc kde data=sashelp.class; bivar height weight / plots=scatter; run; |
I chose the scatter plot in PROC KDE because it has one of the simplest graph templates in all of SAS. The ODS TRACE output shows that the template name is Stat.KDE.Graphics.ScatterPlot. Now list the template:
proc template; list Stat.KDE.Graphics.ScatterPlot; quit; |
Listing of: SASHELP.TMPLSTAT Path Filter is: Stat.KDE.Graphics.ScatterPlot Sort by: PATH/ASCENDING Obs Path Type -------------------------------------------------- 1 Stat.KDE.Graphics.ScatterPlot Statgraph |
It is in the item store SASHELP.TMPLSTAT.
Now display the template source:
proc template; source Stat.KDE.Graphics.ScatterPlot; quit; |
Here is the template source:
define statgraph Stat.KDE.Graphics.ScatterPlot; dynamic _VAR1NAME _VAR1LABEL _VAR2NAME _VAR2LABEL _byline_ _bytitle_ _byfootnote_; BeginGraph; EntryTitle "Distribution of " _VAR1NAME " by " _VAR2NAME; Layout Overlay / xaxisopts=(offsetmin=0.05 offsetmax=0.05) yaxisopts=( offsetmin=0.05 offsetmax=0.05); ScatterPlot x=X y=Y / markerattrs=GRAPHDATADEFAULT; EndLayout; if (_BYTITLE_) entrytitle _BYLINE_ / textattrs=GRAPHVALUETEXT; else if (_BYFOOTNOTE_) entryfootnote halign=left _BYLINE_; endif; endif; EndGraph; end; |
Now explictly specify the item store when displaying the source:
proc template; source Stat.KDE.Graphics.ScatterPlot / store=sashelp.tmplmst; quit; |
PROC TEMPLATE displays this warning since SASHELP.TMPLMST is not a surrogate for SASHELP.TMPLSTAT in PROC TEMPLATE:
WARNING: Path 'Stat.KDE.Graphics.ScatterPlot' does not exist! |
Now display the source, specifically naming the correct item store:
proc template; source Stat.KDE.Graphics.ScatterPlot / store=sashelp.tmplstat; quit; |
Here are the results:
define statgraph Stat.KDE.Graphics.ScatterPlot / store = SASHELP.TMPLSTAT; dynamic _VAR1NAME _VAR1LABEL _VAR2NAME _VAR2LABEL _byline_ _bytitle_ _byfootnote_; BeginGraph; EntryTitle "Distribution of " _VAR1NAME " by " _VAR2NAME; Layout Overlay / xaxisopts=(offsetmin=0.05 offsetmax=0.05) yaxisopts=( offsetmin=0.05 offsetmax=0.05); ScatterPlot x=X y=Y / markerattrs=GRAPHDATADEFAULT; EndLayout; if (_BYTITLE_) entrytitle _BYLINE_ / textattrs=GRAPHVALUETEXT; else if (_BYFOOTNOTE_) entryfootnote halign=left _BYLINE_; endif; endif; EndGraph; end; |
Now you can start to see part of the quandary to which I referred near the top of this post. The following two SOURCE statements do not display the same thing:
proc template; source Stat.KDE.Graphics.ScatterPlot; source Stat.KDE.Graphics.ScatterPlot / store=sashelp.tmplstat; quit; |
The difference is subtle but important. The two DEFINE statements are as follows:
define statgraph Stat.KDE.Graphics.ScatterPlot; define statgraph Stat.KDE.Graphics.ScatterPlot / store = SASHELP.TMPLSTAT; |
The first SOURCE statement finds the template by using the template search path, and the second SOURCE statement explicitly names the item store.
When you modify a template you have two alternatives:
1) Ensure that there is not a modified copy already by deleting the template, then list the template SAS provides.
2) Specify specifically that you want to see the source for a template in a SASHELP item store, but you will need to remove the STORE= option from the template definition before submitting it.
Furthermore, you have three independent alternatives:
1) Store the modified template in the default SASUSER item store, where it will persist to future SAS sessions.
2) Store the modified template in the a WORK item store, where it will not persist beyond this SAS session.
3) Store the modified template in some other item store.
There are no wrong answers here, and this flexibility is valuable. If you want the title of your Kaplan-Meier plots to always say "Kaplan-Meier", make a permanent template change and either store it in SASUSER or some other library that you always use. If you want a more temporary template change, store it in SASUSER and take care to delete the modified template either before you do the modification (to be safe) or after you are done using it. Or instead, store it in WORK. Either way, as you are developing and resubmitting code, you will need to delete old modifications and ensure that you are modifying a template that SAS provides and not one that you just modified. Sometimes you will find it handy to specifically specify the item store in PROC TEMPLATE code, but be aware that you might need to delete it from the DEFINE statement before submitting it to SAS.
The following step stores the PROC KDE scatter plot template, the one that SAS provides in the SASHELP.TMPLSTAT item store, into the file tpl.tpl.
proc template; source Stat.KDE.Graphics.ScatterPlot / store=sashelp.tmplstat file='tpl.tpl'; quit; |
You might need to specify the file name differently depending on your operating system and how you run SAS. I will return to that point later. The following step modifies the graph template to drop the item store specification and change the title to use variable labels instead of variable names:
data _null_; /* Standard boilerplate */ infile 'tpl.tpl' end=eof; /* Standard boilerplate */ input; /* Standard boilerplate */ if _n_ eq 1 then call execute('proc template;'); /* Standard boilerplate */ _infile_ = tranwrd(_infile_, '/ store = SASHELP.TMPLSTAT', ' '); _infile_ = tranwrd(_infile_, '_VAR1NAME " by " _VAR2NAME', '_VAR1LABEL" by " _VAR2LABEL'); call execute(_infile_); /* Standard boilerplate */ if eof then call execute('quit;'); /* Standard boilerplate */ run; /* Standard boilerplate */ |
The following step creates the scatter plot and uses the modified template:
proc kde data=sashelp.class; bivar height weight / plots=scatter; label height = 'Student Height' weight = 'Student Weight'; run; |
This particular example only works this way because PROC KDE passes in the labels as dynamic variables. Not all procedures do this. This technique of modifying templates is discussed in my Avanced ODS Graphics Examples book and also in SAS Global Forum and PharmaSUG papers. My colleage Rick Wicklin recently took up the cause of publicizing this technique, and you can read a gentle introduction in his post A SAS programming technique to modify ODS templates. Rick chooses to prepend a WORK item store rather than using a SASUSER item store. He also shows how to create a temporary file to store the template, which I illustrate next:
filename tmplt TEMP; proc template; source Stat.KDE.Graphics.ScatterPlot / store=sashelp.tmplstat file=tmplt; quit; data _null_; /* Standard boilerplate */ infile tmplt end=eof; /* Standard boilerplate */ input; /* Standard boilerplate */ if _n_ eq 1 then call execute('proc template;'); /* Standard boilerplate */ _infile_ = tranwrd(_infile_, '/ store = SASHELP.TMPLSTAT', ' '); _infile_ = tranwrd(_infile_, '_VAR1NAME " by " _VAR2NAME', '_VAR1LABEL" by " _VAR2LABEL'); call execute(_infile_); /* Standard boilerplate */ if eof then call execute('quit;'); /* Standard boilerplate */ run; /* Standard boilerplate */ proc kde data=sashelp.class; bivar height weight / plots=scatter; label height = 'Student Height' weight = 'Student Weight'; run; |
For final production code, the temporary file has a lot to recommend it. It does not leave a stray file around when it is done and it does not require a hard-coded and operating-system specific file name. However, as you are developing the code, you must look at the original template source code. You cannot modify a template without first seeing the code that you are modifying. You can submit a SOURCE statement without a FILE= option and view the code in the log. However when you do that, the line size is smaller, so you might have different line breaks. If you are going to read the template from a file in a DATA step, you need to first look at the template in that file.
I will end by showing you one of the examples that motivated this discussion. The following steps modify PROC LIFETEST's Kaplan-Meier plot (the failure plot version) to display percentages rather than proportions:
proc template; delete Stat.Lifetest.Graphics.ProductLimitFailure2; source Stat.Lifetest.Graphics.ProductLimitFailure2 / file='tpl.tpl'; quit; data _null_; infile 'tpl.tpl' end=eof; input; if _n_ eq 1 then call execute('proc template;'); _infile_ = tranwrd(_infile_, 'viewmax=1', 'viewmax=100'); _infile_ = tranwrd(_infile_, 'tickvaluelist=(0 .2 .4 .6 .8 1.0)', 'tickvaluelist=(0 20 40 60 80 100)'); _infile_ = tranwrd(_infile_, '1-', '100-100*'); _infile_ = tranwrd(_infile_, 'Failure Probability', 'Failure Percentage'); call execute(_infile_); if eof then call execute('quit;'); run; proc lifetest data=sashelp.BMT plots=survival(cb=hw failure test atrisk(outside maxlen=13)); time T * Status(0); strata Group; run; |
Notice that I delete the template before writing it to a file knowing that the first time I run the code, the modified template will not yet exist. The DATA step changes the option VIEWMAX=1 to VIEWMAX=100, it changes the tick values to range from 0 to 100, and it changes the expression for many Y axis options such as Y=EVAL(1-SURVIVAL) in the STEPPLOT statement to Y=EVAL(100-100*SURVIVAL). Finally, the DATA step changes the Y axis label. Again, you cannot write code like this without first looking at the template stored in the file tpl.tpl. For example, you must correctly specify the case and spacing when you specify the string that is to be translated.
You can delete all templates in SASUSER.TEMPLAT by submitting the following statements:
ods path sashelp.tmplmst(read); proc datasets library=sasuser nolist; delete templat(memtype=itemstor); run; ods path reset; |
You need to change and restore the ODS PATH because you cannot delete an item store while it is being used.
No matter how you access the templates that SAS provides, where you store your compiled modified template, or how you handle work files, SAS provides the tools you you need to customize your results.