【问题标题】:Ranking values based on another data set in SAS根据 SAS 中的另一个数据集对值进行排名
【发布时间】:2015-03-22 12:35:07
【问题描述】:

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

这是数据集 A、B 和 want(所需输出)的简化示例:

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 中的观察数量很少时,这是可以的。但是当涉及到非常大的数字时,它需要很长时间,因此不是一个好的解决方案。

提前感谢您的建议。

【问题讨论】:

    标签: sas rank


    【解决方案1】:

    我会创建格式来做到这一点。您真正要做的是通过 A 定义要应用于 B 的范围。格式非常快 - 在这里假设“A”相对较小,“B”可以随心所欲,而且总是只需只要读取和写入 B 数据集一次,加上对 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;
    

    然后我们创建一个格式输入数据集,并使用排名作为标签。范围是(前一个值 -> 当前值),标签是排名。我把它留给你,你想如何处理关系。

    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;
    

    【讨论】:

    • 感谢乔的建议。对于我的情况,这似乎是最好和最直接的解决方案。事实上,起初我正在寻找一些与 R 中的 findInterval 函数等效的 SAS 功能,但除了 proc rank 之外,我没有想出一个很好的方法来做到这一点。很有帮助。
    【解决方案2】:

    您可以通过单独数据集中的变量进行排名的一种方法是使用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

    【讨论】:

    • 不确定您的测试,但我在一台速度相当快的带有 SSD 的 PC 上用原始 A(5 行)的 1e7 行 B 测试了您的,并在 15 分钟时停止,大约完成了 2/3。
    • 这真的很奇怪。我在旧笔记本电脑上运行 SAS University Edition。如果您想进行第二次运行,我已经添加了我的测试代码。
    • 现在,这很有趣。唯一的区别是 CEIL。我没有打扰我的CEIL;显然,这会产生巨大的影响。我测试了你的(9-10 秒),我的(运行时间长),然后将 CEIL 添加到我的中,得到了相同的 9-10 秒。我不知道为什么这很重要 - 显然当值是整数时会发生某种优化?或者仅仅是 B 中的“不同”值较少(因此它能够识别出 var1 中的 60 个可能值,它不必重复重新查询这么多次)?
    • 是的,就是这样。将 var1/var2/var3 扩展为 1e7 个实际整数值(并将 A 扩展为六个零),它又需要永远。 SQL 正在将数百万个查询优化为 60、600 和 6000 个查询,或者假设一些整数实际上没有被创建,那么这个数字会稍微小一些。
    • 这很有趣,感谢您的调查。我将在答案中添加警告。
    猜你喜欢
    • 2015-08-05
    • 1970-01-01
    • 2020-08-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-12-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多