- Mark as New
- Bookmark
- Subscribe
- Mute
- RSS Feed
- Permalink
- Report Inappropriate Content
The question asks to produce means statistics for a list of number of years for the input dataset, and suggests using a %do %until statement and a %scan function inside a macro to do it. The solution offered is as follows:
For me, this solution is a little bit confusing and complicated because of 1) the %do %until statement is written in a not so frequently used way, i.e., without the i=xxx to xxx part and 2) the logic behind the repeated %let i= and %let yr= is difficult to understand. I think my solution(below) is simpler and easier (without the %do %until and use a %sysfunc(countw) instead) to produce reports or statistics for a list of years (or any other lists of things, as a macro variable).
%macro storms(list);
%let count=%sysfunc(countw(&list));
%do i=1 %to &count;
%let yr=%scan(&list,&i);
title "&yr Storms";
proc means data=mc1.storm_final n min
mean max maxdec=0;
var MaxWindMPH MinPressure;
where season=&yr;
run;
%end;
%mend storms;
%storms(2011 2012 2014);
Accepted Solutions
- Mark as New
- Bookmark
- Subscribe
- Mute
- RSS Feed
- Permalink
- Report Inappropriate Content
That shown style of looping over a list with %DO %UNTIL was common prior to the introduction of the COUNTW function. I think COUNTW was introduced in v7 or v8.
I would agree that using COUNTW to count the number of items in a list seems clearer to me also.
Next up: Rick Wicklin presents Ten Tips for Effective Statistical Graphics (with SAS code) on Wednesday March 26.
Register now at https://www.basug.org/events.
- Mark as New
- Bookmark
- Subscribe
- Mute
- RSS Feed
- Permalink
- Report Inappropriate Content
@dxiao2017 wrote:
The question asks to produce means statistics for a list of number of years for the input dataset, and suggests using a %do %until statement and a %scan function inside a macro to do it. The solution offered is as follows:
For me, this solution is a little bit confusing and complicated because of 1) the %do %until statement is written in a not so frequently used way, i.e., without the i=xxx to xxx part and 2) the logic behind the repeated %let i= and %let yr= is difficult to understand. I think my solution(below) is simpler and easier (without the %do %until and use a %sysfunc(countw) instead) to produce reports or statistics for a list of years (or any other lists of things, as a macro variable).
%macro storms(list);
%let count=%sysfunc(countw(&list));
%do i=1 %to &count;
%let yr=%scan(&list,&i);
title "&yr Storms";
proc means data=mc1.storm_final n min
mean max maxdec=0;
var MaxWindMPH MinPressure;
where season=&yr;
run;
%end;
%mend storms;
%storms(2011 2012 2014);
The problem here is that although the course is about using macros, this is a problem that doesn't need to be solved by macros, and you would be much better off simply producing the results with PROC MEANS and a BY statement. So it is teaching you to use macros when macros are not needed, in my opinion, this is the wrong lesson to learn. I have said this many times: an important piece of learning to use macros is learning when NOT to use macros.
Example using SASHELP.CARS data set
proc sort data=sashelp.cars out=cars;
by origin;
run;
proc means data=cars n min mean max maxdec=0;
where origin='Asia' or origin='Europe';
by origin;
var msrp invoice;
run;
Paige Miller
- Mark as New
- Bookmark
- Subscribe
- Mute
- RSS Feed
- Permalink
- Report Inappropriate Content
Thanks a lot for your comments, I think it's a very good point that use macro (or any other advanced and complex procs and steps) only when it is necessary. However, I really do NOT think that question (as well as the solution) in the lesson was poorly designed, as 1) the learning material was to cover a lot of important contents of macro, and it was also designed for test preparation and had to include all the knowledge and techniques in the exam content guide, and therefore 2) while design the material, (I guess) the course developer was intended to integrate as much as possible contents in one question, and this makes the solution code complicated, and 3) besides the macro part of the built-in base 9.4 guide (SAS 9.4 macro language reference), as a new learner I think Macro1:Essential is the good official learning material I can find now.
For that question in my post, what made the solution code complex was the %let i=%eval(&i+1); statement, which now I guess was perhaps exactly what the material developer wanted learners to comprehend (which is not easy, e.g. how the two repeated %let i=; and %let yr=; statements work) , because that statement is used in a lot of scenarios when there is a do loop inside a macro.
- Mark as New
- Bookmark
- Subscribe
- Mute
- RSS Feed
- Permalink
- Report Inappropriate Content
@dxiao2017 wrote:
Thanks a lot for your comments, I think it's a very good point that use macro (or any other advanced and complex procs and steps) only when it is necessary. However, I really do NOT think that question (as well as the solution) in the lesson was poorly designed, as 1) the learning material was to cover a lot of important contents of macro, and it was also designed for test preparation and had to include all the knowledge and techniques in the exam content guide, and therefore 2) while design the material, (I guess) the course developer was intended to integrate as much as possible contents in one question, and this makes the solution code complicated, and 3) besides the macro part of the built-in base 9.4 guide (SAS 9.4 macro language reference), as a new learner I think Macro1:Essential is the good official learning material I can find now.
For that question in my post, what made the solution code complex was the %let i=%eval(&i+1); statement, which now I guess was perhaps exactly what the material developer wanted learners to comprehend (which is not easy, e.g. how the two repeated %let i=; and %let yr=; statements work) , because that statement is used in a lot of scenarios when there is a do loop inside a macro.
A person who takes this macro course then goes out and has a real problem which is similar to the one presented, the person needs means by different years. And the person says "I learned how to do that in my class! I'll write a macro!" And I contend this is the wrong thing to do, and the person has learned the wrong lesson.
If I was going to design a macro class, I would not include using macros to replace other built-in tools in SAS. If the developer of the class wants to teach %let i=%eval(&i+1); there are plenty of ways to teach this without re-creating a BY statement with macro code.
Paige Miller
- Mark as New
- Bookmark
- Subscribe
- Mute
- RSS Feed
- Permalink
- Report Inappropriate Content
I agree with you on this point that when writing a macro is not necessary then do not use macro, when a macro is necessary then use macro and do not use other procedures. There is another question (m105a04) in the material on the PDF's page 262, the solution of which was to produce separate tables for different levels (categories) of a variable, and a cat() statement in a sql step was used to split the table (and this serves as the first step of producing separate TFLs for different levels of a variable), the code and output are as follows (BTW: in the demo m105d04 next to the question, the code was indeed developed to a macro, which I tried several times and was not able to run through, perhaps due to the difficulty in debugging (for both the cat() statement and the case when then; statement in the subsequent data step which was intended to produce separate datasets):
%let tab=sashelp.cars;
%let col=Origin;
proc sql;
select distinct &col
from &tab;
select distinct cat('when ("', &col, '") output ', &col)
from &tab;
quit;
In the above code, the character string in the cat() statement consumes a lot of eyesight and detailed attention to type it. The more important issue is that I think this is a question which can be properly (and more appropriately) solved through a macro (there are other questions and sections for this type of macro in the material though, btw), i.e., a macro here is necessary (BTW, here as I mentioned in last paragraph, I made a mistake here - the code was indeed developed into a macro, yet which possibly may cost a lot debugging) . However, I guess the cat() statement here in the solution code for this question is to let leaners know that some steps in the macro can be replaced by the cat() statement, i.e., a possible usage of the cat() function and cat() statements in a sql step. The macro that I think can properly solve the question and the output are as follows (BTW: the code below was generated according to the solution code on page 286 of the PDF for another question, m105p01, as the reference code, which uses a proc print; step instead of proc sql; select * from; statement). A further step of consideration may be adding in the create table &xxx as to name the different datasets according to their respective categories/levels of that variable.
%macro splittable(tab,col);
proc sql;
select distinct &col
into :cat1-
from &tab;
quit;
%do i=1 %to &sqlobs;
proc sql outobs=2;
select *
from &tab
where &col="&&cat&i";
quit;
%end;
%mend splittable;
%splittable(sashelp.cars,origin);
- Mark as New
- Bookmark
- Subscribe
- Mute
- RSS Feed
- Permalink
- Report Inappropriate Content
PS: as for the idea and complete code of split table and create separate dataset according to their names, another reference code is from activity 5.05, p268 of SAS Macro Language 1: Essentials course notes PDF.
- Mark as New
- Bookmark
- Subscribe
- Mute
- RSS Feed
- Permalink
- Report Inappropriate Content
My comments in this post also apply to the practice question (level-challenge) on page 317 of SAS SQL1: Essentials course note PDF, the question and answer code (which I guess I would not use the code if there is any other alternative I can choose) are as follows:
And have to say countw() function is not good here because the character string is complex (has several ampersand& and one hyphen-) and struggle many times using %nrstr to deal with delimiters (and this till now I do not think I can figure out the thorough and detailed usage) was not able to fix the errors (see below). The best solution perhaps is the select into :list1- and %do i=1 %to &sqlobs one.
/* %put ®ionvalue; */
/* */
/* %let n=%sysfunc(countw(a&b,c&d,e&f,g-h)); */
/* %put &n; */
/* */
/* %let n=%sysfunc(countw(®ionvalue)); */
/* %put &n; */
/* */
/* %let string=%nrstr(®ionvalue); */
/* %put &string; */
/* %let i=%sysfunc(countw(&string,',')); */
/* %put &i; */
%macro split(tab,col);
proc sql;
select distinct &col
into :colvalue1-
from &tab
where &col^=' ';
quit;
%do i=1 %to &sqlobs;
proc sql;
select *
from &tab
where &col="&&colvalue&i";
quit;
%end;
%mend split;
%split(sq.globalmetadata,region);
- Mark as New
- Bookmark
- Subscribe
- Mute
- RSS Feed
- Permalink
- Report Inappropriate Content
I am not sure where this appears in their overall instruction plan but I would definitely agree that it is better to learn how to perform a simple iterative %DO loop before trying to learn how to do a complex one where you have to code your own increments and boundary tests.
Also I am not sure why they left out the %LOCAL statement that should be there to prevent running this macro from modifying existing macro variables named I or YR.
- Mark as New
- Bookmark
- Subscribe
- Mute
- RSS Feed
- Permalink
- Report Inappropriate Content
There are sections and questions about %local statement in the material. In fact, while doing the question I tried to use %local statement inside the macro but did not run through (as using %local statement inside a macro makes the macro a lot more complicated, will try it later).
- Mark as New
- Bookmark
- Subscribe
- Mute
- RSS Feed
- Permalink
- Report Inappropriate Content
Hi Tom, I have two further questions: 1) could you come up with an example (base on the code in my post) that use %local statement, and 2) explain a bit more on how the %let i=%eval(&i+1); statement work in the code in my post? Many thanks!
- Mark as New
- Bookmark
- Subscribe
- Mute
- RSS Feed
- Permalink
- Report Inappropriate Content
@dxiao2017 wrote:
Hi Tom, I have two further questions: 1) could you come up with an example (base on the code in my post) that use %local statement, and 2) explain a bit more on how the %let i=%eval(&i+1); statement work in the code in my post? Many thanks!
Local statement is very simple. Just list the macro variables that you want defined as local to the macro. In this program that would look like:
%macro storms(list);
%local count i yr ;
%let count=%sysfunc(countw(&list,%str( )));
%do i=1 %to &count;
%let yr=%scan(&list,&i,%str( ));
title "&yr Storms";
proc means data=mc1.storm_final n min
mean max maxdec=0
;
where season=&yr;
var MaxWindMPH MinPressure;
run;
%end;
%mend storms;
%storms(2011 2012 2014);
To see the impact try running this little program with both versions of the macro. What value does COUNT have at the end?
%let count=100;
%storms(2011 2012 2014)
%put &=count;
Also try running it both ways when the COUNT macro variables does not exist before the macro runs:
%symdel count ;
%storms(2011 2012 2014)
%put &=count;
This statement:
%let i=%eval(&i+1);
Is a simple %LET to assign a value (a new value) to a macro variable. The macro function %EVAL() will evaluate simple integer arithmetic (and boolean) expressions. So &I+1 will convert to something like 1+1 or 2+1 and %EVAL() will then convert that into 2 or 3 and the %LET will store that new digit string into the macro variable I.
- Mark as New
- Bookmark
- Subscribe
- Mute
- RSS Feed
- Permalink
- Report Inappropriate Content
Hi Tom, thanks a lot for your feedback, but I am not sure I understand your steps correctly or not, below is what I run and the output, there is no difference in this output and the output of that one without the %local statement. For my second question, I think what I asked was: for what scenario the %let i=%eval(&i+1); statement is useful and necessary, because for any list of strings or numbers, we can use select distinct var into :list; and &sqlobs or call symputx(xxx, yyy); or the countw() function to decide how many times the %do loop run.
%macro storms(list);
%local count i yr;
%let count=%sysfunc(countw(&list));
%do i=1 %to &count;
%let yr=%scan(&list,&i);
title "&yr Storms (local count)";
proc means data=mc1.storm_final n min
mean max maxdec=0;
var MaxWindMPH MinPressure;
where season=&yr;
run;
%end;
%mend storms;
%let count=100;
%storms(2011 2012 2014);
%put &count;
- Mark as New
- Bookmark
- Subscribe
- Mute
- RSS Feed
- Permalink
- Report Inappropriate Content
As your post showed, when you run:
%let count=100;
%storms(2011 2012 2014)
%put &count;
If your macro storms uses the %local statement to create a new local macro variable COUNT, the %PUT statement at the end will show that the global macro variable COUNT still has the value 100.
Now if you remove the %local statement from your macro definition and recompile the macro, then run the same code, the %PUT statement at the end will show that the global macro variable COUNT has a different value. Please try it out.
Next up: Rick Wicklin presents Ten Tips for Effective Statistical Graphics (with SAS code) on Wednesday March 26.
Register now at https://www.basug.org/events.
- Mark as New
- Bookmark
- Subscribe
- Mute
- RSS Feed
- Permalink
- Report Inappropriate Content
Thanks a lot for your suggestion, I tried some thing like this (see below) after read again in the learning material the section (p174 of the PDF) on %local and %global statements. I tested the code below in windowing (base 9.4) environment (this does not make anything different btw just somehow feel like to mention it for no reason). Will try other examples later.
- Mark as New
- Bookmark
- Subscribe
- Mute
- RSS Feed
- Permalink
- Report Inappropriate Content
Your example shows that calling TEST1 did NOT change the value of X defined in the global symbol table. Note that there was not need to wrap the %PUT inside of the macro named TEST2 to show this.
Now make a new version of TEST1 (or make a TEST3) that does NOT include the %LOCAL statement and see what value X has after you call that new macro. It should be 2 instead of the 1 you set it to at the top.