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
设置为TRUE
或FALSE
并链接到action_id=3
。 action_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
我需要的是数据框中的另一列,它给我TRUE
或FALSE
在每个动作(行)发生时两支球队在场上的球员人数是否相等/不相等。
所以game_id=100
在time_ref=1006
有一个action_id=3
和player_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
,随后标记为不均匀或0
。 game_id=101
与 time_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<-()
是一个棘手的操作,使用组索引来更新原始向量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_off
s 使用 spread
并用 0 填充数据中的无效组合 - 例如,在 game_id
= 100 中,time_ref
= 1000 没有 team_id
= 11
取 lag
ged team1
和 team2
向量的累积和(当然,用 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 19
的player_off
被修改为1
,这意味着在游戏中101
,在team 13
之后有1 player off
1004
,team 12
在1008
有1 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 19
和 line 20
、is.even=0
的方法。这不是 op 想要的。
我的代码没有处理NA
s,所以我先把NA
转换成0
。
> dt.1<-as.data.table(dt.1)
> dt.1[is.na(dt.1)]<-0
我的代码会在1008
和1009
时产生正确的输出,其中team 12
和team 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_id
和 team_id
分组)存在一些问题,但它可以很好地处理每个组内的逻辑表达式。这样,通过将team_id == max/min(team_id)
的逻辑输出与player_off
相乘,我们有效地得到team1_off
和team2_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_id
,game_id
)进行分组,我们可以通过game_id
来做cumsum
,然后比较cumsum(team1_off)==cumsum(team2_off)
,还有order
和@ 987654365@ 和 time_ref
,因此结果将具有正确的顺序。
我了解NA
s 在这种情况下可能与0
具有不同的含义。如果您真的很在意,只需创建一个player_off
的dummy
列。
> 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中向具有大量数据集的数据框添加新列的有效方法的主要内容,如果未能解决你的问题,请参考以下文章