ggplot2用图层构建图像

简介

本文主要对ggplot2中图层、几何对象、统计变换和位置调整的工作方式作一些技术说明:如何对它们进行调用和自定义。

创建绘图对象

当我们调用qplot()时,它已经帮我们做了这样一个流程:创建一个图形对象,添加图层并且展示结果。但如果我们想手动创建图形对象,就要用到ggplot()函数。该函数主要有两个参数:数据和图形属性的映射。其余全部参数可查看R帮助文档。举例:数据为diamonds,x为carat,y为price,colour为cut。

p <- ggplot(diamonds, aes(carat, price, colour = cut))

上面的代码就是创建一个图形对象,在加上图层之前是无法显示图形的。从这行代码可以看出图形对象可以存储到一个变量里,这有个好处就是有利于代码避繁就简。在文章后面会展示这一优点。

图层

我们给上面的图形对象添加一个简单的图层,直接添加一个点几何对象。

p <- p + geom_point()

id

因为图形对象可以储存在变量中,我们可以通过summary(p)来查看图形对象的结构。
之前说的手动创建对象的好处可以使代码简洁。例如,一组图形可以先用不同的数据来进行初始化,然后加上相同的图层,如果后面想改变图层,只需要修改一个地方即可。下面的例子创建了一个带有半透明深蓝色回归线的图层。

library(scales)
bestfit <- geom_smooth(method = "lm", se = F, colour = alpha("steelblue", 0.5), size = 2)

用不同的数据来展示:

qplot(sleep_rem, sleep_total, data = msleep) + bestfit

id

qplot(awake, brainwt, data = msleep, log = "y") + bestfit

id

qplot(bodywt, brainwt, data = msleep, log = "xy") + bestfit

id

数据

  • 类型必须是一个数据框;
  • 在更改数据集时,可以任意改变它的值和维数;
  • 在不使用分面的时候不必设定默认的数据集;
  • 数据是以副本而不是引用的形式存储到图形对象中的。

图形属性映射

aes()函数用来将数据变量映射到图形中,从而使变量成为可以被感知的图形属性。举个例子:将x坐标映射到weight,y坐标 映射到height,colour映射到age。前两个参数也可以省略。

aes(x = weight, y = height, colour = age)

注意也可以使用变量的函数值作为参数。

aes(weight, height, colour = sqrt(age))

图和图层

举个例子来看图形对象在新图层里进行的扩充或修改。

p <- ggplot(mtcars, aes(x = mpg, y = wt))
p + geom_point()

id

p + geom_point(aes(colour = factor(cyl)))

id

p + geom_point(aes(y = disp))

id

具体的添加、修改和删除规则见下表:

操作 层图形属性 结果
添加 aes(colour = cyl) aes(mpg, wt, colour = cyl)
修改 aes(y = disp) aes(mpg, disp)
删除 aes(y = NULL) aes(mpg)

设定和映射

我们可以在图层的参数里将映射设定为一个单一值,图形属性可以根据观测的不同而变化,但是参数则不行。下面的例子用图层里的colour参数设定了点的颜色为深蓝色:

p <- ggplot(mtcars, aes(mpg, wt))
p + geom_point(colour = "darkblue")

id

下面这个例子将colour映射到”darkblue”颜色。实际上是先创建了一个只含有”darkblue”字符的变量,然后将colour映射到这个新变量。因为这个新变量的值是离散型的,所以默认的颜色标度将用色轮上等间距的颜色,并且此处新变量只有一个值,因此这个颜色就是桃红色。

p + geom_point(aes(colour = "darkblue"))

id

分组

在ggplot2里,几何对象可以大致分为个体(individual)几何对象和群组(collective)几何对象两大类。个体几何对象对数据框的每一条数据绘制一个可以区别于其他个体的图形对象。而群组几何对象用来表示多个观测,可以是某个统计摘要的一个结果,或者是几何对象的基础展示。

图中所有离散型变量的交互作用被设为分组的默认值,通常这样可以正确地给数据分组,但是如果没能正确分组或者图中没有离散型变量,那么就需要自定义分组结构,即将group映射到一个在不同的组有不同取值的变量。当现有的单个变量不能够正确地分组,而两个变量的组合可以正确分组时,可以使用interaction()函数。这里可以类比SQL语言的分组语句。

通常有三种情况是默认分组不能解决的,下文会介绍。在下面的例子中,数据集采用nlme包中的一个简单的纵向数据集Oxboys。该数据记录了26名男孩(Subjiect)在9个不同时期(Occasion)所测定的身高(height)和中心化后的年龄(age)。

多个分组与单个图形属性

很多时候,我们都想将数据分成若干组,并用相同的方式对每个组进行渲染。当从总体上来查看数据时,我们通常希望区分每个个体而不是识别他们。这类图形被称为“细面图”(spahetti plot)。

画出每个男孩的时间序列图,每条线代表一个男孩,指定了Subject为分组变量。代码如下:

ggplot(Oxboys, aes(age, height, group = Subject)) + geom_line()

id

如果不指定分组变量,就会得到一个无意义的奇怪的折线图:

ggplot(Oxboys, aes(age, height)) + geom_line()

id

不同图层上的不同分组

有时我们想根据不同水平的数据整合来对统计汇总信息(summary)进行图形绘制,从而不同的图层可能有不同的分组图形属性,因此,有的图层展示个体水平的数据,而有的图层则展示更大组群的统计信息。

在前面例子的基础上,假设我们想根据所有男孩的年龄和身高在图中添加一条光滑线条。如果还和前面一样的分组方式,则会给每一个男孩添加一条光滑线条:

p + geom_smooth(aes(group = Subject), method = "lm", se = F)

id

因此,新图层需要的是一个不同的分组图形属性,group = 1,这样所绘出的线条才是基于整体数据的,代码如下:

p + geom_smooth(aes(group = 1), method = "lm", size = 2, se = F)

id

修改默认分组

如果图像中含有离散型变量,而你却想绘制连接所有分组的线条,那么你可以采取绘制交互作用图、轮廓图以及平行坐标图时所用的策略。这里以绘制各个测量时期(Occasion)身高(height)的线箱图为例,这里就没必要设定组图形属性,因为Occasion是一个离散型变量,默认的分组变量就是Occasion。

boysbox <- ggplot(Oxboys, aes(Occasion, height)) + geom_boxplot()

id

如果要在上面的基础上添加个体轨迹,则就要用aes(group = Subject)修改第一层的默认分组:

boysbox + geom_line(aes(group = Subject), colour = "#3366FF")

id

匹配图形属性和图形对象

  • 线条和路径遵循差一原则:观测点比线段数目多一。
  • 对于其他的群组几何对象,如多边形,只有当所有个体的图形属性都相同时,该图形属性才会被使用,否则将使用默认值。

几何对象

  1. 几何图形对象,简称为geom,它执行着图层的实际渲染,控制着生成的图像类型。
  2. 每个几个对象都有一组它能识别的图形属性和一组绘图所需的值。
  3. 有些几何对象主要在它们参数化的方式上有所不同。
  4. 每一个几何对象都有一个默认的统计变换,并且每一个统计变换都有一个默认的几何对象。

统计变换

统计变换,简称为stat,即对数据进行统计变换,它通常以某种方式对数据信息进行汇总。

统计变换可将输入的数据集看做输入,将返回的数据集作为输出,因此统计变换可以向原数据集中插入新的变量。例如,常被用来绘制直方图的stat_bin统计变换会生成如下变量:

  • count,每个组里观测值的数目;
  • density,每个组里观测值的密度(占整体的百分数/组宽);
  • x,组的中心位置。

直方图默认的是将条形高度赋值为观测值的频数(count),也可以用密度(density)来代替。下面的例子是给出钻石数据集中的克拉(carat)的密度直方图。

ggplot(diamonds, aes(carat)) + geom_histogram(aes(y = ..density..), binwidth = 0.1)

id

生成的变量名必须用..围起来。作用是防止原数据集中过的变量和生成变量重名时造成混淆。具体额生成变量名称在每个统计变换的帮助文档中列出了。类似地,用qplot()生成上图的代码如下:

qplot(carat, ..density.., data = diamonds, geom = "histogram", binwidth = 0.1)

位置调整

位置调整指的是对该层中的元素位置进行微调。一般多用于处理离散型数据。一共有如下五种位置调整参数:

  1. dodge:避免重叠,并排放置
  2. fill:堆叠图形元素并将高度标准化为1
  3. identity:不做任何调整
  4. jitter:给点添加扰动避免重合
  5. stack:将图形元素堆叠起来

整合

结合几何对象和统计变换

下面给出例子,都是应用了基于直方图的统计变换,使用了不同的几何对象来展示结果:面积和点。

d <- ggplot(diamonds, aes(carat)) + xlim(0, 3)
d + stat_bin(aes(ymax = ..count..), binwidth = 0.1, geom = "area")

id

d + stat_bin(aes(size = ..density..), binwidth = 0.1, geom = "point", position = "identity")

id

显示已计算过的统计量

如果已经有了汇总过的数据,并且想直接用,可以使用stat_identity(),然后将合适的变量映射到相应的图形属性中。

改变图形属性和数据集

ggplot2还有一个功能就是将不同的数据画在不同的图层上。下面举个例子,还是以之前的Oxboys数据集为例。

首先读入nlme包,然后拟合一个截距和斜率都包含随机效应的混合模型。先建一个图形对象作为模板,然后利用从模型中导出的数据对模型进行渲染。

require(nlme, quietly = T)
model <- lme(height ~ age, data = Oxboys, random = ~ 1 + age | Subject)
oplot <- ggplot(Oxboys, aes(age, height, group = Subject)) + geom_line()

随后,对预测的生长轨迹和实际的生长轨迹进行对比。先建立一个包含所有年龄(age)和个体(subject)组合的网络数据框。接下来把模型的预测值添加到刚刚生成的数据集中,变量名叫height。

age_grid <- seq(-1, 1, length = 10)
subjects <- unique(Oxboys$Subject)
preds <- expand.grid(age = age_grid, Subject = subjects)
preds$height <- predict(model, preds)

得到预测值后,把它和原始数据绘制到同一张图上。因为在新数据集preds里,使用了与原始数据集Oxboys相同的变量名,并且目的是使用相同的分组图形属性,所以不用修改任何图形属性,只需要修改默认的数据集。

oplot + geom_line(data = preds, colour = "#3366FF", size = 0.4)

id

从图形上看,这个模型似乎很好地拟合了该数据更深层次的结构,但是仍然很难分辨细节。我们使用另一种比较模型拟合好坏的方法————观察残差。

首先把拟合值(fitted)和残差(resid)都添加到原数据里去,然后更新数据集,将默认的y图形属性改为resid,最后对整个数据加一条光滑曲线。

Oxboys$fitted <- predict(model)
Oxboys$resid <- with(Oxboys, fitted - height)
oplot %+% Oxboys + aes(y = resid) + geom_smooth(aes(group = 1))

id

从图形可以看到残差并不是随机分布的,因此所建立的模型有缺陷。做一个修改,向模型中添加一个二次项,再次计算拟合值和残差并重新绘制残差图。

model2 <- update(model, height ~ age + I(age ^ 2))
Oxboys$fitted2 <- predict(model2)
Oxboys$resid2 <- with(Oxboys, fitted2 - height)
oplot %+% Oxboys + aes(y = resid2) + geom_smooth(aes(group = 1))

id

从上面的流畅看出来,我们对图形对象的修是非常容易的。更新了数据并且重新作了两次图却没有再次运行过oplot,这正是ggplot2图层功能所秉承的理念:使得反复拟合和评估模型变得轻松而自然。So easy!

参考文献

[1][ggplot2:数据分析与图形艺术]