R语言、ggplot2和图形语法
前言
如果你同时用过 Tableau 和 PowerBI,会发现两款 BI 软件两种截然不同的制作常规统计图表的方式区别来了解了图形语法的意义,和形成历史。 总结来说,基于图形语法的概念,任何常规统计图表可以通过组合不同的图形元素来进行描述和绘制。图形语法中的图形元素包括了:几何图形 geometry shape、数据转换 transformation、scale等。换种说法,任何的常规统计图形都可以这些图形元素进行组合得到,而不需要对各种图形进行枚举和命名。ggplot2 是业界对图形语法理论最完备的实现,今天我们就来了解如何使用 ggplot2 来分析数据和绘制各种统计图形。
认识 R 语言
ggplot2 是使用 R 语言来实现的图形语法,所以在了解ggplot2前,有必要先对 R 语言有一定的了解。R 语言是一门古老的语言,有快30年的历史了,一门专业用在统计计算和图形的语言。首先来看 R 语言的基本数据类型
数值
print(1 / 200 * 30)
## [1] 0.15
sin(pi / 2)
## [1] 1
字符串
print("hello, R language!")
## [1] "hello, R language!"
print("hello, too")
## [1] "hello, too"
print(paste("hello", "R language"))
## [1] "hello R language"
print(paste("hello", "R", sep = "-"))
## [1] "hello-R"
print(format(pi, digits = 9))
## [1] "3.14159265"
print(format(3.14, width = 6))## [1] " 3.14"
vector
R 语言中使用 vector 作为一维数组,vector 中元素要去同类型,同时 vector 会自动转换类型同时 vector 中的元素可以进行命名vector 支持生成序列
print(c(1, 3, 5, 7, 9))
## [1] 1 3 5 7 9
typeof(c(1, 3, 5, 7, 9))
## [1] "double"
print(c(1, 3, 5, 7, 9, "a"))
## [1] "1" "3" "5" "7" "9" "a"
typeof(c(1, 3, 5, 7, 9, "a"))
## [1] "character"
x <- c(C1 = "A", C2 = "B", C = "C")
print(x)
## C1 C2 C
## "A" "B" "C"
print(c(2:11))
## [1] 2 3 4 5 6 7 8 9 10 11
list
list 不同与 vector,list 可以是嵌套的多维数组,list 中各个元素的类型也可以不同
l <- list(1, 3, 5, 7, 9, "a")
print(l)
## [[1]]
## [1] 1
##
## [[2]]
## [1] 3
##
## [[3]]
## [1] 5
##
## [[4]]
## [1] 7
##
## [[5]]
## [1] 9
##
## [[6]]
## [1] "a"
print(list("Hello", "World", c(1, 3, 5), TRUE))
## [[1]]
## [1] "Hello"
##
## [[2]]
## [1] "World"
##
## [[3]]
## [1] 1 3 5
##
## [[4]]
## [1] TRUE
matrix
R 原生支持二位矩形,通过 matrix 函数可以生成矩形
matrix(data, nrow, ncol, byrow, dimnames)
同时 R 语言支持对 Matrix 进行计算
M <- matrix(c(3:14), nrow = 4, byrow = TRUE)
print(M)
## [,1] [,2] [,3]
## [1,] 3 4 5
## [2,] 6 7 8
## [3,] 9 10 11
## [4,] 12 13 14
print(M[1, 1])
## [1] 3
print(M[2, 3])
## [1] 8
print(M[2, ])
## [1] 6 7 8
print(M[, 3])
## [1] 5 8 11 14
m1 <- matrix(c(1, 2, 3, 4), nrow = 2, byrow = TRUE)
print(m1)
## [,1] [,2]
## [1,] 1 2
## [2,] 3 4
m2 <- matrix(c(5, 6, 7, 8), nrow = 2, byrow = TRUE)
print(m2)
## [,1] [,2]
## [1,] 5 6
## [2,] 7 8
print(m1 * 2)
## [,1] [,2]
## [1,] 2 4
## [2,] 6 8
print(m1 + m2)
## [,1] [,2]
## [1,] 6 8
## [2,] 10 12
print(m1 - m2)
## [,1] [,2]
## [1,] -4 -4
## [2,] -4 -4
print(m1 * m2)
## [,1] [,2]
## [1,] 5 12
## [2,] 21 32
print(m1 / m2)
## [,1] [,2]
## [1,] 0.2000000 0.3333333
## [2,] 0.4285714 0.5000000
factor
factor 通常用来对数据进行分类,R 使用 Level 来标记任务的分类
data <- c("male", "female", "unknown", "male", "female", "unknown")
print(data)
## [1] "male" "female" "unknown" "male" "female" "unknown"
print(is.factor(data))
## [1] FALSE
factor_data <- factor(data)
print(factor_data)
## [1] male female unknown male female unknown
## Levels: female male unknown
print(is.factor(factor_data))
## [1] TRUE
data frame
R 语言是用 DataFrame 来表示二维表格数据,比如 SQL 查询结果、CSV 文件内容都适合用 DataFrame 来表示直接创建 data frame
df <- data.frame( friend_id = c(1:15), friend_name = c("Sachin", "Sourav", "Dravid", "Sehwag", "Dhoni"), stringsAsFactors = FALSE )
print(df)
## friend_id friend_name
## 1 1 Sachin
## 2 2 Sourav
## 3 3 Dravid
## 4 4 Sehwag
## 5 5 Dhoni
## 6 6 Sachin
## 7 7 Sourav
## 8 8 Dravid
## 9 9 Sehwag
## 10 10 Dhoni
## 11 11 Sachin
## 12 12 Sourav
## 13 13 Dravid
## 14 14 Sehwag
## 15 15 Dhoni
扩充一个 data frame
df$location <- c("Kolkata", "Delhi", "Bangolore", "Hyderabad", "Chennai")
print(df)
## friend_id friend_name location
## 1 1 Sachin Kolkata
## 2 2 Sourav Delhi
## 3 3 Dravid Bangolore
## 4 4 Sehwag Hyderabad
## 5 5 Dhoni Chennai
## 6 6 Sachin Kolkata
## 7 7 Sourav Delhi
## 8 8 Dravid Bangolore
## 9 9 Sehwag Hyderabad
## 10 10 Dhoni Chennai
## 11 11 Sachin Kolkata
## 12 12 Sourav Delhi
## 13 13 Dravid Bangolore
## 14 14 Sehwag Hyderabad
## 15 15 Dhoni Chennai
从 CSV 文件读取内容作为 data frame
newDF <- read.csv("mycsv.csv")
print(newDF)
## id name age language
## 1 0 Amiya 22 R
## 2 1 Raj 25 Python
## 3 2 Asish 45 Java
从 data frame 中读取制定行,或者指定列的内容
print(newDF[1])
## id
## 1 0
## 2 1
## 3 2
print(newDF[, 1])
## [1] 0 1 2
print(newDF)
## id name age language
## 1 0 Amiya 22 R
## 2 1 Raj 25 Python
## 3 2 Asish 45 Java
根据指定条件过滤 data frame
subDF <- subset(newDF, name == "Raj" | age > 30)
print(subDF)
## id name age language
## 2 1 Raj 25 Python
## 3 2 Asish 45 Java
Tibbles
Tibbles 是著名的 tidyverse 包中实现的结构,他和 R 中原生的 data frame 非常类型,在 tidyverse 中就是用来替代 data frame的。使用 as_tibbles 可以将普通的 data frame 转换成 tibble
as_tibble(iris)
## # A tibble: 150 × 5
## Sepal.Length Sepal.Width Petal.Length Petal.Width Species
## <dbl> <dbl> <dbl> <dbl> <fct>
## 1 5.1 3.5 1.4 0.2 setosa
## 2 4.9 3 1.4 0.2 setosa
## 3 4.7 3.2 1.3 0.2 setosa
## 4 4.6 3.1 1.5 0.2 setosa
## 5 5 3.6 1.4 0.2 setosa
## 6 5.4 3.9 1.7 0.4 setosa
## 7 4.6 3.4 1.4 0.3 setosa
## 8 5 3.4 1.5 0.2 setosa
## 9 4.4 2.9 1.4 0.2 setosa
## 10 4.9 3.1 1.5 0.1 setosa
## # … with 140 more rowstibble( x = 1:5, y = 1, z = x^2 + y )
## # A tibble: 5 × 3
## x y z
## <int> <dbl> <dbl>
## 1 1 1 2
## 2 2 1 5
## 3 3 1 10
## 4 4 1 17
## 5 5 1 26
直接创建 tibble在这里可以看到,创建 tibble 和创建 data frame 非常相似,一点不同的是,记得创建 data frame 的 stringsAsFactors 参数吗? tibble 则相反,tibble 不会主动去改变输入的数据类型。另一种创建 tibble 的方法是使用 tribble,它是 transposed tibble 的缩写
tribble(
~x, ~y, ~z,
#--|--|----
"a", 2, 3.6,
"b", 1, 8.5,
)
## # A tibble: 2 × 3
## x y z
## <chr> <dbl> <dbl>
## 1 a 2 3.6
## 2 b 1 8.5
此外 tidyverse 还提供一系列 readr 函数来导入外部数据,直接得到 tibbles:
t1 <- read_csv("mycsv.csv")
## Rows: 3 Columns: 4
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (2): name, language
## dbl (2): id, age
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.print(class(t1))
## [1] "spec_tbl_df" "tbl_df" "tbl" "data.frame"print(t1)
## # A tibble: 3 × 4
## id name age language
## <dbl> <chr> <dbl> <chr>
## 1 0 Amiya 22 R
## 2 1 Raj 25 Python
## 3 2 Asish 45 Java
R 语言编程
Function
使用 function { } 可以创建 function:
rescale01 <- function(x) {
rng <- range(x, na.rm = TRUE)
(x - rng[1]) / (rng[2] - rng[1])
}
rescale01(c(0,5,10))## [1] 0.0 0.5 1.0
Pipe
R 语言中的 %>% (管道)提供了函数式编程的组合能力 简单来说,比如下列的函数调用表达式 h(f(g(x))) 在使用管道到表达时就可以写为:
x %>%
g %>%
f %>%
h
图形语法
图形语法是一种可以让我们描述图形各组件的工具:它可以让我们超越图表命名方法(如散点图),而是跟深刻地理解统计图表的底层结构。即通过图形预发,则不需要对每一种图形进行分类命名,直接通过组合图形元素就可以表达出各种图形。举个例子,根据 diamonds 数据集
print(diamonds)
## # A tibble: 53,940 × 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.29 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
## 7 0.24 Very Good I VVS1 62.3 57 336 3.95 3.98 2.47
## 8 0.26 Very Good H SI1 61.9 55 337 4.07 4.11 2.53
## 9 0.22 Fair E VS2 65.1 61 337 3.87 3.78 2.49
## 10 0.23 Very Good H VS1 59.4 61 338 4 4.05 2.39
## # … with 53,930 more rows
然后对 diamonds 数据集进行分组统计,得到每一个 cut 类型下的平均价格:
diamondsPriceByCut <- diamonds %>%
group_by(cut) %>%
summarise(mean_price = mean(price, na.rm = TRUE))
print(diamondsPriceByCut)
## # A tibble: 5 × 2
## cut mean_price
## <ord> <dbl>
## 1 Fair 4359.
## 2 Good 3929.
## 3 Very Good 3982.
## 4 Premium 4584.
## 5 Ideal 3458.
根据根据计算出的数据,分别画出以下图形:
ggplot(data = diamondsPriceByCut, aes(x = cut, y = mean_price)) +
geom_line(mapping = aes(group = 1))
ggplot(data = diamondsPriceByCut, aes(x = cut, y = mean_price)) +
geom_bar(stat = 'identity')
ggplot(data = diamondsPriceByCut, aes(x = cut, y = mean_price)) +
geom_bar(stat = 'identity') + coord_flip()
ggplot(data = diamondsPriceByCut, aes(x = cut, y = mean_price)) +
geom_bar(stat = 'identity') + coord_polar()
按照传统的可视化说法,这里分别是折线图、柱形图、条形图、和饼图。他们之间相关关系不大,比如折线图、柱形图、和饼图是完全不同的图形。 但通过代码可以看到,这4个图形其实是很相似的,他们都是通过 + 将一个元素组合起来形成的,这便是图形语法的基本概念:可视化图形可以通过组合不通的基本元素得到,这些基本元素是图形的基本组件。那图形预发有什么意义或者重要性呢?
图形语法的组成:如何构造一个图形?
在图形语法的概念中,一个图形通过使用特定语法来组合不通基本元素就可以表达出来的,那一个图形有哪些基本组成呢?这个问题可以通过来看如何绘制一个图形来回答。那如下数据集举例子,目标是通过下面数据集的x、y、z 来绘制一个点图,每个数据点的point 样式使用 z 字段指定的样式。
data <- tribble(
~x, ~y, ~z,
#--|--|----,
25,11,'circle',
0,0,'circle',
75,53,'square',
200,300,'square'
)
print(data)
## # A tibble: 4 × 3
## x y z
## <dbl> <dbl> <chr>
## 1 25 11 circle
## 2 0 0 circle
## 3 75 53 square
## 4 200 300 square
mapping 数据映射
第一步是映射,我们的输入是数据,输出是图形,即需要将数据映射为图形空间,即 observation -> aesthetics。 这里 observation 是指数据集的一行数据,数据集中 x、y、shape 是变量,每一行数据即称为一个观察点。 aesthetics 指图形通道,比如图形上的x轴、y轴、颜色、形状都是图形通道的一种。对应这里,我们把数据的 x 变量映射为 x轴、y 变量映射为 y 轴、shape 变量映射为 形状:
aes(x = x, y = y, shape = z)
geometric 确定图形元素
这里 geometry 就是图形的基本元素,比如折线的 path、柱形图中的 bar,点图中的 point。在这个示例中我们使用 point
scale 数据值到物理单位的计算
如 x-position 计算到 [0, 200] 空间,y-position 计算到 [0, 300] 空间,其他的通道也是一样,如 z-shape 中 ‘a’ 映射为圆形、‘b’ 映射到正方形等
render 渲染
根据 data 和 geometry 指定的图形元素,计算每一行数据对应的图形元素的图形通道值,如x轴位置、y轴位置,即可以绘制出所有图形图形,然后再渲染其他辅助的组件,比如坐标轴、标题等。渲染步骤
最终效果从上面的从数据如何绘制一个完整的图形示例过程中,就可以看到,图形语法的基本组成了:
- 一个默认的dataset,和一组变量到图形通道的映射
- 一个或多个layer,每个layer有一个图形基本元素 geometric object
- 每个图形通道映射对应的scale
- 一个坐标系
在这些组成部分中,有些可以省略就是默认的,比如上面示例中没有提到坐标系,其实是用默认使用了笛卡尔坐标系。具体来看其中最重要的 geometry。geometry 其实是控制着图形的类型,按照维度分类:
- 0 维: point、text
- 1 维:path、line
- 2 维: polygon、interval
需要注意的是 geometry 是抽象的组件,比如 interval 这一个 geometry,是可以有不同的绘制形式:interval另外一个重要的组成部分就是 scale:它控制的是数据到图形通道的映射计算
scale上面图形中,依次是:
- size scale:控制数据到point大小的映射计算
- continuous color scale:控制连续数据到颜色的映射计算
- shape scale:控制数据到形状的映射计算
- category color scale:控制离散数据到颜色的映射计算
在图形语法下看图形内在联系
回到最开始的4个图形:
ggplot(data = diamondsPriceByCut, aes(x = cut, y = mean_price)) +
geom_line(mapping = aes(group = 1))
ggplot(data = diamondsPriceByCut, aes(x = cut, y = mean_price)) +
geom_bar(stat = 'identity')
ggplot(data = diamondsPriceByCut, aes(x = cut, y = mean_price)) +
geom_bar(stat = 'identity') + coord_flip()
ggplot(data = diamondsPriceByCut, aes(x = cut, y = mean_price)) +
geom_bar(stat = 'identity') + coord_polar()
再来看,这4个图形的,dataset、和 aesthetic 映射都是一样的。
base <- ggplot(data = diamondsPriceByCut, aes(x = cut, y = mean_price))
折线图使用了 line 做为 geometry,没有指定坐标系,其实是默认使用了笛卡尔坐标系:
line <- base + geom_line(mapping = aes(group = 1))
柱形图使用了 bar 做为 geometry,同样默认使用了笛卡尔坐标系(可以手动指定,忽略时就是默认值)
bar <- base + geom_bar(stat = 'identity') + coord_cartesian()
条形图则几乎和柱形图一致,只不过对笛卡尔坐标系做了转置:
barbar + coord_flip()
最后是饼图,这里饼图其实和柱形图都基本一致,只是使用了不同的坐标系,即极坐标
pie <- bar + coord_polar()
使用图形语法绘制各种图形
了解了这些后,其实就可以通过图形语法来组合出不同的图形了,比如在极坐标下使用line来绘制,即可以看到在语法层面合法的图形,但在一般图形分类中没有的图形:
base + geom_line(mapping = aes(group = 1)) + coord_polar()
再如:
base + geom_point(mapping = aes(shape = cut, color = cut)) + coord_polar()
pie + coord_polar(theta = "y")
diamonds %>%
count(color, cut) %>%
ggplot(mapping = aes(x = color, y = cut)) +
geom_tile(mapping = aes(fill = n))
参考资料
转载自:https://juejin.cn/post/7185456072721694757