根据 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 sql
的 correlated 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;
假设我有两个数据集 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 sql
的 correlated 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;