资讯动态

R语言3D直方图分组绘制:科研绘图进阶指南

发布时间:2026/10/16 19:23:24 来源:尧图企业网站定制
做科研绘图的人多少都遇到过这种尴尬数据本身明显有差别可画成普通的2D直方图之后两条折线叠在一起不是互相遮挡就是颜色分不清。尤其当手头数据分成若干组、每组要对比分布形态时平面图的表现力是有限的。R语言里有个没那么热门、但实际非常可靠的手法就是用3D直方图来做分组展示简单说就是给每组数据分别搭一座三维柱状体再放进同一个坐标系里让“组别”成为第三根轴峰位偏移、离散程度差异直接在图上撞见眼睛。这期标题是“R 语言科研绘图第 91 期 --- 3D直方图-分组”我按咱们平常做论文配图的思路把从数据处理到成图优化的完整过程拆开讲一遍。你可以先判断自己是否适合这个内容如果你的数据是连续变量手头有分组因子比如对照组/处理组、多个时间点想在图中直观对比分布形态或者你有两个连续变量想同时看联合分布和组间差异这篇都适用。新手也能跟上代码我会一行一行解释老手可以直接跳到第 3 节和第 4 节抄作业。1. 3D直方图能解决什么实际问题先聊一个问题普通直方图不够用吗够用但只在少量场景下够用。一个连续变量、两个分组把两条直方图叠在一张2D图里只要颜色选得开、透明度控制得当读者还能分辨。一旦分组变成三个、四个或者需要同时展示两个变量的联合分布2D直方图就开始力不从心最常见的两个麻烦折线/柱体大面积互相遮挡图例再多读者也分不清哪个峰属于哪一组两个连续变量的联合分布只能画成热力图热力图虽然能显示密度高低但无法同时展示第三维度的分组差异等于把信息压扁了。3D分组直方图解决的问题恰恰在这里。它把“组别”本身变成一个空间维度每组数据在x轴方向上占据独立的高度柱区域z轴方向则表达频数或密度这样一组一组的分布形态就像并排立着的建筑群峰高、峰位、离散程度一眼能比出来。对于二维联合分布的场景x轴和y轴分别表示两个变量z轴是频数分组则通过柱子的位置和颜色做区块区分呈现在同一张图里。拿我去年帮某课题组做的一批模拟实验数据举例三组样本每组 500 条记录两个测量指标。用2D热力图逐组画三张图摆在一起才能看出差异改用分组3D直方图后一张图里三组峰区的偏移和高度差直接显现组会汇报时导师一眼就抓住了重点。这正是科研绘图“信息密度优先”的思路不是所有数据都要画成3D但当你的对比维度超过两个时立体视角常常比平面叠加更省口舌。下面用一个对比表格说清楚2D重叠直方图和3D分组直方图的差异对比维度2D重叠直方图3D分组直方图视觉对比依赖颜色和透明度组数多时容易混乱空间位置天然分隔组数多也能保持清晰组数扩展2到3组尚可超过3组严重拥挤5到10组可通过柱区错位继续扩展信息密度只能表达一维变量分布可同时表达二维联合分布与分组维度制作成本极低几行代码中等需要理解矩阵到柱体的映射逻辑适合场景快速探索、正文配图场景较少组会汇报、论文配图、复杂对比分析需要说明的是3D直方图不是万能的。如果数据只有一组或者两组差异非常明显根本不需要立体展示老老实实画2D直方图反而更清爽。我个人的判断标准很简单当你需要向别人解释“这组和那组的分布到底哪里不一样”时如果解释起来费劲那大概率就值得上3D。2. 绘图方案选型为什么我推荐plot3D2.1 plot3D的核心优势R语言里做3D静态绘图plot3D包是绕不开的一个选择。它底层基于base R的persp函数但封装得比persp友好太多。hist3D这个函数专门用来画3D柱状直方图输入一个矩阵就能把每一格的高度映射成一根柱子省去手写柱体几何计算的麻烦。它生成的图形是标准矢量图可以无缝导出PDF用于论文投稿这一点比很多交互式3D方案强。另一个隐藏优势是它对R基础语法的兼容度。plot3D不依赖tidyverse也不要求你改造数据结构只要有一个二维矩阵就能画配合R原生legend、axis、title等函数可以在同一张图上做精细的标注调整。对于科研绘图来说这种“随时可以用基础函数修补细节”的灵活度非常重要。2.2 备选方案对比当然能做3D绘图的R包不止plot3D一个我顺手整理了一个对比表格方便你遇到不同场景时快速选型。方案类型优势不足适合场景plot3D静态3D代码简练、参数丰富、矢量输出不支持交互旋转论文配图、固定视角展示rgl交互3D可实时旋转、动态观察输出静态图质量一般、依赖OpenGL环境组会演示、探索性数据分析scatterplot3d静态3D轻量、易上手只能画散点不支持柱状直方图三维散点图Python matplotlib静态3D3D bar功能成熟需要切换到Python生态与R数据处理链路分离已在使用Python的团队从科研论文配图的角度出发plot3D的综合性价比最高。rgl更适合做演示场景但如果你想在论文里放一张可交互的3D图期刊大概率不接收最终还是要落到静态图。所以如果只能学一种方案优先学plot3D。2.3 分组数据的两种组织思路讲代码之前先理清一个问题分组因子怎么映射到3D柱状图上根据数据形态通常有两种组织思路。第一种思路是一维连续变量加分组因子比如测量某种酶活性对照组和处理组各有一列数值。这时我们把分组因子放在x轴方向把连续变量分箱后放在y轴方向z轴是频数。z矩阵的行是分组数列是分箱数。这种结构最简单画出来像是几排并列的“墙”适合快速对比分布的峰位和离散程度。第二种思路是二维联合分布加分组因子比如同时测量身高和体重两个指标分男女两组。这时先把每个组分别做成一个二维频数矩阵再想办法把多个矩阵融合到一个3D坐标系里。这比第一种复杂但信息表达力更强适合同时考察两个变量与分组之间的交互关系。我建议新手先吃透第一种因为它能帮你理解z矩阵、分箱、颜色映射这些底层逻辑做组会汇报或论文配图时第二种往往更“有面子”。后文第 3 节和第 4 节分别讲这两种结构的完整实现。3. 基础版一维分组3D直方图实现3.1 思路与数据准备假设现在有两组数据分别来自两个处理条件每组 500 条连续观测值。目标是画一张3D直方图让两组分布形态并排呈现。第一步不是画图而是把连续数据切成统一的分箱。这一步最容易被忽略两组数据的breaks必须完全一致否则柱子的宽度和位置对不上后续任何图形美化都救不回来。我用模拟数据演示模拟的目的在于过程完全可复现你换成自己的真实数据即可。先生成两组正态分布数据均值不同、标准差略有差异模拟现实中“处理组相对对照组发生偏移”的典型情形。分箱范围设为 0 到 11步长 1共 11 个区间区间太少柱子太粗太多则柱体稀疏对于 500 条数据这个密度是合适的。3.2 代码分步讲解library(plot3D) set.seed(2024) n - 500 group - rep(c(A, B), each n) value - c(rnorm(n, mean 5, sd 1.5), rnorm(n, mean 7.5, sd 1.2)) # 统一分箱 breaks - seq(0, 11, by 1) mid - (breaks[-1] breaks[-length(breaks)]) / 2 # 分别统计两组频数 hA - hist(value[group A], breaks breaks, plot FALSE)$counts hB - hist(value[group B], breaks breaks, plot FALSE)$counts # 构造z矩阵行 组别列 分箱 z - rbind(hA, hB) # 构造颜色向量按column-major对应到z的每个元素 col_vec - rep(c(#4DBBD5, #E64B35), times ncol(z)) hist3D(z z, y mid, col col_vec, border gray30, space 0.3, shade 0.4, theta 40, phi 20, ticktype detailed, xlab 分组, ylab 指标值, zlab 频数)代码里值得重点解释的地方有三个。第一hist函数中plot FALSE的作用。我们只想要分箱后的计数结果不需要它画出默认的2D直方图直接取$counts得到向量。这比手写table(cut(...))简洁而且hist内部会处理好区间端点的问题。第二z矩阵为什么是两行而不是两列hist3D默认把矩阵的行映射到x轴方向列映射到y轴方向。把两组分别放在第 1 行和第 2 行画出来的效果就是x轴方向两组并排。y参数传入mid向量也就是每组内部各分箱的中点位置这样y轴刻度自然显示为指标值的真实坐标而不是1到11的序号。第三颜色向量的写法。hist3D的col参数按column-major顺序逐一对应矩阵中的每个元素矩阵第 1 列的所有行先用掉前几个颜色然后才轮到第 2 列。rep(c(组A色, 组B色), times ncol(z))得到的序列是A色、B色、A色、B色…正好让第 1 行所有元素拿到A色、第 2 行所有元素拿到B色。这个底层规律很隐蔽我见过不少人在多行的矩阵上乱传col参数结果颜色完全错位。3.3 配色、图例与细节调整第 3.2 节画出来的图已经有了分组对比的骨架但直接放进论文还不够。第一个要处理的是图例。hist3D默认会在右侧生成彩色数值条那是给连续渐变颜色用的我这里用颜色表示类别分组数值条毫无意义必须关掉。# 关闭默认颜色条改用基础图例 hist3D(z z, y mid, col col_vec, border gray30, space 0.3, shade 0.4, theta 40, phi 20, ticktype detailed, xlab 分组, ylab 指标值, zlab 频数, colkey FALSE) legend(topright, legend c(组A, 组B), fill c(#4DBBD5, #E64B35), bty n, cex 1.2, border white)这里legend的位置我习惯放在topright因为3D图的柱体通常集中在左下区域右上角空间相对干净。不同视角下遮挡关系会变化如果图例被柱子挡住可以临时把theta调成30再试。另一个重要参数是space。它控制相邻柱子之间的空隙比例取值0到10表示紧密贴合1表示柱子只有一根线粗细。我试过很多组合0.2到0.3之间最耐看太大显得柱子稀疏太小则分不清边界。border参数建议用gray30这种深灰色既勾勒了柱体轮廓又不会像纯黑色那样抢眼。shade参数控制柱体表面的阴影强度0.3到0.5比较合适阴影太重会让柱体表面看起来“油腻”太轻则立体感不足。4. 进阶版二维联合分布的分组3D直方图4.1 数据场景与二维频数统计基础版能解决单变量分组的对比但现实中更常见的是两个变量联合分布的分组对比。比如某课题组研究两种生长条件下植株的株高和分蘖数分布同时采集了两个变量想看看条件A和条件B在二维空间中的峰区是否发生偏移。这种情况下每个组的分布是一个二维平面高度代表联合频数画出来就像一片起伏的地形。处理思路是给每个组分别做一个二维频数矩阵用table(cut(...), cut(...))实现。cut把第一维变量分箱成行因子第二维变量分箱成列因子table统计每个格子里的样本数得到的就是二维直方图的频数矩阵。为了后续拼接方便两个组的breaks必须完全一致否则矩阵尺寸不同。set.seed(2024) n - 400 x1 - rnorm(n, 5, 1.5); y1 - rnorm(n, 4, 1.3) x2 - rnorm(n, 7, 1.2); y2 - rnorm(n, 6, 1.4) bx - seq(0, 11, by 1) by - seq(0, 10, by 1) tab1 - table(cut(x1, breaks bx, include.lowest TRUE), cut(y1, breaks by, include.lowest TRUE)) tab2 - table(cut(x2, breaks bx, include.lowest TRUE), cut(y2, breaks by, include.lowest TRUE)) z1 - matrix(as.numeric(tab1), nrow length(bx) - 1) z2 - matrix(as.numeric(tab2), nrow length(bx) - 1)include.lowest TRUE是为了让最左端点的样本也能进入第一个区间否则数据最小值恰好等于breaks起点时会多出一个空档造成矩阵统计偏差。4.2 拼接矩阵与隔离行技巧有了两组各自的二维频数矩阵之后问题变成了怎么把它们放进同一个坐标系。我试过两种方式先说结论拼接矩阵加隔离行是静态成图最稳的方案。所谓拼接就是把两个矩阵沿行方向叠起来。z1有11行、z2也有11行叠加后变成22行。为了让两组柱体之间有明确的视觉分界我在两个矩阵中间插入一行全为NA的间隔行。NA在hist3D中的处理方式是不绘制该位置柱子效果上相当于“隔离带”。这种方案的优点在于代码简单、不会出现遮挡关系两组柱体在x轴方向上天然分成两个区块读者一眼就能分清归属。代价是x轴的坐标值不再对应真实的变量范围而是行序号所以我通常直接在xlab上写下“组A在上、组B在下”的说明再配合图例语义就完整了。颜色向量的构造是这个方案最容易出错的地方。rbind之后z矩阵的元素在内存中按column-major排列先是组A第 1 列所有行然后是NA行对应位置然后是组B第 1 列所有行接着换到第 2 列继续。因此颜色向量必须按同样的块状结构拼接。z - rbind(z1, matrix(NA, nrow 1, ncol ncol(z1)), z2) # 按列的分块结构构造颜色A色11个 间隔行1个 B色11个重复列数次 cols - rep(c(rep(#4DBBD5, nrow(z1)), white, rep(#E64B35, nrow(z2))), times ncol(z1)) hist3D(z z, col cols, border gray40, space 0.2, shade 0.4, theta 40, phi 20, ticktype detailed, xlab X分箱上组A / 下组B, ylab Y分箱, zlab 频数)这里特别想提醒一个隐蔽问题如果你直接把cols写成“前面全是A色中间一个白色后面全是B色”看起来逻辑正确但柱子位置对不上因为z矩阵按列读取时第 2 列组A的元素会落在颜色向量末尾的B色区域整张图的颜色分区会乱掉。必须按“每一列内部A、间隔、B”的顺序重复填充。这是我在实际使用中踩过的最深的坑之一写在这里帮你避雷。4.3 视觉优化与交替方案拼接方案的好处是并列清晰缺点则是两组之间的站队感过于明显毕竟x轴被虚拟化成了行序号。如果你的数据本身就有天然的分组坐标比如不同实验温度、不同浓度梯度其实可以让组别直接成为x轴坐标而不是靠拼接制造区块。这时可以用add TRUE连续画两次hist3D第二张图叠在第一张上并通过x参数偏移柱体位置来错开两组。# 以x轴表示分组、y轴表示第一变量分箱、z轴表示频数的思路 hist3D(z z1, col #4DBBD5, border gray40, theta 40, phi 20, ticktype detailed, xlab 分组, ylab Y变量, zlab 频数) hist3D(z z2, col #E64B35, border gray40, add TRUE)add叠加方案不需要拼接代码结构简单但有一个天然劣势两组柱子在相同坐标位置上严格重叠第二组会完全盖住第一组。解决方法是给第二组加透明度或者只在峰区差异明显时才用。透明度的处理需要用到plot3D内置的alpha.col函数hist3D(z z2, col alpha.col(#E64B35, alpha 0.6), add TRUE)alpha控制在0.5到0.7之间比较合适太低颜色发灰太高又失去叠加意义。叠加方案的优点是能保留完整的坐标语义适合做探索性分析拼接方案则更适合直接出图进论文。两种方案各有应用场景没有绝对优劣我建议你两种都跑一遍感受差异之后再按需求选择。5. 论文级优化配色、视角、导出与常见问题5.1 配色与视角调优先说配色。3D直方图的信息层次比普通柱状图丰富柱子的颜色不宜过多最好保持“一组一色、全图统一”的原则。我在实际项目中最常用的一组搭配是4DBBD5偏蓝和E64B35偏红这两个颜色在色觉障碍模拟测试中的区分度也比较高适合直接用于论文。如果你需要打印成黑白稿建议改用灰度色阶比如浅灰色和深灰色。视角参数theta和phi是3D图成败的关键。theta控制观察者绕z轴旋转的角度phi控制仰角。我做过一组对照测试theta 40、phi 20是绝大多数柱状布局的稳妥选择峰区不会被前景柱体大面积遮挡。如果个别柱子被挡住可以先试着把theta调小到30柱子会更“正面”地面对读者如果想强调深度感把theta调到50到60视角更倾斜深度对比更强。phi一般不建议超过30仰角太大会弱化高度对比。另一个容易被忽略的参数是ticktype。默认的simple模式只画坐标轴箭头不显示刻度值会让读者完全看不出数量级。设置ticktype detailed后三个轴的刻度数字都会出现信息完整性大幅提升。代价是刻度文字可能互相重叠如果出现这种情况可以缩小cex.lab或调整图形尺寸。5.2 导出设置论文配图对分辨率有硬性要求通常期刊最低要求300 dpi。用png导出时宽度建议在2400像素以上这样才能保证印刷时清晰。PDF矢量导出则不存在分辨率问题而且文件体积更小适合最终投稿。png(3d_hist_group.png, width 2400, height 1800, res 300) # 此处放完整hist3D代码 dev.off() pdf(3d_hist_group.pdf, width 8, height 6) # 此处放完整hist3D代码 dev.off()如果图中有中文标签R的base图形设备默认字体经常无法显示中文表现为方块或乱码。最省事的解决方法是安装showtext包用showtext_auto()开启字体自动匹配然后在绘图前用windowsFonts或font_add指定中文字体。这点在我的经验里几乎是中文科研绘图的必踩坑项在这里特别提醒。5.3 常见问题速查表最后整理一个我在多年使用中积累的问题速查表覆盖从入门到进阶最常见的场景。现象原因解决方法柱子颜色与分组不对应col参数未按column-major规律构造按照矩阵列内分组顺序重复颜色向量有一组柱子完全被另一组遮住add叠加时未设置透明度第二组使用alpha.col透明度0.5到0.7中文标签显示为方块基础设备不含中文字体使用showtext包开启字体自动匹配导出图片文字模糊分辨率不足png设置300 dpi或改用PDF矢量导出x轴刻度全是1、2、3等序号未传入mid向量或原始坐标通过x、y参数传入变量真实坐标柱子之间没有间隙space参数为默认0设置space 0.2到0.3图例颜色条干扰colkey未关闭hist3D中添加colkey FALSE柱体阴影太重看不清shade参数过大设置为0.3到0.5图形四周留白不足默认margins不适合3D图用par(mar c(5, 4, 4, 6))扩边距这个表格并没有覆盖所有问题但已经能解决绝大多数入门场景。如果你在实际操作中遇到了表格之外的情况一个通用排查思路是把问题归因到四个层面数据矩阵是否算对、坐标映射是否正确、颜色向量是否对应、图形设备参数是否合理绝大多数故障都跑不出这四类。6. 扩展转成交互式3D图讲到这里主体内容已经足够你完成一张可以放进论文的分组3D直方图。但有一类场景还需要额外工具就是组会现场演示或数据探索阶段使用者需要旋转视角、观察柱体背后是否藏有细节。这时可以把plot3D的静态图直接转成rgl交互窗口。方法很简单安装plot3Drgl包在绘制完静态图之后调用一句plotrgl()图形就会出现在可拖拽旋转的rgl窗口里。它的原理是把plot3D的坐标系统映射到OpenGL场景中因此前面的所有参数都可以沿用无须重新调整视角。library(plot3Drgl) plotrgl()转成交互图后我习惯先用鼠标旋转几圈确认没有柱体被严重遮挡再切回静态视角出图。这个流程能避免“论文里放了一张有遮挡死角图”的尴尬。需要注意的是rgl窗口在RStudio中打开时有时会弹出一个独立的小窗口如果显示不出来通常是系统缺少OpenGL驱动检查一下R的图形设备配置即可。这个扩展的适用场景很清晰交付给合作者看数据、组会汇报前快速审视图形结果、或者纯粹自己探索数据。交互式3D并不是论文投稿的最终形态但它能提升你的制图效率值得放在工具箱里备用。我个人在实际操作中的体会是画分组3D直方图最大的成本其实不在绘图代码而在数据结构的整理。只要把“矩阵的行列含义”和“颜色的column-major对应关系”这两件事想透再复杂的图也只是参数组合的问题。这篇里的代码我全部跑过参数也是按常见场景调试过的你可以直接复制替换成自己的数据。如果你在分箱数量或者视角选择上拿不准建议先跑模拟数据看效果再决定最终参数这比自己对着空坐标想象快得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑