高效地按组填充缺失值

12
我有一个数据集,我观察一些人的变量,而对其他人不观察。对于那些我观察到变量的个体,我只观察一次。但是,每个个体的观察次数以及观察值的位置都会发生变化。
我想使用非NA值来填充给定个体的所有NA值,如果有非NA值的话。否则,NA应该保持为NA。
以下是一个示例数据集:
#data.frame of 100 individuals with 10 observations each
data <- data.frame(group = rep(1:100,each=10),value = NA)

#first 50 individuals get a value at the fifth observation, others don't have value
data$value[seq(5,500,10)] <- rnorm(50)

目前为止还好,没有大问题。从另一个线程中提取,我们可以使用 dplyrtidyr 来做类似以下的事情:

data <- data %>% 
  group_by(group) %>% #by group
  fill(value) %>% #default direction down
  fill(value, .direction = "up") #also fill NAs upwards

这个方法完美地解决了问题。不过,我需要对大约80mio.的观测数据执行此操作,这需要数小时的时间。有更快的方法吗?我认为data.table可能是一个不错的选择。
如果可以仅填充出现在值之前的NAs,那将非常好。
谢谢!
3个回答

8
您可以使用data.table和dplyr两种方法来处理数据,这种方法简单易行,而且我相信会非常快速高效:
在data.table中:
library(data.table)
setDT(data)
data[, value := value[!is.na(value)][1L], by = group]

或者使用dplyr:
library(dplyr)
data <- data %>% 
  group_by(group) %>% 
  mutate(value = value[!is.na(value)][1L])

重点是每个组中有一个非NA值,恰好出现一次。因此,您不需要最后观察到的向前逻辑。只需取第一个非NA值(如果存在)。


3
我们可以使用 data.table 进行就地赋值。这里,使用来自 zoona.locf 来用相邻的非 NA 元素填充 NA 元素。
library(data.table)
library(zoo)
setDT(data)[, value := na.locf(na.locf(value, na.rm = FALSE), fromLast = TRUE), group]

基准测试

set.seed(24)
data1 <- data.frame(group = rep(1:1e6,each=10),value = NA)
data1$value[seq(5,1e6,10)] <- rnorm(100000)

data2 <- copy(data1)

system.time({setDT(data2)[, value := na.locf(na.locf(value, 
             na.rm = FALSE), fromLast = TRUE), group]})
#   user  system elapsed 
# 70.681   0.294  70.917 


system.time({

data1 %>% 
  group_by(group) %>% #by group
  fill(value) %>% #default direction down
  fill(value, .direction = "up")

})
# 17% ~33 m remaining 

注意:这需要花费很长时间,所以必须中止会话。

注意2:此方法基于以下假设:我们希望将NA元素替换为非NA相邻元素,并且每个组中有多个非NA元素。


1
非常感谢!我也进行了基准测试(10万个观测值),发现你的方法所需时间约为tidyverse方法的五分之一。很有趣,看看它如何扩展! - yrx1702
@Mr.Zen 对我来说,dplyr方法只完成了27%,而且花费了更多的时间。 - akrun

2
这是我使用的代码:您的代码 vs akrun vs 我的代码。有时,zoo 不是最快的过程,但它是最干净的。无论如何,您可以进行测试。
更新: 已经测试了更多数据(100,000),处理 03(子集和合并)远胜出。
最后更新: 使用 rbenchmark 进行函数比较:
library(dplyr)
library(tidyr)
library(base)
library(data.table)
library(zoo)
library(rbenchmark)

#data.frame of 100 individuals with 10 observations each
data <- data.frame(group = rep(1:10000,each=10),value = NA)
data$value[seq(5,5000,10)] <- rnorm(50) #first 50 individuals get a value at the fifth observation, others don't have value

#Process01
P01 <- function (data){
    data01 <- data %>% 
        group_by(group) %>% #by group
            fill(value) %>% #default direction down
            fill(value, .direction = "up") #also fill NAs upwards
    return(data01)
}

#Process02
P02 <- function (data){
    data02 <- setDT(data)[, value := na.locf(na.locf(value, na.rm = FALSE), 
                                             fromLast = TRUE), group]
    return(data02)
}

#Process03
P03 <- function (data){
    dataU <- subset(unique(data), value!='NA') #keep row number
    dataM <- merge(data, dataU, by = "group", all=T) #merge tables
    data03 <- data.frame(group=dataM$group, value = dataM$value.y) #idem shape of data
    return(data03)
}

benchmark("P01_dplyr" = {data01 <- P01(data)},
          "P02_zoo" = {data02 <- P02(data)},
          "P03_data.table" = {data03 <- P03(data)},
          replications = 10,
          columns = c("test", "replications", "elapsed")
          )

数据为10,000,重复10次,使用I5 7400的结果如下:

    test replications elapsed
1      P01_dplyr           10  257.78
2        P02_zoo           10   10.35
3 P03_data.table           10    0.09

2
可以考虑学习 bench 包或 microbenchmark。有时候你需要定义示例数据,有时候不需要,而且 OP 提到他的数据非常大...我想在基准测试时应该考虑到这一点。 - s_baldur
我已经尝试增加数据和处理,03胜出得远。 - César Arquero

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