使用开始日期和结束日期扩展按日期范围排列的行

50

考虑一个形式为数据框的数据结构

       idnum      start        end
1993.1    17 1993-01-01 1993-12-31
1993.2    17 1993-01-01 1993-12-31
1993.3    17 1993-01-01 1993-12-31

具有类型为Datestartend

 $ idnum : int  17 17 17 17 27 27
 $ start : Date, format: "1993-01-01" "1993-01-01" "1993-01-01" "1993-01-01" ...
 $ end   : Date, format: "1993-12-31" "1993-12-31" "1993-12-31" "1993-12-31" ...

我想创建一个新的数据框,每行都有每个月的观察值,包括startend之间的每个月(包括边界):

期望输出

idnum       month
   17  1993-01-01
   17  1993-02-01
   17  1993-03-01
...
   17  1993-11-01
   17  1993-12-01

我不确定month应该采用什么格式,我最终想要按idnummonth分组进行数据集其他部分的回归。

到目前为止,对于每一行数据,seq(from=test[1,'start'], to=test[1, 'end'], by='1 month')可以给出正确的序列 - 但是,一旦我尝试将其应用于整个数据框架,它就无法工作:

> foo <- apply(test, 1, function(x) seq(x['start'], to=x['end'], by='1 month'))
Error in to - from : non-numeric argument to binary operator

作为R的初学者,我应该如何判断答案?是否有一种像Python中的%timeit一样的方法来检查它们的效率? - FooBar
7个回答

47

使用 data.table

require(data.table) ## 1.9.2+
setDT(df)[ , list(idnum = idnum, month = seq(start, end, by = "month")), by = 1:nrow(df)]

# you may use dot notation as a shorthand alias of list in j:
setDT(df)[ , .(idnum = idnum, month = seq(start, end, by = "month")), by = 1:nrow(df)]

setDTdf 转换为一个 data.table。然后对于每一行,by = 1:nrow(df),我们根据要求创建 idnummonth


3
据我所知,最有效的答案是:如果我想在新数据框中列出一长串列而不仅仅是"idnum",有没有更加优雅的方法来实现呢?用colnames(df)替换idnum=idnum显然行不通。 - FooBar
在一个大约有40k条记录的小数据集上,这比dplyr :: rowwise()选项快25倍。 - Jacob
4
如何使用多列代替 idnum? - jeganathan velu
@jeganathanvelu 最好另开一个问题询问。 - Arun

27

使用dplyr

test %>%
    group_by(idnum) %>%
    summarize(start=min(start),end=max(end)) %>%
    do(data.frame(idnum=.$idnum, month=seq(.$start,.$end,by="1 month")))

请注意,此处我不为每一行生成startend之间的序列,而是为每个idnum生成min(start)max(end)之间的序列。如果您需要前者:

test %>%
    rowwise() %>%
    do(data.frame(idnum=.$idnum, month=seq(.$start,.$end,by="1 month")))

11

更新2

使用新版本的 purrr (0.3.0) 和 dplyr (0.8.0),可以使用 map2 来实现此操作。

library(dplyr)
library(purrr)
 test %>%
     # sequence of monthly dates for each corresponding start, end elements
     transmute(idnum, month = map2(start, end, seq, by = "1 month")) %>%
     # unnest the list column
     unnest %>% 
     # remove any duplicate rows
     distinct

已更新

根据@Ananda Mahto的评论

 res1 <- melt(setNames(lapply(1:nrow(test), function(x) seq(test[x, "start"],
 test[x, "end"], by = "1 month")), test$idnum))
同样,
  res2 <- setNames(do.call(`rbind`,
          with(test, 
          Map(`expand.grid`,idnum,
          Map(`seq`, start, end, by='1 month')))), c("idnum", "month"))


  head(res1)
 #  idnum      month
 #1    17 1993-01-01
 #2    17 1993-02-01
 #3    17 1993-03-01
 #4    17 1993-04-01
 #5    17 1993-05-01
 #6    17 1993-06-01

+1. 我已经使用 melt(setNames(lapply(1:nrow(test), function(x) seq(test[x, "start"], test[x, "end"], by = "1 month")), test$idnum)) 来避免不必要地调用 data.frame. - A5C1D2H2I1M1N2O1R2T1
如果所有这些方法都适用于我的R版本,我该如何选择?我是一个完全的新手...这些方法中有一些更好地适用于类似解决方案,或者是较新且不太可能被弃用的吗?是否有性能例程可以用来检查它们? - FooBar
@Ananda Mahto. 谢谢,我用你的代码替换了我的代码。 - akrun
@FooBar,挑选编程方法的原因各有不同,可能是个人偏好,也可能考虑到“六个月后我还能理解这段代码吗?”或者“我的数据规模有多大?”。使用“microbenchmark”包可以帮助你找出哪些方法在计算时间上最为高效。 - A5C1D2H2I1M1N2O1R2T1
@FooBar,对我来说,如果数据集相当大,通常基于dplyrdata.table的解决方案会更快。很难预测哪个会被弃用。 - akrun

8

使用dplyrtidyr为每一行创建一个序列的一个选项可能是:

df %>%
 rowwise() %>%
 transmute(idnum,
           date = list(seq(start, end, by = "month"))) %>%
 unnest(date)

  idnum date      
   <int> <date>    
 1    17 1993-01-01
 2    17 1993-02-01
 3    17 1993-03-01
 4    17 1993-04-01
 5    17 1993-05-01
 6    17 1993-06-01
 7    17 1993-07-01
 8    17 1993-08-01
 9    17 1993-09-01
10    17 1993-10-01
# … with 26 more rows

或者使用分组ID创建序列:

df %>%
 group_by(idnum) %>%
 transmute(date = list(seq(min(start), max(end), by = "month"))) %>%
 unnest(date)

当目标是仅为每个ID创建一个唯一序列时:

df %>%
 group_by(idnum) %>%
 summarise(start = min(start),
           end = max(end)) %>%
 transmute(date = list(seq(min(start), max(end), by = "month"))) %>%
 unnest(date)

   date      
   <date>    
 1 1993-01-01
 2 1993-02-01
 3 1993-03-01
 4 1993-04-01
 5 1993-05-01
 6 1993-06-01
 7 1993-07-01
 8 1993-08-01
 9 1993-09-01
10 1993-10-01
11 1993-11-01
12 1993-12-01

5

另一个与 tidyverse 相关的方法是使用 tidyr::expand

library(dplyr, warn = FALSE)
library(tidyr)

df |> 
  mutate(
    row = row_number()
  ) |> 
  group_by(row) |> 
  expand(idnum, date = seq(start, end, "month")) |> 
  ungroup() |> 
  select(-row)
#> # A tibble: 36 × 2
#>    idnum date      
#>    <int> <date>    
#>  1    17 1993-01-01
#>  2    17 1993-02-01
#>  3    17 1993-03-01
#>  4    17 1993-04-01
#>  5    17 1993-05-01
#>  6    17 1993-06-01
#>  7    17 1993-07-01
#>  8    17 1993-08-01
#>  9    17 1993-09-01
#> 10    17 1993-10-01
#> # … with 26 more rows

4

tidyverse的答案

数据

df <- structure(list(idnum = c(17L, 17L, 17L), start = structure(c(8401, 
8401, 8401), class = "Date"), end = structure(c(8765, 8765, 8765
), class = "Date")), class = "data.frame", .Names = c("idnum", 
"start", "end"), row.names = c(NA, -3L))

回答和输出

library(tidyverse)
df %>%
  nest(start, end) %>%
  mutate(data = map(data, ~seq(unique(.x$start), unique(.x$end), 1))) %>%
  unnest(data)

# # A tibble: 365 x 2
   # idnum       data
   # <int>     <date>
 # 1    17 1993-01-01
 # 2    17 1993-01-02
 # 3    17 1993-01-03
 # 4    17 1993-01-04
 # 5    17 1993-01-05
 # 6    17 1993-01-06
 # 7    17 1993-01-07
 # 8    17 1993-01-08
 # 9    17 1993-01-09
# 10    17 1993-01-10
# # ... with 355 more rows

dplyr版本0.7.4出现错误:每列必须是向量列表或数据框列表[data]。 - Hedgehog

0

使用lubridate进行月份计算的矢量化解决方案。

time_seq_v()seq()的矢量化版本,专门用于日期和日期时间计算。

library(lubridate)
library(data.table)
# remotes::install_github("NicChr/timeplyr")
library(timeplyr)
df <- data.frame(idnum = c(1993.1, 1993.2, 1993.3),
                 start = ymd(rep(19930101, 3)),
                 end = ymd(rep(19931231, 3)))
setDT(df)
df[, list(month = time_seq_v(start, end, by = "month"))]
#>          month
#>  1: 1993-01-01
#>  2: 1993-02-01
#>  3: 1993-03-01
#>  4: 1993-04-01
#>  5: 1993-05-01
#>  6: 1993-06-01
#>  7: 1993-07-01
#>  8: 1993-08-01
#>  9: 1993-09-01
#> 10: 1993-10-01
#> 11: 1993-11-01
#> 12: 1993-12-01
#> 13: 1993-01-01
#> 14: 1993-02-01
#> 15: 1993-03-01
#> 16: 1993-04-01
#> 17: 1993-05-01
#> 18: 1993-06-01
#> 19: 1993-07-01
#> 20: 1993-08-01
#> 21: 1993-09-01
#> 22: 1993-10-01
#> 23: 1993-11-01
#> 24: 1993-12-01
#> 25: 1993-01-01
#> 26: 1993-02-01
#> 27: 1993-03-01
#> 28: 1993-04-01
#> 29: 1993-05-01
#> 30: 1993-06-01
#> 31: 1993-07-01
#> 32: 1993-08-01
#> 33: 1993-09-01
#> 34: 1993-10-01
#> 35: 1993-11-01
#> 36: 1993-12-01
#>          month

使用reprex v2.0.2于2023年5月16日创建


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接