ggplot2:用图层构建对象

ggplot2:用图层构建对象

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

创建绘图对象

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

这个图形对象在加上图层之前无法显示,因此什么都看不见。

图层

1
(p <- p + geom_point())


1
2
p <- ggplot(diamonds, aes(x = carat))
(p <- p + geom_histogram(binwidth = 0.2, fill = 'steelblue'))

所有的快捷函数都有相同的形式——以geom_或者stat_开头:

1
2
geom_XXX(mapping, data, ..., stat, position)
stat_XXX(mapping, data, ..., geom, position)

它们的参数定义了图层的各种组件:

  1. mapping(可选):一组图形属性映射,通过aes()函数设定;
  2. data(可选):一个数据集,它会修改默认数据集;
  3. …:geom或stat参数,例如直方图的binwidth或者loess光滑曲线的bandwidth。我们也可以用图形属性作为参数,这样该属性就被设定为一个固定的值,而不是被映射给数据集中的一个变量;
  4. geom或stat(可选):我们可以修改geom默认的stat值,或者stat默认的geom值。它们是一串字符串,包含了将要使用的几何对象或统计变换的名称。使用默认值会得到标准图形,修改默认值会得到一些新奇的图形。
  5. position(可选):选择一种调整对象重合的方式。
1
ggplot(msleep, aes(sleep_rem/sleep_total, awake)) + geom_point() + geom_smooth()


1
2
3
library(scales)
bestfit <- geom_smooth(method = "lm", se = F, colour = alpha("steelblue", 0.5), size = 4)
qplot(sleep_rem, sleep_total, data = msleep) + bestfit


1
qplot(sleep_rem, sleep_total, data = msleep, log = "y") + bestfit


1
qplot(sleep_rem, sleep_total, data = msleep, log = "xy") + bestfit

数据

ggplot2对于数据集的要求非常简单,必须是一个数据框,这种限制可以方便我们用相同的代码、不同的数据集绘图——只需要改变数据集即可。如下例所示,用%+%来添加新的数据集以替代原有数据集:

1
2
p <- ggplot(mtcars, aes(mpg, wt, colout = cyl)) + geom_point()
p


1
2
mtcars <- transform(mtcars, mpg = mpg^2)
p %+% mtcars

图形属性的映射

图和图层

1
2
p <- ggplot(mtcars)
p <- p + aes(wt, hp)
1
2
p <- ggplot(mtcars, aes(mpg, wt))
p + geom_point()


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


用disp修改y坐标:

1
p + geom_point(aes(y = disp))

图层图形属性可以添加、修改或删除:

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

设定和映射

注意下面三种方式的区别。

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


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


1
p + geom_point(colour = "darkblue")

当颜色映射到’darkblue’时,’darkblue’将被看作一个普通的字符串,使用默认的颜色标度进行标度转换,结果得到了粉红色的点和图例。

分组

多个分组与单个图形属性

很多时候,我们都想将数据分成若干组,并且以相同的方式对每个组进行渲染。当从总体上查看数据时,我们通常希望区分每个个体而不是识别它们,这在含有多个个体的面板数据中是很常见的,而这类图形也常被称为细面图(spaghetti plot)。

1
2
3
data(Oxboys, package = "nlme")
p <- ggplot(Oxboys, aes(age, height, group = Subject)) + geom_line()
p

或者:

1
qplot(age, height, data = Oxboys, group = Subject, geom = "line")

如果不进行分组,则图是这样的,没有任何意义:

1
qplot(age, height, data = Oxboys, geom = "line")

不同图层上的不同分组

有时候我们想根据不同水平下的数据整合来对统计汇总信息进行图形绘制,从而不同的图层可能有不同的分组图形属性,因此,有的图层展示个体水平的数据,而有的图层用于展示更大组群的统计信息。
在前面例子的基础上,加入我们想根据所有男孩的年龄和身高在图中添加一条光滑线条。如果还用和前面绘制折线图时同样的分组方式,我们会得到下图:

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

这并不是我们想要的结果:我们无意间给每一个男孩添加了一条光滑线条。因此这个新图层需要一个不同的分组图形属性,group = 1,这样绘制的线条才是根据整体数据的:

1
p + geom_smooth(aes(group = 1), method = "lm", se = F, colour = "darkorange")

修改默认分组

如果图形中含有离散型变量,而你却想绘制连接所有分组的线条,那么你可以采取绘制交互作用图、轮廓图以及平行坐标图时所用的策略。这里以绘制各个测量时期(Occassion)、身高(height)的箱线图为例:

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

这里没有必要设定分组,因为Occasion是一个离散型变量,所以默认分组就是它。要在此基础上添加个体轨迹,我们需要用aes(group = Subject)修改第一层的默认分组:

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

在第二个图层中,我们该变了线条的颜色以区分箱线图,这又是一个将某个图形属性设定为固定值的例子。此处的颜色是一个渲染属性,在数据中没有对应的变量。

匹配图形属性和图形对象

群组几何对象另一个重要议题是:如何将个体的图形属性映射给整体的图形属性。对于个体几何对象而言这不是个问题,因为每一条观测值都被一个单一的图形元素所表示。然而,高密度数据将会使得区别单个的点变得困难(或者不可能),这也意味着如果单个点几何对象变成一个群组几何对象,就变成了一大团点。

线条和路径遵循差一原则:观测点比险段数目多一,第一条线段将使用第一条观测的图形属性,第二条线段将使用第二条观测的图形属性,依次类推。这就意味着,最后一条观测的图形属性将不会被用到,如下图所示,路径和线条的另一条限制是每个个体的线条类型都必须是一个参数,因为R不会绘制不同线条类型相连接的线条。

1
2
df <- data.frame(x = 1:3, y = 1:3, colour = c(1, 3, 5))
qplot(x, y, data = df, colour = factor(colour), size = I(5)) + geom_line(aes(group = 1), size = 2)


1
qplot(x, y, data = df, colour = colour, size = I(5)) + geom_line(size = 2)

对于线条和路径,线段的图形属性是由起始点的图形属性决定的。如果颜色是离散的(第一幅图),在相邻的颜色间插入其他颜色是没有任何意义的。如果颜色是连续的(第二幅图),可以在相邻的颜色间进行插补,但默认条件下R不会这样做。

还可以想象一个更为复杂的系统,其中的线段平稳地从一种颜色属性变换到另一种图形属性。这种方式对连续型变量很有效,如大小和颜色,但并不适用于渐变的线条类型。如果想得到这种效果,可以用线性插值法来做:

1
2
3
4
5
6
7
(xgrid = with(df, seq(min(x), max(y), length = 50)))
(interp <- data.frame(
x = xgrid,
y = approx(df$x, df$y, xout = xgrid)$y,
colour = approx(df$x, df$colour, xout = xgrid)$y
))
qplot(x, y, data = df, colour = colour, size = I(5)) + geom_line(data = interp, size = 2)

对于其他的群组几何对象,如多边形,只有当所有的个体的图形属性都相同时,该图形属性才会被使用,否则将使用默认值。这适用于fill参数,因为它是整体对象的一个性质:考虑多边形边界上的点填充不同的颜色没有任何意义。

当图形属性映射到连续型变量时,这类问题经常被涉及。因为正如上述,当映射对象是离散型的变量时,它将默认地把几何对象分解成更小的块。这对条形图和面积图的绘制非常有效,因为它将每小块堆积起来就可以得到和原来未分组前一样的形状:

1
(p <- ggplot(diamonds, aes(x = color)) + geom_bar())


1
p + geom_bar(aes(fill = cut))

几何对象

几何对象,简称为geom,它执行着图层的实际渲染,控制者生成的图像类型。
每一个几何对象都有一个默认的统计变换,并且每一个统计变换都有一个默认的几何对象。例如封箱(bin)统计变换默认使用bar几何对象来绘制直方图。修改默认值虽然能生成符合语法的绘图,但是它们可能违反一些绘图惯例。

ggplot2中的几何对象

名称 描述
abline 线,由斜率和截距决定
area 面积图
bar 条形图
bin2d 2维热力图
blank 空白
boxplot 箱线图
contour 等高线图
crossbar 带有水平中心线的盒子图
density 光滑密度曲线图
density2d 二维密度等高线图
dotplot “点直方图”,用点表示观测值的个数
errorbar 误差棒
errorbarh 水平的误差棒
freqplot 频率多边形图
hex 用六边形表示的2维热力图
histogram 直方图
hline 水平线
jitter 给点添加扰动,减轻图形重叠问题
line 按照x坐标的大小顺序依次连接各个观测值
linerange 一条代表一个区间的竖直线
map 基准地图里的多边形
path 按数据的原始顺序连接各个观测值
point 点,用来绘制散点图
pointrange 用一条中间带点的竖直线代表一个区间
polygon 多边形,相当于一个有填充的路径
quantile 添加分位数回归线
raster 高效的矩形瓦片图
rect 2维的矩形图
ribbon 色带图,连续的x的值所对应的y的范围
rug 边际地毯图
segment 添加线段或箭头
smooth 添加光滑的条件均值线
step 以阶梯形式连接各个观测值
text 文本注释
tile 瓦片图
violin 小提琴图
vline 竖直线

统计变换

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

名称 描述
bin 计算封箱数据
bin2d 计算矩形封箱内的观测值的个数
bindot 计算“点直方图”的封箱数据
binhex 计算六边形热图的封箱数据
boxplot 计算组成箱线图的各种元素值
contour 三维数据的等高线
density 一维密度估计
density2d 二维密度估计
function 添加新的函数
identity 不对数据进行统计变换
qq 计算qq图的相关值
quantile 计算连续的分位数
smooth 添加光滑曲线
spoke 将角度和半径转换成xend和yend
sum 计算每个单一值的频数,有助于解决散点图的图形重叠问题
summary 对每个x所对应的y值做统计描述
summary2d 对二维矩形封箱设定函数
summaryhex 对二维六边形封箱设定函数
unique 删除重复值
ydensity 小提琴图,计算1维y轴方向的核密度函数估计值

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

  • count:每个组里观测值的数目;
  • density:每个组里观测值的密度;
  • x:组的中心位置。

这些生成变量可以被直接调用:

1
ggplot(diamonds, aes(carat)) + geom_histogram(aes(y = ..density..), binwidth = 0.1, fill = 'darkorange')

生成变量的名字必须用..围起来。这样可以与原数据集中的变量发生混淆,并且以后处理代码时,你可以很清晰得分辨出哪些是由统计变换生成的。每个统计变换的帮助文档里都列出了其生成变量的名称。

类似,用qplot()生成该图像的代码如下:

1
qplot(carat, ..density.., data = diamonds, geom = "histogram", binwidth = 0.1, fill = I('darkorange'))

位置调整

五种位置调整参数:

名称 描述
dodge 避免重复,并排放置
fill 堆叠图形元素并将高度标准化为1
identity 不做任何调整
jitter 给点添加扰动避免重合
stack 将图形元素堆叠起来

堆叠

1
2
p <- ggplot(diamonds, aes(clarity, fill = cut))
p + geom_bar(position = "stack")

填充

1
p + geom_bar(position = "fill")

并列

1
p + geom_bar(position = "dodge")

identity adjustment

即不做调整,这种不适用与条形图,但是适用于线图:

1
p + geom_bar(position = "identity")


1
2
library(ggthemes)
ggplot(diamonds, aes(clarity)) + geom_line(aes(y = ..count.., group = cut, colour = cut), stat = "count") + theme_igray() + scale_color_colorblind()

整合

结合几何对象和统计变换

将几何对象和不同的统计变换进行组合就能绘制出新颖的图形:

1
2
d <- ggplot(diamonds, aes(carat)) + xlim(0, 3)
d + stat_bin(aes(ymax = ..count..), binwidth = 0.1, geom = "area", fill = "#FC4A1A") + theme_igray(base_family = "")


1
2
3
4
d + stat_bin(aes(size = ..density.., colour = cut),
binwidth = 0.1, geom = "point", position = "identity") +
theme_igray(base_family = "") +
scale_color_colorblind()


1
2
3
4
d + stat_bin2d(
aes(y = 1, fill = ..count..), binwidth = 0.1,
geom = "tile", position = "identity"
) + theme_igray(base_family = "")

改变图形属性和数据集

有时候我们需要把不同的数据画在不同的图层上。
首先我们读入nlme包,然后拟合一个截距和斜率都包含随机效应的混合模型。首先拟合模型和绘制每个男孩的成长轨迹图:

1
2
3
require(nlme, quietly = T, warn.conflicts = F)
model <- lme(height ~ age, data = Oxboys, random = ~ 1 + age | Subject)
(oplot <- ggplot(Oxboys, aes(age, height, group = Subject)) + geom_line() + theme_igray())

随后,我们对预测的生长轨迹和实际的生长轨迹进行对比。我们先建立一个包含所有年龄和个体组合的网格数据框。对于这个简单的线性模型这么做显然有点小题大做,但是仍作为例子放在这里,为以后处理更加复杂的模型提供参考。接下来我们把模型的预测值添加到刚刚生成的数据集中,变量名叫height:

1
2
3
4
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相同的变量名,并且我们想使用相同的分组图形属性,所以我们不用修改任何图形参数,只需修改默认数据集即可。我们还设定了颜色和大小两个图形参数以便与图形的比较:

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

从图形上看,这个模型似乎很好地拟合了该数据更深层次的结构,但是依然很难分辨细节。另一种比较模型好坏的方法是观察残差,所以我们也演示一下这种方法。首先我们把拟合值和残差都添加到原数据集里面,然后更新数据集(用%+%),将默认的y图形属性改成resid,最后对整个数据添加一条光滑曲线:

1
2
3
Oxboys$fitted <- predict(model)
Oxboys$resid <- resid(model)
oplot %+% Oxboys + aes(y = resid) + geom_smooth(aes(group = 1))

从图形中可以看到残差不是随机分布的,因此所建立的模型是有缺陷的。我们可以向模型中添加一个二次项:

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

值得注意的是,我们对图形对象的修改是非常容易的,我们更新了数据并且重新做了两次图,却没有再次运行oplot,这正式ggplot2图层功能所秉承的理念:使得反复拟合和评估模型变得轻松而自然。

# R

评论

程振兴

程振兴 @czxa.top
截止今天,我已经在本博客上写了604.4k个字了!

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×