├── 01-Code ├── Macro │ ├── 说明.md │ ├── DM Macros │ │ ├── DeDup.sas │ │ ├── CheckDsVar.sas │ │ ├── RanSample_Proc.sas │ │ ├── GetCodeBook.sas │ │ ├── RanSample_Data.sas │ │ ├── AddFix.sas │ │ ├── cutdsbyvar.sas │ │ └── Winsor.sas │ ├── Debug Macros │ │ ├── DeBlackHole.sas │ │ ├── 清除输出、日志信息.sas │ │ ├── Clean.sas │ │ └── RunQuit.sas │ ├── FlowManage │ │ ├── Rootpath.sas │ │ ├── makeCodeBook.sas │ │ ├── Make Dir.sas │ │ ├── makeDir.sas │ │ └── LanchExcel.sas │ ├── IO Macros │ │ ├── Single │ │ │ ├── ex_csv.sas │ │ │ ├── im_sav.sas │ │ │ ├── ex_sav.sas │ │ │ ├── im_txt.sas │ │ │ ├── im_csv.sas │ │ │ ├── ex_excel.sas │ │ │ └── im_excel.sas │ │ └── Multi │ │ │ ├── im_1m1excel.sas │ │ │ ├── ex_mmcsv.sas │ │ │ ├── ex_mm1excel.sas │ │ │ ├── ex_m1mexcel.sas │ │ │ ├── im_m1mexcel.sas │ │ │ ├── im_mmmexcel.sas │ │ │ ├── im_mm1excel.sas │ │ │ ├── xls2sas.sas │ │ │ └── xpt2sas.sas │ └── ggBaseline │ │ ├── ggBaseline.sas │ │ ├── ggBaseline1.sas │ │ └── ggBaseline2.sas ├── SAS编程演义-C01.sas ├── SAS编程演义-C02.sas ├── SAS编程演义-C03.sas ├── SAS编程演义-C04.sas ├── SAS编程演义-C05.sas ├── SAS编程演义-C06.sas ├── SAS编程演义-C07.sas ├── SAS编程演义-C08.sas ├── SAS编程演义-C09.sas ├── SAS编程演义-C10.sas └── style │ ├── gghtml2.sas │ └── ggplot2.sas ├── _vnote.json ├── 0-Pre.md ├── README.md ├── Chapter-1.md └── Chatper-2.md /01-Code/Macro/说明.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqgu/Romance-of-SAS-Programming/HEAD/01-Code/Macro/说明.md -------------------------------------------------------------------------------- /01-Code/SAS编程演义-C01.sas: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqgu/Romance-of-SAS-Programming/HEAD/01-Code/SAS编程演义-C01.sas -------------------------------------------------------------------------------- /01-Code/SAS编程演义-C02.sas: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqgu/Romance-of-SAS-Programming/HEAD/01-Code/SAS编程演义-C02.sas -------------------------------------------------------------------------------- /01-Code/SAS编程演义-C03.sas: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqgu/Romance-of-SAS-Programming/HEAD/01-Code/SAS编程演义-C03.sas -------------------------------------------------------------------------------- /01-Code/SAS编程演义-C04.sas: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqgu/Romance-of-SAS-Programming/HEAD/01-Code/SAS编程演义-C04.sas -------------------------------------------------------------------------------- /01-Code/SAS编程演义-C05.sas: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqgu/Romance-of-SAS-Programming/HEAD/01-Code/SAS编程演义-C05.sas -------------------------------------------------------------------------------- /01-Code/SAS编程演义-C06.sas: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqgu/Romance-of-SAS-Programming/HEAD/01-Code/SAS编程演义-C06.sas -------------------------------------------------------------------------------- /01-Code/SAS编程演义-C07.sas: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqgu/Romance-of-SAS-Programming/HEAD/01-Code/SAS编程演义-C07.sas -------------------------------------------------------------------------------- /01-Code/SAS编程演义-C08.sas: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqgu/Romance-of-SAS-Programming/HEAD/01-Code/SAS编程演义-C08.sas -------------------------------------------------------------------------------- /01-Code/SAS编程演义-C09.sas: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqgu/Romance-of-SAS-Programming/HEAD/01-Code/SAS编程演义-C09.sas -------------------------------------------------------------------------------- /01-Code/SAS编程演义-C10.sas: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqgu/Romance-of-SAS-Programming/HEAD/01-Code/SAS编程演义-C10.sas -------------------------------------------------------------------------------- /01-Code/Macro/DM Macros/DeDup.sas: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqgu/Romance-of-SAS-Programming/HEAD/01-Code/Macro/DM Macros/DeDup.sas -------------------------------------------------------------------------------- /01-Code/Macro/Debug Macros/DeBlackHole.sas: -------------------------------------------------------------------------------- 1 | /* ===BalckHole Debug===*/ 2 | %macro DeBlackHole(); 3 | *';*";*);%mend;%put OK!;run; 4 | %mend; -------------------------------------------------------------------------------- /01-Code/Macro/Debug Macros/清除输出、日志信息.sas: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hqgu/Romance-of-SAS-Programming/HEAD/01-Code/Macro/Debug Macros/清除输出、日志信息.sas -------------------------------------------------------------------------------- /01-Code/Macro/Debug Macros/Clean.sas: -------------------------------------------------------------------------------- 1 | %macro clean(); 2 | dm output 'clear' continue; 3 | dm log 'clear' continue; 4 | dm odsresult 'clear' continue; 5 | %mend clean(); 6 | -------------------------------------------------------------------------------- /01-Code/Macro/Debug Macros/RunQuit.sas: -------------------------------------------------------------------------------- 1 | 2 | /*===use it instead of run and quit statements===*/ 3 | 4 | %macro RunQuit(); 5 | ;run;quit; 6 | %if &syserr. ne 0 %then %do; 7 | %abort cancel; 8 | %end; 9 | %mend; 10 | 11 | -------------------------------------------------------------------------------- /01-Code/Macro/FlowManage/Rootpath.sas: -------------------------------------------------------------------------------- 1 | %let CodeRoot= %qsubstr(%sysget(SAS_ExecfilePath), 1, %length(%sysget(SAS_ExecfilePath))-%length(%sysget(SAS_ExecfileName))); 2 | %let ProjRoot=%qsubstr(&CodeRoot,1,%length(&CodeRoot)-18); 3 | %put &ProjRoot; -------------------------------------------------------------------------------- /01-Code/Macro/FlowManage/makeCodeBook.sas: -------------------------------------------------------------------------------- 1 | %macro MakeCodeBook(dslib=sashelp,dsname=class,outdir=); 2 | ods output Variables=CodeBook; 3 | proc contents data=&dslib..&dsname; 4 | run; 5 | 6 | proc sort data=CodeBook; 7 | by num; 8 | run; 9 | 10 | proc export data=CodeBook 11 | outfile="&outdir/&dsname._CodeBook.csv" 12 | dbms=csv replace; 13 | run; 14 | %mend MakeCodeBook; 15 | -------------------------------------------------------------------------------- /01-Code/Macro/DM Macros/CheckDsVar.sas: -------------------------------------------------------------------------------- 1 | %macro CheckDsVar(ds =, var =); 2 | %local dsid check rc; 3 | %let dsid = %sysfunc(open(&ds.)); 4 | %if &dsid=0 %then %DO; %put Dataset &ds. is not exist!; %ABORT; %END; %else %do; 5 | %let check = %sysfunc(varnum(&dsid., &var.)); 6 | %let rc = %sysfunc(close(&dsid.)); 7 | %if &check. = 0 %then %DO ;%put Variable &var is not exists! ; %ABORT; %END; 8 | %end; 9 | %mend CheckDsVar; -------------------------------------------------------------------------------- /01-Code/Macro/IO Macros/Single/ex_csv.sas: -------------------------------------------------------------------------------- 1 | 2 | %macro ex_csv 3 | (/* positional parameters */ 4 | /* .SAS-data-set */ dsn 5 | ,/* filepath */ filepath 6 | 7 | /* key-word parameters */ 8 | ,/*(blank)|REPLACE */ replace=replace 9 | ,/* YES | NO*/ putnames=yes 10 | ); 11 | 12 | proc export data=&dsn 13 | outfile="&filepath" 14 | dbms=csv &replace; 15 | putnames=&putnames; 16 | run; 17 | %mend ex_csv; 18 | 19 | -------------------------------------------------------------------------------- /01-Code/Macro/DM Macros/RanSample_Proc.sas: -------------------------------------------------------------------------------- 1 | %macro ransample_proc(dsin,dsout,Replacement,SampleSize); 2 | %if %upcase("&Replacement")="YES" %then %do; 3 | proc surveyselect data=&dsin out=&dsout method=urs n=&SampleSize; 4 | run; 5 | %end; 6 | %else %if %upcase("&Replacement")="NO" %then %do; 7 | proc surveyselect data=&dsin out=&dsout method=srs n=&SampleSize; 8 | run; 9 | %end; 10 | %else %put ERROR: Replacement shout be yes or no ; 11 | %mendransample_proc; -------------------------------------------------------------------------------- /01-Code/Macro/IO Macros/Multi/im_1m1excel.sas: -------------------------------------------------------------------------------- 1 | 2 | %macro im_1m1excel(RootPath,FileName,Extension); 3 | 4 | libname MyExcel Excel "&RootPath.\&Filename..&Extension"; 5 | proc sql noprint; 6 | select catt(trim(libname),'.',quote(trim(memname)),'n') into: namelist separated by ' ' 7 | from dictionary.tables 8 | where libname in ('MYEXCEL'); 9 | quit; 10 | %put &namelist; 11 | 12 | data &FileName; 13 | set &namelist; 14 | run; 15 | %mend im_1m1excel; 16 | -------------------------------------------------------------------------------- /01-Code/Macro/IO Macros/Single/im_sav.sas: -------------------------------------------------------------------------------- 1 | %macro im_sav 2 | (/*positional parameters*/ 3 | /*.SAS-data-set */ dsn 4 | ,/* fileref | "filepath" */ file 5 | 6 | /* key-words parameters*/ 7 | ,/* (blank) | REPLACE */ replace=replace 8 | ,/* libref.format-catalog 's libref */ libref=work 9 | ,/* libref.format-catalog 's cata */ cata=spssfmt 10 | ); 11 | 12 | proc import out=&dsn 13 | datafile=&file 14 | dbms=spss &replace; 15 | *fmtlib=&libref..&cata.; 16 | run; 17 | %mend im_sav; 18 | 19 | -------------------------------------------------------------------------------- /01-Code/Macro/IO Macros/Single/ex_sav.sas: -------------------------------------------------------------------------------- 1 | %macro ex_sav 2 | (/*positional parameters*/ 3 | /*.SAS-data-set */ dsn 4 | ,/* filepath */ filepath 5 | 6 | /* key-words parameters*/ 7 | ,/* (blank) | REPLACE */ replace=replace 8 | ,/* libref.format-catalog 's libref */ libref=work 9 | ,/* libref.format-catalog 's cata */ cata=spssfmt 10 | ); 11 | 12 | proc export data=&dsn 13 | outfile="&filepath" 14 | dbms=spss &replace; 15 | *fmtlib=&libref..&cata.; 16 | run; 17 | %mend ex_sav; 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /01-Code/Macro/IO Macros/Single/im_txt.sas: -------------------------------------------------------------------------------- 1 | 2 | %macro im_txt 3 | (/*data set name*/ dsn 4 | ,/*fileref | "filepath" */file 5 | ,/*delimiter: TAB | SPACE | other specified*/dlm 6 | ); 7 | 8 | proc import out=&dsn 9 | datafile=&file 10 | dbms=dlm replace; 11 | %if %qupcase(&dlm)=TAB %then %str(delimiter='09'x;); 12 | %else %if %qupcase(&dlm)=SPACE %then %str(delimiter='20'x;); 13 | %else %str(delimiter="&dlm";); 14 | getnames=yes; 15 | run; 16 | %mend im_txt; 17 | 18 | 19 | -------------------------------------------------------------------------------- /01-Code/Macro/IO Macros/Single/im_csv.sas: -------------------------------------------------------------------------------- 1 | 2 | %macro im_csv 3 | (/*positional parameters*/ 4 | /*.SAS-data-set*/ dsn 5 | ,/*fileref | "filepath"*/ file 6 | 7 | /*key-word parameters*/ 8 | ,/*(blank)|REPLACE */ replace=replace 9 | ,/* YES | NO*/ getnames=yes 10 | ,/* 20 | N */ guessingrows=20 11 | ,/* 2 | N */ datarow=2 12 | ); 13 | 14 | proc import out=&dsn 15 | datafile=&file 16 | dbms=csv &replace; 17 | getnames=&getnames; 18 | guessingrows=&guessingrows; 19 | datarow=&datarow; 20 | run; 21 | %mend im_csv; 22 | 23 | -------------------------------------------------------------------------------- /01-Code/Macro/FlowManage/Make Dir.sas: -------------------------------------------------------------------------------- 1 | options 2 | mprint symbolgen 3 | noxwait noxsync 4 | ; 5 | 6 | %let ProjRoot= %qsubstr(%sysget(SAS_ExecfilePath), 1, %length(%sysget(SAS_ExecfilePath))-%length(%sysget(SAS_ExecfileName))); 7 | 8 | %put &ProjRoot; 9 | 10 | %macro MakeDir(ProjRoot); 11 | x " %str(md %"&ProjRoot\01 Doc\SPSAP%" %"&ProjRoot\01 Doc\Ref%" %"&ProjRoot\01 Doc\Notes%" %"&ProjRoot\02 Data\Raw%" %"&ProjRoot\02 Data\Clean%" %"&ProjRoot\03 Code\Macro%" %"&ProjRoot\03 Code\Core%" %"&ProjRoot\04 Out%" %"&ProjRoot\05 Report%" )" ; 12 | %mend; 13 | 14 | %MakeDir(&ProjRoot); 15 | -------------------------------------------------------------------------------- /01-Code/Macro/FlowManage/makeDir.sas: -------------------------------------------------------------------------------- 1 | options 2 | mprint symbolgen 3 | noxwait noxsync 4 | ; 5 | 6 | %let ProjRoot= %qsubstr(%sysget(SAS_ExecfilePath), 1, %length(%sysget(SAS_ExecfilePath))-%length(%sysget(SAS_ExecfileName))); 7 | 8 | %put &ProjRoot; 9 | 10 | %macro MakeDir(ProjRoot); 11 | x " %str(md %"&ProjRoot\01-Data\01-Raw%" %"&ProjRoot\01-Data\02-Tidy%" %"&ProjRoot\01-Data\03-codeBook%" 12 | %"&ProjRoot\02-SAP%" %"&ProjRoot\03-Ref%" %"&ProjRoot\04-Code%" 13 | %"&ProjRoot\05-Out\01-Table%" %"&ProjRoot\05-Out\02-Figure%" %"&ProjRoot\05-Out\03-Draft%" 14 | )" ; 15 | %mend; 16 | 17 | %MakeDir(&ProjRoot); 18 | -------------------------------------------------------------------------------- /01-Code/Macro/IO Macros/Single/ex_excel.sas: -------------------------------------------------------------------------------- 1 | %macro ex_excel 2 | (/*positional parameters:keep in order*/ 3 | /*.SAS-data-set*/ dsn 4 | ,/*filepath*/filepath 5 | 6 | /*keyword parameters*/ 7 | ,/*DBMS types: EXCEL | EXCELCS | XLS | XLSX */ dbms=excel 8 | ,/*replace: (blank) | REPLACE */ replace=replace 9 | ,/*sheet of spreedsheet: 10 | (blank) | sheetname*/sheet=sheet1 11 | ,/*(blank)|LABEL */label= 12 | ,/*NO|YES*/newfile=no 13 | ); 14 | 15 | proc export data=&dsn 16 | outfile="&filepath" 17 | dbms=&dbms &replace &label; 18 | sheet="&sheet"; 19 | newfile=&newfile; 20 | run; 21 | %mend ex_excel; 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /01-Code/Macro/DM Macros/GetCodeBook.sas: -------------------------------------------------------------------------------- 1 | %macro getCodeBook(data=,outdir=); 2 | 3 | %let ndsn=%sysfunc(countw(&data,.)); 4 | 5 | %if &ndsn eq 1 %then %do; 6 | %let libname=WORK; 7 | %let memname=%scan(&data,1, "("); 8 | %end; 9 | 10 | %else %do; 11 | %let libname=%scan(&data,1,.); 12 | %let memname=%scan(%scan(&data,2,.),1, "("); 13 | %end; 14 | 15 | ods output Variables=CodeBook; 16 | proc contents data=&libname..&memname; 17 | run; 18 | 19 | proc sort data=CodeBook; 20 | by num; 21 | run; 22 | 23 | proc export data=CodeBook 24 | outfile="&outdir\&memname._codeBook.csv" 25 | dbms=csv replace; 26 | run; 27 | %mend; 28 | -------------------------------------------------------------------------------- /01-Code/Macro/IO Macros/Multi/ex_mmcsv.sas: -------------------------------------------------------------------------------- 1 | %macro ex_mmcsv 2 | (/*positional parameters:keep in order*/ 3 | /**/ dslib 4 | ,/*rootpath*/rootpath 5 | 6 | /*keyword parameters*/ 7 | ,/*filetype:XLS|XLSX*/ filetype=csv 8 | ,/*(blank)|replace */ replace=replace 9 | ,/*(blank)|LABEL */label= 10 | ); 11 | 12 | proc sql noprint; 13 | select count(distinct memname) into: dsnum 14 | from sashelp.vmember 15 | where libname=upcase("&dslib") and memtype="DATA"; 16 | select distinct memname into:csv1-:csv%left(&dsnum) 17 | from sashelp.vmember 18 | where libname=upcase("&dslib") and memtype="DATA"; 19 | quit; 20 | 21 | %do i=1 %to &dsnum; 22 | proc export data=&&csv&i 23 | outfile="&rootpath.\&&csv&i...&filetype" 24 | dbms=csv replace &label; 25 | run; 26 | %end ; 27 | %mend ex_mmcsv; 28 | 29 | -------------------------------------------------------------------------------- /01-Code/Macro/IO Macros/Multi/ex_mm1excel.sas: -------------------------------------------------------------------------------- 1 | %macro ex_mm1excel 2 | (/*positional parameters:keep in order*/ 3 | /**/ dslib 4 | ,/*rootpath*/rootpath 5 | 6 | /*keyword parameters*/ 7 | ,/*filetype:XLS|XLSX*/ filetype=xlsx 8 | ,/*(blank)|replace */ replace=replace 9 | ,/*(blank)|LABEL */label= 10 | ); 11 | 12 | proc sql noprint; 13 | select count(distinct memname) into: dsnum 14 | from sashelp.vmember 15 | where libname=upcase("&dslib") and memtype="DATA"; 16 | select distinct memname into:sheet1-:sheet%left(&dsnum) 17 | from sashelp.vmember 18 | where libname=upcase("&dslib") and memtype="DATA"; 19 | quit; 20 | 21 | %do i=1 %to &dsnum; 22 | proc export data=&&sheet&i 23 | outfile="&rootpath.\&&sheet&i...&filetype" 24 | dbms=excel replace &label; 25 | sheet="&&sheet&i"; 26 | run; 27 | %end ; 28 | %mend ex_mm1excel; 29 | 30 | 31 | %ex_mm1excel(work,d:\12 tst) 32 | 33 | -------------------------------------------------------------------------------- /01-Code/Macro/IO Macros/Single/im_excel.sas: -------------------------------------------------------------------------------- 1 | %macro im_excel 2 | (/*positional parameters:keep in order*/ 3 | /*.SAS-data-set*/ dsn 4 | ,/*fileref | "filepath"*/file 5 | 6 | /*keyword parameters*/ 7 | ,/*DBMS types: EXCEL | EXCELCS | XLS | XLSX */ dbms=excel 8 | ,/*replace: (blank) | REPLACE */ replace=replace 9 | ,/*range of spreedsheet:(blank) | rangename|sheet$ | sheet$UR:LR */range= 10 | ,/*firstobs : N*/ firstobs=1 11 | ,/*obs: N */ obs=1048576 12 | ,/*YES|NO */ getnames=yes 13 | ,/*YES|NO */ scantext=yes 14 | ,/*YES|NO */ scantime=yes 15 | ,/*YES|NO */ usedate=yes 16 | ,/*YES|NO */ mixed=no 17 | ); 18 | 19 | proc import out=&dsn 20 | datafile=&file 21 | dbms=&dbms &replace; 22 | range="&range"; 23 | dbdsopts="firstobs=&firstobs obs=&obs"; 24 | getnames=&getnames; 25 | scantext=&scantext; 26 | scantime=&scantime; 27 | usedate=&usedate; 28 | mixed=&mixed; 29 | run; 30 | %mend im_excel; 31 | 32 | 33 | -------------------------------------------------------------------------------- /01-Code/Macro/IO Macros/Multi/ex_m1mexcel.sas: -------------------------------------------------------------------------------- 1 | %macro ex_m1mexcel 2 | (/*mulity dataset to 1 exel with multiy sheet*/ 3 | /*positional parameters:keep in order*/ 4 | /**/ dslib 5 | ,/*rootpath*/rootpath 6 | 7 | /*keyword parameters*/ 8 | ,/*outfilename:(blank)|othes*/outname=outexel 9 | ,/*filetype:XLS|XLSX*/ filetype=xls 10 | ,/*(blank)|replace */ replace=replace 11 | ,/*(blank)|LABEL */label= 12 | ); 13 | 14 | proc sql noprint; 15 | select count(distinct memname) into: dsnum 16 | from sashelp.vmember 17 | where libname=upcase("&dslib") and memtype="DATA"; 18 | select distinct memname into:sheet1-:sheet%left(&dsnum) 19 | from sashelp.vmember 20 | where libname=upcase("&dslib") and memtype="DATA"; 21 | quit; 22 | 23 | %do i=1 %to &dsnum; 24 | proc export data=&&sheet&i 25 | outfile="&rootpath.\&outname...&filetype" 26 | dbms=excel replace &label; 27 | sheet="&&sheet&i"; 28 | run; 29 | %end ; 30 | %mend ex_m1mexcel; 31 | 32 | 33 | -------------------------------------------------------------------------------- /01-Code/Macro/DM Macros/RanSample_Data.sas: -------------------------------------------------------------------------------- 1 | %macro ransample_data(dsin,dsout,Replacement,SampleSize); 2 | %if %upcase("&Replacement")="YES" %then %do; 3 | %*creating random sample with replacement; 4 | data &dsout(drop=i); 5 | do i=1to &SampleSize; 6 | PickIt=ceil(ranuni(123)*TotObs); 7 | ObsPicked=PickIt; 8 | set &dsin point=PickIt nobs=TotObs; 9 | output; 10 | end; 11 | stop; 12 | run; 13 | %end; 14 | %else %if %upcase("&Replacement")="NO" %then %do; 15 | %* create random sample without replace; 16 | data &dsout(drop=SampleSize ObsLeft); 17 | SampleSize=&SampleSize; 18 | ObsLeft=TotObs; 19 | do while(SampleSize>0and ObsLeft>0) ; 20 | PickIt+1; 21 | if ranuni(123) flist.txt; 19 | 20 | x 'cd "&dir" '; 21 | x 'dir *.xls /b/o:n > flist.txt '; 22 | 23 | data _null_; 24 | length fname $20.; 25 | infile "&dir\flist.txt"; 26 | input fname $20.; 27 | dname=scan(fname,1,"."); 28 | call symputx(cats('File',_n_),fname); 29 | call symputx(cats('ds',_n_),dname); 30 | call symputx('NumFile',_n_); 31 | run; 32 | *****************************************/ 33 | 34 | %do i=1 %to &NumFile; 35 | proc import out=&&ds&i 36 | datafile="&Dir\&&file&i" 37 | dbms=excel replace; 38 | run; 39 | %end; 40 | %mend; 41 | 42 | 43 | 44 | %im_m1mexcel(dir=F:\PharmaSUGChina\SUB) 45 | -------------------------------------------------------------------------------- /01-Code/Macro/IO Macros/Multi/im_mmmexcel.sas: -------------------------------------------------------------------------------- 1 | %macro im_mmmexcel(dir=); 2 | %let rs=%sysfunc(filename(filref,&dir)); 3 | %if &rs=0 %then 4 | %do; 5 | %let did=%sysfunc(dopen(&filref)); 6 | %if &did>0 %then 7 | %do; 8 | %let nobs=%sysfunc(dnum(&did)); 9 | %if &nobs>0 %then 10 | %do i=1 %to &nobs.; 11 | %let name=%qscan(%qsysfunc(dread(&did,&i)),1,.); 12 | %let ext=%qscan(%qsysfunc(dread(&did,&i)),-1,.); 13 | %if %upcase(&ext)=XLS or %upcase(&ext)=XLSX %then 14 | %do; 15 | 16 | libname MyExcel Excel "&dir.\&name..&ext."; 17 | run; 18 | 19 | proc sql noprint; 20 | select count(distinct memname) into :number 21 | from sashelp.vmember 22 | where libname="MYEXCEL"; 23 | select compress(memname,"$") into :sheet1 - :sheet%left(&number) 24 | from sashelp.vmember 25 | where libname="MYEXCEL"; 26 | quit; 27 | 28 | %do j=1 %to &sqlobs; 29 | proc import out=%cmpres(&name._&&sheet&j) 30 | datafile="&dir.\&name..&ext." 31 | dbms=excel replace; 32 | getnames=yes; 33 | sheet="&&sheet&j"; 34 | mixed=yes; 35 | run; 36 | %end; 37 | %end; 38 | %end; 39 | %end; 40 | %end; 41 | 42 | %let rc=%sysfunc(dclose(&did)); 43 | %mend im_mmmexcel; -------------------------------------------------------------------------------- /01-Code/Macro/DM Macros/cutdsbyvar.sas: -------------------------------------------------------------------------------- 1 | %macro cutdsbyvar(dsin=, dsout=, var=, cut=); 2 | proc univariate data=&dsin; 3 | var &var; 4 | output out=dspctl 5 | %if &cut eq 3 %then pctlpts=33.3 66.6; 6 | %else %if &cut eq 4 %then pctlpts=25 50 75; 7 | %else %if &cut eq 5 %then pctlpts=20 40 60 80; 8 | pctlpre=p; 9 | run; 10 | 11 | data longdspctl; 12 | set dspctl; 13 | array pctl[*] _numeric_; 14 | do i=1 to %sysevalf(&cut-1); 15 | cutpoint=pctl[i]; 16 | output; 17 | end; 18 | run; 19 | 20 | data _null_; 21 | set longdspctl; 22 | call symputx(cats("cutp",_n_),cutpoint ); 23 | run; 24 | 25 | data &dsout; 26 | set &dsin; 27 | length &var.cutgrp $2; 28 | if missing(&var) then call missing( &var.cutgrp); 29 | 30 | %if &cut=3 %then %do; 31 | else if &var<&cutp1 then &var.cutgrp="Q1"; 32 | else if &var<&cutp2 then &var.cutgrp="Q2"; 33 | else &var.cutgrp="Q3"; 34 | %end; 35 | 36 | %if &cut=4 %then %do; 37 | else if &var<&cutp1 then &var.cutgrp="Q1"; 38 | else if &var<&cutp2 then &var.cutgrp="Q2"; 39 | else if &var<&cutp3 then &var.cutgrp="Q3"; 40 | else &var.cutgrp="Q4"; 41 | %end; 42 | 43 | 44 | %if &cut=5 %then %do; 45 | else if &var<&cutp1 then &var.cutgrp="Q1"; 46 | else if &var<&cutp2 then &var.cutgrp="Q2"; 47 | else if &var<&cutp3 then &var.cutgrp="Q3"; 48 | else if &var<&cutp4 then &var.cutgrp="Q4"; 49 | else &var.cutgrp="Q5"; 50 | %end; 51 | run; 52 | 53 | %mend; 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /01-Code/Macro/IO Macros/Multi/im_mm1excel.sas: -------------------------------------------------------------------------------- 1 | 2 | %macro im_mm1excel(dir=); 3 | options symbolgen mprint noxwait noxsync; 4 | %sysexec cd &dir; 5 | %sysexec dir *.xls /b/o:n > flist.txt; 6 | data _indexfile; 7 | length filename $200; 8 | infile "&dir.\flist.txt"; 9 | input filename $; 10 | run; 11 | proc sql noprint; 12 | select count(filename) into :filenum from _indexfile; 13 | %if &filenum>=1 %then %do; 14 | select filename into :file1-:file%left(&filenum) 15 | from _indexfile; 16 | %end; 17 | quit; 18 | %do i=1 %to &filenum; 19 | libname excellib excel "&dir.\&&file&i"; 20 | proc sql noprint; 21 | create table sheetname as 22 | select tranwrd(memname, "''", "'") as sheetname 23 | from sashelp.vstabvw 24 | where libname="EXCELLIB"; 25 | select count(DISTINCT sheetname) into :sheetnum 26 | from sheetname; 27 | select DISTINCT sheetname into :sheet1 - :sheet%left(&sheetnum) 28 | from sheetname; 29 | quit; 30 | data want; 31 | run; 32 | %do j=1 %to &sheetnum; 33 | %let dsname=%sysfunc(compress(%sysfunc(catx( _,%sysfunc(scan(&&file&i,1,".")),&&sheet&j)),$)); 34 | %put &dsname; 35 | proc import datafile="&dir.\&&file&i" 36 | out=&dsname 37 | dbms=excel replace; 38 | sheet="&&sheet&j"; 39 | getnames=yes; 40 | mixed=yes; 41 | run; 42 | data &dsname; 43 | length _excelfilename $100 _sheetname $32; 44 | set &dsname; 45 | _excelfilename="%scan(&&file&i,1)"; 46 | _sheetname="&&sheet&j"; 47 | run; 48 | data want; 49 | set want &dsname; 50 | run; 51 | %end; 52 | libname excellib clear; 53 | %end; 54 | %mend im_mm1excel; 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /01-Code/Macro/IO Macros/Multi/xls2sas.sas: -------------------------------------------------------------------------------- 1 | %macro xls2sas(folder= , subfd= , exclfd= , startrow= ) ; 2 | /************************************ 3 | folder: root folder name to be searched for 4 | subfd: include subfolder (Y/N, default=Y) 5 | exclfd: subfolder list to be excluded 6 | startrow: starting row of Excel worksheet 7 | *************************************/ 8 | 9 | %local _j _cnt _dsid _i _num _s_ext _s_name _filename _rc; 10 | %let _rc=%qsysfunc(filename(filrf,&folder)); 11 | 12 | %if &_rc=0 %then 13 | %do; 14 | %let _dsid=%sysfunc(dopen(&filrf)); 15 | %if &_dsid>0 %then 16 | %do; 17 | %let _num=%sysfunc(dnum(&_dsid)); 18 | %if &_num >0 %then 19 | %do _i=1 %to &_num; 20 | %let _filename=%sysfunc(dread(&_dsid,&_i)); 21 | %let _s_name=%scan(&_filename, 1, .); 22 | %let _s_ext=%scan(&_filename, 2, .); 23 | 24 | %if %upcase(&_s_ext)= XLS or %upcase(&_s_ext)=XLSX %then 25 | %do; 26 | Libname excellib excel "&folder\&_filename"; 27 | data _null_; 28 | set sashelp.vstabvw end=last; 29 | where libname="EXCELLIB"; 30 | memname=upcase(scan(memname, 1, '$')); 31 | call symputx(cats('sheet', _n_), memname, 'L'); 32 | if last then call symputx('_cnt', _n_, 'L'); 33 | run; 34 | 35 | libname excellib clear; 36 | 37 | %do _j=1 %to &_cnt; 38 | proc import datafile="&folder\&_filename" 39 | out=%cmpres(&_s_name._&&sheet&_j) 40 | dbms=excel replace; 41 | range="&&sheet&_j..$&startrow.:65000"; 42 | mixed=yes; 43 | getnames=yes; 44 | run; 45 | %end; 46 | %end; 47 | 48 | %else %if %upcase(&_s_ext)= and &subfd=Y and %qsysfunc(indexw(&exclfd,&_filename))=0 %then 49 | %do; 50 | %let _rc=%sysfunc(dclose(&_dsid)); 51 | %xls2sas(folder=&folder\&_filename, subfd=&subfd, exclfd=&exclfd, startrow=&startrow) 52 | %end; 53 | %end; 54 | %end; 55 | %end; 56 | %let _rc=%sysfunc(dclose(&_dsid)); 57 | %mend xls2sas; -------------------------------------------------------------------------------- /01-Code/Macro/ggBaseline/ggBaseline.sas: -------------------------------------------------------------------------------- 1 | *======================================================= 2 | %ggBaseline: generate Demographic Tables with one group or multiple groups 3 | Maintainer: Hong-Qiu Gu 4 | Date: V20180105 5 | 6 | For details, please see and ref Ann Transl Med, 2018, 6(16): 326. 7 | 8 | Copyright: CC BY-NC-SA 4.0 9 | In short: you are free to share and make derivative 10 | works of the work under the conditions that you appropriately 11 | attribute it, you use the material only for non-commercial 12 | purposes, and that you distribute it only under a license 13 | compatible with this one. 14 | 15 | ======================================================= 16 | %ggBaseline( 17 | data=, 18 | var=var1|test|label1\ 19 | var2|test|label2, or 20 | var1|CTN|label1\ 21 | var2|CTG|label2, 22 | grp=grpvar, 23 | grplabel=grplabel1|grplabel2, 24 | stdiff=N, 25 | totcol=N, 26 | pctype=COL|ROW, 27 | exmisspct=Y|N, 28 | filetype=RTF|PDF, 29 | file=&ProjPath\05-Out\01-Table\, 30 | title=, 31 | footnote=, 32 | fnspace=20, 33 | page=PORTRAIT|LANDSCAPE, 34 | deids=Y|N 35 | ) 36 | 37 | ====================================================; 38 | 39 | %macro ggBaseline( 40 | data=, 41 | var=, 42 | grp=, 43 | grplabel=, 44 | stdiff=N, 45 | totcol=N, 46 | pctype=col, 47 | exmisspct=Y, 48 | showP=Y, 49 | filetype=rtf, 50 | file=, 51 | title=, 52 | footnote=, 53 | fnspace=, 54 | page=portrait, 55 | deids=Y 56 | ); 57 | 58 | 59 | 60 | 61 | %if &grp EQ %str() %then 62 | 63 | %ggBaseline1( 64 | data=&data, 65 | var=&var, 66 | exmisspct=&exmisspct, 67 | filetype=&filetype, 68 | file=&file, 69 | title=&title, 70 | footnote=&footnote, 71 | fnspace=&fnspace, 72 | page=&page, 73 | deids=&deids 74 | ); 75 | 76 | 77 | %else 78 | 79 | %ggBaseline2( 80 | data=&data, 81 | var=&var, 82 | grp=&grp, 83 | grplabel=&grplabel, 84 | stdiff=&stdiff, 85 | totcol=&totcol, 86 | pctype=&pctype, 87 | exmisspct=&exmisspct, 88 | showP=&showP, 89 | filetype=&filetype, 90 | file=&file, 91 | title=&title, 92 | footnote=&footnote, 93 | fnspace=&fnspace, 94 | page=&page, 95 | deids=&deids 96 | ); 97 | 98 | %mend ggBaseline; 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /01-Code/Macro/DM Macros/Winsor.sas: -------------------------------------------------------------------------------- 1 | /********************************************************** 2 | Purpose: trim or winsorize SAS dataset to remove the impact from extreme values 3 | 4 | Input 5 | dsetin : dataset to winsorize/trim 6 | byvar : define subset to winsorize/trim,e.g. 'date'. type 'none' for the whole dataset 7 | type : 'delete' or 'winsor' (delete will trim, winsor will winsorize 8 | vars : subsetting variables to winsorize/trim on; type 'none for no byvar 9 | pctl : the percenagte of left and right tails to trim/winsorize 10 | 11 | Output 12 | dsetout : dataset to output with winsorized/trimmed values 13 | ************************************************************/ 14 | 15 | %macro winsor(dsetin=, dsetout=, byvar=none, vars=, type=winsor, pctl=1 99); 16 | 17 | %if &dsetout = %then %let dsetout = &dsetin; 18 | 19 | %let varL=; 20 | %let varH=; 21 | %let xn=1; 22 | 23 | %do %until ( %scan(&vars,&xn)= ); 24 | %let token = %scan(&vars,&xn); 25 | %let varL = &varL &token.L; 26 | %let varH = &varH &token.H; 27 | %let xn=%EVAL(&xn + 1); 28 | %end; 29 | 30 | %let xn=%eval(&xn-1); 31 | 32 | data xtemp; 33 | set &dsetin; 34 | run; 35 | 36 | %if &byvar = none %then %do; 37 | 38 | data xtemp; 39 | set xtemp; 40 | xbyvar = 1; 41 | run; 42 | 43 | %let byvar = xbyvar; 44 | 45 | %end; 46 | 47 | proc sort data = xtemp; 48 | by &byvar; 49 | run; 50 | 51 | proc univariate data = xtemp noprint; 52 | by &byvar; 53 | var &vars; 54 | output out = xtemp_pctl PCTLPTS = &pctl PCTLPRE = &vars PCTLNAME = L H; 55 | run; 56 | 57 | data &dsetout; 58 | merge xtemp xtemp_pctl; 59 | by &byvar; 60 | array trimvars{&xn} &vars; 61 | array trimvarl{&xn} &varL; 62 | array trimvarh{&xn} &varH; 63 | 64 | do xi = 1 to dim(trimvars); 65 | 66 | %if &type = winsor %then %do; 67 | if not missing(trimvars{xi}) then do; 68 | if (trimvars{xi} < trimvarl{xi}) then trimvars{xi} = trimvarl{xi}; 69 | if (trimvars{xi} > trimvarh{xi}) then trimvars{xi} = trimvarh{xi}; 70 | end; 71 | %end; 72 | 73 | %else %do; 74 | if not missing(trimvars{xi}) then do; 75 | if (trimvars{xi} < trimvarl{xi}) then delete; 76 | if (trimvars{xi} > trimvarh{xi}) then delete; 77 | end; 78 | %end; 79 | 80 | end; 81 | drop &varL &varH xbyvar xi; 82 | run; 83 | 84 | %mend winsor; -------------------------------------------------------------------------------- /01-Code/Macro/IO Macros/Multi/xpt2sas.sas: -------------------------------------------------------------------------------- 1 | /* Macro using PROC COPY and the XPORT engine for reading transport files*/ 2 | %macro xpt2sas(dir,ext,out); 3 | %let filrf=mydir; 4 | /* Assigns the fileref of mydir to the directory and opens the directory */ 5 | %let rc=%sysfunc(filename(filrf,&dir)); 6 | %let did=%sysfunc(dopen(&filrf)); 7 | 8 | /* Returns the number of members in the directory */ 9 | %let memcnt=%sysfunc(dnum(&did)); 10 | 11 | /* Loops through entire directory */ 12 | %do i = 1 %to &memcnt; 13 | /* Returns the extension from each file */ 14 | %let name=%qscan(%qsysfunc(dread(&did,&i)),-1,.); 15 | /* Checks to see if file contains an extension */ 16 | %if %qupcase(%qsysfunc(dread(&did,&i))) ne %qupcase(&ext) %then %do; 17 | /* Checks to see if the extension matches the parameter value */ 18 | /* If condition is true, submit PROC COPY statement */ 19 | %if (%superq(ext) ne and %qupcase(&name) = %qupcase(&ext)) or (%superq(ext) = and %superq(name) ne) %then %do; 20 | libname old xport "&dir.\%qsysfunc(dread(&did,&i))"; 21 | libname new "&out"; 22 | proc copy in=old out=new; 23 | quit; 24 | %end; 25 | %end; 26 | %end; 27 | 28 | /* Close the directory */ 29 | %let rc=%sysfunc(dclose(&did)); 30 | %mend xpt2sas; 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /0-Pre.md: -------------------------------------------------------------------------------- 1 | # 0-作者序 2 | 蠢蠢欲动一年,奋指敲键三月,夜深人静百天,所幸的是这本书稿没有胎死腹中,终于写完了。动笔之前,我曾异常兴奋,我自以为满腹经纶无处释放的日子从此结束。完稿以后,我却沉静了,在接连填了一个又一个自己挖的坑以后,猛然抬头,发现后面其实还有更大的坑要去填,于是乎内心不禁更加焦虑。不过我很感激这份焦虑,虽然它不足以保证我所写出来的文字和代码是字字珠玑,篇篇精华,但是因为它,我可以挺起胸膛,拍着胸脯说:10 章专题 10 多万字,近 180 张图片、 30 多张表格和 200 多段代码, 20 多张语法卡片、 30个原创实用宏程序,这些都是热血铸就的良心作品,最起码它对得起我当初出发时的那份心意。 3 | 4 | ## 缘起 5 | 我还记得初学 SAS 编程时,因为看不懂 SAS Help 而懊恼,因为不理解 @ 与 @@ 的区别而苦恼,因为分不清宏变量的 %STR 、 %NRSTR、 %QUOTE、 %BQOTE、 %NRQUOTE以及 %NRBQUOTE 等诸多 quoting 函数而哀伤。然而,光阴似箭,似水流年,这才不过几年光景,那个曾经面对这些“简单问题”而烧心的少年,在面对后来同样烧心的学弟学妹时竟然一脸诧异:“啊?这个应该很容易理解的吧!” 你看,时间是多么的狡诈,它就这样轻易地抹平了我们学习过程中的苦与痛,当我们走得越远,当初的苦与痛就忘记得越多。庆幸的是,我不是什么大神,走得也不远,那些苦与痛还没有忘得一干二净,那就趁现在,赶紧记录下来,分享出来吧。 6 | 7 | ## 问题 8 | 此前知乎里有一个提问: SAS 入门书籍有哪些值得推荐?在回答里我把 SAS 学习分成了三类(点到即止,套 PROC 型;深入应用,编程统计型;走火入魔,开发工具型)并推荐了相应的书籍。在整理市面上 SAS 相关的书籍时,我总结了三个缺陷:①专门介绍数据整理与图表呈现的书太少、太零碎,即便有,也鲜有高质量者;②几乎都采用语法关键词按字典式的编排方式论述,缺乏从实际问题凝练的良好专题;③编程技术与使用场景割裂,讲技术者纯讲技术,缺少对应的应用场景带入感. 9 | 10 | ## 特色 11 | 本书试图在数据整理与图表呈现的内容上、编排方式上以及论述形式上有所突破和改进。 12 | 13 | 在内容上,顾名思义,专门讨论数据整理和统计图表的制作,不贪大求全、忌蜻蜓点水。精心提炼的 10 个专题总计 10 万字,涉及 SAS 的八卦见闻、 SAS 的基础知识、数据的导入导出方式、变量与观测的各种操作、数据集的各种操作与管理、函数与例程、输入输出格式、统计表格的制作、统计图形的绘制原则、选择方法以及各系列统计图形的绘制实例,此外对 SAS宏变量、宏程序以及开发宏程序的原则、步骤、技巧等内容均有较为详细的论述。在编排上,推陈出新,打破按语法关键字的字典式编排方式,精心挑选的 10 个专题构成 10 个既相对独立又互相联系的章节。小节与小节之间、例子与例子之间,尽量由问题层层引入,逐步推进,减少割裂与唐突感,增加使用场景的带入感。 14 | 15 | 此外,很多 SAS 用户虽然都了解、接受甚是已经受益于 SAS 在数据整理和统计分析方面毋庸置疑的优势,但是在统计结果的呈现上,尤其是统计表格,特别是统计图形方面都或多或少存在不甚了解抑或是误解的情况。因此,本书在统计表格的制作,尤其是统计绘图方面花了大量的笔墨做串讲——是的,用一个又一个层层递进的疑问来串讲,避免单 16 | 纯的介绍绘图语法和 SAS 技术,这在其他书中是很少见的。 17 | 18 | 最后,为了便于读者理解 SAS 运行机制与原理,本书在论述时都尽量采用小数据、小实例以便清晰简洁地说明问题,避免因行业背景的不同陷入具体实例的大坑。同时,为了方便读者练手测试,几乎所有数据均就地取材,采用 SASHelp 库中自带的数据集。 19 | 20 | ## 心得 21 | SAS Help 文档是学习 SAS 不可多得的手边精品材料,如果还没有深刻体会到这一点,那么赶紧去读读 R 包的 Help 文档。很多 SAS 书籍取材于 SAS Help 文档却闭口不提,这是一个巨大的失误。因此,本书会专门引导,鼓励读者去多读 SAS Help 文档、多查 SASHelp 文档。 22 | 23 | 学习知识的理想状况是单调线性、循序渐进的推进,然而现实情况却是:知识本身是错综复杂的网状结构。因此,我们经常需要迂回包抄、循环往复地学习。在介绍知识点时,本书努力做到直线推进、循序渐进,但由于作者精力、能力有限,加之知识网状结构的客观的、存在的现实,希望读者能有一个迂回包抄、循环往复的学习心态。当然,我也有一个迂回包抄、循环往复、精进迭代的心态。本书还有很多的话题,比如 SAS 的综合矩阵语言(Integrated Matrix Language , IML)、输出传递系统(Output delivery System, ODS)、正则表达式等没能在此版付诸实践;已经付诸实践的,也会因笔者的见识、学识以及精力受限,而有所欠缺。因此,诚恳地欢迎诸位读者给出您的建设性建议以及批评性意见,送达地址 guhongqiu(at)yeah(dot)net。有您的反馈,下一版(如果有的话),肯定会更好。 24 | 25 | ## 致谢 26 | 如果您读到这里了,请不要嫌我啰唆,因为一路走来,需要感谢的人特多,而且感谢应该是一个严肃的话题,因此,下面是一本正经的致谢。 27 | 28 | 感谢北京中医药大学曾光教授、刘仁权教授带我叩开流行病与卫生统计领域的大门;感谢中国疾病预防控制中心吴尊友教授教我公共卫生的大义;感谢北京协和医学院李卫教授携我走进临床研究的大门;感谢国家神经系统疾病临床医学研究中心王拥军、王伊龙教授给我机会在实践中提升临床研究思维与技能。感 谢《The Little SAS Book》 的 作 者 Lora D. Delwiche 女 士, 著 名 SAS 绘 图 博 客Graphically Speaking 的博主、众多 SAS 绘图专著的作者 Sanjay Matange 先生,以及《The DS2 Procedure: SAS Programming Methods at Work》的作者 Peter Eberhardt 先生在本书写作 29 | 过程中给予的支持和帮助。 30 | 31 | 感谢 SAS 中国研发中心总经理刘政先生;感谢 SAS 中国研发中心分析产品开发部总监高燕女士、 SAS 中国研发中心商业智能和可视化分析产品部技术总监巫银良先生、 SAS中国区培训经理赵丹先生、 SAS 大中华区市场总监蒋顺利先生的于我准备书稿过程给予的支持;感谢 SAS 中文论坛创始人、前海征信副总经理施亦明先生, SAS 中文资讯网的创始人 sxlion 以及人大经济论坛里的一大波 ID(jingju11、 pobel、 hopewell、 davil2000、kuhasu、 ahuige、soporaeternus、 YueweiLiu、 oloolo、 bobguy、 Imasasor、 playmore、crackman、 dxystata)在 SAS 的江湖里传道解惑。感谢本书的编辑,清华大学出版社的刘洋先生。没有他的信任,这本书可能会散落于江湖;没有他的信任,写作可能会被无数次的催稿打断。还好,他对我和这本书稿一直保持足够的耐心。再次感谢清华大学出版社编辑部,精心挑选每章首页的山水画,配合标题,意境深远。 32 | 33 | 来北京十多年,感谢中国气象科学研究院谷湘潜研究员、首都医科大学附属北京地坛医院江宇泳教授给予的各方面关照;感谢中南大学谷潜平教授的建议;感谢国家神经系统疾病临床医学研究中心的王彩云主任早上的烤红薯——无上美味、香甜至极。最后,感谢因为 SAS、因为此书,和我有了交集的你。 34 | 35 | 谷鸿秋 36 | 2017 年 5 月 17 日 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 0-作者序 2 | 蠢蠢欲动一年,奋指敲键三月,夜深人静百天,所幸的是这本书稿没有胎死腹中,终于写完了。动笔之前,我曾异常兴奋,我自以为满腹经纶无处释放的日子从此结束。完稿以后,我却沉静了,在接连填了一个又一个自己挖的坑以后,猛然抬头,发现后面其实还有更大的坑要去填,于是乎内心不禁更加焦虑。不过我很感激这份焦虑,虽然它不足以保证我所写出来的文字和代码是字字珠玑,篇篇精华,但是因为它,我可以挺起胸膛,拍着胸脯说:10 章专题 10 多万字,近 180 张图片、 30 多张表格和 200 多段代码, 20 多张语法卡片、 30个原创实用宏程序,这些都是热血铸就的良心作品,最起码它对得起我当初出发时的那份心意。 3 | 4 | ## 缘起 5 | 我还记得初学 SAS 编程时,因为看不懂 SAS Help 而懊恼,因为不理解 @ 与 @@ 的区别而苦恼,因为分不清宏变量的 %STR 、 %NRSTR、 %QUOTE、 %BQOTE、 %NRQUOTE以及 %NRBQUOTE 等诸多 quoting 函数而哀伤。然而,光阴似箭,似水流年,这才不过几年光景,那个曾经面对这些“简单问题”而烧心的少年,在面对后来同样烧心的学弟学妹时竟然一脸诧异:“啊?这个应该很容易理解的吧!” 你看,时间是多么的狡诈,它就这样轻易地抹平了我们学习过程中的苦与痛,当我们走得越远,当初的苦与痛就忘记得越多。庆幸的是,我不是什么大神,走得也不远,那些苦与痛还没有忘得一干二净,那就趁现在,赶紧记录下来,分享出来吧。 6 | 7 | ## 问题 8 | 此前知乎里有一个提问: SAS 入门书籍有哪些值得推荐?在回答里我把 SAS 学习分成了三类(点到即止,套 PROC 型;深入应用,编程统计型;走火入魔,开发工具型)并推荐了相应的书籍。在整理市面上 SAS 相关的书籍时,我总结了三个缺陷:①专门介绍数据整理与图表呈现的书太少、太零碎,即便有,也鲜有高质量者;②几乎都采用语法关键词按字典式的编排方式论述,缺乏从实际问题凝练的良好专题;③编程技术与使用场景割裂,讲技术者纯讲技术,缺少对应的应用场景带入感. 9 | 10 | ## 特色 11 | 本书试图在数据整理与图表呈现的内容上、编排方式上以及论述形式上有所突破和改进。 12 | 13 | 在内容上,顾名思义,专门讨论数据整理和统计图表的制作,不贪大求全、忌蜻蜓点水。精心提炼的 10 个专题总计 10 万字,涉及 SAS 的八卦见闻、 SAS 的基础知识、数据的导入导出方式、变量与观测的各种操作、数据集的各种操作与管理、函数与例程、输入输出格式、统计表格的制作、统计图形的绘制原则、选择方法以及各系列统计图形的绘制实例,此外对 SAS宏变量、宏程序以及开发宏程序的原则、步骤、技巧等内容均有较为详细的论述。在编排上,推陈出新,打破按语法关键字的字典式编排方式,精心挑选的 10 个专题构成 10 个既相对独立又互相联系的章节。小节与小节之间、例子与例子之间,尽量由问题层层引入,逐步推进,减少割裂与唐突感,增加使用场景的带入感。 14 | 15 | 此外,很多 SAS 用户虽然都了解、接受甚是已经受益于 SAS 在数据整理和统计分析方面毋庸置疑的优势,但是在统计结果的呈现上,尤其是统计表格,特别是统计图形方面都或多或少存在不甚了解抑或是误解的情况。因此,本书在统计表格的制作,尤其是统计绘图方面花了大量的笔墨做串讲——是的,用一个又一个层层递进的疑问来串讲,避免单 16 | 纯的介绍绘图语法和 SAS 技术,这在其他书中是很少见的。 17 | 18 | 最后,为了便于读者理解 SAS 运行机制与原理,本书在论述时都尽量采用小数据、小实例以便清晰简洁地说明问题,避免因行业背景的不同陷入具体实例的大坑。同时,为了方便读者练手测试,几乎所有数据均就地取材,采用 SASHelp 库中自带的数据集。 19 | 20 | ## 心得 21 | SAS Help 文档是学习 SAS 不可多得的手边精品材料,如果还没有深刻体会到这一点,那么赶紧去读读 R 包的 Help 文档。很多 SAS 书籍取材于 SAS Help 文档却闭口不提,这是一个巨大的失误。因此,本书会专门引导,鼓励读者去多读 SAS Help 文档、多查 SASHelp 文档。 22 | 23 | 学习知识的理想状况是单调线性、循序渐进的推进,然而现实情况却是:知识本身是错综复杂的网状结构。因此,我们经常需要迂回包抄、循环往复地学习。在介绍知识点时,本书努力做到直线推进、循序渐进,但由于作者精力、能力有限,加之知识网状结构的客观的、存在的现实,希望读者能有一个迂回包抄、循环往复的学习心态。当然,我也有一个迂回包抄、循环往复、精进迭代的心态。本书还有很多的话题,比如 SAS 的综合矩阵语言(Integrated Matrix Language , IML)、输出传递系统(Output delivery System, ODS)、正则表达式等没能在此版付诸实践;已经付诸实践的,也会因笔者的见识、学识以及精力受限,而有所欠缺。因此,诚恳地欢迎诸位读者给出您的建设性建议以及批评性意见,送达地址 guhongqiu(at)yeah(dot)net。有您的反馈,下一版(如果有的话),肯定会更好。 24 | 25 | ## 致谢 26 | 如果您读到这里了,请不要嫌我啰唆,因为一路走来,需要感谢的人特多,而且感谢应该是一个严肃的话题,因此,下面是一本正经的致谢。 27 | 28 | 感谢北京中医药大学曾光教授、刘仁权教授带我叩开流行病与卫生统计领域的大门;感谢中国疾病预防控制中心吴尊友教授教我公共卫生的大义;感谢北京协和医学院李卫教授携我走进临床研究的大门;感谢国家神经系统疾病临床医学研究中心王拥军、王伊龙教授给我机会在实践中提升临床研究思维与技能。感 谢《The Little SAS Book》 的 作 者 Lora D. Delwiche 女 士, 著 名 SAS 绘 图 博 客Graphically Speaking 的博主、众多 SAS 绘图专著的作者 Sanjay Matange 先生,以及《The DS2 Procedure: SAS Programming Methods at Work》的作者 Peter Eberhardt 先生在本书写作 29 | 过程中给予的支持和帮助。 30 | 31 | 感谢 SAS 中国研发中心总经理刘政先生;感谢 SAS 中国研发中心分析产品开发部总监高燕女士、 SAS 中国研发中心商业智能和可视化分析产品部技术总监巫银良先生、 SAS中国区培训经理赵丹先生、 SAS 大中华区市场总监蒋顺利先生的于我准备书稿过程给予的支持;感谢 SAS 中文论坛创始人、前海征信副总经理施亦明先生, SAS 中文资讯网的创始人 sxlion 以及人大经济论坛里的一大波 ID(jingju11、 pobel、 hopewell、 davil2000、kuhasu、 ahuige、soporaeternus、 YueweiLiu、 oloolo、 bobguy、 Imasasor、 playmore、crackman、 dxystata)在 SAS 的江湖里传道解惑。感谢本书的编辑,清华大学出版社的刘洋先生。没有他的信任,这本书可能会散落于江湖;没有他的信任,写作可能会被无数次的催稿打断。还好,他对我和这本书稿一直保持足够的耐心。再次感谢清华大学出版社编辑部,精心挑选每章首页的山水画,配合标题,意境深远。 32 | 33 | 来北京十多年,感谢中国气象科学研究院谷湘潜研究员、首都医科大学附属北京地坛医院江宇泳教授给予的各方面关照;感谢中南大学谷潜平教授的建议;感谢国家神经系统疾病临床医学研究中心的王彩云主任早上的烤红薯——无上美味、香甜至极。最后,感谢因为 SAS、因为此书,和我有了交集的你。 34 | 35 | 谷鸿秋 36 | 2017 年 5 月 17 日 37 | -------------------------------------------------------------------------------- /01-Code/style/gghtml2.sas: -------------------------------------------------------------------------------- 1 | *==============================================================================================* 2 | Book: SAS编程演义/Romance of SAS Programming 3 | Author:谷鸿秋/Hongqiu Gu 4 | Contact:guhongqiu(at)yeah(dot)net 5 | Book:https://item.jd.com/12210370.html#crumb-wrap 6 | *==============================================================================================*; 7 | proc template; 8 | define style Styles.Gghtml2; 9 | parent = Styles.Htmlblue; 10 | style Graph from Graph / 11 | width = 14cm 12 | height = 10cm 13 | borderwidth = 0; 14 | style GraphBorderLines from GraphBorderLines / 15 | linethickness = 0px 16 | linestyle = 1; 17 | style GraphOutlines from GraphOutlines / 18 | linestyle = 1 19 | linethickness = 0px; 20 | style GraphWalls from GraphWalls / 21 | frameborder = off 22 | linethickness = 0px 23 | linestyle = 1; 24 | class GraphColors 25 | "Abstract colors used in graph styles" / 26 | 'gcdata7' = cxfb61d7 27 | 'gdata7' = cxfb61d7 28 | 'gcdata6' = cxa58aff 29 | 'gdata6' = cxa58aff 30 | 'gcdata5' = cx00b6eb 31 | 'gdata5' = cx00b6eb 32 | 'gcdata4' = cx00c094 33 | 'gdata4' = cx00c094 34 | 'gcdata3' = cx53b400 35 | 'gdata3' = cx53b400 36 | 'gcdata2' = cxc49a00 37 | 'gdata2' = cxc49a00 38 | 'gcdata1' = cxf8766d 39 | 'gdata1' = cxf8766d 40 | 'gcdata' = cxf8766d 41 | 'gdata' = cxf8766d; 42 | class GraphDataDefault / 43 | markersymbol = "CircleFilled"; 44 | class GraphData1 / 45 | markersymbol = "CircleFilled"; 46 | class GraphData2 / 47 | markersymbol = "CircleFilled"; 48 | class GraphData3 / 49 | markersymbol = "CircleFilled"; 50 | class GraphData4 / 51 | markersymbol = "CircleFilled"; 52 | class GraphData5 / 53 | markersymbol = "CircleFilled"; 54 | class GraphData6 / 55 | markersymbol = "CircleFilled"; 56 | class GraphData7 / 57 | markersymbol = "CircleFilled"; 58 | end; 59 | run; 60 | -------------------------------------------------------------------------------- /01-Code/style/ggplot2.sas: -------------------------------------------------------------------------------- 1 | *==============================================================================================* 2 | Book: SAS编程演义/Romance of SAS Programming 3 | Author:谷鸿秋/Hongqiu Gu 4 | Contact:guhongqiu(at)yeah(dot)net 5 | Book:https://item.jd.com/12210370.html#crumb-wrap 6 | *==============================================================================================*; 7 | 8 | proc template; 9 | define style Styles.ggplot2; 10 | parent = styles.listing; 11 | style color_list from color_list 12 | "Abstract colors used in graph styles" / 13 | 'bgA' = cxffffff; 14 | class GraphColors 15 | "Abstract colors used in graph styles" / 16 | 'gdata' = cxf8766d 17 | 'gcdata' = cxf8766d 18 | 'gdata1' = cxf8766d 19 | 'gcdata1' = cxf8766d 20 | 'gdata2' = cxc49a00 21 | 'gcdata2' = cxc49a00 22 | 'gdata3' = cx53b400 23 | 'gcdata3' = cx53b400 24 | 'gdata4' = cx00c094 25 | 'gcdata4' = cx00c094 26 | 'gdata5' = cx00b6eb 27 | 'gcdata5' = cx00b6eb 28 | 'gdata6' = cxa58aff 29 | 'gcdata6' = cxa58aff 30 | 'gdata7' = cxfb61d7 31 | 'gcdata7' = cxfb61d7 32 | 'ggrid' = cxFFFFFF 33 | 'glegend' = cxebebeb 34 | 'gwalls' = cxebebeb; 35 | class GraphWalls / 36 | color = GraphColors('gwalls') 37 | backgroundcolor = GraphColors('gwalls') 38 | contrastcolor = GraphColors('gwalls') 39 | frameborder = on 40 | linestyle = 1 41 | linethickness = 1px; 42 | class GraphGridLines / 43 | color = GraphColors('ggrid') 44 | contrastcolor = GraphColors('ggrid') 45 | linestyle = 1 46 | linethickness = 1px 47 | displayopts = "on"; 48 | class GraphBox / 49 | displayopts = "fill median mean outliers" 50 | connect = "mean" 51 | capstyle = "serif"; 52 | style Graph from Graph / 53 | borderwidth = 0 54 | height = 10cm 55 | width = 14cm; 56 | style GraphBorderLines from GraphBorderLines / 57 | linestyle = 1 58 | linethickness = 0px; 59 | style GraphOutlines from GraphOutlines / 60 | linethickness = 0px 61 | linestyle = 1; 62 | class GraphDataDefault / 63 | markersymbol = "CircleFilled"; 64 | class GraphData1 / 65 | markersymbol = "CircleFilled"; 66 | class GraphData2 / 67 | markersymbol = "CircleFilled"; 68 | class GraphData3 / 69 | markersymbol = "CircleFilled"; 70 | class GraphData4 / 71 | markersymbol = "CircleFilled"; 72 | class GraphData5 / 73 | markersymbol = "CircleFilled"; 74 | class GraphData6 / 75 | markersymbol = "CircleFilled"; 76 | class GraphData7 / 77 | markersymbol = "CircleFilled"; 78 | class GraphAxisLines / 79 | linestyle = 1 80 | linethickness = 0px 81 | tickdisplay = "outside"; 82 | end; 83 | run; 84 | -------------------------------------------------------------------------------- /01-Code/Macro/ggBaseline/ggBaseline1.sas: -------------------------------------------------------------------------------- 1 | *=================================================== 2 | %ggBaseline1 paramter: for one group baseline infor 3 | Author:Hongqiu Gu 4 | Contact: guhongqiu@yeah.net 5 | Date: V20161220 6 | For details, please see Ann Transl Med, 2018, 6(16): 326. 7 | 8 | Copyright: CC BY-NC-SA 4.0 9 | In short: you are free to share and make derivative 10 | works of the work under the conditions that you appropriately 11 | attribute it, you use the material only for non-commercial 12 | purposes, and that you distribute it only under a license 13 | compatible with this one. 14 | -------------------------------------------------- 15 | 16 | Usage example: 17 | %ggBaseline1( 18 | data=, 19 | var=var1|CTN|label1\ 20 | var2|CTG|label2, 21 | exmisspct=Y|N, 22 | filetype=RTF|PDF, 23 | file=&ProjPath\05-Out\01-Table\, 24 | title=, 25 | footnote=, 26 | fnspace=20, 27 | page=PORTRAIT|LANDSCAPE, 28 | deids=Y|N, 29 | ) 30 | 31 | 32 | Log: 33 | 7.20190320 Change param name exmissing to exmisspct 34 | 6.20190320 Universe encoding for en dash and plus or minus 35 | 5.20181019 Fix nmiss(%) 36 | 4.20180806 Change nMISS to nmiss(%) 37 | 3.20180725 Add exmissing parameter 38 | 2.20161220 Finsh the macro 39 | 1.20161130 Get the idea 40 | ====================================================; 41 | 42 | %macro ggBaseline1( 43 | data=, 44 | var=, 45 | exmisspct=Y, 46 | filetype=rtf, 47 | file=, 48 | title=, 49 | footnote=, 50 | fnspace=, 51 | page=portrait, 52 | deids=Y 53 | ); 54 | 55 | options minoperator nofmterr; 56 | dm odsresults 'clear' continue; 57 | 58 | /*===Delete last time report data===*/ 59 | proc datasets lib=work memtype=data; 60 | delete Base1 desc: dslabel: merge:; 61 | run; 62 | 63 | 64 | /*===Get Libname and memname===*/ 65 | %let ndsn=%sysfunc(countw(&data,.)); 66 | 67 | %if &ndsn eq 1 %then %do; 68 | %let libname=WORK; 69 | %let memname=%scan(&data,1, "("); 70 | %end; 71 | 72 | %else %do; 73 | %let libname=%scan(&data,1,.); 74 | %let memname=%scan(%scan(&data,2,.),1, "("); 75 | %end; 76 | 77 | 78 | 79 | /*===Loop every var===*/ 80 | %let nvar=%sysfunc(countw(&var,%str(\))); 81 | 82 | %do n=1 %to &nvar; 83 | %let var&n=%scan(%qscan(&var,&n,%str(\)),1,|); 84 | %let vartype&n=%scan(%qscan(&var,&n,%str(\)),2,|); 85 | %let varlabel&n=%scan(%qscan(&var,&n,%str(\)),3,|); 86 | 87 | 88 | /*===Check the existance of dataset and var==*/ 89 | %local dsid check rc; 90 | %let dsid = %sysfunc(open(&libname..&memname)); 91 | %if &dsid=0 %then %do; 92 | %put ERROR: Dataset &libname..&memname is not exist!; 93 | %abort; 94 | %end; 95 | %else %do; 96 | %let checkvar = %sysfunc(varnum(&dsid.,&&var&n)); 97 | %let rc = %sysfunc(close(&dsid.)); 98 | %if &checkvar. = 0 %then %do; 99 | %put ERROR: Variable &&var&n does not exists!; 100 | %abort; 101 | %end; 102 | %end; 103 | 104 | 105 | /* Get vtype for format */ 106 | data _null_; 107 | set sashelp.vcolumn; 108 | where upcase(libname)=upcase("&libname") and upcase(memname)=upcase("&memname") and upcase(name)=upcase("&&var&n"); 109 | call symputx(cats("vtype",&n),type); 110 | run; 111 | 112 | %descB1(data=&data, var=&&var&n, vtype=&&vtype&n, vartype=&&vartype&n,varlabel=%quote(&&varlabel&n),n=&n) 113 | %end; 114 | 115 | data Base1; 116 | format order label col1; 117 | set merge:; 118 | if strip(label)="^{nbspace 6}" then label="^{nbspace 6}Missing"; 119 | run; 120 | 121 | proc sort data=Base1; 122 | by order; 123 | run; 124 | 125 | proc sql noprint; 126 | select count(*) into: ntotal 127 | from &data; 128 | quit; 129 | 130 | 131 | %ggReportB1(data=Base1,filetype=&filetype, file=&file, title=&title, footnote=&footnote, fnsapce=&fnspace, page=&page) 132 | 133 | 134 | /*===Delete tmp data==*/ 135 | 136 | %if %upcase(&deids) EQ Y %then %do; 137 | proc datasets lib=work memtype=data; 138 | save Base1; 139 | quit; 140 | %end; 141 | 142 | %mend ggBaseline1; 143 | 144 | 145 | 146 | %macro descB1(data=, var=, vtype=, vartype=,varlabel=,n=); 147 | %if %upcase(&vartype) EQ CTG %then %do; 148 | %desc_ctg_b1(data=&data, var=&var,vtype=&vtype, varlabel=&varlabel,n=&n, exmisspct=&exmisspct) 149 | %end; 150 | 151 | %else %if %upcase(&vartype) EQ CTN %then %do; 152 | %desc_ctn_b1(data=&data, var=&var, varlabel=&varlabel,n=&n) 153 | %end; 154 | %mend descB1; 155 | 156 | 157 | 158 | %macro desc_ctg_b1(data=, var=, vtype=, varlabel=, n=, exmisspct=); 159 | proc freq data=&data noprint; 160 | table &var/ missing outcum out=desc&n._n; 161 | 162 | %if %upcase(&exmisspct) eq Y %then %do; 163 | table &var/outcum out=desc&n._y; 164 | %end; 165 | 166 | 167 | %if &vtype eq char %then %do; 168 | format &var $&var.fmt.; 169 | %end; 170 | %else %do; 171 | format &var &var.fmt.; 172 | %end; 173 | run; 174 | 175 | %if %upcase(&exmisspct) eq Y %then %do; 176 | data desc&n; 177 | update desc&n._n desc&n._Y; 178 | by &var; 179 | run; 180 | %end; 181 | 182 | %else %do; 183 | data desc&n; 184 | set desc&n._n; 185 | run; 186 | %end; 187 | 188 | data desc&n; 189 | set desc&n; 190 | length value $25; 191 | value = compress(put(count,6.)) || ' (' ||compress(put(percent,4.1))||')'; 192 | 193 | run; 194 | 195 | proc sort data=desc&n; 196 | by &var; 197 | run; 198 | 199 | proc transpose data=desc&n out=desc&n (drop=_name_) prefix=col; 200 | by &var; 201 | var value; 202 | run; 203 | 204 | data dslabel&n; 205 | length label $ 85; 206 | label = cats("^S={font_weight=bold}" , "&varlabel"); 207 | run; 208 | 209 | 210 | data merge&n; 211 | keep order label col1; 212 | length label $ 85; 213 | set dslabel&n desc&n; 214 | if _n_ > 1 then label= "^{nbspace 6}" || put(&var,&var.fmt.); 215 | order=&n; 216 | run; 217 | 218 | %mend desc_ctg_b1; 219 | 220 | 221 | %macro desc_ctn_b1(data=, var=, varlabel=, n=); 222 | ods output summary=desc&n; 223 | proc means data=&data n nmiss mean std min max median q1 q3 maxdec=1; 224 | var &var; 225 | run; 226 | 227 | data desc&n(keep= nnmiss meanstd minmax medianIQR); 228 | set desc&n; 229 | format nnmiss meanstd minmax medianIQR; 230 | 231 | if &var._nmiss ^=0 then do; 232 | nnmiss = compress(put(&var._nmiss,6.))||" ("||compress(put(&var._nmiss/nobs*100,4.1))||")"; 233 | end; 234 | 235 | minmax = cats(put(&var._min,12.1),unicode("–","ncr"), put(&var._max,12.1)); 236 | meanstd = cats(put(&var._mean,12.1), byte(177), put(&var._stddev,12.1)); 237 | medianIQR = compress(put(&var._median,6.1))|| " ("||compress(put(&var._q1,6.1))||unicode("–","ncr")||compress(put(&var._q3,6.1))||")"; 238 | run; 239 | 240 | proc transpose data=desc&n out=desc&n prefix=col; 241 | var nnmiss meanstd minmax medianIQR; 242 | run; 243 | 244 | data dslabel&n; 245 | length label $ 85; 246 | label = cats("^S={font_weight=bold}" , "&varlabel"); 247 | run; 248 | 249 | 250 | data merge&n; 251 | keep order label col1 ; 252 | length label $ 85 ; 253 | set dslabel&n desc&n(where=(not missing(col1))); 254 | if _n_ > 1 then 255 | select; 256 | when(_NAME_ = 'nnmiss') label = "^{nbspace 6}Nmiss (%)"; 257 | when(_NAME_ = 'minmax') label = "^{nbspace 6}Min"||unicode("–","ncr")||"Max"; 258 | when(_NAME_ = 'meanstd') label = "^{nbspace 6}Mean%sysfunc(byte(177))SD"; 259 | when(_NAME_ = 'medianIQR') label = "^{nbspace 6}Median (IQR)"; 260 | otherwise; 261 | end; 262 | order=&n; 263 | run; 264 | 265 | 266 | %mend desc_ctn_b1; 267 | 268 | 269 | 270 | %macro ggReportB1(data= ,filetype=, file=, title=, footnote=, fnsapce=, page=); 271 | 272 | options nodate nonumber orientation=&page missing = ''; 273 | ods escapechar='^'; 274 | 275 | %if %upcase(&filetype) EQ RTF %then %do; 276 | ods tagsets.rtf style=journal3a file="&file..&filetype"; 277 | %end; 278 | %if %upcase(&filetype) EQ PDF %then %do; 279 | ods pdf style=journal3a file="&file..&filetype"; 280 | %end; 281 | 282 | title j=center height=12pt "^{nbspace &fnspace}&title"; 283 | 284 | proc report data=&data nowindows spacing=1 headline headskip split = "|" ; 285 | columns (order label col1); 286 | define order/order noprint; 287 | define label /display " Variables"; 288 | define col1 /display %sysfunc(compress("Statistics |(N=&ntotal)")) right ; 289 | 290 | 291 | 292 | %if &footnote NE %str() %then %do; 293 | footnote1 j=left height=10pt "^{nbspace &fnspace}Note: &footnote"; 294 | %end; 295 | run; 296 | 297 | %if %upcase(&filetype) EQ RTF %then %do; 298 | ods tagsets.&filetype close; 299 | %end; 300 | %if %upcase(&filetype) EQ PDF %then %do; 301 | ods pdf close; 302 | %end; 303 | 304 | title; 305 | footnote1; 306 | footnote2; 307 | 308 | %mend ggReportB1; 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | -------------------------------------------------------------------------------- /Chapter-1.md: -------------------------------------------------------------------------------- 1 | # C1-人生若只如初见:初识 SAS 2 | 3 | 清代词人纳兰性德曾有词曰“人生若只如初见,何事秋风悲画扇”。从初识 SAS 到如今每天的工作都和 SAS 纠缠不清,按理来说我和 SAS 应该早已过了“人生若只如初见”的美好阶段,但是每次当我有疑惑再去查阅 SAS Help 文档、琢磨 SAS 时却常有“日暮北风吹雨去,数峰清瘦出云来”的惊喜。我不知道以后我会不会将 SAS 当成秋扇束之高阁,但目前我还有些激情和热血,那就趁热记录、分享一下这一路走来和 SAS 的点点滴滴吧。 4 | 5 | ## 1.1 往事并不如烟 6 | 关于 SAS,这里面有很多有意思的往事。从简简单单的名字发音,到颇为有趣的公司历史,再到刀光剑影,快意人生的网络江湖,每一个话题都值得煮一杯清酒开怀畅谈。 7 | 8 | ### 1.1.1 逗你玩的发音 9 | 第一次听说 SAS(Statistical Analysis System)是在本科的统计软件包课上,当时我以为老师说的是 SARS(Severe Acute Respiratory Syndrome,严重急性呼吸综合征),因为老师的发音大概就是“萨死”,这不禁让我想起 2003 年刚经历的那场不堪回首的全民浩劫“非典”。 10 | 11 | 后来,我才留意到原来我们的英语发音是多么的糟糕,或者说是多么的随意。 SAS 的正式发音大概是“赛死”,所以 SAS 公司在中国注册的中文企业名用的是 “赛仕”,而SARS 的正确发音是“萨而死”,因为中间有卷舌音。更让人啼笑皆非的是 SPSS 的读法,很多统计老师即便是在大庭广众之下也毫无羞涩地脱口而出“死怕死”,其实由于 SPSS没有元音字母,正确的发音应该是“爱死辟,爱死爱死”。 12 | 13 | ### 1.1.2 有点趣的历史 14 | 15 | 关于 SAS(Statistical Analysis System)这一词可以有多种层次的解读。 SAS 可以是业界最负盛名的一个统计分析系统,也可以是一门德高望重的统计编程语言,还可以是一个颇具传奇色彩的商业分析软件与服务供应商。 16 | 17 | SAS 作为一个统计分析系统和一门统计编程语言,要远早于其作为一家商业公司 (见图 1-1)。1966 年,美国农业部(United States Department of Agriculture , USDA)把海量农业数据的计算机化和统计分析需求委托给大学统计师南方实验站(University StatisticiansSouthern Experiment Stations),希望开发出一种具有综合用途的统计软件包,以便分析他们获取的所有农业数据。这个试验站联盟了以北卡罗莱纳州立大学(North Carolina StateUniversity, NCSU)为主的八家政府资助大学,他们从美国农业部获得科研经费,并从美国国立卫生研究院(National Institutes of Health, NIH)获得了一笔捐赠,最终的研究成果即统计分析系统(Statistical Analysis System, SAS)。北卡罗来纳州立大学的教职员工Jim Goodnight 与 Jim Barr(Jim Barr 又名 Anthony James Barr, Tony Barr )为项目负责人,Jim Barr 创造了整个项目框架,而 Jim Goodnight 则负责实施框架之上的各种特性,并拓展了系统的功能。 1972 年 NIH 终止了资助之后,试验站联盟的成员们同意共同出资,每个成员每年出资 5000 美元, NCSU 也由此得以继续开发并维持系统运作,从而支持其统计分析需求。此后, NCSU 的统计系雇员 Jane Helwig、研究生与程序员 John Sall 也加入了该项目。1976 年,他们离开 NCSU,在大学的对面希尔博拉大街 2806 号的一幢办公楼里组建了私人公司 SAS 研究所(SAS Institute Inc.)。 SAS 公司成立早期, Jim Barr、 JimGoodnight 和 John Sall 三人负责敲代码, Jane Helwig 则负责 SAS 文档的规划和书写 (见图 1-2)。目前, Jim Goodnight 仍然是公司的 CEO, John Sall 已经是公司的二把手,他还一手缔造了 SAS 软件的兄弟产品 JMP。 Jim Barr 后来单飞,又创立了 Barr Systems 公司,关于 Jane Helwig,但网上信息寥寥。 18 | 19 | SAS 公司成立当年,他们做了两件大事:一是发布了第一个商用版本 SAS 软件;二是举办了第一届 SAS 用户国际组会(SAS Users Group International, SUGI)。这两件事无论是对 SAS 公司还是对 SAS 用户来说都意义深远。 SAS 软件发布一年后,便入榜Datamation 杂志举办的 DataPro 软件光荣榜,此后三年仍位列榜上。 SAS 软件系统自发布到现在,经历了很多变革。早期版本的 SAS 运行于大型机上, 1985 年 SAS 公司发布了运行于 PC DOS 版本的 SAS 5, 1988 年发布了用 C 语言全部重写的 SAS 6,并开始支持Windows 操作系统, 2000 年 SAS 8 开始支持 Linux 操作系统,目前(2017 年) SAS 软件最新版本是 9.4,包含了支持高性能统计建模、分布式内存计算、可视化统计分析等诸多适应大数据时代的新特性。更多关于 SAS 软件的历史,推荐 SAS 官方的 2 分 24 秒宣传视频:SAS Timeline: A History of the Analytics Leader。 20 | 21 | SUGI 自第一届成功举办以来,每年参加的人数都迅猛上升,成为全球 SAS 用户分享交流的盛宴。 2007 年 SUGI 更名为 SAS 全球论坛(SAS Global Forum, SGF)后,吸引了全球更多行业的 SAS 用户参与分享交流。现如今, SAS 公司仍然是全球最大的商业分析软件与服务供应商,据说 Jim Goodnight 为了保持公司的独立发展战略,一直拒绝上市,在传统的统计软件公司要么消失、要么被合并的洪流中, SAS 公司竟然保持了一枝独秀的状态。目前 SAS 公司全球雇员超过 1 万多名,客户遍及全球 149 个国家,应用领域涉及银行、政府、服务、保险以及生命科学等各行各业(见图 1-3)。 SAS 公司凭借其卓越的表现将诸多殊荣收入囊中,如“在职母亲最适宜公司”“全球最佳雇主”“最受欢迎的百强企业”……这与其创始人 Jim Goodnight 的人才理念不无关系: If you treat employees as if they make a difference to the company, they will make a difference。 22 | 23 | 2000 年 SAS 公司启用新的 Logo 和标语: THE POWER TO KNOW®。通过数据,探知世界,这是数据分析的终极目的,提供探知的力量,这是 SAS 所努力的方向。关于这个宣传语,同样推荐 SAS 公司官方宣传视频: Know all the possibilities with SAS® HighPerformance Analytics。 24 | 25 | ### 1.1.3 逝不去的江湖 26 | 介绍 SAS 的历史,毕竟不是笔者分内的事,聊聊网络江湖中 SASor(SAS 爱好者)的快意恩仇,不失为乐事一件。 27 | 28 | 在微博、微信还没有崛起的年代,网络论坛(Bulletin Board System, BBS)一统天下。在论坛里注册个名号,就像武林人士有了个绰号:比如行者武松、浪子燕青、花和尚鲁智深、一丈青扈三娘什么的,就可以在网上行走江湖了。 SAS 武林,最早可能是在 imoen 创立的 SASOR 论坛(www.sasor.com )里,里面的风云人物如 SAS_Deam、 data _null_ 等。关于 SAS_Deam,网上的痕迹很少,目前可以找到的只有其两篇文章——《关于 SAS 的零碎印象》和《SAS 语言管窥》; data _null_ 在 UGA 大学邮件列表 SAS-L 还有活动记录。 Shiyiming 建立的 SAS 中文论坛(http://www.mysas.net/ )和 sxlion 倒腾的 SAS 资源资讯列表(http://saslist.net/ )也是承载了很多 SASor 记忆的地方。人大经济论坛的 SAS 专版(http://bbs.pinggu.org/forum-68-1.html )可能是目前少有的还在和微博、微信抢流量,做垂死挣扎的网络论坛。 29 | 30 | 网络论坛里有一大批熟悉的 ID,通常为了解决一个小问题,各路大神前赴后继贴代码,只为一比高下,就像武林人士的擂台赛,好生热闹。在微博、微信一统天下后,转发和点赞成为常态,拼代码、讨论帖子已然成为过去。这正如 sxlion 所写的:“可惜美好的时光不长久,春去秋来,草长莺飞。论坛 ID 后面一个个现实生活中的 SASor,或结婚生子,或迁徙他乡,或跳槽转行,人生变幻,几度春秋,论坛里新人经常有,故人不常在。美好时光,竟成稀缺的回忆。”好在论坛里沉淀的帖子记录下了岁月的痕迹,论坛虽然逐渐消逝,但微信公众平台或者其他网络社群会随即出现, SASor 也会不断更替。常言道,有人的地方就有江湖,人就是江湖, SASor 还在, SAS 的江湖如何逝去? 31 | 32 | ## 1.2 选择一厢情愿 33 | 如果我们只是想“放一枪”就走人,断定以后几乎再也不会“拿枪”了,那么可能用SPSS 会更合适我们;如果我们想做一个专业“抢手”,那就应该选择更专业一点儿的武器。古语有言曰“工欲善其事,必先利其器”,诚然,一个合适的统计分析工具可以让我们的统计分析工作事半功倍,而一个蹩脚的统计分析工具则有可能浪费我们大量宝贵的时间和资源。目前的统计分析软件,主要分两大类:一类基于图形用户界面(Graphical User Interface, GUI)(如 SPSS);另一类基于命令行界面(Command Line Interface, CLI)(如SAS、 R 及 Stata)。 GUI 和 CLI 两种形式各有优劣, GUI 通过点击菜单完成数据处理和统 34 | 35 | 计分析,对于非统计人员来说,操作简单容易,但其可重复性差,也不便留痕和记录,此外,菜单式的界面能容纳的统计过程和选项有限,无法快速跟进学科的发展; CLI 则通过命令行或者编程语言完成数据处理和统计分析工作,作业过程灵活,对于自动化和重复性作业有明显优势,适合统计专业人员,更重要是非常契合现在越来流行的“可重复性研究”(Reproducible Research)的理念。 36 | 37 | SAS 软件作为老牌的统计软件,能够称霸统计界,且至今仍然独立运营,实属罕见。在大数据时代, SAS 软件也在与时俱进,开发了很多适应大数据处理的功能和产品,如SAS 网格计算(SAS® Grid Computing)、库内计算(SAS® In-Database)和内存计算(SAS®In-Memory Analytics),等等。虽然 SAS 的安装文件庞大,安装过程也较为费劲,但是这些一劳永逸的付出会让我们在后期觉得这是值得的,至于我们所担心的昂贵的费用问题,那就交给财大气粗的雇主吧。如果没有有钱的雇主,那就用大学版(SAS University Edition)吧,如果连大学版也懒得安装,还可以尝试免费的云端统计分析平台 SODA(SAS®OnDemand for Academics)。反正作为程序员和统计师,不必为软件费用埋单的问题担忧。此外,如果希望进入生物医药领域,特别是临床试验领域,那必需赶紧倒腾 SAS,越早越好。当然,如果你已经习惯了其他 CLI 的统计软件,笔者也不是非要苦口婆心的来劝你改用 SAS,这不是本书的目的。但是,若果你要用或者正在用 SAS,那本书所讨论的一些内容,可能正是你不愿错过的。 38 | 39 | ## 1.3 软件架构 40 | 通常,大众口中所言的 SAS 软件其实是指 SAS Foundation+Windows 的视窗管理系统(Display Management System, DMS)。 SAS Foundation 包括 Base SAS、数据管理和访问、数据分析、报告和图表、可视化和发现、商业解决方案、用户界面、应用扩展以及 Web应用等组成部分,其中 Base SAS 是核心。 SAS 软件的设计思路是在 Base SAS 的基础上,再配合特定的模块完成特定的任务需求。例如,要做统计分析,那就配合使用 SAS/STAT模块;要绘制统计图形,那就配合使用 SAS/GRAPH 模块;要导入各种外部数据,那就配合使用 SAS/ACCESS 模块;要做时间序列分析,那就配合使用 SAS/ETS 模块;要做基因分析;那就搬出 SAS/GENETIC 模块。最基础的 SAS 软件,只需要 Base SAS、 SAS/STAT、 SAS/GRPAH 模块。随着数据分析需求的增加, SAS 公司不断推出各种新的模块、新的过程以及新的选项,据不完全统计,目前 9.4 版本的 SAS 功能模块总计已达 75 个。常见的 SAS 模块及其功能见表 1-1。 41 | 42 | 表 1-1 常见 SAS 模块及其功能简介 43 | |模块名称 |功能简介|主要过程| 44 | | ------------- |:-------------:| -----:| 45 | | Base SAS | SAS 系统的核心模块,是运行 SAS 必须的模块。由 DATA 步、 PROC 步、 MACRO、ODS 以及 SAS 的窗口环境组成 | 基 础 的 统 计 过 程 FREQ、 MEAN、 CORR及 UNIVARIATE; ODS 绘图过程 SGPLOT、SGPANEL、 SGRENDER | 46 | | SAS/ACCESS | 与第三方数据源(各种关系型数据库)进行交互的模块;不同的数据源,需要单独的软件使用许可,如与 Excel 数据交互需要 ACCESS TO PC FILES 组件的许可 | 导 入 导 出 过 程 IMPORT、 EXPORT 以 及ACCESS 和 DBLODAD 过 程, LIBNAME语句 | 47 | | SAS/STAT | 统计分析模块,总计已有 75 个统计分析过程,还在不断更新补充中。 SAS 的“权威”性就在于这些统计分析过程 | 经 典 统 计 分 析 过 程 t 检 验(TTEST)、方 差 分 析(ANOVA)、 回 归(REG)、一般线性回归(GLM)、广义线性回归(GENMOD)、混合效应模型(MIXED)、聚 类(CLUSTER、 VARCLUSTER、FASTCLUS)、判别(DISCRIM)、因子(FACTOR)、 主 成 分(PRINCOMP)、Logistic 回 归(LOGISTIC)、 生 存 分 析(LIFETEST、 LIFEREG、 PHREG) | 48 | | SAS/GRAPH | 绘图模块,绘制常见统计图形 | GCHART、 GPLOT、 GBARLINE、 G3D 等 | 49 | | SAS/ETS | 时间序列分析模块 | ARIMA、 AUTOREG、 COUNTREG、QLIM、 ESM、 UCM、 MODEL 等 | 50 | | SAS/IML | 矩阵语言模块 | IML | 51 | | SAS/GENETIC | 基因分析模块 | ALLELE、 CASECONTROL、 FAMILY、PSMOOTH 等 | 52 | | SAS/OR | 运筹与优化模块 | LP 、 NLP 、 OPTLP 、 OPTMILP 、OPTNET、 OPTMODEL 等 | 53 | 54 | 在大数据时代的浪潮下, SAS 不断扩展其功能组件和产品, SAS® Visual Analytics, SAS® Visual Statistics, SAS® Cloud Analytics,SAS® Viya 等一大批迎接大数据时代数据分析处理需求的产品也逐渐进入程序员、数据挖掘人员、统计分析师以及数据科学家的视野。不过,对于常规的程序员和统计分析师而言, Base SAS +SAS/ACCESS + SAS/STAT+SAS/GRAPH 已基本能满足需求。 55 | 56 | ## 1.4 安装与许可 57 | SAS 可以安装在本地,也可以部署在服务器上,无论哪种方式,要想使用 SAS 软件及其模块,需要:①安装模块:比如想要导入 Excel 文件,就必须安装 SAS/ACCESS to PC files 模块;②获得许可:光是安装了模块还不行,还必须有使用此模块的权限。关于模块的使用权限,可以通过 SAS 安装数据文件(SAS Installation Data, SID)获得, SID 就相当于其他软件的注册码。 58 | 59 | SAS 公司在销售策略上是,将基础的功能模块打包(SAS/BASE、 SAS/ACCESS、SAS/STAT、 SAS/GRAPH),并提供相应模块的 SAS 安装数据文件,用户以租赁的形式获得软件安装包和许可文件,这就是坊间流传的“SAS 只租不卖”。如果需要更多的功能模块,则需要在订单中增加相应的模块才可以获得其安装介质和 SID。 60 | 61 | SAS 公司给安装介质时,一般会邮寄安装光盘。光盘的数量可能会与订购的模块多少有一定关系,一般在 6 ~ 7 张,每张约 4GB 大小。安装时,建议先用 UltralISO 软件把光盘里的内容直接抓取出来打包成 ISO 文件(见图 1-4):一则可防止光盘损坏;二则也方便安装,因为用虚拟光驱安装,免去了在光驱里来回更换安装光盘的麻烦。 62 | 63 | 用虚拟光驱软件 Daemon Tools Lite 打开第一张光盘,在 SAS 安装文件夹 install_doc文件夹下,可以看到一个以数字和字母组合而成的 6 位字符命名的文件夹,此即订单号文件夹,其中的 SOI.HTML 文件即 SAS 订单信息(SAS Order Information)文件,里面包含了购买的 SAS 产品和模块的摘要信息,通过此文件可以了解到用户购买了哪些模块,也即安装文件包含了哪些模块。例如,某研究中心的 SOI 信息如图 1-5 所示,通过此文件可以看到该中心不仅订购了可以导入 Excel 文件的 SAS/ACCESS Interface to PC Files,还购买了 DB2、 Hadoop、 MySQL 等其他众多数据库接口,在统计分析模块里,除了常规的统 64 | 计分析模块 SAS/STAT、还购买了矩阵运算模块 SAS/IML、时间序列模块 SAS/ETS 以及运筹模块 SAS/OR 等。 65 | 66 | 如果 SAS 软件已安装完成,则可以通过编程的方法(PRODUC_STATUS 和 SETINIT过程,详见程序 1-1)查看安装了哪些模块(见图 1-6),获得了哪些模块的许可(见图 1-7)。如果希望查看更完整的安装报告,可以在网上搜宏程序 %sasinstallreporter,运行此程序,即可在 Log 文件中看到 SAS 已经安装的模块、许可的模块及其有效日期(如果现在还对运行代码感到陌生,可以等读完 1.6 后回过头再来测试)。 67 | 68 | 程序1-1 查看SAS安装、许可的模块 69 | ```SAS 70 | *===带*号的行是注释行===; 71 | *===查看SAS已安装的模块; 72 | proc product_status; 73 | run; 74 | *===查看SAS已许可的模块; 75 | proc setinit; 76 | run; 77 | *===查看完整安装报告; 78 | *===SAS程序文件地址依据存储位置自行修改; 79 | %include " D:\03 Writting\01 SAS编程演义\03 Code\fusion_20390_1_ 80 | sasinstallreporter4u.sas"; 81 | %sasinstallreporter; 82 | ``` 83 | 84 | ## 1.5 运行模式 85 | SAS 有多种运行模式——窗口环境模式、非交互式模式、批处理模式及交互式行模式,各模式简要介绍如下: 86 | * 窗口环境模式:是在SAS的视窗管理系统(Display Management System, DMS)下,用户编写SAS程序、提交运行SAS程序、查看日志及结果的模式,这是Windows平台优势模式,也是广大用户最为常用和熟知的模式。 87 | * 非交互式模式:主要用于在不启动DMS的情境下,直接运行保存在SAS软件外部文件中的SAS程序,并将结果和日志保存在指定的位置。 88 | * 批处理模式:可以对SAS 作业进行预定执行,如定期自动运行某程序,在商业智能解决方案中这种模式较为常用。 89 | * 交互式行模式:是UNIX操作系统使用的一种顺序地输入程序语句的运行模式,是一种使用较少的模式。 90 | 91 | ## 1.6 编程界面 92 | ]本书将以最为常用的、 SAS 自带的窗口环境模式为例进行展示。在窗口环境模式下,编程环境和界面其实也有多种选择:①视窗管理系统(Display Management System,DMS);② SAS 企业版(Enterprise Guide, EG);③ SAS 工作室(SAS Studio)。如果安装完全,在 Windows「开始」菜单下,可以看到三种界面的启动链接,如图 1-9 所示。 93 | 94 | ### 1.6.1 DMS界面 95 | DMS(Display Management System, 视窗管理系统)是 SAS 初学者最为常见的编程界面,如图 1-10 所示。 DMS 的五大部分为 Eidtor(编辑器窗格)、 Log(日志窗格)、Ouput(输出窗格)、 Results(结果窗格)以及 Explorer(资源管理器),可以通过底部的选项卡切换。 96 | 97 | ### 1.6.2 EG界面 98 | SAS EG (Enterprise Guide)是基于客户端 / 服务器(Client/Server)架构的客户端,可从从项目工程角度管理相关资源,其界面主要由项目树、工作区、资源窗格组成,如图 1-11 所示。与 DMS 中类似的 Code(程序)、 Log (日志)、 Results(结果)均在工作区内。 99 | 100 | ### 1.6.3 SAS Studio 界面 101 | SAS Studio 基于浏览器 / 服务器(Browser/Server, B/S)架构的浏览器来实现与SAS 本机服务器的交互。 SAS Studio 由左侧的导航面板和右侧的工作区组成,如图 1-12所示。工作区同 SAS EG 类似,有与 DMS 中类似的 Code(程序)、 Log (日志)、Results(结果)。 102 | 103 | SAS DMS、 SAS EG 和 SAS Studio 三种编程环境,究竟有何区别?如何选择呢?其实三者在编程语言上并没有什么区别,不过后两者在编程界面、功能上有很多的改进。三者间更多的区别,可见表 1-2 的总结。 104 | 105 | 表 1-2 SAS 三大编程环境简要比较 106 | | 维 度 | SAS DMS | SAS EG |SAS Studio | 107 | | --- |--- |--- | --- | 108 | | 界面组成 | 五大窗格:编辑器、日志、输出、结果、资源管理器 | 三大窗格:工作区、项目树以及资源窗格 | 两大区域:工作区和导航面板 | 109 | | 软件架构 | 图形用户界面(GUI),集数据存取、代码执行和结果交付为一体的编程环境客户端 | 客户端 / 服务器(C/S)架构,需要 SAS 服务器(可以在本地)存取数据、执行代码 | 浏览器 / 服务器(B/S)架构 | 110 | | 平台支持 | Windows、 UNIX、 Linux、 Z/OS | Windows | Windows、 UNIX、 Linux、MacOS | 111 | | 语法提示 | 除了语法着色,作为一款编程编辑器,在很多方面确实差强人意 | 语法着色、自动补全、语法提示、格式化代码、警告及错误定位 | 语法着色、自动补全、语法提示、警告及错误定位 | 112 | | 优势 | 支 持 % WINDOW、 DDE、 X 命令以及 DM 命令等;反应速度比C/S、 B/S 架构快;自定义宏键盘 |存储过程、工作流、代码生成器;图形化的菜单操作比DMS 更丰富,也更方便 | 工作区与 EG 类似;导航面板功能丰富;任务模板,代码片段比较有特色 | 113 | | 不足 | 语法编辑器功能太单一,基本只有语法着色一项功能 | 反应速度慢,部分 DMS 支持的功能如 % WINDOW、DDE、 X 命令、 DM 命令等它不支持 | 反应速度较慢,部分 DMS 支持 的 功 能 如 % WINDOW、DDE、 X 命 令、 DM 命 令 等它不支持 | 114 | 115 | 总体而言, SAS DMS最为传统,速度最快。SAS EG和 SAS Studio具有良好的语法提示、自动补全等功能,可以在学习 SAS 代码,提升编程效率方面给初学者更多帮助。如果是初学者,建议不妨多在 SAS Studio 里尝试编程,如果追求测试效率,建议在 DMS 里开发,当然,至于最终的选择,可以依据个人喜好和具体业务而定. 116 | 117 | ## 1.7 版本 118 | SAS 在启动时会在日志窗格中显示软件版本号以及相应模块的版本号。在启动后,我们也可以通过宏变量 &SYSVER 或者 &SYSVLONG 获得其版本号,如图 1-13 所示。 119 | 120 | 程序1-2 获取SAS版本号 121 | 122 | ``` 123 | %put SAS 版本号: &SYSVER; 124 | %put SAS 版本号(长): &SYSVLONG; 125 | ``` 126 | 一直以来, SAS 的版本更新比较谨慎,甚至可以说是缓慢。胡江堂和 Rick Wicklin 曾 17经在博客上统计过 SAS 8.0 到 SAS 9.4m4 的发布日期,并制成了图片,如图 1-14 所示,近年来 SAS 虽然没有大版本的更新,但是小版本更新的速度却在不断加快。 127 | 128 | ### 1.7.1 购买版与大学版 129 | 除了上面介绍的版本区别, SAS 还有购买版与大学版的区别(不知道官方具体的称谓,姑且这样描述),以及启动时加载各种语言配置版本的区别。 130 | 131 | SAS 购买版按模块收取年费,而 SAS 的大学版(SAS University Edition)是免费供大家下载使用的。 SAS 大学版本质上是用虚拟机打包的 Redhat 系统里的 SAS,采用 B/S 架构的 SAS Studio 连接,包含了 BASE、 STAT、 ACCESS、 IML 以及 HPS 模块,但是遗憾的是,没有 GRAPH 模块,不过如果熟悉 ODS GRAPH 的话,基本可以不用 GRAPH 模块画图 ,具体可参见本书第 8 章的介绍。 132 | 133 | ### 1.7.2 免费云端版 134 | 如果既没有购买 SAS,也不愿意下载大学版,甚至连安装都嫌麻烦,我们还有什么办法可以用 SAS 吗?确实有,那就是 SODA——一个免费云端版的 SAS,只要有网络,我们就可以随时随地用 SAS 写代码。 135 | 136 | SODA(SAS® OnDemand for Academics)是 SAS 为学术界人士免费提供的在线的、基于 SAS 私有云上的应用服务环境。利用 SODA,我们可以随时随地地在 SAS Studio中编写运行 SAS 代码,而且所有数据和代码都可以存储在云端,所有结果均可以下载保存,每个账号用户有 5120MB 的存储空间。 SODA 可以说是懒人学习 SAS 最方便、快捷的低成本途径了。如果你手头还没有 SAS,而有了 SODA,照样可以一起愉快地学习 SAS。 137 | 138 | 要 使 用 SODA, 首 先 需 要 到(https://odamid.oda.sas.com) 进 行 注 册( 如 果 以 前有 SAS 社区的账号则可直接登录)。注册流程非常简洁,只需姓、名、邮箱、国籍几项信息即可。注册成功后,稍等片刻会收到一封名为 You are ready to start using SASOnDemand for Academics 的邮件,里面有登录 SODA 的用户名(通常是邮箱的前缀)。登录后,单击 SAS Stduio 应用(见图 1-15),即可进入 SAS Studio 编程环境,开启免费云端之旅。 139 | 140 | 此 SAS Studio 的界面(见图 1-16)同本机 SAS Studio 的界面结构(上端的菜单栏、左侧的导航面板、右侧的工作区)几乎一致。不过,其内核可能是不同的,单击右上角问号,查看关于 SAS Studio 的信息,可见此 SAS Studio 的后台是 Linux 系统下的 SAS 9.04.01M3版本,这已经是目前最新版的 SAS 了。我们在程序标签页下运行「Proc setinit; run;」,看看许可了哪些模块。测试结果发现除了常规的 BASE SAS、 SAS/STAT、 SAS/GRAPH、 SAS Enterprise Guide 外, SAS/IML、SAS/ETS、 SAS/OR、 SAS/QC、 SAS/CONNECT 等模块也都赫然在列,甚至连数据挖掘和文本挖掘的产品 SAS Enterprise Miner、 SAS Text Miner 以及可视化分析产品 SAS VisualAnalytics Hub、 Visual Analytics Explorer、 SAS Visual Analytics Services 都囊括其中,不得不说, SAS 公司此举诚意满满。 141 | 142 | 如图 1-17 所示,通过右击左侧的「文件(主目录)」,我们还可以上传自己的数据文件到云端,然后在右侧工作区写代码、运行代码、获取分析结果。数据和代码可以保存在云端,下次登录后仍可利用,而分析结果和中间数据则可以下载到本地,具体可参考SAS 公司大数据与可视化分析产品线负责人巫银良先生的文章:《从程序员到数据科学家:SAS 编程基础 (04)》,本节不再赘述。 143 | 144 | ### 1.7.3 各操作系统平台版 145 | SAS 目前支持的操作系统平台包括 z/OS、 UNIX、 Linux 以及 Windows,各操作系统版本与其兼容的 SAS 版本具体可在 SAS 官网(http://support.sas.com/supportos/list)页面System Requirements 下的 Supported Operating Systems 里查阅。苹果电脑 MacOS 系统目前没有相应的 SAS 版本,如果想在苹果系统中使用 SAS,有三种策略可供参考:①虚拟机软件 +Windows+SAS;②虚拟机软件 +SAS University Edition;③免费在线云端版本 SODA。或者干脆选用 SAS 兄弟产品 JMP 软件。 146 | 147 | ### 1.7.4 各语言版 148 | 如果在安装过程中,选择了中文语言包,配置了 Unicode Support 的话,在开始菜单里我们就可以有多种语言版本的 SAS 可供选择:①英文版;②中文版;③带 DBCS 的英文版;④ Unicode Support 版,如图 1-18 所示。需要留意的是,如果希望我们的 SAS 支持中文字符的话,那么就选择后面三个吧;如果我们希望既能支持中文字符,又想在英文环 21境下使用 SAS,那就选择带 DBCS 的英文版,带 DBCS 的英文版的优势是可以获得英文版的提示信息,方便后续在 SAS Help和搜索工具里检索相关信息,因此,笔者个人推荐此版本。 149 | 150 | ## 1.8 本章小结 151 | 本章从闲聊 SAS 的八卦和历史开始,谈及选择 SAS 的理由,并着重对 SAS 软件进行了一个概要式的说明。通过本章的介绍,希望我们能够从感性上对 SAS 软件的架构、安装技巧、许可文件、运行模式、编程界面以及版本有一个初步的了解。 152 | -------------------------------------------------------------------------------- /01-Code/Macro/ggBaseline/ggBaseline2.sas: -------------------------------------------------------------------------------- 1 | *=================================================== 2 | %ggBaseline2 : for two or more group baseline infor 3 | Author:Hongqiu Gu 4 | Contact: guhongqiu@yeah.net 5 | Date: V20161220 6 | 7 | For details, please see and ref Ann Transl Med, 2018, 6(16): 326. 8 | 9 | Copyright: CC BY-NC-SA 4.0 10 | In short: you are free to share and make derivative 11 | works of the work under the conditions that you appropriately 12 | attribute it, you use the material only for non-commercial 13 | purposes, and that you distribute it only under a license 14 | compatible with this one. 15 | -------------------------------------------------- 16 | 17 | ======================================================= 18 | %ggBaseline2( 19 | data=, 20 | var=var1|test|label1\ 21 | var2|test|label2, 22 | grp=grpvar, 23 | grplabel=grplabel1|grplabel2, 24 | stdiff=N, 25 | totcol=N, 26 | pctype=COL|ROW, 27 | exmisspct=Y|N, 28 | showP=Y|N, 29 | filetype=RTF|PDF, 30 | file=&ProjPath\05-Out\01-Table\, 31 | title=, 32 | footnote=, 33 | fnspace=20, 34 | page=PORTRAIT|LANDSCAPE 35 | deids=Y|N 36 | ) 37 | 38 | for categorical variables, 39 | test should be one of the follow: 40 | CHISQ|CMH1|CMH2|TREND|FISHER| 41 | 42 | for continuous variables, 43 | test should be one of the follow: 44 | TTEST|ANOVA|WILCX|KRSWLS 45 | 46 | 47 | Log: 48 | 10.20190320 Change param name exmissing to exmisspct 49 | 9.20190320 Universe encoding for en dash and plus or minus 50 | 8.20181019 Fixed nmiss(%) 51 | 7.20181010 Add P value display control 52 | 6.20180929 Fixed exmisspct in chisq test 53 | 5.20180919 Fixed the HL calculation 54 | 4.20180806 Change NNmiss to nmiss(%) 55 | 3.20180725 Exclude missing value from statistical percent 56 | 2.20161220 Finish the development of the macro 57 | 1.20161130 Get the idea 58 | ====================================================; 59 | 60 | %macro ggBaseline2( 61 | data=, 62 | var=, 63 | grp=, 64 | grplabel=, 65 | stdiff=N, 66 | totcol=N, 67 | pctype=col, 68 | exmisspct=Y, 69 | showP=Y, 70 | filetype=rtf, 71 | file=, 72 | title=, 73 | footnote=, 74 | fnspace=, 75 | page=portrait, 76 | deids=Y 77 | ); 78 | 79 | options minoperator nofmterr; 80 | dm odsresults 'clear' continue; 81 | 82 | /*===Delete last time report data===*/ 83 | proc datasets lib=work memtype=data; 84 | delete Base2 grplevel desc: eq: tt: hov: anova: dslabel: pvalue: merge: ; 85 | quit; 86 | 87 | 88 | 89 | /*===Get Libname and memname===*/ 90 | %let ndsn=%sysfunc(countw(&data,.)); 91 | 92 | %if &ndsn eq 1 %then %do; 93 | %let libname=WORK; 94 | %let memname=%scan(&data,1, "("); 95 | %end; 96 | 97 | %else %do; 98 | %let libname=%scan(&data,1,.); 99 | %let memname=%scan(%scan(&data,2,.),1, "("); 100 | %end; 101 | 102 | 103 | /*===Check the existance of dataset and grpvar==*/ 104 | %local dsid check rc; 105 | %let dsid = %sysfunc(open(&libname..&memname)); 106 | %if &dsid=0 %then %do; 107 | %put ERROR: Dataset &libname..&memname is not exist!; 108 | %abort; 109 | %end; 110 | %else %do; 111 | %let checkvar = %sysfunc(varnum(&dsid.,&grp)); 112 | %let rc = %sysfunc(close(&dsid.)); 113 | %if &checkvar. = 0 %then %do; 114 | %put ERROR: Variable &grp does not exists!; 115 | %abort; 116 | %end; 117 | %end; 118 | 119 | 120 | /*===Set each grp label===*/ 121 | %let ngrplabel=%sysfunc(countw(&grplabel,%str(|))); 122 | 123 | %do i=1 %to &ngrplabel; 124 | %let grplabel&i=%scan(&grplabel,&i,|); 125 | %end; 126 | 127 | %let grplabel0=Total; 128 | 129 | 130 | 131 | /*===Set a new group var===*/ 132 | proc sql; 133 | create table grplevel as 134 | select distinct &grp as &grp 135 | from &data; 136 | quit; 137 | 138 | data grplevel; 139 | set grplevel; 140 | ngrp+1; 141 | run; 142 | 143 | proc print data=grplevel; 144 | run; 145 | 146 | 147 | proc sql; 148 | create table ads as 149 | select a.*, b.ngrp 150 | from &data as a left join grplevel as b 151 | on a.&grp= b.&grp; 152 | quit; 153 | 154 | 155 | /*===Set total group===*/ 156 | %if %upcase(&totcol) EQ Y %then %do; 157 | data ads; 158 | set ads; 159 | output; 160 | ngrp=0; 161 | output; 162 | run; 163 | %end; 164 | 165 | %let data=ads; 166 | %let grp=ngrp; 167 | 168 | 169 | /*===Get freqs for each grp===*/ 170 | proc sql noprint; 171 | select n( distinct &grp) into: ngrp 172 | from &data; 173 | select count(*) into: nbygrp separated by " " 174 | from &data 175 | group by &grp; 176 | quit; 177 | 178 | 179 | %if %upcase(&totcol) EQ Y %then %do; 180 | %do g=0 %to %sysevalf(&ngrp-1); 181 | %let n&g=%scan(&nbygrp,%sysevalf(&g+1)); 182 | %end; 183 | %end; 184 | 185 | %if %upcase(&totcol) EQ N %then %do; 186 | %do g=1 %to &ngrp; 187 | %let n&g=%scan(&nbygrp,&g); 188 | %end; 189 | %end; 190 | 191 | 192 | 193 | /*===Loop each var===*/ 194 | %let nvar=%sysfunc(countw(&var,%str(\))); 195 | %do n=1 %to &nvar; 196 | %let var&n=%scan(%qscan(&var,&n,%str(\)),1,|); 197 | %let test&n=%scan(%qscan(&var,&n,%str(\)),2,|); 198 | %let varlabel&n=%scan(%qscan(&var,&n,%str(\)),3,|); 199 | 200 | 201 | /*===Check the existance of dataset and var==*/ 202 | %local dsid check rc; 203 | %let dsid = %sysfunc(open(&libname..&memname)); 204 | %if &dsid=0 %then %do; 205 | %put ERROR: Dataset &libname..&memname is not exist!; 206 | %abort; 207 | %end; 208 | %else %do; 209 | %let checkvar = %sysfunc(varnum(&dsid.,&&var&n)); 210 | %let rc = %sysfunc(close(&dsid.)); 211 | %if &checkvar. = 0 %then %do; 212 | %put ERROR: Variable &&var&n is not exists!; 213 | %abort; 214 | %end; 215 | %end; 216 | 217 | 218 | /* Get vtype for format */ 219 | data _null_; 220 | set sashelp.vcolumn; 221 | where upcase(libname)=upcase("&libname") and upcase(memname)=upcase("&memname") and upcase(name)=upcase("&&var&n"); 222 | call symputx(cats("vtype",&n),type); 223 | run; 224 | 225 | %descB2(data=&data, var=&&var&n, vtype=&&vtype&n, grp=&grp, test=&&test&n, n=&n) 226 | %testB2(data=&data, var=&&var&n, vtype=&&vtype&n, grp=&grp, test=&&test&n, n=&n) 227 | %mergeB2(var=&&var&n, test=&&test&n, varlabel=%quote(&&varlabel&n), n=&n) 228 | 229 | %end; 230 | 231 | 232 | 233 | /*===Set dspreort dataset===*/ 234 | data Base2; 235 | %if %upcase(&totcol) eq Y %then %do; 236 | format order label %do j=0 %to %sysevalf(&ngrp-1); col&j %end; pvalue; 237 | %end; 238 | 239 | %if %upcase(&totcol) eq N %then %do; 240 | format order label %do j=1 %to &ngrp; col&j %end; pvalue; 241 | %end; 242 | 243 | set merge:; 244 | if strip(label)="^{nbspace 6}" then label="^{nbspace 6}Missing"; 245 | run; 246 | 247 | proc sort data=Base2; 248 | by order; 249 | run; 250 | 251 | 252 | %ggReportB2(data=Base2,filetype=&filetype, file=&file, title=&title, footnote=&footnote, fnspace=&fnspace, page=&page) 253 | 254 | %if %upcase(&deids) EQ Y %then %do; 255 | proc datasets lib=work memtype=data; 256 | save ads grplevel Base2; 257 | quit; 258 | %end; 259 | 260 | %mend ggBaseline2; 261 | 262 | 263 | 264 | %macro descB2(data=, var=,vtype=, grp=, test=,n=); 265 | %if %upcase(&test) in CHISQ CMH TREND FISHER %then %do; 266 | %desc_ctg_b2(data=&data, var=&var,vtype=&vtype, grp=&grp,n=&n, exmisspct=&exmisspct); 267 | %end; 268 | 269 | %else %if %upcase(&test) in TTEST ANOVA WILCX KRSWLS %then %do; 270 | %desc_ctn_b2(data=&data, var=&var, grp=&grp,n=&n); 271 | %end; 272 | %mend descB2; 273 | 274 | 275 | 276 | %macro desc_ctg_b2(data=, var=, vtype=, grp=,n=, exmisspct=); 277 | proc freq data=&data noprint; 278 | table &var*&grp/missing nopercent outpct out=desc&n._n; 279 | 280 | %if %upcase(&exmisspct) eq Y %then %do; 281 | table &var*&grp/nopercent outpct out=desc&n._Y; 282 | %end; 283 | 284 | 285 | %if &vtype eq char %then %do; 286 | format &var $&var.fmt.; 287 | %end; 288 | %else %do; 289 | format &var &var.fmt.; 290 | %end; 291 | run; 292 | 293 | %if %upcase(&exmisspct) eq Y %then %do; 294 | data desc&n; 295 | update desc&n._n desc&n._Y; 296 | by &var &grp; 297 | run; 298 | %end; 299 | 300 | %else %do; 301 | data desc&n; 302 | set desc&n._n; 303 | run; 304 | %end; 305 | 306 | data desc&n; 307 | set desc&n; 308 | length value $25; 309 | %if %upcase(&pctype) EQ COL %then %do; 310 | value = compress(put(count,6.)) || ' (' ||compress( put(pct_col,4.1))||')'; 311 | %end; 312 | 313 | %if %upcase(&pctype) EQ ROW %then %do; 314 | value = compress(put(count,6.)) || ' (' || compress(put(pct_row,4.1))||')'; 315 | %end; 316 | 317 | run; 318 | 319 | proc sort data=desc&n; 320 | by &var; 321 | run; 322 | 323 | proc transpose data=desc&n out=desc&n (drop=_name_) prefix=col; 324 | by &var; 325 | var value; 326 | id &grp; 327 | run; 328 | 329 | %if &ngrplabel EQ 2 and &stdiff EQ Y %then %do; 330 | data desc&n(drop=Psc Pnsc); 331 | set desc&n; 332 | if not missing(col1) and not missing(col2) then do; 333 | Psc=input(scan(scan(col1,2,"("),1,")"),4.1)/100; 334 | Pnsc=input(scan(scan(col2,2,"("),1,")"),4.1)/100; 335 | stdiff=abs(100*(Psc-Pnsc)/sqrt((Psc*(1-Psc)+ Pnsc*(1-Pnsc))/2)); 336 | end; 337 | run; 338 | %end; 339 | 340 | %mend desc_ctg_b2; 341 | 342 | 343 | %macro desc_ctn_b2(data=, var=, grp=, n=); 344 | ods output summary=desc&n; 345 | proc means data=&data n nmiss mean std min max median q1 q3 maxdec=1; 346 | var &var; 347 | class &grp; 348 | run; 349 | 350 | proc npar1way data=&data(where=(&grp in (1,2))) hl noprint; 351 | class &grp; 352 | var &var; 353 | output out=hl&n hl; 354 | run; 355 | 356 | data _null_; 357 | set hl&n; 358 | if not missing(_HL_) then call symputx("hl&n", _HL_); 359 | else call symputx("hl&n", "NA"); 360 | 361 | run; 362 | 363 | 364 | data desc&n(keep= &grp nnmiss meanstd minmax medianIQR); 365 | set desc&n; 366 | format nnmiss meanstd minmax medianIQR; 367 | if &var._nmiss ^=0 then do; 368 | nnmiss = compress(put(&var._nmiss,6.))||" ("||compress(put(&var._nmiss/nobs*100,4.1))||")"; 369 | end; 370 | minmax = cats(put(&var._min,12.1),unicode("–","ncr"), put(&var._max,12.1)); 371 | meanstd = cats(put(&var._mean,12.1), unicode("±","ncr"), put(&var._stddev,12.1)); 372 | medianIQR = compress(put(&var._median,6.1))|| " ("||compress(put(&var._q1,6.1))||unicode("–","ncr")||compress(put(&var._q3,6.1))||")"; 373 | 374 | run; 375 | 376 | proc transpose data=desc&n out=desc&n prefix=col; 377 | id &grp; 378 | var nnmiss meanstd minmax medianIQR; 379 | run; 380 | 381 | %if &ngrplabel EQ 2 and &stdiff EQ Y %then %do; 382 | data desc&n; 383 | set desc&n; 384 | if _name_="meanstd" then stdiff= 100* ((input(scan(col1,1, unicode("±","ncr")),12.1)-input(scan(col2,1, byte(177)),12.1))/sqrt(( input(scan(col1,2, unicode("±","ncr")),12.1)**2 + input(scan(col2,2,unicode("±","ncr")),12.1)**2 )/2)); 385 | if _name_="medianIQR" then stdiff="&&hl&n"; 386 | run; 387 | %end; 388 | 389 | 390 | %mend desc_ctn_b2; 391 | 392 | 393 | 394 | 395 | %macro testB2(data=, var=, vtype=, grp=, test=, n=); 396 | %if %upcase(&test) in CHISQ CMH TREND FISHER %then %do; 397 | %freq(data=&data, var=&var, vtype=&vtype, grp=&grp, test=&test, n=&n) 398 | %end; 399 | 400 | %else %if %upcase(&test) EQ TTEST %then %do; 401 | %ttest(data=&data, var=&var, grp=&grp,n=&n) 402 | %end; 403 | 404 | %else %if %upcase(&test) EQ ANOVA %then %do; 405 | %anova(data=&data, var=&var, grp=&grp,n=&n) 406 | %end; 407 | 408 | %else %if %upcase(&test) in WILCX KRSWLS %then %do; 409 | %npar1way(data=&data, var=&var, grp=&grp, test=&test,n=&n) 410 | %end; 411 | %mend testB2; 412 | 413 | 414 | 415 | %macro freq(data=, var=, vtype=, grp=, test=, n=); 416 | proc freq data=&data noprint; 417 | %if &totcol eq Y %then %do; 418 | where not missing(&grp) and &grp NE 0; 419 | %end; 420 | 421 | table &grp*&var/ %if %upcase(&exmisspct) EQ N %then %str(missing); noprint &test; 422 | 423 | format &var %if &vtype EQ char %then %str($&var.fmt.);%else %str(&var.fmt.); ; 424 | 425 | %if %upcase(&test) EQ CHISQ %then %do; 426 | output out = pvalue&n(keep=p_pchi rename=(p_pchi=pvalue)) pchi; 427 | %end; 428 | %if %upcase(&test) in CMH1 %then %do; 429 | output out = pvalue&n(keep=P_CMHCOR rename=(P_CMHCOR=pvalue)) cmh1; 430 | %end; 431 | %if %upcase(&test) in CMH2 %then %do; 432 | output out = pvalue&n(keep=P_CMHRMS rename=(P_CMHRMS=pvalue)) cmh2; 433 | %end; 434 | %if %upcase(&test) EQ TREND %then %do; 435 | output out = pvalue&n(keep=P2_TREND rename=(P2_TREND=pvalue)) trend; 436 | %end; 437 | %if %upcase(&test) EQ FISHER %then %do; 438 | output out = pvalue&n(keep=XP2_FISH rename=(XP2_FISH=pvalue)) fisher; 439 | %end; 440 | 441 | run; 442 | 443 | %mend freq; 444 | 445 | 446 | 447 | 448 | %macro ttest(data=, var=, grp=, n=); 449 | ods output equality=eq&n ttests=tt&n; 450 | proc ttest data=&data plots=none; 451 | class &grp; 452 | var &var; 453 | %if &totcol eq Y %then %do; 454 | where not missing(&grp) and &grp NE 0; 455 | %end; 456 | run; 457 | 458 | data _null_; 459 | set eq&n; 460 | call symputx('testeq',ProbF-0.05); 461 | run; 462 | 463 | data pvalue&n(keep=probt rename=(probt=pvalue)); 464 | set tt&n; 465 | %if %quote(&testeq) GE 0 %then %do; 466 | if _n_=1; 467 | %end; 468 | 469 | %if %quote(&testeq) LT 0 %then %do; 470 | if _n_=2; 471 | %end; 472 | run; 473 | %mend ttest; 474 | 475 | 476 | 477 | %macro anova(data=, var=, grp=, n=); 478 | ods output HOVFTest=hov&n ModelANOVA=anova&n; 479 | proc anova data=&data plots=none; 480 | class &grp; 481 | model &var=&grp; 482 | means &grp/hovtest; 483 | %if &totcol eq Y %then %do; 484 | where not missing(&grp) and &grp NE 0; 485 | %end; 486 | run; 487 | 488 | data _null_; 489 | set hov&n; 490 | if _n_=1 then call symputx('hovtest', ProbF-0.05); 491 | run; 492 | 493 | %if %quote(&hovtest) GE 0 %then %do; 494 | data pvalue&n(keep=ProbF rename=(ProbF=Pvalue)); 495 | set anova&n; 496 | run; 497 | %end; 498 | 499 | %if %quote(&hovtest) LT 0 %then %do; 500 | %npar1way(data=&data, var=&var, grp=&grp, test=KRSWLS,n=&n) 501 | %end; 502 | 503 | %mend anova; 504 | 505 | 506 | %macro npar1way(data=, var=, grp=, test=, n=); 507 | proc npar1way data=&data wilcoxon noprint; 508 | class &grp; 509 | var &var; 510 | output out=pvalue&n wilcoxon; 511 | %if &totcol eq Y %then %do; 512 | where not missing(&grp) and &grp NE 0; 513 | %end; 514 | run; 515 | 516 | data pvalue&n; 517 | set pvalue&n; 518 | %if %upcase(&test) EQ WILCX %then %do; 519 | keep p2_wil; 520 | rename p2_wil=pvalue; 521 | %end; 522 | 523 | %if %upcase(&test) EQ KRSWLS %then %do; 524 | keep p_kw; 525 | rename p_kw=pvalue; 526 | %end; 527 | run; 528 | %mend; 529 | 530 | 531 | 532 | %macro mergeB2(var=, test=, varlabel=, n=); 533 | %if %upcase(&test) in CHISQ CMH TREND FISHER %then %do; 534 | %merge_cate_b2(var=&var, varlabel=&varlabel, n=&n) 535 | %end; 536 | 537 | %else %if %upcase(&test) in TTEST ANOVA WILCX KRSWLS %then %do; 538 | %merge_cont_b2(var=&var, varlabel=&varlabel,n=&n) 539 | %end; 540 | %mend mergeB2; 541 | 542 | 543 | 544 | 545 | %macro merge_cate_b2(var=, varlabel=, n=); 546 | data dslabel&n; 547 | set pvalue&n; 548 | length label $ 85; 549 | label = cats("^S={font_weight=bold}" , "&varlabel"); 550 | run; 551 | 552 | 553 | data merge&n; 554 | keep order label col: pvalue %if &ngrplabel EQ 2 and %upcase(&stdiff) EQ Y %then stdiff;; 555 | length label $ 85; 556 | set dslabel&n desc&n(where=(not missing(col1))); 557 | if _n_ > 1 then label= "^{nbspace 6}" ||put(&var,&var.fmt.); 558 | order=&n; 559 | run; 560 | %mend merge_cate_b2; 561 | 562 | 563 | 564 | %macro merge_cont_b2(var=, varlabel=,n=); 565 | data dslabel&n; 566 | set pvalue&n; 567 | length label $ 85; 568 | label = cats("^S={font_weight=bold}" , "&varlabel"); 569 | run; 570 | 571 | 572 | data merge&n; 573 | keep order label col: pvalue %if &ngrplabel EQ 2 and %upcase(&stdiff) EQ Y %then stdiff;; 574 | length label $ 85 ; 575 | set dslabel&n desc&n(where=(not missing(col1))); 576 | 577 | if _n_ > 1 then 578 | select; 579 | when(_NAME_ = 'nnmiss') label = "^{nbspace 6}Nmiss (%)"; 580 | when(_NAME_ = 'minmax') label = "^{nbspace 6}Min"||unicode("–","ncr")||"Max"; 581 | when(_NAME_ = 'meanstd') label = "^{nbspace 6}Mean%sysfunc(byte(177))SD"; 582 | when(_NAME_ = 'medianIQR') label = "^{nbspace 6}Median (IQR)"; 583 | otherwise; 584 | end; 585 | order=&n; 586 | run; 587 | 588 | %mend merge_cont_b2; 589 | 590 | 591 | %macro ggReportB2(data=, filetype=, file=, title=, footnote=, fnspace=, page=); 592 | 593 | options nodate nonumber orientation=&page missing = ''; 594 | ods escapechar='^'; 595 | 596 | %if %upcase(&filetype) EQ RTF %then %do; 597 | ods tagsets.rtf style=journal3a file="&file..&filetype"; 598 | %end; 599 | %if %upcase(&filetype) EQ PDF %then %do; 600 | ods pdf style=journal3a file="&file..&filetype"; 601 | %end; 602 | 603 | title j=center height=12pt "^{nbspace &fnspace}&title"; 604 | proc report data=&data nowindows spacing=1 headline headskip split = "|" ; 605 | 606 | %if %upcase(&totcol) eq Y %then %do; 607 | columns (order label %do j=0 %to %sysevalf(&ngrp-1); col&j %end; 608 | %if %upcase(&showP) EQ Y %then pvalue ; 609 | %if &ngrplabel EQ 2 and %upcase(&stdiff) EQ Y %then stdiff ; ); 610 | define order/ noprint; 611 | define label /display " Variables"; 612 | %do c=0 %to %sysevalf(&ngrp-1); 613 | define col&c /display right "&&grplabel&c.|(N=&&n&c)" ; 614 | %end; 615 | 616 | %if %upcase(&showP) EQ Y %then %do; 617 | define pvalue /display center "P Value" f = pvalue6.4; 618 | %end; 619 | 620 | %if &ngrplabel EQ 2 and %upcase(&stdiff) EQ Y %then %do; 621 | define stdiff /display right "ASD/HL estimator" f=6.1; 622 | %end; 623 | %end; 624 | 625 | %if %upcase(&totcol) eq N %then %do; 626 | columns (order label %do j=1 %to &ngrp; col&j %end; 627 | %if %upcase(&showP) EQ Y %then pvalue ; 628 | %if &ngrplabel EQ 2 and %upcase(&stdiff) EQ Y %then stdiff ; ); 629 | define order/ order noprint; 630 | define label /display " Variables"; 631 | %do c=1 %to &ngrp; 632 | define col&c /display right "&&grplabel&c.|(N=&&n&c)" ; 633 | %end; 634 | 635 | %if %upcase(&showP) EQ Y %then %do; 636 | define pvalue /display center "P Value" f = pvalue6.4; 637 | %end; 638 | 639 | %if &ngrplabel EQ 2 and %upcase(&stdiff) EQ Y %then %do; 640 | define stdiff /display right "ASD/HL estimator" f=6.1; 641 | %end; 642 | %end; 643 | 644 | %if &footnote NE %str() %then %do; 645 | footnote1 j=left height=10pt "^{nbspace &fnspace}Note: &footnote"; 646 | %end; 647 | run; 648 | 649 | %if %upcase(&filetype) EQ RTF %then %do; 650 | ods tagsets.&filetype close; 651 | %end; 652 | %if %upcase(&filetype) EQ PDF %then %do; 653 | ods pdf close; 654 | %end; 655 | 656 | title; 657 | footnote1; 658 | footnote2; 659 | 660 | %mend ggReportB2; 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | -------------------------------------------------------------------------------- /Chatper-2.md: -------------------------------------------------------------------------------- 1 | # C2-清歌苦调两不厌:夯实基础 2 | 3 | SAS 程序是一个庞杂的体系,无论是从原始数据的整理、统计方法的实现,还是统计结果的呈现来说,了解其背后术语和概念、理解其运行原理与机制,对阅读理解 SAS 程序,自己编写 SAS 程序,快速实现数据整理和统计分析需求都将大有裨益。因此,本章我们就来扒一扒那些重要的,尤其是被市面上的 SAS 书籍所忽略的基本概念和基础知识。 4 | 5 | ## 2.1 Foundation SAS 6 | 正如上一章所提及的,我们大部分人首先接触的、日常使用的都是 Windows 平台下的 SAS 视窗管理系统 DMS。我们通过 DMS 与 Foundation SAS 进行交互,从而完成我们的工作。 7 | 8 | ### 2.1.1 Foundation SAS的构成 9 | 在 Windows 下 可 以 看 到 SASHOME 安 装 目 录 下 有 SASFoundation 文 件 夹, 里 面包含了诸如 ACCESS、 BASE、 GRAPH 等诸多组件。正如第一章所提及的,其实整个Foundation 就是由 Base SAS、数据管理与访问、用户界面、报告与绘图、分析、可视化与发现、商业解决方案、应用开发以及网络应用等组件构成的(见图 2-1)。 10 | 11 | | | Foundation SAS | | 12 | | ----------- | -------------- | ------- | 13 | | 报告与绘图 | 数据访问与管理 | 用户界面 | 14 | | 分析 | Base SAS | 应用开发 | 15 | | 可视化与发现 | 商业解决方案 | 网络应用 | 16 | 17 | 概括而言, Foundation SAS 提供了以下功能: 18 | * 管理SAS任务的图形用户界面,如DMS、 EG、 SAS Studio等 19 | * 高度灵活、可扩展的编程语言,即SAS语言 20 | * 丰富的内置SAS过程 21 | * Windows、 UNIX以及z/OS(OS/390)的多平台运行 22 | * 几乎任何数据源的访问,如DB2、 Oracle、 SYBASE、 Teradata、 SAP以及微软的Excel 23 | * 几乎所有主流的字符编码 24 | 25 | ### 2.1.2 Base SAS 26 | Base SAS 是 Foundation SAS 的核心,是运行 SAS 必备模块,由 DATA 步、 PROC 步、MACRO、 DATA 步调试器、 ODS 以及 SAS 窗口环境组成。* DATA步:是由用于操作管理数据的编程语言组成的, SAS 编程优势的集中体现之一就是DATA步编程。 27 | * PROC步(SAS Procedures):是数据处理、统计分析与结果呈现的工具。 BASESAS里的SAS 过程有限,欲完成特定的处理和任务,需要加载特定模块,如SAS/STAT、 SAS/ETS、 SAS/IML等。 28 | * 宏(Macro Facility):宏的本质是文本替换,它可扩展和定制SAS 程序,完成重复、复杂的任务。 29 | * DATA步调试器:追踪DATA步执行情况,便于查找DATA步的运行错误。 30 | * 输出传递系统(Output Delivery System, ODS):将SAS输出以易访问的格式输出,如列表输出(LISTING)、 HTML输出、富文本输出(RTF)、 PDF输出以及以数据集形式输出等。 31 | * SAS窗口环境:开发测试SAS程序的环境,最为广泛的是SAS视窗管理系统(DMS)。 32 | 33 | DATA 步、 PROC 步和 MACRO 是 SAS 程序的三大核心。通常我们说写 SAS 程序就是:在 DMS 的高级程序编辑器里编写 SAS 的 DATA 步、 PROC 步以及宏。不过,在写 SAS程序之前,我们先熟悉下 SAS 处理数据的流程。 34 | 35 | ## 2.2 SAS 数据分析流程 36 | 如图 2-2 所示,在数据处理流程上,同 R 软件把所有数据都存在内存里进行计算不同SAS 读入各种来源的数据(可能是外部的原始数据,也可能是 SAS 可以直接打开的 SAS 数据文件),将其存储在硬盘的 SAS 数据集里,经过进一步的整理、清洗,把数据变成可以直接套用统计模型的数据集,然后运行统计模型、跑出统计分析结果、把结果进一步整理成表格、图片甚至是图文混排的报告,还可以把结果抓取出来另存为数据集,便于后续处理。 37 | 38 | 这个整理数据的过程通常是使用 DATA 步来完成的,分析数据的过程通常是使用 PROC步来完成的,此即人们口中常说的 SAS 两步编程: DATA 步整理数据、 PROC 步分析数据。 39 | 40 | 当然,在实际导入、整理数据的过程中,也不仅仅只限于用 DATA 步,巧妙运用PROC 步往往能事半功倍。同样,在 PROC 步做完分析后,也需配合用 DATA 步做进一步的结果整理,以便输出更易读的结果报告. 41 | 42 | ## 2.3 逻辑库与数据集 43 | SAS 从导入数据到完成统计分析报告,这中间涉及很多重要的基本概念,如数据集、DATA 步, PROC 步等。接下来,我们就顺着这个流程,把最基本、最重要的概念捋一遍,那就从逻辑库和数据集开始吧。 44 | 45 | ### 2.3.1 逻辑库 46 | 从上一小节 SAS 数据分析流程里我们知道,要进行数据分析,需要我们先把外部的Windows 数据文件,比如 CSV 文件转成 SAS 可以直接识别和处理的 SAS 数据集,而 SAS 数据集则必须置于 SAS逻辑库(SAS Library)中。数据存储在数据集里,数据集存放在逻辑库中,数据、数据集和逻辑库的关系就如同数据页、文件夹和抽屉的关系(见图 2-3)。 47 | 48 | 在 Windows 环境下, SAS 逻辑库其实是映射到一个(当然,也可以是多个)文件夹的名字。 SAS 会按照某些约定的格式去读写 SAS 逻辑库中的 SAS 数据集,这些约定的格式,被称为引擎(Engine),如 SAS 9.4 默认的引擎就是 V9, SAS 9.4 会用 V9 这种格式去生成 SAS 数据集、读取数据集。此外, SAS 还可以用其他引擎读取外部数据,如可以 25用 XLSX 引擎读取 Excel 文件, SPSS 引擎读取 SPSS 数据等(见图 2-4)。 49 | 50 | 大多数情况下,我们都希望处理好的 SAS 数据集能够保存在某个文件夹下,以备后用,而对于一些中间数据或者临时数据,我们则希望关掉 SAS 后他们就被自动删除。因此,SAS 给我们分别提供了永久逻辑库和临时逻辑库(见图 2-5)。永久库除了有 SAS 自带的Maps、 Mapsgfk、 Mapssas、 Sashelp 以及 Sasuser 外,我们也可以自建存放自己数据集的永久库,而临时库在 SAS 里就一个,名为 Work 库。我们在 Work 库里倒腾数据时很可能产生了一大堆中间数据集,最后把倒腾好的最终数据集存入永久库即可,关掉 SAS 软件后,那些留在 Work 库里的中间数据集不用我们去操心,会被自动删除。 51 | 52 | 例如,我们想在 D:\03 Writting\01 SAS 编程演义 \02 Data\Clean 的位置建立一个永久逻辑库,取名为“Demo”。一种方法是采用工具按钮如图 2-4 所示方法生成;另一种方法则是通过 LIBNAME 语句生成。基本格式就是: LIBANME 数据库名称 “数据库物理地址”,具体可见程序 2-1 利用 LIBNAME 语句自建永久逻辑库。 53 | 54 | 程序2- 1 利用LIBNAME语句自建永久逻辑库 55 | ```SAS 56 | *===带*号的行是注释行===; 57 | *===自建永久库===; 58 | *===取名Demo,地址: D:\03 Writting\01 SAS编程演义\02 Data\Clean; 59 | libname demo "D:\03 Writting\01 SAS编程演义\02 Data\Clean"; 60 | ``` 61 | 62 | 需要留意的是,通过工具按钮建立永久库时,可以在建立时勾上“Enable at startup”,下次启动时 SAS 就会自动加载这个永久库,而通过 LIBANME 语句自建的永久库在 SAS重新启动时,需要重新运行 LIBNAME 语句才可以重新把永久库的名字和物理地址关联上,然后才能在 SAS Libraries 里看到。如果想要让 SAS 语句建立的永久库在启动后就能看到,可以把包含 LIBNAME 语句的程序命名为autoexec.sas,并放入和 sas.exe 同级的目录中,此后每次 SAS 启动时会自动运行 autoexec.sas 程序,建立永久逻辑库。 63 | 64 | ### 2.3.2 数据集 65 | SAS 数据集有两种:一种是 SAS 数据文件 (SAS Data File);另一种是 SAS 视图 (SAS View)。数据文件和视图用 SAS 打开后都类似于一张表格,不同的是数据文件是真实存在的数据,而视图是运行查询语句后动态生成的数据,两者的图标在 DMS 中也不一样,如图 2-6 所示。不过两者在实际内容效果方面却是一样的,如图 2-7 所示。由此而带来的疑惑是:既然两者的实际内容效果一样,有了数据文件为何还需要视图?谨记:视图是依据查询语句动态生成,因此视图本身几乎是不占存储空间的,利用视图可以节约硬盘空间。 66 | 67 | SAS 数据集的组成成分,在逻辑上可能仅有描述信息(数据视图时的情况),也可能是包含描述信息、数据值(数据文件时的情况)以及可能的索引和其他扩展属性四部分,具体见表 2-1。单就数据集的描述信息来说,包括两部分:一部分是数据集概要信息,如数据集类型、创建引擎、创建时间等;另一部分是变量信息,如变量名称、类型、长度以及格式等。至于数据值,也即如图 2-7 所示的内容,是由行、列组成的表。行和列在 SAS数据集里分别称为观测(observation)和变量(variable),这部分内容是数据文件独有的,SAS 视图只有打开运行时才生成。 68 | 69 | 如果希望查看数据集的描述信息,特别是如图 2-8 所示的第三部分内容:变量列表及 29其属性,则可以通过 PROC CONTENTS 实现,并且可以进一步加工整理成数据库的变量字典,极大地方便后续的工作。当然,也可以通过右击数据集,通过「查看列」来查看变量信息(见图 2-9)。 70 | 71 | 欲查看数据值,可以通过 PROC PRINT 实现,当然,双击数据集也可以。不过在实际操作中,我们不会总是这样查看所有数据值,而是希望通过统计过程查看一些统计信息,如均数、分位数、分布图等。 72 | 由于数据集都在逻辑库下,因此在程序中指定数据集时,需要按照「逻辑库 . 数据集」的二级命名格式来明确告知是哪个逻辑库下的哪个数据集,中间用英文的句号隔开,当逻辑库为临时库 WORK 时,可以省略掉一级命名结构和句号「逻辑库 .」。 73 | 74 | 查看数据集的信息是学会了,但是如何创建数据文件呢?回顾图 2-2,我们知道有两种方式:导入外部数据或者读取既有的 SAS 数据集。导入外部数据,我们既可以用 DATA步,也可以用 PROC 步,具体情况将在第 3 章做详细介绍。读取既有数据集,我们可以用 31DATA 步的 SET 语句,这里我们简单举例,把 SAShelp 库的 Class 数据集读取到我们自建的永久库 Demo 里和临时库 WORK 里,并都命名为 class_datafile。 75 | 76 | 程序2-3 SET语句建立数据文件 77 | ```SAS 78 | *===自建永久库; 79 | libname demo "D:\03 Writting\01 SAS编程演义\02 Data\Clean"; 80 | *===建永久数据集, demo.不可省略; 81 | data demo.class_datafile; 82 | set sashelp.class; 83 | run; 84 | *===建临时数据集, work.被省略; 85 | data class_datafile; 86 | set sashelp.class; 87 | run; 88 | ``` 89 | 建立数据文件学会了,如何建立视图呢?有两种方法: DATA 步的 VIEW 选项和 PROC SQL 的 Create view 语句。 90 | 91 | 程序2-4 创建SAS视图 92 | ```SAS 93 | *===建视图; 94 | *===from data setp; 95 | data demo.class_view/view=demo.class_view; 96 | set sashelp.class; 97 | run; 98 | *===from Proc sql; 99 | proc sql; 100 | create view demo.class_view as 101 | select * 102 | from sashelp.class; 103 | quit; 104 | ``` 105 | 106 | ### 2.3.3 变量 107 | 数据集中最为重要的一个概念莫过于变量(variable)。在 SAS 里,我们可以将变量简单理解为存储数字或者字符的容器,一个变量就是一列。变量有其属性,包括名称、类型、长度、输入格式、输出格式、标签、观测中的位置以及索引类型等。图 2-9 展示了变量的属性信息。 108 | 109 | 变量名有其命名规则,后面会详细介绍。同其他编程语言或者统计软件不同的是,SAS 的变量类型非常简约,只有两种:数字和字符。数字型变量存储浮点数,包括日期和时间(在 SAS 里,日期实际存储的是距离 1960 年 1 月 1 日的天数,而时间实际存储的是距离凌晨的秒数,具体可见程序 2-5);字符型变量存储的是拉丁字母、 0 ~ 9 阿拉伯字母以及其他特殊字符,默认长度是 8 个字节。输入 / 输出格式是 SAS 读取或者显示变量的规则,数字型变量默认输入格式是「w.d」,输出格式是「BEST12.」;字符型默认输入、输出格式均为「$w.」。关于格式,我们将在第 7 章做详细介绍。 110 | 111 | 程序2-5 SAS日期、时间以及日期时间的本质 112 | ```SAS 113 | data tmp; 114 | date="01Jan1960"d; 115 | time="00:00:00"t; 116 | datetime="01Jan1960 00:00:00"dt; 117 | run; 118 | ``` 119 | 120 | ## 2.4 SAS 编程语言 121 | 前面几个小节我们基本上都把 SAS 当作一个软件来进行介绍,辅助性地展示了一些SAS 代码,对于初学者,如果没看懂前面的代码没有关系,理解软件层面的概念即可。从这一节开始,我们一起捋一捋 SAS 作为一门编程语言的基本概念和基础知识。 122 | 123 | ### 2.4.1 SAS程序结构 124 | SAS 程序是由一系列 SAS 语句(statement)组成,所谓 SAS 语句通常是指以 SAS 关键字(keyword)开头,始终以分号(;)结束的代码行。最常见的 SAS 关键字就是「DATA」和「PROC」,因此最常见的语句就是 DATA 语句和 PROC 语句。当然, SAS 的关键字多如牛毛,我们也不必刻意去死记硬背每一个 SAS 关键字。在 DMS、 EG 和 SAS Studio 的编辑器中, SAS 都会自动给关键字着成深蓝或者蓝色, EG 和 SAS Studio 还会给出提示,初学者可以尝试看看。 125 | 126 | 另外,如果从程序块上来讲解, SAS 程序可以分为两大块: DATA 步和 PROC 步。所谓一个「步」(step)是指这样的一个程序块。 127 | 128 | * 以DATA语句或者PROC语句开头。 129 | * 以RUN语句(大多数情况下)、 QUIT语句(部分情况下)、新的DATA语句或者PROC语句结束。 130 | 131 | 在 SAS 编辑器中, SAS 会自动显示横线以隔开 DATA 步或者 PROC 步(见图 2-10)。需要留意的是,有些语句只能在 DATA 步里出现(如 INPUT 语句),有些语句只能在 PROC 步里出现(如 CLASS 语句),有些语句 DATA 步、 PROC 步都可以出现(如FORMAT 语句),而还有些语句可以既不在 DATA 步也不在 PROC 步出现,他们可以单独出现(如前面使用过的 LIBNAME 语句),此即 DATA 步语句、 PROC 步语句及全局语句的概念。 132 | 133 | SAS 程序除了单独的 DATA 步和 PROC 步程序,还有可以把他们打包组合在一起的程序,那就是宏程序,宏程序本质上是文本替代,用更少的文本替代更多的文本。这个话题暂且不做过多介绍,留在后面的第 10 章进行详细说明。 134 | 135 | ### 2.4.2 SAS语法规则 136 | 规则的 SAS 程序书写风格看起来基本就是被 DATA 步和 PROC 步分割的条块,其实SAS 程序书写的格式是比较自由的,如果要真正究其语法规则的话,有两方面:① SAS语句语法规则;② SAS 名语法规则。 137 | 138 | SAS 语句语法规则: 139 | * 分隔单词的可以是一个空格或特殊字符(比如加号、等号等运算符),也可以是 140 | 多个。 141 | * 程序可以在任何列开始,也可以在任何列结束。 142 | * 单个语句可以写在多行,多个语句也可以写在一行。 143 | 144 | SAS名是指 SAS给其一些语言元素(如逻辑库、数据集、变量以及格式等)的名称标记。SAS 名有两类。 145 | 146 | (1) SAS 系统定义名,如自带的库名 WORK、 SASHELP 等;如特殊的数据集名 _NULL_ (不创建数据集)、 _DATA_(自动数据集名)、 _LAST_(最后一个活动数据集);如 SAS DATA 步的自动变量名 _N_ (观测号)、 _ERROR_(错误标识变量);如特殊的变量列表名 _CHARACTER_(所有字符型变量)、 _NUMERIC_(所有数字型变量)、 _ALL_(所有变量);以及 SYS 开头的宏变量名如SYSDATE(日期)、 SYSVER(SAS 版本)等。 147 | (2)用户自定义名,自定义名不能与系统定义名相冲突,且需符合 SAS 命名的语法规则,总结起来可归纳为以下三点。 148 | * 只能由数字、字母、下划线组成。 149 | * 首字符不能是数字。 150 | * 长度限制各有不同,有的最长可以达32个字符(如变量名,宏变量名),有的最长只能有8个字符(如逻辑库、文件引用名以及引擎名)。 151 | 152 | 这个命名规则一定要遵守吗?是的,都应该遵守。这个规则能打破吗?可以,但不推荐。不过,有的时候,我们也确实有特殊需求:比如如何打破规则让 SAS 也可以用中文命名数据集、命名变量呢?这时候,我们可以通过修改系统选项 VALIDMEMNAME 和VALIDVARNAME 的值来实现,如图 2-11 所示. 153 | 154 | 程序2-6 SAS中文名数据集和变量名 155 | ```SAS 156 | *===中文名数据集; 157 | *===中文名变量; 158 | options validmemname=extend validvarname=any; 159 | data 中文名演示; 160 | SAS中文变量名="YES"; 161 | SAS中文变量名="YES"; 162 | '2SAS中文变量名'n="YES"; 163 | '2SAS中文变量名'n="YES"; 164 | 'SAS空 格变量名'n="YES"; 165 | 'SAS空# @ %格特殊字符变量名'n="YES"; 166 | run; 167 | ``` 168 | 语法规则只是对编程的合法性给出了最低的要求。在合法性的基础上,我们还应追求语法风格的统一和规范,这样不仅方便自己日后阅读调试,也方便他人审阅,下面是同一段简单的 SAS 程序,对比左右两边的风格,正常的人类都更愿意看左边的,对吧?编程人士中有一个术语叫 Good Programming Practice, GPP,即良好编程实践,很多编程语言都有推荐的编程规范,遵循这些规范,可以极大地方便与同行的交流,笔者自己总结过一些 SAS 的编程规范,具体可参考附录。 169 | 170 | 程序2-7 编程风格:规范与凌乱 171 | ```SAS 172 | *===自建永久库; 173 | libname demo "D:\03 Writting\01 174 | SAS编程演义\02 Data\Clean"; 175 | *===建永久数据集, demo.不可省略; 176 | data demo.class_datafile; 177 | set sashelp.class; 178 | run; 179 | *===建临时数据集, work.可以省略; 180 | data class_datafile; 181 | set sashelp.class; 182 | run; 183 | 184 | *===凌乱风格; 185 | libname demo "D:\03 Writting\01 186 | SAS编程演义\ 187 | 02 Data\Clean"; 188 | data demo.class_datafile; 189 | set sashelp.class; 190 | run; data class_datafile; 191 | set sashelp.class;run; 192 | ``` 193 | ### 2.4.3 SAS语言元素 194 | 作为一门编程语言, SAS 语言元素除了上面提及的 SAS 语句(statements),还有表达式(expressions)、选项(options)、格式(format)、函数(function)以及 Call 列程(Call 195 | Rountine)等. 196 | 197 | 1. 表达式 198 | 表达式是 SAS 语言中一个非常重要的概念, SAS 在生成一个新变量、给一个变量赋值、计算新值、变量转换以及依据不同的条件进行处理都需要借助表达式来实现。什么是表达式? SAS 官方给表达式的定义比较拗口:表达式是由一系列操作数和操作符构成的、可执行的、并且产生结果值的序列。简单来说,表达式就是告诉 SAS 对什么对象执行什么操作,从而得到一个结果的命令。被操作的对象叫操作数(operands),执行操作用的符号就是操作符(operators),习惯上称运算符的更多,执行的结果可能是一个数字值,也可能是一个字符值,还可能是一个布尔值(是 / 否、真 / 假、 1/0)。 199 | (1)操作数:操作数可以是常量、变量,也可以是表达式。常量,顾名思义,表示一个值是恒常固定的量;同理,变量表示值是可以变化的,有一套数值去刻画某个特征的量。 200 | 201 | 常量有以下四种情况。 202 | * 字符常量:字符常量由1~32767个字符组成,必需放在英文引号内,引号可以是单引号,也可以是双引号。字符常量中包含单引号(双引号)时,可以用双引号(单引号),或者连续的单引号(双引号),如:“Hongqiu Gu’s Book”。 203 | * 数字常量:数字常量无须多言,只需留意除了标准计数法(如: 1, -5, +49,1.23, 01),科学计数法(如: 2E23, 0.5e-10)和十六制计数法(如: 0C1X、9X)也可以。 204 | * 日期时间常量:时间日期常量包括日期、时间、日期时间常量三种,命名是需要采用单引号或双引号加D(日期)、 T(时间)、 DT(日期时间)后缀来分别表示,如‘08Sep2016’D、 ’11:11’T、 ’08Sep201611:11’DT,具体可参考程序 25 SAS日期、时间以及日期时间的本质,这种引号加字母后缀的命名方式称之为名称文字(Name Literal),在使用非规范的数据集名、变量名时也需要用到这种形式。 205 | * 位 测 试 常 量 : 在 引 号 里 由 0 , 1 以 及 点 (. ) 组 成 字 符 串 , 且 后 缀 为 B ,如’..1.0000’b,用来测试对应的位是否为0或1。这种常量使用较少,在此不做具体介绍。 206 | 207 | 变量有两种类型:字符变量和数字变量。日期、时间以及日期时间在 SAS 里其实也是以数字存储的数字变量。如前所述,日期变量的值为距离 1960 年 1 月 1 日的天数,时间变量的值为距离凌晨的秒数,日期时间的值为距离 1960 年 1 月 1 日凌晨的秒数。 208 | 209 | 程序2-8 SAS中的常量 210 | ```SAS 211 | *===常量; 212 | data _null_; 213 | *==字符常量; 214 | c1="Hongqiu Gu's Book"; 215 | c2='Hongqiu Gu''s Book'; 216 | c3='Hongqiu Gu"s Book'; 217 | c4="Hongqiu Gu""s Book"; 218 | *==数字常量; 219 | n1=123; 220 | n2=-123; 221 | n3=+123; 222 | n4=1.23; 223 | n5=0123; 224 | *===日期时间常量; 225 | d='08Sep2016'D; 226 | t='11:11'T; 227 | dt='08Sep2016:11:11'DT; 228 | *===在日志中输出; 229 | put c1-c4 ; 230 | put n1-n5 ; 231 | put d yymmdd10.; 232 | put t time.; 233 | put dt datetime.; 234 | run; 235 | ``` 236 | 237 | (2)运算符: SAS运算符从位置上讲,放在操作数前面的叫前缀运算符(如 +、 -),放在操作数中间的叫中缀运算符(大多数运算都是);从功能上讲,有用于算术运算的算术运算符(如 +、 -、 *、 /),用于比较大小的比较运算符(如 >、 <、 =、 ^=),用于逻辑运算的逻辑运算符(如 ^、 &、 |);算术运算符运算的结果通常为数值,比较和逻辑运算符运算的结果真(1)或假(0)。关于这几种运算符,没有太多可说的,请参考下面的表 2-2、表 2-3及表 2-4。 238 | 239 | 表 2-2 算术运算符 240 | 241 | | 符 号 | 定 义 | 例 子 | 结 果 | 242 | | ----- | ----- | -------------- | ------------------- | 243 | | ** | 指数 | a\*\*3 | | 244 | | * | 乘 | 2*y | 2 乘以 y | 245 | | / | 除 | Var/5 | Var 除以 5 | 246 | | + | 加 | Num+3 | Num 加 3 | 247 | | - | 减 | Sales-Discount | Sales 减去 Discount | 248 | 249 | 注:乘法中, * 号是必需的, 2y 或者 2(y) 都是非法的。 250 | 251 | 252 | 表 2-3 比较运算符 253 | | 符 号 |等 效 字 符 | 定 义 | 例 子 | 254 | | --- | --- | --- | --- | 255 | | = | EQ | 等于 | A=3 | 256 | | ^=、¬=、 ~= | NE | 不等于 | A^=3, A¬=3, A~=3 | 257 | | > | GT | 大于 | Num>8 | 258 | | < | lT | 小于 | Num<8 | 259 | | >= | GE | 大于等于 | Sales>=100 | 260 | | <= | LE | 小于等于 | Sales<=100 | 261 | | | IN | 等于列表中的一个元素 | Num in (3,4,5) | 262 | 263 | 注: *NE 的符号在不同的键盘上可能会有所不同。 264 | **>=、 <= 与以前 SAS 版本兼容。 WEHRE 或 SQL 语句中不支持 265 | 266 | 表 2-4 逻辑运算符 267 | | 符号 | 等效字符 | 例子 |运算符说明| 268 | | --- | --- | --- |---| 269 | | & | AND | (a>b & c>d) | 两边都为真,运算结果为真| 270 | | \|、!、 ¦ | OR* | (a>b or c>d) | 任一边为真,运算结果为真| 271 | | ¬、 ^、 ~ | NOT* | Not(a>b) | 取反面结果 | 272 | 273 | 注: * 不同的操作环境可能符号有所不同。 274 | 275 | 除此之外,还有取小运算符(><)、取大运算符(<>)以及连接运算符(||)。 >< 和<> 分别用来找到两个操作数中的最小值、最大值, || 用来连接前后两字符。如果只是单个运算符时,不会牵涉到运算顺序的问题,但是,当有多个运算符时,就需要理清运算顺序了,如复合表达式中会有多个运算符,其运算顺序的原则是: 276 | (1)先算括号中的表达式,再算括号外。 277 | (2)不同组有不同的优先级。 278 | (3)同组内有不同的运算顺序。 279 | 具体示例详见表 2-5。 280 | 281 | 表 2-5 复合表达式运算顺序 282 | | 优先级 | 运算顺序 |符 号 | 例 子 | 283 | | --- | --- | --- | --- | 284 | | 组 1 | 从右到左 | ** | y=a**2; | 285 | | | | + | y=+(a*b); | 286 | | | | - | z=-(a+b); | 287 | | | | ^¬ ~ | if not z then put x; | 288 | | | | >< | x=(a> | x=(a<>b); | 290 | | 组 2 | 从左到右 | * | c=a*b; | 291 | | | | / | f=g/h; | 292 | | 组 3 | 从左到右 | + | c=a+b; | 293 | | | | - | f=g-h; | 294 | | 组 4 | 从左到右 | || ¦¦ !! | name= ‘J’||’SMITH’; | 295 | | 组 5 | 从左到右 | < |if x= | if y>=a then output; | 300 | | | | > | if z>a then output; | 301 | | | | | if state in (‘NY’,’NJ’,’PA’) then region=’NE’;y = x in (1:10); | 302 | |组 6 | 从左到右 | & | if a=b & c=d then x=1; | 303 | | 组 7 | 从左到右 | \| ¦ ! | if y=2 or x=3 then a=d; | 304 | 305 | 2. 选项 306 | SAS 选项包括系统选项和数据集选项。系统选项主要是一些可以影响整个 SAS 程序执行或 SAS 会话交互的指令,数据集选项是仅用于数据集的选项,如变量的重命名与筛选、观测筛选、数据集权限控制等。 307 | 308 | 3. 格式 309 | 格式依据应用场景,分为输入格式和输出格式;依据定义方式,分为系统格式和自定义格式。格式告诉 SAS 按一定的模式读取、显示数据。关于格式,详见第 7 章。 310 | 311 | 4. 函数与 CALL 例程 312 | SAS 函数可以接收参数,执行一些运算和操作,然后返回一个值。 CALL 例程与 SAS 函数类似,不过不能用在赋值语句或表达式中。关于函数和CALL例程,详细讨论将在第6章进行。 313 | 314 | 我们通过一个综合的例子来简单感受下上面提及的一些概念。 315 | 316 | 程序2-9 SAS语言元素演示 317 | ```SAS 318 | *====概念演示; 319 | data test2; 320 | length ID $ 4; 321 | input Name $ start yymmdd10. end date8. grade; *输入格式; 322 | FirstName=substr(Name,1,1); *函数substr; 323 | GivenName=substr(Name,length(Name)-1,2); *函数substr; 324 | call cats(ID,FirstName, GivenName); *CALL CATS例程; 325 | if grade>=2 and start<'01Jun2016'd then pay=(end-start)*150; 326 | *比较、逻辑、算术运算; 327 | else pay=(end-start)*100; 328 | datalines; 329 | ZhangXL 2016/08/09 06SEP16 1 330 | WangSJ 2016/07/03 09SEP16 2 331 | WenTC 2016/05/05 02SEP16 3 332 | LiWC 2016/04/09 10SEP16 2 333 | ; 334 | run; 335 | options nodate; *系统选项; 336 | proc print data=test2(obs=2); *数据集选项; 337 | var ID start end pay ; 338 | format start yymmdd10. end yymmdd10.; *输出格式; 339 | run; 340 | ``` 341 | ### 2.4.4 三种逻辑结构 342 | 就如人生中面临的三种情境一样:按照既定的步骤去做一些事情、依据不同情境选择性地应对一些事情、在某些情境下重复做相同的事情,几乎所有的编程语言都设计了三种程序逻辑结构:顺序、选择和循环。 343 | 344 | 1. 顺序结构(sequence) 345 | 顺序结构的程序执行时就按照代码出现的顺序依次执行:第一条语句,第二条语句,第三条语句……前面的所有 SAS 代码几乎都是顺序结构式的。 346 | 347 | 2. 选择结构(selection) 348 | 最经典的选择结构语句就是 IF-ELSE/THEN 语句,告诉 SAS 在满足某条件的情况下执行一套操作,不满足则执行另一套操作。例如,我们对 SASHLEP 库 CLASS 数据集的人按男女性别的不同分别抓出来放到 Male 和 Femal 数据集。 349 | 350 | 程序2-10 IF-ELSE/THEN示例 351 | ```SAS 352 | data male female; 353 | set sashelp.class; 354 | if sex="M" then output male; 355 | else if sex="F" then output female; 356 | else put "Invalid sex :" sex ; 357 | run; 358 | ``` 359 | 360 | 需要留意的是: 361 | * 对于情境的分类,要考虑完全。因此,尽量最后加一个ELSE语句,纳入其他所有可能情况。 362 | * 如果某种情境下,希望执行的不仅仅是一个动作,而是多个动作,此时可以在关键词THEN后面用夹板语句DO-END,把多个动作整合在DO-END语句中。例如,我们嫌弃SEX不文雅,把它换成GENDER,用Male、 Female标明男性、女性。 363 | 364 | 程序2-11 IF-ELSE配合DO-END 365 | ```SAS 366 | data male female; 367 | set sashelp.class; 368 | if sex="M" then do; gender="Male "; output male; end; 369 | else if sex="F" then do; gender="Female"; output female; end; 370 | else put "Invalid sex :" sex ; 371 | run; 372 | ``` 373 | 374 | 3. 循环结构(iteration) 375 | 循环结构的程序是只要满足某个特定的条件,就重复进行某些操作。 SAS 里常见的循环语句有三种: DO 循环语句、 DO-WHILE 语句以及 DO-UNTIL 语句。 376 | (1) DO 循环语句。 DO 循环语句其实就是 DO-END 语句的衍生,在 DO 后面添加循环的条件,这个条件可以是数字、字符、日期的列表;可以指定起始值和终止值以及步长;还可以是前面两者的混合。 377 | 程序2-12 DO循环语句 378 | ```SAS 379 | data schedule; 380 | do date='01Sep2016'd to '30Sep2016'd ; *日期循环; 381 | day=weekday(date); 382 | if day in (1,7) then Activity="Running"; 383 | else if day in (2,4,6) then Activity="Writing"; 384 | else Activity="Reading"; 385 | output; 386 | end; 387 | run; 388 | data random; 389 | do i=1 to 10; *数字10次循环; 390 | r=rannor(23); *生成随机数; 391 | output; 392 | end; 393 | run; 394 | ``` 395 | 396 | (2) DO-WHILE 语句。与 DO 循环语句每次按照指示变量的值去执行不同, DOWHILE 语句会先判断是否满足条件,如果满足则执行否则跳出循环。 397 | (3) DO-UNTIL 语句。与 DO-WHILE 语句会先判断是否满足条件不同, DO-UNITL语句不管三七二十一,先执行了本次循环再说,而后再判断条件是否满足。在做条件判断时,DO-UNTIL 与 DO WHILE 的思维也不一样: DO-UNIL 是如果不满足,则继续下一次循环,如果满足,则跳出循环。具体可留意程序 2-13 的条件差异。 398 | 399 | 程序2-13 循环语句DO WHILE 与DO UNTI 400 | ```SAS 401 | data dowhile; 402 | i=0; 403 | do while(i<5); 404 | i+1; 405 | output; 406 | end; 407 | run; 408 | 409 | data dountil; 410 | i=0; 411 | do until(i>=5); 412 | i+1; 413 | output; 414 | end; 415 | run; 416 | ``` 417 | 如果读了上面的文字和程序,对三种逻辑结构还是不太清楚的话,图 2-12 或许能让我们的思维更清晰些。 418 | 419 | ### 2.4.5 数组结构 420 | SAS 编程语言不像其他语言那样有丰富的结构体(struct),用来聚合数据类型,这正如 SAS 的数据类型只有简单的字符和数字两种。不过,其他编程语言的数组(array)的思想倒是在 SAS 编程语言中有充分的利用。 421 | 422 | SAS 编程语言里,数组是一系列有特定顺序的变量组成的一个临时变量组。之所以说是临时的,是因为数组仅仅存在于 DATA 步执行的过程中。数组中的变量必须有相同的数据类型,如果全为字符型,则为字符型数组;如果全为数字型,则为数字型数组。此外,如果数组里的值只在一个维度上排列,比如就一行,这就是一维数组;如果数组里的值在多个维度上排列,比如行列上都有,就像一张 EXCEL 表格,这便是二维数组。 423 | 424 | 在什么场合下会用到数组呢?怎样理解一维和二维数组呢?举例说明:比如某研究项目持续每天测量患者的收缩压(SBP)、舒张压(DBP),并持续了一周,这样就有 7 次收缩压和 7 次舒张压的测量值。当然,我们可以把他们分别存储在 SBP1~SBP7、DBP1~DBP7 这 14 个变量中。但是仅仅这样,可能还不够,如果后期我们发现这批血压仪的测量值有系统偏差, SBP 比正常测值低 5mmHg, DBP 比正常测值低 3mmHg。现在要校正的这些血压值,我们要分别对 SBP、 DBP 写 7 个赋值语句,总计 14 个。这样是不是太烦琐了?是的。这时候数组就可以派上用场了。 425 | 426 | 我们可以建两个数组 SBP、 DBP 分别用来存储 SBP1~SBP7、 DBP1~DBP7。就像下面这样有一排格子,每个格子有一个编号, SAS 依据格子的编号进行数据的存取,这就是一维数组,数据排列就在一个维度上:行。 427 | 428 | 数组 SBP : 429 | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 430 | | --- | --- | --- | --- | --- | --- | --- | 431 | | SBP1 | SBP2 | SBP3 | SBP4 | SBP5 | SBP6 | SBP7 | 432 | 433 | 数组 DBP: 434 | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 435 | | --- | --- | --- | --- | --- | --- | --- | 436 | | DBP1 | DBP2 | DBP3 | DBP4 | DBP5 | DBP6 | DBP7 | 437 | 438 | 当然,我们甚至可以直接建一个数组,同时把 7 次 SPB, DBP 的值打包在一起,这就是二维数组,数据排列在两个维度上:行和列。 439 | 440 | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 441 | | ---- | ---- | ---- | ---- | ---- | ---- | ---- | 442 | | SBP1 | SBP2 | SBP3 | SBP4 | SBP5 | SBP6 | SBP7 | 443 | | DBP1 | DBP2 | DBP3 | DBP4 | DBP5 | DBP6 | DBP7 | 444 | 445 | 上面只是给出了数组的概念示意图。实际操作时涉及两个核心问题:一是如何定义数组;二是如何访问数组。 446 | 447 | 1. 定义数组 448 | SAS DATA 步中,我们通过语句 ARRAY 来定义数组。其具体语法格式请参考语法 2-1: 449 | 450 | > ARRAY array-name{number-of-elements} <$> <(initial-value-list)>; 451 | 452 | 关于数组语法的一些解释如下所述。 453 | * 元素个数可以用{*}代替,表示让SAS自动计数,也可以指定具体的数字,如{7},还可以指定一定的数字范围,如{1:7}。 454 | * 元素名可以是变量名,也可以是SAS自定义的变量,如_ALL_(标示所有定义的变量,但是变量类型需要相同) , _NUMERIC_(所有数字变量)以及_CHARACTER_(所有字符变量),还可以是_TEMPORARY_(临时变量)。 455 | * <>表示其中的内容并非必须有。例如, $只有在数组元素为字符型时才用到, length也是。数组元素及其初始值也并非必需,如果指定数组元素初始值的话,应该在小括号中指定。 456 | 457 | 程序2-14 定义数组 458 | ```SAS 459 | *===定义数组; 460 | *===sbp1-sbp7是sbp1到sbp7的缩略写法; 461 | array sbp{7} sbp1-sbp7; 462 | array dbp{1:7} dbp1-dbp7; 463 | *===带初始值; 464 | array sbp{1:7} sbp1-sbp7 (163 164 167 171 155 158 154); 465 | array dbp{7} dbp1-dbp7 (98 99 92 94 95 93 93); 466 | *===定义二维数组; 467 | array bp{2,1:7} sbp1-sbp7 dbp1-dbp7 ; 468 | array bp{2,7} sbp1-sbp7 dbp1-dbp7 (163 164 167 171 155 158 154 98 99 92 469 | 94 95 93 93); 470 | ``` 471 | 472 | 2. 访问数组 473 | 访问数组的元素时,我们需要告诉 SAS 数组元素的地址,数组中元素的地址用数组名加角标的形式arrayname{i} 表示。配合前面已经介绍过的 DO 循环语句,我们可以遍历 数组中的所有元素(见图 2-13),进行各种数据操作,如果希望进行前面提到的加减校正,把 PUT 语句换成赋值语句即可. 474 | 475 | 程序2-15 访问数组元素 476 | ```SAS 477 | data tmp; 478 | *===定义数组; 479 | array sbp{7} sbp1-sbp7 (163 164 167 171 155 158 154); 480 | array dbp{7} dbp1-dbp7 (98 99 92 94 95 93 93); 481 | array bp{2,7} sbp1-sbp7 dbp1-dbp7 (163 164 167 171 155 158 154 98 99 482 | 92 94 95 93 93); 483 | *===遍历一维数组; 484 | do i=1 to 7; 485 | put "第" i "次测量的SBP为: " sbp{i}; 486 | put "第" i "次测量的DBP为: " dbp{i}; 487 | end; 488 | *===遍历二维数组; 489 | do m=1 to 2; 490 | do n=1 to 7; 491 | put "血压类型为: " m ",血压测量次数为: " n ",血压测量值为: " bp{m,n}; 492 | end; 493 | end; 494 | run; 495 | ``` 496 | ### 2.4.6 函数与CALL例程 497 | 在 SAS 里,特别是 DATA 步中,如果希望更加方便、快捷地处理数据,我们就必须了解函数和 CALL 例程。 SAS 函数可以接收参数,执行一些运算和操作,然后返回一个值。CALL 例程与 SAS 函数类似,不过不能用于赋值的语句或表达式中。我们通过一个简单的例子感受下函数和 CALL 例程的应用。 498 | 499 | 程序2-16 函数与列程应用示例 500 | ```SAS 501 | data _null_; 502 | length FullName_ByFunction FullName_ByRoutine $10; 503 | FamilyName="Gu"; 504 | GivenName="Hongqiu"; 505 | *===用函数生成全名; 506 | FullName_ByFunction=catx(" ",GivenName, FamilyName); 507 | *===用列程生成全名; 508 | call catx(" ",FullName_ByRoutine, GivenName, FamilyName ); 509 | *===Log中查看结果; 510 | put "Fullname Generatedy by Function: " FullName_ByFunction; 511 | put "Fullname Generatedy by Routine: " FullName_ByRoutine; 512 | run; 513 | ``` 514 | 笔者粗略统计了下, SAS 中有将近 30 多类,总计达 520 个函数。这是一个比较庞大的体系,也是一个非常有利的武器,我们将在第 6 章专门论述。 515 | 516 | ### 2.4.7 结构化查询语言SQL 517 | SQL 是结构化查询语言(Structured Query Languag)的简称,自 1970 年 IBM 开发以来,作为关系型数据库查询工具的标准化语言而广泛使用。 SAS 自 6.06 版本引入 SQL 后,一直在增强完善其功能及其与 SAS 软件的兼容性,目前 SAS 9 中的 SQL 已经非常强大。通过 SQL,我们可以进行简单查询、子查询,不用排序就可以进行表的连接、集合运算、创建视图和表、创建宏变量等一系列操作。本小节我们仅就 SQL 语言做一概要式介绍,具体的应用我们会结合后面的实例再讨论。 518 | 519 | SQL 最简单的应用就是用 SELECT 语句做查询。 SELECT 语句包含了一系列有序的从句,具体可见语法 2-2。Help 中 <> 表示里面的东西选用。因此,必用的就只有 SELECT 和 FROM 了,比如下面的例子就用 SQL 查看 sashelp.class 中的姓名、性别以及年龄。 520 | 521 | > Proc sql; 522 | SELECT object-item-1<, object-item-2, ...> 523 | > 525 | FROM from-list 526 | 527 | > 528 | , ...>>; 529 | Quit; 530 | 531 | 程序2-17 最简单的一个SQL过程 532 | ```SAS 533 | proc sql; 534 | select name, sex, age 535 | from sashelp.class; 536 | quit; 537 | ``` 538 | 539 | 当然其他从句也是非常实用的。比如,用 WHERE 可以进行条件筛选,用 GORUPBY 可以进行分组统计,用 HAVING 可以对分组统计的结果进行条件筛选,用 ORDERBY 可以对结果进行排序。初接触时,可能对这些从句的顺序记忆有些混淆,笔者个人就用 SFW、 GHO 来记它。 sfw 是一种位图格式文件的扩展名, gho 是 ghost 镜像文件的扩展名。 540 | 541 | 下面是一个完整的,利用了所有 SELECT 从句的例子。目的是先按性别分组统计人数、平均身高,然后挑出身高大于 62 的组,最后按人数多少排序。 542 | 543 | 程序2-18 PROC SQL SELECT语句全从句示例 544 | ```SAS 545 | proc sql; 546 | select sex, count(name) as cnt_name ,mean(height) as m_height 547 | from sashelp.class 548 | where age>=12 549 | group by sex 550 | having m_height>62 551 | order by cnt_name; 552 | quit; 553 | ``` 554 | 555 | ### 2.4.8 SAS宏MACRO 556 | MACRO(宏)这个术语可能对我们来说并不陌生,宏就是实现自动化操作的一种工具。在 EXCEL 里我们就曾接触过,只是大部分人很少用而已。在 SAS 里,宏工具是一个用来自动化和定制化 SAS 代码的文本处理工具。 557 | 558 | SAS 的强大,很大一部分原因就是宏工具的存在。宏的本质是文本替换,但是通过文本替换,可以实现 SAS 代码的自动化生成,动态生成以及 SAS 代码的条件结构,也就是说,不仅可以让 SAS 代码自己去写 SAS 代码,而且还可以根据不同的条件写不同的代码,这很符合“元编程”的理念。也正是因为这样,很多 SAS 开发者,疯狂开发自己的宏,从而避免很多重复性的代码编写工作,实现更多自动化、智能化的处理。 559 | 560 | SAS宏语言分为两大块:宏变量和宏程序。宏变量是不必限定在 DATA步使用的变量,即独立于数据集的变量。宏变量分为系统宏变量和用户自定义宏变量。最常规的情况下,我们可以用 %LET 语句定义宏变量, %PUT 语句查看宏变量。正如前面所说,宏本质是文本替换,宏变量也是用简单的文本去替换更长更复杂的文本。例如,我们可用一小段文本“PUMC”替换更长的“Peking Union Medical College”。 561 | 562 | 程序2-19 宏变量 563 | ```SAS 564 | *===自定义; 565 | %let PUMC=Peking Union Medical College; 566 | *===查看系统自带; 567 | %put &sysdate; 568 | *===查看自定义; 569 | %put &PUMC; 570 | ``` 571 | 572 | 宏程序同宏变量类似,不过宏程序还有其他特性:①可以包含编程语句,包括 DATA步和 PROC 语句;②可以接受参数。比如,我们可以定义一个打印指定数据集、指定变量的宏。在定义宏程序时,用 %MACRO开头,用 %END结尾,使用宏时,用 %宏名称即可。 573 | 574 | 程序2-20 MACRO定义和调用 575 | ```SAS 576 | *===定义Macro; 577 | *===通过data和var这两个参数指定数据集和变量; 578 | %macro prtdsvar(data=, var=); 579 | proc print data=&data; 580 | var &var; 581 | run; 582 | %mend; 583 | 584 | *===调用Macro; 585 | %prtdsvar(data=sashelp.class, var=name sex) 586 | ``` 587 | 588 | 关于宏,本节仅做概念性介绍,具体的内容我们将在第 10 章进行详细讨论。 589 | 590 | ## 2.5  理解 SAS 运行机制 591 | SAS 的学习曲线比较陡峭,其原因之一就是很多 SAS 学习者没有深入理解 SAS 的运行机制,其中最为重要的机制就是 PDV(Program Data Vector)与 DATA 步自循环。 592 | 593 | ### 2.5.1 PDV与DATA步自循环 594 | 很多时候,即使是写了很多 SAS 程序、用了很长时间 SAS 的人,也总是会对 SAS 595 | DATA 步运行出的结果感到莫名其妙,对发生的错误更是一头雾水,但是如果能够静下心来,了解 PDV、理清 SAS 的运行机制,很多疑惑或许就迎刃而解了。 596 | 597 | SAS 系统处理 SAS DATA步时,分两步:编译和执行。经典的 DATA步,基本按照图 2-14的流程来。 598 | 599 | 具体而言,在编译和执行阶段, SAS 会分别进行见表 2-6 的操作。 600 | 601 | 表 2-6 编译和执行阶段具体动作 602 | 603 | 604 | 1.编译 : 提交代码后, SAS 进行编译,此时 SAS 要确定每个变量的类型和长度,并确定变量是否有必要进行类型转换。具体如下: 605 | * 检查代码的语法 606 | * 将代码翻译成机器语言 607 | * 如果是用 INPUT 语句读入原始数据,则建立输入缓冲区(Input Buffer),如果是读入 SAS数据集,则直接建立程序数据向量 608 | * 建立程序数据向量(Program Data Vector, PDV),包含 609 | * SAS 数据集变量以及 SAS 语句计算生成的变量 610 | * 自动变量 _N_、 _ERROR 611 | * 建立 SAS 数据集以及变量属性的描述信息 612 | * 数据集名字、类型(数据文件、 SAS 视图)、创建日期时间 613 | * 变量名称、类型(字符、数字)、序号等 | 614 | 2. 执行:默认情况下,一条观测要经历一次 DATA 步的迭代 615 | * 从 DATA 语句开始,将 _N_ 值设定为 1(随着 DATA 语句的每次迭代,变量 _N_ 自动加 1),_ERROR_=0(发生错误时 _ERROR_ 会变成 1,程序终止) 616 | * 把 PDV 中的变量设为缺失 617 | * 用 INPUT 语句把一条数据记录从原始数据读入缓存区,或者用 SET, MERGE, MODIFY或 UPDATE 语句,把 SAS 数据集里的一条观测值读入到 PDV 618 | * 对当前观测执行 DATA 步中后续的程序语句 619 | * 执行完最后的语句, SAS 自动完成输出、返回、重设动作 620 | * 输出,即把观测从 PDV 写入数据集,自动变量(_N_、 _ERROR_ 不会输出) 621 | * 返回:系统自动返回 DATA 步开头 622 | * 重置:将 PDV 里由 INPUT 语句和赋值语句创建的变量设置为缺失,但由 SET,MERGE, MODIFY 或 UPDATE 语句读入的变量不置为缺失 623 | * SAS 开始计下一次迭代,读入下一条记录或观测,对当前观测执行后续的编程语句 624 | * 到达要读入的 SAS 数据集或者原始文件的数据末尾时, DATA 步终止 625 | 626 | 在上面的过程中,有两个概念不是很好理解:一是输入缓冲区(Input Buffer);二是 程序数据向量(Program Data Vector)。这两个概念都是内存里的一个逻辑区域,我们简要示图如图 2-15 所示。 627 | 628 | Buffers 是系统内存的缓冲区,我们可以先不细究。如上图,分别展示了读入原始数据和读入 SAS 数据集时的流程。 629 | (1)读入原始数据时:原始数据先读入 Input Buffer,再从 Input Buffer 转换到 PDV,最后从 PDV 输出到 SAS 数据集。 630 | (2)读 SAS 数据时:把数据集观测直接读入到 PDV,再从 PDV 输出到数据集。 631 | 我们再次以一个小程序为例,看看 Input Buffer与 PDV,了解 SAS DATA步的运行机制。 632 | 633 | 程序2-21 PDV演示程序 634 | ```SAS 635 | data demoPDV; 636 | input ID $ Chinese Math English; 637 | Sum=Chinese+Math+English; 638 | datalines; 639 | S001 80 99 93 640 | S002 90 85 95 641 | S003 83 88 81 642 | ; 643 | run; 644 | ``` 645 | 646 | 在编译阶段, SAS 就知道这个要建立的数据集叫 DemoPDV, 有 ID、 Chinese、 Math、English 以及 Sum 五个变量,其中 ID 为字符型。 SAS 给他们建立好 Input Buffer 和 PDV Input Buffer:内存里开辟空间,以便中转数据。 647 | 648 | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 | 1 | 2 | 3 | 649 | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | 650 | | | | | | | | | | | | | | | 651 | 652 | PDV:从 Input 语句或者 SET、 MERGE、 UPDATE 语句获取变量信息,建立好数据变量 653 | 654 | | ID | Chinese | Match | Eng | Sum | _Error_ | _N_ | 655 | | --- | ------- | ----- | --- | --- | ------- | --- | 656 | | | | | | | | | 657 | 658 | 运行阶段: 659 | (1)设置 INPUT 中的变量为缺失 ( 字符变量为空白,数字变量为小数点 ),并设置 660 | 自动变量 _N_=1, _ERROR_=0; 661 | 662 | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 | 1 | 2 | 3 | 663 | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | 664 | | | | | | | | | | | | | | | 665 | 666 | | ID | Chinese | Match | Eng | Sum | _Error_ | _N_ | 667 | | --- | ------- | ----- | --- | --- | ------- | --- | 668 | | | . | . | . | . | 0 | 1 | 669 | 670 | (2) INPUT 语句读入第一条记录, Input Buffer 和 PDV 的状态。方框可以理解为在运行的程序部分; 671 | 开始 INPUT 语句: 672 | ```SAS 673 | data demoPDV; 674 | input ID $ Chinese Math English; 675 | Sum=Chinese+Math+English; 676 | datalines; 677 | S001 80 99 93 678 | S002 90 85 95 679 | S003 83 88 81 680 | ; 681 | run; 682 | ``` 683 | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 | 1 | 2 | 3 | 684 | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | 685 | | S | 0 | 0 | 1 | | 8 | 0 | | 9 | 9 | | 9 | 3 | 686 | 687 | | ID | Chinese | Match | Eng | Sum | _Error_ | _N_ | 688 | | --- | ------- | ----- | --- | --- | ------- | --- | 689 | | | . | . | . | . | 0 | 1 | 690 | 691 | 读入第一个变量 ID: 692 | ```SAS 693 | data demoPDV; 694 | input ID $ Chinese Math English; 695 | Sum=Chinese+Math+English; 696 | datalines; 697 | S001 80 99 93 698 | S002 90 85 95 699 | S003 83 88 81 700 | ; 701 | run; 702 | ``` 703 | 704 | | ID | Chinese | Match | Eng | Sum | _Error_ | _N_ | 705 | | ---- | ------- | ----- | --- | --- | ------- | --- | 706 | | S001 | . | . | . | . | 0 | 1 | 707 | 708 | 读入第二个变量: 709 | ```SAS 710 | data demoPDV; 711 | input ID $ Chinese Math English; 712 | Sum=Chinese+Math+English; 713 | datalines; 714 | S001 80 99 93 715 | S002 90 85 95 716 | S003 83 88 81 717 | ; 718 | run; 719 | ``` 720 | | ID | Chinese | Match | Eng | Sum | _Error_ | _N_ | 721 | | ---- | ------- | ----- | --- | --- | ------- | --- | 722 | | S001 | 80 | . | . | . | 0 | 1 | 723 | 724 | 如此循环,直到最后一个变量 sum: 725 | 726 | ```SAS 727 | data demoPDV; 728 | input ID $ Chinese Math English; 729 | Sum=Chinese+Math+English; 730 | datalines; 731 | S001 80 99 93 732 | S002 90 85 95 733 | S003 83 88 81 734 | ; 735 | run; 736 | ``` 737 | | ID | Chinese | Match | Eng | Sum | _Error_ | _N_ | 738 | | ---- | ------- | ----- | --- | --- | ------- | --- | 739 | | S001 | 80 | 99 | 93 | 272 | 0 | 1 | 740 | 741 | (3)完成所有 DATA 步后续语句, SAS 自动完成输出数据集。 742 | 743 | ```SAS 744 | data demoPDV; 745 | input ID $ Chinese Math English; 746 | Sum=Chinese+Math+English; 747 | datalines; 748 | S001 80 99 93 749 | S002 90 85 95 750 | S003 83 88 81 751 | ; 752 | run; 753 | ``` 754 | | ID | Chinese | Match | Eng | Sum | _Error_ | _N_ | 755 | | ---- | ------- | ----- | --- | --- | ------- | --- | 756 | | S001 | 80 | 99 | 93 | 272 | 0 | 1 | 757 | 758 | 将上面 PDV 里除了自动变量 _ERROR_, _N_ 外,其他变量自动输出到数据集DemoDPV。 759 | 760 | (4)返回 DATA 步第一语句,初始化 PDV。 761 | ```SAS 762 | data demoPDV; 763 | input ID $ Chinese Math English; 764 | Sum=Chinese+Math+English; 765 | datalines; 766 | S001 80 99 93 767 | S002 90 85 95 768 | S003 83 88 81 769 | ; 770 | run; 771 | ``` 772 | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 | 1 | 2 | 3 | 773 | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | 774 | | S | 0 | 0 | 2 | | 9 | 0 | | 8 | 5 | | 9 | 5 | 775 | 776 | | ID | Chinese | Match | Eng | Sum | _Error_ | _N_ | 777 | | --- | ------- | ----- | --- | --- | ------- | --- | 778 | | | . | . | . | . | 0 | 2 | 779 | 780 | (5)开始读入第二条记录的第一个变量 ID: 781 | 782 | | ID | Chinese | Match | Eng | Sum | _Error_ | _N_ | 783 | | --- | ------- | ----- | --- | --- | ------- | --- | 784 | | s002 | . | . | . | . | 0 | 2 | 785 | 786 | (6)如此循环重复,读完最后一条记录的最后一个变量,写入数据集。 787 | 788 | | ID | Chinese | Match | Eng | Sum | _Error_ | _N_ | 789 | | ---- | ------- | ----- | --- | --- | ------- | --- | 790 | | S003 | 83 | 88 | 81 | 252 | 0 | 2 | 791 | 792 | (7)再次返回第一条 DATA 语句,发现已经没有数据可以读取,直到这时,DATA 793 | 步才彻底结束。 794 | 795 | | ID | Chinese | Match | Eng | Sum | _Error_ | _N_ | 796 | | --- | ------- | ----- | --- | --- | ------- | --- | 797 | | s002 | . | . | . | . | 0 | 4 | 798 | 799 | 如何粗略略的验证上述步骤呢?我们可以尝试运行程序 2-22 验证 PDV,看 LOG 窗口给我们的信息提示。 800 | 801 | 程序2-22 验证PDV 802 | ```SAS 803 | data demoPDV; 804 | put "第" _n_ "次运行前: " _all_; 805 | input ID $ Chinese Math English; 806 | Sum=Chinese+Math+English; 807 | put "第" _n_ "次运行后: " _all_; 808 | datalines; 809 | S001 80 99 93 810 | S002 90 85 95 811 | S003 83 88 81 812 | ; 813 | run; 814 | ``` 815 | 816 | LOG 的结果显示: 817 | ```SAS 818 | 第1 次运行前: ID= Chinese=. Math=. English=. Sum=. _ERROR_=0 _N_=1 819 | 第1 次运行后: ID=S001 Chinese=80 Math=99 English=93 Sum=272 _ERROR_=0 _N_=1 820 | 第2 次运行前: ID= Chinese=. Math=. English=. Sum=. _ERROR_=0 _N_=2 821 | 第2 次运行后: ID=S002 Chinese=90 Math=85 English=95 Sum=270 _ERROR_=0 _N_=2 822 | 第3 次运行前: ID= Chinese=. Math=. English=. Sum=. _ERROR_=0 _N_=3 823 | 第3 次运行后: ID=S003 Chinese=83 Math=88 English=81 Sum=252 _ERROR_=0 _N_=3 824 | 第4 次运行前: ID= Chinese=. Math=. English=. Sum=. _ERROR_=0 _N_=4 825 | ``` 826 | 827 | 最后补充说明一下:上面所展示的都是 SAS 默认的、最基础、最简单的运行机制。当 DATA 步有循环、选择语句,有 OUTPUT、 RETAIN 等语句时, SAS 的处理流程会有 828 | 所不同。 829 | 830 | ### 2.5.2 @与@@的困惑 831 | 初学 SAS 者,或多或少都会对 @ 与 @@ 的理解有些吃力。官方对 @ 的说法是:INPUT语句尾部的 @是行保持符,主要作用是保持数据行停留在此行,不要跳到下一行。@ 称为单尾 @, @@ 称为双尾 @,很多情况下,我们连一个 @ 也不用,我姑且称之为无尾。那么什么情况下用无尾、什么情况下用单尾、什么情况下用双尾呢?以下是笔者总结的一些原则: 832 | * 当DATALINES数据行里要读入的数据列数=要读入的变量数,也就是说一行就是一条观测时,无尾。 833 | * 当DATALINES数据行里要读入的数据列数>要读入的变量数,而且是整数倍时,也就是说一行= K*条观测(K为≥1 的整数),用@@。 834 | * 当一个DATA步里有多个INPUT语句时,我们需要单尾@。 835 | 836 | 程序2-23 @与@@示例程序 837 | ```SAS 838 | *=== 数据列数=变量数; 839 | data test1; 840 | input id x y z; 841 | datalines; 842 | 1 98 99 97 843 | 2 93 91 92 844 | ; 845 | run; 846 | 847 | *=== 数据列数=变量数,多个input 语句; 848 | data test2; 849 | input id@; 850 | input x@; 851 | input y@; 852 | input z@; 853 | datalines; 854 | 1 98 99 97 855 | 2 93 91 92 856 | ; 857 | run; 858 | 859 | *=== 数据列数=k*变量数; 860 | data test3; 861 | input id x y z @@; 862 | datalines; 863 | 1 98 99 97 2 93 91 92 864 | ; 865 | run; 866 | ``` 867 | 关于 @、 @@ 与跳行,笔者曾简单总结了如下原则: 868 | 869 | * 无尾Hold不住立即跳。 870 | * 一尾(@) Hold当前INPUTY语句不跳,但若刚好是DATA步最后一个INPUT语句,跳 871 | * 二尾(@@)打死都不跳。 872 | * 最后,无论多少尾,数据行末尾必定自动跳。 873 | 874 | 例如,实例程序 2-24 @ 与 @@ 的辨析中第一个程序,由于 INPUT X 后面有 @,且不是最后一个 INPUT 语句,故读完 X=1 后,不跳行,继续读 Y=2, 由于 INPUT Y 后无尾,立即跳行,故读 Z 时为 Z=4,又因 INPUT Z 后有 @@,虽然这是最后一个 DATA 步的 PUT 语句,不跳,程序返回开头,开始读第二条观测, X=5,不跳, Y=6,跳, Z=7。故最终的结果为两条观测, X, Y, Z 的值分别为: 1, 2, 4; 5, 6, 7。第二个程序,答案是 1,4, 5,各位读者能运用上面的原则得出答案吗? 875 | 876 | 程序2-24 @与@@的辨析 877 | ```SAS 878 | data test; 879 | input x @; /*单个@,能Hold住,读后不跳*/ 880 | input y; /*没有@, Hold不住,读后跳*/ 881 | input z @@; /*两个@, Hold住没问题,但数据行末尾,读后自动跳*/ 882 | datalines; 883 | 1 2 3 884 | 4 5 6 885 | 7 ; 886 | run; 887 | data test; 888 | input x ; /*无@, Hold不住,读后立即跳*/ 889 | input y @@; /*两个@, Hold住,读后不跳*/ 890 | input z @; /*单个@,但是是最后一个INPUT语句,跳*/ 891 | datalines; 892 | 1 2 3 893 | 4 5 6 894 | 7 ; 895 | run; 896 | ``` 897 | 898 | ## 2.6 用好 SAS Help 的秘诀 899 | 很多 SAS 初学者抱怨 SAS 的帮助文档太复杂,难以读懂。其实,真正说起来, SASHelp 文档才是这世界上学习 SAS 最好的教材,对比 R 软件包的 Help 文档, SAS 的文档可以让我们感动到流泪。 900 | 901 | ### 2.6.1 SASHelp知多少 902 | SAS 的帮助文档(SAS Help),窃以为,是市面上所有统计软件里做得最有诚意的作品。 SAS Help 是 SAS 公司投入大量的精力打造的体系最为完整,措辞最为规范,获得最为方便,知识最为权威的 SAS 教材。打开 SAS Help 的官方网站(http://support.sas.com/documentation/),如图 2-16 所示,我们可以感受下那份满满的诚意。 903 | 904 | 我们以最新的 9.4 版本为例,官网上的 Help 文档都可以轻易获得, HTML 或者 PDF任挑(见图2-17),而且 PDF 文档的品质完全可以媲美精美的书籍(见图 2-18),更重要的是,我们都可以免费下载。 905 | 906 | 如果嫌弃 HTML 打开太麻烦, PDF 也懒得去下载,那也没有关系。只要我们在本地安装了 SAS,我们就可以随时在本地查看我们所购买的模块的帮助文档(见图 2-19)。不过需要留意的是,只有购买、安装了某一模块, Help 里才可以查到其相应的文档。 907 | 908 | ### 2.6.2 看懂SAS Help的基本套路 909 | SAS Help 这么好的资源,通常被很多 SAS 教材给忽略了,他们绝口不提这档子事。当然也有一些 SAS 教材强调了多看帮助文档(SAS Help)在学 SAS 时的重要性,但往往另一问题又被忽略了:如何迈出第一步?怎样看懂 SAS Help ? 910 | 911 | SAS Help 里会涉及一些元素和符号,还有一些特定的风格。了解其风格传统后我们再去阅读 SAS Help,就会轻松很多。比如我们看看我们以后将要用到的 ODS 画图过程 PROC SGPLOT 的 Help,就会发现有大小写、有粗细、有斜体,还有 <>, |,…等符号。所有这些,如何理解? 912 | SAS Help 的惯例体系,由四部分组成。 913 | (1)语法成分:包括关键词和参数。关键词通常是语句的第一个单词或者头两个单词(如 PROC 语句、 CALL 列程语句),如 SAS 的过程名,语句名。参数是紧跟关键词后面或者等号后面的常量、变量或者表达式。 914 | (2)风格惯例: SAS Help 里有三种风格,大写粗体、大写以及小写斜体。具体可见图 2-20。其中大写粗体用来标示过程、语句以及函数名称;大写用来标示参数为字符常量;小写斜体用来标示需要用户提供的参数或值。编程时,大写粗体、大写的文字必需原样照抄。 915 | (3)特殊字符:特殊字符包括 <>、 =、 |、…、 “ ”、;等。 916 | * <>,可选用的参数或者选项。 917 | * =,赋值语句。 918 | * |,互斥参数或参数值,只能选择其一,不能同时选择。 919 | * …,重复。 920 | * “”,值必需放在引号中。 921 | * ;,语句结束。 922 | (4) SAS 库及外部文件的引用, libref 以及 fi leref 标示库和文件关联名,用 SASlibrary 和 f le-specif cation 标示外部文件。 923 | 924 | ### 2.6.3 检索SAS Help的小技巧 925 | 如果希望通读 SAS Help,最合适的方式应该是下载 PDF 文件来阅读和做标记。不过更多的使用场景是,对于某个过程或是某个语句,我们记不太清它的语法规则了,这时,我们希望快速查找到其语法参考手册。在 EG和 Studio中,有自动语法补全的提示,在 DMS下,我们可以通过 Help 菜单下的 SAS Help and Documentation 启动 SAS Help 然后进行检索。 926 | 927 | 比如说,我们在导入数据时,对 PROC IMPORT 的语法比较模糊,在检索框里输PROC IMPORT,就可以看到如图 2-21 所示的界面。 928 | 929 | 有时候,很多模块里都包含了检索的内容,需要我们留意下每条检索结果蓝色大写字的下面一行字,这行字给出的是这个链接是属于哪个模块里的语法参考说明。一般对于PROC 步,我个人喜好选择带「Overview:」的那个链接,里面从语法、概览到示例都有,且比较详细(见图 2-22)。此外,在检索时,选择合适的检索词有一定的讲究:检索过程步可以用“PROC ×××”“××× Procedure”;检索语句时可以用“××× Statement”,当然也可以直接用“×××”检索(××× 表示关键词)。 930 | 931 | ### 2.6.4 熟悉SASHelp下的数据集 932 | 为了配合 SAS Help 文档, SASHelp 逻辑库下自带了很多数据集,熟悉这些数据集,有助于我们利用他们做一些简单测试。例如,本书就用了其中的三个常用的数据集,关于其他更多数据库的信息,可以参考帮助文档 SAS Help Datasets。 933 | (1) class 数据集,学生数据集,里面包括了姓名、性别、年龄、身高、体重等信息。 934 | (2) cars 数据集, 2004 年各大品牌汽车基本情况及其售价,包括了品牌、型号、车型、产地、驱动类型、售价、车身情况、速度等信息。 935 | (3) heart 数据集,弗莱明翰心脏研究队列数据,包括生存状态、性别、年龄、身高、体重、吸烟、饮酒以及血压血脂等信息。 936 | 937 | ## 2.7  本章小结 938 | 本章对 SAS 编程里最常用的基本概念,如逻辑库、数据集、 SAS 编程语言、 SAS 运行机制等做了较为详细的介绍。当然,这些介绍仍然比较简略,目的是让大家对 SAS 编程有个框架式的了解,而不是为了替代 SAS 本身的 Help 文档。因此,最后我们详细阐述了 SAS Help 的语法风格,这将有助于我们真正将 SAS Help 查阅起来、用起来。 --------------------------------------------------------------------------------