ProHoster > Blog > administração > Expandindo colunas aninhadas - listas usando a linguagem R (pacote tidyr e funções de família unnest)
Expandindo colunas aninhadas - listas usando a linguagem R (pacote tidyr e funções de família unnest)
Na maioria dos casos, ao trabalhar com uma resposta recebida de uma API, ou com qualquer outro dado que possua uma estrutura em árvore complexa, você se depara com formatos JSON e XML.
Esses formatos têm muitas vantagens: armazenam dados de forma bastante compacta e permitem evitar duplicações desnecessárias de informações.
A desvantagem destes formatos é a complexidade do seu processamento e análise. Dados não estruturados não podem ser usados em cálculos e a visualização não pode ser construída sobre eles.
Este artigo é uma continuação lógica da publicação "Pacote R tidyr e suas novas funções pivot_longer e pivot_wider". Isso o ajudará a trazer estruturas de dados não estruturados para um formato tabular familiar e adequado para análise usando o pacote tidyr, incluído no núcleo da biblioteca tidyverse, e sua família de funções unnest_*().
Conteúdo
Se você estiver interessado em análise de dados, talvez esteja interessado em meu telegrama и Youtube canais. A maior parte do conteúdo é dedicada à linguagem R.
Retângulo(nota do tradutor, não encontrei opções de tradução adequadas para este termo, então deixaremos como está.) é o processo de trazer dados não estruturados com matrizes aninhadas em uma tabela bidimensional que consiste em linhas e colunas familiares. EM tidyr Existem várias funções que ajudarão você a expandir colunas de listas aninhadas e reduzir os dados para um formato tabular retangular:
unnest_longer() pega cada elemento da lista de colunas e cria uma nova linha.
unnest_wider() pega cada elemento da lista de colunas e cria uma nova coluna.
unnest_auto() determina automaticamente qual função é melhor usar unnest_longer() ou unnest_wider().
hoist() igual a unnest_wider() mas seleciona apenas os componentes especificados e permite trabalhar com vários níveis de aninhamento.
A maioria dos problemas associados à transferência de dados não estruturados com vários níveis de aninhamento para uma tabela bidimensional podem ser resolvidos combinando as funções listadas com dplyr.
Para demonstrar essas técnicas, usaremos o pacote repurrrsive, que fornece diversas listas complexas e de vários níveis derivadas de uma API Web.
Vamos começar com gh_users, uma lista que contém informações sobre seis usuários do GitHub. Primeiro vamos transformar a lista gh_users в comer quadro:
users <- tibble( user = gh_users )
Isto parece um pouco contra-intuitivo: por que fornecer uma lista gh_users, para uma estrutura de dados mais complexa? Mas um quadro de dados tem uma grande vantagem: combina vários vetores para que tudo seja rastreado em um objeto.
Cada elemento de objeto users é uma lista nomeada em que cada elemento representa uma coluna.
Neste caso, temos uma tabela composta por 30 colunas, e não precisaremos da maioria delas, então podemos em vez disso unnest_wider() usar hoist(). hoist() nos permite extrair componentes selecionados usando a mesma sintaxe de 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() remove os componentes nomeados especificados de uma lista de colunas usuárioentão você pode considerar hoist() como mover componentes da lista interna de um período para seu nível superior.
Repositórios Github
Alinhamento de lista gh_repos começamos da mesma forma convertendo-o para tibble:
Desta vez os elementos usuário representam uma lista de repositórios pertencentes a este usuário. Cada repositório é uma observação separada, portanto, de acordo com o conceito de dados organizados (dados aproximadamente organizados) eles deveriam se tornar novas linhas, e é por isso que usamos unnest_longer() em vez de 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 atenção ao uso c("owner", "login"): Isso nos permite obter o valor de segundo nível de uma lista aninhada owner. Uma abordagem alternativa é obter a lista inteira owner e então usando a função unnest_wider() coloque cada um de seus elementos em uma coluna:
Em vez de pensar em escolher a função certa unnest_longer() ou unnest_wider() você pode usar unnest_auto(). Esta função utiliza diversos métodos heurísticos para selecionar a função mais adequada para a transformação dos dados e exibe uma mensagem sobre o método escolhido.
got_chars tem uma estrutura idêntica à gh_users: Este é um conjunto de listas nomeadas, onde cada elemento da lista interna descreve algum atributo de um personagem de Game of Thrones. Trazendo got_chars Para a visualização de tabela, começamos criando um quadro de data, assim como nos exemplos anteriores, e depois convertemos cada elemento em uma coluna 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 um pouco mais difícil do que gh_users, porque alguns componentes da lista char eles próprios são uma lista, como resultado obtemos pilares - listas:
Suas ações futuras dependem dos objetivos da análise. Talvez seja necessário colocar informações nas falas de cada livro e série em que o personagem aparece:
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 talvez você queira criar uma tabela que permita combinar o personagem 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
(Observe os valores vazios "" no campo title, isso se deve a erros cometidos ao inserir dados em got_chars: na verdade, personagens para os quais não há títulos de livros e séries de TV correspondentes na área title deve ter um vetor de comprimento 0, não um vetor de comprimento 1 contendo a string vazia.)
Podemos reescrever o exemplo acima usando a função unnest_auto(). Essa abordagem é conveniente para análises únicas, mas você não deve confiar em unnest_auto() para uso regular. A questão é que se sua estrutura de dados mudar unnest_auto() pode alterar o mecanismo de transformação de dados selecionado se inicialmente expandiu as colunas da lista em linhas usando unnest_longer(), então, quando a estrutura dos dados recebidos mudar, a lógica poderá ser alterada em favor unnest_wider(), e usar essa abordagem continuamente pode levar a 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
Geocodificação com Google
A seguir, veremos uma estrutura mais complexa dos dados obtidos do serviço de geocodificação do Google. O armazenamento de credenciais em cache é contra as regras de trabalho com a API do Google Maps, então primeiro escreverei um wrapper simples em torno da API. Que se baseia no armazenamento da chave da API do Google Maps em uma variável de ambiente; Se você não tiver a chave para trabalhar com a API do Google Maps armazenada em suas variáveis de ambiente, os fragmentos de código apresentados nesta seção não serão executados.
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 esta função retorna é bastante complexa:
Felizmente, podemos resolver o problema de converter esses dados em formato tabular passo a passo usando funções tidyr. Para tornar a tarefa um pouco mais desafiadora e realista, começarei geocodificando algumas cidades:
city <- c ( "Houston" , "LA" , "New York" , "Chicago" , "Springfield" ) city_geo <- purrr::map (city, geocode)
Vou converter o resultado resultante em tibble, por conveniência, adicionarei uma coluna com o 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 nível contém componentes status и result, que podemos expandir com 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
Note-se que results é uma lista de vários níveis. A maioria das cidades possui 1 elemento (representando um valor exclusivo correspondente à API de geocodificação), mas Springfield possui dois. Podemos colocá-los em linhas separadas com 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 eles têm os mesmos componentes, o que pode ser verificado 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 as coordenadas de latitude e longitude de cada cidade expandindo 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 então o local para o qual você 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>
Novamente, unnest_auto() simplifica a operação descrita com alguns riscos que podem ser causados pela alteração da estrutura dos dados recebidos:
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>
Também podemos olhar apenas o primeiro endereço 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 um mergulho multinível ir diretamente para 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 de Sharla Gelfand
Por fim, veremos a estrutura mais complexa - a discografia de Sharla Gelfand. Como nos exemplos acima, começamos convertendo a lista em um quadro de dados de coluna única e depois a estendemos para que cada componente seja uma coluna separada. Também transformo a coluna date_added para o formato de data e hora apropriado em 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
Nesse nível, obtemos informações sobre quando cada disco foi adicionado à discografia de Sharla, mas não vemos nenhum dado sobre esses discos. Para fazer isso, precisamos expandir a coluna basic_information:
discs %>% unnest_wider(basic_information)
#> Column name `id` must not be duplicated.
#> Use .name_repair to specify repair.
Infelizmente, receberemos um erro, porque... dentro da lista basic_information existe uma coluna com o mesmo nome basic_information. Se tal erro ocorrer, para determinar rapidamente sua causa, você pode usar names_repair = "unique":
Você pode então juntá-los novamente ao conjunto de dados original, conforme necessário.
Conclusão
Para o núcleo da biblioteca tidyverse inclui muitos pacotes úteis unidos por uma filosofia comum de processamento de dados.
Neste artigo examinamos a família de funções unnest_*(), que visam trabalhar com extração de elementos de listas aninhadas. Este pacote contém muitos outros recursos úteis que facilitam a conversão de dados de acordo com o conceito Dados organizados.