我有两个表:
对于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数据步骤重写该代码以减少时间复杂度?
编辑更多信息:
使用示例数据进行编辑:
%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;
更新: 我将天真的解决方案与昆汀和汤姆的两个提议进行了比较。
如果有人需要完整的测试代码,请随时向我发送消息。
答案 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 ;