Merge 在SAS中,如何基于某个ID变量将多行折叠为一行?

Merge 在SAS中,如何基于某个ID变量将多行折叠为一行?,merge,sas,rows,collapse,transpose,Merge,Sas,Rows,Collapse,Transpose,我正在处理的数据目前的形式如下: 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

我正在处理的数据目前的形式如下:

 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
 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
我想编写SQL代码,以以下形式获取数据:

 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
 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转置来完成,但我不确定执行多重转置的最有效方法。我应该注意到,我有超过50000个独特的ID,每个ID都有不同数量的药物、剂量和相应的填充日期。我想为那些没有数据可填充的列返回null/empty值。提前感谢。

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

唯一需要注意的是提前知道每个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);

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

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

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或2GB的数据在这里可以正常工作,特别是在不需要对它们进行排序的情况下

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


不过,这一切都取决于你的数据,而你的数据实际上是最好的。我可能会先尝试数据步/转置:这是最简单的,也是大多数其他程序员以前见过的,因此它很可能是最好的解决方案,除非它的速度慢得令人望而却步。

以下是我尝试的基于阵列的解决方案:

/*  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版本,还必须从宏变量中删除空白。修正-谢谢汤姆


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

我建议读这篇文章:嗯,这一定是一个重复,但是,在找到一个好的重复问题候选者时遇到了困难。@RobertPenridge我找到了类似的帖子,但没有一篇涉及到在同一集中确实需要转换变量(药物、剂量、填充日期)和不需要转换变量(性别、种族)的问题。似乎必须有一种比逐段转置更有效的方法,然后一次又一次地合并..可能是@RobertPenridge的副本-正式提出的副本。这个问题的一些答案可以推广到转置多个变量,而不需要做太多的工作。我添加了一个基于数组的答案,它分两个数据步骤来完成。如果让SQL为您修剪宏变量,则不必从SQL生成的宏变量中修剪空格<代码>插入:X已修剪或
插入:X,以“”分隔