ProHoster > Blog > Administración > Expansión de columnas aniñadas: listas usando a linguaxe R (paquete tidyr e funcións da familia unnest)
Expansión de columnas aniñadas: listas usando a linguaxe R (paquete tidyr e funcións da familia unnest)
Na maioría dos casos, cando se traballa cunha resposta recibida dunha API ou con calquera outro dato que teña unha estrutura de árbore complexa, atópase con formatos JSON e XML.
Estes formatos teñen moitas vantaxes: almacenan datos de forma bastante compacta e permiten evitar a duplicación innecesaria de información.
A desvantaxe destes formatos é a complexidade do seu procesamento e análise. Os datos non estruturados non se poden usar nos cálculos e non se pode construír a visualización sobre el.
Este artigo é unha continuación lóxica da publicación "Paquete R tidyr e as súas novas funcións pivot_longer e pivot_wider". Axudarache a incorporar estruturas de datos non estruturadas a unha forma tabular familiar e adecuada para a análise mediante o paquete tidyr, incluído no núcleo da biblioteca tidyverse, e a súa familia de funcións unnest_*().
Contido
Se estás interesado na análise de datos, podes estar interesado no meu telegrama и youtube canles. A maior parte do contido está dedicado á linguaxe R.
Rectangular(nota do tradutor, non atopei opcións de tradución adecuadas para este termo, así que o deixaremos como está). é o proceso de traer datos non estruturados con matrices aniñadas nunha táboa bidimensional formada por filas e columnas coñecidas. EN tidyr Hai varias funcións que che axudarán a expandir as columnas de listas aniñadas e reducir os datos a unha forma rectangular e tabular:
unnest_longer() toma cada elemento da lista de columnas e crea unha nova fila.
unnest_wider() toma cada elemento da lista de columnas e crea unha nova columna.
unnest_auto() determina automaticamente que función é mellor usar unnest_longer() ou unnest_wider().
hoist() semellante a unnest_wider() pero selecciona só os compoñentes especificados e permítelle traballar con varios niveis de anidación.
A maioría dos problemas asociados ao traer datos non estruturados con varios niveis de aniñamento nunha táboa bidimensional pódense resolver combinando as funcións listadas con dplyr.
Para demostrar estas técnicas, utilizaremos o paquete repurrrsive, que ofrece varias listas complexas de varios niveis derivadas dunha API web.
Comecemos gh_usuarios, unha lista que contén información sobre seis usuarios de GitHub. Primeiro imos transformar a lista gh_usuarios в tible cadro:
users <- tibble( user = gh_users )
Isto parece un pouco contraintuitivo: por que proporcionar unha lista gh_usuarios, a unha estrutura de datos máis complexa? Pero un marco de datos ten unha gran vantaxe: combina varios vectores para que todo se rastrexa nun mesmo obxecto.
Cada elemento obxecto users é unha lista con nome na que cada elemento representa unha columna.
Neste caso, temos unha táboa formada por 30 columnas, e non necesitaremos a maioría delas, polo que podemos unnest_wider() uso hoist(). hoist() permítenos extraer compoñentes seleccionados utilizando a mesma sintaxe que 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() elimina os compoñentes nomeados especificados dunha lista de columnas usuarioasí podes considerar hoist() como mover compoñentes da lista interna dun marco de data ao seu nivel superior.
Repositorios Github
Aliñación da lista gh_repos comezamos do mesmo xeito converténdoo en tibble:
Esta vez os elementos usuario representan unha lista de repositorios propiedade deste usuario. Cada repositorio é unha observación separada, polo que segundo o concepto de datos ordenados (aprox. datos ordenados) deberían converterse en liñas novas, por iso usamos unnest_longer() pero non 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
Agora podemos usar unnest_wider() ou 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
Preste atención ao uso c("owner", "login"): Isto permítenos obter o valor do segundo nivel dunha lista aniñada owner. Un enfoque alternativo é obter a lista completa owner e despois usando a función unnest_wider() pon cada un dos seus elementos nunha columna:
En vez de pensar en escoller a función correcta unnest_longer() ou unnest_wider() podes usar unnest_auto(). Esta función usa varios métodos heurísticos para seleccionar a función máis adecuada para transformar os datos e mostra unha mensaxe sobre o método escollido.
got_chars ten unha estrutura idéntica á gh_users: Este é un conxunto de listas con nome, onde cada elemento da lista interna describe algún atributo dun personaxe de Game of Thrones. Traendo got_chars Para a vista de táboa, comezamos creando un marco de data, como nos exemplos anteriores, e despois convertemos cada elemento nunha columna separada:
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>
Estrutura got_chars algo máis difícil que gh_users, porque algúns compoñentes da lista char son unha lista, como resultado obtemos piares: listas:
As túas accións posteriores dependen dos obxectivos da análise. Quizais necesites poñer información sobre as liñas de cada libro e serie na que aparece o personaxe:
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
Ou quizais queres crear unha táboa que che permita relacionar o personaxe e a obra:
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
(Teña en conta os valores baleiros "" en campo title, isto débese a erros cometidos ao introducir os datos got_chars: de feito, personaxes para os que non hai títulos de libros e series de televisión correspondentes no campo title debe ter un vector de lonxitude 0, non un vector de lonxitude 1 que conteña a cadea baleira.)
Podemos reescribir o exemplo anterior usando a función unnest_auto(). Este enfoque é conveniente para a análise dunha soa vez, pero non debe confiar unnest_auto() para o seu uso regular. A cuestión é que se cambia a estrutura de datos unnest_auto() pode cambiar o mecanismo de transformación de datos seleccionado se inicialmente expandiu as columnas da lista en filas usando unnest_longer(), entón cando a estrutura dos datos entrantes cambia, a lóxica pódese cambiar a favor unnest_wider(), e usar este enfoque de forma continuada pode provocar erros inesperados.
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
Xeocodificación con Google
A continuación, analizaremos unha estrutura máis complexa dos datos obtidos do servizo de xeocodificación de Google. O almacenamento en caché de credenciais vai en contra das regras de traballo coa API de Google Maps, polo que primeiro escribirei un envoltorio sinxelo arredor da API. Que se basea en almacenar a clave da API de Google Maps nunha variable de ambiente; Se non tes a clave para traballar coa API de Google Maps almacenada nas túas variables de ambiente, non se executarán os fragmentos de código que se presentan nesta sección.
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)
}
A lista que devolve esta función é bastante complexa:
Afortunadamente, podemos resolver o problema de converter estes datos nunha táboa paso a paso mediante funcións tidyr. Para facer a tarefa un pouco máis desafiante e realista, comezarei xeocodificando algunhas cidades:
city <- c ( "Houston" , "LA" , "New York" , "Chicago" , "Springfield" ) city_geo <- purrr::map (city, geocode)
Converterei o resultado resultante en tibble, por comodidade, engadirei unha columna co nome da cidade correspondente.
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]>
O primeiro nivel contén compoñentes status и result, coa que podemos ampliar 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
Nótese que results é unha lista de varios niveis. A maioría das cidades teñen 1 elemento (que representa un valor único correspondente á API de xeocodificación), pero Springfield ten dous. Podemos tiralos en liñas separadas 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
Agora todos teñen os mesmos compoñentes, que se poden verificar usando 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
Podemos atopar as coordenadas de latitude e lonxitude de cada cidade ampliando a lista 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 despois a localización para a que precisa expandir 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>
De novo, unnest_auto() simplifica a operación descrita con algúns riscos que poden producirse ao cambiar a estrutura dos datos entrantes:
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>
Tamén podemos mirar o primeiro enderezo de cada cidade:
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>
Ou use hoist() para ir directamente a unha inmersión de varios niveis 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]>
Discografía de Sharla Gelfand
Finalmente, analizaremos a estrutura máis complexa: a discografía de Sharla Gelfand. Como nos exemplos anteriores, comezamos convertendo a lista nun marco de datos dunha soa columna e, a continuación, ampliamos para que cada compoñente sexa unha columna separada. Tamén transformo a columna date_added ao formato de data e hora apropiado en 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
Neste nivel, obtemos información sobre cando se engadiu cada disco á discografía de Sharla, pero non vemos ningún dato sobre eses discos. Para iso necesitamos ampliar a columna basic_information:
discs %>% unnest_wider(basic_information)
#> Column name `id` must not be duplicated.
#> Use .name_repair to specify repair.
Desafortunadamente, recibiremos un erro, porque... dentro da lista basic_information hai unha columna co mesmo nome basic_information. Se se produce tal erro, podes usar para determinar rapidamente a súa causa names_repair = "unique":
Despois podes unilos de novo ao conxunto de datos orixinal segundo sexa necesario.
Conclusión
Ata o núcleo da biblioteca tidyverse inclúe moitos paquetes útiles unidos por unha filosofía común de procesamento de datos.
Neste artigo examinamos a familia de funcións unnest_*(), que teñen como obxectivo traballar coa extracción de elementos de listas aniñadas. Este paquete contén moitas outras funcións útiles que facilitan a conversión de datos segundo o concepto Datos ordenados.