久しぶりのGTL関連の記事です。SASにかける時間を減らしたいといいつつも、結局書いているんですよねー
まあGTLが現状このブログのメインコンテンツなので、記事の補足資料ということで大目に見てね。。
さて久しぶりにSASユーザー総会の演題を見ていたのですがGTLで散布図行列を作成された方がいらっしゃったので、今回はその改良の提案と今回使用している
layout latticeについてちょっぴり解説していきます。layout latticeは複雑なオブジェクトなので挙動が良くわからない方も多いと思いますが、設計思想を読み解けば多少はわかりやすくなるかもね。
散布図行列とは
散布図行列とは2変数の組み合わせにおけるグラフを格子状に配置したグラフです。変数ペアの相関関係などを一度に確認するために作成されます。
散布図行列というくらいですからグラフは基本的に散布図とその回帰直線などを表示することが一般的です。最近のグラフパッケージは箱ひげ図、バイオリンプロット、KDEプロット、ヒストグラムなど
も表示することもあるようです。
SASではproc sgscatterかGTLのscatterplotmatrixステートメントで実装できますが、各セルで表示したい情報を変更したい場合はGTLで各セルを定義する必要があります。
RではRgally、Pythonだとseabornのpairplotなどで作成することができます。SASよりは多機能っぽいですね。
RのRGallyと同様の散布図行列をGTLで実装した事例が公開されました。今回はこの発表を参考に改良した結果を紹介します。元発表の通りRのRGallyの出力結果に合わせて作成しますが、
できるだけコードの再利用と体裁の微調整の手間の考慮して改良しています。
(SASによる散布図行列の実装、PDFのため直リンクは避けてます)
GTLを用いて散布図行列を作成する。
では改良版のコードはこちらです。はいドーン。
proc template ;
/* ############################### */
/* グラフスタイル定義 */
/* seaborn仕様にする */
/* ############################### */
define style seaborn; parent = styles.HTMLblue;
/* グラフカラーの定義 */
class GraphColors /
'gcdata1' = cx4c72b0
'gcdata2' = cxdd8452
'gcdata3' = cx55a868
'gdata1' = cx4c72b0
'gdata2' = cxdd8452
'gdata3' = cx55a868 ;
/* グラフデータ(表示設定)の定義 */
class GraphData1 /
markersymbol = "circlefilled"
linestyle = 1
contrastcolor = GraphColors('gcdata1')
color = GraphColors('gdata1');
class GraphData2 /
markersymbol = "circlefilled"
linestyle = 1
contrastcolor = GraphColors('gcdata2')
color = GraphColors('gdata2');
class GraphData3 /
markersymbol = "circlefilled"
linestyle = 1
contrastcolor = GraphColors('gcdata3')
color = GraphColors('gdata3');
end;
/* ############################### */
/* 3行3列 散布図行列の定義 */
/* ############################### */
define statgraph scattermatrix; begingraph;
/* ------------------------------- */
/* aatribute map */
/* ------------------------------- */
discreteattrmap name="attrmap";
value "Setosa" / fillattrs=GraphData1 markerattrs=GraphData1 lineattrs=GraphData1;
value "Versicolor" / fillattrs=GraphData2 markerattrs=GraphData2 lineattrs=GraphData2;
value "Virginica" / fillattrs=GraphData3 markerattrs=GraphData3 lineattrs=GraphData3; enddiscreteattrmap; discreteattrvar attrmap="attrmap" var=species attrvar=_grp;
/* ------------------------------- */
/* レイアウトここから */
/* ------------------------------- */
layout lattice /rows=3 columns=3 rowgutter=5 columngutter=5 columndatarange=union;
/* ------------------------------- */
/* 行タイトルの設定 */
/* ------------------------------- */
row2headers;
layout overlay / backgroundcolor=cxD8D8D8 opaque=true;
entry "Sepal Length (mm)" / textattrs=(size=14) rotate=270 ;
endlayout;
layout overlay / backgroundcolor=cxD8D8D8 opaque=true;
entry "Sepal Width (mm)" / textattrs=(size=14) rotate=270;
endlayout;
layout overlay / backgroundcolor=cxD8D8D8 opaque=true;
entry "Species" /textattrs=(size=14) rotate=270;
endlayout;
endrow2headers;
/* ------------------------------- */
/* 列タイトルの設定 */
/* ------------------------------- */
column2headers;
layout overlay / backgroundcolor=cxD8D8D8 opaque=true;
entry "Sepal Length (mm)" / textattrs=(size=14) ;
endlayout;
layout overlay / backgroundcolor=cxD8D8D8 opaque=true ;
entry "Sepal Width (mm)" / textattrs=(size=14) ;
endlayout;
layout overlay / backgroundcolor=cxD8D8D8 opaque=true ;
entry "Species" / textattrs=(size=14) ;
endlayout;
endcolumn2headers;
/* ------------------------------- */
/* 列共通軸の設定 */
/* 行方向はy軸変数が各セルでばらばらなので共通軸は設定しない*/
/* ------------------------------- */
columnaxes;
columnaxis / display=(line ticks tickvalues) griddisplay=on;
columnaxis / display=(line ticks tickvalues) griddisplay=on;
columnaxis / display=(line ticks tickvalues) griddisplay=on;
endcolumnaxes;
/* ------------------------------- */
/* cell 1: KDE */
/* このセルのY軸だけ軸目盛の桁数が多く、体裁が崩れるためフォントを少し小さくする*/
/* ------------------------------- */
layout overlay /
yaxisopts=(display=(line ticks tickvalues)
tickvalueattrs=(size=8) griddisplay=on);
densityplot sepallength /kernel() group=_grp;
endlayout;
/* ------------------------------- */
/* cell 2: 統計情報 */
/* ------------------------------- */
layout lattice / rows=4 columns=1;
entry "Corr: -0.118" / textattrs=(size=14);
entry "Setosa: 0.743***" / textattrs=GraphData1(size=14);
entry "Versicolor: 0.526***" / textattrs=GraphData2(size=14);
entry "Virginica: 0.457***" / textattrs=GraphData3(size=14);
endlayout;
/* ------------------------------- */
/* cell 3: 箱ひげ図 */
/* ------------------------------- */
layout overlay /
yaxisopts=(display=none griddisplay=on);
boxplot x=_grp y=sepallength /
display=(mean median outliers)
group=_grp;
endlayout;
/* ------------------------------- */
/* cell 4: 散布図 */
/* ------------------------------- */
layout overlay / yaxisopts=(display=(line ticks tickvalues) griddisplay=on);
scatterplot x=sepallength y=sepalwidth /
group=_grp;
endlayout;
/* ------------------------------- */
/* cell 5: KDE */
/* ------------------------------- */
layout overlay /
yaxisopts=(display=none griddisplay=on);
densityplot sepalwidth /
kernel()
group=_grp ;
endlayout;
/* ------------------------------- */
/* cell 6: 箱ひげ図 */
/* ------------------------------- */
layout overlay /
yaxisopts=(display=(line) griddisplay=on);
boxplot x=_grp y=sepalwidth /
display=(mean median outliers)
group=_grp;
endlayout;
/* ------------------------------- */
/* cell 7: ヒストグラム */
/* ------------------------------- */
layout lattice / rows=3 columns=1 columndatarange=union;
columnaxes;
columnaxis / display=none griddisplay=on;
endcolumnaxes;
layout overlay /
yaxisopts=(label="" display=(line ticks tickvalues)
griddisplay=on
linearopts=(tickvaluelist=(10 20 30 40) viewmin=0 viewmax=50)
);
histogram eval(ifn(species="Setosa",sepallength,.)) /
group=_grp;
endlayout;
layout overlay /
yaxisopts=(label="" display=(line ticks tickvalues)
griddisplay=on
linearopts=(tickvaluelist=(10 20 30 40) viewmin=0 viewmax=50)
);
histogram eval(ifn(species="Versicolor",sepallength,.)) /
group=_grp;
endlayout;
layout overlay /
yaxisopts=(label="" display=(line ticks tickvalues)
griddisplay=on
linearopts=(tickvaluelist=(10 20 30 40) viewmin=0 viewmax=50)
);
histogram eval(ifn(species="Virginica",sepallength,.)) /
group=_grp;
endlayout;
endlayout;
/* ------------------------------- */
/* cell 8: ヒストグラム */
/* ------------------------------- */
layout lattice / rows=3 columns=1
columndatarange=union;
columnaxes;
columnaxis / display=none griddisplay=on;
endcolumnaxes;
layout overlay /
yaxisopts=(label="" display=none griddisplay=on
linearopts=(tickvaluelist=(10 20 30 40) viewmin=0 viewmax=50) );
histogram eval(ifn(species="Setosa",sepalwidth,.)) /
group=_grp;
endlayout;
layout overlay /
yaxisopts=(label="" display=none griddisplay=on
linearopts=(tickvaluelist=(10 20 30 40) viewmin=0 viewmax=50) );
histogram eval(ifn(species="Versicolor",sepalwidth,.))/
group=_grp;
endlayout;
layout overlay /
yaxisopts=(label="" display=none griddisplay=on
linearopts=(tickvaluelist=(10 20 30 40) viewmin=0 viewmax=50) );
histogram eval(ifn(species="Virginica",sepalwidth,.))/
group=_grp;
endlayout;
endlayout;
/* ------------------------------- */
/* cell 9: 棒グラフ */
/* ------------------------------- */
layout overlay /
yaxisopts=(display=none);
barchart category=species /
group=_grp;
endlayout;
endlayout;
endgraph;
end;
run;
/* ブログ投稿用にpngにしているけど基本的にemfかsvgを使うこと*/
ods listing gpath="画像パス" style=seaborn;
ods graphics / reset=index width=25cm height=25cm imagename="scattermatrix" imagefmt=png border=off; proc sgrender data=sashelp.iris template=scattermatrix;
run;
解説
ROWHEADERS、COLUMNHEADERSを使おう
引用元の発表では行タイトルと列タイトルをsidebarブロックで定義していますが、GTLには各行と各列に対応する情報を表示するROWHEADERSブロックとCOLUMNHEADERSブロックが用意されています。
sidebarブロックはすべての列またはすべての行に共通する情報を表示するためのブロックなので、今回のように各行および各列の変数名を表示するのであればROWHEADERSとCOLUMNHEADERSを使用するのがベストです。
sidebar内にテキストを配置すれば見た目は同じになりますが、表示文字列が各行、各列に紐づけられていないため、テキストの位置調整がとても面倒なことになります。
ROWHEADERSはlatticeレイアウトの左側、COLUMNHEADERSは下部に位置します。一方ROW2HEADERSとCOLUMN2HEADERSというブロックもあり、この二つはそれぞれlatticeレイアウトの右側、上側に位置します。Rの出力結果にそろえるため、今回はROW2HEADERSとCOLUMN2HEADERSを利用しました。
ROWHEADERSとCOLUMNHEADERSの使い方は簡単で、タイトル文字列を順番にentryステートメントで定義するだけでタイトルを表示できます。ROWHEADERSの場合は上から、COLUMNHEADERSは左から順に指定します。
columnheaders;
entry "タイトル1";
entry "タイトル2";
entry "タイトル3";
endcolumnheaders;
今回はタイトルに背景色をつけていましたので、各文字列をlayout overlayで囲んで背景色を定義します。こうすると文字列だけでなくブロック全体に背景色を設定できます。
文字列の配置
「cell2: 統計情報」で相関係数などを表示するために、文字列を表示しています。引用元の発表ではおそらくlayout overlay内にテキストを4つ配置しているか、layout griddedでセルを4分割しているかのいずれかと思います、たぶんね。
前者の場合ですと、各文字列の表示位置をマニュアルで調整するため、おそらくパディングを設定しているのではないでしょうか。それだと最適なパディングの設定値を確認するために何度も再実行と修正を繰り返すため割と面倒です。相対単位でパディングを設定すればある程度使いまわしがきくとは思いますが。layout griddedの場合は特に設定しない場合パディングがほぼない状態で配置されます。
セル内に均等に文字列を配置したい場合はlayout latticeを使ったほうが楽になると思います。layout latticeは特に設定しなくても可能な限りパディングを設定したうえで文字列を配置できます。今回は文字列を1列に配置しているだけですが、2列以上の場合はaxistableを使ったほうが良いでしょう。
実際に試してみます。layout overlay内にテキストを4つ配置するだけだと、padオプションなどでマニュアルで調整しない限りすべてのテキストが同じ位置に配置されてしまいます。
layout overlay / ;
entry "Corr: -0.118" / textattrs=(size=14);
entry "Setosa: 0.743***" / textattrs=GraphData1(size=14);
entry "Versicolor: 0.526***" / textattrs=GraphData2(size=14);
entry "Virginica: 0.457***" / textattrs=GraphData3(size=14);
endlayout;
layout griddedだと特に設定しなくてもテキストが重ならないように配置されますが、表示スペースがあってもパディングは最小限に設定されます。layout gridded内のセルサイズは
セル内の要素に依存して決定されるようです。
layout gridded / rows=4 columns=1;
entry "Corr: -0.118" / textattrs=(size=14);
entry "Setosa: 0.743***" / textattrs=GraphData1(size=14);
entry "Versicolor: 0.526***" / textattrs=GraphData2(size=14);
entry "Virginica: 0.457***" / textattrs=GraphData3(size=14);
endlayout;
layout latticeの場合は利用できる最大のスペースを分割してセルを作成します。セル内の要素には依存しません。そのためパディングをある程度確保したうえで要素を配置することが可能です。
もちろん要素を配置できる十分なスペースがあることが条件ですが。
layout lattice/ rows=4 columns=1;
entry "Corr: -0.118" / textattrs=(size=14);
entry "Setosa: 0.743***" / textattrs=GraphData1(size=14);
entry "Versicolor: 0.526***" / textattrs=GraphData2(size=14);
entry "Virginica: 0.457***" / textattrs=GraphData3(size=14);
endlayout;
パディングを適度に設定された方が見やすいと思うので今回はlayout latticeを使用しています。
マーカーとラインの属性をスタイルで定義する
今回の趣旨からはずれますが、引用元の発表ではRのカラーパレットに合わせて出力しているようでしたので、俺はseabornのカラーパレットを使ってみました。
色やラインの設定をする場合はmarkarattrsやlineattrsオプションを使用してカラーコードを指定するケースが多いかと思いますが、同じ体裁のグラフを複数作成する場合はスタイルにマーカーやラインの属性情報を定義しておくと便利です。今回はデフォルトのスタイルをコピーしてseabornのカラーパレットのカラーコードを設定しました。新規スタイル”seaborn”を作成し、odsのstyleオプションで作成したスタイルを指定しています。
スタイルを使用する方法だと複数の出力物の体裁を一括で変更できるため、Figureをたくさん作成する場合は覚えておいても良いかと思います。
まあここは本筋から外れますので詳細は割愛します。作図コードの「define style seaborn~end」あたりの部分を見てください。
行共通軸と列共通軸を使おう
layout latticeには行ごとまたは列ごとの共通の軸をROWAXESブロックまたはCOLUMNAXESブロックで設定できます。
これらを使用すると各セルの軸は強制的に非表示となり、代わりにレイアウトの下部または左側に共通軸が表示されます。また行、列ごとに軸のスケールを統一するためには親ブロックのlayout latticeにcolumndatarange=unionまたはrowdatarange=unionを指定しましょう。これを設定するとセルの配置と軸がそろうため体裁が良くなります。またROWHEADERとCOLUMNHEADERの大きさも自動調整されます。
今回の場合ですと列方向では各セルの横軸は同じなので、列の共通軸を設定しています。一方行方向は縦軸の変数が各セルでばらばらであるため共通軸の設定ができません(KDEだけ縦軸が密度になっている)。
行方向だけはセルごとに軸設定を記述しています。
共通軸の設定は以下のように記述します。今回は3列なので左から順にcolumnaxisステートメントで3つの軸を設定しました。
columnaxes; columnaxis / display=(line ticks tickvalues) griddisplay=on;
columnaxis / display=(line ticks tickvalues) griddisplay=on;
columnaxis / display=(line ticks tickvalues) griddisplay=on;
endcolumnaxes;
うまく整列されない場合はテキスト設定を見る
上記のような設定をしても各セルの位置がそろわないケースもあると思います。今回だとKDEの縦軸の目盛りの桁数が多くこのままだとセル幅が若干ですが揃いません。
GTLはテキストの表示を優先するようで、テキストの表示スペースが不足する場合はプロットエリアを縮小するか目盛りをトリミングする仕様になっているようです。可読性重視ってことですね。
また軸目盛の配置オプションはいろいろありますが、縦軸の目盛りは横軸よりもオプションが少なくうまくいかないことも。その場合は目盛りの表示テキストを見直すか、テキストサイズを調整することで手間なくうまくいく気がします。今回は少しだけテキストサイズを小さくしました。これでも若干ずれている気がしますが、、、妥協しました。
GTLはマークアップ言語である
ベテラン勢でも「とりあえず目的の表示になっていればコードはそこまでこだわらなくてもいいじゃん」という人もいますが、それが成り立つのは一切コードを再利用をしない場合だけです。
実際は過去に作成したコードを再利用するケースが多いはずで、ブロック構造の意味を考えてコードを書かないと後で修正するときに面倒なことになります。
GTLはSASのデータステップとは異なり、オブジェクト(GTLの場合はグラフ)構造を定義するマークアップ言語の側面を持っています。データステップと同じ感覚で設計すると痛い目に合うかもしれないですよ。
レイアウトブロックは使用用途をあらかじめ定義したうえで必要な属性が設定されていますから、想定された用途以外でレイアウトブロックを使用するとコードの保守や仕様変更が難しくなるでしょう。
例えば先ほど言及した通りsidebarで行と列のタイトルを設定しても確かに見た目は目的の通りになりますが、sidebarは各行と各列のブロックの位置を参照して自動調整してくれませんので、表示文字列の変更やグラフの変更が発生した場合、文字列の体裁をすべてマニュアルで調整しなくてはならなくなります。
行と列の位置に関係なく文字列を配置したいのならsidebarは最適ですが、そうでないのなら手間を増やすだけです。もちろんこれを意識したとしても全くマニュアル調整の手間が発生しないわけではないですが、わずらわしさは軽減されるでしょう。