【问题标题】:R: Quickly Performing Operations on Subsets of a Data Frame, then Re-aggregating the Result Without an Inner FunctionR:对数据框的子集快速执行操作,然后在没有内部函数的情况下重新聚合结果
【发布时间】:2016-05-05 23:53:34
【问题描述】:

我们有一个非常大的数据框df,可以按因素拆分。在此拆分创建的数据帧的每个子集上,我们需要执行一个操作以增加该子集的行数,直到达到某个length。之后,我们rbind 的子集得到更大版本的df

有没有办法在不使用内部函数的情况下快速做到这一点?

假设我们的子集操作(在单独的 .R 文件中)是:

foo <- function(df) { magic }

我们想出了几种方法:

1)

df <- split(df, factor)
df <- lapply(df, foo)
rbindlist(df)

2)

assign('list.df', list(), envir=.GlobalEnv) 
assign('i', 1, envir=.GlobalEnv)

dplyr::group_by(df, factor)
dplyr::mutate(df, foo.list(df.col))
df <- rbindlist(list.df)
rm('list.df', envir=.GlobalEnv)
rm('i', envir=.GlobalEnv)

(In a separate file)
foo.list <- function(df.cols) {
    magic; 
    list.df[[i]] <<- magic.df
    i <<- i + 1
    return(dummy)
}

第一种方法的问题是时间。 lapply 需要很长时间才能真正令人满意(使用我们的数据集大约需要一个小时)。

第二种方法的问题是非常篡改用户全局环境的不良副作用。它明显更快,但如果可以的话,我们宁愿避免这样做。

我们还尝试传入列表和计数变量,然后尝试使用父环境中的变量substitute 它们(一种解决 R 缺乏传递引用的技巧)。

我们查看了一些可能相关的 SO 问题(R applying a function to a subset of a data frameCalculations on subsets of a data frameR: Pass by reference 等),但没有一个问题能很好地解决我们的问题。

如果您想运行代码,可以复制和粘贴以下内容:

 x <- runif(n=10, min=0, max=3)
 y <- sample(x=10, replace=FALSE)
 factors <- runif(n=10, min=0, max=2)
 factors <- floor(factors)
 df <- data.frame(factors, x, y)

df 现在看起来像这样(长度为 10):

 ## We group by factor, then run foo on the groups.

 foo <- function(df.subset) {
   min <- min(df.subset$y)
   max <- max(df.subset$y)

   ## We fill out df.subset to have everything between the min and
   ## max values of y. Then we assign the old values of df.subset
   ## to the corresponding spots.

   df.fill <- data.frame(x=rep(0, max-min+1),
                         y=min:max,
                         factors=rep(df.subset$factors[1], max-min+1))
   df.fill$x[which(df.subset$y %in%(min:max))] <- df.subset$x
   df.fill
 }

所以我可以在第一种方法中使用我的示例代码来构建一个新的 df(长度 18):

【问题讨论】:

  • 您可能需要查看expand.grid 以填写所有值。
  • @Henrik,感谢您的建议!我会做相应的调整。
  • 对于expand.grid,我们肯定考虑过。但是,我们很难让它与分组一起工作。由于每个组都有不同的最小值和最大值,我们需要将其填充到。
  • @Henrik,抱歉不清楚。 Foo(我们相信)尽可能快。我们主要关心的只是将 foo 应用于df 的子组,然后重新聚合。不过谢谢!我已经根据你的 cmets 改进了这个问题。

标签: r dplyr


【解决方案1】:

使用data.table 这不会花费很长时间,因为它具有快速的功能。如果可以,请重写您的函数以使用特定变量。拆分应用组合处理可能会提高性能:

library(data.table)
system.time(
df2 <- setDT(df)[,foo(df), factors]
)
#   user  system elapsed 
#   1.63    0.39    2.03

【讨论】:

    【解决方案2】:

    我认为您的功能没有按预期工作。它依赖于被订购的y

    尝试使用带有分组的 data.table 连接:

    library(data.table)
    setDT(df)
    df2 <- df[, .SD[data.table(y=seq(.SD[, min(y)], .SD[, max(y)], by = 1)), .SD, 
                      on = "y"], #data.table join
                        by = factors] #grouping
    df2[is.na(x), x:= 0]
    setkey(df2, factors, y, x)
    

    【讨论】:

    • 我相信这个例子只是函数的一个小模型,而不是函数本身。
    • @PierreLafortune 只有通过量身定制的解决方案才能实现最高性能。如果函数很慢,那么如何拆分应用组合并不重要。
    • 我同意这一点。要应用的函数应该重写
    • @Henrik 查看 OP 的代码,我不相信他们声称的功能效率。
    【解决方案3】:

    Pierre 和 Roland 已经提供了很好的解决方案。
    如果情况是不仅在时间上而且在内存方面的可扩展性,您可以将数据分散到多个远程 R 实例中。
    在大多数基本设置中,它只需要Rserve/RSclient,因此不需要非 CRAN 部门。

    跨 R 实例传播数据

    为了更容易重现,下面的示例将在单个 localhost 机器上启动两个 R 实例。您需要在远程机器上启动 Rserve 节点以获得真正的可扩展性。

    # start R nodes
    library(Rserve)
    port = 6311:6312
    invisible(sapply(port, function(port) Rserve(debug = FALSE, port = port, args = c("--no-save"))))
    
    # populate data
    set.seed(123)
    x = runif(n=5e6,min=0, max=3)
    y = sample(x=5e6,replace=FALSE)
    factors = runif(n=5e6, min=0, max=2)
    factors = floor(factors)
    df = data.frame(factors, x, y)
    
    # connect Rserve nodes
    library(RSclient)
    rscl = sapply(port, function(port) RS.connect(port = port))
    
    # assign chunks to R nodes
    sapply(seq_along(rscl), function(i) RS.assign(rscl[[i]], name = "x", value = df[df$factors == (i-1),]))
    
    # assign magic function to R nodes
    foo = function(df) df
    sapply(rscl, RS.assign, name = "foo", value = foo)
    

    远程机器上的所有进程都可以并行执行(使用wait=FALSERS.collect),这进一步减少了计算时间。


    使用lapply + RS.eval

    # sequentially
    l = lapply(rscl, RS.eval, foo(x))
    rbindlist(l)
    
    # parallely
    invisible(sapply(rscl, RS.eval, foo(x), wait=FALSE))
    l = lapply(rscl, RS.collect)
    rbindlist(l)
    

    使用big.data.table::rscl.*

    big.data.table 包在 RSclient::RS.* 函数上提供了一些包装器,允许它们接受到 R 节点的连接列表。
    他们不以任何方式使用 data.table,因此 可以有效地应用于 data.frame、vector 或任何可分块的 R 类型。下面的示例使用基本的 data.frame。

    library(big.data.table)
    
    # sequentially
    l = rscl.eval(rscl, foo(x), simplify=FALSE)
    rbindlist(l)
    
    # parallely
    invisible(rscl.eval(rscl, foo(x), wait=FALSE))
    l = rscl.collect(rscl, simplify=FALSE)
    rbindlist(l)
    

    使用big.data.table

    这个例子需要将节点上的数据存储为 data.tables,但提供了一些方便的 api 和许多其他功能。

    library(big.data.table)
    rscl.require(rscl, "data.table")
    rscl.eval(rscl, is.data.table(setDT(x))) # is.data.table to suppress collection of `setDT` results
    
    bdt = big.data.table(rscl = rscl)
    # parallely by default
    bdt[, foo(.SD), factors]
    # considering we have data partitioned using `factors` field, the `by` is redundant in that case
    bdt[, foo(.SD)]
    # optionally use `[[` to access R nodes environment directly
    bdt[[expr = foo(x)]]
    

    清洁工作区

    # disconnect
    rscl.close(rscl)
    
    # shutdown nodes started from R
    l = lapply(setNames(nm = port), function(port) tryCatch(RSconnect(port = port), error = function(e) e, warning = function(w) w))
    invisible(lapply(l, function(rsc) if(inherits(rsc, "sockconn")) RSshutdown(rsc)))
    

    【讨论】:

      【解决方案4】:

      使用data.table..的另一个变体。首先获取min(y):max(y)部分,然后加入+更新:

      require(data.table)
      ans = setDT(df)[, .(x=0, y=min(y):max(y)), by=factors
                    ][df, x := i.x, on=c("factors", "y")][]
      ans
      #     factors          x  y
      #  1:       0 1.25104362  1
      #  2:       0 0.16729068  2
      #  3:       0 0.00000000  3
      #  4:       0 0.02533907  4
      #  5:       0 0.00000000  5
      #  6:       0 0.00000000  6
      #  7:       0 1.80547980  7
      #  8:       1 0.34043937  3
      #  9:       1 0.00000000  4
      # 10:       1 1.51742163  5
      # 11:       1 0.15709287  6
      # 12:       1 0.00000000  7
      # 13:       1 1.26282241  8
      # 14:       1 2.88292354  9
      # 15:       1 1.78573288 10
      

      【讨论】:

      • range 函数可能(?)比minmax 更快。所以有了seq2 = function(x) seq(x[1], x[2]),我们可以使用j = .(x=0, y=seq2(range(y)))
      • @jangorecki,也许.. 不确定。不过很高兴知道,谢谢。
      猜你喜欢
      • 2020-10-25
      • 1970-01-01
      • 1970-01-01
      • 2019-12-17
      • 2016-05-17
      • 2013-06-25
      • 2014-11-10
      • 2022-12-03
      • 1970-01-01
      相关资源
      最近更新 更多