ProHoster > Blog > Uprava > Razširitev ugnezdenih stolpcev - seznami z uporabo jezika R (paket tidyr in funkcije družine unnest)
Razširitev ugnezdenih stolpcev - seznami z uporabo jezika R (paket tidyr in funkcije družine unnest)
V večini primerov se pri delu z odgovorom, prejetim iz API-ja, ali s katerimi koli drugimi podatki, ki imajo kompleksno drevesno strukturo, soočite z oblikama JSON in XML.
Ti formati imajo veliko prednosti: podatke shranjujejo precej kompaktno in vam omogočajo, da se izognete nepotrebnemu podvajanju informacij.
Pomanjkljivost teh formatov je zapletenost njihove obdelave in analize. Nestrukturiranih podatkov ni mogoče uporabiti v izračunih in na njih ni mogoče graditi vizualizacije.
Ta članek je logično nadaljevanje publikacije "R paket tidyr in njegovi novi funkciji pivot_longer in pivot_wider". Z uporabo paketa vam bo pomagal prenesti nestrukturirane podatkovne strukture v znano in za analizo primerno tabelarno obliko tidyr, vključena v jedro knjižnice tidyverse, in njegova družina funkcij unnest_*().
Vsebina
Če vas zanima analiza podatkov, vas bo morda zanimal moj telegram и youtube kanalov. Večina vsebine je posvečena jeziku R.
Pravokotnik(opomba prevajalca, za ta izraz nisem našel ustreznih možnosti prevoda, zato ga bomo pustili tako, kot je.) je postopek vnosa nestrukturiranih podatkov z ugnezdenimi nizi v dvodimenzionalno tabelo, sestavljeno iz znanih vrstic in stolpcev. IN tidyr Obstaja več funkcij, ki vam bodo pomagale razširiti stolpce ugnezdenega seznama in zmanjšati podatke v pravokotno tabelarično obliko:
unnest_longer() vzame vsak element seznama stolpcev in ustvari novo vrstico.
unnest_wider() vzame vsak element seznama stolpcev in ustvari nov stolpec.
unnest_auto() samodejno določi, katero funkcijo je najbolje uporabiti unnest_longer() ali unnest_wider().
hoist() podoben unnest_wider() vendar izbere samo določene komponente in vam omogoča delo z več ravnemi gnezdenja.
Večino težav, povezanih s prenašanjem nestrukturiranih podatkov z več nivoji gnezdenja v dvodimenzionalno tabelo, je mogoče rešiti s kombiniranjem navedenih funkcij z dplyr.
Za predstavitev teh tehnik bomo uporabili paket repurrrsive, ki ponuja več zapletenih seznamov na več ravneh, izpeljanih iz spletnega API-ja.
Začnimo s tem gh_users, seznam, ki vsebuje informacije o šestih uporabnikih GitHub. Najprej preoblikujemo seznam gh_users в žvrgolenje okvir:
users <- tibble( user = gh_users )
To se zdi nekoliko protislovno: zakaj bi ponujal seznam gh_users, do bolj zapletene strukture podatkov? Toda podatkovni okvir ima veliko prednost: združuje več vektorjev, tako da se vsem sledi v enem objektu.
Vsak predmetni element users je poimenovan seznam, v katerem vsak element predstavlja stolpec.
V tem primeru imamo tabelo, sestavljeno iz 30 stolpcev in večine od njih ne bomo potrebovali, zato lahko unnest_wider() uporabo hoist(). hoist() nam omogoča ekstrahiranje izbranih komponent z uporabo iste sintakse kot 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() odstrani navedene imenovane komponente s seznama stolpcev uporabniktako da lahko razmislite hoist() kot premikanje komponent z notranjega seznama datumskega okvira na njegovo najvišjo raven.
Github repozitoriji
Poravnava seznama gh_repos začnemo podobno s pretvorbo v tibble:
Tokrat elementi uporabnik predstavljajo seznam repozitorijev v lasti tega uporabnika. Vsako skladišče je ločeno opazovanje, torej v skladu s konceptom čistih podatkov (približno urejeni podatki) naj bi postale nove linije, zato uporabljamo unnest_longer() 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
Zdaj lahko uporabimo unnest_wider() ali 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
Bodite pozorni na uporabo c("owner", "login"): To nam omogoča, da pridobimo vrednost druge ravni iz ugnezdenega seznama owner. Alternativni pristop je pridobitev celotnega seznama owner in nato uporabite funkcijo unnest_wider() postavite vsak njen element v stolpec:
Namesto da bi razmišljali o izbiri prave funkcije unnest_longer() ali unnest_wider() lahko uporabiš unnest_auto(). Ta funkcija uporablja več hevrističnih metod za izbiro najprimernejše funkcije za transformacijo podatkov in prikaže sporočilo o izbrani metodi.
got_chars ima enako strukturo kot gh_users: To je niz poimenovanih seznamov, kjer vsak element notranjega seznama opisuje nek atribut lika iz Igre prestolov. Prinašanje got_chars Za pogled tabele začnemo z ustvarjanjem datumskega okvira, tako kot v prejšnjih primerih, in nato pretvorimo vsak element v ločen stolpec:
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 nekoliko težje kot gh_users, Ker nekaj komponent seznama char sami so seznam, kot rezultat dobimo stebre - sezname:
Vaša nadaljnja dejanja so odvisna od ciljev analize. Morda boste morali v vrstice vnesti informacije za vsako knjigo in serijo, v kateri se lik pojavi:
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
Morda pa želite ustvariti tabelo, ki vam omogoča ujemanje značaja in dela:
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
(Upoštevajte prazne vrednosti "" na terenu title, je to posledica napak pri vnosu podatkov v got_chars: pravzaprav liki, za katere na tem področju ni ustreznih naslovov knjig in TV serij title mora imeti vektor dolžine 0, ne vektor dolžine 1, ki vsebuje prazen niz.)
Zgornji primer lahko prepišemo s funkcijo unnest_auto(). Ta pristop je primeren za enkratno analizo, vendar se nanj ne smete zanašati unnest_auto() za redno uporabo. Bistvo je, da če se vaša struktura podatkov spremeni unnest_auto() lahko spremeni izbrani mehanizem za pretvorbo podatkov, če je prvotno razširil stolpce seznama v vrstice z uporabo unnest_longer(), ko se spremeni struktura vhodnih podatkov, se lahko logika spremeni v prid unnest_wider(), stalna uporaba tega pristopa pa lahko povzroči nepričakovane napake.
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
Geokodiranje z Googlom
Nato si bomo ogledali bolj zapleteno strukturo podatkov, pridobljenih iz Googlove storitve geokodiranja. Predpomnjenje poverilnic je v nasprotju s pravili dela z API-jem Google Maps, zato bom najprej napisal preprost ovoj okoli API-ja. Ki temelji na shranjevanju ključa Google Maps API v spremenljivki okolja; Če v spremenljivkah vašega okolja nimate shranjenega ključa za delo z API-jem za Google Zemljevide, fragmenti kode, predstavljeni v tem razdelku, ne bodo izvedeni.
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, ki ga vrne ta funkcija, je precej zapleten:
Na srečo lahko problem pretvorbe teh podatkov v tabelarično obliko rešimo korak za korakom s pomočjo funkcij tidyr. Da bo naloga nekoliko bolj zahtevna in realistična, bom začel z geokodiranjem nekaj mest:
city <- c ( "Houston" , "LA" , "New York" , "Chicago" , "Springfield" ) city_geo <- purrr::map (city, geocode)
Dobljeni rezultat bom pretvoril v tibble, za udobje bom dodal stolpec z ustreznim imenom mesta.
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]>
Prva raven vsebuje komponente status и result, s katerim lahko razširimo 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
Prosimo, upoštevajte, da se results je seznam na več ravneh. Večina mest ima 1 element (predstavlja edinstveno vrednost, ki ustreza API-ju za geokodiranje), Springfield pa dva. Lahko jih potegnemo v ločene vrstice z 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
Zdaj imajo vsi enake komponente, kar je mogoče preveriti z uporabo 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
Koordinate zemljepisne širine in dolžine vsakega mesta lahko najdemo tako, da razširimo seznam 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>
In nato lokacijo, za katero se morate razširiti 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>
Spet unnest_auto() poenostavi opisano operacijo z nekaterimi tveganji, ki jih lahko povzroči spreminjanje strukture vhodnih podatkov:
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>
Prav tako lahko samo pogledamo prvi naslov za vsako mesto:
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>
Ali uporabite hoist() za potop na več ravneh, do katerega lahko greste neposredno 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]>
Diskografija Sharla Gelfand
Nazadnje si bomo ogledali najbolj zapleteno strukturo - diskografijo Sharle Gelfand. Kot v zgornjih primerih začnemo s pretvorbo seznama v podatkovni okvir z enim stolpcem in ga nato razširimo, tako da je vsaka komponenta ločen stolpec. Preoblikujem tudi stolpec date_added v ustrezno obliko datuma in ure 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 tej ravni dobimo informacije o tem, kdaj je bila posamezna plošča dodana Sharlini diskografiji, vendar ne vidimo nobenih podatkov o teh ploščah. Če želite to narediti, moramo stolpec razširiti basic_information:
discs %>% unnest_wider(basic_information)
#> Column name `id` must not be duplicated.
#> Use .name_repair to specify repair.
Na žalost bomo prejeli napako, ker... znotraj seznama basic_information obstaja stolpec z istim imenom basic_information. Če pride do takšne napake, lahko uporabite, da hitro ugotovite njen vzrok names_repair = "unique":
Nato jih lahko po potrebi pridružite nazaj izvirnemu naboru podatkov.
Zaključek
V jedro knjižnice tidyverse vključuje veliko uporabnih paketov, ki jih združuje skupna filozofija obdelave podatkov.
V tem članku smo preučili družino funkcij unnest_*(), ki so namenjeni delu z ekstrahiranjem elementov iz ugnezdenih seznamov. Ta paket vsebuje številne druge uporabne funkcije, ki olajšajo pretvorbo podatkov v skladu s konceptom Tidy Data.