在data.table中设置键的目的是什么?


113

我正在使用data.table,并且有很多功能需要我设置一个键(例如X[Y])。因此,我希望了解键的作用,以便在数据表中正确设置键。


我读过的一个资料是 ?setkey

setkey() 排序一个 data.table并将其标记为已排序。排序的列是关键。键可以是任何顺序的任何列。列始终按升序排序。该表通过引用进行了更改。除了临时的工作存储区(最大为一列)外,根本不进行任何复制。

我在这里的要点是,一个键会“排序” data.table,从而产生与 order()。但是,它没有解释拥有钥匙的目的。


data.table常见问题解答3.2和3.3说明:

3.2我没有大桌子上的钥匙,但是分组仍然非常快。这是为什么?

data.table使用基数排序。这比其他排序算法快得多。基数仅用于整数,请参见 ?base::sort.list(x,method="radix")。这也是为什么setkey()快速的原因之一 。如果未设置任何键,或者我们按与键不同的顺序进行分组,则称其为临时。

3.3为什么键中的按列分组比临时按分组更快?

由于每个组在RAM中是连续的,因此可以最大程度地减少页获取,并且可以批量复制内存(memcpy在C中),而不是在C中循环。

从这里开始,我猜想通过设置密钥可以使R在其他算法上使用“基数排序”,这就是为什么它更快的原因。


10分钟快速入门指南还提供了按键指南。

  1. 按键

让我们从考虑data.frame开始,特别是行名(或者用英语,行名)。即,多个名称属于单个行。属于单行的多个名称?那不是我们在data.frame中所习惯的。我们知道每一行最多只有一个名称。一个人至少有两个名字,第一个名字和第二个名字。例如,这对于组织电话目录很有用,该电话目录按姓氏,然后按姓氏排序。但是,data.frame中的每一行只能有一个名称。

键由一行或多列行名组成,这些行名可以是整数,因子,字符或其他某些类,而不仅仅是字符。此外,行按键排序。因此,一个data.table最多可以具有一个键,因为它不能以一种以上的方式进行排序。

不强制唯一性,即允许重复的键值。由于行是按键排序的,因此键中的所有重复项将连续出现

电话簿有助于理解键是什么,但与具有factor列相比,键似乎没有什么不同。此外,它没有解释为什么需要一个键(尤其是使用某些功能)以及如何选择要设置为键的列。另外,似乎在以时间为列的data.table中,将任何其他列设置为键也可能会使时间列混乱,这使我更加困惑,因为我不知道是否允许将其他任何列设置为键。有人可以启发我吗?


“我想通过设置键可以使R在其他算法上使用“基数排序”” –我根本无法从帮助中获得这一点。我的理解是,设置键按键排序。您可以通过除键之外的其他列进行“临时”排序,该排序速度很快,但不如您已经排序的速度快。
Ari B. Friedman

我认为选择行时二进制搜索比矢量扫描更快。我不是计算机科学家,所以我不知道这实际上意味着什么。除常见问题解答外,请参阅介绍
弗兰克,

Answers:


125

较小的更新:请同时参考新的HTML小插曲本期重点介绍了我们计划使用的其他渐晕片。


根据新on=功能(也允许临时加入),我再次更新了此答案(2016年2月)。查看历史记录以获取较早(过时)的答案。

到底是什么 setkey(DT, a, b)做什么的?

它有两件事:

  1. 通过引用按提供的(ab)列对data.table 的行进行重新排序,始终按递增顺序。DT
  2. 标记这些列的设置称为属性栏sortedDT

重新排序既快速(由于data.table的内部基数排序),又具有高效的内存(仅多余一列类型为double的列)分配了)。

什么时候 setkey()需要?

对于分组操作,setkey()从来都不是绝对要求。也就是说,我们可以执行冷淡即席播放

## "cold" by
require(data.table)
DT <- data.table(x=rep(1:5, each=2), y=1:10)
DT[, mean(y), by=x] # no key is set, order of groups preserved in result

然而,之前v1.9.6,加入形式的x[i]需要key进行设定x使用on=v1.9.6 +中的新参数时,这不再成立,因此在这里也不是绝对必须设置键。

## joins using < v1.9.6 
setkey(X, a) # absolutely required
setkey(Y, a) # not absolutely required as long as 'a' is the first column
X[Y]

## joins using v1.9.6+
X[Y, on="a"]
# or if the column names are x_a and y_a respectively
X[Y, on=c("x_a" = "y_a")]

注意,on=即使对于keyed联接,也可以显式指定参数。

需要key绝对设置的唯一操作是foverlaps()函数。但是我们正在开发其他功能,这些功能完成后将消除此要求。

  • 那么实施的原因是什么 on=论证什么呢?

    有很多原因。

    1. 它允许将操作清楚地区分为涉及两个data.tables的操作。X[Y]尽管可以通过适当地命名变量来清楚地看出,但这样做也无法将其区分开。

    2. 通过查看该行代码,也可以立即了解正在执行联接/子集的列(而不必追溯到相应的setkey()行)。

    3. 通过引用添加或更新列的操作中,on=操作的性能要高得多,因为它不需要为添加/更新列而对整个data.table进行重新排序。例如,

      ## compare 
      setkey(X, a, b) # why physically reorder X to just add/update a column?
      X[Y, col := i.val]
      
      ## to
      X[Y, col := i.val, on=c("a", "b")]

      在第二种情况下,我们不必重新排序。它不是在计算耗时的顺序,而是对RAM中的data.table进行物理重新排序,并且通过避免它,我们保留了原始顺序,而且性能也很高。

    4. 甚至否则,除非您重复执行联接,否则联接和临时联接之间应该没有明显的性能差异。

这就引出了一个问题,键控data.table又有什么优势呢?

  • 键入data.table有好处吗?

    对数据表进行键控会根据RAM中的那些列对其进行物理重新排序。计算订单通常不是耗时的部分,而是重新排序本身。但是,一旦我们在RAM中对数据进行了排序,则属于同一组的行在RAM中都是连续的,因此具有非常高的缓存效率。排序可以加快对键化data.tables的操作。

    因此,必须弄清重新排序整个数据表所花费的时间是否值得进行高速缓存有效的连接/聚合。通常,除非对同一个键控数据表执行重复的分组/ 联接操作,否则应该没有明显的区别。

因此,在大多数情况下,不再需要设置密钥。我们建议on=尽可能使用,除非设置键可以显着改善您想利用的性能。

问题:如果您使用键联接data.table进行重新排序并使用,与联接相比,您setorder()性能on=如何?如果到目前为止,您应该可以弄清楚:-)。


3
太好了,谢谢!到目前为止,我还没有考虑过“二进制搜索”的真正含义,也没有真正理解使用它代替哈希的原因。
Frank

@Arun,DT[J(1e4:1e5)]真的等于DF[DF$x > 1e4 & DF$x < 1e5, ]吗?您能指出我什么J意思吗?此外,该搜索不会返回任何行,因为其中sample(1e4, 1e7, TRUE)不包括1e4以上的数字。
fishtank

@fishtank,在这种情况下,它应该是>=<=-固定。J(和.)是的别名list(即,它们是等效的)。在内部,当i是列表时,它将转换为data.table,然后使用二进制搜索来计算行索引。固定1e41e5以避免混淆。感谢发现。请注意,我们现在可以直接使用on=arguments来执行二进制子集,而无需设置key。从新的HTML vignettes中阅读更多内容。并留意该页面上的用于联接的小插曲。
阿伦(Arun)2015年

也许可以进行更彻底的更新?“何时需要”部分似乎已过时,例如
MichaelChirico

什么功能告诉您所使用的钥匙?
skan,

20

关键字基本上是数据集中的索引,它允许非常快速有效的排序,过滤和联接操作。这些可能是使用数据表而不是数据帧的最佳原因(使用数据表的语法也更加用户友好,但这与键无关)。

如果您不了解索引,请考虑以下事项:电话簿按名称“索引”。因此,如果我想查找某人的电话号码,这非常简单。但是,假设我想按电话号码进行搜索(例如,查找谁拥有特定的电话号码)?除非我可以通过电话号码“重新索引”电话簿,否则将需要很长时间。

考虑以下示例:假设我有一个表ZIP,其中包含美国所有邮政编码(> 33,000)以及相关信息(城市,州,人口,中位数收入等)。如果我想查找有关特定邮政编码的信息,那么如果我要搜索的话(搜索)大约要快1000倍setkey(ZIP,zipcode)

另一个好处与联接有关。假设在数据表中有一个人及其邮政编码的列表(称为“ PPL”),我想从ZIP表中添加信息(例如城市,州等)。下面的代码可以做到这一点:

setkey(ZIP,zipcode)
setkey(PPL,zipcode)
full.info <- PPL[ZIP, nomatch=F]

从我要基于公共字段(邮政编码)的2个表中联接信息的意义上来说,这是一个“联接”。在非常大的表上,这样的联接对于数据帧来说非常慢,而对于数据表来说却非常快。在一个实际的示例中,我必须在一张完整的邮政编码表上进行超过20,000个这样的联接。使用数据表,脚本花费了大约20分钟。跑步。我什至没有尝试使用数据帧,因为这将花费超过2周的时间。

恕我直言,您不应该只是阅读而应该学习常见问题解答和介绍材料。如果您有实际问题要解决,则更容易掌握。

[回应@Frank的评论]

回复:排序与索引 -基于对这个问题的回答,看来setkey(...)实际上确实在重新排列了表中的列(例如,物理排序),并且没有在数据库意义上创建索引。这有一些实际的含义:一方面,如果在表中设置键setkey(...),然后更改键列中的任何值,则data.table仅声明该表不再排序(通过关闭sorted属性);它不是动态重指数以保持适当的排序顺序(如将在数据库中发生)。此外,“删除键”使用setky(DT,NULL)没有表恢复到它的原始,未排序的顺序。

回复:过滤器与连接 -实际的区别是过滤从单个数据集中提取了一个子集,而连接则基于一个公共字段合并了两个数据集中的数据。连接有很多不同的类型(内部,外部,左侧)。上面的示例是一个内部联接(仅返回具有两个表共有的键的记录),这与筛选确实有很多相似之处。


1
+1。关于你的第一句话...已经排序正确了吗?联接不是过滤器(或将过滤作为第一步的操作)的特殊情况吗?似乎“更好的过滤”总结了整个好处。
弗兰克(Frank)

1
我想还是更好的扫描。
湿脚

1
@jlhoward谢谢。我以前的信念是,排序不是设置键的好处(因为如果要排序,就应该排序),而且setkey实际上确实会不可逆地对行进行重新排序。如果仅出于显示目的,那么如何按照“ true”顺序(在setkey之前会看到)打印前十行?我敢肯定setkey(DT,NULL)不会这样做...(续)
Frank

...(续)另外,我没有查看包的代码,但是要加入X[Y,...],您需要使用键“过滤” X的行。当然,之后还会发生其他事情(Y的列可用,并且有一个隐式的逐个循环),但是我仍然不认为这是概念上的独特好处。我想您的答案是根据您可能要执行的操作来进行的,但是在区别方面可能会有所帮助。
Frank

1
@Frank-因此setkey(DT,NULL)删除键,但不影响排序顺序。在这里提出一个问题。让我们来看看。
jlhoward13年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.