эффективно извлекать вложенные кадры данных из сложного JSON, используя функции мурлыкания и%>% - PullRequest
1 голос
/ 11 мая 2019

Я пытаюсь построить таблицу, похожую на эту (это всего лишь пара строк, но я пытаюсь получить все попадания из списка игр):

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, который присутствует в некоторых, но не во всех, 80 элементах в списке playEvents, который сам находится в кадре данных 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, хотел бы написать код, используя %>% и map. Это моя первая попытка, и я не уверен, что полностью понимаю этот метод. Если у вас есть решение, не могли бы вы попытаться объяснить, чтобы я мог лучше понять, что происходит, и, надеюсь, применить его к другому коду, если у меня есть аналогичные данные?

Любая помощь очень ценится!

Спасибо !!

Ответы [ 2 ]

2 голосов
/ 12 мая 2019

Вы выбрали сложную задачу для своих первых шагов с помощью трубы magrittr и функций карты! Я сделаю все возможное, чтобы дать вам полезный ответ, но я бы также порекомендовал вам найти более простые данные для работы во время практики. Хорошее место, чтобы узнать о трубе %>% - глава "Трубы" в книге Хэдли Уикхем. Глава о итерации также предлагает хорошее введение в функции map_*. Вы можете вернуться к более сложным проблемам, если у вас есть более четкое концептуальное понимание. Я думаю, что Хэдли объясняет эти инструменты лучше, чем я когда-либо мог, поэтому я не буду вдаваться в подробности о них здесь, а вместо этого сосредоточусь на объяснении, почему ваш код не работает и почему мой.

Анализ вашего кода

Функции карты допускают несколько полезных ярлыков, один из которых вы уже обнаружили - а именно, если вы передаете векторы или списки в качестве аргумента функции, они автоматически преобразуются в функции экстрактора . Итак, вы на правильном пути!

Следует помнить, что функции карты возвращают вектор такой же длины и с теми же именами, что и входной вектор. Ваш входной вектор jsonData, который имеет 5 элементов с именами [1] "copyright" "allPlays" "currentPlay" "scoringPlays" "playsByInning". Когда вы запускаете jsonData %>% map("playEvents") %>% 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 извлекает hitData из каждого элемента списка playEvents. Фреймы данных hitData являются вложенными (они содержат другие кадры данных), поэтому второй вызов map с jsonlite::flatten сглаживает их. Функция safely гарантирует, что R не выдаст ошибку при обнаружении чего-либо, кроме фрейма данных (только 46 элементов содержат hitData). Многие из hitData фреймов данных содержат строки, заполненные NA s, поэтому третий вызов 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 фреймов данных. Как я упоминал выше, только 46 из 80 записей содержат hitData, поэтому мне нужен способ получить соответствующие значения из atBatIndex. Я могу сделать это, генерируя логический вектор с TRUE, когда элемент в hitdata_list содержит фрейм данных, а FALSE в противном случае. Я использую map_lgl, чтобы вернуть логический вектор вместо списка:

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

Затем я использую функцию stringr, чтобы получить game_pk из URL. Я не уверен, что он будет работать с каждым URL, но в этом случае он работает нормально:

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

Наконец, я объединяю atBatIndex и game_pk в таблицу, затем объединяю эту таблицу с данными hitData, используя bind_cols. Фреймы данных 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") возвращает список последовательностей с 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
0 голосов
/ 11 мая 2019

Попробуйте дерзкий "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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...