在开始之前,我们先来聊一聊获取如何获取帮助。对于R语言相关问题,国内的论坛有统计之都,国外网站有stackoverflow,和R-help邮件列表。提问之前先注意以下几点:

推荐书籍:https://adv-r.hadley.nz/

  1. 是不是最新版本的R或者R包
  2. 构建一个可再现的例子,好让帮助者明确了解问题是什么
  3. 花时间搜索相关问题。

1.数据结构

R 语言的基础数据结构有以下几种,所有其他对象都是在这几种基础之上构建的。

想知道一个对象的结构可以使用str函数。

向量

R中最基础的数据结构是向量(原子向量和列表)。向量有三个属性: 1. 类型:typeof 2. 长度 :length 3. 属性:attributes

不要使用is.vector来判断一个对象是不是向量,用is.atomic||is.list 来判断。

原子向量

原子向量的元素都是同一类型,通常有四种:logical,integer,double,character

通常使用c函数创建向量,c是combine的简写

类型和测试

使用typeof来确定向量的类型,或者使用is 类函数进行逻辑判断:

  1. is.character
  2. is.integer
  3. is.double
  4. is.logical
  5. is.numeric :只要是数字,返回都是true
  6. is.atomic

强制转换

原子向量的元素必须是同一类型,如果不是,则会发生强制转换(coerce),数据类型的灵活性从低到高排序为:逻辑,整型,双精度,字符类型。

c(1,"a")
## [1] "1" "a"

强制转换可能会导致数据缺失。使用as类函数可以进行显示转换。

  1. as.character
  2. as.numeric
  3. as.logical etc

列表

列表也是向量,但是列表中的元素可以是任意类型。列表使用list函数创建。

可以使用is.list判断是否是列表,使用unlist可以将list转换成为原子向量。

R中通常会使用列表创建更加复杂的数据了警。

属性

任何对象都可以通过附加属性来储存对象的元数据。可以通过attr函数单独访问对象的每一个元素,也可以使用attributes函数同时访问所有属性。

x <- 1

attr(x,"attr1") <- "this is a value"

attr(x,"attr1")
## [1] "this is a value"
attributes(x)
## $attr1
## [1] "this is a value"

默认情况下,向量被修改后属性会丢失,但是有三种属性不会: 1. 名字 2. 维度 3. 类

这些属性有特定函数提取,names函数,class函数,dim函数,而不是attr(x,names)

创建带名字的向量有几种方式:

x <- c(a=1)
x
## a 
## 1
x <- 1;names(x) <- "a"
x
## a 
## 1
x <- setNames(1,"a")
x
## a 
## 1

取消命名可以是:

names(x) <- NULL

因子

属性的一个重要应用就是因子,因子是只能包含预先定义值的向量,其通常用来存储分类变量。因子建立在向量的基础之上,其有两个重要的属性,类和level。

x <- factor(c("a","b","d"))
x
## [1] a b d
## Levels: a b d
class(x)
## [1] "factor"
levels(x)
## [1] "a" "b" "d"

levels函数可以获取因子的所有可能取值。

x <- factor(c("a","b","c"),levels = c("c","b","a"))
x
## [1] a b c
## Levels: c b a

可以通过levels 参数指定因子的顺序。需要注意的是,因子类型不是字符类型向量,其是整型,因此不能当作字符串处理。

矩阵和数组

给一个原子向量添加维度,dim函数,就变成了多维数组,矩阵是多维数组的一个特例。

matrix(1:6,nrow = 2)
##      [,1] [,2] [,3]
## [1,]    1    3    5
## [2,]    2    4    6
array(1:12,dim = c(2,2,3))
## , , 1
## 
##      [,1] [,2]
## [1,]    1    3
## [2,]    2    4
## 
## , , 2
## 
##      [,1] [,2]
## [1,]    5    7
## [2,]    6    8
## 
## , , 3
## 
##      [,1] [,2]
## [1,]    9   11
## [2,]   10   12
x <- c(1:4)
x
## [1] 1 2 3 4
dim(x) <- c(2,2)
x
##      [,1] [,2]
## [1,]    1    3
## [2,]    2    4

length函数和names函数对于不同数据结构有着通用性。 其他常用函数:nrow,ncol,dim,names,rownames,colnames,dimnames。

另外,对于矩阵而言,c函数相当于cbind函数和rbind函数。t函数可以对矩阵进行转置,对于数组而言使用aperm.

使用is.matrix和is.array函数来判断是不是矩阵或者向量。 使用as.matrix和as.array将其他类型变量转变成为矩阵。

数据框

数据框是非常常用的数据类型。实际上,数据框是由相同长度的向量构成的列表,从结构上看数据框是二维的。所以也可以使用这些函数: names,colnames,rownames,length,nrow,ncol

数据框的创建

使用data.frame函数。需要注意数据框会默认将字符转换成为因子,可以通过stringAsFactors参数进行限制。

类型判断和强制转换

data.frame 是S3类,他的类型反应了构建他的基础结构,也就是列表。判断一个对象是不是数据框使用class函数,或者is.data.frame函数。

typeof(iris)
## [1] "list"
class(iris)
## [1] "data.frame"

合并数据框

这里先不讨论tidyverse,合并数据框主要使用rbind 行合并和cbind,列合并。

2.子集选取

关键符号:[],[[]],$

数据类型

x <- 1:4

# 1 通过一个向量

x[c(1,2,3)]
## [1] 1 2 3
x[1:3] # 需要注意的是从位置从1开始
## [1] 1 2 3
# 2. -表示不选取什么

x[-1]
## [1] 2 3 4
# 3通过逻辑向量选取

x[c(T,T,F,F)]
## [1] 1 2
# 空索引

x[]
## [1] 1 2 3 4
# 0 索引

x[0]
## integer(0)
# 如果向量设置的名字,还可以通过名字进行索引

names(x) <- c("A","B","C","D")
x["A"]
## A 
## 1

列表同样通过[] 进行选择子集合,另外还有[[]]和$

矩阵和数组

高维数组进行索引就是给每一维数据给出一个一维缩影,用逗号隔开。如果某一维索引为空,那么表示全部都要。

x <- matrix(1:10,2)
x[1:2,]
##      [,1] [,2] [,3] [,4] [,5]
## [1,]    1    3    5    7    9
## [2,]    2    4    6    8   10
x[1,1]
## [1] 1

数据框

数据框兼顾了矩阵和列表的特点,

iris$Sepal.Length[1:2]
## [1] 5.1 4.9
iris[1:2,]
##   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
## 1          5.1         3.5          1.4         0.2  setosa
## 2          4.9         3.0          1.4         0.2  setosa

S3 对象

S3对象是由原子向量,数组和列表构成,可以使用str先获取相关信息然后进行选取子集合

S4对象

使用@(等价于$)和slot函数

子集选取运算符

还有另外两个子集合元素运算符号[[]]和$。[] 和[[]]的关系可以如下所示。

简化和保留

选取子集的时候,我们需要注意子集的数据结构是不是和愿数据一致,对子集进行简化会得到尽可能简单的数据结构。以下是一个总结:

\(可以用于数据框的某一列选取 。需要注意的是,\)后不能接变量,一定要是变量名。

iris$Sepal.Length[1:2]
## [1] 5.1 4.9
# 或者
x <- "Sepal.Length"
iris[[x]][1:2]
## [1] 5.1 4.9

越界

以下是一个总结

3.常用函数与数据结构

基础函数

常见数据结构

统计函数

R 常用函数

I/O相关函数

4.R编程风格

formatR 包


library(formatR)

tidy_source("My.R")

Rstudio 美化代码快捷键

# Windows
ctrl + shift + A


# Mac

command + shift + A

文件名

要有意义,有顺序最好加上数字前缀

对象名

变量和函数名用小写字谜,以下划线_ 分割。变量名应该是名词,函数名应该是动词.

语法

所有中缀运算符两边空格。

左打括号不能单独占一行,右半边独占一行或者接else。

缩进两个空格,不要使用tab

注释

注释用一个#+ 空格,注释应该解释为为什么,而不是什么

control +shift +R 创建分割(mac)

赋值

赋值用

<- 

快捷键option - (mac)

5.函数

函数是R的基本单元,其本身就是对象。

函数的组成

  1. body函数,函数的内部代码
  2. formals函数,控制如何调用函数的参数
  3. environment函数,函数变量的环境

可以通过以上三个函数对函数进行修改。

函数同样可以有很多的附加属性,基础R包中使用的一个属性是srcref,其是source reference 的简写,用来指向函数的源代码

原函数

原则上函数有三个部分,但是原函数是例外,例如sum函数,他使用.primitive函数直接调用c代码并且不包含R代码,因此该函数的三个部分都是null。

原函数包含在base包中,他们在低层进行计算,更加高效。

作用域

作用域简单理解就是对象的使用范围。R中作用域有两种,词法作用域(lexical scoping),他在语言层面上自动进行。动态作用域(dynamic scoping),它主要作用是在交互式分析中选择函数来减少键盘输入。

R中的lexical scoping,其中lexing 指的是计算机科学中的词法分析,其是将文本表示的代码转变成为编程语言能够理解的有意义代码的过程的一部分。

R中词法作用域的实现有4个基本步骤:

  1. 名字屏蔽
  2. 函数与变量
  3. 重新开始
  4. 动态查找

名字屏蔽

如果名字不是函数内部创建的,就会取上一层查找。

通常而言,首先在当前函数查找,然后取函数定义的地方,等等,然后到全局变量,到其他家在的添加包中查找。

同样的规则适用于闭包,闭包是由其他函数创建的函数。

函数与变量

函数和变量可以同样,但是需要尽量避免

重新开始

每次调用函数都会创建一个新的环境。

动态查找

R是在调用函数的时候查找这些值,而不是函数创建的时候。

因此,依据环境中变量的函数,在不同时候输出可能不一样.

x <- 1

f <- function(){
  print(x)
}

f()
## [1] 1
x <- 10

f()
## [1] 10

应该要避免这样的情况。codetools包中的findGlobals函数可以返回函数与外界的依赖关系。

每个运算都是函数调用

R 中的运算符其实都是一个函数,你可以重新对他进行定义,虽然这可能不是一件好的事情。

x <- 1
y <- 2

x+y
## [1] 3
`+`(x,y)
## [1] 3

函数参数

函数调用:

将实参映射到形参的方法优先级是:首先将名字完全匹配的参数进行映射;再将前缀匹配的参数进行映射;最后是将位置匹配的参数进行映射。

使用参数列表调用函数

args <- list(1:10,na.rm = TRUE)

do.call(mean,list(1:10,na.rm = TRUE))
## [1] 5.5
# 等价于

mean(1:10,na.rm = TRUE)
## [1] 5.5

默认参数和缺失参数

R中的函数参数可以有默认值,并且,R中的参数使用惰性求值,所以参数的默认值可以通过其他参数来定义。

f <- function(a = 1,b = a*2){
  return(a*b)
}

f()
## [1] 2

默认参数可以用函数内部的变量来定义,虽然这不是一个好办法。

可以用missing函数来确定一个参数是不是被传递。

f <- function(a=1,b){
  c(missing(a),missing(b))
}
f()
## [1] TRUE TRUE
f(a=1)
## [1] FALSE  TRUE

惰性求值

默认情况下,R函数的参数都是惰性求职,他们只有在世纪被使用到才会进行求值。

如果你希望确保对一个参数进行求值,可以使用force函数。

…参数

…是一个特殊参数,这个参数将于所有没有匹配的参数进行匹配。

一个例子就是plot函数。

特殊调用

函数名字在参数的前面,叫做前缀运算符,。也可以创建中缀函数,使函数名字排在参数中间,例如+,所有用户创建的中缀函数必须以%开头,以%结尾。

R中预定义的中缀函数有:%%,%*%,%/%,%in%,%o%,%x%,etc.

返回值

return函数

退出

函数退出的时候可以使用on.exit函数触发其他时间,其通常用来确保全局状态的改变能够恢复原状。

6.面向对象编程

pryr 用来分析面向对象系统(object oriented system)

基础类型

所有R对象的底层都是一个用来描述这个对象如何在内存中存储的C结构体。这个结构体包含这个对象的内容,内存分配信息以及类型。

基础类型不是真正的面向对象系统,只有R的核心团队才能够创建新类型。可以使用typeof函数检测对象的基础类型。

S3

S3是R的第一个OO系统,他是R基础包和统计包中唯一使用的OO系统,也是CRAN软件包中最常用的系统。

检测对象可以使用pryr::otype(),或者

library(pryr)
## 
## Attaching package: 'pryr'
## The following object is masked _by_ '.GlobalEnv':
## 
##     f
otype(iris)
## [1] "S3"
is.object(iris)&(!isS4(iris)) 
## [1] TRUE

在S3中,方法术语函数,这个函数成为泛型函数(generic function),S3的方法不属于对象或者类。为了知道一个函数是不是一个S3泛型函数,可以查看他的源代码,找到函数调用的UseMethod:这个函数指出调用的正确方法,也就是方法分派的过程(method dispathc)。pryr也提供了ftype函数,用于分析与函数相关的对象系统。

mean
## function (x, ...) 
## UseMethod("mean")
## <bytecode: 0x7fa36c1f8e48>
## <environment: namespace:base>
ftype(mean)
## [1] "s3"      "generic"

有些S3泛型函数不调用UseMethod,因为它使用C语言实现。作为替代,他们调用C哈娜恕的DispatchGroup或者DispatchOrEval。在C代码中执行方法分派的函数成为内部泛型(internal generics),可以使用?“internal generic”查看分档.ftype也对这些特殊情况进行描述。

pryr函数中的ftype函数可以检查一个函数是S3方法还是S3泛型函数。

ftype(t.data.frame)
## [1] "s3"     "method"
ftype(t.test)
## [1] "s3"      "generic"

可以使用methods函数来查看属于一个泛型函数的所有方法

methods("mean")
## [1] mean.Date     mean.default  mean.difftime mean.POSIXct  mean.POSIXlt 
## [6] mean.quosure*
## see '?methods' for accessing help and source code
methods("t.test")
## [1] t.test.default* t.test.formula*
## see '?methods' for accessing help and source code
methods("t.data.frame")
## no methods found

大多数的S3方法是不可见的,使用getS3method阅读他们的源代码。

getS3method("predict", "ppr")
## function (object, newdata, ...) 
## {
##     if (missing(newdata)) 
##         return(fitted(object))
##     if (!is.null(object$terms)) {
##         newdata <- as.data.frame(newdata)
##         rn <- row.names(newdata)
##         Terms <- delete.response(object$terms)
##         m <- model.frame(Terms, newdata, na.action = na.omit, 
##             xlev = object$xlevels)
##         if (!is.null(cl <- attr(Terms, "dataClasses"))) 
##             .checkMFClasses(cl, m)
##         keep <- match(row.names(m), rn)
##         x <- model.matrix(Terms, m, contrasts.arg = object$contrasts)
##     }
##     else {
##         x <- as.matrix(newdata)
##         keep <- seq_len(nrow(x))
##         rn <- dimnames(x)[[1L]]
##     }
##     if (ncol(x) != object$p) 
##         stop("wrong number of columns in 'x'")
##     res <- matrix(NA, length(keep), object$q, dimnames = list(rn, 
##         object$ynames))
##     res[keep, ] <- matrix(.Fortran(C_pppred, as.integer(nrow(x)), 
##         as.double(x), as.double(object$smod), y = double(nrow(x) * 
##             object$q), double(2 * object$smod[4L]))$y, ncol = object$q)
##     drop(res)
## }
## <bytecode: 0x7fa36ea87208>
## <environment: namespace:stats>

定义类和创建对象

S3没有正式的定义,为了给一个雷创建一个对象实例,只需要使用已有的基础对象并设置属性。在创建的时候可以使用structure函数,或者使用class <- ()

创建一个S3对象

# 1
test <- structure(list(),class = "test1997")
test
## list()
## attr(,"class")
## [1] "test1997"
# 2 

test <- list()
class(test) <- "test1997"

通常而言,S3对象通常建立在列表或者带有属性的原子向量之上。

可以使用class函数检查对象的属性,也可以使用inherits(x,“classname”)来检查一个对象是否集成某一个类

class(test)
## [1] "test1997"
inherits(test,"test1997")
## [1] TRUE

一个对象的类可以是向量,这个向量描述从最具体到最一般的行为。 类名一般是小写,避免使用.。 大多数S3类都提供一个构造函数:

test <- function(x){
  if(!is.numeric(x)) stop("X need to be numeric")
  structure(list(x),class="test1997")
}

如果S3有构造函数,例如factory,data.frame。构造函数保证正在创建的类包含正确的组成成分。构造函数的名字通常与类的名字相同。

除了开发者提供构造函数外,S3没有正确性检查,这意味着我们可以改变已有对象的类:

# 创建线性回归

mod <- lm(mpg~disp,data = mtcars)

print(mod)
## 
## Call:
## lm(formula = mpg ~ disp, data = mtcars)
## 
## Coefficients:
## (Intercept)         disp  
##    29.59985     -0.04122
class(mod) <- "data.frame"


mod$coefficients
## (Intercept)        disp 
## 29.59985476 -0.04121512

最好不要直接改变类。

创建新的方法和泛型函数

为了添加一个新的泛型函数,创建一个调用UseMethod函数。UseMethod函数两个参数:泛型函数的名字和方法分派参数。如果忽略了第二个参数,就会将第一个参数分配给函数。没有必要讲泛型函数的所有参数都传递给UseMethod。

f <- function(x) UseMethod("f")

f
## function(x) UseMethod("f")

如果没有方法,泛型函数就没有用,为了添加方法,只需要创建一个具有正确名字的普通函数(generic.class):

f.a <- function(x) "class a"

a <- structure(list(),class = "a")

class(a)
## [1] "a"
f(a)
## [1] "class a"
f.b <- function(x) "class b"

b <- structure(list(),class="b")

class(b)
## [1] "b"
f(b)
## [1] "class b"

用同样的方式为一个已有的泛型函数添加一个新方法。

mean.a <- function(x)"a"
mean(a)
## [1] "a"

mean 表示函数 a表示类

创建泛型就是这样的结构。

需要注意的是,方法返回的类是否与泛型函数匹配没有做任何检查。确保创建的新方法会不会与已有的代码发生冲突完全取决于你自己。

方法分派

S3方法的分派非常简单,UseMethod函数创建一个由函数名构成的向量。类似于paste0(“generic”,“.”,c(class(x),“default”)),并且按顺序依次查找。默认类可以为其他为止类创建一个回滚方法。

f <- function(x) UseMethod("f")
f.a <- function(x) "class a"
f.default <- function(x) "Unknown class"

f(structure(list(),class="a"))
## [1] "class a"
f(structure(list(),class = c("b","a")))
## [1] "class b"
f(structure(list(),class="c"))
## [1] "Unknown class"

四个泛型函数以及他们所包含的是:

  1. Math:abs,sign,sqrt,floor,cos,sin,lig…
  2. Ops:+,-,*,/,^,%%,%/%,&,|,!,==,!=,<,<=,>=..
  3. summary:allmany,sum,prod,min,max,range 4.Complex:Arg,Conj,Im,Mod,Re

泛型函数的相对高级的技术,可以使用?groupGeneric来找到更多相关内容。需要注意的,Math,Ops,Summary,Complex并不是真正的函数,而是一组函数的代表。需要注意的是,在组泛型函数内有一个特殊的变量.Generic,它提供了对实际泛型函数的调用

通过?NextMethod查看跟多信息。这些方法都是函数,可以直接调用,但是不建议。

S4

S4和S3类似,但是S4更加的严谨。方法属于函数而不是类,但是: 1. 类有描述他们的字段和继承关系的结构的正式定义 2. 可以给予一个泛型函数的多个参数进行方法分派 3.

与S4相关的方法都存储在methods包中。当你交互式运用R的时候,这个包总是可用的,但是批处理的时候则不能使用methods。因此最好显示的使用methods包。

识别对象,泛型函数和方法

识别S4对象,泛型函数和方法很容易。对一个S4对象使用str函数,返回formal,调用idS4函数,返回值是TRUE。使用pryr::otype函数返回S4.

经常食用的基础包中(stats,graphics,utils,datasets,base等等)基本没有S4.stats4包中有S4类。

library(stats4)
library(methods)
x <- 0:10
y <- c(26, 17, 13, 12, 20, 5, 9, 8, 5, 4, 8)

## Easy one-dimensional MLE:
nLL <- function(lambda) -sum(stats::dpois(y, lambda, log = TRUE))
fit0 <- mle(nLL, start = list(lambda = 5), nobs = NROW(y))


isS4(fit0)
## [1] TRUE
otype(fit0)
## [1] "S4"
is(fit0)
## [1] "mle"
is(fit0,"mle")
## [1] TRUE

可以使用getGenerics函数获得所有S4泛型函数的列表;使用getClasses函数获得所有S4类的列表。使用showMethods函数列出所有的S4方法。

定义类并创建对象

在S3中,我们通过设置属性将任意的对象转变成为一个特定类的对象。S4更加严格:必须使用setClass函数定义一个类的表示,使用new函数来创建一个新对象。

可以使用下面的方法来找到一个类的文档:class?className,例如class?mle

S4类有三个关键性质: 1. 名字 name :一个由字母和数字组成的标识。S4 的命名规则是开头大写字母的驼峰拼法。 2. 带有名字的字段列表(slot)。他定义字段名和允许的类。例如一个person的类可以使用字符型的名字和数值型的年龄:list(name = " character“,age =”nemeric") 3. 一个用来描述他的父类的字符串,或者用S4的术语,它包含contain 的类。可以使用多个类实现多重继承。

S4类还有其他一些可选性质,如validity方法可以用来测试一个对象是否有效,prototype对象可以用来定义默认字段值。可以使用 ?setClass可以查看更多的细节。

下面一个例子创建了一个Person的类,以及一个继承Person类的Employee类。Employee类继承了Person类的字段和方法,并且增加了一个新的字段boss。

library(methods)
setClass("Person",slots = list(name = "character",age = "numeric"))

setClass("Employee",slots = list(boss = "Person"),contains = "Person")

tom <- new("Person",name = "tom",age = 40)
alice <-  new("Employee",name = "alice",age = 20,boss = tom)

大部分的S4类都有一个与类名相同的构造函数:如果存在构造函数,那么可以直接使用构造函数。 获取S4对象的字段可以使用slot函数或者@

tom@name
## [1] "tom"
slot(tom,"name")
## [1] "tom"

需要注意的是(@等价于$,slot等价于[[)

如果一个S4对象继承S3或者一个基础类型,他们就会包含一个特殊的.Data字段,这个字段包含底层基础类型或S3对象。

setClass("RN",slots = list(min = "numeric",max="numeric"),contains = "numeric")

rN <- new("RN",.Data=1:10,min=1,max=10)

rN@min
## [1] 1
rN@.Data
##  [1]  1  2  3  4  5  6  7  8  9 10

因为R是一个交互式编程语言,所以它可以随时创建一个新类或者重新定义现有的类。最好不要对类进行修改,而是重新创建。

创建新方法和泛型函数

S4为创建新的泛型函数和方法提供了特殊函数,setGeneric可以创建新的泛型函数或者将已有的函数转换成为泛型函数。setMethod 的参数包括:泛型函数的名字,与该方法关联在一起的类,执行方法的函数。

setGeneric("union")
## [1] "union"
setMethod("union",c(x="data.frame",y="data.frame"),function(x,y){unique(rbind(x,y))})

如果要重新创建一个新的泛型函数,需要为他提供一个调用standardGeneric的函数

setGeneric("Generic1",function(x){standardGeneric("Generic1")})
## [1] "Generic1"

S3 中使用的UseMethod函数,这里使用的是setMethod

方法分配

使用?Methods可以找到分派的规则,最后,给定一个泛型函数调用的规范,有两种方法可以找到那个方法被调用的。

selectMethod("nobs",list("mle"))
## Method Definition:
## 
## function (object, ...) 
## if ("nobs" %in% slotNames(object)) object@nobs else NA_integer_
## <bytecode: 0x7fa36e1556d0>
## <environment: namespace:stats4>
## 
## Signatures:
##         object
## target  "mle" 
## defined "mle"
method_from_call(nobs(fit0))
## Method Definition:
## 
## function (object, ...) 
## if ("nobs" %in% slotNames(object)) object@nobs else NA_integer_
## <bytecode: 0x7fa36e1556d0>
## <environment: namespace:stats4>
## 
## Signatures:
##         object
## target  "mle" 
## defined "mle"

RC

RC 是R中最新的OO系统,他有几个特点: 1. RC方法属于对象而不是函数 2. RC对象是可变的,不适用于通常R的复制后修改的语义(copy on modify)

RC对象和其他语言的对象类似。

定义RC类和创建对象

R的基础包中没有RC类,RC类适合描述状态对象,对象会随时间发生变化。创建RC与创建S4 类似,使用setRefClass函数而不是setClass函数。第一个必须参数是name。

account <- setRefClass("MyAccount")
account
## Generator for class "MyAccount":
## 
## No fields defined
## 
## Class Methods: 
##      "field", "trace", "getRefClass", "initFields", "copy", "callSuper", 
##      ".objectPackage", "export", "untrace", "getClass", "show", "usingMethods", 
##      ".objectParent", "import"
## 
## Reference Superclasses: 
##      "envRefClass"
account$new()
## Reference class object of class "MyAccount"

setRefClass函数还接受一个定义类字段的名字-类对的列表。传递给new函数的附加名字参数将设置该字段的初始值。可以使用$可以获取和设置某个字段的值。

account <- setRefClass("MyAccount",fields = list(balance="numeric"))

account1 <- account$new(balance = 100)

account1
## Reference class object of class "MyAccount"
## Field "balance":
## [1] 100
account1$balance <- 1000

account1
## Reference class object of class "MyAccount"
## Field "balance":
## [1] 1000

需要注意的是,RC对象是可变的,不是复制后修改。

account2 <- account1

account2
## Reference class object of class "MyAccount"
## Field "balance":
## [1] 1000
account1$balance <- 1

account1
## Reference class object of class "MyAccount"
## Field "balance":
## [1] 1
account2
## Reference class object of class "MyAccount"
## Field "balance":
## [1] 1

由于这个原因,使用RC的copy方法复制对象。

account3 <- account1$copy()

account3
## Reference class object of class "MyAccount"
## Field "balance":
## [1] 1
account1$balance <- 10

account1
## Reference class object of class "MyAccount"
## Field "balance":
## [1] 10
account3
## Reference class object of class "MyAccount"
## Field "balance":
## [1] 1

需要注意这里的等号是<<-

RC方法是与类相关量的,并且可以在原地对其字段进行修改。

account <- setRefClass("MyAccount",fields = list(balance = "numeric"),methods = list(
  withdraw = function(x){
    balance<<-balance - x
  },
  deposit = function(x){
    balance<<- balance+x
  }
))

account1 <- account$new(balance= 1000)

方法和属性一样,可以通过$来进行获取。

account1$deposit(10)

account1
## Reference class object of class "MyAccount"
## Field "balance":
## [1] 1010

setRefClass函数还有一个重要参数contains,他给出当前所继承的父RC类。

account <- setRefClass("MyAccount",fields = list(balance = "numeric"),methods = list(
  withdraw = function(x){
    if(balance<0){
      stop("Not enough money")
    }
    balance<<-balance - x
  },
  deposit = function(x){
    balance<<- balance+x
  }
))

account1 <- account$new(balance= 1000)

account1$withdraw(1001)

# account1$withdraw(1)
# Error in account1$withdraw(1) : Not enough money

需要注意的是,所有的RC类最终都是继承与envRefClass,它提供了一些很有用的方法,例如copy,callSuper调用父类字段,field获取某个字段,export等价于as,show函数控制输出。

识别类

pryr::otype函数

方法分派

当调用x$f()的时候,R将在x类中寻找f方法,然后在父类中查找,然后在父类的父类中查找,等等。

选择什么面向对象系统

对于大多数任务S3足够了。在R中,经常需要为已经存在的泛型函数创建非常简单的方法和对象(例如print,summary),S3可以让我们使用最短的代码完成任务。

如果正在为相互关联的对象创建更加复杂的系统,S4更合适,一个例子是Matrix包。这个包的目的是更有效的存储和计算多种不同类型的稀疏矩阵。Bioconductor包也大量使用了S4。S3和S4思想是一样的,只不是S4更加正式,严格,详细一些。

只有在必须使用可变状态的时候,才使用RC。大部分函数还是应该保持函数化。

7.环境

环境基础

环境的作用就是将一些名字和一些值进行绑定,(bind),如果对象没有指向他的名字,那么这个对象就会被垃圾回收器自动回收(garbage collector)。

每一个环境都有父环境,例如每一个包都是一个子环境。如果一个名字在当前的环境中没有找到,R就会到他的父环境中寻找。

关于环境有几点需要注意: 1. 环境中每一个对象都有一个唯一的名字 2. 环境中的对象是没有顺序的 3. 环境有父环境

环境由两部分组成:对象框 frame,他包含名词-对象的绑定关系;父环境

有四个特殊的环境 1. globalenv ,这个是全局环境,通常情况下,我们就是在这个环境中进行工作。其父环境是由library函数添加的最后一个添加包 2. baseenv,基础环境,他是R基础软件包的环境。他的父环境是空环境 3. emptyenv,他是所有环境的祖先,没有父环境 4. environment ,当前环境

search函数可以列出全局环境的所有父环境。这称为搜索路径。其中有一个Autoloads环境,它可以在需要时通过值家在添加包对象来减少内存使用。

globalenv函数,baseenv函数搜索路径上的环境和emptyenv函数的关系如下:

使用new.env函数可以手动创建一个环境,使用ls函数可以将此环境的对象框中的所有绑定关系列出来。使用parent.env函数可以查看他的父环境。

e <- new.env()

parent.env(e)
## <environment: R_GlobalEnv>

对一个环境中的绑定关系进行修改的最简单方法就是将其看作列表:

e$a <- 1
e$b <- 2

ls(e)
## [1] "a" "b"

默认情况下,ls只能列出不是以.开始的名字,可以通过all.names=TRUE来显示一个环境中的绑定关系

e$.a <- 1

ls(e,all.names = T)
## [1] ".a" "a"  "b"

查看环境的另外一个方法就是ls.str函数,他可以将环境中的所有对象都显示出来。

str(e)
## <environment: 0x7fa36ec96978>
ls.str(e)
## a :  num 1
## b :  num 2

给定一个名字,可以使用$,[[,get函数来获取与其绑定的值。

  1. $和[[只在一个环境中查找,如果不存在就返回NULL
  2. get函数如果没找到绑定就返回错误
e[["a"]]
## [1] 1
get("a",envir = e)
## [1] 1

从环境中删除对象和从列表中删除对象有一些不同,在列表中可以通过将其设置为NULL来删除。在环境中使用rm函数删除。

rm("a",envir = e)

使用exists函数来检查一个绑定是否存在,设置参数inherits=F表示不在父环境中查找。

使用identical函数对两个环境进行比较。

环境递归

环境可以构成一棵树。给定一个名字,pryr::where就会使用R的作用域法则找到定义这个名字的话环境:

library(pryr)

where("mean")
## <environment: base>

where函数有两个函数,1.要查找的名字 2. 开始查找的环境。

where
## function (name, env = parent.frame()) 
## {
##     stopifnot(is.character(name), length(name) == 1)
##     env <- to_env(env)
##     if (identical(env, emptyenv())) {
##         stop("Can't find ", name, call. = FALSE)
##     }
##     if (exists(name, env, inherits = FALSE)) {
##         env
##     }
##     else {
##         where(name, parent.env(env))
##     }
## }
## <bytecode: 0x7fa36d569208>
## <environment: namespace:pryr>

这个函数运行有三种情况: 1. 基本情况:达到空环境,抛出错误 2. 成功:返回环境 3. 递归:当前环境没有找到,尝试在他的父环境进行查找。

函数环境

大多数环境并不是通过new.env函数创建的,而是使用环境的结果。有四种函数相关的环境:1. 封闭 2. 绑定 3. 执行 4. 调用

封闭 enclosing 环境就是创建函数的环境。每个函数有且仅有一个封闭环境。对于其他三种类型的环境,每个函数可以有0,1,或者对歌相关联的环境。 1. 使用 <- 讲一个函数与一个名字进行绑定,这样就可以顶一个绑定binding环境 2. 调用函数创建一个临时执行execution环境,他用来存储执行期间创建的各种变量 3. 每一个执行环境都与一个调用calling环境相关联,他说吗函数在哪里调用

封闭环境

当创建一个函数时,他就获得对创建他环境的引用。这就是封闭环境enclosing envirinment。为了确定一个函数的封闭环境,只需要调用environment函数,并将函数名作为第一个参数。

x <- 1
f <- function(y) x+y
environment(f)
## <environment: R_GlobalEnv>

在下图中,使用圆角矩形来代表函数,黑色圆点表示函数的封闭环境。

绑定环境

函数名字可以通过绑定来定义,一个函数的绑定环境就是与其绑定的所有环境。

当我们讲一个函数分配给另一个不同的环境,情况则有不一样

e <- new.env()

e$g <- function() 1

封闭环境决定了这个函数如何找到值,而绑定环境空间决定如何找到函数。

绑定环境和封闭环境的却别对于软件包的命名空间是非常重要的。软件包的命名空间使软件包之间保持独立。例如,如果软件包A使用了基础包的mean函数,那么如果软件包B也创建了自己的mean函数,如何避免冲突? 命名空间确保软件包A能够继续使用基础包中的mean函数,而不受软件包B的影响。

软件包环境包含所有可以访问的公共函数,并且存放在搜索路径上。下图可以展示这种复杂关系。

当在控制台输入var的时候,R首先在全局环境中进行查找。当sd函数查找var函数的时候,他首先到命名空间环境中超找,并且永远不会在globalenv函数中查找。

执行环境

每次调用函数的时候,都会创建一个新的关于函数肚饿执行环境,执行环境的父环境就是函数的封闭环境。一旦函数执行结束,这个环境就会被销毁。

调用环境

我们查看代码

f <- function(){
  x <- 10
  function(){
    x
  }
}

i <- f()
x <- 20
i()
## [1] 10

f函数首先找自己在哪里定义的,然后再寻找与x相关联的值10.在定义f函数的环境中x为10,在调用f函数的环境中x为20. 我们可以使用parent.frame函数查看父环境的。

f <- function(){
  x <- 10
  function(){
    def <- get("x",environment())
    cll <- get("x",parent.frame())
    list(def,cll)
  }
}

i <- f()
x <- 20
str(i())
## List of 2
##  $ : num 10
##  $ : num 20

绑定名字和赋值

赋值操作其实就是将一个名字与一个值进行绑定。作用域规则决定如何找到与一个名字相关联的值。

?Reserved获取R中保留字列表.<-表示在当前环境中创建一个变量。强制赋值箭头,<<-不会在当前环境中创建变量,但是他修改父环境中已有的变量。

也可以使用assign函数进行深度邦迪:names<<- value等价于assign(“name”,value,inherits = TRUE)

x <- 0
f <- function(){
  x<<-1
}
f()
x
## [1] 1

如果<<-没有找到已有的变量,他就在全局环境中创建一个,但是不建议这样做。

还有两种特殊类型的绑定,延迟绑定和主动绑定。

  1. 延迟绑定delayed binding,不是立即把结果赋值给一个表达式,他创建和存储一个约定,在需要的时候对约定中的表达式进行求值。%<d-%
library(pryr)

system.time(b %<d-% {Sys.sleep(1);1})
##    user  system elapsed 
##       0       0       0
system.time(b)
##    user  system elapsed 
##   0.000   0.000   1.005

%<d-%是对基础delayedAssign函数的封装,如果需要更多的控制可以直接使用这个函数。延时绑定可以用来实现autoload。

  1. 主动绑定avtive binding 不是绑定到常量对象。相反,每次对其进行访问时都要重新计算
x %<a-% runif(1)
x
## [1] 0.5480416
x
## [1] 0.2408762

%<a-%是对于makeActiveBinding 的封装

显示环境

除了服务于作用域之外,环境也是一种有用的数据结构。与R中的大多数对象不同,当你对环境进行修改,R不会对其进行赋值。我们看一个例子

f <- function(x){
  x$a <- 2
  invisible()
}

如果将这个函数应用于列表,原始列表不会被改变,因为修改列表实际上是创建和修改副本。

x_1 <- list()
x_1$a <- 1
f(x_1)

x_1$a
## [1] 1

如果将这个函数应用于环境,那么原始环境就会发生修改。

x_e <- new.env()
x_e$a <- 1
f(x_e)

x_e$a
## [1] 2

就像可以使用列表在函数之间传递数据一样,也可以使用环境。当你创建自己的环境的时候,应该将父环境设置为空环境。

环境是解决下面三类常见问题的有用数据结构: 1. 避免大数据的赋值 2. 管理一个软件包的内部状态 3. 根据名字高效的查找与其绑定的值

8.调试,条件处理与防御式编程

在编写函数的时候,我们可以预测一些潜在的问题,例如文件不存在。来看几种错误:

  1. 致命错误是由于stop函数引起,强制所有执行终止,当函数没有办法继续运行的时候就使用错误error
  2. 警告是warning函数产生,用于显示潜在的问题
  3. 消息是由message函数产生,用于给出充足的输出信息。

withCallingHandlers函数,tryCatch函数,try函数可以进行错误处理。

调试技巧

调试代码的4步:

  1. 意识到漏洞的存在

自动化测试可以查看http://r-pkgs.had.co.nz/tests.html

  1. 重现漏洞

通常情况下,我们需要不断调试缩小范围,确定产生错误的最短代码。

  1. 找到漏洞原因

  2. 修复漏洞并进行测试

调试工具

有三个主要的调试工具:

  1. Rstudio 的错误查看器和traceback函数,他列出导致错的调用顺序
  2. Rstudio 的 rerun with debug 工具和options(error = browser),他在错误发生的地方打开交互式的对话
  3. Rstudio的断点和browser函数,它可以在代码的任意位置打开一个交互式的对话。

如果你在编写新函数,你不应该需要这些工具,如果你需要,那你需要重新考虑你的方法。不要尝试一开始就编写一个具有所有功能的大函数,你应该将所有的功能分称很多的小部分来分别实现。从一段代码开始可以很容易找出问题,如果一开始就是一大堆代码,则很难找到问题的根源。

确定调用顺序

第一个工具是调用栈,导致错误的调用顺序。


f <- function(a) g(a)
g <- function(b) h(b)
h <- function(c) i(c)
i <- function(d) "a"+d
f(10)

我们可以看到如下结果:

错误消息的右侧有两个选项:show Traceback 和Rerun with Debug。如果你惦记Show Traceback,则会显示:

使用traceback函数来获得同样的信息。从下往上进行阅读。可以看到是i()导致错误。

查看错误

点击Rerun with Debug 可以打开交互式调试器,他返回发生错误的那个命令,并且停止在错误发生的地方。

调试模式下有几个特殊的命令(如图顺序)。 1. next :执行下一步 2. Stop init ,与next类似,但是如果下一步是函数,那么单步执行就是执行这个函数 3. Finish :结束当前循环或者函数 4. continue:离开交互式环境并继续函数的正常执行 5. stop:停止调试,终止函数,返回全局空间

查看任意代码

与在出错的地方进入交互式终端一样,你可以进入代码的任意位置,只需啊哟使用rstudio断点或者browser函数。

在Rstudio中,只要在代码的左边行好进行点击即可设置断点,断点有几个小问题 1. 在一些一场情况会失效 2. 不支持条件断点 3. debug函数在指定函数的第一行插入一个浏览器语句,undebug去除。可以使用debugonce函数指浏览下一次运行 4. utils::setBreakpoint函数也一样,但是其不接受函数名,接受文件名和行号。

这两个函数都是trace的特例,trace可以在一个现有函数的任意位置插入任意代码,

调用栈

traceback函数,browser函数+where 和recover函数输出的调用栈是不一样的。下表显示出这三个工具输出的调用栈

其他类型的故障

除了抛出一个错误或者返回一个错误的值,函数还有其他的出错方式: 1. 函数可能产生一个意想不到的警告。查看警告的简单方法是使用options(warn = 2),并使用常规调试工具。如doWithOneRestart函数,withOneRestart函数,withRestarts函数,.signalSimpleWarning函数。 3. 函数可能没有返回值 4. 让R彻底崩溃,这可能错误出现在C代码中。

条件处理

有些错误可以预见,有时候你不希望错误会中断程序。这个时候可以使用错误处理机制。

有三种错误处理机制: 1. try函数 允许我们在错误发生的时候继续执行代码 2. tryCatch函数可以让我们设置一个处理器,该函数控制在条件发生的时候做什么事情。 3. withCallingHandlers函数是tryCatch的一个变体,用的比较少。

try函数

try允许我们在错误发生的时候继续执行代码

f <- function(x){
  try(log(x))
  10
}

f("A")
## Error in log(x) : non-numeric argument to mathematical function
## [1] 10

如果要放长代码,则可以使用{}将他们扩起来。

我们还可以获取try函数的输出,如果成功,则返回代码结果,如果失败,则发挥一个try-error类的不可见对象。

try函数的常见用法是在表达式失败的时候使用默认值,只需啊哟在try代码块的外边进行简单的赋值。另外还可以使用plyr::failwith函数。

tryCatch函数

tryCatch函数处理条件的一个通用工具,可以处理错误,警告,消息和中断。tryCatch 的格式如下所示。

tryCatch(code,
         error = function(x) "error,
         warning = function(x) "warning",
         message = function(x) "message",
         interrupt = function(x) "interrupt",
         finally = function(x) "finally")

finally 参数表示表示永远会执行的内容,在执行清理工作finally参数就很有用,例如删除文件,关闭连接。他在功能上等价于on.exit函数。

withCallingHandlers

withCallingHandlers函数是tryCatch函数的替代函数,很少使用。

自定义信号

大部分的额函数都只使用一个字符串来调用stop函数。如果想要知道到底发生了什么错误,必须阅读错误消息文本。 如果我们想要区分不同的错误,可以定义我们自己的类。stop函数,warning函数,message函数都接受一个字符串或者一个自定义的S3条件对象。

防御性编程

防御性编程的关键是快速失败:如果问题出现就尽快发出错误信息。为了达到这样的效果,有几个编写原则:

  1. 更严格的输入控制
  2. 避免使用非标准计算的函数,如subset。
  3. 避免使用根据输入返回不同类型输出的函数

9.函数式编程

R中的函数可以接受函数作为参数。

匿名函数

在R中函数就是对象。在创建函数的时候,如果不给函数进行密命名,那么就得到了一个匿名函数。

闭包

闭包就是其他函数创建的函数。

匿名函数的用途一般是创建一些没有必要命名的小函数。另一个重要的途径就是创建闭包,即由函数编写函数。闭包将父函数的环境封装并可以访问他的所有变量。

f <- function(exponent){
  function(x){
    x^exponent
  }
}


square <- f(2)
square(2)
## [1] 4

输出闭包

square
## function(x){
##     x^exponent
##   }
## <environment: 0x7fa3abeb3ac8>

查看环境内容的一种方法是将其转换成为列表:

as.list(environment(square))
## $exponent
## [1] 2

另一种方法是:

pryr::unenclose(square)
## function (x) 
## {
##     x^2
## }

闭包的父环境是创建他的函数的执行环境。在函数返回值后,执行环境通常会消失,但是函数能捕获他们的封闭环境。这就意味着当函数a返回函数b 的时候, 函数b能够捕获并且存储函数a 的执行环境,并且他们不会消失。

在R中所有的函数都是闭包,所有的函数都记住创建他们的环境。通常自己写的函数对应全局环境,别人的函数对应某一个包的环境。唯一的例外是元函数 primitive function ,他们使用C代码,没有对应的环境。

函数工厂

上文中的f函数就是一个函数工厂,我们可以描述期望行为的参数调用这个函数,这个函数返回一个符合你要求的函数。

大多数情况而言,函数工厂并不比带有多个参数的函数有用。但是也存在一些特殊情况。 1. 不同层次更加复杂,带有多个参数和复杂的函数体。 2. 当创建函数的时候,有些工作值进行一次

函数工厂的一个例子就是最大似然问题

可变状态

在不同层次管理变量的关键是双箭头赋值符号(<<-),其可以在父环境中寻找匹配的名字,单箭头<-表示在当前状态下进行赋值。

我们看一个函数,这个函数可以记录这恶搞函数被调用了多少次

new_counter <- function(){
  i <- 0
  function(){
    i<<-i + 1
    i
  }
}

count1 <- new_counter()

count1()
## [1] 1
count1()
## [1] 2

通常情况下函数的执行是临时的,但是闭包可以一致访问他所创建的环境.

函数列表

在R中,函数可以存放在列表,这样的好处是可以批量运行函数。例如结合lapply函数。

x <- 1:100
funs <- list(
  sum = sum,
  mean = mean,
  var = var,
  median = median
)

lapply(funs,function(f) f(x,na.rm=TRUE))
## $sum
## [1] 0.8488346
## 
## $mean
## [1] 0.1070514
## 
## $var
## [1] NA
## 
## $median
## [1] 0.240917

10. 泛函

高阶函数就是函数作为输入并且以函数作为输出。上一章节的闭包就是一种,闭包是通过一个函数返回一个函数。闭包的一个补充就是泛函,泛函以函数作为输入并且返回一个向量。

randomise <- function(f) f(runif(1000))

randomise(median)
## [1] 0.5143146
randomise(sum)
## [1] 487.0907

lapply,apply,tapply 就是常用的泛函。泛函的一个常用功能就是替代循环。

lapply

lapply函数接受一个函数,并且将这个函数应用到列表的每一个元素,最后再将结果以列表的形式返回。

lapply用C语言实现,我们可以使用R重新写一个。

lapply2 <- function(x,f,...){
  out <- vector("list",length(x))
  for(i in  seq_along(x)){
    out[[i]] <- f(x[[i]],...)
  }
  out
}

我们从这一段代码可以看到,lapply函数是对一个常见for循环模式的包装。需要注意的是x总是作为f的第一个参数,如果想要修改,可以使用匿名函数。

trims <- c(0,0.2,0.4,0.6)
x <- rcauchy(10000)

lapply(trims,function(trims) mean(x,trim=trims))
## [[1]]
## [1] 0.8702927
## 
## [[2]]
## [1] 0.5800575
## 
## [[3]]
## [1] 0.7458518
## 
## [[4]]
## [1] 0.2270983

循环模式

有三种方法对于向量进行循环

1, for(i in x) 对向量每一个元素遍历 2. 根据元素的数值索引进行循环 for(i in seq_along(x)) 3. 根据元素的名字进行循环 for(i in names(x))

第一种方法通常不是一个好的方法,效率低。第二种方法最简单 对应apply函数也有三种调用方式

  1. lapply(x,function(i){})
  2. lapply(seq_along(x),function(i){})
  3. lapply(names(x),function(i){})

通常情况下,选择第一种方法。

lapply的相似函数

sapply和vapply

这个两个函数与lapply类似,除了最后的输出格式。sapply通过猜测设置输出类型。vapply函数通过一个附加参数来设定输出类型。

多重输入

lapply函数中只有一个参数可以改变,其他参数固定。假设我们有两个列表,一个是观测值,另一个列表是权重,如何计算加权平均。

使用lapply可以实现

library(tidyverse)
x <- replicate(10,runif(10),simplify = FALSE)
w <- replicate(10,rpois(10,5)+1,simplify = FALSE)

lapply(seq_along(x),function(i){
  weighted.mean(x[[i]],w[[i]])
}) %>% unlist()

另外一个替代品是Map函数,其更加的方便:

Map(weighted.mean,x,w) %>% unlist()

Map 和lapply可以相互转换。但是当我们有两个列表或者数据框需要处理的时候,Map函数更加的简洁。

操作矩阵和数据框

  1. apply函数,sweep函数,outer函数 常用于处理矩阵
  2. tapply 函数可以对一个被其他向量分的向量进行汇总
  3. okyr包

矩阵和数组运算

apply函数是sapply函数的变体,它可以处理矩阵和数组。有四个常用参数:

  1. X :矩阵或者数组
  2. Margin,一个整数向量,表示维度,1表示行,2表示列,etc
  3. Fun,函数
  4. 擦描述

sweep函数可以对统计汇总的值进行扫描,他经常和apply函数一起使用来对数组进行标准化。

outer函数,这个函数接受多个向量输入并创建一个矩阵或者数组。

outer(1:3,1:4,"+")
##      [,1] [,2] [,3] [,4]
## [1,]    2    3    4    5
## [2,]    3    4    5    6
## [3,]    4    5    6    7

组应用

tapply是apply的一般化,可以理解为是split函数和sapply函数的结合体

tapply(iris$Sepal.Length,iris$Species,FUN = mean)
##     setosa versicolor  virginica 
##      5.006      5.936      6.588

plyr 包

plyr包提供了一致的命名参数和函数命名

列表操作

可以把泛函看作对列表进行:改变,选取子集,汇总的常用工具集。每个函数式编程语言都有三个工具:Map函数,Reduce函数和Filter函数。

Reduce函数

Reduce函数通过递归调用一个函数f,每一有两个参数,将一个向量简化为一个值。Reduce(f,1:3)等价于f(f(1,2),3)

Reduce(`+`,1:3) # ((1+2)+3)
## [1] 6
Reduce(sum,1:3) 
## [1] 6

判断泛函

判断泛函,就是对列表或者数据框中的每一个元素进行判断。基础包中有三个函数: 1. Filter函数 2. Find函数 3. Position函数 4. where函数

library(tidyverse)
## ── Attaching packages ─────────────────────────────────────── tidyverse 1.3.1 ──
## ✓ ggplot2 3.3.5     ✓ purrr   0.3.4
## ✓ tibble  3.1.3     ✓ dplyr   1.0.7
## ✓ tidyr   1.1.3     ✓ stringr 1.4.0
## ✓ readr   2.1.2     ✓ forcats 0.5.1
## Warning: package 'readr' was built under R version 4.1.2
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## x purrr::compose() masks pryr::compose()
## x dplyr::filter()  masks stats::filter()
## x dplyr::lag()     masks stats::lag()
## x purrr::partial() masks pryr::partial()
Filter(is.factor,iris) %>% head()
##   Species
## 1  setosa
## 2  setosa
## 3  setosa
## 4  setosa
## 5  setosa
## 6  setosa
Find(is.factor,iris) %>% head()
## [1] setosa setosa setosa setosa setosa setosa
## Levels: setosa versicolor virginica
Position(is.factor,iris)
## [1] 5

数学泛函

极限,最大值,求根以及定积分都是泛函。我们来看三个数学泛函。

  1. integrate函数,计算曲线f 下的面积
  2. uniroot 函数计算f=0的值
  3. optimise函数计算f最高点和最低点的位置
integrate(cos,0,pi)
## 4.402787e-17 with absolute error < 2.2e-14
uniroot(cos,interval = c(0,pi))
## $root
## [1] 1.570796
## 
## $f.root
## [1] 6.123234e-17
## 
## $iter
## [1] 2
## 
## $init.it
## [1] NA
## 
## $estim.prec
## [1] 6.103516e-05
optimise(cos,interval = c(0,pi))
## $minimum
## [1] 3.141513
## 
## $objective
## [1] -1

在统计中最大似然估计经常会用到最优化(Maximum Likelihood Estimation ,MLE)。在MLE中。有两个参数集合:数据和参数。这两个参数集合类似的问题非常适合使用闭包。

我们看一个例子,如果我们的数据来自于正态分布,我们如何估计参数。首先创建一个函数工厂,给定一个数据集,他返回参数的负对数似然函数(NLL)

Mypisson <- function(x){
  n <- length(x)
  sum_x <- sum(x)
  function(lambda){
    n * lambda - sum_x * log(lambda)
  }
}

闭包允许我们与计算一些关于数据的常数

x1 <- rpois(100,10)
x2 <- rpois(100,20)

nll1 <- Mypisson(x1)
nll2 <- Mypisson(x2)


optimise(nll1,c(0,50))
## $minimum
## [1] 9.879998
## 
## $objective
## [1] -1275.026
optimise(nll2,c(0,50))
## $minimum
## [1] 19.81
## 
## $objective
## [1] -3934.636

可以看到估计结果是非常接近的。另外一个重要的泛函是optim函数,他是optimise函数的一般化,可以用来处理多于一维的数据。另外Rvmmin包也提供了一个纯R语言版的optim。

泛函之外

有几种循环没有等价的泛函

  1. 原位修改,就是对想有的数据进行修改
  2. 递归关系,当元素之间的关系不是独立的
  3. while循环

创建一个函数系列

这一节抛砖引玉,我们来看如何使用泛函将简单的组件变得功能强大又通用.

我们从一个简单的想法开始,首先计算两个数的加法,然后使用泛函将其拓展,多个数加法,并进化计算,计算累积以及在数据的不同维度之间进行计算。

add <- function(x,y){
  stopifnot(length(x)==1,length(y)==1,is.numeric(x),is.numeric(y))
  
  x+y
}

我们增加一个处理确实值的参数,如果缺失x返回y,如果缺失y返回x,如果都缺失返回一个特殊值

rm_na <- function(x,y,identify){
  if(is.na(x)&&is.na(y)){
    identify
  }else if(is.na(x)){
    y
  }else{
    x
  }
}

然后我们编写一个能够处理确实值的函数

add <- function(x,y,na.rm=FALSE){
  if(na.rm&&(is.na(x)||is.na(y))){
    rm_na(x,y,0)
  }else{
    x+y
  }
}

add(10,NA,na.rm=T)
## [1] 10

基础部分完成,我们将其进行拓展:如果有多个数,add(add(1,2),3).我们可以使用Reduce函数

r_add <- function(x,na.rm=FALSE){
  Reduce(function(x,y)add(x,y,na.rm=na.rm),x)
}

r_add(1:10)
## [1] 55

看起来还不错。但是Reduce对于缺失值的处理有害缺陷:

r_add(NA,na.rm=T)
## [1] NA
r_add(numeric())
## NULL

这是因为,Reduce接收到的是一个长度为1的向量,他就什么也不做,返回原结果,如果长度为0,则返回NULL。这需要跳转Reduce的函数。

r_add <- function(x,na.rm=FALSE){
  Reduce(function(x,y)add(x,y,na.rm=na.rm),x,init = 0)
}

我们在拓展为向量的加法,也就是对向量的两两元素进行求和。可以使用Map函数或者vapply函数实现。

v_add <- function(x,y,na.rm=FALSE){
  stopifnot(length(x)==length(y),is.numeric(x),is.numeric(y))
   
  simplify2array(
    Map(function(x,y)add(x,y,na.rm=na.rm),x,y)
  )
}


v_add2 <- function(x,y,na.rm=FALSE){
  stopifnot(length(x)==length(y),is.numeric(x),is.numeric(y))
  vapply(seq_along(x),function(i)add(x[i],y[i],na.rm =na.rm),numeric(1))
}

v_add(1:10,1:10)
##  [1]  2  4  6  8 10 12 14 16 18 20
v_add2(1:10,2:11)
##  [1]  3  5  7  9 11 13 15 17 19 21

另外一个变体是进行计算累计。将Reduce函数的accumulate参数设置为TRUE即可。

c_add <- function(x,na.rm=FALSE){
  Reduce(function(x,y)add(x,y,na.rm = na.rm),x,accumulate = TRUE)
}

c_add(1:10)
##  [1]  1  3  6 10 15 21 28 36 45 55

我们进一步还可以为更加复杂的数据结构定义加法。

11.函数运算符

函数运算符Function Operator FO以一个函数作为输入,并返回一个函数。函数运算符与泛函类似。虽然不是必须,但是能够让代码更加易读,提高效率。二者不同点在于,泛函提供循环的通用模式,函数运算符提取匿名函数的通用模式。

行为函数运算符

行为函数运算符不会改变函数的输入和输出,而是添加一些信息。我们实现三种行为的函数:

  1. 增加延迟避免服务器被请求淹没
  2. 每n次调用将信息输出到控制台来帮助我们检查一个长时间运行的进程
  3. 缓存上一步的结果来改善新能

假设我们有很多URL需要下载。使用lapply和download_file可以很容易实现:

download_file <- function(url,...){
  download.file(url,basename(url),...)
}
lapply(urls,download_file)

如果列表长,我们希望没10个url就输出一条信息提示我们还在运行。如果是从互联网下载文件,我们可以在下载之间添加小小的延迟,避免服务器负担过重

delay_by <- function(delay,f){
  function(...){
    Sys.sleep(delay)
    f(...)
  }
}

system.time(delay_by(delay = 1,f = runif)(1000))
##    user  system elapsed 
##   0.000   0.000   1.005

这样我们就实现了延迟。我们进一步实现记数:

dot_every <- function(n,f){
  i <- 1
  function(...){
    if(i %%n==0){cat(".")}
    i<<-i+1
    f(...)
  }
}

需要注意的是,每个函数运算符中,我们豆浆函数设置成为最后一个参数。

这样我们就实现了我们的功能

download <- dot_every(10,delay_by(download_file,1))

缓存

下载文件的时候需要避免对一个文件多次下载,可以使用unique函数进行去重来避免这样的问题。另一种方法是使用缓存:对函数进行修改使它自动将结果保存。

library(memoise)
slow_function <- function(x){
  Sys.sleep(1)
  10
}

fast_function <- memoise(slow_function)
system.time(fast_function)
##    user  system elapsed 
##       0       0       0
system.time(fast_function)
##    user  system elapsed 
##       0       0       0

缓存问题是计算机科学中以内存换速度的典型例子。一个被缓存的函数可以运行得非常块。使用缓存的一个典型例子就是狄波拉契数列。

fib <- memoise(function(n){
  if(n<2) return(1)
  fib(n-2)+fib(n-1)
})
system.time(fib(500))
##    user  system elapsed 
##   0.082   0.003   0.086

当然,有些问题不能缓存,例如随机数,如果缓存了就不是随机数了。我们将memoise函数应用到我们上一个问题。

download <- dot_every(10,memoise(delay_by(download_file,1)))

捕获函数调用

我们可以使用tee函数获取函数内部的执行情况。函数tee有三个参数,f是要修改的函数,on_input,与函数f的输入一起被调用的函数,on_output是与函数输出一起被调用的函数。

ignore <- function(...) NULL

tee <- function(f,on_input = ignore,on_output = ignore){
  function(...){
    on_input(...)
    output <- f(...)
    on_output(output)
    output
  }
}

使用tee可以查看泛函uniroot的内部,查看其如何通过迭代得到结果

g <- function(x) cos(x)- x

zero <- uniroot(g,c(-5,5))
show_x <- function(x,...) cat(x,sep = "\n")

zero <- uniroot(tee(g,on_input = show_x),c(-5,5))
## -5
## 5
## 0.2836622
## 0.8752034
## 0.7229804
## 0.7386309
## 0.7390853
## 0.7390243
## 0.7390853

我们创建一个remember函数,让他来捕获调用序列。

remember <- function(){
  memory <- list()
  f <- function(...){
    memory <<-append(memory,list(...))
    invisible()
  }
  structure(f,class = "remember")
}

as.list.remember <- function(x,...){
  environment(x)$memory
}

print.remember <- function(x,...){
  cat("Remembering...\n")
  str(as.listx)
}

我们绘制一幅图来查看unitroot函数如何得到最终解。

locs <- remember()
vals <- remember()

zero <- uniroot(tee(g,locs,vals),c(-10,10))
x <- unlist(as.list(locs))
error <- unlist(as.list(vals))

plot(x,type="b")

plot(error)

输出函数运算符

对函数的输出进行修改

简单修饰

base:Negate函数和plyr::failwith函数提供了简单有用的函数运算符。Negate函数接受一个可以返回逻辑向量的函数,并且返回这个函数的结果的逆。

# Negate <- function(f){
#   force(f)
#   function(...) !f(...)
#   
# }

(Negate(is.null))(NULL)
## [1] FALSE

当错误发生的时候,plyr::failwith函数可以将抛出错误的函数转换成为另一个可以返回默认值的函数。failwith函数的本质是try函数的一个封装。

# failwith <- function(default = NULL,f,quiet =FALSE){
#   force(f)
#   function(...){
#     out <- default
#     try(out <- f(...),silent = quiet)
#     out
#   }
# }

# plyr::failwith(NA,log,quiet = F)("a")

failwith函数和泛函一起使用非常有用,错误不会扩散,外层循环也不会终止。

改变函数的输出

除了返回初始返回值外,还可以让函数返回函数计算的其他一些结果。

  1. 函数返回print函数的输出文本
capture <- function(f){
  force(f)
  function(...){
    capture.output(f(...))
  }
}

str_out <- capture(str)

str(1:10)
##  int [1:10] 1 2 3 4 5 6 7 8 9 10
str_out(1:10)
## [1] " int [1:10] 1 2 3 4 5 6 7 8 9 10"
  1. 返回函数的运行事件
time_it <- function(f){
  force(f)
  function(...){
    system.time(f(...))
  }
}

compute_mean <- list(
  base = function(x) mean(x),
  sum = function(x)sum(x)/length(x)
)

x <- runif(1e1)

call_fun <- function(f,...) f(...)

lapply(compute_mean,time_it(call_fun),x)
## $base
##    user  system elapsed 
##   0.000   0.001   0.000 
## 
## $sum
##    user  system elapsed 
##   0.001   0.000   0.001

输入函数运算符

预填充函数参数

匿名函数经常用来创建一个具有特定参数的函数变体,这成为局部函数应用,由pryr::partial函数实现。

f <- function(a) g(a,b=1)
compact <- function(x) Filter(Negate(is.null),x)
Map(function(x,y) f(x,y,zs),xs,ys)

替换成

f <- partial(g,b=1)
compact <- partial(Filter,Negate(is.null))
Map(partial(f,zs=zs),xs,ys)

可以使用这种方法来简化代码

fun <- list{
  sum = function(...) sum(...,na.rm = TRUE)
}

写成

library(pryr)
fun <- list(
  sum = partial(sum,na.rm = TRUE)
)

改变输入类型

还可以对函数的输入进行各种修改 1. base::Vectorize函数可以将一个标量函数转换成为一个向量函数。 2. splat函数 接受多个参数的函数转换成为只接受一个参数列表的函数 3. plyr::colwise函数将向量函数转换成为处理数据框的函数

组合函数运算符

函数运算符可以接受多个函数作为输入,plyr包中的each函数就是一个例子,他接受一个向量话函数列表并将他们组合成为一个函数。

f <- plyr::each(mean,sd,median)

f(1:20)
##     mean       sd   median 
## 10.50000  5.91608 10.50000

函数符合

将函数进行组合的重要方式是通过:f(g(x))

sapply(mtcars,function(x) length(unique(x)))
##  mpg  cyl disp   hp drat   wt qsec   vs   am gear carb 
##   25    3   27   22   22   29   30    2    2    3    6

一个简单复合函数如下:

compose <- function(f,g){
  function(...){
    f(g(...))
  }
}

pryr::compose函数提供了一个更加完全的替代版本

sapply(mtcars,pryr::compose(length,unique))
##  mpg  cyl disp   hp drat   wt qsec   vs   am gear carb 
##   25    3   27   22   22   29   30    2    2    3    6

12. 非标准计算

访问用来对函数参数进行计算的代码,称为非标准计算

表达式获取

substiture函数查找函数参数并且不看函数的值

substitute(1:10)
## 1:10

deparse函数能够将substitute的结果转化成为一个字符向量。

在子集中进行非标准计算

quote函数和substitute一样捕获表达式,但不会做进一步的转换。

quote(1:10)
## 1:10

eval函数可以对表达式进行求值,无论x是什么eval(quote(x))等价于x。eval函数的第二个参数可以设带代码的环境,也可以是列表或者表达式

作用域问题

eval的第三个参数enclos指定如果当前环境没有表达式,应该去哪里找

13.表达式

表达式的结构

quote函数返回一个表达式,他是一个对象,表示一个可以被R执行的动作

表达式可以抽想成为语法树

pryr::ast(y <- x * 10)
## \- ()
##   \- `<-
##   \- `y
##   \- ()
##     \- `*
##     \- `x
##     \-  10

表达式有4个可能的元素:常量,名字,调用和成对列表

名字

quote函数可以捕获名字,还可以使用as.name函数将字符串转换成为名字。名字也称为符号,因此as.symbol也可以创建,无效的名字会自动加上反引号。

调用

很多种方式

x <- quote(read.csv("importam.csv",row.names=FALSE))
x

x[[1]]

x$row.names

pryr包中的standardise_call函数可以对参数进行标准化

pryr::standardise_call(quote(read.delim(s= ",","data.txt")))
## read.delim(file = "data.txt", sep = ",")

可以使用call或者as.call根据调用元素生成新的调用,call的第一个参数时表示函数名的字符串,其他参数表示调用参数的表达式。

call(":",1,100)
## 1:100
call("mean",quote(1:10),na.rm= TRUE)
## mean(1:10, na.rm = TRUE)

as.call是call的变体,以一个单独列表作为输入,函数一个元素是名字或者调用,其他表示参数

as.call(list(quote(mean),quote(1:10)))
## mean(1:10)

捕获当前调用

  1. sys.call 捕获用户输入
  2. match.call创建一个只使用命名参数的调用

成对列表

行为和列表相同,不常用。除了应用在函数参数中。

解析和逆解析

parse函数和deparse函数功能相反,parse将字符串转换成为表达式 # 14.相关语言

HTML

LaTex

是统计学家和数学家的通用语言

15.性能

R为什么慢

极少数的R语言具有正规的编程和软件开发训练,少数人通过编写R程序谋生,大多数人通过R语言理解数据。

微测试

microbenchmark包提供了非常精确的记时。默认情况下,microbenchmark函数对每一个表达式运行100次,然后对结果进行汇总。

语言性能

R语言性能三个方面的折中: 1. 极端动态性 2. 可变环境的名字查找 3. 函数参数的惰性求值

极端动态性

R语言几乎任何创建后的东西都可以修改。好处是灵活,缺点是很难预测会发生什么。这意味着对解释器和编译器而言,必须要考虑很多的选项。

可变环境的名字搜索

在R中找到一个名字的关联值非常的困难,需要查找搜索路径中的每个环境。

惰性求值

R使用了一个约定对象,它包含计算结果需要的表达式和执行计算的环境。创建这些对象存在一些资源开销。

提升性能

R有20年历史,80w行带阿妹,其中45%C代码,19%R代码,17为Fortran代码。只有R核心组成员才能够修改基础R。目前R核心组有20名成员,但是有6位活跃在日常开发。没有一位是全职。大多数人是统计学教授,他们只花费非常少的事件,并且为了避免破坏现有代码,因此倾向于保守。R核心拒绝可以提高R性能的提议,认为关注点在构建一个数据分析和统计的稳健平台。

其他R实现

最成熟的四个R开源项目

  1. pqR :修复了性能问题,提供更好的内存管理和自动多线程支持
  2. Renjin
  3. FastR
  4. Riposte

16. 代码优化

这一章节主要介绍使用lineprof包来查看R代码性能。

library(lineprof)
source("code.R")

改进性能

  1. 查找已有的解决方案,看看其他人如何解决,查看CRAN task view,去stackoverflow搜索,google等
  2. 减少工作量 ,更具体的输入输出,更具体的定制函数
  3. 向量化
  4. 并行化:使用mclapply替换lapply
library(parallel)
cores <- detectCores()
cores
## [1] 8
pause <- function(i){
  function(x) Sys.sleep(i)
}

system.time(lapply(1:10,pause(0.3)))
##    user  system elapsed 
##   0.001   0.000   3.036
system.time(mclapply(1:10,pause(0.3),mc.cores=cores))
##    user  system elapsed 
##   0.015   0.049   0.618
  1. 避免复制
  2. 字节编码
  3. 使用更快的语言

其他: 1. 阅读Rblog (http://www.r-bloggers.com) 2. book , 3. 学习算法和数据结构(http://www.coursera.org/course/algs4partI) 4. 优化相关的书籍:《Mature optimisation》,《Pragmatic Programmer》

17. 内存

  1. pryr包的,object_size函数 可以查看
  2. pryr的mem_used函数可以查看内存中的总大小
  3. mem_change函数可以查看代码执行过程中内存如何变化
  4. utils::Rprof函数可以对性能进行分析。lineprof包的lineprof

18.Rcpp

使用C++

cppFUnction函数允许我们在R中编写C++代码

library(Rcpp)
cppFunction('int add(int x, int y, int z){
            int sum = x + y + z;
            return sum;
            }')

add

add(1,2,3)

运行这段代码,Rcpp将对C++代码进行编译并构建一个与编译后的C++函数链接在一起的R函数。

sourceCpp函数可以单独保存C++代码。

Rcpp 主页,http://www.rcpp.org 注册一个Rcpp邮件列表即使接受消息

学习C++的资源

  1. Effective C++
  2. C++ Annotations

19. R语言C接口