R语言的层

https://wp.me/p80aHo-2wM

前言

在前面,你学到的不仅仅是如何制作散点图、柱状图和箱线图。你学到的是一个基础,这个基础可以让你用 ggplot2 制作任何类型的图形。

在本章中,你将继续扩展这一基础,学习分层的图形语法。我们将先深入了解美学映射、几何对象和分面。然后,你将学习 ggplot2 在绘图时在底层自动完成的统计变换。这些变换用于计算要绘制的新数值,例如柱状图中柱子的高度或箱线图中的中位数。你还将学习位置调整,它会改变几何对象在图中的显示方式。最后,我们将简要介绍坐标系。

我们不会逐一介绍这些层中的每一个函数和选项,但会带你了解 ggplot2 提供的最重要、最常用的功能,并向你介绍一些可以扩展 ggplot2 的包。

美学映射

请记住,ggplot2 包自带的 mpg 数据框包含 38 款汽车模型的 234 个观测值。

mpg 中的变量包括:

displ:汽车发动机排量,单位为升。数值型变量。

hwy:汽车在高速公路上的燃油效率,单位为每加仑英里数(mpg)。在相同距离下,燃油效率低的汽车比燃油效率高的汽车消耗更多燃料。数值型变量。

class:汽车类型。分类变量。

让我们先可视化不同汽车类型中 displ 和 hwy 之间的关系。我们可以用散点图来实现:将数值型变量映射到 x 和 y 美学,将分类变量映射到颜色或形状等美学属性。

ggplot(mpg, aes(x = displ, y = hwy, color = class)) +
geom_point()
ggplot(mpg, aes(x = displ, y = hwy, shape = class)) +
geom_point()

当将 class 映射到形状时,我们会得到两个警告:

1:形状调色板最多只能处理 6 个离散值,因为超过 6 个就会变得难以区分;而你有 7 个。若必须使用这些形状,请考虑手动指定。

2:移除了 62 行包含缺失值的记录(geom_point())。

由于 ggplot2 一次只会使用 6 种形状,默认情况下,当你使用形状美学时,额外的组将不会被绘制。第二个警告与此有关——数据集中有 62 辆 SUV,但它们没有被绘制出来。

同样,我们也可以把 class 映射到 size 或 alpha 美学上,分别控制点的大小和透明度。

ggplot(mpg, aes(x = displ, y = hwy, size = class)) +
geom_point()
#> Warning: Using size for a discrete variable is not advised.
ggplot(mpg, aes(x = displ, y = hwy, alpha = class)) +
geom_point()
#> Warning: Using alpha for a discrete variable is not advised.

这两种做法也都会产生警告:

不建议将 alpha 用于离散变量。

将无序的离散(分类)变量(class)映射到有序的美学属性(size 或 alpha)通常不是一个好主意,因为这会暗示一种实际上并不存在的排序关系。

一旦你完成了美学映射,ggplot2 会自动处理其余部分。它会为该美学选择一个合适的尺度,并构建一个图例来解释水平与数值之间的映射。对于 x 和 y 美学,ggplot2 不会创建图例,但会创建带有刻度和标签的坐标轴。坐标轴提供的信息与图例相同;它解释了位置与数值之间的映射关系。

你也可以直接在 geom 函数的参数中(也就是 aes() 外部)手动设置几何对象的视觉属性,而不是依赖变量映射来决定外观。例如,我们可以把图中的所有点都设为蓝色:

ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point(color = "blue")

这里,颜色并不传达关于某个变量的信息,而只是改变图形的外观。你需要为该美学属性选择一个合适的值:

  • 颜色的名称,写成字符串,例如 color = "blue"
  • 点的大小,单位为毫米,例如 size = 1
  • 点的形状,写成数字,例如 shape = 1

到目前为止,我们已经讨论了在使用点几何对象绘制散点图时,可以映射或设置的各种美学属性。你可以在美学规范简介(vignette)中了解更多关于所有可能的美学映射:https://ggplot2.tidyverse.org/articles/ggplot2-specs.html。

你在图中可以使用的具体美学属性取决于你用来表示数据的几何对象。在下一节中,我们将更深入地介绍几何对象。

练习

  • 创建一个 hwy 对 displ 的散点图,点为粉色填充三角形。
ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point(color = "pink", shape = 17)
  • 为什么下面的代码没有生成一个带有蓝色点的图?
ggplot(mpg) +
geom_point(aes(x = displ, y = hwy, color = "blue"))
# 正确
ggplot(mpg) +
geom_point(aes(x = displ, y = hwy), color = "blue")
  • stroke 美学属性有什么作用?它适用于哪些形状?(提示:使用 ?geom_point

stroke 用来控制点的边框线宽

它主要适用于 21–25 号形状,也就是既有填充色又有边框的点形状。
例如:

  • shape = 21 圆
  • shape = 22 方形
  • shape = 23 菱形
  • shape = 24 上三角
  • shape = 25 下三角

对这些形状,stroke 会影响边框粗细;对其他大多数形状,作用不明显或不起作用。

几何对象

这两幅图有什么相似之处?

这两个图都包含相同的 x 变量、相同的 y 变量,而且都描述了同一组数据。但这些图并不完全相同。每个图都使用不同的几何对象 geom 来表示数据。左边的图使用 point geom,右边的图使用 smooth geom,即一条拟合数据的平滑线。

要更改图中的 geom,只需修改你添加到 ggplot() 中的 geom 函数。例如,要生成上面的图,你可以使用以下代码:

# Left
ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point()
# Right
ggplot(mpg, aes(x = displ, y = hwy)) +
geom_smooth()
#> `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

ggplot2 中的每个 geom 函数都接受一个映射参数,这个参数可以在 geom 层中局部定义,也可以在 ggplot() 层中全局定义。不过,并不是每种美学都能适用于每个 geom。你可以设置点的形状,但不能设置线的“形状”。如果你尝试这样做,ggplot2 会静默忽略该美学映射。另一方面,你可以设置线的线型。geom_smooth() 会根据你映射到 linetype 的变量的每个唯一值,绘制一条具有不同线型的不同线。

ggplot(mpg, aes(x = displ, y = hwy, shape = drv)) +
geom_smooth()
ggplot(mpg, aes(x = displ, y = hwy, linetype = drv)) +
geom_smooth()

这里,geom_smooth() 根据汽车的 drv 值将汽车分成三条线,drv 值描述的是汽车的传动系统。一条线描述所有 4 值的点,一条线描述所有 f 值的点,还有一条线描述所有 r 值的点。这里,4 代表四轮驱动,f 代表前轮驱动,r 代表后轮驱动。

如果这听起来有些奇怪,我们可以通过将这些线叠加在原始数据之上,并按 drv 对所有内容着色,来让它更清楚一些。

ggplot(mpg, aes(x = displ, y = hwy, color = drv)) +
geom_point() +
geom_smooth(aes(linetype = drv))

注意,这张图在同一个图形中包含了两个 geom。

许多 geom,比如 geom_smooth(),使用单个几何对象来显示多行数据。对于这些 geom,你可以将 group 美学映射设置为一个分类变量,以绘制多个对象。ggplot2 会为分组变量的每个唯一值绘制一个单独的对象。实际上,只要你将某个美学映射到离散变量,ggplot2 就会自动对这些 geom 的数据进行分组(如线型示例所示)。依赖这一特性很方便,因为单独使用 group 美学映射并不会为 geom 添加图例或区分特征。

ggplot(mpg, aes(x = displ, y = hwy)) +
geom_smooth()
ggplot(mpg, aes(x = displ, y = hwy)) +
geom_smooth(aes(group = drv))
ggplot(mpg, aes(x = displ, y = hwy)) +
geom_smooth(aes(color = drv), show.legend = FALSE)

如果你在某个 geom 函数中放置映射,ggplot2 会将它们视为该图层的局部映射。它会使用这些映射来扩展或覆盖全局映射,但仅适用于该图层。这使得在不同图层中显示不同的美学属性成为可能。

ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point()
ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point(aes(color = class))
ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point(aes(color = class)) +
geom_smooth()

你可以使用相同的思路为每一层指定不同的数据。这里,我们使用红色点和空心圆圈来突出显示双座汽车。geom_point() 中的局部 data 参数只会覆盖该图层中的全局 data 参数。

ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point() +
geom_point(
data = dplyr::filter(mpg, class == "2seater"),
color = "red"
) +
geom_point(
data = dplyr::filter(mpg, class == "2seater"),
shape = 1, size = 3, color = "red"
)

Geom 是 ggplot2 的基本构建块。通过更改图中的 geom,你可以完全改变图形的外观,而不同的 geom 还能揭示数据的不同特征。例如,下方的直方图和密度图显示,高速公路里程的分布是双峰且右偏的,而箱线图则揭示了两个潜在的离群值。

ggplot(mpg, aes(x = hwy)) +
geom_histogram(binwidth = 2)
ggplot(mpg, aes(x = hwy)) +
geom_density()
ggplot(mpg, aes(x = hwy)) +
geom_boxplot()

ggplot2 提供了 40 多种 geom,但这些并不能涵盖所有可能绘制的图形。如果你需要不同的 geom,我们建议先查看扩展包,看看是否有人已经实现了它(可参见 https://exts.ggplot2.tidyverse.org/gallery/ 获取一些示例)。例如,ggridges 包(https://wilkelab.org/ggridges)可用于制作山脊图,这类图在可视化数值变量在分类变量不同水平下的分布密度时很有用。在下面的图中,我们不仅使用了一个新的 geom(geom_density_ridges()),还将同一个变量映射到了多个美学属性(将 drv 映射到 yfill 和 color),并且设置了一个美学属性(alpha = 0.5)来使密度曲线透明。

library(ggridges)
ggplot(mpg, aes(x = hwy, y = drv, fill = drv, color = drv)) +
geom_density_ridges(alpha = 0.5, show.legend = FALSE)

了解 ggplot2 提供的所有 geom 以及包中所有函数的全面概览的最佳地点,是参考页面:https://ggplot2.tidyverse.org/reference。若想了解某个具体的 geom,可以使用帮助文档(例如,?geom_smooth)。

练习

  • geom_smooth() 中的 se 参数有什么作用?

se 用来控制是否显示平滑曲线的标准误差带

  1. se = TRUE:显示置信带(默认)
  2. se = FALSE:不显示置信带,只画平滑线

分面

在第 1 章中,你学习了使用 facet_wrap() 进行分面,它会根据一个分类变量把图形拆分成多个子图,每个子图显示数据的一个子集。

# 加载 ggplot2
ggplot(mpg, aes(x = displ, y = hwy)) +
# 绘制散点图
geom_point() +
# 按 cyl 变量分面显示
facet_wrap(~cyl)

要根据两个变量的组合对图形进行分面,可以将 facet_wrap() 改为 facet_grid()facet_grid() 的第一个参数同样是一个公式,但这次是双边公式:rows ~ cols

ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point() +
facet_grid(drv ~ cyl)

默认情况下,每个分面共享相同的 x 轴和 y 轴刻度与范围。当你想在不同分面之间比较数据时,这很有用,但如果你想更好地观察每个分面内部的关系,它可能会受到限制。在分面函数中将 scales 参数设置为 "free_x",可以让各列的 x 轴使用不同的刻度;设置为 "free_y",可以让各行的 y 轴使用不同的刻度;设置为 "free",则两者都可以变化。

ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point() +
facet_grid(drv ~ cyl, scales = "free")

练习

  • 如果对连续变量进行分面,会发生什么?

如果对连续变量进行分面,通常会先把它离散化(分成若干组),然后再分别绘图。

不过要注意:

  1. 分面更适合分类变量
  2. 连续变量如果直接用于分面,通常不太合适
  3. 常见做法是先把连续变量分箱,例如按区间分组,再用 facet_wrap() 或 facet_grid() 分面
ggplot(mpg, aes(x = displ, y = cty)) +
geom_point() +
facet_wrap(~ hwy)
  • 上面使用 facet_grid(drv ~ cyl) 绘制的图中,空白单元格是什么意思?运行下面的代码。它们与生成的图有什么关系?

在 facet_grid() 里,. 表示这一边不使用变量分面

  1. drv ~ .:只按行分面,不按列分面
  2. . ~ cyl:只按列分面,不按行分面

统计变换

考虑一个使用 geom_bar() 或 geom_col() 绘制的基本条形图。下图显示了 diamonds 数据集中按切工分组的钻石总数。diamonds 数据集位于 ggplot2 包中,包含约 54,000 颗钻石的信息,包括每颗钻石的价格、克拉、颜色、净度和切工。该图表显示,高质量切工的钻石数量比低质量切工的钻石数量更多。

ggplot(diamonds, aes(x = cut)) +
geom_bar()

在 x 轴上,图表显示的是 cut,这是 diamonds 中的一个变量。在 y 轴上,显示的是 count,但 count 并不是 diamonds 中的变量!count 是从哪里来的?许多图形,比如散点图,绘制的是数据集的原始值。其他图形,比如条形图,会先计算新的值再进行绘图:

条形图、直方图和频率多边形会先对数据进行分箱,然后绘制每个箱中的计数,也就是落入每个箱内的点的数量。

平滑曲线会先对数据拟合一个模型,然后绘制模型的预测值。

箱线图会计算分布的五数概括,然后把这个概括以一种特殊格式的箱体展示出来。

用于为图形计算新值的算法称为 stat,即 statistical transformation(统计变换)的缩写。图中展示了这一过程在 geom_bar() 中是如何工作的。

你可以通过查看 stat 参数的默认值来了解某个 geom 使用了哪种统计变换。例如,?geom_bar 显示 stat 的默认值是 “count”,这意味着 geom_bar() 使用的是 stat_count()stat_count() 的文档和 geom_bar() 在同一页面中。向下滚动,你会看到一个名为 “Computed variables” 的部分,它说明它会计算两个新变量:count 和 prop

每个 geom 都有一个默认的 stat;每个 stat 也有一个默认的 geom。这意味着你通常可以直接使用 geom,而不必担心底层的统计变换。不过,你可能需要显式使用 stat,主要有三个原因:

你可能想覆盖默认的 stat。在下面的代码中,我们把 geom_bar() 的 stat 从 count(默认值)改为 identity。这样就可以把柱子的高度映射到 y 变量的原始值。

diamonds |>
count(cut) |>
ggplot(aes(x = cut, y = n)) +
geom_bar(stat = "identity")

你可能想要覆盖从变换后的变量到美学属性的默认映射。例如,你可能想显示一个比例条形图,而不是计数条形图:

ggplot(diamonds, aes(x = cut, y = after_stat(prop), group = 1)) +
geom_bar()

要找出 stat 可以计算的可能变量,请查看 geom_bar() 帮助文档中标题为 “computed variables” 的部分。

你可能希望在代码中更突出地显示统计变换。例如,你可以使用 stat_summary(),它会对每个唯一的 x 值汇总 y 值,从而突出你正在计算的汇总结果:

# 使用 ggplot2 对 diamonds 数据集进行绘图
ggplot(diamonds) +
# 添加统计汇总层
stat_summary(
# 指定映射:x 轴为 cut,y 轴为 depth
aes(x = cut, y = depth),
# 计算每组的最小值
fun.min = min,
# 计算每组的最大值
fun.max = max,
# 计算每组的中位数
fun = median
)

位置调整

柱状图还有一个魔法。你可以使用 color 美学映射为柱状图着色,不过更有用的是使用 fill 美学映射:

ggplot(mpg, aes(x = drv, color = drv)) +
geom_bar()
ggplot(mpg, aes(x = drv, fill = drv)) +
geom_bar()

注意,如果你将 fill 美学映射到另一个变量,比如 class,会发生什么:柱子会自动堆叠。每个有颜色的矩形代表 drv 和 class 的一种组合。

ggplot(mpg, aes(x = drv, fill = class)) +
geom_bar()

堆叠是通过 position 参数指定的位置调整自动完成的。如果你不想要堆叠柱状图,可以使用另外三个选项之一:"identity""dodge" 或 "fill"

ggplot(mpg, aes(x = drv, fill = class)) +
geom_bar(alpha = 1/5, position = "identity")
ggplot(mpg, aes(x = drv, color = class)) +
geom_bar(fill = NA, position = "identity")

identity 位置调整对二维几何对象(如点)更有用,因为它是默认设置。

position = “fill” 的作用类似于堆叠,但会让每组堆叠柱子的高度相同。这样更容易比较不同组之间的比例。

position = “dodge” 会将重叠的对象并排放置。这样更容易比较各个单独的数值。

ggplot(mpg, aes(x = drv, fill = class)) +
geom_bar(position = "fill")
ggplot(mpg, aes(x = drv, fill = class)) +
geom_bar(position = "dodge")

还有一种调整对柱状图没什么用,但对散点图非常有用。回想一下我们的第一个散点图。你注意到了吗?尽管数据集中有 234 个观测值,但图中只显示了 126 个点。

hwy 和 displ 的底层数值经过了四舍五入,因此这些点会显示在一个网格上,许多点彼此重叠。这个问题称为过度绘制(overplotting)。这种排列方式会使数据分布变得难以观察。数据点是均匀分布在整张图中,还是存在某个特殊的 hwy 和 displ 组合包含 109 个值?

你可以通过将位置调整设置为 “jitter” 来避免这种网格状排列。position = "jitter" 会为每个点添加少量随机噪声。这样可以把点分散开,因为不太可能有两个点获得完全相同数量的随机噪声。

ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point(position = "jitter")

添加随机性似乎是一种奇怪的改进图形的方法,但虽然它会让你的图在小尺度上不那么精确,却会让你的图在大尺度上更能揭示信息。由于这是一个非常有用的操作,ggplot2 为 geom_point(position = "jitter") 提供了一个简写:geom_jitter()

要了解更多关于位置调整的信息,请查看每种调整对应的帮助页面:?position_dodge?position_fill?position_identity?position_jitter 和 ?position_stack

坐标系

坐标系统大概是 ggplot2 中最复杂的部分。默认的坐标系统是笛卡尔坐标系,其中 x 和 y 的位置相互独立,用来确定每个点的位置。还有另外两种坐标系统,有时也会很有用。

coord_quickmap() 会为地理地图正确设置纵横比。如果你使用 ggplot2 绘制空间数据,这一点非常重要。

# 加载新西兰地图数据
nz <- map_data("nz")
# 绘制普通笛卡尔坐标下的新西兰地图
ggplot(nz, aes(x = long, y = lat, group = group)) +
geom_polygon(fill = "white", color = "black")
# 绘制使用快速地图坐标系的新西兰地图
# coord_quickmap() 会尽量保持地图的纵横比例,避免地理形状失真
ggplot(nz, aes(x = long, y = lat, group = group)) +
geom_polygon(fill = "white", color = "black") +
coord_quickmap()

coord_polar() 使用极坐标。极坐标揭示了条形图和 Coxcomb 图之间的一个有趣联系。

# 创建一个基础柱状图对象 bar
# 使用 diamonds 数据集,
# x 轴为 clarity,
# 并用 clarity 填充颜色
# 不显示图例,柱宽设为 1
# 同时设置图形纵横比为 1:1
bar <- ggplot(data = diamonds) +
geom_bar(
mapping = aes(x = clarity, fill = clarity),
show.legend = FALSE,
width = 1
) +
theme(aspect.ratio = 1)
# 显示柱状图
bar
# 将柱状图翻转为水平图
bar + coord_flip()
# 将柱状图转换为极坐标图
bar + coord_polar()

图形的分层语法

我们可以通过添加位置调整、统计变换、坐标系统和分面来扩展你在前面学到的绘图模板:

ggplot(data = <DATA>) +
<GEOM_FUNCTION>(
mapping = aes(<MAPPINGS>),
stat = <STAT>,
position = <POSITION>
) +
<COORDINATE_FUNCTION> +
<FACET_FUNCTION>

我们的新模板包含七个参数,也就是模板中括号里的那些词。实际上,你很少需要同时提供全部七个参数来绘制图形,因为 ggplot2 会为除数据、映射和几何对象函数之外的所有内容提供有用的默认值。

模板中的这七个参数构成了图形语法(grammar of graphics),这是一种用于构建图形的形式化系统。图形语法基于这样一个观点:你可以将任何图形唯一地描述为数据集、几何对象、映射、统计变换、位置调整、坐标系统、分面方案和主题的组合。

为了理解这一点,不妨考虑如何从头构建一个基础图形:你可以先从一个数据集开始,然后将其转换为你想要展示的信息(通过统计变换)。接着,你可以选择一种几何对象来表示转换后数据中的每个观测值。然后,你可以使用几何对象的美学属性来表示数据中的变量,并将每个变量的取值映射到某种美学属性的不同水平。这些步骤如图所示。之后,你还可以选择一个坐标系统来放置这些几何对象,并利用对象的位置(它本身也是一种美学属性)来显示 x 和 y 变量的值。

此时,你已经拥有一个完整的图形,但你还可以进一步调整坐标系统中各个几何对象的位置(位置调整),或者将图形拆分成多个子图(分面)。你也可以通过添加一个或多个额外图层来扩展图形,其中每个额外图层都使用一个数据集、一个几何对象、一组映射、一个统计变换和一个位置调整。

你可以用这种方法构建你能想象到的任何图形。换句话说,你可以使用本章学到的代码模板来构建成千上万种独特的图形。

评论

发表评论

了解 数据控|突破是我们的每一步 的更多信息

立即订阅以继续阅读并访问完整档案。

继续阅读