SAS

GTLで複数のグラフを出力する(1) by ステートメント

SASユーザー総会は基本的に参加してない私ですが、今年はグラフィックスの発表があったのでプレゼン資料を見させてもらいました。
最近は論文投稿用のFigure作成にGTLを使っている人が増え始めたんでしょうか。でもGTL自体はもう10年以上も前に発表されたものですし
GTLの活用例自体は海外の論文やSAS公式でも発表されていますから、正直かなり遅れていると言わざるを得ませんね・・・

もうgplotやannotationから卒業しようよ・・・

発表者は複数のグラフを配置した複雑なレイアウトのFigure作成を共同研究者から依頼されたらしいので、今回はGTLを使って複数グラフを作成
する方法を改めて紹介します。最終的に発表時に触れられていた参考論文のFigureを実際にGTLで作成してみたいと思います。

画像を複数出力する

単純に単一のグラフを複数枚作成するケースは、大抵の場合サブグループや対象集団ごとに同じグラフを出力する場合かと思います。

その場合はproc sgrenderを実行する際にbyステートメントを使うか、マクロをの%doループをつかってsgrenderの実行を繰り返す方法があります。
今回は前者を紹介します。

proc sgrender data=作図データセット template=テンプレート; 
by グループ変数; run;

GTLはifステートメントが使えるので、サブグループ毎に作図設定を切り替えることもできます。

サブグループ名を表示する

by ステートメントでサブグループ毎に出力する場合は、BYVALという特別なDynamic variableを使用できます。
これはBy変数の値を格納する変数で、これを使うとをグラフ上にサブグループ名を表示できます。
Byステートメントの複数の変数を指定した場合は指定した順に「BYVAL2, BYVAL3 ・・・」といった感じで変数の値を格納した
Dynamic variableを使用できます。

以下のコードはBY変数の値をグラフタイトルに表示します。

proc template; 
define statgraph temp; dynamic _BYVAL_; 
begingraph; 
entrytitle _BYBAL_; 
< GTL statement >; 
endgraph; 
end; 
run;

作図例

今回はSASユーザー総会の発表で少し出てきた論文のFigureをGTLで再現します。
以下の論文のFigure 2Aを作成してみましょう

作図データの構造は以下の通りです。

変数名定義
PARAMN評価項目番号
PARAM評価項目名
TRT1A群(Drug XXXまたはプラセボ)
AVISITN時点
ATPT経過時間(week)
CHGベースラインに対する変化量(最小二乗平均の差)
upper変化量の95%信頼上限
lower変化量の95%信頼上限
atriskリスク集合の大きさ

上記の論文と同様にPARAMN毎に評価項目の変化量をプロットします。なお評価項目ごとにY軸の設定を変更します。
さらにグラフ下部にリスク集合テーブルを表示します。

評価項目の数値の減少は悪化を意味します。わかりやすくするためにグラフ左にスペースを確保し下矢印を表示します。

コードは以下の通りです。

proc template; 
define statgraph figure1a; 

*by変数の値を取得するためにdynamic variableを定義; 
dynamic _BYVAL_ _BYVAL2_; 

begingraph; 

*群の表示設定を定義; 
discreteattrmap name="map"; 
value "Drug XXX" / lineattrs=(color=green pattern=solid) markerattrs=(color=green symbol=trianglefilled); 
value "Placebo" / lineattrs=(color=grey pattern=solid) markerattrs=(color=grey symbol=circlefilled); 
enddiscreteattrmap; 
discreteattrvar var=TRT1A attrmap="map" attrvar=_group; 

*評価項目3のみ項目名に下付き文字が入るため条件分岐; 
*by変数に下付き命令を格納しても機能しない?ので、GTL上で定義する; 

if (_BYVAL_=3) 
   entrytitle "ADAS-Cog" {sub "13"} " Score" / pad=(left=20%); 
else 
   entrytitle _BYVAL2_ " Score" / pad=(left=20%); 
endif; 

*左カラムに矢印を、右カラムにグラフを表示する; 
layout lattice / columns=2 columnweights=(0.1 0.9); 
*矢印の作図 drawtextだと表示がバグるのでentryステートメントで代用; 
   layout overlay; 
      entry textattrs=(family="Arial" size=10) "Worsening" / 
         rotate=90 pad=(bottom=30%) ; 
      drawarrow x1=70 y1=90 x2=70 y2=30 / 
         lineattrs=(color=black) 
         arrowheadshape=filled; 
   endlayout; 

*右カラムをさらに分割、上側にグラフ、下側にリスク集合テーブルを表示する; 
   layout lattice/ rows=2 rowweights=(0.9 0.1) columndatarange=union; 

   *by変数の値ごとにグラフ設定を定義する。layoutブロックオプションを条件分岐させる場合は; 
   *ifブロック内にlayoutブロック全体を入れないといけない点に注意; 

   if (_BYVAL_=1) 
       layout overlay /xaxisopts=(label="Week" 
          linearopts=(viewmax=76 viewmin=0 
             tickvaluesequence=(start=0 end=76 increment=4) tickvalueformat=tick.) 
       ) 
          yaxisopts=(label="Least squares mean / change from baseline" labelsplitchar="/"
            labelfitpolicy=splitalways 
               linearopts=( viewmin=-12 viewmax=2 
                  tickvaluesequence=(start=-12 end=2 increment=2))
          ); 

           seriesplot x=ATPT y=CHG / 
              group=_group 
              display=all 
              name="series" 
              yerrorlower=lower 
              yerrorupper=upper; 
    *参照線; 
           referenceline y=0 / 
              lineattrs=(color=black pattern=shortdash thickness=2); 

           discretelegend "series" / across=1 location=inside autoalign=(bottomleft topright); 
        endlayout; 
    endif; 

    if (_BYVAL_=2) 
        layout overlay /xaxisopts=(label="Week" 
            linearopts=(viewmax=76 viewmin=0 
               tickvaluesequence=(start=0 end=76 increment=4) tickvalueformat=tick.) 
           ) 
           yaxisopts=(label="Least squares mean / change from baseline" labelsplitchar="/"
              labelfitpolicy=splitalways 
              linearopts=( viewmin=-2.5 viewmax=0 
                 tickvaluesequence=(start=-2.5 end=0 increment=0.5))
           ); 

           seriesplot x=ATPT y=CHG / 
              group=_group 
              display=all 
              name="series" 
              yerrorlower=lower 
              yerrorupper=upper; 

           referenceline y=0 / 
              lineattrs=(color=black pattern=shortdash thickness=2); 
 
           discretelegend "series" / 
              across=1 
              location=inside 
              autoalign=(bottomleft topright); 
        endlayout; 
     endif; 

     if (_BYVAL_=3) 
        layout overlay /xaxisopts=(label="Week" 
            linearopts=(viewmax=76 viewmin=0 
               tickvaluesequence=(start=0 end=76 increment=4) tickvalueformat=tick.) 
            ) 
            yaxisopts=(label="Least squares mean / change from baseline" labelsplitchar="/"
               labelfitpolicy=splitalways 
               linearopts=( viewmin=-6 viewmax=1 
                   tickvaluesequence=(start=-6 end=1 increment=1))
            ); 

            seriesplot x=ATPT y=CHG / 
               group=_group 
               display=all 
               name="series" 
               yerrorlower=lower 
               yerrorupper=upper; 

            referenceline y=0 / 
                lineattrs=(color=black pattern=shortdash thickness=2); 

          discretelegend "series" / 
              across=1 
              location=inside 
              autoalign=(bottomleft topright); 
         endlayout; 
      endif; 
     
     if (_BYVAL_=4) 
        layout overlay /
           xaxisopts=(label="Week" 
              linearopts=(viewmax=76 viewmin=0 
                 tickvaluesequence=(start=0 end=76 increment=4) tickvalueformat=tick.) 
              ) 
           yaxisopts=(label="Least squares mean / change from baseline" labelsplitchar="/" 
              labelfitpolicy=splitalways 
              linearopts=( viewmin=-6 viewmax=0 
                 tickvaluesequence=(start=-6 end=0 increment=2))
           ); 

          seriesplot x=ATPT y=CHG / 
             group=_group 
             display=all 
             name="series" 
             yerrorlower=lower 
             yerrorupper=upper; 

          referenceline y=0 / 
             lineattrs=(color=black pattern=shortdash thickness=2); 
          
          discretelegend "series" / 
              across=1 
              location=inside 
              autoalign=(bottomleft topright); 
       endlayout; 
    endif; 

    if (_BYVAL_=5) 
       layout overlay /xaxisopts=(label="Week" 
          linearopts=(viewmax=76 viewmin=0 
             tickvaluesequence=(start=0 end=76 increment=4) tickvalueformat=tick.) 
          ) 
          yaxisopts=(label="Least squares mean / change from baseline" labelsplitchar="/"
             labelfitpolicy=splitalways 
             labelsplitchar="/" 
             linearopts=( viewmin=-4 viewmax=0 tickvaluesequence=(start=-5 end=0 increment=1))
          ); 

          seriesplot x=ATPT y=CHG / 
             group=_group 
             display=all 
             name="series" 
             yerrorlower=lower 
             yerrorupper=upper; 

          referenceline y=0 / 
             lineattrs=(color=black pattern=shortdash thickness=2); 
 
          discretelegend "series" / 
             across=1 
             location=inside 
             autoalign=(bottomleft topright); 
       endlayout; 
    endif; 
   *リスク集合テーブル; 
    layout overlay /walldisplay=none 
         yaxisopts=(display=none) 
         xaxisopts=(display=none 
            linearopts=(viewmax=76 viewmin=0 
               tickvaluesequence=(start=0 end=76 increment=4) tickvalueformat=tick.)
         ); 

         axistable x=ATPT value=atrisk / 
            class=_group 
            title="No. of paticipants"; 
     endlayout; 
   endlayout; 
endlayout; 
endgraph; 
end; 
run; 

ods escapechar="^"; 
ods graphics/ reset=index(1) imagefmt=png border=off imagename="fig2a_"; 
ods listing gpath="グラフパス"; 

proc sgrender data=import2 template=figure1a; 
*評価項目名を取得するのでparamもby変数として指定; 
by paramn param; 
run;
実行結果1
実行結果2
実行結果3
実行結果4
実行結果5

測定結果は縦積みにしてデータセットを作成することが多いのでbyステートメントを使ったサブグループ毎のグラフ作成と相性がいいです。
GTL内でifステートメントを使用することで評価項目ごとに適切なグラフ設定を定義することができるので汎用性は高いと思います。

出力ごとに設定を変える時にdynamicステートメントで出力ごとに設定値を入力する方法もありますが、設定する範囲が多いと逆に大変なのでifステートメントで条件分岐させる方が
実務ではおそらく便利だと思います。

layout datalatticeは使わないの?と思われる方もいるかもしれませんが、datalatticeは制約が多すぎて使える場面は限られます。

datalatticeレイアウトは内部にlayout prototypeしか設置できません。しかも複数のブロックをネストさせて使うこともできませんし、boxplotのような計算が発生するステートメントも使えません。
今回のようにグラフとリスク集合テーブルを分けて配置するレイアウトではdatalatticeを使用することができませんので、layout latticeでグラフをひとつずつ定義しています。

今回は画像として各評価項目を個別に出力していますが、これを格子状に並べて出力する方法は次回紹介したいと思います。