在 R 中根据移动日期 window 缩放变量:脚本有效,但速度慢得令人无法接受。优化方法? rstats

Scale variables over a moving date window in R: script works, but unacceptably slow. Ways to optimize? rstats

我有一个数据框,其中每一行代表特定日期特定类别的数据:

set.seed(1)
k <- 10
df <- data.frame(
    name = c(rep('a',k), rep('b',k)), 
    date = rep(seq(as.Date('2017-01-01'),as.Date('2017-01-01')+k-1, 'days'),2),
    x = runif(2*k,1,20),
    y = runif(2*k,100,300)
    )
View(df)

负责人:

 head(df)
  name       date         x        y
1    a 2017-01-01  6.044665 286.9410
2    a 2017-01-02  8.070354 142.4285
3    a 2017-01-03 11.884214 230.3348
4    a 2017-01-04 18.255948 125.1110
5    a 2017-01-05  4.831957 153.4441
6    a 2017-01-06 18.069404 177.2228

结构:

str(df)
'data.frame':   20 obs. of  4 variables:
 $ name: Factor w/ 2 levels "a","b": 1 1 1 1 1 1 1 1 1 1 ...
 $ date: Date, format: "2017-01-01" "2017-01-02" "2017-01-03" "2017-01-04" ...
 $ x   : num  6.04 8.07 11.88 18.26 4.83 ...
 $ y   : num  287 142 230 125 153 ...

我需要在特定日期 window 缩放此数据的 x 和 y 变量。 我想出的脚本如下:

library(dplyr)
library(lubridate)
df2 <- df
moving_window_days <- 4

##Iterate over each row in df
for(i in 1:nrow(df)){ 
    df2[i,] <- df %>% 
        ##Give me only rows for 'name' on the current row 
        ##which are within the date window of interest
        filter(date <= date(df[i,"date"]) & 
               date >= date(df[i,"date"]) - moving_window_days & 
               name == df[i,"name"]
               ) %>% 
        ##Now scale x and y on this date wondow
        mutate(x = percent_rank(x), 
               y = percent_rank(y)
        ) %>% 
        ##Get rid of the rest of the rows - leave only the row we are looking at
        filter(date == date(df[i,"date"])) 
}

它按预期工作(好吧,我最初想在移动中获得每个观察的百分位数 window,但缩放后的值就可以正常工作) 问题是真实的数据集要大得多:

我 运行 这个脚本基于真实数据,在 30,000 行中 它只能超过 5,0004 小时内... 这是我第一次 运行 遇到这样的问题。

我确定我的脚本效率非常低(我确定是因为我不是 R 专家。我只是假设总有更好的方法)

有什么办法可以优化/改进这个脚本吗?

我能做些什么来以不同的方式解决这个问题?

您可以做的一件事是并行处理。我为此使用 future 包。这可能会惹恼一些人,他们可能认为它是 hack,因为 future 包的目的是......好吧......对于未来(或者 "promises" 如果你是前端开发人员)。这种方法很挑剔,但效果很好。

    library(future)

    # Create a function that iterates over each row in the df:
    my_function <- function(df, x) {
          x <- df
      for(i in 1:nrow(df)){ 
          x[i, ] <- df %>% 
              ##Give me only rows for 'name' on the current row 
              ##which are within the date window of interest
              filter(date <= date(df[i,"date"]) & 
                     date >= date(df[i,"date"]) - moving_window_days & 
                     name == df[i,"name"]
                     ) %>% 
              ##Now scale x and y on this date wondow
              mutate(x = percent_rank(x), 
                     y = percent_rank(y)
              ) %>% 
              ##Get rid of the rest of the rows - leave only the row we are looking at
              filter(date == date(df[i,"date"])) 
      }
          return(x)
    }

    plan(multiprocess) # make sure to always include this in a run of the code.

    # Divide df evenly into three separate dataframes:
    df1 %<-% my_function(df[1:7, ], df1)
    df2 %<-% my_function(df = df[(8 - moving_window_days):14, ], df2) # But from here on out, go back 4 days to include that data in the moving average calculation.
    df3 %<-% my_function(df = df[(15 - moving_window_days):20, ], df3)

# See if your computer is able to split df into 4 or 5 separate dataframes. 

    # Now bind the dataframes together, but get the indexing right:
    rbind(df1, df2[(nrow(df2) - 6):nrow(df2), ], df3[(nrow(df3) - 5):nrow(df3), ])

并行处理是优化代码以提高效率的众多方法之一。过去,这种技术大大 加快了我的代码速度。它已将 运行 节目的时间从一天半缩短到 3 或 4 小时!

现在,理想情况下,我们希望使用 apply 系列和矩阵。这个答案只是我们可以加速代码的众多方法之一。此外,future 包允许我们在不学习新循环结构的情况下并行处理,例如 parallel 包(尽管如此,它仍然是一个了不起的包)。

另请查看 Rcpp 包。这需要一些时间来学习,但对于释放 C++ 的速度来说是不可思议的。

@OP 你应该小心提供的答案

--我的原答案--

library(tidyverse)

我先split将数据帧放入一个按name

分组的数据帧列表
split.df <- split(df, df$name)

使用拆分数据,使用 lapplymap_df 遍历每个分组 df 的行,filter 对于相关 window 时间之间的日期使用 between,然后像以前一样 mutate,然后再次 filter 相关行(我尝试 'copy' 您的代码尽可能接近):

new <- lapply(split.df, function(z) map_df(1:nrow(z), ~z %>% 
                                              filter(between(date, z$date[.x]-moving_window_days, z$date[.x])) %>% 
                                              mutate(x=percent_rank(x),y=percent_rank(y)) %>% 
                                              filter(date==z$date[.x])))

这导致 list。转换回单个数据框

final <- Reduce("rbind",new)

输出(head

   name       date         x    y
1     a 2017-01-01 0.0000000 0.00
2     a 2017-01-02 1.0000000 0.00
3     a 2017-01-03 1.0000000 0.50
4     a 2017-01-04 1.0000000 0.00

让我们确保我的结果与您的结果相符。

identical(final$x, OP.output$x)
[1] TRUE

--我原来的回答结束--

----------------------------比较解决方案------------ ------------------

--@Brian的回答-- @Brian 的回答与您期望的结果不同。你说你的函数 works as intended,所以让我们将 Brian 的结果与你的进行比较。第一个显示 Brian 的结果。第二个显示您的结果。

     name       date         x        y        x2        y2
 1      a 2017-01-01  6.044665 286.9410 0.0000000 1.0000000
 2      a 2017-01-02  8.070354 142.4285 0.0000000 1.0000000
 3      a 2017-01-03 11.884214 230.3348 0.3333333 0.3333333
 4      a 2017-01-04 18.255948 125.1110 0.3333333 1.0000000

   name       date         x    y
1     a 2017-01-01 0.0000000 0.00
2     a 2017-01-02 1.0000000 0.00
3     a 2017-01-03 1.0000000 0.50
4     a 2017-01-04 1.0000000 0.00

identical(Brian.output$x2, OP.output$x, )
[1] FALSE

--结束@Brian的回答--

--@奥德修斯的回答--

@Odysseus 的回答 returns 正确的结果,因为它使用了相同的函数,但您必须手动拆分数据框。请参阅下面调用 my_function

的代码
df1 %<-% my_function(df[1:7, ], df1)
df2 %<-% my_function(df = df[(8 - moving_window_days):14, ], df2) # But from here on out, go back 4 days to include that data in the moving average calculation.
df3 %<-% my_function(df = df[(15 - moving_window_days):20, ], df3)

--结束@Odysseus的回答--

您可能会从@Odysseus 的回答中获得最佳性能增益,但您需要自己对其进行基准测试,因为这取决于您拥有的内核数量。并行化并不总是比矢量化操作更快。但是您需要将他的解决方案扩展到数据框的其余部分。

zoo::rollapply 可以相当快。

df2 <- df %>% 
  group_by(name) %>% 
  mutate(x2 = zoo::rollapply(x, width = 4, FUN = percent_rank, fill = "extend")[,1],
         y2 = zoo::rollapply(y, width = 4, FUN = percent_rank, fill = "extend")[,1])

每次调用 rollapply 都会生成一个包含 n=width 列的矩阵。第一列是从该观察开始的 window 的函数值,而第 n 列是 window 结束的函数值有了那个观察。您可以将 [,1] 更改为您想要的任何列(window 中间的百分位?在末尾?在开头?)。

参数 fill = "extend" 重复 windows 开头或结尾的观察结果,因为最后 n-k 个观察结果有 window.

缺少 k

我将您的数据集扩展为 28,496 行的虚拟数据集,涵盖 26 个名称和 3 年的数据,运行 这个片段的宽度为 90 天。在我 4 岁的台式机上,两个变量用了不到一分钟的时间:

   user  system elapsed 
  37.66    0.01   37.77 

你当然可以使用 purrr::map2 来迭代 6 个变量(而不是在 mutate 中调用 rollapply 6 次),但我不确定它会大大加快速度.