跳到正文

ggplot2图形语法入门:图层、aes映射与几何对象

Excel 里画图是「选中数据 → 插入图表 → 调格式」。ggplot2 不是这个路子。它要求你先说清楚哪一列决定 x 轴、哪一列决定颜色,再说画成点还是线。多写两行代码,换来的是换数据不用重画、改配色只动一行、同一个数据框能出十几张规格统一的图。

ggplot2 的理论基础是 Leland Wilkinson 提出的图形语法(grammar of graphics)。名字听着玄,落到代码里只有三件事:

  • 数据(data):一个数据框,而且必须是长表——一行一个观测,一列一个变量。宽表要先转长表。
  • 映射(aesthetic mapping,即 aes()):把数据框的列对应到图形的视觉属性上,比如 x 位置、y 位置、颜色、大小、透明度、形状。
  • 几何对象(geom):最后真正画出来的东西,点、线、柱子、箱线图。

一句话串起来:数据框的列经过映射交给几何对象,几何对象把它画到画布上。分面、坐标系、标度、主题都是叠在这三层之上的附加系统,后面几篇逐个展开。

mtcars 是 R 内置的 1974 年《Motor Trend》汽车测试数据,32 行、11 列。取车重 wt(单位:千磅)和油耗 mpg(每加仑英里数):

library(ggplot2)
ggplot(data = mtcars, aes(x = wt, y = mpg)) +
geom_point()
图形输出:单面板散点图。x 轴标题 wt,范围 1.5–5.5;y 轴标题 mpg,范围约 10–35。
32 个黑色实心圆点,整体从左上向右下倾斜,车越重油耗越高。
背景为 ggplot2 默认的浅灰底加白色网格线。

三行代码的分工很清楚:data = mtcars 指定数据,aes() 说 wt 管 x、mpg 管 y,geom_point() 说画点。任何一层换掉,图的性质就变了——把 geom_point() 换成 geom_smooth() 就是一条拟合曲线,把 y 换成 hp 就是另一个问题。

有个书写习惯值得一开始就养成:+ 写在行尾,不要写在下一行开头。在控制台里逐行运行时,行首的 + 会被 R 当作一元加号解析,轻则图没画出来,重则报一个跟 ggplot2 毫无关系的错。

这是 ggplot2 最容易踩的坑,没有之一。

# 写法一:color 写在 aes() 里面,属于映射
ggplot(mtcars, aes(x = wt, y = mpg, color = factor(cyl))) +
geom_point()
# 写法二:color 写在 aes() 外面,属于固定值
ggplot(mtcars, aes(x = wt, y = mpg)) +
geom_point(color = "red")
写法一输出:32 个点分成 3 种颜色(cyl = 4 / 6 / 8,默认调色板为桃红、青绿、蓝),
右侧出现标题为 factor(cyl) 的图例。8 缸车型集中在图的下半部分,wt 从 3.17 起、
mpg 全部低于 20;4 缸车型集中在左上,mpg 大多在 21 以上。
写法二输出:同样的 32 个点,全部为红色,没有图例。

为什么 aes() 里的写法这么敏感?因为 aes() 不立即求值,它记录的是一条规则:这个属性由哪一列决定。所以 aes(color = "red") 的后果不是把点画成红色,而是告诉 ggplot2「颜色由这个恒为 "red" 的变量决定」——每个点都会用默认调色板的第一个颜色(一种偏粉的红),而且右侧多出一个写着 red 的图例。判断标准很简单:属性值来自数据列就放进 aes(),是固定常量就放在外面

再说说为什么用 factor(cyl)cyl 本身是数值(4、6、8),直接映射到颜色,ggplot2 会当成连续变量给出渐变色条。想按气缸数分成三组离散颜色,必须先转成因子。同理,iris 里的 Species 本来就是因子,可以直接映射。

还有一对容易混的属性:color 管点和线的颜色,fill 管有面积的图形(柱子、箱体、密度曲线)的填充色。给柱状图上色要用 fill,写 color 只会给柱子描一圈边。

+ 的语义是往画布上追加图层,先加的先画,后加的画在上面。想让回归线压在散点下面,就得先写 geom_smooth()

ggplot(mtcars, aes(x = wt, y = mpg)) +
geom_point(alpha = 0.6) +
geom_smooth(method = "lm", se = FALSE, color = "steelblue")
图形输出:散点图上叠加了一条蓝色直线。散点因 alpha = 0.6 而半透明,重叠处颜色更深。
直线从左上(wt ≈ 1.5、mpg ≈ 30)延伸到右下(wt ≈ 5.5、mpg ≈ 13),
是 mpg 对 wt 的最小二乘回归线。控制台同时打印一行
`geom_smooth()` using formula = 'y ~ x'。

method = "lm" 表示用线性模型拟合。默认的 method = "loess" 是局部加权回归,适合点少、趋势弯曲的数据;观测数超过 1000 时 loess 会明显变慢,ggplot2 会提示改用 method = "gam"se = FALSE 关掉置信带——回归线本身是给读者看趋势的,样本量小的时候那条宽得离谱的阴影只会干扰阅读。

局部映射和全局映射可以共存。写在 ggplot() 里的 aes() 是全局的,所有图层继承;某个图层需要额外或不同的映射,在它自己的 aes() 里写,同名属性以图层内的为准,不同名的则叠加。数据也可以在图层级别覆盖:

ggplot(mtcars, aes(x = wt, y = mpg)) +
geom_point(color = "grey60") +
geom_point(data = subset(mtcars, cyl == 8), color = "firebrick", size = 3)

第二个 geom_point() 换了一份数据,只画 8 缸车型,用更大的红色点标出来。做「整体趋势 + 高亮子集」的图时这个技巧很常用,不需要提前把两份数据合并。

和 base R 的「画完就没了」不同,ggplot2 画出来的东西是一个 R 对象,可以赋值、可以存起来:

p <- ggplot(mtcars, aes(x = wt, y = mpg)) + geom_point()
class(p)
[1] "gg" "ggplot"

它是一个 gg 对象,本质上是列表,+ 做的事情就是往这个列表里追加图层。想知道自己叠了几层,看 length(p$layers) 就行。理解这一点,后面看到「把图存进变量、批量出图、再拼成一张大图」的写法就不会觉得是魔法了。

ggsave("mpg-vs-wt.png", plot = p, width = 6, height = 4, dpi = 300)
Saving 6 x 4 in image

三个参数值得留意。width / height 的单位默认是英寸,也可以写 units = "cm"。图幅决定了字号和点相对于画面的比例:同样一张图压到 3 英寸宽,坐标轴标签就会挤成一团。dpi 只对 PNG、JPEG、TIFF 这类位图有效,存 PDF、SVG 时它不参与计算,因为矢量图放大不会糊。输出格式由扩展名决定,ggsave("fig.pdf") 得到的就是矢量 PDF。

不写 plot = 时,ggsave() 保存的是「最后一次显示出来的图」。在控制台里这样用没问题,在脚本里连着出几十张图时极容易存错,显式传 plot = p 更稳妥。

记住三层结构,ggplot2 就不再是靠背参数使用的工具:数据决定能画什么,映射决定每个视觉属性由谁控制,几何对象决定形状。下一篇把常用几何对象过一遍,见 /r/visualization/geometries/,重点讲 geom_bar()geom_col() 的区别,以及折线图分组要写 group = 还是 color =

如果你也用 Python,同一张散点图在 Matplotlib 里的写法和这里差别不小——那边用 Figure / Axes 对象模型,手动控制坐标轴对象,可以参考 /python/visualization/matplotlib-basics/