今天的帖子是对昨天的补充,也算是对于分组批量操作的总结。
昨天群里有人提问:
如何对数据框进行多变量分组,分别求平均数?
给出的原始数据是这个样子的
## 我喜欢用data.table中的fread读取数据,参数data.table=F,限定读入数据后是data.frame。
data <- data.table::fread("Temperature.txt",data.table = F)
年代是从1990到2005
月份是从1月到12月
要求:
按照Year和Month对行进行分组,求得每组的平均Temperature。
方法一
这个事情,第一反应就是dplyr包中的group_by
联合summarize
:
library(dplyr)
results1 <- data %>%
group_by(Year,Month) %>%
summarise(Mean = mean(Temperature,na.rm = T))
十分方便,最终的结果是这个样子的
方法二
因为群里有人说,常用的实现这个操作的方法至少10种,而我脑子里面只有两种(第一种还有最后一种),所以内心十分恐慌。并且,他们还要求使用apply来完成,我彻底沦陷了。
想来想去,我算是明白了,Hadley Wickham大神的这个包是有毒的,一用上之后便不思进取,因为实在太好用了。在神包tidyverse之前,作者还有一种方法可以实现这个操作,就是plyr包的split-apply-combine思想
用起来也是十分简单,而且,思路和函数组成都很类似,因为是一个作者写的。一行代码就搞定了。
library(plyr)
results2 <- ddply(data,.(Year,Month),summarise,Mean = mean(Temperature,na.rm = T))
方法三
洲更给我提供了一个方法,是aggregate
,因此我跟洲更商量,就此打住,把时间留给更有意义的事情。
results3 <- aggregate(data$Temperature,list(data$Year,data$Month),mean,na.rm=T)
方法四
Y叔说,用tapply也是可以的,不过他返回的是个table,需要转换两次才能得到正确结果
results4 <- as.data.frame(as.table(with(data,tapply(Temperature,list(Year,Month),mean,na.rm=T))))
方法五
已经有了四个一行代码搞定的方法了,不妨再添加一个,这个方法基于data.table语法,所以,先要把data.frame转换成data.table
data1 <- data[,c("Temperature","Year","Month")]
results5 <- data.table::setDT(data1)[ ,lapply(.SD, mean,na.rm=T) , by=c("Year","Month")]
方法六
在大家谈到,有了tidyverse啥都不想要了之后,Y叔叔悠悠地说了一句,那之前的人们是怎么生活的呢?
我想,应该是基于split+批量操作
所以,以下的这些方法都是基于split
一开始,我以为spli只能裂解一个变量,所以就需要在批量操作时分别再次split然后再操作,说实话,写的时候,就像合上电脑走人。
先尝试了,map
函数配合bind_rows
函数,因为我受Hadley Wickham影响很大,喜欢用管道符号,
library(purrr)
library(dplyr)
results6 <- data %>%
select(Year,Month,Temperature) %>%
split(.$Year) %>%
map(function(x){
data.frame(t(bind_cols(map(split(x,x$Month),function(y){
mean =mean(y$Temperature,na.rm = T)
c(y$Year[1],y$Month[1],mean)
}))))
}) %>%
bind_rows()
colnames(results6) <- c("Year","Month","Mean")
方法七
现在请无视这些标题,只作为序号使用。
而map干的事情和lapply是一样的,lapply我更加熟悉一点,在当前情况下更简单。
library(dplyr)
results <- data %>%
select(Year,Month,Temperature) %>%
split(.$Year) %>%
lapply(function(x){
do.call(rbind,lapply(split(x,x$Month),function(y){
mean =mean(y$Temperature,na.rm = T)
c(y$Year[1],y$Month[1],mean)}))
})
results7 <- data.frame(do.call(rbind,results))
colnames(results7) <- c("Year","Month","Mean")
方法八
早上查google的时候,我发现,split可以同时裂解多个变量,那么,这个事情就大大简化了
library(dplyr)
library(purrr)
results8 <- data %>%
select(Year,Month,Temperature) %>%
split(paste(.$Year,.$Month)) %>%
map(function(x){
mean=mean(x$Temperature,na.rm = T)
data.frame(x$Year[1],x$Month[1],mean)
}) %>%
bind_rows()
而且split的裂解语法至少有三种形式,上一个是paste(.$Year,.$Month)
,下面这个是list(.$Year,.$Month)
library(dplyr)
library(purrr)
results8<- data %>%
select(Year,Month,Temperature) %>%
split(list(.$Year,.$Month)) %>%
map(function(x){
mean=mean(x$Temperature,na.rm = T)
data.frame(x$Year[1],x$Month[1],mean)
}) %>%
bind_rows()
还可以写成这个样子的:
library(dplyr)
library(purrr)
results8 <- data %>%
select(Year,Month,Temperature) %>%
split(.[,c("Year","Month")]) %>%
map(function(x){
mean=mean(x$Temperature,na.rm = T)
data.frame(x$Year[1],x$Month[1],mean)
}) %>%
bind_rows()
方法9
在split裂解多个变量的情况下,再使用lapply,简单,效果也很好
library(dplyr)
results9 <- data %>%
select(Year,Month,Temperature) %>%
split(paste(.$Year,.$Month)) %>%
lapply(function(x){
mean=mean(x$Temperature,na.rm = T)
data.frame(x$Year[1],x$Month[1],mean)
})
results9 <- do.call(rbind,results9)
方法10
上一步中裂解后得到的是一个个的小数据框,对这些小数据框可以使用apply,终于用到这玩意了。
library(dplyr)
results10 <- data %>%
select(Year,Month,Temperature) %>%
split(.[,c("Year","Month")]) %>%
map(function(x){
data.frame(t(apply(x,2,function(x){mean(x,na.rm = T)})))
}) %>%
bind_rows()
如果是lapply加上apply,代码更简单一点
library(dplyr)
results <- data %>%
select(Year,Month,Temperature) %>%
split(.[,c("Year","Month")]) %>%
lapply(function(x){
apply(x,2,function(x){mean(x,na.rm = T)})
})
results10 <- data.frame(do.call(rbind,results))
方法11
如果追求只用apply实现,不方便,因为apply接受的是矩阵,或者数据框,我们可以用for循环实现对于split结果的获取。
library(dplyr)
results <- data %>%
select(Year,Month,Temperature) %>%
split(.[,c("Year","Month")])
results11 <- data.frame()
for (i in 1:192) {
results11[i,c(1:3)] = apply(results[[i]],2,function(x){mean(x,na.rm = T)})
}
方法12
理论上,只要能用for循环完成的事情,apply都可以做,所以,我们人为地设置一个序号组成的数据框让apply来批量,然后对于每一个元素,再使用apply批量求平均数。这样,整个代码中就只有apply了。
library(dplyr)
results <- data %>%
select(Year,Month,Temperature) %>%
split(.[,c("Year","Month")])
results12 <- data.frame(t(apply(data.frame(seq(1,192)),1,function(x){
apply(results[[x]],2,function(x){mean(x,na.rm = T)})
})))
方法13
在很长一段时间内,只要是批量的事情,我都是用for循环来搞定的。它解决了我大部分问题,所以,我上课的时候强调,每一个人都要学会写for循环。
一般当你小有所成,就会出现鄙视链,开始瞧不起那些用for循环的初学者,并嘲笑别人说,for循环速度太慢了!但是,对于初学者而言,解决问题才是最重要的,速度可以放在一边,况且,速度并没有那么重要。
可以看看以前写过的这个帖子,for循环50s,并行化8s。我觉得没什么差别。
8秒完成2万个基因的生存分析,人人都可以!
至于用lapply还有map这些批量函数,我纯粹是为了要面子,私底下,大部分时间用的是for循环。不过,用lapply是一种自然而然水到渠成的行为,当你能够随意写出for循环后,你就可以把它改写成函数,一旦写成函数,apply家族的成员就可以批量对其操作。
写好for循环的关键只有一个:
清晰地定义做一件事情的每一个步骤。
具体到当前的情况,我的想法是这样的
1. 先建立一个空的数据框,他有三列,我准备逐行填满
2. 第1行的第1列,我填入的是一个年份,比如1999
3. 确定年份后,在1999年中,我再开始选择第1个月,把这个数值填入第1行第2列
4. 当确定了年份,月份,那么就会产生一个小数据框,我就可以对温度这一列求平均值,填入第1行的第3列。
5. 第一行完成后,下面的行依葫芦画瓢。
基于这个思想,我写出了以下的循环:
results13 <- data.frame()
i=1
for (Year in unique(data$Year)) {
for (Month in unique(data$Month)) {
results13[i,1]= Year
results13[i,2]= Month
results13[i,3]= mean(data[(data$Year==Year & data$Month==Month),"Temperature"],na.rm=T)
i=i+1
}
}
colnames(results13) <- c("Year","Month","Mean")
最终这13个方法的结果一样
reshape2中的melt和dcast函数联合使用也可以达到效果,但是我就是不用!那是一段痛苦的回忆,我在R语言学习路上的一个大拦路虎就是melt和dcast,不理解,也不愿理解,拦了我很久,之后碰到spread和gather才一路通畅。
所以,我对tidyverse这个包,饱含深情,感激涕零。甚至,我看到国外有些大神直接建议,初学者学习R语言,从tidyverse开始,直接跳过基础部分。在这方面,小洁最有发言权,她能熟练使用tidyverse的时候,根本不知道还有R语言基础这件事,属于练了神功再下凡的品类。
总结一下:
- 1.group_by联合summarize是批量分组计算的首选
- 2.还有ddply,aggregate,tapply可以选择。
- 3.基于split裂解+批量也能解决问题
- 4.只要是需要批量做的事情,都可以用for循环来做
- 5.写好for循环的秘诀只有一个:清晰地定义做一件事情的每一个步骤。
如果你需要练习的那个小文件,回复"我爱循环"自助获取。
网友评论