我在r中有一个很大的性能问题。我写了一个迭代data.frame对象的函数。它只是简单地向data.frame添加一个新列并累积一些东西。(操作简单)。data.frame大约有850K行。我的电脑还在工作(大约10小时了),我不知道运行时间。

dayloop2 <- function(temp){
    for (i in 1:nrow(temp)){    
        temp[i,10] <- i
        if (i > 1) {             
            if ((temp[i,6] == temp[i-1,6]) & (temp[i,3] == temp[i-1,3])) { 
                temp[i,10] <- temp[i,9] + temp[i-1,10]                    
            } else {
                temp[i,10] <- temp[i,9]                                    
            }
        } else {
            temp[i,10] <- temp[i,9]
        }
    }
    names(temp)[names(temp) == "V10"] <- "Kumm."
    return(temp)
}

有什么办法可以加快这次行动吗?


当前回答

最大的问题和无效的根源是索引data.frame,我的意思是所有你使用temp[,]的行。 尽量避免这种情况。我把你的函数,更改索引,这里是version_A

dayloop2_A <- function(temp){
    res <- numeric(nrow(temp))
    for (i in 1:nrow(temp)){    
        res[i] <- i
        if (i > 1) {             
            if ((temp[i,6] == temp[i-1,6]) & (temp[i,3] == temp[i-1,3])) { 
                res[i] <- temp[i,9] + res[i-1]                   
            } else {
                res[i] <- temp[i,9]                                    
            }
        } else {
            res[i] <- temp[i,9]
        }
    }
    temp$`Kumm.` <- res
    return(temp)
}

正如你所看到的,我创建了收集结果的向量。最后,我将它添加到data.frame,我不需要打乱名称。 那么它有多好呢?

我用nrow从1000到10,000 × 1000运行data.frame的每个函数,并用system.time测量时间

X <- as.data.frame(matrix(sample(1:10, n*9, TRUE), n, 9))
system.time(dayloop2(X))

结果是

您可以看到您的版本以指数方式依赖于nrow(X)。修正后的模型有线性关系,简单的lm模型预测850,000行计算需要6分10秒。

向量化的力量

正如Shane和Calimo在他们的答案中所述,向量化是获得更好性能的关键。 从你的代码,你可以移动到循环之外:

调节 结果的初始化(是temp[i,9])

这导致了这段代码

dayloop2_B <- function(temp){
    cond <- c(FALSE, (temp[-nrow(temp),6] == temp[-1,6]) & (temp[-nrow(temp),3] == temp[-1,3]))
    res <- temp[,9]
    for (i in 1:nrow(temp)) {
        if (cond[i]) res[i] <- temp[i,9] + res[i-1]
    }
    temp$`Kumm.` <- res
    return(temp)
}

比较这个函数的结果,这次是nrow从10,000到100,000乘10,000。

调谐调谐

另一个调整是将循环索引temp[i,9]更改为res[i](在第i个循环迭代中完全相同)。 这又是索引向量和索引data。frame的区别。 第二件事:当你查看循环时,你可以看到不需要遍历所有的i,而只对符合条件的i进行遍历。 我们开始吧

dayloop2_D <- function(temp){
    cond <- c(FALSE, (temp[-nrow(temp),6] == temp[-1,6]) & (temp[-nrow(temp),3] == temp[-1,3]))
    res <- temp[,9]
    for (i in (1:nrow(temp))[cond]) {
        res[i] <- res[i] + res[i-1]
    }
    temp$`Kumm.` <- res
    return(temp)
}

您获得的性能高度依赖于数据结构。准确地说-在条件中TRUE值的百分比。 对于我的模拟数据,它需要850,000行小于1秒的计算时间。

如果你想更进一步,我认为至少有两件事是可以做到的:

写一个C代码来做条件累加 如果你知道你的数据Max序列不是很大,那么你可以把循环改为向量化 While (any(cond)) { cond <- c(FALSE, cond[-1] & !cond[-n]) Res [indx] <- Res [indx] + Res [which(indx)-1] cond[indx] <- FALSE }


用于模拟和图形的代码可在GitHub上获得。

其他回答

我不喜欢重写代码……当然,ifelse和lapply是更好的选择,但有时很难匹配。

我经常使用data.frames,就像使用df$var[I]这样的列表一样

这里有一个虚构的例子:

nrow=function(x){ ##required as I use nrow at times.
  if(class(x)=='list') {
    length(x[[names(x)[1]]])
  }else{
    base::nrow(x)
  }
}

system.time({
  d=data.frame(seq=1:10000,r=rnorm(10000))
  d$foo=d$r
  d$seq=1:5
  mark=NA
  for(i in 1:nrow(d)){
    if(d$seq[i]==1) mark=d$r[i]
    d$foo[i]=mark
  }
})

system.time({
  d=data.frame(seq=1:10000,r=rnorm(10000))
  d$foo=d$r
  d$seq=1:5
  d=as.list(d) #become a list
  mark=NA
  for(i in 1:nrow(d)){
    if(d$seq[i]==1) mark=d$r[i]
    d$foo[i]=mark
  }
  d=as.data.frame(d) #revert back to data.frame
})

data.frame版本:

   user  system elapsed 
   0.53    0.00    0.53

表版本:

   user  system elapsed 
   0.04    0.00    0.03 

使用向量列表比data.frame快17倍。

对于为什么内部data.frames在这方面这么慢,有什么意见吗?有人会认为它们像列表一样运作……

为了更快地编写代码,使用class(d)='list'而不是d=as.list(d)和class(d)='data.frame'

system.time({
  d=data.frame(seq=1:10000,r=rnorm(10000))
  d$foo=d$r
  d$seq=1:5
  class(d)='list'
  mark=NA
  for(i in 1:nrow(d)){
    if(d$seq[i]==1) mark=d$r[i]
    d$foo[i]=mark
  }
  class(d)='data.frame'
})
head(d)

这里的答案很好。有一个小方面没有被提及,那就是这个问题说的是“我的电脑还在工作(现在大约10小时了),我不知道运行时间”。在开发时,我总是将以下代码放入循环中,以了解更改如何影响速度,并监视完成所需的时间。

dayloop2 <- function(temp){
  for (i in 1:nrow(temp)){
    cat(round(i/nrow(temp)*100,2),"%    \r") # prints the percentage complete in realtime.
    # do stuff
  }
  return(blah)
}

也可以使用lapply。

dayloop2 <- function(temp){
  temp <- lapply(1:nrow(temp), function(i) {
    cat(round(i/nrow(temp)*100,2),"%    \r")
    #do stuff
  })
  return(temp)
}

如果循环中的函数非常快,但循环的数量很大,那么可以考虑偶尔打印一次,因为打印到控制台本身会有开销。如。

dayloop2 <- function(temp){
  for (i in 1:nrow(temp)){
    if(i %% 100 == 0) cat(round(i/nrow(temp)*100,2),"%    \r") # prints every 100 times through the loop
    # do stuff
  }
  return(temp)
}

最大的问题和无效的根源是索引data.frame,我的意思是所有你使用temp[,]的行。 尽量避免这种情况。我把你的函数,更改索引,这里是version_A

dayloop2_A <- function(temp){
    res <- numeric(nrow(temp))
    for (i in 1:nrow(temp)){    
        res[i] <- i
        if (i > 1) {             
            if ((temp[i,6] == temp[i-1,6]) & (temp[i,3] == temp[i-1,3])) { 
                res[i] <- temp[i,9] + res[i-1]                   
            } else {
                res[i] <- temp[i,9]                                    
            }
        } else {
            res[i] <- temp[i,9]
        }
    }
    temp$`Kumm.` <- res
    return(temp)
}

正如你所看到的,我创建了收集结果的向量。最后,我将它添加到data.frame,我不需要打乱名称。 那么它有多好呢?

我用nrow从1000到10,000 × 1000运行data.frame的每个函数,并用system.time测量时间

X <- as.data.frame(matrix(sample(1:10, n*9, TRUE), n, 9))
system.time(dayloop2(X))

结果是

您可以看到您的版本以指数方式依赖于nrow(X)。修正后的模型有线性关系,简单的lm模型预测850,000行计算需要6分10秒。

向量化的力量

正如Shane和Calimo在他们的答案中所述,向量化是获得更好性能的关键。 从你的代码,你可以移动到循环之外:

调节 结果的初始化(是temp[i,9])

这导致了这段代码

dayloop2_B <- function(temp){
    cond <- c(FALSE, (temp[-nrow(temp),6] == temp[-1,6]) & (temp[-nrow(temp),3] == temp[-1,3]))
    res <- temp[,9]
    for (i in 1:nrow(temp)) {
        if (cond[i]) res[i] <- temp[i,9] + res[i-1]
    }
    temp$`Kumm.` <- res
    return(temp)
}

比较这个函数的结果,这次是nrow从10,000到100,000乘10,000。

调谐调谐

另一个调整是将循环索引temp[i,9]更改为res[i](在第i个循环迭代中完全相同)。 这又是索引向量和索引data。frame的区别。 第二件事:当你查看循环时,你可以看到不需要遍历所有的i,而只对符合条件的i进行遍历。 我们开始吧

dayloop2_D <- function(temp){
    cond <- c(FALSE, (temp[-nrow(temp),6] == temp[-1,6]) & (temp[-nrow(temp),3] == temp[-1,3]))
    res <- temp[,9]
    for (i in (1:nrow(temp))[cond]) {
        res[i] <- res[i] + res[i-1]
    }
    temp$`Kumm.` <- res
    return(temp)
}

您获得的性能高度依赖于数据结构。准确地说-在条件中TRUE值的百分比。 对于我的模拟数据,它需要850,000行小于1秒的计算时间。

如果你想更进一步,我认为至少有两件事是可以做到的:

写一个C代码来做条件累加 如果你知道你的数据Max序列不是很大,那么你可以把循环改为向量化 While (any(cond)) { cond <- c(FALSE, cond[-1] & !cond[-n]) Res [indx] <- Res [indx] + Res [which(indx)-1] cond[indx] <- FALSE }


用于模拟和图形的代码可在GitHub上获得。

处理数据。表是一个可行的选择:

n <- 1000000
df <- as.data.frame(matrix(sample(1:10, n*9, TRUE), n, 9))
colnames(df) <- paste("col", 1:9, sep = "")

library(data.table)

dayloop2.dt <- function(df) {
  dt <- data.table(df)
  dt[, Kumm. := {
    res <- .I;
    ifelse (res > 1,             
      ifelse ((col6 == shift(col6, fill = 0)) & (col3 == shift(col3, fill = 0)) , 
        res <- col9 + shift(res)                   
      , # else
        res <- col9                                 
      )
     , # else
      res <- col9
    )
  }
  ,]
  res <- data.frame(dt)
  return (res)
}

res <- dayloop2.dt(df)

m <- microbenchmark(dayloop2.dt(df), times = 100)
#Unit: milliseconds
#       expr      min        lq     mean   median       uq      max neval
#dayloop2.dt(df) 436.4467 441.02076 578.7126 503.9874 575.9534 966.1042    10

如果忽略条件过滤可能带来的收益,它会非常快。显然,如果您可以在数据子集上进行计算,则会有所帮助。

在R中,您通常可以通过使用apply族函数来加速循环处理(在您的示例中,可能是复制)。看一下提供进度条的plyr包。

另一种选择是完全避免循环,用向量化算法代替它们。我不确定你到底在做什么,但你可能可以将你的函数一次性应用到所有行:

temp[1:nrow(temp), 10] <- temp[1:nrow(temp), 9] + temp[0:(nrow(temp)-1), 10]

这将会快得多,然后你可以用你的条件过滤行:

cond.i <- (temp[i, 6] == temp[i-1, 6]) & (temp[i, 3] == temp[i-1, 3])
temp[cond.i, 10] <- temp[cond.i, 9]

向量化算术需要更多的时间和思考问题,但有时可以节省几个数量级的执行时间。