連続・重複した日付期間データをならして最小オブザベーション数にまとめる方法について

イベント開始日と終了日からなるデータがあって、発生ごとに縦積みされるとします。
ただし、諸々の事情で継続途中でオブザベーションが別れたり、重複して記録されてたりするとします。

たとえば

6/1開始 - 6/3終了
6/2開始 - 6/4終了
6/4開始 - 6/5終了

という3行のにわかれたデータを

6/1開始 - 6/5終了

の1行におこしなおすという処理です。
これは結構苦手な人が多いはず。

今回使うのは以下のデータ(ややこいので欠損は含まれないということにしましょう)
でIDとSTDTでソート済みとしましょう

data Q1;
ID=1;STDT='01JUN2016'd;ENDT='03JUN2016'd;output;
ID=1;STDT='02JUN2016'd;ENDT='04JUN2016'd;output;
ID=1;STDT='04JUN2016'd;ENDT='05JUN2016'd;output;
ID=1;STDT='06JUN2016'd;ENDT='09JUN2016'd;output;
ID=1;STDT='12JUN2016'd;ENDT='15JUN2016'd;output;
ID=1;STDT='13JUN2016'd;ENDT='14JUN2016'd;output;
ID=1;STDT='17JUN2016'd;ENDT='17JUN2016'd;output;
ID=2;STDT='01JUN2016'd;ENDT='05JUN2016'd;output;
ID=2;STDT='02JUN2016'd;ENDT='08JUN2016'd;output;
ID=2;STDT='03JUN2016'd;ENDT='04JUN2016'd;output;
ID=2;STDT='10JUN2016'd;ENDT='11JUN2016'd;output;
format STDT ENDT yymmdds10.;
run;


これを

上記のような形にします。

この手の処理を書く場合は、先にガントチャートみたいな図を書いてから考えると楽ですね。
頭の中だけで組むと、結構泥沼になったりします

ちなみにSAS on demandでは、プロダクト「SAS OR」も使えて、そこに
proc ganttっていうガントチャートかけるプロシジャがあったので、使い方よくわからないけど
適当に指定して流してみました(頑張れば凄い綺麗なのが描けるみたいですけど)。
これのコードは本題じゃないので最後に書きます。


そこで、どうやるかですが、多分以下の感じで書くのが一般的ではないでしょうか?

data _Q1;
format r_STDT r_ENDT yymmdds10.;
set Q1;
by ID;
retain r_STDT r_ENDT ;
if first.ID then do;
r_STDT=STDT;
r_ENDT=ENDT;
end;
if STDT > r_ENDT+1 then do;
r_STDT=STDT;
r_ENDT=ENDT;
end;
if ENDT>r_ENDT then r_ENDT=ENDT;
run;
proc sql noprint;
create table A1 as
select ID,r_STDT as STDT,max(r_ENDT) as ENDT format=yymmdds10.
from _Q1
group by ID ,r_STDT;
quit;

1obs読み込みながらリテインしている開始日と終了日を条件に従って更新していくイメージです。
終了日はリテインしている終了日より後なら置き換える。開始日は、リテインしている終了日+1より
大きければそこで置き換える。
そしてできたデータセットについて、リテインしていた開始日でグループ化して、最も最大のリテインしていた終了日のみ残せばOKです。

sqlの部分に書き方はなんでもよくて、IDとr_STDT r_ENDTでソートしてlast.r_STDTでしぼってもOKなわけです。

上記の処理は2ステップでやってます。SASは基本的に行の先読みがやりにくい言語なので
上記の考え方を1ステップで表現するのは、かなり難しいです。どこで切ってアウトプットするかが、先まで読まないと確定できないですからね。

ところが、ハッシュオブジェクトなら、簡単にできちゃうんですよね。
前の記事で紹介したmultidataとfind_nextを使えば、先読みみたいなことが。

data A2;
length _STDT _ENDT 8.;
retain r_ENDT;
set Q1;
if _N_=1 then do;
call missing(_STDT,_ENDT);
declare hash h1(dataset:'Q1(rename=(STDT=_STDT
                                                                      ENDT=_ENDT))',multidata:'Y');
h1.definekey('ID');
h1.definedata('_STDT','_ENDT');
h1.definedone();
end;
 l_ID=lag(ID);
 if STDT<r_ENDT and ID=l_ID then delete;
 else do rc = h1.find() by 0 while (rc = 0) ;
  if _STDT<=ENDT+1 and _ENDT>ENDT then do;
  ENDT=_ENDT;
  end;
  rc= h1.find_next() ;
 end ;
 r_ENDT=ENDT;
 keep ID STDT ENDT;
run;

実は僕は、この処理を考えるとき、ハッシュでのやり方は一瞬で考えてすぐに書けたんですが
最初の書き方が中々でてこなくて手こずりました。
慣れたらこっちの方がわかりやすいと思うんですが、どうなんでしょ。

ちなみに、最初から最後までSQL一本でも表現できますが、かんなり難しいコードになります。
開始日(条件付き)と終了日の全組み合わせを作ってから、中に含まれる重複期間を除外するイメージですが、興味のある方は「SQLパズル: プログラミングが変わる書き方/考え方 第2版」に全コード載っているので参考にしてください。

最後にproc ganttのコードですが、僕もよくわかっとりません。as=でスタート、af=でエンド時点を指定します。
チャートに重ねる文字とかの制御をラベルデータセットで制御するみたいなんですが、まあ、あんまりSAS OR使う機会もないし、調べるの面倒だったので適当です。

data label;
 format _LVAR $10.; 
 _Y = -1;
 _LVAR = "STDT";
 _XVAR = "STDT";
 _CLABEL = "BLUE";
 _YOFFSET = .8; OUTPUT;
 _LVAR = "ENDT";
 _XVAR = "ENDT";
 _YOFFSET = .8;
 _XOFFSET = -0.3;OUTPUT;
RUN;
data GQ1;
set Q1;
format STDT ENDT day.;
run;
PROC GANTT DATA = GQ1 LABDATA=LABEL;
 CHART / AS = STDT AF = ENDT NOLEGEND NOJOBNUM NOTNLABEL 
         TIMEAXISFORMAT= day.;
 ID ID STDT ENDT;
 ;
RUN;




2 件のコメント:

  1. いつもブログを見て勉強させて頂いております。
    現在もブログをチェックされているかわかりかねますが、1点ご質問させてください。

    当記事の日付重複をまとめる手法について、ある程度の日付のgapを許容する方法はありますでしょうか?
    例えば今回の例ですと、ID=1のデータは6/1-6/9、6/12-6/15、6/17-6/17となりましたが、3日までの日付間隔は継続しているものとし、最終的に6/1-6/17という1つのデータに丸めることは可能でしょうか?

    返信削除
  2. いやーほんとすみません

    r_ENDT+1のところを+3などにするとたとえばID1などは6/1-6/17という1つのデータになります
    data _Q1;
    format r_STDT r_ENDT yymmdds10.;
    set Q1;
    by ID;
    retain r_STDT r_ENDT ;
    if first.ID then do;
    r_STDT=STDT;
    r_ENDT=ENDT;
    end;
    if STDT > r_ENDT+3 then do;
    r_STDT=STDT;
    r_ENDT=ENDT;
    end;
    if ENDT>r_ENDT then r_ENDT=ENDT;
    run;
    proc sql noprint;
    create table A1 as
    select ID,r_STDT as STDT,max(r_ENDT) as ENDT format=yymmdds10.
    from _Q1
    group by ID ,r_STDT;
    quit;

    返信削除