使用 mutate 创建新变量时,Dplyr 代码比预期慢

roa*_*rkz 4 r dplyr

我正在使用dplyr在我的数据框上创建三个新变量。数据帧为 84,253 obs。164 个变量。下面是我的代码。

# ptm <- proc.time()
 D04_Base2 <- D04_Base %>% 
    mutate(
        birthyr = year(as.Date(BIRTHDT,"%m/%d/%Y")),
        age = (snapshotDt - as.Date(BIRTHDT,"%m/%d/%Y")) / 365.25,
        age = ifelse(age > 100, NA, age)
        )
# proc.time() - ptm
user  system elapsed 
12.34    0.03   12.42 
Run Code Online (Sandbox Code Playgroud)

但是,我想知道我的代码是否存在明显的问题,因为它运行的时间比我预期的要长得多,或者是其他问题。如上所示,代码完成大约需要 12 秒。

Gre*_*gor 5

是的,您的代码中存在一些效率低下的问题:

  1. 您将该BIRTHDT列转换为Date两次。(这是迄今为止最大的问题。)
  2. base::as.Date不是超级快
  3. 您可以使用dplyr::if_else代替来base::ifelse获得一点性能提升。

让我们做一些测试:

library(microbenchmark)
library(dplyr)
library(lubridate)

mbm = microbenchmark::microbenchmark

# generate big-ish sample data
n = 1e5
dates = seq.Date(from = Sys.Date(), length.out = n, by = "day")
raw_dates = format(dates, "%m/%d/%Y")
df = data.frame(x = 1:n)
Run Code Online (Sandbox Code Playgroud)

日期转换

mbm(
    mdy = mdy(raw_dates),
    base = as.Date(raw_dates, format = "%m/%d/%Y")
)
# Unit: milliseconds
#  expr      min       lq     mean   median       uq      max neval cld
#   mdy 21.39190 27.97036 37.35768 29.50610 31.44242 197.2258   100  a 
#  base 86.75255 92.30122 99.34004 96.78687 99.90462 262.6260   100   b
Run Code Online (Sandbox Code Playgroud)

看起来比这个特定日期转换lubridate::mdy快 2-3 倍。as.Date

提取年份

mbm(
    year = year(dates),
    format = format(dates, "%Y")
)
# Unit: milliseconds
#    expr      min       lq     mean   median       uq      max neval cld
#    year 29.10152 31.71873 44.84572 33.48525 40.17116 478.8377   100  a 
#  format 77.16788 81.14211 96.42225 83.54550 88.11994 242.7808   100   b
Run Code Online (Sandbox Code Playgroud)

同样,lubridate::year(您似乎已经在使用)比base::format提取年份大约快 2 倍。

添加一列:

mbm(
    base_dollar = {dd = df; dd$y = 1},
    base_bracket = {dd = df; dd[["y"]] = 1},
    mutate = {dd = mutate(df, y = 1)},
    mutate_pipe = {dd = df %>% mutate(y = 1)},
    times = 100L
)
# Unit: microseconds
#          expr     min       lq     mean   median       uq      max neval cld
#   base_dollar 114.834 129.1715 372.8024 146.2275 408.4255 3315.964   100 a  
#  base_bracket 118.585 139.6550 332.1661 156.3530 255.2860 3126.967   100 a  
#        mutate 420.515 466.8320 673.9109 554.4960 745.7175 2821.070   100  b 
#   mutate_pipe 522.402 600.6325 852.2037 715.1110 906.4700 3319.950   100   c
Run Code Online (Sandbox Code Playgroud)

在这里我们看到基地做得很好。但还要注意,这些时间以微秒为单位,而上述日期内容的时间以毫秒为单位。无论您使用列base还是dplyr添加列,大约都会有 1% 的时间用于执行日期转换。

如果别的

x = rnorm(1e5)
mbm(
    base_na = ifelse(x > 0, NA, x),
    base_na_real = ifelse(x > 0, NA_real_, x),
    base_replace = replace(x, x > 0, NA_real_),
    dplyr = if_else(x > 0, NA_real_, x),
    units = "ms"
)
# Unit: milliseconds
#          expr      min        lq      mean    median        uq       max neval cld
#       base_na 9.399593 13.399255 18.502441 14.734466 15.998573 138.33834   100  bc
#  base_na_real 8.785988 12.638971 22.885304 14.075802 16.980263 132.18165   100   c
#  base_replace 0.748265  1.136756  2.292686  1.384161  1.802833   9.05869   100 a  
#         dplyr 5.141753  6.875031 14.157227 10.095069 11.561044 124.99218   100  b 
Run Code Online (Sandbox Code Playgroud)

这里的计时仍然以毫秒为单位,但ifelse和之间的差异dplyr::if_else并不那么极端。dplyr::if_else要求返回向量是相同的类型,因此我们必须指定它NA_real_才能处理数字输出。在 Frank 的建议下,我也加入了base::replaceNA_real_它大约快了 10 倍。我认为这里的教训是“使用最简单的有效函数”。


总之,比添加列dplyr慢,但与正在发生的其他事情相比,两者都超级快。base因此,使用哪种列添加方法并不重要。您可以通过不重复计算和使用更大操作的更快版本来加速代码。根据我们所学到的知识,您的代码的更有效版本将是:

library(dplyr)
library(lubridate)
D04_Base2 <- D04_Base %>% 
    mutate(
        birthdate = mdy(BIRTHDT),
        birthyr = year(birthdate),
        age = (snapshotDt - birthdate) / 365.25,
        age = replace(age > 100, NA_real_)
    )
Run Code Online (Sandbox Code Playgroud)

我们可以将 1e5 行的速度增益估计为大约 180 毫秒,如下所示。

  • 170 毫秒(单次lubridate::mdy调用 30 毫秒,而不是两次as.Date调用,每次调用 100 毫秒)
  • 10 毫秒(replace而不是ifelse

添加基准表明我们可以通过不使用管道节省大约 0.1 毫秒。由于我们要添加多个列,因此使用它可能dplyr比使用 单独添加它们更有效$<-,但对于单个列,我们可以通过不使用 节省大约 0.5 毫秒dplyr。由于我们已经加速了 180 毫秒左右,因此不使用而获得的潜在毫秒数mutate只是舍入误差,而不是效率提升。

在这种情况下,您要做的最复杂的事情是Date转换,但如果您要进行更多处理,即使这也可能不是您的瓶颈。要优化代码,您应该查看哪些部分速度较慢,并针对速度较慢的部分进行处理。这称为分析在这个答案中,我曾经microbenchmark对竞争性短方法进行了正面比较,但其他工具(例如lineprof包)更适合识别代码块中最慢的部分。