这份教程面向已经会运行 R 代码、希望系统掌握 ggplot2 常用方法的读者。所有示例都使用 R 自带数据或 ggplot2 内置数据,不需要下载外部数据;代码块可以复制到 RStudio 中直接运行。
建议按下面的顺序思考一张图:
核心心智模型:ggplot(data, aes(…)) + geom_*() + scale_*() +
facet_*() + coord_*() + theme_*()。每个 +
都是在图上增加或修改一层。
ggplot() 创建画布,aes()
声明变量到视觉属性的映射,geom_point()
决定用点来画每一行数据。
ggplot(data = mpg, mapping = aes(x = displ, y = hwy)) +
geom_point() +
labs(
title = "排量越大,通常高速油耗表现越低",
x = "发动机排量(升)",
y = "高速油耗(英里/加仑)"
)图 1:发动机排量与高速油耗的关系。
数据与映射也可分别放在图层里。全局 aes()
会被后续图层继承;局部 aes() 只影响所在图层。
aes()
里面:把变量映射到视觉属性,会产生数据驱动的变化和图例。aes()
外面:把所有标记固定为同一个值,通常不产生图例。ggplot(mpg, aes(displ, hwy)) +
geom_point(aes(color = class), alpha = 0.72, size = 2.2) +
labs(
title = "变量放进 aes(),常量放在 aes() 外",
color = "车型类别",
x = "发动机排量(升)",
y = "高速油耗(英里/加仑)"
) +
guides(color = guide_legend(nrow = 2, byrow = TRUE,
override.aes = list(alpha = 1, size = 3)))图 2:颜色映射车型类别,透明度和点大小固定。
下面是最常见的错误之一:aes(color = "steelblue")
会把字符串当成一个类别并创建图例,而不是把点设为蓝色。
这里点的颜色映射到车型类别,而趋势线只按驱动方式 drv
分组。group
控制哪些观察属于同一条线;show.legend = FALSE
隐藏趋势线图例。
ggplot(mpg, aes(displ, hwy)) +
geom_point(aes(color = class), alpha = 0.55) +
geom_smooth(
aes(group = drv),
method = "lm", formula = y ~ x,
se = FALSE, color = "grey25", linewidth = 0.8,
show.legend = FALSE
) +
labs(
title = "每个图层可以拥有自己的映射",
subtitle = "点按车型类别着色;趋势线按驱动方式分组",
color = "车型类别",
x = "发动机排量(升)",
y = "高速油耗(英里/加仑)"
)图 3:局部映射允许不同图层使用不同分组。
当某层不应该继承全局映射时,可使用
inherit.aes = FALSE,并在该层完整指定自己的
data 和 aes()。
离散或取值重复的数据容易重叠。常见对策是减小点、增加透明度,或使用
geom_jitter() 轻微扰动位置。
ggplot(mpg, aes(factor(cyl), cty, color = factor(cyl))) +
geom_jitter(width = 0.16, height = 0, alpha = 0.5, size = 1.8,
show.legend = FALSE) +
labs(
title = "离散 x 轴上用抖动减少遮挡",
x = "汽缸数",
y = "城市油耗(英里/加仑)"
)图 4:抖动显示原本重叠的离散观察。
geom_jitter()
适合展示个体观察,但抖动只是显示手段,不应被解释成真实测量误差。大数据还可考虑二维分箱、等高线或抽样。
geom_smooth()
默认方法会随数据量变化。为保证分析含义明确,最好显式写出方法:
method = "lm":线性模型;method = "loess":局部平滑,适合较小数据;se = TRUE/FALSE:是否显示置信带;formula = y ~ x:明确模型公式。ggplot(mpg, aes(displ, hwy, color = drv)) +
geom_point(alpha = 0.45) +
geom_smooth(method = "lm", formula = y ~ x, se = TRUE, linewidth = 0.9) +
scale_color_brewer(palette = "Dark2") +
labs(
title = "按组拟合线性趋势",
subtitle = "颜色映射会同时让点和趋势线按 drv 分组",
color = "驱动方式",
x = "发动机排量(升)",
y = "高速油耗(英里/加仑)"
)图 5:散点图叠加线性回归线及置信带。
注意:平滑线描述关联,不自动代表因果关系;外推到数据范围之外尤其危险。
geom_line() 会按 x
排序后连接点,适合时间序列;geom_path()
按数据行顺序连接,适合轨迹。多条线必须有正确的
group,而映射 color、linetype
通常会自动建立分组。
unemp_median <- median(economics$unemploy, na.rm = TRUE)
ggplot(economics, aes(date, unemploy)) +
geom_line(color = "#2c7fb8", linewidth = 0.65) +
geom_hline(yintercept = unemp_median, linetype = "dashed", color = "#d95f0e") +
scale_x_date(date_breaks = "8 years", date_labels = "%Y") +
scale_y_continuous(labels = scales::label_number(big.mark = ",")) +
labs(
title = "美国失业人数时间序列",
subtitle = "虚线表示全时期中位数",
x = NULL,
y = "失业人数(千人)",
caption = "数据:ggplot2::economics"
)图 6:时间序列与参考线。
时间变量应是 Date
或日期时间类型,而不是普通字符串。多组时间序列还要检查每组内部是否按时间排序。
geom_bar() 与 geom_col()这两个函数外观相似,数据含义不同:
geom_bar():只有 x,默认
stat = "count",计算每类行数;geom_col():同时提供 x 和
y,直接画已经汇总好的数值。ggplot(mpg, aes(y = class, fill = class)) +
geom_bar(show.legend = FALSE, width = 0.72) +
scale_fill_brewer(palette = "Blues") +
labs(
title = "geom_bar() 用于原始行的计数",
x = "车型数量",
y = NULL
)图 7:geom_bar() 自动计算每个车型类别的数量。
class_mean <- aggregate(hwy ~ class, data = mpg, FUN = mean)
class_mean$class <- reorder(class_mean$class, class_mean$hwy)
ggplot(class_mean, aes(hwy, class, fill = hwy)) +
geom_col(width = 0.72, show.legend = FALSE) +
scale_fill_gradient(low = "#bdd7e7", high = "#08519c") +
labs(
title = "geom_col() 用于已经计算好的数值",
x = "平均高速油耗(英里/加仑)",
y = NULL
)图 8:geom_col() 绘制预先汇总的平均值。
position 控制同一 x 类别中的多个组如何摆放:
| position | 作用 | 适合回答 |
|---|---|---|
"stack" |
默认堆叠 | 总量和组成是多少? |
"dodge" |
并列 | 组间绝对数量如何比较? |
"fill" |
每柱归一到 100% | 组成比例如何比较? |
"identity" |
原位置重叠 | 已有坐标或配合透明度 |
mpg_three <- subset(mpg, class %in% c("compact", "midsize", "suv"))
ggplot(mpg_three, aes(class, fill = drv)) +
geom_bar(position = "fill", width = 0.72) +
scale_y_continuous(labels = scales::label_percent()) +
scale_fill_brewer(palette = "Set2") +
labs(
title = "100% 堆叠柱图比较组成,而不是数量",
x = NULL,
y = "车型占比",
fill = "驱动方式"
)图 9:position = ‘fill’ 比较每类车型的驱动方式构成。
百分比柱会隐藏样本量差异;若每组总数很重要,应同时标注 n
或另画数量图。
直方图的结论可能随箱宽变化。bins
指箱数,binwidth
指每箱宽度;分析时应根据测量精度与样本规模尝试多个合理值。下面把直方图转换为密度尺度,再叠加核密度曲线。
ggplot(mpg, aes(hwy)) +
geom_histogram(
aes(y = after_stat(density)),
binwidth = 2, boundary = 0,
fill = "#9ecae1", color = "white"
) +
geom_density(color = "#d95f0e", linewidth = 1.05, adjust = 1) +
labs(
title = "明确选择箱宽,再解释分布",
subtitle = "直方图 binwidth = 2;曲线为核密度估计",
x = "高速油耗(英里/加仑)",
y = "密度"
)图 10:高速油耗的密度直方图和核密度曲线。
after_stat(density) 引用统计变换产生的变量,是旧写法
..density.. 的现代替代。
箱线图紧凑地显示中位数、四分位距和异常值;小提琴图显示分布形状。叠加使用时,不要让元素互相遮挡。
mpg_four <- subset(mpg, class %in% c("compact", "midsize", "pickup", "suv"))
ggplot(mpg_four, aes(class, hwy, fill = class)) +
geom_violin(trim = FALSE, alpha = 0.65, color = NA, show.legend = FALSE) +
geom_boxplot(width = 0.14, outlier.shape = NA, fill = "white",
linewidth = 0.45) +
scale_fill_brewer(palette = "Set2") +
labs(
title = "分布形状与稳健摘要可以互补",
x = NULL,
y = "高速油耗(英里/加仑)"
)图 11:小提琴图展示形状,窄箱线图展示稳健摘要。
小样本的小提琴形状可能不稳定;必要时叠加抖动点并报告每组样本量。
stat_summary():边汇总边作图stat_summary() 可直接按 x
分组计算中心值与区间。下面的点表示均值,误差线表示“均值 ± 1
个标准差”;它不是置信区间,图注应写清定义。
mean_minus_sd <- function(z) mean(z, na.rm = TRUE) - sd(z, na.rm = TRUE)
mean_plus_sd <- function(z) mean(z, na.rm = TRUE) + sd(z, na.rm = TRUE)
ggplot(mpg, aes(factor(cyl), cty)) +
stat_summary(
fun = mean,
fun.min = mean_minus_sd,
fun.max = mean_plus_sd,
geom = "pointrange",
color = "#2c7fb8",
linewidth = 0.8
) +
labs(
title = "stat_summary() 在绘图时完成分组汇总",
subtitle = "点 = 均值;线 = 均值 ± 1 个标准差",
x = "汽缸数",
y = "城市油耗(英里/加仑)"
)图 12:各汽缸数的城市油耗均值与一个标准差范围。
探索阶段这样很方便;正式分析中,若汇总逻辑复杂,建议先生成明确的汇总数据框,再用
geom_col()、geom_point() 或
geom_errorbar() 作图,便于审计。
facet_wrap() 与 facet_grid()facet_wrap(~ var):一个分面变量,自动换行;facet_grid(rows ~ cols):行列组合有明确语义;scales = "free_y"
等:允许各面板使用不同标度,但会削弱跨面板的绝对比较。ggplot(mpg, aes(displ, hwy)) +
geom_point(aes(color = class), alpha = 0.6, show.legend = FALSE) +
geom_smooth(method = "lm", formula = y ~ x, se = FALSE,
color = "grey25", linewidth = 0.7) +
facet_wrap(~ drv, nrow = 1) +
labs(
title = "facet_wrap() 保持相同坐标,便于直接比较",
x = "发动机排量(升)",
y = "高速油耗(英里/加仑)"
)图 13:按驱动方式分面的排量—油耗关系。
facet_data <- subset(mpg, cyl %in% c(4, 6, 8))
ggplot(facet_data, aes(class, fill = class)) +
geom_bar(show.legend = FALSE) +
facet_grid(drv ~ cyl, scales = "free_x", space = "free_x") +
labs(
title = "facet_grid() 适合有行列语义的比较",
subtitle = "行 = 驱动方式;列 = 汽缸数",
x = NULL,
y = "车型数量"
) +
theme(axis.text.x = element_text(angle = 45, hjust = 1))图 14:行列分面同时编码驱动方式和汽缸数。
空面板有时是重要信息,说明某些组合不存在;不要只为“好看”就删除它。
标度函数通常采用 scale_<视觉属性>_<类型>()
命名,例如:
scale_x_continuous()、scale_y_log10():位置;scale_color_brewer()、scale_fill_viridis_d():离散颜色;scale_color_viridis_c()、scale_fill_gradient():连续颜色;scale_color_manual():手动指定类别颜色;scale_x_date():日期轴。color 通常控制点、线和轮廓;fill
控制柱、面积与封闭形状的内部填充。
diamond_sample <- diamonds[sample.int(nrow(diamonds), 3000), ]
ggplot(diamond_sample, aes(carat, price, color = cut)) +
geom_point(alpha = 0.42, size = 1.2) +
scale_x_log10(
breaks = c(0.3, 0.5, 1, 2, 3),
labels = scales::label_number()
) +
scale_y_log10(labels = scales::label_dollar()) +
scale_color_viridis_d(option = "C", end = 0.9) +
labs(
title = "对数标度揭示乘法关系",
subtitle = "坐标轴被变换,原始数据没有被改写",
x = "重量(克拉,对数轴)",
y = "价格(美元,对数轴)",
color = "切工"
)图 15:钻石重量和价格同时使用对数标度。
对数轴不能显示零或负值,并会改变视觉距离的含义。轴标题或图注明确说明变换。
手动颜色应使用有名称的向量,使颜色与类别按名称匹配,而不是依赖偶然的因子顺序。
drive_colors <- c("4" = "#1b9e77", "f" = "#d95f02", "r" = "#7570b3")
ggplot(mpg, aes(displ, hwy, color = drv)) +
geom_point(alpha = 0.65, size = 2) +
scale_color_manual(
values = drive_colors,
breaks = c("f", "4", "r"),
labels = c("前轮驱动", "四轮驱动", "后轮驱动")
) +
labs(
title = "手动标度同时控制颜色、顺序和标签",
x = "发动机排量(升)",
y = "高速油耗(英里/加仑)",
color = "驱动方式"
)图 16:使用有名称的向量稳定指定类别颜色。
类别顺序会影响轴、堆叠和图例。常用 base R 方法包括:
coord_cartesian():缩放窗口而不删除数据;coord_fixed(ratio = 1):固定 x、y 单位的物理比例;coord_flip():交换横纵轴,仍可读旧代码;新版 ggplot2
通常更推荐直接交换 aes(x, y);coord_polar():极坐标,可制作饼图/环图,但角度和面积较难准确比较。坐标系统作用在标度之后。对需要精确比较的图,位置和长度通常比角度、面积更易读。
labs()
统一管理标题、副标题、坐标轴、图例标题和图注。annotate()
添加单个说明;geom_text() / geom_label()
则从数据框批量添加标签。
hwy_median <- median(mpg$hwy)
ggplot(mpg, aes(displ, hwy)) +
geom_point(color = "#2c7fb8", alpha = 0.55) +
geom_hline(yintercept = hwy_median, linetype = "dashed",
color = "#d95f0e", linewidth = 0.8) +
annotate(
"label", x = 6.2, y = hwy_median + 1.5,
label = paste0("中位数 = ", hwy_median),
hjust = 1, size = 3.5,
fill = "#fff5eb", color = "#a63603", family = plot_family
) +
annotate(
"curve", x = 3.7, y = 42, xend = 2.4, yend = 43,
curvature = 0.2, arrow = arrow(length = grid::unit(0.12, "inches")),
color = "grey35"
) +
annotate("text", x = 3.8, y = 42, label = "高油耗表现车型",
hjust = 0, color = "grey25", family = plot_family) +
labs(
title = "注释应帮助读者看到结论",
subtitle = "避免给每个点都加标签造成拥挤",
x = "发动机排量(升)",
y = "高速油耗(英里/加仑)",
caption = "虚线为样本中位数"
) +
coord_cartesian(clip = "off")图 17:用参考线和注释突出关键区域。
unit() 与 arrow() 来自 R 的
grid 包;它们随 R 一起安装,无需额外依赖。
主题只控制非数据元素,不会改变数据映射。常用完整主题有
theme_minimal()、theme_classic()、theme_bw();theme()
用于局部调整:
element_text():文字;element_line():网格线和轴线;element_rect():背景和边框;element_blank():移除元素。本教程在 setup 代码块中定义了
theme_guide()。项目中把统一样式封装成函数,比在每张图后复制一长串
theme() 更可靠。
ggplot(mpg, aes(displ, hwy, color = class)) +
geom_point(alpha = 0.65, size = 2) +
scale_color_brewer(palette = "Dark2") +
labs(
title = "主题负责版式,标度负责映射",
subtitle = "图例位于底部,次网格线已移除",
x = "发动机排量(升)",
y = "高速油耗(英里/加仑)",
color = "车型类别"
) +
guides(color = guide_legend(nrow = 2, byrow = TRUE,
override.aes = list(alpha = 1, size = 3))) +
theme_guide(base_size = 13)图 18:可复用主题统一字体、网格、标题和图例位置。
若多个图层映射到同一变量,并且标度名称一致,ggplot2
通常会合并图例。可用 guides()
调整顺序、行数和图例中的示例标记,或用
theme(legend.position = "none") 删除不必要的图例。
当两个轴都是类别、单元格表示数量或数值时,geom_tile()
很实用。
heat <- as.data.frame(table(cyl = mpg$cyl, drv = mpg$drv))
ggplot(heat, aes(cyl, drv, fill = Freq)) +
geom_tile(color = "white", linewidth = 0.8) +
geom_text(aes(label = Freq), color = "white", fontface = "bold",
family = plot_family) +
scale_fill_viridis_c(option = "B", begin = 0.2, end = 0.9) +
labs(
title = "geom_tile() 将二维组合映射为颜色",
x = "汽缸数",
y = "驱动方式",
fill = "车型数量"
) +
coord_fixed()图 19:汽缸数和驱动方式组合的车型数量热图。
连续色板应具有感知上相对均匀的亮度变化。viridis
系列通常对色觉差异和灰度打印更友好。若低值单元格文字不清楚,可根据填充值动态切换文字颜色。
不要再使用已弃用的
aes_string()。在函数中按列名编程,可使用
.data[[...]]:
make_scatter <- function(data, x, y, color = NULL) {
if (is.null(color)) {
mapping <- aes(x = .data[[x]], y = .data[[y]])
} else {
mapping <- aes(
x = .data[[x]],
y = .data[[y]],
color = .data[[color]]
)
}
ggplot(data, mapping) +
geom_point(alpha = 0.65, size = 2) +
labs(x = x, y = y, color = color) +
theme_guide()
}
make_scatter(mpg, "displ", "hwy", "drv")这个函数返回的是 ggplot 对象,所以仍可继续叠加:
ggsave() 默认保存最后显示的图;在脚本和函数中,显式传入
plot 更安全。宽高与 dpi
共同决定栅格图像像素尺寸。
p <- ggplot(mpg, aes(displ, hwy, color = drv)) +
geom_point(alpha = 0.7) +
labs(x = "Engine displacement (L)", y = "Highway MPG") +
theme_guide()
# 适合文档或网页的 PNG:7 × 4.5 英寸,300 dpi
ggsave(
filename = "mpg_scatter.png",
plot = p,
width = 7, height = 4.5, units = "in",
dpi = 300, bg = "white"
)
# 矢量 PDF 适合论文与后期排版
ggsave("mpg_scatter.pdf", p, width = 7, height = 4.5, units = "in")常见建议:
width、height、units 和
bg,避免不同机器输出不一致;| 现象 | 常见原因 | 处理方法 |
|---|---|---|
| 出现一个写着颜色名的奇怪图例 | 常量写进了 aes() |
把 color = "red" 放到 aes()
外 |
| 柱子全是灰色或颜色没变化 | 混淆 color 与 fill |
柱/面积内部通常用 fill |
geom_bar() 报 y 相关错误 |
已汇总数据仍用了计数柱 | 改用 geom_col() |
| 多条线被错误连在一起 | 缺少分组 | 映射 group、color 或
linetype |
| 平滑线或箱线图缩放后改变 | 用 scale_* (limits=...) 删除了数据 |
仅缩放时用 coord_cartesian() |
| 点太黑、看不出密度 | 过度重叠 | 使用较小点、alpha、jitter、分箱或抽样 |
| 直方图结论不稳定 | 依赖默认箱数 | 根据领域意义设置并比较 binwidth |
| 类别顺序不合逻辑 | 字符/因子使用默认顺序 | 显式 factor(levels=...) 或
reorder() |
| 图例重复或无法合并 | 不同图层的映射名/标度不同 | 统一映射变量和 labs(color/fill=...) |
| 警告 “Removed … rows” | 缺失值、越界值或标度限制 | 查 is.na()、数据范围和
limits |
不要为了让警告消失就随手加 na.rm =
TRUE。先确认缺失值为何存在、是否应删除,以及删除是否会改变结论。
| 分析目标 | 首选图层 | 常用补充 |
|---|---|---|
| 两个连续变量的关系 | geom_point() |
alpha、geom_smooth() |
| 连续变量随时间变化 | geom_line() |
geom_point()、参考线 |
| 类别频数 | geom_bar() |
position、coord_flip() |
| 已汇总的类别数值 | geom_col() |
排序、误差线 |
| 单变量分布 | geom_histogram() /
geom_density() |
明确 binwidth / adjust |
| 多组分布比较 | geom_boxplot() /
geom_violin() |
抖动点、样本量 |
| 两个类别的组合数值 | geom_tile() |
连续填充标度、文字标签 |
| 多组重复比较 | facet_wrap() /
facet_grid() |
保持共享标度 |
ggsave()
固定尺寸和格式,并保留生成图表的代码与会话信息。本文档使用现代 ggplot2 语法:线宽使用
linewidth,统计变量使用
after_stat(),编程接口使用 .data[[...]]。
cat("R:", R.version.string, "\n")
#> R: R version 4.6.1 (2026-06-24)
cat("ggplot2:", as.character(packageVersion("ggplot2")), "\n")
#> ggplot2: 4.0.3
cat("knitr:", as.character(packageVersion("knitr")), "\n")
#> knitr: 1.51
cat("rmarkdown:", as.character(packageVersion("rmarkdown")), "\n")
#> rmarkdown: 2.31至此,你已经覆盖了 ggplot2 最常用的图形语法、几何对象、统计变换、位置、分面、标度、坐标、主题、注释、编程与导出方法。实际项目中,优先保证数据含义和比较任务清晰,再追求视觉装饰。