群里大家给出各种解法,但似乎避开了dplyr之后,大家还是会用到plyr或者purrr,没人给出我讲的apply套split的纯base R的解法。

套两重for循环,会让人容易理解一点,而且代码也非常简单,但效率不行,用apply的话,处理各种list,特别如果要list套list,很多人就比较懵逼,需要借助于plyr或purrr,甚至还有人的解法虽然不用dplyr的group_by套summarise,但用了dplyr其它的函数。tidyverse真的太好用,对低端R用户照顾得太好,大家都被惯坏了,但想一下,以前的人真的就不用活了吗?

用我说的split + apply,完全不需要额外的library,其实也蛮简单的,下面是我写的代码,仅供参考:

PS: 尽管这里用到了管道,但管道可有可无,无非是把从里到外变成从左到右,让代码易读性高一点而已。

res <- read.delim("Temperature.txt")[,c("Year", "Month", "Temperature")] %>%
    split(.$Year) %>%
    lapply(function(y) split(y, y$Month)) %>%
    lapply(function(m) lapply(m, function(x)
        mean(x$Temperature, na.rm=TRUE))) %>%
    lapply(function(x)
        data.frame(Month=names(x),
                   Temperature=unlist(x))) %>%
    do.call("rbind", .) %>%
    cbind(Year=sub("\\.\\d{1,2}", "",
                   rownames(.)), .,
          row.names=NULL)

结果如下:

> head(res)
  Year Month Temperature
1 1990     1    6.313889
2 1990     2    6.475000
3 1990     3    8.192857
4 1990     4    9.153333
5 1990     5   14.357576
6 1990     6   16.277273
> tail(res)
    Year Month Temperature
187 2005     7   18.799111
188 2005     8   17.692105
189 2005     9   17.883455
190 2005    10   14.498125
191 2005    11   11.562759
192 2005    12    7.111481
> 

拆解代码

这个代码看着挺复杂,其实很简单,我们来拆解一下:

读数据:

x <- read.delim("Temperature.txt")[,c("Year", "Month", "Temperature")]

切分数据,并计算

按照两个因子分层切分,对第二层数据进行求均值。

x %>%
  split(.$Year) %>%
  lapply(function(y) split(y, y$Month)) %>%
    lapply(function(m) lapply(m, function(x)
        mean(x$Temperature, na.rm=TRUE)))

对于第二步,如果我们套用tapply的话,就可以少一行,因为tapply相当于是sapply + split,所以对于Month可以不用切,交给tapply。

x %>%
    split(.$Year) %>%
    lapply(function(x)
        tapply(x$Temperature, x$Month, mean, na.rm=TRUE))

结果转为data.frame

这里写成函数,因为后面会重用到

ls2df <- function(ls) {
   lapply(ls, function(x)
     data.frame(Month=names(x),
                Temperature=unlist(x))) %>%
     do.call("rbind", .) %>%
     cbind(Year=sub("\\.\\d{1,2}", "",
                    rownames(.)), .,
           row.names=NULL)
}

来个简单版本

其实啊,split可以一次切两个因子,所以用split一切,套个sapply即可。

temp <- sapply(split(x$Temperature, x[,c("Year", "Month")]), mean, na.rm=TRUE)
res <- data.frame(Year = sub("(\\d{4}).*", "\\1", names(temp)),
                  Month = sub("\\d{4}.", "", names(temp)),
                  Temperature = temp,
                  row.names = NULL)

一行代码版本

前面我说过tapply相当于sapply + split,所以用tapply可以一行代码解决:

tapply(x$Temperature, list(x$Year, x$Month), mean, na.rm=TRUE)

不过这个输出是以Year为row, Month为column的matrix,如果要转成像上面的tidy data frame的话,还需要额外的操作。

用aggregate也可以实现一行代码,不过没有tapply快,当然输出不完全一致的情况下,比较速度也有点bias。

b <- microbenchmark(
    tapply(x$Temperature, list(x$Year, x$Month), mean, na.rm=TRUE),
    aggregate(x, by=list(x$Year, x$Month), mean, na.rm=T)
    )

tapply胜出:

> b
Unit: milliseconds
                                                             expr    min
 tapply(x$Temperature, list(x$Year, x$Month), mean, na.rm = TRUE) 1.2762
        aggregate(x, by = list(x$Year, x$Month), mean, na.rm = T) 7.1846
      lq     mean  median     uq     max neval
 1.33325 1.499571 1.41000 1.4750  5.6744   100
 7.37845 8.205050 8.07395 8.3865 15.9764   100

强行总结

尽管split、aggregate和tapply都可以支持多重因子,我还是喜欢各种split+apply套一堆的版本,因为做为练习,这才是好的学习代码,而且我写的这个,已经非常模块化了,三个步骤,非常明确,再者多玩一下list,熟悉一下不规则的数据,才好应对现实世界中的数据,总是data.frame进,data.frame出是不太现实的。尽管tapply一行可以解决,但内部的逻辑无非也是与我们的代码类似。

番外篇:假如我要用R包呢?

plyr

library(plyr)
ddply(x, .(Year,Month), summarize, 
      Temperature=mean(Temperature, na.rm=T))

reshape2

library(reshape2)
dcast(x, Year+Month~..., fun=function(x) mean(x, na.rm=T), 
      value.var = c("Temperature"))

purrr

用purrr就跟用lapply似的,所以并不显示有什么优势,当然如果你直接按照Year + Month去split的话,用lapply跟用map一样,非常简单出结果,也就是上面的来个简单版本所写的版本。并没有什么新玩意。

如果要用purrr的话,我想还是用我原先的代码,先产生list of list,再对第二层求均值,最后用ls2df函数(上面拆解代码的第三步)整理为data.frame,这样子的话,在第二步求均值的时候,用purrr就显得有优势了,因为要对两层list操作的时候,不用套两个lapply,可以用map_depth,这样逻辑还是一样,但代码简洁了,可读性,易于理解等都有所改善。

library(purrr)
lapply(split(x, x$Year), function(.) split(., .$Month)) %>%
  map_depth(2, function(.) mean(.$Temperature, na.rm=T)) %>%
  ls2df

sqldf

让你用SQL语法来summarize数据。

library(sqldf)
sqldf("SELECT Year, Month, avg(Temperature)
       FROM x
       GROUP BY Year, Month")

doBy

顾名思义,干的就是这档事。

library(doBy)
summaryBy(Temperature ~ Year + Month, data = x, FUN=mean, na.rm=T)

data.table

library(data.table)
data.table(x)[, list(mean=mean(Temperature, na.rm=TRUE)), 
              by=c("Year", "Month")]