Advanced R learning


初次阅读,的确学到了挺多高级技术,甚至很多前所未见的东西,比如泛函和泛型……以后应该会时不时翻一下书,第一次理解还是比较浅显。

小技巧列表:

有时候,数据框读入了之后全变成了字符串,想变回数值类型,怎么办?

1
mtcars[] = lapply(mtcars, as.double)

注意这里为什么能用lapply,因为data.frame是特殊的列表
前面使用[],实际上是选择全部子集,因此最后仍然是data.frame。如果不加前面这个,最后会将mtcars变成list

sapply是可以传参的,因为其中有…参数,会自动将后面多出来的参数对应到函数里面去
但需要注意拼写,因为…参数是不检查参数拼写的

1
2
add = function(x,y,z=2){x + y + z}
sapply(1:5, add, y=3)

do.call是传入参数列表的,而不是用来做list循环的~

1
2
args = list(1:10,na.rm = TRUE)
do.call(mean,args)

因此多次循环有两种处理方式:

  1. 将每一次循环处理成参数型,用do.call传入
  2. 将每一次循环保存到一个list中,用apply函数族传入
    二者的区别是,do.call实际上只运行一次函数,但参数是一个列表,但apply是运行多次函数,每一次读入数据的一部分
    所以从性能考虑应该尽量使用apply函数族

自定义中缀函数以实现灵活的小函数调用:
用户定义的中缀函数必须以%开头,以%结尾,所以,马上能想起来的自定义中缀函数是管道操作符:%>%
用反引号括起来的中缀函数定义可以实现各种小功能的简单调用,例如:

1
2
`%+%` = function(a,b){paste(a,b,sep = "")}
"hello" %+% "world!"

函数中可以使用on.exit构建退出触发式
如果在函数中使用了on.exit()函数,则在函数因任何情况退出时,会触发on.exsit中的表达式并执行
如果需要使用多个on.exit,必须显式传入参数add=TRUE,否则每次调用on.exit都会重写其中的内容

1
2
3
4
5
adds = function(x,y){
x+y
on.exit(print("Done!"))
}
adds(3,4)

但是,on.exit不能替代stop或者message之类的响应函数,因为这个函数无论原函数怎么退出都会被执行

对于package,有两个特殊的环境,关系到函数调用,一个是软件包环境(package),另一个就是命名空间环境(namespace)。
软件包环境中,包含了整个软件包里可以公共访问的一堆函数,例如stat软件包里,有sum,mean,var,sd等
而namespace中,则存在所有的函数,包括公共函数和内部函数,以及这些函数的绑定。
因此,在R中调用函数时,例如var,R会在globalenv中进行函数查找,然后去包的环境查找,如果没有就会报错,没有这个函数
但对于软件包中的函数,比如var,它依赖sd函数,但这个函数会首先去namespace中找,但永远不会去globalenv中进行查找
因此,软件包中自己定义的函数即便同名,通常也不会相互影响,因为都在各自的namepsace中,除非显式指定调用

延迟绑定与动态绑定:对赋值操作的进一步封装,使用%

1
2
3
4
require(pryr)
#delay binding
system.time(b %<d-% {Sys.sleep(3);2})
system.time(b)

1
2
3
4
5
6
7
8
9
10
#activate binding
require(pryr)
x %<a-% runif(1)

#first use
x

#second use
x

R中有类似python的错误处理机制,主要有try、tryCatch和withCallingHandlers
try通常用来处理无条件无处理的异常,用try包住代码块即可。
try中的代码如果没有异常,则返回值就是正常函数的返回值,如果出现异常,则返回值是一个不可见的try-error对象
通过赋值操作可以给try中的函数返回设置默认值,如果函数出错,则使用默认值

1
2
default <- NULL
try(default <- read.csv("some_wrong_file"),silent = TRUE)

闭包closure,这是一个重要的概念,其有两个作用,其一是泛函编程,就是“返回函数的函数”,或者“定义在函数内的函数”
其二是扩展/维持变量作用域,定义在闭包内的变量可以被作用域外的变量访问到,联系就是闭包函数
R中的闭包主要用来做泛函编程,比如:

1
2
3
4
5
6
7
8
9
10
missing_fixer <- function(na_value){
function(x){
x[x==na_value] <- NA
x
}
}

#如果想把-99转换为NA,首先创建闭包函数,然后配合lapply就行了
fix_missing_99 <- missing_fixer(-99)
df[] <- lapply(df, fix_missing_99)

匿名函数,由于R中函数本身就是对象(其实python里也是),因此函数不一定需要与名字进行bind
因此,函数很短的时候,无需绑定函数名,即为匿名函数,匿名函数除了没有名字,定义起来和普通函数一样
匿名函数同样存在作用域,而且通常只对base内置的函数进行匿名调用,避免R不知道我们调用的是匿名函数而出现错误
当然,使用匿名函数写起来可能很轻松,但是这会降低代码的可读性和理解性,尽量不要大范围的使用匿名函数

1
lapply(mtcars,function(x) length(unique(x)))

匿名函数的重要作用就是用来创建闭包,闭包打包了父环境和子环境,能够跨环境进行数据和操作访问,其实上面闭包的例子
function(x)就是一个匿名函数
闭包的父环境,是创建闭包的外部函数的环境,无论它是globalenv还是其他环境。

函数列表,由于函数本身是对象,因此函数可以像数据一样以list的形式保存,这样可以很方便地运行一组功能关联的函数
从列表中使用函数也很简单,提取然后使用即可
例如一组跟数据汇总相关的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
compute_mean <- list(
base = function(x) mean(x),
sum = function(x) sum(x) / length(x),
manual = function(x) {
total = 0
n <- length(x)
for(i in seq_along(x)){
total <- total + x[i]/n
}
total
}
)

x <- runif(1e5)

compute_mean$base(x)
compute_mean$sum(x)
compute_meanmanual(x)

或者,在保持内外向量名一致的情况下,可以直接用lapply进行调用,当然前提你得知道拟调用的是一个函数列表
此外,如果想添加一些额外参数,那么可以再lapply调用中添加一些额外参数
不过这里再次强调,这里是不会进行拼写检查的,因此…..得保证输入是正确的

1
2
3
4
lapply(compute_mean,function(f) f(x))

#if omit na is necesary
lapply(compute_mean,function(f) f(x,na.rm=TRUE))

函数列表的一个重要价值是不将函数直接保存在公共的environment中,有些函数可能造成冲突,或者其实就只想用一次
因此,为了对函数之间的冲突进行限制,可以使用with、attach+detach和list2env+rm,将函数列表引入全局,用完再删;
但一定要记得处理掉全局环境中引入的函数,以防发生冲突。

for循环泛函lapply/sapply/vapply与其他
这里要说的不是apply家族,这个家族用的很多,很熟悉。这里引入一个小问题:有两个列表,一个是观测值,一个是权重
怎么循环计算加权平均?
这里用lapply当然可以解决,但是得双重循环,很麻烦,有个更好的解决方案,用Map函数,注意不是purrr里的map

1
2
3
4
xs <- replicate(5,runif(10),simplify = FALSE)
ws <- replicate(5,rpois(10,5)+1,simplify = FALSE)

unlist(Map(weighted.mean,xs,ws))

本质上就是因为,这里需要对xs中的每一个元素,计算匹配的每一ws元素,调用weight.mean函数并作为两个匹配的参数传入
lapply只有一个list可变调用,其余全都是固定的,Map就是为了解决这个问题。
此时,如果我们还需要传入一些常量,那么可以用匿名函数

1
unlist(Map(function(x,w) weighted.mean(x,w,na.rm=TRUE),xs,ws))

从这些例子能看到,在执行复杂需求的时候,除了原型函数,天然需要考虑到apply/Map/sapply/vapply和泛函实现
也即:如果需要重复执行一条函数,则应该考虑使用apply和Map
如果需要重复使用一类函数,则应该考虑使用泛函
如果循环数非常多,而且可以用lapply进行,则可以考虑并行化

1
2
3
4
5
6
7
8
9
10
11
12
boot_df <- function(x) x[sample(nrow(x),replace = T),]
rsquard <- function(mod) summary(mod)$r.square
boot_lm <- function(i){
rsquard(lm(mpg ~ wt + disp,data = boot_df(mtcars)))
}

#非并行化运算:
system.time(lapply(1:500, boot_lm))

#并行化运算:
require(parallel)
system.time(mclapply(1:500,boot_lm))

对于需要递归的场景,一个常见的例子是Reduce函数,这是各种函数式语言都支持的(其实Map也是)
Reduce用于将某个函数f依次应用于一个列表,而且每一次只取前两个元素,返回一个结果,与下一个元素组合继续进行f
最常见的例子,是多重列表取交集,以一个极其简单的办法就能实现任意多的列表取交集

1
2
3
l <- replicate(5,sample(1:10,15,replace = T),simplify = FALSE)

Reduce(intersect,l)

泛函用起来非常强大,但是,泛函并不能解决全部的问题,通常使用泛函与否的判断条件是,如果以泛函解决问题能让编码更加简洁,那么泛函是一个好主意,但是,如果在某个问题中使用泛函导致代码复杂化或者难以理解,那使用泛函就不是一个好主意了。
常见的无法(或者说不应当)使用泛函的场景有:
1.原位修改,比如对数据框中某一列数值部分数值进行修改,那么应当直接使用循环
2.递归关系,如果在循环的时候,第i次循环与第i-1次不独立,那么便几乎无法使用泛函
3.while循环,泛函有一个显著特点,就是知道循环执行多少次,但是有些while并不知道要运行多少次,因此通常不进行转换

组合函数运算,函数之间是可以进行运算的,例如连接,包装,组合,R中提供了对应的方法
例如plyr包中的each函数,能够组合多个函数,它接收多个函数并将这些函数变成函数列表,能更进一步简化调用

1
2
sums <- plyr::each(mean,sd,median)
sums(1:50)

或者pryr中的compose,它接收函数并进行复合,即将f(x)和g(x)复合为f(g(x)),马上就能联想到一个应用场景
1
sapply(mtcars,pryr::compose(length,unique))

每次调用compoese也还是有点麻烦,因此,联想到之前的中缀函数,可以直接自己定义中缀函数即可
%o% <- pryr::compose
然后就可以直接使用中缀函数进行函数复合了

非标准计算,subs,substitute与eval
非标准计算通常用于交互式场景,与标准计算相对,其本质是,获取我们输入的“表达式”本身,而不是对表达式进行求值
最常见的ggplot2,在调用的时候,例如dplyr中的各种函数,有没有好奇过为什么例如select可以直接输入列名?
因为在执行计算之前,dplyr将我们所有输入的信息,转换为纯粹的表达式,而不进行表达式的计算,例如我们输入一个为X1的列明,在进行运算之前,dplyr将X1捕获,并生成一个表达式colname=X1,此时,R没有对我们输入的式子进行运算,而是像写草稿一样,将它们写下来,然后统一进行运算,及保持表达式形式的计算。
这非常有用,比如我们在函数中,希望对x+y这个简单的式子,进行一些操作,因此我们需要保留这个表达式的形式,而不是每次直接进行了计算。所以我们需要substitute,它返回我们输入的表达式。例如

1
substitute(x+y)

看到了吗,它直接返回了表达式,这非常有用,例如对于X可变的循环中,对,就是在ggplot2的循环作图里面。
如果我们想计算它的值,需要用到eval,eval的意思就是对表达式进行运算,它返回表达式的值,二者通常配合使用,substitute用来构建可变表达式,eval用来进行运算。
如果要构建可变表达式,那么需要然substitute返回可变的表达式形式,这可以使用添加list参数来进行

1
2
3
substitute(x+y,list(x=1))

pryr::subs(x+y,list(x=1))

由于在R中,任何操作都是函数调用,所以我们其实连函数调用都能进行赋值
1
pryr::subs(x+y,list("+" = quote(`*`)))

当表达式构建好之后,调用eval对表达式进行求值即可
但是NSE的使用会使得函数失去透明性,也即输入同等的值的引用,返回同样的结果,但NES不是,向NSE的函数中输入x=10,y=10和10很可能出现不同的结果,这使得对于函数的预测非常困难,因此需要谨慎使用,除非有较大的效率提升。

在OOP方面,目前最先进的是R6对象,其次是最流行的S4对象,后者常见于生物信息中,前者则构成了重要的mlr3learn框架

1
2
3
4
5
6
7
8
require(R6)
Accumulator <- R6Class("Accumulator", list(
sum = 0,
add = function(x = 1) {
self$sum <- self$sum + x
invisible(self)
})
)

R6类中带有new方法,这是内置的,用来创建一个新的R6对象,这与其他语言创建新对象差不多。
其实R6对象的常见的方法、形式与诸如python中对象的定义已经非常相似了,在R6对象中,方法(method)已经是对象了,而不再是S4中的泛型,这使得对象中方法的调用可以直接用$调用方法对象来进行。
此外,上面的那个R6对象,是一个R6Generate,与python不一样的是,R6对象直接就实例化了,不用专门实例化,当然在编程中这不是一个好主意,因为通常更加具体的对象拥有很多私有方法,通常这用继承来解决,因此,最好是采用跟其他脚本语言中一样的习惯,以一个R6对象作为基础,重新实例化和继承出新的即时使用的对象。
对于方法,R6中方法的使用和普通调用函数几乎一样,都是调用,使用参数,区别是返回值通常是invisible的返回给了self的某个属性。
1
2
3
4
x <- Accumulator$new()

x$add(4)
x$sum

1
2
x$add()
x$sum