调用执行时出现 SAS 宏错误

SAS Macro error with call execute

我使用以下代码生成过去 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=) 限制 %helper 宏中的观察数量,则此代码运行完美。但是,当我对 obs 数没有限制时,即对数据集 a1 中的每一行执行 %test 宏时,我得到错误。在 SAS EG 中,状态栏挂在 "running data step" 后,我得到一个 "server disconnected" 弹出窗口,而在 Base SAS 9.4 上,我得到的错误是 none 的宏变量已被创建在proc sql 中。

我很困惑,因为代码对于有限数量的观察工作正常,但在尝试整个数据集时它会挂起或出错。我这样做的数据集有大约 130,000 个观察值。

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

解决方案是以不太慢的方式进行,或者如果是,至少不会有太多开销的方式。

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


如果您必须在第二级(或更糟)执行此操作,您需要做的是避免笛卡尔连接,而是跟踪您已经看到的各种记录和总和。算法的简短解释是:

每一行:

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

这通常需要检查每一行两次(除了在奇数边界处,由于月份的天数不同,因此在多次迭代中没有弹出任何行)。这在 O=n 时间上运行,比笛卡尔连接快得多,而且需要的 memory/space 少得多(笛卡尔连接可能需要命中磁盘 space)。

此解决方案的哈希版本如下。这将是我认为比较每一行的最快解决方案。请注意,我有意让测试数据每一行都有 1,每天都有相同的行数;这让您可以很容易地看到它是如何按行方式工作的。 (例如,每 24 小时周期有 481 行,因为我每天准确地制作 480 行,而 481 包括昨天的同一时间 - 如果您将 lt 更改为 le 它将是 480,如果您愿意不包括昨天的同一时间)。您可以看到基于 'month' 的周期在月份变化的边界处会有稍微奇怪的结果,因为“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;

这是一个纯数据步骤非哈希版本。在我的机器上,它实际上比散列解决方案更快;我怀疑它在带有 HDD 的机器上实际上并没有更快(我有一个 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;