R

2020-07-07-R Graphics Cookbook读书笔记2

第三章-条形图

Posted by DL on July 7, 2020

参考资料:R Graphics Cookbook, 2nd edition

第三章 条形图(Bar Graphs)

  条形图可能是最常用的数据可视化类型。它们通常用于显示不同类别(在x轴上)的数值(在y轴上)。举例来说,条形图可以很好地显示四种不同商品的价格。但是,条形图通常不适合显示随时间变化的价格(因为时间是一个连续变量)。

  在制作条形图时,有一点需要意识到:条形图的高度有时候代表的是数据集中case的统计数目,而有时它们代表的则是数据集中具体的数值。由于count和value与具体数据间的关系有着很大的差异,因此可能会引起混淆。

3.1 创建一个简单的条形图

使用ggplot()中的geom_col(),并指定x和y轴的数据。

library(gcookbook)  # Load gcookbook for the pg_mean data set
ggplot(pg_mean, aes(x = group, y = weight)) +
  geom_col()

UkyjPK.png

注:在早期的ggpolot2版本中,使用geom_bar(stat=”identity”)来创建条形图,而新版本则使用geom_col()。

当x是一个连续型(或数字型)变量时,条形图的呈现会略有差异(比如下图x轴在6的地方出现了隔断)。此时可以利用factor()函数将连续型变量转换为离散型变量。

# There's no entry for Time == 6
BOD
#>   Time demand
#> 1    1    8.3
#> 2    2   10.3
#> 3    3   19.0
#> 4    4   16.0
#> 5    5   15.6
#> 6    7   19.8

# Time is numeric (continuous)
str(BOD)
#> 'data.frame':    6 obs. of  2 variables:
#>  $ Time  : num  1 2 3 4 5 7
#>  $ demand: num  8.3 10.3 19 16 15.6 19.8
#>  - attr(*, "reference")= chr "A1.4, p. 270"

ggplot(BOD, aes(x = Time, y = demand)) +
  geom_col()

# Convert Time to a discrete (categorical) variable with factor()
ggplot(BOD, aes(x = factor(Time), y = demand)) +
  geom_col()

UkcnfK.png

Ukc80A.png

  注意,BOD数据中并中没有Time = 6的行。当x为连续型变量时,ggplot2将使用一个数字轴,该轴会为最小值到最大值范围内的所有数值保留空间(在本图中,虽然x中没有6这一行,但是会保留该space)。

  将Time数据转换为factor后,ggplot2会将其当作离散型变量来处理,该变量的值将被视为任意的标签(而非数字),因此它将不会在x轴上为最小值和最大值之间的所有可能数值分配空间。

  ggplot2中条形图的默认颜色为深灰色。若要更换颜色填充,可以利用fill参数进行修改。另外,默认情况下,填充的周围是没有轮廓的,若要添加轮廓,则需使用colour参数。

# 使用淡蓝色的fill和黑色的轮廓
ggplot(pg_mean, aes(x = group, y = weight)) +
  geom_col(fill = "lightblue", colour = "black")

3.2 创建分组条形图(geom_col(position = “dodge”))

下面将使用cabbage_exp数据集进行演示,该数据包含两个分类变量(Cultivar和Date)和一个连续型变量(Weight):

library(gcookbook)  # Load gcookbook for the cabbage_exp data set
cabbage_exp
#>   Cultivar Date Weight        sd  n         se
#> 1      c39  d16   3.18 0.9566144 10 0.30250803
#> 2      c39  d20   2.80 0.2788867 10 0.08819171
#> 3      c39  d21   2.74 0.9834181 10 0.31098410
#> 4      c52  d16   2.26 0.4452215 10 0.14079141
#> 5      c52  d20   3.11 0.7908505 10 0.25008887
#> 6      c52  d21   1.47 0.2110819 10 0.06674995

我们将Date映射到x轴,将Weight映射到y轴,将Cultivar映射到fill color:

ggplot(cabbage_exp, aes(x = Date, y = Weight, fill = Cultivar)) +
  geom_col(position = "dodge")

UV0XUP.png

  对于大部分的条形图来说,x轴一般为分类变量(categorical variable),y轴一般为连续型变量(continuous variable)。有时候我们还想引入另一个分类变量用于创建分组条形图,此时我们便可用position = “dodge参数来完成。

  注意:条形图的x轴一定要是categorical variable,而非continuous variable。如果想给条形图绘制外框,可在geom_col()中加入参数colour=”black”。如果要设置条形图的颜色,则需使用scale_fill_brewer()或scale_fill_manual()参数。下图使用的是RColorBrewer中的Pastel1这个调色板:

ggplot(cabbage_exp, aes(x = Date, y = Weight, fill = Cultivar)) +
  geom_col(position = "dodge", colour = "black") +
  scale_fill_brewer(palette = "Pastel1")

UVBbzF.png


创建基于计数(counts)的条形图

使用geom_bar()函数,但不映射任何值给y轴:

# Equivalent to using geom_bar(stat = "bin")
ggplot(diamonds, aes(x = cut)) +
  geom_bar()

UVs5dA.png

diamonds
#> # A tibble: 53,940 x 10
#>   carat cut       color clarity depth table price     x     y     z
#>   <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
#> 1 0.23  Ideal     E     SI2      61.5    55   326  3.95  3.98  2.43
#> 2 0.21  Premium   E     SI1      59.8    61   326  3.89  3.84  2.31
#> 3 0.23  Good      E     VS1      56.9    65   327  4.05  4.07  2.31
#> 4 0.290 Premium   I     VS2      62.4    58   334  4.2   4.23  2.63
#> 5 0.31  Good      J     SI2      63.3    58   335  4.34  4.35  2.75
#> 6 0.24  Very Good J     VVS2     62.8    57   336  3.94  3.96  2.48
#> # … with 53,934 more rows

上图我们对diamonds数据集中的cut这一列进行了计数,并绘制了条形图,cut列是离散的。如果我们使用连续型变量进行计数条形图的绘制,则效果如下:

UV6fUA.png

x轴连续的条形图类似于直方图,但不相同。在条形图中,每个条形表示一个唯一的x值,而在直方图中,每个条形表示一个x值的范围。

附录:条形图和直方图的区别

1、条形图是用条形的长度表示各类别频数的多少,其宽度(表示类别)则是固定的; 直方图是用面积表示各组频数的多少,矩形的高度表示每一组的频数或频率,宽度则表示各组的组距,因此其高度与宽度均有意义。 

2、由于分组数据具有连续性,直方图的各矩形通常是连续排列,而条形图则是分开排列。

3、条形图主要用于展示分类数据,而直方图则主要用于展示数据型数据


在条形图中使用不同的颜色

此例我们将使用uspopchange数据集,其包含了美国从2000年到2010年的人口变化百分比。我们将使用人口增速最快的前十个州,并绘制出它们的百分比变化。我们还将按地区(东北,南部,中北部或西部)对条形进行着色。首选需要获得排名前十的州的数据:

library(gcookbook) # Load gcookbook for the uspopchange data set
library(dplyr)

upc <- uspopchange %>%
  arrange(desc(Change)) %>%
  slice(1:10)

upc
#>             State Abb Region Change
#> 1          Nevada  NV   West   35.1
#> 2         Arizona  AZ   West   24.6
#> 3            Utah  UT   West   23.8
#>  ...<4 more rows>...
#> 8         Florida  FL  South   17.6
#> 9        Colorado  CO   West   16.9
#> 10 South Carolina  SC  South   15.3

下面开始作图:

ggplot(upc, aes(x = Abb, y = Change, fill = Region)) +
  geom_col()

UZwdgg.png

默认颜色为湖绿和西瓜红,若想更改条形图的颜色,可以使用scale_fill_brewer()函数或scale_fill_manual()函数。此例将使用scale_fill_manual()函数来更改颜色,使用colour=”black”来绘制条形图的外框颜色。

ggplot(upc, aes(x = reorder(Abb, Change), y = Change, fill = Region)) +
  geom_col(colour = "black") +
  scale_fill_manual(values = c("#669933", "#FFCC66")) +
  xlab("State")

UZ0uaq.png

注意,在上述例子中,我们还使用了reorder()函数,基于Change的值来对factor Abb进行重新排序。由上图可知,相比于按照factor Abb的字母顺序,这种按照条形图高度进行排序的方式显得更加合理。


3.5 为正负条形图分别上色

我们将使用一部分气候数据,并创建一个称为pos的新列,该列可以指明该值是正数还是负数:

library(gcookbook) # Load gcookbook for the climate data set
library(dplyr)

climate_sub <- climate %>%
  filter(Source == "Berkeley" & Year >= 1900) %>%
  mutate(pos = Anomaly10y >= 0)

climate_sub
#>       Source Year Anomaly1y Anomaly5y Anomaly10y Unc10y   pos
#> 1   Berkeley 1900        NA        NA     -0.171  0.108 FALSE
#> 2   Berkeley 1901        NA        NA     -0.162  0.109 FALSE
#> 3   Berkeley 1902        NA        NA     -0.177  0.108 FALSE
#>  ...<99 more rows>...
#> 103 Berkeley 2002        NA        NA      0.856  0.028  TRUE
#> 104 Berkeley 2003        NA        NA      0.869  0.028  TRUE
#> 105 Berkeley 2004        NA        NA      0.884  0.029  TRUE

一旦我们获得了数据,便可以开始绘制条形图,我们使用position=”identity”参数,以防因负数而出现警告消息:

ggplot(climate_sub, aes(x = Year, y = Anomaly10y, fill = pos)) +
  geom_col(position = "identity")

UZDqP0.png

上图有几个问题:首先,颜色和我们所期望的相反,一般来说,红色代表热,蓝色代表冷;其次,图例是多余的。

我们可以使用scale_fill_manual()参数来改变颜色,使用guide=FLASE来移除图例,使用colour参数来改变条形图的外框颜色,使用size参数来改变外框的粗细。

ggplot(climate_sub, aes(x = Year, y = Anomaly10y, fill = pos)) +
  geom_col(position = "identity", colour = "black", size = 0.25) +
  scale_fill_manual(values = c("#CCEEFF", "#FFDDDD"), guide = FALSE)

UZrRoR.png


3.6 调整条形图的宽度和间距

标准宽度的条形图:

library(gcookbook) # Load gcookbook for the pg_mean data set

ggplot(pg_mean, aes(x = group, y = weight)) +
  geom_col()

UeBnKg.png

细一些的条形图:

ggplot(pg_mean, aes(x = group, y = weight)) +
  geom_col(width = 0.5)

UeBwZ9.png

宽一些的条形图:

ggplot(pg_mean, aes(x = group, y = weight)) +
  geom_col(width = 1)

UeByRK.png

对于分组条形图来说,分组之间是默认没有间距的:

ggplot(cabbage_exp, aes(x = Date, y = Weight, fill = Cultivar)) +
  geom_col(width = 0.5, position = "dodge")

UeDQyD.png

可以通过position_dodge()参数来增加间距:

ggplot(cabbage_exp, aes(x = Date, y = Weight, fill = Cultivar)) +
  geom_col(width = 0.5, position = position_dodge(0.7))

UeDrwj.png

在默认的position=”dodge”中,默认间距为0.9,可以手动使用position=position_dodge(0.7)来改变间距。如果使整个图形变宽或变窄,则条形尺寸将成比例缩放。


3.7 创建一个堆叠条形图

library(gcookbook) # Load gcookbook for the cabbage_exp data set

ggplot(cabbage_exp, aes(x = Date, y = Weight, fill = Cultivar)) +
  geom_col()

Uerm7j.png

若想调换图例的顺序,则可以使用guides()函数中的fill=guide_legend参数:

ggplot(cabbage_exp, aes(x = Date, y = Weight, fill = Cultivar)) +
  geom_col() +
  guides(fill = guide_legend(reverse = TRUE))

Uerv80.png

若想调换堆叠条形图的顺序,则可在geom_col()函数中使用position=position_stack参数:

ggplot(cabbage_exp, aes(x = Date, y = Weight, fill = Cultivar)) +
  geom_col(position = position_stack(reverse = TRUE)) +
  guides(fill = guide_legend(reverse = TRUE))

UeswZQ.png

最后,还可以使用colour参数来改变外框颜色,使用scale_fill_brewer()函数来改变条形图的颜色:

ggplot(cabbage_exp, aes(x = Date, y = Weight, fill = Cultivar)) +
  geom_col(colour = "black") +
  scale_fill_brewer(palette = "Pastel1")

Ue6M4A.png


3.8 创建一个百分比堆积条形图

使用geom_col(position=”fill”)来绘制百分比堆积条形图:

library(gcookbook) # Load gcookbook for the cabbage_exp data set

ggplot(cabbage_exp, aes(x = Date, y = Weight, fill = Cultivar)) +
  geom_col(position = "fill")

Um0pI1.png

geom_col(position=”fill”)可以将y轴的值缩放至0到1之间,若要将标签也改为百分比,则可使用scale_y_continuous(labels=scales::percent):

ggplot(cabbage_exp, aes(x = Date, y = Weight, fill = Cultivar)) +
  geom_col(position = "fill") +
  scale_y_continuous(labels = scales::percent)

使用scales :: percent是一种使用scales包中的percent函数的方法。也可以改用library(scales),然后只用scale_y_continuous(labels = percent)。

为了让图片更加好看,可以加上外框并改变颜色:

ggplot(cabbage_exp, aes(x = Date, y = Weight, fill = Cultivar)) +
  geom_col(colour = "black", position = "fill") +
  scale_y_continuous(labels = scales::percent) +
  scale_fill_brewer(palette = "Pastel1")

Um04SK.png

与其让ggplot2自动计算比例,不如我们自己来计算比例的值。如果要在其他计算中使用这些值,这将很有用。我们首先需要将y轴的值缩放至100%,这可以通过group_by()和mutate()函数来完成:

library(gcookbook)
library(dplyr)

cabbage_exp
#>   Cultivar Date Weight        sd  n         se
#> 1      c39  d16   3.18 0.9566144 10 0.30250803
#> 2      c39  d20   2.80 0.2788867 10 0.08819171
#> 3      c39  d21   2.74 0.9834181 10 0.31098410
#> 4      c52  d16   2.26 0.4452215 10 0.14079141
#> 5      c52  d20   3.11 0.7908505 10 0.25008887
#> 6      c52  d21   1.47 0.2110819 10 0.06674995

# Do a group-wise transform(), splitting on "Date"
ce <- cabbage_exp %>%
  group_by(Date) %>%
  mutate(percent_weight = Weight / sum(Weight) * 100)

ce
#> # A tibble: 6 x 7
#> # Groups:   Date [3]
#>   Cultivar Date  Weight    sd     n     se percent_weight
#>   <fct>    <fct>  <dbl> <dbl> <int>  <dbl>          <dbl>
#> 1 c39      d16     3.18 0.957    10 0.303            58.5
#> 2 c39      d20     2.8  0.279    10 0.0882           47.4
#> 3 c39      d21     2.74 0.983    10 0.311            65.1
#> 4 c52      d16     2.26 0.445    10 0.141            41.5
#> 5 c52      d20     3.11 0.791    10 0.250            52.6
#> 6 c52      d21     1.47 0.211    10 0.0667           34.9

3.9 为条形图增加标签

通过geom_text()来为条形图增加标签,vset参数可以将标签移至条形图的上方或下方:

library(gcookbook) # Load gcookbook for the cabbage_exp data set

# Below the top
ggplot(cabbage_exp, aes(x = interaction(Date, Cultivar), y = Weight)) +
  geom_col() +
  geom_text(aes(label = Weight), vjust = 1.5, colour = "white")

# Above the top
ggplot(cabbage_exp, aes(x = interaction(Date, Cultivar), y = Weight)) +
  geom_col() +
  geom_text(aes(label = Weight), vjust = -0.2)

Uu8Kun.png

Uu8Orn.png

如果想要给计数型条形图添加标签,则需使用geom_bar()和geom_text():

ggplot(mtcars, aes(x = factor(cyl))) +
  geom_bar() +
  geom_text(aes(label = ..count..), stat = "count", vjust = 1.5, colour = "white")

UuJiY8.png

我们需要告知geom_text()来使用count值,并在x轴的条形图上增加相对应的标签,为了能使用计算后的count数作为标签,我们需要使用以下命令:aes(label= ..count..)。

此外,我们还可以根据以下命令来进一步修改标签的位置:

# Adjust y limits to be a little higher
ggplot(cabbage_exp, aes(x = interaction(Date, Cultivar), y = Weight)) +
  geom_col() +
  geom_text(aes(label = Weight), vjust = -0.2) +
  ylim(0, max(cabbage_exp$Weight) * 1.05)

# Map y positions slightly above bar top - y range of plot will auto-adjust
ggplot(cabbage_exp, aes(x = interaction(Date, Cultivar), y = Weight)) +
  geom_col() +
  geom_text(aes(y = Weight + 0.1, label = Weight))

对于分组条形图来说,我们也需要设置position=position_dodge()并给出dodg的宽度(其默认宽度为0.9)。因为在分组条形图中柱子变窄了,所以我们需要将标签的字体也相应地改小一些:

ggplot(cabbage_exp, aes(x = Date, y = Weight, fill = Cultivar)) +
  geom_col(position = "dodge") +
  geom_text(
    aes(label = Weight),
    colour = "white", size = 3,
    vjust = 1.5, position = position_dodge(.9)
  )

UuYDU0.png

在堆叠条形图上放置标签需要找到每个堆叠的累积总和。为此,请首先确保对数据进行了正确的排序-如果数据排序不正确,则可能会以错误的顺序计算出累计总和。我们将使用dplyr包中的arrange()函数,注意,我们必须使用rev()函数对Cultivar进行倒序:

library(dplyr)

# Sort by the Date and Cultivar columns
ce <- cabbage_exp %>%
  arrange(Date, rev(Cultivar)) #先按照Date升序,再按照Cultivar降序

确定数据正确排序后,我们将使用group_by()函数将数据按照日期进行分组,然后在每个数据块中计算权Weight的累积总和:

# Get the cumulative sum
ce <- ce %>%
  group_by(Date) %>%
  mutate(label_y = cumsum(Weight))

ce
#> # A tibble: 6 x 7
#> # Groups:   Date [3]
#>   Cultivar Date  Weight    sd     n     se label_y
#>   <fct>    <fct>  <dbl> <dbl> <int>  <dbl>   <dbl>
#> 1 c52      d16     2.26 0.445    10 0.141     2.26
#> 2 c39      d16     3.18 0.957    10 0.303     5.44
#> 3 c52      d20     3.11 0.791    10 0.250     3.11
#> 4 c39      d20     2.8  0.279    10 0.0882    5.91
#> 5 c52      d21     1.47 0.211    10 0.0667    1.47
#> 6 c39      d21     2.74 0.983    10 0.311     4.21

ggplot(ce, aes(x = Date, y = Weight, fill = Cultivar)) +
  geom_col() +
  geom_text(aes(y = label_y, label = Weight), vjust = 1.5, colour = "white")

UuNHnH.png

若要将标签放置在每个柱子的中间,则需要对累积总和进行调整:

ce <- cabbage_exp %>%
  arrange(Date, rev(Cultivar))

# Calculate y position, placing it in the middle
ce <- ce %>%
  group_by(Date) %>%
  mutate(label_y = cumsum(Weight) - 0.5 * Weight)

ggplot(ce, aes(x = Date, y = Weight, fill = Cultivar)) +
  geom_col() +
  geom_text(aes(y = label_y, label = Weight), colour = "white")

UuUDUI.png

最后,我们还可以改变条形图的颜色、改变字体大小并使用paste参数在标签后增加“kg”。注意,若要保留小数点后两位数,则可使用format():

ggplot(ce, aes(x = Date, y = Weight, fill = Cultivar)) +
  geom_col(colour = "black") +
  geom_text(aes(y = label_y, label = paste(format(Weight, nsmall = 2), "kg")), size = 4) +
  scale_fill_brewer(palette = "Pastel1")

UuaFMD.png


3.10 创建一个克利夫兰点图(Cleveland Dot Plot)

克利夫兰圆点图是条形图的一种替代图形,可以减少视觉混乱并使其更易于阅读。最简单的创建点图的方式是使用geom_point()函数:

library(gcookbook) # Load gcookbook for the tophitters2001 data set
tophit <- tophitters2001[1:25, ] # Take the top 25 from the tophitters data set

ggplot(tophit, aes(x = avg, y = name)) +
  geom_point()

UMdzvV.png

tophitters2001数据集中含有很多列数据,但我们仅关注其中的三列:

tophit[, c("name", "lg", "avg")]
#>             name lg    avg
#> 1   Larry Walker NL 0.3501
#> 2  Ichiro Suzuki AL 0.3497
#> 3   Jason Giambi AL 0.3423
#>  ...<19 more rows>...
#> 23  Jeff Cirillo NL 0.3125
#> 24   Jeff Conine AL 0.3111
#> 25   Derek Jeter AL 0.3111

  上图是按照字母顺序来排列的,但这并不利于图表的呈现。点图通常按水平轴上连续变量的值排序。name是一个字符向量,因此按字母顺序排列。如果是一个factor,它将使用factor level中定义的顺序。在这种情况下,我们希望按不同的变量avg对名称进行排序。

  为了实现上述目的,我们需要使用reorder(name,avg),这会将name列转换为factor,并基于avg列对其进行排序。为了进一步改善外观,我们将使用主题系统使垂直网格线消失,然后将水平网格线变为虚线:

ggplot(tophit, aes(x = avg, y = reorder(name, avg))) +
  geom_point(size = 3) +  # Use a larger dot
  theme_bw() +
  theme(
    panel.grid.major.x = element_blank(),
    panel.grid.minor.x = element_blank(),
    panel.grid.major.y = element_line(colour = "grey60", linetype = "dashed")
  )

UM0isf.png

也可以交换轴,以使名称沿x轴移动,而值沿y轴移动。我们还将文本标签旋转60度:

ggplot(tophit, aes(x = reorder(name, avg), y = avg)) +
  geom_point(size = 3) +  # Use a larger dot
  theme_bw() +
  theme(
    panel.grid.major.y = element_blank(),
    panel.grid.minor.y = element_blank(),
    panel.grid.major.x = element_line(colour = "grey60", linetype = "dashed"),
    axis.text.x = element_text(angle = 60, hjust = 1)
  )

UM0QyV.png

有时也需要按另一个变量对项目进行分组。在这种情况下,我们将使用factor lg,其具有NL和AL这两个level,分别代表国家联盟和美国联盟。这次我们要先按lg排序,然后再按avg排序。遗憾的是,reorder()函数只能按另一个变量对factor levels进行排序。要通过两个变量对因子水平进行排序,我们必须手动进行:

# Get the names, sorted first by lg, then by avg
nameorder <- tophit$name[order(tophit$lg, tophit$avg)]

# Turn name into a factor, with levels in the order of nameorder
tophit$name <- factor(tophit$name, levels = nameorder)

此外,我们还需要将lg映射至不同的点的颜色。这次,我们将使用geom_segment()函数。请注意,geom_segment()需要x,y,xend和yend的值:

ggplot(tophit, aes(x = avg, y = name)) +
  geom_segment(aes(yend = name), xend = 0, colour = "grey50") +
  geom_point(size = 3, aes(colour = lg)) +
  scale_colour_brewer(palette = "Set1", limits = c("NL", "AL")) +
  theme_bw() +
  theme(
    panel.grid.major.y = element_blank(),   # No horizontal grid lines
    legend.position = c(1, 0.55),           # Put legend inside plot area
    legend.justification = c(1, 0.5)
  )

UMs43j.png

分离两组的另一种方法是使用facets:

ggplot(tophit, aes(x = avg, y = name)) +
  geom_segment(aes(yend = name), xend = 0, colour = "grey50") +
  geom_point(size = 3, aes(colour = lg)) +
  scale_colour_brewer(palette = "Set1", limits = c("NL", "AL"), guide = FALSE) +
  theme_bw() +
  theme(panel.grid.major.y = element_blank()) +
  facet_grid(lg ~ ., scales = "free_y", space = "free_y")

UMsO5F.png