跳转至

Topic 3.2 - R 数据可视化基础

1. ggplot2 包语法预览

ggplot2 这个包作图的基本理念是图层的叠加,我们可以通过添加不同的图层来构建出我们想要的图形:

  • 我们来看一段典型的 ggplot2 代码:
ggplot(data = data) +
  geom_point(aes(x = X, y = Y, color = factor)) +
  geom_smooth(aes(x = X, y = Y), method = "lm") +
  labs(x = "Height", y = "Weight", title = "Height vs Weight") +
  • 通过这段代码,大家可以看出来,ggplot2 的代码结构和我们之前接触的 R 代码风格是有些不同的:

    • 我们前几节接触到的 R 代码基本上是一行代码实现一个功能的
    • ggplot2 的代码是通过多行代码来实现一个图形的构建的,这多行代码使用 + 连接在一起
    • 每一个加号就代表一个图层的叠加,这些图层可以是数据层、几何层、统计层、坐标层、主题层等等
    • 通过这种图层的叠加,我们可以非常灵活地构建出我们想要的图形,这也是 ggplot2 的一个非常重要的特点

本次课我们使用 beer 数据来展示数据可视化的基本功能

  • beer 数据是一个关于啤酒销售的数据集,其中包含了不同类型的啤酒在不同时间段的销售情况
  • 我们首先调用一下本次课要用到的包,再加载一下 beer 数据:
library(tidyverse)  # 包含了 ggplot2、dplyr、readr 等常用数据处理和可视化包
library(patchwork)  # 包含了用于组合多个图形的功能:使用 `|` 来并列图形,使用 `/` 来换行图形
library(scales)     # 包含了用于格式化坐标轴的功能
library(ggokabeito) # 包含了色盲友好的调色板,这是本课程的默认调色板

beer <- read_csv("beer.csv")
beer |> head()
| **store <dbl>** | **week <dbl>** | **brand <chr>** | **upc <dbl>** | **qty <dbl>** | **price <dbl>** | **sales_indicator <lgl>** | **city <chr>** | **price_tier <chr>** | **zone <dbl>** | **zip <dbl>** | **address <chr>** | **latitude <dbl>** | **longtitude <dbl>** | **start_of_week <date>** | **is_holiday_week <lgl>** | **imported <chr>** |
|-----------------|----------------|-----------------|---------------|---------------|-----------------|---------------------------|----------------|----------------------|----------------|---------------|-------------------|--------------------|----------------------|--------------------------|---------------------------|--------------------|
| 86              | 91             | Budweiser       | 1820000016    | 23            | 3.49            | FALSE                     | Chicago        | medium               | 2              | 60618         | 3350 Western Ave  | 41.94235           | -87.68999            | 1991-06-06               | FALSE                     | domestic           |
| 86              | 91             | Corona          | 8066095605    | 13            | 5.79            | FALSE                     | Chicago        | medium               | 2              | 60618         | 3350 Western Ave  | 41.94235           | -87.68999            | 1991-06-06               | FALSE                     | imported           |
| 86              | 91             | Lowenbrau       | 3410021505    | 13            | 3.99            | FALSE                     | Chicago        | medium               | 2              | 60618         | 3350 Western Ave  | 41.94235           | -87.68999            | 1991-06-06               | FALSE                     | imported           |
| 86              | 91             | Miller          | 3410000554    | 15            | 3.69            | FALSE                     | Chicago        | medium               | 2              | 60618         | 3350 Western Ave  | 41.94235           | -87.68999            | 1991-06-06               | FALSE                     | domestic           |
| 86              | 92             | Budweiser       | 1820000016    | 46            | 3.49            | FALSE                     | Chicago        | medium               | 2              | 60618         | 3350 Western Ave  | 41.94235           | -87.68999            | 1991-06-13               | FALSE                     | domestic           |
| 86              | 92             | Corona          | 8066095605    | 24            | 5.79            | FALSE                     | Chicago        | medium               | 2              | 60618         | 3350 Western Ave  | 41.94235           | -87.68999            | 1991-06-13               | FALSE                     | imported           |

我们先使用 ggplot2 包中的 ggplot 函数,将 beer 数据传入,看下效果:

ggplot(beer)
  • 运行这段代码后,我们会得到一个空白的图形,那是因为 ggplot(beer) 这一行代码只创建了第一个图层,也就是数据层
  • 但是我们还没有添加任何几何层来展示数据的结构和特征,所以我们看到的只是一个空白的图形
  • 接下来,我们就在这个基础上添加一些几何层来展示数据的结构和特征了

2. ggplot2 包绘制常见图形类型

(1) 绘制箱形图 (Boxplot)

首先,我们来绘制第一个图形,就是箱形图

  • 我们想使用箱形图来展示价格分布,就可以在 ggplot(beer) 的基础上再加一个 geom_boxplot 图层:
ggplot(beer) +
  geom_boxplot(aes(y = price))
  • 在这段代码中:

    • ggplot(beer) 创建了数据层,告诉 ggplot2 我们要使用 beer 数据来绘图
    • geom_boxplot(aes(y = price)) 添加了一个箱形图的几何层,告诉 ggplot2 我们要绘制价格的箱形图
    • aes 函数用来指定映射关系,在这里我们把价格映射到 y 轴上,这样我们就可以看到价格的分布情况了
  • 注意,这里我们不用使用 beer$price 来指定价格列,因为在 ggplot2 中,数据已经被传入了,所以我们直接使用列名就可以了

接下来,我们来加入一个维度,分别展示进口和非进口啤酒的价格分布:

ggplot(beer) +
  geom_boxplot(aes(y = price, x = imported))
  • 这里我们在 aes 函数中加入了 x = imported,这样我们就可以通过 x 轴来区分进口和非进口啤酒的价格分布了
  • 运行这段代码后,我们就可以看到两个箱形图,一个是进口啤酒的价格分布,另一个是非进口啤酒的价格分布了

这里我们可以看出来,aes 函数中:

  • y 参数指定了图像的 y 轴数据怎样延伸,这里我们把价格填入到 y 轴上,这样我们就可以看到价格的分布情况了
  • x 参数指定了图像的 x 轴怎样延伸,这里我们是通过 imported 这个变量来区分进口和非进口啤酒的价格分布的
  • 通过这种方式,我们就可以非常灵活地控制图像的结构和特征了
  • 我们不妨试试把 yx 参数交换一下位置,看看会发生什么:
ggplot(beer) +
  geom_boxplot(aes(y = imported, x = price))
  • 可以看到,这个时候就会变成一个横向的箱形图了
  • 因此,aes 函数中的 x 就决定了图像的 x 轴数据怎样延伸,y 就决定了图像的 y 轴数据怎样延伸,通过交换它们的位置,我们就可以得到不同方向的箱形图了

我们再来看一个例子,我们展示不同品牌啤酒的价格分布:

ggplot(beer) +
  geom_boxplot(aes(y = price, x = brand))

箱形图的解读:

  • 箱形图中要素的含义:

    • 箱体上界:第三个四分位数(Q3: 75%分位数)
    • 箱体中线:中位数(Q2: 50%分位数)
    • 箱体下界:第一个四分位数(Q1: 25%分位数)
    • 上须线:上界 + 1.5 * 四分位距(IQR: Q3 - Q1)范围内的最大值:根据数据动态调整
    • 下须线:下界 - 1.5 * 四分位距(IQR: Q3 - Q1)范围内的最小值:根据数据动态调整
    • 箱体之外的点:异常值(outliers)
  • 例如,我们有这样一组排好序的数据:20, 55, 58, 60, 62, 65, 68, 70, 72, 90

    • Q3 = 69 = (68 + 70) / 2:10 个数中第 75% 的位置就是第 10 * 75% = 7.5 个数的位置,也就是第 7 个数和第 8 个数的平均值了,所以 Q3 就是 (68 + 70) / 2 = 69
    • Q2 = 63.5 = (62 + 65) / 2:10 个数中第 50% 的位置就是第 10 * 50% = 5 个数的位置,也就是第 5 个数和第 6 个数的平均值了,所以 Q2 就是 (62 + 65) / 2 = 63.5
    • Q1 = 57.5 = (55 + 58) / 2:10 个数中第 25% 的位置就是第 10 * 25% = 2.5 个数的位置,也就是第 2 个数和第 3 个数的平均值了,所以 Q1 就是 (55 + 58) / 2 = 57.5
    • IQR = Q3 - Q1 = 69 - 57.5 = 11.5
    • 上须线的范围:Q3 + 1.5 * IQR = 69 + 1.5 * 11.5 = 86.25:但是数据中没有86.25,因此就往下找,找到的第一个小于86.25的数是72,所以上须线的范围就是72了
    • 下须线的范围:Q1 - 1.5 * IQR = 57.5 - 1.5 * 11.5 = 40.25:但是数据中没有40.25,因此就往上找,找到的第一个大于40.25的数是55,所以下须线的范围就是55了
    • 因此,异常值有两个:20和90,因为它们分别小于下须线的范围55和大于上须线的范围72了
  • 根据箱形图,我们可以大致看出数据的分布情况:

    • 如果下须线比上须线长,且箱体中位数偏下,那么数据可能是左偏的(Negatively skewed)
    • 如果上须线比下须线长,且箱体中位数偏上,那么数据可能是右偏的(Positively skewed)
    • 如果上下须线长度差不多,且箱体中位数在中间,那么数据可能是对称分布的(Symmetric)

(2) 绘制直方图 (Histogram)

直方图展示的是数据的分布情况,使用的函数是 geom_histogram

  • 例如,我们想展示每周销量的分布情况,其中可以使用 aes(x = sales) 来指定销量列作为 x 轴数据
  • aes 函数基本不需要指定 y 轴数据,因为直方图的 y 轴默认就是频数或者密度了,所以我们只需要指定 x 轴数据就可以了
ggplot(beer) +
  geom_histogram(aes(x = qty))

geom_histogram 函数中,我们可以使用两种方式来控制直方图的显示效果:

  • bins 参数:这个参数用来指定直方图的箱数,也就是我们把数据分成多少个区间来展示,默认是 30 个箱子
  • binwidth 参数:这个参数用来指定每个箱子的宽度,也就是我们把数据分成多宽的区间来展示,默认是根据数据的范围和箱数自动计算的

  • 我们来对比一下这两种方式的效果:

ggplot(beer) +
  geom_histogram(aes(x = qty), bins = 20)
ggplot(beer) +
  geom_histogram(aes(x = qty), binwidth = 10)

除了指定箱数和箱宽之外,我们还可以通过 fill 参数来指定是否按照某列来将箱体改为堆叠式箱体:

  • 例如,我们可以使用 aes(fill = imported) 来指定按照进口和非进口啤酒来区分不同颜色的箱体,注意 fill 参数是 aes 函数中的一个参数
  • 这样我们就可以看到进口和非进口啤酒的销量分布情况了
ggplot(beer) +
  geom_histogram(aes(x = qty, fill = imported), binwidth = 10)
  • 这个结果表示的含义是,每个箱体我都分为两部分,一部分是进口啤酒的销量分布,另一部分是非进口啤酒的销量分布
  • 比方说第一个箱体的区间是 0-10,那么这个箱体的下半部分的蓝色部分表示非进口啤酒的销量在 0-10 这个区间内的频数,而上半部分的红色部分表示进口啤酒的销量在 0-10 这个区间内的频数

(3) 绘制密度图 (Density Plot)

密度图可以理解为直方图的变体,它是将直方图有棱角的箱体变成了平滑的曲线,使用的函数是 geom_density

ggplot(beer) +
  geom_density(aes(x = qty))

直方图和密度图都是展示数据分布的图形类型,区别在于:

  • 直方图可以准确表示出每个区间内的频数或者密度,但是箱体不连续导致对数据分布的整体趋势不够直观
  • 密度图则是通过平滑曲线来近似表示数据的分布情况,虽然会丢失一些细节东西,但这样表示更直观一些

当然,密度图也可以通过 aes(fill = imported) 来区分进口和非进口啤酒的密度曲线了,这样我们就可以看到不同类型啤酒的销量分布情况了

ggplot(beer) +
  geom_density(aes(x = qty, fill = imported))

(4) 绘制散点图 (Scatter Plot)

散点图展示的是两个连续变量之间的关系,使用的函数是 geom_point

  • geom_point 函数中,我们可以使用 aes(x = X, y = Y),这时的 xy 都必须是数值型的连续变量,这样我们才能在散点图中展示它们之间的关系了
  • 除此之外我们还可以使用 color 参数来指定按照某列来区分不同颜色的点,例如我们可以使用 aes(color = imported) 来指定按照进口和非进口啤酒来区分不同颜色的点了
  • 这里我们简单区分一下 colorfill 这两个参数:

    • color 参数用来指定点的边框颜色
    • fill 参数用来指定点的填充颜色
    • 这里我们使用 color 而不使用 fill 是因为散点图中的点是没有填充颜色的,整个点全是边框
ggplot(beer) +
  geom_point(aes(y = price, x = qty, color = imported)) +
  scale_color_okabe_ito()

在散点图中,经常与散点一起绘制的还有平滑曲线,我们可以使用 geom_smooth 函数来添加平滑曲线:

  • 首先,geom_smooth 中我们要使用 aes(x = X, y = Y) 来指定 x 轴和 y 轴的数据来源,这个基本上是与 geom_point 中的 aes 函数是一样的
  • 接着,在 geom_smooth 函数中,我们还可以添加以下参数:

    • method 参数:这个参数用来指定平滑曲线的拟合方法,常用的有 "lm"(线性模型)、"loess"(局部回归)等,默认是 "loess"
    • se 参数:这个参数用来指定是否显示平滑曲线的置信区间,默认是 TRUE,表示显示置信区间,如果设置为 FALSE,则不显示置信区间
    • color 参数:这个参数用来指定平滑曲线的颜色
ggplot(beer) +
  geom_point(
    aes(y = price, x = qty, color = imported),
    alpha = 0.25
  ) +
  geom_smooth(
    aes(y = price, x = qty),
    method = "lm", 
    color = palette_okabe_ito()[7],
    se = TRUE
  ) +
  geom_smooth( 
    aes(y = price, x = qty),
    method = "loess", 
    color = palette_okabe_ito()[5],
    se = TRUE
  )

事实上,当我们的多个图层都要使用相同的 aes 映射时,我们可以把这个 aes 映射放在 ggplot 函数中,这样就不需要在每个图层中重复指定了:

ggplot(beer, aes(y = price, x = qty, color = imported)) +
  geom_point(alpha = 0.25) +
  geom_smooth(
    method = "lm", 
    color = palette_okabe_ito()[7],
    se = TRUE
  ) +
  geom_smooth( 
    method = "loess", 
    color = palette_okabe_ito()[5],
    se = TRUE
  )

(5) 绘制柱状图 (Bar Plot)

ggplot2 中,柱状图的绘制要麻烦一点,因为我们要先准备用来绘制柱状图的数据:

  • 例如,如果我们想绘制每个牌子的销量柱状图,我们就需要先对数据进行分组和汇总,得到每个牌子的总销量:

  • 大家先简单知道以下代码在做什么就可以了,后面我们会在数据处理的章节中详细讲解这些代码的含义:

beer_summary <- beer |>
  group_by(brand) |>
  summarise(sum_qty = sum(qty))

beer_summary
| **brand <chr>** | **sum_qty <dbl>** |
|-----------------|-------------------|
| Budweiser       | 16576             |
| Corona          | 11117             |
| Lowenbrau       | 17560             |
| Miller          | 30998             |
  • 之后,我们使用 ggplot(beer_summary) 来创建数据层,以它为基准画柱状图,使用 geom_bar 函数:
ggplot(beer_summary) +
  geom_bar(
    aes(y = sum_qty, x = brand), 
    stat = "identity", # 表示使用原始数据的值而不是计数
    fill = palette_okabe_ito()[2],
    color = "black"
  ) +
  scale_y_continuous(labels = scales::comma) +
  labs(
    x = "Brand", 
    y = "Total Quantity Sold"
  ) +
  theme_bw()

在绘制柱状图的时候,如果想要按照某个顺序来排序柱子,也比较麻烦:

  • 大家可以发现,现在默认的柱子排序是按照字典序的,也就是按照品牌名称的字母顺序来排序的
  • 如果我们想按照销量的多少来排序柱子,我们就需要把品牌这一列改为因子类型
  • 回顾我们之前介绍的因子类型数据,这种类型数据是可以指定顺序的,因此我们可以把品牌这一列改为因子类型,并且指定它的顺序:
# 使用 reorder() 函数将 brand 改为因子类型,并按照 sum_qty 的值进行降序排序
beer_summary <- beer_summary |>
  mutate(brand = reorder(brand, -sum_qty))
  • 之后我们再运行一遍之前的柱状图代码,我们就可以看到柱子已经按照销量的多少来排序了,销量最高的品牌在最左边,销量最低的品牌在最右边了:
ggplot(beer_summary) +
  geom_bar(
    aes(y = sum_qty, x = brand), 
    stat = "identity", # 表示使用原始数据的值而不是计数
    fill = palette_okabe_ito()[2],
    color = "black"
  ) +
  scale_y_continuous(labels = scales::comma) +
  labs(
    x = "Brand", 
    y = "Total Quantity Sold"
  ) +
  theme_bw()

3. 将图像赋值为对象

在 R 中,我们可以将整个图像构建的代码赋值为一个对象,这样我们就可以通过这个对象来展示图像了:

  • 例如,我们将之前的箱式图的代码赋值为一个对象 plt

  • 注意,赋值操作并不会让图像立即展示出来,我们需要通过调用这个对象 plt 来展示图像:

plt <- ggplot(beer) +
  geom_boxplot(aes(y = price, x = brand, fill = brand)) +
  scale_fill_okabe_ito()
plt

将图像赋值的最大好处就是可以将图像的构建和展示分开,这样我们就可以在构建图像的过程中进行一些调整和修改,而不需要每次都重新运行整个代码来展示图像了:

  • 比方说,我们在 plt 的基础上,先展示一个 theme_bw() 的版本:
plt + theme_bw()
  • 然后我们再展示一个 theme_minimal() 的版本:
plt + theme_minimal()
  • 通过这种方式,我们就可以非常方便地在不同的主题之间切换了,而不需要每次都重新运行整个代码来展示图像了

4. R 中的分图

(1) 手动分图

在 R 中,我们可以将多个图形放在一个画布上展示,代码虽然有些复杂,但是逻辑比较简单:

  • 首先,我们需要安装并加载 patchwork 这个包,以下的图像组合符号都是 patchwork 包提供的功能
  • 之后,我们只需将每个图像的代码,放入一个括号中,然后使用 | 来连接这些括号中的代码块,就可以将这些图像放在同一行展示了
  • 如果要换行,我们可以使用 / 来换行,然后继续使用 | 来连接下一行的图像代码块
  • 语法结构如下,以下展示了将6个图像分成2行3列的示例:
(1代码) | (2代码) | (3代码) /
(4代码) | (5代码) | (6代码) 

事实上,手动分图不一定非要行列对齐,我们用 | 来表示并列,用 / 来表示换行,可以根据需要来调整图像的排列方式,例如第一行一张图,第二行两张图:

(1代码) /
(2代码) | (3代码) 

例如我们想将箱形图和散点图放在同一行展示,我们就可以这样写:

(
  ggplot(beer) +
  geom_point(aes(y = price, x = qty, color = imported)) +
  scale_color_okabe_ito()
) |
(
  ggplot(beer) +
  geom_boxplot(aes(y = price, x = imported, fill = imported)) +
  scale_fill_okabe_ito() +
  theme(legend.position = "none")
)

(2) 按要素分图

除此之外,我们还可以使用 facet_wrap 函数来按照某个要素来分图:

  • 例如我们如果想按照进口和非进口啤酒来分图,我们就可以使用 facet_wrap(~ imported) 来指定按照 imported 这个变量来分图了
  • 运行这段代码后,我们就可以看到两个图像了,一个是进口啤酒的价格分布图,另一个是非进口啤酒的价格分布图了
  • 通过这种方式,我们就可以非常方便地按照某个要素来分图了,这也是 ggplot2 的一个非常重要的功能之一
ggplot(beer) +
  geom_point(aes(y = price, x = qty, color = imported)) +
  facet_wrap(~ imported)