ProHoster > Blog > administracja > Rozwijanie kolumn zagnieżdżonych - list z wykorzystaniem języka R (pakiet tidyr i funkcje rodziny unnest)
Rozwijanie kolumn zagnieżdżonych - list z wykorzystaniem języka R (pakiet tidyr i funkcje rodziny unnest)
W większości przypadków podczas pracy z odpowiedzią otrzymaną z interfejsu API lub innymi danymi o złożonej strukturze drzewiastej masz do czynienia z formatami JSON i XML.
Formaty te mają wiele zalet: przechowują dane w dość zwarty sposób i pozwalają uniknąć niepotrzebnego powielania informacji.
Wadą tych formatów jest złożoność ich przetwarzania i analizy. Nieustrukturyzowanych danych nie da się wykorzystać w obliczeniach i nie można na nich budować wizualizacji.
Artykuł ten stanowi logiczną kontynuację publikacji „Pakiet R tidyr i jego nowe funkcje obrotowe_longer i obrotowe_wider”. Pomoże Ci to przenieść nieustrukturyzowane struktury danych do znanej i odpowiedniej do analizy formy tabelarycznej za pomocą pakietu tidyr, zawarte w rdzeniu biblioteki tidyversei jego rodzinę funkcji unnest_*().
Zawartość
Jeśli interesuje Cię analiza danych, być może zainteresuje Cię mój telegram и youtube kanały. Większość treści jest poświęcona językowi R.
Prostokątne(uwaga tłumacza, nie znalazłem odpowiednich opcji tłumaczenia tego terminu, więc zostawimy to tak, jak jest.) to proces przenoszenia nieustrukturyzowanych danych za pomocą zagnieżdżonych tablic do dwuwymiarowej tabeli składającej się ze znanych wierszy i kolumn. W tidyr Istnieje kilka funkcji, które pomogą Ci rozwinąć zagnieżdżone kolumny listy i zredukować dane do prostokątnej, tabelarycznej postaci:
unnest_longer() pobiera każdy element listy kolumn i tworzy nowy wiersz.
unnest_wider() pobiera każdy element listy kolumn i tworzy nową kolumnę.
unnest_auto() automatycznie określa, której funkcji najlepiej użyć unnest_longer() lub unnest_wider().
hoist() podobny do unnest_wider() ale wybiera tylko określone komponenty i pozwala na pracę z kilkoma poziomami zagnieżdżenia.
Większość problemów związanych z wprowadzaniem nieustrukturyzowanych danych z kilkoma poziomami zagnieżdżenia do dwuwymiarowej tabeli można rozwiązać, łącząc wymienione funkcje z dplyr.
Aby zademonstrować te techniki, użyjemy pakietu repurrrsive, który udostępnia wiele złożonych, wielopoziomowych list pochodzących z internetowego interfejsu API.
Zacznijmy od gh_users, lista zawierająca informacje o sześciu użytkownikach GitHub. Najpierw przekształćmy listę gh_users в łkać rama:
users <- tibble( user = gh_users )
Wydaje się to trochę sprzeczne z intuicją: po co podawać listę gh_usersdo bardziej złożonej struktury danych? Ramka danych ma jednak dużą zaletę: łączy wiele wektorów, dzięki czemu wszystko jest śledzone w jednym obiekcie.
Każdy element obiektu users to nazwana lista, w której każdy element reprezentuje kolumnę.
W tym przypadku mamy tabelę składającą się z 30 kolumn i większość z nich nie będzie nam potrzebna, więc zamiast tego możemy unnest_wider() używać hoist(). hoist() pozwala nam wyodrębnić wybrane komponenty przy użyciu tej samej składni co 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() usuwa określone nazwane komponenty z listy kolumn użytkownikwięc możesz rozważyć hoist() jak przenoszenie komponentów z wewnętrznej listy ramki daty na jej najwyższy poziom.
Repozytoria Githuba
Wyrównanie listy gh_repos zaczynamy podobnie, konwertując go na tibble:
Tym razem elementy użytkownik reprezentują listę repozytoriów posiadanych przez tego użytkownika. Każde repozytorium to osobna obserwacja, a więc zgodnie z koncepcją schludnych danych (w przybliżeniu uporządkowane dane) powinny stać się nowymi liniami i dlatego ich używamy unnest_longer() a nie 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
Teraz możemy skorzystać 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
#> <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
Zwróć uwagę na użycie c("owner", "login"): To pozwala nam uzyskać wartość drugiego poziomu z zagnieżdżonej listy owner. Alternatywnym podejściem jest pobranie całej listy owner a następnie za pomocą funkcji unnest_wider() umieść każdy z jego elementów w kolumnie:
Zamiast myśleć o wyborze odpowiedniej funkcji unnest_longer() lub unnest_wider() możesz użyć unnest_auto(). Funkcja ta korzysta z kilku metod heurystycznych w celu wybrania najbardziej odpowiedniej funkcji do przekształcenia danych i wyświetla komunikat o wybranej metodzie.
got_chars ma identyczną strukturę jak gh_users: Jest to zestaw nazwanych list, gdzie każdy element wewnętrznej listy opisuje jakiś atrybut postaci z Gry o Tron. Przynoszący got_chars W przypadku widoku tabeli zaczynamy od utworzenia ramki daty, tak jak w poprzednich przykładach, a następnie konwertujemy każdy element do osobnej kolumny:
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>
Struktura got_chars nieco trudniejsze niż gh_users, ponieważ niektóre elementy listy char same w sobie są listą, w rezultacie otrzymujemy filary - listy:
Twoje dalsze działania zależą od celów analizy. Być może będziesz musiał umieścić informacje w liniach każdej książki i serii, w której 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
#> <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
A może chcesz stworzyć tabelę, która pozwoli Ci dopasować charakter do pracy:
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
(Zwróć uwagę na puste wartości "" w terenie titlewynika to z błędów popełnionych przy wprowadzaniu danych got_chars: w rzeczywistości postacie, dla których nie ma odpowiednich tytułów książek i seriali telewizyjnych w tej dziedzinie title musi mieć wektor o długości 0, a nie wektor o długości 1 zawierający pusty ciąg.)
Możemy przepisać powyższy przykład za pomocą funkcji unnest_auto(). Takie podejście jest wygodne w przypadku jednorazowej analizy, ale nie należy na nim polegać unnest_auto() do regularnego stosowania. Chodzi o to, że jeśli zmieni się struktura danych unnest_auto() może zmienić wybrany mechanizm transformacji danych, jeśli początkowo rozwinął kolumny listy w wiersze za pomocą unnest_longer(), wówczas gdy zmieni się struktura przychodzących danych, logikę można zmienić na korzyść unnest_wider(), a ciągłe stosowanie tego podejścia może prowadzić do nieoczekiwanych błędów.
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
Geokodowanie za pomocą Google
Następnie przyjrzymy się bardziej złożonej strukturze danych uzyskanych z usługi geokodowania Google. Buforowanie danych logowania jest sprzeczne z zasadami pracy z interfejsem API map Google, dlatego najpierw napiszę proste opakowanie wokół interfejsu API. Który opiera się na przechowywaniu klucza API Map Google w zmiennej środowiskowej; Jeśli nie masz klucza do pracy z API Map Google zapisanego w zmiennych środowiskowych, fragmenty kodu przedstawione w tej sekcji nie zostaną wykonane.
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)
}
Lista zwracana przez tę funkcję jest dość złożona:
Na szczęście problem konwersji tych danych do postaci tabelarycznej możemy krok po kroku rozwiązać za pomocą funkcji tidyr. Aby zadanie było trochę trudniejsze i bardziej realistyczne, zacznę od geokodowania kilku miast:
city <- c ( "Houston" , "LA" , "New York" , "Chicago" , "Springfield" ) city_geo <- purrr::map (city, geocode)
Otrzymany wynik przekonwertuję na tibble, dla wygody dodam kolumnę z odpowiednią nazwą miasta.
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]>
Pierwszy poziom zawiera komponenty status и result, o które możemy rozszerzyć 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
Uwaga! results jest listą wielopoziomową. Większość miast ma 1 element (reprezentujący unikalną wartość odpowiadającą API geokodowania), ale Springfield ma dwa. Możemy je rozciągnąć w osobne linie za pomocą 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
Teraz wszystkie mają te same komponenty, co można zweryfikować 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
#> <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
Możemy znaleźć współrzędne szerokości i długości geograficznej każdego miasta, rozwijają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
#> <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>
A następnie lokalizacja, dla której musisz się rozwinąć 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>
Znowu unnest_auto() upraszcza opisaną operację z pewnymi zagrożeniami, które mogą wynikać ze zmiany struktury przychodzących danych:
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>
Możemy też po prostu spojrzeć na pierwszy adres każdego miasta:
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>
Albo użyj hoist() aby przejść bezpośrednio do nurkowania wielopoziomowego 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]>
Dyskografia Sharli Gelfand
Na koniec przyjrzymy się najbardziej złożonej strukturze – dyskografii Sharli Gelfand. Podobnie jak w powyższych przykładach, zaczynamy od konwersji listy do jednokolumnowej ramki danych, a następnie rozwijamy ją tak, aby każdy element był osobną kolumną. Przekształcam także kolumnę date_added do odpowiedniego formatu daty i godziny w formacie 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
Na tym poziomie otrzymujemy informacje o tym, kiedy każda płyta została dodana do dyskografii Sharli, ale nie widzimy żadnych danych na ich temat. Aby to zrobić musimy rozwinąć kolumnę basic_information:
discs %>% unnest_wider(basic_information)
#> Column name `id` must not be duplicated.
#> Use .name_repair to specify repair.
Niestety otrzymamy błąd, ponieważ... wewnątrz listy basic_information istnieje kolumna o tej samej nazwie basic_information. Jeśli wystąpi taki błąd, aby szybko ustalić jego przyczynę, możesz skorzystać names_repair = "unique":
Następnie możesz w razie potrzeby połączyć je z powrotem z oryginalnym zbiorem danych.
wniosek
Do rdzenia biblioteki tidyverse zawiera wiele przydatnych pakietów, których łączy wspólna filozofia przetwarzania danych.
W tym artykule zbadaliśmy rodzinę funkcji unnest_*(), które mają na celu pracę z wyodrębnianiem elementów z zagnieżdżonych list. Pakiet ten zawiera wiele innych przydatnych funkcji ułatwiających konwersję danych zgodnie z koncepcją Porządne dane.