data.table 在 R 中 - 使用多个键的多个过滤器 - 二进制搜索

data.table in R - multiple filters using multiple keys - binary search

我不明白如何根据 data.table 中的多个键进行过滤。取内置的mtcars数据集。

DT <- data.table(mtcars)
setkey(DT, am, gear, carb)

vignette 之后,我知道如果我想要过滤对应于 am == 1 & gear == 4 & carb == 4,我可以说

> DT[.(1, 4, 4)]
   mpg cyl disp  hp drat    wt  qsec vs am gear carb
1:  21   6  160 110  3.9 2.620 16.46  0  1    4    4
2:  21   6  160 110  3.9 2.875 17.02  0  1    4    4

它给出了正确的结果。此外,如果我想要am == 1 & gear == 4 & (carb == 4 | carb == 2),这也有效

> DT[.(1, 4, c(4, 2))]
    mpg cyl  disp  hp drat    wt  qsec vs am gear carb
1: 21.0   6 160.0 110 3.90 2.620 16.46  0  1    4    4
2: 21.0   6 160.0 110 3.90 2.875 17.02  0  1    4    4
3: 30.4   4  75.7  52 4.93 1.615 18.52  1  1    4    2
4: 21.4   4 121.0 109 4.11 2.780 18.60  1  1    4    2

不过,当我想要am == 1 & (gear == 3 | gear == 4) & (carb == 4 | carb == 2)的时候,似是而非

> DT[.(1, c(3, 4), c(4, 2))]
    mpg cyl  disp  hp drat    wt  qsec vs am gear carb
1:   NA  NA    NA  NA   NA    NA    NA NA  1    3    4
2: 30.4   4  75.7  52 4.93 1.615 18.52  1  1    4    2
3: 21.4   4 121.0 109 4.11 2.780 18.60  1  1    4    2

失败。你能给我解释一下什么是正确的方法吗?

您没有从查询中收到错误的原因是 data.table 将重复使用其他值的倍数的值。换句话说,因为 am1 可以使用 2 次,所以它会在不告诉您的情况下执行此操作。如果您要执行一个查询,其中允许值的数量不是彼此的倍数,那么它会给您一个警告。例如

DT[.(c(1,0),c(5,4,3),c(8,6,4))]

将向您发出警告,抱怨 1 项的剩余部分,这与您在键入 data.table(c(1,0),c(5,4,3),c(8,6,4)) 时看到的错误相同。每当合并 X[Y] 时,XY 都应该被认为是 data.table。

如果您改为使用 CJ

DT[CJ(c(1,0),c(5,4,3),c(8,6,4))]

然后它将为您生成所有值的每个组合,data.table 将给出您期望的结果。

来自小插图(粗体是我的):

What’s happening here? Read this again. The value provided for the second key column “MIA” has to find the matching vlaues in dest key column on the matching rows provided by the first key column origin. We can not skip the values of key columns before. Therfore we provide all unique values from key column origin. “MIA” is automatically recycled to fit the length of unique(origin) which is 3.

为了完整起见,矢量扫描语法将在不使用 CJ

的情况下工作
DT[am == 1 & gear == 4 & carb == 4]

DT[am == 1 & (gear == 3 | gear == 4) & (carb == 4 | carb == 2)]

你怎么知道你是否需要二分查找?如果子集化的速度无法忍受,那么你需要一个二进制搜索。例如,我有一个 48M 行 data.table 我正在玩,二分搜索和向量之间的差异相对于彼此来说是惊人的。具体来说,向量扫描耗时 1.490 秒,而二分查找仅耗时 0.001 秒。当然,这假设我已经输入了 data.table。如果我包括设置密钥所需的时间,那么设置密钥和执行子集的组合是 1.628。所以你必须选择你的毒药

此问题现已成为重复问题的目标,我认为可以改进现有答案以帮助新手 data.table 用户。

1。 DT[.()]DT[CJ()] 有什么区别?

根据 ?data.table.()list() 的别名,作为参数 i 提供的 list 被转换为 [=17= 】 内部。所以,DT[.(1, c(3, 4), c(2, 4))] 等同于 DT[data.table(1, c(3, 4), c(2, 4))]

data.table(1, c(3, 4), c(2, 4))
#   V1 V2 V3
#1:  1  3  2
#2:  1  4  4

data.table由两行组成,是最长向量的长度。 1 被回收。

这与 cross join 不同,后者创建所提供向量的所有组合。

CJ(1, c(3, 4), c(2, 4))
   V1 V2 V3
#1:  1  3  2
#2:  1  3  4
#3:  1  4  2
#4:  1  4  4

请注意 setDT(expand.grid()) 会产生相同的结果。

这解释了为什么 OP 会得到两个不同的结果:

DT[.(1, c(3, 4), c(2, 4))]
#   mpg cyl disp  hp drat    wt  qsec vs am gear carb
#1:  NA  NA   NA  NA   NA    NA    NA NA  1    3    2
#2:  21   6  160 110  3.9 2.620 16.46  0  1    4    4
#3:  21   6  160 110  3.9 2.875 17.02  0  1    4    4

DT[CJ(1, c(3, 4), c(2, 4))]
#    mpg cyl  disp  hp drat    wt  qsec vs am gear carb
#1:   NA  NA    NA  NA   NA    NA    NA NA  1    3    2
#2:   NA  NA    NA  NA   NA    NA    NA NA  1    3    4
#3: 30.4   4  75.7  52 4.93 1.615 18.52  1  1    4    2
#4: 21.4   4 121.0 109 4.11 2.780 18.60  1  1    4    2
#5: 21.0   6 160.0 110 3.90 2.620 16.46  0  1    4    4
#6: 21.0   6 160.0 110 3.90 2.875 17.02  0  1    4    4

请注意,参数 nomatch = 0 将删除不匹配的行,即包含 NA.

的行

2。使用 %in%

除了CJ()am == 1 & (gear == 3 | gear == 4) & (carb == 2 | carb == 4),还有第三个使用值匹配的等效选项:

DT[am == 1 & gear %in%  c(3, 4) & carb %in% c(2, 4)]
#    mpg cyl  disp  hp drat    wt  qsec vs am gear carb
#1: 30.4   4  75.7  52 4.93 1.615 18.52  1  1    4    2
#2: 21.4   4 121.0 109 4.11 2.780 18.60  1  1    4    2
#3: 21.0   6 160.0 110 3.90 2.620 16.46  0  1    4    4
#4: 21.0   6 160.0 110 3.90 2.875 17.02  0  1    4    4

请注意,CJ() 需要 data.table 键控,而其他两个变体也适用于未键控的 data.tables。

3。基准测试

数据

为了测试 3 个选项的执行速度,我们需要比 mtcars 的 32 行大得多的 data.table。这是通过重复加倍 mtcars 直到达到 100 万行 (89 MB) 来实现的。然后复制此 data.table 以获得相同输入数据的键控版本。

library(data.table)
# create unkeyed data.table
DT_unkey <- data.table(mtcars)
for (i in 1:15) {
  DT_unkey <- rbindlist(list(DT_unkey, DT_unkey))
  print(nrow(DT_unkey))
}

#create keyed data.table
DT_keyed <- copy(DT_unkey)
setkeyv(DT_keyed, c("am", "gear", "carb"))

# show data.tables
tables()
#     NAME          NROW NCOL MB COLS                                         KEY         
#[1,] DT_keyed 1,048,576   11 89 mpg,cyl,disp,hp,drat,wt,qsec,vs,am,gear,carb am,gear,carb
#[2,] DT_unkey 1,048,576   11 89 mpg,cyl,disp,hp,drat,wt,qsec,vs,am,gear,carb             
#Total: 178MB

运行

为了公平比较,setkey() 操作包含在计时中。此外,data.tables 被显式复制以排除 data.table 引用更新的影响。

result <- microbenchmark::microbenchmark(
  setkey = {
    DT_keyed <- copy(DT)
    setkeyv(DT_keyed, c("am", "gear", "carb"))},
  cj_keyed = {
    DT_keyed <- copy(DT)
    setkeyv(DT_keyed, c("am", "gear", "carb")) 
    DT_keyed[CJ(1, c(3, 4), c(2, 4)), nomatch = 0]},
  or_keyed = {
    DT_keyed <- copy(DT)
    setkeyv(DT_keyed, c("am", "gear", "carb")) 
    DT_keyed[am == 1 & (gear == 3 | gear == 4) & (carb == 2 | carb == 4)]},
  or_unkey = {
    copy = DT_unkey <- copy(DT)
    DT_unkey[am == 1 & (gear == 3 | gear == 4) & (carb == 2 | carb == 4)]},
  in_keyed =  {
    DT_keyed <- copy(DT)
    setkeyv(DT_keyed, c("am", "gear", "carb")) 
    DT_keyed[am %in% c(1) & gear %in%  c(3, 4) & carb %in% c(2, 4)]},
  in_unkey = {
    copy = DT_unkey <- copy(DT)
    DT_unkey[am %in% c(1) & gear %in%  c(3, 4) & carb %in% c(2, 4)]},
  times = 10L)

我们得到

print(result)
#Unit: milliseconds
#     expr       min        lq     mean    median       uq      max neval
#   setkey 198.23972 198.80760 209.0392 203.47035 213.7455 245.8931    10
# cj_keyed 210.03574 212.46850 227.6808 216.00190 254.0678 259.5231    10
# or_keyed 244.47532 251.45227 296.7229 287.66158 291.3811 404.8678    10
# or_unkey  69.78046  75.61220 103.6113  89.32464 111.5240 231.6814    10
# in_keyed 269.82501 270.81692 302.3453 274.42716 321.2935 431.9619    10
# in_unkey  93.75537  95.86832 119.4371 100.19446 126.6605 251.4172    10

ggplot2::autoplot(result)

显然,setkey()是一个相当昂贵的操作。所以,对于一次性任务 矢量扫描操作可能比在键控 table.

上使用二进制搜索更快

基准是 运行,R 版本 3.3.2(x86_64,mingw32),data.table 1.10.4,microbenchmark 1.4-2.1 .