Развиваме вложени колони — списъци с помощта на езика R (пакет tidyr и функции от семейството unnest)

В повечето случаи при работа с отговора, получен от API, или с други данни с комплексна дребнавидна структура, ще се сблъскате с формати JSON и XML.

Тези формати имат много предимства: те съхраняват данни компактно и позволяват избягване на излишно дублиране на информация.

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

Развиваме вложени колони — списъци с помощта на езика R (пакет tidyr и функции от семейството unnest)

Тази статия е логично продължение на публикацията "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
#>                                
#> 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.…
#> # … с още 23 променливи: following_url , gists_url ,
#> #   starred_url , subscriptions_url , organizations_url ,
#> #   repos_url , events_url , received_events_url ,
#> #   type , site_admin , name , company , blog ,
#> #   location , email , public_repos , public_gists ,
#> #   followers , following , created_at , updated_at ,
#> #   bio , hireable

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

пользователи %> % hoist(user, 
  followers = "followers", 
  login = "login", 
  url = "html_url"
)
#> # A tibble: 6 x 4
#>   followers login       url                            user             
#>                                                    
#> 1       303 gaborcsardi https://github.com/gaborcsardi 
#> 2       780 jennybc     https://github.com/jennybc     
#> 3      3958 jtleek      https://github.com/jtleek      
#> 4       115 juliasilge  https://github.com/juliasilge  
#> 5       213 leeper      https://github.com/leeper      
#> 6        34 masalmon    https://github.com/masalmon

hoist() премахва указаните именовани компоненти от списъка-колона user, така че можете да разглеждате hoist() като прехвърляне на компоненти от вътрешния списък на дата фрейма на неговото най-горно ниво.

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

Подравняване на списъка gh_repos започваме по подобен начин, като го преобразуваме в tibble:

repos  # A tibble: 6 x 1
#>   repo       
#>        
#> 1 
#> 2 
#> 3 
#> 4 
#> 5 
#> 6

Този път елементите user представляват списък от хранилища, принадлежащи на този потребител. Всяко хранилище е отделно наблюдение, така че според концепцията за подредени данни (бележка tidy data) те трябва да станат нови редове, поради което използваме unnest_longer() , а не unnest_wider():

repos  % unnest_longer(repo)
repos
#> # A tibble: 176 x 1
#>    repo             
#>               
#>  1 
#>  2 
#>  3 
#>  4 
#>  5 
#>  6 
#>  7 
#>  8 
#>  9 
#> 10 
#> # … с 166 повече реда

Сега можем да използваме 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             
#>                                     
#>  1 gaborcsardi after                   5 
#>  2 gaborcsardi argufy                 19 
#>  3 gaborcsardi ask                     5 
#>  4 gaborcsardi baseimports             0 
#>  5 gaborcsardi citest                  0 
#>  6 gaborcsardi clisymbols  ""             18 
#>  7 gaborcsardi cmaker                  0 
#>  8 gaborcsardi cmark                   0 
#>  9 gaborcsardi conditions              0 
#> 10 gaborcsardi crayon                 52 
#> # … с 166 повече реда

Обърнете внимание на използването на c("owner", "login"): това ни позволява да получим стойността на второто ниво от вложения списък собственикАлтернативен подход е да получите целия списък собственик и след това с функцията 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
#>                                 
#>  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.…
#> # … с 166 повече реда и 11 повече променливи: following_url ,
#> #   gists_url , starred_url , subscriptions_url ,
#> #   organizations_url , repos_url , events_url ,
#> #   received_events_url , type , site_admin , repo

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

tibble(repo = gh_repos) %>% 
  unnest_auto(repo) %>% 
  unnest_auto(repo)
#> Използване на `unnest_longer(repo)`; нямат имена
#> Използване на `unnest_wider(repo)`; елементите имат 68 общи имена
#> # A tibble: 176 x 67
#>        id name  full_name owner private html_url description fork  url  
#>                            
#>  1 6.12e7 after gaborcsa…   2 4.05e7 argu… gaborcsa…   3 3.64e7 ask   gaborcsa…   4 3.49e7 base… gaborcsa…   5 6.16e7 cite… gaborcsa…   6 3.39e7 clis… gaborcsa…   7 3.72e7 cmak… gaborcsa…   8 6.80e7 cmark gaborcsa…   9 6.32e7 cond… gaborcsa… <nam… FALSE   https:/…         TRUE  http…
#> 10 2.43e7 cray… gaborcsa…  # … с 166 повече реда и 58 допълнителни променливи: forks_url ,
#> #   keys_url , collaborators_url , teams_url ,
#> #   hooks_url , issue_events_url , events_url ,
#> #   assignees_url , branches_url , tags_url ,
#> #   blobs_url , git_tags_url , git_refs_url ,
#> #   trees_url , statuses_url , languages_url ,
#> #   stargazers_url , contributors_url , subscribers_url ,
#> #   subscription_url , commits_url , git_commits_url ,
#> #   comments_url , issue_comment_url , contents_url ,
#> #   compare_url , merges_url , archive_url ,
#> #   downloads_url , issues_url , pulls_url ,
#> #   milestones_url , notifications_url , labels_url ,
#> #   releases_url , deployments_url , created_at ,
#> #   updated_at , pushed_at , git_url , ssh_url ,
#> #   clone_url , svn_url , size , stargazers_count ,
#> #   watchers_count , language , has_issues ,
#> #   has_downloads , has_wiki , has_pages ,
#> #   forks_count , open_issues_count , forks ,
#> #   open_issues , watchers , default_branch ,
#> #   homepage

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

got_chars има идентична структура с gh_users: това е набор от именовани списъци, където всеки елемент от вътрешния списък описва определен атрибут на персонаж от Игра на тронове. Превеждане got_chars в табличен вид започваме с създаването на дата фрейм, както и в споменатите по-рано примери, а след това ще преместим всеки елемент в отделна колона:

chars  # A tibble: 30 x 1
#>    char             
#>               
#>  1 
#>  2 
#>  3 
#>  4 
#>  5 
#>  6 
#>  7 
#>  8 
#>  9 
#> 10 
#> # … with 20 more rows

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

Структура got_chars малко по-сложно от gh_users, тъй като някои елементи от списъка char самите по себе си са списъци, в резултат на което получаваме колони — списъци:

chars2 %>% select_if(is.list)
#> # A tibble: 30 x 7
#>    titles    aliases    allegiances books     povBooks  tvSeries  playedBy 
#>                                  
#>  1          
#>  2         
#>  3          
#>  4             
#>  5          
#>  6             
#>  7             
#>  8          
#>  9         
#> 10          
#> # … with 20 повече реда

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

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            
#>                                
#>  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
#> # … с още 170 реда

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

chars2 %>% 
  select(name, title = titles) %>% 
  unnest_longer(title)
#> # A tibble: 60 x 2
#>    name              title                                               
#>                                                                
#>  1 Theon Greyjoy     Prince of Winterfell                                
#>  2 Theon Greyjoy     Captain of Sea Bitch                                
#>  3 Theon Greyjoy     Lord of the Iron Islands (по закон на зелените земи)
#>  4 Tyrion Lannister  Acting Hand of the King (бивш)                     
#>  5 Tyrion Lannister  Master of Coin (бивш)                              
#>  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             ""                                                  
#> # … с още 50 реда

(Обърнете внимание на празните стойности "" в полето 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)
#> Използване на `unnest_wider(char)`; елементите имат 18 общи имена
#> Използване на `unnest_longer(title)`; нито един елемент няма имена
#> # A tibble: 60 x 2
#>    name              title                                               
#>                                                                
#>  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             ""                                                  
#> # … с още 50 реда

Геокодиране с помощта на Google

Следва да разгледаме по-сложната структура на данните, получавани от услугата за геокодиране на Google. Кеширането на идентификационни данни противоречи на условията за работа с API на Google Maps, затова първо ще напиша проста обвивка за API. Тя е основана на съхраняване на ключа на API на Google Maps в променливата среда; ако в променливите на среда нямате запазен ключ за работа с API на Google Maps, кодовите фрагменти, представени в този раздел, няма да бъдат изпълнени.

has_key <- !identical(Sys.getenv("GOOGLE_MAPS_API_KEY"), "")
if (!has_key) {
  message("Няма намерен ключ за API на Google Maps; кодовите блокове няма да бъдат изпълнени")
}

# 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)
}

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

хюстън  Списък от 2
#>  $ results:Списък от 1
#>   ..$ :Списък от 5
#>   .. ..$ address_components:Списък от 4
#>   .. .. ..$ :Списък от 3
#>   .. .. .. ..$ long_name : chr "Хюстън"
#>   .. .. .. ..$ short_name: chr "Хюстън"
#>   .. .. .. ..$ types     :Списък от 2
#>   .. .. .. .. ..$ : chr "locality"
#>   .. .. .. .. ..$ : chr "political"
#>   .. .. ..$ :Списък от 3
#>   .. .. .. ..$ long_name : chr "Окръг Харис"
#>   .. .. .. ..$ short_name: chr "Окръг Харис"
#>   .. .. .. ..$ types     :Списък от 2
#>   .. .. .. .. ..$ : chr "administrative_area_level_2"
#>   .. .. .. .. ..$ : chr "political"
#>   .. .. ..$ :Списък от 3
#>   .. .. .. ..$ long_name : chr "Тексас"
#>   .. .. .. ..$ short_name: chr "TX"
#>   .. .. .. ..$ types     :Списък от 2
#>   .. .. .. .. ..$ : chr "administrative_area_level_1"
#>   .. .. .. .. ..$ : chr "political"
#>   .. .. ..$ :Списък от 3
#>   .. .. .. ..$ long_name : chr "Съединени щати"
#>   .. .. .. ..$ short_name: chr "САЩ"
#>   .. .. .. ..$ types     :Списък от 2
#>   .. .. .. .. ..$ : chr "country"
#>   .. .. .. .. ..$ : chr "political"
#>   .. ..$ formatted_address : chr "Хюстън, TX, САЩ"
#>   .. ..$ geometry          :Списък от 4
#>   .. .. ..$ bounds       :Списък от 2
#>   .. .. .. ..$ northeast:Списък от 2
#>   .. .. .. .. ..$ lat: num 30.1
#>   .. .. .. .. ..$ lng: num -95
#>   .. .. .. ..$ southwest:Списък от 2
#>   .. .. .. .. ..$ lat: num 29.5
#>   .. .. .. .. ..$ lng: num -95.8
#>   .. .. ..$ location     :Списък от 2
#>   .. .. .. ..$ lat: num 29.8
#>   .. .. .. ..$ lng: num -95.4
#>   .. .. ..$ location_type: chr "ПРИБЛИЗИТЕЛНО"
#>   .. .. ..$ viewport     :Списък от 2
#>   .. .. .. ..$ northeast:Списък от 2
#>   .. .. .. .. ..$ lat: num 30.1
#>   .. .. .. .. ..$ lng: num -95
#>   .. .. .. ..$ southwest:Списък от 2
#>   .. .. .. .. ..$ lat: num 29.5
#>   .. .. .. .. ..$ lng: num -95.8
#>   .. ..$ place_id          : chr "ChIJAYWNSLS4QIYROwVl894CDco"
#>   .. ..$ types             :Списък от 2
#>   .. .. ..$ : chr "locality"
#>   .. .. ..$ : chr "political"
#>  $ status : chr "OK"

За щастие, можем стъпка по стъпка да решим проблема с преобразуването на тези данни в табличен формат с помощта на функции tidyr. За да направя задачата малко по-сложна и реалистична, ще започна с геокодирането на няколко града:

  града <- c ( "Хюстън" , "Лос Анджелис" , "Ню Йорк" , "Чикаго" , "Спрингфийлд" )  града_geo <- purrr::map (града, геокод) 

Полученият резултат ще преобразувам в tibble, за удобство ще добавя колона с име на съответстващия град.

loc  # A tibble: 5 x 2
#>   city        json
#>          
#> 1 Хюстън     
#> 2 Лос Анджелис 
#> 3 Ню Йорк    
#> 4 Чикаго     
#> 5 Спрингфийлд

Първото ниво съдържа компоненти status и резултат, които можем да разгърнем с помощта на unnest_wider() :

loc %>%
  unnest_wider(json)
#> # A tibble: 5 x 3
#>   city        results    status
#>               
#> 1 Хюстън      OK
#> 2 Лос Анджелис  OK
#> 3 Ню Йорк     OK
#> 4 Чикаго      OK
#> 5 Спрингфийлд  OK

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

loc %>%
  unnest_wider(json) %>% 
  unnest_longer(results)
#> # A tibble: 5 x 3
#>   city        results          status
#>                      
#> 1 Houston      OK    
#> 2 LA           OK    
#> 3 New York     OK    
#> 4 Chicago      OK    
#> 5 Springfield  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
#>                                       
#> 1 Houst…         Houston, TX, USA <named … ChIJAYWN…  2 LA             Los Angeles, CA… <named … ChIJE9on…  3 New Y…         New York, NY, U… <named … ChIJOwg_…  4 Chica…         Chicago, IL, USA <named … ChIJ7cv0…  5 Sprin…         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
#>                                       
#> 1 Hous…        Houston, TX, USA <name…  2 LA           Los Angeles, CA… <name…  3 New …        New York, NY, U… <name…  4 Chic…        Chicago, IL, USA <name…  5 Spri…        Springfield, MO… <name…  # … with 4 more variables: viewport , place_id , types ,
#> #   status

А след това местоположението, за което е необходимо да се разшири 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
#>%                                       
#>% 1 Hous…        Houston, TX, USA % 2 LA           Los Angeles, CA… % 3 New …        New York, NY, U… % 4 Chic…        Chicago, IL, USA % 5 Spri…        Springfield, MO… % # … with 4 more variables: viewport , place_id , types ,
#>% #   status

Отново, 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
#>%                                       
#>% 1 Hous…        Houston, TX, USA % 2 LA           Los Angeles, CA… % 3 New …        New York, NY, U… % 4 Chic…        Chicago, IL, USA % 5 Spri…        Springfield, MO… % # … with 4 more variables: viewport , place_id , types ,
#>% #   status

Също така можем просто да разгледаме първия адрес за всеки град:

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
#>%                                       
#>% 1 Hous…        Houston, TX, USA % 2 LA           Los Angeles, CA… % 3 New …        New York, NY, U… % 4 Chic…        Chicago, IL, USA % 5 Spri…        Springfield, MO… % # … with 4 more variables: viewport , place_id , types ,
#>% #   status

Или да използваме 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            
#>                       
#> 1 Houston      29.8  -95.4 
#> 2 LA           34.1 -118.  
#> 3 New York     40.7  -74.0 
#> 4 Chicago      41.9  -87.6 
#> 5 Springfield  37.2  -93.3

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

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

discs % 
  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
#>                                          
#>  1   354823933 2019-02-16 17:48:59   7496378      0
#>  2   354092601 2019-02-13 14:13:11   4490852      0
#>  3   354091476 2019-02-13 14:07:23   9827276      0
#>  4   351244906 2019-02-02 11:39:58   9769203      0
#>  5   351244801 2019-02-02 11:39:37   7237138      0
#>  6   351052065 2019-02-01 20:40:53  13117042      0
#>  7   350315345 2019-01-29 15:48:37   7113575      0
#>  8   350315103 2019-01-29 15:47:22  10540713      0
#>  9   350314507 2019-01-29 15:44:08  11260950      0
#> 10   350314047 2019-01-29 15:41:35  11726853      0
#> # … с 145 допълнителни реда

На това ниво получихме информация за времето, когато всеки диск е бил добавен в дискографията на Шарла, но все още не виждаме никакви данни за тези дискове. За да направим това, трябва да разширим стълба 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":

дисков %>% unnest_wider(basic_information, names_repair = "unique")
#> Новые имена:
#> * id -> id...6
#> * id -> id...14
#> # Тиббл: 155 x 15
#>    instance_id date_added          labels  year artists id...6 thumb title
#>                                
#>  1   354823933 2019-02-16 17:48:59 <list…  2015   2   354092601 2019-02-13 14:13:11 <list…  2013   3   354091476 2019-02-13 14:07:23 <list…  2017   4   351244906 2019-02-02 11:39:58 <list…  2017   5   351244801 2019-02-02 11:39:37 <list…  2015   6   351052065 2019-02-01 20:40:53 <list…  2019   7   350315345 2019-01-29 15:48:37 <list…  2014   8   350315103 2019-01-29 15:47:22 <list…  2015   9   350314507 2019-01-29 15:44:08 <list…  2017  10   350314047 2019-01-29 15:41:35 <list…  2017  # … с еще 145 строками и 7 дополнительными переменными: форматы ,
#> #   cover_image , resource_url , master_id ,
#> #   master_url , id...14 , рейтинг

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

дисков %>% 
  select(-id) %>% 
  unnest_wider(basic_information)
#> # Тиббл: 155 x 14
#>    instance_id date_added          labels  year artists     id thumb title
#>                                
#>  1   354823933 2019-02-16 17:48:59 <list…  2015   2   354092601 2019-02-13 14:13:11 <list…  2013   3   354091476 2019-02-13 14:07:23 <list…  2017   4   351244906 2019-02-02 11:39:58 <list…  2017   5   351244801 2019-02-02 11:39:37 <list…  2015   6   351052065 2019-02-01 20:40:53 <list…  2019   7   350315345 2019-01-29 15:48:37 <list…  2014   8   350315103 2019-01-29 15:47:22 <list…  2015   9   350314507 2019-01-29 15:44:08 <list…  2017  10   350314047 2019-01-29 15:41:35 <list…  2017  # … с еще 145 строками и 6 дополнительными переменными: форматы ,
#> #   cover_image , resource_url , master_id ,
#> #   master_url , рейтинг

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

диск %>% 
  hoist(basic_information,
    title = "title",
    year = "year",
    label = list("labels", 1, "name"),
    artist = list("artists", 1, "name")
  )
#> # Тибл: 155 x 9
#>    instance_id date_added          title  year label artist
#>                             
#>  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…
#> # … с 145 другими строками и 3 другими переменными: basic_information ,
#> #   id , rating

Тук аз бързо извличам името на първия лейбъл и изпълнител по индекс, потапяйки се в вложен списък.

По-систематичният подход включва създаване на отделни таблици за изпълнителя и лейбъла:

диск %>% 
  hoist(basic_information, artist = "artists") %>% 
  select(disc_id = id, artist) %>% 
  unnest_longer(artist) %>% 
  unnest_wider(artist)
#> # Тибл: 167 x 8
#>     disc_id join  name        anv   tracks role  resource_url            id
#>                                    
#>  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
#> # … с 157 други реда

dиск %>% 
  hoist(basic_information, format = "formats") %>% 
  select(disc_id = id, format) %>% 
  unnest_longer(format) %>% 
  unnest_wider(format) %>% 
  unnest_longer(descriptions)
#> # Тибл: 280 x 5
#>     disc_id descriptions text  name     qty  
#>                     
#>  1  7496378 Numbered     Black Cassette 1    
#>  2  4490852 LP             Vinyl    1    
#>  3  9827276 "7""          Vinyl    1    
#>  4  9827276 45 RPM         Vinyl    1    
#>  5  9827276 EP             Vinyl    1    
#>  6  9769203 LP             Vinyl    1    
#>  7  9769203 Album          Vinyl    1    
#>  8  7237138 "7""          Vinyl    1    
#>  9  7237138 45 RPM         Vinyl    1    
#> 10 13117042 "7""          Vinyl    1    
#> # … с 270 други реда

След това можете да ги присъедините обратно към оригиналния набор от данни при необходимост.

Заключение

В ядрото на библиотеката tidyverse влизат множество полезни пакети, обединени от обща философия за обработка на данни.

В тази статия разгледахме семейството на функциите unnest_*(), които са насочени към работа с извличането на елементи от вложени списъци. Този пакет съдържа множество други полезни функции, които опростяват трансформирането на данни според концепцията Tidy Data.

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster