How to loop over list items in a macro in sas? - loops

I wanted to perform data calculations with items from a list:
%let list = ("Apple" "Pear" "Grapes")
%macro px(list);
%do k=1 %to dim(&list.);
data data_1;
set output_&item_one.;
(...)
run;
proc sort data = data1;
by &variables.;
run;
data data_2;
set data1;
(...)
run;
proc export data = data_n
outfile= "&input_path.\file_name.xlsx"
dbms=xlsx replace;
sheet = "SUMMARY";
run;
%mend;
%px(list);
So I first take Apple without " do all data calculations and then take Pear etc.
Many thanks!

One way is to use COUNTW and SCAN
A macro list is typically just the items, no parentheses or quoting. However, how the items are utilized when resolved can change you design on what you start with.
However, if the list comes with parentheses and quoted values, you can compress those out.
Example:
%let items = Apple Pear Grapes;
%macro list_each(items=);
%* Many percents = The joy of escapism;
%let items = %sysfunc(compress(&items,%str(%(%"%))));
%local index item;
%do index = 1 %to %sysfunc(countw(&items, %str( )));
%let item = %scan(&items,&index,%str( ));
%put NOTE: &=index &=item;
%end;
%mend;
%list_each (items=&items)
%list_each (items=Alpha Beta Gamma)
%list_each (items=("A" "B" "C"))
Log
### %list_each (items=&items)
NOTE: INDEX=1 ITEM=Apple
NOTE: INDEX=2 ITEM=Pear
NOTE: INDEX=3 ITEM=Grapes
### %list_each (items=Alpha Beta Gamma)
NOTE: INDEX=1 ITEM=Alpha
NOTE: INDEX=2 ITEM=Beta
NOTE: INDEX=3 ITEM=Gamma
### %list_each (items=("A" "B" "C"))
NOTE: INDEX=1 ITEM=A
NOTE: INDEX=2 ITEM=B
NOTE: INDEX=3 ITEM=C

You cannot use a macro to loop over a list directly, but you can do it indirectly. If your list will not contain spaces (i.e. "Delicious Apple"), space-separating is a good way to go. If it will, pipe-separating, |, is the easiest route; however, you can use the Q modifier of %scan() to handle this if you'd like.
We'll do this by using %scan() to pull a single item from the list and loop until the end. We can use %sysfunc(countw()) to count the number of words in the list.
%let list = Apple Pear Grapes;
%macro px(list);
%do i = 1 %to %sysfunc(countw(&list.));
%let k = %scan(&list., &i.);
%put &i. &k.;
%end;
%mend;
%px(&list);
Output:
1 Apple
2 Pear
3 Grapes
If you pipe-separate it, simply add it as an argument to %sysfunc(countw()) and %scan():
%do i = 1 %to %sysfunc(countw(&list., |));
%let k = %scan(&list., &i., |);

To deal with that style of list you could just tell %SCAN() that space and parentheses are the delimiters. To remove the quotes use the DEQUOTE() function.
%macro test(list);
%local i next ;
%do i=1 %to %sysfunc(countw(&list,( ),q));
%let next=%sysfunc(dequote(%scan(&list,&i,( ))));
%put &=i &=next ;
%end;
%mend;
%let list = ("Apple" "Pear" "Grapes");
%test(&list);
Result:
I=1 NEXT=Apple
I=2 NEXT=Pear
I=3 NEXT=Grapes

Related

sas macro variable in loop thru end of month

my idea is to get a data set from tables where the variable is dynamically created based on the end of the month date. I know I'm missing sth with format/informat of a variable, but I'm super lost, please help me understood what I'm missing:
I need variable YYYYMMDD to look like ie20200229 to be sure to get leap year right.
I have database with no partion that's why each end of month is in different table.
%MACRO MEH;
%DO n=2018 %TO 2022;
&LET yyyy = n;
%DO i=1 %TO 12;
%LET mm= i;
%LET YYYYMMDD= %sysfunc(putn(intnx(month,mdy(( input(&mm.),28,input(&yyyy.)),0,e), YYMMDDN8.)); /*IE. 20200229 */
RUN;
/*this part works*/
PROC SORT DATA=XYZ.NAME_OF_TABLE_&YYYYMMDD.
out=work.NEW_NAME&YYYYMMDD. nodupkey;
by SOME_ID;
RUN;
/* MORE CODE HERE*/
DATA work.TEMP2;
MERGE work.TEMP1
work.NEW_NAME&YYYYMMDD;
BY SOME_ID;
RUN;
DATA work.WIN&YYYYMMDD.;
MERGE work.TEMP6
work.TEMP2;
BY SOME_ID;
RUN;
/* MORE CODE Here this code do sorting merging and filtering specific vales i did on SQL but I'm missing some function (like lag, to do in on SQL only) */
/*END OF WORKING CODE*/
/*CEAN UP*/
proc datasets nolist lib=work;
DELETE NEW_NAME: ;
DELETE TEMP: ;
RUN;
%END;
%END;
%MEND MEH;
%MEH;
To loop over date intervals it is usually easier to use an integer index. You can then use INTNX() function to to calculate the next interval's start date. You can use INTCK() to calculate how many iterations are required.
%let start='01JAN2018'd;
%let end='01DEC2012'd;
%do offset=0 %to %sysfunc(intck(month,&start,&end));
%let ymd = %sysfunc(intnx(month,&start,&offset),yymmddn8.);
....
%end;
Drop the YYYY and mm macro variables as they're redundant
Remove the RUN as well, not required
Then you can replace the top of your macro with the following:
%DO n=2018 %TO 2022;
%DO i=1 %TO 12;
%LET YYYYMMDD= %sysfunc(intnx(month, %sysfunc(inputn(&n.&i., yymmn6.)), 0, e), yymmddn8.); /*IE. 20200229 */
%put &yyyymmdd.; * for testing;

SAS macro loop to loop through datasets in Set statement

I have 2 SAS macro loops - 1 to create time series dataset and another to append the datasets into a master dataset.
Macro Date_loop1 creates time series datasets. In this example I am creating dummy datasets. In reality, the real datasets are several MBs large. So using proc append, within Date_loop1, to the master dataset, results in slow progress. Therefore, I have created a second macro, Date_loop2, to stacks all the datasets above in the SET statement. The issue is that the second loop is not working as intended. Can you please help?
Note: if you run Date_loop1, you can see how the datasets are created. Date_loop2 should also work in the similar manner.
%MACRO LOOP1;
DATA ROLLRATE_&ST_YYYYMM._&ED_YYYYMM.;
ST_YYYYMM = &ST_YYYYMM;
ED_YYYYMM = &ED_YYYYMM;
RUN;
%MEND;
%MACRO DATE_LOOP1(START,END);
%LET START_YEAR = %SYSFUNC(FLOOR(&START/100));
%LET START_MONTH = %SYSFUNC(MOD(&START,100));
%LET END_YEAR = %SYSFUNC(FLOOR(&END/100));
%LET END_MONTH = %SYSFUNC(MOD(&END,100));
%LET START_DATE = %SYSFUNC(MDY(&START_MONTH,01,&START_YEAR));
%LET END_DATE = %SYSFUNC(MDY(&END_MONTH,01,&END_YEAR));
%LET DIF=%EVAL(%SYSFUNC(INTCK(MONTH,&START_DATE,&END_DATE)));
/*DATA MASTER_ROLLRATE;RUN;DATA ROLLRATE;RUN;*/
%DO I=0 %TO &DIF;
%DO J=&I %TO &DIF;
%LET ST_DT = %SYSFUNC(INTNX(MONTH,&START_DATE,&I,B)); /*B - DATE AT THE BEGINNING OF THE WEEK/MONTH/YEAR */
%LET ST_YEAR = %SYSFUNC(YEAR(&ST_DT));
%LET ST_MONTH = %SYSFUNC(MONTH(&ST_DT));
%LET ST_YYYYMM = %EVAL(&ST_YEAR*100+&ST_MONTH);
%LET ED_DT = %SYSFUNC(INTNX(MONTH,&START_DATE,&J,B));
%LET ED_YEAR = %SYSFUNC(YEAR(&ED_DT));
%LET ED_MONTH = %SYSFUNC(MONTH(&ED_DT));
%LET ED_YYYYMM = %EVAL(&ED_YEAR*100+&ED_MONTH);
%PUT &ST_YYYYMM &ED_YYYYMM;
%LET START_YYYYMM = &ST_YYYYMM;%LET END_YYYYMM = &ED_YYYYMM;%LOOP1;RUN;
%END;
%END;
%MEND DATE_LOOP1;
%DATE_LOOP1(201901,201902);
%MACRO DATE_LOOP2(START,END);
%LET START_YEAR = %SYSFUNC(FLOOR(&START/100));
%LET START_MONTH = %SYSFUNC(MOD(&START,100));
%LET END_YEAR = %SYSFUNC(FLOOR(&END/100));
%LET END_MONTH = %SYSFUNC(MOD(&END,100));
%LET START_DATE = %SYSFUNC(MDY(&START_MONTH,01,&START_YEAR));
%LET END_DATE = %SYSFUNC(MDY(&END_MONTH,01,&END_YEAR));
%LET DIF=%EVAL(%SYSFUNC(INTCK(MONTH,&START_DATE,&END_DATE)));
DATA MASTER_ROLLRATE;
SET
%DO I=0 %TO &DIF;
%DO J=&I %TO &DIF;
%LET ST_DT = %SYSFUNC(INTNX(MONTH,&START_DATE,&I,B)); /*B - DATE AT THE BEGINNING OF THE WEEK/MONTH/YEAR */
%LET ST_YEAR = %SYSFUNC(YEAR(&ST_DT));
%LET ST_MONTH = %SYSFUNC(MONTH(&ST_DT));
%LET ST_YYYYMM = %EVAL(&ST_YEAR*100+&ST_MONTH);
%LET ED_DT = %SYSFUNC(INTNX(MONTH,&START_DATE,&J,B));
%LET ED_YEAR = %SYSFUNC(YEAR(&ED_DT));
%LET ED_MONTH = %SYSFUNC(MONTH(&ED_DT));
%LET ED_YYYYMM = %EVAL(&ED_YEAR*100+&ED_MONTH);
%LET START_YYYYMM = &ST_YYYYMM;%LET END_YYYYMM = &ED_YYYYMM;
ROLLRATE_&START_YYYYMM._&END_YYYYMM;
%END;
%END;
;
RUN;
%MEND DATE_LOOP1;
%DATE_LOOP2(201901,201902);
You can use name lists in the SET statement and avoid macro altogether.
Every dataset whose name starts with rollrate will be stacked:
data want;
set work. rollrate:;
run;
There are two issues I am currently seeing
The %mend for second loop is date_loop1 instead of date_loop2 (Second last line of code)
%MEND DATE_LOOP1; /Should be DATE_LOOP2/
%DATE_LOOP2(201901,201902);
Please remove semicolon after ROLLRATE_&START_YYYYMM._&END_YYYYMM in date_loop2 macro, because there is a ; after end statements
Turn on the MPRINT option to see what code your macro is generating. The second one is generating code like:
MPRINT(DATE_LOOP2): data master_rollrate;
MPRINT(DATE_LOOP2): set rollrate_201901_201901;
NOTE: Line generated by the macro variable "ED_YYYYMM".
1 rollrate_201901_201902
----------------------
180
MPRINT(DATE_LOOP2): rollrate_201901_201902;
NOTE: Line generated by the macro variable "ED_YYYYMM".
1 rollrate_201902_201902
----------------------
180
MPRINT(DATE_LOOP2): rollrate_201902_201902;
MPRINT(DATE_LOOP2): ;
MPRINT(DATE_LOOP2): run;
Remove the extra semi-colon in the middle of the nested %do loops since you are only generating PART of a statement in those loops.
You can simplify your macro code a great deal by using informats and formats to convert your YYYYMM digit strings to and from actual date values.
%macro date_loop2(start,end);
%local start_date end_date dif i j st_yyyymm ed_yyyymm;
%let start_date = %sysfunc(inputn(&start.01,yymmdd8));
%let end_date = %sysfunc(inputn(&end.01,yymmdd8));
%let dif=%sysfunc(intck(month,&start_date,&end_date));
data master_rollrate;
set
%do i=0 %to &dif;
%do j=&i %to &dif;
%let st_yyyymm = %sysfunc(intnx(month,&start_date,&i,b),yymmn6);
%let ed_yyyymm = %sysfunc(intnx(month,&start_date,&j,b),yymmn6);
rollrate_&st_yyyymm._&ed_yyyymm
%end;
%end;
;
run;
%mend date_loop2;

How to use loop with a macro in SAS

I have a question about how to use loop in % macro. I've written a sas macro that looks like that:
%macro SortDaysRolling(outdat,var);
proc sort data = &outdat. (keep=ACM_ACCT_NUM DIM_DATE_ID &var.); by ACM_ACCT_NUM DIM_DATE_ID; run;
%mend SortDaysRolling;
I need to apply this function to a number of files, i.e.:
%SortDaysRolling(days_rolling_1_1_1_4,count_times_days_1_4);
%SortDaysRolling(days_rolling_1_1_5_9,count_times_days_5_9);
%SortDaysRolling(days_rolling_1_1_10_14,count_times_days_10_14);
%SortDaysRolling(days_rolling_1_1_15_19,count_times_days_15_19);
%SortDaysRolling(days_rolling_1_1_20_24,count_times_days_20_24);
%SortDaysRolling(days_rolling_1_1_25_29,count_times_days_25_29);
%SortDaysRolling(days_rolling_1_1_30_44,count_times_days_30_44);
%SortDaysRolling(days_rolling_1_1_45_59,count_times_days_45_59);
%SortDaysRolling(days_rolling_1_1_60_89,count_times_days_60_89);
%SortDaysRolling(days_rolling_1_1_90,count_times_days_90);
and then
%SortDaysRolling(days_rolling_1_2_1_4,count_times_days_1_4);
%SortDaysRolling(days_rolling_1_2_5_9,count_times_days_5_9);
%SortDaysRolling(days_rolling_1_2_10_14,count_times_days_10_14);
%SortDaysRolling(days_rolling_1_2_15_19,count_times_days_15_19);
%SortDaysRolling(days_rolling_1_2_20_24,count_times_days_20_24);
%SortDaysRolling(days_rolling_1_2_25_29,count_times_days_25_29);
%SortDaysRolling(days_rolling_1_2_30_44,count_times_days_30_44);
%SortDaysRolling(days_rolling_1_2_45_59,count_times_days_45_59);
%SortDaysRolling(days_rolling_1_2_60_89,count_times_days_60_89);
%SortDaysRolling(days_rolling_1_2_90,count_times_days_90);
So the middle index changes. Since i = 1, ..., 35, I don't want to copy-paste all those lines 35 times. Is there any way to do the loop?
Thank you very much.
You need to determine your sequence abstraction. Then construct the sort macro invocations based on that.
%macro SortDaysRolling(out, var);
%put NOTE: &SYSMACRONAME: &=out &=var;
%mend;
%macro sort_loop (I_FROM=1, I_TO=&I_FROM, J_FROM=1, J_TO=&J_FROM);
%local I J;
%do I = &I_FROM %to &I_TO;
%do J = &J_FROM %to &J_TO;
%SortDaysRolling(days_rolling_&I._&J._1_4,count_times_days_1_4);
%SortDaysRolling(days_rolling_&I._&J._5_9,count_times_days_5_9);
%SortDaysRolling(days_rolling_&I._&J._10_14,count_times_days_10_14);
%SortDaysRolling(days_rolling_&I._&J._15_19,count_times_days_15_19);
%SortDaysRolling(days_rolling_&I._&J._20_24,count_times_days_20_24);
%SortDaysRolling(days_rolling_&I._&J._25_29,count_times_days_25_29);
%SortDaysRolling(days_rolling_&I._&J._30_44,count_times_days_30_44);
%SortDaysRolling(days_rolling_&I._&J._45_59,count_times_days_45_59);
%SortDaysRolling(days_rolling_&I._&J._60_89,count_times_days_60_89);
%SortDaysRolling(days_rolling_&I._&J._90,count_times_days_90);
%end;
%end;
%mend;
%sort_loop (J_TO=35);
The 10 count_times_days variables could also be abstracted, or constructed from generation rules.
Generating the numbers in-macro would shorten the code whilst making it a bit cryptic.
The below macro generates the table name and varname, and checks if the table exists before executing the %sortdaysrolling macro block.
%macro SortDaysRolling(outdat,var);
proc sort data = &outdat. (keep=ACM_ACCT_NUM DIM_DATE_ID &var.); by ACM_ACCT_NUM DIM_DATE_ID; run;
%mend SortDaysRolling;
options mprint mlogic;
%macro sortvarloop;
%do i = 1 %to 2;
%let j = 1;
%do %until (&j = 90);
%if &j = 1 %then %let k = 4;
%else %let k = %sysevalf(&j + 4)
%if &j = 90 %then %let tabsuff = %str( );
%else %let tabsuff = _&k;
%if %sysfunc(exist(days_rolling_1_&i._&j.&tabsuff.)) %then %do;
%sortdaysrolling(days_rolling_1_&i._&j.&tabsuff.,count_times_days_&j.&tabsuff.);
%end;
%let j = %sysevalf(&j + 5);
%end;
%end;
%mend sortvarloop; %sortvarloop;
But if you really want to use a macro block, you should think a little ahead and adopt some naming convention for easy and less-harsh-on-memory execution.

SAS code, loop index and more, how to simplify

I have SAS code below and then can run. The challenge is I have to repeat them so many times and it looks really awkward. I need Trans1-Tran2, TransOut1-TransOut100, and the BBB, AAA, VCS are user defined and no trend to follow.
%transform(Trans1,BBB,TransOut1);
%transform(Trans2,AAA,TransOut2);
%transform(Trans3,VCS,TransOut3);
%transform(Trans4,REM,TransOut4);
I am thinking combine do loop and Scan in my code but I do not know how to use index in the variable name so I cannot test my idea. Sample code that cannot run.
%let second= BBB, AAA, VCS,REM;
%macro trans;
%do i=1 %to 4;
%transform(Trans(i)?,%scan(&second.,&i),TransOut(i);
%end;
%mend;
I post some testing code below:
%macro test(data, var);
Data &data.;
Fname= "John";
Phone= 123;
City="Chicago";
Zip=65456;
keep &var.;
Run;
%mend;
%test(test1, Phone);
%test(test2, Fname);
%test(test3, City);
%test(test4, Zip);
You got it almost right already.
(Best leave out the comma in your list).
%let second= BBB AAA VCS REM;
%macro trans;
%do i=1 %to 4;
%transform(Trans&i,%scan(&second.,&i),TransOut&i);
%end;
%mend;
If you are dealing with a long list, you should consider using call execute:
data init;
input index userDef $;
datalines;
1 AAA
2 BBB
3 VCS
4 REM
;
run;
data _null_;
set init;
call execute(
cats(
'%transform(Trans', index, ',', userDef, ',TransOut', index,')'
)
);
run;
edit: using cats now, as suggested by Joe
In addition to Jetzler's answer, note that depending on the situation, it may be easier to include the loops in your macro:
%let trans = trans1 trans2 trans3 trans4;
%let trips = aaa bbb rem vcs;
%let outs = transout1 transout2 transout3 transout4;
%macro transform;
%do i = 1 %to %sysfunc(countw(&trans.));
%do j = 1 %to %sysfunc(countw(&trips.));
%do k = 1 %to %sysfunc(countw(&outs.));
/*your code here*/
%end;
%end;
%end;
%mend transform;
%transform;

looping over character values in SAS

In Stata you can loop over a list of character values with the foreach command. I've been trying to do the same in SAS to no avail so far. I'm trying to run a series of data and proc statements over all the values of character column. I tried with the following:
%let mylist = a b c; * These are all the values of a column called "code";
data mydata_#mylist; * Each element creates a new table;
set mydata;
where code=&mylist;
run;
What am I doing wrong or missing?
Thanks in advance,
Matías
Try this:
%macro loopit(mylist);
%let n = %sysfunc(countw(&mylist));
%do I=1 %to &n;
%let val = %scan(&mylist,&I);
data mydata_&val;
set mydata;
where code = "&val";
run;
%end;
%mend;
%let list=a b c;
%loopit(&list);
Modified version based on what DomPazz provided:
data mydata;
length code $8;
input code;
cards;
a
a
b
c
n
;
run;
%macro loopit(mylist);
%let else=;
%let n = %sysfunc(countw(&mylist));
data
%do I=1 %to &n;
%let val = %scan(&mylist,&I);
mydata_&val
%end;
other
;
set mydata;
%do j=1 %to &n;
%let val = %scan(&mylist,&j);
%if &j ne 1 %then %do;
%let else=else;
%end;
&else if code = "&val" then output mydata_&val;
%if &j = &n %then %do;
else output other;
%end;
%end;
run;
%mend;
options mprint;
%let list=a b c;
%loopit(&list);
Here I process input data only once (for efficiency if you need) creating all output tables in one data step.
Also I'm creating an "Other" table. To process only records with code from list is desired, you can add WHERE statement under SET statement and omit else output other;
%macro getName;
%let name = %sysfunc(translate(&val, __, -/));
%mend;
%macro loopit(mylist);
%let else=;
%let name=;
%let n = %sysfunc(countw(&mylist, %str( )));
data
%do I=1 %to &n;
%let val = %scan(&mylist,&I, %str( ));
%getName
mydata_&name
%end;
other
;
set mydata;
%do j=1 %to &n;
%let val = %scan(&mylist,&j, %str( ));
%getName
%if &j ne 1 %then %do;
%let else=else;
%end;
&else if code = "&val" then output mydata_&name;
%if &j = &n %then %do;
else output other;
%end;
%end;
run;
%mend;
options mprint;
%let list=a-a b/b c-;
%loopit(&list);
This version uses COUNTW and SCAN functions with modifiers to only use space (%str( )) as a word separator.
Also uses new macro getName to compose a name for datasets according to SAS naming rules (note %let name = ; to simply provide variable inside %loopit for %getName to fill).
%getName translates not allowed characters to underscore (a potential name conflict here if you have same values with different separator).

Resources