(2016)[SAS数据处理] 利用二分法思想加速DATA步数据截取操作 [v1.02]

来源:互联网 发布:淘宝卖家流量钱包 编辑:程序博客网 时间:2024/05/14 16:24

在使用SAS软件进行大规模数据处理(large-scale data processing)时,我们常常会遇到以下的数据分布场景:

  • 待处理的数据集(dataset)至少具有10亿级别的观测数(N)和若干个字段(常在20个到200个不等);
  • 数据集已经预先根据某个字段(column)排序过(通常在数据生成过程中进行),例如,根据日期、时间字段等进行升序排列;
  • 业务流程只要求获取满足特定(时间)范围条件的局部数据,而非整个数据集。例如,只选取2015-12-01到2015-12-31日期范围内的数据,或者只截取2016-01-01号的数据。

针对此场景,比较容易想到的是使用DATA步中的WHERE语句进行截取数据;但是其在截取大规模数据时,这种方法显得耗时、低效(因为WHERE需要对每一条观测进行条件比较)。如果使用PROC SQL过程,依然涉及到WHERE语句的使用,且效率更低。为此,基于经典的二分法查找(binary search)思想,我们(HXC + DQQ)编写了一段简易的SAS宏(MACRO)程序,以提高SAS程序的运行效率。

现假设我们面对的是一个具有10亿观测数、50个字段的数据集(且数据集已经根据[date]字段升序排列),我们的需求是截取2015-07-31号的数据,以备后期的数据分析使用(整个数据集的时间跨度为2008-01-01到2016-01-07)。如果在DATA步中使用WHERE语句截取以上的数据集,可能至少需要1个小时;而使用这个简易的SAS宏程序,一般在5分钟之内即可完成任务。但为什么能够取得如此显著的提速呢?主要基于以下两点理由:

  • 1、相对于WHERE语句采用的线性搜索,其时间复杂度为θ(N);而二分法的时间复杂度只为θ(〖log〗_2 N)。例如,在具有10亿观测数的数据集中根据条件定位一条记录,二分法在最坏的情况下也只需要大约30次比较操作;而线性搜索在最坏的情况下却需要10亿次比较(性能差异就是如此明显)。一般而言,数据集的观测数(N)越多,二分法的优势就越明显。但特别需要注意的是:对于二分法而言,要求必须预先对数据集的条件字段进行排序!换言之,二分法不能处理没经过排序的数据集。
  • 2、利用DATA步中的FIRSTOBS和OBS语句(需要组合使用),获取大型数据集中的任意一条记录,所需的时间成本基本上可以完全忽略(这类似于RAM随机访问模式,而非耗时的顺序访问模式)。具体的使用方式,可参见下面的SAS宏程序;或者参见图书《SAS编程与数据挖掘商业案例》(作者:姚志勇)中给出的具体原因解释和实践。

与经典二分法不同,这里的宏程序所面对的是略微不同的需求场景:所要搜索的观测可能不唯一(尤其是在大数据集中存在着大量满足搜索条件的观测),需要找出满足搜索条件的观测范围(即开始节点和结束节点);而经典二分法一般只要求返回一条满足搜索条件的观测即可。虽然the deferred detection algorithm也可以近似地处理以上的需求场景;但由于研究较少,本博客将不予讨论。在此,先给出经典二分法的Python极简易版本代码(以对比冗繁的SAS代码):

基于递归的经典二分法Python代码:
这里写图片描述

基于循环的经典二分法Python代码:
这里写图片描述

不同于以上简洁的Python代码,SAS宏程序要显得冗余许多(但能够处理更一般的情况),其具体的SAS代码如下所示:

/* ******* ******* ******* ******* ******* ******* ******* * * *******       满足特定格式数据的定位截取程序      ******* * 执行前提条件:数据集dataset已按key_col列升序排列 * DESIGN BY: HXC * MODIFY BY: DQQ-077 * VERSION: 29JAN2016 * ******* ******* ******* ******* ******* ******* ******* */options compress=yes mprint;%macro sub_dataset(dataset= /* 输入数据集名称 */        , out= /* 输出数据集名称 */        , start= /* 截取开始节点 */        , end= /* 截取结束节点(满足条件:end >= start)*/        , key_col= /* 数据集dataset已按key_col列升序排列 */        , keep_cols=0 /*数据集读取字段,0表示读取所有字段 */    );    /* 获取数据集总观测数n */    data _null_;        set &dataset. nobs=n;        call symput("n", n);        stop;    run;    /* 基于二分法寻找start的最早位置 */    %let range_start = 1;    %let range_end = &n.;    %let pos_start = 0;    %do %while (&range_start. <= &range_end.);        %let range_middle = %eval(&range_start. + (&range_end. - &range_start.) / 2);        data _null_;            set &dataset.(firstobs=&range_middle. obs=&range_middle.);            call symput("range_middle_key", &key_col.);            stop;        run;        %if &range_middle_key. > &start. %then %do;            %let range_end = %eval(&range_middle. - 1);        %end;        %else %if &range_middle_key. < &start. %then %do;            %let range_start = %eval(&range_middle. + 1);        %end;        %else %do;            data _null_;                set &dataset.(firstobs=&range_start. obs=&range_start.);                call symput("range_start_key", &key_col.);                stop;            run;            %if &range_start_key. = &start. %then %do;                %let pos_start = &range_start.;                %let range_end = %eval(&range_start. - 1);            %end;            %else %do;                %let range_end = &range_middle.;            %end;        %end;    %end;    /* 基于二分法寻找end的最后位置 */    %let range_start = 1;    %let range_end = &n.;    %let pos_end = 0;    %do %while (&range_start. <= &range_end.);        %let range_middle = %eval(&range_start. + (&range_end. - &range_start.) / 2);        data _null_;            set &dataset.(firstobs=&range_middle. obs=&range_middle.);            call symput("range_middle_key", &key_col.);            stop;        run;        %if &range_middle_key. > &end. %then %do;            %let range_end = %eval(&range_middle. - 1);        %end;        %else %if &range_middle_key. < &end. %then %do;            %let range_start = %eval(&range_middle. + 1);        %end;        %else %do;            data _null_;                set &dataset.(firstobs=&range_end. obs=&range_end.);                call symput("range_end_key", &key_col.);                stop;            run;            %if &range_end_key. = &end. %then %do;                %let pos_end = &range_end.;                %let range_end = %eval(&range_start. - 1);            %end;            %else %do;                %if (&range_end. = %eval(&range_middle + 1)) %then %do;                    %let pos_end = &range_middle.;                    %let range_end = %eval(&range_start. - 1);                %end;                %else %do;                    %let range_start = &range_middle.;                %end;            %end;        %end;    %end;    /* 截取满足条件的数据 */    %put pos_start=&pos_start.;    %put pos_end=&pos_end.;    %if (&pos_start. = 0) or (&pos_end. = 0) %then %do;        %put "!!!ERROR!!!error!!!ERROR!!!";    %end;    %else %do;        %if &keep_cols. = 0 %then %do;            data &out.;                set &dataset.(firstobs=&pos_start. obs=&pos_end.);            run;        %end;        %else %do;            data &out.;                set &dataset.(keep=&keep_cols. firstobs=&pos_start. obs=&pos_end.);            run;        %end;    %end;%mend sub_dataset;

有趣的是:以上的宏程序可以看做是对经典二分法的直接扩充;其整体的时间复杂度依然为θ(log2(N))。以上的SAS宏程序对于特定格式的数据集进行定位、截取操作是非常有效的。这也再次在实践上验证了:算法设计的美妙与意义(即通过时间复杂性理论指导算法设计)。

前路漫漫,上下而求索之!

博客相关信息:
SAS宏程序设计:HXC,修改、规范化:DQQ;
博客编写:DQQ,审阅:GJ、HXC,修改:GJ。

如有任何疑问或交流,欢迎联系:DQQ ( duanqq@jointstarc.com )。本博客所涉及到的所有SAS/Python源程序、以及博客的pdf/word版本,都可通过E-mail联系获取(FREE! JUST FOR FUN!)。

[注]: SAS是美国SAS公司的商业产品和商标;Python为有趣的开源编程语言。

参考资料:
[1]. https://en.wikipedia.org/wiki/Binary_search_algorithm
[2]. http://www.mathmeth.com/wf/files/wf2xx/wf214.pdf
[3]. http://www.codecodex.com/wiki/Binary_search
[4]. http://www.sas.com/en_us/software/enterprise-guide.html
[5]. https://www.python.org/
[6]. http://item.jd.com/1060284.html

0 0
原创粉丝点击