從程式設計師到資料科學家:SAS 程式設計基礎 (06)- DATA步與PDV
在BASE SAS 中,DATA 語句用於開始一個數據步, 後續為若干DATA步特定的語句;SAS資料步結束於下一個 DATA 步或 PROC 步開始之處,或者結束於後續顯式指定的RUN語句。
注意:DATA 步是SAS程式語言的基礎,它可以執行在多種執行環境中。本章要講的是傳統意義上的SAS DATA步,後面我們會講到下一代DATA步以及DATA步在SAS 雲分析服務環境中執行的情況。千里之行,始於足下!我們還是從傳統 DS 開始吧…
DATA 語句最常見的呼叫方式有如下幾種:
1)DATA 語句可不指定任何引數,則DATA 步將自動建立一個目標資料集 DATAn,其中 n為從1開始不斷增長的唯一整數。
比如下面的SAS程式碼將在臨時邏輯庫 WORK中建立資料集 Data1,包含1行5列資料。再次執行該程式碼時將生成 Data2…依次類推。
DATA;
Name="Leon"; Sex="M"; Age=30; Weight=83.5; Height=175;
run;
注意:這種方式一般用來生成不在乎輸出資料集名稱的時候,但由於每次執行都會在 WORK 中生成一份新的資料,一般不建議這種用法。通常情況下會顯式指定輸出資料集的名字會更好一些。
對於這種臨時資料,我們依然可以使用系統巨集變數 &SYSLAST來跟蹤,比如下面的程式碼用來顯示上一次生成的臨時資料集的名字,並從
data_null_;
put"&SYSLAST";
run;
procdatasetsnolist ;
delete%scan(&SYSLAST,2);
quit;
2)DATA 語句也可以指定資料集名稱 _NULL_ ,表示不輸出任何目標資料集,通常用於純粹的計算邏輯處理。也經常用於除錯SAS程式碼。
3)DATA 語句也可以指定多個輸出資料集,從而實現在一個DATA步裡輸出各種需要的資料。比如下面的程式碼將同時生成兩份內容相同的資料集 data1和data2。
DATA data1 data2;
Name="Leon"; Sex="M"
run;
一般情況下,我們會顯式指定生成一個目標資料集名稱,並且希望放在特定的目錄中。SAS DATA語句在資料處理中可以展現出令人驚異的行為,比如我們希望把 sashelp.class 中兩種性別的資料分成兩個資料集 classF和clasM,可使用下面的程式碼簡潔地實現。
procprintdata=sashelp.class;run;
DATA classF classM;
set sashelp.class;
if sex="F"thenoutput classF; /*Split F to ClassF*/
elseoutput classM; /*Split M to ClassM*/
run;
procprintdata=classF; run;
procprintdata=classM; run;
上一章的最後,我提到資料集中的觀測(記錄)有各種生成方式,下面我們就一起看看在SAS中如何做到。
l利用內嵌資料行建立SAS資料集
一般情況下,如果我們要生成的資料量比較小,且不希望有獨立的資料檔案,我們可以直接將資料嵌在SAS程式碼中,內嵌資料行有幾種不同的方式:
1)最常見的情況是我們有一系列的資料行,資料項之間用空格分隔。這種情況我們可以使用datalines語句配合input語句直接生成。datalines語句表示下一行將包含資料行。由於資料行並非語句,所以它不需要分號。另外,除了 datalines 語句外,我們也可使用lines(datalines語句的別名)或cards語句,三者等價。
libname mylib "c:\temp";
DATA mylib.myclass;
input Name $ Sex $ Age Height Weight;/*$ 表示字元型變數*/
datalines;
Alfred M 14 112.5 69
Alice F 13 84 56.5
Barbara F 13 98 65.3
Carol F 14 102.5 62.8
Henry M 14 102.5 63.5
James M 12 83 57.3
Jane F 12 84.5 59.8
Janet F 15 112.5 62.5
Jeffrey M 13 84 62.5
John M 12 99.5 59
Joyce F 11 50.5 51.3
Judy F 14 90 64.3
Louise F 12 77 56.3
Mary F 15 112 66.5
Philip M 16 150 72
Robert M 12 128 64.8
Ronald M 15 133 67
Thomas M 11 85 57.5
William M 15 112 66.5
RUN;
系統會生成如下資料集,存放在 C:\temp 目錄中。與利用臨時庫 WORK不同,該資料集在你SAS 會話結束後依然存在,存放在 C:\temp\class.sas7bdat 檔案中。
注意:生成到磁碟上的檔名依賴於作業系統的檔案系統,Windows平臺的FAT/NTFS不區分大小寫,而Linux/Unix 則區分大小寫,SAS一律使用小寫字母生成資料集檔案,在Unix/Linux平臺上,如果我們要讀取包含大寫字母的檔名(即資料集名)時,需要啟用系統選項VALIDMEMNAME=EXTEND,並且資料集的名字必須與磁碟上的檔名大小寫完全匹配。
2)如果資料行包含特定的分隔符,我們可以利用infile語句來指向特殊的檔案引用datalines,並且使用引數delimiter=’<分隔符>’ 來指定分隔符。比如:
libname mylib "c:\temp";
DATA mylib.myclass;
infile datalines delimiter=',';
input Name $ Sex $ Age Height Weight;
datalines;
Alfred,M,14,112.5,69
Alice,F,13,84,56.5
Barbara,F,13,98,65.3
Carol,F,14,102.5,62.8
Henry,M,14,102.5,63.5
James,M,12,83,57.3
Jane,F,12,84.5,59.8
Janet,F,15,112.5,62.5
Jeffrey,M,13,84,62.5
John,M,12,99.5,59
Joyce,F,11,50.5,51.3
Judy,F,14,90,64.3
Louise,F,12,77,56.3
Mary,F,15,112,66.5
Philip,M,16,150,72
Robert,M,12,128,64.8
Ronald,M,15,133,67
Thomas,M,11,85,57.5
William,M,15,112,66.5
RUN;
3)如果資料行本身包含分號(注:由於分號是SAS語句的結束符,因此很特別),我們該如何處理呢?我們可以使用 datalines4 語句來完成。也可以使用該語句的別名為cards4 或lines4。datalines4 語句必須使用4個連續的分號開始的一行來標記資料行結束。下面的例子中,字元型列 Name 包含分號,則我們必須以datalines4 來輸入資料。
libname mylib "c:\temp";
DATA mylib.myclass;
input Name $ Sex $ Age Height Weight;
datalines4;
Alfred; M 14 112.5 69
Alice; F 13 84 56.5
Barbara; F 13 98 65.3
Carol; F 14 102.5 62.8
Henry; M 14 102.5 63.5
James; M 12 83 57.3
Jane; F 12 84.5 59.8
Janet; F 15 112.5 62.5
Jeffrey; M 13 84 62.5
John; M 12 99.5 59
Joyce; F 11 50.5 51.3
Judy; F 14 90 64.3
Louise; F 12 77 56.3
Mary; F 15 112 66.5
Philip; M 16 150 72
Robert; M 12 128 64.8
Ronald; M 15 133 67
Thomas; M 11 85 57.5
William; M 15 112 66.5
;;;;
RUN;
4)如果資料行本身是變長(比如字串變數包含空格字元),也就是說資料行參差不齊,那我們如何輸入呢?我們可以使用SAS提供的列指標,用來明確指定資料行中變數的讀取的起止位置,從而正確讀取變長的字串。比如:
libname mylib "c:\temp";
DATA mylib.myclass;
inputName $1-15 Sex $ Age Height Weight;
datalines;
Alfred Liu M 14 112.5 69
Alice Wang F 13 84 56.5
Barbara Deng F 13 98 65.3
Carol Zhang F 14 102.5 62.8
Henry Kissinger M 14 102.5 63.5
James Michalle M 12 83 57.3
Jane Xu F 12 84.5 59.8
Janet Quin F 15 112.5 62.5
Jeffrey Smith M 13 84 62.5
John Albert M 12 99.5 59
Joyce Betty F 11 50.5 51.3
Judy Yang F 14 90 64.3
Louise Bernard F 12 77 56.3
Mary Kushiner F 15 112 66.5
Philip Pebble M 16 150 72
Robert Chu M 12 128 64.8
Ronald Wiese M 15 133 67
Thomas Berryman M 11 85 57.5
William Wu M 15 112 66.5
RUN;
5)有的同學會問,如果我們在一個數據行上包括多個觀測(記錄),我們該如何讀取?SAS在 input 語句上設計了一個特殊的引數 @@,用來告訴SAS 從資料行完整讀取一個觀測後,不要馬上讀入下一個資料行,而是繼續從當前行的緩衝區中讀取資料填充觀測。這為我們節省程式碼檔案的行數非常有用。比如:
libname mylib "c:\temp";
DATA mylib.myclass;
input Name $ Sex $ Age Height Weight @@;
datalines;
Alfred M 14 112.5 69
Alice F 13 84 56.5 Barbara F 13 98 65.3
Carol F 14 102.5 62.8 Henry M 14 102.5 63.5 James M 12 83 57.3
Jane F 12 84.5 59.8 Janet F 15 112.5 62.5 Jeffrey M 13 84 62.5
John M 12 99.5 59 Joyce F 11 50.5 51.3 Judy F 14 90 64.3
Louise F 12 77 56.3 Mary F 15 112 66.5 Philip M 16 150 72
Robert M 12 128 64.8 Ronald M 15 133 67
Thomas M 11 85 57.5 William M 15 112 66.5
RUN;
l基於外部檔案建立SAS資料集
大部分情況下,資料來自於磁碟上的某個外部檔案,而且通常不是一系列的檔案。比如在C:\temp目錄中有如下文字檔案: myclass.txt, 我們怎麼用 DATA 步來讀取呢?
我們不需要datalines語句,而是在 DATA 步內利用infile語句指定該外部檔案(相當於我們將datalines語句下的資料行移入了外部檔案)。然後再用input語句讀入。比如:
libname mylib "c:\temp";
DATA mylib.MyClass;
infile'c:\temp\myclass.txt';
input Name $ Sex $ Age Height Weight;
RUN;
系統將建立一個完整的資料集 mylib.MyClass。
還有一種更加標準的做法是,我們先用 filename 語句定義一個檔案引用 myfile,然後再在infile 語句中使用該檔案引用。程式碼如下:
libname mylib "c:\temp";
filename myfile 'c:\temp\myclass.txt';
DATA mylib.MyClass;
infile myfile ;
input Name $ Sex $ Age Height Weight;
RUN;
就像前面已經提到的一樣,資料行中可能包含說明文字,或者資料的表頭什麼的。這種情況下我們可以在 infile 語句上指定開始讀取觀測的行 firstobs=,同時也可以指定結束讀取觀測的行obs= 用來限定讀入的資料量,通常結果集的總行數為 obs-firstobs+1行。比如下面的程式碼讀入第二行開始的10行資料。
infile myfile delimiter=',' firstobs=2 obs=11;
l簡單驗證生成的SAS資料集
為了驗證我們自己建立的資料集 MyLib.myclass和系統 SASHELP.CLASS資料集的差異,我們可以呼叫 PROC COMPARE 來比較兩個資料集的異同。
proccomparebase=sashelp.class compare=mylib.myclass;
run;
系統顯示兩個資料集基本相同,除了 sashelp.class 有資料集Label資訊,Sex 列寬度為8位元組外,兩個資料集完全一樣。
l通過已有的SAS 資料集生成資料
很多時候我們都是操作已有的SAS資料集,來進行各種操作生成目標資料集。比如對資料集中資料的增刪改查,資料集的排序、合併、分離、轉置等。
1)增加資料行:在資料集尾部增加資料行
DATA OneRow;
Name="Leon"; Sex="M"; Age=30; Weight=83.5; Height=175;
run;
data Class2;
set sashelp.class OneRow;
run;
也可以在資料的頭部增加資料行,只需要改變 SET 語句中的資料集順序即可。
DATA OneRow;
Name="Leon"; Sex="M"; Age=30; Weight=83.5; Height=175;
run;
data myclass;
setOneRow sashelp.class;
run;
procprintdata=myclass;
run;
注意:細心的讀者可能會發現,輸出的資料集中Name有截斷錯誤,原因是在 SET語句時 PDV的初始結構來自於我們建立的臨時資料集 OneRow,而該資料集中變數Name的長度定義不夠,可以通過增加臨時資料集寬度定義來修正。
DATA OneRow;
length Name $8;
Name="Leon"; Sex="M"; Age=30; Weight=83.5; Height=175;
run;
如果需要在特定行處插入資料,可以使用內部計數器 _N_ 作條件實現:
DATA myclass;
set sashelp.class;
if _N_ = 1thendo; /*在第一行後面插入資料*/
output;
Name="Leon"; Sex="M"; Age=30; Weight=83.5; Height=175;
end;
output;
run;
option obs=3; /*列出前三行*/
procprintdata=myclass;
run;
2)刪除資料行:刪除第三行資料
data myclass;
set sashelp.class;
if _N_ = 3thenreturn;
elseoutput;
run;
procsummarydata=myclass print; run;
當然你也可以刪除滿足指定條件的資料行
data myclass;
set sashelp.class;
if Sex = 'M'thenreturn;
elseoutput;
run;
procprintdata=myclass;
run;
2)修改資料行:滿足特定條件時修改變數的值,比如把第三行的Name改為 "Baby".
Data myclass;
set sashelp.class;
if _N_ = 3then name="Baby";
run;
options obs=3;
procprint ; run;
2)查詢特定資料行:僅輸出滿足特定條件的資料行
libname mylib "c:\temp";
data mylib.myclass;
set sashelp.class;
if name="Alfred"thenoutput;
run;
procprintdata=mylib.myclass;run;
l通過 PROC IMPORT 或PROC SQL 生成
SAS 提供PROC IMPORT 和 PROC EXPORT 來將資料匯入/匯出 SAS 執行環境,常用的資料檔案格式為逗號分隔的 CSV 和微軟的電子表格 EXCEL 檔案,我們可以使用如下程式碼簡單完成。
procimport
datafile='c:\temp\class.csv'dbms=csvout=class replace;
getnames=YES;
mixed=NO;
run;
procprintdata=class;run;
procimport
datafile='c:\temp\class.xlsx'dbms=xlsxout=class replace;
getnames=YES;
mixed=NO;
run;
注意:以上例子需要資料檔案 c:\temp\class.csv 和 class.xlsx,你可以用 proc export 進行生成。
PROCEXPORTdata=sashelp.class outfile='c:\temp\class.xlsx'dbms=xlsxreplace;
run;
PROCEXPORTdata=sashelp.class outfile='c:\temp\class.csv'dbms=csvreplace;
run;
注意:很多人根據幫助文件使用 dbms=EXCEL 匯入 EXCEL 檔案時會出現如下錯誤:
ERROR: Connect: 沒有註冊類
ERROR: Error in the LIBNAME statement.
根本的原因是 dbms=EXCEL 和 dbms=xlsx 在 SAS 裡訪問機制不同,前者需要安裝 ACE 引擎才能工作,而預設情況下我們並沒有安裝它。這是一個令很多程式設計師困惑的技術陷阱。
PROC SQL 讓 SAS 可以呼叫結構化查詢語言 SQL 進行資料操作,廣泛用於關係資料庫管理系統的資料表和檢視的增刪改查,但SAS 技術更高一籌,也可以對 SAS 資料集進行標準的 SQL操作。主要功能包括:建立資料表和資料檢視,對資料列作索引;查詢儲存在資料表和資料檢視中的資料;增刪改資料行和增刪改資料列本身;將資料庫支援的 SQL 語句到資料庫管理系統中進行資料查詢。另外 SAS 也支援將 SQL 查詢結果置入 SAS 巨集變數中,進行資料傳遞功能。PROC SQL 功能非常強大,下面僅列出兩個簡單的例子:建立/查詢資料表。
1)基於已有的資料集建立新的資料集,沒有使用 DATA 步。
libname mylib 'c:\temp';
procsql;
createtable mylib.myclass as
select Name, Sex, Age, Height, Weight format=best.
from sashelp.class;
procprintdata=mylib.myclass; run;
2)使用標準的 SQL 語言建立資料集,如果 mylib 指向某個資料庫管理系統,則會在該資料庫中建立對應的資料表。
libname mylib 'c:\temp';
procsql;
createtable mylib.myclass( Name char(8), Sex char(1), Age num, Height num, Weightnuminformat=best.format=best.);
insertinto mylib.myclass
values('Leon','M',31,175,80)
values('Jim', 'M',30,173,75);
title'Table mylib.myclass';
select * from mylib.myclass;
procprintto; run;
DATA 步的執行機制
前面的例子讓我們看到SAS在處理資料非常方便,但這依然不夠,我們需要深入探索SAS的DATA步是怎麼工作的——即我們需要深刻理解SAS DATA 步的執行機制,這是SAS程式設計的核心內容之一。
首先需要指出的是,SAS語言是按步進行編譯執行的,所以SAS程式與大多數編譯型計算機語言程式一樣,總體上要經過編譯和執行兩個階段。簡要流程如下圖所示:
編譯階段
編譯階段SAS 主要做兩件事:
1)掃描DATA 步內的每一行語句,a) 執行語法檢查:掃描程式碼片段,檢查是否存在於語法錯誤。常見的語法錯誤包括關鍵字缺失或拼寫錯誤、無效變數名稱、標點符缺失或者無效、以及無效的引數或選項等。b)標識每一個變數的名稱,型別和長度等資訊,並且判斷是否需要為後續變數引用作型別轉換等。
2)為程式執行建立必要的資料結構,包括輸入緩衝區IB(INPUT BUFFER)、程式資料向量PDV(Program Data Vector)和輸出資料集描述資訊 DI(Descriptor Information),其中輸入緩衝區 IB只在從外部讀取原始資料檔案時才是建立。
a)輸入緩衝區IB:當DATA 步內執行INPUT語句是從原始資料檔案(Raw Data,比如前面例子中的外部文字檔案)中讀取觀測(記錄)時,SAS會在記憶體中分配一塊邏輯區域作為緩衝區,作為將資料放入PDV之前的臨時緩衝區存在。如果是使用 SET語句來讀取SAS資料集時,SAS則將資料直接拷貝到PDV中,而不需要所謂的輸入緩衝區IB。
b)程式資料向量PDV:DATA步每讀入一行資料時,都需要在記憶體中分配一個邏輯區域,用於存放資料集的變數和計算變數(即計算列)資訊。其資料來自於輸入緩衝區IB或SAS 執行語句。
另外,PDV中還包含2個僅用於系統處理階段的臨時變數,它們不會被寫入目標資料集:
l行計數器 _N_:用來對DATA 步的每次處理進行迴圈計數,從1開始;
l錯誤標誌 _ERROR_:用來標記執行過程中由於資料錯誤引起的錯誤;預設值是0,表示沒有錯誤,否則為1,表示有一個或者多個錯誤。
c)輸出資料集描述符資訊
SAS為每一個輸出資料集建立和維護的元資料資訊,包括資料集屬性和變數屬性。比如資料集名字,成員型別,建立日期,建立時間,觀測數,變數名稱,型別(字元型/數值型)等。
讓我們考察如下程式碼的編譯過程:
/*利用系統 SASHELP.CLASS資料,建立體質指數 BMI,俗稱肥胖指數*/
data myclass;
set sashelp.class;
where age> 12 and sex='男';
BMI=(weight * 0.4535924) / ((height*2.54/100) **2);
format BMI 4.1;
drop weight height;
run;
對於如上程式碼,編譯時 PDV 的變化依次如下圖右側所示。從上到下各行語句編譯時會修改PDV,比如DROP 語句的作用是告訴SAS PDV中哪些變數不需要輸出到輸出資料集,對那些變數設定“刪除”標誌,以免執行時輸出到目標資料集中。
編譯到 RUN 語句時,DATA步編譯宣告結束。SAS會為需要輸出到目標資料集的那些變數,建立必要的描述符資訊——包括資料集中各列的名稱、型別、長度、輸出格式、標籤等。
執行階段
一旦編譯成功,執行階段開始。SAS 的執行主要有如下步驟:
1)SAS 首先會從DATA語句處開始執行,如果是第一次執行,SAS會設定內部變數_N_=1和_ERROR_=0,否則會對內部計數器 _N_自動加1。
2)SAS會以缺失值對程式資料向量(PDV)中那些由INPUT語句和賦值語句建立的變數進行初始化。注意:那些以 SET, MERGE, MODIFY或 UPDATE語句讀取的變數並不會被重置為缺失值。
3)預設情況下,SAS從外部原始資料檔案中讀入一條資料記錄到輸入緩衝區IB中,然後建立對應的PDV,或者直接從SAS資料集中讀入一個觀測到程式資料向量PDV中。SAS語句 INPUT和 SET、 MERGE、 MODIFY、UPDATE都可以用來讀入一條記錄。
4)對當前記錄,執行後續的SAS程式語句,包括賦值、計算和更新等等。
5)當執行到 DATA 步的最後時,隱含的SAS語句 OUTPUT,RETURN和RESET 自動觸發。SAS將當前記錄作為一個觀測寫入到SAS資料集中。執行自動返回到DATA 步的開始處進入下一迴圈。如果SAS 讀取外部檔案結束或SAS資料集結束,則整個 DATA 步執行終止,進入後續的DATA或PROC步的編譯執行。
讓我們考察前面程式碼的執行過程,其中DATA 步中的新變數(如體質指數BMI)首先也會以缺失值 . 進行初始化,隨後在執行賦值語句時對錶達式重新求值,賦給那些新變數;然後執行下一行語句…在 DATA 步的最後,SAS會將PDV中沒有刪除標誌的非臨時變數(除了 height, weight)和值寫入到目標資料集中。然後控制流程再返回到 DATA 步的開始處進入下一個迴圈。正是SAS DATA步這種獨特的隱性迴圈設計,為使用者在進行資料處理時提供了自然的迴圈概念,從而讓使用者對具體檔案的I/O讀寫細節不必過於關注。
當SAS進入下一次迴圈時,臨時變數 _N_ 計數器會自動加1,對於非INPUT語句讀入的情況,SAS 也會保留上次讀入到 PDV中的變數值,直到被新讀入的觀測所覆蓋;對於DATA步中的新變數(如體質指數 BMI),SAS會重新使用缺失值 (.) 進行初始化。當資料被再次讀入時(比如從上面的SET語句所指定的源),SAS會將源資料集中的第二個觀測讀入到 PDV中,並重新計算程式中的新變數 BMI。然後在DATA步的最後,使用 PDV中的值作為第二個觀測(記錄)寫入到輸出資料集中。然後控制再次回到DATA步開始,一直迴圈執行,直到源資料集中的所有觀測都被處理完畢。
結語:本章我們學習了利用SAS建立資料集的幾種靈活方法,然後介紹了SAS DATA 步執行機制的奧祕。深刻理解DATA步的編譯執行機制對掌握SAS 程式設計至關重要,靈活使用SAS DATA步可以為分析準備任何形式的待分析資料。
下面筆者就以如何生成前20個黃金分割數列的簡單SAS程式來結束本章的學習! 黃金分割數列即斐波那契數列,該數列中後一個數與前一個數的比例越往後越接近於黃金比例(1+√5)/2 ,此數列分佈表現出極致的均衡與和諧之美;其前8個數為:1 1 2 3 5 8 13 21…
/*生成前20個黃金分割數列到資料集 WORK.FbNC 中*/
data Fbnc;
do n=1to20;
if n=1 or n=2then y=1;
else y= y1 + y2;
y2=y1; y1=y;
output;
put n= y=; /*列印到SAS日誌*/
end;
drop n y1 y2;
run;
procprintdata=fbnc;run;
輸出:
n=0 y=1
n=1 y=1
n=2 y=2
n=3 y=3
n=4 y=5
n=5 y=8
n=6 y=13
n=7 y=21