【问题标题】:What is the purpose of setting a key in data.table?在 data.table 中设置键的目的是什么?
【发布时间】:2013-12-01 02:30:16
【问题描述】:

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


我读到的一个来源是?setkey

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

我在这里的收获是,一个键会对 data.table 进行“排序”,从而产生与order() 非常相似的效果。但是,它并没有解释拥有密钥的目的。


data.table FAQ 3.2 和 3.3 解释:

3.2 我没有大桌子上的钥匙,但是分组还是很快的。这是为什么呢?

data.table 使用基数排序。这比其他的要快得多 排序算法。基数仅适用于整数,请参阅 ?base::sort.list(x,method="radix")。这也是原因之一 setkey() 很快。当没有设置键,或者我们以不同的顺序分组时 从key的角度,我们称之为ad hoc by。

3.3 为什么在 key 中按列分组比 ad hoc by 更快?

因为每个组在 RAM 中都是连续的,从而最小化页面 提取,并且可以批量复制内存(C中的memcpy)而不是 在 C 中循环。

从这里,我猜想设置一个键以某种方式允许 R 使用“基数排序”而不是其他算法,这就是它更快的原因。


10 分钟快速入门指南也有按键指南。

  1. 钥匙

让我们首先考虑 data.frame,特别是行名(或在 英文,行名)。也就是说,属于一个单一的多个名称 排。属于单行的多个名称?那不是什么 我们习惯于在 data.frame 中。我们知道每一行最多有一个 姓名。一个人至少有两个名字,一个名字和一个名字。 这对于组织电话簿很有用,例如, 按姓氏排序,然后是名字。然而,每一行在一个 data.frame 只能有一个名称。

一个密钥由一个或多个组成 行名列,可以是整数、因子、字符或某些 其他类,不仅仅是性格。此外,行按以下方式排序 钥匙。因此,一个 data.table 最多可以有一个键,因为它 不能以多种方式排序。

不强制唯一性, 即,允许重复的键值。由于行是按 键,键中的任何重复项都会连续出现

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

【问题讨论】:

  • “我猜想以某种方式设置一个键允许 R 使用“基数排序”而不是其他算法”——我根本没有从帮助中得到。我的阅读是设置键按键排序。您可以按除键之外的其他列进行“临时”排序,而且速度很快,但不如您已经排序过的快。
  • 我认为选择行时二进制搜索比矢量扫描更快。我不是计算机科学家,所以我不知道这实际上意味着什么。除了常见问题,请参阅the introduction

标签: r data.table


【解决方案1】:

除了这个答案,请参考小插曲Secondary indices and auto indexingKeys and fast binary search based subset

This issue 突出显示我们计划的其他小插曲。


鉴于新的on= 功能也允许ad-hoc 加入,我再次更新了这个答案(2016 年 2 月)。请参阅历史记录以获取早期(过时)的答案。

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

它做了两件事:

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

重新排序速度很快(由于 data.table 的内部基数排序)和内存效率(只分配了一个额外的 double 类型的列)。

什么时候需要setkey()

对于分组操作,setkey() 从来都不是绝对要求。也就是说,我们可以执行 cold-byadhoc-by

## "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] 形式的连接需要在 x 上设置 key使用 v1.9.6+ 中的新 on= 参数,这不再是正确的,因此设置键也不是这里的绝对要求。

## 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() 行)来立即了解正在执行join/subset 的列。

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

       ## 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. 即使在其他情况下,除非您重复执行联接,否则 keyedad-hoc 联接之间应该没有明显的性能差异。

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

  • 键入 data.table 有什么好处吗?

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

    因此,必须弄清楚花在重新排序整个 data.table 上的时间是否值得花时间进行高效缓存连接/聚合。通常,除非在同一个 keyed data.table 上执行重复的分组/连接操作,否则应该不会有明显的差异。

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

问题:如果您使用 setorder() 重新排序 data.table,您认为与 keyed 连接相比性能如何 并使用on=?如果您到目前为止一直遵循,您应该能够弄清楚:-)。

【讨论】:

  • 酷,谢谢!到目前为止,我还没有想过“二分查找”的真正含义,也没有真正理解为什么使用它而不是哈希。
  • @Arun,DT[J(1e4:1e5)] 真的等同于DF[DF$x &gt; 1e4 &amp; DF$x &lt; 1e5, ] 吗?您能指出J 的含义吗?此外,该搜索不会返回任何行,因为 sample(1e4, 1e7, TRUE) 不包括 1e4 以上的数字。
  • @fishtank,在这种情况下,它应该是 &gt;=&lt;= -- 已修复。 J(和.)是list的别名(即它们是等价的)。在内部,当i 是一个列表时,它会转换为一个 data.table,然后使用二进制搜索来计算行索引。将1e4 固定为1e5 以避免混淆。感谢您的发现。请注意,我们现在可以直接使用on= 参数来执行二进制子集,而不是设置键。从新的HTML vignettes 中了解更多信息。并密切关注该页面以获取连接的小插曲。
  • 也许这可以进行更彻底的更新? “需要时”部分似乎已过时,例如
  • 什么函数告诉你正在使用的密钥?
【解决方案2】:

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

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

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

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

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

从某种意义上说,这是一个“加入”,因为我正在加入来自基于公共字段(邮政编码)的 2 个表的信息。像这样在非常大的表上的连接对于数据帧来说非常慢,而对于数据表来说却非常快。在一个真实的例子中,我必须在一个完整的邮政编码表上进行超过 20,000 次这样的连接。使用数据表,脚本大约需要 20 分钟。跑步。我什至没有尝试使用数据框,因为它会花费超过 2 周的时间。

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

[回复@Frank 的评论]

Re:排序与索引 - 根据对this question 的回答,setkey(...) 似乎确实重新排列了表中的列(例如,物理排序),并且不会在数据库意义上创建索引。这有一些实际意义:一方面,如果您使用setkey(...) 在表中设置键,然后更改键列中的任何值,data.table 仅声明表不再排序(通过关闭sorted 属性);它动态地重新索引以保持正确的排序顺序(就像在数据库中发生的那样)。此外,使用setkey(DT, NULL)“删除密钥”不会将表格恢复到原始的未排序顺序。

Re: filter vs. join - 实际的区别在于过滤从单个数据集中提取子集,而 join 基于公共字段将来自两个数据集的数据组合在一起。有许多不同类型的连接(内连接、外连接、左连接)。上面的例子是一个内连接(只返回两个表共有键的记录),这与过滤有很多相似之处。

【讨论】:

  • +1。关于你的第一句话......它已经排序对了吗?连接不是过滤器的特例(或以过滤为第一步的操作)吗?似乎“更好的过滤”总结了整个好处。
  • 或者我想更好的扫描。
  • @jlhoward 谢谢。我之前的信念是排序不是设置键的好处之一(因为如果你想排序,你应该只排序),而且setkey 实际上确实对行进行了不可逆转的重新排序。如果仅用于显示目的,那么如何根据“真实”顺序打印前十行(我会在 setkey 之前看到)?我很确定setkey(DT,NULL) 不会这样做...(续)
  • ...(续)另外,我还没有查看包的代码,但要加入 X[Y,...],您需要使用键“过滤”X 的行。诚然,在那之后发生了其他事情(Y 的列可用,并且有一个隐含的 by-without-by),但我仍然不认为这是一个概念上不同的好处。不过,我想您的答案是根据您可能想要做的操作提出的,其中的区别可能会有所帮助。
  • @Frank - 所以setkey(DT,NULL) 删除键但不影响排序顺序。对此here提出了一个问题。让我们看看。
猜你喜欢
  • 2012-08-15
  • 1970-01-01
  • 1970-01-01
  • 2011-11-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-10-27
相关资源
最近更新 更多