在 SAS 中,如何根据某个 ID 变量将多行合并为一行?
In SAS, how do you collapse multiple rows into one row based on some ID variable?
我正在处理的数据目前采用以下形式:
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 只需要一行,所有唯一 drug/dose/fill 信息都在列中,而不是在行中。我想它可以使用 PROC TRANSPOSE 来完成,但我不确定进行多重转置的最有效方法。我应该注意到,我有超过 50,000 个唯一 ID,每个 ID 都有不同的药物数量、剂量和相应的填充日期。我想 return null/empty 那些没有数据要填写的列的值。在此先感谢。
考虑以下使用两个派生的 tables(内部和外部)的查询,该查询按 FillDate
顺序建立序数行计数。然后,使用行计数,if/then 或 case/when 逻辑用于迭代列。外部查询采用按 id
、sex
、race
.
分组的最大值
唯一需要注意的是提前知道每个 ID 的预期行数或最大行数(即我们 table 浏览的另一个查询)。因此,根据需要填写省略号 (...
)。请注意,对于不适用于特定 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);
在某种程度上,所需的效率决定了最佳解决方案。
例如,假设您知道填充日期的最大合理数量,您可以使用以下方法快速获得转置 table - 可能是最快的方法 - 但需要付出代价大量 post-processing,因为它会输出大量你并不真正想要的数据。
proc summary data=have nway;
class id sex race;
output out=want (drop=_:)
idgroup(out[5] (drug dose filldate)=) / autoname;
run;
另一方面,就不需要额外步骤而言,垂直转置是 "best" 解决方案;虽然它可能会很慢。
data have_t;
set have;
by id sex race drug dose notsorted;
length varname value ; *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 的数据在这里可以正常工作,特别是如果您不需要对它们进行排序。
最后,还有一些介于两者之间的其他解决方案。您可以在数据步骤中完全使用数组进行转置,一方面,这可能是最好的折衷方案;您必须提前确定数组的最大范围,但这不是世界末日。
不过,这完全取决于您的数据,这确实是最好的。我可能会首先尝试数据 step/transpose:这是最直接的,也是大多数其他程序员以前见过的,所以它很可能是最好的解决方案,除非它非常慢。
这是我对基于数组的解决方案的尝试:
/* Import data */
data have;
input @2 ID @9 Sex . @18 Race . @31 Drug . @44 Dose . @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] ;
array doses[&MAX_DRUGS_PER_ID] ;
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 版本,您还必须 trim 宏变量 中的空格。已修复 - 谢谢汤姆!
与其他答案中的 proc transpose
/ proc sql
选项相比,转置数据步骤可能也处于更有效的范围,因为它只进一步通过数据集 1 , 但它也相当复杂。
我正在处理的数据目前采用以下形式:
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 只需要一行,所有唯一 drug/dose/fill 信息都在列中,而不是在行中。我想它可以使用 PROC TRANSPOSE 来完成,但我不确定进行多重转置的最有效方法。我应该注意到,我有超过 50,000 个唯一 ID,每个 ID 都有不同的药物数量、剂量和相应的填充日期。我想 return null/empty 那些没有数据要填写的列的值。在此先感谢。
考虑以下使用两个派生的 tables(内部和外部)的查询,该查询按 FillDate
顺序建立序数行计数。然后,使用行计数,if/then 或 case/when 逻辑用于迭代列。外部查询采用按 id
、sex
、race
.
唯一需要注意的是提前知道每个 ID 的预期行数或最大行数(即我们 table 浏览的另一个查询)。因此,根据需要填写省略号 (...
)。请注意,对于不适用于特定 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);
在某种程度上,所需的效率决定了最佳解决方案。
例如,假设您知道填充日期的最大合理数量,您可以使用以下方法快速获得转置 table - 可能是最快的方法 - 但需要付出代价大量 post-processing,因为它会输出大量你并不真正想要的数据。
proc summary data=have nway;
class id sex race;
output out=want (drop=_:)
idgroup(out[5] (drug dose filldate)=) / autoname;
run;
另一方面,就不需要额外步骤而言,垂直转置是 "best" 解决方案;虽然它可能会很慢。
data have_t;
set have;
by id sex race drug dose notsorted;
length varname value ; *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 的数据在这里可以正常工作,特别是如果您不需要对它们进行排序。
最后,还有一些介于两者之间的其他解决方案。您可以在数据步骤中完全使用数组进行转置,一方面,这可能是最好的折衷方案;您必须提前确定数组的最大范围,但这不是世界末日。
不过,这完全取决于您的数据,这确实是最好的。我可能会首先尝试数据 step/transpose:这是最直接的,也是大多数其他程序员以前见过的,所以它很可能是最好的解决方案,除非它非常慢。
这是我对基于数组的解决方案的尝试:
/* Import data */
data have;
input @2 ID @9 Sex . @18 Race . @31 Drug . @44 Dose . @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] ;
array doses[&MAX_DRUGS_PER_ID] ;
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 版本,您还必须 trim 宏变量 中的空格。已修复 - 谢谢汤姆!
与其他答案中的 proc transpose
/ proc sql
选项相比,转置数据步骤可能也处于更有效的范围,因为它只进一步通过数据集 1 , 但它也相当复杂。