将基于记录的列表/对象展平为数据框

8

编辑:这个问题已经过时了。 jsonlite 包会自动进行展平。

我正在处理具有基于记录编码的在线数据流,通常为JSON格式。对象的结构(即JSON中的名称)从API文档中已知,然而,大多数情况下值是可选的,并且不在每个记录中都存在。列表可以包含新列表,结构有时相当深。这里是一些GPS数据的简单示例:http://pastebin.com/raw.php?i=yz6z9t25。请注意,在较低的行中,由于没有GPS信号,"l" 对象丢失了。

我正在寻找一种将这些对象展平为数据框的优雅方式。我目前正在使用类似于以下内容的东西:

library(RJSONIO)
library(plyr)

obj <- fromJSON("http://pastebin.com/raw.php?i=yz6z9t25", simplifyWithNames=FALSE, simplify=FALSE)
flatdata <- lapply(obj$data, as.data.frame);
mydf <- rbind.fill(flatdata)

这可以完成工作,但速度较慢且有些容易出错。此方法的问题在于我没有使用我的数据结构(对象名称)知识,而是从数据中推断出来的。当某个属性恰好在每条记录中都不存在时,就会出现问题。在这种情况下,它根本不会出现在数据框中,而不是一个带有NA值的列。这可能会导致下游问题。例如,我需要处理位置时间戳:

mydf$l.t <- structure(mydf$l.t/1000, class="POSIXct")

然而,如果数据集中不存在 l$t 对象,则会导致错误。此外,as.data.framerbind.fill 会使事情变得相当缓慢。示例数据集比较小。有没有更好的实现建议?一个健壮的解决方案总是会产生一个具有相同顺序的相同列的数据框,只有行数不同。
编辑:下面是一个带有更多元数据的数据集。它的大小更大,嵌套更深:
obj <- fromJSON("http://www.stat.ucla.edu/~jeroen/files/output.json", simplifyWithNames=FALSE, simplify=FALSE)

1
你可以尝试使用Python ;-) - John
1
那么你会如何在Python中实现呢? :-) - Jeroen Ooms
3个回答

5
这里有一个解决方案,可以利用您对数据字段名称和类的先前了解。此外,通过避免重复调用 as.data.frame 和一次调用 plyrrbind.fill()(这两个操作非常耗时),它在您的示例数据上运行速度约快60倍。
cols <- c("id", "ls", "ts", "l.lo","l.tz", "l.t", "l.ac", "l.la", "l.pr", "m")   
numcols <- c("l.lo", "l.t", "l.ac", "l.la")

## Flatten each top-level list element, converting it to a character vector.
x <- lapply(obj$data, unlist)
## Extract fields that might be present in each record (returning NA if absent).
y <- sapply(x, function(X) X[cols])
## Convert to a data.frame with columns of desired classes.
z <- as.data.frame(t(y), stringsAsFactors=FALSE)
z[numcols] <- lapply(numcols, function(X) as.numeric(as.character(z[[X]])))

编辑: 为了确认我的方法给出的结果与原问题中的结果相同,我进行了以下测试。(请注意,在两种情况下,我都设置了stringsAsFactors = FALSE 以避免因因子级别排序而导致无意义的差异。)

flatdata <- lapply(obj$data, as.data.frame, stringsAsFactors=FALSE)
mydf <- rbind.fill(flatdata)
identical(z, mydf)
# [1] TRUE

进一步编辑:

仅供参考,以下是上述内容的另一种版本,除此之外还可以自动执行以下操作:

  1. 查找所有数据字段的名称
  2. 确定它们的类/类型
  3. 将最终数据框的列强制转换为正确的类

.

dat <- obj$data

## Find the names and classes of all fields
fields <- unlist(lapply(xx, function(X) rapply(X, class, how="unlist")))
fields <- fields[unique(names(fields))]
cols <- names(fields)

## Flatten each top-level list element, converting it to a character vector.
x <- lapply(dat, unlist)
## Extract fields that might be present in each record (returning NA if absent).
y <- sapply(x, function(X) X[cols])
## Convert to a data.frame with columns of desired classes.
z <- as.data.frame(t(y), stringsAsFactors=FALSE)

## Coerce columns of z (all currently character) back to their original type
z[] <- lapply(seq_along(fields), function(i) as(z[[cols[i]]], fields[i]))

谢谢。我更喜欢不改变类型的解决方案。as.numeric/as.character 可能会丢失精度等。 - Jeroen Ooms
我有同样的担忧,但我相信可以解决(特别是考虑到JSON似乎是一种基于文本的标准)。如果你还没有找到解决方法,我会快速查看可能的修复方案(特别是fromJSON()选项),等我有机会时。 - Josh O'Brien
JSON很明确地区分了字符串和数字。我认为问题不在于JSON解析本身。我们需要的是类似于unlist的功能,它将列表压缩为只有一层深度的列表(而不是向量,将所有内容转换为字符串)。 - Jeroen Ooms
1
Jeroen,由于您最初的问题暗示您需要知道类型(例如,没有记录的列),因此我不会追求不进行类型转换的解决方案(超出fromJSON执行的范围)。沿着这些线路,您可能会对这个代码片段感兴趣:x <- lapply(obj$data, function(X) {rapply(X, expression, how="unlist")[c(TRUE, FALSE)]})。它将每个项目展平为非递归列表,其中每个元素保留其类型。如果您坚持不进行类型转换,可以将其与@JoshuaUlrich的一些聪明想法相结合,使其达到最后的目标... - Josh O'Brien
谢谢,rapply 似乎是缺失的关键。但是,您能解释一下为什么它使用不同的命名方案而不是 unlist 吗?它会在某些名称后缀加上 "1"。 - Jeroen Ooms
显示剩余6条评论

2

下面的方法尝试不对数据类型做任何假设。它比@JoshOBrien的方法稍慢,但比原始解决方案快。

Joshua <- function(x) {
  un <- lapply(x, unlist, recursive=FALSE)
  ns <- unique(unlist(lapply(un, names)))
  un <- lapply(un, function(x) {
    y <- as.list(x)[ns]
    names(y) <- ns
    lapply(y, function(z) if(is.null(z)) NA else z)})
  s <- lapply(ns, function(x) sapply(un, "[[", x))
  names(s) <- ns
  data.frame(s, stringsAsFactors=FALSE)
}

Josh <- function(x) {
  cols <- c("id", "ls", "ts", "l.lo","l.tz", "l.t", "l.ac", "l.la", "l.pr", "m")   
  numcols <- c("l.lo", "l.t", "l.ac", "l.la")
  ## Flatten each top-level list element, converting it to a character vector.
  x <- lapply(obj$data, unlist)
  ## Extract fields that might be present in each record (returning NA if absent).
  y <- sapply(x, function(X) X[cols])
  ## Convert to a data.frame with columns of desired classes.
  z <- as.data.frame(t(y))
  z[numcols] <- lapply(numcols, function(X) as.numeric(as.character(z[[X]])))
  z
}

Jeroen <- function(x) {
  flatdata <- lapply(x, as.data.frame)
  rbind.fill(flatdata)
}

library(rbenchmark)
benchmark(Josh=Josh(obj$data), Joshua=Joshua(obj$data),
  Jeroen=Jeroen(obj$data), replications=5, order="relative")
#     test replications elapsed  relative user.self sys.self user.child sys.child
# 1   Josh            5    0.24  1.000000      0.24        0         NA        NA
# 2 Joshua            5    0.31  1.291667      0.32        0         NA        NA
# 3 Jeroen            5   12.97 54.041667     12.87        0         NA        NA

谢谢。不过有个问题,recursive=FALSE 会导致对嵌套更深的对象无效,不能正常工作。 - Jeroen Ooms
你的代码中还有两处错别字,应该将 n 改为 ns(我想是这样)。 - Jeroen Ooms
@Jeroen:感谢您指出拼写错误。您有更深嵌套的对象示例吗? - Joshua Ulrich
我在文章底部添加了一个更大的数据集。 - Jeroen Ooms

1

为了更清晰明了,我将结合Josh和Joshua的解决方案,这是目前我想到的最好的方案。

flatlist <- function(mylist){
    lapply(rapply(mylist, enquote, how="unlist"), eval)
}

records2df <- function(recordlist, columns) {
    if(length(recordlist)==0 && !missing(columns)){
      return(as.data.frame(matrix(ncol=length(columns), nrow=0, dimnames=list(NULL,columns))))
    }
    un <- lapply(recordlist, flatlist)
    if(!missing(columns)){
        ns <- columns;
    } else {
        ns <- unique(unlist(lapply(un, names)))
    }
    un <- lapply(un, function(x) {
        y <- as.list(x)[ns]
        names(y) <- ns
        lapply(y, function(z) if(is.null(z)) NA else z)})
    s <- lapply(ns, function(x) sapply(un, "[[", x))
    names(s) <- ns
    data.frame(s, stringsAsFactors=FALSE)
}

这个功能运行相当快。不过我仍然认为它应该能够加速:

obj <- fromJSON("http://www.stat.ucla.edu/~jeroen/files/output.json", simplifyWithNames=FALSE, simplify=FALSE)
flatdata <- records2df(obj$data)

它还允许您“强制”某些列,尽管这不会导致太大的加速:

flatdata <- records2df(obj$data, columns=c("m", "doesnotexist"))

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