1. 程式人生 > >從程式設計師到資料科學家:SAS 程式設計基礎 (06)- DATA步與PDV

從程式設計師到資料科學家: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,包含15列資料。再次執行該程式碼時將生成 Data2…依次類推。

DATA;

       Name="Leon"; Sex="M"; Age=30; Weight=83.5; Height=175

run;

注意:這種方式一般用來生成不在乎輸出資料集名稱的時候,但由於每次執行都會在 WORK 中生成一份新的資料,一般不建議這種用法。通常情況下會顯式指定輸出資料集的名字會更好一些。

對於這種臨時資料,我們依然可以使用系統巨集變數 &SYSLAST來跟蹤,比如下面的程式碼用來顯示上一次生成的臨時資料集的名字,並從

WORK中刪除它。

data_null_;

put"&SYSLAST";

run;

procdatasetsnolist ;         

delete%scan(&SYSLAST,2);

quit;

2)DATA 語句也可以指定資料集名稱 _NULL_ ,表示不輸出任何目標資料集,通常用於純粹的計算邏輯處理。也經常用於除錯SAS程式碼。

3)DATA 語句也可以指定多個輸出資料集,從而實現在一個DATA步裡輸出各種需要的資料。比如下面的程式碼將同時生成兩份內容相同的資料集 data1data2

DATA data1 data2;

Name="Leon"; Sex="M"

; Age=30; Weight=83.5; Height=175

run;

一般情況下,我們會顯式指定生成一個目標資料集名稱,並且希望放在特定的目錄中。SAS DATA語句在資料處理中可以展現出令人驚異的行為,比如我們希望把 sashelp.class 中兩種性別的資料分成兩個資料集 classFclasM,可使用下面的程式碼簡潔地實現。

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 語句外,我們也可使用linesdatalines語句的別名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 lines4datalines4 語句必須使用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 printrun;

當然你也可以刪除滿足指定條件的資料行

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;

procprinttorun;

DATA 步的執行機制

前面的例子讓我們看到SAS在處理資料非常方便,但這依然不夠,我們需要深入探索SASDATA步是怎麼工作的——即我們需要深刻理解SAS DATA 步的執行機制,這是SAS程式設計的核心內容之一。

首先需要指出的是,SAS語言是按步進行編譯執行的,所以SAS程式與大多數編譯型計算機語言程式一樣,總體上要經過編譯和執行兩個階段。簡要流程如下圖所示:


編譯階段

編譯階段SAS 主要做兩件事:

1)掃描DATA 步內的每一行語句,a) 執行語法檢查:掃描程式碼片段,檢查是否存在於語法錯誤。常見的語法錯誤包括關鍵字缺失拼寫錯誤無效變數名稱標點符缺失或者無效、以及無效的引數或選項等。b)標識每一個變數的名稱,型別和長度等資訊,並且判斷是否需要為後續變數引用作型別轉換等。

2)為程式執行建立必要的資料結構,包括輸入緩衝區IB(INPUT BUFFER)程式資料向量PDVProgram Data Vector)和輸出資料集描述資訊 DI(Descriptor Information),其中輸入緩衝區 IB只在從外部讀取原始資料檔案時才是建立。

a)輸入緩衝區IB:當DATA 步內執行INPUT語句是從原始資料檔案(Raw Data,比如前面例子中的外部文字檔案)中讀取觀測(記錄)時,SAS會在記憶體中分配一塊邏輯區域作為緩衝區,作為將資料放入PDV之前的臨時緩衝區存在。如果是使用 SET語句來讀取SAS資料集時,SAS則將資料直接拷貝到PDV中,而不需要所謂的輸入緩衝區IB

b)程式資料向量PDVDATA步每讀入一行資料時,都需要在記憶體中分配一個邏輯區域,用於存放資料集的變數和計算變數(即計算列)資訊。其資料來自於輸入緩衝區IBSAS 執行語句。

另外,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 MODIFYUPDATE都可以用來讀入一條記錄。

4)對當前記錄,執行後續的SAS程式語句,包括賦值、計算和更新等等。

5)當執行到 DATA 步的最後時,隱含的SAS語句 OUTPUTRETURNRESET 自動觸發。SAS將當前記錄作為一個觀測寫入到SAS資料集中。執行自動返回到DATA 步的開始處進入下一迴圈。如果SAS 讀取外部檔案結束或SAS資料集結束,則整個 DATA 步執行終止,進入後續的DATAPROC步的編譯執行。

讓我們考察前面程式碼的執行過程,其中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