如何按组替换NA为最近的非NA?

5

我有一个个体数据框,其中包含以下不完整和重复的特征:

    name <- c("A", "A", "B", "B", "B", "C", "D", "D")
    age <- c(28,NA,NA,NA,NA,NA,53,NA)
    birthplace <- c("city1",NA, "city2",NA,NA,NA,NA,NA)
    value <- 100:107
    df <- data.frame(name,age,birthplace,value)

    name age birthplace value
1    A  28      city1   100
2    A  NA       <NA>   101
3    B  NA      city2   102
4    B  NA       <NA>   103
5    B  NA       <NA>   104
6    C  NA       <NA>   105
7    D  53       <NA>   106
8    D  NA       <NA>   107

由于每行的值都是唯一的,我想要用可用人员的详细信息来完成每一行,如下所示:

       name age birthplace value
    1    A  28      city1   100
    2    A  28      city1   101
    3    B  NA      city2   102
    4    B  NA      city2   103
    5    B  NA      city2   104
    6    C  NA       <NA>   105
    7    D  53       <NA>   106
    8    D  53       <NA>   107

我尝试使用

library(zoo)
library(dplyr)
df <- df %>% group_by(name) %>% na.locf(na.rm=F)

但是它的效果并不好。有什么想法可以通过分组实现功能吗?


@alistaire,你指出的问题要求使用dplyr解决方案(即使答案与此无关),而在这里并没有指定这样的限制。 - Martin Morgan
@MartinMorgan 这个问题是,但不包括回答,回答涵盖了base、zoo alone、data.table等。回答中没有功能上的区别;dplyr只是在问题中使用的语法。 - alistaire
8个回答

9
作为另一个基于R语言的解决方案,这是一个穷人用的na.locf。
fill_down <- function(v) {
    if (length(v) > 1) {
        keep <- c(TRUE, !is.na(v[-1]))
        v[keep][cumsum(keep)]
    } else v
}

为了按组填充,可以使用tapply()进行分割和应用到每个组,还可以使用split<-将组合并到原始几何图形中,如下所示:
fill_down_by_group <- function(v, grp) {
    ## original 'by hand':
    ##     split(v, grp) <- tapply(v, grp, fill_down)
    ##     v
    ## done by built-in function `ave()`
    ave(v, grp, FUN=fill_down)
}

要处理多个列,可以使用以下方法:

elts <- c("age", "birthplace")
df[elts] <- lapply(df[elts], fill_down_by_group, df$name)

注释

  1. I would be interested in seeing how a dplyr solution handles many columns, without hard-coding each? Answering my own question, I guess this is

    library(dplyr); library(tidyr)
    df %>% group_by(name) %>% fill_(elts)
    
  2. A more efficient base solution when the groups are already 'grouped' (e.g., identical(grp, sort(grp))) is

    fill_down_by_grouped <- function(v, grp) {
        if (length(v) > 1) {
            keep <- !(duplicated(v) & is.na(v))
            v[keep][cumsum(keep)]
        } else v
    }
    
  3. For me, fill_down() on a vector with about 10M elements takes ~225ms; fill_down_by_grouped() takes ~300ms independent of the number of groups; fill_down_by_group() scales with the number of groups; for 10000 groups ~2s, 10M groups about 36s


1
这是我第一次见到split<-。真的很棒。 - Pierre L
我没有仔细阅读原帖,但这似乎是ave在这里替代了*_by_group的一种方法:lapply(df[elts], function(x) ave(x, df$name, FUN = fill_down)) - Frank
1
@Frank 是的,ave() 是一个我经常忘记的好选择,谢谢。 - Martin Morgan
谢谢你提供的解决方案。它能用,不过为了解决问题而使用两个函数有点过分了,是吧? - Lingyu Kong

3

也可能是:

library(dplyr)
library(tidyr)
df %>% group_by(name) %>% fill(age, birthplace)

# Source: local data frame [8 x 4]
# Groups: name [4]

#     name   age birthplace value
#   <fctr> <dbl>     <fctr> <int>
# 1      A    28      city1   100
# 2      A    28      city1   101
# 3      B    NA      city2   102
# 4      B    NA      city2   103
# 5      B    NA      city2   104
# 6      C    NA         NA   105
# 7      D    53         NA   106
# 8      D    53         NA   107

3
便捷的代码:fill(everything()),意为“填充所有内容”。 - alistaire
@alistaire 像往常一样,更简洁的答案。 - Psidom

2
你可以将na.locf包装在do中。
df %>% group_by(name) %>% do(na.locf(., na.rm = FALSE))

1
do()强制转换为字符; 可能是mutate(age = na.locf(age,na.rm = FALSE),birthplace = na.locf(birthplace,na.rm = FALSE)) - Martin Morgan
1
我认为我们应该使用这个 df %>% group_by(name) %>% mutate_each(funs(na.locf(.,na.rm = FALSE))) - user2100721
2
新版本:df %>% group_by(name) %>% mutate_all(zoo::na.locf, na.rm = FALSE) 或者像 Psidom 的方法一样直接使用 tidyr::fill - alistaire

2

根据您接下来要做的事情,您可能更喜欢以嵌套形式呈现数据。

(nested <- df %>% 
  group_by(name) %>% 
  summarize(
    age = na.omit(age)[1], 
    birthplace = na.omit(birthplace)[1], 
    value = list(value)
  )
)
## # A tibble: 4 x 4
##     name   age birthplace     value
##   <fctr> <dbl>     <fctr>    <list>
## 1      A    28      city1 <int [2]>
## 2      B    NA      city2 <int [3]>
## 3      C    NA         NA <int [1]>
## 4      D    53         NA <int [2]>

如果您需要对单个value进行计算,您随时可以稍后取消嵌套。

nested %>% tidyr::unnest()
## # A tibble: 8 x 4
##     name   age birthplace value
##   <fctr> <dbl>     <fctr> <int>
## 1      A    28      city1   100
## 2      A    28      city1   101
## 3      B    NA      city2   102
## 4      B    NA      city2   103
## 5      B    NA      city2   104
## 6      C    NA         NA   105
## 7      D    53         NA   106
## 8      D    53         NA   107

1
这是一个基本的 R 解决方案:
do.call(rbind,lapply(split(df, df$name), function(x) {
    tempdf <- x
    if (nrow(tempdf) > length(which(is.na(x$birthplace)))) {
        tempdf[which(is.na(x$birthplace)),c("age","birthplace")] <- tempdf[which(is.na(x$birthplace))[1]-1,c("age","birthplace")]
    }
    return(tempdf)
}))

输出:

 name age birthplace value
 A    28  city1      100  
 A    28  city1      101  
 B    NA  city2      102  
 B    NA  city2      103  
 B    NA  <NA>       104  
 C    NA  <NA>       105  
 D    53  <NA>       106  
 D    NA  <NA>       107 

1
这里有一个基于R语言的解决方案。使用fill函数调用ave,并使用na.omit(x)[1],就像Richie Cotton的解决方案一样。
fill <- function(...) ave(..., FUN = function(x) na.omit(x)[1])
transform(df, birthplace = fill(birthplace, name), age = fill(age, name))

注意:这也适用于na.locf。将fill替换为:

library(zoo)
fill <- function(...) ave(..., FUN = function(x) na.locf(x, na.rm = FALSE))

0
你也可以通过合并来实现这个。只需在名称列上执行联接,然后按值进行分组即可。
library(sqldf)
sqldf('select t1.name, t2.age, t2.birthplace,t1.value from df t1 inner join df t2 on t1.name=t2.name group by t1.value')

0

还可以考虑一个嵌套的应用程序基础解决方案,对每列运行一个滚动head()

df <- setNames(data.frame(lapply(names(df), function(d)
               sapply(1:nrow(df), function(i)
                      head(df[df[1:i, c("name")] == df$name[i], c(d)], 1))
        )), names(df))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接