R语言ggplot2分组绘图攻略:两组折线图与散点图同图展示

发布时间:2026/10/16 5:23:56
R语言ggplot2分组绘图攻略:两组折线图与散点图同图展示
用R语言里的ggplot包在同一张图上画两组折线图或者散点图是我这几年做数据分析时被问得最多的一件事。乍一听特别基础真上手却有一堆细节数据到底该怎么组织、颜色图例怎么分开、点太多糊成一团怎么办、为什么我只想画两条线却报错。这篇文章就围绕这个真实需求展开用模拟数据一步步演示两组折线图、两组散点图以及折线叠散点的常见组合画法把背后的ggplot映射逻辑和实际踩过的坑都讲清楚。不管你是刚接触R的初学者还是需要拿图去汇报的进阶用户只要想在同一个画布上比较两组的趋势或分布这篇都能当参考。1. 先搞懂ggplot的绘图思维宽表、长表与映射1.1 分组问题的本质是“美学映射”ggplot2的核心思想不是“画图”而是把数据框里的每一列“映射”到图形的某个属性上比如x轴、y轴、颜色、形状、线型、透明度。想在同一个图上表示两组数据本质就是给ggplot一个信号哪些点属于第一组哪些点属于第二组。这个信号通过aes()传入最常见的就是color group或linetype group。很多人会本能地写两次geom_line分别画每组的数据然后发现图例不好合并、颜色不好统一。其实更稳的思路是用一个geom把组别变量映射到视觉属性上剩下的事情ggplot会自动完成自动分组、自动配颜色、自动生成图例。理解了这一层后面所有看似复杂的问题都会变得简单。1.2 宽表和长表ggplot偏爱“长表”R里常见两种数据结构。一种叫宽表每一行是一个时间点A组和B组各占一列另一种叫长表把A组和B组堆叠起来一列是组别一列是数值。ggplot做多组对比时更偏好长表。宽表的典型长这样df_wide - data.frame( week 1:12, A c(3.2, 3.6, 4.1, 4.5, 5.0, 5.4, 5.9, 6.3, 6.8, 7.1, 7.6, 8.0), B c(5.2, 5.1, 4.9, 5.2, 5.5, 5.0, 4.7, 4.9, 4.6, 4.3, 4.1, 3.9) )长表则要把A、B两列“溶化”成一列组别、一列数值推荐用tidyr包里的pivot_longerlibrary(tidyr) df_long - pivot_longer( df_wide, cols c(A, B), names_to group, values_to value )转换之后df_long就变成了week、group、value三列。这个时候画图aes(x week, y value, color group)一句就能拆开两组。如果坚持用宽表画也不是不行但要分两次geom_line还得手动控制图例过程更别扭。宽表还是长表看似只是数据组织的差异实际直接影响绘图代码的复杂度。我的经验是只要涉及多组比较先转成长表再画能少踩一半的坑。1.3 构造一份可复现的模拟数据为了方便演示这里我直接构造一份长格式模拟数据模拟一个时间序列实验对照组和处理组各测了12周有一个略微不同的趋势再加一点随机波动。用set.seed保证每次运行结果一致方便复现library(ggplot2) set.seed(42) df - data.frame( week rep(1:12, 2), group rep(c(对照组, 处理组), each 12), value c( 3 0.4 * (1:12) rnorm(12, 0, 0.3), 5 - 0.2 * (1:12) rnorm(12, 0, 0.35) ) ) head(df)这份数据结构简单正好覆盖本文所有场景折线图、散点图、分组映射、图例调整。后面所有代码都基于这个df读者直接复制就能跑再换成自己的数据即可。2. 两组折线图从一行代码到定制化输出2.1 基础画法一个geom_line加color映射最基础的两组折线图只需要一行核心代码p - ggplot(df, aes(x week, y value, color group)) geom_line() p因为group是字符型变量ggplot会把它当作离散变量处理自动为两组分配不同颜色并生成一个图例。如果想要进一步区分两组可以把组别同时映射到线型这在黑白打印场景下尤其有用ggplot(df, aes(x week, y value, color group, linetype group)) geom_line(size 1)注意这里linetype group写在了全局aes()里所以图例是统一的一个既显示颜色也显示线型。很多人习惯在geom_line内部额外写linetype group效果一样但后续要通过guides调整图例时容易各自独立。2.2 自定义颜色、线型和图例名称ggplot默认配色是可读性不错的但到了正式汇报场景通常需要固定成品牌色或约定色。用scale_color_manual手动指定p - ggplot(df, aes(x week, y value, color group)) geom_line(aes(linetype group), size 1.2) scale_color_manual(values c(对照组 #4E79A7, 处理组 #E15759)) labs( color 实验分组, linetype 实验分组, x 周数, y 观测值 ) plabs里同时写了color和linetype的标题确保两个图例名称一致。不过因为color和linetype被映射到了同一个离散变量ggplot通常会自动合并成一个图例这时只需在labs里写一个color即可。如果想统一修改也可以写guides(color guide_legend(title 实验分组), linetype guide_legend(title 实验分组))两条设置都写的好处是万一ggplot没有自动合并图例也不会出现两个不同标题。2.3 折线加数据点信息量更完整折线图只反映趋势数据点位置才是原始证据。样本量不大时强烈建议在折线图基础上叠加geom_point。一个很实用的小技巧是同时映射shapeggplot(df, aes(x week, y value, color group, shape group)) geom_line(aes(linetype group), size 1) geom_point(size 3, alpha 0.8) scale_color_manual(values c(对照组 #4E79A7, 处理组 #E15759))这样颜色、线型、形状三者都携带分组信息看图的人即使色弱也能通过形状或线型区分。alpha设到0.8是为了让点有点透气的质感又不至于太淡。如果点被线遮挡可以把geom_point放在geom_line之后让它画在最上层。2.4 数据缺失时折线会断怎么办两组数据如果观测时间点不完全一致比如对照组测了12周处理组中间漏了3周折线图会出现断裂。这种情况最简单也是最重要的原则是不要为了连线而连线。如果x轴是时间点缺失本身就是信息缺失强行用线性插值补全反而可能误导人。建议做法有三种数据量充足时先用df %% tidyr::complete(group, week)补成完整网格缺失值会变成NA折线自动断开。如果希望连线平滑可以用zoo包里的na.approx做线性插值但必须清楚这是估计值不是实测值。直接在图上只画散点不画线让读者看到缺失点的位置。实际场景里需要按周对齐、画线但个别点没采集到我会保留NA并用完整网格画线同时叠加一圈散点标记实测位置。3. 两组散点图颜色、形状与点重叠的处理3.1 最直观的散点图color加shape同时映射散点图和折线图的逻辑一样把组别映射给color或shape即可。散点图里shape的作用尤其重要因为点的颜色在灰度打印或色弱环境下不易区分形状却不会受影响。ggplot(df, aes(x week, y value, color group, shape group)) geom_point(size 3) scale_color_manual(values c(对照组 #4E79A7, 处理组 #E15759))运行这段代码后两组点会分别有不同颜色和不同形状图例里会同时显示颜色和形状信息。默认情况下ggplot会把color和shape的图例合并成一个因为两者映射自同一个变量。这种合并通常是好事图例更简洁。如果想拆成两个图例比如希望shape的图例叫做“组别形状”color的图例叫做“组别颜色”可以分别指定guides。但多数情况下统一图例更符合汇报习惯我不太建议拆开。3.2 折线加散点趋势和原始数据同时出现折线图和散点图不是互斥的。在同一张图上先画半透明的折线反映趋势再画实心点标出原始观测值是两张图合二为一的常见操作。代码非常简单ggplot(df, aes(x week, y value, color group, shape group)) geom_line(aes(linetype group), alpha 0.5, size 1) geom_point(size 3) scale_color_manual(values c(对照组 #4E79A7, 处理组 #E15759))这里的关键在于alpha0.5。把线设为半透明点保持不透明视觉上点会更突出线则成为辅助参考。如果反过来想突出趋势、弱化点就把点的alpha调低。图层的顺序和透明度其实就是你的视觉引导逻辑想好重点在哪里再决定参数。3.3 点重叠太严重用透明度和抖动来救两组散点如果数值接近点会大面积重叠看起来只是一个实心色块。比如对照组和处理组的值都在4.8到5.2之间上下波动叠加后根本分不清哪点多。面对这种重叠我有三个常用手段。第一个是alpha透明度。把点设为alpha 0.5重叠区域颜色会更深不同组的密度分布一下子就能看出来。第二个是position_jitter给点增加非常小的随机抖动避免完全重合。注意抖动只适合离散坐标或展示分布的场景如果x轴是严格时间轴抖动会改变点的位置容易被人质疑。第三个是改用箱线图或小提琴图把分布画出来彻底避开散点重叠问题。这三招可以组合效果最好的是alpha加轻微抖动。3.4 用图层顺序强调重点组两组数据一起画图并不总是要“一视同仁”。如果处理组才是核心对照组只是参考可以让对照组变灰、处理组变亮或者调整图层顺序让处理组点盖在对照组上面。具体实现思路是在scale_color_manual里把对照组设为灰色同时把处理组设为高饱和色ggplot(df, aes(x week, y value, color group)) geom_point(size 3, alpha 0.9) scale_color_manual(values c(对照组 #BDC0C4, 处理组 #D7263D))这种“视觉分层”的做法非常适用于项目汇报能让看的人第一眼就抓住重点。不要小看这个细节一张图读完需要十秒还是一秒往往取决于重点组有没有被视觉强调出来。4. 从两组到多组数据叠加、分面与双y轴的话题4.1 手动叠加另一组数据data参数的用法有时两组数据不在同一个数据框里甚至行数都不一致。这种情况下不需要强行合并数据可以直接用data参数叠加第二个图层df_extra - data.frame( week 3:10, value seq(5.5, 4.8, length.out 8), group 外部参考线 ) ggplot(df, aes(x week, y value, color group)) geom_line() geom_line(data df_extra, aes(linetype group)) scale_color_manual(values c(对照组 #4E79A7, 处理组 #E15759, 外部参考线 black))如果第二组本来就带group列那么它的映射会自然出现在图例里。如果df_extra没有group列可以用一个常数映射去制造图例项比如aes(color 外部参考线)这属于ggplot里非常实用的小技巧。加在这一层之后图例里会多出一个外部参考线的组别颜色和线型都能单独控制。4.2 分面另一种“同图对比”的替代方案不是说所有多组数据都适合硬塞在同一张图里。当组数超过3组或者各组的量纲完全不同时同一张图里的折线很容易互相缠绕。这时候用facet_wrap按组别拆成小图反而更清楚ggplot(df, aes(x week, y value, color group)) geom_line() facet_wrap(~ group)分面的好处是每张子图共用x、y轴刻度方便比较绝对数值和变化趋势。同一张图两条线适合展示“两条线的交叉点和差异”分面适合展示“每一组的自身趋势”。我一般先问自己是想要读者对比两组大小还是想看各组走势前者用叠加后者用分面。4.3 两组数据量纲不同能不能用双y轴最容易被问到的问题之一一组数量级是几十另一组是几万怎么画在同一张图很多人的第一反应是双y轴左侧一个刻度、右侧一个刻度。ggplot的scale_y_continuous也可以配合sec_axis做代码能实现但我不推荐。双y轴的坑在于它会通过调整两个轴的缩放比例让两条线看起来有某种“视觉关系”而这种关系是人为设定的。只要改一个轴的比例两条线的走势对比就可能完全变味。如果你在汇报时遇到懂行的听众双y轴几乎一定会被挑战。更稳妥的办法有两种一是把两个变量都转换成标准化指标比如变化率、指数值放在同一个y轴上比较二是用两个分面图上下排列分别画各自的量纲。后者信息量最大也最不容易误导人。5. 高频报错与排查技巧我实际踩过的坑5.1 Aesthetics must be either length 1 or the same as the data这是ggplot新手最容易遇到的报错通常出现在aes()里写了长度不对的向量。比如数据框有200行却写color c(A, B)ggplot不知道这两个值该分配给哪些行。正确做法是把组别信息放进数据框本身再在aes里引用列名。偶尔也有其他原因比如aes里误用了全局向量color group_vector而不是color group_column。排查思路就是检查aes里每一个变量是不是数据框里的列名或者长度为1的常量。5.2 图例丢失或者出现一条渐变色条图例丢失最常见的原因是把color写在了geom_line外部而不是aes里。比如geom_line(color group)这种写法R会把group当作一个固定颜色值而不是映射关系图例自然不出现。正确写是geom_line(aes(color group))。另一个常见问题是color映射到了一个连续数值变量比如color value这时候ggplot生成的是渐变色条而不是分组图例。解决方法是把该变量转成因子或者在映射时使用color as.factor(group)。5.3 x轴顺序乱了字符型还是因子型很多人画月度数据时图上线段的顺序是乱的比如一月、十一月、十二月排在二月前面。原因是x轴被当成字符型ggplot按字母顺序排序了。解决方式有两个df$month - factor(df$month, levels c(一月, 二月, 三月, 四月, 五月, 六月, 七月, 八月, 九月, 十月, 十一月, 十二月))如果数据本身是日期用as.Date处理更自然df$date - as.Date(df$date, format %Y-%m-%d)因子顺序问题常常藏得很深尤其是导入Excel数据后看似时间列其实以文本格式存储。我建议做图前先用str()或summary()检查列类型再决定是否转因子。5.4 中文乱码与导出图片模糊ggplot默认绘图设备对中文支持不稳定经常在出图时出现方块字。我的最佳实践是用showtext包加载字体并把自动模式打开library(showtext) showtext_auto()然后在theme里指定family为已加载的字体名比如theme(text element_text(family SimHei))。如果是在RStudio里运行每一步都能即时预览很方便。导出图片的清晰度问题多半是因为ggsave缺省尺寸太小、dpi太低。我一般用ggsave(两组折线图.png, p, width 7, height 4.5, dpi 300)这样出来的PNG在PPT或汇报文档里放大也不会发虚。SVG格式也类似如果是网页端展示SVG可能更合适。5.5 图例标题和标签改来改去都没用的疑惑图例标题修改看似简单实际很容易迷失。当color、linetype、shape分别映射到同一个组别变量时统一图例只需改一个当它们映射自不同变量时就得一个个用labs或guides设置。最让我头疼的情况是同时映射了color和size结果出现两个图例一个标题叫“组别”另一个叫“组别Size”。解法还是用labs分别指定或者干脆不要映射多余属性。我个人的原则是一个变量最多映射两个视觉属性否则图例会膨胀到难以阅读。6. 我的个人实操心得画图技术上不难难的是每次都站在看图的读者角度反问一句这张图让我十秒内看懂了吗我会定期检查自己的图颜色是否考虑过色弱读者、线型是否在黑白打印后仍能区分、x轴的顺序是不是真的按时间排的、点有没有重叠到看不清密度。这些小问题积累起来才是图从“能看”变成“好用”的关键。另外一个很实用的习惯是任何正式汇报用的图我都会先保存一个低分辨率的快速预览版扫一眼再输出高清版。因为很多字体、图例重叠问题在RStudio预览里不明显放大到海报尺寸就会暴露。先小后大能省很多往返调整的时间。最后再分享一个我自己的小技巧在做多组对比图时把组别变量统一命名为group把所有数值统一命名为value这样代码几乎永远不用改aes只要换数据源就行。长格式加规范命名是我这几年用ggplot最省心的工作流。