如何在 R 中将重叠时间段分成重叠和非重叠时间段

How to separate overlapping time periods into overlapping and non-overlapping periods in R

我正在寻找将重叠和非重叠周期与 'lubridate' 和 'dplyr' 包(或任何其他可以建议的包)结合起来。这是一个示例数据框:

vacation_start <- as_date('2017-04-19')
vacation_end <- as_date('2017-04-25')
course_start <- as_date('2017-04-12')
course_end <- as_date('2017-04-21')
course_interval <- interval(course_start, course_end)
vacation_interval <- interval(vacation_start, vacation_end)

df <- data.frame(id = "ID", part = c("A", "B"), 
start = c(course_start,vacation_start), 
end = c(course_end, vacation_end), 
interval = c(course_interval, vacation_interval))

数据框如下所示:

id part start end interval
ID A 2017-04-12 2017-04-21 2017-04-12 UTC--2017-04-21 UTC
ID B 2017-04-19 2017-04-25 2017-04-19 UTC--2017-04-25 UTC

我想像这样将它们组合成重叠和不重叠的时期,按 'ID' 和 'part' 分组:

id part start end interval
ID A 2017-04-12 2017-04-18 2017-04-12 UTC--2017-04-18 UTC
ID A,B 2017-04-19 2017-04-21 2017-04-19 UTC--2017-04-21 UTC
ID B 2017-04-22 2017-04-25 2017-04-22 UTC--2017-04-25 UTC

我试图生成具有重叠句点的中间行,但无法使用 'dplyr' 包保持非重叠句点:

df_2 <- df %>%
  group_by(id) %>%
  summarise(drug = paste(drug, collapse = ','),
            start = max(start),
            end = min(end), 
            interval = start %--% end)

非常感谢任何想法或解决方案。谢谢!

我建议分别创建重叠和非重叠。如果您希望输出行数大于输入行数,这通常是必需的。

对于重叠,我会做类似的事情:

overlap_df = df %>%
  inner_join(df, by = "id", suffix = c("_1","_2")) %>%
  filter(part_1 < part_2,
         start_1 <= end_2,
         start_2 <= end_1) %>%
  mutate(part = paste0(part_1,",",part_2), # new part label
         start = ifelse(start_1 < start_2, start_2, start_1), # latest start date
         end = ifelse(end_1 < end_2, end_1, end_2)) %>% # earliest end date
  select(ID, part, start, end)

第一个过滤条件确保每个重叠只有一个订单(例如只有 A,B 而不是 B,A。第二个和第三个过滤条件确保时间段重叠。

对于非重叠,我会区分从不重叠(与另一个时期没有任何重叠的时期)和不重叠(不重叠的时期部分)。

对于永不重叠的我会做这样的事情:

never_overlapped_df = df %>%
  left_join(df, by = "id", suffix = c("_1","_2")) %>%
  filter(part_1 != part_2) %>%
  mutate(overlap = ifelse(start_1 <= end_2 & start_2 <= end_2, 1, 0) %>%
  group_by(id, part_1, start_1, end_1) %>%
  summarise(num = sum(overlap, na.rm = TRUE)) %>%
  filter(is.na(num) | num == 0) %>%
  select(id, part = part_1, start = start_1, end = end_1)

想法是找到并计算所有的重叠,然后只保留没有任何重叠的记录。

对于非重叠我会做这样的事情:

non_overlapped_df = df %>%
  inner_join(df, by = "id", suffix = c("_1","_2")) %>%
  filter(part_1 != part_2,
         start_1 <= end_2,
         start_2 <= end_1) %>% # parts are different and periods overlap
  mutate(start_2 = ifelse(start_1 <= start_2 & start_2 <= end_1, start_2, NA),
         end_2 = ifelse(start_1 <= end_2 & end_2 <= end_1, end_2, NA)) %>%
  # discard start_2 & end_2 that are not within start_1 and end_1
  group_by(id, part_1, start_1, end_1) %>%
  summarise(min_start_2 = min(start_2, na.rm = TRUE),
            max_end_2 = max(end_2, na.rm = TRUE)) %>%
  mutate(start = ifelse(is.na(max_end_2), start_1, max_end_2),
         end = ifelse(is.na(min_start_2), end_1, min_start_2)) %>%
  select(id, part = part_1, start, end)

然后您可以将这些与 rbind 组合在一起:

output_df = rbind(overlap_df, never_overlapped_df, non_overlapped_df)

请注意,我假设一次最多有一个重叠(例如 part = "A,B,C" 不会发生)。这简化了问题。解决任意数量重叠的更一般情况要复杂得多,需要不同的方法。

请注意,您可能还想将“<=”更改为“<”或从结束日期减去 1 天以确保期间不会重叠。这取决于您如何处理时间段的边界条件。

我的第一个回答假设只有两个时期重叠。这意味着它可以为每个比较使用一个连接。尝试在超过两个时间段内重复此过程会导致连接数量增加,从而导致效率低下。

要处理连接任意(或未知)数量的重叠,我们需要一种非常不同的方法。因此,我将此作为单独的答案提供。

第 1 步:获取所有可能的开始日期和结束日期的列表

all_start = df %>%
  select(id, start)
all_end = df %>%
  select(id, start = end)
all_start_and_end = rbind(all_start, all_end) %>%
  distinct()

第 2 步:创建所有可能时期的列表

all_periods = all_start_and_end  %>%
  group_by(id) %>%
  mutate(end = lead(start, 1, order_by = start))

第 3 步:将原始数据与所有时期重叠并汇总

overlapped = all_periods %>%
  left_join(df, by = "id", suffix = c("_1","_2")) %>%
  filter(start_1 <= end_2,
         start_2 <= end_1) %>%
  select(id, part_2, start = start_1, end = end_1) %>%
  group_by(id, start, end) %>%
  summarise(part = toString(part_2))

取决于您的确切数据和情况:

  • 您可能需要将“<=”更改为“<”或从结束日期减去 1 天以确保期间不重叠。这取决于您如何处理时间段的边界条件。
  • 您可能需要删除第 1 步中的 distinct 以允许只有一天的时间段。
  • 在第 1 步中,如果您希望输出包含带有 part = NA.
  • 第三步完成后,您可能需要使用 part = NA.
  • 过滤掉所有句点
  • 根据您的输入数据,您可能会观察到具有相同 part 的相邻输出周期。例如。在第 1 行中:A 部分的结束日期为 2020-01-01,在第 2 行中:A 部分的开始日期为 2020-01-02。查看 gaps-and-islands 标签以了解解决此问题的方法。