SAS或SQL:根据变量值之间的关系重新排列表



在SAS中,我有一个包含称为parent和child的两列(变量)的表。我想把这张桌子变换成两种不同的排列方式。

这个表的一个例子是:

Obs number | parent | child
----------------------------
1          | D      | B
2          | J      | Q
3          | D      | S
4          | K      | J
5          | T      | U
6          | B      | T

创建该表的规则是:

  1. 在每一行(观察)中,父列的值是子列值的父值。例如,在第1行和第2行中,我们发现D是B的父结点,B是D的子结点;J是Q的父级,Q是J的子级。

    Obs number | parent | child
    ----------------------------
    1          | D      | B
    2          | J      | Q
    
  2. 父母可以生几个孩子。例如,在第1行和第3行中,我们发现B和S都是D的子节点。

    Obs number | parent | child
    ----------------------------
    1          | D      | B
    ...        
    3          | D      | S
    
  3. 一个孩子只能有一个父母。例如,B的父级是D,而B不应该是除D之外的任何父级的子级。

    Obs number | parent | child
    ----------------------------
    1          | D      | B
    ...
    n          | C      | B
    

  4. 子节点可以有自己的子节点,父节点可以有自己的父节点。例如,在第1行和第6行中,我们发现B是D的子元素,并且它有自己的子元素T:

    Obs number | parent | child
    ----------------------------
    1          | D      | B
    ...
    6          | B      | T
    

    在第2行和第4行,我们发现J是Q的父结点,并且它有自己的父结点K:

    Obs number | parent | child
    ----------------------------
    2          | J      | Q
    ...
    4          | K      | J
    
  5. 子节点不应该是其父节点的父节点,父节点也不应该是其子节点的子节点。例如,B是D的孩子,它不能再成为D的父母。不允许使用以下格式:

    Obs number | parent | child
    ----------------------------
    1          | D      | B
    ...
    n          | B      | D
    

以上是构建原始表所需的所有规则。然后我想把桌子变成两种不同的排列方式。

首先,我想对这个表进行排序,使父级和子级的相关行聚集在一起,形成从最老的父级到最小的子级的链接。我的意思是从左边的原始表到右边的转换表,如下所示:

Original table:                          Transformed table:
Obs number | parent | child              Obs number | parent | child
----------------------------             ----------------------------
1          | D      | B                  1          | D      | S
2          | J      | Q                  2          | D      | B 
3          | D      | S         TO:      3          | B      | T 
4          | K      | J                  4          | T      | U
5          | T      | U                  5          | K      | J
6          | B      | T                  6          | J      | Q

其次,我想将原始表转换为新表,使新表的每一行都包含从最老的父表到最年轻的子表的列。我指的是像下面这样的转换:

Original table:                          Transformed table:
Obs number | parent | child              Obs number | var1 | var2 | var3 | var4
----------------------------             --------------------------------------
1          | D      | B                  1          | D    | B    | T    | U
2          | J      | Q                  2          | K    | J    | Q    | .
3          | D      | S         TO:      3          | D    | S    | .    | .
4          | K      | J 
5          | T      | U
6          | B      | T

下面是设置原始测试表的SAS代码:

data original_table;
    input parent $ child $;
    datalines;
D      B
J      Q
D      S
K      J
T      U
B      T
;

有人问我我已经尝试过的最小尝试。这就是。实际上,在我发帖几个小时后,我找到了一种方法来做第二次转换。下面是代码:

%macro TransformTable(lib=, dt=, output=);
/* rename the variable in the input table */
proc sql noprint;
    select cats(name,"=",cats(upcase("var"),substr("12", varnum, 1))) into :newvars separated by ' '
    from sashelp.vcolumn
    where libname = upcase("&lib.") and memname=upcase("&dt.") and 
    1 <= varnum <= 2;
quit;
data work._original_table;
    set &lib..&dt.;
    rename &newvars.;
run;
/* select values that are not child of other values (pure parent table) */
proc sql noprint;
    create table _combine1
    as select * from
    _original_table
    where VAR1 not in (select VAR2 from _original_table);
quit;
/* join the pure parent table and its update table to the original table multiple times */
/* move all child and child's child into one line */
%let nonmissing = 1;
%let i = 1;
%do %while(&nonmissing. > 0);
    %let newfile_num = %eval(&i. + 1);
    %let lastcol_num = %eval(&i. + 2);
    proc sql noprint;
        create table _combine&newfile_num.
        as select a.*, b.VAR2 as VAR&lastcol_num.
        from _combine&i. a
        left join
        _original_table b
        on a.VAR&newfile_num. = b.VAR1;
    quit;
    proc sql noprint;
        select count(*) into :nonmissing
        from _combine&newfile_num.(where=(^missing(VAR&lastcol_num.)));
    quit;
    %put &nonmissing.;
    %let i = %eval(&i. + 1);
%end;
/* remove the last empty column */
data &output. (drop = VAR&lastcol_num.); 
    set _combine&newfile_num.;
run;
%mend TransformTable;
%TransformTable(lib=work, dt=original_table, output=Result_for_2ndTrans)

但这还远远不够好。

首先,我使用循环而不是递归,因为我不知道如何在SAS或SQL中进行递归。

第二,为了停止循环,我需要每次检查输出文件。它消耗额外的资源,而且不像递归中的基本情况那么漂亮。

最终输出还将包含一个提取空列,该列需要在该宏的末尾进行额外的处理。如果你能想出一个更好的解决方案,请告诉我。

对于第一个转换,到目前为止我还没有任何想法来处理这个问题。

这里你有这个问题的非常简单的解决方案,使用hash tablehash iterator。我希望这能产生一个正确的结果。你需要检查一下。

proc sort data=original_table;
    by parent child;
run;
data _null_;
    length path $32767 longest 8;
    if _N_ = 1 then do;
        declare hash h();
        h.defineKey('path');
        h.defineData('path','longest');
        h.defineDone();
        call missing(path, longest);
        declare hiter hi('h');
    end;
    set original_table end=last;
    put "PARENT: " parent " CHILD: " child;
    longest = 1;
    path = parent;
    h.ref();
    put "ADD NEW SOURCE: " path=;
    rc = hi.first();
    do while(rc = 0);
        if substr(path, lengthn(path), 1) = parent then do;
            longest = 0;
            h.replace();
            longest = 1;
            path = cats(path, child);
            h.ref();
            put "ADD CHILD: " path=;
        end;
        rc = hi.next();
    end;
    rc = hi.first();
    do while(rc = 0);
        if substr(path, 1, 1) = child then do;
            longest = 0;
            h.replace();
            longest = 1;
            path = cats(parent, path);
            h.ref();
            put "ADD PARENT: " path=;
        end;
        rc = hi.next();
    end;
    if last;
    h.output(dataset: 'result(where=(longest = 1))');
run;
data result;
    set result;
    len = lengthn(path);
run;
proc sort data=result;
    by descending len path;
run;

您要做的是对数据集进行递归处理。SAS并不是在宏处理器之外构建的。您将不得不使用goto之类的东西来构建递归函数。生成的代码相当混乱。哈希表将帮助跟踪已经使用的父/子对。

这个解决方案可以工作,但可以进一步优化:

data transformed_table;
length var1 - var10 $1;
if _n_ = 1 then do;
    declare hash h(dataset:"original_table ");
    declare hiter hi("h");
    h.definekey("parent", "child");
    h.definedone();
    call missing(ref);
end;
set original_table end = eof;
array var [*] var1 - var10;
var1 = parent; ref = 2; 
var[ref] = child;
start:
check = 0;
do i = 1 to (n);
    set original_table (rename=(parent=p1 child=c1) obs=1 ) point=i nobs=n;
    rc = hi.first();
    do while (rc = 0);
        if h.check(key:p1, key:c1) eq 0 and var[ref] = p1 then do;
            ref + 1;
            var[ref] = c1;
            check = 1;
        end;
        rc = hi.next();
        if check = 1 then do;
            h.remove(key:var[ref - 1], key:var[ref]);
            goto start;
        end;
    end;    
end;
keep var1 - var10 ;
if eof then h.output(dataset: 'id');
run;
proc sort data = transformed_table; by var1 var2; run;
proc sort data = id; by parent child; run;
data transformed_table;
    merge transformed_table
          id (in = a rename=(parent=var1 child=var2))
          ;
    by var1 var2;
    if a;
run;

最新更新