| David Franklin, Blog | |
| 2009 Archive | |
| Back | dfranklinnh@gmail.com |
|
JanuaryIt is sometimes necessary to add a number of variables in a record and create a variable containing the total. How can we go about it. Lets say we have the following data in a dataset: SubjectID x1 x2 x3
101 1 2 3
102 3 4 6
To get the total of x1, x2 and x3 it is possible to do the following: Sumx = sum(x1,x2,x3); However, what if we not only wanted the SUM, but also the MEAN, MIN and MAX values? We would have to write four references to the variables and would appear like: Sumx = sum(x1,x2,x3); Meanx = mean(x1,x2,x3); Minx = min(x1,x2,x3); Maxx = max(x1,x2,x3); But what if a variable x4 came along, all four lines will have to be amended. Another way is to put the variables into an array reference and use OF processing, as the following example illustrates: array z{*} x1 x2 x3;
Sumx = sum(of z{*});
Meanx = mean(of z{*});
Minx = min(of z{*});
Maxx = max(of z{*});
Thus if variable x4 suddenly came into the mix then the only thing that would change is the array reference, i.e. array z{*} x1 x2 x3 x4;
For this example, I have used the variables x1, x2 and x3, and have one feature they all start with the letter x. Due to this feature it is possible to use the following code when doing the function calls, as the following example illustrates: Sumx = sum(of x:); Try it and see. FebruaryWhen appending data for the first time, there is one message that appears in the SAS LOG that some just try to get rid of: NOTE: BASE data set does not exist. DATA file is being copied to BASE file. This occurs when the dataset represented in the BASE option does not exist -- SAS will create this dataset from the DATA option by copying the data from DATA to BASE. A trick I use to get rid of this SAS LOG message is to create the BASE dataset with no observations from the first dataset to be appended, as the following example shows: 13 *--------------------------------------;
14 * Take the data from CLASS dataset in;
15 * the SASHELP library and split into;
16 * MALE and FEMALE;
17 *--------------------------------------;
18 data males;
19 set sashelp.class;
20 where sex='M';
21 run;
NOTE: There were 10 observations read from the data set SASHELP.CLASS.
WHERE sex='M';
NOTE: The data set WORK.MALES has 10 observations and 5 variables.
22 data females;
23 set sashelp.class;
24 where sex='F';
25 run;
NOTE: There were 9 observations read from the data set SASHELP.CLASS.
WHERE sex='F';
NOTE: The data set WORK.FEMALES has 9 observations and 5 variables.
26
27 *--------------------------------------;
28 * Create dataset COMBINED from MALES;
29 * with no observations;
30 *--------------------------------------;
31 data combined;
32 set males (obs=0);
33 run;
NOTE: There were 0 observations read from the data set WORK.MALES.
NOTE: The data set WORK.COMBINED has 0 observations and 5 variables.
34
35 *--------------------------------------;
36 * Now APPEND data;
37 *--------------------------------------;
38 proc append base=combined data=males force;
NOTE: Appending WORK.MALES to WORK.COMBINED.
NOTE: There were 10 observations read from the data set WORK.MALES.
NOTE: 10 observations added.
NOTE: The data set WORK.COMBINED has 10 observations and 5 variables.
39 proc append base=combined data=females force;
40 run;
NOTE: Appending WORK.FEMALES to WORK.COMBINED.
NOTE: There were 9 observations read from the data set WORK.FEMALES.
NOTE: 9 observations added.
NOTE: The data set WORK.COMBINED has 19 observations and 5 variables.
The purpose of the data step creating COMBINED before the PROC APPEND call is to create the dataset COMBINED with no observations -- this is a quick step as this will only read in the variable information from the dataset MALES. The two PROC APPEND calls after that bring the data together. Without the datastep, and a note saying that the BASE data set does not exist and that the DATA file is being copied to BASE file will appear. Hope this is useful to you. MarchThere are times, when programming in SAS, where you need to "delete all the datasets in the WORK directory, except for these." I had such a situation recently, where some datasets were being generated before a program I was editing --I wanted to delete all except these. Running the following code would delete ALL the datasets in the WORK library: proc datasets library=work mt=data kill;
quit;
run;
This was not what is wanted. One solution is to create a create a list of datasets in the WORK directory, removing those datasets that were not to be deleted, then using that list in the DELETE statement inside a PROC DATASETS call. Code like this could look like: *** Create a list of members in the WORK library, except the one
that is needed to keep;
proc sql noprint;
select memname
into :mlist separated by ' '
from sashelp.vstable
where libname='WORK' and memname ^in('TREATMENT_MAPPING');
quit;
run;
*** Remove datasets in list;
proc datasets library=work mt=data;
delete &mlist
quit;
run;
Note that in the example above, the dataset TREATMENT_MAPPING is not deleted in the WORK library, but all other datasets are. Another solution is to use the SAVE statement in the DATASETS procedure, as the following example demonstrates: proc datasets library=work mt=data;
save TREATMENT_MAPPING;
quit;
run;
This last solution is more easier to write and maintain. This month the SAS Global Forum is held at the Gaylord National Resort and Convention Center, at National Harbor in Maryland. If you are lucky enough to go, then please use this event, the premier event on the SAS Calendar, to hear what others are doing at the presentations, network, have some fun, and take back to your own groups of some lessons learned. If you cannot go, keep an eye on the SAS website for the videos of selected segments of the conference, and don't miss the papers presented at conference -- there will be a nugget or two for everyone. AprilLast month I presented a paper at SAS Global Forum on merging data eight different ways (the link to the paper is here). One of the common questions raised during the Question and Answer session around the posters was "You show for one variable, TRT_CODE, but lets say I want to add multiple variables, like AGE and SEX, with the PROC FORMAT statement?" This month I will address that question. Lets first look at some data -- for this example I will load the data using a CARDS statement: data PATDATA;
infile CARDS;
input SUBJECT $ 1-6
TRT_CODE $ 8
AGE 10-11
SEX $ 13;
cards;
124263 A 25 M
124264 A 31 F
124265 B 30 M
124266 B 26 F
;
run;
data ADVERSE;
infile CARDS;
input SUBJECT $ 1-6
EVENT $ 8-40;
cards;
124263 HEADACHE
124266 FEVER
124266 NAUSEA
124267 FRACTURE
run;
When merging the data this way, it is necessary to work out which dataset has a unique key for the merging variable -- in this case the dataset PATDATA is unique. The next step is create the format from the PATDATA dataset. The following code will do this: *** To create a format from a dataset there needs to be four variables
present -- FMTNAME (format name), TYPE (type of format, usually
(C)haracter or (N)umeric), START (value), and LABEL (decoded value).
For this example will also create a HLO variable -- this will signal
that OTHER is to be set, and for our purpose it will be set to blank
(this makes sure that subjects in ADVERSE not in PATDATA have a
blank value coming through.;
data FMT;
*** Define START (subject number), out LABEL variable (this will
contain TRT_CODE, AGE and SEX) and HLO (OTHER flag);
length START $8 LABEL $200 HLO $1;
*** FMTNAME is the Format Name, and the TYPE is character;
retain FMTNAME 'TRT_FMT' TYPE 'C';
*** Get our data and create the START and LABEL variable;
set PATDATA end=EOF;
START=cats(SUBJECT);
LABEL=catx('|',TRT_CODE,put(AGE,best.),SEX);
*** Output each record;
output;
*** Create one last record to deal with OTHER;
if EOF then do;
START='**OTHER**';
call missing(LABEL);
HLO='O';
output;
end;
run;
*** Create the format;
proc format cntlin=FMT;
run;
*** Do the merge;
data ALLDATA0;
*** Read in each record of ADVERSE dataset;
set ADVERSE;
*** Create the variables to contain Treatment Code
(TRT_CODE), Age (AGE) and SEX (Sex);
attrib TRT_CODE length=$1 label='Treatment Code'
AGE length=8 label='Age (years)'
SEX length=$1 label='Sex';
*** Now get TRT_CODE, AGE and SEX from format,
using the SCAN function to get the correct data
into the variable. Will use a temporary variable
_TMP to contain the value from format. Also, will
only need to extract if format comes across.;
length _TMP $200;
drop _TMP;
_TMP=put(SUBJECT,$TRT_FMT.);
if ^missing(strip(_TMP)) then do;
TRT_CODE=scan(_tmp,1,'|');
AGE=input(scan(put(subject,$trt_fmt.),2,'|'),best.);
SEX=scan(put(SUBJECT,$TRT_FMT.),3,'|');
end;
run;
Using the PROC FORMAT as in this example is a very interesting method to merge two datasets, from a one-to-one or one-to-many. I have found in the past that the efficiency of this method increases as the number of observations increase when compared against either a MERGE statement inside a datastep or using PROC SQL. I hope you find this interesting. MayThis month I am going to talk about dataset variable labels, and why they are useful. There are many parts to a variable, key among them are the name of the variable (NAME), type (TYPE), and length (LENGTH). Under SAS version 8 and above the name of a variable can be 32 characters long, its type can be either Character or Numeric, and the length for a character variable can be set between 1 and 32767 characters while a numeric variable can be set between 3 and 8 (yes, some operating systems allow for 2 but not all). There are other parts to a variable, but the three most common are the informat, format and label -- we shall look at this last part this month. What exactly is a label and why is it used? In simple terms the label is a short descriptive text giving a more user friendly description of the variable. Take for example a very simple example, the variable WEIGHT in a dataset -- sure it tells me what the variable is (it is weight) but it does not tell me other things I may need to know like what unit it is in or when the weight data point was collected, all of this being very important information. Given that I am allowed 32 characters for a variable name I could change the variable name to reflect this information, but in most cases it is not possible to do this and it indeed impractical. So here comes the use of the LABEL statement. The label itself can be 256 characters, including blanks, in length (40 characters if you are still using SAS version 6.xx) -- this gives us plenty of room to write a good description of the variable. Setting the label is commonly achieved by using the LABEL statement inside a datastep or the LABEL option under the MODIFY statement in the DATASETS procedure, the syntax of which is given below: *** Inside a datastep;
data class;
set class;
label weight='Weight (kg) at Start of Study';
run;
*** Inside the DATASETS procedure;
proc datasets library=work;
modify class;
label weight='Weight (kg) at Start of Study';
quit;
run;
I personally use the datastep method if I am creating or modifying a variable inside that datastep, otherwise I use the DATASETS procedure, but is only my convention -- there is no set rule with this. Before going on to a real world example, lets first see how we would look at what labels are set, if any, inside a dataset. The two easiest ways to look at the data is to either run a CONTENTS procedure call on the dataset, or if you have the SAS Viewer installed, look at the attributes window for that dataset. Now lets look at a real world example where it will all come clearer. For this example I will use the dataset SASHELP.CLASS. Lets look first at the structure of the dataset using the CONTENTS procedure (will actually make a copy first so I don't overwrite the original data): data class; *** Make a copy of the dataset;
set sashelp.class;
run;
proc contents data=class; *** Get structure of the dataset;
run;
Running this code we get the following output (abridged): Alphabetic List of Variables and Attributes # Variable Type Len 3 Age Num 8 4 Height Num 8 1 Name Char 8 2 Sex Char 1 5 Weight Num 8 If a label existed for a variable we would see a column headed "Label", but in this case there is no labels applied to the dataset. So now lets set one for WEIGHT as we indicated above (this time I shall do the content structure not from the CONTENTS procedure, but the CONTENTS statement inside the DATASETS procedure): proc datasets library=work;
modify class;
label weight='Weight (kg) at Start of Study';
contents data=class;
quit;
run;
Running this code we get the following output (abridged): Alphabetic List of Variables and Attributes # Variable Type Len Label 3 Age Num 8 4 Height Num 8 1 Name Char 8 2 Sex Char 1 5 Weight Num 8 Weight (kg) at Start of Study As you can see the variable WEIGHT now has a label -- the reason why it is useful will become clear shortly. For the purposes of this example I will also now add a label to the variable AGE and HEIGHT using the DATASETS procedure as above: proc datasets library=work;
modify class;
label height='Height (in) at Start of Study'
age='Age (years) at Start of Study';
contents data=class;
quit;
run;
Note that I did not redo the label for the variable WEIGHT in the above code as it was already done previously, although I could have put in the step and even replaced it with new text. Now why is the label useful. As you can see already it gives a useful description of what the variable is. Now lets extend that to a small report using the PRINT procedure: proc print data=class noobs; run; that produces the following output (abridged): Name Sex Age Height Weight Alfred M 14 69.0 112.5 Alice F 13 56.5 84.0 Barbara F 13 65.3 98.0 Carol F 14 62.8 102.5 Henry M 14 63.5 102.5 The column headers are the same as the variable names, and we have the same problem of not knowing what the units are for Age, Height and Weight (but did we not put that in a label earlier) but lets add another option to get the labels: proc print data=class noobs LABEL; run; that produces the following output (abridged): Age (years) Height (in) Weight (kg)
at Start at Start at Start
Name Sex of Study of Study of Study
Alfred M 14 69.0 112.5
Alice F 13 56.5 84.0
Barbara F 13 65.3 98.0
Carol F 14 62.8 102.5
Henry M 14 63.5 102.5
Now, as you can see, we have a report that gives a clear description of the variables though the use of dataset variable labels. There will be those reading this who will say that I could have put the label in the PRINT procedure call (yes, the LABEL statement is a global statement that can be used in almost any procedure) and would be done this way using the following code: proc print data=class noobs LABEL;
label weight='Weight (kg) at Start of Study'
height='Height (in) at Start of Study'
age='Age (years) at Start of Study';
run;
But the one reason I don't normally use this method is that the label does not carry forward in the dataset. Now see a complete example where the CLASS data is copied from the SASHELP directory, useful labels put on the dataset, then doing a PRINT, MEANS and TABULATE call on the same data, all with the same labels: proc datasets library=work;
copy in=sashelp out=work;
select class;
modify class;
label weight='Weight (kg) at Start of Study'
height='Height (in) at Start of Study'
age='Age (years) at Start of Study';
quit;
run;
proc print data=class noobs LABEL;
run;
proc means data=class;
var age height weight;
run;
proc tabulate data=class;
class sex;
var age height weight;
tables age*
(n*f=8.0 mean*f=8.2 std*f=8.3
median*f=8.2 min*f=8.0 max*f=8.0)
(height weight)*
(n*f=8.0 mean*f=8.3 std*f=8.4
median*f=8.3 min*f=8.1 max*f=8.1),
sex all='Total';
run;
produces the following output: The PRINT Procedure
Age (years) Height (in) Weight (kg)
at Start at Start at Start
Name Sex of Study of Study of Study
Alfred M 14 69.0 112.5
Alice F 13 56.5 84.0
Barbara F 13 65.3 98.0
Carol F 14 62.8 102.5
Henry M 14 63.5 102.5
James M 12 57.3 83.0
Jane F 12 59.8 84.5
Janet F 15 62.5 112.5
Jeffrey M 13 62.5 84.0
John M 12 59.0 99.5
Joyce F 11 51.3 50.5
Judy F 14 64.3 90.0
Louise F 12 56.3 77.0
Mary F 15 66.5 112.0
Philip M 16 72.0 150.0
Robert M 12 64.8 128.0
Ronald M 15 67.0 133.0
Thomas M 11 57.5 85.0
William M 15 66.5 112.0
The MEANS Procedure
Variable Label N Mean Std Dev Minimum Maximum
---------------------------------------------------------------------------------------------------
Age Age (years) at Start of Study 19 13.3157895 1.4926722 11.0000000 16.0000000
Height Height (in) at Start of Study 19 62.3368421 5.1270752 51.3000000 72.0000000
Weight Weight (kg) at Start of Study 19 100.0263158 22.7739335 50.5000000 150.0000000
---------------------------------------------------------------------------------------------------
The TABULATE Procedure
----------------------------------------------------
| | Sex | |
| |-----------------| |
| | F | M | Total |
|-----------------------+--------+--------+--------|
|Age (years)|N | 9| 10| 19|
|at Start of|-----------+--------+--------+--------|
|Study |Mean | 13.22| 13.40| 13.32|
| |-----------+--------+--------+--------|
| |Std | 1.394| 1.647| 1.493|
| |-----------+--------+--------+--------|
| |Median | 13.00| 13.50| 13.00|
| |-----------+--------+--------+--------|
| |Min | 11| 11| 11|
| |-----------+--------+--------+--------|
| |Max | 15| 16| 16|
|-----------+-----------+--------+--------+--------|
|Height (in)|N | 9| 10| 19|
|at Start of|-----------+--------+--------+--------|
|Study |Mean | 60.589| 63.910| 62.337|
| |-----------+--------+--------+--------|
| |Std | 5.0183| 4.9379| 5.1271|
| |-----------+--------+--------+--------|
| |Median | 62.500| 64.150| 62.800|
| |-----------+--------+--------+--------|
| |Min | 51.3| 57.3| 51.3|
| |-----------+--------+--------+--------|
| |Max | 66.5| 72.0| 72.0|
|-----------+-----------+--------+--------+--------|
|Weight (kg)|N | 9| 10| 19|
|at Start of|-----------+--------+--------+--------|
|Study |Mean | 90.111| 108.950| 100.026|
| |-----------+--------+--------+--------|
| |Std | 19.3839| 22.7272| 22.7739|
| |-----------+--------+--------+--------|
| |Median | 90.000| 107.250| 99.500|
| |-----------+--------+--------+--------|
| |Min | 50.5| 83.0| 50.5|
| |-----------+--------+--------+--------|
| |Max | 112.5| 150.0| 150.0|
----------------------------------------------------
Note that the label we defined in the DATASETS procedure carried forward to the PRINT, MEANS and TABULATE procedures. I hope you find this months tip useful. JuneWhile at PharmaSUG I was asked a question as to how a percentage change value could be displayed prefixed by a '+' or '-'. This can be done in a picture format the following way: proc format;
picture plusminus
low-<0 = '0000.0' (prefix='-')
0 = '0' (noedit)
0<-high = '0000.0' (prefix='+');
run;
Note that in this example we are displaying the values to one decimal place. Now lets look at an example, displaying the Change from Baseline Percentage value to one decimal place: *--------------------------------------;
* Set up display of Cycle/Day in report;
*--------------------------------------;
proc format;
value $CycDay
'C1D2' = 'Cycle 1, Day 2'
'C1D3' = 'Cycle 1, Day 3'
'C2D1' = 'Cycle 2, Day 1'
'C2D2' = 'Cycle 2, Day 2'
'C2D3' = 'Cycle 2, Day 3';
run;
*--------------------------------------;
* For the purposes of this example, the;
* values are coming in from a CARDS ;
* statement. ;
*--------------------------------------;
data BUN_ChangeFromBaseline;
attrib TimePoint length=$4 format=$CycDay.
label='TimePoint'
PercentChange length=8 format=plusminus.
label='% Change from Baseline';
infile cards;
input TimePoint $ PercentChange;
cards;
C1D2 -8.6
C1D3 3.5
C2D1 4.7
C2D2 -3.7
C2D3 -5.9
;
run;
*--------------------------------------;
* Make sure order is correct. ;
*--------------------------------------;
proc sort data=BUN_ChangeFromBaseline;
by TimePoint;
run;
*--------------------------------------;
* Generate listing ;
*--------------------------------------;
options byline nocenter;
run;
title1 "Listing of Percentage Change from Baseline for BUN values, at Timepoint";
proc print data=BUN_ChangeFromBaseline noobs label;
var TimePoint PercentChange;
run;
The following output will appear in the Output Window: Listing of Percentage Change from Baseline for BUN values, at Timepoint
% Change
from
TimePoint Baseline
Cycle 1, Day 2 -8.6
Cycle 1, Day 3 +3.5
Cycle 2, Day 1 +4.7
Cycle 2, Day 2 -3.7
Cycle 2, Day 3 -5.9
Note that the raw values were not changed into a text variable for display, as programs sometimes do. I hope you find this months tip useful. JulyIf I have a character date of '6JUN2009', will this be read the same way as a character date of '06JUN2009'? This was a real life question, the simple answer being 'yes', as the following code demonstrates: data _null_;
txtdate1='6JUN2009';
numdate1=input(txtdate1,date9.);
put 'Test Date 1: ' txtdate1= numdate1= date9.;
txtdate2='06JUN2009';
numdate2=input(txtdate2,date9.);
put 'Test Date 2: ' txtdate2= numdate2= date9.;
run;
Test Date 1: txtdate1=6JUN2009 numdate1=06JUN2009
Test Date 2: txtdate2=06JUN2009 numdate2=06JUN2009
This question actually come up when partial date variables (this is where the day, month and year for a particular event are in three separate variables) existed in a dataset and a date is constructed from this. If the three variables are numeric then it is possible to use the MDY function directly, as the following code fragment suggests: StartDate=MDY(Start_Month,Start_Day,Start_Year); But what if one or more of the three partial date variables are character? In this case the month variable is almost always character and has the first three letters for the month -- in this case we can certainly use an informat for the month, do a character to numeric conversion for the character variables, and then execute the function. There is another better way though in this case, and that is to use a CATS function then an INPUT function with a format, as the following example demonstrates: StartDate=INPUT(CATS(Start_Day,Start_Month,Start_Year),DATE9.); If there are any numeric to character conversions that occur then this should be dealt with inside the CATS function. Now lets look at an example: DATA aedata0;
INFILE cards;
INPUT subjid $ 1-4 event $ 6-20 start_d $ 22-23 start_m $ 25-27 start_y $ 29-32;
ATTRIB start_dt LENGTH=8 FORMAT=date9.;
start_dt=INPUT(CATS(start_d,start_m,start_y),date9.);
PUT (_ALL_) (=);
CARDS;
0123 HEADACHE 13 MAR 2009
0123 FEVER 01 APR 2009
0123 NAUSEA 8 APR 2009
;
RUN;
When the code is run the following appeared in the SAS LOG: 69 DATA aedata0; 70 INFILE cards; 71 INPUT subjid $ 1-4 event $ 6-20 start_d $ 22-23 start_m $ 25-27 start_y $ 29-32; 72 ATTRIB start_dt LENGTH=8 FORMAT=date9.; 73 start_dt=INPUT(CATS(start_d,start_m,start_y),date9.); 74 PUT (_ALL_) (=); 75 CARDS; subjid=0123 event=HEADACHE start_d=13 start_m=MAR start_y=2009 start_dt=13MAR2009 subjid=0123 event=FEVER start_d=01 start_m=APR start_y=2009 start_dt=01APR2009 subjid=0123 event=NAUSEA start_d=8 start_m=APR start_y=2009 start_dt=08APR2009 NOTE: The data set WORK.AEDATA0 has 3 observations and 6 variables. 79 ; 80 RUN; Even though the START_D variable had both a zero led value (observation 2) and a single value (observation 3), SAS still worked out the correct START_DT value. AugustThis month is a small tip on selecting data for a report, more specifically, a request for a report based on a particular month’s data. I had such a request last month and I thought I would pass on a little trick that can help solve such a question. Let’s look at the following data: data sales;
format date date9.;
input date date9. value;
cards;
29JUN2009 500
06JUL2009 490
13JUL2009 480
20JUL2009 510
27JUL2009 520
03AUG2009 530
;
run;
Our request is to print out and give the total value of sales for July 2009. For the purposes of this example I am going to use a simple PROC PRINT call with a SUM statement to give the sum this data, but how about the selection of the records for the month of July? One way to subset the data for the month of July is to use a WHERE statement in the following form: WHERE 01JUL2009<=date<=31JUL2009; but, is there a solution where we do not have to worry about the number of days in the month? SAS does give one method, and that is by the use of the format MONYY, as the following WHERE statement demonstrates: WHERE PUT(date,monyy7.)="JUL2009"; Now putting this together, we have the following SAS code and output using the PRINT procedure: title1 "Listing of Sales for July 2009";
proc print data=sales noobs;
var date value;
sum value;
where put(date,monyy7.)="JUL2009";
run;
Listing of Sales for July 2009 08:40 Thursday, August 6, 2009 1
date value
06JUL2009 490
13JUL2009 480
20JUL2009 510
27JUL2009 520
=====
2000
Using this trick in the WHERE statement allows us not to worry about the number of days in the month (are there 28 or 29 days in February this year?). Of course I could have also used a number of other formats including YYMM and MMYY to do the same -- refer to your local documentation for other formats that are available. Extending this, you could use a format like YYQ for calendar quarterly reports, or YEAR format for annual calendar year reports. Hope you find this useful. SeptemberThis month is a little trick with CALL EXECUTE to compare, using PROC COMPARE, all the datasets in one directory with those in another -- don't be scared about using CALL EXECUTE, it is just another tool for which we can use to make our programs more efficient. One typical approach is to write all the write out all the PROC COMPARE call separately, as the following code demonstrates:
libname source_data "source_data_library"; Libname compare_data "compare_data_library"; PROC COMPARE BASE=source.adae COMPARE=compare.adae; run; PROC COMPARE BASE=source.addm COMPARE=compare.addm; run; PROC COMPARE BASE=source.advs COMPARE=compare.advs; run; For the purposes of this example, there are only three datasets in the directory we are comparing. Now lets look at automating this a bit by getting SAS to create the list of dataset that we are comparing and using CALL EXECUTE to use this list and actually do the dataset compare. The first thing to note that during your SAS session, SAS actually knows a lot about your data. One of the pieces of information that is available is the name of the datasets that the session has access to - this can be found in the view (this is like a window looking at a much larger picture) called VMEMBER located in the SASHELP library. For this approach to work we have to choose one library as the master which is the directory that has the primary list of datasets that we are comparing - this will be used as the list of dataset that is going to be compared. There is also the directory that has the data we are comparing. Now lets write some code: *** Lets define the location of the source and compare data;
libname source_data "source_data_library";
libname compare_data "compare_data_library";
data _null_;
*** Lets get the list of datasets in the SOURCE_DATA
library - that getting datasets only by use of the
MEMTYPE="DATA" statement in the WHERE clause;
set sashelp.vmember;
where libname="SOURCE_DATA" And memtype="DATA";
*** And now the CALL EXECUTE - this will take the list
of datasets in the SOURCE_DATA library and use that
list to write the PROC COMPARE calls;
CALL EXECUTE("proc compare base=source_data."||strip(memname)||
" compare=compare_data."||strip(memname)||"; run;");
run;
The hardest part to understand is the CALL EXECUTE, and the second is how we got the list from the VMEMBER view in the SASHELP library - note that the SASHELP library is a default SAS library that is initiated when SAS starts so there is no need to use a LIBNAME statement to define it. Take this code and try it at your site - after a short time you will understand the code and how the CALL EXECUTE works in this example. OctoberThis month is about the use and abuse of the NODUPKEY option in the SORT procedure. First, lets discuss what exactly is the NODUPKEY option? Referring to the SAS Online help, then NODUPKEY option is: checks for and eliminates observations with duplicate BY values So why, you may ask, did this discussion come about. I was looking at some code recently and the programmer used a NODUPKEY with a WHERE clause to select some data for analysis -- this would only work successfully if it was known that there was a unique "list" for the variables concerned. Lets look at some data that I will use as an example. DATASET: SHOPDATA
SHOP_NUMBER BEAN_COLOR COUNT
001 BLUE 5
003 GREEN 2
002 RED 4
003 RED 3
001 GREEN 6
002 RED 2
002 BLUE 1
003 GREEN 5
001 RED 2
This is not sorted (our data is not regularly sorted in a form that we can use) but we can see in this small set of data that there are two GREEN bean records for 003. Now lets construct a unique set of SHOP_NUMBER and BEAN_COLOR: proc sort data=shopdata (keep=shop_number bean_color)
out=uniquelist nodupkey;
by shop_number bean_color;
run;
The code above will create a dataset with a unique set of SHOP_NUMBER and BEAN_COLOR values. But what of the situation where we create the list of SHOP_NUMBER and BEAN_COLOR values using the following code: proc sort data=shopdata out=uniquelist nodupkey;
by shop_number bean_color;
run;
and then use the following code to find the mean number of beans per shop: proc means data=uniquelist mean;
class shop_number;
var count;
run;
The COUNT variable exists in the dataset UNIQUELIST (it has not been dropped) so the PROC MEANS step will be valid, but what value for COUNT does the step use for SHOP_NUMBER=003 and BEAN_COLOR=GREEN? Is it value 5 or 2? The actual answer is that there is no real way to determine which value is used, sometimes it will be 5 and sometimes it will be 2. The only way to get around this is to be explicit in the selection of records where there may be a multiple. Again, be aware that this is based on an actual example of code so even the best of us get it wrong occasionally. The lesson of this story is do not use the NODUPKEY option in the SORT procedure unless you are creating a unique list of values based on a variable list, but most of all do not use this list (unless you can be absolutely certain that there is no chance of duplicates) for analysis purposes. Another option inside the SORT procedure you may have heard of or used is NODUPRECS (also known as NODUP) which has the following in the SAS Online help: checks for and eliminates duplicate observations The major difference between the NODUPRECS and the NODUPKEY is that the NODUPKEY uses the variables in the BY statement to identify the unique observations, as opposed to the NODUPRECS option that finds unique records based on all the variable values for the entire record -- in our data above, no records would be removed as they are unique. I hope this is useful. See you next month. NovemberThis month I am looking at the DO statement - it its simplest form this specifies a set of statements that are to be done until a matching END statement appears. There are five basic forms of the DO statement:
The simple form of the DO statement is: DO;
*More SAS statements;
END;
This version of the DO statement is more often seen in the selection of statements that are run if a condition is true: IF condition THEN DO;
*More SAS statements;
END;
Before looking at the interactive way the DO statement is used, I will change things around from what you see in most textbooks and talk about two forms that use expressions to determine when the loops stops - the DO WHILE and DO UNTIL. The first is the DO WHILE statement that carries out a set of statements while a condition is true - this has the form: DO WHILE(expression);
*More SAS statements;
END;
The second is the DO UNTIL statement that carries out a set of statements until a condition is true - this has the form: DO UNTIL(expression);
*More SAS statements;
END;
For the WHILE and UNTIL, the expression (an expression is a trail of operands and/or operators that result in a TRUE or FALSE answer) is evaluated. In the case of a DO WHILE, the expression is evaluated at the top the loop, while in the DO UNTIL form the expression is evaluated at the bottom of the loop. The following code will show its use and the difference between these two forms of the DO: 1 data _null_;
2 put 'Example using DO WHILE:';
3 i=0;
4 do while(i<5);
5 put @3 i=;
6 i+1;
7 end;
8
9 put 'Example using DO UNTIL:';
10 i=0;
11 do until(i=5);
12 put @3 i=;
13 i+1;
14 end;
15 run;
Example using DO WHILE:
i=0
i=1
i=2
i=3
i=4
Example using DO UNTIL:
i=0
i=1
i=2
i=3
i=4
Note that a DO UNTIL loop will always do at least one iteration since it is evaluated at the end of the loop. A word of warning though when using the DO WHERE or DO UNTIL - if the expression is never satisfied then the loop will never stop. The Interactive DO statement uses a variable for an index value, and the SAS code between the DO and END statements is repeated for each value of the index variable. There are a couple of versions of this which are shown below: DO index_variable = start_value TO stop_value;
*More SAS statements;
END;
DO index_variable = start_value TO stop_value BY by_value;
*More SAS statements;
END;
Each of these two versions above can be extended with the use of a WHILE or UNTIL as detailed above. Another version of this is where the values for the INDEX_VARIABLE are specified: DO index_variable = value1, value2, valuen;
*More SAS statements;
END;
The last form of the DO statement that I will look at here is the DO OVER statement - this will action the statements within the loop for all the elements defined within an array, as the following example shows: ARRAY monthval {12} jan feb mar apr may jun jul aug sep oct nov dec;
DO OVER monthval;
*More SAS statements;
END;
Below are some examples of valid DO loops: do i=1 to 10; do i=1 to 10 by 2; do i=0 to 10 by 3 while(x To prematurely break out of a DO loop there are two ways of doing this - this is actually very useful to make sure that an infinite loop is not created. The first way is the use of the LEAVE statement that stops the processing of the DO loop and continues with the next statement following the END of the DO loop. The following demonstrates its use: leaving_bonus=0;
DO i=1 TO years_of_service;
leaving_bonus+50;
** Stop if Leaving Bonus > 500;
IF leaving_bonus>=500 THEN LEAVE;
END;
Another way is to use a flag variable - if the flag is 'OFF' then continue, else if 'ON' then stop: leaving_bonus=0;
stopflag=0; ** Flag variable;
DO i=1 TO years_of_service WHILE(stopflag=0);
leaving_bonus+50;
** Stop if Leaving Bonus > 500;
IF leaving_bonus>=500 THEN stopflag=1;
END;
In both of the methods above, the flow of step will continue to any statements set after the END statement associated with the DO statement. DO loops of course can be nested, that is have a DO loop within a DO loop - the number of loops within a loop you can write is for all practical purposes unlimited, but it would be very have to leave if you had too many levels! See the following for an example of a nested DO loop: DO years=1990 TO 2009;
DO months=1 TO 12;
*More SAS statements;
END;
END;
I hope the tip for this month has taught you something about the DO statement. As always, I ask you to take the code and try it out for yourself - this is the best way to learn. See you all in December. DecemberI recently had an email from Nancy in New Jersey on how to do a many-to-many merge in SAS without using SQL, so for this month is a discussion on this issue that often occurs when dealing with data. Lets look at some data first -- this is in the form of SAS card statements (you could also use a DATALINES statement, but for this example I am using the older CARDS statement) so you can copy this data an load it into your SAS Program window: *----------------------------------------------------*;
* Lets load some data;
*----------------------------------------------------*;
data ae; ** Adverse Event Data;
infile cards;
input ptnum $ 1-3 @5 date date9. event $ 15-35;
format date date9.;
cards;
001 16NOV2009 Nausea
002 16NOV2009 Heartburn
002 16NOV2009 Acid Indigestion
002 18NOV2009 Nausea
003 17NOV2009 Fever
003 18NOV2009 Fever
005 17NOV2009 Fever
;
run;
data cm; ** Concomitant Medication Data;
infile cards;
input ptnum $ 1-3 @5 date date9. medication $ 15-35;
format date date9.;
cards;
001 16NOV2009 Dopamine
002 16NOV2009 Antacid
002 16NOV2009 Sodium bicarbonate
002 18NOV2009 Dopamine
003 18NOV2009 Asprin
004 19NOV2009 Asprin
005 17NOV2009 Asprin
;
run;
The merge that is going to be done here is a merge by PTNUM (subject number) and DATE. Some notes on the data:
Lets jump ahead to what we are looking for when we merge the two datasets -- note that we are doing an inner join, i.e. where there is no match the observation is excluded: Obs ptnum date event medication
1 001 16NOV2009 Nausea Dopamine
2 002 16NOV2009 Heartburn Antacid
3 002 16NOV2009 Heartburn Sodium bicarbonate
4 002 16NOV2009 Acid Indigestion Antacid
5 002 16NOV2009 Acid Indigestion Sodium bicarbonate
6 002 18NOV2009 Nausea Dopamine
7 003 18NOV2009 Fever Asprin
8 005 17NOV2009 Fever Asprin
When first looking at merging the two datasets, the first thought may be to use a simple MERGE statement as shown below: ** SAS Code;
*----------------------------------------------------*;
* This will not work!;
*----------------------------------------------------*;
data all0;
merge ae cm;
by ptnum date;
run;
title1 "Merge using the MERGE statement -- this fails";
proc print data=all0;
run;
** SAS Output;
Merge using the MERGE statement -- this fails
Obs ptnum date event medication
1 001 16NOV2009 Nausea Dopamine
2 002 16NOV2009 Heartburn Antacid
3 002 16NOV2009 Acid Indigestion Sodium bicarbonate
4 002 18NOV2009 Nausea Dopamine
5 003 17NOV2009 Fever
6 003 18NOV2009 Fever Asprin
7 004 19NOV2009 Asprin
8 005 17NOV2009 Fever Asprin
As can be seen, this clearly does not work, so how can a many to many merge be done successfully? The most common way that this match is done is with an SQL call, as the following code demonstrates: ** SAS Code;
*----------------------------------------------------*;
* SQL, the most common way that it is seen done;
*----------------------------------------------------*;
proc sql;
create table all0 as
select a.*, b.medication
from ae a inner join cm b
on a.ptnum=b.ptnum and
a.date=b.date;
quit;
run;
title1 "Merge using SQL -- most common way this is seen done";
proc print data=all0;
run;
** SAS Output;
Merge using SQL -- most common way this is seen done
Obs ptnum date event medication
1 001 16NOV2009 Nausea Dopamine
2 002 16NOV2009 Heartburn Antacid
3 002 16NOV2009 Heartburn Sodium bicarbonate
4 002 16NOV2009 Acid Indigestion Antacid
5 002 16NOV2009 Acid Indigestion Sodium bicarbonate
6 002 18NOV2009 Nausea Dopamine
7 003 18NOV2009 Fever Asprin
8 005 17NOV2009 Fever Asprin
This works, but it is not what we want -- we do not want to use SQL in the solution. This brings about the use of the POINT option in the SET statement -- take a look at the following code and try it out in your Program window: ** SAS Code;
*----------------------------------------------------*;
* The datastep with POINT option -- not often seen,;
* but gives most control. Key to this is that the;
* datastep takes each observation in AE, and then;
* tries to match this with each observation in;
* CM -- this is basically a loop within a loop!;
*----------------------------------------------------*;
data all1;
set ae;
drop _:; ** Drop temporary variables;
match=0; ** Match flag;
** Our loop within a loop -- output if match;
do i=1 to xnobs;
** Need to rename the "merging" variables within the CM
dataset;
set cm (rename=(ptnum=_ptnum date=_date)) nobs=xnobs point=i;
** Have to rename matching variables so that they do not overwrite
the original values in AE;
if ptnum=_ptnum and date=_date then do;
match=1; ** Yes, there is a match my the "merging" variables;
output;
end;
end;
run;
title1 "Merge using using the POINT option in a SET statement";
proc print data=all1;
run;
** SAS Output;
Merge using using the POINT option in a SET statement
Obs ptnum date event match medication
1 001 16NOV2009 Nausea 1 Dopamine
2 002 16NOV2009 Heartburn 1 Antacid
3 002 16NOV2009 Heartburn 1 Sodium bicarbonate
4 002 16NOV2009 Acid Indigestion 1 Antacid
5 002 16NOV2009 Acid Indigestion 1 Sodium bicarbonate
6 002 18NOV2009 Nausea 1 Dopamine
7 003 18NOV2009 Fever 1 Asprin
8 005 17NOV2009 Fever 1 Asprin
This is a lot of code. An important note here is that I have had to rename the matching variables in CM so that they do not overwrite the original values in AE (very important) but use the DROP statment to get rid of these when the dataset ALL1 is created. Note also that I have printed out the variable MATCH so that it is easy to see where the match is. Yippie, the output is what we have expected AND the we have not used SQL but instead used a datastep.
There are a couple of other ways to do this merge without SQL, notably a variation on the "Loading Unique Dataset into an Array" which I presented first at NESUG in 2008, which can be seen here, but the technique shown above is a shorter method with similar control (however the use of the array will do a many to many merge with the equivilant SQL OUTER JOIN). Personally I prefer the datastep approach given above since it has the most control. Hope this helps. Have a very happy and safe holiday season. See you all next year. |