【问题标题】:Most efficient way (fastest) to modify a data.frame using indexing使用索引修改 data.frame 的最有效方式(最快)
【发布时间】:2018-02-09 10:30:15
【问题描述】:

问题的小介绍:

我正在开发一个生态生理模型,并使用一个名为 S 的参考类列表,它存储模型输入/输出所需的每个对象(例如气象、生理参数等)。 此列表包含 5 个对象(请参见下面的示例):
- 两个数据框,S$Table_Day(模型的输出)和S$Met_c(输入中的气象),它们都在列中具有变量,在行中具有观察值(输入或输出)。
- 参数列表S$Parameters.
- 矩阵
- 一个向量

该模型以每日时间步长运行许多函数。每一天都在从第一天 i=1 到最后一天 i=n 的 for 循环中计算。这个列表被传递给函数,这些函数通常在输入中从S$Met_c 和/或S$Parameters 获取数据,并使用索引(第 i 天)计算存储在S$Table_Day 中的内容。 S 是一个参考类列表,因为它们避免了修改时的复制,考虑到计算的数量,这非常重要。

问题本身:

由于模型非常慢,我试图通过对不同解决方案进行微基准测试来减少计算时间。
今天,在比较两种存储数据的解决方案时,我发现了一些令人惊讶的事情。通过在预先分配的数据帧之一中建立索引来存储数据比将其存储到未声明的向量中要长。在阅读this 之后,我认为预分配内存总是更快,但似乎 R 在按索引修改时执行了更多操作(可能比较长度、类型等......)。

我的问题是:有没有更好的方法来执行此类操作?换句话说,有没有办法让我更有效地使用/存储输入/输出(在 data.frame、向量列表或其他中)来跟踪每天的所有计算?例如,使用多个向量(每个变量一个)并在结束时将它们重新组合成更复杂的对象(例如数据框列表)会更好吗?

顺便说一句,在将 S 中的大对象传递给函数并在其中进行修改时,我是否正确使用引用类来避免复制大对象?

比较的可重现示例:

SimulationClass <- setRefClass("Simulation",
                           fields = list(Table_Day = "data.frame",
                                         Met_c= "data.frame",
                                         PerCohortFruitDemand_c="matrix",
                                         Parameters= "list",
                                         Zero_then_One="vector"))
S= SimulationClass$new()
# Initializing the table with dummy numbers : 
S$Table_Day= data.frame(one= 1:10000, two= rnorm(n = 10000), three= runif(n = 10000),Bud_dd= rep(0,10000))
S$Met_c= data.frame(DegreeDays= rnorm(n=10000, mean = 10, sd = 1))


f1= function(i){
    a= cumsum(S$Met_c$DegreeDays[i:(i-1000)])
}

f2= function(i){
    S$Table_Day$Bud_dd[(i-1000):i]= cumsum(S$Met_c$DegreeDays[i:(i-1000)])
}

res= microbenchmark(f1(1000),f2(1000),times = 10000)
autoplot(res)

结果:

另外,如果有人在编程此类模型方面有任何经验,我对模型开发的任何建议都非常感兴趣。

【问题讨论】:

  • 如果您对有关数据帧的性能感兴趣,您应该看看库data.table
  • @JulienNavarre 是的,我已经使用它在 S$Table_Day 数据帧上执行复杂任务的计算,但是您是否认为使用 data.table 而不是 data.frame 通过索引来修改值更快? (我不是在谈论执行计算,而是将结果存储在表中)。
  • 哦,是的!在这件事上查看data.table vignettes,你应该确保以最好的方式去做,但它快155倍
  • 您在示例中给出的 10,000 行范围是否代表您的实际数据大小?最快的方法可能会因大小而异。
  • @MattSummersgill 绝对是的。它有 10,000 到 15,000 长。 S$Table_Day 有 157 列,S$Met_c 有 48 列(行数相同)。

标签: r performance data.table


【解决方案1】:

我阅读了有关该问题的更多信息,为了繁荣,我将在这里写下其他帖子中提出的一些解决方案。

显然,在尝试减少按索引分配给 data.frame 的计算时间时,阅读写作都值得考虑。 来源均在其他讨论中找到:

几个解决方案似乎相关:

  1. 如果可能的话,使用matrix 而不是data.frame 来利用就地修改(Advanced R)
  2. 使用list 而不是data.frame,因为[&lt;-.data.frame 不是原始函数(Advanced R)
  3. 用 C++ 编写函数并使用 Rcpp (from this source)
  4. 使用.subset2 代替[ (third source) 进行阅读
  5. 按照@JulienNavarre 和@Emmanuel-Lin 以及不同来源的建议使用data.table,如果使用data.table 没有问题,则使用set 表示data.frame:=
  6. 尽可能使用[[ 而不是[(仅按一个值索引)。这个不是很有效,也很限制,所以我从下面的比较中删除了它。

以下是使用不同解决方案的性能分析:

代码:

# Loading packages :
library(data.table)
library(microbenchmark)
library(ggplot2)

# Creating dummy data :
SimulationClass <- setRefClass("Simulation",
                               fields = list(Table_Day = "data.frame",
                                             Met_c= "data.frame",
                                             PerCohortFruitDemand_c="matrix",
                                             Parameters= "list",
                                             Zero_then_One="vector"))
S= SimulationClass$new()
S$Table_Day= data.frame(one= 1:10000, two= rnorm(n = 10000), three= runif(n = 10000),Bud_dd= rep(0,10000))
S$Met_c= data.frame(DegreeDays= rnorm(n=10000, mean = 10, sd = 1))

# Transforming data objects into simpler forms :
mat= as.matrix(S$Table_Day)
Slist= as.list(S$Table_Day)
Metlist= as.list(S$Met_c)
MetDT= as.data.table(S$Met_c)
SDT= as.data.table(S$Table_Day)

# Setting up the functions for the tests :
f1= function(i){
    S$Table_Day$Bud_dd[i]= cumsum(S$Met_c$DegreeDays[i])
}
f2= function(i){
    mat[i,4]= cumsum(S$Met_c$DegreeDays[i])
} 
f3= function(i){
    mat[i,4]= cumsum(.subset2(S$Met_c, "DegreeDays")[i])
} 
f4= function(i){
    Slist$Bud_dd[i]= cumsum(.subset2(S$Met_c, "DegreeDays")[i])
}
f5= function(i){
    Slist$Bud_dd[i]= cumsum(Metlist$DegreeDays[i])
}
f6= function(i){
    set(S$Table_Day, i=as.integer(i), j="Bud_dd", cumsum(S$Met_c$DegreeDays[i]))
}
f7= function(i){
    set(S$Table_Day, i=as.integer(i), j="Bud_dd", MetDT[i,cumsum(DegreeDays)])
}
f8= function(i){
    SDT[i,Bud_dd := MetDT[i,cumsum(DegreeDays)]]
}


i= 6000:6500
res= microbenchmark(f1(i),f3(i),f4(i),f5(i),f7(i),f8(i), times = 10000)
autoplot(res)

以及由此产生的自动绘图:

使用f1 引用基分配,f2 使用matrix 而不是data.framef3 使用.subset2matrix 的组合,f4 使用list.subset2f5 使用两个lists(读写),f6 使用data.table::setf7 使用data.table::setdata.table 用于累积和,f8使用@ 987654362@:=.

正如我们所见,最好的解决方案是使用列表进行读写。看到data.table 是最糟糕的解决方案,这真是令人惊讶。我相信我做错了什么,因为它应该是最好的。如果你能改进它,请告诉我。

【讨论】:

  • 非常彻底!几个 cmets 1: 看起来 f2f6 没有在您发布的结果中运行。 2: 您的意思是在f6f7 中使用SDT 而不是S$Table_Day 作为set()x 参数吗? 3: as.integer() 强制在这里实际上不是必需的。
  • @Matt Summersgill,非常感谢您的评论!回复您的 cmets :1:我替换了图片(我选错了),谢谢。
  • 对不起,错过了我上一条评论的一些信息,我无法编辑它。这是完整版:1: 我换了一张图片(我选错了),谢谢。 2: 不,我故意将 S$Table_Day 用于 f6f7,因为 set() 与 data.frames 兼容。目的是在不干扰该表的数据结构的情况下测试 set() 的功能。然后测试:=(这是等价的),但代价是将S$Table 转换为data.table3:确实如此。我用它来避免警告(我的i 一开始不是整数)
  • 您应该检查您的结果是否相同。请参阅 ?microbenchmark 中的 my_check
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-08-22
  • 1970-01-01
  • 2022-01-13
  • 2017-08-19
  • 1970-01-01
  • 2011-10-23
  • 2014-03-12
相关资源
最近更新 更多