【问题标题】:extract nested dataframes from complex JSON efficiently using purrr functions and %>%使用 purrr 函数和 %>% 有效地从复杂 JSON 中提取嵌套数据帧
【发布时间】:2019-05-11 13:29:56
【问题描述】:

我正在尝试构建一个与此类似的表格(这只是几行,但我正在尝试从游戏列表中获取所有点击量):

game_pk   atBatIndex  pitchNumber   hardness launchAngle  launchSpeed  location  totalDistance  trajectory   coordX   coordY
565711    4           3             medium   2.74         76.62        9         188.03         ground_ball  177.88   145.11
565711    5           3             hard     15.42        101.26       8         328.08         line_drive   144.79   62.25

我想提取的大部分内容都可以在hitData 中找到,它位于playEvents 列表中的80 个元素中的一部分,但不是全部,它本身就在数据框allPlays 中。您可以使用jsonData$allPlays$playEvents[[80]]$hitData 来查看示例。

这是我正在使用的代码:

library(jsonlite)
library(purrr)
library(dplyr)

url <- "http://statsapi-prod-alt-968618993.us-east-1.elb.amazonaws.com/api/v1/game/565711/playByPlay"

jsonData <- fromJSON(url)

hitDataDF <- data.frame(jsonData %>%
                       map("playEvents") %>%
                       map("hitData") %>%
                       map_df(bind_rows))

不幸的是,它返回错误:

错误:参数 7 不能是包含数据帧的列表

我很难想出处理 JSON 中的列表、数据框、嵌套数据框和向量的集合的方法。

除了hitData,我还想要来自atBatIndex 的数据,这是在jsonData$allPlays$about(也在jsonData$allPlays)和pitchNumber 中找到的数字向量,可以在与hitData 相同的级别找到.

我正在从 URL 中获取 game_pk 数字 565711 并使用以下代码将其添加到数据框中:

hitDataDF$game_pk = str_match(url, '([^/]+)(?:/[^/]+){1}$')[,2]

我是 R 新手,想使用 %&gt;%map 编写代码。这是我第一次尝试它,我不确定我是否完全理解这种方法。如果您有解决方案,能否请您尝试解释一下,以便我更好地了解正在发生的事情,并希望在我提取类似数据时将其应用于其他代码?

非常感谢任何帮助!

谢谢!!

【问题讨论】:

  • map 需要一个向量和一个函数。您能否进一步详细说明您想做什么?也许显示预期的结果?
  • @NelsonGon 我确实显示了预期的结果,这是第一个代码框。我想我不够清楚,我道歉。
  • 问题是很难将 JSON 连接到所需的结果。示例数据框中的某些变量在 JSON 中的任何地方都找不到。您也没有真正为我们指明您想要去的方向 - 这是最重要的,因为 JSON 包含相当深度嵌套的列表和数据框。您似乎还没有努力理解 JSON 的结构。
  • 唯一不在代码中的变量是 game_pk,我展示了从哪里得到它。另外两个,我假设你在谈论 atbatindex 和 pitchnumber,位于其他区域,我在我的代码下的第三段中列出了它们的位置。 atbatindex 在 about 部分,pitchnumber 在 playevents 代码的 details 部分中的 hit 数据下方 我同意很难将 json 数据连接到所需的结果,这就是我来这里询问的原因。我确实了解json的结构,请不要假设。
  • @DonHessey 我选择删除我之前的批评,而只是编辑您的问题。我还在下面写了一个冗长的答案。希望对您有所帮助!

标签: r dplyr purrr jsonlite


【解决方案1】:

您为使用 magrittr 管道和地图函数的第一步选择了一个具有挑战性的问题!我会尽力给你一个有用的答案,但我也建议你在练习时找到一些更容易使用的数据。了解管道 %&gt;% 的好地方是 Hadley Wickham 书中的 "Pipes" 章节。关于iteration 的章节也很好地介绍了map_* 函数。一旦你对概念有了更坚定的理解,你就可以回到更复杂的问题。我认为 Hadley 对这些工具的解释比我以往任何时候都好,所以我不会在这里详细介绍它们,而是重点解释为什么您的代码不起作用,以及为什么我的代码起作用。

对代码的分析

Map 函数允许使用一些有用的快捷方式,您已经发现了其中一个 - 即,如果您将向量或列表作为函数参数传递,它们会自动转换为提取器函数。所以,你在正确的轨道上!

要记住的是,map 函数返回一个与输入向量具有相同长度和名称的向量。您的输入向量是 jsonData,它有 5 个名称为 [1] "copyright" "allPlays" "currentPlay" "scoringPlays" "playsByInning" 的元素。当您运行jsonData %&gt;% map("playEvents") %&gt;% map("hitData") 时,正在提取数据,但 R 仍返回一个包含五个元素且名称与原始向量相同的向量。如果你看一下下面的代码,你会发现你的代码确实是剥离了最上层,但长度保持不变,这不是很有帮助:

> unlist(map(jsonData, class))
    copyright      allPlays   currentPlay  scoringPlays playsByInning 
  "character"  "data.frame"        "list"     "integer"  "data.frame" 

> unlist(map(jsonData %>% map("playEvents"), class))
    copyright      allPlays   currentPlay  scoringPlays playsByInning 
       "NULL"        "list"  "data.frame"        "NULL"        "NULL" 

> unlist(map(jsonData %>% map("playEvents") %>% map("hitData"), class))
    copyright      allPlays   currentPlay  scoringPlays playsByInning 
       "NULL"        "NULL"  "data.frame"        "NULL"        "NULL" 

最终的输出,以及您试图与上面对bind_rows 的调用相结合的内容是:

> jsonData %>% map("playEvents") %>% map("hitData")
$copyright
NULL

$allPlays
NULL

$currentPlay
  launchSpeed launchAngle totalDistance trajectory hardness location coordinates.coordX coordinates.coordY
1          NA          NA            NA       <NA>     <NA>     <NA>                 NA                 NA
2        81.3       61.92         187.5      popup   medium        6              75.78             167.97

$scoringPlays
NULL

$playsByInning
NULL

显然这不是你想要的。经过一番修补,我想出了以下解决方案。

我自己的策略

图书馆:

library(jsonlite)
library(purrr)
library(dplyr)
library(readr)
library(stringr)
library(magrittr)

我使用稍微不同的方法来下载和解析 JSON,因为我需要查看结构。我会把它包括在内,以防你觉得它有用:

url <- paste0("http://statsapi-prod-alt-968618993.us-east-1.elb.amazonaws",
              ".com/api/v1/game/565711/playByPlay")

url %>% read_file() %>% prettify() %>% write_file("bball.json")

jsonData <- fromJSON("bball.json")

我首先提取并清理 hitData 数据帧。我知道它们都可以在playEvents 中找到,所以我可以使用$ 语法跳过几个步骤。第一次调用map 从列表playEvents 的每个元素中提取hitDatahitData 数据框是嵌套的(它们包含其他数据框),因此第二次使用 jsonlite::flatten 调用 map 会使它们变平。函数 safely 确保 R 在遇到数据帧以外的内容时不会抛出错误(只有 46 个元素包含 hitData)。许多hitData 数据帧包含充满NAs 的行,因此对map 的第三次调用使用匿名函数(再次在safely 中)来摆脱这些。第四次调用map 然后从每个元素的result 变量中提取数据帧,该变量由safely 创建(以及我们不需要的error 变量):

hitdata_list <- jsonData$allPlays$playEvents %>% 
    map("hitData") %>% 
    map(safely(jsonlite::flatten)) %>% 
    map(safely(~.$result[complete.cases(.$result),])) %>% 
    map("result")

现在我有一个 hitData 数据框列表。正如我上面提到的,80 个条目中只有 46 个包含 hitData,所以我需要一种从 atBatIndex 获取相应值的方法。当hitdata_list 中的元素包含数据框时,我可以通过使用TRUE 生成逻辑向量来做到这一点,否则FALSE。我使用map_lgl 来返回一个逻辑向量而不是一个列表:

lgl_index <- map_lgl(hitdata_list, ~ !is.null(.))
atbatindex_vec <- jsonData$allPlays$atBatIndex[lgl_index]

然后我使用stringr 函数从URL 中获取game_pk。我不确定它是否适用于每个 URL,但在这种情况下它可以正常工作:

game_pk_vec <- str_match(url, "/(\\d+)/")[2] %>%
    as.integer()

最后,我将atBatIndexgame_pk 组合到一个tibble 中,然后使用bind_cols 将该tibble 与hitData 数据组合起来。 hitData 数据帧仍在列表中,所以我需要先将它们与 bind_rows 结合起来。 set_colnames 函数来自 magrittr 包,并按照它所说的做。我需要设置列名,因为在展平 hitData 数据框时创建了一些复合名称:

hitdata_df <- tibble(game_pk = game_pk_vec, atBatIndex = atbatindex_vec) %>% 
    bind_cols(bind_rows(hitdata_list)) %>% 
    set_colnames(str_extract(names(.), "\\w+$"))

我唯一没有做的是提取pitchNumber。调用 jsonData$allPlays$playEvents %&gt;% map("pitchNumber") 返回序列 1 到 n 的列表,其中每个向量的长度 > 1。我假设您只需要每个序列中的最终数字,但我不确定所以我会省自己的力气。你可以做我用atBatIndex 做的事情来获取相关元素,然后提取你需要的东西。这是最终的数据框:

# A tibble: 46 x 10
   game_pk atBatIndex launchSpeed launchAngle totalDistance trajectory  hardness location coordX coordY
   <chr>        <int>       <dbl>       <dbl>         <dbl> <chr>       <chr>    <chr>     <dbl>  <dbl>
 1 565711           4        76.6        2.74        188.   ground_ball medium   9         178.   145. 
 2 565711           5       101.        15.4         328.   line_drive  hard     8         145.    62.2
 3 565711           6       103.        29.4         382.   line_drive  medium   9         237.    79.4
 4 565711           8       109.        15.6         319.   line_drive  hard     9         181.   102. 
 5 565711           9        75.8       47.8         239.   fly_ball    medium   7          99.8  103. 
 6 565711          10        91.6       44.1         311.   fly_ball    medium   8         140.    69.3
 7 565711          12        79.1       23.4         246.   line_drive  medium   7          52.3  126. 
 8 565711          13        67.3      -21.3         124.   ground_ball medium   6         108.   156. 
 9 565711          14        89.9      -21.6           7.41 ground_ball medium   6         108.   152. 
10 565711          15       110.        27.7         420.   fly_ball    medium   9         250.    69.0
# … with 36 more rows

【讨论】:

  • 这段代码/解释太棒了@gersht!!它经过深思熟虑,非常彻底!我真的很感谢您花时间写出每个步骤的所有解释。几年来我一直在提取这些数据,并慢慢地将我的代码配对,以更快更高效,10 分钟完成 100 场比赛,目前为 2-3 分钟。您的代码大约需要 35 秒,这对我来说太疯狂了……您向我展示了一些新功能,我可以将这些功能用于我正在提取的棒球数据的其他部分。再次感谢您的回答和时间,我不认为这是理所当然的!
  • 我很高兴它有帮助!
【解决方案2】:

尝试一些俗气的“取消列表”。我设法得到了一个无名的数据框——从列表中取出名字似乎很复杂。希望这会有所帮助:

hitData = jsonData %>%
      map("playEvents") %>%
      map("hitData") %>%
      unlist(recursive = F)

numRows = lapply(hitData,length) %>% unique %>% unlist

hitDataFrame = unlist(hitData) %>% matrix(nrow = numRows) %>% as.data.frame

【讨论】:

  • 它并没有真正实现 OP 想要做的事情。
猜你喜欢
  • 2021-10-16
  • 1970-01-01
  • 2021-05-25
  • 2020-04-18
  • 1970-01-01
  • 2018-10-11
  • 1970-01-01
  • 2016-04-17
  • 2021-12-30
相关资源
最近更新 更多