【问题标题】:Generate records in an R data frame between two dates在两个日期之间的 R 数据框中生成记录
【发布时间】:2019-10-14 06:52:01
【问题描述】:

我有一个数据框,其中包含客户预定的订阅付款,如下所示:

CusID <- c(1,2,3)
FromDate <- c(ymd("2019-01-01"), ymd("2019-01-04"), ymd("2019-02-02"))
ToDate <-c(ymd("2019-01-16"), ymd("2019-01-15"), ymd("2019-04-03"))
Amount <- c(5,10,12)
Frequency <- c("Weekly", "Fortnightly", "Monthly")
Input <-  data.frame(CusID, Amount, Frequency, FromDate, ToDate)

对于每一行(客户),我希望从 FromDate 循环到 ToDate,并为这些日期之间的每个预定付款输出一行数据,从而产生以下数据框:

CusID <- c(1,1,1,2,3,3,3)
PaymentDate <- c(ymd("2019-01-01"), ymd("2019-01-08"), ymd("2019-01-15"),
                 ymd("2019-01-04"),ymd("2019-02-02"),ymd("2019-03-02"),ymd("2019-04-02"))
Amount <- c(5,5,5,10,12,12,12)
Output <-  data.frame(CusID, PaymentDate, Amount)

使用 R(最好使用 dplyr / tidyverse 函数)实现此目的的有效方法是什么?

在 SAS 中,我的方法是使用 DO / WHILE LOOP 和 OUTPUT 语句为每个预定付款编写一个新行。例如

data Output;
set Input;
PaymentDate = FromDate;
do while (PaymentDate < ToDate);
Payment = Amount;
PaymentDate = PaymentDate + (7 / 14 / 30 ~ logic based on Frequency);
output;
loop;
run;

(SAS 中的关键是输出语句 - 它每次调用时都会显式写入一条新记录,因此可以在循环中使用每个输入行写入多个输出行)。

R 中是否有等效的方法,或者是否推荐其他方法?

【问题讨论】:

    标签: r dataframe dplyr


    【解决方案1】:

    另一个使用tidyverse的选项

    Input %>%
      mutate(Frequency = case_when(Frequency == "Weekly" ~ 7L,
                                   Frequency == "Fortnightly" ~ 14L,
                                   Frequency == "Monthly" ~ 30L,
                                   TRUE ~ 0L)) %>%
      group_by(CusID) %>%
      group_modify(~ {PaymentDate <- seq.Date(from = .x$FromDate, to = .x$ToDate, by = .x$Frequency) 
                     crossing(.x[,1], PaymentDate)})  
    
    
    # A tibble: 7 x 3
    # Groups:   CusID [3]
      CusID PaymentDate Amount
      <dbl> <date>       <dbl>
    1     1 2019-01-01       5
    2     1 2019-01-08       5
    3     1 2019-01-15       5
    4     2 2019-01-04      10
    5     3 2019-02-02      12
    6     3 2019-03-04      12
    7     3 2019-04-03      12
    

    付款日期与您的预期输出略有不同,因为seq.Date 考虑到这些月份的不同天数,增加了 30 天。

    更新:

    这里有一个更逐字的解决方案

    Input %>% 
      mutate(PaymentDate = FromDate,
             RFrequency = case_when(Frequency == "Weekly" ~ '1 week',
                                    Frequency == "Fortnightly" ~ '2 weeks',
                                    Frequency == "Monthly" ~ '1 month')) %>% 
      group_by(CusID, Amount) %>% 
      expand(PaymentDate = seq.Date(FromDate,ToDate, by = RFrequency)) 
    
    # A tibble: 7 x 3
    # Groups:   CusID, Amount [3]
      CusID Amount PaymentDate
      <dbl>  <dbl> <date>     
    1     1      5 2019-01-01 
    2     1      5 2019-01-08 
    3     1      5 2019-01-15 
    4     2     10 2019-01-04 
    5     3     12 2019-02-02 
    6     3     12 2019-03-02 
    7     3     12 2019-04-02 
    

    【讨论】:

    • 这样更好!
    • 我现在要试试这个。正如您所指出的,这种方法的潜在限制是简化月/ 30 天的事情。我的错 - 我应该更具体一点,因为我可以看到这将是一个问题。在 SAS 中,我将使用 INTNX 日期偏移函数,它允许您移动“整个”月份 - 我猜 R 中的等价物将使用来自 lubridate 的东西。看起来下面的答案(user2474226)将其中一些答案与一种处理方式混合在一起。我会报告我的发现。
    • 如果这是您想要的,您可以更改 case_when() 部分。而不是数字分配"1 week""2 weeks""1 month""" 而不是0L。它会起作用的。
    • 扩展 tidyr 功能 - 完美!
    【解决方案2】:

    我调整了您的 Input data.frame,以便频率值是字符串,而不是因子。

    您可以创建一个帮助表 freq_mapping 将您的频率转换为 R 喜欢的频率格式。这将避免其他答案之一指出的 30 天问题。

    freq_mapping <- data.frame(Frequency=c('Weekly', 'Fortnightly', 'Monthly'), 
                               RFrequency = c('1 week', '2 weeks', '1 month'),
                               stringsAsFactors =  FALSE)
    

    然后将输入与此合并:

    Input <- Input %>%
        inner_join(freq_mapping, by = 'Frequency')
    

    现在您可以创建付款日期:

    Input$PaymentDate <- Input$FromDate
    Input %>% 
        group_by(CusID) %>% 
        complete(PaymentDate = seq.Date(FromDate,ToDate, by = RFrequency)) %>% 
        fill(PaymentDate,Amount) %>% 
        select(CusID, PaymentDate, Amount)
    

    【讨论】:

    • 是的!这样做。我已更新您的解决方案,将频率映射包含到 dplyr 步骤中。
    【解决方案3】:

    对我来说不是那么容易的问题。解决方案并不漂亮,但它应该以某种方式完成工作。您会看到每月付款存在问题,并不总是 30,但否则它应该可以工作。但肯定存在更好的解决方案。

        library(data.table)
    Input <-  data.frame(CusID, Amount, Frequency, FromDate, ToDate)
    
    Input=data.table(Input)
    Input[Frequency=="Weekly",freq:=7][Frequency=="Fortnightly",freq:=14][Frequency=="Monthly",freq:=30]
    Input[,Ratio:=(ToDate-FromDate)/freq]
    
    #What is the maximum rows ? for a customer ?
    NREP=as.integer(max(ceiling(Input$Ratio)))
    
    Input[,Rep:=1][,PaymentDate:=FromDate]
    for(i in 1:NREP){
    Inputtemp=copy(Input)
    Inputtemp[,FromDate:=FromDate+freq]
    Input=rbind(Input,Inputtemp)  
    }
    
    #Remove invalid rows
    Input=unique(Input)
    
    Input=Input[!(FromDate>ToDate),]
    setorder(Input,CusID)
    Input=Input[,c("CusID","FromDate","Amount")]
    setnames(Input,"FromDate","PaymentDate")
    Input==data.table(Output)
    

    【讨论】:

      【解决方案4】:

      Humpelstielzchen 和 user2474226 的答案的混搭,将所有逻辑整合到一个 dplyr 步骤中。

      Output <- Input %>% 
        mutate(PaymentDate = FromDate,
               RFrequency = case_when(Frequency == "Weekly" ~ '1 week',
                                      Frequency == "Fortnightly" ~ '2 weeks',
                                      Frequency == "Monthly" ~ '1 month')) %>% 
        group_by(CusID) %>% 
        complete(PaymentDate = seq.Date(FromDate,ToDate, by = RFrequency)) %>% 
        fill(PaymentDate,Amount) %>% 
        select(CusID, PaymentDate, Amount)
      

      【讨论】:

      • 有趣的混搭。只是好奇:是什么让这个解决方案成为您的理想之选?
      • 我喜欢将完成和填充分隔成两行——你可以通过分成两部分来查看每个步骤的作用。我不熟悉您提出的交叉方法,即使两者都是 tidyr 的一部分,我只是更喜欢完整/填充方法。与 R 中的许多东西一样,似乎有很多方法可以给猫剥皮 :-)
      • 我明白了。如果您更喜欢逐字逐句的解决方案,请查看我的更新。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-07-09
      • 1970-01-01
      • 2021-05-25
      • 2022-01-08
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多