我輩はブロガーではない。ネタもまだない

SASとかDelphiあたりの人様の役に立たないネタを提供します

プログラムデータベクトル(PDV)の込み入った話

SASのデータステップには、プログラムデータベクトル(PDV)というものが存在します。

普通にデータステップ実行している分にはあまり気にすることもなく、認定試験のために勉強して知った、という人がほとんどかと思います。
(自分もですが)

そのPDVがちょっとかなり気になる動作をしているので検証してみました。




PDVについておさらいですが、メモリ上に展開される1オブザベーション分の
領域です。変数以外にも_n_や_ERROR_の一時変数の管理もしています。

例えば以下のデータセットをsetするとき
f:id:japelin:20200906001312p:plain

1.PDVが欠損に初期化され、_N_に1がセットされる
f:id:japelin:20200906001326p:plain

2.obsが読み込まれ、後続のステートメントが実行される
f:id:japelin:20200906001342p:plain

3.ステップの最後でPDVが欠損に初期化され、_N_が+1される
f:id:japelin:20200906001400p:plain

4.obsが読み込まれ、後続のステートメントが実行される
f:id:japelin:20200906001416p:plain

以降データセットの終端まで3と4のループが続きます。

また、SAS使いとしては、次のobsに保持するためにはretainステートメントが必要、というのは常識ですが、これは、setステートメントでobsを読み込む際にPDVが初期化されることがわかっていれば理解できるかと思います。


上記を踏まえて、次のdataステップを実行したとき、どのような動作になるでしょうか。

data a;
  a=1;output;
run;
data b;
  b='x';output;
  b='y';output;
run;

data test1;
  set a b;
run;

もちろん、このようになります。
f:id:japelin:20200906001703p:plain


では、このプログラムではいかがでしょうか。

data test2;
  set a b;
  if _n_=2 then do ;
    a=100;
  end;
run;

結果はこうです。
f:id:japelin:20200906001842p:plain

あれ?こうじゃないの?
f:id:japelin:20200906001911p:plain


と思いますよね。

どうやら、setステートメントでobsを読み込む際に初期化するPDVは読み込むデータセットにある変数およびそのデータステップ生成された変数のみのようです。

つまり、
set a b;
なので
データセットaを読み込むときには変数aは初期化されるが、データセットbを読み込むときには変数aがデータセットbに存在しないので初期化されず、結果的にretainされたのと同じ状況になってしまう、ということのようです。

私の勉強不足かもしれませんが、setステートメントで複数のデータセットを読み込む際にPDVの初期化対象がデータセット依存という情報は見たことがありません。
SAS® 認定プロフェッショナルのための Base Programming for SAS® 9 完全ガイドが手元にないので確認できない(ずっとリモートワーク)のですが、複数のデータセットを処理する際のPDVの動作って書いてありましたっけ?




さて、想定したデータセットにするためにはどうすればいいでしょうか。

data test2;
  set a b;
  if _n_=2 then a=100;
  else a=.;
run;

としちゃうと、_n_=1のときもaが欠損になってしまうので、

data test2;
  set a(inA) b;
  if _n_=2 then a=100;
  else if not(inA) then a=.;
run;

とか

data test2;
  set a b;
run;
data test2;
  set test2;
  if _n_=2 then a=100;
run;

と、2ステップに分けたりしてちょっと気を使ってあげる必要があります。



ところで、この初期化されない(=retainする)変数、何かに使えないでしょうか。
retain自体はキー変数毎のobs数を調べるような時に使われるかと思います。
(freqプロシージャでもいいんですが)

こんな感じの集計ですね。

/* sampleデータセット */
data rand;
  do i=1 to 100;
    x=int(ranuni(1)*10);
    if x>0 then output;
  end;
  drop i;
run;
proc sort data=rand;
  by x;
run;

data result;
  retain cnt;
  set rand;
  by x;
  if first.x then cnt=0;
  cnt+1;
  if last.x;
run;


2つ目のデータステップでは、cntという変数をretainステートメントで指定し、firstとlastで初期化とサブセット化を行う、というよくある手法を使っています。
結果はこんな感じ(ranuni使ってるので実行ごとに結果が変わりますが)
f:id:japelin:20200906004303p:plain

これを例えば、

data base;
  length x 8;
  delete;
run;

data result2;
  set base rand;
  by x;
  if first.x then cnt=0;
  cnt+1;
  if last.x;
run;

結果はこの通り。retainを使わずとも上のプログラムと同じです。
f:id:japelin:20200906004404p:plain


ポイントは
・ベースとなるデータセットにキー変数を入れること(0obsでいい)
・setステートメントでベースとなるデータセットを先に指定すること
くらいでしょうか。

あんまり使い所がわかりませんが、一応動作機序として覚えておいて損はないと思います。(というか知らないと思わぬ結果に繋がりそうです)