【问题标题】:Subsetting a data.table by range making use of binary search使用二分搜索按范围对 data.table 进行子集化
【发布时间】:2014-03-11 08:35:49
【问题描述】:

为了使用二分搜索,您如何通过数字范围对 data.table 进行子集化?

例如:

require(data.table)
set.seed(1)

x<-runif(10000000,min=0,max=10)
y<-runif(10000000,min=0,max=10)

DF<-data.frame(x,y)
DT<-data.table(x,y)

system.time(DFsub<-DF[DF$x>5 & DF$y<7,])
# user  system elapsed 
# 1.529   0.250   1.821 

#subset DT
system.time(DTsub<-DT[x>5 & y<7])
# user  system elapsed 
#0.716   0.119   0.841 

上面没有使用键(矢量扫描),并且加速不是那么显着。使用二进制搜索对 data.table 的数字范围进行子集化的语法是什么? 我在文档中找不到一个很好的例子;如果有人可以使用上面的玩具 data.table 提供示例,那将会很有帮助。

编辑:这个问题很相似,但仍然没有演示如何按范围进行子集化: data.table: vector scan v binary search with numeric columns - super-slow setkey

【问题讨论】:

  • setkey() 对我来说工作正常,需要 0.11 秒。不过改进很小,将用户时间从 0.5 降低到 0.46。
  • 嗯,它现在也对我有用,可能是我的会话
  • 我已经更新了我的问题

标签: r data.table


【解决方案1】:

有趣的问题。首先让我们看一下示例数据:

> print(DT)
                     x        y
       1: 2.607703e-07 5.748127
       2: 8.894131e-07 5.233994
       3: 1.098961e-06 9.834267
       4: 1.548324e-06 2.016585
       5: 1.569279e-06 7.957730
      ---                      
 9999996: 9.999996e+00 9.977782
 9999997: 9.999998e+00 2.666575
 9999998: 9.999999e+00 6.869967
 9999999: 9.999999e+00 1.953145
10000000: 1.000000e+01 4.001616
> length(DT$x)
[1] 10000000
> length(unique(DT$x))
[1] 9988478
> length(DT$y)
[1] 10000000
> length(unique(DT$y))
[1] 9988225
> DT[,.N,by=x][,table(N)]
N
      1       2       3 
9976965   11504       9 
> DT[,.N,by="x,y"][,table(N)]
N
       1 
10000000 
> 

因此,第一列中有近 1000 万个唯一浮点值:几组大小为 2 行和 3 行,但大多数为 1 行组。一旦包含第二列,就有 1000 万个大小为 1 行的唯一组。这是一个相当棘手的问题,因为data.table 更多地是为分组数据而设计的;例如,(id,日期),(id1,id2,日期,时间)等

不过,data.tablesetkey 确实支持键中的浮点数据,所以让我们试一试吧。

在我的慢速上网本上:

> system.time(setkey(DT,x,y))
   user  system elapsed 
  7.097   0.520   7.650 

> system.time(DT[x>5 & y<7])
   user  system elapsed 
  2.820   0.292   3.122 

所以矢量扫描的方法比设置密钥要快(而且我们还没有使用密钥)。鉴于数据是浮点数并且几乎是唯一的,所以这并不太令人惊讶,但我认为这是 setkey 对 1000 万个完全随机且几乎唯一的双精度数进行排序的非常快的时间。

例如,与 base 相比,只对 x 进行排序,甚至对 y 也不进行排序:

> system.time(base::order(x))
   user  system elapsed 
 72.445   0.292  73.072 

假设这个数据代表你的真实数据,你不想只做一次而是多次,所以愿意付出setkey的代价,第一步就很清楚了:

system.time(w <- DT[.(5),which=TRUE,roll=TRUE])
   user  system elapsed 
  0.004   0.000   0.003 
> w
[1] 4999902

但是在这里我们被困住了。像DT[(w+1):nrow(DT)] 这样的下一步是丑陋的和复制的。我想不出一种体面的方式来使用这里的密钥来执行y&lt;7 部分。在其他示例数据中,我们执行DT[.(unique(x), 7), which=TRUE, roll=TRUE] 之类的操作,但在这种情况下,数据是如此独特且浮点数会很慢。

理想情况下,此任务需要range joins (FR#203) 实施。此示例中的语法可能是:

DT[.( c(5,Inf), c(-Inf,7) )]

或者为了更容易,DT[x&gt;5 &amp; y&lt;7] 可以在后台进行优化以做到这一点。允许 i 中的两列范围连接到相应的 x 列可能非常有用,并且已经出现了好几次。

需要先完成 v1.9.2 中的加速,然后我们才能继续做类似的事情。如果您在 v1.8.10 中对此数据尝试 setkey,您会发现 v1.9.2 明显更快。

另见:

How to self join a data.table on a condition

Remove a range in data.table

【讨论】:

  • 我认为针对这个用例进行优化是值得的;我经常需要按特定范围对大型空间/时间数据集进行子集化。现在,范围不受限制是不寻常的。如果您查看我的示例,则范围不能大于 10 或小于 0。
  • @Matt Dowle:我认为它已经实现了。 between 函数不正是这样做的吗?如果有,您可以更新答案。这可能反映了 data.table 当前的潜力!
  • @Ameya 感谢您的关注。现在有不少答案已经过时了。如果您或其他观看者想通过更新这个问题(以及许多其他问题!)做出贡献,所有人都会非常感激。随意直接编辑。与往常一样,请自行运行代码和计时以确认编辑是否正确。
【解决方案2】:

按照 Matt Dowle 的要求,我重新运行了代码和计时,以包含与现在包含在 data.table 包中的 betweenfunction 的比较。似乎矢量扫描浮点列仍然是最有效的方法。

#OP's example data
require(data.table)
set.seed(1)

x<-runif(10000000,min=0,max=10)
y<-runif(10000000,min=0,max=10)

DF<-data.frame(x,y)
DT<-data.table(x,y)

作为data.frame的子集

system.time(DFsub<-DF[DF$x>5 & DF$y<7,])
# user  system elapsed 
# 0.506   0.062   0.576 

作为data.table的子集,带有矢量扫描

system.time(DTsub<-DT[x>5 & y<7])
# user  system elapsed 
# 0.213   0.024   0.238 

介于之间的子集 DT(对于 x 和 y)

system.time(DTsub<-DT[between(x ,5, max(x)) & between(y, 0,7), ])
# user  system elapsed 
# 0.242   0.036   0.279  

替代混合矢量扫描和之间

system.time(DTsub<-DT[x > 5 & between(y, 0,7), ])
# user  system elapsed 
# 0.203   0.017   0.221 

语法之间的替代

system.time(DTsub<-DT[x %between% c(5, max(x)) & y %between% c(0, 7)])
# user  system elapsed 
# 0.227   0.016   0.244  

混合向量扫描和之间(使用替代语法)

system.time(DTsub<-DT[x>5 & y %between% c(0, 7)])
# user  system elapsed 
# 0.203   0.017   0.221

稍微更彻底的评估

library(microbenchmark)

mbm<-microbenchmark(
  "DFsub"={b1<-DF[DF$x>5 & DF$y<7,]},
  "DTsub1"={b2<-DT[x>5 & y<7]},
  "DTsub2"={b3<-DT[between(x ,5, max(x)) & between(y, 0, 7), ]},
  "DTsub3"={b4<-DT[x > 5 & between(y, 0,7), ]},
  "DTsub4"={b5<-DT[x %between% c(5, max(x)) & y %between% c(0, 7)]},
  "DTsub5"={b5<-DT[x>5 & y %between% c(0, 7)]}
)
mbm
Unit: milliseconds
Unit: milliseconds
# expr      min       lq     mean   median       uq       max neval
# DFsub 527.6842 582.3235 635.8846 622.1641 664.3243 1101.2365   100
# DTsub1 220.5086 245.7509 279.5451 263.5527 296.5736  411.5833   100
# DTsub2 249.2093 283.2025 325.4845 304.2361 333.6894  660.5021   100
# DTsub3 215.5454 243.3255 281.3596 270.1108 300.8462  491.8837   100
# DTsub4 250.9431 282.1896 330.0688 305.2094 352.9604  736.2690   100
# DTsub5 218.5458 238.8931 276.7932 262.6675 293.3524  467.5082   100

library(ggplot2)
autoplot(mbm)

【讨论】:

    猜你喜欢
    • 2020-06-13
    • 2020-07-24
    • 2015-08-28
    • 1970-01-01
    • 1970-01-01
    • 2018-10-02
    • 1970-01-01
    • 2015-08-27
    • 1970-01-01
    相关资源
    最近更新 更多