Эффективный способ фильтрации одного фрейма данных по диапазонам в другом

Скажем, у меня есть кадр данных, содержащий кучу данных и столбец даты/времени, указывающий, когда была собрана каждая точка данных. У меня есть другой фрейм данных, в котором перечислены промежутки времени, где столбец "Старт" указывает дату/время начала каждого интервала, а столбец "Конец" указывает дату/время окончания каждого диапазона.

Я создал фиктивный пример ниже, используя упрощенные данные:

main_data = data.frame(Day=c(1:30))

spans_to_filter = 
    data.frame(Span_number = c(1:6),
               Start = c(2,7,1,15,12,23),
               End = c(5,10,4,18,15,26))

Я поиграл с несколькими путями решения этой проблемы и в итоге получил следующее решение:

require(dplyr)    
filtered.main_data =
    main_data %>% 
    rowwise() %>% 
    mutate(present = any(Day >= spans_to_filter$Start & Day <= spans_to_filter$End)) %>% 
    filter(present) %>% 
    data.frame()

Это работает отлично, но я заметил, что может потребоваться некоторое время для обработки, если у меня много данных (я предполагаю, что я выполняю сравнение по ряду). Я все еще изучаю все возможности R, и мне было интересно, есть ли более эффективный способ выполнения этой операции, предпочтительно используя dplyr/tidyr?

Ответ 1

Здесь функция, которую вы можете запустить в dplyr, чтобы найти даты в заданном диапазоне с помощью функции between (от dplyr). Для каждого значения Day, mapply выполняется between для каждой из пар дат Start и End, а функция использует rowSums для возврата TRUE, если Day находится между по крайней мере одним из них. Я не уверен, что это самый эффективный подход, но это приводит к почти четвертому улучшению скорости.

test.overlap = function(vals) {
  rowSums(mapply(function(a,b) between(vals, a, b), 
                 spans_to_filter$Start, spans_to_filter$End)) > 0
}

main_data %>% 
  filter(test.overlap(Day))

Если вы работаете с датами (а не с датами), может быть еще эффективнее создать вектор конкретных дат и проверить членство (это может быть лучшим подходом даже с датами):

filt.vals = as.vector(apply(spans_to_filter, 1, function(a) a["Start"]:a["End"]))

main_data %>% 
  filter(Day %in% filt.vals)

Теперь сравните скорости выполнения. Я сократил ваш код, чтобы потребовать только операцию фильтрации:

library(microbenchmark)

microbenchmark(
  OP=main_data %>% 
    rowwise() %>% 
    filter(any(Day >= spans_to_filter$Start & Day <= spans_to_filter$End)),
  eipi10 = main_data %>% 
    filter(test.overlap(Day)),
  eipi10_2 = main_data %>% 
    filter(Day %in% filt.vals)
  )

Unit: microseconds
     expr      min       lq      mean    median       uq      max neval cld
       OP 2496.019 2618.994 2875.0402 2701.8810 2954.774 4741.481   100   c
   eipi10  658.941  686.933  782.8840  714.4440  770.679 2474.941   100  b 
 eipi10_2  579.338  601.355  655.1451  619.2595  672.535 1032.145   100 a   

ОБНОВЛЕНИЕ: Ниже приведен тест с гораздо большим фреймом данных и несколькими дополнительными диапазонами дат, чтобы они соответствовали (спасибо @Frank за то, что вы предлагаете это в своем комментарии). Оказывается, что в этом случае прирост скорости намного больше (примерно в 200 раз для метода mapply/between и еще гораздо больше для второго метода).

main_data = data.frame(Day=c(1:100000))

spans_to_filter = 
  data.frame(Span_number = c(1:9),
             Start = c(2,7,1,15,12,23,90,9000,50000),
             End = c(5,10,4,18,15,26,100,9100,50100))

microbenchmark(
  OP=main_data %>% 
    rowwise() %>% 
    filter(any(Day >= spans_to_filter$Start & Day <= spans_to_filter$End)),
  eipi10 = main_data %>% 
    filter(test.overlap(Day)),
  eipi10_2 = {
    filt.vals = unlist(apply(spans_to_filter, 1, function(a) a["Start"]:a["End"]))
    main_data %>% 
      filter(Day %in% filt.vals)}, 
  times=10
  )

Unit: milliseconds
     expr         min          lq        mean      median          uq         max neval cld
       OP 5130.903866 5137.847177 5201.989501 5216.840039 5246.961077 5276.856648    10   b
   eipi10   24.209111   25.434856   29.526571   26.455813   32.051920   48.277326    10  a 
 eipi10_2    2.505509    2.618668    4.037414    2.892234    6.222845    8.266612    10  a 

Ответ 2

В пакете data.table, начиная с версии 1.9.8, были реализованы соединения без привязки. При этом я создал функцию-обертку inrange() для таких операций, где задача включает в себя поиск, если точка лежит в любом из предоставленных интервалов, и если так возвращаются TRUE, else FALSE.

require(data.table) # v>=1.9.8
setDT(main_data)[Day %inrange% spans_to_filter[, 2:3]] # inclusive bounds
#     Day
#  1:   1
#  2:   2
#  3:   3
#  4:   4
#  5:   5
#  6:   7
#  7:   8
#  8:   9
#  9:  10
# 10:  12
# 11:  13
# 12:  14
# 13:  15
# 14:  16
# 15:  17
# 16:  18
# 17:  23
# 18:  24
# 19:  25
# 20:  26

Подробнее см. ?inrange.

Ответ 3

Использование Base R:

main_data[unlist(lapply(main_data$Day, 
  function(x) any(x >= spans_to_filter$Start & x <= spans_to_filter$End))),]