Skip to content

Latest commit

 

History

History
503 lines (358 loc) · 20.3 KB

R-Advanced.md

File metadata and controls

503 lines (358 loc) · 20.3 KB

目录

R语言笔记

R语言笔记

1. 用pheatmap画热图 目录

需要输入的数据为matrix,所以若原始数据为第一列是行名的data.frame,则需要将其转换为matrix

> data_matrix<-as.matrix(data[,-1])
> rownames(data_matrix)<-data$col1

用默认参数绘图:

pheatmap(data)#默认参数

pheatmap默认对column和row都进行聚类,基于欧式距离进行聚类

1、 若不想进行聚类,则可以通过cluster_rowscluster_cols参数设置对应维度上的聚类是开启还是关闭

pheatmap(data,...,cluster_rows=F,cluster_cols=F)

2、 均一化后再绘制热图,需要设置scale参数,"row","column" or "none"默认是"none"

pheatmap(data,scale="column",...)
pheatmap(data,scale="row",...)

3、隐藏行名或列名,设置show_colnames或show_rownames参数

pheatmap(data,show_colnames=F,...)

4、给列(一般一列可以看作是一个样本)添加分组信息,需要创建一个用于保存分组信息的数据框

annotation_col<-data.frame(...) # 需要是factor类型
rownames(annotation_col)<-colnames(data)	# 需要将数据框的行名设成输入数据的列名
pheatmap(data,...,annotation_col=annotation_col)

5、添加标题,设置main参数

pheatmap(data,...,main="title")

2. R中的并行化方法 目录

R并行计算的必要性:

R 的内存使用方式和计算模式限制了 R 处理大规模数据的能力

R 采用的是内存计算模式(In-Memory),被处理的数据需要预取到主存(RAM)中,优点是计算效率高、速度快,但缺点是这样一来能处理的问题规模就非常有限(小于 RAM 的大小)

另一方面,R 的核心(R core)是一个单线程的程序

现代的多核处理器上,R 无法有效地利用所有的计算内核

并行计算技术正是为了在实际应用中解决单机内存容量和单核计算能力无法满足计算需求的问题而提出

R 中的并行计算模式大致可以分为隐式和显示两种

  • 隐式并行计算

    隐式计算对用户隐藏了大部分细节,用户不需要知道具体数据分配方式 ,算法的实现或者底层的硬件资源分配。系统会根据当前的硬件资源来自动启动计算核心

    这种模式对于大多数用户来说是最喜闻乐见的。我们可以在完全不改变原有计算模式以及代码的情况下获得更高的性能

  • 显示并行计算

    显式计算则要求用户能够自己处理算例中数据划分,任务分配,计算以及最后的结果收集,因此,显式计算模式对用户的要求更高,用户不仅需要理解自己的算法,还需要对并行计算和硬件有一定的理解

    值得庆幸的是,现有 R 中的并行计算框架,如 parallel (snow,multicores),Rmpi 和 foreach 等采用的是映射式并行模型(Mapping),使用方法简单清晰,极大地简化了编程复杂度

2.1. parallel包 目录

R的并行化是在 *apply 系列函数的基础上产生的,所以在介绍R的并行化之前,有必要对 *apply 系列函数作一个简单的说明,下面只对apply( )进行说明

函数语法格式:apply(x, margin, fun, ...)

  • x:一个data.frame或者是一个matrix

  • margin:选择1或者2,1表示行,2表示列

  • fun:一个函数对象,可以是R自带的,也可以是用户自定义的函数

  • ...:传递给函数fun的其他变量

例如:apply(x,1,sum),将变量x逐行传递给函数sum,进行求和,得到的是变量x每行的和的列表

一个任务之所以能够进行并行化处理,是因为该任务可以被拆分成许多个相互独立的小任务,每个小任务的求解对其他任务没有任何影响,因此可以对它们进行分而治之,最后将每个小任务求解结果进行汇总,即简单地合并,apply函数实现的就是这样的任务,将一个比较大的变量X按照行(margin=1)或列(margin=2)进行拆分,然后对每个行分别独立地进行求解

但是*apply系列函数的分治策略并没有进行并行化,是一个只利用了一个线程的串行任务,但是由于它本身的分治属性,对它进行简单地改造和封装,就可以实现高效地并行化,这便是parallel

library(parallel)

# 初始化一个并行化的集群
## 设置集群使用的核心数
if (is.na(Args[4])){
	no_cores <- detectCores() - 10
} else {
	no_cores <- as.integer(Args[4])
}
## 按照指定的核心数,初始化一个集群对象
cl <- makeCluster(no_cores)

# 调用parApply函数,执行并行化任务
out <- parApply(cl,data,1,fun)

# 任务结束后,关闭集群对象
stopCluster(cl)

注意:

如果有环境里面的外置变量(自己定义)那么需要额外插入,复制到不同核上面,而且如果有不同包里面的函数,都要额外加载、复制多份给不同的电脑核心

xx <- 1

clusterExport(cl, "xx")

2.2. foreach包 目录

初始化的过程有些不同,你需要register注册集群:

library(foreach)
library(doParallel)

cl<-makeCluster(no_cores)
registerDoParallel(cl)

记得最后要结束集群:

stopImplicitCluster()

oreach函数可以使用参数.combine控制你汇总结果的方法:

> foreach(exponent = 2:4, .combine = c)  %dopar%  base^exponent
[1]  4  8 16

> foreach(exponent = 2:4, .combine = rbind)  %dopar%   base^exponent
         [,1]
result.1    4
result.2    8
result.3   16

> foreach(exponent = 2:4, .combine = list, .multicombine = TRUE)  %dopar%   base^exponent
[[1]]
[1] 4

[[2]]
[1] 8

[[3]]
[1] 16

注意到最后list的combine方法是默认的。在这个例子中用到一个.multicombine参数,他可以帮助你避免嵌套列表。比如说list(list(result.1, result.2), result.3)

> foreach(exponent = 2:4, .combine = list)  %dopar%   base^exponent
[[1]]
[[1]][[1]]
[1] 4

[[1]][[2]]
[1] 8


[[2]]
[1] 16

注:

3. R语言向量化技术 目录

什么是向量化计算呢?其实你可以简单的理解成这样:当我们在使用函数或者定义函数的时候发现,我们只能对单个数据进行运算。而这点显然不能满足我们的需求。那么如何使函数可以计算多个数据呢?这时就要采用向量化计算的方法了。

举一个简单的例子来理解向量化技术:

首先,我们先定义一个判断是否为偶数的函数,返回值为布尔值:

func <- function(x){
  if(x %% 2 == 0){
    ret <- TRUE
  } else{
    ret <- FALSE}
  return(ret)
}

当我们使用这个函数时发现他只能对单个数据进行运算,而多个数据时则会报错:

> func(c(123,34,12,43))
[1] FALSE
Warning message:
In if (x%%2 == 0) { :
  the condition has length > 1 and only the first element will be used
>

这个时候如果想对一个向量的每一个元素都执行该函数,如何实现?

  • 第一种方法就是,使用循环操作

     for(i in c(123,34,12,43)){
     	func(i)
     }
    

    当然这样实现完全没有问题,但是一点也不优雅

  • 第二种方法,使用R语言中带有的向量化技术去实现

4. 标量化和向量化的逻辑运算 目录

R语言的逻辑运算有两种:“与”运算和“或”运算

  • “与”运算:&&&
  • “或”运算:|||

其中,运算符“逻辑与”和“逻辑或”存在两种形式,&|作用在对象中的每一个元素上并且返回和比较次数相等长度的逻辑值;&&||只作用在对象的第一个元素上

5. Divide-Conquer:矩阵的分块计算与子块计算结果合并 目录

在对一个大矩阵执行相关性计算或Jaccard Index的计算时,其实执行的是矩阵任意两行(这里假设要进行分析的对象是矩阵的每个行)之间的两两的计算,若这个矩阵的规模非常庞大,有n行时,计算的时间复杂度就是$O(n^2)$,这个时候可以采用并行化策略来加速这个进程(参考上文的 2. R中的并行化方法):

StatOut <- parApply(cl, data, 1, fun, data)

这样就会实现将一个 n vs. n 的问题拆分成 n 个可以并行解决的 1 vs. n 的问题,当然通过设置线程数为$m,,(m\le n)$,使得每次只并行执行m个 1 vs. n 的问题

然后再在函数fun内部再定义一个并行化计算来进一步并行化加速上面产生的 1 vs. n 的计算:

fun <- function(vec, data){
	...
	parApply(cl, data, 1, fun2, vec)
}

在这个函数内部实现了将一个 1 vs. n 的问题拆分成 n 个可以并行解决的 1 vs. 1 的问题

这样就实现了两步并行化,这样在保证硬件条件满足的情况下,的确能显著加快分析速度

但是并行化技术会带来一个问题是,虽然时间开销减少了,但是空间开销显著增加了

比如,第一次并行化,将一个 n vs. n 的问题拆分成 $\frac{n}{m}$ 次可以并行解决的 m个 1 vs. n 的问题,则需要在每一次并行化任务中拷贝一个 1 vs. n 的计算对象,即原始有n行的矩阵被拷贝了m次,则相应的缓存空间也增加了m倍,很显然内存的占用大大增加了

空间开销显著增加带来的后果就是,很容易导致运行内存不足程序运行中断的问题,那该怎么解决这个问题呢?

可以采用分治方法(Divide-Conquer),将原始大矩阵,按照行拆分成多个小的子块,对每个子块执行计算,从而得到每个子块的运算结果,最后再讲每个子块的结果进行合并:

n_row <- nrow(data)
nblock <- 10 # 用于设定子块的数,用户可以自行指定
# 该列表变量用于保存每个子块的计算结果,若总共拆成了m个子块,
# 因为要进行任意两子块的计算,因此会有mxm个子块计算结果,因
# 此该列表要保存的数据为mxm维度的数据,每个元素为一个计算结
# 果,它们都是矩阵
StatOutList <- vector("list", nblock) 

# 1. 开始进行分块计算
print("[Divide-Conquer]Start carry out Divide-Conquer for Large Observed Matrix")
print("##################################################")
for(i in 1:nblock){
	for(j in 1:nblock){
		nrow_start <- (i-1)*nrow_block+1
		nrow_end <- i*nrow_block
		# 并行化计算
		if(!is.na(Args[2])){
			print(paste("[Divide-Conquer]Start carry out statistic Jaccard Index parallel for block: ",i,"-",j,sep=''))
			StatOutList[[i]] <- append(StatOutList[[i]], parApply(cl, data[nrow_start:nrow_end,], 1 , JaccardIndexSer, data))
			print(paste("[Divide-Conquer]Finish run parallel for block: ",i,"-",j,sep=''))
		# 串行计算
		}else{
			print(paste("[Divide-Conquer]Start carry out statistic Jaccard Index serially for block: ",i,"-",j,sep=''))
			StatOutList[[i]] <- append(StatOutList[[i]], apply(data, 1 , JaccardIndexSer, data))
			print(paste("[Divide-Conquer]Finish run serially for block: ",i,"-",j,sep=''))
		}
	}
}

# 2. 结束分治方法的分块计算
if(!is.na(Args[2])){
	print("##################################################")
	print("[Divide-Conquer]Finish parallel running for statistic Jaccard Index!")
	stopCluster(cl)
}else{
	print("##################################################")
	print("[Divide-Conquer]Finish serial running for statistic Jaccard Index!")
}

# 3. 开始进行子块结果的合并
print("[Divide-Conquer]Start bind sub-block statout")
StatOut <- vector("list", nblock)
# 先对列进行合并
for(i in 1:nblock){
	for(block in StatOutList[[i]]){
		StatOut[[i]] <- cbind(StatOut[[i]], block)
	}
}
# 再对行进行合并
StatOutMerge <- data.frame()
for(block in StatOut){
	StatOutMerge <- rbind(StatOutMerge, block)
}

6. match函数:到底谁match谁,傻傻分不清 目录

match函数的一般应用场景:

7. 逐行读入文件 目录

类似于其他编程语言中的操作,先要创建一个文件句柄:

f <- file(filename,'r')

然后逐行读入:

row_current <- readLines(f, n=1) # 用参数n控制一次读入的行数,这里设为1

读入的文本信息一般来说是以制表符形式进行分割的格式,此时可以将读入的行基于分隔符进行打散

Row_current <- strsplit(row_current, '\t')

8. 关于超大矩阵运算的思考 目录

此处不讨论分布式计算的问题,因为本狗(科研狗)也不懂~

首先,对我所谓的“超大矩阵”作一个简单的定义:

它的“大”首先体现在矩阵文件的大小上,一般单个文件就要大于4G,那么如果我们以R中常用的read.table这个系列的函数去读取这个文件的话,由于read.table是一次性将文件内容全部读入内存当中,所以:

  • 大文件不仅意味着大内存消耗(实际的内存消耗要远大于原始文本文件的文件大小,例如原始文本文件可能只有4G,读到内存当中可能就变成了100G);

  • 也意味着较长的读写等待时间,一个文件光将它读入等待的时间就得按照小时计算,那后面的活还干不干了!

  • 读入后的每一步矩阵操作都是慢动作。即使克服了内存消耗的问题(一般的服务缓存空间起码都有500G+,所以还是能够应付咱们的内存消耗问题的),也耐住了寂寞等到了把整个文件读入内存的时刻,但是问题才刚刚开始,要知道对这么一个庞然大物,每一步操作都是慢动作,所以你面对的是等待,等待,还是等待!

总而言之,处理大矩阵,巨大的存储成本和时间成本都是极大的,而其中最重要也最不能让人容忍的就是时间成本,我的时间非常宝贵,我的青春等不起!

那么,有什么办法可以解决哪怕是缓解这个问题呢?

其实,无非就是从两个角度来思考这个问题:

  • 缓解内存消耗

    我不一次性地读入,我将原始文件按照行或者按照列进行拆分

    到底是按行还是按列拆分,具体得依据处理的数据,若矩阵的分析运算是以列为单位进行的,而且列于列之间相对独立,那么就按照列来拆,或者如果是以行为单位进行的,那么就按照行来拆,两种条件都不满足,那就没招了

    拆分玩之后,就得到了若干个小矩阵,将这些小矩阵分批次地读入并分析然后写出,最后将这些结果进行汇总合并

    拆分(行拆分)的极端情况就是将文件逐行读入,大家都知道在perl和python中有逐行读入的操作,其实R也有这种操作,而且操作原理和方法和它们都差不多,都是创建文件句柄对象,然后利用readLines函数实现逐行读取

    将文件逐行读入是行拆分的极端情况,那么有没有方法实现逐列读入呢?很遗憾,没有,考虑到计算机进行文本读取的实际过程就知道,这样的操作其实是不存在的,那些咱们平时用到的像cut这样的命令看起来好像的确是能进行行的提取操作,但是其底层的实现还是需要将原始文件从头到尾扫描一遍,然后将对应的列提出来——但也不是完全没有办法,咱们将原始文件转置一下,不就可以通过对转置后的文件进行逐行读取,从而实现对原始文件的逐列读取了吗?不过转置操作也是相当耗时间的

  • 降低时间成本

    降低时间成本最有效的办法就是采用并行化的策略,利用多线程进行并行化处理,则时间消耗就会成倍地降低

    但是并行化操作是有前提条件的:

    并行化操作的本质是将一个大问题拆解成若干个互不干扰的小问题,则大问题的解就是小问题的解的汇总,例如,对一个矩阵求它的各个列的和,很明显每一列的和只与本列相关,完全不受其他列的影响,所以完全可以每一列独立进行,最后进行汇总

    其实像这种场景是比较多的

9. optparse的使用 目录

optparse可以用来实现类似在Python中的argparse一样的作用,使得我们写的R脚本可以在命令行中通过添加参数的方式来运行,同时有帮助文档可以来帮助使用者快速上手

library(optparse)

option_list <- list(
	make_option(c("-v", "--verbose"), action="store_true", default=TRUE, help="Print extra output [default]"),
	make_option(c("-q", "--quietly"), action="store_false", dest="verbose", help="Print little output"),
	make_option(c("-c", "--count"), type="integer", default=5, help="Number of random normals to generate [default %default]", metavar="number"),
	make_option("--generator", default="rnorm", help = "Function to generate random deviates [default \"%default\"]"),
	make_option("--mean", default=0, help="Mean if generator == \"rnorm\" [default %default]"),
	make_option("--sd", default=1, metavar="standard deviation", help="Standard deviation if generator == \"rnorm\" [default %default]")
)

opt <- parse_args(OptionParser(option_list=option_list))

# print some progress messages to stderr if "quietly" wasn't requested
if ( opt$verbose ) {
	write("writing some verbose output to standard error...\n", stderr())
}
# do some operations based on user input
if( opt$generator == "rnorm") {
	cat(paste(rnorm(opt$count, mean=opt$mean, sd=opt$sd), collapse="\n"))
}
else {
	cat(paste(do.call(opt$generator, list(opt$count)), collapse="\n"))
}
cat("\n")

10. 数字以科学计数法输出 目录

format(0.00001, scientific=TRUE, digit=2)

11. reshape2::melt的使用 目录

reshape2::melt常用于将短表格转换成长表格,以用于ggplot绘图

段表格示例:

Id Var1 Var2 Var3
Id1 a1 b1 c1
Id2 a2 b2 c2
Id3 a3 b3 c3

其对应的长表格为:

Id VAR VALUE
Id1 Var1 a1
Id1 Var2 b1
Id1 Var3 c1
Id2 Var1 a2
Id2 Var2 b2
Id2 Var3 c2
Id3 Var1 a3
Id3 Var2 b3
Id3 Var3 c3

假设原始的短表格保存在数据框变量X中,现要将其转换成以Id列为唯一记录识别项,其他项作为测量项的长表格Y,则可以通过下面的命令实现:

Y <- melt(X, id.vars=1)

参考资料:

(1) 在R中如何逐行读取CSV文件并将内容识别为正确的数据类型?

(2) CSDN·卡西莫多的礼物《R语言 optparse的使用》