在 data.table `by` 中使用 `fifelse`,其中测试列是 `by` 的一部分

Using `fifelse` within data.table `by`, where test column is part of `by`

我想向 data.table 添加一列,该列是使用 by 定义的组内的序列,但对 by 子句中使用的其中一个列使用条件.我尝试使用 fifelse,如下例所示:

dt <- data.table::data.table(
  id = c(1, 1, 2, 2, 2, 3),
  clk = c(1, 1, 0, 2, 2, 5),
  val = LETTERS[1:6]
)

dt[, seq_clk := fifelse(clk != 0, seq_len(.N), NA_integer_), by = .(id, clk)]

这会导致以下错误

Error in fifelse(clk != 0, seq_len(.N), NA_integer_) : Length of 'yes' is 2 but must be 1 or length of 'test' (1).

我期望得到的结果可以通过下面的代码实现

dt[, seq_2 := seq_len(.N), by = .(id, clk)][
, seq_clk := fifelse(clk != 0, seq_2, NA_integer_)][
  , seq_2 := NULL]

这给出了

   id clk val seq_clk
1:  1   1   A       1
2:  1   1   B       2
3:  2   0   C      NA
4:  2   2   D       1
5:  2   2   E       2
6:  3   5   F       1

虽然上面的代码有效,但我不明白为什么第一个例子中的单行代码不起作用。问题似乎在于将 fifelse 应用于 by 子句中列出的列。它适用于不在 by.

中的列

我还注意到在这种情况下其他功能无法正常工作。例如:

dt[, sum_id_by_clk := fifelse(clk != 0, sum(id), NA_integer_), by = .(id, clk)]

没有给出错误,但产生了不正确的结果:

   id clk val sum_id_by_clk
1:  1   1   A             1
2:  1   1   B             1
3:  2   0   C            NA
4:  2   2   D             2
5:  2   2   E             2
6:  3   5   F             3

我希望最后一列中的值对于第 1-2 行为 2,对于第 4-5 行为 4。

我在这里错过了什么?

我们可能会更改分组列的名称

dt[, seq_clk := fifelse(clk != 0, seq_len(.N), NA_integer_),
     by = .(id, clk2 = clk)]

-输出

> dt
      id   clk    val seq_clk
   <num> <num> <char>   <int>
1:     1     1      A       1
2:     1     1      B       2
3:     2     0      C      NA
4:     2     2      D       1
5:     2     2      E       2
6:     3     5      F       1

rep test部分

dt[, seq_clk := fifelse(rep(clk, .N) != 0, seq_len(.N), 
       NA_integer_), by = .(id, clk)]
> 
> dt
      id   clk    val seq_clk
   <num> <num> <char>   <int>
1:     1     1      A       1
2:     1     1      B       2
3:     2     0      C      NA
4:     2     2      D       1
5:     2     2      E       2
6:     3     5      F       1

有可能分组列在条件中使用时,只使用第一个元素

> dt[, length(clk) , clk]
     clk    V1
   <num> <int>
1:     1     1
2:     0     1
3:     2     1
4:     5     1

而通过更改分组列

> dt[, length(clk) , .(clk2 = clk)]
    clk2    V1
   <num> <int>
1:     1     2
2:     0     1
3:     2     2
4:     5     1

fifelse/ifelse 都要求所有参数的长度相同(尽管 NA_integer_ 的长度为 1 - 回收,最好也复制它)

如果我理解正确,OP 想要对每个 id, clk 组中的列进行编号,同时跳过 clk == 0

的行

为此,可以方便地使用data.table的rowid()函数:

dt[clk != 0, seq_clk := rowid(id, clk)][]
      id   clk    val seq_clk
1:     1     1      A       1
2:     1     1      B       2
3:     2     0      C      NA
4:     2     2      D       1
5:     2     2      E       2
6:     3     5      F       1

第 3 行已从 参考更新 中排除,默认为 NA


OP 想知道为什么对分组变量的计算没有给出预期的结果。这是一个解释的尝试。

根据?data.table,data.table语法的一般形式是:

DT[ i,  j,  by ] # + extra arguments
    |   |   |
    |   |    -------> grouped by what?
    |    -------> what to do?
     ---> on which rows?

大声朗读的方法是:“取 DT,子集行 i 然后 计算 j 分组by.

因此,通常出现在 by 中的变量并不意味着 可以在 j 表达式中使用。 特殊符号 .SD的解释进一步强调了这一点(参见?.SD):

.SD is a data.table containing the Subset of x's Data for each group, excluding any columns used in by (or keyby).

这可以通过

验证
dt[clk != 0, print(.SD), by = .(id, clk)][]
      val seq_clk
1:      A       1
2:      B       2
      val seq_clk
1:      D       1
2:      E       2
      val seq_clk
1:      F       1
Empty data.table (0 rows and 2 cols): id,clk

可以通过特殊符号.BY访问分组变量,例如

dt[clk != 0, str(.BY), by = .(id, clk)][]
List of 2
 $ id : num 1
 $ clk: num 1
List of 2
 $ id : num 2
 $ clk: num 2
List of 2
 $ id : num 3
 $ clk: num 5
Empty data.table (0 rows and 2 cols): id,clk

因此,每个分组变量仅对每个组存在一次,这解释了为什么 sum(id) returns 仅 id.