ProHoster > Blog > administration > Extension de colonnes imbriquées - listes utilisant le langage R (package tidyr et fonctions de la famille unnest)
Extension de colonnes imbriquées - listes utilisant le langage R (package tidyr et fonctions de la famille unnest)
Dans la plupart des cas, lorsque vous travaillez avec une réponse reçue d'une API, ou avec toute autre donnée ayant une structure arborescente complexe, vous êtes confronté aux formats JSON et XML.
Ces formats présentent de nombreux avantages : ils stockent les données de manière assez compacte et permettent d'éviter la duplication inutile d'informations.
L'inconvénient de ces formats est la complexité de leur traitement et de leur analyse. Les données non structurées ne peuvent pas être utilisées dans les calculs et la visualisation ne peut pas être construite à partir de celles-ci.
Cet article est la suite logique de la publication "Package R Tidyr et ses nouvelles fonctions pivot_longer et pivot_wider". Il vous aidera à regrouper les structures de données non structurées sous une forme tabulaire familière et adaptée à l'analyse à l'aide du package. tidyr, inclus dans le noyau de la bibliothèque tidyverse, et sa famille de fonctions unnest_*().
Teneur
Si vous êtes intéressé par l'analyse des données, vous pourriez être intéressé par mon télégramme и Youtube canaux. La plupart du contenu est dédié au langage R.
Rectanglage(note du traducteur, je n’ai pas trouvé d’options de traduction adéquates pour ce terme, nous le laisserons donc tel quel.) est le processus consistant à intégrer des données non structurées avec des tableaux imbriqués dans un tableau bidimensionnel composé de lignes et de colonnes familières. DANS tidyr Il existe plusieurs fonctions qui vous aideront à développer les colonnes de liste imbriquées et à réduire les données sous une forme rectangulaire et tabulaire :
unnest_longer() prend chaque élément de la liste de colonnes et crée une nouvelle ligne.
unnest_wider() prend chaque élément de la liste de colonnes et crée une nouvelle colonne.
unnest_auto() détermine automatiquement quelle fonction est la meilleure à utiliser unnest_longer() ou unnest_wider().
hoist() semblable à unnest_wider() mais sélectionne uniquement les composants spécifiés et vous permet de travailler avec plusieurs niveaux d'imbrication.
La plupart des problèmes associés à l'intégration de données non structurées avec plusieurs niveaux d'imbrication dans un tableau bidimensionnel peuvent être résolus en combinant les fonctions répertoriées avec dplyr.
Pour démontrer ces techniques, nous utiliserons le package repurrrsive, qui fournit plusieurs listes complexes à plusieurs niveaux dérivées d'une API Web.
Commençons avec gh_utilisateurs, une liste contenant des informations sur six utilisateurs de GitHub. Transformons d'abord la liste gh_utilisateurs в chatouiller cadre:
users <- tibble( user = gh_users )
Cela semble un peu contre-intuitif : pourquoi fournir une liste gh_utilisateurs, à une structure de données plus complexe ? Mais une trame de données présente un gros avantage : elle combine plusieurs vecteurs afin que tout soit suivi dans un seul objet.
Chaque élément d'objet users est une liste nommée dans laquelle chaque élément représente une colonne.
Dans ce cas, nous avons un tableau composé de 30 colonnes, et nous n'aurons pas besoin de la plupart d'entre elles, nous pouvons donc unnest_wider() utiliser hoist(). hoist() nous permet d'extraire les composants sélectionnés en utilisant la même syntaxe 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() supprime les composants nommés spécifiés d'une liste de colonnes utilisateurpour que tu puisses considérer hoist() comme déplacer des composants de la liste interne d’un cadre de date vers son niveau supérieur.
Dépôts Github
Alignement de la liste gh_repos nous commençons de la même manière par le convertir en tibble:
Cette fois, les éléments utilisateur représente une liste de référentiels appartenant à cet utilisateur. Chaque référentiel est une observation distincte, donc selon le concept de données soignées (données approximatives) ils devraient devenir de nouvelles lignes, c'est pourquoi nous utilisons unnest_longer() plutôt 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
Maintenant nous pouvons utiliser 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
Attention à l'utilisation c("owner", "login"): Cela nous permet d'obtenir la valeur de deuxième niveau à partir d'une liste imbriquée owner. Une approche alternative consiste à obtenir la liste complète owner puis en utilisant la fonction unnest_wider() placez chacun de ses éléments dans une colonne :
Au lieu de penser à choisir la bonne fonction unnest_longer() ou unnest_wider() vous pouvez utiliser unnest_auto(). Cette fonction utilise plusieurs méthodes heuristiques pour sélectionner la fonction la plus adaptée à la transformation des données et affiche un message sur la méthode choisie.
got_chars a une structure identique à gh_users: Il s'agit d'un ensemble de listes nommées, où chaque élément de la liste interne décrit un attribut d'un personnage de Game of Thrones. Apportant got_chars Pour la vue tableau, nous commençons par créer un cadre de date, comme dans les exemples précédents, puis convertissons chaque élément en une colonne distincte :
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>
structure got_chars un peu plus difficile que gh_users, parce que quelques composants de liste char eux-mêmes sont une liste, en conséquence nous obtenons des piliers - des listes :
Vos actions ultérieures dépendent des objectifs de l'analyse. Peut-être avez-vous besoin de mettre des informations sur les lignes de chaque livre et série dans lesquels le personnage apparaît :
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 peut-être souhaitez-vous créer un tableau qui vous permette de faire correspondre le personnage et l'œuvre :
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
(Notez les valeurs vides "" dans le champ title, cela est dû à des erreurs commises lors de la saisie des données dans got_chars: en fait, des personnages pour lesquels il n'existe pas de titres de livres et de séries télévisées correspondants dans le domaine title doit avoir un vecteur de longueur 0, pas un vecteur de longueur 1 contenant la chaîne vide.)
Nous pouvons réécrire l'exemple ci-dessus en utilisant la fonction unnest_auto(). Cette approche est pratique pour une analyse ponctuelle, mais vous ne devez pas vous fier à unnest_auto() pour une utilisation régulière. Le fait est que si votre structure de données change unnest_auto() peut modifier le mécanisme de transformation de données sélectionné s'il a initialement développé les colonnes de la liste en lignes à l'aide de unnest_longer(), puis lorsque la structure des données entrantes change, la logique peut être modifiée en faveur unnest_wider(), et l’utilisation continue de cette approche peut entraîner des erreurs inattendues.
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
Géocodage avec Google
Nous examinerons ensuite une structure plus complexe des données obtenues à partir du service de géocodage de Google. La mise en cache des informations d'identification est contraire aux règles de travail avec l'API Google Maps, je vais donc d'abord écrire un simple wrapper autour de l'API. Qui est basé sur le stockage de la clé API Google Maps dans une variable d'environnement ; Si vous n'avez pas la clé pour travailler avec l'API Google Maps stockée dans vos variables d'environnement, les fragments de code présentés dans cette section ne seront pas exécutés.
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 liste renvoyée par cette fonction est assez complexe :
Heureusement, nous pouvons résoudre le problème de la conversion de ces données sous forme de tableau étape par étape à l'aide de fonctions tidyr. Pour rendre la tâche un peu plus difficile et réaliste, je vais commencer par géocoder quelques villes :
city <- c ( "Houston" , "LA" , "New York" , "Chicago" , "Springfield" ) city_geo <- purrr::map (city, geocode)
Je vais convertir le résultat obtenu en tibble, pour plus de commodité, j'ajouterai une colonne avec le nom de la ville correspondante.
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]>
Le premier niveau contient des composants status и result, que nous pouvons étendre avec 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
S'il vous plaît noter que results est une liste à plusieurs niveaux. La plupart des villes ont 1 élément (représentant une valeur unique correspondant à l'API de géocodage), mais Springfield en a deux. Nous pouvons les regrouper dans des lignes séparées avec 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
Désormais, ils ont tous les mêmes composants, qui peuvent être vérifiés à l'aide de 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
Nous pouvons trouver les coordonnées de latitude et de longitude de chaque ville en élargissant la liste 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>
Et puis l'emplacement pour lequel vous devez vous développer 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>
Encore unnest_auto() simplifie l'opération décrite avec certains risques pouvant être causés par la modification de la structure des données 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>
On peut aussi simplement regarder la première adresse de chaque ville :
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 utiliser hoist() pour une plongée multi-niveaux pour aller directement à 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]>
Discographie de Sharla Gelfand
Enfin, nous examinerons la structure la plus complexe : la discographie de Sharla Gelfand. Comme dans les exemples ci-dessus, nous commençons par convertir la liste en un bloc de données à une seule colonne, puis nous la développons pour que chaque composant soit une colonne distincte. Aussi je transforme la colonne date_added au format de date et d'heure approprié dans 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
À ce niveau, nous obtenons des informations sur le moment où chaque disque a été ajouté à la discographie de Sharla, mais nous ne voyons aucune donnée sur ces disques. Pour ce faire, nous devons agrandir la colonne basic_information:
discs %>% unnest_wider(basic_information)
#> Column name `id` must not be duplicated.
#> Use .name_repair to specify repair.
Malheureusement, nous recevrons une erreur, car... à l'intérieur de la liste basic_information il y a une colonne du même nom basic_information. Si une telle erreur se produit, afin d'en déterminer rapidement la cause, vous pouvez utiliser names_repair = "unique":
Vous pouvez ensuite les joindre à l'ensemble de données d'origine si nécessaire.
Conclusion
Au cœur de la bibliothèque tidyverse comprend de nombreux packages utiles unis par une philosophie commune de traitement des données.
Dans cet article, nous avons examiné la famille de fonctions unnest_*(), qui visent à extraire des éléments de listes imbriquées. Ce package contient de nombreuses autres fonctionnalités utiles qui facilitent la conversion des données selon le concept Des données bien rangées.