【问题标题】:How can I write this -working- code to improve performance?我如何编写这个工作代码来提高性能?
【发布时间】:2019-11-19 05:32:13
【问题描述】:

我从一个更大的数据集(25K 记录)中设置了这个样本。我注意到我的应用程序的代码在这块上变慢了,我想检查一下性能改进。

上下文:我的财政年度从七月开始,到六月结束。因此,我的记录有一个不同于一个月和一个日历年的财务期间和财务年度。我想添加额外的列来指示日历年和月。

FinancialPeriod-FinancialYear:2018 年 1 月 1 日是 2017 年 7 月(2017 年 7 月),
FinancialPeriod-FinancialYear : 07 2018 is 1 2018 (Jan 2018), etc..

可重现的例子:

dt<-data.table(FinancialPeriod =c(3,4,4,5,1,2,8,8,11,12,2,3,10,1,6), FinancialYear=c(2018), Amount=c(12,14,16,18,12))
dt$Month<-dt$FinancialPeriod + 6
dt$Year<-dt$FinancialYear
t1<-proc.time()
for(row in 1:nrow(dt)){
  if (dt[row,"Month"] > 12){
    dt[row,"Month"]<- dt[row,"Month"] -12
  } 
  else {
    dt[row,"Year"]<- dt[row,"Year"] -1
  }
}
proc.time()-t1
dt

上面的代码可以运行,但是运行缓慢。我想就如何改进提出建议。

【问题讨论】:

    标签: r performance for-loop data.table coding-efficiency


    【解决方案1】:

    看起来您使用的是data.table,所以我使用了data.table 语法。有多种解决方案,但一种选择是使用ifelse

    dt[, c("Month", "Year") := list(ifelse(Month > 12, Month-12, Month), ifelse(Month <= 12, Year-1, Year))]
    

    或者,如果您想跳过创建初始变量 MonthYear

    dt[, c("Month", "Year") := list(ifelse(FinancialPeriod + 6 > 12, FinancialPeriod -6, FinancialPeriod + 6), 
                                    ifelse(FinancialPeriod + 6 <= 12, FinancialYear-1, FinancialYear))]
    

    基准测试

    原始数据:

    Unit: microseconds
         expr     min       lq      mean   median       uq     max neval
     for_loop 21183.8 21765.90 23600.586 22169.80 22981.15 73243.2   100
       ifelse   298.7   332.05   350.509   356.35   369.10   495.8   100
    

    更大的数据集:

    data.table(sapply(dt, rep, 100))
    Unit: microseconds
         expr       min      lq       mean     median        uq       max neval
     for_loop 1645857.5 1655792 1690718.78 1696271.35 1704510.1 1746731.2    10
           o1     395.4     398     425.29     425.05     446.6     462.3    10
    

    代码:

    microbenchmark::microbenchmark(
      for_loop = {
        for(row in 1:nrow(dt)){
          if (dt[row,"Month"] > 12){
            dt[row,"Month"]<- dt[row,"Month"] -12
          } 
          else {
            dt[row,"Year"]<- dt[row,"Year"] -1
          }
        }
      },
      ifelse = {
        dt[, c("Month", "Year") := list(ifelse(Month > 12, Month-12, Month), ifelse(Month <= 12, Year-1, Year))]
      },
      times = 100
    )
    

    【讨论】:

    • 非常感谢您的回答,如果我可以将 2 个答案标记为正确,我也会添加您的答案。它也很完美。感谢您的努力。
    【解决方案2】:

    另一种选择:

    i <- DT1[Month > 12L, which=TRUE]
    DT1[i, Month := Month - 12L][-i, Year := Year - 1L]
    

    请注意,rdatatable github 站点上有一个 fifelse 正在开发中。

    使用实际暗度计时:

    # A tibble: 4 x 14
      expression      min     mean   median      max `itr/sec` mem_alloc  n_gc n_itr total_time result                  memory                 time    gc              
      <chr>      <bch:tm> <bch:tm> <bch:tm> <bch:tm>     <dbl> <bch:byt> <dbl> <int>   <bch:tm> <list>                  <list>                 <list>  <list>          
    1 for_loop      1.92s    1.92s    1.92s    1.92s     0.521    4.66GB   110     1      1.92s <NULL>                  <Rprofmem [25,175 x 3~ <bch:t~ <tibble [1 x 3]>
    2 mtd0         1.45ms   2.26ms    1.7ms   7.49ms   442.       6.55MB    31   221   500.02ms <data.table [25,000 x ~ <Rprofmem [67 x 3]>    <bch:t~ <tibble [221 x ~
    3 mtd1       498.46us 650.79us  566.1us   2.98ms  1537.     699.56KB     6   768   499.81ms <data.table [25,000 x ~ <Rprofmem [24 x 3]>    <bch:t~ <tibble [768 x ~
    4 basemtd    950.97us   1.41ms   1.02ms  50.65ms   709.       5.06MB    41   355   500.45ms <dbl [25,000]>          <Rprofmem [52 x 3]>    <bch:t~ <tibble [355 x ~
    

    Base R 对于 25k 行数据已经非常快了。

    数据:

    set.seed(0L)
    DT <- data.table(FinancialPeriod=sample(12L, 25e3L, TRUE), 
        FinancialYear=c(2018))
    DT[, c("Month","Year") := .(FinancialPeriod + 6L, FinancialYear)]
    DT0 <- copy(DT)
    DT1 <- copy(DT)
    DF <- setDF(copy(DT))
    

    计时码:

    bench::mark(
        for_loop = {
            for(row in 1:nrow(DF)){
                if (DF[row,"Month"] > 12){
                    DF[row,"Month"]<- DF[row,"Month"] -12
                } 
                else {
                    DF[row,"Year"]<- DF[row,"Year"] -1
                }
            }
        },
        mtd0=DT0[, c("Month", "Year") := list(ifelse(Month > 12, Month-12, Month), 
            ifelse(Month <= 12, Year-1, Year))],
        mtd1={
            i <- DT1[Month > 12L, which=TRUE]
            DT1[i, Month := Month - 12L][-i, Year := Year - 1L]
        },
        basemtd={
            DF0$Month <- ifelse(DF0$Month > 12L, DF0$Month - 12L, DF0$Month)  
            DF0$Year <- ifelse(DF0$Month <= 12L, DF0$Year - 1L, DF0$Year)  
        },
        check=FALSE
    )
    

    【讨论】:

    • 优秀的答案,它提供了几个解决方案,并清楚地解释了为什么一个是最佳的。我从这个答案中学到了很多东西,非常感谢!
    【解决方案3】:

    我的建议是使用完整的日期,例如,财务期的开始。因此,我们可以使用FinancialDate 代替FinancialPeriod-FinancialYear 组合,例如,OP 的示例

    FinancialPeriod-FinancialYear : 01 2018

    变成

    财务日期:2018-01-01

    这种方法有几个好处:

    1. 只有一个日期对象来描述期间,而不是两个更易于处理的对象,例如用于聚合。如果需要,可以从日期对象中轻松提取月份和年份。
    2. 我们可以利用通常的日期算法,例如lubridate 包,将FinancialDate 转换为CalendarDate
    3. 此外,绘图更适合日期对象。

    代码如下:

    library(lubridate)
    dt[, FinDate := make_date(FinancialYear, FinancialPeriod)]
    dt[, CalDate := FinDate %m+% months(6)]   # convert to calendar date by adding offset
    dt[, c("CalMonth", "CalYear") := list(month(CalDate), year(CalDate))]
    dt
    
        FinancialPeriod FinancialYear Amount Month Year    FinDate    CalDate CalMonth CalYear
     1:               3          2018     12     9 2018 2018-03-01 2018-09-01        9    2018
     2:               4          2018     14    10 2018 2018-04-01 2018-10-01       10    2018
     3:               4          2018     16    10 2018 2018-04-01 2018-10-01       10    2018
     4:               5          2018     18    11 2018 2018-05-01 2018-11-01       11    2018
     5:               1          2018     12     7 2018 2018-01-01 2018-07-01        7    2018
     6:               2          2018     12     8 2018 2018-02-01 2018-08-01        8    2018
     7:               8          2018     14    14 2018 2018-08-01 2019-02-01        2    2019
     8:               8          2018     16    14 2018 2018-08-01 2019-02-01        2    2019
     9:              11          2018     18    17 2018 2018-11-01 2019-05-01        5    2019
    10:              12          2018     12    18 2018 2018-12-01 2019-06-01        6    2019
    11:               2          2018     12     8 2018 2018-02-01 2018-08-01        8    2018
    12:               3          2018     14     9 2018 2018-03-01 2018-09-01        9    2018
    13:              10          2018     16    16 2018 2018-10-01 2019-04-01        4    2019
    14:               1          2018     18     7 2018 2018-01-01 2018-07-01        7    2018
    15:               6          2018     12    12 2018 2018-06-01 2018-12-01       12    2018
    

    【讨论】:

    • 好方法,我在另一个项目中也是这样工作的,它确实是一种简单且更高效的绘图方法。我摆脱它的一个原因是,帐户上的金额实际上是在当月的最后一天而不是第一天衡量的,但我确信这是一个功能。
    • 表示一个月,可以是一个月中的任何一天。但是,一个月的第一天更容易处理。对于打印和绘图,必须仔细选择格式,例如"%Y-%m",以便“隐藏”月份中的日期。因此,将一个月映射到一个月的第一天还是最后一天并不重要。
    • 可以计算一个月的最后一天,例如,library(lubridate); update(x, mday = days_in_month(x)) 其中x 是一个 Date 对象。
    猜你喜欢
    • 1970-01-01
    • 2013-10-15
    • 2010-10-20
    • 1970-01-01
    • 2020-07-20
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多