【问题标题】:Expand start and end dates into a sequence of beginning and ending dates by calendar month将开始日期和结束日期展开为按日历月排列的开始日期和结束日期序列
【发布时间】:2023-03-17 08:23:01
【问题描述】:

给定一张桌子

id   start          end
1   22/03/2016    05/06/2016
2   17/08/2016    29/08/2016
3   22/09/2017    25/12/2017

我正在尝试按日历月份进行拆分,如下表所示

id   start         end
1   22/03/2016    31/03/2016
1   01/04/2016    30/04/2016
1   01/05/2016    05/06/2016
2   17/08/2016    29/08/2016
3   22/09/2017    30/09/2017
3   01/10/2017    31/10/2017
3   01/11/2017    30/11/2017
3   01/12/2017    25/12/2017

我正在尝试修改来自 how to split rows of a dataframe in multiple rows based on start date and end date? 的代码提取,但我无法正确修改代码。问题通常在 30 天的几个月内,也许很容易,但我还不熟悉正则表达式。

#sample data
df <- data.frame("starting_date" = as.Date(c("2016-03-22", "2016-08-17", "2017-09-12")),
             "end_date" = as.Date(c("2016-06-05", "2016-08-29", "2017-12-25")),
             col3=c('1','2', '3'))

df1 <- df[,1:2] %>% 
rowwise() %>%
do(rbind(data.frame(matrix(as.character(c(
.$starting_date, 

seq(.$starting_date, .$end_date, by=1)[grep("\\d{4}-\\d{2}-31|\\d{4}-\\d{2}-01", seq(.$starting_date, .$end_date, by=1))],

.$end_date)), ncol=2, byrow=T))
  )
) %>%
data.frame() %>%
`colnames<-`(c("starting_date", "end_date")) %>%
mutate(starting_date= as.Date(starting_date, format= "%Y-%m-%d"),
     end_date= as.Date(end_date, format= "%Y-%m-%d"))

#add temporary columns to the original and expanded date column dataframes
df$row_idx <- seq(1:nrow(df))
df$temp_col <- (year(df$end_date) - year(df$starting_date)) +1
df1 <- cbind(df1,row_idx = rep(df$row_idx,df$temp_col))

#join both dataframes to get the final result
final_df <- left_join(df1,df[,3:(ncol(df)-1)],by="row_idx") %>%
  select(-row_idx) 
final_df

如果有人知道如何修改代码或更好的方法,我将不胜感激。

【问题讨论】:

    标签: r


    【解决方案1】:

    我们假设问题中的示例输出存在错误,因为第三行跨越两个月的部分时间,因此应分成两行。

    定义Seq,其中给定一个startend 日期变量生成startend 列的data.frame,然后使用group_by 在每个id 上运行它:

    library(dplyr)
    library(zoo)
    
    Seq <- function(start, end) {
      ym <- seq(as.yearmon(start), as.yearmon(end), 1/12)
      starts <- pmax(start, as.Date(ym, frac = 0))
      ends <- pmin(end, as.Date(ym, frac = 1))
      unique(data.frame(start = starts, end = ends))
    }
    
    fmt <- "%d/%m/%Y"
    DF %>%
      mutate(start = as.Date(start, fmt), end = as.Date(end, fmt)) %>%
      group_by(id) %>%
      do(Seq(.$start, .$end)) %>%
      ungroup
    

    给予:

    # A tibble: 9 x 3
         id start      end       
      <int> <date>     <date>    
    1     1 2016-03-22 2016-03-31
    2     1 2016-04-01 2016-04-30
    3     1 2016-05-01 2016-05-31
    4     1 2016-06-01 2016-06-05
    5     2 2016-08-17 2016-08-29
    6     3 2017-09-22 2017-09-30
    7     3 2017-10-01 2017-10-31
    8     3 2017-11-01 2017-11-30
    9     3 2017-12-01 2017-12-25
    

    注意

    输入 DF 以可重现的形式:

    Lines <- "
    id   start          end
    1   22/03/2016    05/06/2016
    2   17/08/2016    29/08/2016
    3   22/09/2017    25/12/2017"
    DF <- read.table(text = Lines, header = TRUE)
    

    【讨论】:

      【解决方案2】:

      所以可能有一种更优雅的方式来完成此任务,我觉得我已经看到了类似的问题,但无法快速找到重复项,所以这里......

      设置

      library(tidyverse)
      library(lubridate)
      
      df <- data.frame(
        id = c('1', '2', '3'),
        starting_date = as.Date(c("2016-03-22", "2016-08-17", "2017-09-12")),
        end_date = as.Date(c("2016-06-05", "2016-08-29", "2017-12-25")),
        stringsAsFactors = FALSE
      )
      
      df
      #>   id starting_date   end_date
      #> 1  1    2016-03-22 2016-06-05
      #> 2  2    2016-08-17 2016-08-29
      #> 3  3    2017-09-12 2017-12-25
      

      解决方案

      df %>%
        group_by(id) %>%
        mutate(
          date_seq = list(seq.Date(starting_date, end_date, by = "month") %>% ceiling_date("month") - 1)
        ) %>%
        unnest() %>%
        mutate(row = row_number()) %>%
        mutate(
          new_end_date = if_else(row == max(row), end_date, date_seq),
          new_start_date = if_else(row == min(row), starting_date, floor_date(new_end_date, "month"))
        ) %>% 
        select(
          id, new_start_date, new_end_date
        )
      #> # A tibble: 8 x 3
      #> # Groups:   id [3]
      #>   id    new_start_date new_end_date
      #>   <chr> <date>         <date>      
      #> 1 1     2016-03-22     2016-03-31  
      #> 2 1     2016-04-01     2016-04-30  
      #> 3 1     2016-06-01     2016-06-05  
      #> 4 2     2016-08-17     2016-08-29  
      #> 5 3     2017-09-12     2017-09-30  
      #> 6 3     2017-10-01     2017-10-31  
      #> 7 3     2017-11-01     2017-11-30  
      #> 8 3     2017-12-01     2017-12-25
      

      解释

      这里发生的大部分事情都发生在第一个mutate 调用中,该调用创建了date_seq。要理解它,请考虑以下几点:

      seq.Date(ymd("2016-03-22"), ymd("2016-06-05"), by = "month")
      # [1] "2016-03-22" "2016-04-22" "2016-05-22"
      
      seq.Date(ymd("2016-03-22"), ymd("2016-06-05"), by = "month") %>% 
        ceiling_date("month")
      # [1] "2016-04-01" "2016-05-01" "2016-06-01"
      
      seq.Date(ymd("2016-03-22"), ymd("2016-06-05"), by = "month") %>% 
        ceiling_date("month") - 1
      # [1] "2016-03-31" "2016-04-30" "2016-05-31"
      

      因此,基本上,在原始开始日期和结束日期之间创建一系列“月末”日期。将它放在列表列中允许我们按 id 进行组织,以便我们适当地unnest。检查unnest()结束后的输出:

      df %>%
        group_by(id) %>%
        mutate(
          date_seq = list(seq.Date(starting_date, end_date, by = "month") %>% ceiling_date("month") - 1)
        ) %>%
        unnest()
      

      从那里我希望事情相对简单。 row_number 可能已经被替换为像 first/last 这样更高级的东西,但我认为这可能更容易理解。

      【讨论】: