缺失时间步骤添加行的最快方法是什么?

38

我有一个数据集中的列,其中时间段 (Time) 是从a到b的整数。对于任何给定的组,有时可能会缺少时间段。我想用 NA 来填充这些行。下面是一个(几千个中的一个)组的示例数据。

structure(list(Id = c(1, 1, 1, 1), Time = c(1, 2, 4, 5), Value = c(0.568780482159894, 
-0.7207749516298, 1.24258192959273, 0.682123081696789)), .Names = c("Id", 
"Time", "Value"), row.names = c(NA, 4L), class = "data.frame")


  Id Time      Value
1  1    1  0.5687805
2  1    2 -0.7207750
3  1    4  1.2425819
4  1    5  0.6821231

如您所见,时间3缺失了。通常一个或多个可能会丢失。我可以自己解决,但是担心我不是以最有效的方式进行操作。我的方法是创建一个函数来实现以下步骤:

min(Time)max(Time)生成一系列时间段

然后使用setdiff来获取缺失的Time值。

将该向量转换为data.frame

提取唯一标识符变量(Id和其他未在上面列出的变量),并将其添加到此数据框中。

合并两个数据框。

从函数返回结果。

因此,整个过程将执行如下:

   # Split the data into individual data.frames by Id.
    temp_list <- dlply(original_data, .(Id)) 
    # pad each data.frame
    tlist2 <- llply(temp_list, my_pad_function)
    # collapse the list back to a data.frame
    filled_in_data <- ldply(tlist2)

有更好的方法来实现这个吗?


我会基本上按照你描述的方式做,只是使用expand.grid,然后与 all = TRUE合并。不确定首先按ID拆分是否必要。 - joran
这里有一个额外的复杂性,有许多id变量。我只需要添加“Time”,并将“Value”设置为“NA”,然后填充其余部分。因此,它变成了data_to_merge <- data.frame(id=unique(data$id),...)(这是一行非常长的代码,如果数据结构发生更改,则不可移植)。但愿我能够高效地合并缺失的时间,添加NA,并从原始数据中获取所有其他内容。 - Maiasaura
我现在已经让它工作了,但仍然需要一个通用解决方案,因为这将是一个包,并且我不知道用户可能会提交什么样的原始数据。 - Maiasaura
对于未分组的数据,还可以参考如何在R中将缺失值添加为零中的几个不错的答案。 - Henrik
4个回答

38

针对Ben Barnes的评论并从他的mydf3开始:

DT = as.data.table(mydf3)
setkey(DT,Id,Time)
DT[CJ(unique(Id),seq(min(Time),max(Time)))]
      Id Time        Value Id2
 [1,]  1    1 -0.262482283   2
 [2,]  1    2 -1.423935165   2
 [3,]  1    3  0.500523295   1
 [4,]  1    4 -1.912687398   1
 [5,]  1    5 -1.459766444   2
 [6,]  1    6 -0.691736451   1
 [7,]  1    7           NA  NA
 [8,]  1    8  0.001041489   2
 [9,]  1    9  0.495820559   2
[10,]  1   10 -0.673167744   1
First 10 rows of 12800 printed. 

setkey(DT,Id,Id2,Time)
DT[CJ(unique(Id),unique(Id2),seq(min(Time),max(Time)))]
      Id Id2 Time      Value
 [1,]  1   1    1         NA
 [2,]  1   1    2         NA
 [3,]  1   1    3  0.5005233
 [4,]  1   1    4 -1.9126874
 [5,]  1   1    5         NA
 [6,]  1   1    6 -0.6917365
 [7,]  1   1    7         NA
 [8,]  1   1    8         NA
 [9,]  1   1    9         NA
[10,]  1   1   10 -0.6731677
First 10 rows of 25600 printed. 

CJ 代表交叉连接,参见 ?CJ。使用 NA 进行填充是因为默认情况下 nomatchNA。将 nomatch 设置为 0 可以去除未匹配项的填充。如果需要的是最接近的一行而不是填充 NA,只需添加 roll=TRUE。这比先用 NA 填充然后再填充 NA 要更有效率。请参见 ?data.table 中的 roll 描述。

setkey(DT,Id,Time)
DT[CJ(unique(Id),seq(min(Time),max(Time))),roll=TRUE]
      Id Time        Value Id2
 [1,]  1    1 -0.262482283   2
 [2,]  1    2 -1.423935165   2
 [3,]  1    3  0.500523295   1
 [4,]  1    4 -1.912687398   1
 [5,]  1    5 -1.459766444   2
 [6,]  1    6 -0.691736451   1
 [7,]  1    7 -0.691736451   1
 [8,]  1    8  0.001041489   2
 [9,]  1    9  0.495820559   2
[10,]  1   10 -0.673167744   1
First 10 rows of 12800 printed. 

setkey(DT,Id,Id2,Time)
DT[CJ(unique(Id),unique(Id2),seq(min(Time),max(Time))),roll=TRUE]
      Id Id2 Time      Value
 [1,]  1   1    1         NA
 [2,]  1   1    2         NA
 [3,]  1   1    3  0.5005233
 [4,]  1   1    4 -1.9126874
 [5,]  1   1    5 -1.9126874
 [6,]  1   1    6 -0.6917365
 [7,]  1   1    7 -0.6917365
 [8,]  1   1    8 -0.6917365
 [9,]  1   1    9 -0.6917365
[10,]  1   1   10 -0.6731677
First 10 rows of 25600 printed. 

不必设置键,可以使用onCJ还带有unique参数。以下是一个包含两个'Id'的小例子:

d <- data.table(Id = rep(1:2, 4:3), Time = c(1, 2, 4, 5, 2, 3, 4), val = 1:7)

d[CJ(Id, Time = seq(min(Time), max(Time)), unique = TRUE), on = .(Id, Time)]
#     Id Time val
# 1:   1    1   1
# 2:   1    2   2
# 3:   1    3  NA
# 4:   1    4   3
# 5:   1    5   4
# 6:   2    1  NA
# 7:   2    2   5
# 8:   2    3   6
# 9:   2    4   7
# 10:  2    5  NA
在这种特殊情况下,如果 CJ 中的一个向量是使用 seq 生成的,则需要明确命名结果以便与 on 中指定的名称匹配。但是,在 CJ 中使用裸变量时(例如此处的 'Id'),它们会自动命名,就像在 data.table(来自 data.table 1.12.2) 中一样。

1
我从未完全理解roll的用途;它如何帮助我获取NA值? - rbatt
2
@sirallen 请参考以下两个链接:Efficient way to Fill Time-Series per groupFilling missing dates by group - Henrik
1
@MattDowle 也许你可以编辑你非常好的答案,加入一些最近的功能,例如on作为setkey的替代品,以及在CJ中使用unique参数。干杯 - Henrik
1
@MattDowle 我进行了编辑。希望看起来没问题。干杯。 - Henrik
1
@Henrik 看起来很棒。谢谢。 - Matt Dowle
显示剩余4条评论

10
你可以使用tidyr来完成这个任务。
使用tidyr::complete函数来填充Time的缺失值,默认缺失值会被填充为NA

创建数据

我扩展了样本数据以显示它可以处理多个Id,即使在一个Id中也没有完整的Time范围。
library(dplyr)
library(tidyr)


df <- tibble(
  Id = c(1, 1, 1, 1, 2, 2, 2),
  Time = c(1, 2, 4, 5, 2, 3, 5),
  Value = c(0.56, -0.72, 1.24, 0.68, 1.46, 0.74, 0.99)
)

df
#> # A tibble: 7 x 3
#>      Id  Time Value
#>   <dbl> <dbl> <dbl>
#> 1     1     1  0.56
#> 2     1     2 -0.72
#> 3     1     4  1.24
#> 4     1     5  0.68
#> 5     2     2  1.46
#> 6     2     3  0.74
#> 7     2     5  0.99

填补缺失的行

df %>% complete(nesting(Id), Time = seq(min(Time), max(Time), 1L))

#> # A tibble: 10 x 3
#>       Id  Time Value
#>    <dbl> <dbl> <dbl>
#> 1      1     1  0.56
#> 2      1     2 -0.72
#> 3      1     3    NA
#> 4      1     4  1.24
#> 5      1     5  0.68
#> 6      2     1    NA
#> 7      2     2  1.46
#> 8      2     3  0.74
#> 9      2     4    NA
#> 10     2     5  0.99

我发现这个特定的解决方案最容易使用,以回答上述相同的问题,尤其是在使用tidyverse进行数据操作时。 - Danielle

5
请参阅Matthew Dowle的答案(希望现在已经在上面)。这里有一个使用data.table包的示例,当存在多个ID变量时可能会有所帮助。它也可能比merge更快,具体取决于您想要的结果如何。我很想进行基准测试和/或建议改进。
首先,创建一些具有两个ID变量的更具挑战性的数据。
library(data.table)

set.seed(1)

mydf3<-data.frame(Id=sample(1:100,10000,replace=TRUE),
  Value=rnorm(10000))
mydf3<-mydf3[order(mydf3$Id),]

mydf3$Time<-unlist(by(mydf3,mydf3$Id,
  function(x)sample(1:(nrow(x)+3),nrow(x)),simplify=TRUE))

mydf3$Id2<-sample(1:2,nrow(mydf3),replace=TRUE)

创建一个函数(此处已进行编辑-请参见历史记录)
padFun<-function(data,idvars,timevar){
# Coerce ID variables to character
  data[,idvars]<-lapply(data[,idvars,drop=FALSE],as.character)
# Create global ID variable of all individual ID vars pasted together
  globalID<-Reduce(function(...)paste(...,sep="SOMETHINGWACKY"),
    data[,idvars,drop=FALSE])
# Create data.frame of all possible combinations of globalIDs and times
  allTimes<-expand.grid(globalID=unique(globalID),
    allTime=min(data[,timevar]):max(data[,timevar]),
    stringsAsFactors=FALSE)
# Get the original ID variables back
  allTimes2<-data.frame(allTimes$allTime,do.call(rbind,
    strsplit(allTimes$globalID,"SOMETHINGWACKY")),stringsAsFactors=FALSE)
# Convert combinations data.frame to data.table with idvars and timevar as key
  allTimesDT<-data.table(allTimes2)
  setnames(allTimesDT,1:ncol(allTimesDT),c(timevar,idvars))
  setkeyv(allTimesDT,c(idvars,timevar))
# Convert data to data.table with same variables as key
  dataDT<-data.table(data,key=c(idvars,timevar))
# Join the two data.tables to create padding
  res<-dataDT[allTimesDT]
  return(res)
}

使用该函数

(padded2<-padFun(data=mydf3,idvars=c("Id"),timevar="Time"))

#       Id Time        Value Id2
#  [1,]  1    1 -0.262482283   2
#  [2,]  1    2 -1.423935165   2
#  [3,]  1    3  0.500523295   1
#  [4,]  1    4 -1.912687398   1
#  [5,]  1    5 -1.459766444   2
#  [6,]  1    6 -0.691736451   1
#  [7,]  1    7           NA  NA
#  [8,]  1    8  0.001041489   2
#  [9,]  1    9  0.495820559   2
# [10,]  1   10 -0.673167744   1
# First 10 rows of 12800 printed.

(padded<-padFun(data=mydf3,idvars=c("Id","Id2"),timevar="Time"))

#      Id Id2 Time      Value
#  [1,]  1   1    1         NA
#  [2,]  1   1    2         NA
#  [3,]  1   1    3  0.5005233
#  [4,]  1   1    4 -1.9126874
#  [5,]  1   1    5         NA
#  [6,]  1   1    6 -0.6917365
#  [7,]  1   1    7         NA
#  [8,]  1   1    8         NA
#  [9,]  1   1    9         NA
# [10,]  1   1   10 -0.6731677
# First 10 rows of 25600 printed.

编辑后的函数会将globalID在组合数据框中拆分成其组成部分,然后再与原始数据合并。我认为这样应该更好。

正确的包,但过于复杂了。你是否错过了专门用于此目的的 roll=TRUE?在表格中保持数据不规则,然后将常规时间序列连接到它上面。请参阅“data.table简介”文档的第3节以及使用 roll=TRUE?data.table 示例。这是该包的主要功能之一。 - Matt Dowle
@MatthewDowle,您比我更了解自己的函数,欢迎提出改进建议。由于OP希望对每个缺失的“Time”用NA填充非ID和非时间变量,因此我认为使用roll=TRUE是错误的方法,因为这会用前一个值进行“填充”,对吗?似乎函数中的大部分复杂性都与考虑多个ID变量有关,而不是与连接部分有关,使用data.table可以非常好地实现连接部分。 - BenBarnes

0

我的一般做法是使用freqTable <- as.data.frame(table(idvar1, idvar2, idvarN)),然后提取出Freq==0的行,必要时进行填充,然后再堆叠回原始数据。


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