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
          ]
        ]
      }
    }
  }
}