SAS 9.4 根据当前值替换当前行之后的所有值

Posted

技术标签:

【中文标题】SAS 9.4 根据当前值替换当前行之后的所有值【英文标题】:SAS 9.4 Replacing all values after current line based on current values 【发布时间】:2018-10-23 15:14:11 【问题描述】:

我正在根据 ID 号匹配文件。我需要用要匹配的ID格式化一个数据集,这样在a列中就不会重复相同的ID号(因为b列的ID是匹配完成后的幸存ID)。我的 ID 列表有超过 100 万个观察值,并且同一 ID 可能在任一列/两列中重复多次。

这是我所拥有/需要的示例:

样本数据

ID1 ID2

1 2    
3 4    
2 5
6 1 
1 7 
5 8    

幸存的 ID 将是:

2    
4    
5

错误 - 1 不再存在 错误 - 1 不再存在 8

我需要什么

ID1 ID2

1 2    
3 4    
2 5    
6 5
5 7
7 8

很明显,我可能是 SAS 新手,但这是我尝试过的,一遍又一遍地重新运行,因为我有一些重复超过 50 次或更多的 ID。

Proc sort data=Have;    
    by ID1;    
run;

这种排序使重复的 ID1 值连续,因此我可以使用 LAG 将被破坏的 ID1 替换为上行中幸存的 ID2。

Data Want;
    set Have;
        by ID1;
    lagID1=LAG(ID1);  
    lagID2=LAG(ID2); 
    If NOT first. ID1 THEN DO;  
        If ID1=lagID1 THEN ID1=lagID2; 
        KEEP ID1 ID2;
        IF ID1=ID2 then delete;
   end;
run;

这种方法有效,但我仍然会得到一些重复项,无论我运行多少次都无法解决(我会循环它,但我不知道如何),因为它们只是在具有其他重复项的 ID 之间来回切换(我可以减少到大约 2,000 个)。

我发现我需要将当前行之后的所有值替换为每个 ID1 值的 ID2,而不是使用 LAG,但我不知道该怎么做。

我想读取观察值 1,在 ID1 或 ID2 列中找到 ID1 值的所有后续实例,并将该值替换为当前观察值的 ID2 值。然后我想用第 2 行重复这个过程,依此类推。

例如,我想在值 1 的第一行之后查找任何实例,并将其替换为 2,因为这是该对的幸存 ID - 1 可能会在任一列,我需要全部替换。第二行将查找后面的值 3 并将它们替换为 4,等等。最终结果应该是 ID 号在 ID1 列中只出现一次(尽管它可能在 ID2 列中出现多次)。

ID1 ID2

1 2    
3 4    
2 5
6 1 
1 7 
5 8 

读取第一行后,数据集如下所示: ID1 ID2

1 2    
3 4    
2 5
6 2 
2 7 
5 8 

读取观察值 2 不会有任何变化,因为 3 不再出现;在观察 3 之后,集合将是:

ID1 ID2

1 2    
3 4    
2 5
6 5 
5 7 
5 8 

同样,观察四不会有任何变化。但观察 5 会导致最终的变化:

ID1 ID2

1 2    
3 4    
2 5
6 5 
5 7 
7 8 

我尝试过使用以下语句,但我什至无法判断我是否走错了路,或者我只是无法弄清楚语法。

Data want;
Set have;
      Do i=_n_;
          ID=ID2;
          Replace next varEUID where (EUID1=EUID1 AND EUID2=EUID1);
      End;
Run;

感谢您的帮助!

【问题讨论】:

您能否更好地解释从数据中获取所需内容的规则是什么? 我希望 ID1 在其第一个实例为 ID1 之后的所有值都替换为相应的第一个实例的 ID2。因此,在示例中,我在第 4 次观察中包含 1 的值将被替换为 2(基于第一次观察),然后是 5,(基于第三次观察)。 仍然没有意义。为什么 6,1 以 6,5 结尾?排序顺序重要吗? 因为在程序中,一旦 1 与 2 匹配,这些就会被输入,因此 1 会被销毁。如果稍后引用 1 则会产生错误,因为 ID 1 不再存在,当记录匹配时它变为 2。 我还是不明白。听起来你在谈论一个像改变人名这样的系统。所以在你的第一个表中,我们看到 1 变为 2,然后 2 变为 5,然后 5 变为 8。所以结果应该是 1、2 和 5 都变为 8。这就是你想要的吗? 【参考方案1】:

无需在数据文件中来回工作。您只需要保留替换信息,以便一次性处理文件。

一种方法是使用 ID 变量的值作为索引创建一个临时数组。对于 ID 值较小的简单示例,这很容易做到。

例如,如果所有 ID 值都是 1 到 1000 之间的整数,那么这一步就可以完成。

data want ;
  set have ;
  array xx (1000) _temporary_;
  do while (not missing(xx(id1))); id1=xx(id1); end;
  do while (not missing(xx(id2))); id2=xx(id2); end;
  output;
  xx(id1)=id2;
run;

您可能需要添加一个测试来防止循环 (1 -> 2 -> 1)。

对于更通用的解决方案,您应该将数组替换为哈希对象。所以是这样的:

data want ;
  if _n_=1 then do;
    declare hash h();
    h.definekey('old');
    h.definedata('new');
    h.definedone();
    call missing(new,old);
  end;
  set have ;
  do while (not h.find(key:id1)); id1=new; end;
  do while (not h.find(key:id2)); id2=new; end;
  output;
  h.add(key: id1,data: id2);
  drop old new;
run;

【讨论】:

ID 实际上是 10 位整数。 100 亿的数组可能对你的记忆来说太大了。所以尝试使用哈希对象来实现相同的逻辑。 使用哈希对象添加示例。【参考方案2】:

这是您建议的算法的实现,使用modify 语句一次加载和重写每一行。它适用于您的简单示例,但对于更混乱的数据,您可能会在 ID1 中获得重复值。

data have;
input ID1 ID2 ;
datalines;
1 2    
3 4    
2 5
6 1 
1 7 
5 8 
;
run;

title "Before making replacements";
proc print data = have;
run;

/*Optional - should improve performance at cost of increased memory usage*/
sasfile have load;

data have;
    do i = 1 to nobs;
        do j = i to nobs;
            modify have point = j nobs = nobs;
            /* Make copies of target and replacement value for this pass */
            if j = i then do;
                id1_ = id1;
                id2_ = id2;
            end;
            else do;
                flag = 0; /* Keep track of whether we made a change */
                if id1 = id1_ then do;
                    id1 = id2_;
                    flag = 1;
                end;
                if id2 = id1_ then do;
                    id2 = id2_;
                    flag = 1;
                end;
                if flag then replace; /* Only rewrite the row if we made a change */                
            end;
        end;
    end;
    stop;
run;

sasfile have close;

title "After making replacements";
proc print data = have;
run;

请记住,由于这会修改数据集,因此在数据步骤运行时中断数据步骤可能会导致数据丢失。确保您有一个备份,以防您需要回滚更改。

【讨论】:

【参考方案3】:

似乎这应该可以解决问题并且相当简单。让我知道它是否是您正在寻找的:

data have;
input id1 id2;
datalines;
1 2    
3 4    
2 5
6 1 
1 7 
5 8 
;
run;

%macro test();
  proc sql noprint;
     select count(*) into: cnt
     from have;
  quit;

  %do i = 1 %to &cnt;
     proc sql noprint;
        select id1,id2 into: id1, :id2
        from have
        where monotonic() = &i;quit;

     data have;
     set have;
     if (_n_ > input("&i",8.))then do;
        if (id1 = input("&id1",8.))then id1 = input("&id2",8.);
        if (id2 = input("&id1",8.))then id2 = input("&id2",8.);
     end;
     run;        
  %end;
%mend test;
%test();

【讨论】:

proc sql 确实效率低下 - 如果要选择特定行,请使用 obsfirstobs 数据集选项来获取该行而不是扫描整个表并寻找匹配的行号。 这到底是如何工作的?我需要递增地从第 1 行到第 n 行 from have where monotonic() = &i 替换为from have(obs = &i firstobs=&i)。两者都给出相同的结果,但前者读取整个表,而后者读取一行。 啊,但不应该是obs=1吗? 不 - obs 选项实际上意味着 lastobs,但它的命名相当混乱。【参考方案4】:

这可能会快一点:

data have2;
input id1 id2;
datalines;
1 2    
3 4    
2 5
6 1 
1 7 
5 8 
;
run;

%macro test2();
   proc sql noprint;
      select count(*) into: cnt
      from have2;
   quit;

   %do i = 1 %to &cnt;
      proc sql noprint;
         select id1,id2 into: id1, :id2
         from have2
         where monotonic() = &i;

         update have2 set id1 = &id2         
         where monotonic() > &i
         and id1 = &id1;
      quit;
      proc sql noprint;
         update have2 set id2 = &id2         
         where monotonic() > &i
         and id2 = &id1;
      quit;
%end;
%mend test2;
%test2();

【讨论】:

以上是关于SAS 9.4 根据当前值替换当前行之后的所有值的主要内容,如果未能解决你的问题,请参考以下文章

9.4/9.5 sed

根据最后一个 LARGEST 值替换数据框中当前值的最快方法

根据最后一个 LARGEST 值替换数据框中当前值的最快方法

六周第二次课

Spark SQL 使用窗口 - 根据列条件从当前行之后的行中收集数据

用RunASDate解决SAS 9.4许可证过期的问题