SAS中的有效滚动总和(窗口汇总)

时间:2018-08-26 00:52:12

标签: sql sas

我有两个表:

  • 待付款:合同编号,付款日期,付款金额
  • tb_reference :合同编号,参考日期

对于tb_reference中的每个(合同编号,reference_date),我想创建一列 sum_payments 作为tb_payments的90天滚动总和。我可以使用以下查询来完成此操作(效率很低):

%let window=90;
proc sql;
    create index contract_id on tb_payments;
quit;
proc sql;
    create table tb_rolling as
    select a.contract_id,
           a.reference_date,
           (select sum(b.payment_value)
            from tb_payments as b
            where a.contract_id = b.contract_id
                  and a.reference_date - &window. < b.payment_date
                  and b.payment_date <= a.reference_date
           ) as sum_payments
    from tb_reference as a;
quit;

如何使用proc sql或SAS数据步骤重写该代码以减少时间复杂度?

编辑更多信息:

  • 我任意选择90天作为窗口,但是我将对几个窗口进行计算。可以同时对多个窗口进行计算的解决方案将是理想的
  • 两个表都可以具有10+百万行,并且数据完全是任意的。我的SAS服务器功能相当强大
  • Contract_ids可以在两个表中重复
  • (合同编号,参考日期)和(合同编号,付款日期)对是唯一的

使用示例数据进行编辑:

%let seed=1111;
data tb_reference (drop=i);
    call streaminit(&seed.);
    do i = 1 to 10000;
        contract_id = round(rand('UNIFORM')*1000000,1);
        output;
    end;
run;
proc surveyselect data=tb_reference out=tb_payments n=5000 seed=&seed.; run;
data tb_reference(drop=i);
    format reference_date date9.;
    call streaminit(&seed.);
    set tb_reference;
    do i = 1 to 1+round(rand('UNIFORM')*4,1);
        reference_date = '01jan2016'd + round(rand('UNIFORM')*1000,1);
        output;
    end;
run;
proc sort data=tb_reference nodupkey; by contract_id reference_date; run;
data tb_payments(drop=i);
    format payment_date date9. payment_value comma20.2;
    call streaminit(&seed.);
    set tb_payments;
    do i = 1 to 1+round(rand('UNIFORM')*20,1);
        payment_date = '01jan2015'd + round(rand('UNIFORM')*1365,1);
        payment_value = round(rand('UNIFORM')*3333,0.01);
        output;
    end;
run;
proc sort data=tb_payments nodupkey; by contract_id payment_date; run;

更新: 我将天真的解决方案与昆汀和汤姆的两个提议进行了比较。

  • merge方法非常快,并且在n = 10000的情况下实现了超过10倍的加速。正如汤姆(Tom)在回答中所展示的那样,它也非常强大。
  • 哈希表的运行速度非常快,并且实现了超过500倍的加速。因为我的数据集很大,所以这是要走的路,但是有一个陷阱:它们需要容纳在RAM中。

如果有人需要完整的测试代码,请随时向我发送消息。

2 个答案:

答案 0 :(得分:4)

如果您已获得许可,则可以使用PROC EXPAND来完成全部操作。但是让我们看看如何做到这一点。

如果所有日期都在PAYMENTS表中,就不那么难了。只需按ID和DATE合并两个表即可。计算运行总和,但还有一点麻烦,那就是要减去滚出窗口​​后部的值。然后只需保留参考文件中的日期即可。

一个问题可能是需要找到CONTRACT_ID的所有可能的日期,以便可以使用LAG()函数。使用PROC MEANS很容易。

proc summary data=tb_payments nway ;
  by contract_id ;
  var payment_date;
  output out=tb_id_dates(drop=_:) min=date1 max=date2 ;
run;

还有一个数据步骤。此步骤也可以是视图。

data tb_id_dates_all ;
  set tb_id_dates ;
  do date=date1 to date2 ;
    output;
  end;
  format date date9.;
  keep contract_id date ;
run;

现在,只需合并三个数据集并计算累计和。请注意,我包括了一个do循环,可以在一天中累积多次付款(删除示例数据生成代码中的nodupkey进行测试)。

如果要生成多个窗口,则需要多个实际的LAG()函数调用。

data want ;
  do until (last.contract_id);
    do until (last.date);
      merge tb_id_dates_all tb_payments(rename=(payment_date=date))
            tb_reference(rename=(reference_date=date) in=in2)
      ;
      by contract_id date ;
      payment=sum(0,payment,payment_value);
    end;
    day_num=sum(day_num,1);

    array lag_days(5) _temporary_ (7 30 60 90 180) ;
    array lag_payment(5) _temporary_ ;
    array cumm(5) cumm_7 cumm_30 cumm_60 cumm_90 cumm_180 ;
    lag_payment(1) = lag7(payment);
    lag_payment(2) = lag30(payment);
    lag_payment(3) = lag60(payment);
    lag_payment(4) = lag90(payment);
    lag_payment(5) = lag180(payment);

    do i=1 to dim(cumm) ;
       cumm(i)=sum(cumm(i),payment);
       if day_num > lag_days(i) then cumm(i)=sum(cumm(i),-lag_payment(i));
       if .z < abs(cumm(i)) < 1e-5 then cumm(i)=0;
    end;
    if in2 then output ;
  end;
  keep contract_id date cumm_: ;
  format cumm_: comma20.2 ;
  rename date=reference_date ;
run;

如果要使代码灵活适应窗口数,则需要添加一些代码生成来创建LAGxx()函数调用。例如,您可以使用以下宏:

%macro lags(windows);
%local i n lag ;
%let n=%sysfunc(countw(&windows));

array lag_days(&n) _temporary_ (&windows) ;
array lag_payment(&n) _temporary_ ;
array cumm(&n)
%do i=1 %to &n ;
  %let lag=%scan(&windows,&i);
 cumm_&lag
%end;
;
%do i=1 %to &n ;
  %let lag=%scan(&windows,&i);
lag_payment(&i) = lag&lag(payment);
%end;
%mend lags;

并通过对宏的此调用将LARAYxx和赋值语句替换为LAGxx()函数:

%lags(7 30 60 90 180)

答案 1 :(得分:4)

这是哈希方法的示例。由于您的数据已经排序,所以我认为哈希方法比汤姆的合并方法没有太大好处。

一般想法是将所有付款数据读入哈希表(如果实际数据太大,则可能会用光内存),然后读取参考日期的数据集。对于每个参考日期,您将查找该contract_id的所有付款,并对其进行迭代,测试以查看付款日期是否在参考日期之前90天,并有条件地增加sum_payments。

应该比您的问题中的SQL方法明显快,但可能会输给MERGE方法。如果未事先对数据进行排序,则可能会浪费时间对两个大型数据集进行排序然后合并。它可以在同一日期处理多次付款。

data want;
  *initialize variables for hash table ;
  call missing(payment_date,payment_value) ;

  *Load a hash table with all of the payment data ;
  if _n_=1 then do ;
    declare hash h(dataset:"tb_payments", multidata: "yes");
    h.defineKey("contract_ID");
    h.defineData("payment_date","payment_value");
    h.defineDone() ;
  end ;

  *read in the reference dates ;
  set tb_reference (keep=contract_id reference_date) ;

  *for each reference date, look up all the payments for that contract_id ;
  *and iterate through them.  If the payment date is < 90 days before reference date then ;
  *increment sum_payments ;

  sum_payments=0 ;
  rc=h.find();  
  do while (rc = 0); *found a record;
    if 0<=(reference_date-payment_date)<90 then sum_payments = sum_payments + payment_value ;
    rc=h.find_next();
  end;
run ;