您将如何查找下一个最接近的值?

How would you lookup the next-closest value?

我有以下 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

NOTE: I would prefer a base R implementation or a zoo implementation

假设您的查找时间是有序的,在 base 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()

1) Base R - sapply 这使用 base R。对于 data.dfdt 的每个组件,它会在 lookup.df 在同一天然后 returns 第一个索引。最后,它将 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) Base R - merge 这是另一种 base R 方法。将日期列附加到每个输入数据框,然后按该列合并两者。删除 lookup.df date/time 小于 data.df date/time 的任何行,然后取从同一原始 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 解决方案,因为它提供了一个特别直接的解决方案将问题向前转换为代码,作为具有复杂条件的自连接。它在指定条件下执行左连接,并采用从 data.df.

中同一行派生的所有行中找到的最小值 ldt
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)

为了完整起见,这是一个使用 data.tablerolling join.

的解决方案

如果我没理解错的话,OP 正在寻找匹配项

  1. 同一天
  2. lookup.df 中遇到的第一个时间戳上或之后 在`data.df
  3. 中给出

第二个条件可以通过简单的rolling join来实现:

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行显然违反了第一个条件。为了满足第一个条件,我们也必须在同一天匹配。这需要向两个数据框添加类型为 Dateday 列:

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.dflookup.df 未修改。

为了完全完整,这里有一个带有 fuzzyjoin 风格的 dplyr 版本:

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