根据 SAS 中的另一个数据集对值进行排名

Ranking values based on another data set in SAS

假设我有两个数据集 A 和 B,它们具有相同的变量,并且想根据 A 中的值而不是 B 本身(如 "PROC RANK data=B" 所做的那样)对 B 中的值进行排名。

这是数据集 A、B 和想要的(期望的输出)的简化示例:

A:
obs_A  VAR1  VAR2  VAR3
1    10    100   2000
2    20    300   1000
3    30    200   4000
4    40    500   3000
5    50    400   5000

B:
obs_B  VAR1  VAR2  VAR3
1    15    150   2234
2    14    352   1555
3    36    251   1000
4    41    350   2011
5    60    553   5012

want:
obs  VAR1  VAR2  VAR3
1    2     2     3
2    2     4     2
3    4     3     1
4    5     4     3
5    6     6     6

我提出了一个涉及 PROC RANK 和 PROC APPEND 的宏循环,如下所示:

%macro MyRank(A,B);
  data AB; set &A &B; run;
  %do i=1 %to 5;
    proc rank data=AB(where=(obs_A ne . OR obs_B=&i) out=tmp;
      var VAR1-3;
    run;
    proc append base=want data=tmp(where=(obs_B=&i) rename=(obs_B=obs)); run;
  %end;
%mend;

当 B 中的观测值较少时,这是可以的。但是当涉及到非常大的数字时,需要很长时间,因此不是一个好的解决方案。

提前感谢您的建议。

您可以按来自单独数据集的变量进行排名的一种方法是使用 proc sqlcorrelated subqueries。本质上,您计算要排名的数据中每个值的查找数据集中较低值的数量。

proc sql;
    create table want as
    select 
        B.obs_B, 
        (
            select count(distinct A.Var1) + 1
            from A
            where A.var1 <= B.var1.
        ) as var1
    from B;
quit;

可以用宏来包装。下面,使用一个宏循环来编写每个子查询。它查看变量列表并根据需要对子查询进行参数设置。

%macro rankBy(
        inScore /*Dataset containing data to be ranked*/, 
        inLookup /*Dataset containing data against which to rank*/, 
        varID /*Variable by which to identify an observation*/, 
        varsRank /*Space separated list of variable names to be ranked*/, 
        outData /*Output dataset name*/);
    /* Rank variables in one dataset by identically named variables in another */
    proc sql;
        create table &outData. as
        select 
            scr.&varID.
            /* Loop through each variable to be ranked */
            %do i = 1 %to %sysfunc(countw(&varsRank., %str( )));
                /* Store the variable name in a macro variable */
                %let var = %scan(&varsRank., &i., %str( ));
                /* Rank: count all the rows with lower value in lookup */
                , (
                    select count(distinct lkp&i..&var.) + 1
                    from &inLookup. as lkp&i.
                    where lkp&i..&var. <= scr.&var.
                ) as &var.
            %end;
        from &inScore. as scr;
    quit;
%mend rankBy;

%rankBy(
    inScore = B,
    inLookup = A,
    varID = obs_B,
    varsRank = VAR1 VAR2 VAR3,
    outData = want);

关于速度,如果你的 A 很大,这会很慢,但是 应该 B 和小 A 没问题.

在一台慢速 PC 上的粗略测试中,我看到:

A: 1e1    B: 1e6    time: ~1s
A: 1e2    B: 1e6    time: ~2s
A: 1e3    B: 1e6    time: ~5s
A: 1e1    B: 1e7    time: ~10s
A: 1e2    B: 1e7    time: ~12s
A: 1e4    B: 1e6    time: ~30s

编辑: 正如 Joe 在下面指出的那样,查询所花费的时间长度不仅取决于数据集中观察的数量,还取决于数据中存在的唯一值的数量。显然,SAS 执行优化以减少对 B 中不同值的比较,从而减少需要对 A 中的元素进行计数的次数。这意味着如果数据集 B 包含大量唯一值(在排名变量中),则该过程将比显示的时间长得多。如果您的数据不是 Joe 演示的整数,则更有可能发生这种情况。


编辑: 运行时测试平台:

data A;
    input obs_A  VAR1  VAR2  VAR3;
datalines;
1    10    100   2000
2    20    300   1000
3    30    200   4000
4    40    500   3000
5    50    400   5000
;
run;
data B;
    do obs_B = 1 to 1e7;
        VAR1 = ceil(rand("uniform")* 60);
        VAR2 = ceil(rand("uniform")* 500);
        VAR3 = ceil(rand("uniform")* 6000);
        output;
    end;
run;
%let start = %sysfunc(time());
%rankBy(
    inScore = B,
    inLookup = A,
    varID = obs_B,
    varsRank = VAR1 VAR2 VAR3,
    outData = want);
%let time = %sysfunc(putn(%sysevalf(%sysfunc(time()) - &start.), time12.2));
%put &time.;

输出:

0:00:12.41

我会创建格式来执行此操作。您真正在做的是通过 A 定义要应用于 B 的范围。格式非常快 - 这里假设 "A" 相对较小,"B" 可以随心所欲地变大,而且它总是将花费与读取和写出 B 数据集一次所花费的时间一样多的时间,再加上一对 read/writes of A.

首先,读入A数据集:

data ranking_vals;
input obs_A  VAR1  VAR2  VAR3;
datalines;
1    10    100   2000
2    20    300   1000
3    30    200   4000
4    40    500   3000
5    50    400   5000
;;;;
run;

然后将其转换为垂直,因为这将是对它们进行排名的最简单方法(只是简单的旧排序,不需要 proc 排名)。

data for_ranking;
  set ranking_vals;
  array var[3];
  do _i = 1 to dim(var);
    var_name = vname(var[_i]);
    var_value = var[_i];
    output;
  end;
run;

proc sort data=for_ranking;
  by var_name var_value;
run;

然后我们创建一个格式输入数据集,并使用排名作为标签。 range为(上一个值->当前值),label为排名。我留给你如何处理领带。

data for_fmt;
  set for_ranking;
  by var_name var_value;
  retain prev_value;
  if first.var_name then do;   *initialize things for a new varname;
    rank=0;
    prev_value=.;
    hlo='l';                   *first record has 'minimum' as starting point;
  end;
  rank+1;
  fmtname=cats(var_name,'F');  
  start=prev_value;            
  end=var_value;
  label=rank;
  output;
  if last.var_name then do;       *For last record, some special stuff;
    start=var_value;
    end=.;
    hlo='h';
    label=rank+1;
    output;                       * Output that 'high' record;
    start=.;
    end=.;
    label=.;
    hlo='o';
    output;                       * And a "invalid" record, though this should never happen;
  end;
  prev_value=var_value;           * Store the value for next row.;
run;


proc format cntlin=for_fmt;
quit;

然后我们测试一下。

data test_b;
input obs_B  VAR1  VAR2  VAR3;
var1r=put(var1,var1f.);
var2r=put(var2,var2f.);
var3r=put(var3,var3f.);
datalines;
1    15    150   2234
2    14    352   1555
3    36    251   1000
4    41    350   2011
5    60    553   5012
;;;;
run;