Разворачиваем вложенные столбцы — списки с помощью языка R (пакет tidyr)

от автора

В большинстве случаев при работе с ответом полученным от API, или с любыми другими данными которые имеют сложную древовидную структуру, вы сталкиваетесь с форматами JSON и XML.

Эти форматы имеют множество преимуществ: они достаточно компактно хранят данные и позволяют избежать излишнего дублирования информации.

Минусом данных форматов является сложность их обработки и анализа. Неструктурированные данные невозможно использовать в вычислениях и нельзя строить на их основе визуализацию.

Данная статья является логическим продолжением публикации "R пакет tidyr и его новые функции pivot_longer и pivot_wider". Она поможет вам привести неструктурированные конструкции данных к привычному, и пригодному для анализа табличному виду с помощью пакета tidyr, входящего в ядро библиотеки tidyverse, и его функций семейства unnest_*().

Содержание

Если вы интересуетесь анализом данных возможно вам будут интересны мои telegram и youtube каналы. Большая часть контента которых посвящены языку R.

  1. Введение
  2. Пользователи GitHub
  3. Репозитории Github
  4. Персонажи Игры Престолов
  5. Геокодирование с помощью Google
  6. Дискография Шарлы Гельфанд
  7. Заключение

Введение

Rectangling (прим. переводчика, не нашел адекватных вариантов перевода этого термина, поэтому оставим его как есть.) — это процесс приведения не структурированных данных с вложенными массивами к двухмерной таблице, состоящей из привычных нам строк и столбцов. В tidyr есть несколько функций, которые помогут вам развернуть вложенные столбцы-списки и привести данные к прямоугольной, табличной форме:

  • unnest_longer() берет каждый элемент списка-столбца и создает новую строку.
  • unnest_wider() берет каждый элемент списка-столбца и создает новый столбец.
  • unnest_auto() автоматически определяет какую из функций лучше использовать
    unnest_longer() или unnest_wider().
  • hoist() похожа на unnest_wider() но отбирает только указанные компоненты и позволяет работать с несколькими уровнями вложенности.

Большинство проблем связанных с приведением не структурированных данных с несколькими уровнями вложенности к двухмерной таблице можно решить, комбинируя перечисленные функции с dplyr.

Для демонстрации этих приемов, мы будем использовать пакет repurrrsive, который предоставляет несколько сложных, многоуровневых списков, полученных из веб-API.

library(tidyr) library(dplyr) library(repurrrsive)

Пользователи GitHub

Начнем с gh_users, списка, который содержит информацию о шести пользователях GitHub. Для начала преобразуем список gh_users в tibble фрейм.:

users <-   tibble( user = gh_users ) 

Это кажется немного нелогичным: зачем приводить список gh_users, к более сложной структуре данных? Но у дата фрейма есть большое преимущество: он объединяет несколько векторов, так что все отслеживается в одном объекте.

Каждый элемент объекта users представляет собой именованный список, в котором каждый элемент представляет столбец.

names(users$user[[1]]) #>  [1] "login"               "id"                  "avatar_url"          #>  [4] "gravatar_id"         "url"                 "html_url"            #>  [7] "followers_url"       "following_url"       "gists_url"           #> [10] "starred_url"         "subscriptions_url"   "organizations_url"   #> [13] "repos_url"           "events_url"          "received_events_url" #> [16] "type"                "site_admin"          "name"                #> [19] "company"             "blog"                "location"            #> [22] "email"               "hireable"            "bio"                 #> [25] "public_repos"        "public_gists"        "followers"           #> [28] "following"           "created_at"          "updated_at"

Есть два способа превратить компоненты списка в столбцы. unnest_wider() берет каждый компонент и создает новый столбец:

users %>% unnest_wider(user) #> # A tibble: 6 x 30 #>   login     id avatar_url gravatar_id url   html_url followers_url #>   <chr>  <int> <chr>      <chr>       <chr> <chr>    <chr>         #> 1 gabo… 6.60e5 https://a… ""          http… https:/… https://api.… #> 2 jenn… 5.99e5 https://a… ""          http… https:/… https://api.… #> 3 jtle… 1.57e6 https://a… ""          http… https:/… https://api.… #> 4 juli… 1.25e7 https://a… ""          http… https:/… https://api.… #> 5 leep… 3.51e6 https://a… ""          http… https:/… https://api.… #> 6 masa… 8.36e6 https://a… ""          http… https:/… https://api.… #> # … with 23 more variables: following_url <chr>, gists_url <chr>, #> #   starred_url <chr>, subscriptions_url <chr>, organizations_url <chr>, #> #   repos_url <chr>, events_url <chr>, received_events_url <chr>, #> #   type <chr>, site_admin <lgl>, name <chr>, company <chr>, blog <chr>, #> #   location <chr>, email <chr>, public_repos <int>, public_gists <int>, #> #   followers <int>, following <int>, created_at <chr>, updated_at <chr>, #> #   bio <chr>, hireable <lgl>

В этом случае мы получили таблицу состоящую из 30 столбцов, и большинство из них нам не понадобятся, поэтому мы можем вместо unnest_wider() использовать hoist(). hoist() позволяет нам извлекать выбранные компоненты, используя тот же синтаксис, что и purrr::pluck():

users %>% hoist(user,    followers = "followers",    login = "login",    url = "html_url" ) #> # A tibble: 6 x 4 #>   followers login       url                            user              #>       <int> <chr>       <chr>                          <list>            #> 1       303 gaborcsardi https://github.com/gaborcsardi <named list [27]> #> 2       780 jennybc     https://github.com/jennybc     <named list [27]> #> 3      3958 jtleek      https://github.com/jtleek      <named list [27]> #> 4       115 juliasilge  https://github.com/juliasilge  <named list [27]> #> 5       213 leeper      https://github.com/leeper      <named list [27]> #> 6        34 masalmon    https://github.com/masalmon    <named list [27]>

hoist() удаляет указанные именованные компоненты из списка-столбца user, поэтому вы можете рассматривать hoist() как перемещение компонентов из внутреннего списка дата фрейма к его верхнему уровню.

Репозитории Github

Выравнивание списка gh_repos мы начинаем аналогично, преобразуя его в tibble:

repos <- tibble(repo = gh_repos) repos #> # A tibble: 6 x 1 #>   repo        #>   <list>      #> 1 <list [30]> #> 2 <list [30]> #> 3 <list [30]> #> 4 <list [26]> #> 5 <list [30]> #> 6 <list [30]>

На этот раз элементы user представляют собой список репозиториев, принадлежащих этому пользователю. Каждый репозиторий является отдельным наблюдением, поэтому согласно концепции аккуратных данных (прим. tidy data) они должны стать новыми строками, в связи с чем мы используем unnest_longer() а не unnest_wider():

repos <- repos %>% unnest_longer(repo) repos #> # A tibble: 176 x 1 #>    repo              #>    <list>            #>  1 <named list [68]> #>  2 <named list [68]> #>  3 <named list [68]> #>  4 <named list [68]> #>  5 <named list [68]> #>  6 <named list [68]> #>  7 <named list [68]> #>  8 <named list [68]> #>  9 <named list [68]> #> 10 <named list [68]> #> # … with 166 more rows

Теперь мы можем использовать unnest_wider() или hoist() :

repos %>% hoist(repo,    login = c("owner", "login"),    name = "name",   homepage = "homepage",   watchers = "watchers_count" ) #> # A tibble: 176 x 5 #>    login       name        homepage watchers repo              #>    <chr>       <chr>       <chr>       <int> <list>            #>  1 gaborcsardi after       <NA>            5 <named list [65]> #>  2 gaborcsardi argufy      <NA>           19 <named list [65]> #>  3 gaborcsardi ask         <NA>            5 <named list [65]> #>  4 gaborcsardi baseimports <NA>            0 <named list [65]> #>  5 gaborcsardi citest      <NA>            0 <named list [65]> #>  6 gaborcsardi clisymbols  ""             18 <named list [65]> #>  7 gaborcsardi cmaker      <NA>            0 <named list [65]> #>  8 gaborcsardi cmark       <NA>            0 <named list [65]> #>  9 gaborcsardi conditions  <NA>            0 <named list [65]> #> 10 gaborcsardi crayon      <NA>           52 <named list [65]> #> # … with 166 more rows

Обратите внимание на использование c("owner", "login"): это позволяет нам получить значение второго уровня из вложенного списка owner. Альтернативный подход состоит в том, чтобы получить весь список owner и затем с помощью функции unnest_wider() поместить каждый его элемент в столбец:

repos %>%    hoist(repo, owner = "owner") %>%    unnest_wider(owner) #> # A tibble: 176 x 18 #>    login     id avatar_url gravatar_id url   html_url followers_url #>    <chr>  <int> <chr>      <chr>       <chr> <chr>    <chr>         #>  1 gabo… 660288 https://a… ""          http… https:/… https://api.… #>  2 gabo… 660288 https://a… ""          http… https:/… https://api.… #>  3 gabo… 660288 https://a… ""          http… https:/… https://api.… #>  4 gabo… 660288 https://a… ""          http… https:/… https://api.… #>  5 gabo… 660288 https://a… ""          http… https:/… https://api.… #>  6 gabo… 660288 https://a… ""          http… https:/… https://api.… #>  7 gabo… 660288 https://a… ""          http… https:/… https://api.… #>  8 gabo… 660288 https://a… ""          http… https:/… https://api.… #>  9 gabo… 660288 https://a… ""          http… https:/… https://api.… #> 10 gabo… 660288 https://a… ""          http… https:/… https://api.… #> # … with 166 more rows, and 11 more variables: following_url <chr>, #> #   gists_url <chr>, starred_url <chr>, subscriptions_url <chr>, #> #   organizations_url <chr>, repos_url <chr>, events_url <chr>, #> #   received_events_url <chr>, type <chr>, site_admin <lgl>, repo <list>

Вместо того, что бы размышлять над выбором нужной функции unnest_longer() или unnest_wider() вы можете использовать unnest_auto(). Эта функция использует несколько эвристических методов для подбора наиболее подходящей функции для трансформации данных, и выводит сообщение о выбранном способе.

tibble(repo = gh_repos) %>%    unnest_auto(repo) %>%    unnest_auto(repo) #> Using `unnest_longer(repo)`; no element has names #> Using `unnest_wider(repo)`; elements have 68 names in common #> # A tibble: 176 x 67 #>        id name  full_name owner private html_url description fork  url   #>     <int> <chr> <chr>     <lis> <lgl>   <chr>    <chr>       <lgl> <chr> #>  1 6.12e7 after gaborcsa… <nam… FALSE   https:/… Run Code i… FALSE http… #>  2 4.05e7 argu… gaborcsa… <nam… FALSE   https:/… Declarativ… FALSE http… #>  3 3.64e7 ask   gaborcsa… <nam… FALSE   https:/… Friendly C… FALSE http… #>  4 3.49e7 base… gaborcsa… <nam… FALSE   https:/… Do we get … FALSE http… #>  5 6.16e7 cite… gaborcsa… <nam… FALSE   https:/… Test R pac… TRUE  http… #>  6 3.39e7 clis… gaborcsa… <nam… FALSE   https:/… Unicode sy… FALSE http… #>  7 3.72e7 cmak… gaborcsa… <nam… FALSE   https:/… port of cm… TRUE  http… #>  8 6.80e7 cmark gaborcsa… <nam… FALSE   https:/… CommonMark… TRUE  http… #>  9 6.32e7 cond… gaborcsa… <nam… FALSE   https:/… <NA>        TRUE  http… #> 10 2.43e7 cray… gaborcsa… <nam… FALSE   https:/… R package … FALSE http… #> # … with 166 more rows, and 58 more variables: forks_url <chr>, #> #   keys_url <chr>, collaborators_url <chr>, teams_url <chr>, #> #   hooks_url <chr>, issue_events_url <chr>, events_url <chr>, #> #   assignees_url <chr>, branches_url <chr>, tags_url <chr>, #> #   blobs_url <chr>, git_tags_url <chr>, git_refs_url <chr>, #> #   trees_url <chr>, statuses_url <chr>, languages_url <chr>, #> #   stargazers_url <chr>, contributors_url <chr>, subscribers_url <chr>, #> #   subscription_url <chr>, commits_url <chr>, git_commits_url <chr>, #> #   comments_url <chr>, issue_comment_url <chr>, contents_url <chr>, #> #   compare_url <chr>, merges_url <chr>, archive_url <chr>, #> #   downloads_url <chr>, issues_url <chr>, pulls_url <chr>, #> #   milestones_url <chr>, notifications_url <chr>, labels_url <chr>, #> #   releases_url <chr>, deployments_url <chr>, created_at <chr>, #> #   updated_at <chr>, pushed_at <chr>, git_url <chr>, ssh_url <chr>, #> #   clone_url <chr>, svn_url <chr>, size <int>, stargazers_count <int>, #> #   watchers_count <int>, language <chr>, has_issues <lgl>, #> #   has_downloads <lgl>, has_wiki <lgl>, has_pages <lgl>, #> #   forks_count <int>, open_issues_count <int>, forks <int>, #> #   open_issues <int>, watchers <int>, default_branch <chr>, #> #   homepage <chr>

Персонажи Игры Престолов

got_chars имеет идентичную структуру с gh_users: это набор именованных списков, где каждый элемент внутреннего списка описывает некоторый атрибут персонажа Игры Престолов. Приведение got_chars к табличному виду мы начинаем с создания дата фрейма, так же как и в приведённых ранее примерах, а затем переведём каждый элемент в отдельный столбец:

chars <- tibble(char = got_chars) chars #> # A tibble: 30 x 1 #>    char              #>    <list>            #>  1 <named list [18]> #>  2 <named list [18]> #>  3 <named list [18]> #>  4 <named list [18]> #>  5 <named list [18]> #>  6 <named list [18]> #>  7 <named list [18]> #>  8 <named list [18]> #>  9 <named list [18]> #> 10 <named list [18]> #> # … with 20 more rows  chars2 <- chars %>% unnest_wider(char) chars2 #> # A tibble: 30 x 18 #>    url      id name  gender culture born  died  alive titles aliases father #>    <chr> <int> <chr> <chr>  <chr>   <chr> <chr> <lgl> <list> <list>  <chr>  #>  1 http…  1022 Theo… Male   Ironbo… In 2… ""    TRUE  <chr … <chr [… ""     #>  2 http…  1052 Tyri… Male   ""      In 2… ""    TRUE  <chr … <chr [… ""     #>  3 http…  1074 Vict… Male   Ironbo… In 2… ""    TRUE  <chr … <chr [… ""     #>  4 http…  1109 Will  Male   ""      ""    In 2… FALSE <chr … <chr [… ""     #>  5 http…  1166 Areo… Male   Norvos… In 2… ""    TRUE  <chr … <chr [… ""     #>  6 http…  1267 Chett Male   ""      At H… In 2… FALSE <chr … <chr [… ""     #>  7 http…  1295 Cres… Male   ""      In 2… In 2… FALSE <chr … <chr [… ""     #>  8 http…   130 Aria… Female Dornish In 2… ""    TRUE  <chr … <chr [… ""     #>  9 http…  1303 Daen… Female Valyri… In 2… ""    TRUE  <chr … <chr [… ""     #> 10 http…  1319 Davo… Male   Wester… In 2… ""    TRUE  <chr … <chr [… ""     #> # … with 20 more rows, and 7 more variables: mother <chr>, spouse <chr>, #> #   allegiances <list>, books <list>, povBooks <list>, tvSeries <list>, #> #   playedBy <list>

Структура got_chars несколько сложнее, чем gh_users, т.к. некоторые компоненты списка char сами по себе являются списком, в результате мы получаем столбы — списки:

chars2 %>% select_if(is.list) #> # A tibble: 30 x 7 #>    titles    aliases    allegiances books     povBooks  tvSeries  playedBy  #>    <list>    <list>     <list>      <list>    <list>    <list>    <list>    #>  1 <chr [3]> <chr [4]>  <chr [1]>   <chr [3]> <chr [2]> <chr [6]> <chr [1]> #>  2 <chr [2]> <chr [11]> <chr [1]>   <chr [2]> <chr [4]> <chr [6]> <chr [1]> #>  3 <chr [2]> <chr [1]>  <chr [1]>   <chr [3]> <chr [2]> <chr [1]> <chr [1]> #>  4 <chr [1]> <chr [1]>  <???>       <chr [1]> <chr [1]> <chr [1]> <chr [1]> #>  5 <chr [1]> <chr [1]>  <chr [1]>   <chr [3]> <chr [2]> <chr [2]> <chr [1]> #>  6 <chr [1]> <chr [1]>  <???>       <chr [2]> <chr [1]> <chr [1]> <chr [1]> #>  7 <chr [1]> <chr [1]>  <???>       <chr [2]> <chr [1]> <chr [1]> <chr [1]> #>  8 <chr [1]> <chr [1]>  <chr [1]>   <chr [4]> <chr [1]> <chr [1]> <chr [1]> #>  9 <chr [5]> <chr [11]> <chr [1]>   <chr [1]> <chr [4]> <chr [6]> <chr [1]> #> 10 <chr [4]> <chr [5]>  <chr [2]>   <chr [1]> <chr [3]> <chr [5]> <chr [1]> #> # … with 20 more rows

Ваши дальнейшие действия зависят от целей анализа. Возможно, вам необходимо поместить в строки информацию по каждой книге и сериале, в котором появляется персонаж:

chars2 %>%    select(name, books, tvSeries) %>%    pivot_longer(c(books, tvSeries), names_to = "media", values_to = "value") %>%    unnest_longer(value) #> # A tibble: 180 x 3 #>    name             media    value             #>    <chr>            <chr>    <chr>             #>  1 Theon Greyjoy    books    A Game of Thrones #>  2 Theon Greyjoy    books    A Storm of Swords #>  3 Theon Greyjoy    books    A Feast for Crows #>  4 Theon Greyjoy    tvSeries Season 1          #>  5 Theon Greyjoy    tvSeries Season 2          #>  6 Theon Greyjoy    tvSeries Season 3          #>  7 Theon Greyjoy    tvSeries Season 4          #>  8 Theon Greyjoy    tvSeries Season 5          #>  9 Theon Greyjoy    tvSeries Season 6          #> 10 Tyrion Lannister books    A Feast for Crows #> # … with 170 more rows

Или, может быть, вы хотите создать таблицу, которая позволит вам сопоставить персонажа и произведение:

chars2 %>%    select(name, title = titles) %>%    unnest_longer(title) #> # A tibble: 60 x 2 #>    name              title                                                #>    <chr>             <chr>                                                #>  1 Theon Greyjoy     Prince of Winterfell                                 #>  2 Theon Greyjoy     Captain of Sea Bitch                                 #>  3 Theon Greyjoy     Lord of the Iron Islands (by law of the green lands) #>  4 Tyrion Lannister  Acting Hand of the King (former)                     #>  5 Tyrion Lannister  Master of Coin (former)                              #>  6 Victarion Greyjoy Lord Captain of the Iron Fleet                       #>  7 Victarion Greyjoy Master of the Iron Victory                           #>  8 Will              ""                                                   #>  9 Areo Hotah        Captain of the Guard at Sunspear                     #> 10 Chett             ""                                                   #> # … with 50 more rows

(Обратите внимание, на пустые значения "" в поле title, это связано с ошибками допущенными при вводе данных в got_chars: на самом деле персонажи для которых нет соответствующих заголовков книг и сериалов в поле title должны иметь вектор длины 0, а не вектор длины 1, содержащий пустую строку.)

Мы можем переписать приведённый выше пример используя функцию unnest_auto(). Этот подход удобен для разового анализа, но не стоит полагаться на unnest_auto() для использования на регулярной основе. Дело в том, что если ваша структура данных изменится unnest_auto() может поменять выбранный механизм преобразования данных, если изначально он разворачивал столбцы-списки в строки используя unnest_longer(), то при изменении структуры входящих данных логика может быть изменена в пользу unnest_wider(), и использование такого подхода на постоянной основе может привести к непредвиденным ошибкам.

tibble(char = got_chars) %>%    unnest_auto(char) %>%    select(name, title = titles) %>%    unnest_auto(title) #> Using `unnest_wider(char)`; elements have 18 names in common #> Using `unnest_longer(title)`; no element has names #> # A tibble: 60 x 2 #>    name              title                                                #>    <chr>             <chr>                                                #>  1 Theon Greyjoy     Prince of Winterfell                                 #>  2 Theon Greyjoy     Captain of Sea Bitch                                 #>  3 Theon Greyjoy     Lord of the Iron Islands (by law of the green lands) #>  4 Tyrion Lannister  Acting Hand of the King (former)                     #>  5 Tyrion Lannister  Master of Coin (former)                              #>  6 Victarion Greyjoy Lord Captain of the Iron Fleet                       #>  7 Victarion Greyjoy Master of the Iron Victory                           #>  8 Will              ""                                                   #>  9 Areo Hotah        Captain of the Guard at Sunspear                     #> 10 Chett             ""                                                   #> # … with 50 more rows

Геокодирование с помощью Google

Далее мы рассмотрим более сложную структуру данных, получаемых от службы геокодирования Google. Кэширование учётных данных противоречит условиям работы с API Google maps, поэтому я сначала напишу простую оболочку к API. Которая основана на хранении ключа API Google карт в переменной среды; если в переменных среды у вас не сохранён ключ для работы с API Google Maps, фрагменты кода представленные в этом разделе выполняться не будут.

has_key <- !identical(Sys.getenv("GOOGLE_MAPS_API_KEY"), "") if (!has_key) {   message("No Google Maps API key found; code chunks will not be run") }  # https://developers.google.com/maps/documentation/geocoding geocode <- function(address, api_key = Sys.getenv("GOOGLE_MAPS_API_KEY")) {   url <- "https://maps.googleapis.com/maps/api/geocode/json"   url <- paste0(url, "?address=", URLencode(address), "&key=", api_key)    jsonlite::read_json(url) }

Список, который возвращает эта функция, довольно сложен:

houston <- geocode("Houston TX") str(houston) #> List of 2 #>  $ results:List of 1 #>   ..$ :List of 5 #>   .. ..$ address_components:List of 4 #>   .. .. ..$ :List of 3 #>   .. .. .. ..$ long_name : chr "Houston" #>   .. .. .. ..$ short_name: chr "Houston" #>   .. .. .. ..$ types     :List of 2 #>   .. .. .. .. ..$ : chr "locality" #>   .. .. .. .. ..$ : chr "political" #>   .. .. ..$ :List of 3 #>   .. .. .. ..$ long_name : chr "Harris County" #>   .. .. .. ..$ short_name: chr "Harris County" #>   .. .. .. ..$ types     :List of 2 #>   .. .. .. .. ..$ : chr "administrative_area_level_2" #>   .. .. .. .. ..$ : chr "political" #>   .. .. ..$ :List of 3 #>   .. .. .. ..$ long_name : chr "Texas" #>   .. .. .. ..$ short_name: chr "TX" #>   .. .. .. ..$ types     :List of 2 #>   .. .. .. .. ..$ : chr "administrative_area_level_1" #>   .. .. .. .. ..$ : chr "political" #>   .. .. ..$ :List of 3 #>   .. .. .. ..$ long_name : chr "United States" #>   .. .. .. ..$ short_name: chr "US" #>   .. .. .. ..$ types     :List of 2 #>   .. .. .. .. ..$ : chr "country" #>   .. .. .. .. ..$ : chr "political" #>   .. ..$ formatted_address : chr "Houston, TX, USA" #>   .. ..$ geometry          :List of 4 #>   .. .. ..$ bounds       :List of 2 #>   .. .. .. ..$ northeast:List of 2 #>   .. .. .. .. ..$ lat: num 30.1 #>   .. .. .. .. ..$ lng: num -95 #>   .. .. .. ..$ southwest:List of 2 #>   .. .. .. .. ..$ lat: num 29.5 #>   .. .. .. .. ..$ lng: num -95.8 #>   .. .. ..$ location     :List of 2 #>   .. .. .. ..$ lat: num 29.8 #>   .. .. .. ..$ lng: num -95.4 #>   .. .. ..$ location_type: chr "APPROXIMATE" #>   .. .. ..$ viewport     :List of 2 #>   .. .. .. ..$ northeast:List of 2 #>   .. .. .. .. ..$ lat: num 30.1 #>   .. .. .. .. ..$ lng: num -95 #>   .. .. .. ..$ southwest:List of 2 #>   .. .. .. .. ..$ lat: num 29.5 #>   .. .. .. .. ..$ lng: num -95.8 #>   .. ..$ place_id          : chr "ChIJAYWNSLS4QIYROwVl894CDco" #>   .. ..$ types             :List of 2 #>   .. .. ..$ : chr "locality" #>   .. .. ..$ : chr "political" #>  $ status : chr "OK"

К счастью, мы можем пошагово решить проблему преобразования этих данных в табличный вид с помощью функций tidyr. Чтобы сделать задачу немного сложнее и реалистичнее, я начну с геокодирования нескольких городов:

  city <-   c ( "Houston" , "LA" , "New York" , "Chicago" , "Springfield" )  city_geo <-   purrr::map (city, geocode) 

Полученный результат я преобразую в tibble, для удобства добавлю столбец с соответствующим названием города.

loc <- tibble(city = city, json = city_geo) loc #> # A tibble: 5 x 2 #>   city        json             #>   <chr>       <list>           #> 1 Houston     <named list [2]> #> 2 LA          <named list [2]> #> 3 New York    <named list [2]> #> 4 Chicago     <named list [2]> #> 5 Springfield <named list [2]>

Первый уровень содержит компоненты status и result, который мы можем развернуть с помощью unnest_wider() :

loc %>%   unnest_wider(json) #> # A tibble: 5 x 3 #>   city        results    status #>   <chr>       <list>     <chr>  #> 1 Houston     <list [1]> OK     #> 2 LA          <list [1]> OK     #> 3 New York    <list [1]> OK     #> 4 Chicago     <list [1]> OK     #> 5 Springfield <list [1]> OK

Обратите внимание, что results является многоуровневым списком. У большинства городов есть 1 элемент (представляющий уникальное значение, соответствующее API геокодирования), но у Спрингфилда их два. Мы можем вытащить их в отдельные строки с помощью unnest_longer() :

loc %>%   unnest_wider(json) %>%    unnest_longer(results) #> # A tibble: 5 x 3 #>   city        results          status #>   <chr>       <list>           <chr>  #> 1 Houston     <named list [5]> OK     #> 2 LA          <named list [5]> OK     #> 3 New York    <named list [5]> OK     #> 4 Chicago     <named list [5]> OK     #> 5 Springfield <named list [5]> OK

Теперь все они имеют одинаковые компоненты, в чём можно убедиться с помощью unnest_wider():

loc %>%   unnest_wider(json) %>%    unnest_longer(results) %>%    unnest_wider(results) #> # A tibble: 5 x 7 #>   city   address_componen… formatted_addre… geometry place_id  types status #>   <chr>  <list>            <chr>            <list>   <chr>     <lis> <chr>  #> 1 Houst… <list [4]>        Houston, TX, USA <named … ChIJAYWN… <lis… OK     #> 2 LA     <list [4]>        Los Angeles, CA… <named … ChIJE9on… <lis… OK     #> 3 New Y… <list [3]>        New York, NY, U… <named … ChIJOwg_… <lis… OK     #> 4 Chica… <list [4]>        Chicago, IL, USA <named … ChIJ7cv0… <lis… OK     #> 5 Sprin… <list [5]>        Springfield, MO… <named … ChIJP5jI… <lis… OK

Мы можем найти координаты широты и долготы каждого города развернув список geometry:

loc %>%   unnest_wider(json) %>%    unnest_longer(results) %>%    unnest_wider(results) %>%    unnest_wider(geometry) #> # A tibble: 5 x 10 #>   city  address_compone… formatted_addre… bounds location location_type #>   <chr> <list>           <chr>            <list> <list>   <chr>         #> 1 Hous… <list [4]>       Houston, TX, USA <name… <named … APPROXIMATE   #> 2 LA    <list [4]>       Los Angeles, CA… <name… <named … APPROXIMATE   #> 3 New … <list [3]>       New York, NY, U… <name… <named … APPROXIMATE   #> 4 Chic… <list [4]>       Chicago, IL, USA <name… <named … APPROXIMATE   #> 5 Spri… <list [5]>       Springfield, MO… <name… <named … APPROXIMATE   #> # … with 4 more variables: viewport <list>, place_id <chr>, types <list>, #> #   status <chr>

А затем местоположение, для чего требуется развернуть location:

loc %>%   unnest_wider(json) %>%   unnest_longer(results) %>%   unnest_wider(results) %>%   unnest_wider(geometry) %>%   unnest_wider(location) #> # A tibble: 5 x 11 #>   city  address_compone… formatted_addre… bounds   lat    lng location_type #>   <chr> <list>           <chr>            <list> <dbl>  <dbl> <chr>         #> 1 Hous… <list [4]>       Houston, TX, USA <name…  29.8  -95.4 APPROXIMATE   #> 2 LA    <list [4]>       Los Angeles, CA… <name…  34.1 -118.  APPROXIMATE   #> 3 New … <list [3]>       New York, NY, U… <name…  40.7  -74.0 APPROXIMATE   #> 4 Chic… <list [4]>       Chicago, IL, USA <name…  41.9  -87.6 APPROXIMATE   #> 5 Spri… <list [5]>       Springfield, MO… <name…  37.2  -93.3 APPROXIMATE   #> # … with 4 more variables: viewport <list>, place_id <chr>, types <list>, #> #   status <chr>

Опять же, unnest_auto() упрощает описанную операцию с некоторыми рисками, которые могут быть вызваны изменением структуры входящих данных:

loc %>%   unnest_auto(json) %>%   unnest_auto(results) %>%   unnest_auto(results) %>%   unnest_auto(geometry) %>%   unnest_auto(location) #> Using `unnest_wider(json)`; elements have 2 names in common #> Using `unnest_longer(results)`; no element has names #> Using `unnest_wider(results)`; elements have 5 names in common #> Using `unnest_wider(geometry)`; elements have 4 names in common #> Using `unnest_wider(location)`; elements have 2 names in common #> # A tibble: 5 x 11 #>   city  address_compone… formatted_addre… bounds   lat    lng location_type #>   <chr> <list>           <chr>            <list> <dbl>  <dbl> <chr>         #> 1 Hous… <list [4]>       Houston, TX, USA <name…  29.8  -95.4 APPROXIMATE   #> 2 LA    <list [4]>       Los Angeles, CA… <name…  34.1 -118.  APPROXIMATE   #> 3 New … <list [3]>       New York, NY, U… <name…  40.7  -74.0 APPROXIMATE   #> 4 Chic… <list [4]>       Chicago, IL, USA <name…  41.9  -87.6 APPROXIMATE   #> 5 Spri… <list [5]>       Springfield, MO… <name…  37.2  -93.3 APPROXIMATE   #> # … with 4 more variables: viewport <list>, place_id <chr>, types <list>, #> #   status <chr>

Мы также можем просто посмотреть на первый адрес для каждого города:

loc %>%   unnest_wider(json) %>%   hoist(results, first_result = 1) %>%   unnest_wider(first_result) %>%   unnest_wider(geometry) %>%   unnest_wider(location) #> # A tibble: 5 x 11 #>   city  address_compone… formatted_addre… bounds   lat    lng location_type #>   <chr> <list>           <chr>            <list> <dbl>  <dbl> <chr>         #> 1 Hous… <list [4]>       Houston, TX, USA <name…  29.8  -95.4 APPROXIMATE   #> 2 LA    <list [4]>       Los Angeles, CA… <name…  34.1 -118.  APPROXIMATE   #> 3 New … <list [3]>       New York, NY, U… <name…  40.7  -74.0 APPROXIMATE   #> 4 Chic… <list [4]>       Chicago, IL, USA <name…  41.9  -87.6 APPROXIMATE   #> 5 Spri… <list [5]>       Springfield, MO… <name…  37.2  -93.3 APPROXIMATE   #> # … with 4 more variables: viewport <list>, place_id <chr>, types <list>, #> #   status <chr>

Или использовать hoist() для многоуровневого погружения, чтобы перейти непосредственно к lat и lng.

loc %>%   hoist(json,     lat = list("results", 1, "geometry", "location", "lat"),     lng = list("results", 1, "geometry", "location", "lng")   ) #> # A tibble: 5 x 4 #>   city          lat    lng json             #>   <chr>       <dbl>  <dbl> <list>           #> 1 Houston      29.8  -95.4 <named list [2]> #> 2 LA           34.1 -118.  <named list [2]> #> 3 New York     40.7  -74.0 <named list [2]> #> 4 Chicago      41.9  -87.6 <named list [2]> #> 5 Springfield  37.2  -93.3 <named list [2]>

Дискография Шарлы Гельфанд

В завершении мы рассмотрим самую сложную конструкцию — дискографию Шарлы Гельфанд. Как и в приведённых выше примерах, мы начинаем с конвертации списка в дата фрейм с одним столбцом, а затем расширим его, чтобы каждый компонент был отдельным столбцом. Также я преобразую столбец date_added в соответствующий формат даты и времени в R.

discs <- tibble(disc = discog) %>%    unnest_wider(disc) %>%    mutate(date_added = as.POSIXct(strptime(date_added, "%Y-%m-%dT%H:%M:%S")))  discs #> # A tibble: 155 x 5 #>    instance_id date_added          basic_information       id rating #>          <int> <dttm>              <list>               <int>  <int> #>  1   354823933 2019-02-16 17:48:59 <named list [11]>  7496378      0 #>  2   354092601 2019-02-13 14:13:11 <named list [11]>  4490852      0 #>  3   354091476 2019-02-13 14:07:23 <named list [11]>  9827276      0 #>  4   351244906 2019-02-02 11:39:58 <named list [11]>  9769203      0 #>  5   351244801 2019-02-02 11:39:37 <named list [11]>  7237138      0 #>  6   351052065 2019-02-01 20:40:53 <named list [11]> 13117042      0 #>  7   350315345 2019-01-29 15:48:37 <named list [11]>  7113575      0 #>  8   350315103 2019-01-29 15:47:22 <named list [11]> 10540713      0 #>  9   350314507 2019-01-29 15:44:08 <named list [11]> 11260950      0 #> 10   350314047 2019-01-29 15:41:35 <named list [11]> 11726853      0 #> # … with 145 more rows

На этом уровне мы получили информацию о том, когда каждый диск был добавлен в дискографию Шарлы, но при этом не видим никаких данных об этих дисках. Для этого нам нужно расширить столбец basic_information:

discs %>% unnest_wider(basic_information) #> Column name `id` must not be duplicated. #> Use .name_repair to specify repair.

К сожалению мы получим ошибку, т.к. внутри списка basic_information есть одноимённый столбец basic_information. При возникновении подобной ошибки, для того, что бы быстро определить её причину можно использовать names_repair = "unique":

discs %>% unnest_wider(basic_information, names_repair = "unique") #> New names: #> * id -> id...6 #> * id -> id...14 #> # A tibble: 155 x 15 #>    instance_id date_added          labels  year artists id...6 thumb title #>          <int> <dttm>              <list> <int> <list>   <int> <chr> <chr> #>  1   354823933 2019-02-16 17:48:59 <list…  2015 <list … 7.50e6 http… Demo  #>  2   354092601 2019-02-13 14:13:11 <list…  2013 <list … 4.49e6 http… Obse… #>  3   354091476 2019-02-13 14:07:23 <list…  2017 <list … 9.83e6 http… I     #>  4   351244906 2019-02-02 11:39:58 <list…  2017 <list … 9.77e6 http… Oído… #>  5   351244801 2019-02-02 11:39:37 <list…  2015 <list … 7.24e6 http… A Ca… #>  6   351052065 2019-02-01 20:40:53 <list…  2019 <list … 1.31e7 http… Tash… #>  7   350315345 2019-01-29 15:48:37 <list…  2014 <list … 7.11e6 http… Demo  #>  8   350315103 2019-01-29 15:47:22 <list…  2015 <list … 1.05e7 http… Let … #>  9   350314507 2019-01-29 15:44:08 <list…  2017 <list … 1.13e7 ""    Sub … #> 10   350314047 2019-01-29 15:41:35 <list…  2017 <list … 1.17e7 http… Demo  #> # … with 145 more rows, and 7 more variables: formats <list>, #> #   cover_image <chr>, resource_url <chr>, master_id <int>, #> #   master_url <chr>, id...14 <int>, rating <int>

Проблема в том, что basic_information повторяет столбец id который также хранится на верхнем уровне, поэтому мы можем просто удалить его:

discs %>%    select(-id) %>%    unnest_wider(basic_information) #> # A tibble: 155 x 14 #>    instance_id date_added          labels  year artists     id thumb title #>          <int> <dttm>              <list> <int> <list>   <int> <chr> <chr> #>  1   354823933 2019-02-16 17:48:59 <list…  2015 <list … 7.50e6 http… Demo  #>  2   354092601 2019-02-13 14:13:11 <list…  2013 <list … 4.49e6 http… Obse… #>  3   354091476 2019-02-13 14:07:23 <list…  2017 <list … 9.83e6 http… I     #>  4   351244906 2019-02-02 11:39:58 <list…  2017 <list … 9.77e6 http… Oído… #>  5   351244801 2019-02-02 11:39:37 <list…  2015 <list … 7.24e6 http… A Ca… #>  6   351052065 2019-02-01 20:40:53 <list…  2019 <list … 1.31e7 http… Tash… #>  7   350315345 2019-01-29 15:48:37 <list…  2014 <list … 7.11e6 http… Demo  #>  8   350315103 2019-01-29 15:47:22 <list…  2015 <list … 1.05e7 http… Let … #>  9   350314507 2019-01-29 15:44:08 <list…  2017 <list … 1.13e7 ""    Sub … #> 10   350314047 2019-01-29 15:41:35 <list…  2017 <list … 1.17e7 http… Demo  #> # … with 145 more rows, and 6 more variables: formats <list>, #> #   cover_image <chr>, resource_url <chr>, master_id <int>, #> #   master_url <chr>, rating <int>

В качестве альтернативы, мы могли бы использовать hoist():

discs %>%    hoist(basic_information,     title = "title",     year = "year",     label = list("labels", 1, "name"),     artist = list("artists", 1, "name")   ) #> # A tibble: 155 x 9 #>    instance_id date_added          title  year label artist #>          <int> <dttm>              <chr> <int> <chr> <chr>  #>  1   354823933 2019-02-16 17:48:59 Demo   2015 Tobi… Mollot #>  2   354092601 2019-02-13 14:13:11 Obse…  2013 La V… Una B… #>  3   354091476 2019-02-13 14:07:23 I      2017 La V… S.H.I… #>  4   351244906 2019-02-02 11:39:58 Oído…  2017 La V… Rata … #>  5   351244801 2019-02-02 11:39:37 A Ca…  2015 Kato… Ivy (… #>  6   351052065 2019-02-01 20:40:53 Tash…  2019 High… Tashme #>  7   350315345 2019-01-29 15:48:37 Demo   2014 Mind… Desgr… #>  8   350315103 2019-01-29 15:47:22 Let …  2015 Not … Phant… #>  9   350314507 2019-01-29 15:44:08 Sub …  2017 Not … Sub S… #> 10   350314047 2019-01-29 15:41:35 Demo   2017 Pres… Small… #> # … with 145 more rows, and 3 more variables: basic_information <list>, #> #   id <int>, rating <int>

Здесь я быстро извлекаю имя первого лейбла и исполнителя по индексу, погрузившись во вложенный список.

Более систематический подход заключается в создании отдельных таблиц для исполнителя и лейбла:

discs %>%    hoist(basic_information, artist = "artists") %>%    select(disc_id = id, artist) %>%    unnest_longer(artist) %>%    unnest_wider(artist) #> # A tibble: 167 x 8 #>     disc_id join  name        anv   tracks role  resource_url            id #>       <int> <chr> <chr>       <chr> <chr>  <chr> <chr>                <int> #>  1  7496378 ""    Mollot      ""    ""     ""    https://api.discog… 4.62e6 #>  2  4490852 ""    Una Bèstia… ""    ""     ""    https://api.discog… 3.19e6 #>  3  9827276 ""    S.H.I.T. (… ""    ""     ""    https://api.discog… 2.77e6 #>  4  9769203 ""    Rata Negra  ""    ""     ""    https://api.discog… 4.28e6 #>  5  7237138 ""    Ivy (18)    ""    ""     ""    https://api.discog… 3.60e6 #>  6 13117042 ""    Tashme      ""    ""     ""    https://api.discog… 5.21e6 #>  7  7113575 ""    Desgraciad… ""    ""     ""    https://api.discog… 4.45e6 #>  8 10540713 ""    Phantom He… ""    ""     ""    https://api.discog… 4.27e6 #>  9 11260950 ""    Sub Space … ""    ""     ""    https://api.discog… 5.69e6 #> 10 11726853 ""    Small Man … ""    ""     ""    https://api.discog… 6.37e6 #> # … with 157 more rows  discs %>%    hoist(basic_information, format = "formats") %>%    select(disc_id = id, format) %>%    unnest_longer(format) %>%    unnest_wider(format) %>%    unnest_longer(descriptions) #> # A tibble: 280 x 5 #>     disc_id descriptions text  name     qty   #>       <int> <chr>        <chr> <chr>    <chr> #>  1  7496378 Numbered     Black Cassette 1     #>  2  4490852 LP           <NA>  Vinyl    1     #>  3  9827276 "7\""        <NA>  Vinyl    1     #>  4  9827276 45 RPM       <NA>  Vinyl    1     #>  5  9827276 EP           <NA>  Vinyl    1     #>  6  9769203 LP           <NA>  Vinyl    1     #>  7  9769203 Album        <NA>  Vinyl    1     #>  8  7237138 "7\""        <NA>  Vinyl    1     #>  9  7237138 45 RPM       <NA>  Vinyl    1     #> 10 13117042 "7\""        <NA>  Vinyl    1     #> # … with 270 more rows

Затем вы можете присоединить их обратно к исходному набору данных по мере необходимости.

Заключение

В ядро библиотеки tidyverse входят множество полезных пакетов объединённые общей философией обработки данных.

В этой статье мы разобрали семейство функций unnest_*(), которые направлены на работу с извлечением элементов из вложенных списков. Данный пакет содержит множество других полезных функций, которые упрощают преобразование данных согласно концепции Tidy Data.

ссылка на оригинал статьи https://habr.com/ru/post/491726/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *