使用 dplyr 按组将 NA 替换为上一个或下一个值

Posted

技术标签:

【中文标题】使用 dplyr 按组将 NA 替换为上一个或下一个值【英文标题】:Replace NA with previous or next value, by group, using dplyr 【发布时间】:2017-02-23 17:57:41 【问题描述】:

我有一个按日期降序排列的数据框。

ps1 = data.frame(userID = c(21,21,21,22,22,22,23,23,23), 
             color = c(NA,'blue','red','blue',NA,NA,'red',NA,'gold'), 
             age = c('3yrs','2yrs',NA,NA,'3yrs',NA,NA,'4yrs',NA), 
             gender = c('F',NA,'M',NA,NA,'F','F',NA,'F') 
)

我希望用以前的值估算(替换)NA 值 并按用户 ID 分组 如果用户 ID 的第一行有 NA,则替换为该用户 ID 组的下一组值。

我正在尝试使用类似这样的 dplyr 和 zoo 包...但它不起作用

cleanedFUG <- filteredUserGroup %>%
 group_by(UserID) %>%
 mutate(Age1 = na.locf(Age), 
     Color1 = na.locf(Color), 
     Gender1 = na.locf(Gender) ) 

我需要这样的结果 df:

                      userID color  age gender
                1     21  blue 3yrs      F
                2     21  blue 2yrs      F
                3     21   red 2yrs      M
                4     22  blue 3yrs      F
                5     22  blue 3yrs      F
                6     22  blue 3yrs      F
                7     23   red 4yrs      F
                8     23   red 4yrs      F
                9     23  gold 4yrs      F

【问题讨论】:

看看这是否有帮助。 Replacing NAs with latest non-NA value 或这个replace NA value with the group value 【参考方案1】:
require(tidyverse) #fill is part of tidyr

ps1 %>% 
  group_by(userID) %>% 
  fill(color, age, gender) %>% #default direction down
  fill(color, age, gender, .direction = "up")

这给了你:

Source: local data frame [9 x 4]
Groups: userID [3]

  userID  color    age gender
   <dbl> <fctr> <fctr> <fctr>
1     21   blue   3yrs      F
2     21   blue   2yrs      F
3     21    red   2yrs      M
4     22   blue   3yrs      F
5     22   blue   3yrs      F
6     22   blue   3yrs      F
7     23    red   4yrs      F
8     23    red   4yrs      F
9     23   gold   4yrs      F

【讨论】:

fill() 函数已更新为允许同时填充两个方向,而无需使用两次。 .direction 选项现在包括 downupupdown 如果你的数据集很大并且你不能写每个列的名字怎么办 @Amit 你可以使用 fill(everything()) 来填充所有列【参考方案2】:

无论userID 组如何,直接在整个data.frame 上使用zoo::na.locf 都会填充NA。不幸的是,包 dplyr 的分组对 na.locf 函数没有影响,这就是我选择拆分的原因:

library(dplyr); library(zoo)
ps1 %>% split(ps1$userID) %>% 
  lapply(function(x) na.locf(na.locf(x), fromLast=T)) %>% 
  do.call(rbind, .)
####      userID color  age gender
#### 21.1     21  blue 3yrs      F
#### 21.2     21  blue 2yrs      F
#### 21.3     21   red 2yrs      M
#### 22.4     22  blue 3yrs      F
#### 22.5     22  blue 3yrs      F
#### 22.6     22  blue 3yrs      F
#### 23.7     23   red 4yrs      F
#### 23.8     23   red 4yrs      F
#### 23.9     23  gold 4yrs      F

它的作用是首先将数据拆分为 3 个 data.frame,然后我应用第一次插补(向下),然后使用 lapply 中的匿名函数向上,最终使用 rbind 带来data.frames 重新组合在一起。你有预期的输出。

【讨论】:

您可以将do.call() 替换为更惯用的bind_rows(),将split(ps1$userID) 替换为split(.$userID) 使用purrr 的另一种选择也可以是:library(purrr); ps1 %&gt;% slice_rows("userID") %&gt;% by_slice(function(x) na.locf(na.locf(x), fromLast=T) , .collate = "rows") @StevenBeaupré 不错!这本身就值得一个新的答案;-)【参考方案3】:

我写了这个函数,它肯定比 fill 快,可能比 na.locf 快:

fill_NA <- function(x) 
  which.na <- c(which(!is.na(x)), length(x) + 1)
  values <- na.omit(x)

  if (which.na[1] != 1) 
    which.na <- c(1, which.na)
    values <- c(values[1], values)
  

  diffs <- diff(which.na)
  return(rep(values, times = diffs))

【讨论】:

感谢您分享您的代码,该代码独立于分组变量(我的数据中没有)并且不需要安装 zoo 包。 除了我一直在使用 tidyr,我发现 tidyr::fill 使用管道更优雅地完成了这项工作,而无需使用 mutate()。【参考方案4】:

将@agenis 方法与na.locf() 结合使用purrr,您可以这样做:

library(purrr)
library(zoo)

ps1 %>% 
  slice_rows("userID") %>% 
  by_slice(function(x)  
    na.locf(na.locf(x), fromLast=T) , 
    .collate = "rows") 

【讨论】:

【参考方案5】:

几年后,我发现情况发生了变化。 使用@Steven Beaupré 的方法,

1) 添加na.rm=F 确保不会删除/排除任何行。 2)slide_rows()函数可以在purrrlyr包中找到。

library(purrrlyr)
library(zoo)

ps1 %>% 
  slice_rows("userID") %>% 
  by_slice(function(x)  
    na.locf(na.locf(x, na.rm=F), fromLast=T, na.rm=F) , 
    .collate = "rows") 

【讨论】:

以上是关于使用 dplyr 按组将 NA 替换为上一个或下一个值的主要内容,如果未能解决你的问题,请参考以下文章

如果所有汇总值都是NA,则dplyr汇总保留NA

使用dplyr将R中的所有NA值替换为0

重塑数据框以按组将值设为列[重复]

R语言dplyr包na_if函数根据条件将数据对象替换为NA值实战

替换列条件 dplyr

按组将一列转换为多列