2024年8月1日木曜日

最近使っているeditorをVSCODEに移行した話

 もともとプログラムを書くのにバキバキに改造した秀丸エディタを使っていたが、最近有識者の手助けもありVSCODEに移行した。基本的に便利。ちょっと文字コードの融通が利かないことがあるが多分私が上手に使えていないだけで良い方法があるんだと思う。VSCODEといえば拡張機能をはじめとした種々の昨日の豊富さが売りなので、私がよく使っている機能を紹介する。あまりにもキーボードショートカットが多すぎて全然把握しきれないし、とりあえずで登録しようとしたら大体何かと衝突すると警告される。

キーボードショートカット

  • ctrl+shift+[: 今カーソルが当たっている個所のインデントを折りたたむ
  • ctrl+shift+]: 今カーソルが当たっている個所のインデントを展開する
  • ctrl+shif+fでgrep検索 …ただし事前にVSCODEでフォルダを開いている必要がある
    • 検索結果から除外するファイル/選択するファイルを正規表現で指定できるので、とりあえず検索対象に*.sasを選択している。この辺の設定は前回の指定を結構覚えているので頻繁に指定しなくても良いので助かっている
    • grep検索した後alt+enterすると検索結果を新規editorを立ち上げて表示してくれる
    • grep検索の結果をサイドバーに出すときは標準で展開された状態だが、右上の端のアイコンを押すと全部折りたたんでくれる
      • 標準で折りたたんだ状態で出してくれる設定とかないんかな。多分あるんだろう
拡張機能
  • SASの公式 
    • これがあるとプロシジャなどの入力補完からハイライトまでしてくれるので必須。設定を頑張ればVSCODE上でSASを動かせるらしいがそこまでは試していない
  • VSCODE日本語化
  • alignment
    • 指定した範囲を事前に指定したキー文字列で縦にそろえてくれる拡張
    • 設定のjsonファイルを開けて、カンマをキー文字列に追加して、要らないキー文字列を削除した後、F1のコマンドパレットからalign all charsするかalt+;で実行
    • setting jsonは左下の歯車→設定→拡張機能→alignment configurationからsetting jsonファイルを開けて中のalignment.charを修正すればキーの指定が可能。 私は; , then,=を指定している

例えば以下のようなカンマ区切りの文字列を、カンマの位置を揃えるように縦に整形する
a,b,c
12,34,56
a  ,b  ,c
12,34,56

その他
SASの関数や引数の補完はSASの公式拡張がやってくれるので、ユーザースニペットを現在の日付や名前などの定型文挿入に使っている。挿入するユーザースニペットは設定のSAS.jsonに指定しているので、SASファイル以外では余計なスニペットを起動しないのが非常に便利。
例えば以下のように、todayと入力すると現在の年月日がハイフン区切りで入力されるものを設定している
  "today":{
        "prefix": "today",
        "body": ["$CURRENT_YEAR-$CURRENT_MONTH-$CURRENT_DATE"] ,
        "description": "today",
    }

ctrl+@で画面下部でコンソールを起動して、そこでpowershellを操作できる。SAS書いているときに使うことはあまりないのだが、データを見ながら手元のeditorで簡単な計算ができるのは結構便利。例えば詳細な指定はできないが2つの日付の間の日数を手元で出すことができるのも結構助かっている。以下のようにすれば前の日付から後ろの日付までの期間を出してくれる。もちろん四則演算も可能。
new-timespan "2022/03/13" "2023/04/14" | format-table -property days 
この下部コンソールのpowershellはVSCODEで開けているフォルダを現在のディレクトリと認識してくれるので、簡単なものなら適当に別フォルダでも使いまわせてしまうのもgood

行全体ではなく矩形選択はctrl+shift+alt+矢印で指定する。これは押すキーが多くてちょっと押しにくく不便。4キー押しはさすがに無理なのでキーバインドを変更したいが良い感じのものが大体既存の設定と競合するので変えれていない。

選択箇所を全て大文字に変換するのは標準だとどのコマンドか忘れてしまった。私はalt+uに割り当てを早々に変えてしまった。

以上が何となく私が使っている設定の一部だ。あまりにも豊富すぎてまだ把握できていない。これからもVSCODEを利用して仲良くなれるようしていきたいと思うが、今まで使っていた秀丸エディタも便利なので場合によって使い分けていきたい。あまりにも融通の利く秀丸マクロの完全な移行が出来ていないので…多分完全に置き換えるのは無理だろうなあ…


2024年7月1日月曜日

sasで変数の和を計算する話

 同じobsの二個以上の変数の和を求めるときは、sum関数を使うか変数を+で足すかのどちらかです。どちらも似たような処理ですが、書き方で微妙に欠損の取り扱いが違うので注意が必要です。足す変数に欠測が含まれるときはsum関数は欠測を飛ばして足し算して、+で足す時は足し算そのものが行われません。

3157  data _null_ ;
3158  a = 2 ; b = . ; c = a+b ;  putlog c= ; output ;
3159  a = 2 ; b = . ; c = sum(a,b) ; putlog c= ; output ;
3160
3161  run ;

c=.
c=2
NOTE: 欠損値を含んだ計算により、以下の箇所で欠損値が生成されました。
      (回数)(行:カラム)
      1 3158:22
NOTE: DATAステートメント処理(合計処理時間):
      処理時間           0.00 秒
      CPU時間            0.00 秒

出てるlogは読もうね。欠損の処理がめんどくさいしNoteだからと欠損値を含んだ計算…をlogに残したままにするとこの手の結果が変わるやつを見逃すから、この手のはできればlogに残さないようにしてね。
あと誰かのを引き継いだ時に欠損値を含んだ計算…のlogが残っていると何がどうなってるのかの確認を全部しないといけないからね。出来ればlogに残さないようにしてね(憤怒)

2024年6月1日土曜日

excel日付とsas日付の違いの話

excel日付とSAS日付はどちらも日付を数値にしたものですが、そういえばこの二つは開始日が違ったな…ということがありましたので、忘れないように記事にします。こんな事めったにないとは思いますが。

この上記のexcelは、1行目に変数ラベル、2行目の変数名、3行目に日付(excel日付)が格納されています。これをsasにimportすると以下のようになります。仮なので読み込む際にオプションのgetnamesをyesにしたとしています。

データとしてはexcel日付の列ですが、変数名がデータの値として格納されているので文字列として扱われた結果2024/5/2がexcel日付のまま45414として文字列になっています。ところで2024/5/2のsas日付は23498なのでこのまま日付を型変換すると2024/5/2になりません。45414は2084/5/3です。

11   data _null_ ;
12       a = '02may2024d'd ;
13       putlog a= ;
14   run ;

a=23498

excel日付は1が1900-1-1なのに対し、SAS日付は1が1960-1-2なので60年と1日の差があります。ちなみにSAS日付上での1960-1-1は0です。excel日付は1始まりですがSAS日付は0始まりということもこの時初めて知りました。

なのでexcel日付からSAS日付に変換する際には1900-1-1と1960-1-2の差分の(21915 + 1)をexcel日付側から引けば日付が一致します。以下の実行ログを見ると、引き算した後のEXDTには正しく2024-5-2が格納されていて、excelから読み込んだものをそのままformat当てたORGDTは2084-5-3となっています。

130
131  data _null_ ;
132      set sh1 ;
133
134      format EXDT  yymmdd10. ;
135      format ORGDT yymmdd10. ;
136
137      if _n_ = 2 then do ;
138          ORGDT = label ;
139          EXDT = label - (21915 + 1) ;
140
141          putlog ORGDT= EXDT= ;
142      end ;
143
144  run ;

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

      138:17   139:16

ORGDT=2084-05-03 EXDT=2024-05-02

NOTE: データセットWORK.SH1から2オブザベーションを読み込みました。

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

      処理時間           0.00 秒

      CPU時間            0.00 秒

普段こんなことはほとんど起こらないのですが、読み込むexcelファイルの日付がexcel日付で入力されている+その列にexcel日付以外の文字列が存在することで今回のようなことが起こります。例えば今回のような1行目が変数ラベルで2行目が変数名になっていたりとか、SASで取り込む際には不要ですけどファイルの作成者名が上の方に入ってたりとか、で紛れ込んできます。読み込むexcelをきれいに加工してからSASにimportすれば良いのですが、あまりexcelに変に手を加えるのも憚られる時があるし…で難しいところです。

2024年5月1日水曜日

階層構造をもったjsonをSASに読み込む話

 少し前(2019年は"少し"前です)のSASユーザー会で、jsonに対応したLibnameステートメントを使って単階層のJSONをSASに読み込む内容の発表がありました。…そういえば複数階層を持つjsonをSASで読み込むとどうなるのかって気になりますよね。私はなりました。時間が空いていますがちょっと調べたので記事にします。件のSASユーザー会の資料は何となく怖いのでリンクしません。「JSONという奇妙な拡張子とSAS」で調べてみてください。HITすると良いんですが。

用意したのは以下の構造のjsonです。このブログは容赦なくベタ張りするのでスクロールが大変ですね。すみません。改善する気は今のところありません。例えばid:1は子階層childrenを持っています。

{

  "id": "1",
  "name": "root",
  "children": [
    {
      "id": "2",
      "name": "child1",
      "Detail": [
        {
          "id": "3",
          "name": "grandchild1",
          "items": [{"itemID": "1A", "itemprice": 280}]
        },
        {
          "id": "4",
          "name": "grandchild2",
          "items": []
        }
      ]
    },
    {
      "id": "5",
      "name": "child2",
      "Detail": [
        {
          "id": "6",
          "name": "grandchild3",
          "items": [{"itemID": "1B", "itemprice": 500}]
        }
      ]
    }
  ]
}

これをSAS以下のプログラムでSASに読み込みます。encodingをutf-8にしているのは何となくです。このデータはシングルバイトのみなので違いはありません。

filename TG "piyopiyo\Hierarchy.json" encoding="utf-8" ;

libname IN json fileref = TG ;

proc copy in = IN out = WORK ; run ;

libname IN clear ;

読み込むと以下の通りAllData、Children、Children_detail、Detal_items、rootのデータセットになります。


それぞれが各階層のデータに対応していますが、階層間のつながりはAllDataを見ればわかるようになっています。各階層が縦に積まれているので、実際には転置したほうが使いやすいかもしれません。

以下の画像はAlldataデータセットの内容で、例えば1階層目にはidが1、NameがRoot、子階層としてChildrenを持っていて、2階層目のChildrenはidが2、nameがchild1で子階層(1階層目から見たら孫階層)としてDetailを持っている、ことなどがわかります。

Libnameでとりあえず読み込んだだけにしては階層の情報も含めていい感じに取得できていると思います。SASが自動で作成するmapファイルもなかなかのもんじゃないですか。



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 )