2010年3月25日 星期四

R对象结构,用.Call的调用方式更灵活地写R-package

虽然R的使用资料很多也很齐备,但R的开发资料却少得可怜,写一个能管点用途的package也得大费周章。于是蜀中无大将、廖化当先锋,官方文档那区区130页不齐备而又晦涩的<Writing R Extensions>就成了红宝书,天天抱着啃,啃得呕心呖血再加上翻着几个现成package的源代码看,终有小小成。

R与C的调用接口有好几种,以前介绍过的.C形式是一种,此外还有.Call形式和.External形式。.Call使用最为广泛,功能也比.C形式大很多,因为使用它可以直接操作R的数据结构,可以实现在C/C++中操作R的数据及调用R的函数,然后封装为包供R使用。

R的所有数据类型(无论数据或类或函数)都是对象,都统一定义为结构SEXPREC,由指针SEXP(不要想歪,我也不明白作者为何要取这个名字)所指向。这个结构在Rinternal.h中定义为:

typedef struct SEXPREC {
SEXPREC_HEADER;
union {
struct primsxp_struct primsxp;
struct symsxp_struct symsxp;
struct listsxp_struct listsxp;
struct envsxp_struct envsxp;
struct closxp_struct closxp;
struct promsxp_struct promsxp;
} u;
} SEXPREC, *SEXP;

它包含一个HEADER及一个UNION,HEADER又是由如下宏定义:

#define SEXPREC_HEADER \
struct sxpinfo_struct sxpinfo; \
struct SEXPREC *attrib; \
struct SEXPREC *gengc_next_node, *gengc_prev_node

它包含一个32位的sxpinfo的信息头,一个指向属性的指针,两个指向前后节点的指针构成双向链表。

sxpinfo这个信息头的定义如下:

struct sxpinfo_struct {
SEXPTYPE type : 5;
unsigned int obj : 1;
unsigned int named : 2;
unsigned int gp : 16;
unsigned int mark : 1;
unsigned int debug : 1;
unsigned int trace : 1; /* functions and memory tracing */
unsigned int spare : 1; /* currently unused */
unsigned int gcgen : 1; /* old generation number */
unsigned int gccls : 3; /* node class */
}; /* Tot: 32 */

简单说几个主要字段的作用:type表示数据类型,如数字型、字符型、列表型等等;named控制引用时拷贝与否;mark为垃圾回收所用(GC)。

使用.C调用方式来做R的扩展是最简单的方式,因为不需要理会SEXP这些复杂的R结构,只需要把数据拆解为一个个向量传递给C函数即可,详情参见我以前的描述。如果.C可以实现的功能,倒不必要一定要折腾到.Call,因为后者要牵涉到很多的R API,这些API还都没有文档说明其作用及调用方式,只能根据<Writing R Extensions>以及参考一些包的用法来猜测着使用。不过好处是.Call的功能更丰富而且稳定,.C方式的指针纵横交错,极易使程序崩溃。

这里先用官方文档里的一个例子拿出来加以解说,阐述个大体流程,以后实践深入再结合一些经验来谈谈技巧。

在C里处理R对象,以下是一个计算外积的例子:

在C文件中的源代码为:

#include
#include // 所有R API都定义在这里,只可惜没有太多说明,需要细细看
SEXP out(SEXP x, SEXP y) // 无论参数,还是返回值,都要定义为SEXP指针类型
{
int i, j, nx, ny;
double tmp, *rx = REAL(x), *ry = REAL(y), *rans; // REAL函数取得SEXP结构中的数据指针(如果是整型,则用INTEGER),以方便对数据的操作
SEXP ans;
nx = length(x); ny = length(y);
PROTECT(ans = allocMatrix(REALSXP, nx, ny)); // 分配空间,返回一个SEXP指向的对象,REALSXP是指实数类型,这里生成一个实数矩阵。PROTECT保护该内存不被垃圾回归机制回收。
rans = REAL(ans);
for(i = 0; i < nx; i++) {
tmp = rx[i];
for(j = 0; j < ny; j++)
rans[i + nx*j] = tmp * ry[j];
}
UNPROTECT(1); // 放弃对以上PROTECT声明内存段的保护,参数1指示放弃1次,次数必须跟上面使用PROTECT次数一致。
return(ans);
}

假如文件存储为out.c,则用R CMD SHLIB out.c命令可以把源代码编译为out.so共享库。然后在R里使用如下代码即可使用:

a=1:3
b=4:6
dyn.load("out.so")
.Call("out", a, b)

把R的dataframe当成数据库来查ZZ

从cos上看到一个很好玩的包:sqldf,可以把一个或多个dataframe组成数据库,然后用SQL语句来查询,这样对于一些查询计算会非常方便。其原理是先把dataframe转成SQLite的表,然后进行查询。

由于今天早上起来r-project主站无法连上,只能用镜像网站下载安装了:install.packages(“sqldf”, repos=”http://mirrors.geoexpat.com/cran/”)

使用,假设工作区里有两个dataframe:x和y,那么可以用如下方式查询

sqldf(’select * from x limit 10′)

sqldf(’select distinct id from y where status=”P” limit 10′)

sqldf(’select status, count(1) from y group by status’)

sqldf(’select y.name from x, y where x.id=1 and y.rid=x.id’)

是不是很像数据库查询,这时你的工作区就是一个数据库了。

注意:有时候表实际的列名会与dataframe的名字不一致,也即x[1:5, ]和sqldf(’select * from x limit 5′)看到的字段名是有可能会不一样的,这种情况下,以后者为准。比如我x中有一个字段user到了sqldf就变成了user__1,这时只能用user__1来作为字段名进行查询,不能再用user。最好先用sqldf(’select * from x limit 5′)确认一下具体的字段名称。

速度如何?经我的试验,10万的数据都是瞬间完成,在100万的量级都还是很快的,无论对哪个字段作为查询条件都很快,估计不是因为有索引,而是采用高效的向量化运算,并且数据都在内存里的缘故。因为即使不用sqldf,直接对data.frame作判断查询,速度也不慢,而sqldf不过作了一个漂亮的sql包装。对于更大的数据集,要考虑到内存的占用。所以如果有一个或几个可以载入到内存的数据集(又或是一些临时的结果),想做一些简单的查询计算,用sqldf就可以很快地得到结果(而且语法很简洁)。当然前提是你必须得懂简单的SQL。

不足之处在于需要依赖tcltk,这是个图形相关的包,所以在非图形界面的服务器上不会被编译安装。其实很让人费解为什么需要依赖图形包。

更新:今天收到益辉同学发来R-dev邮件组的一篇讨论帖,说sqldf并非直接依赖tcltk,而是为了加快查询而依赖gsubfn,而gsubfn依赖tcltk的缘故。我查了一下这两个包的说明,确实如此。事实上,gsubfn并非一定得依赖tcltk,后者只是起一个加速的作用,所以开发者们也在考虑是否应该把gsubfn对tcltk的depend关系改为suggest关系。我很支持,因为我在gentoo服务器上装的R是不带X进行编译的,所以不能安装tcltk。

reference:http://www.wentrue.net/blog/?p=453

R package的最简单制作ZZ

R的强大之处在于它包罗万有的包,几乎任何领域都可以从CRAN里找到你所需要的实现。

如果有一些功能你希望自己来实现,又或者是用别人的包用多了,希望自己做一个,那么这是一个简单的向导。可以告诉你怎么去制作一个最简单的R包,如果你需要用到复杂的功能,可以再深入地查看资料,我以后也会根据自己的实践深入写一写。

其实最原始最详尽的R包制作指南应该是官方文档<Writing R extensions>,但看过的人无一不觉得这是个累赘,它面面俱到地说了所有的事情,但令初学制作者无所适从。那么,摒弃里面的大部分内容,你真正需要的东西是这样的(以下说明基于linux平台,windows用户也可参考):

准备工作指定两个目录,一个是工作目录mydir(/home/wentrue/work),一个是包目录mylib(如/home/wentrue/Rlibs)。前者是你写R代码、运行R console的地方,后者是安装包的地方。
默认情况下在mydir是找不到mylib下的包的,因为mylib不在包的搜索路径里,解决这个问题只需要在mydir新建一个文件.Rprofile文件,里面写上:.libPaths(“/home/wentrue/Rlibs”)即可。这样在mydir运行R脚本或启动R终端,mylib就会被添加到包搜索路径中。
添加目录与文件在mydir新建一个目录,名为once,作为包名。然后生成一些目录,目录树结构见下:

$ tree once
once
|– DESCRIPTION
|– R
| |– test.R
| `– zzz.R
|– data
|– man
`– src

目录说明:必需的是DESCRIPTION文件、man目录和R目录,剩下的都是可选的。DESCRIPTION文件描述包的meta信息,后面会有一个附例;R目录下面存放R脚本文件,里面的函数可导出作为包函数库提供给外部使用;如果要在包里放一些试验数据,可以放在data目录里,常用是以csv格式存放,在R终端里data(***)可以载入,这里留空;man目录是R的帮助文档,即?xxx时显示的那些,有一定的格式要求,这里也留空;src存放c/c++/fortran源代码,必须同时放置Makefile或Makevars文件指导编译程序工作,这里留空。
作为试验,我在test.R里写了一个简单的函数,内容见下,里面的oncetest函数在once被装载时就可以被R直接调用。

oncetest <- function(x, y)
{
return(x*y)
}

zzz.R可以在载入包时做一些事情,这里留空。
一个简单的包就这样做好了,是不是很简单。如果有其它需要,只要往R目录或src目录添加文件就可以了。
安装与试验(以下步骤都在mydir目录进行)运行R CMD check once,R会检查包看是否能正确安装(并未实际安装),如果不成功会返回ERROR,并有出错信息。这个实验里会有一些warning,是因为一些目录留空的缘故,不用管它。
运行R CMD build once,会生成一个once_0.1.tar.gz安装包,其中的数字是我在DESCRIPTION里写的版本号。
运行R CMD INSTALL -l /home/wentrue/Rlibs once_0.1.tar.gz,就可以把包安装到mylib里。
运行R,进入R终端;library(once),载入刚制作的包;search(),可以看到once已经被载入。
在R终端运行oncetest(2,3),返回6,试验成功。
附:我的DESCRIPTION文件内容

Package: once
Version: 0.1
Date: 2009-07-31
Title: Once Test
Author: Guozhu Wen
Maintainer: Guozhu Wen
Depends: R (>= 1.9.0)
Description: A Once Test Description
License: GPL version 2 or later

参考:http://www.maths.bris.ac.uk/~maman/computerstuff/Rhelp/Rpackages.html

补充:最典型的一个应用是,你已经积累下来一批自己写的常用的R函数,但又不想每次都一个个文件source进来,那么,把它们分类做成package吧,就可以像平常用其它package一样使用这些函数了,发布给别人用也方便得多。

reference:http://www.wentrue.net/blog/?p=395

R程序调试–DEBUG R

1、browser(): 在脚本文件适当的位置插入browser(),重新载入模块,运行,程序会在该行中断。命令:n: step; c: continue; where: print the call back; q: quit; enter: last command;

2、traceback(): 运行最后的出错信息;

3、定义一个全局变量捕捉中间数据信息,比如一个全局变量a,可以在函数内a<<-***来给全局变量赋值;

4、设定options(warn=1),即时提示warning信息,不设置则警告信息会在程序执行完毕之后才会输出;

5、try() 和tryCatch()是两个很好的处理error的函数;

6、warning()输出一个警告信息,stop()函数终止程序运行并退出,geterrmessage()得到最后一次出错信息

reference: http://www.wentrue.net/blog/?cat=45

VIM编辑R脚本配置ZZ

我的R脚本通常用*.R作为后缀,用VIM编辑R文件时有时会无法正确检测出文件类型来,从而无法进行正确地颜色高亮。另外,默认的配置用来编辑R脚本也不够用,翻了翻资料,作了一些简单的配置。

在我的.vimrc文件里加了如下三行配置:

imap <-
autocmd FileType r set fdm=indent
au BufNewFile,BufRead *.R set ft=r


说明:

1、第一行是在“输入模式”下作了一个映射,把ctrl+b映射为” <- “这几个字符(指代空格符),因为R里推荐的赋值操作符是”<-”,打起来很麻烦,虽然我常用的是”=”,但有时还是用得上的。

2、第二行设定当文件类型是R时,把fdm这个option设置为indent,这样可以实现代码折叠,即按z+c就可以把大括号中间的部分折叠起来,编辑多函数的文件时非常方便。用:help可以查看到autocmd这个命令的定义为:autocmd [group] {event} {pattern} [nested] {cmd},即在{event}事件触发时,如果文件名符合{pattern}模式,则执行{cmd}的命令。具体在这里就是当FileType r这个事件被触发时,执行set fdm=indent命令。

3、第三行的au是autocmd的缩写,功能完全一样。这里的定义是指:当BufNewFile,BufRead事件被触发时,如果文件名为*.R,则set ft=r,自动设定好FileType,这就实现了根据文件名后缀自动检测文件类型。其它文件后缀与文件类型的绑定也可以通过这种方法来实现。

4、说到文件类型的检测,还得补充几句,通常你需要默认打开文件类型检测,即设定filetype plugin indent on,可以在vim里输入:filetype查看是否已经开启。除了上述提到的在.vimrc中设定之外,还有两种方法可以设定文件类型,一种是在文件的开头或结尾加上” vim: ft=c “的注释,注意两边的空格,还有不同类型的文件有不同的注释方式;另一种方法是打开文件后直接通过:set ft=r来设定。

简简单单的三行配置就使得自己的工作环境大大改善,很强大吧。


Reference: http://www.wentrue.net/blog/?p=625