使用mutate

时间:2017-02-28 22:09:14

标签: 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 

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

1 个答案:

答案 0 :(得分:4)

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

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

    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)
    

    日期转换

    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
    

    在此特定日期转换时,lubridate::mdy看起来比as.Date快2-3倍。

    提取年份

    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
    

    同样地,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
    

    在这里,我们看到基地做得很好。但是请注意,这些时间是微秒,而日期的上述时间是毫秒。无论您使用base还是dplyr添加列,都只占用于执行日期转换的时间的1%。

    ifelse

    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 
    

    此处的时间仍然以毫秒为单位,但ifelsedplyr::if_else之间的差异并不是那么极端。 dplyr::if_else要求返回向量是相同的类型,因此我们必须指定NA_real_才能使用数字输出。在弗兰克的建议中,我也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_)
        )
    

    我们可以在大约180毫秒的时间内对1e5行的速度增益进行调整,如下所示。

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

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

    在这种情况下,您所做的最复杂的事情是Date转换,但如果您正在进行更多处理,即使此也可能不是您的瓶颈。要优化代码,您应该看到哪些部分很慢,并且处理慢速位。 This is called profiling.在这个答案中,我使用microbenchmark来比较竞争的短方法,但其他工具(如lineprof包)更适合识别块的最慢部分代码。