R 语言摘记(R-lang, Part II)

R 的 OOP 实现是比较怪异的,它依赖于 .Generic、.Class、.Method 和 .Group 这四个对象,对象使用的方法(generic function)依赖于 UseMethod() 和 NextMethod() 这两个 dispatching 函数。一般我们需要一个接口,如 mean() 函数,对于 vector 和 data.frame 应该是两个不同的函数实现,因此我们需要定义一个 generic function,然后为不同的类实现各自的版本。

实现 generic function 的时候,我们就需要使用前面所说的对象和方法了。如 mean 就应该定义为 function(x, …) UseMethod( “mean” ),那么在调用 mean() 的时候,.Class 会被设置为 class(x),这往往是一个类的继承列表,最前面的是 class hierarchy 最下面的,.Generic 被设置为调用的方法名 mean,然后我们根据这个去搜索对应的函数,搜索的次序是寻找 mean.class 这样的函数,class 从最具体的类开始,如果没找到,就会寻找 mean.default(),我们可以通过 getAnywhere() 看看系统自己的 mean(),不难发现 mean() 本身是一个 generic function,为 vector 使用的是 package:base 里面的 mean.default(),而为 data.frame 调用的则是 package:base 中的 mean.data.frame()。

需要注意的是,UseMethod() 之后函数并不返回到 generic function,因此在 generic function 调用 UseMethod() 之后的语句没有任何效果。前面另外两个对象,.Method 一般是被调用的方法名,另外如果是通过内部调用的可能有 .Group 对象。

为了实现所谓的继承类函数调用,如子类的对象被 generic function 调用后,可能需要同类的某些其他方法,这时使用 NextMethod(generic, obj) 调用会根据当前 .Generic 或指定的 generic 和 .Class 寻找对应的函数调用。

方法存在一定的分组,现在没有语句可以操作分组,但是常用的三个分组有 Math、Summary 和 Ops。在对应的方法里面会自动设定 .Group 对象。

下面我们将讨论 R 语言里面 functional programming 的一部分,重要的函数就是 quote(),这是一个 special 对象,它的作用就是产生没有 evaluated 的 language 对象,比如 quote(1+2) 返回的是一个 language 对象,其 mode 是 call,这种对象可以用类似 list 的方式操纵,如果里面有 =,如 exp<-quote(plot(x = age)),则可以用 exp$x 获得 age,又或者通过 exp[[ ]] 这种方式。我们可以用 as.name() 把一些字符串转换成为对应的 symbol 替换某些 expression 里面的操作。

可见使用 quote() 之后,expression 已经被 parse 过了,可以用 deparse() 将其还原为字符串。这个最常见的应用就是 plot() 里面的 xlab 这类,我们用 substitute 获得 promise 里面的 expression,然后 deparse() 成为字符串就可以画到图上了。

我们可以用 eval() 执行一段 expression,但是需要指定 environment,而使用 eval.parent() 可以在当前 frame 里面执行。

与 quote() 不同的是 expression(),这产生的是 expression 对象,因此只能对应一个表达式,且 eval() 之后还是 exzpression。

在一个函数里面通过 sys.call() 可以获得对应的 environment,这常在调试中应用。更常用的是 match.call(),这样获得的是匹配好的参数列表,那么当我们需要从这个函数调用一个参数一样的函数就比较方便。我们通过操纵 language 对象改变其调用函数即可。

R 与 OS 交互的函数通过 Sys 开头的函数,如Sys.get/putenv(),Sys.get/putlocale(),Sys.time() 等。对文件操作可以用 file.copy()、file.rename() 等。使用 .C() 或者 .Fortran() 我们可以使用编译好的 .so 文件,而 .Call() 和 .External() 允许我们用编译好的 C 代码操纵 R 里面的对象。

R 的异常处理比较简单,主要有 stop()、warning() 和 on.exit()机制,这跟原来 Matlab 的 error、warning 很像,只是 Matlab 后面提供了 try catch 机制。R 的调试主要是通过 browser() 和 debug()/undebug(),trace()/untrace()。

Advertisements
R 语言摘记(R-lang, Part II)

R 语言摘记(R-lang, Part I)

R 语言某些地方可能和 C 类似,但是用过一段时间你就会发现其实它和 Lisp 这类 functional language 才是一家人,它们都允许对“语言”本身进行操作。因此为了更好的理解 R,我们其实要理解 R 的解释器怎么看待我们的输入的,即看看词法和语法分析。

在 R 里面有 typeof() 函数(看看为什么叫对“语言本身操作”),可以返回输入里面一些元素的类型,这和 mode() 返回的是不一样的

  • NULL 就是 NULL,没有 type 也没有可更改的属性,可以用 is.null() 检测。
  • symbol 变量名,任意 R 对象的名字通常是一个 symbol,可以通过 as.name() 或者 quote() 创建。
  • pairlist 主要是内部使用的 pairlist 对象,如 .Options 里面的,处理起来和 list 类型类似。
  • closure 函数,含有三个成员,正规参数 list,body 和一个 environment,参数列表里面是 symbol 或者 symbol=value 的形式,或者是 …。body 是 parsed 的 R 表达式,也可以是单独一个 symbol 或者常数。函数的 enviroment 就是函数被调用时创建的上下文环境。这三个部分我们可以用 formals()、body() 和 environment() 获得和赋值。
  • environment 环境,可以认为 environment 含有两部分,一个是 frame,即 symbol-value 对,用于查询,一个是指向上一级 environment 的指针。我们可以用 emptyenv() 创建空的 environment,使用 baseenv() 获得最上面一级的 environment。通过 ls() 可以列出某个 environment 里面的 symbols,get()/assign() 获得该 symbol 的值或者对它进行赋值。通过 parent.env() 获得上一级 environment。
  • promise,是 lazy evaluation 产生的对象,它含有三个部分,值、表达式和对应的 environment,当函数调用后,首先进行参数匹配,然后其值实际上尚未获得,直到需要获得的时候才在对应的 environment 里面将 expression evaluate,产生值供以后使用。可以用 substitute() 函数(这是因为它是 special 类型,因此这个不会直接 evaluate 这个 promise 的 expression 部分)将 expression 部分抽取出来,这样程序员其实既能访问 expression 本身也可以获得其值,比如 plot() 函数里面就利用了这一点,默认的 sub、xlab、ylab 都是这样设定的。另有 delayAssign() 函数可以创建 promise 对象。但是实际上没有 R 代码能检测一个对象是不是 promise。
  • language,R 语言的结构。一般分为 call、expression 和 name。不过这里的 expression 应该和下面的区分。
  • special,不对参数取值的内部函数。
  • builtin,对参数取值的内部函数。和 special 类似,可以认为是一种函数,但是是没有那三个部分的,可以通过 typeof() 将三者区分开来,比如 is.null() 就是 builtin,lm() 就是 closure,special 的一个例子就是前面的 substitute()。
  • char,数量形式的字符串对象,仅在内部使用。
  • logical,保存 bool 值的 vector。
  • integer,保存整型的 vector。
  • double,双精度浮点 vector。
  • complex,复数类型的 vectoer。
  • character,字符类型的 vector。
  • … 可变长度参数,可以通过 list() 将其转换成为 list,然后就可以用 R 代码访问了。
  • any 对任意类型都能匹配上的特殊类型,很少使用,如 as.vector(x, “any”) 表达的意思是说不需要做类型转换。
  • expression,表达式,特指语法正确的一个或者多个 R 表达式,与 language 不同之处主要是 expression 可以看作是 language 中 expression 的 list,语法正确,且已经被 parse 了,只是没有 evaluate,通常可以用 eval() 函数执行,而 language 对象也许已经被 evaluate 过了。
  • list 就是 list。
  • bytecode,内部使用的 bycode 类型。
  • externalptr,指向外部对象的指针。
  • weakref,weak reference 对象。
  • raw,含有数据的 vector,是什么不关心。
  • S4 是一个 S4 对象。

这就是 R 解释器看到的基本元素。我们可以比较一下 mode()、typeof() 和 storage.mode() 判别类型的不同之处。

R 中除了 NULL 以外,所有的对象都有属性,前面我们也知道可以用 attributes() 和 attr() 访问或者添加属性。前面我们也知道 matrix 或者 array 只是 vector 含有 dim 属性,另外可选的属性还有 dimnames。属性的作用就是形成 R 里面的类结构。下面我们举出一些常见的属性:

  • names,这个常见于 list 对象,表示每一个元素都是一个单独的 vector 或者 list,可以用于索引,比如 [“key”] 和 $key 方式。
  • dim,用于实现 array 和 matrix,储存方式和 Matlab 一样,也是 column-major。
  • dimnames 用于在输出的时候更加漂亮,这是一个字符串的 list。
  • class,是一个字符串,用于实现 OOP 里面的类方法,但是与 C++ 等语言不同,R 不做类型检查,,需要转换的时候一般 as.* 就好了。
  • tsp 属性是时间序列的一些相关信息,如开始、终结、频率等。

一个对象被复制的时候其属性是否应该被复制是一个复杂的问题。常见的规则有标量函数应该保持属性,两元操作应保证较长的操作数的属性,取子集应该修改 names、dim 或者 dimnames 等属性,但是保留其他的,对子集赋值应该保持属性不变,转换应该丢弃属性。

我们前面接触到的 factor 是含有 levels 和 class 为 factor/ordered 的 vector,另外还可能有 contrast 属性。另外 data.frame 我们也知道是 list 的一种,除了有 names 以外,还可以有 row.names。

下面我们来看看 R 是如何 evaluate 表达式的。常数的值是已知的,对变量赋(常数)值时,对应的 symbol 和 value 被添加到对应的 environment 里面,引用时通过查询获得其值。函数调用是通过 search() 给出的环境搜索获得。有两种函数需要区分,我们通过 class() 获得的类名以及通过 class() <- 修改类名对应的是两个不同的函数,后者其实调用的是 class<-() 这个函数。运算符对应着函数,一般是写成 `op`(),如 1+1 对应 `+`(1, 1)。R 里面的运算符除了 +-*/ 还有 ^(指数),%% 取模,%/% 整数除法,%*% 矩阵乘法,%o% 外积,%x% Knronecker 成绩,%in% 匹配,逻辑运算里面常用的 !<><=>= & | (& 和 && 不同在于后者不是 vectorized)以外,赋值有 <- 和 ->,list 的操作 $ 也是一个运算符。索引使用的 `[` 和 `[[` 对应的 <- 版本也是函数。

前面讲 flow control 的时候没有提到 switch,它更像一个函数,switch( statemane, list),如果 statement 的值是整数,如果是 list 长度以内的则返回 list 里面对应项的值,否则返回 NULL。又或者 statement 是字符串,则会对 list 里面的 symbol-value pair 进行匹配。

函数调用会产生所谓的 call stack,这个结构也就产生了 environment 的树状结构。我们可以用 sys.* 函数访问这个 call stack,如 sys.call() 返回当前(或者通过 which 参数表示更上几个层次的)函数,sys.frame() 返回当前 environment 的 frame,sys.function() 返回的是当前函数,sys.parent(0 返回的是上级 environment,对应还有复数版本,比如 sys.functions() 就是获得调用栈里面所有函数。传递参数使用的 symbol=value 形式也可以用 missing() 函数判断(这样就不用写成那种形式了)。(给非 primitive 函数)传递参数时,如果是给定参数,则在调用函数的 evaluation frame 就已经 evaluate 了,而如果是使用的默认参数,则是进入到被调用函数里面才 evaluate。这也就是所谓的传值(相对于传引用而言),在函数内部改变参数不会影响主调函数里面的变量。因此如果需要访问未经 evaluate 的表达式,就需要用 substitue() 函数获得对应的 promise。

R 语言摘记(R-lang, Part I)

R 语言摘记(R-intro, Part III)

R 里面有一种特殊的表达模型结构的方式,即 response ~ input op input op …,这里 op 可以用诸如 + – * / ^ 等符号,但是表达的意思完全不同,如 x1 + x2 表示使用两个输入特征 x1 和 x2 进行回归,而 – 则表示不使用某个 input,常数项可以用 1 表示,一般 + 表示的是 additive model,而使用 * 和 / 表达 non-additive model,另外可用 poly() 表达多项式 feature 以及 Error() 表达误差项。使用 : 表示 tensor product。下面我们来看看使用这种表达式进行回归的例子。我们另外知道调用 R 进行数据分析的方式一般是,通过 lm()/glm() 等函数决定模型的种属,如是线性模型还是广义线性模型还是别的什么,然后通过 data 决定分析的数据,模型通过前面的表达式决定,可以用专门的函数来处理获得的模型,如 anova() 进行方差分析,plot() 进行绘制等,另外可以用 update() 函数获得在原模型上做修改的模型结果。如果是一般的优化目标,可以用 nlm() 进行拟合。其他的模型,比如混合模型,可以用 nlme 包中的 lme() 和 nlme() 函数;局部回归模型可以用 stats 包里面的 loess() 函数;魯棒回归可以用 MASS 包里面的 lqs();additive 模型可以用 acepack 或者 mda、gam、mgcv 里面的函数。另外一些决策树算法也有一些对应的包,如 rpart 和 tree。

下面我们结合 Hastie 的那本 The Elements of Statistical Learning 书中的例子来分析数据。

第一个例子是 prostate,见该书 3.2.1 节

# read the data and preprocess
prostate <- read.table( "../../datasets/prostate.data", head=TRUE ) ;
attach( prostate ) ;
prostate$lcavol  <- scale( lcavol ) ;
prostate$lweight <- scale( lweight ) ;
prostate$age     <- scale( age ) ;
prostate$lbph    <- scale( lbph ) ;
prostate$svi     <- scale( svi ) ;
prostate$lcp     <- scale( lcp ) ;
prostate$pgg45   <- scale( pgg45 ) ;
prostate$gleason <- scale( gleason ) ;

# corrlation
corre <- cor( prostate[1:7] ) ;

# linear regression for lpsa
lm.pro <- lm( lpsa ~ lcavol + lweight + age + lbph
             + svi + lcp + gleason + pgg45 + 1,
             data=prostate, subset=train ) ;
summary( lm.pro )

# remove the insignificant predictors
lm.prol <- lm( lpsa ~ lcavol + lweight + lbph + svi + 1,
             data=prostate, subset=train ) ;
summary( lm.prol )

# compare the two models
anova( lm.pro, lm.prol )

# predict
Y <- subset( prostate, subset=!train )
Y.predict <- predict( lm.pro, newdata=Y )
var( Y.predict - Y$lpsa )

# the end
detach( prostate ) ;

可见使用 R 对数据拟合的一般形式。下面我们使用 MASS 的 ridge regression 和 glmnet 的 lasso 进行回归,这也很简单

lm.ridge.pro <- lm.ridge( lpsa ~ lcavol + lweight + age + lbph
             + svi + lcp + gleason + pgg45 + 1,
             data=prostate, subset=train, lambda = 0.1 ) ;

但是 glmnet 并不是使用 formula 确定的关系,因此写起来和其他语言的风格类似。

这里有几个注意点:

  • 使用了 attach() 以后,如果对 data.frame 的数据要更该,前缀还是不能缺省的,这里如果去掉了,后面可能会出问题。
  • lm() 返回的是一个对象,它包含了很多信息,我们往往使用某些函数提取这些信息,另外使用 predict() 的时候,newdata 必须和原来的 data.frame 结构一样。
  • 有意思的是,用户自己写库的时候如何处理 formula 呢?感觉上直接声明输入而把形式包含在程序内部更加方便一些。
R 语言摘记(R-intro, Part III)