18  分层数据-Hierarchical data

数据矩形化(data rectangling)技术:将具有层次结构或树状结构的数据转换为由行和列组成的矩形数据框。这项技术在当下数据科学的大环境显得尤为重要,因为层次化数据在实际应用中极为常见,特别是用于处理网络数据。

library(tidyverse)
library(repurrrsive)  # 练习数据集
library(jsonlite) # JSON文件读取为R列表

18.1 列表

如果想在同一个向量中存储不同类型的元素,就需要使用列表(list),可以通过list()创建。

x1 <- list(1:4, "a", TRUE)
x1
[[1]]
[1] 1 2 3 4

[[2]]
[1] "a"

[[3]]
[1] TRUE
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
str(x1)  # 使用紧凑的结构输出列表
List of 3
 $ : int [1:4] 1 2 3 4
 $ : chr "a"
 $ : logi TRUE

18.1.1 层级结构

列表中可以包含其他列表,即所谓的层级结构

x3 <- list(list(1, 2), list(3, 4))
str(x3)
List of 2
 $ :List of 2
  ..$ : num 1
  ..$ : num 2
 $ :List of 2
  ..$ : num 3
  ..$ : num 4

随着列表复杂程度的提升,str()会越来越有用,但如果列表变得过于庞大,使用View()更为合适。但这个功能仅在Rstudio中可以使用。

18.1.2 列表列

  • 列表也可以存在于 tibble 中,称为列表列(list-columns)。其实列表列没什么特别之处,其操作与其他任何类型的列相同。
  • 列表列在 tidymodels 中被广泛的使用。这本笔记中的第七部分也是tidymodels的相关内容。
# 生成列表列
df <- tibble(
  x = 1:2, 
  y = c("a", "b"),
  z = list(list(1, 2), list(3, 4, 5))
)
df
# A tibble: 2 × 3
      x y     z         
  <int> <chr> <list>    
1     1 a     <list [2]>
2     2 b     <list [3]>
# 提取并查看列表列
df |> 
  pull(z) |> # pull()的作用与$符号类似
  str()
List of 2
 $ :List of 2
  ..$ : num 1
  ..$ : num 2
 $ :List of 3
  ..$ : num 3
  ..$ : num 4
  ..$ : num 5

18.2 解除嵌套

列表列有些像嵌套结构,在列中再嵌套一层数据,但这样不利于我们分析和观察。那么如何将列表列转换为常规的行列结构呢?

列表列通常有两种基本形式:命名的未命名的

  • 被命名的子元素通常在各行中具有相同的名称,在下例df1中,列表列y的每个元素都有两个名为a和b的子元素。
    • 使用unnest_wider()将列表列各元素按名称拆开分列。
    • 命名的列表列在解除嵌套后,每个命名的元素均会构成一个新列。
df1 <- tribble(
  ~x , ~y                   ,
   1 , list(a = 11, b = 12) ,
   2 , list(a = 21, b = 22) ,
   3 , list(a = 31, b = 32) ,
)

df1 |>
  unnest_wider(
    col = 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
  • 若子元素未被命名,元素数量通常在不同行间会有所变化,在下例df2中,列表列y的元素未被命名且长度从一到三不等。

    • 使用unnest_longer()进行拆分。
    • 未命名的列表列解除嵌套后,每个子元素会生成单独的一行
    • 如果列表列的其中列没有元素,则在解除嵌套后不显示输出。如果想保留该行,也就是在y中保留NA,则需设置参数keep_empty = TRUE
df2 <- tribble(
  ~x , ~y               ,
   1 , list(11, 12, 13) ,
   2 , list(21)         ,
   3 , list(31, 32)     ,
   4 , list()
)
df2
# A tibble: 4 × 2
      x y         
  <dbl> <list>    
1     1 <list [3]>
2     2 <list [1]>
3     3 <list [2]>
4     4 <list [0]>
df2 |>
  unnest_longer(
    col = y,
    keep_empty = T
  )
# A tibble: 7 × 2
      x     y
  <dbl> <dbl>
1     1    11
2     1    12
3     1    13
4     2    21
5     3    31
6     3    32
7     4    NA

处理列表列,尤其是包含不同类型向量的列表列是一个很大的挑战,我们通常需要使用 章节 21 中的purrr工具。

18.3 案例研究

简单例子与真实数据的主要区别在于,真实数据通常包含多个层次嵌套,需要多次调用 unnest_longer() 和/或 unnest_wider()。为了展示这一点,本节利用 repurrrsive 软件包中的数据集,解决三个真实的矩形难题。

18.3.1 非常宽的数据

gh_repos 数据集是一个包含通过 GitHub API 检索的 GitHub 仓库集合的数据列表。这是一个非常深层嵌套的列表,因此很难展示其结构。

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]>
# 未命名列表列,使用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]>
 7 <named list [68]>
 8 <named list [68]>
 9 <named list [68]>
10 <named list [68]>
# ℹ 166 more rows
# 使用unnest_wider()
repos |> 
  unnest_longer(json) |> 
  unnest_wider(json)
# A tibble: 176 × 68
        id name  full_name owner        private html_url description fork  url  
     <int> <chr> <chr>     <list>       <lgl>   <chr>    <chr>       <lgl> <chr>
 1  6.12e7 after gaborcsa… <named list> FALSE   https:/… Run Code i… FALSE http…
 2  4.05e7 argu… gaborcsa… <named list> FALSE   https:/… Declarativ… FALSE http…
 3  3.64e7 ask   gaborcsa… <named list> FALSE   https:/… Friendly C… FALSE http…
 4  3.49e7 base… gaborcsa… <named list> FALSE   https:/… Do we get … FALSE http…
 5  6.16e7 cite… gaborcsa… <named list> FALSE   https:/… Test R pac… TRUE  http…
 6  3.39e7 clis… gaborcsa… <named list> FALSE   https:/… Unicode sy… FALSE http…
 7  3.72e7 cmak… gaborcsa… <named list> FALSE   https:/… port of cm… TRUE  http…
 8  6.80e7 cmark gaborcsa… <named list> FALSE   https:/… CommonMark… TRUE  http…
 9  6.32e7 cond… gaborcsa… <named list> FALSE   https:/… <NA>        TRUE  http…
10  2.43e7 cray… gaborcsa… <named list> FALSE   https:/… R package … FALSE http…
# ℹ 166 more rows
# ℹ 59 more variables: forks_url <chr>, keys_url <chr>,
#   collaborators_url <chr>, teams_url <chr>, hooks_url <chr>,
#   issue_events_url <chr>, events_url <chr>, assignees_url <chr>,
#   branches_url <chr>, tags_url <chr>, blobs_url <chr>, git_tags_url <chr>,
#   git_refs_url <chr>, trees_url <chr>, statuses_url <chr>,
#   languages_url <chr>, stargazers_url <chr>, contributors_url <chr>, …

以上的输出结构仍然非常复杂,因为有很多列包含嵌套的列表,且仍包含很多冗余的列。

  • 先从现有列中挑选部分我们需要的列。
  • 如果有需要,可以继续使用unnest_wider()来展开这些列,直到获得一个纯粹的矩形数据框。
repos_unnested <- repos |>
  unnest_longer(json) |>
  unnest_wider(json) |>
  select(id, full_name, owner, description)
repos_unnested
# A tibble: 176 × 4
         id full_name               owner             description               
      <int> <chr>                   <list>            <chr>                     
 1 61160198 gaborcsardi/after       <named list [17]> Run Code in the Background
 2 40500181 gaborcsardi/argufy      <named list [17]> Declarative function argu…
 3 36442442 gaborcsardi/ask         <named list [17]> Friendly CLI interaction …
 4 34924886 gaborcsardi/baseimports <named list [17]> Do we get warnings for un…
 5 61620661 gaborcsardi/citest      <named list [17]> Test R package and repo f…
 6 33907457 gaborcsardi/clisymbols  <named list [17]> Unicode symbols for CLI a…
 7 37236467 gaborcsardi/cmaker      <named list [17]> port of cmake to r        
 8 67959624 gaborcsardi/cmark       <named list [17]> CommonMark parsing and re…
 9 63152619 gaborcsardi/conditions  <named list [17]> <NA>                      
10 24343686 gaborcsardi/crayon      <named list [17]> R package for colored ter…
# ℹ 166 more rows
# 继续使用unnest_wider()
repos_unnested <- repos_unnested |>
  unnest_wider(
    col = owner,
    names_sep = "_"
  )
repos_unnested
# A tibble: 176 × 20
         id full_name    owner_login owner_id owner_avatar_url owner_gravatar_id
      <int> <chr>        <chr>          <int> <chr>            <chr>            
 1 61160198 gaborcsardi… gaborcsardi   660288 https://avatars… ""               
 2 40500181 gaborcsardi… gaborcsardi   660288 https://avatars… ""               
 3 36442442 gaborcsardi… gaborcsardi   660288 https://avatars… ""               
 4 34924886 gaborcsardi… gaborcsardi   660288 https://avatars… ""               
 5 61620661 gaborcsardi… gaborcsardi   660288 https://avatars… ""               
 6 33907457 gaborcsardi… gaborcsardi   660288 https://avatars… ""               
 7 37236467 gaborcsardi… gaborcsardi   660288 https://avatars… ""               
 8 67959624 gaborcsardi… gaborcsardi   660288 https://avatars… ""               
 9 63152619 gaborcsardi… gaborcsardi   660288 https://avatars… ""               
10 24343686 gaborcsardi… gaborcsardi   660288 https://avatars… ""               
# ℹ 166 more rows
# ℹ 14 more variables: owner_url <chr>, owner_html_url <chr>,
#   owner_followers_url <chr>, owner_following_url <chr>,
#   owner_gists_url <chr>, owner_starred_url <chr>,
#   owner_subscriptions_url <chr>, owner_organizations_url <chr>,
#   owner_repos_url <chr>, owner_events_url <chr>,
#   owner_received_events_url <chr>, owner_type <chr>, …

以上repos_unnested虽然仍是一个较大的数据集,但已经没有嵌套的列表列,数据结构也清晰了不少。

18.3.2 关系型数据

嵌套数据有时用来表示我们通常会分散在多个数据帧上的数据。以 got_chars 为例,它包含了《权力的游戏》书籍和电视剧中出现角色的数据。

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]>
 7 <named list [18]>
 8 <named list [18]>
 9 <named list [18]>
10 <named list [18]>
# ℹ 20 more rows
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                                             
 7  1166 Captain of the Guard at Sunspear                                       
 8  1295 Maester                                                                
 9   130 Princess of Dorne                                                      
10  1303 Queen of the Andals and the Rhoynar and the First Men, Lord of the Sev…
# ℹ 42 more rows

我们可以为每个列表列创建一个类似titles的表,然后根据需要使用联接将它们与字符数据组合起来。

18.3.3 深度嵌套

locations <- gmaps_cities |>
  unnest_wider(json) |>
  select(-status) |> # 此列均为OK,在实际工作中,我们需要分析不为OK的行。
  unnest_longer(results) |> 
  unnest_wider(results)
locations
# A tibble: 7 × 6
  city       address_components formatted_address   geometry     place_id types 
  <chr>      <list>             <chr>               <list>       <chr>    <list>
1 Houston    <list [4]>         Houston, TX, USA    <named list> ChIJAYW… <list>
2 Washington <list [2]>         Washington, USA     <named list> ChIJ-bD… <list>
3 Washington <list [4]>         Washington, DC, USA <named list> ChIJW-T… <list>
4 New York   <list [3]>         New York, NY, USA   <named list> ChIJOwg… <list>
5 Chicago    <list [4]>         Chicago, IL, USA    <named list> ChIJ7cv… <list>
6 Arlington  <list [4]>         Arlington, TX, USA  <named list> ChIJ05g… <list>
7 Arlington  <list [4]>         Arlington, VA, USA  <named list> ChIJD6e… <list>
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
7 Arlington  Arlington, VA, USA    38.9  -77.0   38.8  -77.2

vignette("rectangling", package = "tidyr") 中看到更多例子。