1.3 数据可视化
数据可视化是数据分析流程中离人眼最近的一环。它把抽象的数值矩阵翻译成形状、颜色与位置,让模式、异常与趋势在没有语言介入之前就被识别出来。R 语言在可视化领域拥有特别的地位,原因在于它把图形本身当作一种可组合的语法对象来对待。本节聚焦于 ggplot2 这一现代 R 可视化体系的核心,从其设计哲学出发,逐层展开图层、分面、统计变换、主题与扩展生态
1.3.1 图形的语法:ggplot2 的设计哲学
ggplot2 由 Hadley Wickham 在 2005 年的博士论文中提出,其理论根源是 Leland Wilkinson 在 1999 年出版的 The Grammar of Graphics。Wilkinson 试图回答一个看似简单实则重要的问题:一张统计图能否被分解为一组可复用的、正交的组件?他给出的答案是肯定的,并把这套组件体系称为图形语法。Wickham 把这套思想引入 R,简化掉一些数学细节,形成了一个工程上可用的实现,即 ggplot2。
Wilkinson 的图形语法把一张图拆分为若干独立的组件。最底层的组件是数据与变量代数,描述数据如何被引用与变换;其上是标度与坐标,描述数据值到几何空间的映射;再上是几何对象与统计变换,决定图层呈现为何种形状以及如何被汇总;最上层是引导与美学,控制图例、标题、颜色等修饰。每一层都是独立的,可以单独替换或重组。这种正交设计意味着改变一个组件不会牵动其他组件,从而让图形具备了可组合性。
ggplot2 的核心是分层设计。一张图被视为若干层的叠加,每一层都由相同的几类元素构成:数据是图所依附的来源;映射描述数据列如何对应到视觉属性;几何对象决定图层呈现为何种形状;统计变换决定数据在绘制前如何被汇总;标度控制从数据值到视觉属性值的转换规则;坐标系界定视觉属性被投射到什么空间中;分面把数据按条件切分为多张子图;主题控制所有与数据无关的视觉细节。这一组词汇构成了 ggplot2 的语言骨架。
这种设计与 base R 的 plot() 函数有根本差异。plot() 是命令式的:调用一次画一张图,再调用 points()、lines() 等函数时,是在已有画布上叠加笔触,所有状态都隐藏在图形设备里。ggplot2 是声明式的:你描述图应该是什么样子,而无需关心先画什么再画什么;每一层之间的关系通过 + 运算符显式组合,整张图在打印时才被渲染。这种差别带来的实际后果是:ggplot2 的图天然可复用、可改造,并且其代码本身就构成一份关于图的文档。
ggplot2 的分层结构在代码上对应着 + 的链式调用。这里的 + 并非算术加法,它把右边的图层追加到左边对象上,构成一个新的图层组合对象。这种语法后来被 Python 的 plotnine 完整复刻,以便在 Python 中以相同的思维模式使用 ggplot 语法。
理解 ggplot2 的最佳入口是它的最小骨架:一个 ggplot() 调用加上一个 geom_*() 图层,就足以产出第一张图。本节后续所有内容都是对这个骨架在不同方向上的扩展。这一扩展过程的真正目的,是逐步揭开每一层组件背后的设计意图:为什么需要映射、为什么需要标度、为什么需要统计变换。只有理解了意图,才能在面对新数据时知道该调哪个组件、不该调哪个组件。
下面这段代码展示了一个 ggplot2 最小可运行示例:调用 ggplot() 传入数据与映射,叠加 geom_point() 画散点。代码很短,但它已经包含 ggplot2 重要的两类组件:数据来源与映射声明。
library(ggplot2)
ggplot(data = mpg, mapping = aes(x = displ, y = hwy)) +
geom_point()这个最小骨架的扩展路径非常清晰:要加颜色就再加一个映射或一个标度;要加趋势线就再加一个 geom_smooth();要分面就再加一个 facet_*();要改主题就再加一个 theme_*()。每一项扩展都通过 + 运算符追加,前后顺序不影响最终结果(标度与主题的某些参数除外)。这种线性可叠加的设计,让 ggplot2 的代码读起来像一条流水线:从数据出发,逐层添加修饰,最终到达成图。
如果你刚接触 ggplot2,建议从最小骨架开始:先用 ggplot(data, aes(...)) + geom_point() 画出第一张图,确认数据流是通的;再逐项添加映射、标度、主题。一次性写二十行 ggplot2 代码再调试往往找不到问题源头,因为每一层都可能藏着一个 typo。
1.3.2 图层系统:从几何对象到标度
图层是 ggplot2 的最小有意义单元。每一层至少包含一个几何对象、一组映射与一份默认统计变换。当你写下 geom_point(),ggplot2 默认它的统计变换是恒等变换,把数据原样画出来;当你写下 geom_histogram(),它默认的统计变换是 stat_bin,把连续变量切分为区间并计数。理解这一点有助于解释为何同一份数据用不同 geom 出来的形状差异如此之大。
映射通过 aes() 函数声明。aes(x = displ, y = hwy, color = class) 的含义是:把数据框的 displ 列映射到 x 位置,hwy 列映射到 y 位置,class 列映射到颜色。映射区别于直接设置:在 aes() 里写 color = "blue" 会得到一条名为 blue 的图例,而把 color = "blue" 写在 geom_point() 外面则会把所有点染成蓝色。这是初学者最常踩的坑之一,记住一个原则即可避开:变量进 aes,常量出 aes。
下面这段代码用 ggplot2 内置的 mpg 数据集演示一个完整的散点图。mpg 描述了 1999 至 2008 年间部分车型的油耗与排量,是 ggplot2 教学常用的样本之一。
library(ggplot2)
ggplot(data = mpg,
mapping = aes(x = displ, y = hwy, color = class)) +
geom_point(size = 2, alpha = 0.7) +
labs(title = "排量与高速油耗",
x = "发动机排量 (L)", y = "高速油耗 (mpg)",
color = "车型")geom_* 函数族覆盖了统计图形的常见类型。geom_point 画散点,geom_line 与 geom_path 画折线和路径,geom_bar 与 geom_col 画柱状图,前者对离散变量计数、后者直接给定高度,geom_histogram 与 geom_density 画直方图与密度曲线,geom_boxplot 与 geom_violin 画箱线图与小提琴图。每个 geom 都有自己默认的视觉属性与默认的统计变换,理解这两者就理解了该 geom 的全部行为。
下面的代码把几种常用几何对象并排展示,便于对照它们对同一份数据的不同诠释。注意 geom_bar 不需要 y 美学,因为它内部会调用 stat_count 计算频数;而 geom_col 需要显式给出 y,因为它默认使用恒等统计变换。
library(patchwork)
p_bar <- ggplot(mpg, aes(x = class)) +
geom_bar(fill = "steelblue") +
coord_flip() +
labs(title = "geom_bar:自动计数")
p_col <- ggplot(mpg, aes(x = reorder(class, hwy, median), y = hwy)) +
geom_col(fill = "steelblue") +
coord_flip() +
labs(title = "geom_col:给定高度")
p_hist <- ggplot(mpg, aes(x = hwy)) +
geom_histogram(bins = 15, fill = "steelblue", color = "white") +
labs(title = "geom_histogram:分箱计数")
p_dens <- ggplot(mpg, aes(x = hwy, fill = drv)) +
geom_density(alpha = 0.4) +
labs(title = "geom_density:核密度")
(p_bar | p_col) / (p_hist | p_dens)geom_line 与 geom_path 都画折线,但连接点的方式不同。geom_line 按 x 值排序后连接,适合时间序列;geom_path 按数据原始顺序连接,适合运动轨迹。在生信场景中,geom_line 用于绘制基因表达时间序列,geom_path 用于绘制降维后的细胞轨迹(如 Monocle 拟时间)。下面这段代码演示两者的差异:同一份数据用不同 geom 出来的折线方向截然不同。
set.seed(42)
df <- data.frame(
t = 1:20,
y = cumsum(rnorm(20))
)
p_line <- ggplot(df, aes(x = t, y = y)) +
geom_line(color = "steelblue") +
geom_point(color = "steelblue") +
labs(title = "geom_line:按 x 排序连接")
p_path <- ggplot(df[sample(20), ], aes(x = t, y = y)) +
geom_path(color = "tomato") +
geom_point(color = "tomato") +
labs(title = "geom_path:按行顺序连接")
p_line | p_pathgeom_area 在折线下方填充颜色,用于强调累计量;geom_ribbon 画两条线之间的色带,常用于显示置信区间。下面这段代码用 geom_ribbon 把预测均值的上下界画出来,这是生信时序建模常见的可视化模式。
set.seed(7)
df <- data.frame(
t = 1:30,
y = sin(1:30 / 5) + rnorm(30, 0, 0.2)
)
df$yhat <- predict(loess(y ~ t, df), se = TRUE)$fit
df$se <- predict(loess(y ~ t, df), se = TRUE)$se.fit
df$ymin <- df$yhat - 1.96 * df$se
df$ymax <- df$yhat + 1.96 * df$se
ggplot(df, aes(x = t)) +
geom_ribbon(aes(ymin = ymin, ymax = ymax),
fill = "steelblue", alpha = 0.3) +
geom_line(aes(y = yhat), color = "steelblue", size = 1) +
geom_point(aes(y = y), color = "grey30", alpha = 0.6) +
labs(title = "LOESS 拟合与 95% 置信带",
x = "时间", y = "观测与预测") +
theme_minimal()标度 scale_* 控制映射的转换规则。当你写下 aes(color = class),ggplot2 会自动调用 scale_color_discrete() 给每个类别分配一个默认颜色。若你希望改用 ColorBrewer 的 Set1 调色板,就把默认标度替换为 scale_color_brewer(palette = "Set1")。标度的命名遵循 scale_<视觉属性>_<映射类型> 的模式:scale_x_log10 把 x 轴做对数变换,scale_fill_gradient 把连续填充做成渐变色,scale_size_continuous 控制点大小的连续映射范围。标度是 ggplot2 中最细致的一块,但只要掌握其命名规律,几乎不必死记参数。
下面这段代码演示了几种常用标度的组合:ColorBrewer 离散色板、viridis 连续色板、对数 x 轴、自定义图例顺序。这种组合方式在生信可视化中尤其常见,例如基因表达热图、火山图、富集分析图都需要精细的标度控制。
ggplot(mpg, aes(x = displ, y = hwy, color = cty, size = cyl)) +
geom_point(alpha = 0.7) +
scale_color_viridis_c(option = "plasma") +
scale_size_continuous(range = c(2, 8), breaks = c(4, 6, 8)) +
labs(title = "排量、油耗、气缸数联合可视化",
x = "排量 (L)", y = "高速油耗 (mpg)",
color = "城市油耗", size = "气缸数") +
theme_minimal()下面这段 Python 代码用 matplotlib 与 seaborn 实现类似的散点图。可以对照看出,ggplot2 把数据到视觉属性的映射放在 aes() 里集中声明,而 matplotlib 则需要在每次绘图时把映射手动算成具体颜色。
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
mpg = sns.load_dataset("mpg")
fig, ax = plt.subplots(figsize = (7, 5))
classes = mpg["origin"].unique()
palette = sns.color_palette("Set1", len(classes))
for cls, color in zip(classes, palette):
sub = mpg[mpg["origin"] == cls]
ax.scatter(sub["displacement"], sub["mpg"],
s = 30, alpha = 0.7, color = color, label = cls)
ax.set_title("排量与油耗")
ax.set_xlabel("发动机排量")
ax.set_ylabel("油耗 (mpg)")
ax.legend(title = "车型")
plt.show()当 ggplot2 默认的颜色不够用或不够美,几乎总能通过替换 scale_color_* 或 scale_fill_* 解决。熟悉 scale_color_brewer、scale_color_viridis_d、scale_color_manual 这三个就足以应对大多数场景。ColorBrewer 适合离散分类型,viridis 适合连续数值型且对色盲友好,manual 适合自定义品牌色或对照色。
标度除了控制颜色,还控制坐标轴。scale_x_log10 与 scale_y_log10 把坐标轴做对数变换;scale_x_continuous 接收 trans 参数做任意变换;scale_x_date 处理日期轴的刻度与标签;scale_x_reverse 把轴方向反转。下面这个例子用对数 x 轴展示经济数据中常见的长尾分布。
ggplot(diamonds, aes(x = price, fill = cut)) +
geom_density(alpha = 0.5) +
scale_x_log10(labels = scales::dollar) +
scale_fill_brewer(palette = "Set2") +
labs(title = "钻石价格分布(对数尺度)",
x = "价格 (USD, log)", y = "密度", fill = "切工") +
theme_minimal()1.3.3 分面:条件分布的可视化
分面是 ggplot2 中最具洞察力的功能之一。它解决一个常见的问题:当你想看一个变量在不同条件下的分布差异时,把所有曲线叠在一张图上往往会互相遮挡,颜色或形状区分不足以让眼睛分辨。分面的做法是按条件变量把数据切成若干子集,每个子集画一张小图,所有小图共享坐标轴与标度,从而视觉上具备可比性。
ggplot2 提供两种分面函数。facet_wrap() 接收一个公式(如 ~ class),把子图按一维方式排列,自动换行,适合条件变量取值较多的情况。facet_grid() 接收形如 row_var ~ col_var 的公式,按两个条件变量组成二维网格,适合同时观察两个维度的细分。两者共享同一组参数:scales 控制子图坐标轴是否自由,labeller 控制子图标题样式,nrow 与 ncol 控制排列方式。
下面这段代码用 facet_wrap 把不同车型的油耗分布并排展示。共享标度让对比变得公平:所有子图用同一直方图分箱,同一 x 轴范围。
ggplot(mpg, aes(x = hwy)) +
geom_histogram(bins = 15, fill = "steelblue", color = "white") +
facet_wrap(~ class, scales = "free_y") +
labs(x = "高速油耗 (mpg)", y = "车型数") +
theme_minimal()scales 参数控制子图是否自由设定坐标轴。默认是 fixed,所有子图共用刻度,便于横向比较;设为 free 时各子图独立缩放,便于观察各自内部结构。这是一个常被误用的参数:当你的目的是比较子图间的绝对水平,应当用 fixed;当你的目的是看各组的分布形态,可以用 free。free_x 与 free_y 分别只放开一个方向的标度。
下面是用 facet_grid 同时按驱动方式与年份切分的例子。二维分面在生信场景中常用于按处理条件与时间点切分实验数据,便于一眼看出交互效应。
ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point(alpha = 0.6) +
geom_smooth(method = "lm", se = FALSE, color = "tomato") +
facet_grid(drv ~ year) +
labs(x = "排量", y = "高速油耗")labeller 参数让分面标题可以更可读。label_both 会同时显示变量名与变量值(如 drv: 4),label_parsed 允许使用数学表达式。下面这段代码演示如何自定义分面标签。
ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point(alpha = 0.5) +
facet_wrap(~ class, ncol = 4,
labeller = labeller(class = label_both)) +
labs(x = "排量", y = "高速油耗") +
theme_minimal()当条件变量取值差异较大时,free 与 fixed 的对比明显。下面的例子把同一份数据用两种 scales 设置画出来:左图所有子图共用坐标轴,便于看到 compact 与 pickup 在油耗分布上的绝对差异;右图各子图独立缩放,便于看到每一类内部的形状特征。
library(patchwork)
p_fixed <- ggplot(mpg, aes(x = hwy)) +
geom_histogram(bins = 12, fill = "steelblue") +
facet_wrap(~ class, scales = "fixed") +
labs(title = "scales = fixed") +
theme_minimal()
p_free <- ggplot(mpg, aes(x = hwy)) +
geom_histogram(bins = 12, fill = "steelblue") +
facet_wrap(~ class, scales = "free") +
labs(title = "scales = free") +
theme_minimal()
p_fixed | p_free当分面变量很多时,可以通过 ggplot(mpg, aes(...)) + facet_wrap(~ var, ncol = X) 控制每行的子图数量,让布局更紧凑。strip.position 参数控制子图标题的位置(top、bottom、left、right),在水平排列时把标题放到顶部,垂直排列时放到右侧,可以让子图占用更少空间。
ggplot(mpg, aes(x = hwy)) +
geom_histogram(bins = 12, fill = "steelblue", color = "white") +
facet_wrap(~ class, ncol = 4, strip.position = "top") +
labs(x = "高速油耗", y = NULL) +
theme_minimal() +
theme(axis.text.y = element_blank(),
panel.spacing = unit(0.1, "lines"))分面是探索性数据分析的利器。当一张图信息量过大、曲线互相覆盖时,第一反应应当是分面而不是改颜色。分面把噪声从一张图分散到多张图,让规律自然浮现。这一思路在生信分析中有用:把基因表达按组织类型或疾病状态分面,往往能立刻看出组织特异性与状态特异性的差异。
1.3.4 统计图层:平滑线与汇总
ggplot2 的图层可以画原始数据,也可以画统计变换后的结果。两个常用的统计图层是 geom_smooth() 与 stat_summary()。它们体现了 ggplot2 的一个核心理念:把统计计算嵌入图形。这样数据更新时图自动更新,统计变换与可视化保持一致,避免传统流程中先汇总再画的脱节。
geom_smooth() 在散点图上叠加一条平滑曲线,默认使用 LOESS 局部多项式回归,样本量较大时自动切换为 GAM。它同时画出置信区间带,由 se = TRUE 控制。method 参数可以切换为线性回归 "lm"、广义线性模型 "glm"、稳健回归 "rlm"、分位数回归 "rq" 等。这一层把散点图从一锅散乱的点提升为一条带置信区间的趋势线,让眼睛立刻抓住主方向。
ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point(alpha = 0.5) +
geom_smooth(method = "loess", span = 0.6,
color = "darkorange", fill = "darkorange",
alpha = 0.2) +
labs(title = "排量与油耗的趋势关系")span 参数控制 LOESS 平滑的窗口大小,取值 0 到 1 之间。值越大曲线越平滑,越小越贴近数据。同一段数据用不同 span 出来的曲线可能差异显著,因此科研图中应当显式声明 span 取值,避免审稿人无法复现。
下面这段代码把 LOESS、线性回归、GAM 三种平滑方法画在同一张图上对比。每种方法有不同的适用假设:线性回归假设线性关系,LOESS 假设局部平滑,GAM 假设加性结构。在不同数据结构下,它们的表现差异很大。
library(patchwork)
p_loess <- ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point(alpha = 0.4) +
geom_smooth(method = "loess", span = 0.4, color = "darkorange") +
labs(title = "LOESS (span = 0.4)")
p_lm <- ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point(alpha = 0.4) +
geom_smooth(method = "lm", color = "steelblue") +
labs(title = "线性回归")
p_gam <- ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point(alpha = 0.4) +
geom_smooth(method = "gam", formula = y ~ s(x, bs = "cs"),
color = "forestgreen") +
labs(title = "GAM")
(p_loess | p_lm | p_gam) &
theme_minimal()stat_summary() 是一个更通用的汇总层。它接收一个汇总函数(如 mean_cl_boot、median_hilow、mean_se),在每个 x 取值处计算该函数并绘制结果。它常用于带误差条的均值图,是 ggpubr 类科研图表的底层实现。
ggplot(mpg, aes(x = class, y = hwy)) +
stat_summary(fun = "mean", geom = "point", size = 3) +
stat_summary(fun.data = "mean_cl_boot", geom = "errorbar", width = 0.2) +
labs(x = "车型", y = "高速油耗均值 (95% CI)") +
theme_minimal()fun 接收单值汇总函数(如 mean、median),输出一个数;fun.data 接收多值汇总函数(如 mean_cl_boot、median_hilow),输出 ymin、y、ymax 三个数。Hmisc 包提供了常用的多值汇总函数:mean_cl_normal 给正态近似的置信区间,mean_cl_boot 给 Bootstrap 置信区间,median_hilow 给中位数与四分位距。下面这段代码自定义一个汇总函数,画出中位数与 10/90 分位数。
my_summary <- function(x) {
data.frame(y = median(x),
ymin = quantile(x, 0.10),
ymax = quantile(x, 0.90))
}
ggplot(mpg, aes(x = class, y = hwy)) +
stat_summary(fun.data = my_summary, geom = "pointrange",
color = "steelblue", size = 0.8) +
labs(x = "车型", y = "高速油耗 (中位数 + 10/90 分位)") +
theme_minimal()geom_smooth() 与 stat_summary() 都属于把统计计算嵌入图形的层。它们的共同特点是不需要你预先汇总数据,而是在绘图时即时计算。这意味着当数据更新时,图也自动更新,不会出现图与表不一致的问题。在科研图中,置信区间比单独的均值点更重要,因为它同时表达了点估计与不确定性。
默认的 geom_smooth(method = "loess") 在样本量超过 1000 时会被自动降级为 gam。如果你需要严格可控的平滑结果,请显式指定 method 与 formula,并设置 n、span 等参数。否则同一份代码在不同样本量下可能产出肉眼可辨的差异,导致结果不可复现。
置信区间的可视化是统计图层的一个副产品。geom_smooth 的色带、stat_summary 的误差条都是置信区间的视觉表达。在生信场景中,这种表达适用于基因表达差异、生存曲线、剂量响应曲线等分析。下面这段代码用 geom_smooth 演示按分组分别拟合线性回归并显示 95% 置信带。
ggplot(mpg, aes(x = displ, y = hwy, color = drv, fill = drv)) +
geom_point(alpha = 0.4) +
geom_smooth(method = "lm", formula = y ~ x, alpha = 0.15) +
labs(title = "按驱动方式分组的线性趋势",
x = "排量 (L)", y = "高速油耗 (mpg)", color = "驱动", fill = "驱动") +
theme_minimal()分位数回归是另一种常见的统计图层。geom_quantile() 把数据按分位数分别拟合回归,常用 0.25、0.5、0.75 三个分位数,揭示不同分布位置的响应模式。在医学研究中,分位数回归常用于分析不同百分位的体格指标对结局的影响,比单纯均值回归信息更丰富。
ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point(alpha = 0.3) +
geom_quantile(quantiles = c(0.25, 0.5, 0.75),
color = c("grey60", "steelblue", "tomato"),
size = 1) +
labs(title = "分位数回归:不同分布位置的拟合",
x = "排量 (L)", y = "高速油耗 (mpg)") +
theme_minimal()下面这段代码对照三种不同的区间函数:标准误、正态置信区间、Bootstrap 置信区间。它们的差异体现了对数据分布假设的不同:标准误假设正态;正态 CI 假设正态并按 1.96 倍放大;Bootstrap CI 不假设分布,通过重采样得到。
library(patchwork)
base <- ggplot(mpg, aes(x = class, y = hwy))
p_se <- base +
stat_summary(fun.data = "mean_se", geom = "pointrange",
color = "steelblue") +
labs(title = "mean_se:均值 ± 标准误") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 30, hjust = 1))
p_cln <- base +
stat_summary(fun.data = "mean_cl_normal", geom = "pointrange",
color = "darkorange") +
labs(title = "mean_cl_normal:正态 95% CI") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 30, hjust = 1))
p_clb <- base +
stat_summary(fun.data = "mean_cl_boot", geom = "pointrange",
color = "forestgreen") +
labs(title = "mean_cl_boot:Bootstrap 95% CI") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 30, hjust = 1))
(p_se | p_cln | p_clb)1.3.5 主题与坐标系统
主题控制所有与数据无关的视觉元素:背景色、网格线、字体、刻度长短、图例位置、边距。ggplot2 内置了几套预设主题,常用的是 theme_gray()(默认)、theme_bw()、theme_minimal() 与 theme_classic()。预设主题的差异主要在于背景色与网格线密度,选择哪一种取决于出版要求与个人审美。生物医学论文通常偏好 theme_classic() 与 theme_bw(),因其去除多余装饰,更接近期刊风格。
当预设主题不够用时,可以通过 theme() 函数逐项定制。theme() 接受大量参数,每一个对应一类视觉元素。常见的是 plot.title、axis.text、legend.position、panel.grid、plot.margin。每个元素通过 element_text()、element_rect()、element_line() 或 element_blank() 来描述。element_blank() 把元素完全删除,常用于去除多余网格线或边框。
ggplot(mpg, aes(x = displ, y = hwy, color = class)) +
geom_point(size = 2) +
labs(title = "排量与油耗",
x = "排量", y = "高速油耗", color = "车型") +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", hjust = 0.5),
legend.position = "bottom",
panel.grid.minor = element_blank(),
plot.margin = margin(15, 15, 15, 15)
)theme() 的参数体系庞大但有序。文本类元素通过 element_text() 描述字体属性(family、face、size、color、hjust、vjust、angle、lineheight);矩形类元素通过 element_rect() 描述填充与边框(fill、color、size、linetype);线条类元素通过 element_line() 描述线条属性(color、size、linetype、lineend)。所有元素都可以传 inherit.blank = TRUE 来从父元素继承空白。
theme() 中常用的元素参数包括:plot.title、plot.subtitle、plot.caption(标题、副标题、说明)、axis.title、axis.text、axis.ticks、axis.line(轴标题、轴文字、轴刻度、轴线)、panel.background、panel.grid.major、panel.grid.minor、panel.border(面板背景、网格、边框)、legend.position、legend.title、legend.text、legend.key(图例位置、标题、文字、色块)、strip.text、strip.background(分面标题、背景)。掌握这套命名规则就掌握了 theme() 的全部能力。
下面这段代码演示图例位置与样式的完整定制。legend.position 接受 "none"(隐藏)、"left"、"right"(默认)、"bottom"、"top",或者形如 c(0.8, 0.2) 的归一化坐标放在图内任意位置。guide_legend() 与 guide_colorbar() 提供更细粒度的图例控制。
ggplot(mpg, aes(x = displ, y = hwy, color = cty)) +
geom_point(size = 2) +
scale_color_viridis_c() +
labs(title = "排量、油耗、城市油耗联合可视化",
x = "排量", y = "高速油耗", color = "城市油耗") +
theme_minimal() +
theme(
legend.position = c(0.85, 0.25),
legend.direction = "vertical",
legend.background = element_rect(fill = "white", color = "grey80"),
legend.title = element_text(face = "bold"),
legend.key.width = unit(0.3, "cm"),
legend.key.height = unit(0.8, "cm")
) +
guides(color = guide_colorbar(title.position = "top",
title.hjust = 0.5,
barwidth = 0.5,
barheight = 6))写论文图表时,把一组合适的 theme() 调用封装成一个函数,例如 my_theme(),在每张图最后 + my_theme() 即可。这比每次重复一遍二十行 theme 参数要清爽得多,也保证整篇文档风格一致。这也是科研团队内部建立图表风格统一的有效方式。
下面是一个封装自定义主题的示例。这种写法在生信团队的图表规范中常见:固定字体大小、去除次要网格、调整图例位置、留出足够的边距避免 PDF 裁切。
my_theme <- function(base_size = 11) {
theme_minimal(base_size = base_size) +
theme(
plot.title = element_text(face = "bold", hjust = 0.5,
margin = margin(b = 8)),
plot.subtitle = element_text(hjust = 0.5, color = "grey40"),
axis.title = element_text(face = "bold"),
axis.text = element_text(color = "grey20"),
panel.grid.minor = element_blank(),
legend.position = "bottom",
legend.title = element_text(face = "bold"),
plot.margin = margin(15, 20, 15, 20)
)
}
ggplot(mpg, aes(x = displ, y = hwy, color = class)) +
geom_point(size = 2, alpha = 0.7) +
labs(title = "排量与油耗", x = "排量", y = "高速油耗") +
my_theme()坐标系决定了图层最终被投射到什么空间。常用的是笛卡尔坐标系 coord_cartesian(),它支持 xlim 与 ylim 来限定视野但不丢弃数据,这与 xlim() 标度函数的截断行为不同:coord_cartesian(xlim = c(0, 10)) 仍会绘制超出范围的点(在视觉边界外),而 xlim(0, 10) 会把超出范围的点从数据中删除,可能影响 geom_smooth 的拟合结果。这是初学者常忽略的细节。
coord_flip() 把 x 与 y 轴对调,常用于把垂直柱状图转成水平柱状图,让长标签可读。coord_polar() 把笛卡尔坐标映射到极坐标,是饼图与小提琴图背后的实现机制。coord_fixed() 固定纵横比,对地图与几何图形很关键。coord_trans() 在坐标系层面做变换,与标度层面的变换有所不同:标度变换先变换数据再绘图,坐标系变换先绘图再变换坐标。
下面是一个用 coord_flip 让水平柱状图更易读的例子,同时切换到 theme_classic。
ggplot(mpg, aes(x = reorder(class, hwy, median), y = hwy)) +
geom_boxplot(fill = "lightblue") +
coord_flip() +
labs(x = NULL, y = "高速油耗") +
theme_classic()注意 reorder(class, hwy, median) 这一行:它按 hwy 在每个 class 内的中位数对 class 排序。这种基于数据自身重排的分类轴是 ggplot2 中常见的模式,避免手动维护因子水平。
coord_polar 把柱状图变成饼图。这一用法揭示了饼图的几何本质:饼图就是极坐标下的堆叠柱状图。理解这一点有助于解释为何饼图常被批评:人类对角度的辨别能力远低于对长度的辨别能力,因此柱状图通常比饼图更易读。
ggplot(mpg, aes(x = "", fill = class)) +
geom_bar(width = 1) +
coord_polar(theta = "y") +
labs(title = "车型占比(饼图)", x = NULL, y = NULL) +
theme_void() +
theme(legend.position = "right")coord_fixed 固定纵横比,让一个数据单位在 x 与 y 方向上占据相同的视觉长度。在地图、基因覆盖图、PCA 散点图等场景中,纵横比不一致会让距离与角度被视觉扭曲。coord_fixed(ratio = 1) 是常用的取值,等价于强制单位长度一致。
ggplot(mpg, aes(x = cty, y = hwy)) +
geom_point(alpha = 0.5) +
geom_abline(slope = 1, intercept = 0, linetype = "dashed",
color = "grey50") +
coord_fixed(ratio = 1, xlim = c(0, 50), ylim = c(0, 50)) +
labs(title = "城市与高速油耗(1:1 等比坐标)",
x = "城市油耗", y = "高速油耗") +
theme_minimal()coord_trans 在坐标系层面做变换,与标度层面的 scale_*_log10 不同。标度变换会先变换数据再绘图,因此拟合曲线是在变换后空间上做的;坐标系变换会先在原始空间绘图再变换坐标,因此拟合曲线仍在原始空间。在展示幂律关系或重尾分布时,两者给出的曲线形状会有微妙差异。
ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point(alpha = 0.5) +
geom_smooth(method = "lm", color = "tomato") +
coord_trans(x = "log10", y = "log10") +
labs(title = "对数-对数坐标下的线性拟合",
x = "排量 (log)", y = "高速油耗 (log)") +
theme_minimal()1.3.6 扩展生态
ggplot2 本身只是一个内核。围绕它生长出的扩展包构成了 R 可视化生态的真正规模。这里只介绍三个常用的扩展:patchwork、plotly 与 ggsave,并简要提及若干其他值得关注的扩展。
patchwork 解决多图拼接问题。在 patchwork 出现之前,拼接多张 ggplot2 图需要 gridExtra 或 cowplot,语法相对繁琐。patchwork 引入运算符重载,让多个图通过 +、| 与 / 像代数表达式一样组合:p1 + p2 横向拼接,p1 / p2 纵向拼接,(p1 | p2) / p3 先横后纵。plot_layout() 控制宽高比例,plot_annotation() 添加整体标题与子图编号。
library(patchwork)
p1 <- ggplot(mpg, aes(x = displ, y = hwy)) + geom_point()
p2 <- ggplot(mpg, aes(x = hwy)) + geom_histogram(bins = 20)
p3 <- ggplot(mpg, aes(x = class, y = hwy)) + geom_boxplot()
p4 <- ggplot(mpg, aes(x = class)) + geom_bar() + coord_flip()
(p1 | p2) / (p3 | p4) +
plot_annotation(
title = "mpg 数据集的四个视角",
tag_levels = "A"
) &
theme_minimal()注意末尾的 & 运算符:它把主题应用到拼图中的所有子图,而 + 只会作用于最后一个子图。这是 patchwork 与 ggplot2 交互时最容易混淆的一点。plot_layout() 提供更细粒度的布局控制,包括宽度比例、高度比例、引导位置等。
p1 + p2 + p3 +
plot_layout(widths = c(2, 1, 1), guides = "collect") +
plot_annotation(tag_levels = "1")patchwork 还支持子图嵌入。inset_element() 把一张小图嵌入到主图的指定区域,常用于在散点图中插入一个分布密度图作为附加说明。坐标系统是 0 到 1 的归一化空间,左下角为原点。
main <- ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point(alpha = 0.6) +
labs(title = "排量与油耗")
inset <- ggplot(mpg, aes(x = hwy)) +
geom_density(fill = "steelblue", alpha = 0.5) +
theme_void()
main +
inset_element(inset,
left = 0.05, bottom = 0.7,
right = 0.35, top = 1)plotly 把 ggplot2 对象转换为交互式 HTML 图表。一行 ggplotly(p) 即可让静态图获得悬停提示、缩放、框选、点击等能力。这在探索性分析与网页仪表盘中很有用。需要注意交互式图表的代价:渲染慢、文件大、复杂主题支持不全。科研论文仍以静态图为主,交互图表更适合演讲、网页与 shiny 应用。
library(plotly)
p <- ggplot(mpg, aes(x = displ, y = hwy, color = class,
text = paste("车型:", model, "<br>年份:", year))) +
geom_point(size = 2, alpha = 0.7) +
labs(title = "排量与油耗")
ggplotly(p, tooltip = "text")tooltip 参数控制悬停时显示哪些字段。默认显示所有映射到美学的变量;指定 tooltip = "text" 后只显示 text 美学中的内容,便于定制提示文案。plotly 也支持 plot_ly() 原生语法,提供更细的交互控制,但语法与 ggplot2 差异较大。
ggsave 是图表输出的标准方式。它根据文件扩展名自动选择设备:.png、.pdf、.svg、.tiff 等。width 与 height 以英寸为单位,dpi 控制像素分辨率。一个常见的坑是 ggsave 保存的图与屏幕上看到的不一致,原因通常是设备字体或矢量图渲染差异。把 device = cairo_pdf 或 device = ragg::agg_png 用上可以避免大部分中文与字体问题。
ggsave("mpg_plot.pdf", plot = p,
width = 8, height = 5, device = cairo_pdf)
ggsave("mpg_plot.png", plot = p,
width = 8, height = 5, dpi = 300,
device = ragg::agg_png)ggsave 默认保存最后一张显示的图,但显式传入 plot = p 是更好的习惯。scale 参数控制输出尺寸相对于英寸的倍率;limitsize 防止输出超过 50 英寸的过大图。科研论文通常要求 300 dpi 以上的位图或矢量 PDF,所以 dpi = 300 是基线值。
输出 SVG 矢量图时,建议显式指定 device = "svg",让期刊编辑能在 Illustrator 中继续编辑。批量输出多张图时,可以用 pdf() 设备配合 print() 在一个 PDF 中嵌入多页,便于把同一项目的所有图集中管理。
ggsave("mpg_plot.svg", plot = p,
width = 8, height = 5, device = "svg")
pdf("mpg_report.pdf", width = 8, height = 5)
print(p1); print(p2); print(p3)
dev.off()ggsave 在保存含有中文字符的 PDF 时,若不指定 device = cairo_pdf,中文字符可能显示为方框。这是因为默认 PDF 设备依赖的字体表不全。cairo_pdf 使用系统字体渲染,能正确显示中文、日文、韩文等 CJK 字符。同样的,PNG 推荐用 ragg::agg_png 替代默认的 png,因为它对字体的处理更可靠。
生态中还有大量其他扩展包:ggpubr 提供科研论文风格的统计检验标注,ggrepel 解决标签重叠问题,ggbeeswarm 改善分类散点的密集显示,ggdist 与 distributional 把贝叶斯后验可视化提升到新高度,gganimate 把静态图扩展为动画,ggiraph 提供 SVG 交互式输出,GGally 提供 ggpairs 矩阵图。这些包都是 ggplot2 图层模型的成功证据:只要遵守图层协议,扩展就能与原系统无缝协作。
ggrepel 是处理标签重叠的利器。当数据点很多且每个点都需要文字标签时,geom_text() 的标签会互相覆盖;geom_text_repel() 通过简单的物理仿真让标签互相排斥,自动调整位置并加上连接线,使标签可读。
library(ggrepel)
ggplot(mpg[1:40, ], aes(x = displ, y = hwy, label = model)) +
geom_point(color = "steelblue") +
geom_text_repel(size = 3, max.overlaps = 15) +
labs(title = "带标签的散点图") +
theme_minimal()ggpubr 把常见的科研统计图与组间检验封装为简洁函数,例如 stat_compare_means() 自动加上 p 值与显著性标记。它底层仍是 ggplot2,所以可以继续用 + 链叠加主题与其他图层。
library(ggpubr)
ggboxplot(mpg, x = "drv", y = "hwy", fill = "drv") +
stat_compare_means(method = "anova") +
labs(title = "不同驱动方式的油耗分布") +
theme_minimal()GGally 提供 ggpairs() 函数,把多个变量的两两关系一次性画出来,包含散点图、相关系数、密度图与分位数图。这在生信数据探索中常用,例如查看多个基因表达指标之间的关系。
library(GGally)
ggpairs(mpg, columns = c("displ", "cty", "hwy"),
aes(color = drv, alpha = 0.5),
upper = list(continuous = "cor"),
lower = list(continuous = "points"),
diag = list(continuous = "densityDiag")) +
theme_minimal()gganimate 在 ggplot2 之上增加动画层。它通过 transition_*() 系列函数控制动画过渡,例如 transition_states() 按离散状态切换,transition_time() 按时间连续过渡。配合 ease_aes() 控制缓动函数,可以做出平滑的数据故事叙述。生信场景中可用于展示细胞分化轨迹、基因表达动态变化等。
library(gganimate)
p <- ggplot(mpg, aes(x = displ, y = hwy, color = class)) +
geom_point(size = 2, alpha = 0.7) +
theme_minimal() +
labs(title = "车型: {closest_state}")
p + transition_states(class, transition_length = 1, state_length = 2) +
ease_aes("cubic-in-out")ggdist 把分布作为美学元素直接画到图上,例如 stat_halfeye() 画半边小提琴图加点估计,stat_dotsinterval() 把分布画成点阵。这类图层在贝叶斯后验可视化中非常实用,能同时表达分布形状、点估计与不确定性。
library(ggdist)
ggplot(mpg, aes(x = hwy, y = class)) +
stat_halfeye(fill = "steelblue", alpha = 0.6,
point_interval = "median_hilow",
.width = c(0.5, 0.95)) +
labs(title = "各车型油耗分布(半眼图)",
x = "高速油耗", y = "车型") +
theme_minimal()最后需要一提的是 ggplot2 与 base R 之间并不存在替代关系。base R 的 plot() 在快速查看与脚本调试中依然最快;lattice 包提供条件绘图能力;基础图形 par() 在多图布局上仍简洁。ggplot2 的优势在于复杂图表的可维护性与扩展性,因此它的适用场景是论文图、报告图、可重复分析流程,而不是临时探索。
下面这段代码把本节讨论过的多个组件综合到一张出版级图表上:分层叠加(散点 + 平滑线 + 置信带)、自定义标度(ColorBrewer 调色板、点大小映射)、分面(按驱动方式切分)、主题定制(去除网格、加粗标题、调整边距)、统计注释(自动标注分组均值)。这种组合方式是科研论文图表的典型模板。
library(ggplot2)
library(ggrepel)
library(scales)
# 取每个 class 中位数最高的若干点用于标注
top_points <- mpg[mpg$class %in% c("compact", "midsize", "suv"), ]
top_points <- top_points[top_points$hwy > quantile(top_points$hwy, 0.95), ]
ggplot(mpg, aes(x = displ, y = hwy, color = class)) +
geom_point(size = 2, alpha = 0.7) +
geom_smooth(method = "lm", formula = y ~ x,
se = TRUE, alpha = 0.15, size = 0.8) +
geom_label_repel(data = top_points,
aes(label = model),
size = 2.5, max.overlaps = 10,
label.size = 0.1, label.padding = 0.2) +
scale_color_brewer(palette = "Set2") +
scale_x_continuous(breaks = pretty_breaks(6)) +
scale_y_continuous(breaks = pretty_breaks(6)) +
facet_wrap(~ drv, ncol = 3) +
labs(title = "排量与高速油耗的关系",
subtitle = "按驱动方式分面,按车型着色",
caption = "数据源: ggplot2::mpg",
x = "发动机排量 (L)", y = "高速油耗 (mpg)",
color = "车型") +
theme_minimal(base_size = 11) +
theme(
plot.title = element_text(face = "bold", size = 14),
plot.subtitle = element_text(color = "grey40"),
plot.caption = element_text(color = "grey50", hjust = 0),
panel.grid.minor = element_blank(),
legend.position = "bottom",
legend.title = element_text(face = "bold"),
strip.text = element_text(face = "bold", size = 11),
strip.background = element_rect(fill = "grey95", color = NA),
plot.margin = margin(15, 20, 15, 20)
)这张图把本节涉及的核心概念都串起来:数据进入 ggplot() 后被 aes() 映射到视觉属性;散点图层 geom_point 与平滑层 geom_smooth 叠加;geom_label_repel 提供可读的标注;标度 scale_color_brewer 控制离散颜色,scale_x/y_continuous 控制刻度;facet_wrap 按驱动方式分面;labs() 与 theme() 完成所有标题与装饰。每一层都独立可控,整张图因此可被精确塑造。这种从数据到呈现的全链路可控,就是 ggplot2 长期占据 R 可视化生态核心位置的真正原因。
1.3.7 常见图表类型与应用场景
前面几节从 ggplot2 的设计哲学出发,逐层拆解了图层、标度、分面、统计变换与主题。本节把这些组件重新组合,聚焦于实际分析中常用的图表类型。每一种图表都有其擅长的数据结构与表达意图,选错图表类型是数据可视化中常见的错误。
需要注意的是,由于该架构不支持R语言运行并插入图片,以下图形仅用于展示,与配套代码毫无关联,以下图片是使用html写的,你可以在整个项目的主文件下找到一个名为R语言.exe的压缩包,加压后里面有对应的rst源码和html文件,这些文件是上一个版本,其中包含大量可运行示例R示例代码与运行结果和生成的图表,如果你注意到了这里,强烈推荐你打开看一看
散点图
散点图用于展示两个连续变量之间的关系,是探索性数据分析的第一步。当数据点较多时,可以通过透明度(alpha)或分箱(geom_bin2d)缓解过度绘制。
ggplot(mpg, aes(x = displ, y = hwy, color = class)) +
geom_point(size = 2, alpha = 0.7) +
geom_smooth(method = "lm", se = FALSE) +
labs(x = "排量", y = "高速油耗", color = "车型")适用场景:基因表达量相关性分析、PCA 降维结果展示、药物剂量-响应曲线。
柱状图
柱状图用于展示分类变量的频数或汇总值。geom_bar 自动计数,geom_col 直接给定高度。
ggplot(mpg, aes(x = reorder(class, hwy, median))) +
geom_bar(fill = "steelblue") +
coord_flip() +
labs(x = NULL, y = "车型数量")适用场景:基因组注释统计、差异基因数量对比、样本分组分布。
直方图
直方图把连续变量切分为等宽区间并计数,用于观察数据的整体分布形态,包括偏度、峰度与多峰性。
ggplot(mpg, aes(x = hwy)) +
geom_histogram(bins = 15, fill = "steelblue", color = "white") +
labs(x = "高速油耗", y = "频数")适用场景:基因表达量分布、测序深度分布、QC 指标分布检查。
密度图
密度图用核密度估计替代分箱计数,给出连续平滑的分布曲线,适合比较多组数据的分布差异。
ggplot(mpg, aes(x = hwy, fill = drv)) +
geom_density(alpha = 0.4) +
labs(x = "高速油耗", fill = "驱动方式")适用场景:基因表达分布的组间比较、细胞表达量分布、批次效应检查。
箱线图
箱线图用五数概括(最小值、Q1、中位数、Q3、最大值)展示分组数据的分布摘要,是科研论文常用的图表之一。箱体高度代表四分位距,须线延伸至 1.5 倍 IQR 范围内的极值,超出此范围的点标记为异常值。
ggplot(mpg, aes(x = class, y = hwy, fill = class)) +
geom_boxplot() +
labs(x = NULL, y = "高速油耗") +
theme(legend.position = "none")适用场景:基因表达的组间差异展示、临床指标分组比较、批次效应可视化。
小提琴图
小提琴图结合了箱线图的摘要与密度图的形状,既显示分布摘要又显示分布形态,信息量比单独的箱线图更大。当组内样本量较少时,可以叠加散点图(geom_jitter)让每个数据点可见。
ggplot(mpg, aes(x = class, y = hwy, fill = class)) +
geom_violin(alpha = 0.6) +
geom_boxplot(width = 0.15, fill = "white") +
labs(x = NULL, y = "高速油耗") +
theme(legend.position = "none")适用场景:单细胞基因表达分布、药物响应分布的组间比较、多组连续变量比较。
折线图
折线图按 x 值排序连接数据点,用于展示时间序列或有序变量的趋势变化。配合 geom_smooth 可以叠加趋势线,配合 geom_ribbon 可以展示置信区间。
ggplot(economics, aes(x = date, y = unemploy)) +
geom_line(color = "steelblue") +
geom_smooth(method = "loess", color = "tomato", se = TRUE,
fill = "tomato", alpha = 0.15) +
labs(x = "日期", y = "失业人数")适用场景:基因表达时间序列、细胞增殖曲线、流行病学趋势图。
饼图
饼图用极坐标下的堆叠柱状图实现,展示分类占比。饼图的可读性不如柱状图(人类对角度的辨别能力远低于对长度的辨别能力),但在类别较少(3-5 个)且需要强调占比时仍有用。
ggplot(mpg, aes(x = "", fill = class)) +
geom_bar(width = 1) +
coord_polar(theta = "y") +
theme_void() +
labs(fill = "车型")适用场景:样本类型占比、基因功能分类占比、突变类型分布。
热图
热图用颜色深浅表示矩阵中数值的大小,是生物信息学中常用的多维数据可视化方式。ggplot2 可以通过 geom_tile 实现热图,但复杂热图通常使用 pheatmap 或 ComplexHeatmap 包,因为它们内置了聚类、注释条与分块切割功能。
library(pheatmap)
set.seed(42)
mat <- matrix(rnorm(100), nrow = 10)
rownames(mat) <- paste0("Gene_", 1:10)
colnames(mat) <- paste0("Sample_", 1:10)
pheatmap(mat,
scale = "row",
cluster_rows = TRUE,
cluster_cols = TRUE,
color = colorRampPalette(c("blue", "white", "red"))(50))适用场景:基因表达矩阵、样本相关性矩阵、基因组变异位点分布、DNA 甲基化热图。
分面图
分面是一种布局策略,按条件变量把数据切分为子集,每个子集画一张小图。分面能把多维信息分散到多张图上,避免单张图信息过载。所有子图共享坐标轴与标度,从而视觉上具备可比性。
ggplot(mpg, aes(x = hwy)) +
geom_histogram(bins = 12, fill = "steelblue", color = "white") +
facet_wrap(~ class, ncol = 4) +
labs(x = "高速油耗", y = "频数") +
theme_minimal()适用场景:多组织基因表达对比、多时间点细胞状态分布、多批次 QC 指标。
本节小结
ggplot2 的真正贡献在于把图形本身定义为可组合、可扩展、可文档化的语法对象。漂亮只是这套语法的副产品。本节按照这条主线展开:先从 Wilkinson 的图形语法理论出发,理解 ggplot2 的分层设计;再深入图层系统,掌握 aes() 映射、geom_* 函数族与 scale_* 标度的协作机制;接着用分面把条件分布拆开来看,让规律从噪声中浮出;之后用 geom_smooth() 与 stat_summary() 把统计计算嵌入图形;再用 theme() 与 coord_* 控制所有与数据无关的视觉细节;最后用 patchwork、plotly、ggsave 把图形组合、交互化、输出。
学习 ggplot2 的过程大致分三个阶段。第一阶段是记住常用 geom 与参数,能把基本图画出来。第二阶段是理解标度与映射的差别,能精确控制每一条线、每一个点的颜色与位置。第三阶段是理解分层与组合,能把复杂的数据故事拆解为一组相互配合的图层。本节的目标是把读者从第一阶段带到第二阶段,并给第三阶段一个入口。剩下的熟练度来自反复地把真实数据画成图,再回头检查代码能否表达清楚自己当初想说什么。
把这套语法内化之后,读者应当能够在面对任何新数据时无需逐个查阅 geom 的参数文档,直接从数据故事出发:选择哪些变量映射到哪些美学、需要哪几种图层叠加、是否需要分面或统计变换、最终如何定制主题与输出。这种从意图到代码的直觉,是 ggplot2 长期训练的最终回报。
练习题
第1题 aes 映射与常量设置
给定 df <- data.frame(x = 1:10, y = rnorm(10)),写出两种代码把散点染成红色:一种会把红色误当作变量生成图例,另一种不会。说明两者的区别。
参考答案
df <- data.frame(x = 1:10, y = rnorm(10))
# 错误写法:color 进了 aes,生成图例
ggplot(df, aes(x = x, y = y, color = "red")) +
geom_point()
# 正确写法:color 放在 aes 外,直接设置颜色
ggplot(df, aes(x = x, y = y)) +
geom_point(color = "red")aes() 内的 color = "red" 把字符串 "red" 当作一个单水平变量映射到颜色,因此会生成一个名为 red 的图例,且实际颜色是 ggplot2 默认的第一种离散色(通常为粉红),而非红色。把 color = "red" 写在 geom_point() 参数里,则是直接设置视觉属性,不产生图例。原则是变量进 aes,常量出 aes。
第2题 分面与统计变换
用 ggplot2 内置的 mpg 数据集,画一张按驱动方式 drv 分面的散点图,x 轴为 displ,y 轴为 hwy,并在每个分面内叠加一条线性回归趋势线(不带置信区间)。
参考答案
library(ggplot2)
ggplot(mpg, aes(x = displ, y = hwy)) +
geom_point(alpha = 0.5) +
geom_smooth(method = "lm", se = FALSE, color = "tomato") +
facet_wrap(~ drv) +
labs(x = "排量", y = "高速油耗") +
theme_minimal()facet_wrap(~ drv) 按驱动方式切分为子图,所有子图共享坐标轴。geom_smooth(method = "lm", se = FALSE) 在每个分面内独立拟合线性回归,se = FALSE 关闭置信区间带。分面会让 geom_smooth 自动按组分别拟合,无需额外声明。
第3题 标度替换
给定散点图 ggplot(mpg, aes(x = displ, y = hwy, color = cty)) + geom_point(),默认颜色标度是蓝色渐变。改用 viridis 的 plasma 色板,并把图例标题改为城市油耗。
参考答案
ggplot(mpg, aes(x = displ, y = hwy, color = cty)) +
geom_point() +
scale_color_viridis_c(option = "plasma", name = "城市油耗") +
labs(x = "排量", y = "高速油耗")scale_color_viridis_c() 用于连续型颜色映射,option = "plasma" 选择 plasma 色板,name 参数设置图例标题。也可以用 labs(color = "城市油耗") 统一管理图例标题,效果等价。
常见错误
错误 1 · aes() 内写常量生成意外图例
原因:ggplot(df, aes(x, y, color = "blue")) 中,"blue" 被当作单水平因子映射到颜色,生成名为 blue 的图例,且实际颜色是默认离散调色板的第一色而非蓝色。
解决:常量颜色写在 aes() 外,即 geom_point(color = "blue")。判断依据是该值是否来自数据框的某一列:是则进 aes(),否则出 aes()。
错误 2 · geom_bar 与 geom_col 混用报错
原因:geom_bar() 默认调用 stat_count 对 x 变量计数,不需要 y 美学;geom_col() 默认使用恒等统计变换,需要显式给出 y 值。把已有高度的数值向量交给 geom_bar() 会报 stat_count() must not be used with a y aesthetic。
解决:已有高度值时用 geom_col();需要对分类变量计数时用 geom_bar()。两者也可以通过 stat 参数互换,但遵循默认语义更清晰。
错误 3 · xlim() 截断导致拟合曲线变形
原因:xlim(0, 10) 会把超出范围的点从数据中删除,geom_smooth() 在剩余数据上重新拟合,曲线形状会改变。这是标度层面的截断。
解决:只想缩放视野而不影响数据,用 coord_cartesian(xlim = c(0, 10))。它在视觉层面限制坐标轴范围,数据仍参与统计计算。两者的区别在含平滑层或汇总层的图中关键。
错误 4 · ggsave 保存 PDF 中文显示为方框
原因:默认 PDF 设备依赖的字体表不含中文字符,渲染时找不到字形就显示为方框。
解决:指定 device = cairo_pdf,它使用系统字体渲染,能正确显示 CJK 字符。PNG 输出推荐用 device = ragg::agg_png 替代默认 png(),字体处理更可靠。