【问题标题】:Replace column value in a data frame based on other columns根据其他列替换数据框中的列值
【发布时间】:2018-11-06 01:05:56
【问题描述】:

我有以下按名称和时间排序的数据框。

set.seed(100)
df <- data.frame('name' = c(rep('x', 6), rep('y', 4)), 
                 'time' = c(rep(1, 2), rep(2, 3), 3, 1, 2, 3, 4),
                 'score' = c(0, sample(1:10, 3), 0, sample(1:10, 2), 0, sample(1:10, 2))
                 )
> df
   name time score
1     x    1     0
2     x    1     4
3     x    2     3
4     x    2     5
5     x    2     0
6     x    3     1
7     y    1     5
8     y    2     0
9     y    3     5
10    y    4     8

df$score 中有零后跟未知数量的实际值,即df[1:4,],有时两个df$score == 0 之间有重叠的df$name,即df[6:7,]

我想将df$time 更改为df$score != 0。具体来说,如果df$name 匹配,我想用df$score == 0 分配最近的上一行的时间值。

以下代码提供了良好的输出,但我的数据有数百万行,因此此解决方案效率非常低。

score_0 <- append(which(df$score == 0), dim(df)[1] + 1)

for(i in 1:(length(score_0) - 1)) {
  df$time[score_0[i]:(score_0[i + 1] - 1)] <-
    ifelse(df$name[score_0[i]:(score_0[i + 1] - 1)] == df$name[score_0[i]], 
           df$time[score_0[i]], 
           df$time[score_0[i]:(score_0[i + 1] - 1)])
 }

> df
   name time score
1     x    1     0
2     x    1     4
3     x    1     3
4     x    1     5
5     x    2     0
6     x    2     1
7     y    1     5
8     y    2     0
9     y    2     5
10    y    2     8

score_0 给出df$score == 0 所在的索引。我们看到df$time[2:4] 现在都等于 1,在df$time[6:7] 中只有第一个发生了变化,因为第二个有df$name == 'y',而最接近df$score == 0 的上一行有df$name == 'x'。最后两行也已正确更改。

【问题讨论】:

  • 如果df [ 7 , "time" ] 等于 2 会怎样?是否会更改为1,因为它是name==y 的第一个条目,或者您将保持不变?
  • @Masoud,您只需将其保持不变,因为 df$name 与最接近 df$score == 0 的上一行不匹配
  • 只是一个建议,当使用样本或其他随机函数时,使用set.seed,这样每个人都会得到相同的输出。干杯,欢迎加入社区。​​span>
  • @Masoud,感谢set.seed() 的提示和简洁的答案!

标签: r dataframe


【解决方案1】:

你可以这样做:

library(dplyr)
df %>% group_by(name) %>% mutate(ID=cumsum(score==0)) %>% 
       group_by(name,ID) %>% mutate(time = head(time,1)) %>% 
       ungroup() %>%  select(name,time,score) %>% as.data.frame()

#       name time  score
# 1     x    1     0
# 2     x    1     8
# 3     x    1    10
# 4     x    1     6
# 5     x    2     0
# 6     x    2     5
# 7     y    1     4
# 8     y    2     0
# 9     y    2     5
# 10    y    2     9

【讨论】:

    【解决方案2】:

    使用dplyrdata.table 的解决方案:

    library(data.table)
    library(dplyr)
    
    df %>%
      mutate(
        chck = score == 0,
        chck_rl = ifelse(score == 0, lead(rleid(chck)), rleid(chck))) %>% 
      group_by(name, chck_rl) %>% mutate(time = first(time)) %>% 
      ungroup() %>% 
      select(-chck_rl, -chck)
    

    输出:

    # A tibble: 10 x 3
       name   time score
       <chr> <dbl> <int>
     1 x         1     0
     2 x         1     2
     3 x         1     9
     4 x         1     7
     5 x         2     0
     6 x         2     1
     7 y         1     8
     8 y         2     0
     9 y         2     2
    10 y         2     3
    

    仅使用data.table的解决方案:

    library(data.table)
    
    setDT(df)[, chck_rl := ifelse(score == 0, shift(rleid(score == 0), type = "lead"), 
        rleid(score == 0))][, time := first(time), by = .(name, chck_rl)][, chck_rl := NULL]
    

    输出:

       name time score
     1:    x    1     0
     2:    x    1     2
     3:    x    1     9
     4:    x    1     7
     5:    x    2     0
     6:    x    2     1
     7:    y    1     8
     8:    y    2     0
     9:    y    2     2
    10:    y    2     3
    

    【讨论】:

    • 考虑使用cumsumrleid 在许多场景中起作用,是一个强大的工具,但对于这个特定问题效率不高。您的data.table 解决方案可以通过它得到改进,但是它已经是一个很好的实现。 +1
    • 非常感谢@Masoud 的建议。我一直优先考虑 rleid 而不是 cumsum,但经过一些基准测试后,它确实表明它可能并不总是最好的替代品。
    猜你喜欢
    • 1970-01-01
    • 2021-02-20
    • 2020-10-21
    • 2013-10-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多