【问题标题】:Why is split inefficient on large data frames with many groups?为什么在具有许多组的大型数据帧上拆分效率低下?
【发布时间】:2017-01-25 12:05:48
【问题描述】:
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 %&gt;% map(sequentially_process_each_row_of_df)
  • 还要考虑,ordering dfsplitting 之前,这样.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 实际上,排序会创建行名,而不是改善内存访问模式——比较 .row_names_info(df).row_names_info(df[order(df$x),]);第一种情况下的负值表示行名紧凑地存储为c(NA, 1000000),第二种情况下的正值表示它们按字面意思存储为整数向量。
  • @MartinMorgan:你说得对——我完全错过了,谢谢。设置row.names() = NULL 会显着增加执行时间。此外,我猜,因为每个df$x 包含少量元素,连续填充每个组的索引(在内部拆分中)不应该产生这种差异。

标签: r performance purrr


【解决方案1】:

更多的是解释而不是答案。子设置大数据框比子设置小数据框成本更高

> 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() 返回一个向量——row.names 是动态创建的,对于大型 data.frame,会创建大量 row.names

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

所以人们可能会认为即使是无意义的 row.names 也会加快计算速度

> 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 问题,对于许多组而言,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添加的,devel版的安装非常简单

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

    更多信息请关注Installation wiki

    【讨论】:

    • split.data.table 明显更快。我最终使用 data.table 重写了部分代码。
    【解决方案3】:

    利用 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)
    

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

    【讨论】:

      猜你喜欢
      • 2016-06-29
      • 2023-04-04
      • 1970-01-01
      • 2021-04-09
      • 1970-01-01
      • 2020-10-06
      • 2016-02-28
      • 1970-01-01
      • 2011-04-24
      相关资源
      最近更新 更多