msgbartop
List for SAS fans and programmer
msgbarbottom

11 6月 11 SAS9数据步的新发现 旧稿新编 2006


大家都知道数据步(DATA step)是SAS系统的基石,最基本也是最重要的。在5.5年前,也就是2006年,我在当时人气最旺的SAS中文技术论坛SASOR发了一篇翻译稿“SAS9数据步的新发现(一)(二)(三)(四)”,《The DATA step in SAS9: Whats New? 》一文所作的翻译草稿,原文来自SAS SUGI proceedings 作者Jason Secosky, SAS, Cary, NC. 。

主要内容包括SAS9.0和9.1版本四个方面新功能:1,PERL正则表达式、2, 哈希表(Hash tables)、3,新函数和4,原来函数,操作符和宏功能的扩展。由于SAS9是SAS一个变化非常重大的版本,所以当时就翻译了这篇文章,尽管差不多6年过去了,这些语言元素对于SAS用户来说,仍然还是很fashion的。据传SAS9.3带着万众瞩目的新功能如高性能计算,可视化增强和云计算将于今年(2011)秋季美国发布,不过现在是找不到The DATA step in SAS9.3,有What’s New in SAS9.2,但是里面DATA step的亮点不多。

下面我对以前的旧稿进行整理,主要把翻译润色了下,还有把代码都跑通了一遍(其实这个工作是2008年做的)。现在在这里给大家分享下,一来是这些功能大家现在用得还是很少(谁叫SAS语言这么有特色呢,几年不充电的SAS程序员照样能活得好好的),可以尝试下,用新的语言功能来改进现在的SAS运行性能;二来放在硬盘的0101制文件也是会“烂”掉的,不如趁SAS9.3没出来之前拿出来晒晒,也许对大家还有些帮助。

SAS9.0数据步的新功能

导读:

这篇文章阐述了SAS9.0和9.1版本中数据步(DATA STEP)语言改进提高的部分,其中包括用于文本快速查找和替换的perl正则表达式;用于基于关键词查询的可扩展字典的哈希表(Hash tables)和用数据步函数进行排序;加快原来代码的执行速度,并提供输出日志信息和找回变量值的新方法。

PERL正则表达式

Perl正则表达式(regexp)是一种简单高效用于文本查找、替换和提取的语言。以往用函数INDEX,SCAN,SUBSTR和连接符做简单的文本查找和替换操作。用这些函数做通常需要多步并且不可避免地会出错,还有查询动态文本困难等。Perl正则表达式将几个操作集中到单个函数调用,减小出错率,更容易维护和清理,并且可能提高执行速度。下面几节讨论一下几个用Perl正则表达式作数据有效性校验、文本替换和提取文本的常用例子。

前言

Perl正则表达式(regexp)是一种用于查询由字母、字符和一种叫元字符的特殊字符组成的字符串的语言。当用Perl正则表达式查询时,在regexp中的一个字母字符与被查询字符串的字符相匹配。比如,Perl正则表达式/abc/将查找一个字符串中的字符abc。所有regexp以一个分隔符开始和结束。如/abc/中,分隔符是一个右划线/。分隔符可以是所有非数字字符或双小括号。因为空格在Perl正则表达式中可以匹配一个字符串中的空格,分隔符是必需的,所以让regexp清楚的指定一个分隔符也是必要的。

Perl正则表达式的功能取决于元字符。当进行查找时regexp元字符进行特别的操作,象强制匹配到特别的位置或匹配成特别的字符集。举例来说吧,perl 正则表达式/abc\d/将与abc后带一个数字的字符匹配。一个数字的元字符是\d,其他的元字符将会在本文中讲到,在SAS9 Language Reference Dictionary 中讨论。http://regexp.info或参考文献2中都有详细的介绍。

数据校验

数据的校验是关于检查一个是否符合特别的要求。下面的例子将验证电话号码以保证符合三个数字在一个括号中,后面接着7位数,其中前三位和后四位之间有一横杠。如(919)677-8000是一个有效的电话号码,而919-677-8000则不是,因为前三个数字没用小括号括起来。

SAS提供了函数PRXPARSE和PRXMATCH正则表达式来操作查找。PRXPARSE引用了一个正则表达式并在正则表达式编译后执行返回一个模式指定的号码(),下面的例子,被指定的数字re被保留因为regexp没有改变,不需要为数据步每个重复编译一次。PRXMATCH引用一个模式指定号码,并返回正则表达式匹配的位置,若没找到匹配的字符就返回0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
data _null_;
   length first last phone $ 16;
   retain re;
   if _N_ = 1 then do;
      re = prxparse("/\([2-9]\d\d\) ?" || "[2-9]\d\d-\d\d\d\d/");
   end;
 
   input first last phone & $16.;
 
   if ^prxmatch(re, phone) then
      putlog "NOTE: Invalid, "  
             first last phone;
datalines;
Thomas Archer     (919)319-1677
Lucy Mallory      800-899-2164
Tom Joad          (508) 852-2146
Laurie Jorgensen  (252)152-7583
;

Output
NOTE: Invaild, Lucy Mallory 800-899-2164
NOTE: Invalid,Laurie Jorgensen (252)152-7583
传给PRXPARSE的正则表达式包括元字符\(来匹配括号,\d一个数字,[2-9]匹配2到9的数字,?匹配前一个字符1次或0次。本例中前一个字符为空格,所以0或1个空格将被匹配。这个例子说明了用一个regexp进行简单的查找。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
data _null_;
   length first last phone $ 16;
   input first last phone & $16.;
   if ^prxmatch("/\([2-9]\d\d\) ?" ||
                "[2-9]\d\d-\d\d\d\d/",
                phone) then
      putlog "NOTE: Invalid, "  
             first last phone;
datalines;
Thomas Archer     (919)319-1677
Lucy Mallory      800-899-2164
Tom Joad          (508) 852-2146
Laurie Jorgensen  (252)152-7583
;

传递一个regexp到PRXMATCH使得PROC SQL可以用regexp。下面例子用PROC SQL选出电话号码中的无效电话号码。

1
2
3
4
5
6
proc sql;
   select * from phone_numbers
   where  
   not prxmatch("/\([2-9]\d\d\) ?" ||
                "[2-9]\d\d-\d\d\d\d/",phone);

quit;

在where语句中用PRXMATCH和用like操作符类似,然而regexp有更多特定的元字符。比如说,因为原来没有特定元字符去查找一个数字,不能用Like操作符校验电话号码,不过SUBSTR和字符比较函数过去常常用来校验电话号码的每个字符,这种类型的where语句程序编起来又臭又长,并且执行时间很长。

文本替换

Perl正则表达式(regexp)使查找和替换操作变得容易,它用一个regexp来查找和提供一个值替换来实现。PRXPARSE编译regexp,但不是用PRXPARSE查找一个匹配。PRXCHANGE过去常常用来查找匹配和执行替换。PAXCHANGE取得一个模式识别号码,执行查找和替换的次数和一个执行替换的字符变量。

用于替换的regexp必须包括一个regexp来查找和为替换准备的文本。这类regexp的格式是S/regexp/replacement-text/. 在regexp前的“S”字符表明这是一个用来替换的regexp。下面的例子替换所有带&it的符号。这是当从文本text转化为html时常用的替代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
data _null_;
   retain re;
   if _N_ = 1 then do;
      re = prxparse('s/</&lt;/');
   end;
 
   input;
   call prxchange(re, -1, _infile_);
   put _infile_;
datalines;
x + y < 15
x < 10 < y
y < 11
;

Output:
x + y < 15
x < 10 < y
y < 11

注意-1被传递给PRXCHANGE用来表示执行替换的次数。-1是一个特定值表明所有可能的替换都执行。
在以往的DATA步中,查找和替换需要多个函数调用和连接,用regexp的话,一个函数就可以搞定。

替换文本—9.1

在SAS9.1中PRXCHANGE简化而不必调用PRXPARSE来编译regexp。为传递一个regexp到PAXCHANGE,PRXCHAGNE函数被用来替换CALL PRXCHAGE。PRXCHAGNE函数取得一regexp,执行查找和替换的次数和一个输入字符。这个函数在执行所有的询问查询和替换操作后返回输出字符。

和PRXMATCH和相似,PRXCHANGE在第一次调用目标自动编译regexp。如果有可能,在完成调用过程中利用重复已编译regexp。下面的例子说明了与上一个例子一样的查找和替换操作,但是代码更少。

1
2
3
4
5
6
7
8
9
10
data _null_;
   input;
   _infile_ = prxchange('s/</&lt;/',  
                        -1, _infile_);
   put _infile_;
datalines;
x + y < 15
x < 10 < y
y < 11
;

因为PRXCHANGE可以传递一个regexp到PRXCHAGNE并返回一个值,所以可在PROC SQL查询中调用PRXCAHGNE。下面的查询产生一列与上例一样的文本变化。查询先从输入表text lines取出数据,改变列line中的文本,然后在html_line列放置结果。

1
2
3
4
5
proc sql;
   select prxchange('s/</&lt;/', -1, line)
   as html_line
   from text_lines;
quit;

用regexp可以不需要调用PRXPARSE,进一步简化了代码,还可以在DATA步以外使用regexp。

提取文本

在regexp中圆括号是元字符,能调用PRXPOSN来提取部分regexp匹配的文本。一个插入的子语句能得到位置和总配对部分的长度。举例,regexp / (\d\d\d)-(\d\d\d)/将会匹配文本123-456。两对括号能提取部分或副配对的位置和长度。既然这样,副配对#1是123,副配对#2是456。调用带副配对号码的PRXPOSE得到一个副配对的位置和长度。

下面的例子提取了电话号码的区号,并检查区号是不是在北卡,regexp的区号部分用粗体括号括起来,用来表示区号数字是副配对#1,既然这样,就把1传给PRXPOSN可得到副配对#1区号数字的位置和长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
data _null_;
   length first last phone $ 16;
   retain re;
   if _N_ = 1 then do;
      re = prxparse("/\(([2-9]\d\d)\) ?" ||
                    "[2-9]\d\d-\d\d\d\d/");
   end;
 
   input first last phone & $16.;
 
   if prxmatch(re, phone) then do;
      call prxposn(re, 1, pos, len);
      area_code = substr(phone, pos, len);
      if area_code ^in ("828" "336"  
                        "704" "910"
                        "919" "252") then
         putlog "NOTE: Not in N. Carolina: "
                first last phone;
   end;
datalines;
Thomas Archer     (919)319-1677
Lucy Mallory      (800)899-2164
Tom Joad          (508) 852-2146
Laurie Jorgensen  (252)352-7583
;

Outoput:
NOTE: Not in NC, Lucy Mallory (800)899-2164
NOTE: Not in NC, Tom Joad (508) 852-2164

提取文本 SAS9.1

在SAS9.1中使用PRXPOSN不需要调用SUBSTR来为submatch取的文本。与CALL PRXPOSN相对立,PRXPOSN函数被的是查找文本而不是位置和长度变量。该函数返回副配对文本。下面是用PRXPOSN函数写的用于提取区号的程序,新行为黑体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
data _null_;
   length first last phone $ 16;
   retain re;
   if _N_ = 1 then do;
      re = prxparse("/\(([2-9]\d\d)\) ?" ||
                    "[2-9]\d\d-\d\d\d\d/");
   end;
 
   input first last phone & $16.;
 
   if prxmatch(re, phone) then do;
      area_code = prxposn(re, 1, phone);
      if area_code ^in ("828" "336"  
                        "704" "910"
                        "919" "252") then
         putlog "NOTE: Not in N. Carolina: "
                first last phone;
   end;
datalines;
Thomas Archer     (919)319-1677
Lucy Mallory      (800)899-2164
Tom Joad          (508) 852-2146
Laurie Jorgensen  (252)352-7583
;

为了使用PRXPSON,模式识别号码必须被传递给PRXMATCH。与此相反,传递一个regexp的形式更简单。调用PRXMATCH后,副配对的位置和长度与模式识别号码相联系。这样传递一个regexp到PRXMATCH的简单形式不能被使用。

PRX函数

下面给出了少数几个PRX函数,其他包括,

CALL  PRXSUBSTR 返回整个配对的位置和长度,
CALL  PRXNEXT   被用作在文本中重复几个regexp配对,
PRXPAREN         返回找到的最后一个副配对,
CALL PRXFREE     清空使用过的编译regexp的内存,
CALL PRXDEBUG   将Perl的调试信息转出到SAS日志中。

比较PERL和RX
PRX函数建立在与Perl5.6.1一样的源代码上的,这意味着PRX的执行速度和功能与Perl的相似,Perl和PRX语法不同之处在于,Perl语言元素被用于一个regexp中,比如,Perl变量和声明中不容许存在regexp中,因为regexp在SAS环境 中操作而不在Perl。除了有一点不同的regexp语法,SAS RX函数执行的操作与PRX类似。 SAS中RX语法由其他的regexp语法组成,提供了得分配对,匹配平衡符号和替换文本中有多一点的操作。RX函数一般比PRX函数慢。并且需要更多步来执行文本提取。RX函数不能用于PROC SQL和where语句中。

对于每个例子,仅用一个regexp可写出一个等效的代码。不有regexp将会需要更多的函数调用,管理文本中的字符位置和执行文本部分。 这使得代码更少出错,更容易维护,可能也会提高执行速度。

组件对象接口

为了整和新数据结构到DATA步中,添加了DECLARE语句和对象点语法。这样可以创建一个类实例(也叫对象),然后用点语法调用对象方法,如OBJ.METHOD(),提供的初始类是一个哈希表。

哈希表可以将数据存储到一个可扩展的   表中,这样具有快速查询的功能它与数据步数组类似,索引在数组中用来存储和提取数据。数组用标记,而哈希表能用字符、数字或字符和数字的组合做标记。与数组不同,创建哈希表时,存储值的个数的不被指定,只要要内存容许,可以保存足够多的数值。

当添加数据到哈希表中,索引通常称为关键词,常常用做复制数据到表中某个位置。当从一个哈希表中提取数据时,如果数据表中有带关键词的数据,可根据这个关键词进行快速非线性搜索,返回带关键词的数据。

关键词和数据是个一个字符矢量,数字矢量或字符数字矢量。如一个字符社会安全号码(SSN)可以是一个关键词去定位一个人的姓名、地址和其他信息。当添加数据时,一个SSN和个人信息都添加都哈希表中,当提取数据时,根据给出的SSN就可以返回根据以此SSN为关键词存储的数据。

这种类型的操作在数据步中早有了,不过用哈希表来做速度更快。带关键词的set语句根据给定的关键词将一排数据载入到PDV中,用哈希表的话,不需要搜索数据集的索引,查询在内存中执行而不是在硬盘上。 带by的Merge也可以用,不过需要已排序或索引的输入数据集。装载到哈希表中的数据不需要预先排好序,格式可以用于大的未排序数据集中查询表方法。哈希表比用格式更快,内存占用更少,能根据一个关键词存储多个数据项。用大量数据时,格式方法占用大量的硬盘空间,而哈希表则不需要。]

下节将讲述数据步中哈希表的用法。

用哈希表做关键词查询。

哈希表的常用用法是用关键词/数据装载,然后通过带关键词的哈希表查询数据集。下面的例子说明怎么装载一个主数据集,怎么用一个执行数据集来查询,主数据集有变量key和val,执行数据集有变量key。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
data merged;
   length key $ 13;
   length val 8;
   if _N_ = 1 then do;
      declare Hash ht(dataset: "master");
      ht.defineKey("key");
      ht.defineData("val");
      ht.defineDone();
   end;
 
   /* Load key and query hash table */
   set trans;
   if ht.find() = 0;
run;

在这个例子中,申明并初始化了一个新的哈希表ht。构造器被指令装载名为master的数据集到此哈希表中,命名参数dataset用来表明需要被装载的数据集。命名参数允许以任何顺序的多个参数传递,帮助记录出现的参数。与临时数组类似,保留对象放弃所有输出数据集。

在数据装载到一个数据集之前,哈希表的关键词和数据变量必须被指定方法defineKey, defineData执行这步操作。这里指定了一个关键词变量key和一个数据变量val。多个关键词和数据变量可以用defineKey,defineData指定更多的变量名。

当关键词和数据变量定义完成后,调用defineDone方法,这将使数据集装载到哈希表中,请注意哈希表的大小没有指定,哈希表将加载的数据量在内存中增大。

在程序查询阶段,用set语句装载执行数据集中的值,find方法用关键词变量的当前值在哈希表中执行查询。如果匹配就移动数据变量的值。如果成功find方法返回值为0,失败则为非零。

关键词查询执行

为了测试执行速度,用format,带key的set,带by的merge做哈希表同一个搜索操作,结果见下表,这个例子中,执行同样的查询,哈希表比其他的方法少用1.9到5.9倍的时间。用format,花在format和format查询都比用哈希表查询长。带key的set方法要花时间建立索引,并且硬盘查询不内存中查询慢。带by的merge速度不错,但输入数据集必须排序或索引,对于查询无序数据,内存允许的话,哈希表是一个不错的选择。

执行查找实际时间

SET with KEY=            108.14s
Build Index                         11.33s
Search                             96.81s
Format and PUT()           89.03s
Build Format                       67.32s
Search                            21.71s
MERGE with BY            35.78s
Sort Data Sets                      31.60s
Search                            4.18s
Hash Table                 18.35s
Load and Search                    18.35s

每次测试,表中第一行是总时间,缩进行分别是每步操作的所用的时间,都是运行在Windows 系统SAS9.1记录的时间。给出了三个执行最快的一个。测试在CPU为1.6G,内存为1G,奔四机器上,操作系统为XP PROFESSIONAL。数据集 master有500万条key/data对数据集trans中有500万搜索关键词。关键词是一个13个字符值和一个8字节数字值。半数的搜索关键词存在于master数据集中。

添加数据

哈希表中的数据可以从数据集以外的数据源中加载,如果没有数据集被传递到哈希表构造器中,就会初始一个空的哈希表,add方法可以把数据添加到哈希表中。下面的例子是从输入文件装载数据到哈希表中,在数据装载后执行查找。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
data _null_;
   length first last title $ 16;
   length born died 8;
   if _N_ = 1 then do;
      declare Hash ht();
      ht.defineKey("first", "last");
      ht.defineData("born","died","title");
      ht.defineDone();
   end;
 
   infile datalines eof=search;
   input first last born died title & $16.;
 
   ht.add();
   /*  
      ht.add() is the same as:    
      ht.add(key:first, key:last,  
          data:born, data:died,
          data:title);
   */

   return;
 
search:
   rc = ht.find(key: "John", key: "Keats");
   if rc = 0 then
      put "Found John Keats " title $QUOTE.;
datalines;
William Blake  1757 1827 Spring
John Keats     1795 1821 To Autumn
Mary Shelley   1797 1851 Frankenstein
;

Output:
Found John Keats “To Autumn”

如果在没有参数情况下调用add方法,将会复制当前的关键词和数据变量到哈希表中。如果给出了参数,那些值就会在哈希表中用到参数的次序必须和定义的关键词和数据变量的次序一致。比喻,如果参数born和died在用add方法时被调换,那么在取数据时,出生年的数据放入died中,死亡年的数据放入born中。这个例子同样示例了一个关键词的多个值或一个数据的多个值。注意数据项是混合类型的。

哈希表输出和排序

转载好一个哈希表后,哈希表中的数据就可以用output方法不断地输出数据到数据集中。这些数据集可以被其中proc步使用,也可以用进一步的DATA步装载到哈希表中。

除非建立哈希表的时候通过命名参数ordered传递ascending或decending传值,用输出方法得到的行输出没有排序。当用了参数ordered,数据项根据关键词排序。下面给出了个例子,怎么将Patient和diacharger数据装入一个哈希表中,并从哈希表中输出到按patient排序的数据集sort_ids中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
data _null_;
  length patient_id $ 16 discharge 8;
  if _N_ = 1 then do;
    declare Hash ht(ordered:"yes");
    ht.defineKey("patient_id");
    ht.defineData("patient_id", "discharge");
    ht.defineDone();
  end;
 
  infile datalines eof=output;
  input patient_id discharge:DATE9.;
  ht.add();
  return;
 
output:
  ht.output(dataset: "sorted_ids");
datalines;
Smith-4123 15MAR2004
Hagen-2834 23APR2004
Smith-2437 15JAN2004
Flinn-2940 12FEB2004
;
 
data _null_;
  set sorted_ids;
  put patient_id discharge:DATE9.;
run;

Output:
Flinn-2940 12FEB2004
Hagen-2834 23APR2004
Smith-2437 15JAN2004
Smith-4123 15MAR2004

注意可以用output方法输出数据变量,对应的数值,而不能输出关键词对应的数值。为了能保留关键词的数值。关键词必须加入哈希表的数据部分。上面的例子用Patient_id既可以作关键词变量又做数据变量。如果patient_id仅做为关键词变量,那么patient_id对应的值不会出现在输出数据集中。

哈希表重复

在哈希表组件中,还有一个哈希表重复组件。hash iterator能顺序地获得哈希表中的数据元素。当hash iterator建立时,hash iterator能进行全部重复。下面的例子装载patient_id和discharge数据到哈希表中,并用一个hash iterator输出各项到log中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
data _null_;
  length patient_id $ 16 discharge 8;
  if _N_ = 1 then do;
    declare Hash ht(ordered:"yes");
    ht.defineKey("patient_id");
    ht.defineData("patient_id", "discharge");
    ht.defineDone();
  end;
   infile datalines eof=process;
  input patient_id discharge:DATE9.;
  ht.add();
  return;
 
process:
  declare HIter iter('ht');
  rc = iter.first();
  do while (rc=0);
    put patient_id discharge:DATE9.;
    rc = iter.next();
  end;
datalines;
Smith-4123 15MAR2004
Hagen-2834 23APR2004
Smith-2437 15JAN2004
Flinn-2940 12FEB2004
;

Output:
Flinn-2940 12FEB2004
Hagen-2834 23APR2004
Smith-2437 15JAN2004
Smith-4123 15MAR2004
除了数据总输出到log中而不是数据集中。这个程序与演示output方法的例子有点像。语句declare hiter被用于建立名为iter的hash iterator。当iterator建立后,哈希表名迭代过头,“ht”被传到iteror构造器中。 interator一经建立,有四个方法FIRST,LAST,NEXT和PREV可被用来从哈希表中提取数据。上面例子中,FIRST方法被用来得到哈希表中第一项。NEXT方法用来得到哈希表中下一项。所有iterator方法在该项找到时返回0,找不到就返回1。这个例子中,当NEXT方法返回1时,DO WHILE循环停止,也就意味着再没有元素从哈希表中返回。

因为在output方法例子中,命名参数ordered为hash iterator赋值过头导致iterator按哈希表关键词返回排序的数据。如果ordered参数没有被设置,返回的数据就不能确定是否排序。

哈希表INTERNALS

哈希表是一个bucket数组,当以一个关键词增加数据或搜索数据时,关键词被传递到能返回插入数据的或找到一个bucket号码的哈希函数。当bucket中有多个keys hash时,key/data对存在AVL树中。

如果放入哈希表中key/data对的数组个数大于bucket的个数时。因为AVL树将包含一些项目,所以执行速度减慢。搜索一个AVL树比搜索一个全部项放入哈希表bucket要慢。

与此相反,如果放入哈希表中的key/data对的数目小于bucket,那么许多bucket就会是空的。没有用的bucket会浪费内存,然而大多情况下,浪费的总内存是很小的,如缺省的bucket数为256个,每个bucket为8字节,所以就算只有到一半的bucket,也只有1024字节被浪费。作为一个执行调整参数,被哈希表用到的bucket个数能改变,可以用哈希表构造器hashexp命名参数改变bucket数组。hashexp值为在项目a power of two 的哈希表指示bucket数目。如

1
declare Hash ht ( hashexp: 6 );

将会为哈希表初始化26即64个bucket。Hashexp最大值为16。一般来说,百万条的key/data对28或29bucket的哈希表中丝毫不会让速度打折扣。一个哈希表能不断增大,直到内存耗尽为止。但是调整数组大的key/data对的bucket个数可能会提高add和find方法的执行速度。

_NEW_ OPERATOR AND DELETE METHOD _NEW_操作符和DELETE方法

随着可以用DECLARE语法可以创建一个对象,_NEW_操作符可以用_NEW_在类名和任何构造器参数前面。为释放一个存在的对象可以调用对象的delete方法。每个对象都有delete方法,用这个方法也不需要参数。下面的例子用_NEW_操作符建立了一个哈希表。一个新的哈希表将被建立和删除。注意DCL是DECLARE语句的简写形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
data _null_;
   length key1 data1 $ 8;
   length key2 data2 8;
 
   dcl Hash ht;
   ht = _new_ Hash ();
   ht.defineKey("key1", "key2");
   ht.defineData("data1", "data2");
   ht.defineDone();
 
   /* DATA step processing */
 
   ht.delete();
run;

文件

对象点语法和哈希表在SAS9.0数据步中是测试用,在SAS9.1则为正式的产品。除了上面已经讲到的方法,文档中还讲到了本文中没有提到的remove和replace方法和哈希表的num_items属性。文档中也描叙了哈希表iterator 的LAST和PREV方法。

函数

下面几节着重许多新的DATA步函数和CALL程序。请参考SAS9 Language Reference Dictionary获得全部的信息。

函数CALL SORTN,CALL SORTC

CALL SORTN 和CALL SORTC给所有通过的变量值排序。SORTN排数字变量次序。SORTC排字符变量次序。对于字符变量的排序,要求字符长度必须相同。当程序执行返回时,变量值以升序形式排列。SORTN和SORTC不会替代SORT语句,但是提供了一种在DATA步给少量数据排序的途径。SORTN和SORTC在SAS9.0和SAS9.1中处于试用调试阶段。

1
2
3
4
5
6
7
8
data _null_;
   array rnd[5];
   do i = 1 to dim(rnd);
      rnd[i] = floor(ranuni(1)*100);
   end;
   call sortn(of rnd[*]);
   put "rnd[*] = (" rnd[*] +(-1) ")";
run;

Output:
rnd[*] = (18 25 39 92 97)

函数VVALUE,VVALUEX

函数VVALUE取得一个变量,返回一个一字符串形式的值。VVAULEX取得一个包含一个变量名字符串返回以字符值形式的变量值。这个就象用put函数时不必须指定一个格式一样。用于格式化的格式通常那种类型对应的缺省值。对于数字值,缺省值为BEST12.。对于字符值,缺省形式为$W,其中W为字符的长度值。可以用format来改变缺省值。

1
2
3
4
5
6
data _null_;
   n = 123.456;
   a = vvalue(n);
   b = vvaluex('n');
   put "a = " a $12. / "b = " b $12.;
run;

Output:
a = 123.456
b = 123.456

函数CAT,CATS,CATT,CATX

CAT函数连接并返回通过的值。CAT象连接操作符||一样连接字符。CATT在连接前除掉在每个语法尾随的空格(想想CAT加TRIM)。CAT在连接前去掉前面和后面的空格。CATX想CATS,但在连接的值之间放入了一个分隔符。对于小数位多的数字值被格式化成格式为BEST32.的字符值。小数位小的书就格式化为BEST12.的字符值。这些函数减少了TRIM,LEFT和带连接操作符的PUT函数的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
data _null_;
   length a b c $ 8;
   a = 'some';
   b = '  text';
   n = 123.456;
   c = 'together';
   cop = trim(a) || trim(left(b)) ||
         trim(left(vvalue(n))) || c;
   cat  = cat(a, b, n, c);
   catt = catt(a, b, n, c);
   cats = cats(a, b, n, c);
   catx = catx(',', a, b, n, c);
   put +1 cop= / +1 cat= / catt= / cats= /
catx=;
run;

Output:
cop=sometext123.456together
cat=some text 123.456together
catt=some text123.456together
cats=sometext123.456together
catx=some,text,123.456,together

函数SCANQ, COUNT,COUNTC

SCANQ函数解析可能含一个分界符的值,SCANQ操作象SCAN函数但是忽略了在引号内文本中的空格。COUNT函数有两个字符参数,数出第二个参数在第一个参数中出现的次数。COUNTC函数有两个参数数出第二个参数中每个字符在第一个参数中出现的次数。下面的例子,COUNTC被用来获得一个字符值中逗号的个数。

1
2
3
4
5
6
7
8
9
data _null_;
   txt = "some,'comma,separated',text";
   put @2 'i' @5 'scan' @16 'scanq' / 32*'-';
   do i = 1 to countc(txt,',')+1;
      scan_word = scan(txt, i, ',');
      scanq_word = scanq(txt, i, ',');
      put @2 i @5 scan_word @16 scanq_word;
   end;
run;

Output:

i scan scanq
——————————–
1 some some
2 ‘comma ‘comma,separated’
3 separated’ text
4 text

在SAS9.1中,COUNT和COUNTC有一个可选的第三个语句作为选项,其中一个可以指定大小写独立的查询。

函数SUBSTRN,LENGTH

SUBSTRN函数用法象SUBSTR函数,不过长度参数可以为0。当长度参数是0时,返回一个长度为0的值。SURSTRN在可能出现零的regexp配对中有用。LENGTH函数用法象LENGTH,不过,当一个值都为空格时返回一个零。下面这个例子,注意当SUBSTR遇到一个零长度时NOTE输出。SUBTRN不能产生象上面一样的NOTEA输出。

1
2
3
4
5
6
7
8
9
10
11
data _null_;
   length empty $ 8;
 
   substr = substr("some text", 1, 0);
   substrn = substrn("some text", 1, 0);
   put substr= / substrn=;
 
   length = length(empty);
   lengthn = lengthn(empty);
   put length= / lengthn=;
run;

Output:
NOTE: Invalid third argument to function
SUBSTR at line XX column XX.
substr=some text
substrn=
length=1
lengthn=0

函数CALL SYMPUTX

CALL SYMPUTX操作用法像CALL SYMPUT,不过它去除了宏变量名称和数据参数的前面和后面尾随的空格。如果一个数值作为第二个参数放到宏变量中,当该数的小数位很多时,就会用BEST32.的格式来格式化它。当小数位少时,就用BEST12.来格式化它。SYMPUTX减少了数据步类型转换的NOTE。去除了宏变量中不希望的空格。在SAS9.1的SYMPUTX增加了一个可选的第三个语句去指定哪个域(全局或局部)的一个宏变量被替代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
data _null_;
   call symputx("CHARVAR1", "  some text  ");
   call symput ("CHARVAR2", "  some text  ");
 
   call symputx("NUMVAR1", 123.456);
   call symput ("NUMVAR2", 123.456);
 
   num = 12345678901234.123456;
   call symputx("NUMVAR3", num);
   call symput ("NUMVAR4", num);
run;
%put !&charvar1!     !&charvar2!;
%put !&numvar1!      !&numvar2!;
%put !&numvar3!      !&numvar4!;

Output:
NOTE: Numeric values have been converted to
character values at the places given by:
(Line):(Column).
!some text! ! some text !
!123.456! ! 123.456!
!12345678901234.1! !1.2345679E13!

函数MEDIAN,PCTL

MEDIAN函数返回通过函数非缺省值的中位数。如果所有语句都缺失,返回一个缺失值。

PCTLn函数得到一个百分数和一系列值。PCTLn返回非缺失值百分位数相应的百分数形式。N是从1到5的数,用来指定要计算的百分数。如果n没有指定,默认的就是5。

1
2
3
4
5
6
7
8
9
10
11
data _null_;
   x=median(2,4,1,3);  
   y=median(5,8,0,3,4);
   median=pctl(50,2,4,1,3);
   lower_quartile=pctl(25,2,4,1,3);
   percentile_def2=pctl2(25,2,4,1,3);
 
   put x= / y= / median=;
   put lower_quartile=;
   put percentile_def2=;
run;

Output:
x=2.5
y=4
median=2.5
lower_quartile=1.5
percentile_def2=1

用于计算MEDIAN与PCTL函数的公式和PROC UNIVARIATE一样。

FIND

Find函数用来查找一字符串中的另一个字符串。如果配对并返回所在的位置数值。FIND与INDEX类似,然而,FIND有两个以上的语句,开始查找和修改的位置。如果开始的位置参数附值为正,则从左往右查找,如果开始位置参数附值为负,从右往左查找。修改器允许caller在查找前指定一个大小写敏感的查找或在输入语句被整理。下面的例子,用FIND做一个大小写敏感向后查找文本‘CPU’。

1
2
3
4
   source = "Single cpu, Multi-CPU.";
   location = find(source, "cpu", -999, "i");
   put location=;
run;

Output:
location=19

开始位置-999指定了从字符串尾部向后查询,修改符“i”指定了一个大小写敏感的查询。

PROPCASE

在SAS9.1中,PROPCASE函数取的一个字符串,返回除了尾随在分隔符后的所有转化为小写的字符串。跟在分隔符后的都转化成为大写。缺省分隔符为空格,正斜线“/”,连字符“-”,左括号,句号或制表符。PROPCASE的第二个可语句,用来指定可替代的分隔符。

1
2
3
4
5
6
7
8
9
data _null_;
  input name $32.;
  name = propcase(name);
  put name=;
datalines;
MIKE LEARY
tonya bayer-macnie
GlOriA MaCDoNaLd
;

Output:
name=Mike Leary
name=Tonya Bayer-Macnie
name=Gloria Macdonald

注意,Macdonald应该大小写是“MacDonald”.PROPCASE不能确定这种情况下的合适大小写,函数DQCASE可以处理这种类型的名字,不过它是SAS Data Quality产品的一部分(要注册才能用)。

函数SYMEXIST, CALL SYMDEL

函数SYMEXIST以一个宏变量的名称为参数,如果这个宏变量名存在则就返回1,不存在则就返回0。CALL SYMDE以一个宏变量名称为参数,如果宏变量存在,就会被删掉。下面例子先看宏变量是否存在,然后输出MVAR1的值,并删掉变量:

1
2
3
4
5
6
7
8
9
%let mvar1 = Inventory;
data _null_;
  if symexist('mvar1') then do;
    str = symget('mvar1');
    put str=;
    call symdel('mvar1');
  end;
run;
%put &mvar1;

Output:
str=Inventory
WARNING: Apparent symbolic reference MVAR1
not resolved.

函数CALL  ALLPERM,CALL RANPERM,CALL  RANPERK

CALL程序计算输入变量的排列。ALLPERM计算所有输入变量的排列。RANPERM随机排列输入值。RANPERK随机排列输入变量,并返回n个观测值中的k个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
data _null_;
  array vals[3] $ 3 ('PBS' 'NPR' 'PRI');
 
  do i = 1 to fact(dim(vals));
    call allperm(i, of vals[*]);
    put i vals[*];
  end;
 
  put;
  seed = 44;
  do i = 1 to 4;
    call ranperm(seed, of vals[*]);
    put i vals[*];
  end;
run;

Output:
1 PBS NPR PRI
2 PBS PRI NPR
3 PRI PBS NPR
4 PRI NPR PBS
5 NPR PRI PBS

1 PRI NPR PBS
2 PBS PRI NPR
3 PBS NPR PRI
4 NPR PBS PRI

PANPERK的例子能在在线文档中找到,函数FACT计算阶乘。在例子中用做计算ALL PERM执行排列的个数。

执行性能的改进

函数 LENGTH和 TRIM

函数LENGTH和 TRIM在所有主机上进行了优化使得末值查询更有效。不同于从最后开始一次一个的查找第一个非空字符,64位整数用来向后每次按8个字节。用64位整数比较了需要查找最后一个非空字符的次数。

当含许多空格的长值时,提高的速度最为明显。用LENGTH和TRIM函数可以提高4倍的速度。

没有溢出的文件视图

一个数据步以随机,two pass或by group rewind的模式打开时,视图打开一个将包括所有已输出的观测值的spill文件。Spill文件被用来提取可能被要求的前一个观测值。用视图返回大量数据时,需要大量硬盘空间。如果硬盘空间不够时就会产生问题。

当一个视图用two pass模式打开时,spill文件不需要。相反,视图可以为每个pass重新启动,当一个视图以BY group rewind模式打开时,就会产生一个spill文件。缺省的spill=YES,在以前的SAS版本中会出现同样的spill。
————(不知所云,这里看不懂)————-

数据步语言扩展

integer ranges和数组的‘in’查找

可用IN操作符查找integer ranges和数组。用ARRAY和RETAIN语句在integer ranges内初始一个数组。一个integer ranges从M到N(M,N包括在内)用M:N来表示。下面例子给出了怎么用integer ranges。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
data _null_;
   array arrA[10] (1:10);
   array arrB[10];
   retain arrB (2*1:5);
 
   put 'arrA=' arrA[*] / 'arrB=' arrB[*];
 
   x = 5;
   if x in (1:10000) then  
      put 'X in range 1-10000';
 
   if x in (1 2 5:10) then
      put 'X in 1, 2, 5-10';
 
   if x in arrA then
      put 'X in arrA';
run;

Output:
arrA=1 2 3 4 5 6 7 8 9 10
arrB=1 2 3 4 5 1 2 3 4 5
X in range 1-10000
X in 1, 2, 5-10
X in arrA

当用IN操作符查找interge range,就会进行整数校验和比较上下限。

PUTLOG

语句PUTLOG和语句PUT功能有点相似。不过所有输出都被送到SAS log而非当前目标文件中。PUTLOG语句在当不知道macro将在哪里用到和一个非日志外部目标文件是否最近的macro中使用方便。如果第一个字符输出为“NOTE:”,“WARNING:”,或“ERROR:”时,在日志中的输出会以适当的颜色出现。

1
2
3
4
5
6
7
8
9
10
11
12
data _null_;
   file tmp;
   input lastName $;
   put lastName;
   if lastName =: 'J' then
      putlog 'NOTE: J last name, ' lastName;
datalines;    
Harris
Jones
Barber
Johnson
;

Output:
NOTE: J last name, Jones
NOTE: J last name, Johnson
NOTE: 4 records were written to the file TMP.
The minimum record length was 5.
The maximum record length was 7.

SAS宏语言选项

一些新的SAS宏语言系统选项不属于DATA步的改进。但是因为DATA步可能发现它们是有用的,所以也应该在这里提一下。

选项MCOMPILENOTE=[NONE|ALL|NOAUTOCALL]

当选项MCOMPILENOTE起作用时,如果宏成功编译,SAS将输出一个NOTE到log中去。这个选项的有效值用NONE,ALL和NOAUTOCALL,NOTE是缺省的,ALL将在宏编译完成后显示。NOAUTOCALL在非自动调用宏被编译时输出一个NOTE。

1
2
3
4
options mcompilenote=all;
%macro mymacro;
   data _null_; x=1; put x=; run;
%mend;

NOTE: The macro MYMACRO completed compilation
without errors.
选项MPRINTNEST|NOMPREINTNEST

当用选项MPRINTEST和MPRINT时,用MPRINT日志输出显示嵌套信息。下面的例子,宏MAC1嵌套在MAC2内,当MAC2在MAC2中扩展时,MPINT用MPRINT(MAC2,MAC1)来提示。

1
2
3
4
5
6
7
8
9
10
11
12
13
options mprint mprintnest;
%macro mac1;
   put 'mac1';
%mend;
 
%macro mac2;
   %mac1;
   put 'mac2';
%mend;
 
data _null_;
   %mac2;
run;

MPRINT(MAC2.MAC1): put ‘mac1’;
MPRINT(MAC2): ;
MPRINT(MAC2): put ‘mac2’;
选项 MAUTOLOCDISPLAY|NOMAUTOLODISPLAY

使用选项MAUTOLOCDISPLAY在使用自动宏变量时自动调用宏的源地址作为一个NOTE输出。这有利于用自动宏语言调试路径问题。

总结

SAS9.1中增加了新的功能和作了改进,可以简化编程,提高执行速率和写出新的风格的程序。Perl regexp和哈希表组件的增加简化了很多原来的数据步编程,并提高了执行速率。大多新的函数简化了数据步中的通用操作。视图中新spill=数据集选项帮助减少和限制了一个视图所需要的硬盘要求。所有新功能和改进来自用户的建议。我们欢迎您提出对功能的要求。请拨打技术支持电话:*¥#·*()……:

原创文章: ”SAS9数据步的新发现 旧稿新编 2006“,转载请注明: 转自SAS资源资讯列表

本文链接地址: http://saslist.net/archives/141


Reader's Comments

  1.    

    谢谢,辛苦了。

    Reply to this comment

Leave a Comment