公告

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

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


2007年4月8日 星期日

Presentation Quality Graphs With SAS/GRAPH

原文載點:http://www.nesug.info/Proceedings/nesug06/hw/hw01.pdf

SAS 比較讓人詬病的一點是繪圖程式相當困難,比較複雜一點的圖形可能用現成的 SAS/GRAPH 程序做不出來。但其實應該是沒有 SAS 做不出來的圖,端看使用者有沒有辦法熟用程式的運用。很多基本的製圖設定,看手冊是可以學的起來。但比較關係到細部設定的程式,若沒有人指點,翻遍手冊也找不到解決的方法。Michael G. Eberhart 在 2006 年的 NESUG 發表了一篇篇幅不短的教學文章,可以視為讓進階使用者能夠快速對 SAS/GRAPH 進階用法入門的一個良好的教學。

COMBINING PLOT LINE AND BAR CHART

有時候我們習慣在一個柱狀的趨勢圖上再加上折線圖,如下圖所示:



折線圖主要適用 PROC GPLOT,而柱狀圖主要是用 PROC GCHART,要如何將兩種不同程序所做出的圖形重疊在一起,就是一門學問了。以下利用一個小範例來說明該如何繪製這類的圖形。首先,使用的資料如下所示:



data disacases;
input rate numcase year ;
datalines;
1.3 21 1995
16.9 269 1996
11.0 175 1997
8.4 133 1998
3.9 62 1999
16.0 255 2000
6.4 98 2001
4.6 70 2002
11.7 179 2003
2.6 39 2004
;




首先,先用 PROC GPLOT 把折線圖畫出來,程式如下:


goptions reset=all device=png htext=8pt;
axis1 order=(0 to 20 by 5) value=none origin=(20,20) pct length=57 pct
label=none major=none minor=none;
axis2 order=(1995 to 2004 by 1) value=none origin=(,22.5) pct length=70 pct
label=none major=none minor=none offset=(4,4);
axis3 order=(0 to 20 by 5) origin=(90,20) pct length=57 pct value=(f=swissb)
label=(a=270 h=11pt f=swissb "Rate per 100,000 Persons");
title f=swissb h=14pt move=(17,23) "Figure 1. Disease A, Philadelphia 1995-2004";
title2 f=swissb h=14pt move=(33,21) "Cases and Rates";
symbol1 color=blue interpol=join /*sets color and joins markers in a line*/
line=1 /*sets type of line e.g. solid, dashed, etc*/
width=4 value=none; /*sets width and assigns unique symbol*/
symbol2 color=blue interpol=join line=1 width=4 value=none;
legend1 across=2
down=1
cborder=black
position=(bottom right outside)
mode=protect
label=none
offset=(-2cm,)
value=(h=8pt f=swissb justify=right 'Phila. Rate');
proc gplot gout=work.gseg data=disacases;
plot rate*year / overlay vaxis=axis1 haxis=axis2 legend=legend1;
plot2 rate*year / overlay vaxis=axis3 legend=legend1;
run;
quit;


座標軸(axis)的語法和說明:
  1. Order:可以指定標示在座標軸上數據的起點、終點和間距。如:order=(0 to 20 by 5) 就表示座標軸從 0 開始,每五格標示一次(5、10、15),直到終點 20。
  2. Major:控制大間距刻度是否出現,本例設定是 none,就代表隱藏大間距刻度。
  3. Minor:控制小間距刻度是否出現,本例設定是 none,就表示隱藏小間距刻度。
  4. Value:修改間距標示所顯示的值,本例設定是 none,就表示不做調整。
  5. Origin:控制座標軸在圖上開始繪製的起點。本例的縱軸(axis1)設定是(20, 20),表示該軸是從圖形展示介面從下方和從左方算起的第 20% 的位子開始描繪,詳情請見下圖。。橫軸(axis2)設定是(,22.5)表示在橫軸下方挪出 22.5(單位不明)的空間。
  6. Length:決定座標軸的長度。
  7. Label:決定座標軸的標籤。其中,a 可以決定文字角度,h 可以決定文字大小,f 可以決定文字字形。
  8. Offset:決定折線起點距離座標軸原點的位置。


圖示說明(legend)的語法和說明:
  1. across:定義圖示說明內的欄位數量。
  2. down:定義圖示說明內的列位數量。
  3. position:決定圖示說明的位置。
  4. mode:宣告被圖示說明佔據的空間不得被其他元件使用。
  5. offset:定義圖示說明的細部位置(類似 axis 的 offset)。
  6. value:設定圖示說明的字形和字體大小。
點(symbol)的語法和說明:(原文沒有,我自己補充的)
  1. color:點的顏色。
  2. interpol:點的連結狀態(none 表示不連結,j 表示連結)。
  3. line:連結線的樣式(請參考手冊,大概有二三十種)。
  4. width:線的寬度。
  5. value:點的樣式(請參考手冊,大概也有十來種)。
  6. height:點的大小。
最後的 PROC GPLOT 讓一口氣繪製兩張折線圖,並用 overlay 的語法讓兩張圖疊在一起。這個目的是,先讓第一張圖把座標軸、折線、標題、圖示說明全部畫出來,但 axis1 和 axis2 並沒有設定刻度,然後在第二張圖上畫出同樣的圖形,但卻在圖形右側標上 Y 軸(axis3)的標籤和刻度。這樣可以讓接下來要做出的柱狀圖中的左側放上另一個 Y 軸的標籤和刻度。完成後如下圖所示:



折線圖畫好後,就可以開始繪製柱狀圖。在此之前,有些額外的元件需要用 annotate 來設定。程式如下所示:

data anno;
length function color style $8 position $1 text $20;
retain xsys '5' ysys '5' when 'a';
function='move'; x=46.8; y=7; output;
function='bar'; x=54; y=6; color='blue'; style='solid'; output;
function='move'; x=69; y=8;output;
function='bar'; x=74; y=5;color='red'; style='solid'; output;
function='move'; x=80; y=3;output;
function='label'; style='swissb'; text='Phila. Cases'; color='black'; size=.8;
position='2';output;
function='move'; x=55; y=9;output;
function='label'; style='swissb'; text='Year of Report'; color='black'; size=1;
position='2';output;
run;


語法說明如下:
  1. XSYS 和 YSYS:這是設定座標軸系統的變數名稱,有不同的代碼,詳情需查詢 SAS 手冊才能瞭解各代號的意義。
  2. when:表示 annotate 繪製的時間,a 表示先畫圖再畫 annotate,b 表示先畫 annotate 再畫圖。
  3. position:表示文字字串的位置。
  4. style:在 function='bar' 下表示指定 bar 的形式,在 function='label' 下表示指定 label 的形式。
  5. function:表示想要進行的動作。有 move、bar、label、pie 等功能可用。
  6. color 和 size:指定圖形或文字的顏色和大小。
接著就可以把上面新建的 annotate 資料代入 PROC CHART 裡面:

goptions reset=all device=png htext=8pt;
axis1 order=(0 to 300 by 50) value=(f=swissb)origin=(20,20)pct length=57 pct
label=(a=90 h=11pt f=swissb "Number of Reported Cases ");
axis2 order=(1995 to 2004 by 1) value=(f=swissb) origin=(,22.5)pct length=70 pct
label=none;
proc gchart data=disacases gout=work.gseg;
vbar year /sumvar=numcase
raxis=axis1
space=1
maxis=axis2
discrete
annotate=anno;/*identifies the annotate dataset*/
run;
quit;



PROC GCHART 的參數說明如下:
  1. raxis:指定 Y 軸。
  2. space:指定 bar 與 bar 之間的距離。
  3. maxis:指定 X 軸。
  4. discrete:定義 X 軸的變數是離散變數。
  5. annotate:呼叫之前寫的 annotate 資料集。
完成後的柱狀圖如下所示:


最後,用 PROC GREPLAY 把兩張圖何在一起,程式如下:

filename figures "d:\nesug\nesug_disease_a.png"; /*output destination*/
goptions reset=all device=png gsfname=figures htext=8pt
xpixels=3600 ypixels=2400 lfactor=6
/*settings for xpixels and ypixels generates image at 600dpi*/
/*lfactor (line factor) is increased by a factor of 6*/
xmax=6.4in ymax=3.4in hsize=6.4in vsize=3.4in;
proc greplay igout=work.gseg nofs tc=sashelp.templt template=whole;
treplay 1:gchart
1:gplot;
run;
quit;


GREPLAY 的語法說明:
  1. igout:指定圖形輸出設定所需的 SAS 資料夾。
  2. nofs:抑制 SAS 預設的設定。
  3. tc=sashelp.templt:SAS 有提供許多模版,全部放在 sashelp.templt 這個資料夾裡面,必須要用 tc 指令指定才能使用。
  4. template=whole:指定輸出模版,whole 表示全部的圖都放在同一個模版下。
  5. treplay statement:指定哪些圖形使用哪些模版。如果每個輸出圖形所指定的模版都一樣,就表示把圖形疊在一起顯示。如範例所示。
這樣就大功告成了!!順帶注意一點是,上述許多環境設定參數會一直遺留到後續的製圖上。如果不加以還原,這張圖的設定就會沿用到下一張圖。簡單的解決方法是重新啟動 SAS,這樣所有的參數語法設定會還原。不然也可以跑下面這個程式來還原:

proc greplay igout=work.gseg nofs; delete _all_;run;

DOUBLE REVERSE HBAR GRAPH

這種圖形有點像金字塔圖。大概長的像這個樣子:



這次用的資料如下:

data disbrate;
input frate mrate agegrp $;
datalines;
16.6 12.0 0-4
3.6 1.8 5-9
798.2 90.9 10-14
9282.6 2960.7 15-19
5332.4 2799.3 20-24
2439.0 1412.7 25-29
1029.1 706.0 30-34
506.4 475.5 35-39
241.1 305.8 40-44
101.6 176.0 45-54
25.8 52.3 55-64
4.5 6.3 65+
1440.0 712.5 All
;
run;


proc format;
value $agrp '0-4'=' 0 to 4 '
'5-9'=' 5 to 9 '
'10-14'='10 to 14'
'15-19'='15 to 19'
'20-24'='20 to 24'
'25-29'='25 to 29'
'30-34'='30 to 34'
'35-39'='35 to 39'
'40-44'='40 to 44'
'45-54'='45 to 54'
'55-64'='55 to 64'
'65+'=' 65+ '
'All'='All Ages';
run;


這個資料並沒有辦法直接套用到繪圖上,而是要先做一點整理。這邊我們利用 PROC TRANSPOSE 的語法將資料作一點轉置的動作。

/*Transpose dataset to create individual variables for each age group*/
proc transpose data=disbrate out=disbanno;
id agegrp; var frate mrate;
format agegrp $agrp.;
run;
/*Make male rates negative and apply format*/
data disbrate; set disbrate;
mrate=-mrate;
format agegrp $agrp.;
run;


接著,仍舊是要設定 annotate 資料集,而且一次要做兩個(一邊用一個):

/*First annotate dataset for female rates graph*/
data anno1;
length function style color $8 position $1 text $20;
retain xsys '1' ysys '1' when 'a' style 'swissb' position '8' size .8;
set work.disbanno;
where _name_='frate';
function='move'; x=2; y=102;output;
function='label'; text=(put(_0_to_4,comma8.1)); color='black';output;
function='move'; x=0; y=95; output;
function='label'; text=(put(_5_to_9,comma8.1)); color='black';output;
function='move'; x=11; y=88; output;
function='label'; text=(put(_10_to_14,comma8.1)); color='black';output;
function='move'; x=84; y=80; output;
function='label'; text=(put(_15_to_19,comma8.1)); color='white';output;
function='move'; x=61; y=72; output;
function='label'; text=(put(_20_to_24,comma8.1)); color='black';output;
function='move'; x=33; y=65; output;
function='label'; text=(put(_25_to_29,comma8.1)); color='black';output;
function='move'; x=17; y=58; output;
function='label'; text=(put(_30_to_34,comma8.1)); color='black';output;
function='move'; x=9; y=50; output;
function='label'; text=(put(_35_to_39,comma8.1)); color='black';output;
function='move'; x=7; y=43; output;
function='label'; text=(put(_40_to_44,comma8.1)); color='black';output;
function='move'; x=4; y=36; output;
function='label'; text=(put(_45_to_54,comma8.1)); color='black';output;
function='move'; x=2; y=28; output;
function='label'; text=(put(_55_to_64,comma8.1)); color='black';output;
function='move'; x=0; y=21; output;
function='label'; text=(put(_65P,comma8.1)); color='black';output;
function='move'; x=22; y=13; output;
function='label'; text=(put(All_Ages,comma8.1)); color='black';output;
run;

/*Second annotate dataset for male rates graph*/
data anno2;
length function style color $8 position $1 text $20;
retain xsys '1' ysys '1' when 'a' style 'swissb' position '8' size .8 color
'black';
set work.disbanno;
where _name_='mrate';
function='move'; x=90; y=102;output;
function='label';text=(put(_0_to_4,comma8.1));output;
function='move'; x=90; y=95; output;
function='label';text=(put(_5_to_9,comma8.1));output;
function='move'; x=89; y=88; output;
function='label';text=(put(_10_to_14,comma8.1));output;
function='move'; x=61; y=80; output;
function='label';text=(put(_15_to_19,comma8.1));output;
function='move'; x=63; y=72; output;
function='label';text=(put(_20_to_24,comma8.1));output;
function='move'; x=77; y=65; output;
function='label';text=(put(_25_to_29,comma8.1));output;
function='move'; x=83; y=58; output;
function='label';text=(put(_30_to_34,comma8.1));output;
function='move'; x=85; y=50; output;
function='label';text=(put(_35_to_39,comma8.1));output;
function='move'; x=87; y=43; output;
function='label';text=(put(_40_to_44,comma8.1));output;
function='move'; x=88; y=36; output;
function='label';text=(put(_45_to_54,comma8.1));output;
function='move'; x=89; y=28; output;
function='label';text=(put(_55_to_64,comma8.1));output;
function='move'; x=90; y=21; output;
function='label';text=(put(_65P,comma8.1));output;
function='move'; x=83; y=13; output;
function='label';text=(put(All_Ages,comma8.1));output;
function='move'; x=1; y=0;output;
function='bar'; x=0; y=100;color='white';output;
run;


然後設定一個 format 讓負值顯示成正值:

/*Create picture format to display negative values as positive*/
proc format;
picture positive low-high='000,000';
run;


然後用 PROC GCHART 來畫第一張圖,並把那兩個 annotate 中的 anno1 套用上先:

goptions reset=all device=png htext=8pt ftext=swissb;
axis1 order=(0 to 10000 by 2000) value=(f=swissb)origin=(55,17.3)pct length=45pct
label=(c=cxFF00FF f=swissb h=10pt 'Females' h=8pt c=black ' (Rate per 100,000)') mino=none;
axis2 order=(' 0 to 4 ' ' 5 to 9 ' '10 to 14' '15 to 19' '20 to 24' '25 to 29' '30 to 34' '35
to 39' '40 to 44' '45 to 54' '55 to 64' ' 65+ ' 'All Ages') value=(j=c f=swissb)
origin=(55,17.3)pct label=none minor=none length=65pct;
title h=10pt 'Figure XX. Rates of Disease B per 100,000 Population by Age and Gender:';
title2 h=10pt 'Philadelphia, 2004';
pattern1 value=solid color=cxFF00FF; /*color set with RGB values*/
proc gchart data=disbrate gout=work.gseg;
hbar agegrp /sumvar=frate
raxis=axis1 /*sets vertical axis*/
space=.25 /*controls space between bars*/
maxis=axis2 /*sets horizontal axis*/
discrete
nostats /*suppress stats*/
noframe /*suppress frame around graph*/
width=1 /*controls width of bars*/
annotate=anno1; /*identifies annotate dataset*/
format _numeric_ comma6.; /*sets format for all numeric variables*/
run;
quit;


再寫一個額外的資料集把一些顏色的設定給補好:

data hex;
length red green blue 3.;
red=255;
green=0;
blue=255;
color=COMPRESS('CX'||put(red,hex2.)||put(green,hex2.)||put(blue,hex2.));
run;


最後把 anno2 也套進去畫第二張:

goptions reset=all device=png htext=8pt ftext=swissb;
axis1 order=(-10000 to 0 by 2000) value=(f=swissb)origin=(1.4,17.3)pct length=44pct
minor=none label=(c=cx0000FF f=swissb h=10pt 'Males' h=8pt c=black ' (Rate per 100,000)');
axis2 order=(' 0 to 4 ' ' 5 to 9 ' '10 to 14' '15 to 19' '20 to 24' '25 to 29' '30 to 34' '35
to 39' '40 to 44' '45 to 54' '55 to 64' ' 65+ ' 'All Ages') value=none origin=(1.4,17.3)pct
label=none minor=none length=65pct;
title;
pattern1 value=solid color=cx0000FF; /*color set with RGB values*/
proc gchart data=disbrate gout=work.gseg;
hbar agegrp /sumvar=mrate
raxis=axis1 /*sets vertical axis*/
space=.25 /*controls space between bars*/
maxis=axis2 /*sets horizontal axis*/
discrete
nostats /*suppress stats*/
noframe /*suppress frame around graph*/
width=1 /*controls width of bars*/
annotate=anno2; /*identifies annotate dataset*/
format _numeric_ positive.; /*sets format for all numeric variables*/
run; quit;


這樣就完成了嗎?當然還沒。是有美中不足的地方,那就是標題只有顯示在左圖,但我們需要他「跨圖置中」。這個動作可再度透過 GREPLAY 來完成:

filename figures "d:\nesug\nesug_disease_b.png"; /*output destination*/
goptions reset=all device=png gsfname=figures htext=8pt
xpixels=3600 ypixels=2400 lfactor=6
/*settings for xpixels and ypixels generates image at 600dpi*/
/*lfactor (line factor) is increased by a factor of 6*/
xmax=6.4in ymax=3.4in hsize=6.4in vsize=3.4in;
proc greplay igout=work.gseg nofs tc=sashelp.templt template=whole;
treplay 1:gchart
1:gchart1;
run;
quit;


這個階段完成後才是最終極的完成品!!

VERTICAL BAR CHART WITH GROUPS


這種圖其實 EXCEL 就可以做出來了,如下圖所示:


我們套用上例的資料。先用個 PROC FORMAT 把年齡分群格式做好:

proc format;
value agefmt 0='0-9'
1='10-19'
2='20-29'
3='30-39'
4='40-49'
5='50-59'
6='60-69'
7='70+'
;run;


接著用 PROC SUMMARY 把資料作一些整合,並把結果存成另一個新得資料集名為 totals:

proc summary data=discdata.arcount; class sex agecat; output out=totals;
run;


按照慣例,還是先做一個 annotate 資料集:

/*Create annotate dataset to place N=_freq_ on the graph and label midpoint axis*/
data anno;
length function style color $8 position $1 text $20;
retain function 'label' x 10 y 8 xsys '5' ysys '5' when 'a';
set work.totals;
where sex=' ' and agecat=.;
function='label'; style='swissb'; text=compress('N='||(put(_freq_,comma6.))); color='black';
size=1; position='8'; cborder='black';output;
function='move'; x=x+46; y=y; output;
function='label'; position='8'; style='swissb';cborder='white';text='Age Group (Years)';
color='black';size=1;output;
run;


然後就可以套用主程式繪圖了:

goptions reset=all device=png ftext=swissb htext=8pt;
axis1 order=(0 to 1400 by 200) value=(f=swissb h=11pt) minor=none label=(h=11pt f=swissb angle=90 'Number of Reported Cases');
axis2 minor=none value=none label=none;
axis3 order=(0 to 7 by 1) minor=none value=(h=11pt f=swissb) label=none;
legend1 across=1 /*Number of columns across*/
down=2 /*number of columns down*/
cborder=black /*draws a black border around legend*/
position=(top left inside)
mode=protect /*allows legend to cover space within plot area*/
label=(f=swissb h=11pt position=(top center) justify=center'Gender')
value=(f=swissb h=11pt justify=left 'Female' 'Male');
title f=swissb height=12pt "Figure 3. Disease C by Age Group and Gender: Philadelphia,
2004";
footnote1 " ";/*Empyt footnotes leave room at bottom of graph for N*/
footnote2 " ";
pattern1 value=solid color=red;
pattern2 value=x3 color=blue;
proc gchart data=discdata.arcount;
vbar sex / group=agecat
subgroup=sex /*same as chart variable (for legend)*/
patternid=subgroup /*controls when pattern changes*/
raxis=axis1 /*response axis*/
maxis=axis2 /*midpoint axis*/
gaxis=axis3 /*group axis*/
legend=legend1
annotate=anno/*references annotate dataset created above*/
autoref/*Draws reference lines for the response axis*/
clipref/*Clips reference lines at bars*/
gspace=1 /*Space between groups of bars*/
width=3; /*sets width of bars*/
format agecat agefmt.;
run;
quit;


再說明一些此例用到的 GCHART 語法:
  1. group:指定主要分群變數名稱,此例是 agecat。
  2. subgroup:指定次要分群變數名稱,此例是 sex。
  3. patternid:指定圖例所要顯示的分群變數,此例是 subgroup(也就是 sex)。
  4. autoref:選擇是否顯示參考橫線。
  5. clipref:讓 bar 蓋在參考橫線的上面。
  6. gspace:控制 group 變數的間距。
  7. width:控制 bar 的寬度。
  8. legend:呼叫 legend。
  9. annotate:呼叫 annotate。
然後用 GREPLAY 把這張圖再修飾一下:

filename figures "d:\nesug\disease_c_nesug.png"; /*output destination*/
goptions reset=all device=png gsfname=figures htext=8pt
xpixels=3600 ypixels=2400 lfactor=6
/*settings for xpixels and ypixels generates image at 600dpi*/
/*lfactor (line factor) is increased by a factor of 6*/
xmax=6.4in ymax=3.4in hsize=6.4in vsize=3.4in;
proc greplay igout=work.gseg nofs tc=sashelp.templt template=whole;
treplay 1:gchart;
run;
quit;


此圖就很簡單地完成了。

最後,就是如何將圖輸出(如下所示),該文使用 ODS 系統來處理,有興趣的人可參考本站其他跟 ODS 相關的文章,裡面會有比較詳盡的討論。



CONTACT INFORMATION

Your comments and questions are valued and encouraged. Contact the author at:
Michael Eberhart, MPH
Philadelphia Department of Public Health
500 S. Broad Street
Philadelphia, PA 19146
Work Phone: 215-685-6603
Email: michael.eberhart@phila.gov
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; }