有時候要針對一組資料裡面不同類別的觀測值進行分析,在所有的 PROC 程序裡面都可以利用 BY 這個語法來完成。但是 BY 並沒有辦法在某些特殊情況下使用,因此利用 macro 進行遞迴式的處理就變成一另一種可行的方式。Katie Joseph 和 Taylor Lewis 在 NESUG 2007 發表了一篇利用 macro + do loop + %scan 來完成 BY 所無法完成的工作,可供有需要的人參考。
假設有一組資料如下所示:
每個 agency 有數筆重複觀測資料,Q1-Q73 為 73 個類別變數,weight 是連續變數表「權重」,而 strata 則是一個獨立的分層變數,同一個 agency 可能有兩種以上不同的分層變數。
假設我們要計算抽樣母體的平均值,則可使用下列程式:
proc surveymeans data=survey total=frametotals;
strata STRATA;
var Q1-Q73;
weight weight;
domain agency;
ods output domain=outstats;
run;
這個程式特殊的地方在於他有使用 domain statement 來執行 domain analysis。關於 domain analysis 可以參考下面這個網址:
http://support.sas.com/rnd/app/da/new/801ce/stat/chap13/sect9.htm#smeansdomainanalysis
Domain analysis 在變數分層多的時候會消耗大量電腦記憶體,而此例的 agency 總共有 87 層,一般電腦可能會出現「out of memory」的訊息而中斷程序。因此得轉個彎讓每個 agency 跑一次 proc surveymeans,然後一個一個去跑 domain analysis 就不會有記憶體不足的問題。假設只跑 agy1 這一層,則程式如下:
data split;
set survey;
length split $1;
if agency='agy1' then split='Y';
else split='N';
run;
proc surveymeans data=split total=frametotals;
strata STRATA;
var Q1-Q73;
weight weight;
domain split;
ods output domain=outstats_agy1 (where=(split='Y'));
run;
第一個 data step 設定一個新的變數 split,當 agency=agy1 時則 split = 'Y',反之則 split = 'N'。然後執行 proc surveymeans 時,把 split 放進 domain statement 裡面。由於 split 只是個二項變數,因此 domain 只要處理兩層就好。然後用一個 ods output 把 domain analysis 的表格輸出並存成 outstats_agy1。由於我們只關心 agy1 的結果,所以只要留下 split='Y' 的部分,至於 split='N' 的部分就不是我們所關心的了,這就是 ods output 後面要用一個 where 來抑制 split='N' 部分的輸出。
根據這個程式,我們可用一個 macro + do loop 讓他對每個 agency 都執行一次 proc surveymeans 程序。程式如下:
%macro agydoloop();
%do agynum=1 %to 87;
data split;
set survey;
length split $1;
if agency="agy&agynum" then split='Y';
else split='N';
run;
proc surveymeans data=split total=frametotals;
strata STRATA;
var Q1-Q73;
weight weight;
domain split;
ods output domain=outstats_agy&agynum (where=(split='Y'));
run;
%end;
%mend agydoloop;
這個程式首先先用 %macro agydoloop(); 和 %mend agydoloop; 把整個程式包起來。由於沒有額外的巨集參數需要使用,所以 %macro agydoloop(); 裡面就流空白即可。然後裡面再用一個 %do agynum=1 %to 87; 和 %end; 把那個 data step 以及 proc surveymeans 程序給包起來。特別注意是 do, to 和 end 若使用在 macro 裡面時,前面需加上百分比符號「%」。之後再把 1 改成 &agynum 即可,這樣讓 do loop 在跑時能把 1 到 87 遞迴地代入 agynum 裡面。如此一來 87 次 proc surveymeans 就可以輕鬆完成了。
如果不想一次把 Q1 到 Q73 跑完,而也想用 do loop 分批跑的話,只要再加上一層 do loop 即可。程式如下:
%macro agyandQdoloop();
%do agynum=1 %to 87;
data split;
set survey;
length split $1;
if agency="agy&agynum" then split='Y';
else split='N';
run;
%do qnum=1 %to 73;
proc surveymeans data=split total=frametotals;
strata STRATA;
var Q&qnum;
weight weight;
domain split;
ods output domain=outstats_agy&agynum._Q&qnum (where=(split='Y'));
run;
%end; /* qnum do loop */
%end; /* agynum do loop */
%mend agyandQdoloop;
由於 Qn 變數只出現在 proc surveymeans,所以新的 do loop 只需要放在 proc surveymeans 的頭尾即可。然後用 &qnum 來替換數字 1 到 73。特別一提的是,此處的 ods output 在設定輸出資料名稱時,原先的 &agynum 由於後面接了一個特殊符號「_」,因此 &agynum 後面要先打上一個句點「.」再接「_」。如果沒有打上句點,則這個 macro 會無法分辨出 &agynum 是個參數而非定值。這個規則通用於各種情況,只要巨集變數後面接上符號如「_」,「.」或「\」,都需要打上一個句點先。
可是,當類別變數全都是字串時,do loop 就無法處理了。因此需要利用 %scan 來輔助完成。%scan 函式是用來切割一長串的字串。其語法為:
%scan(參數, n [,分隔符號]);
其中,n 代表要抽出切割後的第幾個字串,而[,分隔符號]是一個選擇性的參數。在沒有額外設定的情況下,預設值為「空白」,「.」,「<」,「(」,「+」,「|」,「&」,「$」,「*」,「)」,「;」,「﹁」,「-」,「/」,「,」,「%」,「¢」。 假設 agency 變數不再是 agy1-agy87,而是 ED, BO, CM 這三個字串,則程式如下:
%macro agyscanloop(agylist=);
%let num=1;
%let agy=%scan(&agylist,&num);
%do %while (&agy ne );
data split;
set survey;
length split $1;
if agency="&agy" then split='Y';
else split='N';
run;
proc surveymeans data=split total=frametotals;
strata STRATA;
var Q1-Q73;
weight weight;
domain split;
ods output domain=outstats_&agy(where=(split='Y'));
run;
%let num=%eval(&num+1); *** increase by one the position pointer;
%let agy=%scan(&agylist,&num);
%end;
%mend agyscanloop;
其中,agy=%scan(&agylist,&num)是要把巨集參數 agylist 裡面所打的字串切割並取第 &num 個部分。由於一開始 &num 設定為 1,所以每跑完一次之後要把 &num 加一,這樣跑第二次時就會去取切割後的第二個字串。而遞迴的過程是由 %do %while (&agy ne); 來驅動。這個道理很簡單,只要當 &agy=%scan(&agylist, &num) 的結果不是空白(即 missing data),則這個 do while 就會一直跑,直到 &agy 是 missing data 為止。要怎樣讓這個 do while 結束,就是靠之後得 %let num=%eval(&num+1); 和 %let agy=%scan(&agylist, &num); 來控制。當跑完第一次時,num=%eval(&num+1) 會讓 &num = 2。之所以要用 %eval 的原因是因為任何四則運算在 macro 裡面都會被視為字串。只有包在 %eval 函式裡面的四則運算過程才會真正被計算出來並輸出結果。所以如果沒有用 %eval 函式的話,num 會變成字串型的「1+1」,而非數值型的「2」。當 num=2 後代入下面那個 %let agy=%scan(&agylist, &num); 時,這行就會把 &agylist 所代表的字串再次切割,然後取出第二個部分。所以這個 do while 就會繼續去執行,直到 num=3 時才會跳出。
因此當執行下面這行時:
%agyscanloop(agylist=ED BO CM);
此巨集會把「ED BO CM」從中間的空白處切成「ED」,「BO」以及「CM」,然後再一個一個去執行 proc surveymeans。
雖然 %scan 可以幫忙切割字串,但是如果 agency 這個變數有數十個字串型的類別,而非上例的只有三個類別,則在輸入巨集參數 &agylist 時便會顯得相當相當耗時。作者提出了一個 proc sql 程序來解決這項煩人的工作。程式改寫如下:
proc sql;
select distinct agy into :agylist separated by ' '
from survey;
%put &agylist;
quit;
%macro agySQLscanloop();
%let num=1;
%let agy=%scan(&agylist,&num);
%do %while (&agy ne );
*** PROC SURVEYMEANS Code - uses &agy as the agency code;
%let num=%eval(&num+1);
%let agy=%scan((&agylist,&num);
%end;
%mend agySQLscanloop;
這個 proc sql 會把 agy 所有的類別黏成一串,並用空白符號分隔開來,然後把黏成一串的結果用 %put 函式放進一個巨集變數 &agylist 裡面。由於這個 &agylist 變數已經在外部設定好了,所以之後寫 macro 時就不用在 %macro agySQLscanloop(); 裡面放 agylist 了。至於 macro 裡面的內容和設定則完全跟前例一樣。
CONTACT INFORMATION
Your comments and questions are valued and encouraged. Contact the authors at:
Katie Joseph
US Office of Personnel Management
1900 E St, NW, Room 7439
Washington, DC 20415
Work Phone: (202) 606-1817
Fax: (202) 606-1719
E-mail: Katie.Joseph@opm.gov
Taylor Lewis
U.S. Office of Personnel Management (OPM)
1900 E St., NW, Room 7439
Washington, DC 20415
Work Phone: (202) 606-1309
Fax: (202) 606-1719
E-mail: Taylor.Lewis@opm.gov
沒有留言:
張貼留言
要問問題的人請在文章下方的intensedebate欄位留言,請勿使用blogger預設的意見表單。今後用blogger意見表單留言的人我就不回應了。