嵌套循环的替代方法,具有过滤的计算和数据导出功能

时间:2019-08-02 23:34:28

标签: r nested-loops

我有一个大的数据文件(1100万个观测值),并且有用于ID,年,月,时间段(以及变量,例如我感兴趣的速度)的列。我想对每个计算结果进行计算,然后将结果汇总为新的CSV,以便对结果进行格式化,并对每个唯一ID /年/月/小时的行进行格式化。

我能够通过一系列嵌套循环来完成此操作,当文件较小(数千次观察)时,嵌套循环可以很好地工作。我一直在尝试寻找一种具有Apply函数的更好的方法,但是无法获得相同的结构。我正在使用groupby在循环之前创建一些新列,该列运行速度很快,但是没有提供摘要输出csv。

results = NULL
data.calc = NULL
tmp = NULL
PERIOD = 5:9
YEAR = 2014:2017
LINK = 1:5
MONTH = 1:12

for(link in LINK,
  for (year in YEAR){
    for (month in MONTH){
      for (period in PERIOD){

      data.calc = filter(data, 
        LinkID_Int == link,
        Year==year, 
        MONTH==month,
        Period==period
        )

      #Speed
      spd.5 = quantile(data.calc$speed, 0.05)
      spd.20 = quantile(data.calc$speed, 0.20)
      spd.50 = quantile(data.calc$speed, 0.50)
      spd.85 = quantile(data.calc$speed, 0.85)
      spd.SD = sd(data.calc$speed)

       tmp = tibble(link, 
                   year, 
                   month,
                   period, 

                   spd.5, spd.20, spd.50, spd.85, 
                   spd.SD, 

                   )

      results = rbind(results, tmp)

    }
  }
}
}

write.csv(results, file="C:/Users/...", row.names = FALSE)

此代码有效,但是运行了几个小时却没有结果。我喜欢for循环的逻辑,这意味着我很容易阅读和理解正在发生的事情,但是我已经看到很多文章,它们提供了更快的解决方法。我在循环中运行着大约30个实际计算,涉及几个不同的变量。

我真的很感谢任何指导。

2 个答案:

答案 0 :(得分:1)

我认为您的许多速度下降是因为您反复filter数据(耗时1100万行)。由于您已经在使用dplyr(对于::filter),我建议您使用一种“整洁”的方法。由于没有您的数据,我将用mtcars进行演示:

library(dplyr)
mtcars %>%
  group_by(gear, vs, am) %>%
  summarize_at(vars(disp), .funs = list(~n(), ~mean(.), ~sd(.), q50 = ~quantile(.,0.5)))
# # A tibble: 7 x 7
# # Groups:   gear, vs [6]
#    gear    vs    am     n  mean    sd   q50
#   <dbl> <dbl> <dbl> <int> <dbl> <dbl> <dbl>
# 1     3     0     0    12 358.   71.8 355  
# 2     3     1     0     3 201.   72.0 225  
# 3     4     0     1     2 160     0   160  
# 4     4     1     0     4 156.   14.0 157. 
# 5     4     1     1     6  88.9  20.4  78.8
# 6     5     0     1     4 229.  114.  223  
# 7     5     1     1     1  95.1 NaN    95.1

您可以看到如何为该函数自动命名某些列,以及我如何推崇。这是可以导出(例如,导出到CSV)的“另一帧”。

如果您有多个要汇总统计信息的变量,只需在调用vars时将其包括在内,列名就会适当地出现:

mtcars %>%
  group_by(gear, vs, am) %>%
  summarize_at(vars(mpg, disp), .funs = list(~n(), ~mean(.), ~sd(.), q50 = ~quantile(.,0.5)))
# # A tibble: 7 x 11
# # Groups:   gear, vs [6]
#    gear    vs    am mpg_n disp_n mpg_mean disp_mean mpg_sd disp_sd mpg_q50 disp_q50
#   <dbl> <dbl> <dbl> <int>  <int>    <dbl>     <dbl>  <dbl>   <dbl>   <dbl>    <dbl>
# 1     3     0     0    12     12     15.0     358.    2.77    71.8    15.2    355  
# 2     3     1     0     3      3     20.3     201.    1.93    72.0    21.4    225  
# 3     4     0     1     2      2     21       160     0        0      21      160  
# 4     4     1     0     4      4     21.0     156.    3.07    14.0    21      157. 
# 5     4     1     1     6      6     28.0      88.9   5.12    20.4    28.8     78.8
# 6     5     0     1     4      4     19.1     229.    5.02   114.     17.8    223  
# 7     5     1     1     1      1     30.4      95.1 NaN      NaN      30.4     95.1

再加上一个“ BTW”:使用rbind(results, tmp)迭代构建结果可以在几次迭代中很好地工作,但是速度确实很慢。因为:每次rbind,它都会同时复制两者中的所有数据。如果results在调用rbind之前是1M行,则在进行行绑定时,一次内存中(至少)有2M行(1M行,两个副本)。通常,这样做一次或两次是没有问题的,但是您可以看到这样做数百次或数千次(取决于您所拥有的因素的数量)是有问题的。

更好的做法包括:

  • 使用以下内容预分配输出list

    out <- vector("list", prod(length(LINK), length(YEAR), length(MONTH), length(PERIOD))
    ind <- 0L
    for (...) {
      for (...) {
        for (...) {
          for (...) {
            tmp <- (do-stuff-here)
            ind <- ind + 1L
            out[[ind]] <- tmp
          }
        }
      }
    }
    out <- do.call(rbind, out)
    
  • lapply内完成并将输出分配给out,尽管将四个嵌套的for组合成一个lapply有点困难

我仍然认为尝试嵌套for并在每次通过中过滤数据是一个不好的起点。即使您可以使用每次迭代-rbind来消除每次复制数据的效率低下,您仍然会有不必要的过滤开销。

但是,如果您必须,则可以考虑对每个{for进行过滤:

out <- vector("list", prod(...)) # as above
ind <- 0L
for (lk in LINK) {
  dat_l <- mydat[LinkID_Int == lk,,drop=FALSE]
  for (yr in YEAR) {
    dat_y <- dat_l[Year == yr,,drop=FALSE]
    for (mh in MONTH) {
      dat_m <- dat_y[Month == mh,,drop=FALSE]
      for (pd in PERIOD) {
        data.calc <- dat_m[Period == pd,,drop=FALSE]
        tmp <- {do-stuff-here}
        ind <- ind + 1L
        out[[ ind ]] <- tmp
      }
    }
  }
}

在这种情况下,至少每个内部循环都对少得多的数据进行过滤。 这仍然是低效率的,但效果稍差。

(我仍然认为上述dplyr解决方案更具可读性,可能更快,更易于维护且更具扩展性。)

答案 1 :(得分:1)

始终避免循环运行rbind,因为它会导致内存中的大量复制。请参见R Inferno的Patrick Burns的第2圈“增长的对象”。

由于您需要内联分组聚合,因此请考虑基R的ave,其返回的长度与输入向量相同,因此可以分配给新列。

results <- transform(data, 
      spd.5 = ave(speed, LinkID_Int, Year, MONTH, Period, FUN=function(x) quantile(x, 0.05)),
      spd.20 = ave(speed, LinkID_Int, Year, MONTH, Period, FUN=function(x) quantile(x, 0.2)),
      spd.50 = ave(speed, LinkID_Int, Year, MONTH, Period, FUN=function(x) quantile(x, 0.5)),
      spd.85 = ave(speed, LinkID_Int, Year, MONTH, Period, FUN=function(x) quantile(x, 0.85)),
      spd.SD = ave(speed, LinkID_Int, Year, MONTH, Period, FUN=sd)
)

对于数据的完整分组汇总,请考虑基数R的aggregate

agg_raw <- aggregate(speed ~ Year + MONTH + Period, 
                     function(x) c(spd.5 = unname(quantile(x, 0.05)),
                                   spd.20 = unname(quantile(x, 0.2)),
                                   spd.50 = unname(quantile(x, 0.5)),
                                   spd.85 = unname(quantile(x, 0.85)),
                                   spd.SD = sd(x))
       )

results <- do.call(data.frame, agg_raw)
colnames(results) <- gsub("speed.", "", colnames(results))