第2章 数据集与DATA步
操纵数据是数据学家的重要工作内容之一,主要用来为数据分析或创建报表准备必要的内容。传统商业智能包括数据仓库建模(Data Warehouse Modeling)、联机分析处理(Online Analytical Processing, OLAP)和数据挖掘(Data Mining, DM)。其中数据仓库建模后的数据准备工作由ETL或ETCL作业完成,具体包括数据的抽取(Extract)、转换(Transformation)、清洗(Clean)和加载(Load)等内容,传统的ETCL在现代商业分析领域已发展为包括各种数据转换处理的交互式数据管理(Data Management)系统,数据处理也是数据科学家除了数据分析之外的日常工作。
SAS组织管理数据的最基本单位是SAS逻辑库(SAS Library)和SAS数据集(SAS Dataset)。细心的读者也许会记得SAS的HelloWorld程序,第一行都是以Data语句开头。那是因为SAS语言就是面向数据分析而设计的专门语言,在SAS的程序世界里数据是分析的基础,它是数据到信息、知识到智能整个分析链条的基石。
2.1 SAS逻辑库
SAS逻辑库是SAS面向数据处理而设计的存储和引用单位,是SAS组织数据的顶级单位。一个SAS逻辑库可以包含若干成员(Member),其中最常用的成员为SAS数据集(SAS Dataset)。SAS逻辑库和SAS数据集的概念可分别对应传统关系型数据库(RDBMS)的数据库和数据表这两个概念,但SAS逻辑库比数据库包含更丰富的数据内容和更灵活的结构。
SAS系统预定义了若干系统逻辑库,每次启动SAS运行环境(也称为建立一个SAS会话)这些系统逻辑库就已经自动为用户建立。根据逻辑库的内容在SAS会话结束后是否存在,可以将逻辑库分为永久库和临时库。一般情况下SAS默认包含5个永久库和1个临时库Work,其中5个永久逻辑库分别为Sashelp、Sasuser以及3个地图专用逻辑库Maps、Mapssas和Mapsgfk。(如图2-1左侧“SAS资源管理器”所示)。
图2-1 SAS系统逻辑库
(1)临时库:在每次启动SAS运行环境的时候,SAS都会建立一个临时逻辑库Work。临时库Work用于在SAS会话期间临时存储和访问数据,当SAS会话结束,即退出SAS运行环境后,临时库Work和它的内容会被SAS系统自动删除。
在Windows平台的SAS环境中,每次启动一个SAS会话,SAS系统会在操作系统临时目录(系统环境变量%TEMP%所指定)下的“SAS Temporary Files”目录中创建一个会话特定的临时路径(比如:C:\Users\sbjyiw\AppData\Local\Temp\SAS Temporary Files\_TD8212_SBJYIW_),这是SAS当前会话存活期间存放各种临时数据的磁盘路径。
(2)永久库:永久库中的数据并不因SAS会话结束而消失,也就是SAS运行结束后存在于永久库的那些数据依然存在于磁盘或数据库服务器。SAS会话中的永久库由SAS启动过程中使用的那个配置文件sasv9.cfg指定。
由于SAS支持几乎所有的语言和编码,在SAS安装环境中sasv9.cfg配置文件有多个,默认提供英文版、支持DBCS的英文版、Unicode版和其他十余种语言特定的版本。它们分别对应SAS安装后在Windows开始菜单中的各启动项。sasv9.cfg也支持使用-config参数指向另一个配置文件sasv9.cfg来重定向系统设置,从而构建多层次的配置文件体系。
Sashelp逻辑库:它是系统预定义用于提供系统初始化后可用数据的系统逻辑库。逻辑库Sashelp在sasv9.cfg中由以下配置被映射到多个目录,表示从这些目录中读取可用的数据集文件,并在SAS系统中随时可用。这样多个目录中的文件在SAS会话中都可使用SASHELP.*的形式进行引用。其中!SASROOT和!SASCFG分别是磁盘上SAS的根路径和配置文件路径。
Sasuser逻辑库:它是系统预定义用来在SAS会话运行期间存放当前用户特定的数据。一般用于隔离不同SAS用户的数据。比如在Windows系统上,多个用户使用安装在同一路径下的SAS软件,各用户对应的Sasuser逻辑库会被分别映射到不同的用户主目录。在以服务器模式运行的SAS环境中,来自客户端的多个用户在服务器上具有不同的映射目录。
逻辑库Sasuser在sasv9.cfg配置文件中是使用-SASUSER指定的,通常对应于操作系统中的用户主目录或者数据库中的特定用户数据库。比如在Windows系统中,-SASUSER被指向“?CSIDL_PERSONAL\My SAS Files\9.3”,对应的操作系统路径为用户目主目录下的某个地址,如C:\Users\sbjyiw\Documents\My SAS Files\9.3。
(3)用户自定义逻辑库:它是用户在SAS程序中用LIBNAME语句建立的若干用户库,用户用它来引用存放于磁盘或者数据库服务器的持久化数据。程序2-1基于系统数据集SASHELP.CLASS创建了用户数据mylib.foo,该存储实体保存在目录C:\temp中。如果希望用户自定义逻辑库在每次启动SAS时都可用,可将Lihname语句放到自动执行的autoexec.sas文件中。
如果数据存储是基于数据库而非文件系统,则SAS访问引擎将会隔离这种复杂性,只需要提供类似ODBC数据访问连接字符串即可。比如连接最常用的SQL Server和Oracle数据库可以使用如下类似代码。其中对于Oracle数据库还要求安装Oracle Database Instance Client客户端访问软件,其PATH键值可以直接写连接串,也可以是C:\Program Files (x86)\Oracle\Instant Client\network\admin\tnsnames.ora文件中的访问入口,如果XXX=为如下第二段path=后面引号内的字符串,则其libname可以简化。
不管SAS运行在Windows操作系统还是UNIX操作系统,你的SAS代码总是可用相同的逻辑库引用名(Library Reference Name)来引用你的数据。逻辑库引用名是当前SAS会话能够识别的逻辑名称,指向某个操作系统能够识别的物理位置或者数据访问引擎的访问入口。SAS逻辑库引用的根本作用是在SAS代码中隔离了操作系统或数据库系统的物理位置,提高了SAS代码的可移植性,也就是说任何数据在SAS代码中引用的时候都是“逻辑库.数据集”,这样就保持了SAS分析代码的一致性和简洁性。
用户在任何需要时可改变某个逻辑库引用的物理指向或者清除已经分配的某个逻辑库引用,比如程序2-2改变逻辑库mylib指向:
程序2-2 改变逻辑库指向或清除逻辑库 libname mylib "C:\windows"; *注意:改变逻辑库 mylib 的指向为 C:\windows;
libname mylib clear; *注意:清除逻辑库 mylib,此语句后 mylib 将不再可用,SAS并不删除C:\temp 目录中任何物理文件, 只切断了SAS中的引用标识而已;
在SAS代码里,如果你想要删除C:\temp目录中的物理文件foo.sas7bdat,可以调用如下语句(见程序2-3):
在Base SAS运行环境中,你可以用鼠标右键单击逻辑库来查看逻辑库的属性,可查看到它所映射的磁盘路径(见图2-2)。
图2-2 SASHELP逻辑库属性
用户也可用如下代码查看Sashelp映射的目录和包含的内容摘要,相当于列出数据库的摘要信息(见程序2-4)。
你也可以使用LIBNAME语句的LIST功能在日志窗口中显示特定逻辑库的粗略信息,也可使用关键字_ALL_列出系统中所有可用逻辑库的映射信息(见图2-3)。
libname mylib list;
图2-3 代码中列出逻辑库信息
libname _ALL_ list;
在日志窗口中将输出如下内容(见图2-4)。
图2-4 查看所有逻辑库信息
2.2 SAS数据集
SAS数据集是一种SAS特定的结构化数据文件,这种表状数据由变量(Variable)和观测(Observation)组成,变量和观测分别对应传统数据库中表的列和行。实际上,变量和观测这两个术语来自统计分析学科,表示对某个事件的属性或特征的连续观测结果。
SAS数据集从结构上看包括描述部分和数据部分。描述部分是数据集的元数据,主要包括数据集的属性信息,包括数据集名称、文件编码、创建日期,观测数目(行数)、变量数目(列数)以及每一个变量的具体属性定义(包括名称、类型、长度、标签、输入/输出格式)等。数据部分就是每一行记录本身。要查看数据集的元数据(见图2-5),可以使用PROC CONTENTS过程步来显示(见程序2-5)。
图2-5 查看数据集的元数据
在SAS代码中,数据集使用两级名称[LIBREF.]DSNAME进行引用,如果是临时库Work中的数据,用户可以省略逻辑库引用名LIBREF,也就说当未指定逻辑库名称时,系统默认使用临时库Work中的数据。
• SAS数据集中的每一列,在SAS中称为变量。变量名称由字母、下划线和数字组成,名称必须以字母或下划线(不能以数字开始)开始。变量名称的长度范围为1~32字节。SAS内部使用大写形式创建变量,因此SAS代码中对变量名称不区分大小写;另外,SAS步中语句需要指定变量列表时,除了可使用减号指定区间外,还可使用冒号:作为通配符,比如var x2-x3;或keep x:;等。
• SAS变量类型包括字符型和数值型,缺省都是8字节长。字符类型的变量值可为1~32767个字节长度的文本,数值型可为2~8任意字节长的整数或浮点数。其中在IBM大型机上运行的SAS支持2~8字节长,而在Windows/Linux操作系统上运行的SAS支持3~8字节长。
在此谈论的SAS变量类型是传统意义上SAS数据集中的数据类型,即DATA步中的变量类型。在本书后面的章节中我们将看到PROC DS2能支持更加丰富的数据类型,但那些数据类型只是在运行时在内存中起作用,一旦将内存数据持久化输出到SAS数据集,则只有定长字符型和数值型两大类数据类型。程序员可能觉得数据类型偏少,但在统计科学中,数据所包含的信息由定类、定序、定距和定比四种变量描述,即变量只包含类别变量(定类、定序)和量化变量(定距、定比)两大类,因此字符型和数值型完全可以覆盖所有的数据类型。相反地,传统编程语言中的字节型、短整型、长整型以及单精度,双精度浮点型更多的是面向计算机存储而设计的数据类型,并不是面向数据分析关注变量本身所包含的信息量而设计的数据类型。
• SAS变量的输出格式用于指定该变量(列)的默认输出格式。这样一来相同的SAS内部存储数据可以根据不同的输出格式生成不同的字符串表达。SAS变量的输入格式用于告诉SAS按照该输入格式从外部数据源读取数据到SAS数据集中。后面一节将详细讲解SAS的输入格式和输出格式。
SAS输入/输出格式
SAS输入/输出格式在表现形式上就是一个包含小数点“.”的特殊文本标记,它不需要用引号括起来。完整的SAS格式定义包括可选的格式前缀$(如果是字符型格式的话)、格式名称(FormatName)、输出宽度w以及数值型变量特有的小数点位数d等信息。SAS输入/输出格式完整语法形式如下:
<$>格式名称<w>.<d>
SAS输入/输出格式是数据的存储表达和SAS内部存储之间的转换桥梁。根据SAS数据类型的不同可以将SAS格式主要分为字符型格式和数值型格式两大类,其中数值类型格式可细分为数值型、货币型、日期/时间/日期时间型3个子类的格式。格式用于从SAS外部输入SAS系统的转换称为输入格式(INFORMAT),相反用于输出SAS变量的称为输出格式(FORMAT)。
字符型格式必须在格式名前面加美元符$(源自英文字符串String的首字母)表示,w表示输出的总宽度;而数值型变量还可指定小数点后的位数d。下面列出一些最常用的格式例子:
• 字符型格式:$w.表示w个字节宽度的字符
• 数值格式:w.d
(1)COMMAw.d表示用逗号做千位分隔符,总长度为w,包含d位小数。
(2)COMMAXw.d表示用“.”作千位分隔符,逗号作小数点,用于部分欧洲国家(如法国)的数值表示,功能与COMMAw.d类似。
• 货币格式:
(1)DOLLARw.d表示变量按美元格式输出:输出以美元符开头,逗号作为千位分隔符,使用标准小数点格式。
(2)EUROw.d表示变量按欧元格式输出:以欧元符号开头,逗号千位分隔符,标准小数点格式。
(3)EUROXw.d与EUROw.d格式相同,但以“.”作千位分隔符,逗号作小数点符号。
运行程序2-6代码,读者可检查输出结果以了解SAS输出格式的作用:
系统输出如下:
char=abcdefghijklmn char=abcdefghijklmn char=abcdefghijklmn w.d= 1234.568 COMMAw.d= 1,234.568 COMMAXw.d= 1.234,568 DOLLARw.d= $1,234.568 EUROw.d= E1,234.568 EUROXw.d= E1.234,568
• 日期格式:
(1)DATEw.比如DATE7.显示英语国家的格式16JAN17。
(2)MMDDYYw.比如MMDDYY10.显示01/01/1960。
(3)YEAR4.显示年份信息,如1960。
(4)NLDATE.显示本地语言格式的日期,此时NL开头的日期格式会根据不同的SAS会话Locale系统选项输出不同的结果,比如在中文SAS上显示1960年01月01日,在法文SAS环境中则按法文Locale的格式显示01 janvier 1960。
• 时间:
(1)TIMEw.比如TIME.显示22:16:27。
(2)NLTIME.显示本地语言格式的时间,如中文SAS上显示22时16分27秒。
• 日期时间:为前面日期和时间的组合
(1)DATETIME.比如DATETIME.显示01JAN60:22:16:27。
(2)NLDATM.显示本地语言格式的日期时间,如1960年01月01日22时16分27秒。
程序2-7显示了SAS代码中日期、时间以及日期时间格式的基本使用。
系统输出:
22JAN17 01/22/17 2017 17:18:31 22JAN17:17:18:31 2017年01月22日 17时18分30秒 2017年01月22日 17时18分30秒
在SAS中有一系列以NL字符开头的国家语言特定的输入/输出格式(如NLDATE.),这些格式在不同的SAS会话中由于系统选项locale的设置不同,因此输出不同。比如在zh_CN locale下输出符合中国简体中文格式的信息,在法文fr_FR locale下则会按法文习惯的格式输出信息。格式化只是纯粹显示数据,它并不转换数据。因此,同样的数值以法郎显示时并不会根据货币汇率进行转换显示(见程序2-8)。
系统输出如下所示:
2017年11月08日 08 novembre 2017
SAS格式为我们在同一SAS程序代码中输出符合不同国家语言和风俗习惯的数据表示成为可能,这是国际化软件公司在将软件产品走向全球化市场时必须提供的产品特性。SAS提供了非常全面的国际化格式支持。
SAS组织数据的基本容器是SAS数据集,更上一级为SAS逻辑库。SAS数据集包含描述数据的元数据部分(即数据表的属性以及列定义信息)以及真正包含数据的观测行组成。不管数据是存在磁盘上,还是存在外部数据库或其他应用系统中,在SAS程序员的世界里都被统一成逻辑库引用进行数据访问。这样用户就根本不必关注数据存储的细节,而是将更多的精力放在数据分析和展现本身上。因此,不管是Oracle数据表还是SQL Server数据表,或是SAS数据集,在SAS程序里都是简单地通过“逻辑库.数据集”(如myoracle.table2或mysqlsvr.table3)两级方式进行引用。
SAS数据集中的观测一般有3种生成方式。
(1)通过DATA步内嵌数据行,或基于已有的SAS数据集或外部数据库系统中的表来生成。
(2)通过PROC IMPORT或者其他面向数据操作的PROC步,如PROC SQL来生成。
(3)通过面向分析的PROC步作为输出或副产品生成,一般是中间临时数据或最终分析结果数据集。
程序2-9生成了一个有代表性的SAS数据集(见图2-6),可帮助理解SAS数据集的逻辑结构。
图2-6 简单数据集范例
2.3 DATA步
在SAS程序中DATA语句用于开始一个数据步,后续为若干DATA步特定的SAS语句。SAS数据步结束于下一个DATA步或PROC步开始之处,或者结束于后续显式指定的RUN语句。DATA语句最常见的调用方式有如下几种。
(1)DATA语句可不指定任何参数,则DATA步将自动在临时逻辑库Work中创建一个输出数据集DATA#,其中#为从1开始不断增长的唯一整数。因此,每次执行代码都会在Work中创建新的数据集。程序2-10 SAS代码将在临时逻辑库Work中创建数据集DATA1,包含1行5列数据。再次运行该代码则将生成DATA2,依此类推(见图2-7)。
图2-7 Work.Datal内容
这种方式一般用在生成临时数据集而且不关心输出数据集的名称时使用,由于每次执行都会在Work中生成一份新的数据,它对应在操作系统磁盘上一个单独的文件存储,因此一般不建议使用这种方法。通常情况下会显示指定输出数据集的名字会更好一些,避免反复执行数据分析任务将磁盘空间耗尽。
这种临时数据集的名称可用SAS系统宏变量&SYSLAST跟踪(见程序2-11),该宏变量用于跟踪上一次生成的数据集名称,并在打印结束后将它从临时逻辑库中自动删除。
(2)DATA语句也可指定特殊名称_NULL_,表示不输出任何目标数据集,此时数据步通常用于纯粹的计算,或出于调试SAS代码的目的,不希望生成默认数据集时通常使用此方法。程序2-12只在日志中打印最近使用的那个数据集的名称,并不生成任何数据集。
(3)DATA语句也可指定多个输出数据集,从而实现在一个DATA步里输出多个数据集。程序2-13可生成两份内容完全相同的数据集data1和data2。
如果我们希望把SASHELP.CLASS中所有数据行分成13岁以下的一组和大于13岁的一组,形成两个数据集classA和clasB,则可使用下面的代码简洁地实现(见程序2-14)。
如果想把数据集纵向上进行分割,如在classA中保留name,age列,而在classB中保留name weight,height列,则可以使用如下灵活方式实现(见程序2-15)。
在数据分析实践中,SAS数据集中的观测有各种各样的生成方式,主要包括下面几种。
2.3.1 内嵌数据行或外部数据文件
1. 利用内嵌数据行创建SAS数据集
一般情况下,如果我们要生成的分析数据比较小,且不希望有独立的外部数据文件,可以直接将数据本身嵌在SAS代码中,内嵌数据行又有如下几种不同方式。
(1)最常见的情况是我们有一系列的数据行,数据项之间用空格分隔。这种情况我们可以使用DATALINES语句配合INPUT语句直接生成。DATALINES语句表示后面的行是数据行。由于数据行并非SAS语句,因此它不需要分号,但该文本是格式敏感的数据。另外,除了DATALINES语句外,我们也可使用DATALINES语句的别名LINES或CARDS语句,它们三者等价。程序2-16代码生成2个字符串变量和3个数值变量的3行数据。
系统会生成如下数据集(见图2-8),存放在C:\temp目录中。与利用临时库Work不同,该数据集在你SAS会话结束后依然存在,存放在C:\temp\class.sas7bdat文件中。
图2-8 由数据生成的MYLIB.CLASS数据集内容
磁盘上的数据文件名称依赖于操作系统的文件系统,如Windows平台的FAT/FAT32和NTFS文件系统,不区分大小写。而Linux/Unix平台则区分大小写,SAS则一律使用小写字母生成数据集文件名。如果我们要读取Unix/Linux平台上包含大写字母的文件名,需要在SAS会话中启用系统选项VALIDMEMNAME=EXTEND并且数据集的名称必须与磁盘上的文件名大小写完全匹配才能正常读取。
(2)如果数据行包含特定的分隔符,我们可以利用INFILE语句来指向系统特殊的文件引用DATALINES,并且使用语句参数DELIMITER='<分隔符>'来指定分隔符。指定分隔符时也可以使用十六进制方式,如DELIMITER='2C'X;它与下面的代码使用逗号分隔符等价(见程序2-17),代码输出结果与程序2-16相同。
(3)如果数据行本身包含SAS语句的结束标志符分号“;”的话,则我们必须使用DATALINES4语句来标记后续数据行,该语句表示后续数据行是用4个连续的分号来标记数据行结束的。DATALINES4也可以使用该语句的别名为LINES4或CARDS4表示。程序2-18中字符型变量NAME的值包含分号,则我们必须以DATALINES4来输入数据。
数据包含引号并且引号中的数据包含字段分隔符本身时,我们需要在INFILE语句上启用分隔符敏感数据选项DSD(DELIMITOR-SENSITIVE DATA, DSD)。此时如果数据行中甚至还包含SAS语句结束符分号,则我们同时需要使用DATALINES4而非DATALINES语句进行标记。下面的代码详细展示了此时如何正确读取数据(见程序2-19)。
读取后的数据中可包括空格、逗号与分号(见图2-9)。
图2-9 读取数据中包含空格和分号
(4)如果数据行本身变长,也就是说数据行参差不齐,其中字符型变量又包含空格字符。此时我们需要使用SAS提供的列指针来明确指定每一个数据行中变量读取的起止位置,从而正确地截取变长的字符串。如下代码对字符型NAME变量明确指示从数据行前15个字节中读取值,而对字符型SEX变量则没有指定列指针,默认读取前一个变量之后到下一个变量之前之间的字符。这种方式一般用于表单化的数据读取(见程序2-20),生成的结果如图2-10所示。
图2-10 读取变长数据
(5)如果在一个数据行包括多个观测,数据呈锯齿状。我们该如何读取呢?SAS在INPUT语句上设计了一个特殊选项@@,用于告诉SAS从数据行完整读取一个观测后不要马上读取下一个数据行,而是继续从当前的数据缓冲区中继续读取数据填充观测。这种灵活设计为读取各种复杂格式的数据,节省SAS代码内嵌数据行数非常有用。比如下面的代码依然可以读取数据行中的锯齿状数据(见程序2-21)。
(6)如果数据集的一个观测来自多行文本,此时需要联合游标控制符#和@来读取数据,它们分别表示对应数据行和列的偏移位置。此时,input语句有若干参数可指定用来接收执行过程中所读取的文件指针信息。下面的代码中数据行格式比较混乱,我们需要一次读取3行文本才能获得一个观测的完整数据(见程序2-22)。
2. 基于外部数据文件创建SAS数据集
大部分情况下,数据来自磁盘上的某个外部文件,而且通常是一系列的文件。比如在C:\TEMP目录中有如下文本文件MYCLASS.TXT(见图2-11),我们怎么用DATA步来读取呢?
图2-11 待读取的文本文件
此时我们不再需要数据行语句DATALINES,而是在DATA步中利用INFILE语句指定该外部文件,相当于我们将DATALINES语句后的数据行移到了外部文件,然后再用INPUT语句读取。假如MYCLASS.TXT是包含数据的文本文件,可用如下4行语句进行读取数据生成了数据集C:\temp\myclass.sas7bdat(见程序2-23),结果如图2-12所示。
图2-12 从外部文件读取的数据集
还有一种更加标准的做法是先用filename语句定义一个“文件引用”myfile,然后再在infile语句中使用该文件引用(见程序2-24)。
很多时候外部数据文件中可能包含说明文字和注释,以及数据的表头信息。这种情况下我们真正读取数据时需要忽略掉这些说明性的内容,此时可在infile语句上可指定读取观测的起始行firstobs=,同时也可以用obs=指定需要读取的观测行数或者限定读取的观测数,此时结果数据集的总行数为obs-firstobs+1行。比如下面的代码读取第2行开始的10行数据。
infile myfile delimiter=',' firstobs=2 obs=11;
如前面章节指出的,如果外部数据文件的编码与当前SAS会话的编码不同,就可能导致读取的数据出现乱码。此时需要我们在SAS代码中明确指定数据源文件的编码格式来正确读取数据,SAS会根据指定的输入编码自动进行数据转码。比如我们要导入的文本文件为UTF-8编码格式的数据,则需要指定如下选项。
infile myfile encoding="utf-8";
3. 验证生成的SAS数据集
数据导入结束后一定要仔细验证结果数据集,除了前面提到的PROC CONTENTS和PROC PRINT外,SAS也提供专门为了比较数据集的过程步。比如为了验证我们自己创建的数据集MYLIB.MYCLASS和系统SASHELP.CLASS数据集的差异,可以调用PROC COMPARE来比较两个数据集的异同(见程序2-25),其中base=选项用来指定基准数据集,compare=选项指定需要比较的数据集。
系统显示两个数据集基本相同,除了SASHELP.CLASS有数据集LABEL信息,SEX列宽度为8字节外,两个数据集完全一样(见图2-13)。
图2-13 SAS数据集比较
2.3.2 通过已有SAS数据集生成
很多时候我们都是操作已有的SAS数据集,此时可通过特定变换来生成目标数据集。比如对别人已经提供的SAS数据集进行增删改查以及排序、合并、分离、转置等操作来生成新的数据集。这种非分析的数据处理在数据工作中也是重要的组成部分。
(1)追加数据行:比如在SASHELP.CLASS数据集尾部追加一行数据生成CLASS2数据集,可使用SET语句进行(见程序2-26),输出结果如图2-14所示。
图2-14 尾部追加数据行
也可在数据头部增加数据行,只需改变SET语句中的数据集顺序即可(见程序2-27),结果如图2-15所示。
图2-15 头部插入数据行
细心的读者可能会发现,输出的数据集中NAME有截断错误,如ALFRED变成了Alfr,原因是SET语句执行时所用的PDV初始结构来自我们创建的临时数据集ONEROW,而该数据集中变量NAME的默认长度定义来自初始值LEON,其长度为4,不足以存储大于4字节的值,我们可以通过显示指定onerow数据集的宽度定义来修正这个问题(见程序2-28)。
如果需要在特定行处插入数据,也可以使用内部行计数器_N_作控制实现。下面的代码在第2行插入一个观测(见程序2-29),结果如图2-16所示。
图2-16 指定位置插入数据行
(2)删除数据行:可以将原始数据集中的某些行数据进行剔除,形成新的数据集。下面的代码剔除了SASHELP.CLASS数据集19行数据中的第3行数据,生成了18行的数据集myclass(见程序2-30)。
当然你也可以删除满足指定条件的数据行,实际用户可以构造出任何复杂的删除逻辑(见程序2-31),结果如图2-17所示。
图2-17 剔除Sex=M数据行
(3)修改数据行:满足特定条件时修改变量的值,如把第3行的Name改为“Baby”(见程序2-32),结果如图2-18所示。
图2-18 修改第3行数据
(4)查询特定数据行:程序2-33仅输出满足特定条件的1行数据。
2.3.3 通过PROC IMPORT或PROC SQL生成
SAS提供PROC IMPORT和PROC EXPORT来将数据导入和导出SAS运行环境,如最常用的数据文件格式为逗号分隔的CSV和微软的电子表格EXCEL文件,我们可以使用如下代码完成数据的导入和导出(见程序2-34)。
导入EXCEL格式的文件,需要指定dbms为xlsx或xls,分别对应不同版本的Excel电子表格数据(见程序2-35)。
上面例子中需要的数据文件C:\TEMP\CLASS.CSV和CLASS.XLSX可通过PROC EXPORT从SAS内置的数据集SASHELP.CLASS生成,代码如下(见程序2-36)。
需要注意的一点是,很多人根据SAS帮助文档导入EXCEL文件时往往使用DBMS=EXCEL进行导入,结果发现数据导入失败并且系统会报告如下错误:
ERROR:Connect: 没有注册类 ERROR:Error in the LIBNAME statement.
根本原因是DBMS=XLSX和DBMS=EXCEL在SAS里访问机制是不同的,后者需要单独安装微软的ACE引擎才能工作,而默认情况下并没有安装它,这是一个令很多程序员感到困惑以为默认情况下SAS连EXCEL文件都不能导入的技术陷阱,其实是参数错误。
PROC SQL是SAS语言中非常强大的过程步,它让用户可以在SAS语言中使用SQL语言对数据进行增删改查操作,广泛用于关系数据库管理系统的数据表和视图的增删改查。其主要功能包括:创建数据表和数据视图,对数据列作索引;查询存储在数据表和数据视图中的数据;增删改数据行或列本身,甚至将某关系型数据库特有的SQL语句发送到数据库管理系统中进行数据查询。另外,SAS也支持将SQL查询结果置于SAS宏变量中,从而实现数据在数据库空间和SAS程序空间的传递功能,这一点非常有用。下面若干的例子来说明PROC SQL的用途,如创建/查询数据表。
(1)基于已有的数据集创建新的数据集,原来的数据集既可以是SAS数据集,也可以是数据库里面的表(见程序2-37)。SAS逻辑库对SAS程序员隐藏了数据库访问的细节,因此非常方便快捷。
(2)使用标准的SQL语言创建数据集(见程序2-38),如果目标逻辑库mylib指向的是某个外部数据库管理系统,则SAS会自动在该数据库中创建对应的数据表。SAS这一强大的数据库隔离功能使数据分析人员不用关心后台数据库到底是怎么存储的,不管是Oracle、DB2还是SQLServer,在SAS看来都是一样的。
(3)PROC SQL语言配合宏变量在SAS中可以实现数据在不同过程步之间的传递。比如下面的代码(见程序2-39)筛选sashelp.class中体重大于平均值的那些人,首先要找到平均体重。
如果要将大于平均体重的学生输出到另一个数据集,我们只需要用多行SQL语句或配合data步生成即可,代码如下(见程序2-40)。
或者如程序2-41:
实际上,除了前面的几种数据生成方式以外,SAS还可以根据数学函数凭空生成分析数据。SAS作为“诗一般的计算机语言”提供了极其灵活的机制,如下面的代码(见程序2-42)可以生成sin(x)和cos(x)函数的坐标数据,再调用SAS的图形过程步绘制函数曲线(见图2-19)。
图2-19 由函数生成数据绘图
2.4 DATA步的运行机制
前一节的例子让我们看到SAS在处理数据时极其方便快捷,但这依然不够,我们还需要深入探索SAS的DATA步的工作原理,也就是需要深刻理解SAS DATA步的运行机制,这是SAS数据步编程的核心内容之一,也是精通SAS编程的分析人员和一般水平分析人员的重要差别;理解DATA步的编译运行机制与下文SAS特有的一个核心概念PDV有关,可以说“平生不识PDV,十年SAS亦枉然”!
首先需要指出的是,SAS语言是按步进行编译运行的,SAS程序与大多数编译型计算机语言程序一样,总体上要经过编译和运行两个阶段。简要流程如图2-20所示。
图2-20 数据步的编译运行
2.4.1 编译阶段
编译阶段SAS主要做两件事。
(1)扫描DATA步内的每一行语句,执行语法检查和变量标识工作。编译器扫描代码片段,检查是否存在语法错误。常见的语法错误包括关键字缺失或拼写错误、无效变量名称、缺失或无效的标点符号、无效的参数或选项等。编译器也会标识每一个变量的名称、类型和长度等信息,并判断是否需要为后续变量引用作必要的类型转换。
(2)为程序执行创建必要的内部数据结构,包括输入缓冲区(Input Buffer, IB)、程序数据向量(Program Data Vector, PDV)和输出数据集描述信息(Descriptor Information, DI),其中输入缓冲区IB只在从外部原始数据文件中读取数据时才会创建,从SAS数据集中读取数据时并不需要建立输入缓冲区。
①输入缓冲区:当DATA步内执行INPUT语句是从原始数据文件(如外部文件)中读取数据记录时,SAS会在内存中分配一块逻辑区域作为缓冲区,作为将数据放入PDV之前的临时缓冲区存在。如果是使用SET语句来读取SAS数据集时,SAS则将数据直接复制到PDV中,而不需要所谓的输入缓冲区IB。
②程序数据向量PDV:当DATA步每读入一行数据时,都需要在内存中分配一个逻辑区域,用于存放数据集的变量和计算列信息。其数据来自于输入缓冲区IB或SAS执行语句;PDV中还包含若干用于处理阶段的临时系统变量,它们不会被写入目标数据集。
• 行计数器_N_:用来对DATA步的每次执行进行循环计数,从1开始;
• 错误标志_ERROR_:用来标记执行过程是否发生错误,默认值为0表示没有错误,否则为1,表示遇到一个或者多个错误。
③输出数据集描述符信息:SAS为每一个输出数据集创建和维护的元数据信息,包括数据集属性和变量属性。比如数据集名字、成员类型、创建日期、创建时间、观测数、变量名称、类型(字符型/数值型)等。
下面我们考察一段SAS代码的编译过程来了解细节,首先需要注意DATA步内的SAS语句分成两种:一种是声明性(DECLARATIVE)语句,用于为SAS提供信息并在编译阶段起作用;另一种是可运行(EXECUTABLE)语句,在DATA步的每一次隐性循环时执行某个动作。
对于下面的例子(见程序2-43),注释是声明性的语句在DATA步内的顺序不那么重要,由于它们在编译时被SAS解析使用,因此并不涉及执行顺序问题。然而,由于某些声明性语句引用的参数可能需要依赖于前面某条语句的编译结果,因此声明性语句依然有潜在的编译顺序问题需要考虑。比如下面的例子中,声明性语句WHERE就由于AGE,SEX变量依赖于SASHELP.CLASS,因此需要把WHERE语句放到可执行语句SET之后,只有编译了SET语句才能在WHERE语句中使用AGE, SEX变量。
上面的代码进行编译,第1行告诉SAS编译器开始一个数据步,输出数据集的名字是myclass;第2行告诉编译器从sashelp.class中读取数据;第3行则告诉SAS编译器只需要读取年龄大于12且为男性的数据;第4行告诉SAS编译器新建一个数值型计算变量BMI,即体质指数Body Mass Index,它和身高体重符合指定的数学计算关系;第5行则告诉编译器设置新建变量的输出格式是长度为4位但保留1位小数;第6行则告诉编译器设置新建变量的文本标签为“体质指数”;第7行则告诉编译器在输出数据集时剔除变量height和weight。一旦数据步编译遇到RUN语句时,SAS步宣告编译结束并自动开始执行代码。
2.4.2 运行阶段
一旦编译成功,执行阶段开始。SAS的执行主要包含如下步骤。
(1)SAS首先会从DATA语句处开始执行,如果是第一次执行,SAS会设置内部变量_N_=1和_ERROR_=0,否则会对内部计数器_N_自动加1,用于执行计数。
(2)默认情况下,SAS从外部原始数据文件中读取一条数据记录到输入缓冲区IB中,然后创建对应的PDV,或者直接从SAS数据集中读取一个观测直接复制到程序数据向量PDV中。SAS会以缺失值对程序数据向量(PDV)中那些由INPUT语句和赋值语句创建的变量进行初始化;DATA步的子语句INPUT和SET、MERGE、MODIFY、UPDATE都可以用来读取一条记录,但那些以SET、MERGE、MODIFY或UPDATE语句读取的变量并不会被重置为缺失值。
(3)对当前记录,执行后续的SAS可执行语句,包括赋值、计算和更新等。
(4)当执行到DATA步的最后时,隐含的SAS语句OUTPUT、RETURN和RESET被自动触发。如果DATA语句包含输出数据集名称,SAS会将当前PDV中的变量作为一个观测写入输出数据集。程序执行自动返回到DATA步开始处进行下一次隐性循环。如果SAS读取外部文件或SAS数据集结束,则整个DATA步执行终止,进入下一个DATA步或PROC步的编译执行过程。
考察前面样例代码的运行过程,执行时最重要的内存结构就是程序数据向量PDV,SAS会为需要输出到目标数据集的那些变量,创建必要的描述符信息——包括数据集中各列的名称、类型、长度、输出格式、标签等。上面例子中PDV在运行时变化简单如图2-21所示。感兴趣的读者也可以在每行语句后面插入语句PUT_ALL_;来查看PDV中各变量的变化过程。
图2-21 DATA步语句与PDV运行时变化
其中DATA步中的新建变量(如体质指数BMI)首先也会以缺失值“.”进行初始化,随后在执行赋值语句时对表达式重新求值,赋给新变量然后执行下一行语句。在DATA步的最后,SAS会将PDV中除了HEIGHT,WEIGHT外没有删除标志的非临时变量及其值写入目标数据集,然后控制流程再返回DATA步的开始处进入下一次循环。
当SAS进入下一次循环时,PDV内部临时变量_N_计数器会自动加1,对于非INPUT语句读取的数据,SAS会保留上次读取PDV的变量值直到被新读取的观测所覆盖。对于DATA步中的新建变量(如体质指数BMI),SAS会重新使用缺失值“.”进行初始化。当运行到读取数据的SAS语句SET时,SAS会自动将输入数据集SASHELP.CLASS中的第二个观测读取PDV并重新计算变量的BMI值;然后在DATA步的最后将PDV中的值作为第二个观测(记录)输出到结果数据集MYCLASS中。最后控制流程再次返回DATA步的开始处,重复执行上面的处理逻辑直到源数据集或外部文件所有数据被处理完毕。
从上面的执行逻辑可以看出,DATA步此时是自带“隐性循环”,原因是代码中用了SET语句。SAS数据步在读取数据集或外部文件时的这种独特隐性循环设计,可为用户处理数据时提供了非常简洁自然的处理逻辑。比如下面几行简洁的代码就可以读取当前计算机的ODBC配置信息,并将每一行显示在SAS日志窗口。代码中看不到循环语句却自带循环处理机制(见程序2-44)。
2.5 DATA步语句快速索引
表2-1列出了SAS DATA步支持的全部子语句和功能描述,第1列类别为D表示该语句为声明性语句,否则为可执行语句。SAS语句在功能上可分为六大类:信息语句、控制语句、动作语句、文件处理语句、窗口语句和其他语句。后面的章节中将会对部分语句进行深入展开。
表2-1 SAS数据步语句和功能描述
(续表)
本章我们先介绍了利用SAS创建数据集的几种灵活方法,随后介绍了DATA步运行机制。深刻理解DATA步的编译运行机制对掌握SAS编程至关重要,灵活使用DATA步可为数据分析之前的数据处理提供各种变换和处理的功能。
下面以一个简单的SAS程序(见程序2-45)结束本章学习,该程序用于生成黄金分割数列的前10项。黄金分割数列又称斐波那契数列,第1项和第2项为1,从第3项开始任一项为前面两项之和;随着项数的增加,后一数与前一数的比例越来越接近于黄金比例。黄金分割数列的分布在平面上呈现出极致的均衡与和谐之美,它以完整铺满整个几何平面;著名的帕斯卡三角式的浅对角线数字之和也刚好构成黄金分割数列(见图2-22)。长宽比为ϕ的矩形被称为黄金矩形,据说古希腊雅典卫城的帕台农神庙的宽度和高度比接近于黄金比例ϕ。
图2-22 黄金分割数列的正方形可螺旋铺满整个平面
系统输出如下(见图2-23)。
图2-23 前10个黄金分割数列
通过修改参数,上面的程序最多能够计算出前1476个黄金分割数列,从第1477位开始将出现计算溢出现象。由于SAS能够保持16位有效精度,因此数列中前78个是毫无精度损失的计算结果。持久化输出到SAS数据集后从第79个到第154个数虽然系统用完整的整数表示,但其精度已经部分丢失,系统从155位开始使用科学计数法表示。因此,严格地说上面的程序只能处理78个精确表示的黄金数列。如果用C语言或Java语言实现上面的逻辑结果会更糟,要解决这种问题需要引入高精度计算机制,后面的章节中会探讨如何生成任意长度,无精度损失的黄金分割数列。