为什么在有许多分组的大型数据框上使用split效率低下?

8
df %>% split(.$x)

对于大量唯一的x值,数据框架变得缓慢。相反,如果我们手动将数据框架拆分成较小的子集,然后对每个子集执行分割,我们可以将时间至少降低一个数量级。

library(dplyr)
library(microbenchmark)
library(caret)
library(purrr)

N      <- 10^6
groups <- 10^5
df     <- data.frame(x = sample(1:groups, N, replace = TRUE), 
                     y = sample(letters,  N, replace = TRUE))
ids      <- df$x %>% unique
folds10  <- createFolds(ids, 10)
folds100 <- createFolds(ids, 100)

运行 microbenchmark 命令会给我们以下结果:
## Unit: seconds

## expr                                                  mean
l1 <- df %>% split(.$x)                                # 242.11805

l2 <- lapply(folds10,  function(id) df %>% 
      filter(x %in% id) %>% split(.$x)) %>% flatten    # 50.45156  

l3 <- lapply(folds100, function(id) df %>% 
      filter(x %in% id) %>% split(.$x)) %>% flatten    # 12.83866  

split是否不适用于大型群体?除了手动初始子集,是否有其他替代方案?

我的笔记本电脑是2013年后期的Macbook Pro,2.4GHz 8GB。


我想并行处理结果列表中的项目,即 list_of_dataframes %>% map(sequentially_process_each_row_of_df) - Rickard
考虑在拆分之前对 df 进行排序,以便 .Internal(split()) 更连续地访问内存 -- system.time({ a = split(df, df$x) }); system.time({ odf = df[order(df$x), ]; b = split(odf, odf$x) }); identical(a, b) - alexis_laz
@alexis_laz 实际上,排序会创建行名称,而不是改善内存访问模式--比较.row_names_info(df).row_names_info(df[order(df$x),]);第一个情况中的负值表示行名称被紧凑地存储为c(NA, 1000000),第二个情况中的正值表示它们被字面上存储为整数向量。 - Martin Morgan
@MartinMorgan: 你说得对 - 我完全忽略了那个,谢谢。将 row.names() = NULL 设置为null会显著增加执行时间。此外,我猜想,由于每个 df$x 包含少量元素,因此为每个组连续填充索引(在内部拆分中)不应该有太大的影响。 - alexis_laz
3个回答

11

这更像是一种解释而不是回答。从一个大的数据框中子集化比从一个小的数据框中子集化成本更高。

> df100 = df[1:100,]
> idx = c(1, 10, 20)
> microbenchmark(df[idx,], df100[idx,], times=10)
Unit: microseconds
         expr     min      lq     mean  median      uq     max neval
    df[idx, ] 428.921 441.217 445.3281 442.893 448.022 475.364    10
 df100[idx, ]  32.082  32.307  35.2815  34.935  37.107  42.199    10

split() 对每个组都付出这个代价。

原因可以通过运行 Rprof() 看到。

> Rprof(); for (i in 1:1000) df[idx,]; Rprof(NULL); summaryRprof()
$by.self
       self.time self.pct total.time total.pct
"attr"      1.26      100       1.26       100

$by.total
               total.time total.pct self.time self.pct
"attr"               1.26       100      1.26      100
"[.data.frame"       1.26       100      0.00        0
"["                  1.26       100      0.00        0

$sample.interval
[1] 0.02

$sampling.time
[1] 1.26

所有时间都花费在调用attr()上。使用debug("[.data.frame")逐步执行代码可以发现问题涉及像这样的调用:

attr(df, "row.names")

这个小例子展示了 R 用来避免表示不存在的行名的技巧:使用 c(NA, -5L),而不是 1:5

> dput(data.frame(x=1:5))
structure(list(x = 1:5), .Names = "x", row.names = c(NA, -5L), class = "data.frame")

请注意,attr()返回一个向量 -- 行名称是即时创建的,对于大型数据框来说会创建大量行名称

> attr(data.frame(x=1:5), "row.names")
[1] 1 2 3 4 5

因此,即使是毫无意义的行名,也可以加速计算。

> dfns = df; rownames(dfns) = rev(seq_len(nrow(dfns)))
> system.time(split(dfns, dfns$x))
   user  system elapsed 
  4.048   0.000   4.048 
> system.time(split(df, df$x))
   user  system elapsed 
 87.772  16.312 104.100 

分割向量或矩阵也将非常快速。


2
这并不是严格的 split.data.frame 问题,对于许多组的数据框来说,存在更普遍的可扩展性问题。
如果你使用 split.data.table,你可以获得相当不错的速度提升。我在常规 data.table 方法的基础上开发了这种方法,在这里似乎表现得非常好。
system.time(
    l1 <- df %>% split(.$x)   
)
#   user  system elapsed 
#200.936   0.000 217.496 
library(data.table)
dt = as.data.table(df)
system.time(
    l2 <- split(dt, by="x")   
)
#   user  system elapsed 
#  7.372   0.000   6.875 
system.time(
    l3 <- split(dt, by="x", sorted=TRUE)   
)
#   user  system elapsed 
#  9.068   0.000   8.200 

sorted=TRUE会按照data.frame方法返回相同顺序的列表,而默认情况下data.table方法会保留输入数据中存在的顺序。如果您想坚持使用data.frame,则可以在最后使用lapply(l2, setDF)

PS. split.data.table在1.9.7中添加,安装开发版本非常简单。

install.packages("data.table", type="source", repos="http://Rdatatable.github.io/data.table")

关于这个更多的信息,请参考安装wiki


1
split.data.table 的速度显著更快。最终我重新编写了使用 data.table 的部分代码。 - Rickard

-1
一个非常好的作弊方法,利用dplyr 0.8.3或更高版本的group_split
random_df <- tibble(colA= paste("A",1:1200000,sep = "_"), 
                    colB= as.character(paste("A",1:1200000,sep = "_")),
                    colC= 1:1200000)

random_df_list <- split(random_df, random_df$colC)

random_df_list <- random_df %>% group_split(colC)

将几分钟的操作缩短到几秒钟!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接