Как сгладить / объединить перекрывающиеся периоды времени

У меня есть большой набор данных о периодах времени, определенных столбцами «начало» и «конец». Некоторые периоды совпадают.

Я хотел бы объединить (сгладить / объединить / свернуть) все перекрывающиеся периоды времени, чтобы иметь одно «начальное» значение и одно «конечное» значение.

Некоторые примеры данных:

  ID      start        end
1  A 2013-01-01 2013-01-05
2  A 2013-01-01 2013-01-05
3  A 2013-01-02 2013-01-03
4  A 2013-01-04 2013-01-06
5  A 2013-01-07 2013-01-09
6  A 2013-01-08 2013-01-11
7  A 2013-01-12 2013-01-15

Желаемый результат:

  ID      start        end
1  A 2013-01-01 2013-01-06
2  A 2013-01-07 2013-01-11
3  A 2013-01-12 2013-01-15

Что я пробовал:

  require(dplyr)
  data <- structure(list(ID = structure(c(1L, 1L, 1L, 1L, 1L, 1L, 1L), class = "factor", .Label = "A"), 
    start = structure(c(1356998400, 1356998400, 1357084800, 1357257600, 
    1357516800, 1357603200, 1357948800), tzone = "UTC", class = c("POSIXct", 
    "POSIXt")), end = structure(c(1357344000, 1357344000, 1357171200, 
    1357430400, 1357689600, 1357862400, 1358208000), tzone = "UTC", class = c("POSIXct", 
    "POSIXt"))), .Names = c("ID", "start", "end"), row.names = c(NA, 
-7L), class = "data.frame")

remove.overlaps <- function(data){
data2 <- data
for ( i in 1:length(unique(data$start))) {
x3 <- filter(data2, start>=data$start[i] & start<=data$end[i])
x4 <- x3[1,]
x4$end <- max(x3$end)
data2 <- filter(data2, start<data$start[i] | start>data$end[i])
data2 <- rbind(data2,x4)  
}
data2 <- na.omit(data2)}

data <- remove.overlaps(data)

person Jonno Bourne    schedule 09.03.2015    source источник


Ответы (4)


Вот возможное решение. Основная идея здесь состоит в том, чтобы сравнить отложенную start дату с максимальной датой окончания «до сих пор» с помощью функции cummax и создать индекс, который разделит данные на группы.

data %>%
  arrange(ID, start) %>% # as suggested by @Jonno in case the data is unsorted
  group_by(ID) %>%
  mutate(indx = c(0, cumsum(as.numeric(lead(start)) >
                     cummax(as.numeric(end)))[-n()])) %>%
  group_by(ID, indx) %>%
  summarise(start = first(start), end = last(end))

# Source: local data frame [3 x 4]
# Groups: ID
# 
#   ID indx      start        end
# 1  A    0 2013-01-01 2013-01-06
# 2  A    1 2013-01-07 2013-01-11
# 3  A    2 2013-01-12 2013-01-15
person David Arenburg    schedule 09.03.2015
comment
Спасибо за такой отличный ответ! Вопрос, однако, когда я использовал функцию на реальном наборе данных, даты в конечном итоге сохранялись во втором формате, мне пришлось обернуть итоговые переменные в as.POSIXct (), чтобы преобразовать их обратно, какие-либо идеи, почему? - person Jonno Bourne; 10.03.2015
comment
Не уверен, что вы имеете в виду ... Когда я сохраняю результат в какой-то переменной, оба start и end относятся к классу _3 _... - person David Arenburg; 10.03.2015
comment
Кстати, если вы используете несколько идентификаторов, вам нужно упорядочить их по порядку (данные, идентификатор, начало), поскольку на задержку не влияет группировка, и поэтому могут быть взяты даты за пределами группы идентификаторов, испортив окончательную структуру. Это не было частью вопроса, но я узнал послесловие на собственном горьком опыте. - person Jonno Bourne; 22.05.2015
comment
Что делает [-n()]? Я смог приспособить это к своим собственным потребностям (аналогичная ситуация, но с допуском в 90 дней между датами, которые все еще считаются перекрывающимися), но мне пришлось скопировать [-n()] дословно, не понимая, что он делает. - person Dannid; 02.02.2019
comment
Ага! Я понял. (он удаляет последний элемент в cumsum, чтобы разместить добавленный 0 в начале вектора.) - person Dannid; 02.02.2019
comment
@DavidArenburg Я думаю, ваше решение предполагает, что данные упорядочены по дате начала. Если вы измените порядок строк, результат также может измениться. Поэтому я предлагаю добавить аранжировку (ID, начало) после того, как группировка будет выполнена. - person peer; 02.05.2019
comment
@peer, да, это уже упоминалось в комментариях выше и других ответах. - person David Arenburg; 02.05.2019
comment
Следуя комментарию @Dannid, как можно адаптировать этот код, чтобы разрешить касание или слияние очень близких интервалов, то есть, скажем, 5-дневный промежуток? - person Fred-LM; 28.08.2019

Ответ @David Arenburg отличный, но я столкнулся с проблемой, когда более ранний интервал заканчивался после более позднего интервала, но использование last в вызове summarise привело к неправильной дате окончания. Я предлагаю заменить first(start) и last(end) на min(start) и max(end)

data %>%
  group_by(ID) %>%
  mutate(indx = c(0, cumsum(as.numeric(lead(start)) >
                     cummax(as.numeric(end)))[-n()])) %>%
  group_by(ID, indx) %>%
  summarise(start = min(start), end = max(end))

Кроме того, как упомянул @Jonno Bourne, сортировка по start и любым группирующим переменным важна перед применением метода.

person zack    schedule 16.11.2017

Для полноты картины пакет IRanges на Bioconductor имеет несколько полезных функций. который можно использовать для работы с датами или временными диапазонами. Одна из них - функция reduce(), которая объединяет перекрывающиеся или смежные диапазоны.

Однако есть недостаток, потому что IRanges работает с целочисленными диапазонами (отсюда и название), поэтому удобство использования функций IRanges достигается за счет преобразования объектов Date или POSIXct туда и обратно.

Кроме того, кажется, что dplyr не очень хорошо работает с IRanges (по крайней мере, если судить по моему ограниченному опыту работы с dplyr), поэтому я использую data.table:

library(data.table)
options(datatable.print.class = TRUE)
library(IRanges)
library(lubridate)

setDT(data)[, {
  ir <- reduce(IRanges(as.numeric(start), as.numeric(end)))
  .(start = as_datetime(start(ir)), end = as_datetime(end(ir)))
}, by = ID]
       ID      start        end
   <fctr>     <POSc>     <POSc>
1:      A 2013-01-01 2013-01-06
2:      A 2013-01-07 2013-01-11
3:      A 2013-01-12 2013-01-15

Вариант кода

setDT(data)[, as.data.table(reduce(IRanges(as.numeric(start), as.numeric(end))))[
  , lapply(.SD, as_datetime), .SDcols = -"width"], 
  by = ID]

В обоих вариантах используется as_datetime() из пакетов lubridate, который позволяет указать источник при преобразовании чисел в POSIXct объекты.

Было бы интересно увидеть эталонное сравнение подходов IRanges с ответом Дэвида.

person Uwe    schedule 20.11.2017
comment
Помимо сворачивания строк с перекрывающимися интервалами, если бы я также хотел взять минимальное значение другого столбца, как мы можем это сделать? например data <- structure(list(ID = structure(c(1L, 1L, 1L, 1L), .Label = "A", class = "factor"), start = structure(c(15706, 15706, 15707, 15709), class = "Date"), end = structure(c(15710, 15710, 15708, 15711), class = "Date"), value = c(3L, 7L, 8L, 5L)), class = "data.frame", row.names = c(NA, -4L)), тогда столбец value дает 3. - person HNSKD; 03.06.2020
comment
@HNSKD, это следует опубликовать как отдельный вопрос с собственным минимальным воспроизводимым примером, пожалуйста. Но быстрый ответ: library(data.table); setDT(data)[order(start, end), grp := cumsum(cummax(shift(as.numeric(end), fill = 0)) < as.numeric(start))][, .(start = min(start), end = max(end), value = min(value)), by = grp] - person Uwe; 03.06.2020

Похоже, я немного опоздал на вечеринку, но я взял код @zach и переписал его, используя data.table ниже. Я не проводил всестороннего тестирования, но он работал примерно на 20% быстрее, чем версия tidy. (Я не смог протестировать метод IRange, потому что пакет еще не доступен для R 3.5.1)

Кроме того, принятый ответ не учитывает крайний случай, когда один диапазон дат полностью находится в пределах другого (например, от 2018-07-07 до 2017-07-14 находится в пределах от 2018-05-01 до 2018-12-01). Ответ @zach действительно отражает этот крайний случай.

library(data.table)

start_col = c("2018-01-01","2018-03-01","2018-03-10","2018-03-20","2018-04-10","2018-05-01","2018-05-05","2018-05-10","2018-07-07")
end_col = c("2018-01-21","2018-03-21","2018-03-31","2018-04-09","2018-04-30","2018-05-21","2018-05-26","2018-05-30","2018-07-14")

# create fake data, double it, add ID
# change row 17, such that each ID grouping is a little different
# also adds an edge case in which one date range is totally within another
# (this is the edge case not currently captured by the accepted answer)
d <- data.table(start_col = as.Date(start_col), end_col = as.Date(end_col))
d2<- rbind(d,d)
d2[1:(.N/2), ID := 1]
d2[(.N/2 +1):.N, ID := 2]
d2[17,end_col := as.Date('2018-12-01')]

# set keys (also orders)
setkey(d2, ID, start_col, end_col)

# get rid of overlapping transactions and do the date math
squished <- d2[,.(START_DT = start_col, 
                  END_DT = end_col, 
                  indx = c(0, cumsum(as.numeric(lead(start_col)) > cummax(as.numeric(end_col)))[-.N])),
               keyby=ID
               ][,.(start=min(START_DT), 
                    end = max(END_DT)),
                 by=c("ID","indx")
                 ]
person enmyj    schedule 21.12.2018