【SAS】SPDEとTHREADによる並列処理

今まで、大規模なトランザクションデータについてはパススルークエリを投げてDB側で処理をしてもらってからSASに持ってくるようにしていたので、SASでの並列処理は考えなくても不便はありませんでした。
今回、諸事情によりSAS側で大きな(100GB程度)のトランザクションデータを処理しなければいけなくなり、実際にやってみると実行時間が気になってきたので、並列処理ができないか検討しました。
その結果、割と簡単なやり方で実行時間が2/3程度には圧縮できたのでメモとして残しておきます。

やったこととやらなかったこと

SPDEを利用したデータセットの分散とTHREADによるCPUコアに処理を分散させることをしました。
SPDサーバの導入や、DS2によるTHREADはやっていません。
非常に簡易な分散処理です。

THREADについて

THREADについては、デフォルトでオンになっている環境もあるかもしれません。
BASE SASのINDEXINGと、BASE SASの一部のプロシジャ(SORT、SUMMARY、MEANS、REPORT、TABULATE、SQL)、SAS/STATの一部のプロシジャについて、処理をCPUコアで分散してくれます。
設定方法は、オプションに以下の様に書くだけです。

OPTIONS THREADS CPUCOUNT=ACTUAL;

「CPUCOUNT」はCPUのコア数で数値で指定することもできますが、その場合でも普通は実際のCPUコア数を指定するみたいです。
実際のCPUコア数よりも多い数を指定すると、実行速度が遅くなる場合もあるみたいです。

SAS(R) 9.4 Language Reference: Concepts, Fifth Edition

SPDEについて

SPDサーバというのがデータセットを分散して格納するSASサーバらしいのですが、SPDE(Scalable Performance Data Engine)というのはその簡易版だと思えば良いらしいです。
SASがデータセットにどのようにアクセスするかはLIBNAMEを設定する際のエンジンに依存するのですが、そのエンジンを通常使っているV9からSPDEに変更することで、データセットが分散して格納されます。
データサイズが小さい場合は、SPDEの方がアクセス速度が遅くなる場合もあるらしいので、どのような場合にSPDEを使えば良いかは、下記を参照してください。

When Should You Use the SPDE Engine

SPDEを使用するにはLIBNAME宣言を以下の様にすれば良いです。

LIBNAME mywork SPDE "C:\folder";

そうすると、ログに以下の様に出ると思います。

NOTE: ライブラリ参照名myworkを次のように割り当てました。
エンジン:SPDE
物理名:C:\folder\

ちなみに、SPDEを付けないでLIBNAME宣言をすると、以下の様なログが出ます。

NOTE: ライブラリ参照名myworkを次のように割り当てました。
エンジン:V9
物理名:C:\folder\

SASの環境によりエンジンは異なるかもしれませんが、SPDEではないエンジンだと思います。ぱっと使った中で、いくつか気づいた点があります。

SPDEとV9の互換性

SPDEとV9は厳密には互換性が無いです。
V9エンジンで宣言したライブラリ内にデータセットを作成すると、sas7bdat形式のファイルが1データセットに1つ対応して作成されると思います。SPDEで宣言したライブラリ内にデータセットを作成すると、大量のspds9形式のファイルが作成されると思います。よく見るとmdf.0.0.0.spds9という拡張子のファイルがあるので、おそらくこれが分割されたファイル間の関係を記述しているもので、他の連番のファイルが分割されたデータだと思います。

EGとかからだと、どちらも一つのデータセットに見えるのですが、物理的な保存形式が違うため単純な読み書きの互換性もありません。例えば、「C:\folder\test.sas7bdat」というファイルがあったとして、「LIBNAME mywork SPDE "C:\folder";」とSPDEでライブラリを宣言すると、このデータセットは存在しないように見えます。つまり、同じようなデータセットに見えてしまうのですが、sas7bdatとspds9はSASを介して変換してあげる必要があります。

試していないので分からないのですが、sas7bdat形式のデータセットとspds9形式のデータセットのjoinは、spds9形式のデータセット同士のjoinよりも遅くなると思います。SAS EMでハイパフォーマンスノードと通常のノードに互換性が無いのは、裏側でのデータの持ち方に違いがあるせいですかね。

その他にも、オプションや関数で違いが出てくる場合もあります。FIRSTOBSは使えないみたいです。options dlcreatedirもダメでした。

Syntax for the SPD Engine

SPDEのWORK領域

おそらく、SPDEを使用するのであれば、全てのデータセットをSPDEで参照できる形にしておかないと、全体の効率は落ちると思います。そうすると、WORK領域もSPDEにしないといけないのですが、コンフィグの変更になってしまう気がして面倒くさいので、なにかないかと思って探していたらありました。LIBNAMEの宣言時にTEMPオプションをYESにするとWORK領域のように使えます。

LIBNAME mywork SPDE "C:\folder" TEMP=YES;

このように宣言すると、指定したフォルダ配下にSASセッション一つに付き一つのサブフォルダを作成して、そのSASセッションが終了するときにはサブフォルダを削除するということをしてくれます。SASセッションごとに固有の名前のサブフォルダが生成されるので、複数のSASセッションから同じフォルダをLIBNAME宣言しても、お互いが影響を与えることはありません。

【Ubuntu】VirtualBoxのUbuntuでLauncherが消えた場合

VirtualBox 1.5.2 + Ubuntu 16.04 LTSの環境で、Ubuntuをアップデートしたら左側のLauncherと上のタスクバーが消えた。Unityの設定を変えて再起動したり、いろいろ試してみたが、実はすごく単純なところで引っかかっていたので忘れないようにメモ。

まず、PCがUnityをサポートしているかチェックする。
Terminalを起動して以下を入力。

/usr/lib/nux/unity_support_test -p

このとき
Unity 3D supported:No
となっていたら、Virtual Boxの設定が問題の可能性

Virtual Boxの設定で
ディスプレイ -> スクリーン -> アクセサレーション
で、3Dアクセラレーションを有効化にチェックを入れる。

【SAS】ランダムな文字列を生成する

ランダムな変数を返す関数はRAND()ですが、ランダムな文字列を返す関数が見つからなかったので作ってみました。
うまいやり方があると思いますが、これで十分でした。まず、自作関数を定義します。

proc fcmp outlib=XXXX.fucntions.test;
  function getRandomStr(strlen) $;
    length str $200;
    abc = 'abcdefghijklmnopqrstuvwxyz';
    do i=1 to strlen;
      index = MOD(INT(RAND('UNIFORM')*10000), 26) + 1;
      str = trim(left(str)) || trim(left(substr(abc, index, 1)));
      put(str);
    end;
    return(str);
  endsub;
run;

使うときは、オプションで作成した関数を使用できるようにします。

options cmplib = XXXX.functions;

data work.test;
  x = getRandomStr(10);
run;

ちなみに、

str = trim(left(str)) || trim(left(substr(abc, index, 1)));

の部分を

str = cats(str, substr(abc, index, 1));

と書いても動きます。ご指摘ありがとうございました。

str= catx(str, substr(abc, index, 1))

と書いていたら空文字になって返ってきて、どはまりしてしまいました。
たぶん、catxの仕様なんでしょうが、よく分かりません。

【SAS】SAS日付と8桁数値の変換

SAS日付と8桁数値の変換方法はいくつかあるみたいなので、覚え書き。

SAS日付→8桁数値

日付関数を使う
YEAR(SAS_DATE)*10000 + MONTH(SAS_DATE)*100 + DAY(SAS_DATE)
文字値に一度変換する
INPUT(NLDATE(SAS_DATE, '%Y%m%d'), best8.)
文字値に一度変換する(マクロバージョン)

マクロで使う場合は、%をエスケープしなければいけないので

%SYSFUNC(INPUTN(%SYSFUNC(NLDATE(SAS_DATE, %NRSTR(%Y%m%d))), best8.))

8桁数値→SAS日付

MDY関数を使う
MDY(input(substr(put(DATE, best8.), 5, 2), best2.), input(substr(put(DATE, best8.), 7, 2), best2.), input(substr(put(DATE, best8.), 1, 4), best4.))
文字値に一度変換する
input(put(DATE, best8.), yymmdd8.)

【SAS】マクロ変数の更新されるタイミング

おそらく、SASマクロ初心者にありがちなミスだと思うのですが、マクロ変数の値が思ったタイミングで更新されなくて悩んでいました。
いろいろ実験した結果、SASのマクロってやっぱりよく分からないという結論に達しました。

DATAステップ内でマクロ変数に代入した直後は、まだ代入されていない

やりたかったこととしては、ループさせて一定時間が経過したらタイムアウトさせるというマクロを組みたかったので、以下の様な感じでコードを書いたのですが、elapsedTimeの値をログ出力させてみると何かがおかしい…

/*マクロ変数の削除と初期化*/
%symdel waitStart elapsedTime / nowarn;
%let elapsedTime = 0;

%macro wait();
  /*wait開始時刻*/
  data _null_;
    datetime = datetime();
    call symputx("waitStart", datetime, "G");
    put "■Start waiting: " datetime NLDATMM24.;
  run;

  /*とりあえず無限ループ*/
  %do %while (1);
    data _null_;
      /*10秒wait*/
      time_slept = sleep(10, 1);
      /*wait開始からの経過時間*/
      datetime = datetime();
      elapsed = INT(datetime - &waitStart);
      call symputx("elapsedTime", elapsed, "G");
      put "■Waiting elapsed time: " elapsed;
      put "マクロ変数elapsedTimeの確認1: &elapsedTime";
    run;

    %put マクロ変数elapsedTimeの確認2:  &elapsedTime;
  %end;

%mend;

%wait();

そうすると、ログにはこんな感じで出力されます。

■Start waiting: 2015/11/05 10:00:00

■Waiting elapsed time: 10
マクロ変数elapsedTimeの確認1: 0
マクロ変数elapsedTimeの確認2: 10

■Waiting elapsed time: 20
マクロ変数elapsedTimeの確認1: 10
マクロ変数elapsedTimeの確認2: 20

おいおい、なんで確認1でずれてるの?と思ったのですが、データステップの外に出ないとマクロ変数は更新されないようですね。
探したらSASトラブルシューティングに書いてありました。
マクロのトラブルシューティング

【SAS】PROC DS2でhttpのgetとJSONのパースをやってみた

同じ状況になる人はあまり居ないと思いますが、SASからREST APIを使ってJSON形式で情報を取ってきた後、JSONをパースしてデータセットに格納しています。

proc ds2;
  data xxx (overwrite=yes);
    /* GlobalでJSONパッケージインスタンスの宣言 */
    dcl package json j();
    
    /* 書き出す変数の宣言 */
    dcl bigint MODIFICATION_TIME having format 16.0;
    dcl nvarchar(128) PATH_SUFFIX;
    
    /* http GETのユーザ関数定義 */
    method getResponse(varchar(32767) URL) returns varchar(32767);
      /* ローカルでhttpパッケージインスタンスの宣言 */
      dcl package http webQuery();
      /* GETで返ってくる値を格納する変数 */
      dcl varchar(32767) character set utf8 RESPONSE;
      /* return code */
      dcl integer rc;
      rc = 0;
      /* GETを生成 */
      webQuery.createGetMethod(URL);
      /* GETを実行 */
      webQuery.executeMethod();
      /* 返ってきた値のbodyを文字値で取得 */
      webQuery.getResponseBodyAsString(RESPONSE, rc);
      return RESPONSE;
    end;
    
    /* parseJSONのユーザ定義関数 */
    method parseJSON(package json j); *packageを引数で渡す場合の動作がまだよく分かっていない;
      /* JSONパッケージで返ってくる値を格納する変数 */
      dcl int tokenType parseFlag rc;
      dcl nvarchar(128) token;
      rc = 0;
      
      /* JSONの最後までループ */
      do while (rc = 0);
        j.getNextToken(rc, token, tokenType, parseFlags);
        
        /* 取得したいmodification timeを格納 */
        if (token = 'modificationTime') then do;
          j.getNextToken(rc, token, tokenType, parseFlags);
          MODIFICATION_TIME = token;
        end;
        
        /* 取得したいpathSuffixを格納 */
        if (token = 'pathSuffix') then do;
          j.getNextToken(rc, token, tokenType, parseFlags);
          PATH_SUFFIX = token;
          /* 1行分の処理が終わったところでoutput */
          output;
        end;
      end;
    end;
    
    method init(); *runが適当?;
      dcl varchar(32767) character set utf8 URL RESPONSE;
      dcl int rc tokenType parseFlags;
      dcl nvarchar(128) token;
      rc = 0;
      
      /* 指定したURLで返ってくるJSONをRESPONSEに格納 */
      URL = 'http://xxx/yyy/?op=LISTSTATUS';
      RESPONSE = getResponse(URL);
      put 'URL:' URL;
      put 'Response:' RESPONSE;
      /* RESPONSEをJSONオブジェクトにセット */
      rc = j.createParser(RESPONSE);
      j.getNexToken(rc, token, tokenType, parseFlags);
      /* 今回はFileStatusをパースしたい */
      if (token = 'FileStatus') then do;
        parseJSON(j);
      end;
    end;
    
    /* JSONパッケージオブジェクトの破棄 */
    method term();
      dcl int rc;
      rc = j.destroyParser();
    end;
  enddata;
  run;
quit;

ds2は他のプログラミング言語のように書けるので、変数のスコープを意識しましたが、packageオブジェクトのスコープがよく分かりませんでした。
Packages and Scope

参考にしたサイトblogs.sas.com

【SAS】PROC HTTPを使ってみた

SASはPROC HTTPを使えば、GETやPUTなど基本的なことはできるようです。
REST APIで情報を取得するのに必要でした。
ここでは、GETした内容をローカルのファイルに書き出しています。
簡単ですね。

FILENAME resp "C:\xxx";

PROC HTTP
  METHOD="GET"
  URL="http://www.xxx"
  OUT=resp;
RUN;