ProHoster > Blog > administración > Columnas anidadas expandibles: listas que utilizan el lenguaje R (paquete tidyr y funciones de la familia unnest)
Columnas anidadas expandibles: listas que utilizan el lenguaje R (paquete tidyr y funciones de la familia unnest)
En la mayoría de los casos, cuando se trabaja con una respuesta recibida de una API, o con cualquier otro dato que tenga una estructura de árbol compleja, se enfrenta a formatos JSON y XML.
Estos formatos tienen muchas ventajas: almacenan datos de forma bastante compacta y permiten evitar duplicaciones innecesarias de información.
La desventaja de estos formatos es la complejidad de su procesamiento y análisis. Los datos no estructurados no se pueden utilizar en los cálculos y la visualización no se puede basar en ellos.
Este artículo es una continuación lógica de la publicación. "Paquete R tidyr y sus nuevas funciones pivot_longer y pivot_wider". Le ayudará a convertir las estructuras de datos no estructuradas en una forma tabular familiar y adecuada para el análisis utilizando el paquete. tidyr, incluido en el núcleo de la biblioteca tidyverse, y su familia de funciones unnest_*().
contenido
Si te interesa el análisis de datos, quizás te interese mi Telegram. и Youtube canales La mayor parte del contenido está dedicado al lenguaje R.
Rectángulo(Nota del traductor: no encontré opciones de traducción adecuadas para este término, así que lo dejaremos como está). es el proceso de reunir datos no estructurados con matrices anidadas en una tabla bidimensional que consta de filas y columnas familiares. EN tidyr Hay varias funciones que le ayudarán a expandir las columnas de la lista anidada y reducir los datos a una forma tabular rectangular:
unnest_longer() toma cada elemento de la lista de columnas y crea una nueva fila.
unnest_wider() toma cada elemento de la lista de columnas y crea una nueva columna.
unnest_auto() determina automáticamente qué función es mejor utilizar unnest_longer() o unnest_wider().
hoist() Similar a unnest_wider() pero selecciona solo los componentes especificados y le permite trabajar con varios niveles de anidamiento.
La mayoría de los problemas asociados con la incorporación de datos no estructurados con varios niveles de anidamiento en una tabla bidimensional se pueden resolver combinando las funciones enumeradas con dplyr.
Para demostrar estas técnicas, usaremos el paquete repurrrsive, que proporciona múltiples listas complejas de varios niveles derivadas de una API web.
Comencemos con usuarios_gh, una lista que contiene información sobre seis usuarios de GitHub. Primero transformemos la lista. usuarios_gh в titubear marco:
users <- tibble( user = gh_users )
Esto parece un poco contradictorio: ¿por qué proporcionar una lista? usuarios_gh, a una estructura de datos más compleja? Pero un marco de datos tiene una gran ventaja: combina múltiples vectores para que todo se rastree en un solo objeto.
Cada elemento del objeto users es una lista con nombre en la que cada elemento representa una columna.
En este caso, tenemos una tabla que consta de 30 columnas y no necesitaremos la mayoría de ellas, por lo que podemos unnest_wider() utilizar hoist(). hoist() nos permite extraer componentes seleccionados usando la misma sintaxis 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 los componentes con nombre especificados de una lista de columnas usuariopara que puedas considerar hoist() como mover componentes de la lista interna de un marco de fecha a su nivel superior.
Repositorios de Github
Alineación de lista gh_repos comenzamos de manera similar convirtiéndolo a tibble:
Esta vez los elementos usuario representan una lista de repositorios propiedad de este usuario. Cada repositorio es una observación separada, por lo que de acuerdo con el concepto de datos ordenados (aprox. datos ordenados) deberían convertirse en nuevas líneas, por eso usamos unnest_longer() más bien que 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
Ahora podemos usar 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
Presta atención al uso c("owner", "login"): Esto nos permite obtener el valor de segundo nivel de una lista anidada owner. Un enfoque alternativo es obtener la lista completa. owner y luego usando la función unnest_wider() poner cada uno de sus elementos en una columna:
En lugar de pensar en elegir la función adecuada unnest_longer() o unnest_wider() puedes usar unnest_auto(). Esta función utiliza varios métodos heurísticos para seleccionar la función más adecuada para transformar los datos y muestra un mensaje sobre el método elegido.
got_chars tiene una estructura idéntica a gh_users: Este es un conjunto de listas con nombres, donde cada elemento de la lista interna describe algún atributo de un personaje de Juego de Tronos. trayendo got_chars Para la vista de tabla, comenzamos creando un marco de fechas, como en los ejemplos anteriores, y luego convertimos cada elemento en una 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>
Estructura got_chars algo más difícil que gh_users, porque algunos componentes de la lista char ellos mismos son una lista, como resultado obtenemos pilares - listas:
Sus acciones futuras dependen de los objetivos del análisis. Quizás necesites poner información en las líneas de cada libro y serie en la que aparece el personaje:
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 tal vez quieras crear una tabla que te permita relacionar el personaje y la 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
(Tenga en cuenta los valores vacíos "" en el campo title, esto se debe a errores cometidos al ingresar datos en got_chars: de hecho, personajes para los que no existen títulos de libros ni series de televisión correspondientes en el campo title debe tener un vector de longitud 0, no un vector de longitud 1 que contenga la cadena vacía).
Podemos reescribir el ejemplo anterior usando la función unnest_auto(). Este enfoque es conveniente para un análisis único, pero no debe confiar en unnest_auto() para su uso de forma regular. El punto es que si su estructura de datos cambia unnest_auto() puede cambiar el mecanismo de transformación de datos seleccionado si inicialmente expandió las columnas de la lista en filas usando unnest_longer(), luego, cuando la estructura de los datos entrantes cambia, la lógica se puede cambiar a favor unnest_wider(), y el uso continuo de este enfoque puede provocar errores 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
Geocodificación con Google
A continuación, veremos una estructura más compleja de los datos obtenidos del servicio de codificación geográfica de Google. El almacenamiento en caché de credenciales va en contra de las reglas de trabajo con la API de Google Maps, por lo que primero escribiré un contenedor simple para la API. El cual se basa en almacenar la clave API de Google Maps en una variable de entorno; Si no tiene la clave para trabajar con la API de Google Maps almacenada en sus variables de entorno, los fragmentos de código presentados en esta sección no se ejecutará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)
}
La lista que devuelve esta función es bastante compleja:
Afortunadamente, podemos resolver el problema de convertir estos datos en forma tabular paso a paso usando funciones tidyr. Para hacer la tarea un poco más desafiante y realista, comenzaré geocodificando algunas ciudades:
city <- c ( "Houston" , "LA" , "New York" , "Chicago" , "Springfield" ) city_geo <- purrr::map (city, geocode)
Convertiré el resultado resultante en tibble, por conveniencia, agregaré una columna con el nombre de la ciudad correspondiente.
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]>
El primer nivel contiene componentes. status и result, que podemos ampliar con 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
Tenga en cuenta que results es una lista de varios niveles. La mayoría de las ciudades tienen 1 elemento (que representa un valor único correspondiente a la API de codificación geográfica), pero Springfield tiene dos. Podemos separarlos en líneas 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
Ahora todos tienen los mismos componentes, que se pueden 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 encontrar las coordenadas de latitud y longitud de cada ciudad ampliando la 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>
Y luego la ubicación para la cual necesitas expandirte. 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 nuevo unnest_auto() simplifica la operación descrita con algunos riesgos que pueden surgir al cambiar la estructura de los 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>
También podemos mirar la primera dirección de cada ciudad:
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>
O usar hoist() para una inmersión multinivel para ir directamente a 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, veremos la estructura más compleja: la discografía de Sharla Gelfand. Como en los ejemplos anteriores, comenzamos convirtiendo la lista en un marco de datos de una sola columna y luego la ampliamos para que cada componente sea una columna separada. También transformo la columna. date_added al formato de fecha y 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
En este nivel, obtenemos información sobre cuándo se agregó cada disco a la discografía de Sharla, pero no vemos ningún dato sobre esos discos. Para hacer esto necesitamos expandir la columna. basic_information:
discs %>% unnest_wider(basic_information)
#> Column name `id` must not be duplicated.
#> Use .name_repair to specify repair.
Desafortunadamente, recibiremos un error, porque... dentro de la lista basic_information hay una columna con el mismo nombre basic_information. Si se produce un error de este tipo, para determinar rápidamente su causa, puede utilizar names_repair = "unique":
Luego puede volver a unirlos al conjunto de datos original según sea necesario.
Conclusión
Al núcleo de la biblioteca tidyverse Incluye muchos paquetes útiles unidos por una filosofía de procesamiento de datos común.
En este artículo examinamos la familia de funciones. unnest_*(), que tienen como objetivo trabajar con la extracción de elementos de listas anidadas. Este paquete contiene muchas otras funciones útiles que facilitan la conversión de datos según el concepto. Datos ordenados.