在臨床試驗TFL編程中,簡單的描述性統(tǒng)計量與頻數匯總表格的數量占表格總量的絕對大頭。從提高編程效率的角度看,為這兩類表格建立穩(wěn)定的宏程序輸出是一件非常高效率的事情。
這篇文章介紹,數值變量的簡單描述性統(tǒng)計量輸出的一些考慮,主要有4方面的內容:
- 試驗匯總組的處理
- 橫向數據轉換為縱向數據
- 文字說明行的處理
- 小數位數的處理
完整宏程序代碼在文章的第5節(jié),如果讀者想要引用宏程序的話,可能需要根據自己項目要求更新統(tǒng)計量的布局和小數位保留的設置。
簡單描述性統(tǒng)計量輸出的樣式,各家公司大同小異,這篇文章采用以下樣式:

計算統(tǒng)計量的過程步選擇Proc Means, 使用其他過程步也可以。
分析數據來源于SASHELP.CLASS數據集,簡單處理下,新建一個分組變量:
data class0;
set sashelp.class;
if _n_ < 10 then trt01pn = 1;
else trt01pn = 2;
run;
1. 試驗匯總組的處理
TFLShell中會明說明輸出Table是否有匯總組。最常見的處理匯總組的方式是,在原始數據集利用Data步的Output語句,新建匯總組;我推薦大家嘗試使用Format過程步中的Multilabel選項進行構建匯總組,這個方法不需要在原始數據集中進行新建分組處理,一定程度減少了分析數據集的觀測數。
具體介紹文章參考:SAS編程:生成Table時,匯總組(Total)組如何處理?。
示例代碼如下:
proc format;
value trt01pn (notsorted multilabel)
1 = 1
2 = 2
1,2 = 99
;
run;
proc means data = class0 nway completetypes;
format trt01pn trt01pn.;
class trt01pn / preloadfmt mlf order = data;
var height;
output n=n_ mean=mean_ std=sd_ median=median_ q1=q1_ q3=q3_ min=min_ max=max_ out=stat1;
run;
程序運行結果如下:

結果中匯總組(trt01pn = 99)的相關統(tǒng)計量也被輸出來。這里Means過程步中使用的選項nway、preloadfmt、mlf都不能被省略,關于各個選項作用,讀者可以參考SAS官方文檔,這里就不再贅述。
2. 橫向數據轉換為縱向數據
輸出統(tǒng)計量數值之后,需要再根據Shell中具體的Layout進行處理。對于試驗分組受試者頻數為0的情形,shell中可能有特別的顯示,代碼示例如下:
data stat2;
set stat1;
length n mean sd median q1q3 minmax $200;
if n_ ne 0 then do;
n = strip(put(n_, 8.0));
mean = strip(put(mean_, 8.1));
sd = strip(put(sd_, 8.1));
median = strip(put(median_, 8.1));
q1q3 = strip(put(q1_, 8.1))||", "||strip(put(q3_, 8.1));
minmax = strip(put(min_, 8.1))||", "||strip(put(max_, 8.1));
end;
else do;
n = strip(put(n_, 8.0));
mean = "-";
sd = "-";
median = "-";
q1q3 = "-, -";
minmax = "-, -";
end;
run;
輸出結果如下:

數據整理完畢后,需要將橫向數據轉換為縱向數據,常用的方法有2種:
- Data步中Output語句;
- Transpose過程步
從代碼和輸出數據集的簡潔角度考慮,使用Transpose過程步要好一些:
proc transpose data = stat2 out = stat3 prefix = trt_;
var n mean sd median q1q3 minmax;
id trt01pn;
run;
結果如下:

Shell中的內容已大體具備,首列的內容通常以_NAME_取值的Format進行展示。同時,為了后續(xù)排序方便,也會新建排序變量??紤]到,宏程序批量處理內容比較多,新建Section變量,方便標識輸出結果的不同部分。統(tǒng)計量的排序變量,以_NAME_取值的Informat進行展示。
proc format;
value $stat
"n" = "n"
"mean" = "Mean"
"sd" = "SD"
"median" = "Median"
"q1q3" = "Q1, Q3"
"minmax" = "Min, Max"
;
invalue statn
"n" = 1
"mean" = 2
"sd" = 3
"median" = 4
"q1q3" = 5
"minmax" = 6
;
run;
data stat4;
retain section cat1n col1 trt_:;
set stat3;
section = 1;
cat1n = input(_name_, statn.);
length col1 $200;
col1 = put(_name_, stat.);
keep section cat1n col1 trt_:;
run;
輸出結果如下,主體顯示內容已經完成:

3. 文字說明行的處理
統(tǒng)計量首行展示的文字說明行,通常有2種做法。
第1種,新建一個包含文字說明信息的數據集與主體數據集進行縱向拼接;第2種,在主體數據集中使用Output語句,多生成一行記錄用于放置文字說明信息。
這里提供第3種方法,在橫向數據轉換成縱向數據之前,數據集保留一個空變量。同時,也對這個空變量進行轉置,這樣輸出數據集就會有多出一行。這一行就用于放置文字說明信息,informat中的排序變量值也需要進行相應的設置,其對應的說明信息直接在Data步中進行賦值,程序如下:(Stat2數據集中多了textline變量)
data stat2;
set stat1;
length textline n mean sd median q1q3 minmax $200;
if n_ ne 0 then do;
textline = "";
n = strip(put(n_, 8.0));
mean = strip(put(mean_, 8.1));
sd = strip(put(sd_, 8.1));
median = strip(put(median_, 8.1));
q1q3 = strip(put(q1_, 8.1))||", "||strip(put(q3_, 8.1));
minmax = strip(put(min_, 8.1))||", "||strip(put(max_, 8.1));
end;
else do;
textline = "";
n = strip(put(n_, 8.0));
mean = "-";
sd = "-";
median = "-";
q1q3 = "-, -";
minmax = "-, -";
end;
run;
proc transpose data = stat2 out = stat3 prefix = trt_;
var textline n mean sd median q1q3 minmax;
id trt01pn;
run;
proc format;
value $stat
"n" = "n"
"mean" = "Mean"
"sd" = "SD"
"median" = "Median"
"q1q3" = "Q1, Q3"
"minmax" = "Min, Max"
;
invalue statn
"textline" = 0
"n" = 1
"mean" = 2
"sd" = 3
"median" = 4
"q1q3" = 5
"minmax" = 6
;
run;
data stat4;
retain section cat1n col1 trt_:;
set stat3;
section = 1;
cat1n = input(_name_, statn.);
length col1 $200;
if _name_ ne "textline" then col1 = put(_name_, stat.);
else col1 = "Height (cm)";
keep section cat1n col1 trt_:;
run;
這樣,簡單描述性統(tǒng)計量就輸出完畢:

4. 小數位數的處理
4.1 統(tǒng)計量小數位數宏變量的生成
關于各統(tǒng)計量的小數位數,不同的公司、不同的統(tǒng)計師可能有不同的要求。在確定小數位數要求后,常規(guī)做法,是將原始小數位數作為宏變量,代入Data步中的Format。
不過,上述操作的代碼會顯得雜亂。我們可以在Data步之外提前設置好各統(tǒng)計量Format的宏變量,后續(xù)直接引用宏變量。
%macro deci(dp=);
%let d_n = 8.0; %put d_n = &d_n.;
%let d_mean = 8.%eval(&dp.+1); %put d_mean = &d_mean.;
%let d_sd = 8.%eval(&dp.+1); %put d_sd = &d_sd.;
%let d_medi = 8.%eval(&dp.+1); %put d_median = &d_medi.;
%let d_qq = 8.%eval(&dp.+1); %put d_qq = &d_qq.;
%let d_mm = 8.%eval(&dp.+0); %put d_mm = &d_mm.;
%mend;
%deci(dp = 0);
運行輸出后,日志顯示如下:

通常小數位數的展示不超過4位,輸出時需要對小數位數進行限制,當小數位數超過4時,小數位數取4:
%macro deci(dp=);
%global d_n d_mean d_sd d_medi d_qq d_mm ;
%let d_n = 8.0; %put d_n = &d_n.;
%let d_mean = 8.%sysfunc(min( %eval(&dp.+1), 4 )); %put d_mean = &d_mean.;
%let d_sd = 8.%sysfunc(min( %eval(&dp.+1), 4 )); %put d_sd = &d_sd.;
%let d_medi = 8.%sysfunc(min( %eval(&dp.+1), 4 )); %put d_medi = &d_medi.;
%let d_qq = 8.%sysfunc(min( %eval(&dp.+1), 4 )); %put d_qq = &d_qq.;
%let d_mm = 8.%sysfunc(min( %eval(&dp.+0), 4 )); %put d_mm = &d_mm.;
%mend;
%deci(dp = 5);
以上程序運行輸出如下:

4.2 “-0”問題的處理
在之前的文章中介紹過,保留小數位數,常用的方法是直接put具體的數字格式。不過,這種處理方式在負數十分位向0進位時,可能會產生一些偏誤。
我用代碼進行實例,對數字0.4, 0.5, -0.4, -0.49 -0.5四舍五入保留整數。使用兩種方法,一種是直接put數值格式8.0;一種是使用函數Round處理之后,再put數值格式8.0。
data tmp;
input a @@;
bput = strip(put(a, 8.0));
bround = strip(put(round(a, 1), 8.0));
datalines;
0.4
0.5
-0.4
-0.49
-0.5
;
run;

從結果中可以看到,對于-0.4和-0.49這2個數值,四舍五入應該進位成0,但是直接Put時,結果是-0,與想要的結果不同。更一般的,對于(-0.5, 0)這個范圍的數值,put函數保留整數時,結果為-0。
我沒細究這個現(xiàn)象的原因,建議讀者在保留小數位數時,先使用Round函數,再put相應的格式。t同時,兩個函數保留的小數位數要相同,否則會產生二次誤差。
結合上面的描述,還需設置round函數精度,方便調用,代碼如下:
%macro deci(dp=);
%global d_n d_mean d_sd d_medi d_qq d_mm r_n r_mean r_sd r_medi r_qq r_mm;
%let d_n = 8.0; %put d_n = &d_n.;
%let d_mean = 8.%sysfunc(min( %eval(&dp.+1), 4 )); %put d_mean = &d_mean.;
%let d_sd = 8.%sysfunc(min( %eval(&dp.+1), 4 )); %put d_sd = &d_sd.;
%let d_medi = 8.%sysfunc(min( %eval(&dp.+1), 4 )); %put d_medi = &d_medi.;
%let d_qq = 8.%sysfunc(min( %eval(&dp.+1), 4 )); %put d_qq = &d_qq.;
%let d_mm = 8.%sysfunc(min( %eval(&dp.+0), 4 )); %put d_mm = &d_mm.;
%let r_n = 1; %put r_n = &r_n.;
%let r_mean = %sysevalf(0.1**(%sysfunc(min( %eval(&dp.+1), 4 )) )); %put r_mean = &r_mean.;
%let r_sd = %sysevalf(0.1**(%sysfunc(min( %eval(&dp.+1), 4 )) )); %put r_sd = &r_sd.;
%let r_medi = %sysevalf(0.1**(%sysfunc(min( %eval(&dp.+1), 4 )) )); %put r_medi = &r_medi.;
%let r_qq = %sysevalf(0.1**(%sysfunc(min( %eval(&dp.+1), 4 )) )); %put r_qq = &r_qq.;
%let r_mm = %sysevalf(0.1**(%sysfunc(min( %eval(&dp.+0), 4 )) )); %put r_mm = &r_mm.;
%mend;
%deci(dp = 5);
日志輸出結果如下:

Data步中,對應的更新如下:
%deci(dp =1);
data stat2;
set stat1;
length textline n mean sd median q1q3 minmax $200;
if n_ ne 0 then do;
textline = "";
n = strip(put(round(n_, &r_n.), &d_n.));
mean = strip(put(round(mean_, &r_mean.), &d_mean.));
sd = strip(put(round(sd_, &r_sd.), &d_sd.));
median = strip(put(round(median_, &r_medi.), &d_medi.));
q1q3 = strip(put(round(q1_, &r_qq.), &d_qq.))||", "||strip(put(round(q3_, &r_qq.), &d_qq.));
minmax = strip(put(round(min_, &r_mm.), &d_mm.))||", "||strip(put(round(max_, &r_mm.), &d_mm.));
end;
else do;
textlin = "";
n = strip(put(n_, 8.0));
mean = "-";
sd = "-";
median = "-";
q1q3 = "-, -";
minmax = "-, -";
end;
run;
輸出結果如下:

輸出結果已經按照設置的小數位數展示,這樣每次只需要更新宏參數dp就可以更新所有統(tǒng)計量的小數位。
至于小數位數的確認,取決于統(tǒng)計師或公司的要求。如果是從實際數據中獲取,還需要額外的獲取處理,這里就不進一步展開。
5. 宏程序匯總
宏程序需要使用統(tǒng)計量的Format($stat.)進行第一列展示,也需要Informat(statn.)生成排序變量??紤]到,如果這些格式生成放在宏程序中,每次調用宏都會新生成一次。所以,我將這些程序放到宏程序之外。
***1. Macro for decimal places;
%macro deci(dp=);
%global d_n d_mean d_sd d_medi d_qq d_mm r_n r_mean r_sd r_medi r_qq r_mm;
%let d_n = 8.0; %put d_n = &d_n.;
%let d_mean = 8.%sysfunc(min( %eval(&dp.+1), 4 )); %put d_mean = &d_mean.;
%let d_sd = 8.%sysfunc(min( %eval(&dp.+1), 4 )); %put d_sd = &d_sd.;
%let d_medi = 8.%sysfunc(min( %eval(&dp.+1), 4 )); %put d_medi = &d_medi.;
%let d_qq = 8.%sysfunc(min( %eval(&dp.+1), 4 )); %put d_qq = &d_qq.;
%let d_mm = 8.%sysfunc(min( %eval(&dp.+0), 4 )); %put d_mm = &d_mm.;
%let r_n = 1; %put r_n = &r_n.;
%let r_mean = %sysevalf(0.1**(%sysfunc(min( %eval(&dp.+1), 4 )) )); %put r_mean = &r_mean.;
%let r_sd = %sysevalf(0.1**(%sysfunc(min( %eval(&dp.+1), 4 )) )); %put r_sd = &r_sd.;
%let r_medi = %sysevalf(0.1**(%sysfunc(min( %eval(&dp.+1), 4 )) )); %put r_medi = &r_medi.;
%let r_qq = %sysevalf(0.1**(%sysfunc(min( %eval(&dp.+1), 4 )) )); %put r_qq = &r_qq.;
%let r_mm = %sysevalf(0.1**(%sysfunc(min( %eval(&dp.+0), 4 )) )); %put r_mm = &r_mm.;
%mend;
***2. Macro for Statistics;
%macro stat(indt=, trtvar=, trtfmt=, anlvar=, dp=, textline=, section=, outdt=);
**2.1 Get statistics;
proc means data = &indt. nway completetypes;
format &trtvar. &trtfmt..;
class trt01pn / preloadfmt mlf order = data;
var &anlvar.;
output n=n_ mean=mean_ std=sd_ median=median_ q1=q1_ q3=q3_ min=min_ max=max_ out=stat1;
run;
**2.2 Get decimal places for statistic;
%deci(dp = &dp.);
**2.3 Table display;
data stat2;
set stat1;
length textline n mean sd median q1q3 minmax $200;
if n_ ne 0 then do;
textline = "";
n = strip(put(round(n_, &r_n.), &d_n.));
mean = strip(put(round(mean_, &r_mean.), &d_mean.));
sd = strip(put(round(sd_, &r_sd.), &d_sd.));
median = strip(put(round(median_, &r_medi.), &d_medi.));
q1q3 = strip(put(round(q1_, &r_qq.), &d_qq.))||", "||strip(put(round(q3_, &r_qq.), &d_qq.));
minmax = strip(put(round(min_, &r_mm.), &d_mm.))||", "||strip(put(round(max_, &r_mm.), &d_mm.));
end;
else do;
textlin = "";
n = strip(put(n_, 8.0));
mean = "-";
sd = "-";
median = "-";
q1q3 = "-, -";
minmax = "-, -";
end;
run;
**2.4 Transpose results;
proc transpose data = stat2 out = stat3 prefix = trt_;
var textline n mean sd median q1q3 minmax;
id &trtvar.;
run;
**2.5 Create output dataset;
data &outdt.;
retain section cat1n col1 trt_:;
set stat3;
section = §ion.;
cat1n = input(_name_, statn.);
length col1 $200;
if _name_ ne "textline" then col1 = put(_name_, stat.);
else col1 = "&textline.";
keep section cat1n col1 trt_:;
run;
%mend stat;
***3. Invoke the macro;
proc format;
value trt01pn (notsorted multilabel)
1 = 1
2 = 2
1,2 = 99
;
value $stat
"n" = "n"
"mean" = "Mean"
"sd" = "SD"
"median" = "Median"
"q1q3" = "Q1, Q3"
"minmax" = "Min, Max"
;
invalue statn
"textline" = 0
"n" = 1
"mean" = 2
"sd" = 3
"median" = 4
"q1q3" = 5
"minmax" = 6
;
run;
%stat(
indt = class0
,trtvar = trt01pn
,trtfmt = trt01pn
,anlvar = height
,dp = 1
,textline = Heigth (cm)
,section = 1
,outdt = sec_1
);
以上程序,運行結果如下:

6. 擴展與延伸
從宏的程序看,只設置了一個試驗分組變量。這里可以有讀者要問,如果輸出表格需要多個試驗分組變量,如何處理呢?
我建議,這種情況不使用宏程序,程序不復雜直接手動編寫,簡潔方便。
宏程序一般處理重復的編程內容,例如,Baseline Demogrphic這類匯總表,有多個不同的分析變量,內容高度重復化,程序中宏程序會調用很多次。但對于多個分組變量的情形,有這樣的宏,程序中也只會調用一次。在我看來,這是沒有必要的。
當然,對于多個分組變量的亞組分析,每個亞組也可以看成是重復單一的內容。但是,這樣簡單機械的處理抹去了,各亞組之間的聯(lián)系。這就像數學的幾何題一樣,我們可以通過復雜推理得到正確結果,不過,有時候一條輔助線可以讓整個解題過程簡潔的多得多。
這里舉個LB簡單描述統(tǒng)計量輸出的例子,LB中可能有二三十個Parameter,如果單獨依次出每個Paramter對應的內容,那過程就太過繁瑣;如果在宏程序中添加多個分組變量,一次調用就能解決,那跟手動寫出完整輸出過程又沒什么區(qū)別。
proc means data = adlb noprint nway completetypes;
by trt01an parcat1n paramn paramcd avisitn;
format anrindn anrindn.;
class anrindn / preloadfmt order = data;
var aval ;
output n = n_ mean=mean_ median=median_ q1=q1_ q3=q3_ min=min_ max=max_ out = stat1;
run;
當然,這種情況下,如果不了解頻數表的輸出的邏輯與過程,確實是直接調用宏程序,來得更簡單高效一點。
總結
這篇文章介紹了,簡單描述性統(tǒng)計量的宏程序輸出,計算統(tǒng)計量的匯總組時采用Format過程步中的multilabel選項,使用轉置的方式進行統(tǒng)計量Layout的處理,小數位數的保留使用round、put函數一起進行處理。
希望對讀者日常編程工作,有所幫助。
感謝閱讀, 歡迎關注:SAS茶談!
若有疑問,歡迎評論交流!