使用dplyr进行逐行操作

JR *_*mus 15 parallel-processing performance r 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")
                                          )
                               )
Run Code Online (Sandbox Code Playgroud)

我们希望在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()
Run Code Online (Sandbox Code Playgroud)

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

我的问题是

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

  • 有没有人有建议用不同的R包和方法执行这个计算?

Dav*_*urg 15

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


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

因此,第一步是减少" 通过 "操作.通过查看您的代码,您似乎正在根据以下方式扩大数据集的大小userID,start并且end- 所有其余操作都可以在后面进行(因此可以进行矢量化).此外,seq按行添加两次(这本身不是一个非常有效的功能)不会增加任何内容.最后,呼吁明确seq.POSIXtPOSIXt类将节省您的方法调度的开销.

我不确定如何有效地使用它dplyr,因为mutate无法处理它并且do功能(IIRC)总是被证明是高效率的自我.因此,让我们尝试一下data.table可以轻松处理此任务的包

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

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


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

虽然,矢量化并不是故事的结局.我们还需要考虑类转换,方法调度等.例如,我们可以创建hourlydatehournr使用不同的Date类函数或使用format或甚至可以substr.需要考虑的权衡是,例如,substr将是最快的,但结果将是一个character向量而不是Date一个 - 由您来决定您是否更喜欢最终产品的速度或质量.有时你可以赢得两者,但首先你应该检查你的选择.让我们以3种不同的矢量化方式计算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  
Run Code Online (Sandbox Code Playgroud)

data.table::hour速度和质量都是明显的赢家(结果是一个整数向量而不是一个字符),同时将你以前的解决方案的速度提高了~x12,000倍(我甚至没有测试它对你的行实现).

现在让我们尝试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 
Run Code Online (Sandbox Code Playgroud)

似乎第一和第三选项在速度方面几乎相同,而我更喜欢as.IDate因为integer存储模式.


既然我们知道效率和质量在哪里,我们就可以通过运行来完成任务

res[, `:=`(hourlydate = as.IDate(V1), hournr = hour(V1))]
Run Code Online (Sandbox Code Playgroud)

(然后,您可以使用res[, yourcolname := NULL]我将留给您的类似语法轻松删除不必要的列)


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

作为旁注,如果您想进一步研究data.table语法/功能,这里是一个很好的阅读

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