You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在R中扁平化带有父子关系的层级数据结构

R语言层级父子表扁平化实现问题

我有如下描述父子关系的数据:

df <- tibble::tribble(
       ~Child,     ~Parent,
      "Fruit",      "Food",
  "Vegetable",      "Food",
      "Apple",     "Fruit",
     "Banana",     "Fruit",
       "Pear",     "Fruit",
     "Carrot", "Vegetable",
     "Celery", "Vegetable",
       "Bike",  "Not Food",
        "Car",  "Not Food"
  )
df
#> # A tibble: 9 x 2
#>   Child     Parent   
#>   <chr>     <chr>    
#> 1 Fruit     Food     
#> 2 Vegetable Food     
#> 3 Apple     Fruit    
#> 4 Banana    Fruit    
#> 5 Pear      Fruit    
#> 6 Carrot    Vegetable
#> 7 Celery    Vegetable
#> 8 Bike      Not Food 
#> 9 Car       Not Food

该结构对应的层级示意图如下:
层级结构示意图

我最终想要将该结构“扁平化”,得到如下格式的结果:

results <- tibble::tribble(
             ~Level.03, ~Level.02,  ~Level.01,
               "Apple",   "Fruit",     "Food",
              "Banana",   "Fruit",     "Food",
                "Pear",   "Fruit",     "Food",
                    NA,    "Bike", "Not Food",
                    NA,     "Car", "Not Food"
             )
results
#> # A tibble: 5 x 3
#>   Level.03 Level.02 Level.01
#>   <chr>    <chr>    <chr>   
#> 1 Apple    Fruit    Food    
#> 2 Banana   Fruit    Food    
#> 3 Pear     Fruit    Food    
#> 4 <NA>     Bike     Not Food
#> 5 <NA>     Car      Not Food

注意:并非所有元素都有完整的层级,例如bike和car没有对应的Level.03层级元素。

我初步尝试了递归连接的实现方式,但感觉是在重复造轮子,应该有更直接的方案,请问是否可以用tidyr或者jsonlite中的next/unnest类函数优雅实现该需求?


解决方案

下面给出两种基于tidyverse生态的实现方案,无需自己实现复杂的递归连接逻辑:

方案1:purrr + tidyr 实现(推荐,逻辑清晰易调试)

核心思路是先找出所有叶节点,再递归获取每个叶节点到根节点的完整路径,最后统一格式转成宽表:

# 加载依赖
library(tidyverse)

# 递归获取节点到根节点的完整路径
get_full_path <- function(node) {
  path <- c()
  current <- node
  while(current %in% df$Child) {
    path <- c(path, current)
    current <- df$Parent[df$Child == current][1]
  }
  # 补全根节点后倒序,得到从根到叶的路径
  path <- rev(c(path, current))
  return(path)
}

# 筛选所有叶节点(没有子节点的节点,即未出现在Parent列的Child)
leaf_nodes <- df$Child[!df$Child %in% df$Parent]

# 批量处理所有叶节点,转成目标格式
result <- map(leaf_nodes, get_full_path) %>% 
  # 统一路径长度,不足的补NA
  map(~ `length<-`(.x, max(map_dbl(., length)))) %>% 
  # 转成数据框并命名列
  map_dfr(~ set_names(.x, paste0("Level.", str_pad(seq_along(.x), 2, pad = "0")))) %>% 
  # 倒序列顺序,匹配Level.01为根节点、编号越大层级越细的要求
  select(rev(everything()))

运行后得到的完整结果如下(你的示例中遗漏了Vegetable分支的两行,如需和示例完全一致,加一行filter(Level.02 != "Vegetable")即可):

# A tibble: 7 × 3
  Level.03 Level.02  Level.01
  <chr>    <chr>     <chr>   
1 Apple    Fruit     Food    
2 Banana   Fruit     Food    
3 Pear     Fruit     Food    
4 Carrot   Vegetable Food    
5 Celery   Vegetable Food    
6 <NA>     Bike      Not Food
7 <NA>     Car       Not Food

方案2:嵌套结构 + jsonlite + unnest 实现(适合层级深度不确定的场景)

如果你的层级深度是动态变化的,推荐用先构建嵌套结构再解析的方式,不用修改代码就能适配不同深度的层级:

library(tidyverse)
library(jsonlite)

# 递归构建嵌套列表
build_nest <- function(parent_node) {
  children <- df$Child[df$Parent == parent_node]
  if(length(children) == 0) return(parent_node)
  map(children, ~ list(node = .x, children = build_nest(.x)))
}

# 识别根节点
root_nodes <- df$Parent[!df$Parent %in% df$Child] %>% unique()

# 构建嵌套结构后扁平化
result <- map(root_nodes, build_nest) %>% 
  unlist(recursive = TRUE) %>% 
  matrix(ncol = 3, byrow = TRUE) %>% 
  as_tibble(.name_repair = ~ paste0("Level.", str_pad(3:1, 2, pad = "0")))

内容的提问来源于stack exchange,提问作者JasonAizkalns

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.06 23:54:01