SAS 快速概览

以前在几个分析里用过 SAS,最近又需要用这货,然而 SAS 的语言设计实在是太陈旧了,和习惯差得有点远,官方文档又比较冗长,所以弄一个快速概览用于 rapid prototyping。

Basics

Language

SAS 的脚本是由很多个分块组成的,每个分块官方叫做步骤 (step)。 步骤以其名字开头,以运行指令 run; 结尾。

每个步骤中有若干行指令,叫做语句 (statement),以分号结尾。

下面是一个只有两个步骤的示例程序

/* Comments are enclosed like this. */

/* This is a example of a step called "data" */
data work.exampledataset;
    /* There are 3 statements in this step */
    i = 1;
/* Generally all steps require a "run;" statement 
to indicate the end of the step */
run;

/* Another step called "proc", with 2 statements */
proc freq data=work.exampledataset;
run;

步骤有很多种,不过关键的步骤只有两个,data 和 proc。 data 步用于创造数据,proc 步可以调用一些预先写好的程序用于处理数据,相当于 call。

不难发现,在 SAS 脚本的最上层,也就是步骤的这一层,从设计上是不存在真正的编程的,它实际上更类似于 shell script,只是用于表示数据处理的流程。 SAS 支持一些 macro,因此一定要说的话也确实可以在这一层编程,不过显然并不是非常友好。

主要的真正编程是在 data 步和 proc iml 中。 data 步里支持一些常见的 control flow 和 data structure,可以创建变量、数组等。 proc iml 是一个特殊的 proc 步,相当于 SAS 的线性代数库,可以完成一些简单的矩阵运算等。 这两个步中的 statement 比较接近我们一般说的编程语句。

在其它一般的 proc 步中,比如 proc freq,里面的 statement 主要就是设定参数。 官方出于某种原因故意混淆了几种非常不同的意思,容易造成一些混乱。 值得注意的是,许多 proc 步中的参数,有的是紧跟在名字后面,有的则是要放在步骤中的 statement 中。 大体上看,比较关键的,并且短一些的参数似乎更倾向于出现在名字后面,而复杂一些的参数则倾向于出现在 statement 中,不过也有很多反例,具体哪个参数要放在何处,需要参阅官方的文档。

data step

我们首先介绍 data 步。

data 步以 data example_name; 语句开头,意思是创建一个名为 example_name 的数据集。

奇妙的数据结构

data 步中可以创建变量,只要用 i=1; 这样的语句就可以。 同样也可以创造数组,用 array x{100} _temporary_; 可以创建一个 1*100 的数组。

要注意的是,SAS 中所有变量与传统意义的变量不同,它实际上是数据库表的一个列名。 换句话说,所有的变量都是数组,其中的一个元素叫做一个观测(observation)。 我们用 i=1; 创造了一个变量,实际上得到的是一个数组,其第一个元素是 1。 因此,我们创造一维数组的时候,不难想到我们创造的实际上是二维数组。

接下来当我们更改 i 的值的时候,会有一些奇异的事情。 前面说到,变量实际上是数组。 在我们初次创建这个变量之后,i 实际上指向的是数组的第 1 个元素。 因此,如果我们此时修改 i 的值,我们会修改到数组的第 1 个元素。

“output;” 语句会保存当前的所有变量,在当前所有”指针”的位置后方插入一个新元素,并且将指针指向这个新元素。

因此,如果我们运行了一次 output; 语句,则我们在数组 i 的第 1 个元素后面插入了一个新元素,并且将指针指向这个新元素即第2个元素。再修改 i 的值时,修改的是数组的第 2 个元素。

最后,当 run; 指令运行的时候,数据集会被创造,其内容就是所有的变量对应的数组。

我们来看一个例子

data work.exampledataset;
	i=1;
	output;
	i=2;
	i=3;
	output;
	j=i;
	output;
run;

它的输出是

Obsij
11.
23.
333

以上的运算规则还不算最为诡异的。 SAS 中的 data 步提供了一些读入外部数据集的办法。 “set datasetname;” 语句可以读入一个之前创造的数据集。 刚才我们说到,当新建一个变量 i 时,i 指向数组的第 1 个元素。 那么,当我们读入的数据集有一个列名是 i 时,i 指向哪个元素呢? 答案是,i 指向数组的所有元素。

一个有用的指令是 “put varname;” ,它可以将某个string或者变量的值输出到 log。 如果我们在上一个例子中 put i; 则会在 log 中输出一个数。 而如果我们将上一个例子得到的数据集读入,然后 put i; ,输出是

1
3
3

可见此时 i 选中了这个数组的所有元素。 那么,考虑一个有趣的问题。这个时候运行 output; 会发生什么? 答案是,所有被选中的元素后面都会被插入一个新元素,然后被选中的元素变成这些新插入的元素。

来看这个例子

data work.exampledataset;
	i=1;
	output;
	i=3;
	output;
run;

data work.data2;
	set work.exampledataset;
	output;
	j = i + 1;
	output;
run;

它的输出是

Obsij
11.
212
33.
434

我们从上面的例子还可以看到,任何算数符号都是 element-wise 的,比如说等号就会将当前选中的所有值依次拷贝到另一个变量中,并且保持位置不变。

最后,有没有办法把指针向前移动呢? 也就是说我们怎样返回去修改某个变量数组之前的值? 答案是没有办法,因为从逻辑上看,每次 output; 之后,所有当前变量的值都已经被保存为了一个 observation,既然已经输出出去了,那就不应该再重新拿回来修改了。

奇妙的数组

我们再来看 SAS 的数组,我们刚刚说定义数组的语句是

array x{100} _temporary_ ;

那么其中的 _temporary_ 是什么意思呢? 实际上,按官方的文档,定义数组的方法是

array x{3} name1 name2 name3;

也就是说,其实数组是要给每个元素起名字的。那显然这个根本不是什么数组,一般我们好像把这种结构叫做字典。

更骚的是,我们定义了数组之后,如何访问其中的元素呢? 实际上我们根本不用管数组名 x ,直接用元素的名字比如说 name2 就可以访问到数组 x 的第二个元素。 所以我们这个数组实际上可能都不能算字典,数组指令的用处实际上是用来批量创建很多个变量。 这种奇妙的数组实际上是因为 SAS 保存数据集的时候不允许某个列没有列名造成的。

可以看这个例子

data work.asdf;
	array x(3) x1 x2 x3 (1,2,3) ;
	x2 = 4;
    output;
run;

它的输出是

Obsx1x2x3
1143

那我们怎么才能定义一个比较像是真的数组的东西呢? 官方提供了一个选项,也就是我们上面用的 _temporary_ ,加上这个选项之后,就不用为每个元素指定名字了,要访问其中的元素时也只能用下标,并且,在输出时这个数组也不会被输出出去。

作为例子,筛法求质数

data whatever;
	ARRAY pn{10000} _TEMPORARY_;

	DO i=2 TO dim(pn);
		pn(i)=i;
	END;

	DO i=2 TO dim(pn);

		IF pn(i)=. THEN
			CONTINUE;

		DO j=i*2 TO dim(pn) BY i;
			pn(j)=.;
		END;
		output;
	END;

	DROP j;
run;

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.