摘自:SAS入门之三:数据管理
 
前面说过,SAS语言是一种专用的数据管理、分析语言,它提供了很强的数据操作能力。这些数据操作能力表现在它可以轻易地读入任意复杂格式的输入数据,并可以对输入的数据进行计算、子集选择、更新、合并、拆分等操作。另外,SAS系统 还提供了用来访问其它数据库系统如Sybase、Oracle的接口,访问各种微机用数据库文件如FoxPro、Excel的接口及向导,并提供了一个 SQL过程来实现数据库查询语言SQL的功能。
SAS语言直接、间接用于数据管理的语句很多,这里只能拣最常用的介绍。

SAS数据步的运行机制

SAS语言的自编程计算功能主要在数据步实现,一个SAS数据步相当于一个单独运行的程序。但是,SAS语言又是一个专用数据处理语言,所以SAS数据步有其它语言所没有的特点。我们以下面的简单例子说明这一点:
data a;
put x= y= z=;
input x y;
z=x+y;
put x= y= z=;
cards;
10 20
100 200
;
run;
运行后在LOG窗口显示如下记录:

……

X=. Y=. Z=.
X=10 Y=20 Z=30
X=. Y=. Z=.
X=100 Y=200 Z=300
X=. Y=. Z=.
NOTE: The data set WORK.A has 2 observations and 3 variables.
……
这个程序的运行流程是这样的:
  • DATA语句标志了数据步开始,并指定了数据步结束时要生成的数据集名字为A(实际是WORK.A)。
  • 第一个PUT语句要输出变量X、Y、Z的值但它们还都没有定义,所以LOG窗口的结果显示为三个缺失值。
  • 下面是INPUT语句,它从CARDS语句后面的数据行中读取变量X的值10,变量Y的值20。
  • 下一个赋值语句计算变量Z的值得到30。因此,LOG 中的第二行输出显示三个变量的值分别为10、20、30。
  • 从CARDS语句开始到空分号行的各行是非执行的,程序运行到RUN语句,发现这是本数据步的最后一个语句,按一般的程序语言的规则,程序到这里就应该结束了,但是,SAS是一个专用数据处理语言,如果按一般语言的规则,程序中的第二行数据(100 200)就不能被读入。所以,这个程序运行到RUN语句后,把读入的观测(这是第一号观测)写入输出数据集,
  • 又返回到DATA语句后的第一个可执行语句开始执行,并先把所有的变量置初值为缺失值。于是,第一个PUT语句的结果显示三个变量均为缺失值,而不是上一步的10、20、30。
  • 下一个INPUT语句从数据行中读入下一个观测,把变量X 、Y赋值100、200。读取位置由运行时设置的一个数据指针指示。然后计算变量Z的值得300。于是PUT语句输出的X、Y、Z值分别为100、200、300。
  • 然后,运行控制跳过CARDS语句到空语句,到数据步结尾,把第二号观测输出到数据集,
  • 再返回到数据步开头,把变量值赋初值为缺失值,所以第一个PUT语句输出的三个变量值为缺失值。
  • 然后运行到INPUT语句,应该读入下一个观测,但是查询数据指针发现已经读完了所有数据,所以本数据步结束,并把两个观测写入数据集WORK.A中。
提交PROC PRINT;RUN;就可以显示此数据集的内容如下:
                                   OBS     X      Y      Z
 
1 10 20 30
2 100 200 300

从 这个例子可以看出SAS数据步程序和普通程序的一个重大区别:SAS数据步如果有数据输入,比如用INPUT、SET、MERGE、UPDATE、 MODIFY等语句读入数据,则数据步中隐含了一个循环,即数据步程序执行到最后一个语句后,会返回到数据步内的第一个可执行语句开始继续执行,直到读入 数据语句(INPUT、SET、MERGE、UPDATE、MODIFY等)读入了数据结束标志为止才停止执行数据步,并把读入的各个观测写入在DATA 语句中指定的数据集。如果没有数据输入而只是直接计算,则数据步程序不需要此隐含循环。数据步因为有这样一个隐含循环,所以也提供了用来查询某一步是第几 次循环的特殊变量 _N_,它的值为数据步循环计数值。 数据步流程见图 1。
图 1 数据步流程图

用INPUT语句输入数据

在数据步中输入数据可以从原始数据输入,也可以从已有数据集输入。从原始数据输入要使用INPUT语句来指定输入的变量和格式。数据行写在CARDS语句和一个只有一个顶头的分号的行之间。
最简单的INPUT语句使用自由格式:按顺序列出每个观测的各个变量名,中间用空格分开。变量如果是字符型的需要在变量名后面加一个$符号,$符与变量名可以直接相连也可以隔一个空格。例如:
data c9501;
input name $ sex $ math chinese;
cards;
李明 男 92 98
张红艺 女 89 106
王思明 男 86 90
张聪 男 98 109
刘颍 女 80 110
;
run;
注意这个例子的数据有五个观测,四个变量,每行数据的各变量之间用空格分隔。为输入这些数据,INPUT语句中依次列出了四个变量名,并在字符型变量NAME和SEX后加了$符。要生成一个数据集这是最简单的写法。
使用自由格式也有一些限制条件,如果不满足这些条件时需要改用其它输入格式:
  • 数据每行为一个观测,各数据值之间用空格或制表符分隔
  • 无论是字符型还是数值型缺失数据都必须用小数点表示
  • 字符型数据长度不能超过8个字符,不允许完全是空白,中间不允许有空白,开头和结尾如果有空白将被忽略
  • 在INPUT语句中必须列出观测中的每一项数据对应的变量名而不能省略中间的某一个
在满足以上条件时就可以使用自由格式,它也有明显的优点:使用简单;输入数据时不必上下对齐;不需要知道每个变量的具体列数而只需知道它的次序。
如果各数据行的各个数据项是上下对齐的,还可以使用INPUT语句的列方式。这时,除了在INPUT关键字后面列出变量名外,还需要在每个变量名(及$符)后面列出该变量在数据行中所占据的列起始位置与结束位置,比如上面的例子可以改写成:
data c9501;
input name $ 1-10 sex $ 11-13 math 14-16 chinese 18-20;
cards;
李明 男 92 98
张红艺 女 89 106
王思明 男 86 90
张聪 男 98 109
刘颍 女 80 110
;
run;
使用列方式时一定要正确数出每一项所占的位置。列方式有如下特点:
  • 要求数据行各项上下对齐
  • 各项之间可以没有任何分隔,连续写在一起
  • 字符型数据长度可以超过8个字符,中间可以有空格,头尾的空格仍将被忽略。
  • 不论字符型变量还是数值型变量如果指定列位置都是空白则输入值为缺失值。小数点仍表示数值型和字符型变量的缺失值。
  • 可以只输入数据行中的某些项而忽略其它项。
列方式不要求数据项之间分开,所以经常用来输入紧缩格式的数据。比如,我们要输入一批×××号码,但只输入其中的出生年、月、日信息,就可以用如下程序:
data pids;
input year 7-8 mon 9-10 day 11-12;
cards;
110103751209223
110101690215005
;
run;
列格式可以与自由格式混用,见1.1.3的例子。
如果需要完全原样地输入字符型数据(包括头尾空格、单独的小数点),可以用有格式输入,即在字符型变量名和$符后加上一个输入格式如CHAR10.表示读入10个字符。
有特殊格式的数据需要用有格式输入,即在变量名后加格式名。其中最常见的是用来输入日期。数据中的日期写法经常是多种多样的,比如1998年10月9日可以写成“1998-10-9 ”,“19981009”,“9/10/98”等等,为读入这样的日期数据就需要为它指定特殊的日期输入格式。另外,日期数据在SAS中是按数值存储的,所以如果要显示日期值,也需要为它指定特殊的日期输出格式。例如:
data a;
input date yymmdd8. sales;
format date yymmdd10.;
cards;
56-6-13 1100
67.12.15 1200
78 10 2 1300
891001 1400
19960101 1500
20020901 1600
;
run;
proc print;run;
其中日期数据占据8列位置,如果不满8列要用空格补充,不能让后面的数据进入这8列。这样可以输入没有世纪数,年、月、日之间用减号、小数点、空格分隔的日期,可以输入YYMMDD 格式的六位数的日期(一位数的月、日前面补0),可以输入带世纪数的YYYYMMDD格式的日期(一位数的月、日前面补0)。FORMAT语句规定输出日期变量时使用的显示格式。结果为:
                                   1     1956-06-13     1100                 
2 1967-07-11 1200
3 1978-10-02 1300
4 1989-10-01 1400
5 1996-01-01 1500
6 2002-09-01 1600
用YYMMDD10.输入格式可以输入带世纪数的中间有分隔符或无分隔的日期,如:
data b;
input date yymmdd10. sales;
format date yymmdd10.;
cards;
56-6-13 1100
67.12.15 120078 10 2 1300
891001 1400
19960101 1500
20020901 1600
1956-6-13 1100
1967.12.15 1200
1978 10 2 1300
19891001 1400
19960101 1500
20020901 1600
;
run;
proc print;run;
如 果日期变量不是第一个,则它的前一项最好使用列格式并且指定结束列号为日期值的前一列,或者前一项也使用指定输入格式的输入方法,并且使前一项的输入域宽 占满日期前的列。如果用自由格式则当前一项与日期数据之间间隔超过一个空格时有可能导致日期读入时对不准位置。如果数据是按列对齐的,还可以在日期变量前 加上“@开始列号”说明日期变量开始读取的位置,比如:
data b;
input sales @15 date yymmdd10. ;
format date yymmdd10.;
put date=;
cards;
1100 56-6-13
1200 67.12.15
;
run;
数据中日期是从第15列开始的。如果在上面去掉“@15”,则读入的DATE变量为缺失值。但是如果把日期值与前一个数据值只隔开一个空格则可以用自由格式。
有格式读取还可以与自由格式结合起来使用,在INPUT语句中指定"变量名 : 格式" 表示按位置读取当前第一个非空列开始的值并用指定的输入格式转换,比如,中间有日期又没有对齐时就可以用下例这样的方法读取:
data b;
input sales date : yymmdd10. ;
format date yymmdd10.;
put date=;
cards;
1100 56-6-13
1200 67.12.15
;
run;
INPUT语句有十分复杂的使用方法,可以处理几乎是任意复杂的数据输入问题,这里我们就不详细讲了,有兴趣的读者可以参考《SAS系统-Base SAS软件使用手册》。

读入外部数据

一、文本格式的数据文件
对 于小量的数据,用CARDS语句和空语句把数据夹在中间放在数据步程序中就可以用INPUT 语句输入数据。如果数据量很大(有时可以有上亿行、几千列),直接把数据放在程序中不利于程序和数据的维护。这时,一种办法是把原始数据放在一个普通的文 本格式的文件中,然后用INFILE语句指定输入文件名。例如,我们可以把1.1.3中的数据行单独生成一个文本文件stud.txt,假设放在了C:\ SAS中,可以用如下程序读入文件中的数据并生成数据集:
data c9501;  
infile 'c:\sas\stud.txt';
input name $ 1-10 sex $ math chinese;
run;
proc print;run;
注意INFILE语句要写在INPUT语句之前,有INFILE语句就不再有CARDS语句和空语句。INFILE 关键字后面跟的是一个包含文件名的字符串,可以使用全路径名,如果只有文件名则在当前工作目录寻找。
二、微机格式的数据文件
SAS 还可以读入其它格式的文件,比如FoxPro、Excel等微机格式数据文件。这样的读入不必编程,而可以使用SAS系统File菜单中的Import命 令完成。虽然这不是SAS语言的内容,但因为实际工作中我们经常会需要读入这样的数据文件,所以我们在这里加以简单介绍。
新版SAS提供了一个用于把其他格式的数据文件转换为SAS数据集的导入向导(import wizard) 。在SAS/Base的支持下可以转换用分隔符分隔的文件,用逗号分隔的文件,用制表键分隔的文件,用户自定义的格式。在SAS/ACCESS关于PC文件的接口的支持下可以转换dBase,Excel ,Lotus文件等特殊格式的文件。导入向导可以从程序编辑窗口启动。例如,我们有一个Excel5 文件(如果是Excel97文件需要在Excel中另存为Excel 5.0/95格式)在 C:\LDF\SAS\NEWBOOK\C9501.XLS中(见图 2):
为了由它转换得到SAS数据集,在程序窗口中启动File菜单,选“Import”,这时出现一个选择文件类型的画面:

选中标准文件格式(Standard file format),并单击向下箭头打开一个下拉列表,从中选“Excel 5 or 7 Spreadsheets(*.xls)”,按Next钮继续,出现一个选择文件名的画面,可以在文本框中直接输入Excel文件的全路径名,或按Browse 钮从目录中选取文件。继续后出现选择目标位置的画面,这是要求输入一个结果数据集的名字和数据库位置,数据库已选WORK我们可以不变,在数据集名处输入C9501A,按Finish钮可以生成数据集WORK.C9501A。
三、与大型数据库的接口
SAS提供了两种办法可以访问大型数据库。SAS/ACCESS可以直接连接Oracle、Sybase 、SQL Server等大型数据库。为了访问储存在这些数据库中的表,需要对数据库中的表在SAS 中建立访问描述文件(access descriptor),和视图描述文件(view descriptor)。例如,在数据库服务器DBIN中有一个数据库Finance,其中有一个表Sales,用户名guest用密码anyone 可以访问此库,就可以用以下程序在SAS中建立访问描述文件和视图文件:
PROC ACCESS DBMS=SYBASE;
CREATE sasuser.sales.ACCESS;
SERVER='DBIN';
DATABASE='Finance';
TABLE='Sales';
USER='guest';
PASSWORD='anyone';
CREATE sasuser.salesall.VIEW;
SELECT ALL;
RUN;

其中大写的部分是固定的。这段程序首先生成了访问描述文件SASUSER.SALES.ACCESS,然后由此访问描述文件生成了视图文件SASUSER.SALESALL.VIEW。在SAS中视图文件和数据集的使用是一样的,可以使用数据集的地方都可以使用视图文件。
对 于SAS没有直接支持的数据库管理系统,我们在MS Windows下总可以使用ODBC数据库接口来连接到数据库,这要求我们在安装SAS时安装了ODBC驱动。为了访问外部数据库,首先要在计算机上安装 该数据库的客户端驱动程序,然后在Windows的控制面板中打开ODBC的控制,新建一项ODBC数据源,输入该数据库管理器的名字,数据库名。然后, 使用PROC SQL程序来建立视图我们以前的数据库为例,假设建立了ODBC数据源finodb:
PROC SQL;
CONNECT TO ODBC (DSN='finodb' UID='guest' PWD='anyone');
CREATE VIEW sasuser.sales2 AS
SELECT * FROM CONNECTION TO ODBC (
SELECT * FROM Sales );
QUIT;

其 中的CONNECT TO语句用来建立到数据库的连接,后面的CREATE VIEW语句建立一个视图,但是图的数据来源是ODBC数据源,所以AS后面是关键字SELECT * FROM CONNECTION TO ODBC ,然后在括号中给出了具体的从外部数据库中取得一个查询结果的SQL语句。这个SQL语句可以用来取表的一个子集构成视图。生成的 SASUSER.SALES2是一个SAS视图,在访问时将用保存的连接参数临时去访问外部数据库来得到数据。
这种使用PROC SQL直接连接外部数据库的方法也适用于非ODBC的数据源。

数据集的复制与修改

前面讲述了如何从原始数据生成SAS数据集。我们还可以用SET语句把一个已有数据集复制到一个新数据集,同时还可以进行修改。
比如要把数据集WORK.C9501复制为数据集SASUSER.CLS,只要用如下程序:
data sasuser.cls;
set c9501;
run;
这样的程序流程中也有一个隐含循环,程序在数据步内反复循环,直到输入数据集C9501 最后一个观测读过。
我们还可以用SAS程序语句对生成的数据集进行修改。比如,我们把超过100分的语文成绩都改为100分,就可以用如下程序:
data c9501a;
set c9501;
if chinese>100 then chinese=100;
run;
当然,这种修改也可以在读入原始数据的数据步中使用而不限于使用SET的数据步。也可以生成新的变量。
在数据步中可以用KEEP语句或DROP语句指定要保留的变量或要丢弃的变量。比如,
data c9501b;
set c9501;
keep name avg;
run;
生成的数据集C9501B只包含NAME和AVG两个变量。用KEEP语句指定要保留的变量。用DROP 语句指定要丢弃的变量,比如上例中的KEEP语句可以换成:
  drop sex math chinese;
用这种方法可以取出数据集的一部分列组成的子集。
也可以指定一个条件取出数据集的某些行组成的子集。比如,我们希望取出数学分数90 分以上,语文分数100分以上的学生的观测,可以用如下的“子集IF语句”:
data c9501c;
set c9501;
IF math>=90 and chinese>=100;
run;
注意子集IF语句不同于我们前面所讲的分支语句,它没有THEN部分,只有条件,用于取出满足条件的行子集。

用SET和OUTPUT语句拆分数据集

有时我们需要根据某一分类原则把数据行分别存放到不同的数据集。比如,我们希望把数据集C9501中的所有男生的观测放到数据集C9501M中,把所有女生的观测放到C9501F中,可以使用如下程序:
data c9501m c9501f;
set c9501;
select(sex);
when('男') output c9501m;
when('女') output c9501f;
otherwise put sex= '有错';
end;
drop sex;
run;
proc print data=c9501m;run;
proc print data=c9501f;run;

这 个程序中有两个地方需要注意:在DATA语句中,我们指定了两个数据集名,这表示要生成两个数据集。程序中用SET语句引入了一个数据集,这个数据集的观 测如何分配到两个结果数据集中呢?关键在于OUTPUT语句。OUTPUT语句是一个可执行语句,它使得当前观测被写到语句指定的数据集中。这样,我们根 据SELECT的结果把不同性别分别放到了两个不同数据集中。
OUTPUT 语句还可以用来强行写入数据集而不必象我们在数据步流程图中说明的那样等到数据步最后一个语句完成。数据步中有了OUTPUT语句后数据步流程中不再有自 动写入观测的操作,而只能由OUTPUT语句指定输出。不指定数据集名的OUTPUT语句输出到第一个结果数据集。比如下面的程序生成一个包含1到10的 及其平方的有10个观测的数据集:
data sq;
do i=1 to 10;
j=i*i;
output;
end;
run;
proc print;run;
如果删去上面的OUTPUT语句则结果数据集中只有i=11,j=100的一个观测。

数据集的纵向合并

几个结构相同的数据集可以上下地连接到一起。比如,我们有四个班的学生情况的数据集Class1-Class4,每个数据集包含一个班学生的学号、姓名、性别信息,我们希望把这些数据集合并为一个大数据集,可以用如下代码:
data classes;
set class1 class2 class3 class4;
run;
可见,要把若干个结构相同的数据集合并为一个数据集,只要在DATA语句中指定要生成的大数据集的名字,然后在数据步中使用SET语句并在SET语句中依次列出各小数据集。
有 时我们需要在合并数据集时加入一个变量来指示每一个观测原来来自哪一个小数据集,这可以在SET语句的每一个数据集名后面加一个括号,里面写上IN=变量 名,变量名所给的变量取1表示观测来自此数据集,取0表示观测非来自此数据集。例如,在2.3.5中我们把C9501 数据集按男、女拆分成了C9501M和C9501F两个数据集并抛弃了性别变量,就可以用如下程序连接两个数据集并恢复性别信息:
data new;
set c9501m(in=male) c9501f(in=female);
if male=1 then sex='男';
if female=1 then sex='女';
run;
在数据步中,如果观测来自C9501M,则变量MALE值为1,如果观测来自C9501F则变量FEMALE 值为1,可以使用这两个变量的值定义新变量SEX。用数据集选项的IN=指定的变量不能直接进入结果数据集而只能用于数据步程序中。

数据集的横向合并

两 个(或多个)数据集如果包含了同样的一些观测的不同属性(变量),比如,数据集C9501U包含学生的姓名、性别,数据集C9501V包含学生的数学成 绩,数据集C9501W包含学生的语文成绩,且各数据集的观测是按顺序一一对应的,就可以用如下带有MERGE语句的数据步把它们左右横向合并到一个数据 集NEW:
data new;
merge c9501u c9501v c9501w;
run;
这 样虽然可以横向合并数据集,但是如果各数据集的观测顺序并不一样,就会把不同人的成绩合并到一起。所以横向合并一般应该采用按关键字合并的办法,即先把每 个数据集按照相同的、能唯一区分各观测的一个(或几个)变量排序,然后用BY语句和MERGE语句联合使用,这样即使原来观测顺序不一致也可以保证横向合 并的结果没有错。下例先把C9501数据集横向拆分为包含姓名、性别的数据集C9501X和包含姓名、数学成绩、语文成绩的数据集C9501Y ,然后按关键字横向合并:
data c9501x;
set c9501;
keep name sex;
run;
data c9501y;
set c9501;
keep name math chinese;
run;
 
proc sort data=c9501x;
by name;
run;
proc sort data=c9501y;
by name;
run;
data new;
merge c9501x c9501y;
by name;
run;
proc print;run;
其中的PROC SORT是排序过程,用来把数据集按照某个变量的次序排序(这里是按变量NAME 的次序排列,用BY语句指定排序的变量名)。

用UPDATE语句更新数据集

如果我们发现数据集中的某些数据值有错误或者现在的值已经改变了,我们可以从更正了的原始数据重新生成数据集,或者使用更有效的方法,即建立一个只包含新数据值的数据集,用此数据集修改原数据集。使用如下的DATA步中可以实现数据集的更新:
  DATA  新数据集名;
UPDATE 原数据集 更新用数据集;
BY 关键变量;
RUN;

例如,比如我们发现数据集C9501中王思明的语文成绩实际应该是91分,张红艺性别应为男,可以先生成如下的只包含更正数据值的数据集,不需要改的观测不列入,不需要改的变量不列入或取缺失值:
data upd;
input name $ sex $ chinese;
cards;
张红艺 男 .
王思明 . 91
;
run;
然后,把原数据集C9501和更新用数据集UPD均按姓名(NAME)排序:
proc sort data=c9501;
by name;
run;
proc sort data=upd;
by name;
run;
最后用UPDATE和BY更新得到新数据集NEW,其中王思明的语文成绩改成了91分,张红艺性别改成了男。
data new;
update c9501 upd;
by name;
run;
proc print;run;
但是,这个新数据集中有一个错误:王思明的语文成绩修改以后他的平均分也应作相应改动。所以此例应改为:
data new;
update c9501 upd(in=in_upd);
if in_upd=1 then
avg = math*0.5 + chinese/120*100*0.5;
by name;
run;
proc print;run;