2.4 SAS编程语言

前面几个小节我们基本上都把SAS当作一个软件来进行介绍,辅助性地展示了一些SAS代码,对于初学者,如果没看懂前面的代码没有关系,理解软件层面的概念即可。从这一节开始,我们一起捋一捋SAS作为一门编程语言的基本概念和基础知识。

→2.4.1 SAS程序结构

SAS程序是由一系列SAS语句(statement)组成,所谓SAS语句通常是指以SAS关键字(keyword)开头,始终以分号(;)结束的代码行。最常见的SAS关键字就是「DATA」和「PROC」,因此最常见的语句就是DATA语句和PROC语句。当然,SAS的关键字多如牛毛,我们也不必刻意去死记硬背每一个SAS关键字。在DMS、EG和SAS Studio的编辑器中,SAS都会自动给关键字着成深蓝或者蓝色,EG和SAS Studio还会给出提示,初学者可以尝试看看。

另外,如果从程序块上来讲解,SAS程序可以分为两大块:DATA步和PROC步。所谓一个「步」(step)是指这样的一个程序块。

● 以DATA语句或者PROC语句开头。

● 以RUN语句(大多数情况下)、QUIT语句(部分情况下)、新的DATA语句或者PROC语句结束。

在SAS编辑器中,SAS会自动显示横线以隔开DATA步或者PROC步(见图2-10)。需要留意的是,有些语句只能在DATA步里出现(如INPUT语句),有些语句只能在PROC步里出现(如CLASS语句),有些语句DATA步、PROC步都可以出现(如FORMAT语句),而还有些语句可以既不在DATA步也不在PROC步出现,它们可以单独出现(如前面使用过的LIBNAME语句),此即DATA步语句、PROC步语句及全局语句的概念。

图2-10 DATA步与PROC步

SAS程序除了单独的DATA步和PROC步程序,还有可以把它们打包组合在一起的程序,那就是宏程序,宏程序本质上是文本替代,用更少的文本替代更多的文本。这个话题暂且不做过多介绍,留在后面的第10章进行详细说明。

→2.4.2 SAS语法规则

规则的SAS程序书写风格看起来基本就是被DATA步和PROC步分割的条块,其实SAS程序书写的格式是比较自由的,如果要真正究其语法规则的话,有两方面:①SAS语句语法规则;②SAS名语法规则。

SAS语句语法规则:

● 分隔单词的可以是一个空格或特殊字符(比如加号、等号等运算符),也可以是多个。

● 程序可以在任何列开始,也可以在任何列结束。

● 单个语句可以写在多行,多个语句也可以写在一行。

SAS名是指SAS给其一些语言元素(如逻辑库、数据集、变量以及格式等)的名称标记。SAS名有两类。

(1)SAS系统定义名,如自带的库名WORK、SASHELP等;如特殊的数据集名_NULL_(不创建数据集)、_DATA_(自动数据集名)、_LAST_(最后一个活动数据集);如SAS DATA步的自动变量名_N_(观测号)、_ERROR_(错误标识变量);如特殊的变量列表名_CHARACTER_(所有字符型变量)、_NUMERIC_(所有数字型变量)、_ALL_(所有变量);以及SYS开头的宏变量名如SYSDATE(日期)、SYSVER(SAS版本)等。

(2)用户自定义名,自定义名不能与系统定义名相冲突,且需符合SAS命名的语法规则,总结起来可归纳为以下三点。

● 只能由数字、字母、下划线组成。

● 首字符不能是数字。

● 长度限制各有不同,有的最长可以达32个字符(如变量名,宏变量名),有的最长只能有8个字符(如逻辑库、文件引用名以及引擎名)。

这个命名规则一定要遵守吗?是的,都应该遵守。这个规则能打破吗?可以,但不推荐。不过,有的时候,我们也确实有特殊需求:比如如何打破规则让SAS也可以用中文命名数据集、命名变量呢?这时候,我们可以通过修改系统选项VALIDMEMNAME和VALIDVARNAME的值来实现,如图2-11所示。

图2-11 SAS中文名数据集和变量名

程序2-6 SAS中文名数据集和变量名

    *===中文名数据集;
    *===中文名变量;


    options validmemname=extend validvarname=any;
    data 中文名演示;
          SAS中文变量名="YES";
          SAS中文變量名="YES";
          '2SAS中文变量名'n="YES";
          '2SAS中文變量名'n="YES";
          'SAS空 格变量名'n="YES";
          'SAS空#  @ %格特殊字符变量名'n="YES";
    run;

语法规则只是对编程的合法性给出了最低的要求。在合法性的基础上,我们还应追求语法风格的统一和规范,这样不仅方便自己日后阅读调试,也方便他人审阅,下面是同一段简单的SAS程序,对比左右两边的风格,正常的人类都更愿意看左边的,对吧?编程人士中有一个术语叫Good Programming Practice,GPP,即良好编程实践,很多编程语言都有推荐的编程规范,遵循这些规范,可以极大地方便与同行的交流,笔者自己总结过一些SAS的编程规范,具体可参考附录。

程序2-7 编程风格:规范与凌乱

    *===自建永久库;
    libname    demo  "D:\03  Writting\01
    SAS编程演义\02 Data\Clean";


    *===建永久数据集,demo.不可省略;
    datademo.class_datafile;
        set sashelp.class;
    run;


    *===建临时数据集,work.可以省略;
    data  class_datafile;
        set sashelp.class;
    run;
                                                    libname    demo  "D:\03  Writting\01
                                                    SAS编程演义\
                                                    02 Data\Clean";


                                                    data     demo.class_datafile;
                                                        set sashelp.class;
                                                    run;   data  class_datafile;
                                                    set sashelp.class;run;

→2.4.3 SAS语言元素

作为一门编程语言,SAS语言元素除了上面提及的SAS语句(statements),还有表达式(expressions)、选项(options)、格式(format)、函数(function)以及Call列程(Call Rountine)等。

1. 表达式

表达式是SAS语言中一个非常重要的概念,SAS在生成一个新变量、给一个变量赋值、计算新值、变量转换以及依据不同的条件进行处理都需要借助表达式来实现。什么是表达式? SAS官方给表达式的定义比较拗口:表达式是由一系列操作数和操作符构成的、可执行的、并且产生结果值的序列。简单来说,表达式就是告诉SAS对什么对象执行什么操作,从而得到一个结果的命令。被操作的对象叫操作数(operands),执行操作用的符号就是操作符(operators),习惯上称运算符的更多,执行的结果可能是一个数字值,也可能是一个字符值,还可能是一个布尔值(是/否、真/假、1/0)。

(1)操作数:操作数可以是常量、变量,也可以是表达式。常量,顾名思义,表示一个值是恒常固定的量;同理,变量表示值是可以变化的,有一套数值去刻画某个特征的量。

常量有以下四种情况。

● 字符常量:字符常量由1~32767个字符组成,必需放在英文引号内,引号可以是单引号,也可以是双引号。字符常量中包含单引号(双引号)时,可以用双引号(单引号),或者连续的单引号(双引号),如:“Hongqiu Gu’s Book”。

● 数字常量:数字常量无须多言,只需留意除了标准计数法(如:1,-5,+49,1.23,01),科学计数法(如:2E23,0.5e-10)和十六制计数法(如:0C1X、9X)也可以。

● 日期时间常量:时间日期常量包括日期、时间、日期时间常量三种,命名是需要采用单引号或双引号加D(日期)、T(时间)、DT(日期时间)后缀来分别表示,如'08Sep2016'D、'11:11'T、'08Sep201611:11'DT,具体可参考程序25 SAS日期、时间以及日期时间的本质,这种引号加字母后缀的命名方式称之为名称文字(Name Literal),在使用非规范的数据集名、变量名时也需要用到这种形式。

● 位测试常量:在引号里由0,1以及点(.)组成字符串,且后缀为B,如'..1.0000'b,用来测试对应的位是否为0或1。这种常量使用较少,在此不做具体介绍。

变量有两种类型:字符变量和数字变量。日期、时间以及日期时间在SAS里其实也是以数字存储的数字变量。如前所述,日期变量的值为距离1960年1月1日的天数,时间变量的值为距离凌晨的秒数,日期时间的值为距离1960年1月1日凌晨的秒数。

程序2-8 SAS中的常量

    *===常量;
    data_null_;
      *==字符常量;
      c1="Hongqiu Gu's Book";
      c2='Hongqiu Gu''s Book';


      c3='Hongqiu Gu"s Book';
      c4="Hongqiu Gu""s Book";


      *==数字常量;
      n1=123;
      n2=-123;
      n3=+123;
      n4=1.23;
      n5=0123;


      *===日期时间常量;
      d='08Sep2016'D;
      t='11:11'T;
      dt='08Sep2016:11:11'DT;


      *===在日志中输出;
      put   c1-c4 ;
      put   n1-n5 ;
      put d yymmdd10.;
      put t time.;
      put dt datetime.;
    run;

(2)运算符:SAS运算符从位置上讲,放在操作数前面的叫前缀运算符(如+、-),放在操作数中间的叫中缀运算符(大多数运算都是);从功能上讲,有用于算术运算的算术运算符(如+、-、*、/),用于比较大小的比较运算符(如>、<、=、^=),用于逻辑运算的逻辑运算符(如^、&、|);算术运算符运算的结果通常为数值,比较和逻辑运算符运算的结果为真(1)或假(0)。关于这几种运算符,没有太多可说的,请参考下面的表2-2、表2-3及表2-4。

表2-2 算术运算符

注:乘法中,*号是必需的,2y或者2(y)都是非法的。

表2-3 比较运算符

注:EQ=EQual, NE=Not Equal, GT=Greater Than, GE=Greater than or Equal to, LE=Less than or Equal to, IN=In the list。

*NE的符号在不同的键盘上可能会有所不同。

**>=、<=与以前SAS版本兼容。WHERE或SQL语句中不支持。

表2-4 逻辑运算符

注:*不同的操作环境可能符号有所不同。

除此之外,还有取小运算符(><)、取大运算符(<>)以及连接运算符(||)。><和<>分别用来找到两个操作数中的最小值、最大值,||用来连接前后两字符。

如果只是单个运算符时,不会牵涉运算顺序的问题,但是,当有多个运算符时,就需要厘清运算顺序了,如复合表达式中会有多个运算符,其运算顺序的原则是:

(1)先算括号中的表达式,再算括号外。

(2)不同组有不同的优先级。

(3)同组内有不同的运算顺序。

具体示例详见表2-5。

表2-5 复合表达式运算顺序

2. 选项

SAS选项包括系统选项和数据集选项。系统选项主要是一些可以影响整个SAS程序执行或SAS会话交互的指令,数据集选项是仅用于数据集的选项,如变量的重命名与筛选、观测筛选、数据集权限控制等。

3. 格式

格式依据应用场景,分为输入格式和输出格式;依据定义方式,分为系统格式和自定义格式。格式告诉SAS按一定的模式读取、显示数据。关于格式,详见第7章。

4. 函数与CALL例程

SAS函数可以接收参数,执行一些运算和操作,然后返回一个值。CALL例程与SAS函数类似,不过不能用在赋值语句或表达式中。关于函数和CALL例程,详细讨论将在第6章进行。

我们通过一个综合的例子来简单感受上面提及的一些概念。

程序2-9 SAS语言元素演示

→2.4.4 三种逻辑结构

就如人生中面临的三种情境一样:按照既定的步骤去做一些事情、依据不同情境选择性地应对一些事情、在某些情境下重复做相同的事情,几乎所有的编程语言都设计了三种程序逻辑结构:顺序、选择和循环。

1.顺序结构(sequence)

顺序结构的程序执行时就按照代码出现的顺序依次执行:第一条语句,第二条语句,第三条语句……前面的所有SAS代码几乎都是顺序结构式的。

2.选择结构(selection)

最经典的选择结构语句就是IF-ELSE/THEN语句,告诉SAS在满足某条件的情况下执行一套操作,不满足则执行另一套操作。例如,我们对SASHLEP库CLASS数据集的人按男女性别的不同分别抓出来放到Male和Female数据集。

程序2-10 IF-ELSE/THEN示例

    datamale female;
      set sashelp.class;
            if   sex="M" then output male;
      else if   sex="F" then output female;
      else put "Invalid sex :" sex ;
    run;

需要留意的是:

● 对于情境的分类,要考虑完全。因此,尽量最后加一个ELSE语句,纳入其他所有可能情况。

● 如果某种情境下,希望执行的不仅仅是一个动作,而是多个动作,此时可以在关键词THEN后面用夹板语句DO-END,把多个动作整合在DO-END语句中。例如,我们嫌弃SEX不文雅,把它换成GENDER,用Male、Female标明男性、女性。

程序2-11 IF-ELSE配合DO-END

    datamale female;
      set sashelp.class;
            if   sex="M" then do;   gender="Male "; output male; end;
      else if   sex="F" then do; gender="Female"; output female; end;
      else put "Invalid sex :" sex ;
    run;

3. 循环结构(iteration)

循环结构的程序是只要满足某个特定的条件,就重复进行某些操作。SAS里常见的循环语句有三种:DO循环语句、DO-WHILE语句以及DO-UNTIL语句。

(1)DO循环语句。DO循环语句其实就是DO-END语句的衍生,在DO后面添加循环的条件,这个条件可以是数字、字符、日期的列表;可以指定起始值和终止值以及步长;还可以是前面两者的混合。

程序2-12 DO循环语句

    dataschedule;
      do date='01Sep2016'dto '30Sep2016'd ;*日期循环;
        day=weekday(date);
        if day in (1,7)then Activity="Running";
        else if day in (2,4,6)then Activity="Writing";
        else Activity="Reading";
        output;
      end;
    run;


    datarandom;
      do i=1to 10;    *数字10次循环;
        r=rannor(23);*生成随机数;
        output;
    end;
    run;

(2)DO-WHILE语句。与DO循环语句每次按照指示变量的值去执行不同,DO-WHILE语句会先判断是否满足条件,如果满足则执行否则跳出循环。

(3)DO-UNTIL语句。与DO-WHILE语句会先判断是否满足条件不同,DO-UNITL语句不管三七二十一,先执行了本次循环再说,而后再判断条件是否满足。在做条件判断时,DO-UNTIL与DO WHILE的思维也不一样:DO-UNIL是如果不满足,则继续下一次循环,如果满足,则跳出循环。具体可留意程序2-13的条件差异。

程序2-13 循环语句DO WHILE与DO UNTIL

    datadowhile;
      i=0;
      do while(i<5);
          i+1;
        output;
      end;
    run;


    datadountil;
      i=0;
      do until(i>=5);
          i+1;
        output;
      end;
    run;

如果读了上面的文字和程序,对三种逻辑结构还是不太清楚的话,图2-12或许能让我们的思维更清晰些。

图2-12 程序的三种逻辑结构

→2.4.5 数组结构

SAS编程语言不像其他语言那样有丰富的结构体(struct),用来聚合数据类型,这正如SAS的数据类型只有简单的字符和数字两种。不过,其他编程语言的数组(array)的思想倒是在SAS编程语言中有充分的利用。

SAS编程语言里,数组是一系列有特定顺序的变量组成的一个临时变量组。之所以说是临时的,是因为数组仅仅存在于DATA步执行的过程中。数组中的变量必须有相同的数据类型,如果全为字符型,则为字符型数组;如果全为数字型,则为数字型数组。此外,如果数组里的值只在一个维度上排列,比如就一行,这就是一维数组;如果数组里的值在多个维度上排列,比如行列上都有,就像一张EXCEL表格,这便是二维数组。

在什么场合下会用到数组呢?怎样理解一维和二维数组呢?举例说明:比如某研究项目持续每天测量患者的收缩压(SBP)、舒张压(DBP),并持续了一周,这样就有7次收缩压和7次舒张压的测量值。当然,我们可以把它们分别存储在SBP1~SBP7、DBP1~DBP7这14个变量中。但是仅仅这样,可能还不够,如果后期我们发现这批血压仪的测量值有系统偏差,SBP比正常测量值低5mmHg, DBP比正常测量值低3mmHg。现在要校正的这些血压值,我们要分别对SBP、DBP写7个赋值语句,总计14个。这样是不是太烦琐了?是的。这时候数组就可以派上用场了。

我们可以建两个数组SBP、DBP分别用来存储SBP1~SBP7、DBP1~DBP7。就像下面这样有一排格子,每个格子有一个编号,SAS依据格子的编号进行数据的存取,这就是一维数组,数据排列就在一个维度上:行。

当然,我们甚至可以直接建一个数组,同时把7次SPB,DBP的值打包在一起,这就是二维数组,数据排列在两个维度上:行和列。

上面只是给出了数组的概念示意图。实际操作时涉及两个核心问题:一是如何定义数组;二是如何访问数组。

1.定义数组

SAS DATA步中,我们通过语句ARRAY来定义数组。其具体语法格式请参考语法2-1:

语法2-1 定义数组语句ARRAY语法参考卡片

关于数组语法的一些解释如下所述。

● 元素个数可以用{*}代替,表示让SAS自动计数,也可以指定具体的数字,如{7},还可以指定一定的数字范围,如{1:7}。

● 元素名可以是变量名,也可以是SAS自定义的变量,如_ALL_(标示所有定义的变量,但是变量类型需要相同), _NUMERIC_(所有数字变量)以及_CHARACTER_(所有字符变量),还可以是_TEMPORARY_(临时变量)。

● <>表示其中的内容并非必须有。例如,$只有在数组元素为字符型时才用到,length也是。数组元素及其初始值也并非必需,如果指定数组元素初始值的话,应该在小括号中指定。

程序2-14 定义数组

    *===定义数组;
    *===sbp1-sbp7是sbp1到sbp7的缩略写法;
    array sbp{7} sbp1-sbp7;
    array dbp{1:7} dbp1-dbp7;


    *===带初始值;
    array sbp{1:7} sbp1-sbp7 (163164 167171 155158 154);
    array dbp{7} dbp1-dbp7 (98 99 92 94 95 93 93);


    *===定义二维数组;
    array bp{2,1:7} sbp1-sbp7 dbp1-dbp7 ;
    array bp{2,7} sbp1-sbp7 dbp1-dbp7 (163164 167171 155158 154 98 99 92
    94 95 93 93);

2.访问数组

访问数组的元素时,我们需要告诉SAS数组元素的地址,数组中元素的地址用数组名加角标的形式arrayname{i} 表示。配合前面已经介绍过的DO循环语句,我们可以遍历数组中的所有元素(见图2-13),进行各种数据操作,如果希望进行前面提到的加减校正,把PUT语句换成赋值语句即可。

图2-13 遍历数组元素结果

程序2-15 访问数组元素

    datatmp;
    *===定义数组;
      array sbp{7} sbp1-sbp7 (163164 167171 155158 154);
      array dbp{7} dbp1-dbp7 (98 99 92 94 95 93 93);
      array bp{2,7} sbp1-sbp7 dbp1-dbp7 (163164 167171 155158 154 98 99
    92 94 95 93 93);
     *===遍历一维数组;
      do i=1to 7;
        put "第" i "次测量的SBP为:" sbp{i};
        put "第" i "次测量的DBP为:" dbp{i};
      end;
     *===遍历二维数组;
      do m=1to 2;
        do n=1to 7;
        put "血压类型为:" m ",血压测量次数为:" n  ",血压测量值为:" bp{m,n};
        end;
      end;
    run;

→2.4.6 函数与CALL例程

在SAS里,特别是DATA步中,如果希望更加方便、快捷地处理数据,我们就必须了解函数和CALL例程。SAS函数可以接收参数,执行一些运算和操作,然后返回一个值。CALL例程与SAS函数类似,不过不能用于赋值的语句或表达式中。我们通过一个简单的例子感受下函数和CALL例程的应用。

程序2-16 函数与例程应用示例

    data_null_;
      length   FullName_ByFunction FullName_ByRoutine $10;
      FamilyName="Gu";
      GivenName="Hongqiu";
      *===用函数生成全名;
      FullName_ByFunction=catx(" ",GivenName, FamilyName);
      *===用例程生成全名;
      call catx(" ",FullName_ByRoutine, GivenName, FamilyName );
      *===Log中查看结果;
      put "Fullname Generatedy by Function: " FullName_ByFunction;
      put "Fullname Generatedy by Routine: " FullName_ByRoutine;
    run;

笔者粗略统计了下,SAS中有将近30多类,总计达520个函数。这是一个比较庞大的体系,也是一个非常有力的武器,我们将在第6章专门论述。

→2.4.7 结构化查询语言SQL

SQL是结构化查询语言(Structured Query Language)的简称,自1970年IBM开发以来,作为关系型数据库查询工具的标准化语言而广泛使用。SAS自6.06版本引入SQL后,一直在增强完善其功能及其与SAS软件的兼容性,目前SAS 9中的SQL已经非常强大。通过SQL,我们可以进行简单查询、子查询,不用排序就可以进行表的连接、集合运算、创建视图和表、创建宏变量等一系列操作。本小节我们仅就SQL语言做一概要式介绍,具体的应用我们会结合后面的实例再讨论。

SQL最简单的应用就是用SELECT语句做查询。SELECT语句包含了一系列有序的从句,具体可见语法2-2。

Help中<>表示里面的东西选用。因此,必用的就只有SELECT和FROM了,比如下面的例子就用SQL查看sashelp.class中的姓名、性别以及年龄。

语法2-2 PROC SQL SELECT语句语法参考卡片

程序2-17 最简单的一个SQL过程

    proc sql;
        select name, sex, age
        from sashelp.class;
    quit;

当然其他从句也是非常实用的。比如,用WHERE可以进行条件筛选,用GROUP BY可以进行分组统计,用HAVING可以对分组统计的结果进行条件筛选,用ORDER BY可以对结果进行排序。初接触时,可能对这些从句的顺序记忆有些混淆,笔者个人就用SFW、GHO来记忆它。sfw是一种位图格式文件的扩展名,gho是ghost镜像文件的扩展名。

下面是一个完整的,利用了所有SELECT从句的例子。目的是先按性别分组统计人数、平均身高,然后挑出平均身高大于62的组,最后按人数多少排序。

程序2-18 PROC SQL SELECT语句全从句示例

    proc sql;
        select sex, count(name) as cnt_name ,mean(height) as m_height
        from sashelp.class
        where age>=12
        group by sex
        having m_height>62
        order by cnt_name;
    quit;

→2.4.8 SAS宏MACRO

MACRO(宏)这个术语可能对我们来说并不陌生,宏就是实现自动化操作的一种工具。在EXCEL里我们就曾接触过,只是大部分人很少用而已。在SAS里,宏工具是一个用来自动化和定制化SAS代码的文本处理工具。

SAS的强大,很大一部分原因就是宏工具的存在。宏的本质是文本替换,但是通过文本替换,可以实现SAS代码的自动化生成,动态生成以及SAS代码的条件结构,也就是说,不仅可以让SAS代码自己去写SAS代码,而且还可以根据不同的条件写不同的代码,这很符合“元编程”的理念。也正是因为这样,很多SAS开发者,疯狂开发自己的宏,从而避免很多重复性的代码编写工作,实现更多自动化、智能化的处理。

SAS宏语言分为两大块:宏变量和宏程序。宏变量是不必限定在DATA步使用的变量,即独立于数据集的变量。宏变量分为系统宏变量和用户自定义宏变量。最常规的情况下,我们可以用%LET语句定义宏变量,%PUT语句查看宏变量。正如前面所说,宏本质是文本替换,宏变量也是用简单的文本去替换更长更复杂的文本。例如,我们可用一小段文本“PUMC”替换更长的“Peking Union Medical College”。

程序2-19 宏变量

    *===自定义;
    %let PUMC=Peking Union Medical College;


    *===查看系统自带;
    %put &sysdate;


    *===查看自定义;
    %put &PUMC;

宏程序同宏变量类似,不过宏程序还有其他特性:①可以包含编程语句,包括DATA步和PROC语句;②可以接受参数。比如,我们可以定义一个打印指定数据集、指定变量的宏。在定义宏程序时,用%MACRO开头,用%END结尾,使用宏时,用%宏名称即可。

程序2-20 MACRO定义和调用

关于宏,本节仅作概念性介绍,具体的内容我们将在第10章详细讨论。