Rozwijanie zagnieżdżonych kolumn — list z użyciem języka R (pakiet tidyr i funkcje rodziny unnest)

W większości przypadków, gdy pracujesz z odpowiedzią uzyskaną z API lub z innymi danymi o złożonej drzewiastej strukturze, napotykasz na formaty JSON i XML.

Te formaty mają wiele zalet: przechowują dane w sposób stosunkowo kompaktowy i pozwalają uniknąć zbędnego powielania informacji.

Wadą tych formatów jest ich złożoność w przetwarzaniu i analizie. Dane nieustrukturalizowane nie mogą być używane w obliczeniach ani nie można na ich podstawie tworzyć wizualizacji.

Rozwijanie zagnieżdżonych kolumn — list z użyciem języka R (pakiet tidyr i funkcje rodziny unnest)

Ten artykuł jest logiczną kontynuacją publikacji "Pakiet R tidyr i jego nowe funkcje pivot_longer i pivot_wider". Pomoże Ci to przekształcić nieustrukturalizowane konstrukcje danych w znany i użyteczny do analizy format tabelaryczny za pomocą pakietu tidyr, który jest częścią biblioteki tidyverse, oraz jego funkcji z rodziny unnest_*().

Spis treści

Jeśli interesujesz się analizą danych, to mogą Cię zainteresować moje telegram i youtube kanały. Większość zawartości poświęcona jest językowi R.

  1. Wprowadzenie
  2. Użytkownicy GitHub
  3. Repozytoria Github
  4. Postacie z Gry o Tron
  5. Geokodowanie za pomocą Google
  6. Dyskografia Szarly Gelfand
  7. Podsumowanie

Wprowadzenie

Rectangling (przyp. tłumacza: nie znalazłem adekwatnych opcji tłumaczenia tego terminu, dlatego zostawimy go bez zmian.) — to proces przekształcania nieustrukturalizowanych danych z zagnieżdżonymi tablicami w dwuwymiarową tabelę składającą się z znanych nam wierszy i kolumn. W tidyr istnieje kilka funkcji, które pomogą Ci rozwinąć zagnieżdżone kolumny-listy i przekształcić dane w prostokątną, tabelaryczną formę:

  • unnest_longer() bierze każdy element kolumny-listy i tworzy nowy wiersz.
  • unnest_wider() bierze każdy element kolumny-listy i tworzy nową kolumnę.
  • unnest_auto() automatycznie określa, którą z funkcji najlepiej użyć.
    unnest_longer() lub unnest_wider().
  • hoist() przypomina unnest_wider() ale wybiera tylko wskazane komponenty i pozwala pracować z wieloma poziomami zagnieżdżenia.

Większość problemów związanych z przekształceniem nieustrukturalizowanych danych z wieloma poziomami zagnieżdżenia w dwuwymiarową tabelę można rozwiązać, łącząc wymienione funkcje z dplyr.

Aby zademonstrować te techniki, będziemy używać pakietu repurrrsive, który dostarcza kilka złożonych, wielopoziomowych list uzyskanych z API sieci Web.

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

Użytkownicy GitHub

Zacznijmy od gh_users, listy zawierającej informacje o sześciu użytkownikach GitHub. Na początek przekształćmy listę gh_users do tibble w ramkę.:

users <- tibble( user = gh_users ) 

To może wydawać się nieco nielogiczne: po co tworzyć listę gh_users, do bardziej złożonej struktury danych? Ale ramka danych ma dużą zaletę: łączy kilka wektorów, więc wszystko jest śledzone w jednym obiekcie.

Każdy element obiektu users to nazwany wykaz, w którym każdy element reprezentuje kolumnę.

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"

Są dwa sposoby na przekształcenie komponentów wykazu w kolumny. unnest_wider() przyjmuje każdy komponent i tworzy nową kolumnę:

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>

W tym przypadku otrzymaliśmy tabelę składającą się z 30 kolumn, i większość z nich nie będzie nam potrzebna, więc możemy zamiast unnest_wider() już trzeba coś wykorzystać (pracuję z klastrowym Proxmox VE 5.x i ZFS over iSCSI). hoist(). hoist() pozwala nam wydobywać wybrane komponenty, używając tej samej składni, co purrr::pluck():

użytkownicy %>% 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() usuwa wskazane nazwane komponenty z listy-kolumny użytkownik, więc można to traktować hoist() jako przeniesienie komponentów z wewnętrznej listy ramki danych na jej górny poziom.

Repozytoria Github

Wyrównanie listy gh_repos zaczynamy podobnie, przekształcając ją w tibble:

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

Tym razem elementy użytkownik reprezentują listę repozytoriów należących do tego użytkownika. Każde repozytorium jest osobnym obserwowaniem, dlatego zgodnie z koncepcją czystych danych (przyp. tidy data) powinny stać się nowymi wierszami, dlatego używamy unnest_longer() a nie unnest_wider():

repos % unnest_longer(repo)
repos
#> # A tibble: 176 x 1
#>    repo             
#>               
#>  1 
#>  2 
#>  3 
#>  4 
#>  5 
#>  6 
#>  7 
#>  8 
#>  9 
#> 10 
#> # … z 166 więcej wierszy

Teraz możemy użyć unnest_wider() lub 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 
#> # … z 166 więcej wierszy

Zwróć uwagę na użycie c("owner", "login"): pozwala to uzyskać wartość drugiego poziomu z zagnieżdżonej listy ownerAlternatywne podejście polega na uzyskaniu pełnej listy owner i następnie przy użyciu funkcji unnest_wider() umieścić każdy jej element w kolumnie:

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.…
#> # … z 166 dodatkowymi wierszami, i 11 dodatkowymi zmiennymi: following_url ,
#> #   gists_url , starred_url , subscriptions_url ,
#> #   organizations_url , repos_url , events_url ,
#> #   received_events_url , type , site_admin , repo

Zamiast zastanawiać się nad wyborem odpowiedniej funkcji unnest_longer() lub unnest_wider() możesz skorzystać z unnest_auto(). Ta funkcja wykorzystuje kilka metod heurystycznych do dopasowania najbardziej odpowiedniej funkcji do transformacji danych i wyświetla komunikat o wybranym sposobie.

tibble(repo = gh_repos) %>% 
  unnest_auto(repo) %>% 
  unnest_auto(repo)
#> Używając `unnest_longer(repo)`; żaden element nie ma nazw
#> Używając `unnest_wider(repo)`; elementy mają 68 wspólnych nazw
#> # 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…  # … z 166 więcej wierszy oraz 58 kolejnych zmiennych: 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

Postacie z Gry o Tron

got_chars ma identyczną strukturę jak gh_users: to zestaw nazwanych list, w którym każdy element wewnętrznej listy opisuje pewną cechę postaci Gry o Tron. Przekształcenie got_chars do formy tabelarycznej rozpoczynamy od stworzenia ramki danych, podobnie jak w wcześniej podanych przykładach, a następnie przekształcimy każdy element w osobną kolumnę:

chars  # A tibble: 30 x 1
#>    char             
#>               
#>  1 
#>  2 
#>  3 
#>  4 
#>  5 
#>  6 
#>  7 
#>  8 
#>  9 
#> 10 
#> # … z jeszcze 20 wierszy

chars2 % unnest_wider(char)
chars2
#> # A tibble: 30 x 18
#>    url      id name  gender culture born  died  alive titles aliases father
#>     <int> <chr> <chr>  <chr>   <chr> <chr> <lgl> <list> <list>  <chr> 
#>  1 http…  1022 Theo… Mężczyzna Żelazne… In 2… ""    TRUE  <chr … <chr [… ""    
#>  2 http…  1052 Tyri… Mężczyzna ""      In 2… ""    TRUE  <chr … <chr [… ""    
#>  3 http…  1074 Vict… Mężczyzna Żelazne… In 2… ""    TRUE  <chr … <chr [… ""    
#>  4 http…  1109 Will  Mężczyzna ""      ""    In 2… FALSE <chr … <chr [… ""    
#>  5 http…  1166 Areo… Mężczyzna Norvos… In 2… ""    TRUE  <chr … <chr [… ""    
#>  6 http…  1267 Chett Mężczyzna ""      At H… In 2… FALSE <chr … <chr [… ""    
#>  7 http…  1295 Cres… Mężczyzna ""      In 2… In 2… FALSE <chr … <chr [… ""    
#>  8 http…   130 Aria… Kobieta Dornijska In 2… ""    TRUE  <chr … <chr [… ""    
#>  9 http…  1303 Daen… Kobieta Valyriań… In 2… ""    TRUE  <chr … <chr [… ""    
#> 10 http…  1319 Davo… Mężczyzna Wester… In 2… ""    TRUE  <chr … <chr [… ""    
#> # … z jeszcze 20 wierszy, i 7 dodatkowych zmiennych: matka <chr>, małżonek <chr>,
#> #   przynależności <list>, książki <list>, książki POV <list>, seriale TV <list>,
#> #   grani przez <list>

Struktura got_chars nieco bardziej złożone niż gh_users, ponieważ niektóre komponenty listy char same w sobie są listą, w rezultacie otrzymujemy kolumny — listy:

chars2 %>% select_if(is.list)
#> # A tibble: 30 x 7
#>    titles    aliases    allegiances books     povBooks  tvSeries  playedBy 
#>                                  
#>  1          
#>  2         
#>  3          
#>  4    <łuk>          
#>  5          
#>  6    <łuk>          
#>  7    <łuk>          
#>  8          
#>  9         
#> 10          
#> # … z jeszcze 20 wierszy

Twoje dalsze działania zależą od celów analizy. Być może będziesz musiał wprowadzić do wierszy informacje o każdej książce i serialu, w którym pojawia się postać:

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
#> # … with 170 more rows

Lub może chcesz stworzyć tabelę, która pozwoli ci powiązać postać z dziełem:

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 (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

(Zwróć uwagę na puste wartości "" w polu title, jest to spowodowane błędami popełnionymi podczas wprowadzania danych w got_chars: w rzeczywistości postacie, dla których nie ma odpowiednich nagłówków książek i seriali w polu title powinny mieć wektor długości 0, a nie wektor długości 1 zawierający pusty ciąg.)

Możemy przepisać powyższy przykład, używając funkcji unnest_auto(). To podejście jest wygodne do jednorazowej analizy, ale nie warto polegać na unnest_auto() do stałego użytku. Rzecz w tym, że jeśli twoja struktura danych się zmieni unnest_auto() może zmienić preferowany mechanizm przetwarzania danych, jeśli początkowo rozszerzał kolumny-listy do wierszy, używając unnest_longer(), to przy zmianie struktury danych wejściowych logika może zostać zmieniona na korzyść unnest_wider(), a takie podejście na stałe może prowadzić do nieprzewidzianych błędów.

tibble(char = got_chars) %>% 
  unnest_auto(char) %>% 
  select(name, title = titles) %>% 
  unnest_auto(title)
#> Używając `unnest_wider(char)`; elementy mają 18 wspólnych nazw
#> Używając `unnest_longer(title)`; żaden element nie ma nazw
#> # A tibble: 60 x 2
#>    name              title                                               
#>                                                                
#>  1 Theon Greyjoy     Książę Winterfell                                   
#>  2 Theon Greyjoy     Kapitan Morza Bitch                                 
#>  3 Theon Greyjoy     Władca Żelaznych Wysp (według prawa zielonych ziem)
#>  4 Tyrion Lannister  Pełniący obowiązki Ręki Króla (były)                
#>  5 Tyrion Lannister  Mistrz Monet (były)                                 
#>  6 Victarion Greyjoy Dowódca Żelaznej Floty                           
#>  7 Victarion Greyjoy Mistrz Żelaznego Zwycięstwa                          
#>  8 Will              ""                                                  
#>  9 Areo Hotah        Kapitan Gwardii w Sunspear                          
#> 10 Chett             ""                                                  
#> # … z 50 innymi wierszami

Geokodowanie za pomocą Google

Następnie przyjrzymy się bardziej złożonej strukturze danych, uzyskiwanych od usługi geokodowania Google. Cachowanie danych uwierzytelniających jest sprzeczne z warunkami korzystania z API Google Maps, więc najpierw napiszę prostą powłokę do API. Opiera się ona na przechowywaniu klucza API Google Maps w zmiennej środowiskowej; jeśli w zmiennych środowiskowych nie masz zapisanego klucza do korzystania z API Google Maps, fragmenty kodu przedstawione w tej sekcji nie będą wykonywane.

has_key <- !identical(Sys.getenv("GOOGLE_MAPS_API_KEY"), "")
if (!has_key) {
  message("Nie znaleziono klucza API Google Maps; fragmenty kodu nie będą uruchamiane")
}

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

Lista, którą zwraca ta funkcja, jest dość złożona:

houston  Lista 2
#>  $ wyniki:Lista 1
#>   ..$ :Lista 5
#>   .. ..$ adres_components:Lista 4
#>   .. .. ..$ :Lista 3
#>   .. .. .. ..$ long_name : chr "Houston"
#>   .. .. .. ..$ short_name: chr "Houston"
#>   .. .. .. ..$ types     :Lista 2
#>   .. .. .. .. ..$ : chr "locality"
#>   .. .. .. .. ..$ : chr "political"
#>   .. .. ..$ :Lista 3
#>   .. .. .. ..$ long_name : chr "Harris County"
#>   .. .. .. ..$ short_name: chr "Harris County"
#>   .. .. .. ..$ types     :Lista 2
#>   .. .. .. .. ..$ : chr "administrative_area_level_2"
#>   .. .. .. .. ..$ : chr "political"
#>   .. .. ..$ :Lista 3
#>   .. .. .. ..$ long_name : chr "Texas"
#>   .. .. .. ..$ short_name: chr "TX"
#>   .. .. .. ..$ types     :Lista 2
#>   .. .. .. .. ..$ : chr "administrative_area_level_1"
#>   .. .. .. .. ..$ : chr "political"
#>   .. .. ..$ :Lista 3
#>   .. .. .. ..$ long_name : chr "Stany Zjednoczone"
#>   .. .. .. ..$ short_name: chr "US"
#>   .. .. .. ..$ types     :Lista 2
#>   .. .. .. .. ..$ : chr "country"
#>   .. .. .. .. ..$ : chr "political"
#>   .. ..$ formatted_address : chr "Houston, TX, USA"
#>   .. ..$ geometry          :Lista 4
#>   .. .. ..$ bounds       :Lista 2
#>   .. .. .. ..$ northeast:Lista 2
#>   .. .. .. .. ..$ lat: num 30.1
#>   .. .. .. .. ..$ lng: num -95
#>   .. .. .. ..$ southwest:Lista 2
#>   .. .. .. .. ..$ lat: num 29.5
#>   .. .. .. .. ..$ lng: num -95.8
#>   .. .. ..$ location     :Lista 2
#>   .. .. .. ..$ lat: num 29.8
#>   .. .. .. ..$ lng: num -95.4
#>   .. .. ..$ location_type: chr "APPROXIMATE"
#>   .. .. ..$ viewport     :Lista 2
#>   .. .. .. ..$ northeast:Lista 2
#>   .. .. .. .. ..$ lat: num 30.1
#>   .. .. .. .. ..$ lng: num -95
#>   .. .. .. ..$ southwest:Lista 2
#>   .. .. .. .. ..$ lat: num 29.5
#>   .. .. .. .. ..$ lng: num -95.8
#>   .. ..$ place_id          : chr "ChIJAYWNSLS4QIYROwVl894CDco"
#>   .. ..$ types             :Lista 2
#>   .. .. ..$ : chr "locality"
#>   .. .. ..$ : chr "political"
#>  $ status : chr "OK"

Na szczęście, możemy krok po kroku rozwiązać problem konwersji tych danych do formatu tabelarycznego za pomocą funkcji tidyr. Aby zadanie było trochę trudniejsze i bardziej realistyczne, zacznę od geokodowania kilku miast:

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

Uzyskany wynik przekształcę na tibble, dla wygody dodam kolumnę z odpowiadającą nazwą miasta.

loc  # A tibble: 5 x 2
#>   city        json            
#>                    
#> 1 Houston     
#> 2 LA          
#> 3 Nowy Jork   
#> 4 Chicago     
#> 5 Springfield

Pierwszy poziom zawiera komponenty status i result, które możemy rozwinąć za pomocą unnest_wider() :

loc %>%
  unnest_wider(json)
#> # A tibble: 5 x 3
#>   city        results    status
#>                
#> 1 Houston      OK    
#> 2 LA           OK    
#> 3 Nowy Jork    OK    
#> 4 Chicago      OK    
#> 5 Springfield  OK

Zauważ, że wyniki jest wielopoziomową listą. Większość miast ma 1 element (reprezentujący unikalną wartość zgodną z API geokodowania), ale Springfield ma ich dwa. Możemy je wydobyć w osobne wiersze przy użyciu 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

Teraz wszystkie mają te same komponenty, co można sprawdzić za pomocą 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

Możemy znaleźć współrzędne szerokości i długości geograficznej każdego miasta rozszerzając listę 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

A następnie lokalizacja, do czego wymagane jest rozszerzenie lokacja:

lok %>%
  unnest_wider(json) %>%
  unnest_longer(results) %>%
  unnest_wider(results) %>%
  unnest_wider(geometry) %>%
  unnest_wider(location)
#> # A tibble: 5 x 11
#>   miasto  komponent_adresu… sformatowany_adres… granice   lat    lng typ_lokalizacji
#>                                                
#> 1 Hous…          Houston, TX, USA     2 LA             Los Angeles, CA…     3 Nowe …         Nowy Jork, NY, U…   4 Chic…          Chicago, IL, USA    5 Spri…          Springfield, MO…   # … z 4 więcej zmiennymi: viewport , place_id , types ,
#> #   status

Ponownie, unnest_auto() upraszcza opisaną operację, wiążąc się z pewnym ryzykiem, które mogą być spowodowane zmianą struktury danych wejściowych:

lok %>%
  unnest_auto(json) %>%
  unnest_auto(results) %>%
  unnest_auto(results) %>%
  unnest_auto(geometry) %>%
  unnest_auto(location)
#> Użycie `unnest_wider(json)`; elementy mają 2 wspólne nazwy
#> Użycie `unnest_longer(results)`; żaden element nie ma nazw
#> Użycie `unnest_wider(results)`; elementy mają 5 wspólnych nazw
#> Użycie `unnest_wider(geometry)`; elementy mają 4 wspólne nazwy
#> Użycie `unnest_wider(location)`; elementy mają 2 wspólne nazwy
#> # A tibble: 5 x 11
#>   miasto  komponent_adresu… sformatowany_adres… granice   lat    lng typ_lokalizacji
#>                                                
#> 1 Hous…          Houston, TX, USA     2 LA             Los Angeles, CA…     3 Nowe …         Nowy Jork, NY, U…   4 Chic…          Chicago, IL, USA    5 Spri…          Springfield, MO…   # … z 4 więcej zmiennymi: viewport , place_id , types ,
#> #   status

Możemy także po prostu spojrzeć na pierwszy adres dla każdego miasta:

lok %>%
  unnest_wider(json) %>%
  hoist(results, first_result = 1) %>%
  unnest_wider(first_result) %>%
  unnest_wider(geometry) %>%
  unnest_wider(location)
#> # A tibble: 5 x 11
#>   miasto  komponent_adresu… sformatowany_adres… granice   lat    lng typ_lokalizacji
#>                                                
#> 1 Hous…          Houston, TX, USA     2 LA             Los Angeles, CA…     3 Nowe …         Nowy Jork, NY, U…   4 Chic…          Chicago, IL, USA    5 Spri…          Springfield, MO…   # … z 4 więcej zmiennymi: viewport , place_id , types ,
#> #   status

Lub użyć hoist() do wielopoziomowego zagłębiania, aby przejść bezpośrednio do lat i lng.

lok %>%
  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 Nowy Jork   40.7  -74.0 
#> 4 Chicago      41.9  -87.6 
#> 5 Springfield  37.2  -93.3

Dyskografia Szarly Gelfand

Na zakończenie omówimy najbardziej skomplikowaną konstrukcję — dyskografię Sharly Gel'fand. Tak jak w podanych powyżej przykładach, zaczynamy od konwersji listy do ramki danych z jedną kolumną, a następnie rozszerzymy ją, aby każdy komponent był osobną kolumną. Również przekształcę kolumnę data_added do odpowiedniego formatu daty i czasu w 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
#> # … with 145 more rows

Na tym etapie uzyskaliśmy informacje o tym, kiedy każdy dysk został dodany do dyskografii Sharly, jednak nie widzimy żadnych danych o tych dyskach. W tym celu musimy rozszerzyć kolumnę basic_information:

discs %>% unnest_wider(basic_information)
#> Nazwa kolumny `id` nie może być zduplikowana.
#> Użyj .name_repair, aby określić naprawę.

Niestety otrzymamy błąd, ponieważ w obrębie listy basic_information jest kolumna o tej samej nazwie basic_information. W przypadku wystąpienia takiego błędu, aby szybko zidentyfikować jego przyczynę, możesz użyć names_repair = "unique":

dyski %>% unnest_wider(basic_information, names_repair = "unique")
#> Nowe nazwy:
#> * id -> id...6
#> * id -> id...14
#> # A tibble: 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  # … z 145 więcej wierszy i 7 więcej zmiennych: formats ,
#> #   cover_image , resource_url , master_id ,
#> #   master_url , id...14 , rating

Problem polega na tym, że basic_information powtarza kolumnę id, która jest również przechowywana na górnym poziomie, więc możemy po prostu ją usunąć:

dyski %>% 
  select(-id) %>% 
  unnest_wider(basic_information)
#> # A tibble: 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  # … z 145 więcej wierszy oraz 6 więcej zmiennych: formats ,
#> #   cover_image , resource_url , master_id ,
#> #   master_url , rating

Alternatywnie moglibyśmy użyć hoist():

dyski %>% 
  hoist(basic_information,
    title = "title",
    year = "year",
    label = list("labels", 1, "name"),
    artist = list("artists", 1, "name")
  )
#> # A tibble: 155 x 9
#>    instance_id data_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…
#> # … with 145 more rows, and 3 more variables: basic_information ,
#> #   id , rating

Tutaj szybko wyodrębniam imię pierwszej wytwórni i artysty według indeksu, zagłębiając się w zagnieżdżoną listę.

Bardziej systematyczne podejście polega na utworzeniu odrębnych tabel dla artysty i wytwórni:

dyski %>% 
  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
#>                                    
#>  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

dyski %>% 
  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  
#>                     
#>  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    
#> # … with 270 more rows

Następnie można je ponownie połączyć z zestawem danych źródłowych w razie potrzeby.

Podsumowanie

W jądro biblioteki tidyverse wchodzą liczne przydatne pakiety połączone wspólną filozofią przetwarzania danych.

W tym artykule omówiliśmy rodzinę funkcji unnest_*(), które są przeznaczone do pracy z wydobywaniem elementów z zagnieżdżonych list. Ten pakiet zawiera wiele innych użytecznych funkcji, które upraszczają przekształcanie danych zgodnie z koncepcją Tidy Data.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster