【问题标题】:Cumulative vector in data table数据表中的累积向量
【发布时间】:2021-08-15 13:59:55
【问题描述】:

我有如下数据表:

library(data.table)
dat = data.table(j = c(3,8,9,11,10,28), gr = c(9,9,9,9,10,10))
> dat
    j gr
1:  3  9
2:  8  9
3:  9  9
4: 11  9
5: 10 10
6: 28 10

有两个组(由“gr”指定)并且它们是有序的。现在我想要实现的是为每组的每一行创建一个累积的“j”值向量。结果应该是list 列,如下所示:

res_dat = data.table(j = c(3,8,9,11,10,28), gr = c(9,9,9,9,10,10),
                     res = list(3, c(3,8), c(3,8,9), c(3,8,9,11),
                                10, c(10, 28)))
> res_dat
    j gr         res
1:  3  9           3
2:  8  9         3,8
3:  9  9       3,8,9
4: 11  9  3, 8, 9,11
5: 10 10          10
6: 28 10       10,28

我尝试了以下方法:

首先,我创建了一个虚拟列,其中包含每个组的每行数。

dat[, tmp:= seq_len(.N), by = gr]

我的计划是使用该数字来对 j 向量进行子集化,但我没有做到。这些都不起作用:

dat[, res := list(j[1:tmp]), by = gr]
dat[, res := list(list(j[1:tmp])), by = gr] # based on https://stackoverflow.com/questions/22531477/using-lists-inside-data-table-columns

我收到以下错误:

Warning messages:
1: In 1:tmp : numerical expression has 4 elements: only the first used
2: In 1:tmp : numerical expression has 2 elements: only the first used

这确实有助于理解它是如何失败的,但我不知道如何让它成功。有什么想法吗?

【问题讨论】:

  • 一种可能性:dat[, res := .(sapply(seq_len(.N), function(x) j[seq_len(x)])), by = gr]
  • 虽然Reduce(c, j, accumulate=TRUE) 在控制台上工作,但它不能在dat 环境中工作,令人沮丧。 @Henrik 的评论(我相信)是最简单的,尽管我建议使用 lapplysapply(., simplify=FALSE) 来排除极端情况的差异。
  • @r2evans 把它包裹在list:dat[, res2 := .(Reduce(c, j, accumulate=TRUE)), by = gr]
  • 该死,我以为我已经尝试过了。谢谢@Henrik ...不妨回答一下!它很优雅,data-table-canonical。 (在我看来,它获取列表返回值并在赋值中丢弃了大部分,这对我来说似乎很奇怪。来自Reduce(..., accum=T) 的返回值已经是list,不知道为什么这里严格需要双重列表。)

标签: r data.table


【解决方案1】:

这是 Henrik 的回答(如果他们回来了,我很乐意给他们这个答案……不知何故):

dat[, res := .(Reduce(c, j, accumulate=TRUE)), by = gr]
#        j    gr         res
#    <num> <num>      <list>
# 1:     3     9           3
# 2:     8     9         3,8
# 3:     9     9       3,8,9
# 4:    11     9  3, 8, 9,11
# 5:    10    10          10
# 6:    28    10       10,28

Reducesapply 类似,只是它对当前值和上一个操作的结果进行操作。例如,我们可以看到

sapply(1:3, function(z) z*2)
# [1] 2 4 6

这个,展开后,等于

1*2 # 2
2*2 # 4
3*2 # 6

也就是说,对向量/列表的一个元素的计算是完全独立的,永远不知道之前迭代的结果。

但是,Reduce 明确给出了先前计算的结果。默认情况下,它只会返回最后一次计算,类似于tail(sapply(...), 1)

Reduce(function(prev, this) prev + this*2, 11:13)
# [1] 61

这似乎有点晦涩......让我们看看所有中间步骤,上面的答案是最后一个:

Reduce(function(prev, this) prev + this*2, 11:13, accumulate = TRUE)
# [1] 11 35 61

在这种情况下(不指定init=,等待它),第一个结果只是x=中的第一个值,不是通过函数运行。如果我们展开这个,我们会看到

11        # 11 is the first value in x
   _________/
  /
 v
11 + 12*2 # 35
35 + 13*2 # 61

有时我们需要在函数中运行x= 中的第一个值,并带有一个起始条件(当我们没有使用先前的迭代时,prev 的第一次值)。为此,我们可以使用init=;通过查看两个完全等效的调用,我们可以想到 init= 的用法:

Reduce(function(prev, this) prev + this*2, 11:13, accumulate = TRUE)
Reduce(function(prev, this) prev + this*2, 12:13, init = 11, accumulate = TRUE)
# [1] 11 35 61

(如果没有init=,Reduce 将获取x= 的第一个元素并将其分配给init= 并将其从x= 中删除。)

现在假设我们希望起始条件(注入的“先前”值)为 0,那么我们会这样做

Reduce(function(prev, this) prev + this*2, 11:13, init = 0, accumulate = TRUE)
# [1]  0 22 46 72


### unrolled
 0        # 0 is the init= value
   ________/
  /
 v
 0 + 11*2 # 22
22 + 12*2 # 46
46 + 13*2 # 72

让我们回到这个问题和这个数据。我将注入一个browser() 并稍微更改函数,以便我们可以查看所有中间值。

> dat[, res := .(Reduce(function(prev, this) { browser(); c(prev, this); }, j, accumulate=TRUE)), by = gr]
Called from: f(init, x[[i]])
Browse[1]> debug at #1: c(prev, this)
Browse[2]> prev                                    # group `gr=9`, row 2
[1] 3
Browse[2]> this
[1] 8
Browse[2]> c(prev, this)
[1] 3 8
Browse[2]> c                                       # 'c'ontinue

Browse[2]> Called from: f(init, x[[i]])
Browse[1]> debug at #1: c(prev, this)
Browse[2]> prev                                    # group `gr=9`, row 3
[1] 3 8
Browse[2]> this
[1] 9
Browse[2]> c(prev, this)
[1] 3 8 9
Browse[2]> c                                       # 'c'ontinue

Browse[2]> Called from: f(init, x[[i]])
Browse[1]> debug at #1: c(prev, this)
Browse[2]> prev                                    # group `gr=9`, row 4
[1] 3 8 9
Browse[2]> this
[1] 11
Browse[2]> c(prev, this)
[1]  3  8  9 11
Browse[2]> c                                       # 'c'ontinue

Browse[2]> Called from: f(init, x[[i]])
Browse[1]> debug at #1: c(prev, this)
Browse[2]> prev                                    # group `gr=10`, row 6
[1] 10
Browse[2]> this
[1] 28
Browse[2]> c(prev, this)
[1] 10 28
Browse[2]> c                                       # 'c'ontinue

注意我们没有“看到”第 1 行或第 5 行,因为它们是减少的 init= 条件(每个组中看到的第一个 prev 值)。

Reduce 可能是一个难以可视化和使用的函数。当我使用它时,我几乎总是将browser() 预先插入到匿名函数中并完成前三个步骤:第一个确保init= 正确,第二个确保匿名函数在做什么我想我想要使用 init 和 next 值,以及第三个以确保它正常继续。这类似于“演绎证明”:nth 计算将是正确的,因为我们知道 (n-1)th 计算是正确的。

【讨论】:

  • 很高兴您发布了@r2evans!我忙着喝咖啡。干杯
  • highfive 表示中性语言。
  • 这有助于解释它吗?
  • 这比我想象的要多得多!谢谢
  • 很高兴它有帮助。我相信sapplylapply 对大多数人来说相对简单; apply 只是更容易掌握。然而,mapply/Map 对某些人来说可能有点困难,而Reduce 很容易被误解。像这样走过似乎(恕我直言)是一种很好的方式来可视化它在引擎盖下所做的事情。
【解决方案2】:

这是一个简单的tidyverse 解决方案,不幸的是我还没有开始学习data.table,但是在你得到一个相关的解决方案之前它会很好:

library(dplyr)
library(purrr)

dat %>%
  group_by(gr) %>%
  mutate(res = accumulate(j[-1], .init = j[1], ~ c(.x, .y)))

# A tibble: 6 x 3
# Groups:   gr [2]
      j    gr res      
  <dbl> <dbl> <list>   
1     3     9 <dbl [1]>
2     8     9 <dbl [2]>
3     9     9 <dbl [3]>
4    11     9 <dbl [4]>
5    10    10 <dbl [1]>
6    28    10 <dbl [2]>

或者在基础 R 中我们可以这样做:

do.call(rbind, lapply(unique(dat$gr), function(a) {
  tmp <- subset(dat, gr == a)
  tmp$res <- Reduce(c, tmp$j, accumulate = TRUE)
  tmp
}))

甚至是亲爱的@Henrik 提出的这个了不起但简洁的建议:

do.call(rbind, by(dat, dat$gr, function(d){
  cbind(d, res = I(Reduce(c, d$j, accumulate=TRUE)))}))

甚至这个也是亲爱的@Henrik:

dat$res = ave(dat$j, dat$gr, FUN = function(x) Reduce(c, x, accumulate=TRUE))

【讨论】:

    猜你喜欢
    • 2015-05-05
    • 2013-12-21
    • 1970-01-01
    • 1970-01-01
    • 2017-06-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-08-30
    相关资源
    最近更新 更多