2026年5月4日月曜日

複数の文字変数の非欠損の数を数える話

SASで複数の変数の欠損の数を数えるにはcmiss関数があります。cmiss関数は便利な関数で、1obsの中で文字だろうが数字だろうが指定した変数の中の欠損の数を数えてくれます。非欠損の変数の数を数える際は、数字変数ならn関数というこれまた便利な関数があります。

一方文字変数の中で非欠損の数を数えたいときはいつも難儀していました。良い感じの関数が多分sasにはないです。あったらすみません。しょうがないので配列で数えたいうのが今回の記事です。文字変数と銘打っていますが欠測かどうかをmissing関数で調べているので、多分文字でも数字でも非欠損の数を出せると思います。文字でしか試してないのでerrorになるかもしれませんが。

対象の変数を指定している配列の部分をマクロ変数化すればいい感じのマクロになるんじゃないでしょうか?可変なのがその部分だけ、強いて言えばそこと結果を格納する変数名の部分だけなので。



data hoge ;
    a = "a" ; b = "" ; c = "" ; output ;
    a = "a" ; b = "b" ; c = "" ; output ;
run ;

data _null_ ;
    set hoge ;

    res = 0 ;
    array vars  a b c;
    do over vars ;
        if missing(vars) = 0 then res = res + 1;
    end;

    putlog res = ;

run;

/*>>>>>---------- 以下実行log ----------<<<<<*/

3956
3957  data _null_ ;
3958      set hoge ;
3959
3960      res = 0 ;
3961      array vars  a b c;
3962      do over vars ;
3963          if missing(vars) = 0 then res = res + 1;
3964      end;
3965
3966      putlog res = ;
3967
3968  run;

res=1
res=2

2026年3月1日日曜日

フォルダに格納したsasファイルを全部読み込む話

 先日紹介したsas公式のgithubの中に、指定したフォルダ内の全てのsasプログラムを読み込むマクロが含まれている。ある程度指定した順番に読み込んだりと高機能なものだ。

その辺の微調整が出来ないもっと単純な読み込みプログラムを私も持っているので、せっかくということで記事にする。以下の1行で指定したフォルダ、今回はhoge直下の.sasファイルを全部読み込んでいる。読み込む順番が調整できなかったりなどできないことが多いが、まあ気を付けていれば私には今の所コレで十分ではある

%include "%nrbquote(hoge\*.sas)";

2026年2月1日日曜日

0件のデータセット作るときに初期化が云々のメッセージって出ないように出来ませんでしたっけ?

変数ラベルや長さなどの変数情報だけを確定させるために0obsのデータセットを作ることがあるのですが、昔はstopステートメントを仕込めば初期化されていませんのメッセージを出さないように出来……ましたよね?0件のデータセット作るときのログに初期化されていませんのメッセージ出るとちょっと嫌だったので……

どうもちょっと前からstopを仕込んでもログに初期化されて~のメッセージが出てる気がするんですよね…そもそも昔からこのメッセージは出ていたのか?手癖で処理していたのでちゃんと覚えてない自分が恨めしい

以下の様に変数aの定義だけ持たせた0件のデータを作成すると、値が無いのでメッセージが出ます

17   data piyo ;

18   attrib  a length = $10 ;

19   run ;

NOTE: 変数aは初期化されていません。  <- これ

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

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

      処理時間           0.03 秒

      CPU時間            0.01 秒


ここに以下の様にデータステップ内にstopステートメントを仕込むと、このメッセージを出なく出来ていた……ような……あいまいな記憶の記事ですみません。気のせいですかね

21   data piyo ;

22   attrib  a length = $10 ;

23   stop ;

24   run ;

NOTE: 変数aは初期化されていません。 <- これが昔は出なかったような…

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

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

      処理時間           0.06 秒

      CPU時間            0.01 秒


2026年1月19日月曜日

データセットがキーとなる変数で重複が無いかを確認する

 キーとなる変数でデータセットが一意に定まるかを確認する時はproc sortを使っています。nouniquekeyオプションを使えば、重複してるレコードをだけをデータセットとして作れるので便利ですね。

仮にデータセットhogeが以下の様にあったとして、変数aの値が1obsと2obsで1で重複しています

後は以下の通り重複するobsをKEY_NO_UNIQUEデータセットに格納して、このデータセットにレコードがあればログにerrorを出す、とすれば重複があるかどうかはログを見るだけでokです。詳しい重複の内容はデータセットを見てください。

proc sort data = hoge out = KEY_NO_UNIQUE nouniquekey  ;

  by a  ;

run ;

data _null_ ;

    if 0 then set KEY_NO_UNIQUE nobs = _nobs ;

    if _nobs ^= 0 then putlog "E" "RROR NoUnique: Check KEY_NO_UNIQUE dataset" ;

    stop ;

run ;


2025年12月22日月曜日

sasの公式がgithubつくってるやん

 2025年のsasユーザー会には都合が合わなかったので参加していなかったのだが、資料を振り返ると面白そうなのがあった。SAS公式がお出ししたSASのパッケージだ。最近何かと流行っているSASPACの仕組みを利用したパッケージ群にSASが公式が乗り出す形になっている。SASToolboxとか言う公式にふさわしい名前だ。これについての発表資料はこちら

データセットのutf8変換、フォルダ内のファイルの一括sas7bdat化、フォルダ内のsasファイルを一括インクルードとどれも使う場面の多いマクロが公開されている。一括インクルードは私も自作のがあるが、読み込み順をある程度引数で調整できるようになっているのは画期的だ。私にはここまで実装するのは難しかったのでプログラムファイル名の工夫でこの順番を制御していた。先頭に数字付けたりとか……

こうやってみんなが公開されてる同じパッケージの関数を使いだすと、業界的に必須のダブルプログラムが形ばかりになるよなとは思っている。正副で同じ関数使うと必然的に同じ結果が出てくるので何の検証にもならない。まあこの問題は突き詰めると最近流行りのOSSでも同じことが言えるので、その内偉い人がこうしよう的な話を出してくるだろう。下請けの私は右向け言われたら右向きます。

みんなで色んなものを公開して修正していくのは嫌いではない。メンテナ誰がやるねんとか、いつまで更新されてるんや!?とかプルリクエスト対応ってもしかしてタダ働きですか!?とか付随する様々な問題は既に見えているが、今はこの甘美なバタバタに身を任せたい。

それは別にして会社のパソコンからgithubにアクセスしてアップロード/ダウンロードするの怖いよね。セキュリティが。これは情報セキュリティ的に問題ない通信です!ってどうやって説明しているのだろう。こんな古臭いことを考えてるのは私だけか?

2025年8月30日土曜日

複数のエクセルファイルを結合し、指定したシートの順番に並び替えるマクロ

 複数のエクセルを結合して一つのブックにまとめる際に、シートの順番もこちらで指定したいときがあるとします。ありますよね?あるとしましょう。例えば100でも200でも良いのですが結果の図表をエクセルに出す必要があるとき、二人で各50個作って最後にまとめる時…とかです。既にあるエクセルファイルに後からシートを指定した位置に追加する時もそうです。ブックの結合もめんどくさいですし、シート順の調整なんて手でやっていたら夜どころか朝になってしまいます。

以下に記載のVBAでそれなりに結合してシート順も指定できて便利です、との自作VBA自慢の記事です。この手の記事はLLMの登場以降ニーズが少ない気もしますが…LLMに聞けば出してくれますしね。そんなことは知らん。自慢すると言っておろうが。

1シート目に実行ボタンを付けて、結合前のファイルを格納したフォルダと結合後のファイルを出力するフォルダをそれぞれhogeとpiyoに指定します。sheetSortOrderシートには結合後のシートの順番を記載しています。後は画像下にあるvbaのコードを張り付けて実行すれば結合が出来るというわけです。


罫線を入れているA列に結合後のシート順をしてください。


複数ファイルを指定すると結合+シート順の並び替えになりますが、結合前フォルダに1つのエクセルファイルだけにするとシート順の並び替えだけになります。これだけでもとても便利。以下がVBAのソースコードになります。

Sub combineMacro()

    Dim folderPath As String

    Dim filename As String

    Dim targetBook As Workbook

    Dim afterBook As Workbook

    Dim targetSheet As Worksheet

    Dim sheetSordOrder As Range

    Dim sheetName As String

    Dim sortFlag As Boolean

    Dim runTime As String

    Dim savePath As String

        

    ' 結合するExcelブックが保存されているフォルダのパスを指定

    folderPath = ThisWorkbook.Sheets("Sheet1").Range("D12").Value & "\"

    

    ' 新しいブックを作成

    Set afterBook = Workbooks.Add

    

    ' 結合マクロが保存されているブックのSheetSortOrderに記載されているシート順を取得

    Set sheetSordOrder = ThisWorkbook.Sheets("SheetSortOrder").Range("A1").CurrentRegion

    

    ' フォルダ内のExcelブックを結合

    filename = Dir(folderPath & "*.xlsx")

    Do While filename <> ""

        Set targetBook = Workbooks.Open(folderPath & filename)

        

        ' シートを結合ブックに追加

        For Each targetSheet In targetBook.Sheets

            targetSheet.Copy After:=afterBook.Sheets(afterBook.Sheets.Count)

        Next targetSheet

        

        targetBook.Close False

        filename = Dir

    Loop

    

    ' シートを並び替える

    For Each targetSheet In afterBook.Sheets

        sheetName = targetSheet.Name

        sortFlag = False

        

        ' シート名がシート順に含まれているかチェック

        For Each V_CELL In sheetSordOrder

            If V_CELL.Value = sheetName Then

                targetSheet.Move Before:=afterBook.Sheets(V_CELL.Row)

                sortFlag = True

                Exit For

            End If

        Next V_CELL

        

        ' シート名がシート順に含まれていない場合は削除

        If Not sortFlag Then

            Sheets(sheetName).Delete

        End If

    Next targetSheet

    

    ' 結合ブックを保存

    ' 実行日時を取得

    runTime = Format(Now, "yyyymmdd_hhmmss")

    

    ' 保存パスを取得

    savePath = ThisWorkbook.Sheets("Sheet1").Range("D14").Value & "\"

    afterBook.SaveAs savePath & "結合後" & runTime & ".xlsx"

    ' ブックを閉じる

    afterBook.Close   

    ' メッセージを表示

    MsgBox "結合が完了しました。"

End Sub



2025年7月1日火曜日

OSSのライセンスについての話

 最近はRやpythonなどのOSSの利用が進んでいる。COREなんかはMITライセンスなのであまり細かいことは気にしなくても良いが、利用するパッケージによってはAGPL>3.0やapach>2.0等のライセンスが設定されている。apach>2.0はそのライセンスのOSS利用した場合は特許や許諾の表示を入れなければならないし*1、AGPL>3.0は成果物が公表された場合は利用したソースコードを公開しなければならない*2

…等と定められているが、例えばAGPL>3.0は2.Basic Permissionsに「The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work.」…実行された対象からの出力が、内容から判断してその対象物に該当する限りライセンスの対象にする…とある。製薬業界ではライセンス対象であるソースコード…往々にしてこれは当局への説明資料…が、最終成果物である医薬品には直接使用されていないので、ライセンス対象にはならないのではないか。もちろんこれは1ペーペーのおっさんが垂れ流している駄文なので正しいかはわからない。法務に詳しい方が見れば何を当たり前のことを…?/何を的外れなことを…?となるかもしれないが、現時点で何となく私の考えは上述の通り。

これまでSASという大きな傘の下にいたがOSSという大海原に漕ぎ出すにあたって、ライセンスなどの基本的な事項について一度立ち止まって考える機会はこれからも大切にしていきたい。


*1

https://licenses.opensource.jp/Apache-2.0/Apache-2.0.html

4.再頒布より

*2

https://www.gnu.org/licenses/agpl-3.0.html

2.Basic Permissionsより

2025年6月4日水曜日

Rで二つの日付の間の期間の日数を求める

 二つの期間の間の日数を求めたい時ありますよね?ADaM的にはADYの変数で定義されているようなやつです。せっかくなので流行りの{admiral}のお試しがてら期間の変数を算出します。別に{admiral}である必要はないのですが、せっかくよさげなパッケージがあるなら使ってみるのがOSSの醍醐味でしょう。

なにはともあれまずはパッケージのインポートですね。なかったらinstall.packages()してください。諸々実行して最後のオブジェクトHOGEにxxdyの変数が作成されています。便利な関数だなあ…!(ダイマ)


library(admiral)
library(dplyr)
library(lubridate) # 日付を操作するパッケージ
library(tibble) # テストデータを作るのに使用(tribble関数)

# データ作成
datain <- tribble(
  ~TRTSDTM,              ~ASTDTM,               ~AENDT,
  "2014-01-17T23:59:59", "2014-01-18T13:09:09", "2014-01-20"
) %>%
  mutate(
    TRTSDTM = as_datetime(TRTSDTM),
    ASTDTM = as_datetime(ASTDTM),
    AENDT = ymd(AENDT)
  )

head(datain)

'''
> head(datain)
# A tibble: 1 × 3
  TRTSDTM             ASTDTM              AENDT
  <dttm>              <dttm>              <date>
1 2014-01-17 23:59:59 2014-01-18 13:09:09 2014-01-20
'''

# 相対日を追加
# source_varsに期間の後端を示す変数を格納する。
# HOGEDY = TRTSDTMのようにすると、出力する期間の変数名を指定できる
# 変数名のみ指定すると期間の変数名はxxDYの形に指定される
# (個人的にはプログラム中に出力変数名が出てこないのは嫌なので変数名は指定したい)
hoge <- derive_vars_dy(
  datain,
  reference_date = TRTSDTM,
  source_vars = exprs(HOGEDY = TRTSDTM, ASTDTM, AENDT)
)

head(hoge)
'''
> head(hoge)
# A tibble: 1 × 6
  TRTSDTM             ASTDTM              AENDT      HOGEDY ASTDY AENDY
  <dttm>              <dttm>              <date>      <dbl> <dbl> <dbl>
1 2014-01-17 23:59:59 2014-01-18 13:09:09 2014-01-20      1     2     4
'''

2025年5月1日木曜日

指定したフォルダに格納されているファイルの、ファイル名と更新日時を取得する

vbscriptが廃止になると聞いて…(略。powershell5で指定したフォルダの直下にあるファイルのファイル名と最終更新日を取得するものを作成した。powershellなので実行には癖があるため実行用exeを作っても良い。こいつまたpowershellで変なもの作ってるな。

変数_ipathに対象のフォルダを指定して実行すればok。powershell本体のps1ファイルがある場所に結果となるoutput.txtが保存され、そのtxtを自動で開く形にしている。ps1ファイルの場所を自動で取得する$PSScriptRoot変数は結構便利。ちなみにソース中の#はコメントだ。

今回のは指定フォルダ内のサブフォルダは取得対象外にしているが、サブフォルダも含めたい場合はget-childitemに-recurseオプションを付ければよい。また最終更新日はこのままだと時分まで取得するので、日付までで良い場合はToStringの箇所をyyyy/mm/ddとする。

オサレポイントは出力時に`tと、ファイル名と日時をタブ区切りにしているところか。丸ごとエクセルにコピペするとファイル名セルと日時セルに分かれるのでちょっと便利。そのほかにもGet-childitemはいろいろ取得できるので必要に応じて修正できる。

最後は取得したGet-childitemをパイプでout-fileで外部ファイルのoutput.txtに出力している。パイプは便利だが結構何が送られているのかを見逃しがちになるので使いどころには気を付けたい。と言いつつもGet-childitemで取得したフォルダの中身をパイプでForEach-Objectに送って全件ループ、みたいな書き方が結構便利。わざわざforでループ用変数作って…みたいなことをしなくて良いのが助かる。


以下ソースコード

$_ipath = ""

$_opath = $PSScriptRoot

Write-Host "$_ipath 直下のファイルの更新日時を取得します"


# ファイルの更新日時を取得してテキストの書き込み

# サブフォルダの中身を含めて取得するにはget-childitemに-recurseを付ける

# 更新日時が日付までで良い場合はToString以下をyyyy/mm/ddにする


Get-childitem -Path $_ipath -File | ForEach-Object {

    $format_date = $_.LastWriteTime.ToString("yyyy/MM/dd HH:mm")

    "$($_.Name)`t$format_date" 

} | Out-File -filepath $_opath\output.txt


#結果ファイルを開く

invoke-item $_opath\output.txt


Write-Host "end"

2025年4月1日火曜日

複数の変数に同じ処理をかける時は配列が使えるの話

 例えばA、B、Cの3つの変数に同じ処理…今回はそれぞれに+1する…を実行する時、arrayが使えることもある。同じ処理をピコピコと繰り返し書くのが面倒な時は配列に記載してやればいっぱい似たようなものを書かなくて良い。具体例は以下の通り。

配列hogeを定義して、do overで全部にdo以下の処理を当てはめている。dimで要素数取って、とかをやっても良いのだが、今の場合のように全部に処理をすることが明らかな場合はわざわざdimで要素数を取らなくても良い。

data _null_ ;

    a = 1 ;

    b = 2 ;

    c = 3 ;

    array hoge a b c ;

    do over hoge ;

        hoge = hoge + 1 ;

    end ;

    putlog a b c ;

run ;

2025年3月1日土曜日

rtfの表に上付きの🄬を出力する

rtfの表内に上付き文字を出したいときはインラインフォーマットで^{super A}とすると上付き文字のAが出せますし、ギリシャ文字なんかを出すときは^{unicode 03B2}で出したりします。

環境依存文字の🄬も似たように^{unicode 24C7}でrtf上に出せますが、上付きの🄬…ユニコード指定の文字を上付き…で出すには工夫が要るのか…?という話です。特段の工夫とかは要らず、hoge^{super ^{unicode 24C7}}とやるとhogeの右肩に🄬を出すことができます。便利ですね。


インラインフォーマット指定前の^はods escapecharacterで指定しています。試してないのですが、defaultの(*esc*)だと重ねれない気がするので何かしらの文字をエスケープ文字として設定してください 

2025年2月7日金曜日

最新の更新日時のフォルダ/ファイルを取得する

指定したフォルダの中から最も新しいファイル(例えばxlsxなりcsv…) のファイル名を取得したい時があります。ファイル名が同じで中身だけ更新されるならプログラムを変えなくても良いですが、更新のたびにな前の末尾に日付が入ったりしてファイル名が更新される時があります。そんな時に毎度プログラムを更新するのはちょっとめんどくさいので、更新日時が一番最後のファイルの名前をマクロ変数に格納するプログラムの紹介です。

以下のデータステップで、if文に記載の拡張子が.xlsxとファイル名に_Specを持つファイルの中から、最も更新日が新しいファイルの名前をマクロ変数_spec_fileに格納しています。

%let _path = hogehoge ;

filename PPASS pipe "dir /od /b &_path ";

data _null_ ;

  length _inpass $200 ;

  infile PPASS DLM='09'X DSD MISSOVER LRECL=5000 ;

  input _inpass $ ;

  if ( index(_inpass,".xlsx")>0 and index(_inpass, "_Spec")>0 ) then call symputx("_SPEC_FILE",_inpass) ;

run ;

重要なのはfilename PPASS pipe "dir &_mspec_path /b /od";の部分です。コマンドプロンプトで使用するdirコマンドをsasで使用して、その結果をinfileでデータステップに回収しています。/odで更新日時順、/bでファイル名だけ取っています。データステップに更新日時順でファイル名を持ったレコードが発生するので、一番最後を取れば更新日時が一番最後、つまり一番最新のファイルが取得できる…というわけです。便利ですね。

更新日時が一番最後のフォルダを取るときはデータステップのif文で条件を変えても行けますが、余計なファイルをはじくようにdirのオプションに/adを指定しても良いです。/adでフォルダだけ取得できるので…

これで勝手に取得できますが、念のため何をマクロ変数に入れたかは都度確認が必要です。意図通りのものと違うものがマクロ変数化されているとややこしいので…

2025年1月9日木曜日

powershellで作るlog確認ツールの話

 そろそろvbsが非推奨になるとかなんとかがで、log確認ツールをpowershellで作った。もちろんpowershellである必要は無いのだが、powershellだとwindowsで標準なので特段の準備が不要なのはとても良い(powershell5.xに限る)…7だと標準じゃないので、それ使うならpythonなり使った方が融通が利いて便利そう…

ps1ファイルの実行には癖があるが、実行用batを作るのは一つの手ではないか。ps1ファイルと同じ場所に以下の実行用batを置いておくと、batをクリックして実行することができる。ps1のファイル名は適当なので適宜変更してほしい。
set _folder=%~dp0
powershell -ExecutionPolicy RemoteSigned -File %_folder%_logCheck.ps1

実際のプログラム本体は末尾に示すとしていくつか説明を。確認したいlogファイルを収めたフォルダに、本体のps1ファイルと検索したいワードを収めた"_logChrList.txt"の二つを格納する。_logChrList.txtの中身の例は以下の画像に示す。

powershell5ではtextファイルのエンコードを自動で正しく取れないので、実行時にファイルのエンコードをs/uで入力する。これはlogファイルごとに判定しているのではなく、フォルダ内の全てのlogのエンコードを一括で指定しているので、フォルダ内に複数のエンコードが混在していると文字化けしてしまう。

実行すると同じフォルダに_LogCheckResult.txtの名前でチェック結果が格納される。フォルダ内のすべてのlogファイル内に、_logChrList.txtで指定した文字列が何件含まれているかを出力する。この際ファイル毎/文字列毎に件数を出す。上記の画像の例だとerrorの件数とwarningの件数をそれぞれ数える。面倒だったので実行結果を固定名で出しているので、同じファイル名の実行結果があると問答無用で上書きされる。

繰り返すがチェックしたいlogが格納されているフォルダに、ps1ファイルの本体と、_logChrList.txtの二つを追加して実行、実行結果が同じフォルダに_LogCheckResult.txtが出力される。この時注意としては_logChrList.txtはutf8で作成し、_LogCheckResult.txtはutf8で出力される。

以下がps1ファイルの中身。


#エンコードを入力して指定

do {

    $input = Read-Host "logのエンコード方式を選択してください ('s' で shift-jis, 'u' で utf8)"

    switch ($input) {

        's' { $encode = 'default'; break }

        'u' { $encode = 'utf8'; break }

        default { Write-Host "無効な入力です。再度入力してください。" }

    }

} while ($input -ne 's' -and $input -ne 'u')


#本体の処理

$_inpath = $PSScriptRoot ;

$_list = Get-Content -Encoding utf8 "$_inpath\_logChrList.txt"


$_resFileName = "_LogCheckResult.txt"

New-Item -ItemType file -Path $_inpath\$_resFileName -Force 


Get-ChildItem -Path $_inpath -Filter *.log -Recurse | ForEach-Object {


    # logファイルの中身を取得 

    $_content = Get-Content -Encoding $encode $_.FullName 


    # ファイル名を出力

    Add-Content -Path $_inpath\$_resFileName -Value "$($_.FullName):" 


    $count = @{}

    foreach ($_msg in $_list) {

        

        $count[$_msg] = ($_content | Select-String -Pattern $_msg).count


        #該当箇所が0件でなかったら出力

        if ($($count[$_msg]) -ne 0) {

            Add-Content -Path $_inpath\$_resFileName -Value " $_msg : $($count[$_msg])" 

        }

    }


    # ファイルの区切りを判別するために空行を挿入

    Add-Content -Path $_inpath\$_resFileName -Value " "


}


#BOMアリのutf8をbom無しに変換する powershell5は標準出力がbomアリのutf8のため変換が必要

$_outfile = -join($_inpath,"\",$_resFileName)

$_chgnobom = Get-Content $_outfile

$Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding($False)

[System.IO.File]::WriteAllLines($_outfile, $_chgnobom, $Utf8NoBomEncoding)


write-host "end"

2024年12月1日日曜日

日付が欠測の日付をsas日付に変換する

 例えばjun/2023のように年月だけの日付をmmm/yyyyで表した文字値があるとして、これをsas日付に変換するにはmonyy8.formatが使えます。sas日付にさえできればjun/2023の文字を2023-06等表記を変えた文字値に変換することもできるので意外と使い道が多く助かっています。


data _null_ ;

   a = "24/jun/2023" ; b = put(input(a, date11.), yymmdd10.) ; putlog a= b= ; output ;

   a = "jun/2023"    ; b = put(input(a, monyy8.), yymmd7.)   ; putlog a= b= ; output ;

run ;

a=24/jun/2023 b=2023-06-24

a=jun/2023 b=2023-06


sas日付からyyyy-mm形式の文字値に変換する際は上記の通りyymmd.formatを使います。ややこしいのですがyymmw.formatとyymmxw.formatがそれぞれ別のものとしてあって、今回はyymmxw.formatのxの部分にdを指定してハイフン区切りの出力としています。yymms.とすると/区切りになります。

2024年11月1日金曜日

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

 先日sasの棒発表会に参加した。偉大な主催者の人徳により今回も盛況に終わった。午後の間に発表を21本聞くのは本当に疲れた…

一番興味深いのはSASPACに関する発表。SASは作ったマクロなどを公開するには、ブログなりにコードを記載するくらいしかなかったのが、SASPACを使えば統一されたformatで公開できるというわけだ。あまりにも便利そう。そんな場所に公開できるほどの立派な代物をひとつも持っていないことが大きな問題か…

私も発表…はしたのだが、勉強会公式で資料が公開されているし、身バレが怖いのでここでは資料の公開はしない。身バレコワイ。今年も無事開催になり、私に発表の機会を与えてくれた運営の皆様には感謝の限り。来年のネタないんだがどうしよう…

2024年10月14日月曜日

sasユーザー会2024に参加した話

 なんだか数年ぶりにユーザー会に参加した。今年の会場も東大だったがキャンパスが違うとのこと。まあ何となく人の流れに乗っていくと無事ついたので良かった。自分の発表が一日目の午前であることに気が付いたのが当日3日前とかだったので、諸々の申請が間に合わず頑張って朝7時前の新幹線に乗って会場に突入。前泊…したかったな…。現地ではまさかトラック3がトラック2より大きい会場とは思わず、随分会場付近をトラック3の場所を求めてノベルティでもらったsasエナジードリンクを片手にウロウロとしてしまったのはご愛嬌。多分かなり怪しい人物になっていたと思う。…sasエナジードリンク!?恐ろしいノベルティだ。エナジーが尽きてもsasを書けということか(個人の見解です)

ぶっちゃけ今年はあまり発表するつもりはなかったが、初参加の後輩が発表すると言っているのに見捨てるわけにはいかないと思って急遽発表資料をこしらえた。かつて私が初めて発表する際には偉い人が二人も登壇していたことに随分助けられたので、今度は自分の番がきたというわけだ。自己満の領域だが私には発表登録する十分な理由だった。急遽決めたのであらゆる申請がぎりぎりだったがよく間に合った。多大なご迷惑をおかけしたことでしょう…ありがとうございました…

今年の発表も面白そうなのが多かった。文字コード、dataset-json、グラフ軸目盛の自動調整、SASでゲーム…sasでゲームの発表はぜひ聞きたかったが2日目は諸般の事情により午前で引き上げざるを得なかった。後日発表資料を読むもまあ内容にピンと来ない。ある程度powershellは触ったことがあるので何となくわかるかなーと思いきや全然である。まだまだ自分のpowershell力が足りないな…これでは立派なpowershellおじさんを名乗れないではないか。名乗る必要があるのか?pythonでも書いてろ。

なんにせよ今年のユーザー会も無事終わって何より。終わってからしばらくは咳が止まらずとんでもない体調不良に見舞われたがそれも無事収まってよかった。息か咳かどっちしてるのかわからん位の状態になるのは本当に大変だった。久しぶりの遠出に肉体が付いていかなかったのだろう。あまりにも貧弱な体には困ったものだ。毎年恒例だが来年の発表のめどが何もついていないのでこちらもどうしたものやら。結局発表するにしても締め切りぎりぎりに登録することになるのだろう…


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ファイルもなかなかのもんじゃないですか。