| David Franklin, Blog | |
| 2015 Archive | |
| Back | dfranklinnh@gmail.com |
|
JanuaryDates and times are something programmers deal with every day. It is easy to see a date and/or time in character form, but how does SAS store that date in a numeric form? To bring dates or times in character format, SAS has many ways that this can be done, but here are some examples: Date/Time Text SAS Code 12MAY2014 var=INPUT(text,DATE9.); 12MAY14 var=INPUT(text,DATE7.); 12/05/2014 var=INPUT(text,DDMMYY10.); 05/12/2014 var=INPUT(text,MMDDYY10.); MAY2014 var=INPUT(text,MONYY7.); 2014-05-12 var=INPUT(text,YYMMDD10.); 15:32 var=INPUT(text,TIME5.); 12MAY2014:15:32. var=INPUT(text,DATETIME15.); The second parameter to the INPUT function is called an INFORMAT which tells SAS how to read the text – SAS has a load of them but the ones above are the most common for bringing in date and/or times from a text string. Now how does SAS store these dates and times numerically? For dates, “day 0” is January 1st, 1960 and has a value of zero. The date range that is possible in SAS is year 1582 to year 19,900. As the date is stored as a number it is easy to calculate durations, i.e. day number of one date minus the day number of another date. For times, this is the number of seconds from mid-night, and has a range of 0 to 86,400 (there are 86,400 seconds in a day). Lastly, a SAS datetime value is a value representing the number of seconds between January 1, 1960 and an hour/minute/second within a specified date. However, knowing these numbers, it is impossible to see what date and/or time it is. Above we used INFORMATS to read the dates in, now we can display them using some formats: FORMAT Name Display DATE9. 12MAY2014 TIME5. 15:32 DATETIME15. 12MAY2014:15:32 Occasionally I will get a datetime variable but I just need the date portion for the program – this can be done using the DATEPART function as the following code illustrates: var = DATEPART(datetime_numeric_variable); I hope this small introduction into the world of dates and times was useful. FebruaryI received a question from a reader on how to concatenate many individual outputs into a single file. This is a very interesting question and is not as simple as it may seem. The first question that must be asked is whether the page numbers on the individual outputs should remain on the combined document, e.g. for the first file it may have page 1 of 4, on the second file it may have page 1 of 876. The reason for asking this question first is that the output file does not have a concept of "pages" and sets the count after the entire document is loaded into word (ever have the situation where when you open a file and it says Page 1 of 3 but you know there are over 100 pages?). If it is the case that each documents Page x of y values be preserved then the best approach is to create PDF files of each individual output and use a free tool such as PDFTK to combine the individual PDFs into one document. If however you want to create a "master document" with a unique page number for each page, you have a couple of ways of doing this. If you use the same font and color palette and the pages all have the same margins, it is possible to use the following SAS macro: %let rptdir=%str(G:\TEMP); *Change this to the directory you are dealing with;
*Bring in every RTF file;
data _allouts0;
attrib _txt length=$1024 informat=$char1024. format=$char1024.
_fn length=$1024
_myinfile length=$1024
_ln length=8
_myinline length=8;
infile "&rptdir.\*.rtf"
lrecl=1024 filename=_myinfile line=_myinline length=len;
input _txt $varying1024. len; _fn=scan(strip(_myinfile),-1,'\'); _ln=_n_;
run;
*Sort by file name and line number;
proc sort data=_allouts0;
by _fn _ln;
run;
*Do some manipulation;
data _allouts1 _printlist0 (keep=_fn);
length _flg 8; *1=Use in combined file;
retain _flg;
set _alllogs0 end=eof;
by _fn _ln;
if first._fn then do;
output _printlist0; *List of files read;
if _ln=1 then _flg=1; *Knows it is a RTF;
else _flg=0;
end;
else if _txt=:'\widowctrl\' then _flg=1;
if last._fn then do;
if eof then _txt='\pard}'; *Close file;
else _txt='\pard\sect\sectd\sbkpage';
*Add page break and ready for another file;
end;
output _alllogs1; *Output to combined file;
run;
*Create COMBINED.RTF document;
data _null_;
file “&rptdir.\combined.rtf” lrecl=1024;
set _allouts1;
where _flg=1; *Select only lines we need;
put _txt;
run;
*Print list of outputs created;
proc print data=_printlist0;
var _fn;
label _fn='Output Name';
run;
There is one other dependency - this code will only work with files created using ODS RTF in SAS 9.2, all of them using a single template (however the code can be adapted for 9.3 and above if needed) as it exploits the structured way SAS creates the RTF file with ODS. Note also that the output file created is in the same directory as the individual files and is called COMBINED.RTF. Another way that you can combine the documents into one is to create a Master document in word (you only have to create this once) and it will update as documents are updated. This has the advantage of storing documents with different fonts, i.e. some containing Times Roman, some containing Arial and others containing Courier New. SAS is not required and Word is very good at handling Master documents. Unfortunately I have not found a way to combine the documents using a Visual Basic macro that can be imbedded in Microsoft Word. Hope this was useful. MarchThis 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: data class; *** Include a dataset;
set class;
label weight='Weight (kg) at Start of Study';
run;
proc datasets library=work; *** DATASETS procedure;
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 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.3 std*f=8.4
median*f=8.2 min*f=8.0 max*f=8.0)
(height weight)*(n*f=8.0 mean*f=8.2 std*f=8.3
median*f=8.3 min*f=8.1 max*f=8.1),
sex all='Total';
run;
produces the following output (abridged): 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
The MEANS Procedure
Var Label N Mean STD Min Max
Age Age (years) at Start of Study 19 13 1 11 16
Height Height (in) at Start of Study 19 62 5 51 72
Weight Weight (kg) at Start of Study 19 100 23 51 150
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|
Note that the label we defined in the DATASETS procedure carried forward to the PRINT, MEANS and TABULATE procedures. Hope this was useful. AprilThis month I am going away from SAS briefly and into the world of VBA, but this tip is still very useful for SAS programmers. Typically when someone wants to print all MS Word or RTF files from a directory the common way is to select all the relevant files and select print which will print all pages of all documents selected. There are cases however where this is a waste of paper and we only need to print a few pages of each document. This can be easily carried out using the VBA code below: 01 Sub PrintFirstPages() 02 Dim strFileName As String 03 Dim strPath As String 04 strPath = "c:\temp\" 05 strFileName = Dir(strPath + "*.rtf", vbNormal) 06 Do While strFileName <> "" 07 Documents.Open FileName:=strPath + strFileName 08 Documents(strFileName).PrintOut Range:=wdPrintFromTo, From:="1", To:="2" 09 Documents.Close 10 strFileName = Dir 11 Loop 12 End Sub Line 1 defines the procedure, and lines 2 and 3 set the two internal variables. Line 4 specifies the directory path to use - - note, it must end with the '\'. Line 5 selects the files to print -- in this example it is the files with the extension .RTF but this is modifiable. Line 6 starts the loop where each file is opened (lines 7 and 8) and the range of pages specified in line 10 is printed -- in this code pages 1 and 2 are printed, however if there is only one page in the document then that page is printed. The document is finally closed at lines 11 and 12, and the next file in the selection is selected for print at line 12. To enter the VBA code into Microsoft Word select Tools > Macro > Visual Basic Editor then Insert > Module then paste the code above and then press File > Close and Return to Microsoft Word The sequence above may vary due to the version of Microsoft Word that is on your system, but the basic steps are the same. The VBA macro is now ready for use within Word and can be accessed from the VBA macro library. Hope this was useful. MayIn SAS we can group results using formats as given in the example below: Using the following data find the Number of Observations, Mean, Standard Deviation, Median, Minimum and Maximum values for the following data, grouping by Gender and age groups 18-64 and 65-75: ** DATA **
SUBJECT AGE GENDER
001 35 M
002 70 M
003 65 M
004 40 F
005 66 F
006 36 F
** SAS PROGRAM **
PROC FORMAT;
VALUE agex 18-64='18-64' 65-75='65-75' other='Unexpected';
RUN;
PROC MEANS DATA=demog N MEAN STD MEDIAN MIN MAX;
CLASS gender age;
VAR age;
FORMAT age agex.;
RUN;
Using the AGE variable both in the CLASS and VAR statements it is treated both as a category and analysis variable. Note that the median calculation came into the MEANS procedure in SAS version 7 - prior to that the median could be calculated using the UNIVARIATE procedure. Hope this was useful. JuneRecently I was reminded that knowing your data is one of the important things to understand when you start programming. As always, lets start looking at some data - in this example it is beans in a cup from 50 people: data _testdat;
infile cards;
input beans @@;
cards;
2 7 32 10 11 7 20 0 20 37
26 2 26 41 8 16 23 21 23 .
1 6 26 22 47 18 18 12 -5 10
;
run;
The request is simple - want to know the N, maximum and minimum values for the groups 0 to 20, 21 to 40 and >41. What programmers would typically write is something along the lines of: proc format;
value beancatf
1='0-20'
2='21-40'
3='>40';
run;
data _test1;
set _testdat;
* Create a categorized weekly beanage variable;
if (beans le 20) then bean_cat=1;
else if (21 le beans le 40) then bean_cat=2;
else if (beans > 40) then bean_cat=3;
format bean_cat beancatf.;
run;
proc means data=_test1
nmiss min max missing maxdec=0;
class bean_cat;
var beans;
run;
which results in the following output: The MEANS Procedure
Analysis Variable : beans
N N
bean_cat Obs Miss Minimum Maximum
-----------------------------------------------------
0-20 33 3 -5 20
21-40 15 0 21 37
>40 2 0 41 47
-----------------------------------------------------
For some we have done what is asked, lets save the output and send off. There are however two interesting results here which as programmers we should look at, both highlighted. The first is the Number Missing of 3 – this is usually important to those analyzing data as the number of missing can influence the results. This count should be considered a separate category. The second number of interest is the minimum value at the 0-20 category as we are expecting a value somewhere between 0 and 20. This then raises the issue of whether the value of -5 was correctly entered. For the purposes of this article we shall treat this as an outlier, a value is present but the value is ‘out of range’. So lets now alter the program to deal with these two situations, the missing and ‘outlier’ data: proc format;
value beancatf
.='Missing'
0='Other'
1='0-20'
2='21-40'
3='>40';
run;
data _test1;
set _testdat;
* Create a categorized weekly beanage variable;
if (0 le beans le 20) then bean_cat=1;
else if (21 le beans le 40) then bean_cat=2;
else if (beans > 40) then bean_cat=3;
else if ^missing(beans) then bean_cat=0;
format bean_cat beancatf.;
run;
proc means data=_test1
nmiss min max missing maxdec=0;
class bean_cat;
var beans;
run;
which results in the following output: The MEANS Procedure
Analysis Variable : beans
N N
bean_cat Obs Miss Minimum Maximum
-----------------------------------------------------
Missing 3 3 . .
Other 3 0 -5 -1
0-20 27 0 0 20
21-40 15 0 21 37
>40 2 0 41 47
-----------------------------------------------------
Now we have something which is presentable to our audience that makes sense. The moral of this article is to show that although results do appear on an output, it is your responsibility to all see that they are reasonable. As an aside, the next question is, can we get the same result without having to code the data into the bean_cat variable? Answer is yes, as we can us the format to do this for us. Lets look at the following code: proc format;
value beancatf
. ='Missing'
0-20='0-20'
21-40='21-40'
41-HIGH='>40'
OTHER ='Other';
run;
proc means data=_test1 nmiss min max missing maxdec=0;
class beans;
var beans;
format beans beancatf.;
run;
which results in the following output: The MEANS Procedure
Analysis Variable : beans
N N
beans Obs Miss Minimum Maximum
-----------------------------------------------------
Missing 3 3 . .
Other 3 0 -5 -1
0-20 27 0 0 20
21-40 15 0 21 37
>40 2 0 41 47
-----------------------------------------------------
Note that the code the variable BEANS is being used as both the analysis variable and the class variable, with the classes being formed by the format BEANCATF. Hope this was useful. JulyLets say we have a dataset with variables A1, A2, A3, …., A20 and I want to find the average. We could write: avg=(a1+a2+a3+a4+…+a19+a20)/20; But this is inefficient and if any of the variables in the list are missing the variable AVG will be missing. We could then change it to: avg=MEAN(a1,a2,a3,a4,…,a19,a20); But writing out this list is tedious. Better is to use the OF statement and a list of the variables as shown below: avg=MEAN(OF a1-a20); Another way is to use an array, as the following example shows: array zaz{*} a1-20;
avg=MEAN(OF zaz(*));
This example uses the MEAN function but the same idea of using the OF function can also be used for N, NMISS, STD, MIN, MAX, STDERR, MEDIAN, GEOMEAN, SUM, VAR, PCTL or the myriad of other descriptive statistic functions. Hope this was useful. AugustHow do we put a ‘0’ in our report for a category that has no records? For example, there are age groups 0-17, 18-65, and >65, but you have no records with a age of 0-17? Lets first look at some data as an example: data _agegrp;
infile cards;
input age @@;
if age>65 then agegrp=3;
else if age>=18 then agegrp=2;
else if n(age) then agegrp=1;
cards;
33 39 52 73 47 45 26 57 82 90
;
run;
and this results in the following output table: Obs age agegrp 1 33 2 2 39 2 3 52 2 4 73 3 5 47 2 6 45 2 7 26 2 8 57 2 9 82 3 10 90 3 Also for the purposes of this article, the following format will be applied: proc format;
value agegrpf
1='0-17'
2='18-65'
3='>65';
run;
One way to make sure all age groups are represented in the output is to get the frequency, merge it with a dummy table showing all possible results and then use a PROC PRINT to do the output, as shown below: proc freq data=_agegrp;
tables agegrp /noprint out=_tmp;
data _allpos;
do agegrp=1 to 3;
output;
end;
data _final;
merge _allpos _tmp;
by agegrp;
count=max(0,count);
proc print data=_final;
var agegrp count;
format agegrp agegrpf.;
run;
with the following output: Obs agegrp COUNT 1 0-17 0 2 18-65 7 3 >65 3 While this works, it does involve four steps - lets see if we can do it in one! Lets look at PROC TABULATE. There are two options that are useful for this, PRINTMISS and PRELOADFMT, as the following code demonstrates: proc tabulate data=_agegrp;
format agegrp agegrpf.;
class agegrp /preloadfmt;
var age;
table agegrp='Age Group',age=''*n='N'*f=12.
/printmiss;
run;
with the output ----------------------------- | | N | |--------------+------------| |Age Group | | |--------------| | |0-17 | 0| |--------------+------------| |18-65 | 7| |--------------+------------| |>65 | 3| ----------------------------- Related is using PROC REPORT and the COMPLETEROWS and, again, the PRELOADFMT options, as the following code demonstrates: proc report data=_agegrp completerows nowindows;
format agegrp agegrpf.;
column agegrp age;
define agegrp /group order=internal preloadfmt width=10 'Age Group';
define age / analysis n 'N' width=5;
quit;
run;
with the output Age Group N
0-17 0
18-65 7
>65 3
Next question, can't PROC MEANS do it? Answer is yes with the COMPLETETYPES, and, again, the PRELOADFMT options, as the following code demonstrates: proc means data=_agegrp completetypes n;
format agegrp agegrpf.;
class agegrp /preloadfmt;
var age;
run;
with the following output The MEANS Procedure
Analysis Variable : age
N
agegrp Obs N
-------------------
0-17 0 0
18-65 7 7
>65 3 3
-------------------
Presented have been three methods that can put a ‘0’ in our report for a category that has no records, in one step. Hope this was useful. SeptemberHow can an external file be deleted using SAS? One way is to use the 'X' command and use a system command like 'DEL' in DOS. But another way is to use the FDELETE function inside a datastep as the following example demonstrates: data _null_;
** Assign the external file being deleted to a
reference 'fn' using the FILENAME statement;
rc=FILENAME('fn',"c:\temp\dmext.txt");
** Now delete the file using the FDELETE
statement -- if RC does not equal zero then
there is an issue;
rc=FDELETE('fn');
if rc=0 then put "File deleted";
else put "ERR"
"OR: Issue with trying to delete the file";
** Clear the FILENAME reference 'fn';
rc=FILENAME('fn');
run;
In the example the dataset DMEXT.TXT in the directory C:\TEMP is deleted -- there is also an additional piece of code that puts out a message as to whether there is an issue with the deleting of the file or not. Hope this was useful. OctoberSAS provides several ways for a user to input data using a CARDS or DATALINES statement. In the examples the CARDS statement is used but the two statements for the most part can be used interchangeably. (The CARDS statement originates from the time when punch cards were used to store data.) Note that for each example the following statement was used to define the CITY and DISTANCE variables: length city $15 distance 8; List Input, known for its simplicity input city $ distance; cards; Amsterdam 370 Montreal 5200 Auckland 22720 ; Delimited Input, using the DLM= option (example uses '~' but it is possible to use other characters as the delimiter) infile cards dlm='~'; input city $ distance; cards; Paris~330 New York~5530 Sydney~215660 ; "Double Space" as a delimiter input city $ & distance; cards; Frankfurt 640 Rio de Janeiro 11060 Singapore 10810 ; Named Input input city= $ distance=; cards; city=Copenhagen distance=953 distance=6800 city=Nairobi city=Tokyo distance=15260 ; Column Input input city $ 1-11 distance 13-20; cards; Rome 1430 Mexico City 10640 Hong Kong 13200 ; Formatted Input input city $11. @13 distance 5.; cards; Stockholm 1450 Los Angeles 8780 Bangkok 12860 ; It is possible to mix the methods inside an INPUT statement but use caution as unexpected results can easily occur. Hope this was useful. NovemberOne of those very important lessons you learn as a programmer starting out, particularly in our field, is to ALWAYS run your production code in batch mode, not interactive mode. A skilled programmer will almost always write their code in such a way that the program will not produce output unless you are in batch mode. The first reason is that it makes sure that there are no dependencies lurking about that may cause an issue, e.g. %let foot2=Source &pgm Created &sysdate9.; %let pgm=T14.2.1; This example was from an actual program, amended to protect those who wrote the code and those who QCed the program. What you may ask is wrong with this? The value of macro variable PGM is set AFTER it is used in the FOOTNOTE2 macro variable, so it actually is not assigned on the first time around, but if you run the program a second time while in interactive mode the macro variable PGM is already set so is available in the FOOTNOTE2 macro variable – the issue is not detected. A second reason is that it will produce a clean LOG file showing what was initiated when the SAS program started, what was run and the actual output from each step, and when it was run (SAS in interactive mode has an internal date and time of the session start, not the time of program run, unless you do a trick or two). The third reason is that any program must be able to be reproducible at any time – a client or regulatory review can ask you as a SAS programmer run the program and months after it was run and then check that the output is the same, except of runtime dates. This last item is a debatable item, but the most important file for any archive is the SAS LOG – from it you can get the date and time the program is run, what was run, and be able to recreate the program from it. Hope this was useful. DecemberHere is some code that looks okay. . proc delete data=work._all_;
run;
data dset1;
name='Tom'; ina='Yes';
output;
data dset2;
name='Tommy'; inb='Yes';
output; run;
data _all0;
merge dset1 dset2;
by name;
run;
However output will be a dataset with one record: The SAS System Obs name ina inb 1 Tom Yes Yes There is an indication that there is an issue in the SAS LOG, namely WARNING: Multiple lengths were specified for the BY variable name by input
data sets. This may cause unexpected results.
and yes THIS DOES cause unexpected results. Any WARNING message in your SAS LOG is something you should not ignore. Another piece of code: 19 data _null_;
20 age = 10; *Add a;label
21 age = age + 5; *Add 5 to age;
22 run;
NOTE: DATA statement used (Total process time):
real time 0.00 seconds
cpu time 0.00 seconds
What do you think the value of age is after this code is run? If you said 15, then you are wrong - it is actually 10 as the label for AGE is 'age + 5', look at the placement of the second semicolon in the log file line 20 (a LABEL statement does not need quotes around the text, actually it is not needed in the title or footnote statements either!). A warning message will not always appear when there is an issue with the code! Hope this is useful. |