R data.table:具有不同条件的总和

时间:2017-08-05 05:34:20

标签: r data.table cumsum

简介

我写这个作为一个问题(阅读本简介并跳到最后一节),并作为一个练习,以帮助我更清楚地思考。如果这被认为不适合SO,请指出并将其移至其他地方。

我有一份付款记录清单,即现金流量,具有以下结构:

  • 每份合约都有ID,开始日期,(预计)结束日期和多个付款记录。
  • 每笔付款都有一个号码,截止日期,收到日期,到期金额和收到的金额。
  • 某些付款可能会违约,即逾期。
  • 有些付款尚未发生,收到的日期为NA,收到的金额为0。
  • 对于每份合同,第一笔付款应在合同签订后30天到期。
  • 对于每份合同,实际结束日期是收到所有付款的日期。

出于本练习的目的,我构建了一个data.table,其中包含以下模拟数据:

require(data.table)
require(lubridate)

dA <- as.Date('2016-08-02')
dB <- as.Date('2017-01-25')
dC <- as.Date('2017-05-10')
ws <- data.table(
  ID       = c(rep('A', 12L), rep('B', 12L), rep('C', 12L)),
  pmt_no   = rep(1L:12L, 3),
  start    = c(
    rep(dA, 12L),
    rep(dB, 12L),
    rep(dC, 12L)
  ),
  end      = c(
    rep(dA %m+% days(360L), 12L),
    rep(dB %m+% days(360L), 12L),
    rep(dC %m+% days(360L), 12L)
  ),
  due_date = c(
    dA %m+% days(seq(30L, 360L, by = 30L)),
    dB %m+% days(seq(30L, 360L, by = 30L)),
    dC %m+% days(seq(30L, 360L, by = 30L))
  ),
  rec_date = c(
    dA %m+% days(seq(30L, 360L, by = 30L)),
    dB %m+% days(seq(30L, 360L, by = 30L)),
    dC %m+% days(seq(30L, 360L, by = 30L))
  ),
  due_amt  = c(rep(120, 12L), rep(230, 12L), rep(180, 12L)),
  rec_amt  = c(rep(120, 12L), rep(230, 12L), rep(180, 12L))
)[
  due_date >= '2017-08-01',
  `:=`(rec_date = NA, rec_amt = 0)
][
  c(2L:5L,14L), rec_date := rec_date %m+% days(13L)
][
  c(11L,18L), `:=`(rec_date = NA, rec_amt = 0)
]

一些特征:

  • 3份合约的36笔付款,每笔12份。
  • 有些付款违约,其中一些仍未付款。
  • 数据日期为2017-08-01。因此,超过此日期的任何付款尚未发生。

这就是它的样子:

   ID pmt_no start_date   end_date   due_date   rec_date due_amt rec_amt
1:  A      1 2016-08-02 2017-07-28 2016-09-01 2016-09-01     120     120
2:  A      2 2016-08-02 2017-07-28 2016-10-01 2016-10-14     120     120
3:  A      3 2016-08-02 2017-07-28 2016-10-31 2016-11-13     120     120

使用数据:sumcumsum

考虑以下问题:

  • 每个月底有多少付款,到期金额是多少?每个月底收集了多少和多少金额?

我的本​​能方法是构造一个date向量,然后循环几次以获取所有数字。但是我认为以下内容更符合data.table方式:

# amount due and received during each month, and
# total due and total received by (the end of) each month

merge(
  ws[
    due_date < '2017-08-01',
    .(due_amt = sum(due_amt)),
    keyby = .(year(due_date), month(due_date))
  ][
    , 
    due_total := cumsum(due_amt)
  ],
  ws[
    rec_date < '2017-08-01',
    .(rec_amt = sum(rec_amt)),
    keyby = .(year(rec_date), month(rec_date))
  ][
    , 
    rec_total := cumsum(rec_amt)
  ],
  all = TRUE
)

请注意,我已经省略了计数。结果很愉快:

    year month due_amt due_total rec_amt rec_total
 1: 2016     9     120       120     120       120
 2: 2016    10     240       360     120       240
 3: 2016    11     120       480     120       360
 <truncated>

条件:.SDsum

  • 每个月的过期到期总金额?

仍然可以解决,但我不得不操纵收到的日期以便于与截止日期进行比较:

ws[is.na(rec_date), rec_date := as.Date('2099-12-31')]
ws[
  due_date < '2017-08-01',
  .(
    over_total = .SD[rec_date > due_date, sum(due_amt)],
    over_outst = .SD[rec_date > '2017-08-01', sum(due_amt)]
  ),
  keyby = .(year(due_date), month(due_date))
]

结果:

    year month over_total over_outst
 1: 2016     9          0          0
 2: 2016    10        240          0
<truncated>
10: 2017     6        120        120
11: 2017     7        230        230

深入研究:前进cumsum

到目前为止,我做得很好。但请考虑一下:

  • 我们将来会收到多少金额(包括过去的会费), 2017年3月底的时候

对于单个日期,这不是什么大问题。虽然要注意排除在该日期之前尚未生效的任何合同。此外,如果您跳过中间部分,我已操纵rec_date以进行比较:

ws[is.na(rec_date), rec_date := as.Date('2099-12-31')]
ws[
  rec_date >= '2017-04-01' & start < '2017-04-01',
  .(bal = sum(due_amt))
]

结果:

bal
1: 3010

但是,是否有data.table方法可以为过去一个月找到这个数额?我可以写一个循环,但循环是麻烦,丑陋和缓慢。我想我正在寻找类似转发cumsum的内容,因此与keyby相结合,它或多或少等同于以下内容:

# To-be-collected amount by the end of each month, the loop way
result <- data.table(
  date = as.Date('2016-09-01') %m+% months(0L:11L)
)
for (i in 1L:result[, .N]) {
  temp <- ws[
    rec_date >= result[i, date] & start < result[i, date],
    .(bal = sum(due_amt))
  ]
  result[i, bal := temp]
}

结果:

          date  bal
 1: 2016-09-01 1440
 2: 2016-10-01 1320
 3: 2016-11-01 1200
 4: 2016-12-01 1080
 5: 2017-01-01  960
 6: 2017-02-01 3480
 7: 2017-03-01 3130
 8: 2017-04-01 3010
 9: 2017-05-01 2430
10: 2017-06-01 4240
11: 2017-07-01 3830
12: 2017-08-01 3530

0 个答案:

没有答案