ProHoster > Блог > адміністрування > Розгортаємо вкладені стовпці - списки за допомогою мови R (пакет tidyr та функції сімейства unnest)
Розгортаємо вкладені стовпці - списки за допомогою мови R (пакет tidyr та функції сімейства unnest)
У більшості випадків при роботі з відповіддю отриманою від API, або з будь-якими іншими даними, які мають складну деревоподібну структуру, ви стикаєтеся з форматами JSON та XML.
Ці формати мають безліч переваг: вони досить компактно зберігають дані та дозволяють уникнути зайвого дублювання інформації.
Мінусом даних форматів є складність їх обробки та аналізу. Неструктуровані дані неможливо використовувати у обчисленнях і не можна будувати на основі візуалізацію.
Ця стаття є логічним продовженням публікації "R пакет tidyr та його нові функції pivot_longer та pivot_wider". Вона допоможе вам привести неструктуровані конструкції даних до звичного і придатного для аналізу табличного вигляду за допомогою пакета. tidyr, що входить до ядра бібліотеки tidyverse, та його функцій сімейства unnest_*().
Зміст
Якщо ви цікавитеся аналізом даних можливо вам будуть цікаві мої телеграма и YouTube канали. Більшість контенту яких присвячені мові R.
Rectangling(прим. перекладача, не знайшов адекватних варіантів перекладу цього терміну, тому залишимо його як є.) - Це процес приведення не структурованих даних з вкладеними масивами до двомірної таблиці, що складається зі звичних рядків і стовпців. У tidyr є кілька функцій, які допоможуть вам розгорнути вкладені стовпці-списки та привести дані до прямокутної, табличної форми:
unnest_longer() бере кожен елемент списку-стовпця і створює новий рядок.
unnest_wider() бере кожен елемент списку-стовпця і створює новий стовпець.
unnest_auto() автоматично визначає яку з функцій краще використовувати unnest_longer() або unnest_wider().
hoist() схожа на unnest_wider() але відбирає лише зазначені компоненти і дозволяє працювати з кількома рівнями вкладеності.
Більшість проблем, пов'язаних з приведенням не структурованих даних з кількома рівнями вкладеності до двомірної таблиці, можна вирішити, комбінуючи перелічені функції з dplyr.
Для демонстрації цих прийомів ми будемо використовувати пакет repurrrsive, який надає кілька складних багаторівневих списків, отриманих з веб-API.
Почнемо з gh_users, списку, який містить інформацію про шести користувачів GitHub Для початку перетворюємо список gh_users в tibble кадр.:
users <- tibble( user = gh_users )
Це здається трохи нелогічним: навіщо наводити список gh_usersдо більш складної структури даних? Але дата кадру має велику перевагу: він об'єднує кілька векторів, так що все відстежується в одному об'єкті.
Кожен елемент об'єкту users є іменований список, в якому кожен елемент представляє стовпець.
У цьому випадку ми отримали таблицю, що складається з 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() видаляє зазначені іменовані компоненти зі списку-стовпця користувачтому ви можете розглядати hoist() як переміщення компонентів із внутрішнього списку дата кадру до його верхнього рівня.
Репозиторії Github
Вирівнювання списку gh_repos ми починаємо аналогічно, перетворюючи його на tibble:
Цього разу елементи користувач є список репозиторіїв, що належать цьому користувачеві. Кожен репозиторій є окремим спостереженням, тому згідно з концепцією акуратних даних (Прим. 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() помістити кожен його елемент у стовпець:
Замість того, щоб розмірковувати над вибором потрібної функції unnest_longer() або unnest_wider() Ви можете використовувати unnest_auto(). Ця функція використовує кілька евристичних методів для вибору найбільш підходящої функції для трансформації даних, і виводить повідомлення про обраний спосіб.
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(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)
}
Список, який повертає ця функція, досить складний:
На щастя, ми можемо крок за кроком вирішити проблему перетворення цих даних у табличний вигляд за допомогою функцій 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 є багаторівневим списком. У більшості міст є один елемент (який представляє унікальне значення, що відповідає 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":
Потім ви можете приєднати їх назад до вихідного набору даних за необхідності.
Висновок
У ядро бібліотеки tidyverse входять безліч корисних пакетів, об'єднані загальною філософією обробки даних.
У цій статті ми розібрали сімейство функцій unnest_*(), які спрямовані на роботу із вилученням елементів із вкладених списків. Цей пакет містить безліч інших корисних функцій, які спрощують перетворення даних згідно з концепцією Tidy Data.