生态统计做图之 ggplot2 基础
1 学习目标
本讲义只介绍该脚本实际需要的知识,不追求覆盖 ggplot2 的全部功能。读完本讲义后能回答:
- 图使用了哪个数据框?
- 哪些变量放在横轴、纵轴或颜色上?
- 每个图层画了什么?
- 哪些设置影响数据含义,哪些设置只改变外观?
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)
)三层分别是:
geom_jitter():显示每个个体的血红蛋白测量值;stat_summary(fun.data = mean_se):计算均值及其上下一个标准误,画误差条;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 代码,可以依次问:
data =后面是哪一个数据框?- 用
head()、str()能看到哪些列? aes()把哪些变量映射给了x、y、fill等属性?- 变量是数值型还是分类变量?顺序是否合理?
- 使用了哪个
geom_*()?每一行数据会变成什么图形元素? - 是否有统计变换,如计数、分箱或均值?
- 是否有位置调整,如并排、堆积、抖动或平移?
- 是否有多个图层?后面的图层增加了什么?
- 标签是否包含变量名称和单位?
- 主题修改是否只影响外观?
例如:
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 自测
请尝试回答以下问题:
- 为什么
geom_histogram()只需要x,不需要提供y? fill = response写在aes()内与fill = "firebrick"写在外面有何区别?geom_bar(stat = "count")与geom_bar(stat = "identity")分别适合什么数据?- 为什么分组后的观测点常用
geom_jitter(),而不是只用geom_point()? facet_wrap(~ population, scales = "free_y")会给比较带来什么便利和限制?
如果能根据代码准确回答以上问题,就具备了读懂本次 R Lab 的核心 ggplot2 基础。