公告

[公告]
2014/01/17
由於已經是faculty的關係,不太有足夠時間寫部落格。因此更新的速度會相當緩慢。再加上近幾年來SAS GLOBAL FORUM沒有出現讓我覺得驚艷的技術文件,所以能分享的文章相對也減少許多。若有人推薦值得分享的SAS技術文件,請利用『問題討論區』告知。

2013/07/19
臉書留言板的功能因為有不明原因故障,因此特此移除。而intensedebate的留言板因管理不易,也一併移除。目前已經開啟內建的 G+ 留言系統,所以請有需要留言的朋友,可直接至『問題討論區』裡面留言。


2008年9月11日 星期四

Names, Names, Names - Make Me a List

原文載點:http://www2.sas.com/proceedings/forum2007/052-2007.pdf

如果你曾經讀過去年的這一篇文章「Text Utility Macros for Manipulating Lists of Variable Names」,而且也親自使用過該原文作者所提供的 text utility macro 的話,想必一定會對其強大的批次 rename 功能印象深刻。不過這個 macro 有個缺點,那就是在使用前要先定義這個東西:
%let orig_vars = a01a a01b d01 d02 t1_r t2_r;

這個 %let 的作用是把所有原始變數集中定義成 orig_vars 這個變數,然後再讓 text utility 來使用。不過,如果當變數很多時,我們需要手動去輸入,仍舊是相當耗時。而之前我曾經發過這篇文章「Automatically Renaming Common Variables Before Merging」,裡面是利用 PROC SQL 來抓出舊變數名稱。不過,如果舊變數名稱是有規則可尋,比方說 y1~y1000 時( %let 不允許使用 y1-y1000 這種寫法),則這篇在 SAS Global Forum 2007 由 Ian Whitlock 發表的技術文件可以有很大的幫助。

這篇技術文件一共發佈了五個有用的 macro 讓使用者可以輕易操作大量變數名稱。以下就一一介紹:

。%ZIP
這個 macro 顧名思義就和壓縮檔 zip 一樣,可以組合數個不同的變數或 index 來產生有規則的變數。語法如下:

%zip (l1= , lv1= , sep1=, l2= , lv2= , sep2= , osep= )

變數解釋如下:
。l1= 第一列字串
。lv1= 取代第一列字串的字串(可省略不用)
。sep1= 用來區隔第一列字串的字元,預設值是空白(%str())
。l2= 第二列字串
。lv2= 取代第二列字串的字串(可省略不用)
。sep2= 用來區隔第二列字串的字元,預設值是空白(%str())
。osep= 用來區隔輸出字串的字元,預設值是空白(%str())

比方說想要生成「Ax By Cz」這三個字串,可使用下列程式:
%zip(l1=A B C, l2=x y z);

l1 所表示的「A B C」會去抓在 l2 相對應位置的「x y z」,這便是 %zip 所呈現的功能。

。XPROD
這是可以拿來產生連續字尾或字首的 macro。語法如下:
%xprod ( l1= , lv1= , l2= , lv2=, sep1=, sep2=, osep=) )

使用方式和 %zip 一模一樣,但產生出來的效果完全不同。比方說想要產生「lib.w1 lib.w2 lib.w3」這三個字串,可發現這三個字串只有尾數不同,所以可以用 %xprod 來產生:
%xprod(l1=lib.w, l2=1 2 3);

換句話說,在 %xprod 裡面,l1 是拿來放共同字串,l2 是拿來放會改變的字串。他甚至可以做出更複雜的效果。假設想要產生「a_x a_y b_x b_y c_x c_y」這六個字串,我們可以想像這是一個 3X2 的矩陣:
      x   y
a_
b_
c_

而實際上 %xprod 就是用這種原理去做字串合併,因此我們只需要輸入:
%xprod(l1=a_ b_ c_, l2=x y);

就可自動產生那六個字串出來。

。RANGE
這是拿來生成連續數字的程式。語法如下:
%range ( to=, from= , step= , sep= )

變數解釋:
。to= 連續數字最後一位數,預設值是 1
。from= 連續數字的第一位數,預設值是 1
。step= 跳號的間格數,預設值是 1
。sep= 分隔連續數字的字元,預設值是空格(%str( ))

顧名思義,如果要生出「1 2 3 4 5」這串連續數字,則可使用:
%range(to=5);

。REPLACE
這是拿來取代字元的 macro,特別是使用在重新命名的動作裡面。語法如下:
%replace ( l= , lv= , code= , key= , lsep=, osep=)

變數解釋:
。l= 輸入字串
。lv= 取代上列字串的字串(可省略不用)
。code= 包含 key 所代表的符號變數的字串,通常是在程式中不會被變動的字串
。key= 用來當作取代「l」代表的字串的符號變數,預設值是 #
。lsep= 用來區隔輸入字串的字元,預設值是空白(%str())
。osep= 用來區隔輸出字串的字元,預設值是空白(%str())

舉例來說,如果在套入一組資料時要順便改變某些變數名稱,則程式碼為:
data ww;
 set w(rename=(x=__x y=__y z=__z);
run;

則可使用 %replace 來簡化:
data ww;
 set w(rename=(%replace(l=x y z, code=#=__#));
run;

這個程式會先把 l 所定義的 x y z 用依序一個 # 來取代,而 code 的作用就是把 x y z 一一套入「#=__#」的框架裡面,進而產生「x=__x y=__y z=__z」這三個字串出來。

另外還有更進階的寫法,就是把 %replace 放進另一個自創的 %rename 裡面,程式如下:
%macro rename ( list, pref=__) ;
%replace ( l=&list, code = # = &pref# )
%mend rename ;


%rename 的第一個參數就等於 %replace 的第一個參數,而 pref 就等於輸出字串裡面共有的字串(此例預設值是「__」)。因此原來的程式可以更簡化為:
data ww;
 set w(rename=(%rename(x y z)));
run;

這時「x y z」就等於「list」,也等於「l」,而 pref 所表示的「__」被丟進 %replace 的 code 參數裡面,則 code 就變成「code= #=__#」,這就便回原來只用 %replace 的那個程式碼了。

%replace 也可以使用在變數轉換上面。比方說上面這個例子,在 rename 後要把新變數轉換成 best 32. 的格式。如果不用額外的 macro 則語法是:
data ww;
  set w(rename=(%rename(x y z)));
 x=input(__x, best32.);
 y=input(__y, best32.);
 z=input(__z, best32.);
run;

同樣地,我們可以另外自製一個命為 %CHAR2NUM 的 macro,然後套用 %replace 去寫。方法如下:
%macro char2num ( list , pref = __ ) ;
%replace ( l=&list
, code= %str(# = input(&pref#,best32.);)
)
%mend char2num ;

這個新 macro 的參數用法和 %rename 一樣,但我們來看看 %replace 在裡面發會什麼功用。l 去呼叫 list 所定義的字串,而 code 生出「#=input(&pref#,best32.);」,其中 # 就是暫時替代 l/list 參數所定義的字串,pref 是利用預設值「__」,則 code 其實就是產生下面這三行:
x=input(__x, best32.);
y=input(__y, best32.);
z=input(__z, best32.);

因此原本的程式可以改寫成:
data ww;
   set w(rename=(%rename(x y z)));
 %char2num(x y z);
run;

。QT
這個 macro 只是很簡單地幫每個字元加上雙引號。語法如下:
%qt ( l=, lv= , lsep= %str( ), qt = %str(%"), osep=%str( ) )

變數解釋:
。l= 輸入字串
。lv= 取代上列字串的字串(可省略不用)
。lsep= 用來區隔輸入字串的字元,預設值是空白(%str())
。qt= 幫每個字元左右加上的符號,預設值是雙引號(")
。osep= 用來區隔輸出字串的字元,預設值是空白(%str())

因此,如果要生成出「"a" "b" "c"」只需要簡單地使用:
%qt(l=a b c);

以下是各 macro 的原始碼:
。ZIP
%macro zip
( l1= /* first list */
, lv1= /* external variable override for first list */
, sep1=%str( ) /* separator between the joined elements */
, l2= /* second list */
, lv2= /* external variable override for second list */
, sep2=%str( ) /* separator between the joined elements */
, osep=%str( ) /* separator between new elements */
) ;
/* %zip ( l1= a b , l2= c d ) produces ac bd
so does
%let list1 = a b ;
%let list2 = c d ;
%zip (lv1=list1, lv2=list2)
If lists do not have same length shorter length used and
warning to the log. Empty lists result in empty list and
no message.
If the LV options are used then L1, L2, and ZIP_: should be
avoided for variable names.
*/
%local zip_i zip_1 zip_2 zip_list ;
%if %length(&lv1) = 0 %then
%let lv1 = l1 ;
%if %length(&lv2) = 0 %then
%let lv2 = l2 ;
%do zip_i = 1 %to &sysmaxlong ;
%let zip_1 = %qscan(%superq(&lv1) , &zip_i, &sep1 ) ;
%let zip_2 = %qscan(%superq(&lv2) , &zip_i, &sep2 ) ;
%if %length(&zip_1) = 0 or %length(&zip_2) = 0 %then
%goto check ;
%if &zip_i = 1 %then
%let zip_list = &zip_1&zip_2 ;
%else
%let zip_list = &zip_list&osep&zip_1&zip_2 ;
%end ;
%check:
%if %length(&zip_1) > 0 or %length(&zip_2) > 0 %then
%put WARNING: Macro ZIP - list lengths do not match - shorter used. ;
%unquote(&zip_list)
%mend zip ;


。XPROD
%macro xprod
( l1= /* first list */
, lv1= /* external variable override for first list */
, sep1=%str( ) /* separator between elements of first list */
, l2= /* second list */
, lv2= /* external variable override for second list */
, sep2=%str( ) /* separator between elements of second list */
, osep=%str( ) /* separator between elements of new list */
) ;
%local xp_i xp_j xp_1 xp_2 xp_list ;
%if %length(&lv1) = 0 %then
%let lv1 = l1 ;
%if %length(&lv2) = 0 %then
%let lv2 = l2 ;
%do xp_i = 1 %to &sysmaxlong ;
%let xp_1 = %qscan(%superq(&lv1), &xp_i, &sep1) ;
%if %length(&xp_1) = 0 %then %goto endloop1 ;
%do xp_j = 1 %to &sysmaxlong ;
%let xp_2 = %qscan(%superq(&lv2), &xp_j, &sep2) ;
%if %length(&xp_2) = 0 %then %goto endloop2 ;
%if &xp_i = 1 and &xp_j = 1 %then
%let xp_list = &xp_1&xp_2 ;
%else
%let xp_list = &xp_list&osep&xp_1&xp_2 ;
%end ;
%endloop2:
%end ;
%endloop1:
%unquote(&xp_list)
%mend xprod ;


%RANGE
%macro range /* second more efficient version due to Chang Chung */
( to=1 /* end integer value */
, from=1 /* starting integer value */
, step=1 /* increment integer */
, osep=%str( ) /* sparator between integers */
) ;
/*
return sequence of integers starting at &FROM going to &TO
in steps of &step
%range(to=5) produces 1 2 3 4 5
*/
%local rg_i ;
%do rg_i = &from %to &to %by &step ;
%if &rg_i = &from %then
%do;&rg_i%end ;
%else
%do;&osep&rg_i%end ;
%end ;
%mend range ;


。REPLACE
%macro replace
( l= /* value list */
, lv= /* external variable override for value list */
, lsep=%str( ) /* separator between values */
, code= /* block of code containing symbolic variable */
, key=# /* symbolic variable to replace (#abc# etc.) */
, osep=%str( ) /* separator between new elements */
/* may be %str(;) when code is statement */
/* if so remember to add closing semicolon */
) ;
%local rg_i rg_w rg_list ;
%if %length(&lv) = 0 %then
%let lv = l ;
%if %length(%superq(&lv)) = 0 /*or %index(%superq(code),&key) = 0*/ %then
%do ;
%let rg_list = %superq(code) ;
%goto mexit ;
%end ;
%do rg_i = 1 %to &sysmaxlong ;
%let rg_w = %qscan(%superq(&lv),&rg_i,&lsep) ;
%if %length(&rg_w) = 0 %then %goto mexit ;
%if &rg_i = 1 %then
%let rg_list = %sysfunc(tranwrd(%superq(code),&key,&rg_w)) ;
%else
%let rg_list =
&rg_list&osep%sysfunc(tranwrd(%superq(code),&key,&rg_w)) ;
%end ;
%mexit:
%unquote(&rg_list)
%mend replace ;


Contact information
Ian Whitlock
29 Lonsdale Lane
Kennett Square, PA 19348
Ian.Whitlock@comcast.net
CODE { display: block; /* fixes a strange ie margin bug */ font-family: Courier New; font-size: 8pt; overflow:auto; background: #f0f0f0 url(http://klcintw.images.googlepages.com/Code_BG.gif) left top repeat-y; border: 1px solid #ccc; padding: 10px 10px 10px 21px; max-height:200px; height:200px; // for IE6 line-height: 1.2em; }