data.table的join和j-expression意外行为

5
在R 2.15.0和data.table 1.8.9中:
d = data.table(a = 1:5, value = 2:6, key = "a")

d[J(3), value]
#   a value
#   3     4

d[J(3)][, value]
#   4

我原本期望两者都能产生相同的输出结果(第二个),而我认为它们应该如此。
为了澄清这不是一个 J 语法问题,在以下表达式(与上述相同)中也适用同样的期望。
t = data.table(a = 3, key = "a")
d[t, value]
d[t][, value]

我希望两者都能返回完全相同的输出。
因此,让我重新阐述问题——为什么(`data.table`被设计成)在`d[t, value]`中自动打印出键(key)列?
更新(根据下面的答案和评论):感谢@Arun等人,我现在理解了设计原因。以上的原因是因为每次通过`X[Y]`语法进行`data.table`合并时,都会有一个隐藏的`by`,而该`by`是按键分组的。之所以设计成这样似乎是出于以下原因——由于在合并时必须执行`by`操作,因此如果要按合并的键执行`by`,那么可以利用这一点,而不必再执行另一个`by`。
话虽如此,我认为这是语法设计上的缺陷。我读取`data.table`语法`d[i, j, by=b]`的方式是
取`d`,应用`i`操作(无论是子集还是合并或其他操作),然后按`b`执行`j`表达式"by"
没有按照`by`执行`by`会导致这种读法无效,并引入需要特别考虑的情况(我是否在合并`i`,`by`是否只是合并的键等)。我认为这应该是`data.table`的任务——为了使`data.table`在合并的一种特定情况下更快,即`by`等于键的情况下,应该以另一种方式实现(例如,通过在内部检查`by`表达式是否实际上是合并的键)。

现在它的功能是什么;另外值得一提的是,d[J(3), value := 10] 的效果如预期。 - eddi
5
请问能否修改这个问题的标题?这种行为是完全符合预期并经常被利用的。 - Ricardo Saporta
2
@Arun 想象一下我写了一个函数 fancy_sum(x, y),它会计算 xy 的和,除非 x 等于 10,此时它会执行乘法。想象一下这也是文档化的行为,称为 product-instead-of-sum。虽然这是一个文档化的 语法设计选择,但我认为这是一个明显的 语法设计缺陷 - eddi
1
@Arun,请阅读FR底部的评论。如果那仍然无法传达重点,我不知道还有什么能做到。你谈论的是事物如何运作以及为什么提供不同类别的输入会产生不同的结果,而我谈论的是事物应该如何运作,以及为什么不同类别的输入产生不同的结果会破坏用户的期望。 - eddi
这似乎很快就会被更改。请参见@Arun在此处的评论https://dev59.com/h3rZa4cB1Zd3GeqP8eYM#20914724 - Ari B. Friedman
显示剩余2条评论
4个回答

11

无限编辑编号:常见问题1.12确切地回答了你的问题:(还有相关的有用信息,请查看常见问题1.13,未在此处粘贴)。

1.12 X[Y]和merge(X,Y)之间有什么区别?
X[Y]是一种连接方式,使用Y(或如果Y有索引,则使用其键)作为索引查找X的行。Y[X]是一种连接方式,使用X(或如果X有索引,则使用其键)作为索引查找Y的行。merge(X,Y)1同时进行两种连接方式。通常,X[Y]和Y[X]返回的行数不同;而merge(X,Y)和merge(Y,X)返回的行数相同。但这忽略了主要问题。大多数任务需要在连接或合并后对数据进行处理。为什么要合并所有数据列,然后只使用其中的一小部分?
您可以建议merge(X[,ColsNeeded1],Y[,ColsNeeded2]),但那会复制数据子集,并且需要程序员计算出哪些列是必需的。在data.table中,X[Y,j]一步完成了所有操作。当您编写X[Y,sum(foo*bar)]时,data.table会自动检查j表达式以查看它使用哪些列。它只会对这些列进行子集操作;其他列将被忽略。仅为j使用的列创建内存,并且Y列在每个组的上下文中享有标准的R回收规则。假设foo在X中,bar在Y中(还有Y中的其他20列)。与合并后进行子集操作相比,X[Y,sum(foo*bar)]编程速度和运行速度更快吗?


旧答案未能回答OP的问题(来自OP的评论),因此保留在此处。

当您在data.table中为j给出一个值,例如d[, 4]d[, value]时,j将作为expression进行评估。从访问DT[, 5](第一个常见问题)的data.table FAQ 1.1中可以了解到:

因为,默认情况下,与数据框不同,第二个参数是在DT范围内评估的表达式。5评估为5。

因此,首先要理解的是,在您的情况下:

d[, value] # produces a "vector"
# [1] 2 3 4 5 6

当查询i是基本索引时,这与其他情况没有区别

d[3, value] # produces a vector of length 1
# [1] 4

然而,当i本身是一个data.table时,情况就不同了。从data.table介绍(第6页)可以看出:

d[J(3)] # is equivalent to d[data.table(a = 3)]

在这里,您正在执行一个“join”操作。如果您只执行 d[J(3)],那么您将获得与该连接对应的所有列。如果您执行,
d[J(3), value] # which is equivalent to d[J(3), list(value)]

既然你说这个答案对你的问题没有帮助,我会指出我认为你“重新表述”的问题的答案在哪里:---> 然后你将只得到那一列,但由于你正在执行连接操作,键列也将被输出(因为它是基于键列连接两个表的)。


编辑:根据您的第二次编辑,如果您的问题是为什么这样?那么我勉强(或者说无知地)回答,Matthew Dowle设计了这个来区分一个基于数据表的join-based-subset和一个基于index-based-subsetting操作。

你的第二种语法等同于:

d[J(3)][, value] # is equivalent to:

dd <- d[J(3)]
dd[, value]

再次提醒,在dd[, value]中,j被视为表达式,因此您将获得一个向量。


回答您修改后的第三个问题:我已经第三次回答了,这是因为它是基于关键列连接两个data.table。如果我连接两个data.table,我希望得到一个data.table

再次引用data.table介绍:

将一个 data.table 传递到另一个 data.table 子集中类似于基本R中的 A[B] 语法,其中A是矩阵,B是2列矩阵。事实上,基本R中的A[B]语法启发了data.table包。


1
我的问题是为什么打印出来的是键,而不是它被打印出来的解释! - eddi
是的,就像我执行 A[B] 时一样。但是 merge 没有 j 语法让我演示 A[B, value],所以我不会从那里带来任何期望。 - eddi
X[Y, list(value)]怎么样? - Arun
最后一个问题:当您执行 d[, list(.), by = .] 时,为什么您期望在结果中也包含 by 中的列?为什么不只是 list(.) 的条目? - Arun
如果你想要期望值 - 这就是 by 的作用,它不会破坏我的 d[i,j] 期望值,因为有一个额外的 by;作为一个设计决策,这也很明显,因为大多数 by 的使用都需要在最后有 by 列。但对于 X[Y, j] 来说并非如此 - 至少我很难想到一个使用情况,在那种情况下,如果我没有在 j 中显式请求,则不希望将键放在那里。 - eddi
显示剩余25条评论

6
data.table 1.9.3 开始, 默认行为已更改,下面的示例会产生相同的结果。 要获得 by-without-by 结果,现在必须指定显式的 by=.EACHI
d = data.table(a = 1:5, value = 2:6, key = "a")

d[J(3), value]
#[1] 4

d[J(3), value, by = .EACHI]
#   a value
#1: 3     4

下面是一个稍微复杂一些的例子,用来说明它们之间的区别:

d = data.table(a = 1:2, b = 1:6, key = 'a')
#   a b
#1: 1 1
#2: 1 3
#3: 1 5
#4: 2 2
#5: 2 4
#6: 2 6

# normal join
d[J(c(1,2)), sum(b)]
#[1] 21

# join with a by-without-by, or by-each-i
d[J(c(1,2)), sum(b), by = .EACHI]
#   a V1
#1: 1  9
#2: 2 12

# and a more complicated example:
d[J(c(1,2,1)), sum(b), by = .EACHI]
#   a V1
#1: 1  9
#2: 2 12
#3: 1  9

由于这篇文章很可能是关注这个变化的唯一来源,因此详细回答是有意义的。我也会尽力在有时间时进行编辑。 - Arun
@Arun 我添加了一个更复杂的例子,显然你可以随意编辑。 - eddi
1
@Arun 这个改变对我来说是一个重大的干扰。我同意从新用户的角度来看,这可能更直观,但是,如果保留 = .EACHI 作为向后兼容的默认值,那么这不是更好吗?至少,非常重要的是提前通知所有依赖于 data.table 的其他软件包。 - Juancentro

4
这不是意外行为,而是有记录的行为。Arun在FAQ中已经很好地解释和演示了这一点,在那里可以清楚地看到记录。
有一个功能请求FR 1757,建议在这种情况下使用drop参数。
当实现时,您想要的行为可能会被编码。
d = data.table(a = 1:5, value = 2:6, key = "a")

d[J(3), value, drop = TRUE]

谢谢,我现在理解了设计原因(即,如果您支持通过-without-by,则那是一个隐藏的“by”,因此应该有“by”列),到目前为止,所有这些的结论(对我来说)似乎是通过-without-by是一个不好的语法特性(与手动指定“by”并且“X [Y,j]”的默认行为相反[Y] [, j]相同)。如果有一些例子可以说服我,我非常愿意改变我的看法。 - eddi
1
by-without-byby甚至是带键的by显著更快。我认为多按几个键是值得的。如果你想要进行长时间的讨论,请移步到data.table邮件列表。如果你想对drop = TRUE的功能请求发表评论,请这样做。 - mnel
如果该功能作为额外选项提供,而不会对正常语法和理解造成影响,那么这个功能应该是可以的;无论如何,我想我的原始问题已经得到了回答;另外,在这个“by-without-by”的业务中,另一个不一致之处在于隐藏的“by”列实际上在“j”表达式中不可用,因此“d[J(3), a]”会失败。 - eddi
2
by-without-by是正常的data.table语法。你可以提出一个功能请求来删除by-without-by,但我认为问题在于你的理解和期望,这显然与data.table作者(以及大多数用户)不一致。d[.(3), a]是一个有趣的案例,我不确定它是如何得到它所做的结果的。如果你在i中命名它,它就可以工作d[.(a=3), a]。也许这值得单独提出一个问题(也许是在邮件列表上)。 - mnel
@eddi -- 我已经创建了FR 2693,以便在未命名列表传递给i时自动使用关键列名。 - mnel
1
@mnel +1 给 FR#2693。我会尝试实现它。 - Matt Dowle

3
我同意Arun的回答。这里有另一种措辞:在连接(join)后,您通常会使用连接列作为参考或输入进一步转换。因此,您保留它,并可以选择使用更加迂回的双重[语法来丢弃它。从设计的角度来看,保留频繁相关的信息,然后在需要时丢弃,比早期丢弃并冒失丢失难以重建的数据风险更小。
另一个您想保留连接列的原因是可以在执行连接操作的同时执行聚合操作(没有聚合名)。例如,通过包含连接列,以下结果更加清晰:
d <- data.table(a=rep.int(1:3,2),value=2:7,other=100:105,key="a")
d[J(1:3),mean(value)]
#   a  V1
#1: 1 3.5
#2: 2 4.5
#3: 3 5.5

1
正如@Simon101引用Ripley教授所说:“你的偏好是无关紧要的。文档才是最重要的”(而且它不会缺少解释)。话虽如此,MatthewDowle非常友善和乐于接受,他可能会配合。我对现状非常满意。 - Arun
1
@eddi by-without-by 的动机是在你拥有已知子集的群组时提高速度。假设你有1000个群组(每个群组有1000行和100列),但你只想要6个群组中一个列的平均值。对于这些6个群组的所有列的子集,再加上一个 by 操作会比直接使用精细的 j 列使用检查的 by-without-by 更慢。请参见 FAQ 1.12 获取更多相关信息。 - Matt Dowle
1
@MatthewDowle 我并不反对使用没有 by 的 概念,我的抱怨是针对它的 语法。我认为语法应该是“正常”的 by,所有的魔法都应该在内部发生。 - eddi
1
我想要的是按键加速的概念,但不是当前的语法。我不想要静默的按键。 - eddi
显示剩余12条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接