2024年4月1日月曜日

SASの勉強会に行ってきた話

先日某所にてSASの某発表会に参加した。開催当初から参加しているが、ずいぶん大所帯になったものだ。すげーわ。これが主催者の某氏の人徳かと恐れ入るばかり。最初は4人だか5人だかくらいだったのが今は40弱て。 

私の発表資料はこちら。今回はproc sgpie…またこいつグラフ書いてんな。他にやることないんか。円グラフについてのプロシジャを紹介する内容だ。GTLで良くない?みたいな正論パンチはご遠慮いただきたく。何の反論もございません。この資料は例によってマイクロソフトのswayを使っている。発表に使ったPPTを雑に公開するにはこれ以上ない便利ツールなんだが私以外の人が使っているのを見たことがない。お願いだからマイクロソフト君はこんな便利なサービスを終えないでくれ。

10分程度の短い発表とはいえ10本強の発表を聞くのはすごいカロリーを要する。どれも面白くて興味深いのだが。ワーッと発表がきてうわーっとなるのはSASユーザ会みたいな30分と発表の枠の大きなところでは味わえない醍醐味か。どちらもとても楽しい。

関係者各位の多大なご厚意によってこの勉強会は成り立っています。みなさままことにありがとうございました。

2024年3月1日金曜日

proc jsonでdataset jsonを作ってみる話

 某有名ブログでもすでに取り上げらていますが、CDISCからxptに代わるデータフォーマットとしてJSON形式のdataset-jsonが提案されています。先のCDISCリンクのSpecificationタブでその仕様の例示がされています。せっかくなのでSASで出してみよう…だと某ブログと同じなので、dataset-jsonに必要なメタデータをマクロ変数に格納してある程度融通が利きそうにしてみようというブログです。

先のCDISCのページでは以下のような記載もあり、2024年1Qにinitial reportが出てくるみたいですね。どうなるのか楽しみです。このブログはinitial reportが出る前に書いていますが、公開される頃にはすでにreportが出て古い内容になっているかもしれません。そのときは更新記事を(書けたら)書きます。

The pilot will be split into short-term goals of the acceptance of Dataset-JSON as a transport format option (in addition to existing XPT format), as well as the development of the future strategy relating to the adoption of advanced Dataset-JSON. An initial report is planned for Q1 2024.

必要な情報をマクロ変数に格納するにあたって、datasetJSONVersion(dataset-jsonのバージョン)とかoriginator(dataset-jsonを作った組織)とかstudyOIDは多分大きく変わらないので仕様書にでも書いておくのが良い気がしました。また仕様書に書く内容が増えますね。つらい。変数の定義とかも仕様書を参照するのが良いのですが、レコード数の情報はデータセットを参照せざるを得ないですね…今回はどうせレコード数を参照しにデータセットを使うので、データセットの名前とデータセットラベルもデータセットを参照して作成しています。この二つはデータセット以外からも参照できると思います。

今回使用するデータセットはCDISCのページにあるものと概ね同じとし、以下のものとします。最後のjsonファイルもCDISCのページにあるものと大体同じにしていますが、optionalの項目も結構あるので、実際に出す必要があるのはどれかを確認しないといけませんね。項目は少ない方がミスする箇所が少ないので…

data DM ;

    attrib STUDYID label = "Study identifier" length = $7

           USUBJID label = "Unique Subject Identifier" length = $3

           DOMAIN  label = "Domain Identifier" length = $2

           AGE     label = "Subject Age" length = 8

    ;

    STUDYID = "MyStudy" ; USUBJID = "001" ; DOMAIN = "DM" ; AGE = 56 ; output ;

    STUDYID = "MyStudy" ; USUBJID = "002" ; DOMAIN = "DM" ; AGE = 26 ; output ;

run ;

/*---------- cdiscのページにはあるITEMGROUPDATASEQは面倒だったので省略した ----------*/

proc sort data = DM out = DM(label = "Demographics") ;

    by STUDYID USUBJID ;

run ;

このデータセットのほかに、各種仕様書からdataset-jsonに格納するべき内容を集めた以下のデータセットを作って、マクロ変数に入れたとします。ただしcreationDateTimeとasOfDateTimeはプログラム実行時に作成します。asOfDateTimeってdataset-jsonを作成するためにデータセットにアクセスした日時を格納するらしいのですが、それってほとんどdataset-jsonを作成した日時と同じになりませんかね…?同じとするとcreationDateTimeとasOfDateTimeの日時って常にだいたい同じものが入るのでは…?今回は同じ日付を入れていますが、実は私の解釈違いでasOfDateTimeはdataset-jsonを作成するためのデータセットの作成日時、とかかもしれません。エイゴヨメマセン

これは蛇足ですがマクロ変数に上記の中身を入れた時のプログラムです。

data _null_ ;

    set topMeta clinicalMeta ;   

    call symputx( cats("_" , A) , B) ;

run ;

その次に日時は以下のようにそれぞれマクロ変数化しておきます

/*---------- creationDateTime ----------*/

data _null_ ;

    CRTDT = put(datetime(), e8601dt.) ;

    call symputx("_CRTDT" , CRTDT) ;

run ;

/*---------- asOfDateTime(ソースデータにアクセスが行われた日付) ----------*/

data _null_ ;

    OFDT = put(datetime(), e8601dt.) ;

    call symputx("_ofdt" , OFDT) ;

run ;

メタデータをマクロ変数に入れたら、次はデータセットのレコード数、名前、ラベルをマクロ変数に入れます。レコード数をマクロ変数に入れるのはいろんなやり方がありますが、ここでは上述の通りデータセット名なども一緒に参照するのでproc contentsにしています。単にレコード数をとるならif 0 then set とかのほうが早いと思いますし、うっかり0件のデータセットが紛れてきたときに都合が良さそうです。

proc contents data = DM out = CONT noprint ;

run ;

data _null_ ;

    set cont ;

    if _n_ = 1 then do ;

        call symputx("_records" , NOBS) ;

        call symputx("_name" , MEMNAME) ;

        call symputx("_label" , MEMLABEL) ;

    end ;

run ;

データセット内の変数名や変数ラベルの情報はマクロ変数ではなくデータセットにします。変数名なんて数が多いのでデータセット化してそのままproc jsonで出してしまうのが一番良い。今回は省略して以下の表のような形のものを準備しています。一行目が変数名なのですが、この名前がdataset-jsonの仕様で決められているのでここは今のところ変更不可です。仕様書なり読み込んだ後に変数名を変えてあげましょう。
一番左のOIDはdefineのItemRefタグのItemOIDと一致させると決められています(OID of a variable (must correspond to the variable OID in the Define-XML file))。といってもここってIT+ドメイン名+変数名だと思うので、上記の変数名を変えているときに一緒に作成すればdefineを参照しなくてもできると思います。正直define周りはあまり詳しくないので違うかもしれませんが…
今回は以下の変数情報を格納したデータセットをSPECという名前にしています。

準備するのは次が最後、itemGroupDataです。これはObject containing dataset informationと決められていて、Defineの itemGroupDefタグのOIDと一致させる必要があります。正直これもIG+データセット名だと思うので、Defineを参照せずにマクロ変数に作ってしまいしょう。
There must be only one dataset per Dataset-JSON file. The attribute name is OID of a described dataset, which must be the same as the OID of the corresponding itemGroupDef in the Define-XML file.

%let _target = DM ;
data _null_ ;
    call symputx("_itemGroupData" , cats("IG.", "&_target.") ) ;
run ;

今回はdefineを準備していないのでDefineと一致させる必要のあるこの2個所は、どうせ毎回パターンが一緒でしょうということにしてマクロ変数にしていますが、厳密にはdefineを参照するべきです。defineを読み込んでItemRef ItemOID=で抽出したら1個目の変数のOIDが、ItemGroupDef OID=を条件にしたら2個目のitemGroupDataが取れてくるはずです。ここの値の規則性が試験ごとに代わることは少ないと思っていますが…

それはさておきこれで必要な情報はそろいましたので、以下のproc jsonでdataset-jsonとして出力しましょう。マクロ変数さえ調整すれば、そのほかのwrite valueの部分とかは固定なので使いまわせるんじゃないですか?各箇所のAttributeは手打ちしているので、そこがCDISCによって変更されたら変えないといけませんが…
以下の部分は先達のブログと同じです。ほかにも書けるかなと思ったら思いのほか同じになって残念です。

proc json out = "piyopiyo\json\&_target..json" pretty ;
    write values "creationDateTime"     "&_crtdt." ;
    write values "datasetJSONVersion"  "&_datasetJSONVersion." ;
    write values "fileOID"                     "&_fileOID." ;
    write values "asOfDateTime"           "&_ofdt." ;
    write values "originator"                 "&_originator." ;
    write values "sourceSystem"           "&_sourceSystem." ;
    write values "sourceSystemVersion" "&_sourceSystemVersion." ;

    write values "clinicalData" ;
    write open object ;
        write values "studyOID"                  "&_studyOID." ;
        write values "metaDataVersionOID" "&_metaDataVersionOID." ;
        write values "metaDataRef"             "&_metaDataRef." ;

        write values "itemGroupData" ;
        write open object ;

            write values "&_itemGroupData." ;  /*注: defineのItemGroupDefのOIDと一致した値*/
            write open object ;

                write values "records" "&_records." ;
                write values "name"    "&_name." ;
                write values "label"     "&_label." ;

                write values "items" ;
                write open array ;
                    export SPEC / nosastags ;
                write close ;

                write values "itemData" ;
                write open array ;
                    export &_target. / nosastags nokeys ;
                write close ;

            write close ;

        write close ;

    write close ;

run ;

上記を実行すると以下のjsonファイルが作成できます。prettyオプションの都合で一番最後のitemdataの部分も縦積みになっていますが、そこはご愛嬌ということで…最後の部分だけ各要素を横持ちにすることができませんでした…CDISCのページの例だと最後のitemdataの部分だけ横持ちになってるんですけどアレどうやったんでしょうか、気になります。
{
  "creationDateTime": "yyyy-mm-ddThh:mm:ss形式で実行日時が入る",
  "datasetJSONVersion": "1.0.0",
  "fileOID": "www.sponsor.org.project123.final",
  "asOfDateTime": "yyyy-mm-ddThh:mm:ss",
  "originator": "Sponsor XYZ",
  "sourceSystem": "Software ABC",
  "sourceSystemVersion": "1.2.3",
  "clinicalData": {
    "studyOID": "xxx",
    "metaDataVersionOID": "xxx",
    "metaDataRef": "https://metadata.location.org/api.link",
    "itemGroupData": {
      "IG.DM": {
        "records": "2",
        "name": "DM",
        "label": "Demographics",
        "items": [
          {
            "OID": "IT.DM.STUDYID",
            "name": "STUDYID",
            "label": "Study identifier",
            "type": "string",
            "length": 7,
            "keySequence": 1
          },
          {
            "OID": "IT.DM.USUBJID",
            "name": "USUBJID",
            "label": "Unique Subject Identifier",
            "type": "string",
            "length": 3,
            "keySequence": 2
          },
          {
            "OID": "IT.DM.DOMAIN",
            "name": "DOMAIN",
            "label": "Domain Identifier",
            "type": "string",
            "length": 2,
            "keySequence": null /*注:CDISCの例だと欠測のkeySequenceは要素自体が起きていないが、値がnullで要素自体は起きるはず*/
          },
          {
            "OID": "IT.DM.AGE",
            "name": "AGE",
            "label": "Subject Age",
            "type": "integer",
            "length": 2,
            "keySequence": null
          }
        ],
        "itemData": [
          [
            "MyStudy",
            "001",
            "DM",
            56
          ],
          [
            "MyStudy",
            "002",
            "DM",
            26
          ]
        ]
      }
    }
  }
}





2024年2月2日金曜日

EXCELファイルをRでxptファイルに変換し、SASで回収する話

 BASE SASではproc importのdbmsにxlsxあたりを指定できないという話があります。excelを読み込みたくばSAS/ACCESS Interface to PC Filesを導入しろとのSAS社からの思し召しなのですが、そう言われるとどうにかならんのかと考えてしまいますね。こんな取り組みは弊社の業務では1mmも使わないので無駄なのですが、気に食わないものはしょうがない。最近はpythonがどうだと大盛り上がりですが、今回はRでexcelファイルをxptに変換して、sasでそれを読み込むことを目指します。sas7bdatは仕様が公開されていないらしいので、どうしてもsas以外を使うとXPTファイルにしかならないのは考え物です…案外私が知らないだけでどうにかなるのかもしれませんが。

まずはR恒例のパッケージインストールから。以下のコードを実行すると、パッケージがパソコンに入っていると普通にロードを、なければダウンロードします。今回はdplyrとtidyverseを使います。libsにほしいパッケージを指定すれば、なにもこの二つに限らず実行できます。

libs <- c("dplyr","haven","readxl")

requireLibs <- function(libs) {

  for(lib in libs){

    if(!require(lib, character.only = T)){

      install.packages(lib)

      require(lib, character.only = T)

    }

  }

}

requireLibs(libs)

まずはexcelを読み込みます。Rはいろんなパッケージがあって、同じ処理でもいろんな関数があって困るのですが、私はread_xlsxを使っています。よく似た名前のread.xlsxもあるので難しいですね。read_xlsxは標準だと変数の型推定が guess_max = min(1000, n_max),となっているので1000行まで判定してくれます。データが多そうなら適宜この引数を変えましょう。 何もしなくても1000まで判定してくれるのえらいですね。

inpath <- "piyopiyo/book1.xlsx"

infile <- read_xlsx(path = inpath, sheet = "Sheet1")

いざ中身を見ると以下の内容です。全部数値のAは数字に、文字を含んでいるBとCは文字値になっていて、Cの11行目の長い文字列も切れずに読み込めています。データはtibble型になっていますがデータフレームです。tidyverseではこっちをよく使っているようですが。

head(infile, 11)

# A tibble: 11 × 3

       a b     c         

   <dbl> <chr> <chr>     

 1     1 1     xxxx      

 2     2 2     2         

 3     3 3     3         

 4     4 4     4         

 5     5 5     5         

 6     6 6     6         

 7     7 7     7         

 8     8 8     8         

 9     9 9     9         

10    10 10    10        

11    11 xxxx  xxxxxxxxxx

このinfileをそのままxptにしてもよいのですが、すべての変数を文字値として扱いたい時を想定して一処理追加します。

chr2_infile <- infile %>% mutate_if(. , .predicate=is.numeric, .funs= ~as.character(.) )

上記でinfileの数字変数、今回でいうところのAを文字に変換して、chr2_infileという名前に変えています。もう少し解説すると、mutate関数でデータフレームの列単位で一括処理ができますが、今回はmutate_ifにすることで、数字変数に対して一括で文字値化する処理をしています。.predicateに処理をかける変数の条件を、.funsに実際に行う処理を指定します。変数単位で指定して処理を書くのはSASではあまりない考えで好きです。まだ私もピンと来ていないところが多々ありますが…
作ったchr2_infileの中身を見てみると、Aが無事chrに変換されているのがわかります。
 head(chr2_infile, 11)
# A tibble: 11 × 3
   a     b     c         
   <chr> <chr> <chr>     
 1 1     1     xxxx      
 2 2     2     2         
 3 3     3     3         
 4 4     4     4         
 5 5     5     5         
 6 6     6     6         
 7 7     7     7         
 8 8     8     8         
 9 9     9     9         
10 10    10    10        
11 11    xxxx  xxxxxxxxxx

最後に作ったchr2_infileをxptファイルに変換します。version引数は標準だと8なのですがなぜか8だと作ったxptをsasで読み込めません。sas側がerror出します。仕方がないのでversionは5とします。この引数は5か8しか指定できません。
お察しの通りxpt version 5で作成しますので、変数名が長すぎたりするとうまく作れないと思います。試してませんけど…

write_xpt(chr2_infile, "piyopiyo/book1.xpt", version = 5 )

上記を実行すると第二引数に指定したパスとファイル名でxptファイルが出力されますので、sasから回収に向かいましょう。これは私のいつもの書き方なのですが、Libnameステートメントにxptファイルを指定して、それをworkにコピーしています。コピーしたら元のlibnameは要らないのでクリアします。

libname INXPT xport "piyopiyo\book1.xpt" ;

proc copy in=INXPT out = work ; 
run ;

libname INXPT clear ;

workにコピーしたbook1データセットを見に行くと、無事以下のように格納されています。文字長に余裕とかは無いので、Aはlengthが2です。この辺はRから作ったxptファイルなのでしょうがないですね。そもそも固定長じゃないしね。


ブログの途中であったxptのversionが5じゃないとSAS側がerror出すというのは結構今一ポイントです。一応githubにxptをproc copeyではなくて自動マクロの%xpt2locでworkに移せばversion 8のxptでも読み込めるよとあるのですが、実際にやってみると実行時にerrorは出ませんが0obsのxptとして読み込まれてしまうので、書き出し時のxptのverisonは5を指定する必要があります。…今後の修正と更新に期待です。xptのversion5って変数名8文字とかのとんでもなく厳しい縛りがあるのであまり使いたくないんですよね…

それはそうとてBASE SASしかないとEXCELファイルを読み込むのにずいぶん難儀すると思いますが、Rを使ったこの方法で何とかBASE SASしかない環境でもEXCELが読み込めるようになるとよいのですが。xptのversionだけはどうにかならんのか。今時5て。自分の業務には1mmも関係ないですけど…
ちょっと記事内にとっ散らかったので最後にR側のコードをまとめておきます。

#パッケージインポート
libs <- c("dplyr","haven","readxl")
requireLibs <- function(libs) {
  for(lib in libs){
    if(!require(lib, character.only = T)){
      install.packages(lib)
      require(lib, character.only = T)
    }
  }
}
requireLibs(libs)

#excelをRに読み込み
inpath <- "piyopiyo/book1.xlsx"
infile <- read_xlsx(path = inpath, sheet = "Sheet1")

#数字変数を文字変数に変換
chr2_infile <- infile %>% mutate_if(. , .predicate=is.numeric, .funs= ~as.character(.) )

#xptに変換
write_xpt(chr2_infile, "piyopiyo/book1.xpt", version = 5 )




2024年2月1日木曜日

SASでexcelを読み込むときのdbmsはまずxlsxを使う

 何度目かもわかりませんが、excelをsasに読み込む話をまたします。表題の通りSASでexcelファイルを読み込むときにはまずdbms=XLSXを指定するのが私のやり方です。

dbmsにはやれexcelだxlsxだexcelcsだといろいろ指定できるのが厄介で、それぞれどう違うんだという話への回答がsasの日本語フォーラムにあります。検索してもこの回答にたどり着くまでが遠すぎるのでこのブログでも紹介します。マジで見てくれ。お願いだ。これで今回の記事はおしまいですが、それだと寂しいのでもうちょっと追記します。

リンク先の記事では以下の画像のように回答されていて、excelcsはsasとexcelのbit数が違う場合に、excelはbit数が同じ場合に、xlsxはbitの違いやexcelの有無にかかわらず使用できます。とあります。excelは途中まで数字、途中から文字値の列を読み込んだら文字値の部分を欠測で認識するとんでもない欠陥があるのでそもそも使い物にならないこともあって、bit数にかかわらず使用できるうえにまだ文字値と数字をちゃんと判別するxlsxをまず試してみましょう…という話です。この辺のことは前に記事にしています。


これでexcelは無事sasで読み込めました、めでたしめでたしとはなりません。BASE SASのpric importはdbmsにxlsxと指定できないのです。なんとCSV,DLM,JMP,TABの4つにしか対応していない。こうなると泣く泣くCSVで読み込むしかないのが現状です…




2024年1月1日月曜日

[R] admiralで使われているpipe演算子の紹介

せっかく各所でadmiralの話を聞いたので時流に乗っていくとします。また直近の仕事で使いもしないものに手を出すのか。よくないですね。どんな関数がadmiralにあるかを見る前に、pipe演算子について紹介します。これはpowershellとかではよく見かけるのですが、そういえばsasにはないですね…

rを触るのは実に久しぶりです。正直全く覚えていないので初学者と全く同じです。pythonよりは慣れている感がありますが…

紹介に際してパッケージをパソコンにインストールしないと話が始まらないので、まずはinstall.packages("admiral")でadmiralをパソコンにインストールしてください。2回目以降はパソコンの中にパッケージはもう存在するので、require("admiral")でokです。

pipe演算子とは左辺の出力を右辺の関数の第一引数に格納するものです。admiralではtidyverseで定義されている%>%が使われています。tidyverseのmagrittrライブラリですね。admiralをインストールしたら一緒についてくるのであまり意識することはありませんが。左辺を右辺の第一引数に格納すると書きましたが、それは何も指定しない場合です。どこに格納するかを指定すれば第一引数以外にも左辺の結果を格納することはできます。

例えばRには標準でirisというデータセットが入っていて、これの中身を確認するときにhead()を使います。

head(iris)
> head(iris)
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1          5.1         3.5          1.4         0.2  setosa
2          4.9         3.0          1.4         0.2  setosa
3          4.7         3.2          1.3         0.2  setosa
4          4.6         3.1          1.5         0.2  setosa
5          5.0         3.6          1.4         0.2  setosa
6          5.4         3.9          1.7         0.4  setosa
こうするとirisデータセットの最初から5obsまでがログに表示されます。irisデータセットの全部を表示したいときには、dim()でirisデータの行数を調べて、それをhead()の第二引数に格納すればokです。通常は以下のように書く必要があります。実行すると150行もデータがログにドバドバ出るので気を付けてください。お勧めしません

a <- dim(iris)
head(iris, a)

これと同じものをpipe演算子を使うと以下のように書けます。実行すると上記の2行を実行したときと同じ結果が得られます。さっきはdim()の結果を変数aに入れていましたが、今回は左辺に置いています。これを右辺に格納するためにパイプ演算子を使用します。標準だと右辺の第一引数に格納されると先ほど書きましたが、今回はhead()の第二引数に[.]を指定しています。この右辺の関数の引数の[.]に左辺の結果が格納されます。便利ですね。中間変数なりをあまり作らなくてもよいのは助かります。head()の第二引数は本来表示させたい行数を整数で記載するのですが、今回はここに左辺のdim()の結果を入れています。

dim(iris) %>% head(iris , .)

パイプ演算子は結構便利なのですが、慣れない間は左辺の結果を右辺のどこに格納しているかを見落としたり、そもそも何してるかがわからなかったりするので要注意です。あと勢いで何でもかんでもpipeで渡すと、ここに格納してる途中のやつは何?みたいになるので適度なマナーをもって使うのが良いと思っています。

昔私がrを触っていた時にはpipe演算子(どころかtidyverseそのものが)無かったので、ついにrにもパイプ演算子が入ったのかと驚いています。進歩ってすごいですね。最近だとrの組み込みでパイプ演算子があるらしいですが、tidyverseのパイプと微妙に挙動が違うらしいです。違いは正直わかりませんが、admiralでは%>%が使用されているのでtidyverseのものだけ紹介すればいいかということにしています。

記事カテゴリをその他にしましたが、流石に別カテゴリを作ったほうがいい気がしています。Rとかになるかな…admiralカテゴリはちょっと狭いか?などと…

2023年12月1日金曜日

各種クオーテーションマークを含む文字列を持つマクロ変数を展開する話

 仕様を私がきちんと理解しているわけではないですが、クオーテーションマークを含んだ文字列を持つマクロ変数を展開するにはsymget関数を使うと便利ですよ、との紹介です。確かに展開は出来ているのですがどうしてこうなっているのかはわかっていないのであまり深く掘り下げないでください。

事の起こりは会社でよくできる後輩君から掲題のことを聞かれました。私のところでは紆余曲折あってマクロ変数を無事展開できたのですが、どうも後輩君のところではうまく展開できずにいました。手元ではできるのになんでだろうな…と思っていると、実際の展開箇所でsymget関数を使って展開するのと、マクロ変数を""で挟んで展開しているという違いに行きつきました。私はsymget関数を使っていましたが、正直この関数はcall symputxで作ったマクロ変数を同じdata stepで展開できる関数くらいにしか思っておらず、その他のマクロ変数の展開ではマクロ変数を""で挟むのもsymget関数を使うのも同じ挙動を示すと思っていました。ところが違うんですねえ…どうやら。

仮に以下のようなシングル、ダブルクオーテーションを両方含んだ文字列を持つマクロ変数hogeを準備して、データステップで展開するとします。

%let hoge = I'm a "sas" user' ;

このマクロ変数をダブルクオーテーションで挟んで展開すると、案の定errorが出ます。今の場合ではerrorが出るのでデータステップが完了していません。

  data aa ;

      hage = "&hoge" ;

NOTE: マクロ変数"HOGE"によって生成されたライン

1      "I'm a "sas" user ;'

       -----------

       49      388

               202

NOTE 49-169: 引用符で囲まれた文字列の後の識別子の意味は、将来のSASリリースで変わる可能性があります。

             引用符で囲まれた文字列と識別子の間にスペースを挿入することをお勧めします。

ERROR 388-185: 算術演算子を指定してください。

ERROR 202-322: オプションまたはパラメータを認識できません。無視します。

2238  run ;

NOTE: 以下の箇所で文字値を数値に変換しました。(行:カラム)

      1:2    1:13

NOTE: エラーが発生したため、このステップの処理を中止しました。

WARNING: データセットWORK.AAは未完成です。このステップは、0オブザベーション、2変数で停止しました。

WARNING: このステップを中止したため、データセットWORK.AAを置き換えていません。

NOTE: DATA ステートメント処理(合計処理時間):

      処理時間           0.03 秒

      CPU時間            0.03 秒


ところがマクロ変数hogeの展開をsymget関数で行うとerrorが出ず実行されます。

  data aa ;

      piyo = symget("hoge") ;

  /*    hage = "&hoge" ;*/

  run ;

NOTE: データセットWORK.AAは1オブザベーション、1変数です。

NOTE: DATA ステートメント処理(合計処理時間):

      処理時間           0.03 秒

      CPU時間            0.01 秒


出来上がったデータセットAAを見ると、不思議と格納されています。これなんでなんでしょうね。私は意識せずsymgetをなんでか使っていたので特に意識することなくここまで来てしまったのですが…






2023年10月24日火曜日

複合パネルのグラフの作り方についての紹介

 先日のsasユーザー総会で散布図行列の作り方なる素晴らしい発表があってからというもの、あちこちで散布図行列の作り方や一ページに複数枚のグラフを貼った、複合パネルのグラフの作り方が紹介されている。これだけ色々なところで取り上げられているなら便乗しないともったいない。

例えばかの有名なブログではRWIで複合パネルのグラフを作る方法について紹介されている
https://sas-tumesas.blogspot.com/2023/10/blog-post.html

普通に考えればGTLでやればいいというのは全くその通りなのだが、sgplotで書いたグラフ画像をproc reportを使って一ページに複数枚配置する方法について紹介する。需要?多分無いよ

グラフについては各々sgplotで出力してもらうとして、今回は以下のようなpngを3*3で配置してみようと思う。どこにでもあるpngですね。PharmaSUG 2023 SDEは大阪で開催予定です。ぜひご参加下さい。

この画像を3*3にproc reportで並べたものをrtf出力すると以下のようなイメージに。もうちょっと縦に伸ばすとぴったりはいるが、どうせ脚注やら何かしらを入れるので余白のままにしている。そこまでの調整がめんどくさかったわけではないんです本当です信じてください。


以下が出力プログラム。データセットに出力したいpng名を指定した変数を作って、proc reportでそれぞれを呼び出して並べています。あくまでpingを並べているだけなので相関係数などの文字列だけ出したいときは、それだけの画像を良い感じに準備すればok。散布図行列ではないので複数グラフ共通のラベルなどは出せないのであしからず。

%let path = piyopiyo ;

data V_GRAPH ;

    L1 = "&path.\PharmaSUG.png" ; R1 = "&path.\PharmaSUG.png" ; output ;

    L1 = "&path.\PharmaSUG.png" ; R1 = "&path.\PharmaSUG.png" ; output ;

    L1 = "&path.\PharmaSUG.png" ; R1 = "&path.\PharmaSUG.png" ; output ;   

run ;


title1 ;

title2 ;


options orientation=portrait papersize=A4;

ods rtf file = "&path.\multi.rtf" nogtitle nogfootnote ;

      proc report data=V_GRAPH nowindows style(report)=[rules = none frame = void] ;

         column L1 R1 ;

        /*- style --*/

        define L1   / style={cellwidth=9.5 cm cellheight = 6.5 cm } "" ;

        define R1   / style={cellwidth=9.5 cm cellheight = 6.5 cm} "" ;

        compute L1 ;

            call define(_col_ , "style" , "style=[preimage='"||L1||"' just = center]") ;

            L1 = "" ;

        endcomp ;

        compute R1 ;

            call define(_col_ , "style" , "style=[preimage='"||R1||"' just = center]");

            R1 = "" ;

        endcomp ;

        footnote1 j=l "test" ;       

      run;

ods rtf close;