写在前面。
最近的两篇文章分别介绍了我编写下面这两个宏时整体的思路和方法。菜鸟小白一枚,很欢迎有大佬能够阅读并批评指点。
SAS编程实践---宏:按系统术语、首选术语和严重程度分层次计算受试者发生不良反应(AE)例数和例次
SAS编程实践---宏:按系统术语和首选术语分层次计算受试者发生不良反应(AE)例数和例次
文章中呈现的是最终的宏代码,但是在实际编写过程中,步骤顺序
可能会和文章的结构顺序不同,毕竟写文章和写代码还是有区别的
。
代码编写时可能会经历,编写---替换修改---增添---替换修改---...
,这样的步骤很多次才形成了最终的代码。
不过目前我坚持的编写宏程序的总体结构
还是如之前的文章一样,也即:
*_1. pre-processing;
*_2.main statistical steps;
*_3 processing step of stat;
* _4.output steps;
- 预处理(宏变量的处理和输入数据集的预处理)
- 主要的统计步骤
- 统计步骤后的处理
- 输出步骤
基本原则
在实际操作中,最开始进行目标表格拆解分析
和模拟数据生成
后,进行到宏程序代码编写步骤,会先进行上述第1步中的输入数据集的预处理
和第2步的统计步骤
。
这两步是处理数据的最核心的步骤,而其他的步骤,更多是为了进行表格拼接修改
,使达到目标表格样式
;或者基于SAS宏的”文本替换“
原则进行的辅助步骤。
- 说明一下,很多步骤使用
PROC SQL
语句会简单许多,但是本文中除了少量的宏变量的赋值会使用PROC SQL
语句以外,还是以使用基础DATA
步和过程步
为主。
准备步骤
说了这么多,下文我便一步一步实践去编写一个”基本描述性统计量表格“
的宏的编写。
目标表格拆解分析
图中是将要生成的目标表格的模板:
指明了
数值型变量
,进行核心统计步proc means
需要计算的统计指标有哪些;需要注意nminssing
,可能存在分析数据集里观测的缺失
,所以我一般使用adsl
数据集进行各组受试者数量的确定;表明了
在拼接步需要拼接成的样式
,需要注意,小数点位数的保留问题
,因此需要在核心步骤之外进行小数点位数保留问题的处理;合计列是否计算
,需要条件选择语句
,以及相应的数据预处理和计算步骤;指明了
分组
,分组变量是必须存在的
,且我一般尽量会处理为数值型
。
模拟数据生成
通过目标表格的拆解,明确了那些模拟数据需要那些变量。
分析数据集
分析数据集
必须要包含了分组变量
和待统计分析的数值型变量
。
我使用如下代码生成模拟数据:
%let seed1 = 33333333;
data ad0;
do ii = 1 to 3;
armn = ii;
arm = cats("第",put(ii, best.) ,"组");
do jj = 1 to 100;
usubjid = cats( "X",put(ii, best.) ,"-", put(jj, z3.));
if mod(jj,3) = 0 then
SEX = "女";
else SEX = "男";
HEIGHT = round(ranuni(&seed1.)*30 + 155, 0.1);
output;
end;
end;
run;
同时,人为地造成了若干条观测的缺失
:
data ad;
set ad0;
if jj in (2 13 17) and sex = "男" then
delete;
if jj in (12 7 24) and sex = "女" then
delete;
run;
adsl数据集
adsl
数据集主要用来计算受试者的数量
,需要包含分组变量信息
;
data adsl;
do ii = 1 to 3;
armn = ii;
arm = cats("第",put(ii, best.) ,"组");
do jj = 1 to 100;
usubjid = cats( "X",put(ii, best.) ,"-", put(jj, z3.));
output;
end;
end;
run;
宏编写步骤
模拟数据有了,明确了所需的统计指标,接下来就是先使用模拟数据把数据预处理
和统计的步骤
写出来,生成目标表格
。
数据预处理和核心统计步骤
分组统计
- 我们这个例子是进行身高的描述性统计,使用
proc means
根据分组变量统计HEIGHT
;
* _2.main statistical steps; ;
* _2.1 Calculation of target variable statistics;
* _2.1.1 by group ;
* _2.1.1.1 target variable;
proc means data=ad noprint ;
var HEIGHT;
by armn ;
output out=_bygrpm0 N= n nmiss=nmiss mean=mean STD=std median = mid q1=q1 q3= q3 max=max min=min;
run;
- 分组统计
adsl
数据集中受试者的数量,为了计算nmissing
指标;
* _2.1.1.2 Calculation of the number of every group;
proc freq data=adsl ;
table armn / out=_bygrpnum;
run;
注意,这一步也可以使用proc sql进行计算然后赋值为宏变量会更加简单。
- 合并上述两个数据集
* _2.1.1.3 combine ;
proc sort data=_bygrpm0; by armn;run;
data _bygrp;
merge _bygrpm0
_bygrpnum(drop = percent);
by armn;
run;
分组统计目标变量到这里其实都准备好了。
合计列
接下来是合计列指标的计算;
- 合计数据的预处理
*_1. pre-processing;
* _1.2.1 for sum column ;
data _adsum;
set ad;
armn = 999;
arm = "合计";
run;
data _adslall;
set adsl;
armn = 999;
arm = "合计";
run;
- 合计列统计量的计算
* _2.main statistical steps; ;
* _2.2 Calculation of sum column statistics;
* _2.2.1 Calculation of sum column of target variable statistics;
proc means data=_adsum noprint ;
var HEIGHT;
by armn;
output out=_summ0(drop=_TYPE_) N= n nmiss=nmiss mean=mean STD=std median = mid q1=q1 q3= q3 max=max min=min;
run;
- 受试者总数,为了构建nmissing
* _2.2.2 Calculation of number of all subject;
proc freq data=_adslall noprint;
table armn / out=_sumnum;
proc sort ; by armn;
run;
- 合并上述两个数据集
* _2.2.3 combine ;
proc sort data=_summ0; by armn;run;
data _sum;
merge _summ0
_sumnum(drop = percent);
by armn;
run;
统计后处理-数据拼接
接下来的步骤,我一般会这样做,根据横向排列的统计量,使用字符拼接函数拼成所需的样式,然后使用proc transpose
进行重新的塑型生成目标样式
。
不过为了简便些,可以先把分组统计的和最后的合计列数据集先set
,然后再进行后续步骤,在宏代码里,就涉及到条件判断了。文章中为了分步展示,我先赋值个宏变量(包装进宏时删除就好),拆开步骤单独展示。
判断是否计算合计列的宏参数我预设为rowsumyn
,为了提醒自己,我会在日志中产生一个警告
:
%let rowsumyn = Y;
* _3 processing step of stat;
* _3.1 processing step of stat;
%if %sysfunc(upcase(&rowsumyn.)) = %str(Y) %then %do;
%put WARNING: 将计算合计列;
data _dt4comb;
set _bygrp
_sum;
run;
%end;
%else %do;
%put WARNING: 不计算合计列;
data _dt4comb;
set _bygrp;
run;
%end;
- 接下来是拼接步骤
* _3.2 Variable construction of the target format;
data _combdt;
length _1nnmiss _2meansd _3median _4q1q3 _5minmax $200.;
set _dt4comb;
_1nnmiss =cats( strip(put(n ,8.0)) , "(",strip(put(count - n , 8.0)), ")");
_2meansd = cats( strip(put( round(mean , 0.01 ), 8.2) ) , "(" ,strip(put( round(std, 0.001), 8.3) ) , ")");
_3median = cats( strip(put( round(mid , 0.01 ) , 8.2) ) );
_4q1q3 = cats( strip(put( round(q1 , 0.01 ) , 8.2) ) , "~" ,strip(put( round(q3 , 0.01 ) , 8.2) ) );
_5minmax = cats( strip(put( round(min , 0.01 ) , 8.1) ) , "~" ,strip(put( round(max , 0.01 ) , 8.1) ) );
keep armn _1nnmiss _2meansd _3median _4q1q3 _5minmax;
proc sort; by armn;
run;
注意,这个步骤的代码是初步的,因为这里是硬性规定了位数的取舍,而实际上,是还有一步关于小数位数问题的宏变量的预处理
。
统计后处理-数据塑形
- 之后通过
2次数据塑形
,形成目标表格的样式
* _3.3 transpose;
proc transpose data=_combdt out=_trans1;
var _1nnmiss _2meansd _3median _4q1q3 _5minmax;
by armn;
proc sort; by _NAME_ ;
run;
proc transpose data=_trans1 out=_trans2 prefix= _grp;
id armn;
var col1;
by _NAME_ ;
run;
如下图所示:
可以看到,和目标表格基本上只有细节上的差别的,这些差别的处理就是一些辅助步骤要解决的问题了。
替换并包装成宏
宏参数及替换
接下来就是确定宏参数名称,这个宏我限定如下这些参数:
%macro MeanT(
libin=,
dtin=,
adsl=,
var=,
grpvarn=,
rowsumyn=,
label=,
append=,
libout,
dtout=
);
替换这一步我就不展示。使用SAS EG
的工具栏-编辑-替换
或者Ctrl + H
进行代码替换
。
然后包装进宏里,试着运行一下。
注意到这一步,不要最后的4
个参数,是可以运行成功的。
小数点位数
不同的统计指标要保留的小数点位数是不一样的,更和原始数据的位数有关。
我不知道不同公司是否要求有所不同,我依据我们公司的原则,限定如下:
-
n
和nmissing
都是整数部分;
min
和max
指标保留位数,和原始数据最大小数位数一致
;- 当原始数据最大小数位数
(decmax)
小于等于2
时:mean、median、q1
和q3
保留decmax + 1
位;std
保留decmax + 2
位;- 当原始数据最大小数位数
(decmax)
等于3
时:mean、median、q1
和q3
保留decmax + 1
位;std
保留4
位;- 当原始数据最大小数位数
(decmax)
大于3
时:mean、median、q1、q3
和std
均保留4
位;
好的,根据这些原则,下面利用代码实现。
- 待分析变量的小数位数宏变量
data _stdt0;
set &libin..&dtin.;
run;
data _adsl;
set &adsl.;
run;
* _1. pre-processing;
* _1.1 decimals ;
data _dec;
set _stdt0;
dec = length(scan(strip(put(&var., best.)),2,"."));
if not index(&var.,".") then dec = 0;
run;
proc sql ;
select max(dec) into: decmax
from _dec
;
quit;
%put 最大小数位数:&decmax.;
通过上面的代码,将待分析变量的最大小数位数赋值给宏变量decmax
。
- 拼接步骤代码的修改
这样子,上述拼接步骤的代码就可以按照小数点保留的原则
进行修改了,修改后的代码如下:
* _3.2 Variable construction of the target format;
data _combdt;
length _1nnmiss _2meansd _3median _4q1q3 _5minmax $200.;
set _dt4comb;
_1nnmiss =cats( strip(put(n ,8.0)) , "(",strip(put(count - n , 8.0)), ")");
_5minmax = cats( strip(put( round(min , 10**-( &decmax.) ) , 8.%eval( &decmax.)) ) , "~" ,strip(put( round(max , 10**-( &decmax.) ) , 8.%eval( &decmax.)) ) );
%if &decmax. <= 2 %then %do;
_2meansd = cats( strip(put( round(mean , 10**-( &decmax. + 1) ), 8.%eval( &decmax. + 1) ) ) , "(" ,strip(put( round(std, 10**-( &decmax. + 2)), 8.%eval( &decmax. + 2)) ) , ")");
_3median = cats( strip(put( round(mid , 10**-( &decmax. + 1) ) , 8.%eval( &decmax. + 1) ) ) );
_4q1q3 = cats( strip(put( round(q1 , 10**-( &decmax. + 1) ) , 8.%eval( &decmax. + 1) ) ) , "~" ,strip(put( round(q3 , 10**-( &decmax. + 1) ) , 8.%eval( &decmax. + 1) ) ) );
%end;
%if &decmax. = 3 %then %do;
_2meansd = cats( strip(put( round(mean , 10**-( &decmax. + 1) ), 8.%eval( &decmax. + 1) ) ) , "(" ,strip(put( round(std, 0.0001), 8.4) ) , ")");
_3median = cats( strip(put( round(mid , 10**-( &decmax. + 1) ) , 8.%eval( &decmax. + 1) ) ) );
_4q1q3 = cats( strip(put( round(q1 , 10**-( &decmax. + 1) ) , 8.%eval( &decmax. + 1) ) ) , "~" ,strip(put( round(q3 , 10**-( &decmax. + 1) ) , 8.%eval( &decmax. + 1) ) ) );
%end;
%if &decmax. >3 %then %do;
_2meansd = cats( strip(put( round(mean , 0.0001 ), 8.4 ) ) , "(" ,strip(put( round(std, 0.0001), 8.4) ) , ")");
_3median = cats( strip(put( round(mid , 0.0001 ) , 8.4 ) ) );
_4q1q3 = cats( strip(put( round(q1 ,0.0001 ) , 8.4 ) ) , "~" ,strip(put( round(q3 , 0.0001 ) , 8.4 ) ) );
%end;
keep &grpvarn. _1nnmiss _2meansd _3median _4q1q3 _5minmax;
proc sort; by &grpvarn.;
run;
列名的修改
*_3.4 rename column;
proc contents data=_trans2 out=_info noprint;
proc sort;
by varnum;
run;
proc sql noprint;
select count(distinct NAME) , NAME into: varn, :vnam1 -:vnam99 from _info;
quit;
data _&dtout.;
set _trans2;
%do ii = 1 %to &varn.;
%let jj = %eval(&ii. - 1);
rename &&vnam&ii. = C&jj.;
%end;
run;
label
和第一列
的修改
在目标表格的第一行是我们统计的指标的名称,添加代码如下:
* _3.5 label ;
data _row1;
length C0 $200.;
C0 = cats("&label.");
run;
data _&dtout._;
set _row1
_&dtout.;
if c0 = "_1nnmiss" then C0 = "n(nmisssing)";
if c0 = "_2meansd" then C0 = "Mean(SD)";
if c0 = "_3median" then C0 = "Median";
if c0 = "_4q1q3" then C0 = "Q1~Q3";
if c0 = "_5minmax" then C0 = "Min~Max";
run;
append
有时候要统计的变量不止一个,而我们又想下一个跟在上一个之后,因此设置了一个append
参数
* _3.6 append ;
%if %sysfunc(upcase(&append.)) = %str(Y) and %sysfunc(exist(&dtout., data) ) %then %do;
data &dtout.;
set %if %sysfunc(exist(&dtout., data) ) %then &dtout.; _&dtout._ ;
run;
%end;
%else %do;
%put WARNING: 未追加数据集;
data &dtout.;
set _&dtout._ ;
run;
%end;
数据集输出和清除临时数据集
* _4 output data ;
* _4.1 output ;
data &libout..&dtout.;
set &dtout.;
run;
* _4.2 remove temp datasets ;
proc datasets lib=work;
delete _:;
run;
总结
以上就是我写这个宏的最主要代码,还有一些小判断就不展示了。
完整代码如下:
%macro MeanT(
libin=,
dtin=,
adsl=,
var=,
grpvarn=,
rowsumyn=,
label =,
append=,
libout=,
dtout=
);
data _stdt0;
set &libin..&dtin.;
run;
data _adsl;
set &adsl.;
run;
* _1. pre-processing;
* _1.1 decimals ;
data _dec;
set _stdt0;
dec = length(scan(strip(put(&var., best.)),2,"."));
if not index(&var.,".") then dec = 0;
run;
proc sql ;
select max(dec) into: decmax
from _dec
;
quit;
%put 最大小数位数:&decmax.;
* _1.2.1 for sum column ;
data _adsum;
set _stdt0;
&grpvarn. = 999;
arm = "合计";
run;
data _adslall;
set _adsl;
&grpvarn. = 999;
arm = "合计";
run;
* _2.main statistical steps; ;
* _2.1 Calculation of target variable statistics;
* _2.1.1.1 by group;
proc means data=_stdt0 noprint ;
var &var.;
by &grpvarn. ;
output out=_bygrpm0 N= n nmiss=nmiss mean=mean STD=std median = mid q1=q1 q3= q3 max=max min=min;
run;
* _2.1.1.2 Calculation of number of every group;
proc freq data=_adsl noprint;
table &grpvarn. / out=_bygrpnum;
proc sort ; by &grpvarn.;
run;
* _2.1.1.3 combine ;
proc sort data=_bygrpm0; by &grpvarn.;run;
data _bygrp;
merge _bygrpm0
_bygrpnum(drop = percent);
by &grpvarn.;
run;
* _2.main statistical steps; ;
* _2.2 Calculation of sum column statistics;
* _2.2.1 Calculation of sum column of target variable statistics;
proc means data=_adsum noprint ;
var &var.;
by &grpvarn.;
output out=_summ0(drop=_TYPE_) N= n nmiss=nmiss mean=mean STD=std median = mid q1=q1 q3= q3 max=max min=min;
run;
* _2.2.2 Calculation of number of all subject;
proc freq data=_adslall noprint;
table &grpvarn. / out=_sumnum;
proc sort ; by &grpvarn.;
run;
* _2.2.3 combine ;
proc sort data=_summ0; by &grpvarn.;run;
data _sum;
merge _summ0
_sumnum(drop = percent);
by &grpvarn.;
run;
* _3 processing step of stat;
* _3.1 processing step of stat;
%if %sysfunc(upcase(&rowsumyn.)) = %str(Y) %then %do;
%put WARNING: 将计算合计列;
data _dt4comb;
set _bygrp
_sum;
run;
%end;
%else %do;
%put WARNING: 不计算合计列;
data _dt4comb;
set _bygrp;
run;
%end;
* _3.2 Variable construction of the target format;
data _combdt;
length _1nnmiss _2meansd _3median _4q1q3 _5minmax $200.;
set _dt4comb;
_1nnmiss =cats( strip(put(n ,8.0)) , "(",strip(put(count - n , 8.0)), ")");
_5minmax = cats( strip(put( round(min , 10**-( &decmax.) ) , 8.%eval( &decmax.)) ) , "~" ,strip(put( round(max , 10**-( &decmax.) ) , 8.%eval( &decmax.)) ) );
%if &decmax. <= 2 %then %do;
_2meansd = cats( strip(put( round(mean , 10**-( &decmax. + 1) ), 8.%eval( &decmax. + 1) ) ) , "(" ,strip(put( round(std, 10**-( &decmax. + 2)), 8.%eval( &decmax. + 2)) ) , ")");
_3median = cats( strip(put( round(mid , 10**-( &decmax. + 1) ) , 8.%eval( &decmax. + 1) ) ) );
_4q1q3 = cats( strip(put( round(q1 , 10**-( &decmax. + 1) ) , 8.%eval( &decmax. + 1) ) ) , "~" ,strip(put( round(q3 , 10**-( &decmax. + 1) ) , 8.%eval( &decmax. + 1) ) ) );
%end;
%if &decmax. = 3 %then %do;
_2meansd = cats( strip(put( round(mean , 10**-( &decmax. + 1) ), 8.%eval( &decmax. + 1) ) ) , "(" ,strip(put( round(std, 0.0001), 8.4) ) , ")");
_3median = cats( strip(put( round(mid , 10**-( &decmax. + 1) ) , 8.%eval( &decmax. + 1) ) ) );
_4q1q3 = cats( strip(put( round(q1 , 10**-( &decmax. + 1) ) , 8.%eval( &decmax. + 1) ) ) , "~" ,strip(put( round(q3 , 10**-( &decmax. + 1) ) , 8.%eval( &decmax. + 1) ) ) );
%end;
%if &decmax. >3 %then %do;
_2meansd = cats( strip(put( round(mean , 0.0001 ), 8.4 ) ) , "(" ,strip(put( round(std, 0.0001), 8.4) ) , ")");
_3median = cats( strip(put( round(mid , 0.0001 ) , 8.4 ) ) );
_4q1q3 = cats( strip(put( round(q1 ,0.0001 ) , 8.4 ) ) , "~" ,strip(put( round(q3 , 0.0001 ) , 8.4 ) ) );
%end;
keep &grpvarn. _1nnmiss _2meansd _3median _4q1q3 _5minmax;
proc sort; by &grpvarn.;
run;
* _3.3 transpose;
proc transpose data=_combdt out=_trans1;
var _1nnmiss _2meansd _3median _4q1q3 _5minmax;
by &grpvarn.;
proc sort; by _NAME_ ;
run;
proc transpose data=_trans1 out=_trans2 prefix= _grp;
id &grpvarn.;
var col1;
by _NAME_ ;
run;
*_3.4 rename column;
proc contents data=_trans2 out=_info noprint;
proc sort;
by varnum;
run;
proc sql noprint;
select count(distinct NAME) , NAME into: varn, :vnam1 -:vnam99 from _info;
quit;
data _&dtout.;
set _trans2;
%do ii = 1 %to &varn.;
%let jj = %eval(&ii. - 1);
rename &&vnam&ii. = C&jj.;
%end;
run;
* _3.5 label ;
data _row1;
length C0 $200.;
C0 = cats("&label.");
run;
data _&dtout._;
set _row1
_&dtout.;
if c0 = "_1nnmiss" then C0 = "n(nmisssing)";
if c0 = "_2meansd" then C0 = "Mean(SD)";
if c0 = "_3median" then C0 = "Median";
if c0 = "_4q1q3" then C0 = "Q1~Q3";
if c0 = "_5minmax" then C0 = "Min~Max";
run;
* _3.6 append ;
%if %sysfunc(upcase(&append.)) = %str(Y) and %sysfunc(exist(&dtout., data) ) %then %do;
data &dtout.;
set %if %sysfunc(exist(&dtout., data) ) %then &dtout.; _&dtout._ ;
run;
%end;
%else %do;
%put WARNING: 未追加数据集;
data &dtout.;
set _&dtout._ ;
run;
%end;
* _4 output data ;
* _4.1 output ;
data &libout..&dtout.;
set &dtout.;
run;
* _4.2 remove temp datasets ;
proc datasets lib=work;
delete _:;
run;
%mend;