Have you ever been working in the macro facility and needed a macro function, but you could not locate one that would achieve your task? With the %SYSFUNC macro function, you can access most SAS® functions. In this blog post, I demonstrate how %SYSFUNC can help in your programming needs when a macro function might not exist. I also illustrate the formatting feature that is built in to %SYSFUNC. %SYSFUNC also has a counterpart called %QSYSFUNC that masks the returned value, in case special characters are returned.
%SYSFUNC enables the execution of SAS functions and user-written functions, such as those created with the FCMP procedure. Within the DATA step, arguments to the functions require quotation marks, but because %SYSFUNC is a macro function, you do not enclose the arguments in quotation marks. The examples here demonstrate this.

%SYSFUNC has two possible arguments. The first argument is the SAS function, and the second argument (which is optional) is the format to be applied to the value returned from the function. Suppose you had a report and within the title you wanted to issue today’s date in word format:

 title "Today is %sysfunc(today(),worddate20.)";

The title appears like this:

 "Today is July 4, 2018"

Because the date is right-justified, there are leading blanks before the date. In this case, you need to introduce another function to remove the blank spaces. Luckily %SYSFUNC enables the nesting of functions, but each function that you use must have its own associated %SYSFUNC. You can rewrite the above example by adding the STRIP function to remove any leading or trailing blanks in the value:

 title "Today is %sysfunc(strip(%sysfunc(today(),worddate20.)))";

The title now appears like this:

 "Today is July 4, 2018"

The important thing to notice is the use of two separate functions. Each function is contained within its own %SYSFUNC.

Suppose you had a macro variable that contained blank spaces and you wanted to remove them. There is no macro COMPRESS function that removes all blanks. However, with %SYSFUNC, you have access to one. Here is an example:

 %let list=a b c; %put %sysfunc(compress(&list));

The value that is written to the log is as follows:

 abc

In this last example, I use %SYSFUNC to work with SAS functions where macro functions do not exist.

The example checks to see whether an external file is empty. It uses the following SAS functions: FILEEXIST, FILENAME, FOPEN, FREAD, FGET, and FCLOSE. There are other ways to accomplish this task, but this example illustrates the use of SAS functions within %SYSFUNC.

 %macro test(outf); %let filrf=myfile;   /* The FILEEXIST function returns a 1 if the file exists; else, a 0 is returned. The macro variable &OUTF resolves to the filename that is passed into the macro. This function is used to determine whether the file exists. In this case you want to find the file that is contained within &OUTF. Notice that there are no quotation marks around the argument, as you will see in all cases below. If the condition is false, the %ELSE portion is executed, and a message is written to the log stating that the file does not exist.*/   %if %sysfunc(fileexist(&outf)) %then %do;   /* The FILENAME function returns 0 if the operation was successful; else, a nonzero is returned. This function can assign a fileref for the external file that is located in the &OUTF macro variable. */   %let rc=%sysfunc(filename(filrf,&outf));   /* The FOPEN function returns 0 if the file could not be opened; else, a nonzero is returned. This function is used to open the external file that is associated with the fileref from &FILRF. */   %let fid=%sysfunc(fopen(&filrf));   /* The %IF macro checks to see whether &FID has a value greater than zero, which means that the file opened successfully. If the condition is true, we begin to read the data in the file. */   %if &fid > 0 %then %do;   /* The FREAD function returns 0 if the read was successful; else, a nonzero is returned. This function is used to read a record from the file that is contained within &FID. */   %let rc=%sysfunc(fread(&fid));   /* The FGET function returns a 0 if the operation was successful. A returned value of -1 is issued if there are no more records available. This function is used to copy data from the file data buffer and place it into the macro variable, specified as the second argument in the function. In this case, the macro variable is MYSTRING. */   %let rc=%sysfunc(fget(&fid,mystring));   /* If the read was successful, the log will write out the value that is contained within &MYSTRING. If nothing is returned, the %ELSE portion is executed. */   %if &rc = 0 %then %put &mystring; %else %put file is empty;   /* The FCLOSE function returns a 0 if the operation was successful; else, a nonzero value is returned. This function is used to close the file that was referenced in the FOPEN function. */   %let rc=%sysfunc(fclose(&fid)); %end;   /* The FILENAME function is used here to deassign the fileref FILRF. */   %let rc=%sysfunc(filename(filrf)); %end; %else %put file does not exist; %mend test; %test(c:\testfile.txt)

There are times when the value that is returned from the function used with %SYSFUNC contains special characters. Those characters then need to be masked. This can be done easily by using %SYSFUNC’s counterpart, %QSYSFUNC. Suppose we run the following example:

 %macro test(dte); %put &dte; %mend test;   %test(%sysfunc(today(), worddate20.))

The above code would generate an error in the log, similar to the following:

 1 %macro test(dte); 2 %put &dte; 3 %mend test; 4 5 %test(%sysfunc(today(), worddate20.)) MLOGIC(TEST): Beginning execution. MLOGIC(TEST): Parameter DTE has value July 20 ERROR: More positional parameters found than defined. MLOGIC(TEST): Ending execution.

The WORDDATE format would return the value like this: July 20, 2017. The comma, to a parameter list, represents a delimiter, so this macro call is pushing two positional parameters. However, the definition contains only one positional parameter. Therefore, an error is generated. To correct this problem, you can rewrite the macro invocation in the following way:

 %test(%qsysfunc(today(), worddate20.))

The %QSYSFUNC macro function masks the comma in the returned value so that it is seen as text rather than as a delimiter.

For a list of the functions that are not available with %SYSFUNC, see the “How to expand the number of available SAS functions within the macro language was published on SAS Users.

The release of SAS Viya 3.3 has brought some nice data quality features. In addition to the visual applications like Data Studio or Data Explorer that are part of the Data Preparation offering, one can leverage data quality capabilities from a programming perspective.

For the time being, SAS Viya provides two ways to programmatically perform data quality processing on CAS data:

• The Data Step Data Quality functions.
• The profile CAS action.

To use Data Quality programming capabilities in CAS, a Data Quality license is required (or a Data Preparation license which includes Data Quality).

### Data Step Data Quality functions

The list of the Data Quality functions currently supported in CAS are listed here and below:

They cover casing, parsing, field extraction, gender analysis, identification analysis, match codes and standardize capabilities.

As for now, they are only available in the CAS Data Step. You can’t use them in DS2 or in FedSQL.

To run in CAS certain conditions must be met. These include:

• Both the input and output data must be CAS tables.
• All language elements must be supported in the CAS Data Step.
• Others.

Let’s look at an example:

cas mysession sessopts=(caslib="casuser") ;   libname casuser cas caslib="casuser" ;   data casuser.baseball2 ; length gender $1 mcName parsedValue tokenNames lastName firstName varchar(100) ; set casuser.baseball ; gender=dqGender(name,'NAME','ENUSA') ; mcName=dqMatch(name,'NAME',95,'ENUSA') ; parsedValue=dqParse(name,'NAME','ENUSA') ; tokenNames=dqParseInfoGet('NAME','ENUSA') ; if _n_=1 then put tokenNames= ; lastName=dqParseTokenGet(parsedValue,'Family Name','NAME','ENUSA') ; firstName=dqParseTokenGet(parsedValue,'Given Name','NAME','ENUSA') ; run ; Here, my input and output tables are CAS tables, and I’m using CAS-enabled statements and functions. So, this will run in CAS, in multiple threads, in massively parallel mode across all my CAS workers on in-memory data. You can confirm this by looking for the following message in the log: NOTE: Running DATA step in Cloud Analytic Services. NOTE: The DATA step will run in multiple threads. I’m doing simple data quality processing here: • Determine the gender of an individual based on his(her) name, with the dqGender function. • Create a match code for the name for a later deduplication, with the dqMatch function. • Parse the name using the dqParse function. • Identify the name of the tokens produced by the parsing function, with the dqParseInfoGet function. • Write the token names in the log, the tokens for this definition are: Prefix,Given Name,Middle Name,Family Name,Suffix,Title/Additional Info • Extract the “Family Name” token from the parsed value, using dqParseTokenGet. • Extract the “Given Name” token from the parsed value, again using dqParseTokenGet. I get the following table as a result: Performing this kind of data quality processing on huge tables in memory and in parallel is simply awesome! ### The dataDiscovery.profile CAS action This CAS action enables you to profile a CAS table: • It offers 2 algorithms, one is faster but uses more memory. • It offers multiple options to control your profiling job: • Columns to be profiled. • Number of distinct values to be profiled (high-cardinality columns). • Number of distinct values/outliers to report. • It provides identity analysis using RegEx expressions. • It outputs the results to another CAS table. The resulting table is a transposed table of all the metrics for all the columns. This table requires some post-processing to be analyzed properly. Example: proc cas; dataDiscovery.profile / algorithm="PRIMARY" table={caslib="casuser" name="product_dim"} columns={"ProductBrand","ProductLine","Product","ProductDescription","ProductQuality"} cutoff=20 frequencies=10 outliers=5 casOut={caslib="casuser" name="product_dim_profiled" replace=true} ; quit ; In this example, you can see: • How to specify the profiling algorithm (quite simple: PRIMARY=best performance, SECONDARY=less memory). • How to specify the input table and the columns you want to profile. • How to reduce the number of distinct values to process using the cutoff option (it prevents excessive memory use for high-cardinality columns, but might show incomplete results). • How to reduce the number of distinct values reported using the frequencies option. • How to specify where to store the results (casout). So, the result is not a report but a table. The RowId column needs to be matched with A few comments/cautions on this results table: • DoubleValue, DecSextValue, or IntegerValue fields can appear on the output table if numeric fields have been profiled. • DecSextValue can contain the mean (metric #1008), median (#1009), standard deviation (#1022) and standard error (#1023) if a numeric column was profiled. • It can also contain frequency distributions, maximum, minimum, and mode if the source column is of DecSext data type which is not possible yet. • DecSext is a 192-bit fixed-decimal data type that is not supported yet in CAS, and consequently is converted into a double most of the time. Also, SAS Studio cannot render correctly new CAS data types. As of today, those metrics might not be very reliable. • Also, some percentage calculations might be rounded due to the use of integers in the Count field. • The legend for metric 1001 is not documented. Here it is: 1: CHAR 2: VARCHAR 3: DATE 4: DATETIME 5: DECQUAD 6: DECSEXT 7: DOUBLE 8: INT32 9: INT64 10: TIME A last word on the profile CAS action. It can help you to perform some identity analysis using patterns defined as RegEx expressions (this does not use the QKB). Here is an example: proc cas; dataDiscovery.profile / table={caslib="casuser" name="customers"} identities={ {pattern="PAT=-]? ?999[- ]9999",type="USPHONE"}, {pattern= "PAT=^99999[- ]9999$",type="ZIP4"}, {pattern= "PAT=^99999$",type="ZIP"}, {pattern= "[^ @]+@[^ @]+\.[A-Z]{2,4}",type="EMAIL"}, {pattern= "^(?i:A[LKZR]|C[AOT]|DE|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY])$",type="STATE"} } casOut={caslib="casuser" name="customers_profiled" replace="true"} ; quit ;

I hope this post has been helpful.

SAS Data Studio is a new application in SAS Viya 3.3 that provides a mechanism for performing simple, self-service data preparation tasks to prepare data for use in SAS Visual Analytics or other applications. It is accessed via the Prepare Data menu item or tile on SAS Home. Note: A user must belong to the Data Builders group in order to have access to this menu item.

In SAS Data Studio, you can either select to create a new data plan or open an existing one. A data plan starts with a source table and consists of transforms (steps) that are performed against that table. A plan can be saved and a target table can be created based on the transformations applied in the plan.

SAS Data Studio

In a previous blog post, I discussed the Data Quality transforms in SAS Studio.  This post is about the Code transform which enables you to create custom code to perform actions or transformations on a table. To add custom code using the Code transform, select the code language from the drop-down menu, and then enter the code in the text box.  The following code languages are available: CASL or DATA step.

Code Transform in SAS Data Studio

Each time you run a plan, the table and library names might change. To avoid errors, you must use variables in place of table and caslib names in your code within SAS Data Studio. Indicating variables in place of table and library names eliminates the possibility that the code will fail due to name changes.  Errors will occur if you use literal values. This is because session table names can change during processing.  Use the following variables:

• _dp_inputCaslib – variable for the input CAS library name.
• _dp_inputTable – variable for the input table name.
• _dp_outputCaslib – variable for the output CAS library name.
• _dp_outputTable –  variable for the output table name.

Note: For DATA step only, variables must be enclosed in braces, for example, data {{_dp_outputTable}} (caslib={{_dp_outputCaslib}});.

The syntax of “varname”n is needed for variable names with spaces and/or special characters.  Refer to the Avoiding Errors When Using Name Literals help topic for more Information.  There are also several

CASL Code Example

The CASL code example above uses the ActionSet fedSQL to create a summary table of counts by the standardized State value.  The results of this code are pictured below.

Results from CASL Code Example

DATA Step Code Example

In this DATA step code example above, the BY statement is used to group all records with the same BY value. If you use more than one variable in a BY statement, a BY group is a group of records with the same combination of values for these variables. Each BY group has a unique combination of values for the variables.  On the CAS server, there is no guarantee of global ordering between BY groups. Each DATA step thread can group and order only the rows that are in the same data partition (thread).  Refer to the help topic

Results from DATA Step Code Example

For more information about DATA step, refer to the In my next blog post, I will review some more code examples that you can use in the Code transform in SAS Data Studio. For more information on SAS Data Studio and the Code transform, please refer to this SAS Data Studio Code Transform (Part 1) was published on SAS Users.

It is not laziness—it is efficiency!!! Programmers are often called lazy; we even call ourselves lazy. But we are not lazy, we are just being efficient. It makes no sense to type the same code over and over again or use more keystrokes than are absolutely necessary.

### Keyboard Macros

You might not have heard of keyboard macros. Or, perhaps, you do not know how they could help you. I am very fond of keyboard macros; let me show you why!

In SAS Technical Support, supporting the SAS® Output Delivery System (ODS) and Base SAS® procedures, I often use the same statements to set up test programs. For example, I want any style templates that I create to go into the Work directory. I also use the same data set name all of the time. I have created keyboard macros for the statements, data set names, and options that I use daily.

When I press Ctrl+Alt+w, the following is inserted into my program:

ods path(prepend) work.templat(update);

When I press Ctrl+Alt+p, the following is inserted into my program:

sashelp.class

How did I do that? I recorded a keyboard macro that contains the code that I want. Then, I assigned keys that insert the code when I press them.

Here are the steps for recording your very own keyboard macro in the SAS Enhanced Editor:

1.  Select Tools ► Keyboard Macros ► Record New Macro.

2.  Enter the code that you want to be your new keyboard macro. Consider typing slowly because any backspaces that you use are included in the recording.

3.  After you are done entering text, you need to tell SAS to stop recording. Select Tools ►Keyboard Macros ►Stop Recording.

4.  A pop-up dialog box appears that lets you give the new macro a name and assign the keys that you want to be associated with the macro. You can set the key combination that make sense to you. Just make sure that you do not use a combination that is already assigned to another macro.

Now, whenever you need to insert that piece of code, just use the keys that you assigned!

In SAS® Enterprise Guide®, you can find keyboard macros under Program ► Editor Macros, instead of the Tools drop-down menu. The recording and key assignment steps are the same in both applications.

You can also create keyboard macros that perform tasks.

The Macros selection opens a pop-up dialog box that contains a Create button.

Clicking Create opens another dialog box.

With the Categories option set to All, you can see all of the commands that are already available. Moving these over to the Keyboard macro contents section enables you to build a macro that performs a task that you need to accomplish on a regular basis.

For example, I have combined these commands to select a whole block of code, like from the PROC statement down to the RUN statement.

Keyboards macros are available in the Enhanced Editor in Display Manager SAS (DMS) and in SAS Enterprise Guide. They cannot be used with the Program Editor in DMS or in SAS® Studio.

You can export and import keyboard macros. The file created when you export has the .kmf extension. You can find the options for importing and exporting in the Macros dialog box. You can share your keyboard macros with your friends, or just to keep them as a backup copy in case you need to reinstall SAS.

For more information, see the Using "Keyboard Macros" section in "Using the Enhanced Editor."

### Function Keys

You have probably used the F8 key to submit your program, or the F4 key to recall your last program. Did you know that you can set or change those instructions?

In the Enhanced Editor, you can get the list of assigned keys by entering keys into the command bar or by selecting Keys under Tools ► Options.

I test a lot, which means that I am routinely clearing the log, the results viewer, and the output window. I have assigned an F key, F12, to clear everything and bring the focus back to the Enhanced Editor (see the commands in the screenshot below). I have to press only one key to clean everything up! I use the F12 key over and over again.

The keys that you assign in DMS are valid from both the Enhanced Editor and the Program Editor.

SAS Enterprise Guide includes a large number of commands by default. A lot of them already have keys assigned, but some do not. You can see the list of the commands and their assigned keys by selecting Enhanced Editor Keys under the Program drop-down menu.

Currently, it is not possible to modify the function keys in SAS Studio. However, a number of keys are already defined that you might find useful. You can see the function key shortcuts by clicking the question mark in the upper right, choosing SAS Studio help, and then selecting the option for Accessibility Features. Here are links to additional resources:

I highly recommend using keyboard macro and function keys. Why type the same thing over and over again? Increase your productivity by handing the repetitive tasks over to SAS.

Are you interested in using SAS Visual Analytics 8.2 to visualize a state by regions, but all you have is a county shapefile?  As long as you can cross-walk counties to regions, this is easier to do than you might think.

Here are the steps involved:

Step 1

Obtain a county shapefile and extract all components to a folder. For example, I used the US Counties shapefile found in this SAS Visual Analytics community post.

Note: Shapefile is a geospatial data format developed by ESRI. Shapefiles are comprised of multiple files. When you unzip the shapefile found on the community site, make sure to extract all of its components and not just the .shp. You can get more information about shapefiles from this Wikipedia article:  https://en.wikipedia.org/wiki/Shapefile.

Step 2

Run PROC MAPIMPORT to convert the shapefile into a SAS map dataset.

libname geo 'C:\Geography'; /*location of the extracted shapefile*/   proc mapimport datafile="C:\Geography\UScounties.shp" out=geo.shapefile_counties; run;

Step 3

Add a Region variable to your SAS map dataset. If all you need is one state, you can subset the map dataset to keep just the state you need. For example, I only needed Texas, so I used the State_FIPS variable to subset the map dataset:

proc sql; create table temp as select *, /*cross-walk counties to regions*/ case when name='Anderson' then '4' when name='Andrews' then '9' when name='Angelina' then '5' when name='Aransas' then '11', <……> when name='Zapata' then '11' when name='Zavala' then '8' end as region from geo.shapefile_counties /*subset to Texas*/ where state_fips='48'; quit;

Step 4

Use PROC GREMOVE to dissolve the boundaries between counties that belong to the same region. It is important to sort the county dataset by region before you run PROC GREMOVE.

proc sort data=temp; by region; run;   proc gremove data=temp out=geo.regions_shapefile nodecycle; by region; id name; /*name is county name*/ run;

Step 5

To validate that your boundaries resolved correctly, run PROC GMAP to view the regions. If the regions do not look right when you run this step, it may signal an issue with the underlying data. For example, when I ran this with a county shapefile obtained from Census, I found that some of the counties were mislabeled, which of course, caused the regions to not dissolve correctly.

proc gmap map=geo.regions_shapefile data=geo.regions_shapefile all; id region; choro region / nolegend levels=1; run;

Here’s the result I got, which is exactly what I expected:

Step 6

Add a sequence number variable to the regions dataset. SAS Visual Analytics 8.2 needs it properly define a custom polygon inside a report:

data geo.regions_shapefile; set geo.regions_shapefile; seqno=_n_; run;

Step 7

Load the new region shapefile in SAS Visual Analytics.

Step 8

In the dataset with the region variable that you want to visualize, create a new geography variable and define a new custom polygon provider.

Geography Variable:

Polygon Provider:

Step 9

Now, you can create a map of your custom regions:

If Necessity is the mother of Invention, then, perhaps, the father of Automation is Laziness. Automation is all about convenience, comfort, and productivity. Why do it yourself if you can devise something to do it for you!

In my previous post Running SAS programs in batch under Unix/Linux, we learned that in order to automate SAS jobs submissions, they are often run in batch mode. We also learned that we usually create batch scripts as a convenient way to run SAS programs in batch. To create a unique SAS log file generated with each batch submission, a typical batch script may look like follows:

#!/usr/bin/sh dtstamp=$(date +%Y.%m.%d_%H.%M.%S) pgmname="/sas/code/project1/program1.sas" logname="/sas/code/project1/program1_$dtstamp.log" /sas/SASHome/SASFoundation/9.4/sas $pgmname -log$logname

It will allow you to submit your SAS program /sas/code/project1/program1.sas in batch, and also capture SAS log file with a convenient date-time suffix in the same directory.

## SAS program to write batch scripts

But what if we are deploying multiple SAS programs? Well, then we would need to create a batch script for each of them. They will all look similar to each other, and that is when most human errors usually occur – when we do something similar, monotonously, over and over again.  Besides, I found working with the Unix Visual Editor (“vi editor”) is not quite a 21st century experience.

What would a normal SAS programmer do in such a situation? That’s right – we would write a SAS program to write a batch script file! Let’s do it. Let’s automate the automation.

In its simplest form, to replicate the above batch script example our SAS program would look like this:

filename b '/sas/code/project1/program1.sh';   data _null_; file b; input; put _infile_; datalines; #!/usr/bin/sh dtstamp=$(date +%Y.%m.%d_%H.%M.%S) pgmname="/sas/code/project1/program1.sas" logname="/sas/code/project1/program1_$dtstamp.log" /sas/SASHome/SASFoundation/9.4/sas $pgmname -log$logname ;

## Setting up batch file permissions

As we already know from my previous post, we need to assign certain permissions to our batch file in order to make it executable. For example, if you want to give yourself (Owner) and Group execution permissions then your script file permissions can be as:

-rwxr-x---, or 750 in octal representation.

In order to do that you can to add to your SAS code the following x-statement:

options noxwait; x 'chmod 750 /sas/code/project1/program1.sh';

Alternatively, you can use %SYSEXEC macro statement (no quoting for the OS command) or SYSTASK statement, or CALL SYSTEM routine (used within a data step).

When you create a batch script by running the above code in SAS Enterprise Guide (EG), you don’t have to leave the comfort of your SAS environment or even touch Unix vi editor. Moreover, you can even submit your SAS job in batch mode right from your SAS EG Program Editor.

However, all of these will work fine, unless XCMD System Option is disabled (NOXCMD).

## Assigning batch file permissions when XCMD System Option is disabled

ERROR: Shell escape is not valid in this SAS session.

Bummer! Have you ever seen this error message in SAS Enterprise Guide while trying to run SAS code with the X statement? It indicates that executing OS commands in the SAS environment is not allowed.

In many organizations, IT department policies do not allow enabling the SAS XCMD system option due to cyber-security concerns. This is usually done system-wide for the whole SAS Enterprise client-server installation via SAS configuration.  In this case, no operating system command is allowed to be executed from within SAS.

Of course, this substantially limits SAS’ automation power, but that is the goal and the price to pay for enhanced security.

Still, even without OS command execution at our disposal, we can set Unix script file permissions using FILENAME statement’s PERMISSION= option. Then our above filename statement will look like this:

filename b '/sas/code/project1/program1.sh' permission='A::u::rwx,A::g::r-x,A::o::---';

However, it is important to realize that your ability to fully control file permissions via FILENAME statement’s PERMISSION= option is still restricted by the Unix umask value set by your IT system administrator. But usually, it is not overly restrictive, at least for the purpose of creating executable files in the environments I have worked with.

The double benefit of the FILENAME statement’s PERMISSION= option is that it can be used for setting up file permissions in any SAS installation whether the XCMD system option is enabled or disabled.

## SAS macro to create batch script files

Let’s wrap all the above SAS code pieces into a SAS macro that writes batch scripts. Here is the macro code definition:

%macro write_shell(code); %let fdir = %substr(&code,1,%sysfunc(findc(&code,/,b)));   options dlcreatedir; libname _flib "&fdir"; libname _flib;   %let core = %substr(&code,1,%eval(%length(&code)-4)); filename _fout "&core..sh" permission='A::u::rwx,A::g::r-x,A::o::---'; data _null_; file _fout; put '#!/bin/sh' // 'now=$(date +%Y.%m.%d_%H.%M.%S)' / "pgmname=""&code""" / "logname=""&core._$now.log""" / 'sas $pgmname -log$logname' ; run; filename _fout; %mend write_shell;

The single macro parameter (code) represents full path name of your SAS code. And here is a macro invocation example:

  %write_shell(/sas/code/project1/program1.sas)

The assumption here is that the script file gets created in the same directory as the relevant SAS code and SAS logs for each of the batch runs. It will be assigned the same name as your SAS program, only with the .sh name extension. As you can see, we do some string parsing to derive directory name, script file name and SAS log file name from the single macro parameter representing full path name of your SAS code. As an added bonus, if a specified directory (/sas/code/project1/) does not yet exist, it will be created by this macro. DLCREATEDIR System Option (along with the two subsequent libname statements) are responsible for the directory creation.

If you want to create many script files for your multiple SAS programs, you just invoke the macro as many times. You can even go totally data-driven for mass script file creation.

## Do you find this useful?

Please let me know in the comments section below if you find this blog post useful. Thank you for reading! I also invite you to share your ideas and experiences on the topic.

Let SAS write batch scripts for you was published on SAS Users.

Most people who work with optimization are familiar with Linear and Integer Programming, to their toolkit they could add Constraint Programming. Constraint Programming is a powerful technique that is used to solve powerful “real-world” problems in a variety of areas, such as, planning, scheduling, DNA Sequencing, computer graphics and natural language processing.

Constraint Programming is a powerful paradigm which can be used by itself or in combination with Integer Programming. In this article, I’ll show you how to implement a simple Constraint Programming example that solves Sudoku puzzles using the CLP functionality in SAS Optimization.

Have you ever wondered after working in a particularly difficult Sudoku puzzle if the puzzle can be solved? Would you like to schedule your child’s little league games like a pro using the Round-Robin tournament format, just like it is done in professional sport leagues?

If so, Constraint Programming is the answer. But what is Constraint Programming?  Let’s start answering this question by reviewing the familiar Linear and Integer Programming formulations and then comparing them with the one for Constraint Programming.

Most people have heard about Linear Programming and Integer Programming, where the typical mathematical structure for an Integer Programming model is:

Max    c1x1+ c2x2+ … + cnxn

Subject to
a11x1 + a12x2+ … + a1nxnb1
….
an1x1+ an2x2+ … + annxnbn

xj  integer for  all j = 1 to n

These equations describe a problem where the goal (or objective) is to maximize a metric that is related to a set of variables (x1, …, xn) to be determined by solving the problem. The goal (or objective) to be maximized could be, for example, profit, amount of food distributed, etc. The set of variables are related to the goal, and in a typical marketing problem would represent marketing campaigns, customer response, channels used to distribute those campaigns, etc. Constraints are the rules that relate the variables to the available resources to solve the problem. In a marketing problem, b1 could represent the available budget, …, bn could represent the capacity of the call center.

When all variables are continuous we have a linear program; when some of the variables must be integers, we have a mixed integer programming problem. Notice that the constraints in the formulation above simply describe a logical relationship among several variables. Because each variable must take an integer value, their domain is the set of integers.

In Constraint Programming the relationships between variables are stated in the form of constraints. Constraints specify the properties of a solution to be found. A key insight for Constraint Programming is to understand that a constraint is simply a logical relationship among several finite unknowns (or variables), each taking a value in a finite domain. A constraint thus restricts the possible values that the variables can simultaneously take, it represents some partial information about the variables of interest.

An example of a scheduling problem described using the Constraint Programming approach is below All tasks relationships are of type “FS” which means “finish-to-start” and can be used to indicate which task precedes another one:

Forall (j in Jobs)

/* Indicates which task precedes another one */

forall ( j in Jobs)

/* Indicates which tools to be used */

In this scheduling problem, the goal is to find the task sequence for each job while satisfying the constraints on task precedence and tool availability.

More formally, a Constraint Program can be defined using a triple X, D, C, where

• X = { X1, …, Xn}  is a finite set of variables
• D = {D1, …, Dn}  is a finite set of domains, where Di is a finite set of possible values that the variable Xi can take. Di is known as the domain of variable Xi
• C = {C1, …, Cn}  is a finite set of constraints that restrict the values that the variables can simultaneously take.

Constraint solvers find an assignment to the variables that satisfies all the constraints using constraint propagation, backtracking, branch and bound algorithms or local search. There are many specialized resources (books, articles, etc.) that describe these methods.

Many times for complex problems, a hybrid approach is used, that is, an approach that uses Integer Programming, Constraint Programming and Heuristic procedures.

Let’s solve the simple Send More Money and the Sudoku puzzles to make clear the formal Constraint Program formulation given above.

### Send More Money Puzzle

The Send More Money puzzle consists of finding unique digits for the letters D, E, M, N, O, R, S, and Y such that S and M are different from zero (no leading zeros) and the following equation is satisfied:

S E N D
+   M O R E

M O N E Y

Step #1: Define the variables:

S, E, N, D, M, O, R, E, Y

Step #2: Define the Domain of those variables

1. S, E, N, D, M, O, R, E, Y must take integer values between 1 and 9
2. S can’t be zero
3. M can’t be zero

Step #2: Define the Domain of those variables

1. S * 1000 + E * 100 + N * 10 + D + M * 1000 + O * 100 + R * 10 + E =
10000 * M + O * 1000 + N * 100 + E * 10 + Y
2. All variables must be different

The unique solution to this problem is

 S E N D M O R Y 9 5 6 7 1 0 8 2

And can be found using the CLP procedure in SAS Optimization, with this code

proc clp dom=[0,9] /* Define the default domain */ out=out; /* Name the output data set */ var S E N D M O R Y; /* Declare the variables */ lincon /* Linear constraints */   /* SEND + MORE = MONEY */ 1000*S + 100*E + 10*N + D + 1000*M + 100*O + 10*R + E = 10000*M + 1000*O + 100*N + 10*E + Y,       S<>0, M<>0; /* No leading zeros */   alldiff(); /* All variables have pairwise distinct values*/ run;

### The Sudoku Puzzle

We are searching for 81 variables that are arranged in a 9×9 matrix, let Cij represent the value of the cell in the ith row and the jth column, where i=1, …, 9 and j=1, …, 9

Step # 2: Define the Domain of those variables

Cij can take any integer value between 1 and 9

Step # 3: Define the Constraints.

1. For each row i, all values in that row must be different.
2. For each column j, all values in that column must be different.
3. For each 3×3 block Bb all values in that block must be different.

Then the solution is

This solution can be found using the CLP procedure in SAS Optimization, with this code (note that the initial puzzle is entered in the step data indata and the final solution is nicely printed with the macro printSol).

data indata; input C1-C9; datalines; . . 5 . . 7 . . 1 . 7 . . 9 . . 3 . . . . 6 . . . . . . . 3 . . 1 . . 5 . 9 . . 8 . . 2 . 1 . . 2 . . 4 . . . . 2 . . 6 . . 9 . . . . 4 . . 8 . 8 . . 1 . . 5 . . ; run; %macro store_initial_values; /* store initial values into macro variable C_i_j */ data _null_; set indata;   array C{9}; do j = 1 to 9; i = _N_; call symput(compress("C_" || put(i,best.) || "_" || put(j,best.)), put(C[j],best.)); end; run; %mend store_initial_values; %store_initial_values;   %macro solve; proc clp out=outdata;   %do i = 1 %to 9; var (X_&i._1-X_&i._9) = [1,9]; alldiff(X_&i._1-X_&i._9); %end;   %do j = 1 %to 9; alldiff( %do i = 1 %to 9; X_&i._&j %end; ); %end;   %do s = 0 %to 2; %do t = 0 %to 2; alldiff( %do i = 3*&s + 1 %to 3*&s + 3; %do j = 3*&t + 1 %to 3*&t + 3; X_&i._&j %end; %end; ); %end; %end;     %do i = 1 %to 9; %do j = 1 %to 9; %if &&C_&i._&j ne . %then %do; lincon X_&i._&j = &&C_&i._&j; %end; %end; %end; run; %put &_ORCLP_; %mend solve; %solve;   %macro printSol; data final (keep= A1 A2 A3 A4 A5 A6 A7 A8 A9); set outdata; array A{9}; %do i = 1 %to 9; %do j = 1 %to 9; A(&j)=X_&i._&j; %end; output ; %end; run; %mend printSol; %printSol;

### Conclusion

Every optimization person could benefit from using Constraint programming. It is a powerful tool, which can be used in hybrid approaches with Integer Programming and heuristic procedures.

This article and accompanying technical white paper are written to help SAS 9 users process existing SAS 9 code multi-threaded in SAS Viya 3.3.

### The Future is Multi-threaded Processing Using SAS® Viya®

When I first began researching how to run SAS 9 code as multi-threaded in SAS Viya, I decided to compile all the relevant information and detail the internal processing rules that guide how the SAS Viya Cloud Analytics Services (CAS) server handles code and data. What I found was that there are a simple set of guidelines, which if followed, help ensure that most of your existing SAS 9 code will run multi-threaded. I have stashed a lot of great information into a single whitepaper that is available today called “Getting Your SAS 9 Code to Run Multi-threaded in SAS Viya”.

Starting with the basic distinctions between single and parallel processing is key to understanding why and how some of the parallel processing changes have been implemented. You see, SAS Viya technology constructs code so that everything runs in pooled memory using multiple processors. Redefining SAS for this parallel processing paradigm has led to huge gains in decreasing program run-times, as well as concomitant increases in accuracy for a variety of machine learning techniques. Using SAS Viya products helps revolutionize how we think about undertaking large-scale work because now we can complete so many more tasks in a fraction of the time it took before.

The new SAS Viya products bring a ton of value compared to other choices you might have in the analytics marketplace. Unfortunately most open source libraries and packages, especially those developed for use in Python and R, are limited to single-threading. SAS Viya offers a way forward by coding in these languages using an alternative set of SAS objects that can run as parallel, multi-threaded, distributed processes. The real difference is in the shared memory architecture, which is not the same as parallel, distributed processing that you hear claimed from most Hadoop and cloud vendors. Even though parallel, distributed processing is faster than single-threading, it proverbially hits a performance wall that is far below what pooled and persisted data provides when using multi-threaded techniques and code.

For these reasons, I believe that SAS Viya is the future of data/decision science, with shared memory running against hundreds if not thousands of processors, and returning results back almost instantaneously. And it’s not for just a handful of statistical techniques. I’m talking about running every task in the analytics lifecycle as a multi-threaded process, meaning end-to-end processing through data staging, model development and deployment, all potentially accomplished through a point-and-click interface if you choose. Give SAS Viya a try and you will see what I am talking about. Oh, and don’t forget to read my technical white paper that provides a checklist of all the things that you may need to consider when transitioning your SAS 9 code to run multi-threaded in SAS Viya!

If you have any questions about the details found in this paper, feel free to leave them in the comments field below.

The

 data myanno; length function color $8; function='move'; x=0; y=0; output; function='draw'; x=100; y=100; color='red'; output; run; proc gplot data=sashelp.cars; plot mpg_highway*cylinders / vaxis=axis1 haxis=axis2 annotate=myanno; symbol1 interpol=none value=dot color=blue; axis1 label=(angle=90); axis2 offset=(2,2)pct; run; quit; The following annotate errors are written to the SAS log when I run this code: NOTE: ERROR DETECTED IN ANNOTATE= DATASET WORK.MYANNO. NOTE: PROBLEM IN OBSERVATION 2 - A CALCULATED COORDINATE LIES OUTSIDE THE VISIBLE AREA X A CALCULATED COORDINATE LIES OUTSIDE THE VISIBLE AREA Y Here is the resulting graph: The annotated line is drawn outside the axis area. But why? I defined my X and Y coordinates for the MOVE and DRAW functions correctly, did I not?  data myanno; length function color$8; retain xsys ysys '1'; function='move'; x=0; y=0; output; function='draw'; x=100; y=100; color='red'; output; run;   proc gplot data=sashelp.cars; plot mpg_highway*cylinders / vaxis=axis1 haxis=axis2 annotate=myanno; symbol1 interpol=none value=dot color=blue; axis1 label=(angle=90); axis2 offset=(2,2)pct; run; quit;

Here is the graph containing the correct line:

### Creating Multiple Graphs with an Annotate Data Set, BY-and-BY

The need to generate multiple graphs from one procedure using a BY statement is very common. However, using an Annotate data set with a BY statement can be a little tricky. Here are the general rules for using an Annotate data set with a SAS/GRAPH procedure that creates multiple graphs with a BY statement:

1. Make sure that the Annotate data set and the input data set for the procedure include the same BY variables. The BY variables must also be the same data type in both data sets.
2. Both the Annotate data set and the input data set must be sorted by the BY variables.
3. Include the ANNOTATE= (or ANNO=) option in the action statement of the SAS/GRAPH procedure.

The goal of the following program is to create two graphs using a BY statement in which the annotation is specific to each graph. The Annotate data set draws the maximum MPG_Highway value at the maximum point for each X value.

/* Compute the maximum MPG_Highway values */ proc sort data=sashelp.cars(where=(origin in('USA' 'Europe'))) out=cars; by origin cylinders; run;   proc means data=cars noprint; by origin cylinders; var mpg_highway; output out=meansout max=max; run;   data myanno; length function color text $8; retain xsys ysys '2' color 'black' position '2' size 1.5; set meansout; function='label'; x=cylinders; y=max; text=strip(max); output; run; proc gplot data=cars annotate=myanno; by origin; plot mpg_highway*cylinders / vaxis=axis1 haxis=axis2; symbol1 interpol=none value=dot color=blue; axis1 label=(angle=90); axis2 offset=(2,2)pct; run; quit; Here are the resulting graphs: There are two issues here. First, there should be only one maximum value displayed for each X value. There are duplicate values of the annotated text on each graph. Second, the following messages are written to the SAS log: NOTE: ERROR DETECTED IN ANNOTATE= DATASET WORK.MYANNO. NOTE: PROBLEM IN OBSERVATION 1 - DATA SYSTEM REQUESTED, BUT VALUE IS NOT ON GRAPH 'Y' NOTE: PROBLEM IN OBSERVATION 5 - DATA SYSTEM REQUESTED, BUT VALUE IS NOT ON GRAPH 'X' NOTE: The above message was for the following BY group: Origin=USA These notes tell me that either the X or the Y coordinate in two of the observations in the Annotate data set do not exist on one of the graphs. This issue occurs because the Annotate coordinates for each of the BY values are different for each graph. The axis ranges are different on the two graphs. So, when all of the annotation, instead of the annotation for only each BY value, is drawn on each graph, some of the Annotate coordinates cannot be found on the graph. Both of these issues occur because the ANNOTATE=Myanno option is in the PROC GPLOT statement instead of in the action (PLOT) statement. Moving the ANNOTATE=Myanno option to the PLOT statement generates the expected output: proc gplot data=cars; by origin; plot mpg_highway*cylinders / vaxis=axis1 haxis=axis2 annotate=myanno; symbol1 interpol=none value=dot color=blue; axis1 label=(angle=90); axis2 offset=(2,2)pct; run; quit; ### Off the Grid Another common issue with using an Annotate data set is when a coordinate in the Annotate data set lies outside the range of an axis on the graph. For example, I will chart the mean MPG_Highway values with the GCHART procedure and draw a symbol at the maximum value for each country of origin using an Annotate data set: proc sort data=sashelp.cars out=cars; by origin; run; /* Compute the mean and the max */ proc means data=cars noprint; by origin; var mpg_highway; output out=meansout mean=mean max=max; run; data myanno; length function color$8 text \$14; retain xsys ysys '2' color 'red' position '2' size 2; set meansout;   function='symbol'; midpoint=origin; y=max; text='diamondfilled'; output; run;   proc gchart data=meansout; vbar origin / sumvar=mean annotate=myanno raxis=axis1; axis1 label=(angle=90); run; quit;

When I run this program, the following graph is produced, excluding the annotated symbols:

The following annotate error messages are written to the SAS log:

NOTE: ERROR DETECTED IN ANNOTATE= DATASET WORK.MYANNO. NOTE: PROBLEM IN OBSERVATION 1 - DATA SYSTEM REQUESTED, BUT VALUE IS NOT ON GRAPH 'RESPONSE' NOTE: PROBLEM IN OBSERVATION 2 - DATA SYSTEM REQUESTED, BUT VALUE IS NOT ON GRAPH 'RESPONSE' NOTE: PROBLEM IN OBSERVATION 3 - DATA SYSTEM REQUESTED, BUT VALUE IS NOT ON GRAPH 'RESPONSE'

These messages tell me that multiple response values (Y coordinates) in the Annotate data set lie outside the range of the Y axis. The procedure does not automatically extend the Y-axis range to accommodate the annotation, so I need to do this by including the ORDER= option in the AXIS1 statement:

proc gchart data=meansout; vbar origin / sumvar=mean annotate=myanno raxis=axis1; axis1 label=(angle=90) order=(0 to 70 by 10); run; quit;

The correct graph is now generated:

Annotation is a useful tool that enables you to draw features on a graph that the graphics procedure might not have the capability to draw. Using an Annotate data set is easier once you understand what the SAS log messages are telling you and can take steps to avoid common issues. Don’t be afraid to dive in!

Happy drawing!

Common annotate pitfalls and how to avoid them was published on SAS Users.

Another report requirement came my way and I wanted to share how to use our Visual Analytics’ out-of-the-box relative period calculations to solve it.

Essentially, we had a customer who wanted to see a metric for every month, the previous month’s value next to it, and lastly the difference between the two.

To do this in SAS Visual Analytics, which is available in versions 7.3 and above, use the relative periodic operators. I am going to use the Mega_Corp data which has a date data item called Date by Month using the format: MMMYYYY. SAS Visual Analytics supports relative period calculations for month, quarter and year.
The first two columns, circled in red, are straight from the data. The metric we are interested in for this report is Profit.

Next, we will create the last column, Profit (Difference from Previous Period), which is an aggregated measure that uses the periodic operators.

From the Data pane, select the metric used in the list table, Profit. Then right-click on Profit and navigate the menus: Create / Difference from Previous Period / Using: Date by Month.

A new aggregated measure will be created for you:

If you right-click on the aggregated measure and select Edit Aggregated Measure…, you will see this relative period calculation, where it is taking the current period (notice the 0) minus the value for the previous period (notice the -1).

Okay – that’s it. This out-of-the-box relative period calculation is ready to be added to the list table. Notice the other Period Operators available in the list. These support SAS Visual Analytics’ additional out-of-the-box aggregated measure calculations such as the Difference between Parallel Periods, Year to Date cumulative calculations, etc.

Now we have to create the final column to meet our report requirement: the Previous Period column.

To do this we are going to leverage the out-of-the-box functionality of the relative period calculation. Since this aggregated measure calculates the previous period for the subtraction – let’s use this to our advantage.

Duplicate the out-of-the-box relative period calculation by right-clicking on Profit (Difference from Previous Period) and select Duplicate Data Item.

Then right-click on the new data item, and select Edit Aggregated Measure….

Now delete everything highlighted in yellow below, remember to also delete the minus sign. And give the data item a new name. Click OK. This will create an aggregated measure that will calculate the previous period.

The final result should look like this from either the Visual tab or Text tab:

Now we have all the columns to meet our report requirement:

Now that I’ve piqued your interest, I’m sure you are wondering if you could use this technique to create aggregated data items to represent the Period -1, -2, -3 offset? YES! This is absolutely possible.
Also, I went ahead and plotted the Difference from Previous Period on a line chart. This is an extremely useful visualization to gage if the variance between periods is acceptable. You can easily assign display rules to this visualization to flag any periods that may need further investigation.

Relative Period Report in SAS Visual Analytics was published on SAS Users.