【问题标题】:In SAS, how do you collapse multiple rows into one row based on some ID variable?在 SAS 中,如何根据某个 ID 变量将多行折叠成一行?
【发布时间】:2015-12-10 21:24:34
【问题描述】:

我正在处理的数据目前采用以下形式:

 ID     Sex      Race         Drug         Dose          FillDate  
 1      M        White        ziprosidone  100mg         10/01/98     
 1      M        White        ziprosidone  100mg         10/15/98
 1      M        White        ziprosidone  100mg         10/29/98
 1      M        White        ambien       20mg          01/07/99
 1      M        White        ambien       20mg          01/14/99
 2      F        Asian        telaprevir   500mg         03/08/92
 2      F        Asian        telaprevir   500mg         03/20/92
 2      F        Asian        telaprevir   500mg         04/01/92

我想编写 SQL 代码来获取以下形式的数据:

 ID     Sex    Race      Drug1        DrugDose1     FillDate1_1     FillDate1_2     FillDate1_3    Drug2     DrugDose2   FillDate2_1     FillDate2_2     FillDate2_3     
 1      M      White     ziprosidone  100mg         10/01/98        10/15/98        10/29/98       ambien    20mg        01/07/99        01/14/99        null
 2      F      Asian     telaprevir   500mg         03/08/92        03/20/92        04/01/92       null      null        null            null            null

每个唯一 ID 只需要一行,所有唯一药物/剂量/填充信息在列中,而不是行中。我想它可以使用 PROC TRANSPOSE 来完成,但我不确定进行多个转置的最有效方法。我应该注意,我有超过 50,000 个唯一 ID,每个 ID 都有不同数量的药物、剂量和相应的填充日期。我想为那些没有数据要填写的列返回空值/空值。提前致谢。

【问题讨论】:

  • 我建议阅读这篇论文:sascommunity.org/wiki/…
  • 嗯,这肯定是个骗子,不过很难找到一个好的重复问题候选者。
  • @RobertPenr​​idge 我发现 similar 帖子,但没有一个解决需要转置变量(药物、剂量、填充日期)且不需要转置的问题(性别、种族)在同一个集合中。似乎必须有一种比零碎转置然后一遍又一遍合并更有效的方法..
  • @RobertPenr​​idge - 正式提出重复。该问题的一些答案可以概括为转置多个变量而无需太多工作。

标签: merge sas rows collapse transpose


【解决方案1】:

在某种程度上,这种期望的效率决定了最佳的解决方案。

例如,假设您知道填充日期的最大合理数量,您可以使用以下方法非常快速地获得一个转置表 - 这可能是最快的方法 - 但代价是需要大量的后期 -处理,因为它会输出很多你并不真正想要的数据。

proc summary data=have nway;
class id sex race;
output out=want (drop=_:) 
        idgroup(out[5] (drug dose filldate)=) / autoname;
run;

另一方面,就不需要额外步骤而言,垂直转置是“最佳”解决方案;虽然它可能很慢。

data have_t;
  set have;
  by id sex race drug dose notsorted;
  length varname value $64; *some reasonable maximum, particularly for the drug name;
  if first.ID then do;
    drugcounter=0;
  end;     
  if first.dose then do;
    drugcounter+1; 
    fillcounter=0;
    varname = cats('Drug',drugcounter);
    value   = drug;
    output;
    varname = cats('DrugDose',drugcounter);
    value = dose;
    output;
  end;
  call missing(value);
  fillcounter+1;
  varname=cats('Filldate',drugcounter,'_',fillcounter);
  value_n = filldate;
  output;
run;
proc transpose data=have_t(where=(not missing(value))) out=want_c;
  by id sex race ;
  id varname;
  var value;
run;
proc transpose data=have_t(where=(not missing(value_n))) out=want_n;
  by id sex race ;
  id varname;
  var value_n;
run;

data want;
  merge want_c want_n;
  by id sex race;
run;

确实,这并不是很慢,而且很可能对您的 50k ID 来说没问题(尽管您没有说有多少毒品)。 1 或 2 GB 的数据在这里可以正常工作,尤其是当您不需要对它们进行排序时。

最后,还有其他一些介于两者之间的解决方案。您可以在数据步骤中完全使用数组进行转置,这可能是最好的折衷方案;您必须提前确定数组的最大界限,但这不是世界末日。

不过,这完全取决于您的数据,这确实是最好的。我可能会先尝试数据步进/转置:这是最直接的,也是大多数其他程序员以前见过的,所以它很可能是最好的解决方案,除非它非常慢。

【讨论】:

  • 我添加了一个基于数组的答案,它在 2 个数据步骤中完成。
【解决方案2】:

考虑以下使用两个派生表(内表和外表)的查询,该表按FillDate 顺序建立了序数行计数。然后,使用行数、if/then 或 case/when 逻辑用于迭代列。外部查询采用idsexrace 分组的最大值。

唯一需要注意的是提前知道每个 ID 的预期或最大行数(即我们的表浏览的另一个查询)。因此,根据需要填写省略号 (...)。请注意,对于不适用于特定 ID 的列,将生成缺失。当然,请调整为实际的数据集名称。

proc sql;
CREATE TABLE DrugTableFlat AS ( 
SELECT id, sex, race,
       Max(Drug_1) As Drug1, Max(Drug_2) As Drug2, Max(Drug_3) As Drug3, ...
       Max(Dose_1) As Dose1, Max(Dose_2) As Dose2, Max(Dose_3) As Dose3, ...
       Max(FillDate_1) As FillDate1, Max(FillDate_2) As FillDate2, 
       Max(FillDate_3) As FillDate3 ...
FROM 
   (SELECT id, sex, race,
       CASE WHEN RowCount=1 THEN Drug END AS Drug_1,
       CASE WHEN RowCount=2 THEN Drug END AS Drug_2,
       CASE WHEN RowCount=3 THEN Drug END AS Drug_3,
       ...
       CASE WHEN RowCount=1 THEN Dose END AS Dose_1,
       CASE WHEN RowCount=2 THEN Dose END AS Dose_2,
       CASE WHEN RowCount=3 THEN Dose END AS Dose_3,
       ...
       CASE WHEN RowCount=1 THEN FillDate END AS FillDate_1,
       CASE WHEN RowCount=2 THEN FillDate END AS FillDate_2,
       CASE WHEN RowCount=3 THEN FillDate END AS FillDate_3,
       ...
    FROM
       (SELECT t1.id, t1.sex, t1.race, t1.drug, t1.dose, t1.filldate,
          (SELECT Count(*) FROM DrugTable t2 
           WHERE t1.filldate >= t2.filldate AND t1.id = t2.id) As RowCount
        FROM DrugTable t1) AS dT1
    ) As dT2
GROUP BY id, sex, race);

【讨论】:

    【解决方案3】:

    这是我对基于数组的解决方案的尝试:

    /*  Import data */
     data have; 
     input @2 ID  @9 Sex $1. @18 Race $5. @31 Drug $11. @44 Dose $5. @58 FillDate mmddyy8.;
     format filldate yymmdd10.;
     cards;
     1      M        White        ziprosidone  100mg         10/01/98     
     1      M        White        ziprosidone  100mg         10/15/98
     1      M        White        ziprosidone  100mg         10/29/98
     1      M        White        ambien       20mg          01/07/99
     1      M        White        ambien       20mg          01/14/99
     2      F        Asian        telaprevir   500mg         03/08/92
     2      F        Asian        telaprevir   500mg         03/20/92
     2      F        Asian        telaprevir   500mg         04/01/92
     ;
     run;
    
    
    /* Calculate array bounds - SQL version  */
    proc sql _method noprint;
        select DATES into :MAX_DATES_PER_DRUG trimmed from 
            (select count(ID) as DATES from have group by ID, drug, dose)
            having DATES = max(DATES);
        select max(DRUGS) into :MAX_DRUGS_PER_ID trimmed from 
            (select count(DRUG) as DRUGS from 
                (select distinct DRUG, ID from have)
                group by ID
            )
        ;       
    quit;
    
    /* Calculate array bounds - data step version */
    data _null_;
        set have(keep = id drug) end = eof;
        by notsorted id drug;
        retain max_drugs_per_id max_dates_per_drug;
        if first.id   then drug_count = 0;
        if first.drug then do;
            drug_count + 1;
            date_count = 0;
        end;
        date_count + 1;
        if last.id      then max_drugs_per_id   = max(max_drugs_per_id,     drug_count);
        if last.drug    then max_dates_per_drug = max(max_dates_per_drug,   date_count);
        if eof then do;
            call symput("max_drugs_per_id"  ,cats(max_drugs_per_id));
            call symput("max_dates_per_drug",cats(max_dates_per_drug));     
        end;
    run;
    
    
    /* Check macro vars */
    %put MAX_DATES_PER_DRUG = "&MAX_DATES_PER_DRUG";
    %put MAX_DRUGS_PER_ID   = "&MAX_DRUGS_PER_ID";
    
    /* Transpose */
    data want;
        if 0 then set have;
        array filldates[&MAX_DRUGS_PER_ID,&MAX_DATES_PER_DRUG] 
        %macro arraydef;
            %local i;
            %do i = 1 %to &MAX_DRUGS_PER_ID;
                filldates&i._1-filldates&i._&MAX_DATES_PER_DRUG
            %end;
        %mend arraydef;
        %arraydef;
        array drugs[&MAX_DRUGS_PER_ID] $11;
        array doses[&MAX_DRUGS_PER_ID] $5;
        drug_count = 0;
        do until(last.id);
            set have;
            by ID drug dose notsorted;
            if first.drug then do;
                date_count = 0;
                drug_count + 1;
                drugs[drug_count] = drug;
                doses[drug_count] = dose;
            end;
            date_count + 1;
            filldates[drug_count,date_count] = filldate;
        end;
        drop drug dose filldate drug_count date_count;
        format filldates: yymmdd10.;
    run;
    

    用于计算数组边界的数据步骤代码可能比 SQL 版本更有效,但也更冗长。 另一方面,在 SQL 版本中,您还必须从宏变量中删除空格。已修复 - 感谢 Tom!

    与其他答案中的 proc transpose / proc sql 选项相比,转置数据步骤可能也更有效,因为它只进一步通过数据集,但同样也相当复杂。

    【讨论】:

    • 如果您告诉 SQL 为您修剪它们,您不必修剪 SQL 生成的宏变量中的空格。 into :X trimmedinto :X separated by ' '
    猜你喜欢
    • 2017-11-14
    • 1970-01-01
    • 2021-04-07
    • 2021-10-22
    • 1970-01-01
    • 2017-11-10
    • 2020-05-25
    • 2018-10-13
    • 1970-01-01
    相关资源
    最近更新 更多