这个是对于《高效R语言编程》的读书记录
性能和机能测试
基准测试
我们在写代码的时候,要去考虑代码的运行速度的。目前有两种方法来评价我们的运行速度。
-
system.time()
可以来对代码运行一次的时间进行评价。 -
microbenchmark::microbenchmark
来进行运行多次时间评选。同时也可以比较多个代码之间的优劣
library(microbenchmark)
df <- data.frame(v = 1:4, name = letters[1:4])
## 三种取第三行第二列的方式
microbenchmark(df[3,2], df[3,"name"], df$name[3])
## Unit: nanoseconds
## expr min lq mean median uq max neval cld
## df[3, 2] 9309 9575.5 9752.87 9687.0 9817.5 12468 100 b
## df[3, "name"] 9192 9585.5 10217.35 9707.5 9875.5 50222 100 b
## df$name[3] 973 1158.5 1317.04 1327.0 1384.5 4715 100 a
性能测试
基准测试通常是测试一个函数和其他函数比较的运行时间。而性能测试则是测试一大块代码当中哪个行代码更占用
时间。我们通过profvis::profvis
来进行性能测试。
# Load the data set
data(movies, package = "ggplot2movies")
# Load the profvis package
library(profvis)
# Profile the following code with the profvis function
profvis({
# Load and select data
comedies <- movies[movies$Comedy == 1, ]
# Plot data of interest
plot(comedies$year, comedies$rating)
# Loess regression line
model <- loess(rating ~ year, data = comedies)
j <- order(comedies$year)
# Add fitted line to the plot
lines(comedies$year[j], model$fitted[j], col = "red")
}) ## Remember the closing brackets!
image
通过上图我们就可以发现我们在构建模型的时候。花的时间是最多的。
R高效配置
R启动文件
每次启动R的时候都将读入两个文件。.Renviron以及.Rprofile。
-
.Renviron:
首要目的是设置环境变量。这些变量通知R去何处查找程序等等的。 - .Rprofile: 用于每次启动R前简单的运行几行R代码。
两个文件一般来说是先查找.Renviron然后在查找.Rprofile
启动文件位置
启动文件可以放到三个位置上:
- R_HOME: 可以通过
R.home()
来查看 - HOME: 可以通过
Sys.getenv("HOME)
来查看 - R当前的工作目录: 可以通过
getwd()
来查看
R寻找启动文件的顺序是3>2>1的。如果前一个有相关文件就不会在后面进行查找了
.Rprofile文件
这个文件其实类似于在启动的时候。加载一个简单的脚本。这样的脚本可以放置一些我们的常规的设置。比如定义一些常规的函数以及对于options()
的修改。
一些有用的功能。
##一些options的设置
options(stringsAsFactors = F) ## 把文本转让为因子去掉
options(prompt = "R>") ## 把代码的提示符从">"变为"R>"
options(continue = " ") ## 把相同代码的不同行之间的"+"设置为空白
## 定义一些简单的函数
hf <- function(dat) dat[1:5,1:5] ### 查看文件的前五行
## 缩写一些函数
v = utils::View
R启动参数
在shell下运行R的时候,我们可以设置一些简单的参数
-
--no-environ
/--no-init
: 通知R尽在当前目录下查找启动文件。 -
--no-restore
: 通知R不加载可能存在于当前目录下的.RData文件 -
--no-save
:
通知R,当用户使用q()
的退出的时候,不询问是否保存文件。
高效编程
1. 尽量不要增加向量的大小
只要有可能,提前分配好向量的大小然后填入数据
### 比较两个不同函数的时间
method1 <- function(x){ ###逐渐增加向量
vec <- NULL
for(i in seq_len(x)){
vec <- c(vec, i)
}
vec
}
method2 <- function(x){ ### 提前制定好变量长度
vec <- numeric(x)
for(i in seq_len(x)){
vec[i] <- i
}
vec
}
microbenchmark(method1(10000), method2(10000), unit = "s")
## Unit: seconds
## expr min lq mean median uq
## method1(10000) 0.1388324 0.147609943 0.1567819665 0.1530471155 0.1606227850
## method2(10000) 0.0005931 0.000636882 0.0007747023 0.0006527875 0.0006813865
## max neval cld
## 0.239146280 100 b
## 0.008395814 100 a
2.尽可能向量化代码
尽可能的访问底层的C/Fortran代码。基于这个原则,大量的R函数被向量化,这意味着,函数的输入和
/输出通常是向量。从而降低了函数调用请求的数量。
3.适当时机下使用因子(factor).
因子字符的编码方式上。其实因子本质是数字。只是赋予了不同的字符。而字符就单纯的是字符。所以因子稍微的节约空间
FactorVec <- iris$Species
ChaVec <- as.character(iris$Species)
sapply(list(FactorVec, ChaVec), pryr::object_size)
## Registered S3 method overwritten by 'pryr':
## method from
## print.bytes Rcpp
## [1] 1248 1432
4.通过缓存变量避免不必要的计算
只计算对象一次的并在重用结果是提高代码速度的简单直接的方法。例如👇两个代码:
### 方法一:
apply(x, 2, function(i) mean(i)/sd(x))
### 方法二:
sd_X <- sd(x)
apply(x, 2, function(i) mean(i)/sd_X)
相较于方法一:方法二当中的sd只计算了一次。所以相对来说会一些。
缓存更高级的形式是使用memoise
包。如果一个函数在相同输入下被调用多次,将已知结果存入可检索的缓存中可以加快运行速度。这是一种通过内存消耗来换取速度的方法。
plot_mpg <- function(x){
data(mpg, package = "ggplot2")
mpg <- mpg[-x,]
plot(mpg$cty, mpg$hwy)
lines(lowess(mpg$cty, mpg$hwy),col = 2)
}
m_plot_mpg <- memoise::memoise(plot_mpg)
microbenchmark(unit = "ms", plot_mpg(10), m_plot_mpg(10))
image
5. 字节编译包可使性能轻而易举的提升
通过compiler
包可以将函数编译成字节代码。这样可以增加函数运行的速度。
mean_r <- function(x){
m = 0
n = length(x)
for(i in seq_len(n))
m = m + x[i]/n
m
}
cpm_mean_r <- compiler::cmpfun(mean_r)
x = rnorm(1000)
microbenchmark(times = 10, unit = "ms", mean_r(x), cpm_mean_r(x), mean(x))
## Unit: milliseconds
## expr min lq mean median uq max neval
## mean_r(x) 0.050395 0.050956 0.3490756 0.0516625 0.094729 2.926259 10
## cpm_mean_r(x) 0.050556 0.051660 0.0608802 0.0526610 0.068143 0.101249 10
## mean(x) 0.004334 0.004560 0.0099136 0.0047280 0.010270 0.042967 10
## cld
## a
## a
## a
高效工作流
5条建议
- 编程前要有清晰头脑,或许你可以准备好纸和笔,以保证你牢记自己的目标,而不是沉溺于技术中。
- 制定计划,计划的规模和类型取决于项目。
- 今早选择项目中要用到的包。
- 记录你的每个阶段的工作。
- 尽可能使你的整个工作流可复制。
R包的查找
- 我们可以在谷歌上进行直接的搜索
- rdocumentation.org提供了可以检索R包的地方。我们可以在这里进行搜索
- 在R当中使用
RSiteSearch()
也是进行搜索R包的一个方法
高效输入/输出(I/O)
关于数据I/O的5条技巧
- 如果可能,不管文件是从互联网下载还是拷贝导你的电脑上,保留本地文件名不变。这将有助于将来跟踪数据来源。
- R语言有自己的数据格式.Rds。可使用
readRDS()
和saveRDS()
导入和导出该格式文件。是一种速度雨空间储存都非常高效的数据格式 - 使用
rio
包当中的import()
可导入各种可是的数据。从而避免了特定格式的代码选择 - 大文件的导入推荐使用
data.table
和readr
。现在还有vroom
可以使用 - 使用
file.size()
和object.size()
跟踪文件与R对象大小,以便在他们过大的时候提前预防
高效优化
1.在开始优化代码之前,使用代码分析器
通过profivs
来对代码进行性能分析
2. 知道一些高效的R基础
if()函数和ifelse()函数
虽然ifelse()
在代码上保持了简洁性。但是在运行速度上却要比if()
慢很多。另外在dplyr::if_else
的运行速度要比ifelse()
快但是仍是if()
快一些。至于基于if_else
上创建的case_when
速度就又要稍微慢一些了。
ifelseFunc <- function(x){
ifelse(x > 40, "pass", "fail")
}
IfFunc <- function(x){
result <- rep("fail", length(x))
result[x > 40] <- "pass"
}
if_elseFunc <- function(x){
dplyr::if_else(x > 40, "pass", "fail")
}
caseWhenFunc <- function(x){
dplyr::case_when(
x > 40 ~ "pass",
TRUE ~ "fail"
)
}
marks <- runif(10e4, min = 30, max = 99)
microbenchmark(unit = "ms",ifelseFunc(marks), IfFunc(marks), if_elseFunc(marks),caseWhenFunc(marks))
## Unit: milliseconds
## expr min lq mean median uq
## ifelseFunc(marks) 13.071040 15.093970 16.171436 15.558156 16.974654
## IfFunc(marks) 1.177236 1.825465 2.162302 1.959760 2.069466
## if_elseFunc(marks) 4.173324 5.412398 6.378833 5.799150 6.325605
## caseWhenFunc(marks) 7.194478 8.988347 14.369881 9.676258 10.968151
## max neval cld
## 21.610459 100 c
## 6.518785 100 a
## 15.240395 100 b
## 172.603551 100 c
使用&&代替&,以及使用||代替|
x < 0.4 | x > 0.6
一般而言逻辑判断是&或者|,在R进行比较的时候。会先计算x >
0.6的结果而不管x <
0.4的结果。想比较而言,非向量的版本&&,只有在非要情况下执行第二个条件。
但是仍需要担心的时候,不要使用&&或者||操作向量,因为它只会计算向量的第一个然后返回一个结果
x <- c(0.8,0.5,0.6)
x < 0.4 | x > 0.6
## [1] TRUE FALSE FALSE
x < 0.4 || x > 0.6
## [1] TRUE
如果数据类型是一个类型的话,可以考虑将数据框转换为矩阵
对于data.frame每一列可以是同一个数据格式。但是matrix这个矩阵只有一个数据格式。由于有不同的数据格式,所以data.frame在保存数据的时候每一列的是分开的。这就导致,我们在提取一行的时候的时候,其实是要从不同的保存位置提取数据。这样速度就慢了。相比较而言matrix由于数据类型一直,所以保存的都在一起。这样提取的时候就方便很多。另外数据的大小而言,matrix也要比data.frame小一些。
需要注意的是,数据要一致。如果有数字还有字符。则会都转换为字符。
3.尽快能的使用专用的行列函数
对于行列的统计计算。我们可以通过apply
来进行操作。但是会降低速度。可以使用一些专用的函数类似rowSums()
,colSums()
,rowMean()
以及colMean()
这些的。其中matrixStats
包含有很多类似的优化的函数。
4.为了性能优化,考虑用C++重写代码中的关键部分。
这个很好理解的,毕竟底层语言嘛。还是要快的。
网友评论