SAS Macros - The Python way

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";

this code will resolve to:

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

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";

%newdata(, var=league, value=American);

%newdata(, var=league, value=National);

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”[@templates].

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;')
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";
logic = """
data {{ output}} ;
 set {{ input }};
  where {{ var }} = {{value}};
newdata = Template(logic)

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

data American ;
 where league = "American";

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='',output='American',var='league',value='"American"'))
27   ods listing close;ods html5 (id=saspy_internal) file=stdout options(bitmap_mode='inline') device=svg; ods graphics on /
27 ! outputfmt=png;
30   data American ;
31    set;
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
35   ods html5 (id=saspy_internal) close;ods listing;


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 = ('','American','league','"American"')
task2 = ('','National','league','"National"')
task3 = ('','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])
Processing:  ('', 'American', 'league', '"American"')
Processing:  ('', 'National', 'league', '"National"')
Processing:  ('', 'hits_eq_140', 'CrHits', '140')
Processing:  ('sashelp.class', 'F', 'sex', '"F"')
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.

49   ods listing close;ods html5 (id=saspy_internal) file=stdout options(bitmap_mode='inline') device=svg; ods graphics on /
49 ! outputfmt=png;
52   data National ;
53    set;
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
57   ods html5 (id=saspy_internal) close;ods listing;


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!!!


comments powered by Disqus