生态统计做图之 ggplot2 基础

作者

李勤

发布于

2026年10月10日

1 学习目标

本讲义只介绍该脚本实际需要的知识,不追求覆盖 ggplot2 的全部功能。读完本讲义后能回答:

  1. 图使用了哪个数据框?
  2. 哪些变量放在横轴、纵轴或颜色上?
  3. 每个图层画了什么?
  4. 哪些设置影响数据含义,哪些设置只改变外观?

2 一张 ggplot 图的基本结构

ggplot2 采用“图层叠加”的方式作图:

ggplot(data = 数据框, aes(x = 横轴变量, y = 纵轴变量)) +
  geom_xxx() +
  labs(x = "横轴名称", y = "纵轴名称") +
  theme_classic()

可以把它读成:

使用某个数据框,把变量映射到横轴和纵轴,画出指定的几何对象,再设置标签和主题。

例如脚本中的散点图:

ggplot(data = guppyFatherSonData,
       aes(x = fatherOrnamentation, y = sonAttractiveness)) +
  geom_point(size = 3, col = "firebrick") +
  xlab("Father's ornamentation") +
  ylab("Son's attractiveness") +
  theme_classic()
  • data = guppyFatherSonData:使用哪个数据框;
  • aes(...):变量与图形属性的对应关系;
  • geom_point():把每一行观测画成一个点;
  • xlab()、ylab():修改轴标题;
  • theme_classic():使用简洁的外观主题;
  • 每一层之间用 + 连接,+ 必须放在尚未结束的表达式行末。

3 数据框、变量和基本检查

读取 CSV 后,先看数据结构,再作图:

bird_data <- read.csv("R-Labs/chap02e2bDesertBirdAbundance.csv")

head(bird_data)  # 前 6 行
names(bird_data) # 列名
str(bird_data)   # 每列的数据类型
summary(bird_data)

在表达式 bird_data$abundance 中:

  • bird_data 是数据框;
  • $ 表示从数据框中选择一列;
  • abundance 是列名。

在 ggplot(data = bird_data, aes(x = abundance)) 内部已经指定了数据框,所以 aes() 中通常只写列名,不写 bird_data$abundance。

3.1 分类变量与数值变量

ggplot2 会根据变量类型决定坐标轴形式:

  • 数值变量通常使用连续轴;
  • 字符或因子变量通常使用离散的分类轴。

脚本中多次使用:

aes(x = as.character(year), y = spendingPerStudent)

as.character(year) 强制把年份作为类别处理,使每个年份对应一根条。类似地,处理时间虽然写成数字,若代表几个实验组,也可转换为字符或因子:

aes(x = as.character(treatmentTime), y = serotoninLevel)

更正式的做法是提前转换为因子并设置顺序:

dat$treatment <- factor(dat$treatment,
                        levels = c("Control", "Low", "High"))

levels 决定分类轴和图例中的顺序。

4 aes():把变量映射到图形属性

aes() 是 aesthetic mapping,即“美学映射”。常见属性包括:

  • x、y:横轴和纵轴;
  • colour 或 color:点、线或边框颜色;
  • fill:柱、箱体或小提琴内部的填充颜色;
  • shape:点形;
  • size:点或线的大小;
  • alpha:透明度;
  • group:分组。

4.1 映射与固定设置的区别

如果颜色随某个变量变化,把它写在 aes() 内:

ggplot(birdMalariaData,
       aes(x = treatment, fill = response)) +
  geom_bar()

这里不同 response 类别会得到不同填充颜色,并自动生成图例。

如果所有点都使用同一种颜色,把颜色写在 aes() 外:

ggplot(guppyFatherSonData,
       aes(x = fatherOrnamentation, y = sonAttractiveness)) +
  geom_point(color = "firebrick", size = 3)

牢记:

# 按变量着色,会产生图例
aes(color = population)

# 全部设为固定颜色,通常不产生图例
geom_point(color = "firebrick")

不要把固定颜色写成 aes(color = "firebrick"),否则 ggplot2 会把字符串当成一个类别,并产生不必要的图例。

5 几何对象 geom_*()

geom_*() 决定“画什么”。一个图可以叠加多个几何对象。

5.1 散点图:geom_point()

ggplot(locustData,
       aes(x = as.character(treatmentTime), y = serotoninLevel)) +
  geom_point(color = "firebrick", size = 3)

每一行数据对应一个点。如果同组观测值重叠,可以改用抖动点:

geom_jitter(color = "firebrick", size = 3, width = 0.15)

width = 0.15 只让点在横向轻微移动,以便看见重叠点;这种移动没有改变原始测量值,不应被解释为真实的横轴差异。

5.2 直方图:geom_histogram()

ggplot(bird_data, aes(x = abundance)) +
  geom_histogram(
    fill = "firebrick",
    colour = "black",
    binwidth = 50,
    boundary = 0,
    closed = "left"
  )
  • binwidth = 50:每个区间宽 50;
  • boundary = 0:让区间边界从 0 开始对齐;
  • closed = "left":区间包含左端点,不包含右端点;
  • fill:柱内部颜色;
  • colour:柱边框颜色。

直方图用于数值变量,区间宽度会影响所看到的分布形状。它不同于分类变量的条形图。

5.3 箱线图:geom_boxplot()

ggplot(hemoglobinData,
       aes(x = population, y = hemoglobin)) +
  geom_boxplot(fill = "goldenrod1", colour = "black", width = 0.25)

箱线图显示中位数、四分位数、须以及须外的潜在离群值。R/ggplot2 中的须通常不是简单的最大值和最小值。

5.4 小提琴图:geom_violin()

ggplot(hemoglobinData,
       aes(x = population, y = hemoglobin)) +
  geom_violin(fill = "goldenrod1", colour = "black")

小提琴的宽度表示该数值附近估计的数据密度。它能显示分布形状,但小样本时形状可能不稳定,最好结合原始数据点或样本量阅读。

6 条形图最关键的区别:count 与 identity

脚本中出现了两种条形图,它们的数据形式不同。

6.1 原始数据:stat = "count"

如果数据是一行一个个体或事件,ggplot2 需要自行计数:

ggplot(data_tiger, aes(x = activity_ordered)) +
  geom_bar(stat = "count", fill = "firebrick")

此时没有必要提供 y,柱高由每个类别的行数决定。geom_bar() 的默认值就是 stat = "count",因此也可简写为:

geom_bar(fill = "firebrick")

6.2 已汇总数据:stat = "identity"

脚本先用 table() 生成类别频数:

tiger_number <- data.frame(
  sort(table(data_tiger$activity), decreasing = TRUE)
)

生成的数据框含有类别列 Var1 和频数列 Freq。频数已经存在,因此柱高直接使用 Freq:

ggplot(tiger_number, aes(x = Var1, y = Freq)) +
  geom_bar(stat = "identity", fill = "firebrick")

更简洁的等价写法是:

ggplot(tiger_number, aes(x = Var1, y = Freq)) +
  geom_col(fill = "firebrick")

判断方法:

  • 一行一个观测,需要 ggplot2 数数 → geom_bar() / stat = "count";
  • 数据中已有柱高 → geom_col() / stat = "identity"。

7 分组条形图与位置调整

脚本用两个分类变量绘制鸟类疟疾数据:

ggplot(birdMalariaData,
       aes(x = treatment, fill = response)) +
  geom_bar(
    stat = "count",
    position = position_dodge2(preserve = "single")
  )
  • x = treatment:外层分组;
  • fill = response:每个处理内按响应类别填色;
  • position_dodge2():让不同响应类别的柱并排,而不是上下堆积;
  • preserve = "single":尽量保持单根柱的宽度一致。

常见位置设置:

position = "stack"  # 堆积,geom_bar() 的默认值
position = "dodge"  # 并排
position = "fill"   # 每组缩放到总高度 1,比较比例

手动指定填充色:

scale_fill_manual(values = c("firebrick", "goldenrod1"))

如果希望颜色与类别明确对应,建议使用带名称的向量:

scale_fill_manual(values = c(
  "malaria" = "firebrick",
  "no malaria" = "goldenrod1"
))

实际类别名称必须与数据中的 levels(birdMalariaData$response) 一致。

8 图层叠加与 stat_summary()

ggplot2 按代码顺序叠加图层,后写的图层通常画在前一层之上。脚本用三层同时显示原始值、均值和均值的标准误:

ggplot(hemoglobinData,
       aes(x = population, y = hemoglobin)) +
  geom_jitter(
    color = "firebrick", shape = 1,
    alpha = 0.5, size = 1, width = 0.1
  ) +
  stat_summary(
    fun.data = mean_se,
    geom = "errorbar",
    colour = "black", width = 0.1,
    position = position_nudge(x = 0.2)
  ) +
  stat_summary(
    fun = mean,
    geom = "point",
    colour = "firebrick", size = 2,
    position = position_nudge(x = 0.2)
  )

三层分别是:

  1. geom_jitter():显示每个个体的血红蛋白测量值;
  2. stat_summary(fun.data = mean_se):计算均值及其上下一个标准误,画误差条;
  3. stat_summary(fun = mean):计算并画出均值点。

position_nudge(x = 0.2) 把汇总点和误差条稍向右移,避免遮住原始点。

注意:

  • SD 描述样本中个体的离散程度;
  • SE 描述样本均值的不确定性;
  • 图注必须说明误差条表示 SD、SE 还是置信区间;
  • 显示原始数据点通常比只画“均值 ± 误差”提供更多信息。

9 分面:把不同组放入不同小图

脚本将不同人群的直方图分别放入多行面板:

ggplot(hemoglobinData, aes(x = hemoglobin)) +
  geom_histogram(
    fill = "firebrick", colour = "black",
    binwidth = 1, boundary = 0, closed = "left"
  ) +
  facet_wrap(
    ~ population,
    ncol = 1,
    scales = "free_y",
    strip.position = "right"
  )
  • ~ population:每个人群一个面板;
  • ncol = 1:排成一列;
  • scales = "free_y":不同面板可使用不同纵轴范围;
  • strip.position = "right":把人群名称放在右侧。

自由纵轴有利于观察每组自身的形状,但不利于直接比较组间频数大小。若要比较绝对频数,可保留相同纵轴尺度。

10 标签、主题与文字方向

10.1 标签

以下两种写法等价:

xlab("Activity") + ylab("Frequency")
labs(x = "Activity", y = "Frequency")

labs() 还可设置标题、图例标题等:

labs(
  title = "Bird malaria by treatment",
  x = "Treatment",
  y = "Frequency",
  fill = "Response"
)

10.2 主题

theme_classic()

主题改变背景、坐标轴、字体等非数据元素,不会改变数据本身。

脚本用下面的代码旋转拥挤的横轴文字:

theme(
  axis.text.x = element_text(angle = 45, hjust = 1)
)
  • angle:旋转角度;
  • hjust:旋转后文字的水平对齐方式。

标签很多时,也可以考虑水平条形图,而不只是把文字旋转 90 度。

11 保存图对象与显示图形

ggplot 图可以先存入对象:

tiger_plot <- ggplot(tiger_number, aes(x = Var1, y = Freq)) +
  geom_col(fill = "firebrick") +
  theme_classic()

在控制台输入对象名即可显示:

tiger_plot

还可以继续增加图层,而不修改原对象:

tiger_plot + labs(x = "Activity", y = "Frequency")

保存图形可使用:

ggsave(
  filename = "tiger_activity.png",
  plot = tiger_plot,
  width = 7,
  height = 5,
  dpi = 300
)

width 和 height 默认以英寸为单位,dpi = 300 适合多数报告和打印用途。

12 脚本中不是 ggplot2 的代码

该脚本同时使用了 base R 作图函数:

mosaicplot(...)
plot(...)
boxplot(...)
hist(...)

它们不使用 ggplot() + geom_*() 的图层语法。例如:

plot(sonAttractiveness ~ fatherOrnamentation,
     data = guppyFatherSonData,
     pch = 16,
     col = "firebrick")

其中 y ~ x 是 R 的公式语法,读作“用 x 展示或解释 y”。同样的公式语法也可用于:

boxplot(hemoglobin ~ population, data = hemoglobinData)

学习时应先判断当前代码属于哪套系统:

  • 看到 ggplot(...) + geom_*() → ggplot2;
  • 看到 plot()、hist()、boxplot()、mosaicplot() → base R。

两套系统都能完成很多相同任务,但参数名称和组织方式不同。

13 逐段读代码的方法

遇到一段 ggplot2 代码,可以依次问:

  1. data = 后面是哪一个数据框?
  2. 用 head()、str() 能看到哪些列?
  3. aes() 把哪些变量映射给了 x、y、fill 等属性?
  4. 变量是数值型还是分类变量?顺序是否合理?
  5. 使用了哪个 geom_*()?每一行数据会变成什么图形元素?
  6. 是否有统计变换,如计数、分箱或均值?
  7. 是否有位置调整,如并排、堆积、抖动或平移?
  8. 是否有多个图层?后面的图层增加了什么?
  9. 标签是否包含变量名称和单位?
  10. 主题修改是否只影响外观?

例如:

ggplot(data = educationSpending,
       aes(x = as.character(year), y = spendingPerStudent)) +
  geom_bar(stat = "identity", fill = "firebrick") +
  ylab("Education spending ($ per student)") +
  xlab("") +
  theme_classic()

可读成:

使用 educationSpending 数据;把年份转换为分类变量后放在横轴,把每名学生的教育支出放在纵轴;由于柱高已经存储在数据中,直接按数值画柱;所有柱使用固定的砖红色;最后修改轴标签并使用简洁主题。

14 最小知识清单

读懂本次脚本,至少应记住:

  • ggplot(data, aes(...)) 指定数据和变量映射;
  • + 用来增加图层;
  • geom_point()、geom_jitter()、geom_histogram()、geom_boxplot()、geom_violin() 和 geom_bar() 分别画什么;
  • 写在 aes() 内表示随变量变化,写在外面表示固定设置;
  • fill 通常控制内部填充,colour 通常控制点、线或边框;
  • 原始记录计数用 geom_bar(),已有柱高优先用 geom_col();
  • position = "dodge" 并排,"stack" 堆积,"fill" 比较比例;
  • stat_summary() 可以在作图时计算组均值和误差;
  • facet_wrap() 按分类变量拆分为多个小图;
  • labs() 改标签,theme_*() 和 theme() 改外观;
  • 因子的 levels 决定类别顺序;
  • ggplot2 代码与 base R 作图代码属于两套不同语法。

15 自测

请尝试回答以下问题:

  1. 为什么 geom_histogram() 只需要 x,不需要提供 y?
  2. fill = response 写在 aes() 内与 fill = "firebrick" 写在外面有何区别?
  3. geom_bar(stat = "count") 与 geom_bar(stat = "identity") 分别适合什么数据?
  4. 为什么分组后的观测点常用 geom_jitter(),而不是只用 geom_point()?
  5. facet_wrap(~ population, scales = "free_y") 会给比较带来什么便利和限制?

如果能根据代码准确回答以上问题,就具备了读懂本次 R Lab 的核心 ggplot2 基础。