【问题标题】:How would you lookup the next-closest value?您将如何查找下一个最接近的值?
【发布时间】:2020-03-07 13:53:54
【问题描述】:

我有以下 2 个数据帧:

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 在同一天。如果没有日期满足该要求,则不适用。所以在这个例子中,理想的输出是:

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 实现

【问题讨论】:

  • 为什么投反对票?

标签: r dataframe data-manipulation zoo posixct


【解决方案1】:

1) Base R - sapply 这使用 base R。对于 data.dfdt 的每个组件,它会在同一日期在 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) Base R - 合并 这是一种替代的 base 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 解决方案,因为它提供了将问题特别直接地转换为代码的自联接复杂的情况。它对指定条件执行左连接,并采用从 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)

【讨论】:

  • 对引号感到抱歉。这是在 iPad 上写的。更新了问题。
  • as.Date(...) 在从 POSIXct 对象转换时遇到问题,因为它假定日期是 UTC。见这里:stackoverflow.com/questions/60329452/…
  • 已添加tz = ""
【解决方案2】:

为了完整起见,这里有一个使用data.table滚动连接的解决方案。

如果我理解正确,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 行显然违反了第一个条件。为了满足第一个条件,我们也必须在同一天匹配。这需要将Date 类型的day 列添加到两个数据框:

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 不会被修改。

【讨论】:

    【解决方案3】:

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

    【讨论】:

    • 会选择这个答案,但是当你有一个非常大的查找表时,这种方式太慢了。最终使用了@Grothendieck 的方法,因为它非常快。我很欣赏这个答案,因为它是最易读的!
    【解决方案4】:

    为了完整和完全的完整性,这里有一个带有 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
    

    【讨论】:

      猜你喜欢
      • 2021-02-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-08-12
      • 1970-01-01
      • 1970-01-01
      • 2013-03-01
      • 1970-01-01
      相关资源
      最近更新 更多