ProHoster > blogg > administration > Expandera kapslade kolumner - listor med R-språket (tidyr-paketet och funktionerna i unnest-familjen)
Expandera kapslade kolumner - listor med R-språket (tidyr-paketet och funktionerna i unnest-familjen)
I de flesta fall, när du arbetar med ett svar från ett API, eller med andra data som har en komplex trädstruktur, ställs du inför JSON- och XML-format.
Dessa format har många fördelar: de lagrar data ganska kompakt och låter dig undvika onödig dubblering av information.
Nackdelen med dessa format är komplexiteten i deras bearbetning och analys. Ostrukturerad data kan inte användas i beräkningar och visualisering kan inte byggas på den.
Den här artikeln är en logisk fortsättning på publikationen "R-paket tidyr och dess nya funktioner pivot_longer och pivot_wider". Det hjälper dig att föra ostrukturerade datastrukturer till en välbekant och lämplig tabellform för analys med hjälp av paketet tidyr, ingår i kärnan av biblioteket tidyverse, och dess familj av funktioner unnest_*().
Innehåll
Om du är intresserad av dataanalys kan du vara intresserad av min telegram и Youtube kanaler. Det mesta av innehållet är tillägnat R-språket.
Rektangel(översättarens anteckning, jag hittade inte lämpliga översättningsalternativ för denna term, så vi lämnar det som det är.) är processen att föra ostrukturerad data med kapslade arrayer till en tvådimensionell tabell bestående av välbekanta rader och kolumner. I tidyr Det finns flera funktioner som hjälper dig att utöka kapslade listkolumner och reducera data till en rektangulär tabellform:
unnest_longer() tar varje element i kolumnlistan och skapar en ny rad.
unnest_wider() tar varje element i kolumnlistan och skapar en ny kolumn.
unnest_auto() bestämmer automatiskt vilken funktion som är bäst att använda unnest_longer() eller unnest_wider().
hoist() Liknande unnest_wider() men väljer bara de angivna komponenterna och låter dig arbeta med flera nivåer av kapsling.
De flesta av problemen förknippade med att föra in ostrukturerad data med flera nivåer av kapsling i en tvådimensionell tabell kan lösas genom att kombinera de listade funktionerna med dplyr.
För att demonstrera dessa tekniker kommer vi att använda paketet repurrrsive, som tillhandahåller flera komplexa listor på flera nivåer härledda från ett webb-API.
Låt oss börja med gh_users, en lista som innehåller information om sex GitHub-användare. Låt oss först omvandla listan gh_users в tibbla ram:
users <- tibble( user = gh_users )
Detta verkar lite kontraintuitivt: varför tillhandahålla en lista gh_users, till en mer komplex datastruktur? Men en dataram har en stor fördel: den kombinerar flera vektorer så att allt spåras i ett objekt.
Varje objektelement users är en namngiven lista där varje element representerar en kolumn.
I det här fallet har vi en tabell som består av 30 kolumner, och vi kommer inte att behöva de flesta av dem, så vi kan istället unnest_wider() att använda hoist(). hoist() tillåter oss att extrahera utvalda komponenter med samma syntax som 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() tar bort de angivna namngivna komponenterna från en kolumnlista användareså du kan överväga hoist() som att flytta komponenter från den interna listan för en datumram till dess översta nivå.
Github-förråd
Listjustering gh_repos vi börjar på liknande sätt med att konvertera det till tibble:
Den här gången elementen användare representerar en lista över arkiv som ägs av denna användare. Varje förråd är en separat observation, så enligt konceptet med snygga data (ca städad data) de ska bli nya linjer, det är därför vi använder unnest_longer() och inte 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
Nu kan vi använda unnest_wider() eller 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
Var uppmärksam på användningen c("owner", "login"): Detta gör att vi kan hämta värdet på andra nivån från en kapslad lista owner. Ett alternativt tillvägagångssätt är att få hela listan owner och sedan använda funktionen unnest_wider() placera vart och ett av dess element i en kolumn:
Istället för att tänka på att välja rätt funktion unnest_longer() eller unnest_wider() du kan använda unnest_auto(). Denna funktion använder flera heuristiska metoder för att välja den mest lämpliga funktionen för att transformera data, och visar ett meddelande om den valda metoden.
got_chars har en identisk struktur med gh_users: Detta är en uppsättning namngivna listor, där varje element i den inre listan beskriver något attribut hos en Game of Thrones-karaktär. Att ta med got_chars För tabellvyn börjar vi med att skapa en datumram, precis som i de tidigare exemplen, och konverterar sedan varje element till en separat kolumn:
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>
Struktur got_chars något svårare än gh_users, därför att några listkomponenter char själva är en lista, som ett resultat får vi pelare - listor:
Dina fortsatta åtgärder beror på målen för analysen. Kanske måste du lägga information på raderna för varje bok och serie där karaktären förekommer:
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
Eller så kanske du vill skapa en tabell som låter dig matcha karaktären och arbetet:
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
(Observera de tomma värdena "" i fält title, detta beror på fel som gjordes vid inmatning av data got_chars: faktiskt karaktärer för vilka det inte finns några motsvarande bok- och tv-serietitlar på området title måste ha en vektor med längden 0, inte en vektor med längden 1 som innehåller den tomma strängen.)
Vi kan skriva om exemplet ovan med funktionen unnest_auto(). Detta tillvägagångssätt är bekvämt för engångsanalys, men du bör inte lita på unnest_auto() för regelbunden användning. Poängen är att om din datastruktur ändras unnest_auto() kan ändra den valda datatransformeringsmekanismen om den initialt utökade listkolumner till rader med hjälp av unnest_longer(), då strukturen för inkommande data ändras, kan logiken ändras till förmån unnest_wider(), och att använda detta tillvägagångssätt på en kontinuerlig basis kan leda till oväntade fel.
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
Geokodning med Google
Därefter ska vi titta på en mer komplex struktur av data som erhålls från Googles geokodningstjänst. Cachning av autentiseringsuppgifter strider mot reglerna för att arbeta med Google maps API, så jag ska först skriva ett enkelt omslag runt API:et. Som bygger på att lagra Google Maps API-nyckel i en miljövariabel; Om du inte har nyckeln för att arbeta med Google Maps API lagrad i dina miljövariabler, kommer kodfragmenten som presenteras i det här avsnittet inte att köras.
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)
}
Listan som denna funktion returnerar är ganska komplex:
Lyckligtvis kan vi lösa problemet med att konvertera dessa data till en tabellform steg för steg med hjälp av funktioner tidyr. För att göra uppgiften lite mer utmanande och realistisk, börjar jag med att geokoda några städer:
city <- c ( "Houston" , "LA" , "New York" , "Chicago" , "Springfield" ) city_geo <- purrr::map (city, geocode)
Jag kommer att konvertera resultatet till tibble, för enkelhetens skull kommer jag att lägga till en kolumn med motsvarande stadsnamn.
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]>
Den första nivån innehåller komponenter status и result, som vi kan utöka med 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
Observera att results är en lista på flera nivåer. De flesta städer har ett element (representerar ett unikt värde som motsvarar geokodnings-APIet), men Springfield har två. Vi kan dra dem i separata rader med 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
Nu har de alla samma komponenter, som kan verifieras med hjälp av 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
Vi kan hitta latitud- och longitudkoordinaterna för varje stad genom att utöka listan 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>
Och sedan platsen som du behöver expandera för 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>
Återigen igen~~POS=HEADCOMP, unnest_auto() förenklar den beskrivna operationen med några risker som kan orsakas av att strukturen för inkommande data ändras:
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>
Vi kan också bara titta på den första adressen för varje stad:
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>
Eller använd hoist() för ett flernivådyk att gå direkt till 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]>
Diskografi av Sharla Gelfand
Slutligen kommer vi att titta på den mest komplexa strukturen - Sharla Gelfands diskografi. Som i exemplen ovan börjar vi med att konvertera listan till en dataram med en kolumn och sedan utöka den så att varje komponent är en separat kolumn. Jag förvandlar också kolumnen date_added till lämpligt datum- och tidsformat i 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
På den här nivån får vi information om när varje skiva lades till Sharlas diskografi, men vi ser ingen data om dessa skivor. För att göra detta måste vi utöka kolumnen basic_information:
discs %>% unnest_wider(basic_information)
#> Column name `id` must not be duplicated.
#> Use .name_repair to specify repair.
Tyvärr kommer vi att få ett felmeddelande, eftersom... inne i listan basic_information det finns en kolumn med samma namn basic_information. Om ett sådant fel uppstår, för att snabbt fastställa dess orsak, kan du använda names_repair = "unique":
Du kan sedan koppla tillbaka dem till den ursprungliga datamängden efter behov.
Slutsats
Till kärnan av biblioteket tidyverse innehåller många användbara paket förenade av en gemensam databehandlingsfilosofi.
I den här artikeln undersökte vi familjen av funktioner unnest_*(), som syftar till att arbeta med att extrahera element från kapslade listor. Detta paket innehåller många andra användbara funktioner som gör det lättare att konvertera data enligt konceptet Tidy Data.