ProHoster > blog > amministrazione > Espansione di colonne nidificate: elenchi utilizzando il linguaggio R (pacchetto tidyr e funzioni della famiglia unnest)
Espansione di colonne nidificate: elenchi utilizzando il linguaggio R (pacchetto tidyr e funzioni della famiglia unnest)
Nella maggior parte dei casi, quando si lavora con una risposta ricevuta da un'API o con qualsiasi altro dato che abbia una struttura ad albero complessa, ci si trova di fronte ai formati JSON e XML.
Questi formati presentano molti vantaggi: memorizzano i dati in modo abbastanza compatto e consentono di evitare inutili duplicazioni di informazioni.
Lo svantaggio di questi formati è la complessità della loro elaborazione e analisi. I dati non strutturati non possono essere utilizzati nei calcoli e la visualizzazione non può essere costruita su di essi.
Questo articolo è una logica continuazione della pubblicazione "Pacchetto R tidyr e le sue nuove funzioni pivot_longer e pivot_wider". Ti aiuterà a portare le strutture dati non strutturate in un formato tabellare familiare e adatto all'analisi utilizzando il pacchetto tidyr, incluso nel nucleo della biblioteca tidyversee la sua famiglia di funzioni unnest_*().
contenuto
Se sei interessato all'analisi dei dati, potresti essere interessato al mio Telegram и youtube canali. La maggior parte del contenuto è dedicata al linguaggio R.
Rettangolare(nota del traduttore, non ho trovato opzioni di traduzione adeguate per questo termine, quindi lo lasceremo così com'è.) è il processo di portare dati non strutturati con array nidificati in una tabella bidimensionale composta da righe e colonne familiari. IN tidyr Esistono diverse funzioni che ti aiuteranno a espandere le colonne degli elenchi nidificati e a ridurre i dati in una forma tabellare rettangolare:
unnest_longer() prende ogni elemento dell'elenco di colonne e crea una nuova riga.
unnest_wider() prende ogni elemento dell'elenco di colonne e crea una nuova colonna.
unnest_auto() determina automaticamente quale funzione è meglio utilizzare unnest_longer() o unnest_wider().
hoist() simile a unnest_wider() ma seleziona solo i componenti specificati e consente di lavorare con diversi livelli di nidificazione.
La maggior parte dei problemi associati all'inserimento di dati non strutturati con diversi livelli di annidamento in una tabella bidimensionale possono essere risolti combinando le funzioni elencate con dplyr.
Per dimostrare queste tecniche, utilizzeremo il pacchetto repurrrsive, che fornisce più elenchi complessi e multilivello derivati da un'API Web.
Iniziamo con gh_users, un elenco che contiene informazioni su sei utenti GitHub. Per prima cosa trasformiamo l'elenco gh_users в bocconcino telaio:
users <- tibble( user = gh_users )
Questo sembra un po’ controintuitivo: perché fornire un elenco gh_users, a una struttura dati più complessa? Ma un frame di dati ha un grande vantaggio: combina più vettori in modo che tutto sia tracciato in un unico oggetto.
Ogni elemento dell'oggetto users è un elenco denominato in cui ciascun elemento rappresenta una colonna.
In questo caso, abbiamo una tabella composta da 30 colonne e non avremo bisogno della maggior parte di esse, quindi possiamo invece unnest_wider() utilizzare hoist(). hoist() ci permette di estrarre i componenti selezionati utilizzando la stessa sintassi di 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() rimuove i componenti denominati specificati da un elenco di colonne Utentequindi puoi considerare hoist() come spostare i componenti dall'elenco interno di una cornice di data al suo livello superiore.
Repository Github
Allineamento dell'elenco gh_repos iniziamo in modo simile convertendolo in tibble:
Questa volta gli elementi Utente rappresentano un elenco di repository di proprietà di questo utente. Ogni repository è un'osservazione separata, quindi secondo il concetto di dati ordinati (dati approssimativi) dovrebbero diventare nuove linee, motivo per cui le usiamo unnest_longer() anziché 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
Ora possiamo usare unnest_wider() o 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
Attenzione all'uso c("owner", "login"): Questo ci consente di ottenere il valore di secondo livello da un elenco nidificato owner. Un approccio alternativo consiste nell'ottenere l'intero elenco owner e poi utilizzando la funzione unnest_wider() metti ciascuno dei suoi elementi in una colonna:
Invece di pensare a scegliere la funzione giusta unnest_longer() o unnest_wider() Puoi usare unnest_auto(). Questa funzione utilizza diversi metodi euristici per selezionare la funzione più adatta per trasformare i dati e visualizza un messaggio sul metodo scelto.
got_chars ha una struttura identica a gh_users: questo è un insieme di elenchi con nome, in cui ciascun elemento dell'elenco interno descrive alcuni attributi di un personaggio di Game of Thrones. Portare got_chars Per la visualizzazione tabella, iniziamo creando un frame di data, proprio come negli esempi precedenti, quindi convertiamo ciascun elemento in una colonna separata:
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>
Struttura got_chars un po' più difficile di gh_users, Perché alcuni componenti dell'elenco char essi stessi sono un elenco, di conseguenza otteniamo pilastri: elenchi:
Le tue ulteriori azioni dipendono dagli obiettivi dell'analisi. Forse dovresti inserire informazioni sulle righe per ogni libro e serie in cui appare il personaggio:
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
O forse vuoi creare una tabella che ti permetta di abbinare il personaggio e l'opera:
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
(Nota i valori vuoti "" nel campo title, ciò è dovuto ad errori commessi durante l'immissione dei dati got_chars: insomma, personaggi per i quali non esistono titoli di libri e serie tv corrispondenti in campo title deve avere un vettore di lunghezza 0, non un vettore di lunghezza 1 contenente la stringa vuota.)
Possiamo riscrivere l'esempio precedente utilizzando la funzione unnest_auto(). Questo approccio è conveniente per l'analisi una tantum, ma non dovresti fare affidamento su di esso unnest_auto() per l'uso regolare. Il punto è che se la struttura dei dati cambia unnest_auto() può modificare il meccanismo di trasformazione dei dati selezionato se inizialmente ha espanso le colonne dell'elenco in righe utilizzando unnest_longer(), quindi quando la struttura dei dati in entrata cambia, la logica può essere cambiata a favore unnest_wider()e l'utilizzo di questo approccio su base continuativa può portare a errori imprevisti.
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
Geocodifica con Google
Successivamente, esamineremo una struttura più complessa dei dati ottenuti dal servizio di geocodifica di Google. La memorizzazione nella cache delle credenziali è contraria alle regole per lavorare con l'API di Google Maps, quindi scriverò prima un semplice wrapper attorno all'API. Che si basa sulla memorizzazione della chiave API di Google Maps in una variabile di ambiente; Se non disponi della chiave per lavorare con l'API di Google Maps memorizzata nelle variabili di ambiente, i frammenti di codice presentati in questa sezione non verranno eseguiti.
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)
}
L'elenco restituito da questa funzione è piuttosto complesso:
Fortunatamente, possiamo risolvere passo dopo passo il problema di convertire questi dati in una forma tabellare utilizzando le funzioni tidyr. Per rendere il compito un po' più impegnativo e realistico, inizierò geocodificando alcune città:
city <- c ( "Houston" , "LA" , "New York" , "Chicago" , "Springfield" ) city_geo <- purrr::map (city, geocode)
Convertirò il risultato risultante in tibble, per comodità, aggiungerò una colonna con il nome della città corrispondente.
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]>
Il primo livello contiene componenti status и result, con cui possiamo espandere 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
Si noti che results è un elenco a più livelli. La maggior parte delle città ha 1 elemento (che rappresenta un valore univoco corrispondente all'API di geocodifica), ma Springfield ne ha due. Possiamo inserirli in linee separate con 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
Ora hanno tutti gli stessi componenti, che possono essere verificati utilizzando 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
Possiamo trovare le coordinate di latitudine e longitudine di ciascuna città espandendo l'elenco 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>
E poi la posizione per la quale devi espanderti 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>
Anche in questo caso, unnest_auto() semplifica l'operazione descritta con alcuni rischi che potrebbero derivare dalla modifica della struttura dei dati in ingresso:
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>
Possiamo anche guardare solo il primo indirizzo per ogni città:
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>
Oppure utilizzare hoist() per un'immersione multilivello a cui andare direttamente 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]>
Discografia di Sharla Gelfand
Infine, esamineremo la struttura più complessa: la discografia di Sharla Gelfand. Come negli esempi precedenti, iniziamo convertendo l'elenco in un frame di dati a colonna singola, quindi lo estendiamo in modo che ciascun componente sia una colonna separata. Inoltre trasformo la colonna date_added nel formato di data e ora appropriato in 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
A questo livello, otteniamo informazioni su quando ciascun disco è stato aggiunto alla discografia di Sharla, ma non vediamo alcun dato su tali dischi. Per fare ciò dobbiamo espandere la colonna basic_information:
discs %>% unnest_wider(basic_information)
#> Column name `id` must not be duplicated.
#> Use .name_repair to specify repair.
Sfortunatamente, riceveremo un errore perché... all'interno dell'elenco basic_information c'è una colonna con lo stesso nome basic_information. Se si verifica un errore di questo tipo, per determinarne rapidamente la causa, è possibile utilizzare names_repair = "unique":
È quindi possibile unirli nuovamente al set di dati originale secondo necessità.
conclusione
Al centro della biblioteca tidyverse include molti pacchetti utili uniti da una filosofia comune di elaborazione dei dati.
In questo articolo abbiamo esaminato la famiglia delle funzioni unnest_*(), che hanno lo scopo di lavorare con l'estrazione di elementi da elenchi nidificati. Questo pacchetto contiene molte altre funzionalità utili che semplificano la conversione dei dati in base al concetto Dati ordinati.