Рекурсивное обеспечение тибблов вместо фреймов данных при разборе / манипулировании вложенными JSON - PullRequest
2 голосов
/ 13 января 2020

Мне приходится иметь дело с JSON документами, которые содержат вложенных документов и на каком-то уровне имеют массив , который, в свою очередь, содержит отдельные документы , которые по своей сути будут отображение обратно на «строки фрейма данных» при чтении / разборе JSON в R.

enter image description here

Проблема / вопрос первого порядка

Я ищу способ убедиться, что

  • либо все data frames всегда превращаются в tibbles

  • , либо в как минимум, «листовые фреймы данных» становятся tibbles, тогда как «родительские фреймы данных» могут становиться lists

для произвольных вложенных структур, либо непосредственно при синтаксическом анализе через {jsonlite} или после этого через {purrr}.

Проблема / вопрос второго порядка

Как пройти списки и применить map рекурсивно с {purrr} "правильным способом "?

Связано


Пример

Пример данных

json <- '[
  {
    "labels": ["label-a", "label-b"],
    "levelOne": {
      "levelTwo": {
        "levelThree": [
          {
            "x": "A",
            "y": 1,
            "z": true
          },
          {
            "x": "B",
            "y": 2,
            "z": false
          }
          ]
      }
    },
    "schema": "0.0.1"
  },
  {
    "labels": ["label-a", "label-b"],
    "levelOne": {
      "levelTwo": {
        "levelThree": [
          {
            "x": "A",
            "y": 10,
            "z": false
          },
          {
            "x": "B",
            "y": 20,
            "z": true
          }
          ]
      }
    },
    "schema": "0.0.1"
  }
]'

Результат после анализа и преобразования в tibble

x <- json %>% jsonlite::fromJSON()

x %>% str()
# 'data.frame': 2 obs. of  3 variables:
#  $ labels  :List of 2
#   ..$ : chr  "label-a" "label-b"
#   ..$ : chr  "label-a" "label-b"
#  $ levelOne:'data.frame': 2 obs. of  1 variable:
#   ..$ levelTwo:'data.frame':  2 obs. of  1 variable:
#   .. ..$ levelThree:List of 2
#   .. .. ..$ :'data.frame':    2 obs. of  3 variables:
#   .. .. .. ..$ x: chr  "A" "B"
#   .. .. .. ..$ y: int  1 2
#   .. .. .. ..$ z: logi  TRUE FALSE
#   .. .. ..$ :'data.frame':    2 obs. of  3 variables:
#   .. .. .. ..$ x: chr  "A" "B"
#   .. .. .. ..$ y: int  10 20
#   .. .. .. ..$ z: logi  FALSE TRUE
#  $ schema  : chr  "0.0.1" "0.0.1"

x_tbl <- x %>% tibble::as_tibble()

x_tbl %>% str()
# Classes ‘tbl_df’, ‘tbl’ and 'data.frame': 2 obs. of  3 variables:
#  $ labels  :List of 2
#   ..$ : chr  "label-a" "label-b"
#   ..$ : chr  "label-a" "label-b"
#  $ levelOne:'data.frame': 2 obs. of  1 variable:
#   ..$ levelTwo:'data.frame':  2 obs. of  1 variable:
#   .. ..$ levelThree:List of 2
#   .. .. ..$ :'data.frame':    2 obs. of  3 variables:
#   .. .. .. ..$ x: chr  "A" "B"
#   .. .. .. ..$ y: int  1 2
#   .. .. .. ..$ z: logi  TRUE FALSE
#   .. .. ..$ :'data.frame':    2 obs. of  3 variables:
#   .. .. .. ..$ x: chr  "A" "B"
#   .. .. .. ..$ y: int  10 20
#   .. .. .. ..$ z: logi  FALSE TRUE
#  $ schema  : chr  "0.0.1" "0.0.1"

Желаемый результат

x_tbl$levelOne <- x_tbl$levelOne %>% tibble::as_tibble()
x_tbl$levelOne$levelTwo <- x_tbl$levelOne$levelTwo %>% 
  tibble::as_tibble()
x_tbl$levelOne$levelTwo$levelThree <- x_tbl$levelOne$levelTwo$levelThree %>% 
  purrr::map(tibble::as_tibble)

x_tbl %>% str()
# Classes ‘tbl_df’, ‘tbl’ and 'data.frame': 2 obs. of  3 variables:
#  $ labels  :List of 2
#   ..$ : chr  "label-a" "label-b"
#   ..$ : chr  "label-a" "label-b"
#  $ levelOne:Classes ‘tbl_df’, ‘tbl’ and 'data.frame': 2 obs. of  1 variable:
#   ..$ levelTwo:Classes ‘tbl_df’, ‘tbl’ and 'data.frame':  2 obs. of  1 variable:
#   .. ..$ levelThree:List of 2
#   .. .. ..$ :Classes ‘tbl_df’, ‘tbl’ and 'data.frame':    2 obs. of  3 variables:
#   .. .. .. ..$ x: chr  "A" "B"
#   .. .. .. ..$ y: int  1 2
#   .. .. .. ..$ z: logi  TRUE FALSE
#   .. .. ..$ :Classes ‘tbl_df’, ‘tbl’ and 'data.frame':    2 obs. of  3 variables:
#   .. .. .. ..$ x: chr  "A" "B"
#   .. .. .. ..$ y: int  10 20
#   .. .. .. ..$ z: logi  FALSE TRUE
#  $ schema  : chr  "0.0.1" "0.0.1"

Если я попытаюсь сделать это через dplyr::mutate() или purrr::map*_df(), я получу Error: Columnis of unsupported class data.frame ошибку

Текущая реализация

У меня есть кое-что, что выглядит на первый взгляд хорошо, но дублирует вложенную структуру, как только вы приведете list к tibble. Даже если он работал должным образом, он кажется сложным и хрупким, поскольку был спроектирован с учетом одной конкретной структуры c прецедент / JSON:

tidy_nested_data_frames <- function(
  x
) {
  is_data_frame_that_should_be_list <- function(x) {
    is.data.frame(x) && purrr::map_lgl(x, is.data.frame)
  }
  y <- x %>%
    purrr::map_if(is_data_frame_that_should_be_list, as.list)

  # Check for next data frame columns to handle:
  false <- function(.x) FALSE
  class_info <- y %>%
    purrr::map_if(is.list, ~.x %>% purrr::map(is.data.frame), .else = false)

  trans_to_tibble <- function(x) {
    x %>% purrr::map(tibble::as_tibble)
  }
  purrr::map2(class_info, y, function(.x, .y) {
    go_deeper <- .x %>% as.logical() %>% all()

    if (go_deeper) {
      # Continue if data frame columns have been detected:

      tidy_nested_data_frames(.y)
    } else {
      # Handle data frames that have list columns that themselves carry the data
      # frames we want to turn into tibbles:

      # NOTE:
      # This probably does not generalize well yet as the logic seems to much
      # tied to my current use case!

      if (.y %>% is.data.frame()) {
        .y %>%
          purrr::map_if(is.list, trans_to_tibble)
      } else {
        .y
      }
    }
  })
}

Тестирование

x <- json %>% 
  jsonlite::fromJSON() %>% 
  tidy_nested_data_frames()

x %>% str()
# List of 3
#  $ labels  :List of 2
#   ..$ : chr [1:2] "label-a" "label-b"
#   ..$ : chr [1:2] "label-a" "label-b"
#  $ levelOne:List of 1
#   ..$ levelTwo:List of 1
#   .. ..$ levelThree:List of 2
#   .. .. ..$ :Classes ‘tbl_df’, ‘tbl’ and 'data.frame':    2 obs. of  3 variables:
#   .. .. .. ..$ x: chr [1:2] "A" "B"
#   .. .. .. ..$ y: int [1:2] 1 2
#   .. .. .. ..$ z: logi [1:2] TRUE FALSE
#   .. .. ..$ :Classes ‘tbl_df’, ‘tbl’ and 'data.frame':    2 obs. of  3 variables:
#   .. .. .. ..$ x: chr [1:2] "A" "B"
#   .. .. .. ..$ y: int [1:2] 10 20
#   .. .. .. ..$ z: logi [1:2] FALSE TRUE
#  $ schema  : chr [1:2] "0.0.1" "0.0.1"

x_tbl <- x %>% tibble::as_tibble()

x_tbl %>% str()
# Classes ‘tbl_df’, ‘tbl’ and 'data.frame': 2 obs. of  3 variables:
#  $ labels  :List of 2
#   ..$ : chr  "label-a" "label-b"
#   ..$ : chr  "label-a" "label-b"
#  $ levelOne:List of 2
#   ..$ levelTwo:List of 1
#   .. ..$ levelThree:List of 2
#   .. .. ..$ :Classes ‘tbl_df’, ‘tbl’ and 'data.frame':    2 obs. of  3 variables:
#   .. .. .. ..$ x: chr  "A" "B"
#   .. .. .. ..$ y: int  1 2
#   .. .. .. ..$ z: logi  TRUE FALSE
#   .. .. ..$ :Classes ‘tbl_df’, ‘tbl’ and 'data.frame':    2 obs. of  3 variables:
#   .. .. .. ..$ x: chr  "A" "B"
#   .. .. .. ..$ y: int  10 20
#   .. .. .. ..$ z: logi  FALSE TRUE
#   ..$ levelTwo:List of 1
#   .. ..$ levelThree:List of 2
#   .. .. ..$ :Classes ‘tbl_df’, ‘tbl’ and 'data.frame':    2 obs. of  3 variables:
#   .. .. .. ..$ x: chr  "A" "B"
#   .. .. .. ..$ y: int  1 2
#   .. .. .. ..$ z: logi  TRUE FALSE
#   .. .. ..$ :Classes ‘tbl_df’, ‘tbl’ and 'data.frame':    2 obs. of  3 variables:
#   .. .. .. ..$ x: chr  "A" "B"
#   .. .. .. ..$ y: int  10 20
#   .. .. .. ..$ z: logi  FALSE TRUE
#  $ schema  : chr  "0.0.1" "0.0.1"

РЕДАКТИРОВАТЬ 2020-01-14

Опробовать подход Алланда Кэмерона "как есть" я получаю:

library(tibble)

x %>% 
  recursive_tibble() %>% 
  str()
# List of 3
#  $ labels  :Classes ‘tbl_df’, ‘tbl’ and 'data.frame': 2 obs. of  2 variables:
#   ..$ V1: chr [1:2] "label-a" "label-b"
#   ..$ V2: chr [1:2] "label-a" "label-b"
#  $ levelOne:List of 1
#   ..$ levelTwo:List of 1
#   .. ..$ levelThree:Classes ‘tbl_df’, ‘tbl’ and 'data.frame': 2 obs. of  6 variables:
#   .. .. ..$ x1: chr [1:2] "A" "A"
#   .. .. ..$ x2: chr [1:2] "B" "B"
#   .. .. ..$ y1: chr [1:2] "1" "10"
#   .. .. ..$ y2: chr [1:2] "2" "20"
#   .. .. ..$ z1: chr [1:2] "TRUE" "FALSE"
#   .. .. ..$ z2: chr [1:2] "FALSE" "TRUE"
#  $ schema  : chr [1:2] "0.0.1" "0.0.1"

Информация о сеансе

sessioninfo::session_info()
# ─ Session info ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
#  setting  value                       
#  version  R version 3.6.1 (2019-07-05)
#  os       Pop!_OS 19.10               
#  system   x86_64, linux-gnu           
#  ui       RStudio                     
#  language en_US:en                    
#  collate  en_US.UTF-8                 
#  ctype    en_US.UTF-8                 
#  tz       UTC                         
#  date     2020-01-14                  
# 
# ─ Packages ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
#  ! package       * version     date       lib source                         
#    askpass         1.1         2019-01-13 [1] CRAN (R 3.6.1)                 
#    assertthat      0.2.1       2019-03-21 [1] CRAN (R 3.6.1)                 
#    backports       1.1.5       2019-10-02 [1] CRAN (R 3.6.1)                 
#    bmp             0.3         2017-09-11 [1] CRAN (R 3.6.1)                 
#    callr           3.4.0       2019-12-09 [1] CRAN (R 3.6.1)                 
#    cli             2.0.1       2020-01-08 [1] CRAN (R 3.6.1)                 
#    colorspace      1.4-1       2019-03-18 [1] CRAN (R 3.6.1)                 
#    config          0.3         2018-03-27 [1] CRAN (R 3.6.1)                 
#    confx           0.0.0.9012  2020-01-05 [1] github (rappster/confx@9695409)
#    crayon          1.3.4       2017-09-16 [1] CRAN (R 3.6.1)                 
#    curl            4.3         2019-12-02 [1] CRAN (R 3.6.1)                 
#  R depot.dts.dce * 0.1.1.9003  <NA>       [?] <NA>                           
#    desc            1.2.0       2018-05-01 [1] CRAN (R 3.6.1)                 
#    devtools        2.2.1       2019-09-24 [1] CRAN (R 3.6.1)                 
#    digest          0.6.23      2019-11-23 [1] CRAN (R 3.6.1)                 
#    dplyr           0.8.3       2019-07-04 [1] CRAN (R 3.6.1)                 
#    ellipsis        0.3.0       2019-09-20 [1] CRAN (R 3.6.1)                 
#    fansi           0.4.1       2020-01-08 [1] CRAN (R 3.6.1)                 
#    fs              1.3.1       2019-05-06 [1] CRAN (R 3.6.1)                 
#    glue            1.3.1       2019-03-12 [1] CRAN (R 3.6.1)                 
#    here            0.1         2017-05-28 [1] CRAN (R 3.6.1)                 
#    igraph          1.2.4.2     2019-11-27 [1] CRAN (R 3.6.1)                 
#    imager          0.41.2      2019-01-23 [1] CRAN (R 3.6.1)                 
#    jpeg            0.1-8.1     2019-10-24 [1] CRAN (R 3.6.1)                 
#    jsonlite        1.6         2018-12-07 [1] CRAN (R 3.6.1)                 
#    knitr           1.26        2019-11-12 [1] CRAN (R 3.6.1)                 
#    later           1.0.0       2019-10-04 [1] CRAN (R 3.6.1)                 
#    lifecycle       0.1.0       2019-08-01 [1] CRAN (R 3.6.1)                 
#    lubridate       1.7.4       2018-04-11 [1] CRAN (R 3.6.1)                 
#    magick          2.2         2019-08-26 [1] CRAN (R 3.6.1)                 
#    magrittr        1.5         2014-11-22 [1] CRAN (R 3.6.1)                 
#    memoise         1.1.0       2017-04-21 [1] CRAN (R 3.6.1)                 
#    mongolite       2.1.0       2019-05-09 [1] CRAN (R 3.6.1)                 
#    munsell         0.5.0       2018-06-12 [1] CRAN (R 3.6.1)                 
#    openssl         1.4.1       2019-07-18 [1] CRAN (R 3.6.1)                 
#    pillar          1.4.3       2019-12-20 [1] CRAN (R 3.6.1)                 
#    pkgbuild        1.0.6       2019-10-09 [1] CRAN (R 3.6.1)                 
#    pkgconfig       2.0.3       2019-09-22 [1] CRAN (R 3.6.1)                 
#    pkgload         1.0.2       2018-10-29 [1] CRAN (R 3.6.1)                 
#    plyr            1.8.5       2019-12-10 [1] CRAN (R 3.6.1)                 
#    png             0.1-7       2013-12-03 [1] CRAN (R 3.6.1)                 
#    prettyunits     1.0.2       2015-07-13 [1] CRAN (R 3.6.1)                 
#    processx        3.4.1       2019-07-18 [1] CRAN (R 3.6.1)                 
#    promises      * 1.1.0       2019-10-04 [1] CRAN (R 3.6.1)                 
#    ps              1.3.0       2018-12-21 [1] CRAN (R 3.6.1)                 
#    purrr           0.3.3       2019-10-18 [1] CRAN (R 3.6.1)                 
#    R6              2.4.1       2019-11-12 [1] CRAN (R 3.6.1)                 
#    Rcpp            1.0.3       2019-11-08 [1] CRAN (R 3.6.1)                 
#    readbitmap      0.1.5       2018-06-27 [1] CRAN (R 3.6.1)                 
#    remotes         2.1.0       2019-06-24 [1] CRAN (R 3.6.1)                 
#    renv            0.9.2       2019-12-09 [1] CRAN (R 3.6.1)                 
#    rlang           0.4.2       2019-11-23 [1] CRAN (R 3.6.1)                 
#    rprojroot       1.3-2       2018-01-03 [1] CRAN (R 3.6.1)                 
#    rstudioapi      0.10        2019-03-19 [1] CRAN (R 3.6.1)                 
#    scales          1.1.0       2019-11-18 [1] CRAN (R 3.6.1)                 
#    sessioninfo     1.1.1       2018-11-05 [1] CRAN (R 3.6.1)                 
#    stringi         1.4.3       2019-03-12 [1] CRAN (R 3.6.1)                 
#    stringr         1.4.0       2019-02-10 [1] CRAN (R 3.6.1)                 
#    testthat      * 2.3.1       2019-12-01 [1] CRAN (R 3.6.1)                 
#    tibble        * 2.1.3       2019-06-06 [1] CRAN (R 3.6.1)                 
#    tidyr           1.0.0       2019-09-11 [1] CRAN (R 3.6.1)                 
#    tidyselect      0.2.5       2018-10-11 [1] CRAN (R 3.6.1)                 
#    tiff            0.1-5       2013-09-04 [1] CRAN (R 3.6.1)                 
#    usethis         1.5.1       2019-07-04 [1] CRAN (R 3.6.1)                 
#    utf8            1.1.4       2018-05-24 [1] CRAN (R 3.6.1)                 
#    vctrs           0.2.99.9001 2020-01-08 [1] github (r-lib/vctrs@ab84679)   
#    withr           2.1.2       2018-03-15 [1] CRAN (R 3.6.1)                 
#    xfun            0.11        2019-11-12 [1] CRAN (R 3.6.1)                 
#    yaml            2.2.0       2018-07-25 [1] CRAN (R 3.6.1)                 
# 
# [1] /home/janko/R/x86_64-pc-linux-gnu-library/3.6
# [2] /usr/local/lib/R/site-library
# [3] /usr/lib/R/site-library
# [4] /usr/lib/R/library

1 Ответ

1 голос
/ 13 января 2020

Полагаю, вам придется использовать рекурсию к go по списку. Вот моя идея, но я мог заставить ее работать только с fromJSON из пакета r json, а не из пакета jsonlite.

Первый шаг - определить рекурсивную функцию для проверки глубины. элемента списка:

depth <- function(list_entry)
{
  if (is.list(list_entry) & !is.tibble(list_entry)) 
      return(max(sapply(list_entry, depth)) + 1)
  else 
      return(0)
}

Следующая функция рекурсивно пытается сделать разметку из элементов глубины 1 (если они являются векторами) или из элементов глубины 2 (если значения разметки перечислены индивидуально ). Если он найдет элемент глубины 0, он вернет его без изменений, а если элемент имеет глубину> 2 или не подходит для превращения в тиббл, он рекурсивно пропустит дочерние узлы для той же обработки.

recursive_tibble <- function(json_list)
{
  lapply(json_list, function(y)
  {
    if(depth(y) == 0)
      return(unlist(y))

    if(depth(y) == 1)
    {
        if (length(y) < 2) 
          return(unlist(y))

        if (length(unique(names(y))) == 1)
          return(as_tibble(do.call("rbind", lapply(y, unlist))))

        if (length(unique(unlist(lapply(y, length)))) == 1)
          return(as_tibble(do.call("cbind", lapply(y, unlist))))

        else return(unlist(y))
    }

    if (depth(y) == 2)
    {
        if (length(y) < 2) 
          return(recursive_tibble(y))

        if (all(do.call(`==`, lapply(y, names))))     
          return(as_tibble(do.call("rbind", lapply(y, unlist))))
    }

    else return(recursive_tibble(y))
  })
}

Так что теперь вы можете сделать:

recursive_tibble(x)
#> List of 2
#>  $ :List of 5
#>   ..$ _id      : chr "1234"
#>   ..$ createdAt: chr "2020-01-13 09:00:00"
#>   ..$ labels   : chr [1:2] "label-a" "label-b"
#>   ..$ levelOne :List of 1
#>   .. ..$ levelTwo:List of 1
#>   .. .. ..$ levelThree:Classes ‘tbl_df’, ‘tbl’ and 'data.frame': 2 obs. of  3 variables:
#>   .. .. .. ..$ x: chr [1:2] "A" "B"
#>   .. .. .. ..$ y: chr [1:2] "1" "2"
#>   .. .. .. ..$ z: chr [1:2] "TRUE" "FALSE"
#>   ..$ schema   : chr "0.0.1"
#>  $ :List of 5
#>   ..$ _id      : chr "5678"
#>   ..$ createdAt: chr "2020-01-13 09:01:00"
#>   ..$ labels   : chr [1:2] "label-a" "label-b"
#>   ..$ levelOne :List of 1
#>   .. ..$ levelTwo:List of 1
#>   .. .. ..$ levelThree:Classes ‘tbl_df’, ‘tbl’ and 'data.frame': 2 obs. of  3 variables:
#>   .. .. .. ..$ x: chr [1:2] "A" "B"
#>   .. .. .. ..$ y: chr [1:2] "1" "2"
#>   .. .. .. ..$ z: chr [1:2] "TRUE" "FALSE"
#>   ..$ schema   : chr "0.0.1"


...