ProHoster > блог > Администрација > Проширување на вгнездени колони - списоци со користење на јазикот R (пакет tidyr и функции од семејството unnest)
Проширување на вгнездени колони - списоци со користење на јазикот R (пакет tidyr и функции од семејството unnest)
Во повеќето случаи, кога работите со одговор добиен од API или со кој било друг податок што има сложена структура на дрво, се соочувате со JSON и XML формати.
Овие формати имаат многу предности: тие ги складираат податоците доста компактно и ви овозможуваат да избегнете непотребно дуплирање на информации.
Недостаток на овие формати е сложеноста на нивната обработка и анализа. Неструктурираните податоци не можат да се користат во пресметките и не може да се изгради визуелизација на нив.
Оваа статија е логично продолжение на публикацијата „R пакет tidyr и неговите нови функции pivot_longer и pivot_wider“. Тоа ќе ви помогне да ги доведете неструктурираните структури на податоци во позната и погодна табеларна форма за анализа користејќи го пакетот tidyr, вклучени во јадрото на библиотеката tidyverse, и неговото семејство на функции unnest_*().
содржина
Ако сте заинтересирани за анализа на податоци, можеби ќе ве интересира мојата телеграма и YouTube канали. Најголем дел од содржината е посветена на јазикот Р.
Правоаголник(забелешка на преведувачот, не најдов соодветни опции за превод за овој термин, па ќе го оставиме како што е.) е процес на внесување неструктурирани податоци со вгнездени низи во дводимензионална табела која се состои од познати редови и колони. ВО tidyr Постојат неколку функции кои ќе ви помогнат да ги проширите колоните со вгнездени списоци и да ги намалите податоците во правоаголна, табеларна форма:
unnest_longer() го зема секој елемент од списокот со колони и создава нов ред.
unnest_wider() го зема секој елемент од списокот со колони и создава нова колона.
unnest_auto() автоматски одредува која функција е најдобро да се користи unnest_longer() или unnest_wider().
hoist() слично на unnest_wider() но ги избира само наведените компоненти и ви овозможува да работите со неколку нивоа на гнездење.
Повеќето од проблемите поврзани со внесување на неструктурирани податоци со неколку нивоа на вгнездување во дводимензионална табела може да се решат со комбинирање на наведените функции со dplyr.
За да ги покажеме овие техники, ќе го користиме пакетот repurrrsive, кој обезбедува повеќе сложени списоци на повеќе нивоа добиени од веб API.
Да почнеме со gh_users, листа која содржи информации за шест корисници на GitHub. Прво да ја трансформираме листата gh_users в трепкање рамка:
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:
Овој пат елементите корисникот претставуваат листа на складишта во сопственост на овој корисник. Секое складиште е посебно набљудување, па според концептот на уредни податоци (приближно уредни податоци) тие треба да станат нови линии, поради што користиме 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, затоа прво ќе напишам едноставна обвивка околу API-то. Која се заснова на складирање на клучот на Google Maps API во променлива на околината; Ако го немате клучот за работа со Google Maps API зачуван во променливите на вашата околина, фрагментите од кодот претставени во овој дел нема да се извршат.
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 е листа на повеќе нивоа. Повеќето градови имаат 1 елемент (што претставува единствена вредност што одговара на API за геокодирање), но Спрингфилд има два. Можеме да ги повлечеме во посебни линии со unnest_longer() :
loc %>%
unnest_wider(json) %>%
unnest_longer(results)
#> # A tibble: 5 x 3
#> city results status
#> <chr> <list> <chr>
#> 1 Houston <named list [5]> OK
#> 2 LA <named list [5]> OK
#> 3 New York <named list [5]> OK
#> 4 Chicago <named list [5]> OK
#> 5 Springfield <named list [5]> OK
Сега сите ги имаат истите компоненти, кои може да се потврдат со користење unnest_wider():
loc %>%
unnest_wider(json) %>%
unnest_longer(results) %>%
unnest_wider(results)
#> # A tibble: 5 x 7
#> city address_componen… formatted_addre… geometry place_id types status
#> <chr> <list> <chr> <list> <chr> <lis> <chr>
#> 1 Houst… <list [4]> Houston, TX, USA <named … ChIJAYWN… <lis… OK
#> 2 LA <list [4]> Los Angeles, CA… <named … ChIJE9on… <lis… OK
#> 3 New Y… <list [3]> New York, NY, U… <named … ChIJOwg_… <lis… OK
#> 4 Chica… <list [4]> Chicago, IL, USA <named … ChIJ7cv0… <lis… OK
#> 5 Sprin… <list [5]> Springfield, MO… <named … ChIJP5jI… <lis… OK
Можеме да ги најдеме координатите на географската ширина и должина на секој град со проширување на списокот geometry:
loc %>%
unnest_wider(json) %>%
unnest_longer(results) %>%
unnest_wider(results) %>%
unnest_wider(geometry)
#> # A tibble: 5 x 10
#> city address_compone… formatted_addre… bounds location location_type
#> <chr> <list> <chr> <list> <list> <chr>
#> 1 Hous… <list [4]> Houston, TX, USA <name… <named … APPROXIMATE
#> 2 LA <list [4]> Los Angeles, CA… <name… <named … APPROXIMATE
#> 3 New … <list [3]> New York, NY, U… <name… <named … APPROXIMATE
#> 4 Chic… <list [4]> Chicago, IL, USA <name… <named … APPROXIMATE
#> 5 Spri… <list [5]> Springfield, MO… <name… <named … APPROXIMATE
#> # … with 4 more variables: viewport <list>, place_id <chr>, types <list>,
#> # status <chr>
А потоа локацијата за која треба да се проширите location:
loc %>%
unnest_wider(json) %>%
unnest_longer(results) %>%
unnest_wider(results) %>%
unnest_wider(geometry) %>%
unnest_wider(location)
#> # A tibble: 5 x 11
#> city address_compone… formatted_addre… bounds lat lng location_type
#> <chr> <list> <chr> <list> <dbl> <dbl> <chr>
#> 1 Hous… <list [4]> Houston, TX, USA <name… 29.8 -95.4 APPROXIMATE
#> 2 LA <list [4]> Los Angeles, CA… <name… 34.1 -118. APPROXIMATE
#> 3 New … <list [3]> New York, NY, U… <name… 40.7 -74.0 APPROXIMATE
#> 4 Chic… <list [4]> Chicago, IL, USA <name… 41.9 -87.6 APPROXIMATE
#> 5 Spri… <list [5]> Springfield, MO… <name… 37.2 -93.3 APPROXIMATE
#> # … with 4 more variables: viewport <list>, place_id <chr>, types <list>,
#> # status <chr>
Сепак, unnest_auto() ја поедноставува опишаната операција со некои ризици кои можат да бидат предизвикани од промена на структурата на дојдовните податоци:
loc %>%
unnest_auto(json) %>%
unnest_auto(results) %>%
unnest_auto(results) %>%
unnest_auto(geometry) %>%
unnest_auto(location)
#> Using `unnest_wider(json)`; elements have 2 names in common
#> Using `unnest_longer(results)`; no element has names
#> Using `unnest_wider(results)`; elements have 5 names in common
#> Using `unnest_wider(geometry)`; elements have 4 names in common
#> Using `unnest_wider(location)`; elements have 2 names in common
#> # A tibble: 5 x 11
#> city address_compone… formatted_addre… bounds lat lng location_type
#> <chr> <list> <chr> <list> <dbl> <dbl> <chr>
#> 1 Hous… <list [4]> Houston, TX, USA <name… 29.8 -95.4 APPROXIMATE
#> 2 LA <list [4]> Los Angeles, CA… <name… 34.1 -118. APPROXIMATE
#> 3 New … <list [3]> New York, NY, U… <name… 40.7 -74.0 APPROXIMATE
#> 4 Chic… <list [4]> Chicago, IL, USA <name… 41.9 -87.6 APPROXIMATE
#> 5 Spri… <list [5]> Springfield, MO… <name… 37.2 -93.3 APPROXIMATE
#> # … with 4 more variables: viewport <list>, place_id <chr>, types <list>,
#> # status <chr>
Можеме само да ја погледнеме првата адреса за секој град:
loc %>%
unnest_wider(json) %>%
hoist(results, first_result = 1) %>%
unnest_wider(first_result) %>%
unnest_wider(geometry) %>%
unnest_wider(location)
#> # A tibble: 5 x 11
#> city address_compone… formatted_addre… bounds lat lng location_type
#> <chr> <list> <chr> <list> <dbl> <dbl> <chr>
#> 1 Hous… <list [4]> Houston, TX, USA <name… 29.8 -95.4 APPROXIMATE
#> 2 LA <list [4]> Los Angeles, CA… <name… 34.1 -118. APPROXIMATE
#> 3 New … <list [3]> New York, NY, U… <name… 40.7 -74.0 APPROXIMATE
#> 4 Chic… <list [4]> Chicago, IL, USA <name… 41.9 -87.6 APPROXIMATE
#> 5 Spri… <list [5]> Springfield, MO… <name… 37.2 -93.3 APPROXIMATE
#> # … with 4 more variables: viewport <list>, place_id <chr>, types <list>,
#> # status <chr>
Или користете hoist() за нуркање на повеќе нивоа да се оди директно до lat и lng.
loc %>%
hoist(json,
lat = list("results", 1, "geometry", "location", "lat"),
lng = list("results", 1, "geometry", "location", "lng")
)
#> # A tibble: 5 x 4
#> city lat lng json
#> <chr> <dbl> <dbl> <list>
#> 1 Houston 29.8 -95.4 <named list [2]>
#> 2 LA 34.1 -118. <named list [2]>
#> 3 New York 40.7 -74.0 <named list [2]>
#> 4 Chicago 41.9 -87.6 <named list [2]>
#> 5 Springfield 37.2 -93.3 <named list [2]>
Дискографија на Шарла Гелфанд
Конечно, ќе ја разгледаме најкомплексната структура - дискографијата на Шарла Гелфанд. Како и во горенаведените примери, започнуваме со конвертирање на листата во рамка со податоци со една колона, а потоа ја прошируваме така што секоја компонента е посебна колона. Исто така ја трансформирам колоната date_added до соодветен формат на датум и време во Р.
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_*(), кои се насочени кон работа со извлекување елементи од вгнездени списоци. Овој пакет содржи многу други корисни функции кои го олеснуваат конвертирањето на податоците според концептот Уредни податоци.