23 계층적 데이터
23.1 소개
이 장에서는 데이터 직사각형화(rectangling) 기술을 배울 것입니다. 이는 근본적으로 계층적이거나 트리 형태인 데이터를 행과 열로 구성된 직사각형 데이터 프레임으로 변환하는 것입니다. 계층적 데이터는 놀라울 정도로 흔하며, 특히 웹에서 가져온 데이터로 작업할 때 더욱 그렇기 때문에 이 작업은 중요합니다.
직사각형화에 대해 배우려면 먼저 계층적 데이터를 가능하게 하는 데이터 구조인 리스트에 대해 배워야 합니다. 그런 다음 두 가지 중요한 tidyr 함수인 tidyr::unnest_longer()와 tidyr::unnest_wider()를 배울 것입니다. 그 후 실제 문제를 해결하기 위해 이 간단한 함수들을 반복해서 적용하는 몇 가지 사례 연구를 보여줄 것입니다. 마지막으로 계층적 데이터셋의 가장 빈번한 소스이자 웹에서 데이터 교환을 위한 일반적인 형식인 JSON에 대해 이야기하며 마무리하겠습니다.
23.1.1 선수 지식
이 장에서는 tidyverse의 핵심 멤버인 tidyr의 많은 함수를 사용할 것입니다. 또한 직사각형화 연습을 위한 흥미로운 데이터셋을 제공하는 repurrrsive를 사용하고, 마지막으로 JSON 파일을 R 리스트로 읽기 위해 jsonlite를 사용할 것입니다.
23.2 리스트(Lists)
지금까지 정수, 숫자, 문자, 날짜-시간 및 팩터와 같은 단순한 벡터를 포함하는 데이터 프레임으로 작업했습니다. 이러한 벡터는 동질적(homogeneous), 즉 모든 요소가 동일한 데이터 유형이기 때문에 단순합니다. 다른 유형의 요소를 동일한 벡터에 저장하려면 list()로 생성하는 리스트(list) 가 필요합니다:
x1 <- list(1:4, "a", TRUE)
x1
#> [[1]]
#> [1] 1 2 3 4
#>
#> [[2]]
#> [1] "a"
#>
#> [[3]]
#> [1] TRUE리스트의 구성 요소 또는 자식(children) 의 이름을 지정하는 것이 종종 편리한데, 이는 티블의 열 이름을 지정하는 것과 같은 방식으로 할 수 있습니다:
x2 <- list(a = 1:2, b = 1:3, c = 1:4)
x2
#> $a
#> [1] 1 2
#>
#> $b
#> [1] 1 2 3
#>
#> $c
#> [1] 1 2 3 4이러한 매우 단순한 리스트의 경우에도 인쇄하면 꽤 많은 공간을 차지합니다. 유용한 대안은 내용보다는 구조(structure)의 압축된 표시를 생성하는 str()입니다:
보시다시피 str()은 리스트의 각 자식을 별도의 줄에 표시합니다. 이름이 있으면 이름을 표시하고, 그 다음 유형의 약어, 그 다음 처음 몇 개의 값을 표시합니다.
23.2.1 계층 구조(Hierarchy)
리스트는 다른 리스트를 포함하여 모든 유형의 객체를 포함할 수 있습니다. 이로 인해 계층적(트리 형태) 구조를 나타내기에 적합합니다:
이것은 평면 벡터를 생성하는 c()와 눈에 띄게 다릅니다:
리스트가 복잡해짐에 따라 계층 구조를 한눈에 볼 수 있게 해주는 str()이 더 유용해집니다:
리스트가 훨씬 더 크고 복잡해지면 결국 str()도 실패하기 시작하고 View()1로 전환해야 합니다. Figure 23.1 는 View(x5)를 호출한 결과를 보여줍니다. 뷰어는 리스트의 최상위 수준만 보여주는 것으로 시작하지만, Figure 23.2 처럼 구성 요소를 대화식으로 확장하여 더 많은 내용을 볼 수 있습니다. RStudio는 또한 Figure 23.3 처럼 해당 요소에 액세스하는 데 필요한 코드를 보여줍니다. 이 코드가 어떻게 작동하는지는 Section 27.3 에서 다시 다룰 것입니다.
x5[[2]][[2]][[2]]입니다.
23.2.2 리스트 열(List-columns)
리스트는 티블 내부에도 존재할 수 있으며, 이를 리스트 열이라고 부릅니다. 리스트 열은 일반적으로 티블에 속하지 않을 객체를 티블에 배치할 수 있게 해주기 때문에 유용합니다. 특히 리스트 열은 tidymodels 생태계에서 많이 사용되는데, 모델 출력이나 리샘플링과 같은 것들을 데이터 프레임에 저장할 수 있게 해주기 때문입니다.
다음은 리스트 열의 간단한 예입니다:
티블 안의 리스트라고 해서 특별한 점은 없으며, 다른 열과 똑같이 작동합니다:
df |>
filter(x == 1)
#> # A tibble: 1 × 3
#> x y z
#> <int> <chr> <list>
#> 1 1 a <list [2]>리스트 열로 계산하는 것은 더 어렵지만, 그것은 일반적으로 리스트로 계산하는 것이 더 어렵기 때문입니다. 이에 대해서는 Chapter 26 에서 다시 다룰 것입니다. 이 장에서는 리스트 열을 일반 변수로 중첩 해제(unnesting)하여 기존 도구를 사용할 수 있도록 하는 데 집중할 것입니다.
기본 인쇄 방법은 내용의 대략적인 요약만 표시합니다. 리스트 열은 임의로 복잡할 수 있으므로 인쇄하는 좋은 방법이 없습니다. 그 내용을 보고 싶다면 리스트 열 하나만 뽑아내어 위에서 배운 기술 중 하나를 적용해야 합니다. 예: df |> pull(z) |> str() 또는 df |> pull(z) |> View().
리스트를 data.frame의 열에 넣는 것은 가능하지만, data.frame()이 리스트를 열의 리스트로 취급하기 때문에 훨씬 더 까다롭습니다:
data.frame(x = list(1:3, 3:5))
#> x.1.3 x.3.5
#> 1 1 3
#> 2 2 4
#> 3 3 5리스트 I()로 감싸서 data.frame()이 리스트를 행의 리스트로 취급하도록 강제할 수 있지만, 결과가 특별히 잘 인쇄되지는 않습니다:
data.frame(
x = I(list(1:2, 3:5)),
y = c("1, 2", "3, 4, 5")
)
#> x y
#> 1 1, 2 1, 2
#> 2 3, 4, 5 3, 4, 5티블에서 리스트 열을 사용하는 것이 더 쉬운데, tibble()이 리스트를 벡터처럼 취급하고 인쇄 방법이 리스트를 염두에 두고 설계되었기 때문입니다.
23.3 중첩 해제(Unnesting)
리스트와 리스트 열의 기초를 배웠으니 이제 어떻게 일반 행과 열로 되돌릴 수 있는지 알아봅시다. 여기서는 기본 아이디어를 얻을 수 있도록 매우 간단한 샘플 데이터를 사용할 것입니다. 다음 섹션에서는 실제 데이터로 전환하겠습니다.
리스트 열은 일반적으로 명명된 것과 이름이 없는 것의 두 가지 기본 형태가 있습니다. 자식이 이름이 있는(named) 경우, 모든 행에서 동일한 이름을 갖는 경향이 있습니다. 예를 들어 df1에서 리스트 열 y의 모든 요소는 a와 b라는 이름의 두 요소를 갖습니다. 이름이 있는 리스트 열은 자연스럽게 열로 중첩 해제됩니다: 각 명명된 요소는 새로운 명명된 열이 됩니다.
자식이 이름이 없는(unnamed) 경우, 요소의 수는 행마다 다른 경향이 있습니다. 예를 들어 df2에서 리스트 열 y의 요소는 이름이 없고 길이가 1에서 3까지 다양합니다. 이름이 없는 리스트 열은 자연스럽게 행으로 중첩 해제됩니다: 각 자식에 대해 하나의 행을 얻게 됩니다.
tidyr은 이 두 가지 경우를 위해 unnest_wider()와 unnest_longer()라는 두 가지 함수를 제공합니다. 다음 섹션에서 작동 방식을 설명합니다.
23.3.1 unnest_wider()
df1처럼 각 행이 동일한 이름을 가진 동일한 수의 요소를 가지고 있을 때, unnest_wider()를 사용하여 각 구성 요소를 자체 열에 넣는 것이 자연스럽습니다:
df1 |>
unnest_wider(y)
#> # A tibble: 3 × 3
#> x a b
#> <dbl> <dbl> <dbl>
#> 1 1 11 12
#> 2 2 21 22
#> 3 3 31 32기본적으로 새 열의 이름은 전적으로 리스트 요소의 이름에서 가져오지만, names_sep 인수를 사용하여 열 이름과 요소 이름을 결합하도록 요청할 수 있습니다. 이는 반복되는 이름의 모호함을 해결하는 데 유용합니다.
df1 |>
unnest_wider(y, names_sep = "_")
#> # A tibble: 3 × 3
#> x y_a y_b
#> <dbl> <dbl> <dbl>
#> 1 1 11 12
#> 2 2 21 22
#> 3 3 31 32
23.3.2 unnest_longer()
각 행에 이름이 없는 리스트가 포함된 경우, unnest_longer()를 사용하여 각 요소를 자체 행에 넣는 것이 가장 자연스럽습니다:
df2 |>
unnest_longer(y)
#> # A tibble: 6 × 2
#> x y
#> <dbl> <dbl>
#> 1 1 11
#> 2 1 12
#> 3 1 13
#> 4 2 21
#> 5 3 31
#> 6 3 32y 내부의 각 요소에 대해 x가 어떻게 복제되는지 주목하세요: 리스트 열 내부의 각 요소에 대해 하나의 출력 행을 얻습니다. 하지만 다음 예제처럼 요소 중 하나가 비어 있으면 어떻게 될까요?
df6 <- tribble(
~x, ~y,
"a", list(1, 2),
"b", list(3),
"c", list()
)
df6 |> unnest_longer(y)
#> # A tibble: 3 × 2
#> x y
#> <chr> <dbl>
#> 1 a 1
#> 2 a 2
#> 3 b 3출력에서 행이 0개가 되므로 행이 사실상 사라집니다. 해당 행을 보존하여 y에 NA를 추가하고 싶다면 keep_empty = TRUE를 설정하세요.
23.3.3 일관성 없는 유형
서로 다른 유형의 벡터를 포함하는 리스트 열을 중첩 해제하면 어떻게 될까요? 예를 들어 리스트 열 y에 두 개의 숫자, 문자, 논리형이 포함된 다음 데이터셋을 예로 들어보겠습니다. 이들은 일반적으로 단일 열에 섞일 수 없습니다.
unnest_longer()는 행의 수를 변경하면서 항상 열의 집합을 변경하지 않은 상태로 유지합니다. 그렇다면 어떤 일이 벌어질까요? unnest_longer()는 어떻게 y에 있는 모든 것을 유지하면서 5개의 행을 생성할까요?
df4 |>
unnest_longer(y)
#> # A tibble: 4 × 2
#> x y
#> <chr> <list>
#> 1 a <dbl [1]>
#> 2 b <chr [1]>
#> 3 b <lgl [1]>
#> 4 b <dbl [1]>보시다시피 출력에는 리스트 열이 포함되어 있지만 리스트 열의 모든 요소에는 단일 요소가 포함되어 있습니다. unnest_longer()가 벡터의 공통 유형을 찾을 수 없기 때문에 원래 유형을 리스트 열에 유지합니다. 이것이 모든 열의 요소가 동일한 유형이어야 한다는 계명을 어기는 것인지 궁금할 수 있습니다. 그렇지 않습니다: 내용이 다른 유형이더라도 모든 요소는 리스트입니다.
일관성 없는 유형을 다루는 것은 어렵고 세부 사항은 문제의 정확한 성격과 목표에 달려 있지만, 아마도 Chapter 26 의 도구가 필요할 것입니다.
23.3.4 기타 함수
tidyr에는 이 책에서 다루지 않을 몇 가지 다른 유용한 직사각형화 함수가 있습니다:
-
unnest_auto()는 리스트 열의 구조를 기반으로unnest_longer()와unnest_wider()중에서 자동으로 선택합니다. 빠른 탐색에는 좋지만, 궁극적으로는 데이터가 어떻게 구성되어 있는지 이해하도록 강제하지 않고 코드를 이해하기 어렵게 만들기 때문에 좋지 않은 생각입니다. -
unnest()는 행과 열을 모두 확장합니다. 리스트 열에 데이터 프레임과 같은 2차원 구조가 포함된 경우에 유용합니다. 이 책에서는 보지 못하겠지만 tidymodels 생태계를 사용한다면 만날 수도 있습니다.
이러한 함수들은 다른 사람의 코드를 읽거나 더 드문 직사각형화 문제에 직면했을 때 접할 수 있으므로 알고 있는 것이 좋습니다.
23.3.5 연습문제
df2와 같은 이름이 없는 리스트 열에unnest_wider()를 사용하면 어떻게 됩니까? 이제 어떤 인수가 필요합니까? 결측값은 어떻게 됩니까?df1과 같은 이름이 있는 리스트 열에unnest_longer()를 사용하면 어떻게 됩니까? 출력에서 어떤 추가 정보를 얻습니까? 그 추가 세부 정보를 어떻게 억제할 수 있습니까?-
때때로 값이 정렬된 여러 리스트 열이 있는 데이터 프레임을 만나게 됩니다. 예를 들어 다음 데이터 프레임에서
y와z의 값은 정렬되어 있습니다(즉,y와z는 행 내에서 항상 같은 길이를 가지며,y의 첫 번째 값은z의 첫 번째 값에 해당합니다). 이 데이터 프레임에 두 번의unnest_longer()호출을 적용하면 어떻게 됩니까?x와y사이의 관계를 어떻게 보존할 수 있습니까? (힌트: 문서를 주의 깊게 읽으세요).
23.4 사례 연구
위에서 사용한 간단한 예제와 실제 데이터의 주요 차이점은 실제 데이터에는 일반적으로 여러 번의 unnest_longer() 및/또는 unnest_wider() 호출이 필요한 여러 수준의 중첩이 포함되어 있다는 것입니다. 이를 실제로 보여주기 위해 이 섹션에서는 repurrrsive 패키지의 데이터셋을 사용하여 세 가지 실제 직사각형화 과제를 해결합니다.
23.4.1 매우 넓은 데이터
gh_repos부터 시작하겠습니다. 이것은 GitHub API를 사용하여 검색된 GitHub 저장소 컬렉션에 대한 데이터가 포함된 리스트입니다. 매우 깊게 중첩된 리스트이므로 이 책에서 구조를 보여주기가 어렵습니다. 계속하기 전에 View(gh_repos)로 직접 탐색해 보는 것을 추천합니다.
gh_repos는 리스트이지만 우리의 도구는 리스트 열과 함께 작동하므로 먼저 티블에 넣는 것으로 시작하겠습니다. 나중에 설명할 이유 때문에 이 열을 json이라고 부릅니다.
repos <- tibble(json = gh_repos)
repos
#> # A tibble: 6 × 1
#> json
#> <list>
#> 1 <list [30]>
#> 2 <list [30]>
#> 3 <list [30]>
#> 4 <list [26]>
#> 5 <list [30]>
#> 6 <list [30]>이 티블에는 gh_repos의 각 자식에 대해 하나씩 6개의 행이 포함되어 있습니다. 각 행에는 26개 또는 30개의 행이 있는 이름이 없는 리스트가 포함되어 있습니다. 이들은 이름이 없으므로 각 자식을 자체 행에 넣기 위해 unnest_longer()로 시작하겠습니다:
repos |>
unnest_longer(json)
#> # A tibble: 176 × 1
#> json
#> <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]>
#> # ℹ 170 more rows처음에는 상황이 개선되지 않은 것처럼 보일 수 있습니다: 행은 더 많아졌지만(6개 대신 176개) json의 각 요소는 여전히 리스트입니다. 하지만 중요한 차이점이 있습니다: 이제 각 요소가 이름이 있는 리스트이므로 unnest_wider()를 사용하여 각 요소를 자체 열에 넣을 수 있습니다:
repos |>
unnest_longer(json) |>
unnest_wider(json)
#> # A tibble: 176 × 68
#> id name full_name owner private html_url
#> <int> <chr> <chr> <list> <lgl> <chr>
#> 1 61160198 after gaborcsardi/after <named list> FALSE https://github…
#> 2 40500181 argufy gaborcsardi/argu… <named list> FALSE https://github…
#> 3 36442442 ask gaborcsardi/ask <named list> FALSE https://github…
#> 4 34924886 baseimports gaborcsardi/base… <named list> FALSE https://github…
#> 5 61620661 citest gaborcsardi/cite… <named list> FALSE https://github…
#> 6 33907457 clisymbols gaborcsardi/clis… <named list> FALSE https://github…
#> # ℹ 170 more rows
#> # ℹ 62 more variables: description <chr>, fork <lgl>, url <chr>, …작동은 했지만 결과가 약간 압도적입니다: 열이 너무 많아서 티블이 전부 인쇄하지도 못할 정도입니다! names()로 모두 볼 수 있으며, 여기서는 처음 10개를 살펴봅니다:
repos |>
unnest_longer(json) |>
unnest_wider(json) |>
names() |>
head(10)
#> [1] "id" "name" "full_name" "owner" "private"
#> [6] "html_url" "description" "fork" "url" "forks_url"흥미로워 보이는 것 몇 개를 뽑아보겠습니다:
repos |>
unnest_longer(json) |>
unnest_wider(json) |>
select(id, full_name, owner, description)
#> # A tibble: 176 × 4
#> id full_name owner description
#> <int> <chr> <list> <chr>
#> 1 61160198 gaborcsardi/after <named list [17]> Run Code in the Backgro…
#> 2 40500181 gaborcsardi/argufy <named list [17]> Declarative function ar…
#> 3 36442442 gaborcsardi/ask <named list [17]> Friendly CLI interactio…
#> 4 34924886 gaborcsardi/baseimports <named list [17]> Do we get warnings for …
#> 5 61620661 gaborcsardi/citest <named list [17]> Test R package and repo…
#> 6 33907457 gaborcsardi/clisymbols <named list [17]> Unicode symbols for CLI…
#> # ℹ 170 more rows이것을 사용하여 gh_repos가 어떻게 구성되었는지 거꾸로 이해할 수 있습니다: 각 자식은 자신이 만든 최대 30개의 GitHub 저장소 리스트를 포함하는 GitHub 사용자였습니다.
owner는 또 다른 리스트 열이며 이름이 있는 리스트를 포함하므로 unnest_wider()를 사용하여 값을 가져올 수 있습니다:
repos |>
unnest_longer(json) |>
unnest_wider(json) |>
select(id, full_name, owner, description) |>
unnest_wider(owner)
#> Error in `unnest_wider()`:
#> ! Can't duplicate names between the affected columns and the original
#> data.
#> ✖ These names are duplicated:
#> ℹ `id`, from `owner`.
#> ℹ Use `names_sep` to disambiguate using the column name.
#> ℹ Or use `names_repair` to specify a repair strategy.이런, 이 리스트 열에도 id 열이 포함되어 있고 동일한 데이터 프레임에 두 개의 id 열을 가질 수 없습니다. 제안된 대로 names_sep을 사용하여 문제를 해결해 보겠습니다:
repos |>
unnest_longer(json) |>
unnest_wider(json) |>
select(id, full_name, owner, description) |>
unnest_wider(owner, names_sep = "_")
#> # A tibble: 176 × 20
#> id full_name owner_login owner_id owner_avatar_url
#> <int> <chr> <chr> <int> <chr>
#> 1 61160198 gaborcsardi/after gaborcsardi 660288 https://avatars.gith…
#> 2 40500181 gaborcsardi/argufy gaborcsardi 660288 https://avatars.gith…
#> 3 36442442 gaborcsardi/ask gaborcsardi 660288 https://avatars.gith…
#> 4 34924886 gaborcsardi/baseimports gaborcsardi 660288 https://avatars.gith…
#> 5 61620661 gaborcsardi/citest gaborcsardi 660288 https://avatars.gith…
#> 6 33907457 gaborcsardi/clisymbols gaborcsardi 660288 https://avatars.gith…
#> # ℹ 170 more rows
#> # ℹ 15 more variables: owner_gravatar_id <chr>, owner_url <chr>, …이렇게 하면 또 다른 넓은 데이터셋이 생성되지만, owner에 저장소를 “소유”한 사람에 대한 많은 추가 데이터가 포함되어 있음을 알 수 있습니다.
23.4.2 관계형 데이터
중첩된 데이터는 때때로 우리가 보통 여러 데이터 프레임에 분산시킬 데이터를 나타내는 데 사용됩니다. 예를 들어 왕좌의 게임(Game of Thrones) 책과 TV 시리즈에 등장하는 인물에 대한 데이터가 포함된 got_chars를 예로 들어보겠습니다. gh_repos와 마찬가지로 리스트이므로 먼저 티블의 리스트 열로 변환하는 것으로 시작합니다:
chars <- tibble(json = got_chars)
chars
#> # A tibble: 30 × 1
#> json
#> <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]>
#> # ℹ 24 more rowsjson 열에는 이름이 있는 요소가 포함되어 있으므로 먼저 넓게 만드는 것으로 시작하겠습니다:
chars |>
unnest_wider(json)
#> # A tibble: 30 × 18
#> url id name gender culture born
#> <chr> <int> <chr> <chr> <chr> <chr>
#> 1 https://www.anapio… 1022 Theon Greyjoy Male "Ironborn" "In 278 AC or …
#> 2 https://www.anapio… 1052 Tyrion Lannist… Male "" "In 273 AC, at…
#> 3 https://www.anapio… 1074 Victarion Grey… Male "Ironborn" "In 268 AC or …
#> 4 https://www.anapio… 1109 Will Male "" ""
#> 5 https://www.anapio… 1166 Areo Hotah Male "Norvoshi" "In 257 AC or …
#> 6 https://www.anapio… 1267 Chett Male "" "At Hag's Mire"
#> # ℹ 24 more rows
#> # ℹ 12 more variables: died <chr>, alive <lgl>, titles <list>, …그리고 읽기 쉽게 몇 개의 열을 선택합니다:
characters <- chars |>
unnest_wider(json) |>
select(id, name, gender, culture, born, died, alive)
characters
#> # A tibble: 30 × 7
#> id name gender culture born died
#> <int> <chr> <chr> <chr> <chr> <chr>
#> 1 1022 Theon Greyjoy Male "Ironborn" "In 278 AC or 27… ""
#> 2 1052 Tyrion Lannister Male "" "In 273 AC, at C… ""
#> 3 1074 Victarion Greyjoy Male "Ironborn" "In 268 AC or be… ""
#> 4 1109 Will Male "" "" "In 297 AC, at…
#> 5 1166 Areo Hotah Male "Norvoshi" "In 257 AC or be… ""
#> 6 1267 Chett Male "" "At Hag's Mire" "In 299 AC, at…
#> # ℹ 24 more rows
#> # ℹ 1 more variable: alive <lgl>이 데이터셋에는 많은 리스트 열도 포함되어 있습니다:
chars |>
unnest_wider(json) |>
select(id, where(is.list))
#> # A tibble: 30 × 8
#> id titles aliases allegiances books povBooks tvSeries playedBy
#> <int> <list> <list> <list> <list> <list> <list> <list>
#> 1 1022 <chr [2]> <chr [4]> <chr [1]> <chr [3]> <chr> <chr> <chr>
#> 2 1052 <chr [2]> <chr [11]> <chr [1]> <chr [2]> <chr> <chr> <chr>
#> 3 1074 <chr [2]> <chr [1]> <chr [1]> <chr [3]> <chr> <chr> <chr>
#> 4 1109 <chr [1]> <chr [1]> <NULL> <chr [1]> <chr> <chr> <chr>
#> 5 1166 <chr [1]> <chr [1]> <chr [1]> <chr [3]> <chr> <chr> <chr>
#> 6 1267 <chr [1]> <chr [1]> <NULL> <chr [2]> <chr> <chr> <chr>
#> # ℹ 24 more rowstitles 열을 살펴보겠습니다. 이름이 없는 리스트 열이므로 행으로 중첩 해제합니다:
chars |>
unnest_wider(json) |>
select(id, titles) |>
unnest_longer(titles)
#> # A tibble: 59 × 2
#> id titles
#> <int> <chr>
#> 1 1022 Prince of Winterfell
#> 2 1022 Lord of the Iron Islands (by law of the green lands)
#> 3 1052 Acting Hand of the King (former)
#> 4 1052 Master of Coin (former)
#> 5 1074 Lord Captain of the Iron Fleet
#> 6 1074 Master of the Iron Victory
#> # ℹ 53 more rows이 데이터는 필요에 따라 인물 데이터에 조인하기 쉬울 것이기 때문에 자체 테이블에서 볼 수 있을 것이라고 예상할 수 있습니다. 그렇게 해보겠습니다. 약간의 정리가 필요합니다: 빈 문자열을 포함하는 행을 제거하고, 각 행에 이제 단일 제목만 포함되므로 titles를 title로 이름을 바꿉니다.
titles <- chars |>
unnest_wider(json) |>
select(id, titles) |>
unnest_longer(titles) |>
filter(titles != "") |>
rename(title = titles)
titles
#> # A tibble: 52 × 2
#> id title
#> <int> <chr>
#> 1 1022 Prince of Winterfell
#> 2 1022 Lord of the Iron Islands (by law of the green lands)
#> 3 1052 Acting Hand of the King (former)
#> 4 1052 Master of Coin (former)
#> 5 1074 Lord Captain of the Iron Fleet
#> 6 1074 Master of the Iron Victory
#> # ℹ 46 more rows각 리스트 열에 대해 이와 같은 테이블을 만든 다음, 필요에 따라 조인을 사용하여 인물 데이터와 결합하는 것을 상상할 수 있습니다.
23.4.3 깊게 중첩된 데이터
매우 깊게 중첩되어 있어 반복적인 unnest_wider() 및 unnest_longer() 호출이 필요한 리스트 열인 gmaps_cities로 이 사례 연구를 마무리하겠습니다. 이것은 5개의 도시 이름과 구글의 지오코딩 API를 사용하여 해당 위치를 확인한 결과가 포함된 두 개의 열로 된 티블입니다:
gmaps_cities
#> # A tibble: 5 × 2
#> city json
#> <chr> <list>
#> 1 Houston <named list [2]>
#> 2 Washington <named list [2]>
#> 3 New York <named list [2]>
#> 4 Chicago <named list [2]>
#> 5 Arlington <named list [2]>json은 내부 이름이 있는 리스트 열이므로 unnest_wider()로 시작합니다:
gmaps_cities |>
unnest_wider(json)
#> # A tibble: 5 × 3
#> city results status
#> <chr> <list> <chr>
#> 1 Houston <list [1]> OK
#> 2 Washington <list [2]> OK
#> 3 New York <list [1]> OK
#> 4 Chicago <list [1]> OK
#> 5 Arlington <list [2]> OK이렇게 하면 status와 results를 얻게 됩니다. 상태가 모두 OK이므로 상태 열은 삭제하겠습니다. 실제 분석에서는 status != "OK"인 모든 행을 캡처하고 무엇이 잘못되었는지 파악하고 싶을 것입니다. results는 하나 또는 두 개의 요소를 가진 이름이 없는 리스트이므로(이유는 곧 알게 될 것입니다) 행으로 중첩 해제합니다:
gmaps_cities |>
unnest_wider(json) |>
select(-status) |>
unnest_longer(results)
#> # A tibble: 7 × 2
#> city results
#> <chr> <list>
#> 1 Houston <named list [5]>
#> 2 Washington <named list [5]>
#> 3 Washington <named list [5]>
#> 4 New York <named list [5]>
#> 5 Chicago <named list [5]>
#> 6 Arlington <named list [5]>
#> # ℹ 1 more row이제 results는 이름이 있는 리스트이므로 unnest_wider()를 사용합니다:
locations <- gmaps_cities |>
unnest_wider(json) |>
select(-status) |>
unnest_longer(results) |>
unnest_wider(results)
locations
#> # A tibble: 7 × 6
#> city address_components formatted_address geometry
#> <chr> <list> <chr> <list>
#> 1 Houston <list [4]> Houston, TX, USA <named list [4]>
#> 2 Washington <list [2]> Washington, USA <named list [4]>
#> 3 Washington <list [4]> Washington, DC, USA <named list [4]>
#> 4 New York <list [3]> New York, NY, USA <named list [4]>
#> 5 Chicago <list [4]> Chicago, IL, USA <named list [4]>
#> 6 Arlington <list [4]> Arlington, TX, USA <named list [4]>
#> # ℹ 1 more row
#> # ℹ 2 more variables: place_id <chr>, types <list>이제 왜 두 도시가 두 개의 결과를 얻었는지 알 수 있습니다: Washington은 워싱턴 주와 워싱턴 DC 모두와 일치했고, Arlington은 버지니아 주 알링턴과 텍사스 주 알링턴과 일치했습니다.
여기서부터 갈 수 있는 몇 가지 다른 방향이 있습니다. geometry 리스트 열에 저장된 일치 항목의 정확한 위치를 확인하고 싶을 수 있습니다:
locations |>
select(city, formatted_address, geometry) |>
unnest_wider(geometry)
#> # A tibble: 7 × 6
#> city formatted_address bounds location location_type
#> <chr> <chr> <list> <list> <chr>
#> 1 Houston Houston, TX, USA <named list [2]> <named list> APPROXIMATE
#> 2 Washington Washington, USA <named list [2]> <named list> APPROXIMATE
#> 3 Washington Washington, DC, USA <named list [2]> <named list> APPROXIMATE
#> 4 New York New York, NY, USA <named list [2]> <named list> APPROXIMATE
#> 5 Chicago Chicago, IL, USA <named list [2]> <named list> APPROXIMATE
#> 6 Arlington Arlington, TX, USA <named list [2]> <named list> APPROXIMATE
#> # ℹ 1 more row
#> # ℹ 1 more variable: viewport <list>그러면 새로운 bounds(직사각형 영역)와 location(점)을 얻게 됩니다. location을 중첩 해제하여 위도(lat)와 경도(lng)를 볼 수 있습니다:
locations |>
select(city, formatted_address, geometry) |>
unnest_wider(geometry) |>
unnest_wider(location)
#> # A tibble: 7 × 7
#> city formatted_address bounds lat lng location_type
#> <chr> <chr> <list> <dbl> <dbl> <chr>
#> 1 Houston Houston, TX, USA <named list [2]> 29.8 -95.4 APPROXIMATE
#> 2 Washington Washington, USA <named list [2]> 47.8 -121. APPROXIMATE
#> 3 Washington Washington, DC, USA <named list [2]> 38.9 -77.0 APPROXIMATE
#> 4 New York New York, NY, USA <named list [2]> 40.7 -74.0 APPROXIMATE
#> 5 Chicago Chicago, IL, USA <named list [2]> 41.9 -87.6 APPROXIMATE
#> 6 Arlington Arlington, TX, USA <named list [2]> 32.7 -97.1 APPROXIMATE
#> # ℹ 1 more row
#> # ℹ 1 more variable: viewport <list>경계(bounds)를 추출하려면 몇 단계가 더 필요합니다:
locations |>
select(city, formatted_address, geometry) |>
unnest_wider(geometry) |>
# 관심 있는 변수에 집중
select(!location:viewport) |>
unnest_wider(bounds)
#> # A tibble: 7 × 4
#> city formatted_address northeast southwest
#> <chr> <chr> <list> <list>
#> 1 Houston Houston, TX, USA <named list [2]> <named list [2]>
#> 2 Washington Washington, USA <named list [2]> <named list [2]>
#> 3 Washington Washington, DC, USA <named list [2]> <named list [2]>
#> 4 New York New York, NY, USA <named list [2]> <named list [2]>
#> 5 Chicago Chicago, IL, USA <named list [2]> <named list [2]>
#> 6 Arlington Arlington, TX, USA <named list [2]> <named list [2]>
#> # ℹ 1 more row그런 다음 southwest와 northeast(직사각형의 모서리)의 이름을 변경하여 names_sep을 사용하여 짧으면서도 기억하기 쉬운 이름을 만들 수 있습니다:
locations |>
select(city, formatted_address, geometry) |>
unnest_wider(geometry) |>
select(!location:viewport) |>
unnest_wider(bounds) |>
rename(ne = northeast, sw = southwest) |>
unnest_wider(c(ne, sw), names_sep = "_")
#> # A tibble: 7 × 6
#> city formatted_address ne_lat ne_lng sw_lat sw_lng
#> <chr> <chr> <dbl> <dbl> <dbl> <dbl>
#> 1 Houston Houston, TX, USA 30.1 -95.0 29.5 -95.8
#> 2 Washington Washington, USA 49.0 -117. 45.5 -125.
#> 3 Washington Washington, DC, USA 39.0 -76.9 38.8 -77.1
#> 4 New York New York, NY, USA 40.9 -73.7 40.5 -74.3
#> 5 Chicago Chicago, IL, USA 42.0 -87.5 41.6 -87.9
#> 6 Arlington Arlington, TX, USA 32.8 -97.0 32.6 -97.2
#> # ℹ 1 more rowunnest_wider()에 변수 이름 벡터를 제공하여 두 열을 동시에 중첩 해제하는 방법에 유의하세요.
관심 있는 구성 요소에 도달하는 경로를 발견했다면 또 다른 tidyr 함수인 hoist()를 사용하여 직접 추출할 수 있습니다:
이러한 사례 연구가 실제 직사각형화에 대한 흥미를 유발했다면 vignette("rectangling", package = "tidyr")에서 몇 가지 예제를 더 볼 수 있습니다.
23.4.4 연습문제
gh_repos가 언제 생성되었는지 대략적으로 추정해 보세요. 왜 날짜를 대략적으로만 추정할 수 있습니까?각 소유자가 많은 저장소를 가질 수 있기 때문에
gh_repo의owner열에는 중복된 정보가 많이 포함되어 있습니다. 각 소유자에 대해 하나의 행을 포함하는owners데이터 프레임을 구성할 수 있습니까? (힌트:distinct()가list-cols와 함께 작동합니까?)titles에 사용된 단계를 따라 왕좌의 게임 인물들의 별칭(aliases), 충성(allegiances), 책(books) 및 TV 시리즈(TV series)에 대한 유사한 테이블을 만드세요.-
다음 코드를 한 줄씩 설명하세요. 왜 흥미롭습니까? 왜
got_chars에서는 작동하지만 일반적으로는 작동하지 않을 수 있습니까?tibble(json = got_chars) |> unnest_wider(json) |> select(id, where(is.list)) |> pivot_longer( where(is.list), names_to = "name", values_to = "value" ) |> unnest_longer(value) gmaps_cities에서address_components에는 무엇이 들어 있습니까? 왜 행마다 길이가 다릅니까? 그것을 파악하기 위해 적절하게 중첩 해제하세요. (힌트:types는 항상 두 개의 요소를 포함하는 것으로 보입니다.unnest_longer()보다unnest_wider()가 작업하기 더 쉬운가요?)
23.5 JSON
이전 섹션의 모든 사례 연구는 야생에서 수집된 JSON에서 가져온 것입니다. JSON은 javascript object notation의 약자로 대부분의 웹 API가 데이터를 반환하는 방식입니다. JSON과 R의 데이터 유형은 꽤 비슷하지만 완벽한 1대1 매핑은 아니므로 문제가 발생할 경우를 대비해 JSON에 대해 조금 이해하는 것이 중요합니다.
23.5.1 데이터 유형
JSON은 인간이 아니라 기계가 쉽게 읽고 쓸 수 있도록 설계된 간단한 형식입니다. 6가지 주요 데이터 유형이 있습니다. 그중 4개는 스칼라(scalar)입니다:
- 가장 간단한 유형은 null(
null)로 R의NA와 같은 역할을 합니다. 데이터의 부재를 나타냅니다. - 문자열(string) 은 R의 문자열과 매우 유사하지만 항상 큰따옴표를 사용해야 합니다.
-
숫자(number) 는 R의 숫자와 유사합니다: 정수(예: 123), 소수(예: 123.45) 또는 지수(예: 1.23e3) 표기법을 사용할 수 있습니다. JSON은
Inf,-Inf또는NaN을 지원하지 않습니다. -
불리언(boolean) 은 R의
TRUE및FALSE와 유사하지만 소문자true및false를 사용합니다.
JSON의 문자열, 숫자 및 불리언은 R의 문자형, 수치형 및 논리형 벡터와 매우 유사합니다. 주요 차이점은 JSON의 스칼라는 단일 값만 나타낼 수 있다는 것입니다. 여러 값을 나타내려면 나머지 두 가지 유형인 배열과 객체 중 하나를 사용해야 합니다.
배열과 객체는 모두 R의 리스트와 유사합니다. 차이점은 이름이 있는지 여부입니다. 배열(array) 은 이름이 없는 리스트와 같으며 []로 씁니다. 예를 들어 [1, 2, 3]은 3개의 숫자를 포함하는 배열이고, [null, 1, "string", false]는 null, 숫자, 문자열 및 불리언을 포함하는 배열입니다. 객체(object) 는 이름이 있는 리스트와 같으며 {}로 씁니다. 이름(JSON 용어로는 키)은 문자열이므로 따옴표로 둘러싸야 합니다. 예를 들어 {"x": 1, "y": 2}는 x를 1에, y를 2에 매핑하는 객체입니다.
JSON에는 날짜나 날짜-시간을 나타내는 기본 방법이 없으므로 종종 문자열로 저장되며, 올바른 데이터 구조로 변환하려면 readr::parse_date() 또는 readr::parse_datetime()을 사용해야 합니다. 마찬가지로 JSON에서 부동 소수점 숫자를 나타내는 규칙은 약간 부정확하므로 때때로 문자열에 저장된 숫자를 발견할 수도 있습니다. 올바른 변수 유형을 얻기 위해 필요에 따라 readr::parse_double()을 적용하세요.
23.5.2 jsonlite
JSON을 R 데이터 구조로 변환하려면 Jeroen Ooms가 만든 jsonlite 패키지를 권장합니다. 여기서는 두 가지 jsonlite 함수인 read_json()과 parse_json()만 사용합니다. 실제 생활에서는 디스크에서 JSON 파일을 읽기 위해 read_json()을 사용하게 될 것입니다. 예를 들어 repurrrsive 패키지는 gh_user에 대한 소스를 JSON 파일로 제공하며 read_json()으로 읽을 수 있습니다:
# 패키지 내부의 json 파일 경로:
gh_users_json()
#> [1] "/Users/jinhwan/Library/R/arm64/4.5/library/repurrrsive/extdata/gh_users.json"
# read_json()으로 읽기
gh_users2 <- read_json(gh_users_json())
# 이전에 사용하던 데이터와 동일한지 확인
identical(gh_users, gh_users2)
#> [1] TRUE이 책에서는 parse_json()도 사용하는데, JSON을 포함하는 문자열을 인수로 받기 때문에 간단한 예제를 생성하는 데 좋기 때문입니다. 시작하기 위해 숫자 하나로 시작하여 배열에 몇 개의 숫자를 넣고, 그 배열을 객체에 넣는 세 가지 간단한 JSON 데이터셋이 있습니다:
str(parse_json('1'))
#> int 1
str(parse_json('[1, 2, 3]'))
#> List of 3
#> $ : int 1
#> $ : int 2
#> $ : int 3
str(parse_json('{"x": [1, 2, 3]}'))
#> List of 1
#> $ x:List of 3
#> ..$ : int 1
#> ..$ : int 2
#> ..$ : int 3jsonlite에는 fromJSON()이라는 또 다른 중요한 함수가 있습니다. 여기서는 사용하지 않는데, 자동 단순화(simplifyVector = TRUE)를 수행하기 때문입니다. 이것은 특히 간단한 경우에 잘 작동하지만, 무슨 일이 일어나는지 정확히 알 수 있고 가장 복잡한 중첩 구조를 더 쉽게 처리할 수 있도록 직접 직사각형화를 수행하는 것이 더 낫다고 생각합니다.
23.5.3 직사각형화 프로세스 시작하기
대부분의 경우 JSON 파일은 최상위 배열 하나를 포함합니다. 여러 “것”(예: 여러 페이지, 여러 레코드 또는 여러 결과)에 대한 데이터를 제공하도록 설계되었기 때문입니다. 이 경우 각 요소가 행이 되도록 tibble(json)으로 직사각형화를 시작합니다:
json <- '[
{"name": "John", "age": 34},
{"name": "Susan", "age": 27}
]'
df <- tibble(json = parse_json(json))
df
#> # A tibble: 2 × 1
#> json
#> <list>
#> 1 <named list [2]>
#> 2 <named list [2]>
df |>
unnest_wider(json)
#> # A tibble: 2 × 2
#> name age
#> <chr> <int>
#> 1 John 34
#> 2 Susan 27더 드문 경우로, JSON 파일이 하나의 “것”을 나타내는 최상위 JSON 객체 하나로 구성되는 경우가 있습니다. 이 경우 티블에 넣기 전에 리스트로 감싸서 직사각형화 프로세스를 시작해야 합니다.
json <- '{
"status": "OK",
"results": [
{"name": "John", "age": 34},
{"name": "Susan", "age": 27}
]
}
'
df <- tibble(json = list(parse_json(json)))
df
#> # A tibble: 1 × 1
#> json
#> <list>
#> 1 <named list [2]>
df |>
unnest_wider(json) |>
unnest_longer(results) |>
unnest_wider(results)
#> # A tibble: 2 × 3
#> status name age
#> <chr> <chr> <int>
#> 1 OK John 34
#> 2 OK Susan 27대안으로 파싱된 JSON 내부를 파고들어 실제로 관심 있는 부분부터 시작할 수 있습니다:
df <- tibble(results = parse_json(json)$results)
df |>
unnest_wider(results)
#> # A tibble: 2 × 2
#> name age
#> <chr> <int>
#> 1 John 34
#> 2 Susan 2723.5.4 연습문제
-
아래의
df_col과df_row를 직사각형화하세요. 이들은 JSON에서 데이터 프레임을 인코딩하는 두 가지 방법을 나타냅니다.json_col <- parse_json(' { "x": ["a", "x", "z"], "y": [10, null, 3] } ') json_row <- parse_json(' [ {"x": "a", "y": 10}, {"x": "x", "y": null}, {"x": "z", "y": 3} ] ') df_col <- tibble(json = list(json_col)) df_row <- tibble(json = json_row)
23.6 요약
이 장에서는 리스트가 무엇인지, JSON 파일에서 어떻게 리스트를 생성할 수 있는지, 그리고 그것들을 어떻게 직사각형 데이터 프레임으로 변환하는지 배웠습니다. 놀랍게도 리스트 요소를 행에 넣기 위한 unnest_longer()와 리스트 요소를 열에 넣기 위한 unnest_wider()라는 두 개의 새로운 함수만 필요했습니다. 리스트 열이 얼마나 깊게 중첩되어 있는지는 중요하지 않습니다. 이 두 함수를 반복해서 호출하기만 하면 됩니다.
JSON은 웹 API가 반환하는 가장 일반적인 데이터 형식입니다. 웹사이트에 API가 없지만 웹사이트에서 원하는 데이터를 볼 수 있다면 어떻게 될까요? 그것이 다음 장의 주제입니다: 웹 스크래핑, 즉 HTML 웹페이지에서 데이터를 추출하는 것입니다.
이것은 RStudio 기능입니다.↩︎