第 1 章 使用 ggplot2 进行数据可视化

第 1 章 使用ggplot2进行数据可视化

1.1 简介

简单的图形对数据分析者的启示比任何其他方法都要多。

——John Tukey

本章将教你如何使用 ggplot2 进行数据可视化。R 有好几种绘图工具,但 ggplot2 是其中最优雅、功能最全面的一个。ggplot2 实现了图形语法,这是一套用来描述和构建图形的连贯性语法规则。掌握 ggplot2 后,你便可以在多个场景中使用,从而显著提高工作效率。

在开始学习之前,如果想要了解 ggplot2 背后的更多理论基础,推荐你读一读“A Layered Grammar of Graphics”。

准备工作

本章重点讨论 tidyverse 的一个核心 R 包——ggplot2。为了访问本章用到的数据集、帮助页面和函数,需要先运行以下代码来加载 tidyverse:

library(tidyverse)
#> Loading tidyverse: ggplot2
#> Loading tidyverse: tibble
#> Loading tidyverse: readr
#> Loading tidyverse: purrr
#> Loading tidyverse: dplyr
#> Conflicts with tidy packages --------------------------------
#> filter(): dplyr, stats
#> lag():    dplyr, stats

这一行代码加载了 tidyverse 的核心 R 包。在几乎所有的数据分析任务中,你都会用到这些 R 包。这行代码还会告诉你 tidyverse 中的哪些函数与基础 R 包(或者已加载的其他 R 包)中的函数有冲突。

如果运行这行代码时出现错误消息“there is no package called‘tidyverse’”,那么你需要先安装 tidyverse,然后再运行 library() 函数:

install.packages("tidyverse")
library(tidyverse)

R 包只需安装一次,但每次开始新会话时都要重新加载。

如果想要明确指出某个函数(或数据集)的来源,那么可以使用特殊语法形式 package::function()。例如,ggplot2::ggplot() 明确指出了我们使用的是 ggplot2 包中的 ggplot() 函数。

1.2 第一步

我们使用第一张图来回答问题:大引擎汽车比小引擎汽车更耗油吗?你可能已经有了答案,但应该努力让答案更精确一些。引擎大小与燃油效率之间是什么关系?是正相关,还是负相关?是线性关系,还是非线性关系?

1.2.1 mpg数据框

你可以使用 ggplot2 包中的 mpg 数据框(即 ggplot2::mpg)来检验自己的答案。数据框是变量(列)和观测(行)的矩形集合。mpg 包含了由美国环境保护协会收集的 38 种车型的观测数据。

mpg
#> # A tibble: 234 × 11
#>   manufacturer model displ  year   cyl      trans   drv
#>          <chr> <chr> <dbl> <int> <int>      <chr> <chr>
#> 1         audi    a4   1.8  1999     4   auto(l5)     f
#> 2         audi    a4   1.8  1999     4 manual(m5)     f
#> 3         audi    a4   2.0  2008     4 manual(m6)     f
#> 4         audi    a4   2.0  2008     4   auto(av)     f
#> 5         audi    a4   2.8  1999     6   auto(l5)     f
#> 6         audi    a4   2.8  1999     6 manual(m5)     f
#> # ... with 228 more rows, and 4 more variables:
#> # cty <int>, hwy <int>, fl <chr>, class <chr>

mpg 中包括如下变量。

  • displ:引擎大小,单位为升。
  • hwy:汽车在高速公路上行驶时的燃油效率,单位为英里 / 加仑(mpg)。与燃油效率高的汽车相比,燃油效率低的汽车在行驶相同距离时要消耗更多燃油。

要想了解更多关于 mpg 的信息,可以使用 ?mpg 命令打开其帮助页面。

1.2.2 创建ggplot图形

为了绘制 mpg 的图形,运行以下代码将 displ 放在 x 轴,hwy 放在 y 轴:

ggplot(data = mpg) +
  geom_point(mapping = aes(x = displ, y = hwy))

上图显示出引擎大小(displ)和燃油效率(hwy)之间是负相关关系。换句话说,大引擎汽车更耗油。这张图是证实了你对燃油效率和引擎大小之间关系的假设,还是推翻了它?

在 ggplot2 中,你可以使用 ggplot() 函数开始绘图。ggplot() 创建了一个坐标系,你可以在它上面添加图层。ggplot() 的第一个参数是要在图中使用的数据集。ggplot(data = mpg) 会创建一张空白图,因为这张图没什么意思,所以就不在这里展示了。

ggplot() 中添加一个或多个图层就可以完成这张图。函数 geom_point() 向图中添加一个点层,这样就可以创建一张散点图。ggplot2 中包含了多种几何对象函数,每种函数都可以向图中添加不同类型的图层。你将在本章中学到大量的几何对象函数。

ggplot2 中的每个几何对象函数都有一个 mapping 参数。这个参数定义了如何将数据集中的变量映射为图形属性。mapping 参数总是与 aes() 函数成对出现,aes() 函数的 x 参数和 y 参数分别指定了映射到 x 轴的变量与映射到 y 轴的变量。ggplot2 在 data 参数中寻找映射变量,本例中就是 mpg

1.2.3 绘图模板

我们将上面的代码转换为一个可重用的 ggplot2 绘图模板。要想生成一张图,将以下代码中的尖括号部分替换为数据集、几何对象函数或映射集合即可:

ggplot(data = <DATA>) +
  <GEOM_FUNCTION>(mapping = aes(<MAPPINGS>))

本章其余内容将向你展示如何完成并扩展这个模板,以制作出各种类型的图。我们将从 <MAPPINGS> 部分开始。

1.2.4 练习

(1) 运行 ggplot(data = mpg),你会看到什么?

(2) 数据集 mpg 中有多少行?多少列?

(3) 变量 drv 的意义是什么?使用 ?mpg 命令阅读帮助文件以找出答案。

(4) 使用 hwycyl 绘制一张散点图。

(5) 如果使用 classdrv 绘制散点图,会发生什么情况?为什么这张图没什么用处?

1.3 图形属性映射

图片的最大价值在于促使我们发现从未预料到的事情。

——John Tukey

下图中有一组点(显示为红色)似乎位于线性趋势之外。这些汽车比预期具有更高的里程数。如何解释这种现象呢?

我们可以假设这些汽车是混合动力车。检验这种假设的一个方法是查看每辆汽车的 class 值。mpg 数据集中的 class 变量对汽车进行了分类,比如小型、中型和 SUV。如果那些离群点是混合动力车,那么它们应该分类为小型车,也可能是微型车(注意,这份数据是在混合动力车和 SUV 流行前收集的)。

可以向二维散点图中添加第三个变量,比如 class,方式是将它映射为图形属性。图形属性是图中对象的可视化属性,其中包括数据点的大小、形状和颜色。通过改变图形属性的值,可以用不同的方式来显示数据点(如下图所示)。因为已经使用“value”这个词来表示数据的值,所以下面使用“level”(水平)这个词来表示图形属性的值。我们来改变一个点的大小、形状和颜色的水平,分别让它变小、变为三角形和变为蓝色,如下所示。

通过将图中的图形属性映射为数据集中的变量,可以传达出数据的相关信息。例如,可以将点的颜色映射为变量 class,从而揭示每辆汽车的类型:

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

(如果你更喜欢英式英语,就像 Hadley 一样,那么可以使用 colour 代替 color。)

要想将图形属性映射为变量,需要在函数 aes() 中将图形属性名称和变量名称关联起来。ggplot2 会自动为每个变量值分配唯一的图形属性水平(本例中是唯一的颜色),这个过程称为标度变换。ggplot2 还会添加一个图例,以表示图形属性水平和变量值之间的对应关系。

颜色揭示出的信息是,那些离群点多数是双座汽车。这些汽车不像是混合动力车;实际上,它们是跑车!跑车像 SUV 和皮卡一样配有大引擎,但车身却类似于中型车和小型车,这样就提高了它的里程数。现在想来,这些车不会是混合动力的,因为它们具有大引擎。

在上例中,我们将 class 映射为颜色,但也可以用同样的方式将其映射为点的大小。在下面的示例中,每个点的实际大小表示其所属的类别。这里我们收到一条警告信息,因为将无序变量(class)映射为有序图形属性(size)可不是好主意。

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

或者我们也可以将 class 映射为控制数据点透明度的 alpha 图形属性,还可以将其映射为点的形状。

# 上图
ggplot(data = mpg) +
  geom_point(mapping = aes(x = displ, y = hwy, alpha = class))

# 下图
ggplot(data = mpg) +
  geom_point(mapping = aes(x = displ, y = hwy, shape = class))

SUV 怎么了? ggplot2 只能同时使用 6 种形状。默认情况下,当使用这种图形属性时,多出的变量值将不会出现在图中。

对你所使用的每个图形属性来说,函数 aes() 都可以将其名称与一个待显示变量关联起来。aes() 将图层中使用的每个图形属性映射集合在一起,然后传递给该图层的映射参数。这一语法强调了关于 xy 的重要信息:数据点的 x 轴位置和 y 轴位置本身就是图形属性,即可以映射为变量来表示数据信息的可视化属性。

一旦映射了图形属性,ggplot2 会处理好其余的事情。它会为图形属性选择一个合适的标度,并创建图例来表示图形属性水平和变量值之间的映射关系。ggplot2 不会为 xy 这两个图形属性创建图例,而会创建带有刻度标记和标签的坐标轴。坐标轴就相当于图例,可以体现出位置和变量值之间的映射关系。

还可以手动为几何对象设置图形属性。例如,我们可以让图中的所有点都为蓝色:

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

此时颜色不会传达关于变量的信息,只是改变图的外观。要想手动设置图形属性,需要按名称进行设置,将其作为几何对象函数的一个参数。这也就是说,需要在函数 aes() 的外部进行设置。此外,还需要为这个图形属性选择一个有意义的值。

  • 颜色名称是一个字符串。
  • 点的大小用毫米表示。
  • 点的形状是一个数值,如图 1-1 所示。有些形状相同,比如 0、15 和 22 都是正方形。形状之间的区别在于 colorfill 这两个图形属性。空心形状(0~14)的边界颜色由 color 决定;实心形状(15~20)的填充颜色由 color 决定;填充形状(21~24)的边界颜色由 color 决定,填充颜色由 fill 决定。

{%}

图 1-1:用数值进行标识的 R 的 25 种内置形状

练习

(1) 以下这段代码有什么错误?为什么点不是蓝色的?

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

(2) mpg 中的哪些变量是分类变量?哪些变量是连续变量?(提示:输入 ?mpg 来阅读这个数据集的文档。)当调用 mpg 时,如何才能看到这些信息?

(3) 将一个连续变量映射为 colorsizeshape。对分类变量和连续变量来说,这些图形属性的表现有什么不同?

(4) 如果将同一个变量映射为多个图形属性,会发生什么情况?

(5) stroke 这个图形属性的作用是什么?它适用于哪些形状?(提示:使用 ?geom_point 命令。)

(6) 如果将图形属性映射为非变量名对象,比如 aes(color = displ < 5),会发生什么情况?

1.4 常见问题

当开始运行 R 代码时,你很可能会遇到问题。不用担心,每个人都会遇到问题。我已经编写 R 代码多年了,但还是每天都会写出不能正常运行的代码。

首先,将你需要运行的代码与书中的代码进行仔细对比。R 极其挑剔,即使一个字母放错了位置,也可能会造成问题。确保每个 ( 都有一个 ) 与之匹配,并且每个 " 后面都跟着另一个 "。有时运行了代码却什么也没有发生。检查一下控制台左侧:如果有一个 + 号,那么说明 R 认为你没有输入完整的表达式,正在等待你完成输入。这种情况下,按 Esc 键中止当前执行的命令就可以重新开始。

创建 ggplot2 图形时的一个常见问题是将 + 号放错了位置:+ 必须放在一行代码的末尾,而不是开头。换句话说,请确保你没有粗心地写出以下这样的代码:

ggplot(data = mpg)
+ geom_point(mapping = aes(x = displ, y = hwy))

如果还是有问题,那么可以看看帮助页面。通过在控制台中运行 ? 函数名,或者在 RStudio 中选定函数名称后按 F1 键,你可以获得任何 R 函数的帮助信息。如果帮助页面看上去没什么用,也不要着急,你可以跳过这些帮助信息,向下找到示例部分,并查看与你的需求相匹配的代码。

如果还是没什么用,那么再仔细阅读一下错误消息。有时答案就隐藏在其中!但如果你是 R 语言新手,那么即使答案就在错误消息中,你也很难理解。另一个非常好的工具就是 Google:试着搜索一下错误消息,因为别人很可能也遇到过同样的问题,并在网上得到了答案。

1.5 分面

添加额外变量的一种方法是使用图形属性。另一种方法是将图分割成多个分面,即可以显示数据子集的子图。这种方法特别适合添加分类变量。

要想通过单个变量对图进行分面,可以使用函数 facet_wrap()。其第一个参数是一个公式,创建公式的方式是在 ~ 符号后面加一个变量名(这里所说的“公式”是 R 中的一种数据结构,不是数学意义上的公式)。传递给 facet_wrap() 的变量应该是离散型的。

ggplot(data = mpg) +
  geom_point(mapping = aes(x = displ, y = hwy)) +
  facet_wrap(~ class, nrow = 2)

要想通过两个变量对图进行分面,需要在绘图命令中加入函数 facet_grid()。这个函数的第一个参数也是一个公式,但该公式包含由 ~ 隔开的两个变量名。

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

如果不想在行或列的维度进行分面,你可以使用 . 来代替变量名,例如 + facet_grid(. ~ cyl)

练习

(1) 如果使用连续变量进行分面,会发生什么情况?

(2) 在使用 facet_grid(drv ~ cyl) 生成的图中,空白单元的意义是什么?它们和以下代码生成的图有什么关系?

ggplot(data = mpg) +
  geom_point(mapping = aes(x = drv, y = cyl))

(3) 以下代码会绘制出什么图? . 的作用是什么?

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

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

(4) 查看本节的第一个分面图:

ggplot(data = mpg) +
  geom_point(mapping = aes(x = displ, y = hwy)) +
  facet_wrap(~ class, nrow = 2)

与使用图形属性相比,使用分面的优势和劣势分别是什么?如果有一个更大的数据集,你将如何权衡这两种方法的优劣?

(5) 阅读 ?facet_wrap 的帮助页面。nrowncol 的功能分别是什么?还有哪些选项可以控制分面的布局?为什么函数 facet_grid() 没有变量 nrowncol

(6) 在使用函数 facet_grid() 时,一般应该将具有更多唯一值的变量放在列上。为什么这么做呢?

1.6 几何对象

以下两张图的相似程度有多大?

两张图有同样的 x 变量和 y 变量,而且描述的是同样的数据。但这两张图并不一样,它们各自使用不同的可视化对象来表示数据。在 ggplot2 语法中,我们称它们使用了不同的几何对象

几何对象是图中用来表示数据的几何图形对象。我们经常根据图中使用的几何对象类型来描述相应的图。例如,条形图使用了条形几何对象,折线图使用了直线几何对象,箱线图使用了矩形和直线几何对象。散点图打破了这种趋势,它们使用点几何对象。如上面的两幅图所示,我们可以使用不同的几何对象来表示同样的数据。左侧的图使用了点几何对象,右侧的图使用了平滑曲线几何对象,以一条平滑曲线来拟合数据。

要想改变图中的几何对象,需要修改添加在 ggplot() 函数中的几何对象函数。举例来说,要想绘制出上图,你可以使用以下代码:

# 左图
ggplot(data = mpg) +
  geom_point(mapping = aes(x = displ, y = hwy))

# 右图
ggplot(data = mpg) +
  geom_smooth(mapping = aes(x = displ, y = hwy))

ggplot2 中的每个几何对象函数都有一个 mapping 参数。但是,不是每种图形属性都适合每种几何对象。你可以设置点的形状,但不能设置线的“形状”,而可以设置线的线型。

geom_smooth() 函数可以按照不同的线型绘制出不同的曲线,每条曲线对应映射到线型的变量的一个唯一值:

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

{%}

根据表示汽车驱动系统的 drv 变量的值,这里的 geom_smooth() 函数分别用 3 条曲线来表示汽车。一条线表示 drv 值为 4 的所有汽车,一条线表示 drv 值为 f 的所有汽车,另一条线表示 drv 值为 r 的所有汽车。其中 4 表示四轮驱动,f 表示前轮驱动,r 表示后轮驱动。

如果你觉得这有些难以理解,我们可以将这些曲线覆盖在原始数据上,并按照 drv 值对所有的点和线进行着色,这样你就能看得更清楚一些了。

{%}

注意,我们刚才在同一张图中使用了两种几何对象,多么激动人心!稍安勿躁,我们将在下一节中学习如何在同一张图中放置多个几何对象。

ggplot2 提供了 30 多种几何对象,其扩展包甚至提供了更多(可以在 https://www.ggplot2-exts.org 查看更多样例)。如果想全面地了解这些对象,最好的方式是学习 ggplot2 速查表(参见 http://rstudio.com/cheatsheets)。如果想掌握更多关于某个几何对象的知识,那么可以使用帮助,如 ?geom_smooth

geom_smooth() 一样,很多几何对象函数使用单个几何对象来表示多行数据。你可以将这些几何对象的 group 图形属性设置为一个分类变量,这样 ggplot2 就会为这个分类变量的每个唯一值绘制一个独立的几何对象。实际上,只要将一个图形属性映射为一个离散变量(如上个示例中的 linetype),ggplot2 就会自动对数据进行分组来绘制多个几何对象。

这个功能非常方便,因为按照图形属性的这种分组不用添加图例,也不用为几何对象添加区分特征:

ggplot(data = mpg) +
  geom_smooth(mapping = aes(x = displ, y = hwy))

ggplot(data = mpg) +
  geom_smooth(mapping = aes(x = displ, y = hwy, group = drv))

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

要想在同一张图中显示多个几何对象,可以向 ggplot() 函数中添加多个几何对象函数:

ggplot(data = mpg) +
  geom_point(mapping = aes(x = displ, y = hwy)) +
  geom_smooth(mapping = aes(x = displ, y = hwy))

但是,这样代码就产生了一些重复。假如你想将 y 轴上的变量从 hwy 改成 cty,那么就要在两个地方修改这个变量,但你或许会漏掉一处。避免这种重复的方法是将一组映射传递给 ggplot() 函数。ggplot2 会将这些映射作为全局映射应用到图中的每个几何对象中。换句话说,以下代码将绘制出与上面代码同样的图:

ggplot(data = mpg, mapping = aes(x = displ, y = hwy)) +
  geom_point() +
  geom_smooth()

如果将映射放在几何对象函数中,那么 ggplot2 会将其看作这个图层的局部映射,它将使用这些映射扩展或覆盖全局映射,但仅对该图层有效。这样一来,我们就可以在不同的图层中显示不同的图形属性:

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

同理,你也可以为不同的图层指定不同的数据。下图中的平滑曲线表示的只是 mpg 数据集的一个子集,即微型车。geom_smooth() 函数中的局部数据参数覆盖了 ggplot() 函数中的全局数据参数,当然仅对这个图层有效:

ggplot(data = mpg, mapping = aes(x = displ, y = hwy)) +
  geom_point(mapping = aes(color = class)) +
  geom_smooth(
    data = filter(mpg, class == "subcompact"),
    se = FALSE
  )

(你将在下一章中学习 filter() 函数的用法,现在只需要知道这个命令仅选取出微型车就够了。)

练习

(1) 在绘制折线图、箱线图、直方图和分区图时,应该分别使用哪种几何对象?

(2) 在脑海中运行以下代码,并预测会有何种输出。接着在 R 中运行代码,并检查你的预测是否正确。

ggplot(
  data = mpg,
  mapping = aes(x = displ, y = hwy, color = drv)
) +
  geom_point() +
  geom_smooth(se = FALSE)

(3) show.legend = FALSE 的作用是什么?删除它会发生什么情况?你觉得我为什么要在本章前面的示例中使用这句代码?

(4) geom_smooth() 函数中的 se 参数的作用是什么?

(5) 以下代码生成的两张图有什么区别吗?为什么?

ggplot(data = mpg, mapping = aes(x = displ, y = hwy)) +
  geom_point() +
  geom_smooth()

ggplot() +
  geom_point(
    data = mpg,
    mapping = aes(x = displ, y = hwy)
  ) +
  geom_smooth(
    data = mpg,
    mapping = aes(x = displ, y = hwy)
  )

(6) 自己编写 R 代码来生成以下各图。

1.7 统计变换

接下来我们看一下条形图。条形图虽然简单,但很有意思,因为它可以揭示出图形中的一些微妙信息。我们看一下用 geom_bar() 函数就可以绘制的基本条形图。下面的条形图显示了 diamonds 数据集中按照 cut 变量分组的各种钻石的总数量。diamonds 数据集是 ggplot2 的内置数据集,包含大约 54 000 颗钻石的信息,每颗钻石具有 pricecaratcolorclaritycut 变量。条形图显示,高质量切割钻石的数量要比低质量切割钻石的数量多:

ggplot(data = diamonds) +
  geom_bar(mapping = aes(x = cut))

条形图 x 轴显示的是 cut,这是 diamonds 数据集中的一个变量。y 轴显示的是 count,但 count 不是 diamonds 中的变量!那么 count 来自哪里呢?很多图形绘制的是数据集的原始数据,比如散点图。另外一些图形则可以绘制那些计算出的新数据,比如条形图。

  • 条形图、直方图和频率多边形图可以对数据进行分箱,然后绘制出分箱数量和落在每个分箱的数据点的数量。
  • 平滑曲线会为数据拟合一个模型,然后绘制出模型预测值。
  • 箱线图可以计算出数据分布的多种摘要统计量,并显示一个特殊形式的箱体。

绘图时用来计算新数据的算法称为 stat(statistical transformation,统计变换)。下图描述了 geom_bar() 函数的统计变换过程。

通过查看 stat 参数的默认值,你可以知道几何对象函数使用了哪种统计变换。例如,?geom_bar 显示出 stat 的默认值是 count,这说明 geom_bar() 使用 stat_count() 函数进行统计变换。stat_count() 在文档中与 geom_bar() 位于同一页,如果继续向下看,你可以发现名为“Computed variables”的一节,它告诉我们 stat_count() 会计算出两个新变量:countprop

通常来说,几何对象函数和统计变换函数可以互换使用。例如,你可以使用 stat_count() 替换 geom_bar() 来重新生成前面那张图:

ggplot(data = diamonds) +
  stat_count(mapping = aes(x = cut))

可以这样做的原因是,每个几何对象函数都有一个默认统计变换,每个统计变换函数都有一个默认几何对象。一般情况下,这意味着你在使用几何对象函数时不用担心底层的统计变换。想要显式使用某种统计变换的 3 个原因如下。

  • 你可能想要覆盖默认的统计变换。在以下代码中,我们将 geom_bar() 函数的统计变换从计数(默认值)修改为标识。这样我们就可以将条形的高度映射为 y 轴变量的初始值。遗憾的是,当随意说起条形图时,人们指的可能就是这种条形图,其中条形高度已经存在于数据中,而不是像前一个图一样,条形高度由对行进行计数来生成:

    demo <- tribble(
      ~a,      ~b,
      "bar_1", 20,
      "bar_2", 30,
      "bar_3", 40
    )
     
    ggplot(data = demo) +
      geom_bar(
        mapping = aes(x = a, y = b), stat = "identity"
      )
    
    

    (你还不知道 <-tibble() 的含义?别担心,根据上下文就能猜出它们的含义,而且你很快就会学习它们了。)

  • 你可能想要覆盖从统计变换生成的变量到图形属性的默认映射。例如,你或许想显示一张表示比例(而不是计数)的条形图:

    ggplot(data = diamonds) +
      geom_bar(
        mapping = aes(x = cut, y = ..prop.., group = 1)
      )
    
    

    如果想要找出由统计变换计算出的变量,可以查看帮助文件中的“Computed variables”一节。

  • 你可能想要在代码中强调统计变换。例如,你可以使用 stat_summary() 函数将人们的注意力吸引到你计算出的那些摘要统计量上。stat_summary() 函数为 x 的每个唯一值计算 y 值的摘要统计:

    ggplot(data = diamonds) +

     stat_summary(
       mapping = aes(x = cut, y = depth),
       fun.ymin = min,
       fun.ymax = max,
       fun.y = median
    

    )

ggplot2 提供了 20 多个统计变换以供你使用。每个统计变换都是一个函数,因此你可以按照通用方式获得帮助,例如 ?stat_bin。如果想要查看全部的统计变换,可以使用 ggplot2 速查表。

练习

(1) stat_summary() 函数的默认几何对象是什么?不使用统计变换函数的话,如何使用几何对象函数重新生成以上的图?

(2) geom_col() 函数的功能是什么?它和 geom_bar() 函数有何不同?

(3) 多数几何对象和统计变换都是成对出现的,总是配合使用。仔细阅读文档,列出所有成对的几何对象和统计变换。它们有什么共同之处?

(4) stat_smooth() 函数会计算出什么变量?哪些参数可以控制它的行为?

(5) 在比例条形图中,我们需要设定 group = 1,这是为什么呢?换句话说,以下两张图会有什么问题?

ggplot(data = diamonds) +
  geom_bar(mapping = aes(x = cut, y = ..prop..))
ggplot(data = diamonds) +
  geom_bar(
    mapping = aes(x = cut, fill = color, y = ..prop..)
  )

1.8 位置调整

条形图还有一项神奇的功能,你可以使用 color 或者 fill(这个更有用)图形属性来为条形图上色:

ggplot(data = diamonds) +
  geom_bar(mapping = aes(x = cut, color = cut))
ggplot(data = diamonds) +
  geom_bar(mapping = aes(x = cut, fill = cut))

注意,如果将 fill 图形属性映射到另一个变量(如 clarity),那么条形会自动分块堆叠起来。每个彩色矩形表示 cutclarity 的一种组合。

ggplot(data = diamonds) +
  geom_bar(mapping = aes(x = cut, fill = clarity))

这种堆叠是由 position 参数设定的位置调整功能自动完成的。如果不想生成堆叠式条形图,你还可以使用以下 3 种选项之一:"identity""fill""dodge"

  • position = "identity" 将每个对象直接显示在图中。这种方式不太适合条形图,因为条形会彼此重叠。为了让重叠部分能够显示出来,我们可以设置 alpha 参数为一个较小的数,从而使得条形略微透明;或者设定 fill = NA,让条形完全透明:

    ggplot(
      data = diamonds,
      mapping = aes(x = cut, fill = clarity)
    ) +
      geom_bar(alpha = 1/5, position = "identity")
    ggplot(
      data = diamonds,
      mapping = aes(x = cut, color = clarity)
    ) +
      geom_bar(fill = NA, position = "identity")
    

    标识位置调整更适合 2D 几何对象,比如,点的标识位置调整是默认设置。

  • position = "fill" 的效果与堆叠相似,但每组堆叠条形具有同样的高度,因此这种条形图可以非常轻松地比较各组间的比例:

    ggplot(data = diamonds) +
      geom_bar(
        mapping = aes(x = cut, fill = clarity),
        position = "fill"
      )
    
    

  • position = "dodge" 将每组中的条形依次并列放置,这样可以非常轻松地比较每个条形表示的具体数值:

    ggplot(data = diamonds) +
      geom_bar(
        mapping = aes(x = cut, fill = clarity),
        position = "dodge"
      )
    
    

此外还有一种位置调整,虽然不适合条形图,但非常适合散点图。回忆一下我们的第一张散点图。你是否发现,虽然数据集中有 234 个观测值,但散点图中只显示了 126 个点?

因为 hwydispl 的值都进行了舍入取整,所以这些点显示在一个网格上时,很多点彼此重叠了。这个问题称为过绘制。点的这种排列方式很难看出数据的聚集模式。数据点是均匀地分布在图中,还是存在 hwydispl 的特殊组合,其中包括了 109 个点?

通过将位置调整方式设为“抖动”,可以避免这种网格化排列。position = "jitter" 为每个数据点添加一个很小的随机扰动,这样就可以将重叠的点分散开来,因为不可能有两个点会收到同样的随机扰动:

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

添加随机性来改善图形似乎是一种奇怪的方式,然而尽管这种方式会损失图形的精确性,但可以大大提高图形的启发性。因为这种操作的用处非常大,所以 ggplot2 提供了 geom_point(position = "jitter") 的一种快速实现方式:geom_jitter()

要想了解有关位置调整的更多信息,可以查看每种调整方式的帮助页面:?position_dodge?position_fill?position_identity?position_jitter?position_stack

练习

(1) 以下图形有什么问题?应该如何改善?

ggplot(data = mpg, mapping = aes(x = cty, y = hwy)) +
  geom_point()

{%}

(2) geom_jitter() 使用哪些参数来控制抖动的程度?

(3) 对比 geom_jitter()geom_count()

(4) geom_boxplot() 函数的默认位置调整方式是什么?创建 mpg 数据集的可视化表示来演示一下。

1.9 坐标系

坐标系可能是 ggplot2 中最复杂的部分。默认的坐标系是笛卡儿直角坐标系,可以通过其独立作用的 x 坐标和 y 坐标找到每个数据点。偶尔也会用到一些其他类型的坐标系。

  • coord_flip() 函数可以交换 x 轴和 y 轴。当想要绘制水平箱线图时,这非常有用。它也非常适合使用长标签,但要想在 x 轴上不重叠地安排好它们是非常困难的:

    ggplot(data = mpg, mapping = aes(x = class, y = hwy)) +
      geom_boxplot()
    ggplot(data = mpg, mapping = aes(x = class, y = hwy)) +
      geom_boxplot() +
      coord_flip()
    
    

  • coord_quickmap() 函数可以为地图设置合适的纵横比。当使用 ggplot2 绘制空间数据时,这个函数特别重要(遗憾的是本书不涉及空间数据):

    nz <- map_data("nz")
     
    ggplot(nz, aes(long, lat, group = group)) +
      geom_polygon(fill = "white", color = "black")
     
    ggplot(nz, aes(long, lat, group = group)) +
      geom_polygon(fill = "white", color = "black") +
      coord_quickmap()
    
    

  • coord_polar() 函数使用极坐标系。极坐标系可以揭示出条形图和鸡冠花图间的一种有趣联系:

    bar <- ggplot(data = diamonds) +
      geom_bar(
        mapping = aes(x = cut, fill = cut),
        show.legend = FALSE,
        width = 1
      ) +
      theme(aspect.ratio = 1) +
      labs(x = NULL, y = NULL)
     
    bar + coord_flip()
    bar + coord_polar()
    

练习

(1) 使用 coord_polar() 函数将堆叠式条形图转换为饼图。

(2) labs() 函数的功能是什么?阅读一下文档。

(3) coord_quickmap() 函数和 coord_map() 函数的区别是什么?

(4) 下图表明城市和公路燃油效率之间有什么关系?为什么 coord_fixed() 函数很重要?geom_abline() 函数的作用是什么?

ggplot(data = mpg, mapping = aes(x = cty, y = hwy)) +
  geom_point() +
  geom_abline() +
  coord_fixed()

{%}

1.10 图形分层语法

在前面几节中,你学到的绝不仅仅是如何绘制散点图、条形图和箱线图,而是使用 ggplot2 绘制任何类型图形的基础知识。为了说明这一点,我们向前面的代码模板中添加位置调整、统计变换、坐标系和分面:

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

新模板有 7 个参数,即模板中尖括号内的部分。实际上,绘图时几乎不需要提供所有的 7 个参数,因为除了数据、映射和几何对象函数,ggplot2 为所有其他参数提供了非常有用的默认设置。

模板中的 7 个参数一同组成了图形语法,即用于建立图形的一个正式语法系统。你可以将任何图形精确地描述为数据集、几何对象、映射集合、统计变换、位置调整、坐标系和分面模式的一个组合,图形语法正是基于这样的深刻理解构建出来的。

为了说明图形语法的工作方式,我们看一下如何从头开始构建一个基本图形:首先需要有一个数据集,然后(通过统计变换)将其转换为想要显示的信息。

接下来,你可以选择一个几何对象来表示转换后的数据中的每个观测值,然后选择几何对象的图形属性来表示数据中的变量,这会将每个变量的值映射为图形属性的水平。

下一步是选择放置几何对象的坐标系。你可以使用对象位置(对象本身的一个图形属性)来显示 x 变量和 y 变量的值。这样就生成了一张完整的图。但你还可以进一步调整几何对象在坐标系中的位置(位置调整),或者将图划分为多个子图(分面)。你还可以通过添加一个或多个附加图层对图进行扩展,其中每个附加图层都使用一个数据集、一个几何对象、一个映射集合、一个统计变换和一个位置调整。

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

目录

  • 版权声明
  • O'Reilly Media, Inc. 介绍
  • 前言
  • 第一部分 探索
  • 第 1 章 使用 ggplot2 进行数据可视化
  • 第 2 章 工作流:基础
  • 第 3 章 使用 dplyr 进行数据转换
  • 第 4 章 工作流:脚本
  • 第 5 章 探索性数据分析
  • 第 6 章 工作流:项目
  • 第二部分 数据处理
  • 第 7 章 使用 tibble 实现简单数据框
  • 第 8 章 使用 readr 进行数据导入
  • 第 9 章 使用 dplyr 处理关系数据
  • 第 10 章 使用 stringr 处理字符串
  • 第 11 章 使用 forcats 处理因子
  • 第 12 章 使用 lubridate 处理日期和时间
  • 第三部分 编程
  • 第 13 章 使用 magrittr 进行管道操作
  • 第 14 章 函数
  • 第 15 章 向量
  • 第 16 章 使用 purrr 实现迭代
  • 第四部分 模型
  • 第 17 章 使用 modelr 实现基础模型
  • 第 18 章 模型构建
  • 第 19 章 使用 purrr 和 broom 处理多个模型
  • 第五部分 沟通
  • 第 20 章 R Markdown
  • 第 21 章 使用 ggplot2 进行图形化沟通
  • 第 22 章 R Markdown 输出类型
  • 第 23 章 R Markdown 工作流
  • 作者简介
  • 封面简介