☰
R语言绘制环形树状图:从层次聚类到ggtree可视化实操
2026/10/5 4:02:52 网站建设 项目流程

1. 环形树状图到底能干什么,什么场景下值得用

我第一次看到环形树状图,是在一次物种多样性分析的报告里。满屏线性树状图看得人头晕,突然出现一张圆形的、像年轮一样层层展开的图,信息一目了然。当时的第一反应是“这图真漂亮”,第二反应是“这到底是怎么画出来的”。后来自己用R折腾了一圈,才明白环形树状图(circular dendrogram)本质上并不是一种新的统计方法,而是层次聚类结果的一种极坐标可视化形式。它把传统的、从上往下或从左往右伸展的树状图,绕着圆心重新排版,让所有叶子节点均匀分布在圆周上,内部节点向圆心方向汇拢。

这个布局的实用价值非常大。普通线性树状图在叶子节点数量超过30个之后,横向宽度会变得不可控,要么压缩到看不清标签,要么横向滚动条拖到手酸。环形布局等于把一维的展开方向变成了二维的圆周方向,同样一个平面空间能容纳的叶子数量呈几何级增长。所以它特别适合展示基因表达聚类、菌群物种组成、问卷题项分类、产品功能分组这类叶子多、层次结构清晰的数据。R作为统计分析领域最常用的语言,做这种图有几套完全不同的方案,从“一行代码出图”到“全手动ggplot2定制”,丰俭由人。这篇文章我会把主流的几条路线都过一遍,讲清楚它们各自的原理、适用场景,以及我实际使用时踩过的坑。

但我也要先泼一盆冷水:环形树状图不是万能的。如果叶子节点只有十几个,线性树状图更直观;如果数据本身没有明显的层次结构,强行聚类画环形图只会得到一张“什么都说明不了但确实对称”的图。判断用不用环形布局,最简单的一个标准是:你的叶子节点数量是否超过30个,以及你是否需要强调样本之间的远近关系而不是精确的分支长度。这两个条件都满足,环形树状图就是最佳选择。

2. 开工前的数据准备:从原始矩阵到一棵可画的树

2.1 搞清楚你的数据离“树”还有几步

很多R初学者拿到数据的第一反应是找“画环形图的函数”,但环形树状图的输入是树形结构,不是数据框。所以核心问题在于:你的原始数据得先走完“数据矩阵 → 距离矩阵 → 层次聚类 → 树形对象”这条流水线,才能送到绘图函数里。

假设你现在有一份20个样本、10个特征的数据矩阵,行是样本(或者叫观测对象),列是特征。我想要知道这些样本之间的亲疏关系,那第一步就是计算两两之间的距离。R里面这一步极其简单,dist()函数默认计算欧氏距离,也可以用method参数切换为"manhattan"、"minkowski"等。第二步是层次聚类,hclust()基于距离矩阵做凝聚式聚类,默认是"complete"方法。这两步一过,你就得到一棵完整的层次树,它能告诉我们哪些样本先抱团、哪些样本最后才合并。

set.seed(42) mat <- matrix(rnorm(200), nrow = 20, dimnames = list(paste0("Sample", 1:20), paste0("Feature", 1:10))) # 第一步:算距离 d <- dist(mat, method = "euclidean") # 第二步:层次聚类 hc <- hclust(d, method = "ward.D2")

注意,这里的hc是hclust类的对象,绝大多数绘图包不能直接识别,后面要转换。还有一点必须强调:dist()默认按行计算距离,也就是说每一行是一个样本、每一列是一个特征。如果你的原始数据刚好是反的(行是特征、列是样本),记得先转置,否则算出来的树描述的是特征之间的相似性,而不是样本之间的,图上标的全是反的。

2.2 聚类方法选不对,图再好看也是错的

我见过太多人忽略这一步:距离算完直接丢给hclust()默认参数,然后只看图不看统计合理性。实际上method参数的选择会显著影响树的形状。complete(最长距离法)倾向于生成紧凑的球状簇,single(最短距离法)容易产生链条状的细长结构,ward.D2则让簇内方差最小化,在生物学和医学数据分析里最常用。我的个人建议是:如果你没有特殊理由,优先用"ward.D2";如果你希望结果更稳健,可以多跑几种方法对比,看主要分支是否一致。

还有一个坑藏在hclust()的默认参数里。R老版本里hclust()默认使用method = "complete",而dist()默认是欧氏距离。欧氏距离对量纲极其敏感,比如特征1的取值范围是0-1,特征2的取值范围是1000-10000,那距离矩阵基本被特征2主导。所以在计算距离之前,必须对数据做标准化,否则树画的再漂亮,反映的也只是个别大数值特征的“一家之言”。

# 标准化后再算距离,这是推荐姿势 mat_scaled <- scale(mat) d <- dist(mat_scaled, method = "euclidean") hc <- hclust(d, method = "ward.D2")

2.3 树形对象的必备转换:hclust、phylo与treedata

现在你手上有hc,它是hclust对象。想要在ggtree里画图,通常需要转成phylo对象;ape包的as.phylo()干的就是这个活。还有一条更现代的路:直接用ggtree内部函数把hclust转成treedata对象,但as.phylo(hc)依然是兼容性最好的中转站。

library(ape) phylo_tree <- as.phylo(hc)

这一步转换看起来平淡无奇,但它是后面所有方案的地基。phylo对象是系统发育分析领域的标准格式,ape、ggtree、phytools这些主流包都以它为输入。如果你从外部拿到的是Newick格式的树文件(后缀通常是.nwk或.tre),可以用ape::read.tree()直接读进来,那就跳过了聚类步骤,直接有了树。

3. 三条绘制路线对比:ggtree、ggplot2原生极坐标、ape速成

3.1 ggtree:专业树形可视化,环形布局的最省心方案

ggtree是台湾学者余光磊团队开发的树形数据可视化包,现在已经被整合进tidytree生态,语法风格跟ggplot2高度一致。它最省心的一点是:内部封装好了树的坐标计算,你只需要指定layout = "circular",剩下的分支、节点、标签位置都自动排布,几乎不需要手动调坐标。

library(ggtree) p <- ggtree(phylo_tree, layout = "circular") + geom_tiplab(size = 3, offset = 0.5) + xlim(c(-4, 4)) p

就这么几行,一张标准的环形树状图就出来了。要注意xlim这一步:环形布局的树根在圆心,叶子在最外圈,但ggtree默认的坐标范围有时候会把最外层的标签截掉,手动扩大xlim是惯用操作。offset控制叶子标签离树梢的远近,值太小标签会贴着树枝,值太大图会松散,自己多试几次就知道手感了。

ggtree还支持layout = "fan",也就是扇形树状图。它跟circular的区别是:circular画完整的一个圆,fan只画一个扇形,可以用open.angle参数控制扇形的开角。比如open.angle = 90就留出四分之一圆的缺口。这个功能在做报告时特别实用——你可以把样本分组信息、热图、柱状图嵌在缺口位置,形成组合图,信息密度远高于完整圆。

3.2 ggplot2原生路线:coord_polar 的灵活与代价

如果你不想依赖ggtree,又想完全掌控每一个图形元素,那ggdendro提取树形数据,再用ggplot2的coord_polar()把线性树弯成环形,是一条经典路线。这套思路的核心逻辑是:先把树拆解成“线段”和“标签”两类基本图形元素,然后让坐标系从笛卡尔坐标变成极坐标。

library(ggdendro) library(ggplot2) dend <- as.dendrogram(hc) ddata <- dendro_data(dend, type = "rectangle") ggplot(segment(ddata)) + geom_segment(aes(x = x, y = y, xend = xend, yend = yend)) + coord_polar(theta = "x", start = 0, direction = 1) + theme_void()

这段代码里最关键的就是coord_polar(theta = "x"),它把横坐标映射为极坐标的角度,纵坐标映射为半径。原始树状图的根在左侧,变成极坐标后根就到了圆心,叶子沿圆周均匀排开。这个方案的灵活性是最高的,因为segment(ddata)返回的是一个普通数据框,你可以随意指定每一根线的颜色、粗细、透明度,甚至只绘制某一层的分支。想加背景色、加注释、加分组色块,都是普通ggplot2语法,没有任何黑魔法。

但代价也很明显:标签角度不会自动旋转,所有文本都是水平排版,叶子一多必然互相挤压;而且树的分支如果比较高,靠近圆心的地方会挤成一团,缺乏ggtree那种自动避让能力。

3.3 ape一行出图:草图阶段的最佳选择

ape是R语言系统发育分析的元老级包,它提供了一个极其实用的绘图函数plot.phylo(),支持type = "fan"。我在项目早期要快速看数据聚类效果时,经常用它出一张草图。

library(ape) plot(phylo_tree, type = "fan", cex = 0.7, edge.width = 1.2, label.offset = 0.3, no.margin = TRUE)

cex控制标签字号,edge.width控制树枝粗细,label.offset控制标签离叶子的距离。基础绘图系统的好处是快,适应各种输出设备,改起来也不含糊。缺点是这图是“一次性”的,想要叠加图层、做交互、嵌进ggplot2的组合图里就比较费劲。所以我把ape定位成“草图工具”,真正要精修出图时,还是回到ggtree。

3.4 三种方案怎么选:一张表说清楚

方案上手难度定制灵活性适合场景
ggtree低高,自带树形坐标与自动标签旋转正式出图、组合图、发表级别
ggplot2 + coord_polar中极高,所有元素都可控特殊定制需求,比如只画局部层次
ape plot.phylo极低低,基础绘图不可叠加快速预览聚类结果、草图沟通

我的建议是:默认选ggtree,不满足需求再降级或升级。ggtree不会让你失望,除非你对“标签倾斜角度”有极其特殊的审美要求,那种情况再考虑ggplot2手搓。

4. 把树从“方”变“圆”:极坐标换算原理与你必须知道的参数

4.1 圆形布局背后的数学直觉

很多人学会调用layout = "circular"之后,依然不理解图是怎么生成的,遇到问题就抓瞎。其实原理不复杂。普通的矩形树状图有两个坐标轴:x轴对应叶子展开的顺序,y轴对应聚类的合并高度(或者说分支长度)。环形树状图做的是一次极坐标变换:把原来的x轴映射成圆周角度,原来的y轴映射成半径。

这个过程很像物理里的“卷纸”:把一条矩形纸带绕成一个圆柱。原来纸带横向的长度变成了圆柱的周长,纵向的高度变成了圆柱的半径。所以树根位于圆心,越靠外的叶子,对应的聚类合并高度越高。反过来说,图上相邻的两片叶子,在原始树状图里也是相邻的,这个顺序性被完整保留了下来。

了解这个机制对调参特别重要。比如你发现图上某两片叶子之间的距离特别大,视觉上像是被硬掰开的,这往往不是布局算法的问题,而是原始聚类里这两个样本合并得晚、分支很长。换个角度说:圆上两片叶子的夹角越小,代表它们在聚类树上的合并距离越近。这个直觉能帮你快速解读图上的结构,而不是只看“哪几个挨得近”。

4.2 ggtree中控制扇形范围与起始角度的实操

ggtree里想控制扇形范围,主要靠三个参数:layout = "fan"、open.angle、以及xlim。

p_fan <- ggtree(phylo_tree, layout = "fan", open.angle = 120) + geom_tiplab(size = 3, offset = 0.3) + xlim(c(-5, 5)) p_fan

open.angle = 120表示扇形的缺口是120度,也就是说只画240度的弧。这样做的实际好处有两点:一是标签有地方“透气”,特别适合叶子数量多的树,避免标签绕成一整圈最难读的底部区域;二是给组合图留出位置,缺口处可以放置图例、热图或者其他统计图,这在微生物多样性报告中是常见操作。

起始角度的调整则不那么直观。ggtree没有直接的start.angle参数,通常的做法是让ggtree对象返回后,通过ggplot2的coord_polar的start参数来微调,但这样会和layout = "circular"内部已算好的角度冲突,容易翻车。更稳妥的做法是直接接受默认的起始角度,毕竟树的阅读顺序是顺时针还是逆时针,并不影响结构判断。若实在需要旋转,可以在聚类前对样本顺序做因子水平排序,从而间接改变树的叶子顺序。

4.3 叶子顺序不满意?在聚类之前就动手

这是一个经常被忽略、但能解决大多数“图不好看”问题的技巧:环形树状图的叶子顺序由聚类结果决定,但你可以通过调整样本标签的排序和距离矩阵的行顺序,间接改变叶子初始排列。hclust()聚类的合并顺序虽然是确定性的,但树的左右子节点交换时,ggtree会按内部算法重新排列。如果你希望某些特定样本始终在图的顶部区域,更靠谱的方法是先对距离矩阵的行列顺序做手动排列,或者使用ape::rotate()函数旋转phylo对象的内部节点。举个例子:

library(ape) # 对某个内部节点做旋转,交换它下边两支的位置 phylo_rotated <- rotate(phylo_tree, node = 25)

node的编号可以通过plot(phylo_tree); nodelabels()交互查看。这个操作虽然繁琐,但在出图前的微调阶段非常实用,尤其是你要确保某一组样本在图上有特定的相对位置时。

5. 从“能看”到“能发”:颜色映射、分支高亮与标签防重叠

5.1 给不同分支上色:用groupClade快速实现

环形树状图一开始只有黑色线条和黑色标签,信息量大但视觉重点不突出。给分支分色是最高性价比的美化手段。ggtree的分组上色有几种做法,最省事的是groupClade()按节点分组:

p <- ggtree(phylo_tree, layout = "circular") + geom_tiplab(size = 3, offset = 0.3) # 根据内部节点编号分组,node参数可以是向量 p <- groupClade(p, node = c(25, 38)) + aes(color = group) + scale_color_manual(values = c("black", "#E64B35", "#4DBBD5")) + theme(legend.position = "right") p

groupClade会把指定内部节点下的所有后代样本归为一组,这在基因家族分析里几乎是标配操作:先找到目标分支的节点编号,把整个分支高亮成红色,其他分支保持灰色,读者一眼就能锁定关键簇。注意,node编号在不同数据里是完全不同的,需要先用ggtree::geom_nodelab()或者ape::nodelabels()把编号标出来看一看,再写进代码里。

5.2 分支高亮的进阶:geom_highlight与层叠图层

如果你想让某一整个分支被半透明的色带包裹,画一个凸起的背景区域,可以用geom_highlight():

p + geom_highlight(node = 38, fill = "#E64B35", alpha = 0.3, extend = 0.1, extendto = max(get("x", envir = p$data)))

这个几何对象会在环形图上给指定分支画一片高亮扇区,非常适合在圈内圈外同时标注多个重点分支。不过要注意,geom_highlight和某些xlim设置结合时,高亮区域容易被裁剪,需要给extendto留足余量。层层叠加图层时,+的顺序也有讲究:高亮背景要放在geom_tiplab之前渲染,否则标签会被色带盖住。

5.3 标签防重叠的完整策略

环形树状图最头疼的问题永远是标签重叠。叶子少还好说,一旦超过50个叶子,下半个圆的标签几乎必然叠成一团。我总结了一套组合拳,按需使用:

  1. 缩小字号:size = 2或2.5,适合预览阶段,但不适合放到PPT里。
  2. 增大半径留白:xlim(c(-8, 8))配合offset = 2,给标签更宽的环形跑道,实际上把图放大了。
  3. 只标注感兴趣的子集:手动构造一个标签数据框,只对特定样本加标签,其他叶子只画点不写字:
label_subset <- data.frame( label = c("Sample1", "Sample5", "Sample12"), stringsAsFactors = FALSE ) p + geom_tiplab(size = 3, offset = 0.5, data = label_subset, # 只显示指定的叶子 aes(label = label))

这个方案是我在80个样本的聚类图里最常用的,既不牺牲关键信息,又保住了排版整洁度。

  1. 用geom_tiplab2::geom_tiplab2或者等距扇形布局:geom_tiplab2在某些场景能提供更均匀的标签排布,但它只适用于layout = "fan",而且对ggtree版本的依赖较强,新版可能改名,遇到报错别慌。

5.4 在图的外圈叠加额外信息

当环形树状图不再只是一棵孤零零的树,而是和柱状图、热图、分组色块组合在一起时,信息量会再上一个台阶。ggtree提供了geom_fruit()(来自ggtreeExtra包)来对接外部数据层:

library(ggtreeExtra) sample_info <- data.frame( label = paste0("Sample", 1:20), Group = sample(c("A", "B", "C"), 20, replace = TRUE), Value = runif(20, 0, 10) ) p + geom_fruit( data = sample_info, geom = geom_col, mapping = aes(x = Value, fill = Group), offset = 0.5, pwidth = 0.5 )

这样的组合图在医学多组学数据分析、微生物多样性的文章里非常常见:树在中央展示样本层级关系,外圈用柱状图映射某个指标,用色块映射分组信息,读者看到一张图就能同时掌握结构与数值两个维度的信息。pwidth控制外围柱状图占的绘图区域宽度,offset控制它与树之间的距离,这两个参数需要反复调试才能得到美观的比例。

6. 避坑实录:环形树状图最常见的五个问题与排查思路

6.1 中文标签全部变成方块,且保存的图片上也是乱码

这个问题在Windows上尤其高发。R图形设备默认字体不支持中文字符,ggtree输出到屏幕时还能勉强显示,一旦ggsave()保存为PDF或PNG就会变成方块。解决方案是引入showtext包,强制指定中文字体:

library(showtext) font_add("SimSun", "simsun.ttc") # 宋体,Windows系统 showtext_auto() # 之后正常绘图、保存,中文就不会乱码了 ggsave("circular_dendrogram.pdf", width = 8, height = 8, dpi = 300)

如果你用的是macOS,字体名称换成"PingFang SC"或"Heiti SC"。Linux服务器上没有图形界面,则需要先安装中文字体文件再注册到showtext里。这个坑我踩了不止一次,每次换电脑都要重新配置一遍,后来我把字体配置写成了一个小函数,塞进个人Rprofile里,一劳永逸。

6.2 叶子标签重叠成一坨,怎么调都分不开

如果标签数量确实很多(超过60个),或者标签文本本身太长(比如完整的基因名),单纯靠offset和size是无解的。我的排查链路是:先砍文本长度(用gsub去掉物种命名里的冗余部分,或者统一缩写成首字母加编号),再砍数量(只标注目标分支),最后才考虑缩放字号。顺序不要反,标号缩写的收益远大于调字号。还有一个隐藏技巧:把叶子标签按圆周的角度做旋转对齐。ggtree的circular布局会自动做这个旋转,但如果用ggplot2 + coord_polar自制图,就需要手动添加角度映射:

ggplot(segment(ddata)) + geom_segment(aes(x = x, y = y, xend = xend, yend = yend)) + geom_text(data = label(ddata), aes(x = x, y = y, label = label, angle = 90 - 360 * x / max(ddata$segments$x)), hjust = 0, size = 2.5) + coord_polar(theta = "x") + theme_void()

6.3 图保存后四周被裁切,叶子尖端没了

这个几乎是ggtree环形布局的新手必经坑。原因很简单:树形图的坐标系范围默认只覆盖到叶子节点,而标签、外圈注释是在这个范围之外绘制的,画面输出时被自动裁剪。解决办法就是那条“万金油”xlim(c(-5, 5))。这里的数值没有标准答案,取决于你的树有多大、offset设了多少。一个判断技巧是:如果叶子尖端看着贴边,就往大调;如果树在圆心部分显得特别空,就往小收。同理,ggsave时留出足够的width和height,不要贪小尺寸。

6.4 聚类结果和预期不符,画出来全是“大杂烩”

这不是绘图问题,是统计方法选择问题。我遇到过好几次:明明两组样本应该有明显差异,聚类图却交错分布。排查时先别怀疑数据,先把dist()和hclust()的method都换一遍。比如欧氏距离对高维数据非常不友好,可以考虑method = "manhattan"(曼哈顿距离)或method = "minkowski";如果你的数据是百分比或丰度数据,vegdist()(来自vegan包)里的method = "bray"更合适。聚类方法方面,ward.D2在大多数场景表现稳健,但遇到类别数不均衡的数据时,complete或average可能更合理。多做几组对比,不要在一棵树上吊死。

6.5 ggtree版本更新后,旧代码报错或者参数失效

ggtree是目前TreeTools生态里迭代较快的包之一。版本升级后,我遇到过的典型变化包括:某些几何对象的参数名变化、geom_tiplab2被整合进其他函数、aes(color = group)的写法需要显式引入groupClade的结果。我的应对策略有三个:第一,脚本开头用library(ggtree)但调用时尽量写ggtree::前缀,减少命名空间冲突;第二,升级前先读一下包的NEWS文档,通常里面会列出破坏性变更;第三,实在不行就用remotes::install_version("ggtree", version = "旧版本号")回退版本。新版本不一定是更好的版本,够用就好。

7. 一个提高出图效率的小习惯:把“聚类+画图”封装成自己的函数模板

写了这么多,最后分享一个我个人的工作方式。环形树状图本身代码量不大,但每次都要从dist()开始写,容易遗漏标准化、聚类方法选择、标签防重叠这些细节。我把它封装成了一个函数,参数暴露了最常用的几个开关:数据框、是否标准化、聚类方法、布局类型、标签是否全标。这样无论是做分析报告还是临时看数据,都只需要一行调用:

plot_circular_dendrogram <- function(data, scale_data = TRUE, hc_method = "ward.D2", layout = "circular", label_all = TRUE, label_subset = NULL, open_angle = NULL, offset = 0.3, xlim_range = c(-5, 5)) { mat <- if (scale_data) scale(as.matrix(data)) else as.matrix(data) hc <- hclust(dist(mat), method = hc_method) phylo_tree <- as.phylo(hc) p <- ggtree(phylo_tree, layout = layout, open.angle = if (!is.null(open_angle)) open_angle else 0) + xlim(xlim_range) if (label_all) { p <- p + geom_tiplab(size = 3, offset = offset) } else if (!is.null(label_subset)) { p <- p + geom_tiplab(size = 3, offset = offset, data = label_subset) } p } plot_circular_dendrogram(mat, label_all = TRUE)

我用这个函数跑过基因表达矩阵、用户调研问卷、商品销售结构,全部都是一行出图。最核心的经验是:把重复劳动变成参数化调用,你才有精力去关注图背后的数据结构和业务解释,而不是每次都在同一个地方调字体、调xlim。你第一次画环形树状图可能会被各种小问题绊住,但用熟了之后,它会是你在R语言可视化武器库里最趁手的一件工具。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询