R中向具有大量数据集的数据框添加新列的有效方法

Posted

技术标签:

【中文标题】R中向具有大量数据集的数据框添加新列的有效方法【英文标题】:Efficient way in R to add a new column to a dataframe with huge dataset 【发布时间】:2018-09-11 14:43:55 【问题描述】:

我真的需要加快一些 R 代码的速度。我有一个来自特定运动的大型数据集。数据框中的每一行代表游戏中的某种类型的动作。对于每场比赛 (game_id),我们都有两支球队 (team_id) 参加比赛。数据框中的time_ref 是每个游戏按时间顺序排列的动作。 type_id 是游戏中的动作类型。 player_off 设置为TRUEFALSE 并链接到action_id=3action_id=3 表示玩家拿到一张牌,player_off 设置为TRUE/FALSE,如果玩家在拿到那张牌时被罚下。示例数据框:

> df

game_id team_id action_id   player_off  time_ref
100     10         1             NA       1000
100     10         1             NA       1001
100     10         1             NA       1002
100     11         1             NA       1003
100     11         2             NA       1004
100     11         1             NA       1005
100     10         3             1        1006
100     11         1             NA       1007
100     10         1             NA       1008
100     10         1             NA       1009
101     12         3             0        1000
101     12         1             NA       1001
101     12         1             NA       1002
101     13         2             NA       1003
101     13         3             1        1004
101     12         1             NA       1005
101     13         1             NA       1006
101     13         1             NA       1007
101     12         1             NA       1008
101     12         1             NA       1009

我需要的是数据框中的另一列,它给我TRUEFALSE 在每个动作(行)发生时两支球队在场上的球员人数是否相等/不相等。

所以game_id=100time_ref=1006 有一个action_id=3player_off=1 用于team_id=10。因此,我们知道在这一点之前,球队的场上球员人数是相等的,但在比赛的剩余时间里是不平等的 (time_ref>1006)。同样的事情也发生在game_id=101

这是一个数据框示例,我希望为数据集添加一个额外的列。

>df
game_id team_id action_id   player_off  time_ref    is_even
100      10        1            NA        1000         1
100      10        1            NA        1001         1
100      10        1            NA        1002         1 
100      11        1            NA        1003         1
100      11        2            NA        1004         1
100      11        1            NA        1005         1
100      10        3            1         1006         1
100      11        1            NA        1007         0
100      10        1            NA        1008         0
100      10        1            NA        1009         0
101      12        3            0         1000         1
101      12        1            NA        1001         1
101      12        1            NA        1002         1
101      13        2            NA        1003         1
101      13        3            1         1004         1
101      12        1            NA        1005         0
101      13        1            NA        1006         0
101      13        1            NA        1007         0
101      12        1            NA        1008         0
101      12        1            NA        1009         0

因此您可以看到,在game_id=100 中,一名球员在time_ref=1006 被罚下场,因此之前的所有行都标记为is_even=1,随后标记为不均匀或0game_id=101time_ref=1004 类似。

实现这一额外列的最有效方法是什么?最好不要使用 for 循环。

【问题讨论】:

“最有效”的方法可能是为您的特殊情况编写 C 代码;这真的是你想要的吗? 我知道你的意思,但实际上我更愿意将它保留在 R 中。 该评论仍然适用——您不是在寻找“最有效的”,而是对于手头的任务来说相当有效的东西。任务不是如何向数据框中添加新列,而是如何按组转换列。我想我是想告诉你修改你的标题(这可能会帮助你找到现有的问题和答案)。 是的。我已经从标题中删除了“大多数”。 使用 data.table,您的示例由 mDT = DT[player_off == 1, .(game_id, time_ref)]; DT[, is_even := 1L][mDT, on=.(game_id, time_ref > time_ref), is_even := 0L] 处理,但我猜您的示例不够通用(例如,没有显示在两支球队都有一名球员离开甚至再次出现之后会发生什么)。 【参考方案1】:

对于一些向量

x = c(0, NA, NA, NA, 1, NA, NA, NA)

编写一个函数来标准化数据(0或1名玩家丢失),计算累计丢失玩家数量,并将其与零进行比较,

fun0 = function(x)  
    x[is.na(x)] = 0
    cumsum(x) == 0

对于多个组,将ave() 与分组变量一起使用

x = c(x, rev(x))
grp = rep(1:2, each = length(x) / 2)
ave(x, grp, FUN = fun0)

对于问题中的数据,试试

df$is_even = ave(df$player_off, df$game_id, FUN = fun)

从语义上讲,fun0() 似乎比这个解决方案中暗示的更复杂,特别是如果每​​支球队失去一名球员,他们就会再次平局,正如@SunLisa 所说。如果是,请清理数据

df$player_off[is.na(df$player_off)] = 0

并更改fun0(),例如,

fun1 <- function(x, team) 
    is_team_1 <- team == head(team, 1) # is 'team' the first team?
    x1 <- x & is_team_1                # lost player & team 1
    x2 <- x & !is_team_1               # lost player & team 2
    cumsum(x1) == cumsum(x2)           # same total number of players?

(将逻辑返回值强制为整数似乎不是一个好主意)。这可以按组应用

df$is_even = ave(seq_len(nrow(df)), df$game_id, FUN = function(i) 
    fun1(df$player_off[i], df$team_id[i])
)

split(df$is_even, df$game_id) <-
    Map(fun1,
        split(df$player_off, df$game_id),
        split(df$team_id, df$game_id)
    )

ave() 的实现很有用,重要的一行是

split(x, g) <- lapply(split(x, g), FUN)

右侧将x 按组g 拆分,然后将FUN() 应用于每个组。左边的split&lt;-()是一个棘手的操作,使用组索引来更新原始向量x

评论

最初的问题要求“没有循环”,但实际上lapply()(在ave())和Map() 正是如此; ave() 相对高效,因为它采用了拆分-应用-组合策略,而不是 OP 可能实现的策略,它可能会遍历游戏,对数据帧进行子集化,然后为每个游戏更新 data.frame。子集将复制整个数据集的子集,特别是更新将至少复制每个分配的整个结果列;这种复制会大大减慢执行速度。 OP也有可能在fun0()上苦苦挣扎;这将有助于澄清问题,尤其是标题,以将其识别为问题。

有更快的方法,尤其是使用 data.table 包,但原理是一样的——识别一个以你喜欢的方式对向量进行操作的函数,然后按组应用它。

按照this suggestion 的另一种全矢量化解决方案按组计算累积总和。对于fun0(),将x标准化为在特定时间点离开游戏的玩家数量,没有NAs

x[is.na(x)] = 0

对于相当于fun(),计算离开游戏的玩家的累计总和,不考虑分组

cs = cumsum(x)

更正此累积和适用的组

in_game = cs - (grp - 1)

并在 0 名玩家离开游戏时将此设置为 'TRUE'

is_even = (in_game == 0)

这依赖于grp从1到组数的索引;对于这里的数据,可能是grp = match(df$game_id, unique(df$game_id))fun1() 也有类似的解决方案。

【讨论】:

尝试运行您的代码,is_even 列似乎超出了 1 个单元格。例如,在game_id = 100 中,答案应该是 7 个 1,其余为 0,但在您的答案中,答案是 8 个 1,其余为 0。 这是一个不错的开始,但它没有返回正确的向量。它给出了 is_even 中的最后 5 个数据点为 1,0,0,1,1,因为当球员在 time_ref=1004 被罚下时,它们都应该是假的。 所以对于ave(),team_id 无关紧要;不要将其作为分组变量。 很好,如果您从ave 中排除df$team_id,它会起作用。你能解释一下你的代码是做什么的,特别是你创建的fun 函数和基本的ave 函数吗? @Anonymous 我更新了答案以更详细地浏览代码。【参考方案2】:

这里有一个dplyr + tidyr 解决问题的方法,并总结了所做的工作:

    通过将player_off 中的所有 NA 转换为 0 来操作数据,以便于求和并将较小的 team_num(假设只有 2 个)分配给 team1,另一个分配给 team2 “统计”player_offs 使用 spread 并用 0 填充数据中的无效组合 - 例如,在 game_id = 100 中,time_ref = 1000 没有 team_id = 11 取 lagged team1team2 向量的累积和(当然,用 0 填充 NA)

代码如下:

require(dplyr)
require(tidyr)

df %>%
  group_by(game_id) %>%
  mutate(
    player_off = player_off %>% replace(list = is.na(.), values = 0),
    team_num = if_else(team_id == min(team_id), "team1", "team2")
  ) %>%
  spread(key = team_num, value = player_off, fill = 0) %>%
  arrange(game_id, time_ref) %>%
  mutate(
    team1_cum = cumsum(lag(team1, default = 0)),
    team2_cum = cumsum(lag(team2, default = 0)),
    is_even = as.integer(team1_cum == team2_cum)
  ) %>%
  ungroup() %>%
  select(-team1, -team2, -team1_cum, -team2_cum)

输出:

# A tibble: 20 x 5
   game_id team_id action_id time_ref is_even
     <int>   <int>     <int>    <int>   <int>
 1     100      10         1     1000       1
 2     100      10         1     1001       1
 3     100      10         1     1002       1
 4     100      11         1     1003       1
 5     100      11         2     1004       1
 6     100      11         1     1005       1
 7     100      10         3     1006       1
 8     100      11         1     1007       0
 9     100      10         1     1008       0
10     100      10         1     1009       0
11     101      12         3     1000       1
12     101      12         1     1001       1
13     101      12         1     1002       1
14     101      13         2     1003       1
15     101      13         3     1004       1
16     101      12         1     1005       0
17     101      13         1     1006       0
18     101      13         1     1007       0
19     101      12         1     1008       0
20     101      12         1     1009       0

【讨论】:

【参考方案3】:

这是我的想法:

data.table 会很好地工作,尤其是在处理大型数据集时。它更快。我们只需要将它分组,cumsum 2 队的裁员,看看他们是否相等。

首先我要说:

(问题由 Martin Morgan 解决,他更新的答案不再有这个错误)

我不认为@Martin Morgan 的回答是正确的。让我们想象一个特定的案例:

当第 1 队让一名球员下场,然后第 2 队让另一名球员下场,那么 2 队应该是平的,但@Martin Morgan 的输出将是FALSE

我将用这个数据集做一个例子,其中record 19player_off被修改为1,这意味着在游戏中101,在team 13之后有1 player off1004team 1210081 player off,即使在1009 也可以组成2 个团队。

> dt.1
   game_id team_id action_id player_off time_ref
1      100      10         1         NA     1000
2      100      10         1         NA     1001
3      100      10         1         NA     1002
4      100      11         1         NA     1003
5      100      11         2         NA     1004
6      100      11         1         NA     1005
7      100      10         3          1     1006
8      100      11         1         NA     1007
9      100      10         1         NA     1008
10     100      10         1         NA     1009
11     101      12         3          0     1000
12     101      12         1         NA     1001
13     101      12         1         NA     1002
14     101      13         2         NA     1003
15     101      13         3          1     1004
16     101      12         1         NA     1005
17     101      13         1         NA     1006
18     101      13         1         NA     1007
19     101      12         1          1     1008
20     101      12         1         NA     1009

但是@Martin Morgan 的函数会产生这个输出:

> dt.1$is_even = ave(df$player_off, df$game_id, FUN = fun)
> dt.1
   game_id team_id action_id player_off time_ref is_even
1      100      10         1         NA     1000       1
2      100      10         1         NA     1001       1
3      100      10         1         NA     1002       1
4      100      11         1         NA     1003       1
5      100      11         2         NA     1004       1
6      100      11         1         NA     1005       1
7      100      10         3          1     1006       1
8      100      11         1         NA     1007       0
9      100      10         1         NA     1008       0
10     100      10         1         NA     1009       0
11     101      12         3          0     1000       1
12     101      12         1         NA     1001       1
13     101      12         1         NA     1002       1
14     101      13         2         NA     1003       1
15     101      13         3          1     1004       1
16     101      12         1         NA     1005       0
17     101      13         1         NA     1006       0
18     101      13         1         NA     1007       0
19     101      12         1          1     1008       0
20     101      12         1         NA     1009       0

请注意line 19line 20is.even=0 的方法。这不是 op 想要的。

我的代码没有处理NAs,所以我先把NA转换成0

> dt.1<-as.data.table(dt.1)
> dt.1[is.na(dt.1)]<-0

我的代码会在10081009 时产生正确的输出,其中team 12team 13 均获得1 分,两支球队平分。

> dt.1[,.(action_id,team2_off=(team_id==max(team_id))*player_off,team1_off=(team_id==min(team_id))*player_off,team_id,time_ref,player_off),by=game_id][order(game_id,time_ref)][,.(team_id,time_ref,action_id,player_off,even=as.numeric(cumsum(team2_off)==cumsum(team1_off))),by=game_id]
    game_id team_id time_ref action_id player_off even
 1:     100      10     1000         1          0    1
 2:     100      10     1001         1          0    1
 3:     100      10     1002         1          0    1
 4:     100      11     1003         1          0    1
 5:     100      11     1004         2          0    1
 6:     100      11     1005         1          0    1
 7:     100      10     1006         3          1    0
 8:     100      11     1007         1          0    0
 9:     100      10     1008         1          0    0
10:     100      10     1009         1          0    0
11:     101      12     1000         3          0    1
12:     101      12     1001         1          0    1
13:     101      12     1002         1          0    1
14:     101      13     1003         2          0    1
15:     101      13     1004         3          1    0
16:     101      12     1005         1          0    0
17:     101      13     1006         1          0    0
18:     101      13     1007         1          0    0
19:     101      12     1008         1          1    1
20:     101      12     1009         1          0    1

我知道这是一段看起来很乱的 data.table 代码,让我一步一步解释。

dt[, .(
  action_id,
  team2_off = (team_id == max(team_id)) * player_off,
  team1_off = (team_id == min(team_id)) * player_off,
  team_id,
  time_ref,
  player_off
), by = game_id][order(game_id, time_ref)][, .(team_id,
                                               time_ref,
                                               action_id,
                                               player_off,
                                               even = cumsum(team2_off) == cumsum(team1_off)), by = game_id]

首先,我们取data.table dt,按game_id分组,然后计算:

  team2_off = (team_id == max(team_id)) * player_off,
  team1_off = (team_id == min(team_id)) * player_off

data.table 同时进行 2 个分组(按 game_idteam_id 分组)存在一些问题,但它可以很好地处理每个组内的逻辑表达式。这样,通过将team_id == max/min(team_id) 的逻辑输出与player_off 相乘,我们有效地得到team1_offteam2_off。当两者都为 1 时,输出将为 1,这意味着所选球队中有 1 名球员下场。

现在我们有一个数据表:

> dt.1[,.(action_id,team2_off=(team_id==max(team_id))*player_off,team1_off=(team_id==min(team_id))*player_off,team_id,time_ref,player_off),by=game_id]
    game_id action_id team2_off team1_off team_id time_ref player_off
 1:     100         1         0         0      10     1000          0
 2:     100         1         0         0      10     1001          0
 3:     100         1         0         0      10     1002          0
 4:     100         1         0         0      11     1003          0
 5:     100         2         0         0      11     1004          0
 6:     100         1         0         0      11     1005          0
 7:     100         3         0         1      10     1006          1
 8:     100         1         0         0      11     1007          0
 9:     100         1         0         0      10     1008          0
10:     100         1         0         0      10     1009          0
11:     101         3         0         0      12     1000          0
12:     101         1         0         0      12     1001          0
13:     101         1         0         0      12     1002          0
14:     101         2         0         0      13     1003          0
15:     101         3         1         0      13     1004          1
16:     101         1         0         0      12     1005          0
17:     101         1         0         0      13     1006          0
18:     101         1         0         0      13     1007          0
19:     101         1         0         1      12     1008          1
20:     101         1         0         0      12     1009          0

现在我们不再需要按两组(team_idgame_id)进行分组,我们可以通过game_id 来做cumsum,然后比较cumsum(team1_off)==cumsum(team2_off),还有order 和@ 987654365@ 和 time_ref,因此结果将具有正确的顺序。

我了解NAs 在这种情况下可能与0 具有不同的含义。如果您真的很在意,只需创建一个player_offdummy 列。

> dt$dummy<-dt$player_off
> dt$dummy[is.na(dt$dummy)]<-0
> dt<-as.data.table(dt)
> dt[, .(
+   action_id,
+   team2_off = (team_id == max(team_id)) * dummy,
+   team1_off = (team_id == min(team_id)) * dummy,
+   team_id,
+   time_ref,
+   player_off
+ ), by = game_id][order(game_id, time_ref)][, .(team_id,
+                                                time_ref,
+                                                action_id,
+                                                player_off,
+                                                even = as.numeric(cumsum(team2_off) == cumsum(team1_off))), by = game_id]
    game_id team_id time_ref action_id player_off even
 1:     100      10     1000         1         NA    1
 2:     100      10     1001         1         NA    1
 3:     100      10     1002         1         NA    1
 4:     100      11     1003         1         NA    1
 5:     100      11     1004         2         NA    1
 6:     100      11     1005         1         NA    1
 7:     100      10     1006         3          1    0
 8:     100      11     1007         1         NA    0
 9:     100      10     1008         1         NA    0
10:     100      10     1009         1         NA    0
11:     101      12     1000         3          0    1
12:     101      12     1001         1         NA    1
13:     101      12     1002         1         NA    1
14:     101      13     1003         2         NA    1
15:     101      13     1004         3          1    0
16:     101      12     1005         1         NA    0
17:     101      13     1006         1         NA    0
18:     101      13     1007         1         NA    0
19:     101      12     1008         1         NA    0
20:     101      12     1009         1         NA    0

我真的认为你的问题很有趣,我致力于使用 data.table 来解决这个问题。花了我几个小时,我几乎放弃了 data.table,认为 data.table 不能一次处理两个分组。我最终用逻辑乘法解决了它。

我玩得很开心

  team1_off = (team_id == min(team_id)) * dummy
  team2_off = (team_id == max(team_id)) * dummy

【讨论】:

我同意我的回答不适用于两支球队都输球的情况;最初的问题并不清楚,尽管语义上is_even 暗示了您的解释。我更新了我的回复以反映不同的解释。 @Martin Morgan,您的函数解决方案给我留下了深刻的印象。正如您在帖子中所说,最终的想法是获得一个可以产生正确结果的函数。虽然这个问题有点棘手,但您的解决方案非常鼓舞人心!谢谢你的灵感。我需要一些时间来理解你的功能。

以上是关于R中向具有大量数据集的数据框添加新列的有效方法的主要内容,如果未能解决你的问题,请参考以下文章

在r语言中怎样在数据框中添加新列

在具有循环或 lambda 的多个数据帧中添加具有相同字符串值的列的更有效方法?

在 R 中组合大量数据集的更快方法?

将宽数据帧转换为具有特定条件并添加新列的长数据帧

如何根据应用于大量列的“不等于”标准对数据框进行子集化?

R中的dplyr mutate - 添加列作为列的连接