SAS Macros - The Python way

Using jinja2 template engine to replicate SAS macro functionality in Python

Alberto Negron true
12-07-2018

SAS has a powerful programming feature called Macros which allows us to avoid repetitive sections of code and to use them again and again when needed. At the most basic level we can do tasks such as the follow one:


%let sex=M;
data &sex;
set sashelp.class;
where sex = "&sex";
run;

this code will resolve to:


data M;
set sashelp.class;
where sex = "M";
run;

46   ods listing close;ods html5 (id=saspy_internal) file=stdout options(bitmap_mode='inline') device=svg; ods graphics on /
46 ! outputfmt=png;
NOTE: Writing HTML5(SASPY_INTERNAL) Body file: STDOUT
47
48 %let sex=M;
49 data &sex;
50 set sashelp.class;
51 where sex = "&sex";
52 run;
NOTE: There were 10 observations read from the data set SASHELP.CLASS.
WHERE sex='M';
NOTE: The data set WORK.M has 10 observations and 5 variables.
NOTE: DATA statement used (Total process time):
real time 0.00 seconds
cpu time 0.00 seconds

53
54
55 ods html5 (id=saspy_internal) close;ods listing;

56

and we can go a step further and wrap it up in a more generic-ish macro function to create new datasets based on values of a character variable:


%macro newdata(ds_in=,var=,value=);
  data &value;
   set &ds_in;
   where &var = "&value";
%mend;

%newdata(ds_in=sashelp.baseball, var=league, value=American);

125  ods listing close;ods html5 (id=saspy_internal) file=stdout options(bitmap_mode='inline') device=svg; ods graphics on /
125! outputfmt=png;
NOTE: Writing HTML5(SASPY_INTERNAL) Body file: STDOUT
126
127 %newdata(ds_in=sashelp.baseball, var=league, value=American);
NOTE: There were 175 observations read from the data set SASHELP.BASEBALL.
WHERE league='American';
NOTE: The data set WORK.AMERICAN has 175 observations and 24 variables.
NOTE: DATA statement used (Total process time):
real time 35.01 seconds
cpu time 0.40 seconds

128
129 ods html5 (id=saspy_internal) close;ods listing;

130

%newdata(ds_in=sashelp.baseball, var=league, value=National);

139  ods listing close;ods html5 (id=saspy_internal) file=stdout options(bitmap_mode='inline') device=svg; ods graphics on /
139! outputfmt=png;
NOTE: Writing HTML5(SASPY_INTERNAL) Body file: STDOUT
140
141 %newdata(ds_in=sashelp.baseball, var=league, value=National);
NOTE: There were 147 observations read from the data set SASHELP.BASEBALL.
WHERE league='National';
NOTE: The data set WORK.NATIONAL has 147 observations and 24 variables.
NOTE: DATA statement used (Total process time):
real time 19.56 seconds
cpu time 0.17 seconds

142
143 ods html5 (id=saspy_internal) close;ods listing;

144

The above is probably one of the simplest tasks one can accomplish in SAS using macros. Can we do the same in python? The short answer is YES! but before we delve into the topic I would like to make clear that python is not a full replacement for SAS macros. The idea of this post is to provide ideas and tips to SAS users on how they can take advantage of python to automate code using SASpy in a away that looks familiar.

Without further ado, let’s jump in the topic of this post and introduce the reader to the world of Template Engines.

“Template engines take in tokenized strings and produce rendered strings with values in place of the tokens as output. Templates are typically used as an intermediate format written by developers to programmatically produce one or more desired output formats, commonly HTML, XML or PDF.”

Why are template engines important?

“Template engines allow developers to generate desired content types, such as HTML, while using some of the data and programming constructs such as conditionals and for loops to manipulate the output. Template files that are created by developers and then processed by the template engine consist of prewritten markup and template tag blocks where data is inserted”(fullstackpython.com, n.d.).

There are several template engines in Python. We will be using the jinja2 template engine for 2 reasons:

Let’s see very basic example you can start getting the idea on how templates work:


from jinja2 import Template

s = "Hello {{ name }}!!!"
basic_template = Template(s)
print(basic_template.render(name = "Alberto"))

Hello Alberto!!!

Easy isn’t it? but what have we done??

Variables and/or logic are placed between tags or delimiters. Jinja2 uses {{ ... }} to replace placeholders (e.g. name) with the values passed to the render method.

Also notice that jinja2 uses {% ... %} syntax for expressions or logic (e.g. if-else and loops).

Another example:


print(basic_template.render(name = "Victoria"))

Hello Victoria!!!

now let’s do it a bit more sassy!


proc_print = Template('proc print data={{ name }}; run;')
print(proc_print.render(name='sashelp.class'))

proc print data=sashelp.class; run;

you’re getting the idea now, aren’t you?

Let’s replicate our original macro:


%macro newdata(ds_in=,var=,value=);
  data &value;
   set &ds_in;
   where &var = "&value";
%mend;

logic = """
data {{ output}} ;
 set {{ input }};
  where {{ var }} = {{value}};
run;
"""
newdata = Template(logic)

Ok, we are ready to test our template, let’s pass some params to see how it looks like:


print(newdata.render(input='sashelp.baseball',output='American',var='league',value='"American"'))

data American ;
 set sashelp.baseball;
 where league = "American";
run;

Let’s push this to saspy, shall we?


import saspy

sas = saspy.SASsession()

Using SAS Config named: default SAS Connection established. Subprocess id is 18856


s = sas.submit(newdata.render(input='sashelp.baseball',output='American',var='league',value='"American"'))

print(s['LOG'])

27   ods listing close;ods html5 (id=saspy_internal) file=stdout options(bitmap_mode='inline') device=svg; ods graphics on /
27 ! outputfmt=png;
NOTE: Writing HTML5(SASPY_INTERNAL) Body file: STDOUT
28   
29   
30   data American ;
31    set sashelp.baseball;
32     where league = "American";
33   run;
NOTE: There were 175 observations read from the data set SASHELP.BASEBALL.
      WHERE league='American';
NOTE: The data set WORK.AMERICAN has 175 observations and 24 variables.
NOTE: DATA statement used (Total process time):
      real time           0.00 seconds
      cpu time            0.01 seconds
      
34   
35   ods html5 (id=saspy_internal) close;ods listing;

36   

Hoooray!!! it worked like a charm! Now you can pass any combination or parameters to create new datasets to our template and push it to SAS!

Now, allow me to show you one more example:

Let’s say you need to accomplish the following task:

In reality the above is just 4 calls to our already defined template but what if you need to create larger number of tables based on values a given lookup table? The following code will help you accomplish this.

We are going to use a combination of list and tuples (it could have been done with list of lists too):


task1 = ('sashelp.baseball','American','league','"American"')
task2 = ('sashelp.baseball','National','league','"National"')
task3 = ('sashelp.baseball','hits_eq_140','CrHits','140')
task4 = ('sashelp.class','F','sex','"F"')

all_tasks = [task1,task2,task3,task4]
r = {} # hold results for each submit
for item in all_tasks:
    print("Processing: ",item)
    render = newdata.render(input=item[0],output=item[1],var=item[2],value=item[3])
    r[item[1]]=sas.submit(render)

Processing:  ('sashelp.baseball', 'American', 'league', '"American"')
Processing:  ('sashelp.baseball', 'National', 'league', '"National"')
Processing:  ('sashelp.baseball', 'hits_eq_140', 'CrHits', '140')
Processing:  ('sashelp.class', 'F', 'sex', '"F"')

print(r.keys())

dict_keys([‘F’, ‘American’, ‘hits_eq_140’, ‘National’])

With all results saved in the r dictionary we can easily check log for any of the submits.


print(r['National']['LOG'])

49   ods listing close;ods html5 (id=saspy_internal) file=stdout options(bitmap_mode='inline') device=svg; ods graphics on /
49 ! outputfmt=png;
NOTE: Writing HTML5(SASPY_INTERNAL) Body file: STDOUT
50   
51   
52   data National ;
53    set sashelp.baseball;
54     where league = "National";
55   run;
NOTE: There were 147 observations read from the data set SASHELP.BASEBALL.
      WHERE league='National';
NOTE: The data set WORK.NATIONAL has 147 observations and 24 variables.
NOTE: DATA statement used (Total process time):
      real time           0.01 seconds
      cpu time            0.01 seconds
      
56   
57   ods html5 (id=saspy_internal) close;ods listing;

58   

As you can see, it is really easy to create templates that accept values ala SAS macros and the only limitation is the imagination. I will cover loops and if/else statements in other posts.

Hope you have enjoyed it!!!

fullstackpython.com. n.d. “Template Engines.” https://www.fullstackpython.com/template-engines.html.

Reuse

Text and figures are licensed under Creative Commons Attribution CC BY 4.0. The figures that have been reused from other sources don't fall under this license and can be recognized by a note in their caption: "Figure from ...".

Citation

For attribution, please cite this work as

Negron (2018, Dec. 7). Data Addict's Secret Diary: SAS Macros - The Python way. Retrieved from http://www.dataaddict.me/posts/2018-12-07-sas-macros-the-python-way/

BibTeX citation

@misc{negron2018sas,
  author = {Negron, Alberto},
  title = {Data Addict's Secret Diary: SAS Macros - The Python way},
  url = {http://www.dataaddict.me/posts/2018-12-07-sas-macros-the-python-way/},
  year = {2018}
}