ProHoster > BLOG > administrare > Extinderea coloanelor imbricate - liste folosind limbajul R (pachetul tidyr și funcțiile familiei unnest)
Extinderea coloanelor imbricate - liste folosind limbajul R (pachetul tidyr și funcțiile familiei unnest)
În cele mai multe cazuri, când lucrați cu un răspuns primit de la un API sau cu orice alte date care au o structură arborescentă complexă, vă confruntați cu formatele JSON și XML.
Aceste formate au multe avantaje: stochează datele destul de compact și vă permit să evitați duplicarea inutilă a informațiilor.
Dezavantajul acestor formate este complexitatea procesării și analizei lor. Datele nestructurate nu pot fi utilizate în calcule și vizualizarea nu poate fi construită pe ele.
Acest articol este o continuare logică a publicației „Pachetul R tidyr și noile sale funcții pivot_longer și pivot_wider”. Vă va ajuta să aduceți structurile de date nestructurate într-o formă tabelară familiară și adecvată pentru analiză folosind pachetul tidyr, inclusă în miezul bibliotecii tidyverse, și familia sa de funcții unnest_*().
Conținut
Dacă ești interesat de analiza datelor, s-ar putea să fii interesat de analiza mea telegramă и youtube canale. Cea mai mare parte a conținutului este dedicat limbajului R.
Dreptunghiulare(Nota traducătorului, nu am găsit opțiuni de traducere adecvate pentru acest termen, așa că îl vom lăsa așa cum este.) este procesul de aducere a datelor nestructurate cu matrice imbricate într-un tabel bidimensional format din rânduri și coloane familiare. ÎN tidyr Există mai multe funcții care vă vor ajuta să extindeți coloanele liste imbricate și să reduceți datele la o formă dreptunghiulară, tabelară:
unnest_longer() preia fiecare element din lista de coloane și creează un nou rând.
unnest_wider() ia fiecare element din lista de coloane și creează o nouă coloană.
unnest_auto() determină automat ce funcție este cea mai bună de utilizat unnest_longer() sau unnest_wider().
hoist() similar cu unnest_wider() dar selectează numai componentele specificate și vă permite să lucrați cu mai multe niveluri de imbricare.
Majoritatea problemelor asociate cu aducerea datelor nestructurate cu mai multe niveluri de imbricare într-un tabel bidimensional pot fi rezolvate prin combinarea funcțiilor enumerate cu dplyr.
Pentru a demonstra aceste tehnici, vom folosi pachetul repurrrsive, care oferă liste multiple complexe, pe mai multe niveluri, derivate dintr-un API web.
Să începem gh_users, o listă care conține informații despre șase utilizatori GitHub. Mai întâi să transformăm lista gh_users в tibble cadru:
users <- tibble( user = gh_users )
Acest lucru pare puțin contraintuitiv: de ce să furnizați o listă gh_users, la o structură de date mai complexă? Dar un cadru de date are un mare avantaj: combină mai mulți vectori, astfel încât totul să fie urmărit într-un singur obiect.
Fiecare element obiect users este o listă numită în care fiecare element reprezintă o coloană.
În acest caz, avem un tabel format din 30 de coloane și nu vom avea nevoie de majoritatea dintre ele, așa că putem în schimb unnest_wider() pentru a utiliza hoist(). hoist() ne permite să extragem componentele selectate folosind aceeași sintaxă ca 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() elimină componentele numite specificate dintr-o listă de coloane utilizatorastfel încât să puteți lua în considerare hoist() cum ar fi mutarea componentelor din lista internă a unui cadru de dată la nivelul său superior.
Arhivele Github
Alinierea listei gh_repos începem în mod similar prin a-l converti în tibble:
De data aceasta elementele utilizator reprezintă o listă de depozite deținute de acest utilizator. Fiecare depozit este o observație separată, deci conform conceptului de date ordonate (aprox. date ordonate) ar trebui să devină linii noi, motiv pentru care folosim unnest_longer() dar nu 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
Acum putem folosi unnest_wider() sau 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
Acordați atenție utilizării c("owner", "login"): Acest lucru ne permite să obținem valoarea de al doilea nivel dintr-o listă imbricată owner. O abordare alternativă este obținerea întregii liste owner și apoi folosind funcția unnest_wider() puneți fiecare dintre elementele sale într-o coloană:
În loc să te gândești să alegi funcția potrivită unnest_longer() sau unnest_wider() poți să folosești unnest_auto(). Această funcție folosește mai multe metode euristice pentru a selecta cea mai potrivită funcție pentru transformarea datelor și afișează un mesaj despre metoda aleasă.
got_chars are o structură identică cu gh_users: Acesta este un set de liste cu nume, în care fiecare element al listei interioare descrie anumite atribute ale unui personaj din Game of Thrones. Aducând got_chars Pentru vizualizarea tabelului, începem prin a crea un cadru de dată, la fel ca în exemplele anterioare, apoi convertim fiecare element într-o coloană separată:
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>
Structura got_chars ceva mai dificil decât gh_users, deoarece unele componente ale listei char ei înșiși sunt o listă, ca rezultat obținem piloni - liste:
Acțiunile dumneavoastră ulterioare depind de obiectivele analizei. Poate că trebuie să puneți informații pe rândurile pentru fiecare carte și serie în care apare personajul:
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
Sau poate doriți să creați un tabel care vă permite să potriviți personajul și lucrarea:
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
(Rețineți valorile goale "" în câmp title, acest lucru se datorează erorilor făcute la introducerea datelor în got_chars: de fapt, personaje pentru care nu există titluri de carte și seriale corespunzătoare în domeniu title trebuie să aibă un vector de lungime 0, nu un vector de lungime 1 care conține șirul gol.)
Putem rescrie exemplul de mai sus folosind funcția unnest_auto(). Această abordare este convenabilă pentru o analiză unică, dar nu ar trebui să vă bazați unnest_auto() pentru utilizare în mod regulat. Ideea este că dacă structura datelor dvs. se modifică unnest_auto() poate schimba mecanismul de transformare a datelor selectat dacă a extins inițial coloanele listei în rânduri folosind unnest_longer(), atunci când se modifică structura datelor primite, logica poate fi schimbată în favoarea unnest_wider(), iar utilizarea acestei abordări în mod continuu poate duce la erori neașteptate.
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
Geocodare cu Google
În continuare, ne vom uita la o structură mai complexă a datelor obținute de la serviciul de geocodare Google. Memorarea în cache a acreditărilor este împotriva regulilor de lucru cu API-ul Google Maps, așa că mai întâi voi scrie un simplu wrapper în jurul API-ului. Care se bazează pe stocarea cheii API Google Maps într-o variabilă de mediu; Dacă nu aveți cheia pentru lucrul cu API-ul Google Maps stocată în variabilele dvs. de mediu, fragmentele de cod prezentate în această secțiune nu vor fi executate.
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)
}
Lista pe care o returnează această funcție este destul de complexă:
Din fericire, putem rezolva problema conversiei acestor date într-o formă tabelară pas cu pas folosind funcții tidyr. Pentru a face sarcina un pic mai provocatoare și mai realistă, voi începe prin a geocoda câteva orașe:
city <- c ( "Houston" , "LA" , "New York" , "Chicago" , "Springfield" ) city_geo <- purrr::map (city, geocode)
Voi converti rezultatul rezultat în tibble, pentru comoditate, voi adăuga o coloană cu numele orașului corespunzător.
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]>
Primul nivel conține componente status и result, cu care ne putem extinde 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
Vă rugăm să rețineți faptul că results este o listă cu mai multe niveluri. Majoritatea orașelor au 1 element (reprezentând o valoare unică corespunzătoare API-ului de geocodare), dar Springfield are două. Le putem trage în linii separate cu 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
Acum toate au aceleași componente, care pot fi verificate folosind 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
Putem găsi coordonatele de latitudine și longitudine ale fiecărui oraș prin extinderea listei 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>
Și apoi locația pentru care trebuie să te extinzi 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>
Din nou, unnest_auto() simplifică operațiunea descrisă cu unele riscuri care pot fi cauzate de modificarea structurii datelor primite:
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>
De asemenea, ne putem uita doar la prima adresă pentru fiecare oraș:
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>
Sau folosiți hoist() pentru o scufundare pe mai multe niveluri la care să mergeți direct 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 lui Sharla Gelfand
În cele din urmă, ne vom uita la cea mai complexă structură - discografia lui Sharla Gelfand. Ca și în exemplele de mai sus, începem prin a converti lista într-un cadru de date cu o singură coloană, apoi o extindem astfel încât fiecare componentă să fie o coloană separată. De asemenea, transform coloana date_added la formatul adecvat de dată și oră în 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
La acest nivel, obținem informații despre când fiecare disc a fost adăugat la discografia lui Sharla, dar nu vedem date despre acele discuri. Pentru a face acest lucru, trebuie să extindem coloana basic_information:
discs %>% unnest_wider(basic_information)
#> Column name `id` must not be duplicated.
#> Use .name_repair to specify repair.
Din păcate, vom primi o eroare, deoarece... în interiorul listei basic_information există o coloană cu același nume basic_information. Dacă apare o astfel de eroare, pentru a determina rapid cauza acesteia, puteți utiliza names_repair = "unique":
Problema este că basic_information repetă coloana id care este, de asemenea, stocată la nivelul superior, astfel încât să o putem elimina pur și simplu:
Puteți apoi să le uniți înapoi la setul de date original după cum este necesar.
Concluzie
Până la miezul bibliotecii tidyverse include multe pachete utile unite printr-o filozofie comună de prelucrare a datelor.
În acest articol am examinat familia de funcții unnest_*(), care au ca scop lucrul cu extragerea elementelor din liste imbricate. Acest pachet conține multe alte caracteristici utile care facilitează conversia datelor conform conceptului Date ordonate.