ProHoster > Blog > administration > Udvidelse af indlejrede kolonner - lister ved hjælp af R-sproget (tidyr-pakke og funktioner i unnest-familien)
Udvidelse af indlejrede kolonner - lister ved hjælp af R-sproget (tidyr-pakke og funktioner i unnest-familien)
I de fleste tilfælde, når du arbejder med et svar modtaget fra en API, eller med andre data, der har en kompleks træstruktur, står du over for JSON- og XML-formater.
Disse formater har mange fordele: de gemmer data ret kompakt og giver dig mulighed for at undgå unødvendig duplikering af information.
Ulempen ved disse formater er kompleksiteten af deres behandling og analyse. Ustrukturerede data kan ikke bruges i beregninger, og visualisering kan ikke bygges på det.
Denne artikel er en logisk fortsættelse af publikationen "R-pakke tidyr og dens nye funktioner pivot_longer og pivot_wider". Det vil hjælpe dig med at bringe ustrukturerede datastrukturer til en velkendt og egnet til analyse tabelform ved hjælp af pakken tidyr, inkluderet i kernen af biblioteket tidyverse, og dens familie af funktioner unnest_*().
Indhold
Hvis du er interesseret i dataanalyse, er du måske interesseret i min telegram и youtube kanaler. Det meste af indholdet er afsat til R-sproget.
Rektangel(Oversætterens note, jeg fandt ikke passende oversættelsesmuligheder for dette udtryk, så vi lader det være som det er.) er processen med at bringe ustrukturerede data med indlejrede arrays ind i en todimensionel tabel bestående af velkendte rækker og kolonner. I tidyr Der er flere funktioner, der hjælper dig med at udvide indlejrede listekolonner og reducere dataene til en rektangulær tabelform:
unnest_longer() tager hvert element i kolonnelisten og opretter en ny række.
unnest_wider() tager hvert element i kolonnelisten og opretter en ny kolonne.
unnest_auto() bestemmer automatisk, hvilken funktion der er bedst at bruge unnest_longer() eller unnest_wider().
hoist() svarende til unnest_wider() men vælger kun de angivne komponenter og giver dig mulighed for at arbejde med flere niveauer af indlejring.
De fleste af problemerne forbundet med at bringe ustrukturerede data med flere niveauer af indlejring ind i en todimensionel tabel kan løses ved at kombinere de anførte funktioner med dplyr.
For at demonstrere disse teknikker, vil vi bruge pakken repurrrsive, som giver flere komplekse lister på flere niveauer afledt af en web-API.
Lad os begynde med gh_brugere, en liste, der indeholder oplysninger om seks GitHub-brugere. Lad os først omdanne listen gh_brugere в tibble ramme:
users <- tibble( user = gh_users )
Dette virker lidt kontraintuitivt: hvorfor give en liste gh_brugere, til en mere kompleks datastruktur? Men en dataramme har en stor fordel: den kombinerer flere vektorer, så alt spores i ét objekt.
Hvert objektelement users er en navngivet liste, hvor hvert element repræsenterer en kolonne.
I dette tilfælde har vi en tabel bestående af 30 kolonner, og vi skal ikke bruge de fleste af dem, så vi kan i stedet unnest_wider() at bruge hoist(). hoist() giver os mulighed for at udtrække udvalgte komponenter ved hjælp af samme syntaks 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() fjerner de angivne navngivne komponenter fra en kolonneliste brugerså du kan overveje hoist() som at flytte komponenter fra den interne liste af en datoramme til dens øverste niveau.
Github-depoter
Listejustering gh_repos vi starter på samme måde med at konvertere det til tibble:
Denne gang elementerne bruger repræsentere en liste over depoter, der ejes af denne bruger. Hvert depot er en separat observation, så ifølge konceptet med pæne data (ca. ryddelige data) de skal blive til nye linjer, det er derfor vi bruger unnest_longer() og ikke 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 bruge 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
Vær opmærksom på brugen c("owner", "login"): Dette giver os mulighed for at hente værdien på andet niveau fra en indlejret liste owner. En alternativ tilgang er at få hele listen owner og derefter bruge funktionen unnest_wider() sæt hvert af dets elementer i en kolonne:
I stedet for at tænke på at vælge den rigtige funktion unnest_longer() eller unnest_wider() du kan bruge unnest_auto(). Denne funktion bruger flere heuristiske metoder til at vælge den mest egnede funktion til at transformere dataene og viser en besked om den valgte metode.
got_chars har samme struktur som gh_users: Dette er et sæt navngivne lister, hvor hvert element i den indre liste beskriver en eller anden egenskab ved en Game of Thrones-karakter. Medbringer got_chars Til tabelvisningen starter vi med at oprette en datoramme, ligesom i de foregående eksempler, og konverterer derefter hvert element til en separat kolonne:
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 noget sværere end gh_users, fordi nogle listekomponenter char selv er en liste, som et resultat får vi søjler - lister:
Dine videre handlinger afhænger af målene for analysen. Måske skal du sætte oplysninger på linjerne for hver bog og serie, hvor karakteren optræder:
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 måske vil du lave en tabel, der giver dig mulighed for at matche karakteren og værket:
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
(Bemærk de tomme værdier "" i marken title, dette skyldes fejl ved indtastning af data got_chars: faktisk karakterer, for hvilke der ikke findes tilsvarende bog- og tv-serietitler på området title skal have en vektor med længde 0, ikke en vektor med længde 1, der indeholder den tomme streng.)
Vi kan omskrive ovenstående eksempel ved hjælp af funktionen unnest_auto(). Denne tilgang er praktisk til engangsanalyse, men du bør ikke stole på unnest_auto() til regelmæssig brug. Pointen er, at hvis din datastruktur ændres unnest_auto() kan ændre den valgte datatransformationsmekanisme, hvis den oprindeligt udvidede listekolonner til rækker ved hjælp af unnest_longer(), så når strukturen af de indgående data ændres, kan logikken ændres til fordel unnest_wider(), og at bruge denne tilgang løbende kan føre til uventede fejl.
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
Dernæst vil vi se på en mere kompleks struktur af de data, der er opnået fra Googles geokodningstjeneste. Caching af legitimationsoplysninger er imod reglerne for at arbejde med Google maps API, så jeg vil først skrive en simpel indpakning omkring API'en. Som er baseret på lagring af Google Maps API-nøglen i en miljøvariabel; Hvis du ikke har nøglen til at arbejde med Google Maps API gemt i dine miljøvariabler, vil kodefragmenterne, der præsenteres i dette afsnit, ikke blive udført.
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)
}
Listen, som denne funktion returnerer, er ret kompleks:
Heldigvis kan vi løse problemet med at konvertere disse data til en tabelform trin for trin ved hjælp af funktioner tidyr. For at gøre opgaven lidt mere udfordrende og realistisk, vil jeg starte med at geokode et par byer:
city <- c ( "Houston" , "LA" , "New York" , "Chicago" , "Springfield" ) city_geo <- purrr::map (city, geocode)
Jeg vil konvertere det resulterende resultat til tibble, for nemheds skyld tilføjer jeg en kolonne med det tilsvarende bynavn.
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]>
Det første niveau indeholder komponenter status и result, som vi kan udvide 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
Bemærk, at results er en liste på flere niveauer. De fleste byer har 1 element (der repræsenterer en unik værdi svarende til geokodnings-API'en), men Springfield har to. Vi kan trække dem i separate linjer 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 alle de samme komponenter, som kan verificeres vha 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 finde bredde- og længdegradskoordinaterne for hver by ved at udvide listen 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>
Og så det sted, som du skal udvide til 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>
Endnu engang, unnest_auto() forenkler den beskrevne operation med nogle risici, der kan være forårsaget af ændring af strukturen af de indgående data:
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 også bare se på den første adresse for hver by:
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 brug hoist() for et dyk på flere niveauer at gå direkte til 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 af Sharla Gelfand
Til sidst vil vi se på den mest komplekse struktur - Sharla Gelfands diskografi. Som i eksemplerne ovenfor starter vi med at konvertere listen til en enkelt-kolonne dataramme og derefter udvide den, så hver komponent er en separat kolonne. Jeg transformerer også kolonnen date_added til det passende dato- og 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å dette niveau får vi information om, hvornår hver disk blev tilføjet til Sharlas diskografi, men vi ser ingen data om disse diske. For at gøre dette skal vi udvide kolonnen basic_information:
discs %>% unnest_wider(basic_information)
#> Column name `id` must not be duplicated.
#> Use .name_repair to specify repair.
Vi modtager desværre en fejl, fordi... inde på listen basic_information der er en kolonne af samme navn basic_information. Hvis en sådan fejl opstår, for hurtigt at bestemme årsagen, kan du bruge names_repair = "unique":
Du kan derefter slutte dem tilbage til det originale datasæt efter behov.
Konklusion
Til kernen af biblioteket tidyverse indeholder mange nyttige pakker forenet af en fælles databehandlingsfilosofi.
I denne artikel undersøgte vi familien af funktioner unnest_*(), som har til formål at arbejde med at udtrække elementer fra indlejrede lister. Denne pakke indeholder mange andre nyttige funktioner, der gør det nemmere at konvertere data efter konceptet Ryddige data.