David Franklin, Blog
2008 Archive
Back dfranklinnh@gmail.com

 

January

When looking at a SAS program you are likely to see something like the following when dealing with text strings:

    rslt_string=TRIM(LEFT(original_string));

What this did was left align the text in the variable original_string and then remove any blanks after the last character.

In SAS 9 a new function was introduced called STRIP that does the same task but with one function so the original code above can instead be written as:

    rslt_string=STRIP(original_string);

If you are still using an older version of SAS you could do the same thing with a macro, using the following code:

    %MACRO STRIP(intxt); /*INTXT=text variable*/
        TRIM(LEFT(&intxt))
    %MEND STRIP;
    rslt_string=%STRIP(original_string);

You may wonder why there is no ';' at the end of the code inside the macro definition? This allows for the macro to be called inside the code multiple times, as the following example demonstrates:

    rslt_string=%STRIP(original_string_A)||%STRIP(original_string_B);

February

I had an occasion recently to need negative values to to be displayed with a '()' around the value, rather than the more usual '-' in front of the value. Below is the code that was used to do this and a sample of its use:

    proc format;
        picture negvals
           low-<0='00000.0)' (prefix="(" )
           0-high="00000.0" ;
        run;

    72 data _null_;
    73    do pctchng="50" to 50 by 20;
    74       dispval=put(pctchng,negvals.);
    75       put pctchng= dispval=;
    76    end;
    77 run;
    pctchng=50 dispval=50.0
    pctchng=30 dispval=30.0
    pctchng=10 dispval=10.0
    pctchng=-10 dispval=(10.0)
    pctchng=-30 dispval=(30.0)
    pctchng=-50 dispval=(50.0)

March

I sometimes want to get a listing of all the SAS programs for a particular directory. There are a large number of solutions available, but here is my favourite using PIPE within SAS:

    filename dircmd pipe "dir /b C:\TUAI\TABLES";
    data dirfile;
       length filelst $256;
       infile dircmd missover length=len;
       input filelst $varying246. len;
    run;

April

When data is transposed it is possible to put both a label onto the transposed variable inside the TRANSPOSE procedure using the IDLABEL statememt. The following code shows how this is done:

    DATASET: VITAL_IN
    SUBJECT  TEST  TSTLABEL     TSTVALUE
     00-01   HGT   Height (cm)  175
     00-01   WGT   Weight (kg)  75

    proc sort data=vital_in;
       by subject;
    run;
    proc transpose data=vital_in out=vital_out (drop=_name_);
       by subject;
       id test;
       idlabel tstlabel;
       var tstvalue;
    run;
    options nocenter;
    proc print data=vital_out label;
    run;

                      Height    Weight
    Obs    subject     (cm)      (kg)
     1      00-01       175       75

May

In Windows, running a set of programs in batch mode is a common task. Below is an example of a batch file (filename is RUNALL_Tables.BAT) using two notable items:

  • The use of a windows system variable to store the location of the SAS.EXE program (this is used later in the program to call the SAS.EXE)

  • The options –NOSTATUSWIN and –NOLOGO when running the SAS programs (reduces the number of windows that have to be run)

An example of a batch fiile that I use is given below:

   rem Point to location where SAS.EXE is located
   set xsas=C:\Program Files\SAS\SAS 9.1

   rem Run our programs
   "%xsas%\sas" Demographics.sas -nostatuswin -nologo
   "%xsas%\sas" AdverseEvents.sas -nostatuswin -nologo

One advantage of this type of programming is that if the SAS executable gets moved it would be just a case of changing one line rather than several.

June

Despite procedures like CPORT and CIMPORT that put data into a way that it can be copied from one computer to another, the old Version 5 Transport file is still the most common way of transferring data between computers, apart for TAB delimited for CSV files (that for another day). One reason is that the Version 5 Transport file format is in the public domain while the format created and read by CPORT and CIMPORT is not.

The limitations of the Version 5 Transport files are that they can only contain data (no view or catalogs for example), have variable names that are SAS version 5/6 compatible (starting with a character A-Z, no spaces, and be a maximum of 8 characters in length), and character variables of no more than 200 characters. Procedures CPORT and CIMPORT have no such limitations.

The most common way to create a Version 5 Transport file is the use of the COPY procedure, as the following example demonstrates:

   libname derived_data 'C:\TAWI\RAWDATA';
   libname xportout xport 'C:\TAWI\TRANSFER\TR002.XPT';
   proc copy in=derived_data out=xportout memtype=data;
   run;

In the example the datasets in the directory referenced by the LIBNAME DERIVED_DATA are copied into the transfer file TR002.XPT. It is also possible to use the SELECT and EXCLUDE statements to restrict the datasets that are processed. SAS documentation suggests that it is important to use the "memtype=data" option so that files selected for the transfer are only those of dataset type. I personally suggest the use of the XPT filename extension as this is the most way Version 5 Transport files are named.

To restore a Version 5 Transport file using the COPY procedure is done a similar way, as the following example demonstrates:

   libname xportin xport 'C:\TAWI\TRANSFER\TR002.XPT ';
   libname derived_data 'C:\TAWI\RAWDATA ';
   proc copy in=xportin out=derived_data;
   run;

In the example all the datasets that are in the transport file TR002.XPT are restored in the directory referenced by the LIBNAME DERIVED_DATA, however this can be restricted if need be by the SELECT and EXCLUDE statements in the COPY procedure call.

July

Sometimes you need to set all variables of an observation to blank, as you do when you are creating a blank dataset (in the LOG you will see at message coming up that variables are initialized). The following example creates a dataset with zero observations and uses the CALL MISSING routine to avoid the “initialized variable” message appearing:

    7 data PopulationData2008;
    8    attrib year length=8 label='Year'
    9    population length=8 format=comma10.
   10    label='Population Count';
   11    call missing(of _ALL_);
   12    delete;
   13 run;

NOTE: The data set WORK.POPULATIONDATA2008 has 0 observations and 2 variables.

August

Transferring a numeric variable to a character value is a common task, a lot more common than people realize – infact most people do not realize they are doing it. Take for example the following code that is inside a datastep:

   if hours>40 then
     flagtxt='NOTE: Hours reported are greater than 40 – reported '||
             'hours are: '||compress(put(hours,8.2.));

In the example we are placing the HOURS value that is stored as numeric to a character string. If we had just done:

   if hours>40 then
     flagtxt='NOTE: Hours reported are greater than 40 – reported '||
             'hours are: ‘||hours;

SAS would have brought up a message in the SAS LOG indicating that a numeric to character conversion had been done within SAS – these messages should be looked at.

In the above example, the conversion is easy to spot, but not something like:

   Tothours = tothours + hours;

where the variable HOURS is numeric and TOTHOURS is character – here some very funny things can go on and unexpected results occur.

Let me show you now a classic example that I show people regarding this issue:

   191 data _null_;
   192 length x $8;
   193 x = 1/3;
   194 x = x * 3;
   195 put x=;
   196 run;

   NOTE: Numeric values have been converted to character values
         at the places given by: (Line):(Column).
         193:9 194:10
   NOTE: Character values have been converted to numeric values
         at the places given by: (Line):(Column).
         194:8
   x=0.999999

The answer for x should of course be 1, not 0.999999 – what the program first did was calculate 1/3 and stored it as a character, then multiplied that value by 3. I did not copy across the LOG messages but it said that “NOTE: Numeric values have been converted to character values at the places given by …”

To avoid the issues of numeric to character conversion, please always use a PUT statement with a format to put the correct numeric value into a character variable.

Next month, Character to Numeric conversion.

September

Continuing on from last month, conversion of numeric to character, or character to numeric conversion is an issue. Lets take a look at the following example:

   277 data TestDates;
   278    infile cards;
   279    input SubjectID $ 1-6 TestDate $ 8-15
   280          @17 FirstTreatmentDate date9.;
   281    put (_all_) (=);
   282 cards;

   SubjectID=000101 TestDate=06152008 FirstTreatmentDate=17198
   SubjectID=000102 TestDate=08082008 FirstTreatmentDate=17332
   NOTE: The data set WORK.TESTDATES has 2 observations and 3 variables.

   285 ;
   286 run;
   287 data TestDatesModified;
   288    attrib TestDateN length=8 format=date9.;
   289    set TestDates;
   290    TestDateN = TestDate; ** Convert TestDate to Numeric;
   291    DaysSinceFirstTreatment = TestDateN - FirstTreatmentDate;
   292    put (_all_) (=);
   293 run;

   NOTE: Character values have been converted to numeric
         values at the places given by: (Line):(Column).
         290:16
   TestDateN=21AUG**** SubjectID=000101 TestDate=06152008 FirstTreatmentDate=17198
   DaysSinceFirstTreatment=6134810
   TestDateN=********* SubjectID=000102 TestDate=08082008 FirstTreatmentDate=17332
   DaysSinceFirstTreatment=8064676
   NOTE: There were 2 observations read from the data set WORK.TESTDATES.
   NOTE: The data set WORK.TESTDATESMODIFIED has 2 observations and 5 variables.

As can be seen in the example, because we just blindly converted the TestDate variable from character to numeric, the values for TestDateN were strange. Note the log message above that gives an indication of the issue. To fix this we need to use and input statement with an informat, as the following example will show:

   294 data TestDates;
   295    infile cards;
   296    input SubjectID $ 1-6 TestDate $ 8-15
   297          @17 FirstTreatmentDate date9.;
   298    put (_all_) (=);
   299 cards;

   SubjectID=000101 TestDate=06152008 FirstTreatmentDate=17198
   SubjectID=000102 TestDate=08082008 FirstTreatmentDate=17332
   NOTE: The data set WORK.TESTDATES has 2 observations and 3 variables.

   302 ;
   303 run;
   304 data TestDatesModified;
   305    attrib TestDateN length=8 format=date9.;
   306    set TestDates;
   307    TestDateN = input(TestDate,mmddyy8.); ** Convert TestDate to Numeric;
   308    DaysSinceFirstTreatment = TestDateN - FirstTreatmentDate;
   309    put (_all_) (=);
   310 run;

   TestDateN=15JUN2008 SubjectID=000101 TestDate=06152008 FirstTreatmentDate=17198
   DaysSinceFirstTreatment=500
   TestDateN=08AUG2008 SubjectID=000102 TestDate=08082008 FirstTreatmentDate=17332
   DaysSinceFirstTreatment=420
   NOTE: There were 2 observations read from the data set WORK.TESTDATES.
   NOTE: The data set WORK.TESTDATESMODIFIED has 2 observations and 5 variables.

To avoid the issues of character to numeric conversion, please always use a INPUT statement with a informat to put the correct numeric value into a character variable.

October

Making a copy of a SAS dataset is a basic operation in SAS. This is usually done when we want to bring a dataset from a permanent area to a WORK area, or from a WORK area and put the dataset into a permanent area.

One way of copying a SAS dataset from one directory to another, and to some is the most efficient, is to either use the COPY procedure, or the COPY statement in the DATASETS procedure.

The syntax for the COPY procedure, when copying data, is:

   PROC COPY IN=source-library OUT=target-library MEMTYPE=DATA;
      SELECT member-list;
      EXCLUDE member-list;
   RUN;

where

Source-library the libname pointing to the source directory
Target-library the libname pointing to the target directory

If the SELECT or EXCLUDE statements are not used then all datasets in the source directory will be copied to the target directory. If only select datasets are needed to be copied then the SELECT or EXCLUDE statements are useful - in the case of SELECT only those datasets that are listed will be copied, while in the case of EXCLUDE all datasets except those listed will be included.

The following example selects the dataset CLASS from the SASHELP library and copies it to the WORK library, maybe for later manipulation:

   PROC COPY IN=sashelp OUT=work MEMTYPE=data;
      SELECT class;
   RUN;

The PROC DATASETS equivalent call for the above example is:

   PROC DATASETS LIBRARY=work;
      COPY IN=sashelp OUT=work MEMTYPE=data;
      SELECT class;
      QUIT;
   RUN;

November

A month ago I had a programmer ask me how could she set all the character variables in a SAS dataset to missing. This follows a tip I gave some time ago referring to creating a dataset with no observations, avoiding the message indicating that there were initialized variables.

In SAS, the variables in a dataset are grouped by numeric (_NUMERIC_) and character (_CHAR_). Using this it is possible to set all the numeric or character variables to missing. The following code shows its use by setting the numeric variables to missing if the condition is met:

   141 *** Load our test data;
   142 data PregnancyTestResults;
   143    infile cards;
   144    input SubjectID $ 1-6 Sex $ 8 Tested Result;
   145    ** TESTED and RESULT have codes 1=Yes, 0=No;
   146    put (_all_) (=);
   147 cards;

   SubjectID=000101 Sex=M Tested=1 Result=.
   SubjectID=000102 Sex=F Tested=1 Result=0
   SubjectID=000103 Sex=F Tested=0 Result=.
   SubjectID=000104 Sex=M Tested=. Result=.
   SubjectID=000105 Sex=F Tested=1 Result=1
   153 ;
   154 run;
   NOTE: The data set WORK.PREGNANCYTESTRESULTS has 5 observations and 4 variables.
   155 *** If Male then set TESTED and RESULT to missing (both numeric);
   156 data PregnancyTestResultsModified;
   157    set PregnancyTestResults;
   158    if sex='M' then do;
   159       put 'WAR' 'NING: Male subject cannot have ' /
   160         @10 'a pregnancy test -- TESTED and RESULT ' /
   161         @10 'will be set to blank: ' /
   162         @10  (_all_) (=);
   163       call missing(of _numeric_);
   164    end;
   165 run;

   WARNING: Male subject cannot have
            a pregnancy test -- TESTED and RESULT
            will be set to blank:
            SubjectID=000101 Sex=M Tested=1 Result=.
   WARNING: Male subject cannot have
            a pregnancy test -- TESTED and RESULT
            will be set to blank:
            SubjectID=000104 Sex=M Tested=. Result=.
NOTE: There were 5 observations read from the data set WORK.PREGNANCYTESTRESULTS.
NOTE: The data set WORK.PREGNANCYTESTRESULTSMODIFIED has 5 observations and 4 variables.

   166 *** Put the dataset out to the SAS LOG, like a mini PROC PRINT;
   167 data _null_;
   168    set PregnancyTestResultsModified;
   169    if _n_=1 then put 'Pregnancy Test Results' /;
   170    put (_all_) (=);
   171 run;

   Pregnancy Test Results

   SubjectID=000101 Sex=M Tested=. Result=.
   SubjectID=000102 Sex=F Tested=1 Result=0
   SubjectID=000103 Sex=F Tested=0 Result=.
   SubjectID=000104 Sex=M Tested=. Result=.
   SubjectID=000105 Sex=F Tested=1 Result=1
NOTE: There were 5 observations read from the data set WORK.PREGNANCYTESTRESULTSMODIFIED.

December

A couple of months ago I used the COPY and DATASETS procedure to copy a dataset from one library to another, but what if instead we wanted to make a copy of a dataset in the same directory, e.g. take the dataset CLASS (that was already copied from the SASHELP directory to the WORK directory) and make it CLASSCOPY (maybe we want to manipulate it without effecting the original dataset).

You can certainly use a datastep as given in the following following example:

   DATA classcopy;
      SET class;
   RUN;

But another way is to use the AGE statement in the DATASETS procedure as given below:

   PROC DATASETS LIBRARY=work;
      AGE class classcopy;
      QUIT;
   RUN;

Using the AGE statement will make a copy of the dataset CLASS and name it CLASSCOPY - if CLASSCOPY already exists then it will be overwritten by the data in CLASS.

While this example is not a use that I would see very useful, it does have one other application. When creating a number of datasets that we put into a permanent area, it is useful to have a copy of the previous version for QC purposes, as the following example demonstrates:

   PROC DATASETS LIBRARY=derived_data;
      AGE ae ae_prevversion;
      COPY IN=WORK OUT=derived_data;
         SELECT ae;
      QUIT;
   RUN;

In the example above, before the latest version of AE is put into the DERIVED_DATA library, a copy is made and named AE_PREVVERSION - this then can be used to compare against the latest version AE and see that changes expected actually occurred.