ProHoster > Blog > podávání > Rozšíření vnořených sloupců - seznamy pomocí jazyka R (balíček tidyr a funkce rodiny unnest)
Rozšíření vnořených sloupců - seznamy pomocí jazyka R (balíček tidyr a funkce rodiny unnest)
Ve většině případů se při práci s odpovědí přijatou z rozhraní API nebo s jakýmikoli jinými daty, která mají složitou stromovou strukturu, setkáváte s formáty JSON a XML.
Tyto formáty mají mnoho výhod: ukládají data poměrně kompaktně a umožňují vyhnout se zbytečnému zdvojování informací.
Nevýhodou těchto formátů je složitost jejich zpracování a analýzy. Nestrukturovaná data nelze použít ve výpočtech a nelze na nich postavit vizualizaci.
Tento článek je logickým pokračováním publikace "R balíček tidyr a jeho nové funkce pivot_longer a pivot_wider". Pomůže vám pomocí balíčku převést nestrukturované datové struktury do známé a pro analýzu vhodné tabulkové formy tidyr, která je součástí jádra knihovny tidyversea jeho rodina funkcí unnest_*().
Obsah
Pokud vás zajímá analýza dat, mohla by vás zajímat moje telegram и Youtube kanály. Většina obsahu je věnována jazyku R.
Obdélníkové(Poznámka překladatele, nenašel jsem adekvátní možnosti překladu tohoto výrazu, takže to necháme tak, jak to je.) je proces přenesení nestrukturovaných dat s vnořenými poli do dvourozměrné tabulky sestávající ze známých řádků a sloupců. V tidyr Existuje několik funkcí, které vám pomohou rozšířit vnořené sloupce seznamu a zredukovat data do obdélníkové, tabulkové formy:
unnest_longer() vezme každý prvek seznamu sloupců a vytvoří nový řádek.
unnest_wider() vezme každý prvek seznamu sloupců a vytvoří nový sloupec.
unnest_auto() automaticky určí, která funkce je nejlepší použít unnest_longer() nebo unnest_wider().
hoist() podobný unnest_wider() ale vybere pouze zadané komponenty a umožní vám pracovat s několika úrovněmi vnoření.
Většinu problémů spojených s přenesením nestrukturovaných dat s několika úrovněmi vnoření do dvourozměrné tabulky lze vyřešit kombinací uvedených funkcí s dplyr.
K demonstraci těchto technik použijeme balíček repurrrsive, která poskytuje několik komplexních, víceúrovňových seznamů odvozených z webového rozhraní API.
Začněme gh_users, seznam, který obsahuje informace o šesti uživatelích GitHubu. Nejprve transformujme seznam gh_users в tibble rám:
users <- tibble( user = gh_users )
Zdá se to trochu kontraintuitivní: proč poskytovat seznam gh_users, do složitější datové struktury? Datový rámec má ale velkou výhodu: kombinuje více vektorů, takže vše je sledováno v jednom objektu.
Každý prvek objektu users je pojmenovaný seznam, ve kterém každý prvek představuje sloupec.
V tomto případě máme tabulku skládající se z 30 sloupců a většinu z nich nebudeme potřebovat, takže můžeme unnest_wider() k použití hoist(). hoist() nám umožňuje extrahovat vybrané komponenty pomocí stejné syntaxe jako 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() odebere zadané pojmenované komponenty ze seznamu sloupců uživateltakže můžete zvážit hoist() jako přesun komponent z interního seznamu datového rámce na jeho nejvyšší úroveň.
Repozitáře Github
Zarovnání seznamu gh_repos začneme podobně tím, že jej převedeme na tibble:
Tentokrát prvky uživatel představují seznam úložišť vlastněných tímto uživatelem. Každé úložiště je samostatné pozorování, tedy podle konceptu úhledných dat (přibližně uklizené údaje) měly by se stát novými liniemi, proto používáme unnest_longer() a ne 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
Nyní můžeme použít unnest_wider() nebo 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
Věnujte pozornost použití c("owner", "login"): To nám umožňuje získat hodnotu druhé úrovně z vnořeného seznamu owner. Alternativní přístup je získat celý seznam owner a poté pomocí funkce unnest_wider() vložte každý jeho prvek do sloupce:
Místo přemýšlení o výběru správné funkce unnest_longer() nebo unnest_wider() Můžeš použít unnest_auto(). Tato funkce používá několik heuristických metod k výběru nejvhodnější funkce pro transformaci dat a zobrazuje zprávu o zvolené metodě.
got_chars má stejnou strukturu jako gh_users: Toto je sada pojmenovaných seznamů, kde každý prvek vnitřního seznamu popisuje nějaký atribut postavy ze Hry o trůny. Přinášení got_chars Pro zobrazení tabulky začneme vytvořením rámce data, stejně jako v předchozích příkladech, a poté převedeme každý prvek do samostatného sloupce:
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 poněkud obtížnější než gh_users, protože některé komponenty seznamu char samy o sobě jsou seznamem, v důsledku toho dostaneme pilíře - seznamy:
Vaše další akce závisí na cílech analýzy. Možná budete muset uvést informace do řádků pro každou knihu a sérii, ve které se postava vyskytuje:
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
Nebo možná chcete vytvořit tabulku, která vám umožní sladit postavu a práci:
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
(Všimněte si prázdných hodnot "" v oboru title, je to způsobeno chybami při zadávání dat got_chars: ve skutečnosti postavy, pro které v poli nejsou žádné odpovídající názvy knih a televizních seriálů title musí mít vektor délky 0, nikoli vektor délky 1 obsahující prázdný řetězec.)
Výše uvedený příklad můžeme přepsat pomocí funkce unnest_auto(). Tento přístup je vhodný pro jednorázovou analýzu, ale neměli byste na něj spoléhat unnest_auto() pro pravidelné používání. Jde o to, že pokud se změní vaše datová struktura unnest_auto() může změnit vybraný mechanismus transformace dat, pokud původně rozbalil sloupce seznamu na řádky pomocí unnest_longer(), pak když se změní struktura příchozích dat, logika může být změněna ve prospěch unnest_wider()a průběžné používání tohoto přístupu může vést k neočekávaným chybám.
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
Geokódování pomocí Google
Dále se podíváme na složitější strukturu dat získaných z geokódovací služby Google. Ukládání přihlašovacích údajů do mezipaměti je proti pravidlům práce s Google maps API, takže nejprve napíšu jednoduchý obal kolem API. Což je založeno na uložení klíče API Map Google v proměnné prostředí; Pokud nemáte klíč pro práci s Google Maps API uložený v proměnných prostředí, fragmenty kódu uvedené v této části nebudou provedeny.
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)
}
Seznam, který tato funkce vrací, je poměrně složitý:
Naštěstí můžeme problém převodu těchto dat do tabulkové podoby vyřešit krok za krokem pomocí funkcí tidyr. Aby byl úkol trochu náročnější a realističtější, začnu geokódováním několika měst:
city <- c ( "Houston" , "LA" , "New York" , "Chicago" , "Springfield" ) city_geo <- purrr::map (city, geocode)
Výsledný výsledek převedu do tibble, pro usnadnění přidám sloupec s odpovídajícím názvem města.
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]>
První úroveň obsahuje komponenty status и result, kterou můžeme rozšířit 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
Vezměte prosím na vědomí, že results je víceúrovňový seznam. Většina měst má 1 prvek (představující jedinečnou hodnotu odpovídající geokódovacímu API), ale Springfield má dva. Můžeme je natáhnout do samostatných řádků 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
Nyní mají všechny stejné komponenty, které lze ověřit 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
Rozbalením seznamu můžeme najít souřadnice zeměpisné šířky a délky každého města 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 pak umístění, pro které potřebujete expandovat 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>
Znovu, unnest_auto() zjednodušuje popsanou operaci s některými riziky, která mohou být způsobena změnou struktury příchozích dat:
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>
Můžeme se také podívat na první adresu každého města:
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>
Nebo použít hoist() pro víceúrovňový ponor, do kterého se dostanete přímo 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]>
Diskografie Sharly Gelfandové
Nakonec se podíváme na nejsložitější strukturu – diskografii Sharly Gelfand. Stejně jako ve výše uvedených příkladech začneme převodem seznamu na datový rámec s jedním sloupcem a poté jej rozšíříme tak, aby každá komponenta byla samostatným sloupcem. Také transformuji sloup date_added do příslušného formátu data a času v 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 této úrovni získáváme informace o tom, kdy byl každý disk přidán do diskografie Sharly, ale nevidíme žádná data o těchto discích. K tomu potřebujeme rozbalit sloupec basic_information:
discs %>% unnest_wider(basic_information)
#> Column name `id` must not be duplicated.
#> Use .name_repair to specify repair.
Bohužel se zobrazí chyba, protože... uvnitř seznamu basic_information je tam sloupec se stejným názvem basic_information. Pokud k takové chybě dojde, můžete pro rychlé určení její příčiny použít names_repair = "unique":
Podle potřeby je pak můžete připojit zpět k původní datové sadě.
Závěr
K jádru knihovny tidyverse obsahuje mnoho užitečných balíčků spojených společnou filozofií zpracování dat.
V tomto článku jsme zkoumali rodinu funkcí unnest_*(), které jsou zaměřeny na práci s extrahováním prvků z vnořených seznamů. Tento balíček obsahuje mnoho dalších užitečných funkcí, které usnadňují převod dat podle konceptu Uklizená data.