【问题标题】:Fast subset/lookup/filter in large datasets大型数据集中的快速子集/查找/过滤器
【发布时间】:2017-10-24 17:34:49
【问题描述】:

我需要在包含因子和数字列的大型(许多 GB)表中反复查找“最近”行。使用dplyr,它看起来像这样:

df <- data.frame(factorA = rep(letters[1:3], 100000),
             factorB = sample(rep(letters[1:3], 100000), 
                              3*100000, replace = FALSE),
             numC = round(rnorm(3*100000), 2),
             numD = round(rnorm(3*100000), 2))

closest <- function(ValueA, ValueB, ValueC, ValueD) {
  df_sub <- df %>%
    filter(factorA == ValueA,
           factorB == ValueB,
           numC >= 0.9 * ValueC,
           numC <= 1.1 * ValueC,
           numD >= 0.9 * ValueD,
           numD <= 1.1 * ValueD)

  if (nrow(df_sub) == 0) stop("Oh-oh, no candidates.")

  minC <- df_sub[which.min(abs(df_sub$numC - ValueC)), "numC"]

  df_sub %>%
    filter(numC == minC) %>%
    slice(which.min(abs(numD - ValueD))) %>%
    as.list() %>%
    return()
}

这是上面的一个基准:

> microbenchmark(closest("a", "b", 0.5, 0.6))
Unit: milliseconds
                        expr      min       lq     mean   median       uq      max neval
 closest("a", "b", 0.5, 0.6) 25.20927 28.90623 35.16863 34.59485 35.25468 108.3489   100

优化此功能以提高速度的最佳方法是什么?即使内存中有很大的df,也有空闲的RAM,但考虑到对这个函数的多次调用,我希望它尽可能快。

使用data.table 代替dplyr 会有帮助吗?


这是我迄今为止尝试过的两个优化:

dt <- as.data.table(df)

closest2 <- function(ValueA, ValueB, ValueC, ValueD) {
  df_sub <- df %>%
    filter(factorA == ValueA,
           factorB == ValueB,
           dplyr::between(numC, 0.9 * ValueC, 1.1 * ValueC),
           dplyr::between(numD, 0.9 * ValueD, 1.1 * ValueD))

  if (nrow(df_sub) == 0) stop("Oh-oh, no candidates.")

  minC <- df_sub[which.min(abs(df_sub$numC - ValueC)), "numC"]

  df_sub %>%
    filter(numC == minC) %>%
    slice(which.min(abs(numD - ValueD))) %>%
    as.list() %>%
    return()
}

closest3 <- function(ValueA, ValueB, ValueC, ValueD) {

  dt_sub <- dt[factorA == ValueA & 
                 factorB == ValueB & 
                 numC %between% c(0.9 * ValueC, 1.1 * ValueC) &
                 numD %between% c(0.9 * ValueD, 1.1 * ValueD)]

  if (nrow(dt_sub) == 0) stop("Oh-oh, no candidates.")

  dt_sub[abs(numC - ValueC) == min(abs(numC - ValueC))][which.min(abs(numD - ValueD))] %>%
    as.list() %>%
    return()
}

基准测试:

> microbenchmark(closest("a", "b", 0.5, 0.6), closest2("a", "b", 0.5, 0.6), closest3("a", "b", 0.5, 0.6))
Unit: milliseconds
                         expr      min       lq     mean   median       uq       max neval cld
  closest("a", "b", 0.5, 0.6) 25.15780 25.62904 36.52022 34.68219 35.27116 155.31924   100   c
 closest2("a", "b", 0.5, 0.6) 22.14465 22.46490 27.81361 31.40918 32.04427  35.79021   100  b 
 closest3("a", "b", 0.5, 0.6) 13.52094 13.77555 20.04284 22.70408 23.41452 142.73626   100 a  

这可以进一步优化吗?

【问题讨论】:

  • 如何获取 C 和 D 的顺序索引并使用二进制搜索?
  • 怎么样?我试过setkey(dt, numC, numD),但似乎没有什么不同。
  • 如果您可以并行查找这些(使用 ValueA、ValueB、ValueC、ValueD 的向量而不是单个值),我想您将能够比顺序查找快得多(即显然,您打算如何“重复”执行此操作,因为您正在以这种方式进行基准测试)。
  • 感谢@Frank 的建议。我只能在有限的范围内并行化(例如,不是调用函数一百万次,而是使用长度为 5 的值向量调用它 200,000 次)。鉴于此,并行化会有所帮助吗?

标签: r performance dplyr data.table subset


【解决方案1】:

如果您可以并行而不是顺序调用许多值元组...

set.seed(1)
DF <- data.frame(factorA = rep(letters[1:3], 100000),
             factorB = sample(rep(letters[1:3], 100000), 
                              3*100000, replace = FALSE),
             numC = round(rnorm(3*100000), 2),
             numD = round(rnorm(3*100000), 2))

library(data.table)
DT = data.table(DF)

f = function(vA, vB, nC, nD, dat = DT){

  rs <- dat[.(vA, vB, nC), on=.(factorA, factorB, numC), roll="nearest",
    .(g = .GRP, r = .I, numD), by=.EACHI][.(seq_along(vA), nD), on=.(g, numD), roll="nearest", mult="first", 
    r]

  df[rs]
}

# example usage
mDT = data.table(vA = c("a", "b"), vB = c("c", "c"), nC = c(.3, .5), nD = c(.6, .8))

mDT[, do.call(f, .SD)]

#    factorA factorB numC numD
# 1:       a       c  0.3 0.60
# 2:       b       c  0.5 0.76

与必须按行运行的其他解决方案相比...

# check the results match
library(magrittr)
dt = copy(DT)
mDT[, closest3(vA, vB, nC, nD), by=.(mr = seq_len(nrow(mDT)))]

#    mr factorA factorB numC numD
# 1:  1       a       c  0.3 0.60
# 2:  2       b       c  0.5 0.76

# check speed for a larger number of comparisons

nr = 100
system.time( mDT[rep(1:2, each=nr), do.call(f, .SD)] )
#    user  system elapsed 
#    0.07    0.00    0.06 

system.time( mDT[rep(1:2, each=nr), closest3(vA, vB, nC, nD), by=.(mr = seq_len(nr*nrow(mDT)))] )
#    user  system elapsed 
#   10.65    2.30   12.60 

它是如何工作的

对于.(vA, vB, nC) 中的每个元组,我们查找与vAvB 完全匹配的行,然后“滚动”到最接近的nC 值——这与OP 的规则不太匹配(在 nC*[0.9, 1.1]) 的范围内查看,但该规则可以很容易地在事后应用。对于每个匹配,我们获取元组的“组号”、.GRP、匹配的行号以及这些行上的 numD 的值。

然后我们加入组号和nD,在前者上完全匹配,在后者上滚动到最近。如果有多个最接近的匹配,我们使用mult="first" 取第一个。

然后我们可以获取每个元组匹配的行号并在原始表中查找。

性能

因此,与 R 一样,矢量化解决方案似乎具有很大的性能优势。

如果您一次只能传递约 5 个元组(至于 OP)而不是 200 个,那么这种方法与 which.min 和类似的方法相比仍然可能有好处,这要归功于二进制搜索,如 @F.Privé建议在评论中。

正如@HarlanNelson 的回答中所述,向表中添加索引可能会进一步提高性能。查看他的回答和?setindex

修复 numC 滚动到一个值

感谢 OP 发现这个问题:

DT2 = data.table(id = "A", numC = rep(c(1.01,1.02), each=5), numD = seq(.01,.1,.01))
DT2[.("A", 1.011), on=.(id, numC), roll="nearest"]
#    id  numC numD
# 1:  A 1.011 0.05

在这里,我们看到一排,但我们应该看到五排。一种解决方法(虽然我不确定为什么)是转换为整数:

DT3 = copy(DT2)
DT3[, numC := as.integer(numC*100)]
DT3[, numD := as.integer(numD*100)]
DT3[.("A", 101.1), on=.(id, numC), roll="nearest"]
#    id numC numD
# 1:  A  101    1
# 2:  A  101    2
# 3:  A  101    3
# 4:  A  101    4
# 5:  A  101    5

【讨论】:

  • 非常好!你能解释一下c(.(g = .GRP, r = .I), .SD), by=.EACHI]吗?恐怕我的data.table 知识非常有限(与tidyverse 相比)。
  • @Victor 谢谢,我现在已经简化了该部分并添加了解释。我没有意识到我在那里不需要 .SD,并解释了 .GRP 和 .I 是什么。如果还有什么不清楚的,请告诉我
  • 我实现了你的建议,确实很快。但是,它有一个不受欢迎的行为:如果滚动最后一个值(在这种情况下为 numC),即使有多个行的“滚动到”值为 numC,也只会返回一个匹配项。这打破了numD 的第二个滚动连接:之前选择的单个值可以使numDnD 相距甚远。有什么想法吗?
  • @Victor 哦,好点。如果您的数字确实四舍五入为两位数,一种解决方法是改用整数(请参阅编辑答案),尽管对我来说这似乎是一个错误,现在发布在这里:github.com/Rdatatable/data.table/issues/2444
  • 实际上我的 numC 是一个日期,但同样的错误也适用。我想解决方法是“永久”添加另一列将 numC 转换为整数并在滚动连接中使用该列。 (我的表没有改变——它只是一个巨大的查找表——所以添加另一列是可行的。)
【解决方案2】:

这是作弊,因为我在基准测试之前进行了索引,但我假设您会在同一个 data.table 上多次运行查询。

library(data.table)
dt<-as.data.table(df)
setkey(dt,factorA,factorB)

closest2 <- function(ValueA, ValueB, ValueC, ValueD) {

  dt<-dt[.(ValueA,ValueB), on = c('factorA','factorB')]
  df_sub <- dt %>%
    filter( numC >= 0.9 * ValueC,
           numC <= 1.1 * ValueC,
           numD >= 0.9 * ValueD,
           numD <= 1.1 * ValueD)

  if (nrow(df_sub) == 0) stop("Oh-oh, no candidates.")

  minC <- df_sub[which.min(abs(df_sub$numC - ValueC)), "numC"]

  df_sub %>%
    filter(numC == minC) %>%
    slice(which.min(abs(numD - ValueD))) %>%
    as.list() %>%
    return()
}

library(microbenchmark)
microbenchmark(closest("a", "b", 0.5, 0.6))
microbenchmark(closest2("a", "b", 0.5, 0.6))


Unit: milliseconds
                       expr      min       lq     mean   median       uq      max neval
closest("a", "b", 0.5, 0.6) 20.29775 22.55372 28.08176 23.20033 25.42154 127.7781   100
Unit: milliseconds
                       expr      min       lq     mean   median      uq      max neval
 closest2("a", "b", 0.5, 0.6) 8.595854 9.063261 9.929237 9.396594 10.0247 16.92655   100

【讨论】:

  • 也许称它为最接近的 2b 或与 OP 的称为最接近 2 的函数区分开来的东西?顺便说一句,在我看来,只要我们假设表键在函数的使用之间没有被破坏,它就不是作弊。
  • 啊,你就是这样索引的!谢谢哈兰。索引numCnumD 能否产生更好的结果?
猜你喜欢
  • 2016-10-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-12-29
  • 2016-12-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多