调用执行时出现SAS宏错误

时间:2017-03-20 09:54:31

标签: sas sas-macro

我有以下代码用于生成过去1天,7天,1个月,3个月和6个月的功能运行总计。

LIBNAME A "C:\Users\James\Desktop\data\Base Data";
LIBNAME DATA "C:\Users\James\Desktop\data\Data1";

%MACRO HELPER(P);

data a1;
set data.final_master_&P. ;
QUERY = '%TEST('||STRIP(DATETIME)||','||STRIP(PARTICIPANT)||');';
CALL EXECUTE(QUERY);
run;

%MEND;

%MACRO TEST(TIME,PAR);
proc sql;
select SUM(APP_1), SUM(APP_2), sum(APP_3), SUM(APP_4), SUM(APP_5) INTO :APP_1_24, :APP_2_24, :APP_3_24, :APP_4_24, :APP_5_24
FROM A1
WHERE DATETIME BETWEEN INTNX('SECONDS',&TIME.,-60*60*24) AND &TIME.;

/* 7 Days */
select SUM(APP_1), SUM(APP_2), sum(APP_3), SUM(APP_4), SUM(APP_5) INTO :APP_1_7DAY, :APP_2_7DAY, :APP_3_7DAY, :APP_4_7DAY, :APP_5_7DAY
FROM A1
WHERE DATETIME BETWEEN INTNX('SECONDS',&TIME.,-60*60*24*7) AND &TIME.;

/* One Month */

select SUM(APP_1), SUM(APP_2), sum(APP_3), SUM(APP_4), SUM(APP_5) INTO :APP_1_1MONTH, :APP_2_1MONTH, :APP_3_1MONTH, :APP_4_1MONTH, :APP_5_1MONTH
FROM A1
WHERE DATETIME BETWEEN INTNX('SECONDS',&TIME.,-60*60*24*7*4) AND &TIME.;

/* Three Months */

select SUM(APP_1), SUM(APP_2), sum(APP_3), SUM(APP_4), SUM(APP_5) INTO :APP_1_3MONTH, :APP_2_3MONTH, :APP_3_3MONTH, :APP_4_3MONTH, :APP_5_3MONTH
FROM A1
WHERE DATETIME BETWEEN INTNX('SECONDS',&TIME.,-60*60*24*7*4*3) AND &TIME.;

/* Six Months */

select SUM(APP_1), SUM(APP_2), sum(APP_3), SUM(APP_4), SUM(APP_5) INTO :APP_1_6MONTH, :APP_2_6MONTH, :APP_3_6MONTH, :APP_4_6MONTH, :APP_5_6MONTH
FROM A1
WHERE DATETIME BETWEEN INTNX('SECONDS',&TIME.,-60*60*24*7*4*6) AND &TIME.;

quit;

DATA T;
PARTICIPANT = &PAR.;
DATETIME = &TIME;
APP_1_24 =  &APP_1_24.;
APP_2_24 =  &APP_2_24.;
APP_3_24 =  &APP_3_24.;
APP_4_24 =  &APP_4_24.;
APP_5_24 =  &APP_5_24.;
APP_1_7DAY =  &APP_1_7DAY.;
APP_2_7DAY =  &APP_2_7DAY.;
APP_3_7DAY =  &APP_3_7DAY.;
APP_4_7DAY =  &APP_4_7DAY.;
APP_5_7DAY =  &APP_5_7DAY.;
APP_1_1MONTH =  &APP_1_1MONTH.;
APP_2_1MONTH =  &APP_2_1MONTH.;
APP_3_1MONTH =  &APP_3_1MONTH.;
APP_4_1MONTH =  &APP_4_1MONTH.;
APP_5_1MONTH =  &APP_5_1MONTH.;
APP_1_3MONTH =  &APP_1_3MONTH.;
APP_2_3MONTH =  &APP_2_3MONTH.;
APP_3_3MONTH =  &APP_3_3MONTH.;
APP_4_3MONTH =  &APP_4_3MONTH.;
APP_5_3MONTH =  &APP_5_3MONTH.;
APP_1_6MONTH =  &APP_1_6MONTH.;
APP_2_6MONTH =  &APP_2_6MONTH.;
APP_3_6MONTH =  &APP_3_6MONTH.;
APP_4_6MONTH =  &APP_4_6MONTH.;
APP_5_6MONTH =  &APP_5_6MONTH.;
FORMAT DATETIME DATETIME.;
RUN;

PROC APPEND BASE=DATA.FLAGS_&par. DATA=T;
RUN;

%MEND;

%helper(1);

如果我在创建a1数据集时使用(obs =)限制%帮助程序宏中的观察数量,则此代码运行完美。但是,当我对obs数量没有限制时,即对数据集a1中的每一行执行%test宏时,都会出错。在SAS EG中,我得到一个"服务器断开连接"状态栏在"运行数据步骤"以及在Base SAS 9.4上挂起后弹出,我得到的错误是没有解析在proc sql中创建的宏变量。

我很困惑,因为代码适用于有限数量的观察,但在尝试整个数据集时,它会挂起或出错。我执行此操作的数据集有大约130,000个观测值。

2 个答案:

答案 0 :(得分:1)

您实际问题的答案是,您只是生成了太多宏代码,甚至可能只是花费了太多时间。你这样做的方式是在O = n ^ 2级别上运行,因为你基本上是对每条记录进行笛卡尔连接,然后是一些。 130,000 * 130,000是一个相当不错的数字,最重要的是你实际上每130,000行打开几次SQL环境。哎哟。

解决方案是以不太慢的方式执行此操作,或者以一种不会产生太多开销的方式执行此操作。

快速解决方案是进行笛卡尔联接,或限制需要加入的数量。一个好的解决方案是重构问题,而不是要求每个记录进行比较,而是考虑每个日历日,比如一个时期,特别是在超过24小时的时间段内(24小时你可能按照你的方式做,但不是其他四个)。 1个月,3个月等,你真的需要弄清楚时间吗?可能不会有太大的区别。如果你可以摆脱它,那么你可以使用内置的PROC预编译所有可能的1个月期间,所有可能的3个月期间等,然后加入适当的。但这不会对其中的130,000个起作用;只有你可以将它限制在每天一个,它才会起作用。

如果你必须在第二级(或更糟)做到这一点,你要做的就是避免笛卡尔加入,而是跟踪你已经看过的各种记录和总和。该算法的简短说明是:

每行:

  • 将此行的值添加到滚动总和(在队列末尾)
  • 检查队列的当前项是否在期间之外;如果是,则从滚动总和中减去它,并检查下一项(重复直到不在期间之外),更新当前队列位置
  • 此时返还总和

这需要通常两次检查每一行(除了在几个迭代中没有行弹出的奇数边界,因为几个月有不同的天数)。这在O = n时间运行,比笛卡尔连接快得多,并且最重要的是需要更少的内存/空间(笛卡尔连接可能需要占用磁盘空间)。

此解决方案的哈希版本如下。这将是我认为比较每一行的最快解决方案。请注意,我故意让每天的每行和相同行数的测试数据为1;这可以让你很容易地看到它如何以行方式工作。 (例如,每24小时有481行,因为我每天精确制作480行,而481包含昨天相同的时间 - 如果您将lt更改为le,它将为480,如果您愿意不包括昨天的同一时间)。您可以看到,基于“月份”的时段在月份变化的边界处将略有奇怪的结果,因为“01FEB20xx”到“01MAY20xx”期间的天数(因此行数)远远少于“01JUL20xx”到“01OCT20xx”期间, 例如;更好的是30/90/180天。

data test_data;
  array app[5] app_1-app_5;
  do _i = 1 to 130000;
    dt_var = datetime() - _i*180;
    do _j = 1 to dim(app);
      *app[_j] = floor(rand('Uniform')*6); *generate 0 to 5 integer;
      app[_j]=1;
    end;
    output;
  end;
  format dt_var datetime17.;
run;

proc sort data=test_data;
  by dt_var;
run;



%macro add(array=);
      do _i = 1 to dim(app);
        &array.[_i] + app[_i];
      end;
%mend add;



%macro subtract(array=);
      do _i = 1 to dim(app);
        &array.[_i] + (-1*app[_i]);
      end;
%mend subtract;

%macro process_array_add(array=);

  array app_&array. app_&array._1-app_&array._5;


  %add(array=app_&array.);

%mend process_array_add;

%macro process_array_subtract(array=, period=, number=);

  if _n_ eq 1 then do;
    declare hiter hi_&array.('td');
    rc_&array. = hi_&array..first();
  end;
  else do;
    rc_&array. = hi_&array..setcur(key:firstval_&array.);
  end;

  do while (intnx("&period.",dt_var,&number.,'s') lt curr_dt_var and rc_&array.=0);
    %subtract(array=app_&array.);
    rc_&array. = hi_&array..next();
  end;

  retain firstval_&array.;
  firstval_&array. = dt_var; 

%mend process_array_subtract;


data want;
  set test_data;

 * if _n_ > 10000 then stop;
  curr_dt_var = dt_var;
  array app[5] app_1-app_5;


  if _n_ eq 1 then do;
    declare hash td(ordered:'a');
    td.defineKey('dt_var');
    td.defineData('dt_var','app_1','app_2','app_3','app_4','app_5');
    td.defineDone();
  end;


  rc_a = td.add();

  *start macro territory;

  %process_array_add(array=24h);
  %process_array_add(array=1wk);
  %process_array_add(array=1mo);
  %process_array_add(array=3mo);
  %process_array_add(array=6mo);



  %process_array_subtract(array=24h,period=DTDay, number=1);
  %process_array_subtract(array=1wk,period=DTDay, number=7);
  %process_array_subtract(array=1mo,period=DTMonth, number=1);
  %process_array_subtract(array=3mo,period=DTMonth, number=3);
  %process_array_subtract(array=6mo,period=DTMonth, number=6);

  *end macro territory;

  rename curr_dt_var=dt_var;
  format curr_dt_var datetime21.3;
  drop dt_var rc: _:;

  output;


run;

答案 1 :(得分:1)

这是一个纯数据步骤非哈希版本。在我的机器上,它实际上比哈希解决方案更快;我怀疑它在带有硬盘的机器上实际上并不快(我有一个SSD,所以点访问并不比散列访问快得多,我避免加载哈希)。如果你不熟悉或者根本不知道哈希,我建议使用它,因为它更容易排除故障,并且它的扩展方式也相似。对于大多数行,它访问11行,当前行和其他五行两次(一行,减去它,然后减去另一行),总共大约一百五十万行读取130k行。 (与笛卡儿的大约170亿读数相比......)

我使用“_2”对宏进行后缀,以区别于散列解决方案中的宏。

data test_data;
  array app[5] app_1-app_5;
  do _i = 1 to 130000;
    dt_var = datetime() - _i*180;
    do _j = 1 to dim(app);
      *app[_j] = floor(rand('Uniform')*6); *generate 0 to 5 integer;
      app[_j]=1;
    end;
    output;
  end;
  format dt_var datetime17.;
run;

proc sort data=test_data;
  by dt_var;
run;

%macro add_2(array=);
      do _i = 1 to dim(app);
        &array.[_i] + app[_i];
      end;
%mend add;



%macro subtract_2(array=);
      do _i = 1 to dim(app);
        &array.[_i] + (-1*app[_i]);
      end;
%mend subtract;

%macro process_array_add_2(array=);

  array app_&array. app_&array._1-app_&array._5;   *define array;

  %add_2(array=app_&array.);                       *add current row to array;
%mend process_array_add_2;

%macro process_array_sub_2(array=, period=, number=);
  if _n_ eq 1 then do;                             *initialize point variable;
     point_&array. = 1;
  end;
  else do;                                         *do not have to do this _n_=1 as we only have that row;
    set test_data point=point_&array.;             *set the row that we may be subtracting;
  end;

  do while (intnx("&period.",dt_var,&number.,'s') lt curr_dt_var and point_&array. < _N_);  *until we hit a row that is within the period...;
    %subtract_2(array=app_&array.);                *subtract the rows values;
    point_&array. + 1;                             *increment the point to look at;
    set test_data point=point_&array.;             *set the new row;
  end;

%mend process_array_sub_2;


data want;
  set test_data;

  *if _n_ > 10000 then stop;                       *useful for testing if you want to check time to execute;
  curr_dt_var = dt_var;                            *save dt_var value from originally set record;
  array app[5] app_1-app_5;                        *base array;

  *start macro territory;  
  %process_array_add_2(array=24h);                 *have to do all of these adds before we start subtracting;
  %process_array_add_2(array=1wk);                 *otherwise we have the wrong record values;
  %process_array_add_2(array=1mo);
  %process_array_add_2(array=3mo);
  %process_array_add_2(array=6mo);

  %process_array_sub_2(array=24h,period=DTDay, number=1);   *now start checking to subtract what we need to;
  %process_array_sub_2(array=1wk,period=DTDay, number=7);
  %process_array_sub_2(array=1mo,period=DTMonth, number=1);
  %process_array_sub_2(array=3mo,period=DTMonth, number=3);
  %process_array_sub_2(array=6mo,period=DTMonth, number=6);

  *end macro territory;

  rename curr_dt_var=dt_var;
  format curr_dt_var datetime21.3;
  drop dt_var _:;

  output;                                          *unneeded in this version but left for comparison to hash;


run;