找出向量中的每个元素属于数据框中哪个区间行。

12

我有一个数字元素的向量和一个包含两列的数据框,这两列定义了区间的起始点和终止点。数据框中的每一行都是一个区间。我想找出向量中的每个元素属于哪个区间。

以下是示例数据:

# Find which interval that each element of the vector belongs in

    library(tidyverse)
    elements <- c(0.1, 0.2, 0.5, 0.9, 1.1, 1.9, 2.1)

    intervals <-  frame_data(~phase, ~start, ~end,
                               "a",     0,     0.5,
                               "b",     1,     1.9,
                               "c",     2,     2.5)

对于那些反对 tidyverse 的人,这里提供同样的示例数据:

elements <- c(0.1, 0.2, 0.5, 0.9, 1.1, 1.9, 2.1)

intervals <- structure(list(phase = c("a", "b", "c"), 
                            start = c(0, 1, 2), 
                            end = c(0.5, 1.9, 2.5)), 
                       .Names = c("phase", "start", "end"), 
                       row.names = c(NA, -3L), 
                       class = "data.frame")

这是一种做法:

    library(intrval) 
    phases_for_elements <- 
    map(elements, ~.x %[]% data.frame(intervals[, c('start', 'end')])) %>% 
      map(., ~unlist(intervals[.x, 'phase'])) 

以下是输出结果:

    [[1]]
    phase 
      "a" 

    [[2]]
    phase 
      "a" 

    [[3]]
    phase 
      "a" 

    [[4]]
    character(0)

    [[5]]
    phase 
      "b" 

    [[6]]
    phase 
      "b" 

    [[7]]
    phase 
      "c" 

但是我正在寻找一种更简单的方法,打字更少。我在相关问题中看到了findInterval,但我不确定如何在这种情况下使用它。


4
@Ben 您创建示例数据的方法需要安装和加载大量软件包,这些软件包甚至会掩盖基本函数。dput仅需要基础R,非常方便(对于接收数据的人以及共享数据的人)。 - Roland
2
@Roland,看到我的更新了吗?但如果你觉得我的问题不方便,就不必费心了。 - Ben
7个回答

17

这里提供了一种使用 data.table (v>=1.9.8) 中的新的“non-equi”连接的可能解决方案。虽然我怀疑您可能不喜欢这种语法,但它应该是非常高效的解决方案。

此外,关于 findInterval ,该函数假定间隔连续,而在这种情况下并非如此,因此我怀疑使用它没有直接的解决方案。

library(data.table) #v1.10.0
setDT(intervals)[data.table(elements), on = .(start <= elements, end >= elements)]
#    phase start end
# 1:     a   0.1 0.1
# 2:     a   0.2 0.2
# 3:     a   0.5 0.5
# 4:    NA   0.9 0.9
# 5:     b   1.1 1.1
# 6:     b   1.9 1.9
# 7:     c   2.1 2.1

关于上述代码,我认为它相当易于理解:按照on操作符中指定的条件将intervalselements连接起来。基本上就是这样。

然而这里有一个特殊情况,即startendelements应该是相同类型的,因此如果其中一个是integer,它应首先转换为numeric


1
这真的很棒。基本上使我之前使用滚动连接和重叠等方式的做法无效了。 - thelatemail
相同但不同:d = data.table(elements); d[setDT(intervals), on = .(elements >= start, elements <= end), phase := phase]. - Henrik

5

cut 在这里可能会有用。

out <- cut(elements, t(intervals[c("start","end")]))
levels(out)[c(FALSE,TRUE)]  <- NA
intervals$phase[out]
#[1] "a" "a" "a" NA  "b" "b" "c"

5

受 @thelatemail 的 cut 解决方案的启发,这里提供一个使用findInterval的解决方案,虽然仍需要大量输入:

out <- findInterval(elements, t(intervals[c("start","end")]), left.open = TRUE)
out[!(out %% 2)] <- NA
intervals$phase[out %/% 2L + 1L]
#[1] "a" "a" "a" NA  "b" "b" "c"

注意 cutfindInterval 使用左开区间。因此,使用 cutfindInterval 的解决方案与Ben使用intrval、David使用data.table的非等值连接以及我的其他解决方案使用foverlaps不相等的


谢谢,我认为用cut函数的正确参数可以改变左开区间,是这样吗? - Ben
恐怕那只会改变方向。帮助文档说:“right 是一个逻辑值,指示间隔区间是否应该在右侧关闭(左侧开放)或反之。” - Uwe

5

David Arenburg提到的非等值连接对于理解这种一般性问题非常有帮助(谢谢!)。现在我可以看到它在dplyr中未实现。通过这个答案,我发现有一个fuzzyjoin包可以用相同的风格来完成它。但是,它几乎没有比我上面的map解决方案更简单(尽管在我看来更易读),也无法与thelatemail的cut答案相比简洁。

对于我上面的示例,fuzzyjoin解决方案将是

library(fuzzyjoin)
library(tidyverse)

fuzzy_left_join(data.frame(elements), intervals, 
                by = c("elements" = "start", "elements" = "end"), 
                match_fun = list(`>=`, `<=`)) %>% 
  distinct()

这将会给出:

    elements phase start end
1      0.1     a     0   0.5
2      0.2     a     0   0.5
3      0.5     a     0   0.5
4      0.9  <NA>    NA    NA
5      1.1     b     1   1.9
6      1.9     b     1   1.9
7      2.1     c     2   2.5

4

只需要使用lapply函数即可:

l <- lapply(elements, function(x){
    intervals$phase[x >= intervals$start & x <= intervals$end]
})

str(l)
## List of 7
##  $ : chr "a"
##  $ : chr "a"
##  $ : chr "a"
##  $ : chr(0) 
##  $ : chr "b"
##  $ : chr "b"
##  $ : chr "c"

或者在purrr中,如果你喜欢它。
elements %>% 
    map(~intervals$phase[.x >= intervals$start & .x <= intervals$end]) %>% 
    # Clean up a bit. Shorter, but less readable: map_chr(~.x[1] %||% NA)
    map_chr(~ifelse(length(.x) == 0, NA, .x))
## [1] "a" "a" "a" NA  "b" "b" "c"

3

这里有一种“单行代码”,它(误用)了data.table包中的foverlaps,但David的非等连接仍然更简洁:

library(data.table) #v1.10.0
foverlaps(data.table(start = elements, end = elements), 
          setDT(intervals, key = c("start", "end")))
#   phase start end i.start i.end
#1:     a     0 0.5     0.1   0.1
#2:     a     0 0.5     0.2   0.2
#3:     a     0 0.5     0.5   0.5
#4:    NA    NA  NA     0.9   0.9
#5:     b     1 1.9     1.1   1.1
#6:     b     1 1.9     1.9   1.9
#7:     c     2 2.5     2.1   2.1

好的,谢谢。我看到foverlaps受到了IRanges::findOverlaps的启发,我也用它来解决这个问题。甚至比我在问题中展示的更加棘手。 - Ben

2
为了完整起见,这里还有另一种方法,使用intervals包:
library(tidyverse)
elements <- c(0.1, 0.2, 0.5, 0.9, 1.1, 1.9, 2.1)

intervalsDF <- 
  frame_data(  ~phase, ~start, ~end,
               "a",     0,      0.5,
               "b",     1,      1.9,
               "c",     2,      2.5
  )

library(intervals)
library(rlist)

interval_overlap(
  Intervals(intervalsDF %>% select(-phase) %>% as.matrix, closed = c(TRUE, TRUE)),
  Intervals(data_frame(start = elements, end = elements), closed = c(TRUE, TRUE))
) %>% 
  list.map(data_frame(interval_index = .i, element_index = .)) %>% 
  do.call(what = bind_rows)

# A tibble: 6 × 2
#  interval_index element_index
#           <int>         <int>
#1              1             1
#2              1             2
#3              1             3
#4              2             5
#5              2             6
#6              3             7

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接