使用dplyr进行逐行操作

时间:2016-01-26 19:50:35

标签: r performance parallel-processing dplyr

我正在处理2,3万条记录的R中的大型数据框,其中包含具有开始和停止时间的位置的用户交易。我的目标是创建一个新的数据框,其中包含每个用户/每个位置连接的时间量。我们称这是每小时连接一次。

交易可以从8分钟到48小时不等,因此目标数据框将是大约1亿条记录,并且每个月都会增长。

下面的代码显示了最终数据框的开发方式,尽管总代码更复杂。在英特尔(R)Xeon(R)CPU E5-2630 v3 @ 2.40GHz,16核128GB RAM上运行总代码大约需要9个小时。

library(dplyr)

numsessions<-1000000
startdate <-as.POSIXlt(runif(numsessions,1,365*60*60)*24,origin="2015-1-1")

df.Sessions<-data.frame(userID = round(runif(numsessions,1,500)),
           postalcode = round(runif(numsessions,1,100)),
           daynr = format(startdate,"%w"),
              start =startdate ,
              end=   startdate + runif(1,1,60*60*10)
           )


dfhourly.connected <-df.Sessions %>% rowwise %>% do(data.frame(userID=.$userID,
                                          hourlydate=as.Date(seq(.$start,.$end,by=60*60)),
                                          hournr=format(seq(.$start,.$end,by=60*60),"%H")
                                          )
                               )

我们希望在16个核心(部分)上并行化此过程以加快程序。第一次尝试是使用multidplyr包。分区基于daynr

df.hourlyconnected<-df.Sessions %>% 
                      partition(daynr,cluster=init_cluster(6)) %>%
                      rowwise %>% do(data.frame(userID=.$userID,
                            hourlydate=as.Date(seq(.$start,.$end,by=60*60)),
                            hournr=format(seq(.$start,.$end,by=60*60),"%H")
                              )
                            ) %>% collect()

现在,rowwise函数似乎需要数据帧作为输入而不是分区。

我的问题是

  • 是否有解决方法对每个核心的分区执行逐行计算?

  • 是否有人建议使用不同的R包和方法执行此计算?

1 个答案:

答案 0 :(得分:15)

(我认为将此作为答案发布可能会使对未来对有效编码感兴趣的读者受益。)

R是一种矢量化语言,因此按行操作是最昂贵的操作之一;特别是如果你正在评估许多函数,调度方法,转换类和创建新的数据集。

因此,第一步是减少“ by ”操作。通过查看您的代码,您似乎正在根据userIDstartend扩大数据集的大小 - 其余所有操作都可以在后面进行(因此被矢量化)。此外,按行添加seq(它本身不是一个非常有效的函数)两次也不会增加任何内容。最后,在seq.POSIXt类上显式调用POSIXt将为您节省方法调度的开销。

我不确定如何使用dplyr有效地执行此操作,因为mutate无法处理它,do函数(IIRC)总是被证明是非常低效的。因此,让我们试试可以轻松处理此任务的data.table

library(data.table) 
res <- setDT(df.Sessions)[, seq.POSIXt(start, end, by = 3600), by = .(userID, start, end)] 

请再次注意,我将“ by row ”操作最小化为单个函数调用,同时避免了方法调度

现在我们已准备好数据集,我们不再需要任何行操作,从现在开始,所有内容都可以进行矢量化。

虽然,矢量化并不是故事的结局。我们还需要考虑类转换,方法调度等。例如,我们可以使用不同的hourlydate类函数或使用{{1}创建hournrDate。或者甚至可能format。需要考虑的权衡是,例如,substr将是最快的,但结果将是substr向量而不是character向量 - 它取决于您可以决定是否更喜欢最终产品的速度或质量。有时你可以赢得两者,但首先你应该检查你的选择。让我们以3种不同的矢量化方式计算Date变量

hournr

library(microbenchmark) set.seed(123) N <- 1e5 test <- as.POSIXlt(runif(N, 1, 1e5), origin = "1900-01-01") microbenchmark("format" = format(test, "%H"), "substr" = substr(test, 12L, 13L), "data.table::hour" = hour(test)) # Unit: microseconds # expr min lq mean median uq max neval cld # format 273874.784 274587.880 282486.6262 275301.78 286573.71 384505.88 100 b # substr 486545.261 503713.314 529191.1582 514249.91 528172.32 667254.27 100 c # data.table::hour 5.121 7.681 23.9746 27.84 33.44 55.36 100 a 是速度和质量的明显赢家(结果是整数向量而不是字符1),同时提高先前解决方案的速度 ~x12,000 (我甚至没有针对你的行实现测试它。)

现在让我们为data.table::hour

尝试3种不同的方式
data.table::hour

似乎第一个和第三个选项在速度方面几乎相同,而我更喜欢microbenchmark("as.Date" = as.Date(test), "substr" = substr(test, 1L, 10L), "data.table::as.IDate" = as.IDate(test)) # Unit: milliseconds # expr min lq mean median uq max neval cld # as.Date 19.56285 20.09563 23.77035 20.63049 21.16888 50.04565 100 a # substr 492.61257 508.98049 525.09147 515.58955 525.20586 663.96895 100 b # data.table::as.IDate 19.91964 20.44250 27.50989 21.34551 31.79939 145.65133 100 a 因为as.IDate存储模式。

现在我们知道效率和质量在哪里,我们可以通过运行

来完成任务
integer

(然后您可以使用res[, `:=`(hourlydate = as.IDate(V1), hournr = hour(V1))] 的类似语法轻松删除不必要的列,我会留给您)

可能有更有效的方法来解决这个问题,但这证明了如何提高代码效率的可行方法。

作为旁注,如果您想进一步调查res[, yourcolname := NULL]语法/功能,请阅读以下内容

https://github.com/Rdatatable/data.table/wiki/Getting-started