Столбцы Tibble класса Tibble вместо фрейма данных класса - PullRequest
2 голосов
/ 14 января 2020

Как правильно иметь tibble столбцы класса tibble (вместо класса list или data.frame)?

Очевидно, что столбцы класса data.frame в * вполне могут быть 1008 * с (см. Пример ниже), но ни один из «аккуратных способов манипулирования данными» (т.е. dplyr::mutate() или purrr::map*_df()), похоже, не работает для меня при попытке привести столбцы к tibble вместо data.frame

Текущий выход jsonlite::fromJSON()

# '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"

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

# 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"

Почему наличие data.frame столбцов может быть очень обманчивым

https://hendrikvanb.gitlab.io/2018/07/nested_data-json_to_tibble/

Связано


Пример

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

library(magrittr)

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"
  }
]'

. Визуализируя это, вы увидите, что есть тонкое, но важное различие между объектами (которые сопоставляются с data.frame s) и массивом * 10 52 * (который соответствует list с):

enter image description here

Синтаксический анализ JSON и преобразование в tibble

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

x %>% 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"

Так что вполне возможно иметь столбцы класса data.frame.

Приведение столбцов data.frame к tibble: «плохой путь»

Но я бы хотел тиблы вместо фреймов данных, давайте попробуем только то, что мне нужно: явное переназначение соответствующих уровней списка или столбцов фрейма / столбца данных, чтобы быть более точным:

# Make a copy so we don't mess with the initial state of `x`
y <- x

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

x %>% 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"

Это работает, но не соответствует "аккуратным манипуляциям с данными".

Приведение data.frame к tibble столбцам: "лучший путь" (пробный и неудачный)

# Yet another copy so we can compare:
z <- x

# Just to check that this works
z$levelOne %>% 
    tibble::as_tibble()
# # A tibble: 2 x 1
#   levelTwo$levelThree
#   <list>             
# 1 <df[,3] [2 × 3]>   
# 2 <df[,3] [2 × 3]>   

# Trying to get this to work with `dplzr::mutate()` fails:
z %>% 
  dplyr::mutate(levelOne = levelOne %>% 
    tibble::as_tibble()
  )
# Error: Column `levelOne` is of unsupported class data.frame

z %>% 
  dplyr::transmute(levelOne = levelOne %>% 
    tibble::as_tibble()
  )
# Error: Column `levelOne` is of unsupported class data.frame

# Same goes for `{purrr}`:
z %>% 
  dplyr::mutate(levelOne = levelOne %>% 
    purrr::map_df(tibble::as_tibble)
  )
# Error: Column `levelOne` is of unsupported class data.frame

z %>% 
  tibble::add_column(levelOne = z$levelOne %>% tibble::as_tibble())
# Error: Can't add duplicate columns with `add_column()`:
# * Column `levelOne` already exists in `.data`.

# Works, but not what I want:
z %>% 
  tibble::add_column(test = z$levelOne %>% tibble::as_tibble()) %>% 
  str()
# Classes ‘tbl_df’, ‘tbl’ and 'data.frame': 2 obs. of  4 variables:
#  [...]
#  $ test    :Classes ‘tbl_df’, ‘tbl’ and '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

Единственное это сработало (это не то, что нам нужно)

Обтекание tibble::as_tibble() на purrr::map() кажется работающим, но результат явно не тот, что мы хотим, поскольку мы дублируем все ниже levelOne (сравните с желаемым выводом выше)

# Works, but not what I want:
z_new <- z %>% 
  dplyr::mutate(levelOne = levelOne %>% 
    purrr::map(tibble::as_tibble)
  )

z_new %>% 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
#   ..$ :Classes ‘tbl_df’, ‘tbl’ and '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
#   ..$ :Classes ‘tbl_df’, ‘tbl’ and '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"

РЕДАКТИРОВАТЬ (последующее расследование)

Работать с помощью Хендрика!

Все еще ИМО, эта топи c поднимает некоторые интересные дополнительные вопросы, касающиеся того, следует ли - или даже может - сделать это любым другим способом, если основная цель состоит в том, чтобы получить аккуратные вложенные тиблы, которые играют хорошо с tidyr::unnset() и tidyr::nest() (см. комментарии в ответе Хендрика ниже).

Что касается предложенного подхода в https://hendrikvanb.gitlab.io/2018/07/nested_data-json_to_tibble/: я мог бы пропустить что-то очевидное, но я думаю, он работает только для JSON документов с одним документом.

Во-первых, давайте изменим df_to_tibble() (см. ответ Хендрика ниже), чтобы превратить только «листовые» фреймы данных в тиблы, а «ветвящиеся» фреймы данных в списки:

leaf_df_to_tibble <- function(x) {
  if (is.data.frame(x)) {
    if (!any(purrr::map_lgl(x, is.list))) { 
      # Only captures "leaf" DFs:
      tibble::as_tibble(x) 
    } else {
      as.list(x)
    }
  } else {
    x
  }
}

Это даст нам результаты, которые соответствуют предлагаемому способу в сообщении в блоге, но только для "одного объекта" JSON документов, как показано ниже

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

# Only take the first object from the parsed JSON:
df_subset <- df[1, ]

Преобразование df_subset:

df_subset_tibble <- purrr::reduce(
  0:purrr::vec_depth(df_subset),
  function(x, depth) {
    purrr::modify_depth(x, depth, leaf_df_to_tibble, .ragged = TRUE)
  }, 
  .init = df_subset
) %>% 
  tibble::as_tibble()

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

Преобразование df:

df_tibble <- purrr::reduce(
  0:purrr::vec_depth(df),
  function(x, depth) {
    purrr::modify_depth(x, depth, leaf_df_to_tibble, .ragged = TRUE)
  }, 
  .init = df
) %>% 
  tibble::as_tibble()

df_tibble %>% 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"

Как мы видим, "листинг" вложенных JSON структур на самом деле может привести к копированию "листьев" ». Он просто не прыгает на вас, пока n = 1 (количество JSON документов), но поражает вас, как только n > 1.

1 Ответ

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

Фон

Приведенные выше комментарии поднимают некоторые допустимые моменты. Тем не менее, я верю, что есть способ достичь того, чего вы хотите (независимо от того, является ли это особенно хорошей идеей, не совсем понятно), используя три функции из пакета purrr в комбинации:

  1. purrr::vec_depth позволяет нам получить (вложенность) глубину данного списка,
  2. purrr::modify_depth позволяет нам применить функцию к списку на указанном уровне глубины, и
  3. purrr::reduce позволяет нам итеративно применять функцию и передавать результат каждой итерации в качестве входных данных для последующей итерации.

Общий подход

По сути, мы хотим преобразовать любой data.frame найденный на любом уровне в списке в tibble. Этого легко достичь, используя несколько раундов purrr::modify_depth, где мы просто изменяем глубину в зависимости от уровня списка, на который мы нацеливаемся sh. Важно отметить, однако, что мы хотим сделать это таким образом, чтобы изменения уровня 1, например, были сохранены, когда мы перейдем к уровню 2; изменения уровня 1 и 2 сохраняются при переходе на уровень 3; и так далее. Вот где приходит purrr::reduce: каждый раз, когда мы применяем purrr::modify_depth для преобразования data.frame в тиббл, мы гарантируем, что результирующий вывод будет передан в качестве ввода для следующей итерации. Это показано в MWE ниже

MWE

Начните с базовой c настройки структур данных и библиотек

#> Load libraries ----
library(tidyverse)

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"
  }
]'  

# convert json to a nested data.frame
df <- jsonlite::fromJSON(json)

Теперь мы создадим простой помощник функция, которая может условно преобразовывать data.frame в tibble

# define a simple function to convert data.frame to tibble
df_to_tibble <- function(x) {
  if (is.data.frame(x)) as_tibble(x) else x
}

Теперь для критически важной процедуры: принимая df в качестве начальной отправной точки (.init = df), применяйте функцию df_to_tibble на каждом уровень df (0:purrr::vec_depth(df)) с использованием purrr::modify_depth. Используйте purrr::reduce, чтобы гарантировать, что результаты каждой отдельной итерации будут переданы в качестве входных данных для следующей итерации.

# create df_tibble by reducing the result of applying df_to_tibble to each level
# of df via purrr's modify_depth function %>% lastly, ensure that the top level
# data.frame is also converted to a tibble
df_tibble <- purrr::reduce(
  0:purrr::vec_depth(df),
  function(x, depth) {
    purrr::modify_depth(x, depth, df_to_tibble, .ragged = TRUE)
  }, 
  .init = df
) %>% 
  as_tibble()
# show the structure of df_tibble
str(df_tibble)
#> 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"
...