使用 purrr 函数和 %>% 从复杂的 JSON 中高效地提取嵌套数据帧
extract nested dataframes from complex JSON efficiently using purrr functions and %>%
我正在尝试构建一个与此类似的 table(这只是几行,但我正在尝试从游戏列表中获取所有点击):
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))
不幸的是,它 returns 错误:
Error: Argument 7 can't be a list containing data frames
我很难想出方法来处理 JSON 中的列表、数据框、嵌套数据框和向量的集合。
除了 hitData
,我还想要来自 atBatIndex
的数据,一个在 jsonData$allPlays$about
(也在 jsonData$allPlays
)和 pitchNumber
中找到的数字向量,可以在与 hitData
.
相同的级别找到
我正在从 URL 中获取 game_pk
编号 565711 并使用以下代码将其添加到数据框中:
hitDataDF$game_pk = str_match(url, '([^/]+)(?:/[^/]+){1}$')[,2]
我是 R 的新手,想使用 %>%
和 map
编写代码。这是我第一次尝试,我不确定我是否完全理解这种方法。如果您有解决方案,能否请您尝试解释一下,以便我更好地理解正在发生的事情,并希望在我提取类似数据时将其应用于其他代码?
非常感谢任何帮助!!
谢谢!!
尝试一下 'unlist' 的厚脸皮。我设法得到了一个无名的数据框——从列表中取出名字似乎很复杂。希望这会有所帮助:
hitData = jsonData %>%
map("playEvents") %>%
map("hitData") %>%
unlist(recursive = F)
numRows = lapply(hitData,length) %>% unique %>% unlist
hitDataFrame = unlist(hitData) %>% matrix(nrow = numRows) %>% as.data.frame
您在使用 magrittr 管道和 map 函数的第一步中选择了一个具有挑战性的问题!我会尽我所能给你一个有用的答案,但我也建议你在练习时找一些更容易处理的数据。了解管道 %>%
的一个好地方是 "Pipes" chapter in Hadley Wickham's book. The chapter on iteration 也提供了对 map_*
函数的很好介绍。一旦您对概念有了更牢固的理解,就可以 return 解决更复杂的问题。我认为 Hadley 比我以往任何时候都更好地解释了这些工具,所以我不会在这里详细介绍它们,而是重点解释为什么您的代码不起作用,以及为什么我的代码起作用。
代码分析
映射函数允许一些有用的快捷方式,您已经发现了其中之一 - 即,如果您传入向量或列表作为函数参数,它们会自动转换为 提取函数。所以,你走对了!
要记住的是 map 函数 return 一个与输入向量长度相同且名称相同的向量。您的输入向量是 jsonData
,它有 5 个名称为 [1] "copyright" "allPlays" "currentPlay" "scoringPlays" "playsByInning"
的元素。当你 运行 jsonData %>% map("playEvents") %>% map("hitData")
时,正在提取数据,但 R 仍然 return 是一个包含五个元素且与原始向量同名的向量。如果你看一下下面的代码,你会发现你的代码确实剥离了最上层,但长度保持不变,这不是很有帮助:
> 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
的每个元素中提取 hitData
。 hitData
数据帧是嵌套的(它们包含其他数据帧),因此第二次使用 jsonlite::flatten
调用 map
会将它们展平。函数 safely
确保 R 在遇到数据帧以外的内容时不会抛出错误(只有 46 个元素包含 hitData
)。许多 hitData
数据帧包含充满 NA
的行,因此对 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
到 return 逻辑向量而不是列表:
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()
最后,我将 atBatIndex
和 game_pk
合并到一个小标题中,然后使用 bind_cols
将该小标题与 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 %>% map("pitchNumber")
returns 序列 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
我正在尝试构建一个与此类似的 table(这只是几行,但我正在尝试从游戏列表中获取所有点击):
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))
不幸的是,它 returns 错误:
Error: Argument 7 can't be a list containing data frames
我很难想出方法来处理 JSON 中的列表、数据框、嵌套数据框和向量的集合。
除了 hitData
,我还想要来自 atBatIndex
的数据,一个在 jsonData$allPlays$about
(也在 jsonData$allPlays
)和 pitchNumber
中找到的数字向量,可以在与 hitData
.
我正在从 URL 中获取 game_pk
编号 565711 并使用以下代码将其添加到数据框中:
hitDataDF$game_pk = str_match(url, '([^/]+)(?:/[^/]+){1}$')[,2]
我是 R 的新手,想使用 %>%
和 map
编写代码。这是我第一次尝试,我不确定我是否完全理解这种方法。如果您有解决方案,能否请您尝试解释一下,以便我更好地理解正在发生的事情,并希望在我提取类似数据时将其应用于其他代码?
非常感谢任何帮助!!
谢谢!!
尝试一下 'unlist' 的厚脸皮。我设法得到了一个无名的数据框——从列表中取出名字似乎很复杂。希望这会有所帮助:
hitData = jsonData %>%
map("playEvents") %>%
map("hitData") %>%
unlist(recursive = F)
numRows = lapply(hitData,length) %>% unique %>% unlist
hitDataFrame = unlist(hitData) %>% matrix(nrow = numRows) %>% as.data.frame
您在使用 magrittr 管道和 map 函数的第一步中选择了一个具有挑战性的问题!我会尽我所能给你一个有用的答案,但我也建议你在练习时找一些更容易处理的数据。了解管道 %>%
的一个好地方是 "Pipes" chapter in Hadley Wickham's book. The chapter on iteration 也提供了对 map_*
函数的很好介绍。一旦您对概念有了更牢固的理解,就可以 return 解决更复杂的问题。我认为 Hadley 比我以往任何时候都更好地解释了这些工具,所以我不会在这里详细介绍它们,而是重点解释为什么您的代码不起作用,以及为什么我的代码起作用。
代码分析
映射函数允许一些有用的快捷方式,您已经发现了其中之一 - 即,如果您传入向量或列表作为函数参数,它们会自动转换为 提取函数。所以,你走对了!
要记住的是 map 函数 return 一个与输入向量长度相同且名称相同的向量。您的输入向量是 jsonData
,它有 5 个名称为 [1] "copyright" "allPlays" "currentPlay" "scoringPlays" "playsByInning"
的元素。当你 运行 jsonData %>% map("playEvents") %>% map("hitData")
时,正在提取数据,但 R 仍然 return 是一个包含五个元素且与原始向量同名的向量。如果你看一下下面的代码,你会发现你的代码确实剥离了最上层,但长度保持不变,这不是很有帮助:
> 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
的每个元素中提取 hitData
。 hitData
数据帧是嵌套的(它们包含其他数据帧),因此第二次使用 jsonlite::flatten
调用 map
会将它们展平。函数 safely
确保 R 在遇到数据帧以外的内容时不会抛出错误(只有 46 个元素包含 hitData
)。许多 hitData
数据帧包含充满 NA
的行,因此对 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
到 return 逻辑向量而不是列表:
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()
最后,我将 atBatIndex
和 game_pk
合并到一个小标题中,然后使用 bind_cols
将该小标题与 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 %>% map("pitchNumber")
returns 序列 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