【问题标题】:SAS hierarchical structure sumSAS层次结构总和
【发布时间】:2017-05-17 22:05:40
【问题描述】:

我有一个带有分层编码列表变量的数据集。 层次的逻辑由 LEVEL 变量和 CODE 字符变量的前缀结构决定。 共有 6 个(代码长度从 1 到 6)“聚合”级别和终端级别(代码长度为 10 个字符)。

我需要更新节点变量(终端节点的计数 - 聚合级别不计入“更高”聚合,只计算终端节点) - 所以一个级别的计数总和,例如每个级别 5 的总数计数与每个级别 6 相同。 我需要计算(总结)“更高”级别节点的权重。

注意:我偏移了输出表的 NODES 和 WEIGHT 变量,以便您可以更好地了解我在说什么(只需将每个偏移量中的数字相加,您就会得到相同的值)。

EDIT1:同一代码可以有多个观察值。一个独特的观察是 3 个变量 code + var1 + var2 的组合。

输入表:

ID   level code         var1  var2  nodes  weight  myIndex
1    1     1            .     .     999    999     999
2    2     11           .     .     999    999     999
3    3     111          .     .     999    999     999
4    4     1111         .     .     999    999     999
5    5     11111        .     .     999    999     999
6    6     111111       .     .     999    999     999
7   10     1111119999   01    1     1      0.1     105,5
8   10     1111119999   01    2     1      0.1     109,1
9    6     111112       .     .     999    999     999
10  10     1111120000   01    1     1      0.5      95,0
11   5     11119        .     .     999    999     999
12   6     111190       .     .     999    999     999
13  10     1111901000   01    1     1      0.1      80,7
14  10     1111901000   02    1     1      0.2     105,5

所需的输出表:

ID   level code         var1  var2  nodes    weight              myIndex
1    1     1            .     .     5        1.0                  98,1
2    2     11           .     .     5        1.0                  98,1
3    3     111          .     .     5        1.0                  98,1
4    4     1111         .     .     5        1.0                  98,1
5    5     11111        .     .       3          0.7              98,5
6    6     111111       .     .         2            0.2         107,3
7   10     1111119999   01    1           1               0.1    105,5  
8   10     1111119999   01    2           1               0.1    109,1
9    6     111112       .     .         1            0.5          95,0
10  10     1111120000   01    1           1               0.5     95,0
11   5     11119        .     .       2          0.3              97,2
12   6     111190       .     .         2            0.3          97,2
13  10     1111901000   01    1           1               0.1     80,7
14  10     1111901000   02    1           1               0.2    105,5

这是我想出的代码。它就像我想要的那样工作,但是伙计,它真的很慢。我需要更快的方法,因为这是 Web 服务的一部分,必须根据请求“立即”运行。 欢迎任何有关加快代码速度或任何其他解决方案的建议。

%macro doit;

data temporary;
    set have;
run;

%do i=6 %to 2 %by -1;
    %if &i = 6 %then %let x = 10;
    %else %let x = (&i+1);

    proc sql noprint;
        select count(code)
        into :cc trimmed
        from have
        where level = &i;

        select code
        into :id1 - :id&cc
        from have
        where level = &i;
    quit;

    %do j=1 %to &cc.;

        %let idd = &&id&j;

        proc sql;
        update have t1
            set nodes = (
                       select sum(nodes)
                       from temporary t2
                       where t2.level = &x and t2.code like ("&idd" || "%")),
            set weight = (
                       select sum(weight)
                       from temporary t2
                       where t2.level = &x and t2.code like ("&idd" || "%"))   
            where (t1.level = &i and t1.code like "&idd");
        quit;
    %end;
%end;
%mend doit;

基于@Quentin 解决方案的当前代码:

data have;
input ID level code : $10. nodes weight myIndex;
cards;
1    1  1            .   .    .
2    2  11           .   .    .
3    3  111          .   .    .
4    4  1111         .   .    .
5    5  11111        .   .    .
6    6  111111       .   .    .
7   10  1111110000   1   0.1  105.5
8   10  1111119999   1   0.1  109.1
9    6  111112       .   .    .
10  10  1111129999   1   0.5  95.0
11   5  11119        .   .    .
12   6  111190       .   .    .
13  10  1111900000   1   0.1  80.7
14  10  1111901000   1   0.2  105.5
;

data want (drop=_:);

    *hash table of terminal nodes;
    if (_n_ = 1) then do;
        if (0) then set have (rename=(code=_code weight=_weight));
        declare hash h(dataset:'have(where=(level=10) rename=(code=_code weight=_weight myIndex=_myIndex))');
        declare hiter iter('h');
        h.definekey('ID');
        h.definedata('_code','_weight','_myIndex');
        h.definedone();
    end;

    set have;

    *for each non-terminal node, iterate through;
    *hash table of all terminal nodes, looking for children;
    if level ne 10 then do;
        call missing(weight, nodes, myIndex);

        do _n_ = iter.first() by 0 while (_n_ = 0);
            if trim(code) =: _code then do;  
                weight=sum(weight,_weight);
                nodes=sum(nodes,1);
                myIndex=sum(myIndex,_myIndex*_weight);
            end;
            _n_ = iter.next();
        end;
        myIndex=round(myIndex/weight,.1);
    end;
    output;
run;

【问题讨论】:

  • 真实数据有多大?我正在考虑一些丑陋的蛮力方法。
  • “在每个偏移量中添加数字”是什么意思?你能在这里使用多级格式吗?
  • @Quentin 真实数据是动态变化的,但总是在 4-5000 次观察之间,所以不是很大。
  • @Reeza 我只是想更好地解释/可视化我想要通过偏移量实现的目标。 misalingned/offseted 值来自同一层次结构级别,因此您可以更好地看到,它总是加起来相同的数字(权重或节点数)。我不知道多级格式是如何工作的,但会检查一下。
  • @Quentin, user667489 Quentin 解决方案的平均 CPU 时间为 6.84 秒,而 user667489 的方法为 0.82 秒。那是一些 -88% 的 CPU 时间改进。绝对实时时间几乎与 -88% 的相对改进相同。汤姆的接近花了半多分钟。哈希表查找的速度给我留下了深刻的印象。

标签: sql sas sum hierarchy recursive-query


【解决方案1】:

这是另一种哈希方法。

这不是使用哈希对象进行笛卡尔连接,而是将每个级别 10 节点的节点和权重添加到 6 个适用的父节点中的每一个。这可能比 Quentin 的方法稍微快一些,因为没有多余的哈希查找。

在构造散列对象时,它比 Quentin 的方法需要更长的时间,并且使用更多的内存,因为每个终端节点使用不同的键添加 6 次,并且现有条目通常必须更新,但之后每个父节点只必须查找自己的个人统计信息,而不是遍历所有终端节点,这样可以节省大量资金。

加权统计也是可能的,但您必须更新两个循环,而不仅仅是第二个。

data want;
if 0 then set have;
dcl hash h();
h.definekey('code');
h.definedata('nodes','weight','myIndex');
h.definedone();
length t_code $10;
do until(eof);
  set have(where = (level = 10)) end = eof;
  t_nodes = nodes;
  t_weight = weight;
  t_myindex = weight * myIndex;
  do _n_ = 1 to 6;
    t_code = substr(code,1,_n_);
    if h.find(key:t_code) ne 0 then h.add(key:t_code,data:t_nodes,data:t_weight,data:t_myIndex);
    else do;
      nodes + t_nodes;
      weight + t_weight;
      myIndex + t_myIndex;
      h.replace(key:t_code,data:nodes,data:weight,data:MyIndex);
    end;
  end;
end;
do until(eof2);
  set have end = eof2;
  if level ne 10 then do;
    h.find();
    myIndex = round(MyIndex / Weight,0.1);
  end;
  output;
end;
drop t_:;
run;

【讨论】:

  • 感谢 @user667489,将在我的确切数据集上试用它,并让您知道它在 cpu 时间上与之前接受的哈希解决方案的比较。
  • 看起来很有希望。 @Martin,请让我们知道它们在 CPU 时间和实时方面的比较。也很好奇 Tom 的 SQL 解决方案如何比较,因为它很可爱。
  • @user667489 这个解决方案不是微不足道,而是快了 n 倍,但在我弄清楚如何将它应用到同一个数据集之前,我无法获得确切的数字。我需要对一些变量(针对父节点)实施加权平均计算,我在 cmets 中向 Quentin 解释了他的回答。是否也可以使用此解决方案来实现它?
  • 值 = sum(weight_child*value_child)/sum(weight_child)
  • nvm... 我想我明白了。我想我可以在第二个做直到循环(eof2)中做到这一点。如果我做错了会写。
【解决方案2】:

下面是一种蛮力哈希方法,用于在 SQL 中进行类似的笛卡尔积。加载终端节点的哈希表。然后读取节点的数据集,对于每个不是终端节点的节点,遍历哈希表,识别出所有的终端子节点。

我认为@joop 描述的方法可能更有效,因为这种方法没有利用树结构。所以有很多重新计算。对于 5000 条记录和 3000 个终端节点,这将进行 2000*3000 次比较。但可能不会那么慢,因为哈希表在内存中,所以你不会有过多的 I/O ....

data want (drop=_:);

   *hash table of terminal nodes;
   if (_n_ = 1) then do;
      if (0) then set have (rename=(code=_code weight=_weight));
      declare hash h(dataset:'have(where=(level=10) rename=(code=_code weight=_weight))');
      declare hiter iter('h');
      h.definekey('ID');
      h.definedata('_code','_weight');
      h.definedone();
   end;

   set have;

   *for each non-terminal node, iterate through;
   *hash table of all terminal nodes, looking for children;
   if level ne 10 then do;
      call missing(weight, nodes);

      do _n_ = iter.first() by 0 while (_n_ = 0);
         if trim(code) =: _code then do;  
           weight=sum(weight,_weight);
           nodes=sum(nodes,1);
         end;
         _n_ = iter.next();
      end;
   end;
   output;
run;

【讨论】:

  • @martin:如果哈希值很吓人,我可以使用相同的方法尝试数组解决方案。将终端节点加载到数组中,读取节点并搜索数组,而不是搜索哈希表。让我知道这听起来是否有用。
  • 我将您的答案标记为解决方案。你的哈希表方法比我的代码快得多,将来我也会尝试 joop 的解决方案,但现在我会坚持这个。还有一个问题:我正在尝试计算另一个变量加权平均值。值 = 总和(权重孩子 * 值孩子)/总和(体重孩子)。逻辑和以前一样,需要从子终端节点中为每个非终端节点计算。是否可以使用相同的方法?
  • 我正在考虑只做 value=sum(value, _value * _weight) 和 weight=sum(weight, _weight) 并在该特定父节点的迭代结束时将 value 除以重量。
  • 同意@Martin,听起来像是在此设置中加权平均的正确方法。
  • 我添加了another hash-based answer,以避免笛卡尔循环导致的冗余哈希查找。
【解决方案3】:

看起来很简单。只需加入自身并计数/求和即可。

proc sql ;
create table want as
 select a.id, a.level, a.code , a.var1, a.var2
      , count(b.id) as nodes
      , sum(b.weight) as weight
 from have a
 left join have b
 on a.code eqt b.code
 and b.level=10
 group by 1,2,3,4,5
 order by 1
;
quit;

如果您不想使用 EQT 运算符,则可以使用 SUBSTR() 函数。

 on a.code = substr(b.code,1,a.level)
 and b.level=10

【讨论】:

  • 谢谢汤姆。您的方法也有效,但哈希表解决方案在我的确切数据集上更快。
【解决方案4】:

既然您使用的是 SAS,那么在这里使用proc summary 来完成繁重的工作怎么样?不需要笛卡尔连接!

与其他选项相比,此选项的一个优点是,如果您想为多个变量计算大量更复杂的统计数据,它更容易概括。

data have;
input ID level code : $10. nodes weight myIndex;
format myIndex 5.1;
cards;
1    1  1            .   .    .
2    2  11           .   .    .
3    3  111          .   .    .
4    4  1111         .   .    .
5    5  11111        .   .    .
6    6  111111       .   .    .
7   10  1111110000   1   0.1  105.5
8   10  1111119999   1   0.1  109.1
9    6  111112       .   .    .
10  10  1111129999   1   0.5  95.0
11   5  11119        .   .    .
12   6  111190       .   .    .
13  10  1111900000   1   0.1  80.7
14  10  1111901000   1   0.2  105.5
;
run;


data v_have /view = v_have;
  set have(where = (level = 10));
  array lvl[6] $6;
  do i = 1 to 6;
    lvl[i]=substr(code,1,i);
  end;
  drop i;
run;

proc summary data = v_have;
  class lvl1-lvl6;
  var nodes weight;
  var myIndex /weight = weight;
  ways 1;
  output out = summary(drop = _:) sum(nodes weight)= mean(myIndex)=;
run;

data v_summary /view = v_summary;
  set summary;
  length code $10;
  code = cats(of lvl:);
  drop lvl:;
run;

data have;
  modify have v_summary;
  by code;
  replace;
run;

理论上,散列的散列也可能是一种合适的数据结构,但是对于相对较小的好处而言,这将非常复杂。无论如何,我可能会去学习一下......

【讨论】:

  • 我是 SAS 新手,我使用它更像是一种数据操作方式,而不是数据分析方式。尚未使用 proc summary,但会查看文档。我也喜欢这个解决方案。谢谢。
【解决方案5】:

一种方法(我认为)是制作笛卡尔积,并找到与每个节点“匹配”的所有终端节点,然后对权重求和。

类似:

data have;
  input ID level code : $10. nodes weight ;
  cards;
1    1  1            .   .
2    2  11           .   .
3    3  111          .   .
4    4  1111         .   .
5    5  11111        .   .
6    6  111111       .   .
7   10  1111110000   1   0.1
8   10  1111119999   1   0.1
9    6  111112       .   .
10  10  1111129999   1   0.5
11   5  11119        .   .
12   6  111190       .   .
13  10  1111900000   1   0.1
14  10  1111901000   1   0.2
;


proc sql;
  select min(id) as id
       , min(level) as level 
       , a.code
       , count(b.weight) as nodes   /*count of terminal nodes*/
       , sum(b.weight) as weight    /*sum of weights of terminal nodes*/
    from 
      have as a 
     ,(select code , weight
       from have
       where level=10   /*selects terminal nodes*/
       ) as b
    where a.code eqt b.code        /*EQT is equivalent to =: */
    group by a.code
  ;
quit;

我不确定这是否正确,但它为示例数据提供了所需的结果。

【讨论】:

  • 我试图在真实数据集上运行它,但它没有给我想要的解决方案。我猜这是因为在真实的数据集中我没有空值/缺失值。我把缺失的值放在那里,所以我想更新什么是清晰可见的,但我想这不是一个好主意。在真实数据集中我已经计算过一次节点和权重,我需要一遍又一遍地计算它,因为终端节点会动态变化(插入、删除和更新权重)。
  • 还有一件更重要的事情我忘了说。同一个代码可以有多个观察值。唯一的密钥对由 3 个变量组成,其中的代码就是其中之一。
  • 更新为使用 where level=10 来查找终端节点。那应该行得通。我认为您可以将 var1 和 var2 添加到 select 语句中,SAS 将为您“重新合并”。尚未测试此编辑。
  • 我认为哈希解决方案会更快。将所有终端节点加载到哈希表中。然后对于数据集中的每条记录,通读哈希表,将与每个节点“匹配”(子节点?)的终端节点的权重相加。这是相同的笛卡尔积思想,但使用哈希表来做这件事可能比 SQL 方法更快。
  • 如果可以有两个终端记录具有相同的代码值,我认为上面的行不通,因为它会将 sum(weight) 分配给它们。我将尝试在接下来的 18 小时内发布哈希方法。
【解决方案6】:

这是估计每条记录的父记录所需的 SQL。它只使用字符串函数(位置和长度),因此它应该适用于任何 SQL 方言,甚至可能是 SAS。 (CTE 可能需要重写为子查询或视图)这个想法是:

  • 向数据集添加 parent_id 字段
  • 查找代码子串最长的记录
  • 并使用它的 id 作为我们 parent_id 的值
  • (之后)从它们的 direct 子节点(child.parent_id = this.id 的节点)的 sum(nodes),sum(weight) 更新记录

顺便说一句:我本可以使用 LEVEL 而不是 LENGTH(code) ;这方面的数据有点多余。


WITH sub AS (
        SELECT id, length(code) AS len
        , code
        FROM tree)
UPDATE tree t
SET parent_id = s.id
FROM sub s
WHERE length(t.code) > s.len AND POSITION (s.code IN t.code) = 1
AND NOT EXISTS (
        SELECT *
        FROM sub nx
        WHERE nx.len > s.len AND POSITION (nx.code IN t.code ) = 1
        AND nx.len < length(t.code) AND POSITION (nx.code IN t.code ) = 1
        )
        ;

SELECT * FROM tree
ORDER BY parent_id DESC NULLS LAST
        , id
        ;

找到父母后,整个表应该从自身更新(重复) 喜欢:


-- PREPARE omg( integer) AS
UPDATE tree  t
SET nodes = s.nodes ,  weight = s.weight
FROM ( SELECT parent_id , SUM(nodes) AS nodes , SUM(weight) AS weight
        FROM tree GROUP BY parent_id) s
WHERE s.parent_id = t.id
        ;

在 SAS 中,这可能通过对 {0-parent_id, id} 进行排序并执行一些保留+求和魔术来完成。 (我的SAS在这方面有点生疏)


更新:如果只有叶节点的 {nodes, weight} 具有非 NULL(非缺失)值,则可以在一次扫描中完成整个树的聚合,而无需先计算 parent_ids:

UPDATE tree  t
SET nodes = s.nodes ,  weight = s.weight
FROM ( SELECT p.id , SUM(c.nodes) AS nodes , SUM(c.weight) AS weight
        FROM tree p
        JOIN tree c ON c.lev > p.lev AND POSITION (p.code IN c.code ) = 1
        GROUP BY p.id
        ) s
WHERE s.id = t.id
        ;

{lev,code} 上的索引可能会加快速度。 (假设 id 上有一个索引)

【讨论】:

  • 谢谢@joop。我也会研究这个解决方案,因为稍后我会做一些代码优化。现在我会坚持使用 Quentin 的解决方案,因为我需要测试整个系统是否正常工作。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-01-09
  • 2021-06-14
  • 2013-02-09
  • 2012-11-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多