R for data scienceR数据处理R语言训练

R语言里面的分组操作,不止group_by

2019-05-07  本文已影响15人  9d760c7ce737

今天的帖子是对昨天的补充,也算是对于分组批量操作的总结。


昨天群里有人提问:

如何对数据框进行多变量分组,分别求平均数?

给出的原始数据是这个样子的

## 我喜欢用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语言基础这件事,属于练了神功再下凡的品类。

总结一下:

如果你需要练习的那个小文件,回复"我爱循环"自助获取。

上一篇下一篇

猜你喜欢

热点阅读