Как бы вы нашли ближайшее ближайшее значение?

У меня есть следующие 2 data.frames:

data.df <- data.frame(dt = as.POSIXct(c('2020-01-08 11:30:00', 
    '2020-01-10 11:30:00', '2020-01-11 12:30:00')), 
  v1=c(1,2,3))

lookup.df <- data.frame(ldt = as.POSIXct(c('2020-01-08 11:29:00', 
  '2020-01-08 11:30:00', '2020-01-08 11:31:00', '2020-01-10 10:30:00', 
  '2020-01-10 11:31:00', '2020-01-11 11:30:00', '2020-01-12 11:30:00')), 
   lv = 1:7)

Для каждой строки в data.df я хотел бы получить индекс строки в lookup.df (чтобы соответствовать строкам в слиянии), где lookup.df $ ldt> = data.df $ dt в тот же день. Если ни одна дата не соответствует этому требованию, тогда NA. Итак, в этом примере идеальным результатом будет:

dt                    |   v1   |  ldt                 |  lv
2020-01-08 11:30:00        1      2020-01-08 11:30:00     2
2020-01-10 11:30:00        2      2020-01-10 11:31:00     5
2020-01-11 12:30:00        3       NA                     NA

ПРИМЕЧАНИЕ: я бы предпочел базовую реализацию R или реализацию zoo.


person Denis    schedule 07.03.2020    source источник
comment
Почему отрицательные голоса?   -  person Denis    schedule 07.03.2020


Ответы (4)


1) Base R - sapply. Используется база R. Для каждого компонента dt в data.df он находит все даты, превышающие его в lookup.df на ту же дату, а затем возвращает индекс первого. Наконец, он объединяет data.df и строки этих индексов lookup.df.

ix <- sapply(data.df$dt, function(dt) with(lookup.df, 
  which(ldt >= dt & as.Date(ldt, tz = "") == as.Date(dt, tz = ""))[1]
))
res <- cbind(data.df, lookup.df[ix, ])
rownames(res) <- NULL

давая:

> res
                   dt v1                 ldt lv
1 2020-01-08 11:30:00  1 2020-01-08 11:30:00  2
2 2020-01-10 11:30:00  2 2020-01-10 11:31:00  5
3 2020-01-11 12:30:00  3                <NA> NA

2) База R - объединить. Это альтернативный подход к основанию R. Добавьте столбец даты в каждый фрейм входных данных, а затем объедините два по этому столбцу. Удалите любую строку, для которой дата / время lookup.df меньше даты / времени data.df, а затем возьмите первую строку каждого набора строк, полученных из той же исходной строки data.df. При этом будут получены совпадения, за исключением того, что будут пропущены строки, у которых вообще нет совпадений, поэтому выполните второе слияние, чтобы вернуть их.

data.df$date <- as.Date(data.df$dt, tz = "")
lookup.df$date <- as.Date(lookup.df$ldt, tz = "")

m <- merge(data.df, lookup.df, by = "date", all.x = TRUE, all.y = FALSE)
m <- subset(m, dt <= ldt)
m <- m[!duplicated(m[1:3]), ]
merge(data.df[-3], m[-1], by = c("dt", "v1"), all.x = TRUE, all.y = FALSE)

давая:

                   dt v1                 ldt lv
1 2020-01-08 11:30:00  1 2020-01-08 11:30:00  2
2 2020-01-10 11:30:00  2 2020-01-10 11:31:00  5
3 2020-01-11 12:30:00  3                <NA> NA

3) SQL. Хотя вопрос задан для базового решения R, здесь дополнительно было добавлено решение sql, поскольку оно обеспечивает особенно прямой перевод проблемы в код в виде самостоятельного соединения со сложным условием. Он выполняет левое соединение по указанному условию и берет минимум ldt, найденный по всем строкам, полученным из одной и той же строки в data.df.

library(sqldf)

data.df$date <- as.Date(data.df$dt, tz = "")
lookup.df$date <- as.Date(lookup.df$ldt, tz = "")

sqldf("select D.dt, D.v1, min(L.ldt) as ldt, L.lv
  from [data.df] D left join [lookup.df] L
  on D.dt <= L.ldt and D.date == L.date
  group by D.rowid")

давая:

                   dt v1                 ldt lv
1 2020-01-08 11:30:00  1 2020-01-08 11:30:00  2
2 2020-01-10 11:30:00  2 2020-01-10 11:31:00  5
3 2020-01-11 12:30:00  3                <NA> NA

Примечание

В вопросе была проблема с причудливыми цитатами, которые R не может прочитать, поэтому мы использовали это в качестве ввода:

data.df <- data.frame(dt = as.POSIXct(c('2020-01-08 11:30:00', 
    '2020-01-10 11:30:00', '2020-01-11 12:30:00')), 
  v1=c(1,2,3))

lookup.df <- data.frame(ldt = as.POSIXct(c('2020-01-08 11:29:00', 
  '2020-01-08 11:30:00', '2020-01-08 11:31:00', '2020-01-10 10:30:00', 
  '2020-01-10 11:31:00', '2020-01-11 11:30:00', '2020-01-12 11:30:00')), 
   lv = 1:7)
person G. Grothendieck    schedule 07.03.2020
comment
Извините за цитаты. Написал это на айпаде. Обновил вопрос. - person Denis; 07.03.2020
comment
as.Date (...) имеет проблемы с преобразованием из объектов POSIXct, поскольку предполагает, что дата указана в формате UTC. См. Здесь: stackoverflow.com/questions/60329452/ - person Denis; 07.03.2020
comment
Добавили tz = "" - person G. Grothendieck; 07.03.2020

Для полноты, вот решение, в котором используется скользящее соединение data.table.

Если я правильно понял, ОП ищет совпадения

  1. в тот же день и
  2. на первой отметке времени, обнаруженной в lookup.df после отметки времени, указанной в `data.df

Второе условие достигается простым скользящим соединением:

library(data.table)
setDT(lookup.df)[setDT(data.df), on = .(ldt = dt), .(dt, v1, ldt = x.ldt, lv), roll = -Inf]
                    dt v1                 ldt lv
1: 2020-01-08 11:30:00  1 2020-01-08 11:30:00  2
2: 2020-01-10 11:30:00  2 2020-01-10 11:31:00  5
3: 2020-01-11 12:30:00  3 2020-01-12 11:30:00  7

Однако очевидно, что первое условие для строки 3 нарушено. Чтобы выполнить первое условие, мы также должны провести матч в тот же день. Для этого необходимо добавить столбец day типа Date в оба фрейма данных:

library(data.table)
setDT(lookup.df)[, .(ldt, lv, day = as.IDate(ldt))][
  setDT(data.df)[, .(dt, v1, day = as.IDate(dt))], 
  on = .(day, ldt = dt), .(dt, v1, ldt = x.ldt, lv), roll = -Inf]
                    dt v1                 ldt lv
1: 2020-01-08 11:30:00  1 2020-01-08 11:30:00  2
2: 2020-01-10 11:30:00  2 2020-01-10 11:31:00  5
3: 2020-01-11 12:30:00  3                <NA> NA

Обратите внимание, что data.df и lookup.df не изменяются.

person Uwe    schedule 07.03.2020

Предполагая, что время поиска упорядочено, в базе R вы можете:

lv <- sapply(data.df$dt, function(x){
  which(substr(lookup.df$ldt, 1, 10) == substr(x, 1, 10) & lookup.df$ldt >= x)[1]
})

cbind(data.df, lookup.df[lv,])
#>                     dt v1                 ldt lv
#> 2  2020-01-08 11:30:00  1 2020-01-08 11:30:00  2
#> 5  2020-01-10 11:30:00  2 2020-01-10 11:31:00  5
#> NA 2020-01-11 12:30:00  3                <NA> NA

Если вы не против использовать lubridate, вы можете использовать date() вместо substr()

person Allan Cameron    schedule 07.03.2020
comment
Выбрал бы этот ответ, но этот способ настолько медленный, когда у вас очень большая таблица поиска. Закончилось использованием метода @ Grothendieck, поскольку он довольно быстрый. Я ценю ответ, так как он наиболее читаемый! - person Denis; 08.03.2020

И для полноты и абсолютной полноты, вот версия dplyr с оттенком fuzzyjoin:

library(fuzzyjoin)
library(dplyr)

fuzzy_left_join(data.df, lookup.df, by = c("day" = "day", "dt" = "ldt"), 

                match_fun = list(`==`, `<=`)) %>%
    select(-c(day.x, day.y)) %>%
    group_by(v1) %>% slice(1)

  dt                     v1 ldt                    lv
  <dttm>              <dbl> <dttm>              <int>
1 2020-01-08 11:30:00     1 2020-01-08 11:30:00     2
2 2020-01-10 11:30:00     2 2020-01-10 11:31:00     5
3 2020-01-11 12:30:00     3 NA                     NA
person Edward    schedule 08.03.2020