章节概述
本章以 ggplot2
为核心框架,系统梳理数据可视化的基础机制:通过映射与几何对象将变量编码为图形要素,并在此基础上掌握常用标度、图例与主题设置,形成可复用的图形构建流程(grammar of graphics)。
在实践层面,本章围绕 9 类高频统计图形构建可迁移的“图形工具箱”:散点图、折线图、柱状图、直方图、雷达图、箱线图、流向图、山峦图与热力图。各图形均结合典型应用场景,提供可直接复现的绘图语法与简要的读图要点,用于支持对分布、差异、关系与结构路径的分析表达。
在方法与规范层面,本章归纳学术制图的关键要求,包括图形—数据匹配、可比性与尺度一致、单位与标注完整性、以及图例与版式管理等;并在规范基础上讨论配色与视觉层级优化,用于提升图形的可读性与发表适配性。
本章学习内容
欢迎开始 第 3 章 的学习。请点击 下方导航卡 进入相应小节:
可视化导论
研究证据表达
语法框架
ggplot2基本语法
常用图形
分布/关系/趋势
制图规范
颜色/轴/注释
制图优化
配色与美化
章节练习
复现与挑战
💡 提示:学习完一个小节后,请再次点击 屏幕右下角的章节主页按钮回到本导航页
© 华东师范大学 社会发展学院 人口研究所 | DAWN 研究组 | yzliu@soci.ecnu.edu.cn
课程负责人:刘贇喆 本章作者:刘贇喆 | 蒋娴静
最后更新:2026年03月19日 构建环境:R version 4.5.2 (2025-10-31)
基本概念
数据可视化 (Data Visualisation) 是指将抽象的数据按照明确的编码规则转换为可读的图形表达:通过位置、长度、颜色、形状等视觉通道来映射变量取值,从而呈现数据的分布、结构与关系。
在学术与实证研究语境中,可视化不仅是结果呈现的手段,更是认知数据的核心工具。它强调可解释性与证据力:读者应能依据图形元素准确还原变量含义、测量尺度与研究口径,而非仅仅被视觉效果所吸引。
可视化的本质是将数值信息压缩为视觉模式,以提升信息传达。
在量化研究的全流程中,数据可视化通常承担着三个不可替代的核心职能:
1. 探索与诊断 (Exploration & Diagnosis)
在正式建立统计模型前,可视化是检验数据质量与揭示潜在特征的首要步骤。单纯的汇总统计量 (如均值、方差、相关系数) 往往会掩盖数据的真实结构 (如经典的安斯库姆四重奏,Anscombe’s quartet),而图形能够直观且客观地暴露数据的全貌。
“数值计算是精确的,而图形展示是粗略的。”
1973年,统计学家 Frank Anscombe 针对当时学术界这一普遍存在的偏见,构建了四组特殊的数据集进行反驳。他指出:
若脱离了可视化审视,单纯依赖统计指标往往会得出具有误导性的结论。
从基础统计摘要的维度审视,这四组数据几乎不可区分:
然而,当我们引入可视化视角时,数据的真实结构差异便一目了然:
Figure 3.1. 经典:安斯库姆四重奏
Dataset I:符合线性假设的常规分布。
Dataset II:呈现明显的非线性(曲线)关系,线性模型完全失效。
Dataset III:整体呈线性,但受单点离群值影响,回归参数发生了偏移。
Dataset IV:虚假相关,两者本无线性关系,因一个极端值导致相关系数虚高。
这一案例证明了:
统计摘要可能会掩盖数据的本质特征,而可视化则是检验假设、发现异常模式不可或缺的手段。
2. 解释与推断 (Interpretation & Inference)
在统计建模与数据分析阶段,可视化不仅用于验证理论假设,还能将抽象的模型结果转化为直观的逻辑链条,辅助研究者进行科学推断。
效应比较:
直观呈现不同组别 (如控制组与实验组、不同分类维度)
间的统计差异、效应量 (Effect Size)
及其作用方向。
动态与趋势:
揭示时间序列或纵向数据 (Longitudinal Data)
的演变规律,精准定位发展趋势、结构性拐点 (Turning
Points) 与周期性特征。
不确定性量化:
借助误差棒 (Error Bars) 或置信区间带 (Confidence
Bands),图形化呈现参数估计的统计显著性、不确定性及模型预测的稳定性。
Figure 3.2. 数据可视化 - 解释与推断
3. 沟通与传播 (Communication & Dissemination)
在学术出版与会议报告中,高质量的图表是提升信息传递效率的核心媒介。优秀的可视化能够有效降低读者的认知负荷 (Cognitive Load),将高度抽象的统计模型推断转化为直观且具说服力的实证研究证据,进而增强学术论证的清晰度与整体影响力。
Figure 3.3. 数据可视化 - 沟通与传播
重点
学术图表不同于商业报表或艺术设计,其首要目标是遵循“信、达、雅”的表达规范,以确保视觉呈现准确、高效且不具误导性。
1. 诚实原则 (Integrity) —— “信”
图形必须忠实反映数据特征,严禁通过视觉偏差扭曲客观事实。
2. 自明原则 (Self-Explanatory) —— “达”
图表应具备独立表意的能力,确保读者即使脱离正文,也能准确获取核心信息。
3. 极简原则 (Simplicity) —— “雅”
学术图表的美学建立在克制与高效之上,其核心是提升 Edward Tufte 提出的数据-墨水比 (Data-Ink Ratio)。
核心理念:图形中的每一滴墨水都应服务于数据表达。
ggplot2?在 R 语言的生态中,Base R (基础绘图系统)
虽然功能完备,但其采用的是指令式绘图逻辑,即类似“纸笔作画”,不同图形间的语法一致性较弱,且后期修改底层元素较为繁琐。为建立可迁移与可复用的可视化范式,本教程将采用
ggplot2 作为核心制图工具。
ggplot2 的理论内核源自 Leland Wilkinson 的《图形语法》
(The Grammar of
Graphics)。该包打破了传统“画点、画线”的机械操作,将数据可视化抽象为一套可组合的图层构建系统。它要求使用者明确数据
(Data)、美学映射 (Aesthetics)、几何对象 (Geometries) 与统计变换 (Statistics)
之间的逻辑关系,进而逐层构建图表。
在学术制图中使用 ggplot2 的核心优势体现在:
+
符号连接),使得图形的精细微调与迭代优化更加直观。tidyverse
的核心组件之一,ggplot2 与
dplyr、tidyr
等数据处理工具无缝衔接。在实际工作流中,通常仅需执行
library(tidyverse)
即可完成加载,实现从数据清洗到可视化的流畅闭环。Figure 3.4. ggplot2 可视化工具与优势
ggplot2
的强大之处在于其通用性。无论是基础统计图,还是复杂的地理空间图或多维分面图,遵循的都是同一套图层语法。以下是使用该包绘制的一些典型案例:
Figure 3.5. ggplot2 多样化绘图示例
© 华东师范大学 社会发展学院 人口研究所 | DAWN 研究组 | yzliu@soci.ecnu.edu.cn
课程负责人:刘贇喆 本章作者:刘贇喆 | 蒋娴静
最后更新:2026年03月19日 构建环境:R version 4.5.2 (2025-10-31)
工具基础
ggplot2 是 R
语言中最具影响力的可视化工具包,其命名源自统计学家 Leland
Wilkinson 的经典著作 《图形语法》 (The Grammar of Graphics)。与 Excel 或 Base R
这种“预设图表类型” (如:选择‘柱状图’或‘饼图’)
的绘图逻辑不同,ggplot2
采用了一套构建式的逻辑 (见Fig3.6):
它认为任何统计图形都可以拆解为一组独立的组件 (Components):数据、坐标系、几何对象与统计变换。用户通过将这些组件像“图层”一样叠加,即可构建出无限可能的图形组合。
Figure 3.6. ggplot2 基本语法示意图
在开始绘制之前,请确保已正确加载了核心工具包:
理解 ggplot2 的关键在于掌握其“分层”(Layered)语法结构。
一张统计图形,本质上是由若干核心要素通过加号
+逐层叠加而成。
如 Fig. 3.6 所示,一个标准的绘图语句通常包含以下三个必须要素:
要素1 数据层(Data)
“画什么?” 绘图的基础必须是一个数据框(data frame / tibble)。
ggplot2 通常不以“单独向量”为输入,而是要求数据以整洁数据(Tidy
Data)形式组织:每一列是一个变量,每一行是一个观测。
要素2 映射层(Aesthetics Mapping)
“怎么画?” 建立数据变量与视觉属性之间的对应关系。
这种对应关系称为映射(mapping),通常写在 aes() 中:
要素3 几何对象层(Geometric Objects)
“画成什么样?” 决定数据在图中呈现的具体形态。
ggplot2 中,控制“画成什么样”的函数通常以
geom_ 开头,例如:
geom_point():画点(散点图)geom_line():画线(折线图)geom_bar():画条形(柱状图;常用于类别计数)geom_col():画条形(柱状图;y
由数据提供)geom_histogram():画直方图(连续变量分布)geom_density():画密度曲线(连续变量分布)geom_boxplot():画箱线图(组间分布比较)geom_violin():画小提琴图(分布形态更直观)geom_smooth():画趋势线/拟合线(可带置信区间)geom_errorbar():画误差棒(不确定性表达)ggplot2 与
gapminder基础 · 代码实战
为练习 ggplot2 的分层逻辑,本节使用最小可运行代码构建一张基础散点图,暂不涉及主题、配色与图例等美化设置。目标是基于gapminder数据中
2007 年数据绘制 人均 GDP 与
预期寿命 的关系,并用颜色区分大洲
# 0. 准备工作
library(tidyverse)
library(gapminder)
# 筛选 2007 年数据
data_2007 <- gapminder %>%
filter(year == 2007)
# 1. 基础绘图语句
ggplot(
data = data_2007, # 数据源:也可写成 data_2007 %>% ggplot(...)
mapping = aes( # mapping = 可省略:直接写 aes(x = ..., y = ...)
x = gdpPercap, # x 轴:人均 GDP
y = lifeExp, # y 轴:预期寿命
color = continent # 颜色映射:按大洲分组
)
) +
geom_point() # 几何层:用“点”呈现观测值逻辑对应
该示例直观地展示了 2.1 节 的三个核心要素如何在代码中落地:
1. 数据层 (Data):ggplot(data = data_2007, ...)
gdpPercap、lifeExp 与
continent 均直接读取自该数据框。2. 映射层 (Mapping):aes(...)
gdpPercap \(\rightarrow\) x (横坐标)lifeExp \(\rightarrow\) y (纵坐标)continent \(\rightarrow\) color (颜色)3. 几何层 (Geom):geom_point()
重要步骤
默认情况下,ggplot2
在渲染中文标题或标签时可能会出现乱码(显示为方框
□□),这是因为系统默认字体不支持中文字符。
为了确保图表中的中文能正常显示,我们需要引入
showtext 包。它可以帮助 R
更好地调用系统字体或加载网络字体。
请在运行绘图代码前,务必先运行以下配置代码:
进阶 · 代码实战
为进一步理解 ggplot2 的分层语法,本节以
gapminder 数据集为例,复现 Hans Rosling
经典的“财富与健康”气泡图思路。
我们截取 2007 年数据,重点回答三个问题:
人均 GDP(经济发展) 与
预期寿命(健康水平)
之间呈现何种关系?不同大洲国家在该关系中的分布差异如何?人口规模如何通过气泡大小被编码并辅助解读?
请在 RStudio 中运行下方代码,并对照后续小节逐行理解:每一句分别对应哪一层(数据/映射/几何/标度/主题),以及它如何影响最终图形的呈现。
# ==============================================================================
# 0. 环境准备与字体配置
# ==============================================================================
library(tidyverse) # 核心数据科学包 (包含 ggplot2, dplyr 等)
library(gapminder) # 演示数据
library(showtext) # 字体渲染包 (解决中文乱码的关键)
# [字体配置] 注册谷歌在线字体
# 说明:自动下载 "Noto Sans SC" (思源黑体) 并注册名为 "my_font"
# 优势:该方案在 Windows/Mac/Linux 上通用,无需依赖用户本地字库
font_add_google(name = "Noto Sans SC", family = "my_font")
showtext_auto() # 启用自动字体渲染
options(scipen = 9999) # 调整科学计数法策略,使大数值显示为普通数字
# ==============================================================================
# 1. 数据准备
# ==============================================================================
# 提取 2007 年的横截面数据
data_2007 <- gapminder %>% filter(year == 2007)
# ==============================================================================
# 2. 绘图构建
# ==============================================================================
ggplot(data = data_2007,
mapping = aes(x = gdpPercap, y = lifeExp)) +
# --- 图层 A: 统计趋势线 (LOESS 回归) ---
# 说明:我们在此处手动映射 linetype 和 fill,是为了强制生成一个图例项
geom_smooth(mapping = aes(linetype = "LOESS回归线 & 95%置信区间",
fill = "LOESS回归曲线 & 95%置信区间"),
method = "loess", # 使用局部加权回归算法
se = TRUE, # 显示置信区间阴影
color = "darkred", # 设置线条的物理颜色 (不参与映射)
linewidth = 0.8) + # 设置线条粗细
# --- 图层 B: 散点气泡图 ---
geom_point(mapping = aes(size = pop/10000, # 大小映射:人口 (单位:万人)
color = continent), # 颜色映射:大洲
alpha = 0.7, # 设置点的透明度
stroke = 0.5) + # 设置点的描边粗细
# --- 标度设置 (Scales) ---
scale_x_log10() + # x轴对其进行对数变换,拉开数据间距
scale_color_brewer(palette = "Set1") + # 使用 Set1 预设色盘
# 控制气泡面积的视觉范围
scale_size(range = c(2, 12),
name = "人口规模 (万人)") +
# [图例自定义] 定义回归线图例的具体样式
# values = "dashed" 确保了【图表画布中】的回归线保持为虚线
scale_linetype_manual(name = "拟合模型", values = "dashed") +
scale_fill_manual(name = "拟合模型", values = "lightblue") +
# --- 标签设置 (Labels) ---
labs(
title = "2007年全球经济与健康状况",
subtitle = "数据来源: Gapminder Project",
x = "人均 GDP (美元, 对数尺度)",
y = "预期寿命 (岁)",
color = "大洲"
) +
# --- 图例控制 (Guides) ---
# 作用:对自动生成的图例进行精细化修饰
guides(
# 1. 修改 [颜色/大洲] 图例:在图例中强制放大圆点,便于识别
color = guide_legend(override.aes = list(size = 5), order = 1),
# 2. 设定 [大小/人口] 图例的排列顺序
size = guide_legend(order = 2),
# 3. 修改 [回归线] 图例:强制覆写图例样式
# 说明:虽然图中是虚线,但为了图例清晰,强制将其显示为“实线+深红+蓝底”
linetype = guide_legend(
order = 3,
override.aes = list(
linetype = "solid", # 图例显示为实线
color = "darkred", # 图例线色为深红
fill = "lightblue", # 图例填充为浅蓝
linewidth = 1 # 图例线加粗
)
),
# 4. 隐藏 [填充] 图例 (已合并至 linetype,无需重复显示)
fill = "none"
) +
# --- 主题设置 (Theme) ---
theme_minimal() +
theme(
# 字体设置:调用开头注册的 "my_font" (思源黑体)
plot.title = element_text(family = "my_font", face = "bold", size = 14, hjust = 0),
plot.subtitle = element_text(family = "my_font", size = 9, color = "grey50", face = "italic", margin = margin(b = 15)),
# 坐标轴文字
axis.title = element_text(family = "my_font", face = "bold", size = 10),
axis.text = element_text(size = 10),
# 图例布局
legend.position = "right",
legend.text = element_text(size = 9),
legend.title = element_text(size = 10, face = "bold"),
legend.spacing.y = unit(0.2, "cm"),
legend.margin = margin(t = 0, b = 0, l = 0, r = 0)
)深度解析
上一节案例虽然代码较长,但核心仍是 ggplot2
的分层语法:先确定数据与全局映射,再按功能叠加图层,最后通过标度与主题完成出版级表达。本节按该案例的实际写法,拆解为四个关键维度。
ggplot(data = data_2007, mapping = aes(x = gdpPercap, y = lifeExp))
在最顶层的 ggplot() 中,我们只定义 x/y
的全局映射,为后续图层提供共同的坐标框架。
geom_smooth() 与
geom_point() 默认继承 x 与
y,无需重复声明。filter(year == 2007)
构造截面数据,避免把不同年份混在同一张图里造成解释偏差。
ggplot2按代码顺序绘制:先写在下,后写在上
底层(Layer
1):geom_smooth(...)
先画 LOESS 趋势线及其 95%
置信区间,使散点气泡覆盖在线条之上,避免线条穿插点形影响读图。
顶层(Layer
2):geom_point(...)
点层单独承担“主要信息编码”:
color = continent 用于大洲分组;size = pop/10000
用于人口规模(单位换算为“万人”)。darkred,与点层的颜色映射相互隔离。该案例的图例设计有两个目标:该出现的必须出现,出现后的必须可读。
A. 图例项的“人为生成”
在
geom_smooth() 中使用:
aes(linetype = "LOESS回归线 & 95%置信区间", fill = "LOESS回归曲线 & 95%置信区间")
aes()
中写入字符串,相当于构造一个“伪分类变量”,从而强制 ggplot2
生成对应图例项。scale_linetype_manual(name = "拟合模型", values = "dashed")scale_fill_manual(name = "拟合模型", values = "lightblue")B. 覆写图例样式(guides &
override.aes)
默认图例会复刻图中元素,但在信息密集图里往往不够清晰,因此使用
override.aes 做“图例可读性优化”:
override.aes = list(size = 5)
强制放大图例圆点,提升颜色辨识度。override.aes = list(linetype = "solid", color = "darkred", fill = "lightblue", linewidth = 1)fill = "none"
隐藏填充图例,避免与 linetype 图例重复呈现。细节
scale_x_log10()
用于处理 GDP
的右偏分布,减少尺度压缩,使低—中收入区间的点位差异更可见。scale_color_brewer(palette = "Set1")
提供一致的离散配色方案,便于跨图复现。scale_size(range = c(2, 12), name = "人口规模 (万人)")showtext + font_add_google() 注册并调用
family = "my_font",保证标题、轴标签与图例文本在不同系统下保持一致渲染;其余字号、加粗、边距等由
theme() 集中管理。ggplot() 里定义
x/y,再在各 geom_
层补充该层独有的映射与设置。aes();固定样式写在 aes() 外(如回归线
color = "darkred")。scale 与
guides 驱动;想控制图例,优先改
scale_*(),再用 override.aes 提升可读性。© 华东师范大学 社会发展学院 人口研究所 | DAWN 研究组 | yzliu@soci.ecnu.edu.cn
课程负责人:刘贇喆 本章作者:刘贇喆 | 蒋娴静
最后更新:2026年03月19日 构建环境:R version 4.5.2 (2025-10-31)
ggplot2 可视化图谱
“一图胜千言”。本章精选了学术与商业分析中最高频使用的
9 种统计图形。
点击下方卡片,快速跳转至对应图形的绘制语法、应用场景与美化技巧。
3.1 散点图
geom_point | 相关性
3.2 折线图
geom_line | 时间序列
3.3 柱状图
geom_col / bar | 类别比较
3.4 直方图
geom_histogram | 频数分布
3.5 雷达图
ggradar插件 | 多维比较
3.6 箱线图
geom_boxplot | 离群值
3.7 流向图
ggalluvial | 流向与迁移
3.8 山峦图
ggridges | 脊线与层叠
3.9 热力图
geom_tile | 矩阵可视化
💡 学习建议:所有的几何对象 (geom_*)
都遵循相同的语法逻辑。
掌握前三种基础图形后,其余图形只需更换函数名即可融会贯通。
© 华东师范大学 社会发展学院 人口研究所 | DAWN 研究组 | yzliu@soci.ecnu.edu.cn
课程负责人:刘贇喆 本章作者:刘贇喆 | 蒋娴静
最后更新:2026年03月19日 构建环境:R version 4.5.2 (2025-10-31)
基础 · 图形概念
散点图(Scatter Plot)用于探索两个连续变量之间的关系。它将每条观测映射到二维坐标系中,通过点的空间分布呈现变量间的共变模式、分层结构与异常观测。
1. 核心用途
相关性探索:
观察两变量是否呈现正相关、负相关或弱相关关系。
(例如:人均 GDP
上升时,预期寿命是否同步上升?)
结构识别:
判断数据是否存在自然分组或聚集现象。
(Clusters)
离群值发现:
识别远离主要分布区域的异常观测。
(Outliers)
2. 核心函数
在 ggplot2 中,散点图对应的几何对象函数为:
geom_point()
基础 · 实战代码
本节继续使用 gapminder 数据集,并选取 2007
年 的截面数据,绘制 人均 GDP 与
预期寿命 的散点图。每个点代表一个国家或地区,用于直观观察两变量之间的分布关系。
代码逻辑:
Step 1 加载数据与绘图环境
导入 tidyverse 与
gapminder,用于数据筛选与 ggplot2 绘图。
Step 2 构建横截面样本(2007
年)
使用 filter(year == 2007)
提取单一年份数据,避免不同时期混合导致关系被“时间趋势”干扰。
(横截面图更适合做变量关系的直观初判)
Step 3
建立映射关系(x–y–分组颜色)
将
gdpPercap 映射到 x、lifeExp
映射到 y,并将 continent 映射到
color:
- x 与 y
给出两个连续变量的二维位置关系;
- color
用于区分大洲,便于比较不同区域的分布层次与聚集模式。
Step 4
绘制散点并补全图形信息
使用
geom_point() 绘制国家点位;再通过 labs()
明确标题、坐标轴含义与图例名称,最后用 theme_minimal()
降低背景干扰,突出数据结构。
library(tidyverse)
library(gapminder)
library(showtext)
showtext.auto()
# 1. 数据预处理:提取 2007 年度全球跨国横截面观测样本 (Cross-sectional Data)
data_scatter <- gapminder %>%
filter(year == 2007)
# 2. 视觉映射与统计逻辑:基于经济水平与健康产出维度的关联性分析
ggplot(
data = data_scatter,
mapping = aes(
x = gdpPercap, # 解释变量 (Independent Variable): 人均 GDP
y = lifeExp, # 响应变量 (Dependent Variable): 预期寿命
color = continent # 类别变量映射:大洲分类属性
)
) +
# 3. 几何图层:利用散点特征揭示双变量间的相关性趋势及样本离散分布状态
geom_point() +
# 4. 语义标注与视觉排版净化
labs(
title = "2007 年全球人均 GDP 与预期寿命分布特征分析",
x = "人均 GDP (单位: 美元)",
y = "预期寿命 (单位: 岁)",
color = "大洲分类"
) +
theme_minimal() # 采用极简设计原则,通过提升“数据墨水比”聚焦核心统计关联读图方法
从图中可直接读出的主要信息
进阶 · 实战代码
本节在同一张散点图中同时呈现变量关系、拟合结果与跨期对比:以
log10(gdpPercap) 为横轴、lifeExp
为纵轴,叠加线性回归拟合与置信区间,并在每个年份分面内标注回归方程与
Pearson 相关系数。
代码逻辑:
Step 1 数据筛选与变量变换
从 gapminder 中筛选 1952 / 1977 / 2007
三个年份,并构造三类派生变量:
- year 转为因子(用于分面)
- log_gdp = log10(gdpPercap)(压缩经济量级跨度,提升可读性与拟合稳定性)
- pop_wan = pop / 10000(人口单位换算为“万人”,用于点大小映射)
Step 2 分年统计:回归系数 +
相关系数 + 注释位置
按 year
分组计算每个分面的统计量:
- 线性模型 lifeExp ~ log_gdp 的系数
b0, b1(用于可读化方程文本)
- Pearson 相关系数 r 与样本量 n(用于描述线性相关强度与信息量)
同时基于该分面的数据范围生成
x_pos, y_pos,使标注在不同分面中都能稳定落在画布内、避免越界。
Step 3 构建图层:拟合带 + 气泡散点
+ 分面标注
在 ggplot(log_gdp, lifeExp)
的基础上叠加三类核心图层:
- geom_smooth(method = "lm", se = TRUE):绘制回归线与 95%
置信区间;并通过映射 linetype/fill
人为生成“拟合模型”图例项(便于读图说明)
-
geom_point(aes(color = continent, size = pop_wan)):绘制国家散点,颜色区分大洲、点大小编码人口规模
-
facet_wrap(~ year, ncol = 1):按年份纵向排列,保证跨期对照结构清晰
- geom_text(data = stats_year, ...):将“方程 + r +
n”写入每个分面,形成可直接阅读的统计摘要
Step 4
标度与图例组织(保证信息不重复)
分别设置三类标度:
- scale_color_brewer():大洲配色
- scale_size():人口大小范围与图例标题
- scale_linetype_manual() 与
scale_fill_manual():统一“拟合模型”图例命名,并在
guides() 中将 fill 合并到
linetype 图例,避免重复出现两个模型图例。
Step 5
版式与字体:统一学术排版风格
使用
theme_minimal() 并在 theme() 中统一设置
my_font,同时规范标题层级、图例文字与间距,以保证跨图一致性与出版友好。
# =============================================================
# 综合可视化:全球经济水平与预期寿命的跨时域关联分析
# 技术要点:对数变换、OLS 回归、组内统计量推导、分面布局优化
# =============================================================
library(tidyverse)
library(gapminder)
library(showtext)
# 1. 绘图环境配置与字体初始化
font_add_google(name = "Noto Sans SC", family = "my_font")
showtext_auto()
options(scipen = 9999) # 禁用科学计数法,确保坐标轴标注的规范性
# 2. 数据处理与特征工程:样本筛选、对数变换与量级归一化
data_adv <- gapminder %>%
filter(year %in% c(1952, 1977, 2007)) %>%
mutate(
year = factor(year), # 离散化年份变量,作为分面依据
log_gdp = log10(gdpPercap), # 对数变换 (base 10):压缩长尾分布,构建线性关联基础
pop_wan = pop / 10000 # 归一化处理:将人口单位转化为“万人”,优化气泡大小映射
)
# 3. 组内统计推断:计算相关系数、回归系数与动态标注坐标
# 目标:为每个分面生成独立的描述性统计量与方程文本
stats_year <- data_adv %>%
group_by(year) %>%
summarise(
n = n(),
r = cor(log_gdp, lifeExp, method = "pearson", use = "complete.obs"), # Pearson $r$
b0 = coef(lm(lifeExp ~ log_gdp))[1], # 截距 $\beta_0$
b1 = coef(lm(lifeExp ~ log_gdp))[2], # 斜率 $\beta_1$
# 启发式坐标计算:根据各分面数据极值动态生成标注位置,防止文字重叠与越界
x_pos = min(log_gdp, na.rm = TRUE) + 0.03 * diff(range(log_gdp, na.rm = TRUE)),
y_pos = max(lifeExp, na.rm = TRUE) - 0.05 * diff(range(lifeExp, na.rm = TRUE)),
.groups = "drop"
) %>%
mutate(
label = paste0(
"拟合模型: $y$ = ", round(b0, 2), " + ", round(b1, 2), "$x$\n",
"Pearson $r$ = ", round(r, 2), " (n = ", n, ")"
)
)
# 4. 可视化构建:层级化图形呈现
ggplot(data_adv, aes(x = log_gdp, y = lifeExp)) +
# 图层 A:统计推断层 (OLS Regression)
# 通过人为映射 linetype 与 fill 构建统一的模型统计量图例
geom_smooth(
aes(
linetype = "线性回归线(lm)",
fill = "95% 置信区间"
),
method = "lm",
se = TRUE,
color = "darkred",
linewidth = 0.8
) +
# 图层 B:原始观测值层 (Bubble Plot)
# 实施多维度美学映射:颜色 (大洲类别) 与 面积 (人口规模)
geom_point(
aes(color = continent, size = pop_wan),
alpha = 0.35,
stroke = 0.25
) +
# 图层 C:空间分解层 (Faceting)
# 沿时间维度进行纵向切片对比
facet_wrap(~ year, ncol = 1) +
# 图层 D:信息增强层 (Statistical Annotation)
# 注入预计算的回归方程与相关性指标
geom_text(
data = stats_year,
aes(x = x_pos, y = y_pos, label = label),
inherit.aes = FALSE,
hjust = 0, vjust = 1,
lineheight = 1.1,
size = 3.4,
family = "my_font"
) +
# 标度层 (Scales):色彩、大小与线性样式的精细化控制
scale_color_brewer(palette = "Set1", name = "大洲分类") +
scale_size(
range = c(1.5, 10),
name = "人口规模(万人)"
) +
scale_linetype_manual(
name = "模型声明",
values = c("线性回归线(lm)" = "dashed")
) +
scale_fill_manual(
name = "模型声明",
values = c("95% 置信区间" = "lightblue")
) +
# 标注层 (Labels):定义学术标题与物理单位说明
labs(
title = "人均 GDP 与预期寿命的关联性分析:跨年份演进对比",
subtitle = "数据呈现:散点大小代表人口规模;红色虚线为 OLS 回归线;蓝色阴影为 95% 置信区间",
x = "$log_{10}$(人均 GDP)",
y = "预期寿命(岁)"
) +
# 视觉引导层 (Guides):图例优先级配置与视觉属性覆盖 (Override)
guides(
color = guide_legend(override.aes = list(size = 5), order = 1),
size = guide_legend(order = 2),
linetype = guide_legend(
order = 3,
override.aes = list(
linetype = "solid",
color = "darkred",
fill = "lightblue",
linewidth = 1
)
),
fill = "none" # 合并至 linetype 图例中
) +
# 主题层 (Theme):极简风格与出版级排版优化
theme_minimal() +
theme(
text = element_text(family = "my_font"),
plot.title = element_text(face = "bold", size = 14, hjust = 0),
plot.subtitle = element_text(size = 9, color = "grey50", face = "italic", margin = margin(b = 12)),
axis.title = element_text(face = "bold", size = 10),
legend.title = element_text(face = "bold", size = 10),
legend.position = "right",
panel.grid.minor = element_blank()
)© 华东师范大学 社会发展学院 人口研究所 | DAWN 研究组 | yzliu@soci.ecnu.edu.cn
课程负责人:刘贇喆 本章作者:刘贇喆 | 蒋娴静
最后更新:2026年03月19日 构建环境:R version 4.5.2 (2025-10-31)
基础 · 图形概念
折线图(Line Chart)用于呈现指标随时间或有序序列变化的趋势。它通过连接相邻观测点,强调变化的方向与节奏,适合表达长期上升/下降、阶段性波动与结构性拐点等动态特征。
1. 核心用途
趋势刻画:
识别指标随时间演化的总体方向与增长/下降速率。
(如:过去数十年预期寿命的变化轨迹)
多组对比:
在同一时间轴上比较不同对象的变化幅度、速度与阶段差异。
(如:不同国家的增长路径是否同步)
拐点识别:
定位趋势发生明显转折或出现异常波动的时间点/阶段。
(Turning points / shocks)
2. 核心函数
在 ggplot2 中,折线图对应的几何对象函数为:
geom_line()
关键逻辑:当图中包含多条线时,需要明确“哪些点属于同一条线”。最常见做法是将分组变量映射到
colour(同时完成分组);若不希望上色,也可显式设置
aes(group = ...)。
基础 · 代码实战
本节使用 gapminder 数据,选取 China、United
States、Japan、India 四个国家,绘制 1952—2007
年预期寿命的变化轨迹,用于对比不同国家的长期趋势与阶段性差异。
代码逻辑:
Step 1 加载数据与绘图环境
导入 tidyverse 与
gapminder,确保数据处理与 ggplot2
绘图流程可用。
Step 2
筛选研究对象(四国样本)
使用
filter(country %in% ...)
仅保留四个国家的全时期记录,保证折线反映同一指标(lifeExp)的可比时间序列。
Step 3 建立映射关系(时间 × 指标 ×
分组)
将 year 映射到
x、lifeExp 映射到 y,并将
country 映射到 color。
关键点:
color = country同时实现“区分颜色”和“自动分组连线”,避免不同国家的数据被错误连接。
Step 4
叠加几何对象并强化可读性
用
geom_line() 表达趋势,用 geom_point()
标记每个观测年份的离散点,便于识别阶段性变化与局部波动;最后通过
labs() 与 theme_minimal()
完成标题、坐标轴与基础主题设置。
library(tidyverse)
library(gapminder)
# 1. 数据预处理:构建跨时域多国对比样本
# 从 Gapminder 数据库中提取特定国家的时间序列观测值,用于呈现社会发展指标的动态演化
data_line <- gapminder %>%
filter(country %in% c("China", "United States", "Japan", "India"))
# 2. 视觉映射与美学逻辑构建
ggplot(
data = data_line,
mapping = aes(
x = year,
y = lifeExp,
color = country # 将类别变量映射至色彩通道,实现多组样本的自动分组与区分
)
) +
# 3. 几何图层:趋势轨迹与观测落点的协同表述
# geom_line 渲染演化趋势;geom_point 用于强调各年度的具体观测精度,提升数据信度
geom_line(linewidth = 1) +
geom_point(size = 1.8, alpha = 0.8) +
# 4. 语义标注与视觉排版净化
labs(
title = "1952–2007:特定国家预期寿命的演进特征",
subtitle = "趋势线揭示长周期演变路径,散点对应年度统计观测值",
x = "年份 (Year)",
y = "预期寿命 (Life Expectancy, 岁)",
color = "国家/地区"
) +
theme_minimal() + # 采用极简主义主题,通过提升“数据墨水比”聚焦核心统计信息
theme(
legend.position = "bottom", # 图例下置以优化绘图区域的纵向视觉纵深
plot.title = element_text(face = "bold", size = 14)
)怎么读
据此读出的主要发现
重要
折线图的核心语义是连续变化:连线默认表示相邻点之间存在可解释的过渡关系(斜率具有实际含义)。当 x 轴不具备连续性或不代表同一对象的连续过程时,连线容易制造“伪趋势”,应避免使用。
1. 无序类别变量(Nominal Data)
2. 离散区间 / 队列(Discrete Cohorts)
[10–19], [20–29],
[30–39])。使用原则:
只有当你能合理说明相邻 x
值的斜率代表变化速率时,才使用折线图;否则优先采用离散比较图形(柱状图/点图/箱线图等)。
进阶 · 代码实战
基础折线图强调“水平随时间变化”。但在跨地区比较中,更关键的问题是:不同地区是否同步改善、改善速度是否一致、以及在某些时期是否出现阶段性加速/放缓。因此,本节将折线图升级为“同图对照”:在同一坐标框架下同时呈现 各大洲的长期趋势与世界平均水平,并加入洲内分布带以提示洲内差异范围。
代码逻辑:
Step 1
构建“世界基准线”(World
reference/baseline)
按 year
分组计算世界人口加权平均预期寿命
world_lifeExp_w,并将其重命名为
lifeExp_w。
(人口加权使大人口国家对总体水平的贡献更合理,避免“小国均值”放大波动)
Step 2
构建“洲—年趋势”与“洲内差异带”(Continent trend + IQR band)
按
continent × year 分组计算:
-
lifeExp_w = Σ(lifeExp × pop) / Σ(pop)(洲级人口加权均值)
- q25 与 q75(洲内 25%/75%
分位数,用于 IQR 带)
随后用
geom_ribbon(ymin = q25, ymax = q75)
绘制阴影带,作为“洲内差异范围”的背景提示,并通过
show.legend = FALSE 避免图例冗余。
Step 3 合并绘图序列并区分 World /
Continent(用于线型与线宽)
将
cont_year 与 world_year 合并为
line_df,并创建 group_type:
- Continent:各大洲趋势(实线、较细)
- World:世界参照线(虚线、较粗、darkred)
从而在同一图层中用 linetype/linewidth/size
实现“视觉强调”但不增加额外图例项。
Step 4
统一配色并加入图内说明
使用
RColorBrewer::Set2 为五大洲提供一致色值,并额外指定
World = darkred。同时让分布带 fill 与折线
colour 共享同一套映射,确保“带—线”一一对应。
最后用 annotate("label", x = Inf, y = -Inf, ...)
在图内空白处标注:阴影带表示洲内 25%–75% 区间(IQR)。
# ==============================================================================
# 进阶时序分析:大洲趋势与全球基准对照 (加权均值与离散分布带)
# 核心技术:人口加权均值、分位数估计、复合图层叠加 (Ribbon + Line)
# ==============================================================================
library(tidyverse)
library(gapminder)
library(RColorBrewer)
library(showtext)
# ------------------------------------------------------------------------------
# 绘图环境配置:学术级字体渲染与数值显示优化
# ------------------------------------------------------------------------------
font_add_google(name = "Noto Sans SC", family = "my_font")
showtext_auto()
options(scipen = 9999) # 禁用科学计数法以增强轴标签的可读性
# ------------------------------------------------------------------------------
# 0) 统计量推导:洲级人口加权均值与四分位区间 (IQR) 估计
# ------------------------------------------------------------------------------
cont_year <- gapminder %>%
group_by(continent, year) %>%
summarise(
# 计算人口加权均值:确保趋势反映的是“人的平均水平”而非“国家的平均水平”
lifeExp_w = sum(lifeExp * pop, na.rm = TRUE) / sum(pop, na.rm = TRUE),
# 提取洲内 25% 与 75% 分位数,用于呈现组内离散程度
q25 = quantile(lifeExp, 0.25, na.rm = TRUE),
q75 = quantile(lifeExp, 0.75, na.rm = TRUE),
.groups = "drop"
)
# ------------------------------------------------------------------------------
# 1) 参考基准计算:全球人口加权均值 (World Reference)
# ------------------------------------------------------------------------------
world_year <- gapminder %>%
group_by(year) %>%
summarise(
lifeExp_w = sum(lifeExp * pop, na.rm = TRUE) / sum(pop, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(continent = "World")
# ------------------------------------------------------------------------------
# 2) 结构化合并:构建包含参考组 (World) 的统一数据集
# ------------------------------------------------------------------------------
line_df <- bind_rows(
cont_year %>% select(continent, year, lifeExp_w),
world_year %>% select(continent, year, lifeExp_w)
) %>%
mutate(
# 定义分组属性:区分全球基准与大洲观察组,为后续美学映射奠定逻辑基础
group_type = if_else(continent == "World", "World", "Continent"),
continent = factor(continent, levels = c(levels(gapminder$continent), "World"))
)
# ------------------------------------------------------------------------------
# 3. 美学映射设计:高对比度学术色盘与元数据定义
# ------------------------------------------------------------------------------
set2_cols <- brewer.pal(5, "Set2")
cols_line <- c(
"Africa" = set2_cols[1], "Americas" = set2_cols[2], "Asia" = set2_cols[3],
"Europe" = set2_cols[4], "Oceania" = set2_cols[5], "World" = "darkred"
)
iqr_note <- "Shaded band = within-continent 25%–75% range (IQR)"
# ------------------------------------------------------------------------------
# 4. 可视化构建:分层图形呈现
# ------------------------------------------------------------------------------
ggplot() +
# A. 离散度分布层:通过 geom_ribbon 呈现 IQR 阴影带,表征大洲内部的异质性
geom_ribbon(
data = cont_year,
aes(x = year, ymin = q25, ymax = q75, fill = continent),
alpha = 0.12, colour = NA, show.legend = FALSE
) +
# B. 趋势轨迹层:利用 linetype 与 linewidth 构建视觉等级 (Hierarchy)
# 全球基准 (World) 采用深红色虚线与加粗处理以实现视觉隔离
geom_line(
data = line_df,
aes(x = year, y = lifeExp_w, colour = continent,
linetype = group_type, linewidth = group_type)
) +
# C. 观测落点层:强调年度统计节点
geom_point(
data = line_df,
aes(x = year, y = lifeExp_w, colour = continent, size = group_type),
alpha = 0.90
) +
# D. 元数据注释层:利用 Inf 坐标实现标注内容的边界对齐
annotate(
"label",
x = Inf, y = -Inf, label = iqr_note,
hjust = 1.02, vjust = -0.25, size = 3.1,
label.padding = unit(0.18, "lines"), linewidth = 0.25,
colour = "grey20", fill = "white", alpha = 0.92, family = "my_font"
) +
# ----------------------------------------------------------------------------
# E. 标度控制与视觉排版净化
# ----------------------------------------------------------------------------
scale_colour_manual(values = cols_line, name = "Group") +
scale_fill_manual(values = cols_line) +
# 隐藏冗余的线型/大小图例,聚焦于色彩驱动的分组信息
scale_linetype_manual(values = c("Continent" = "solid", "World" = "dashed"), guide = "none") +
scale_linewidth_manual(values = c("Continent" = 0.5, "World" = 1.2), guide = "none") +
scale_size_manual(values = c("Continent" = 1.7, "World" = 2.0), guide = "none") +
labs(
title = "Life expectancy trends by continent (1952–2007)",
subtitle = "Solid lines: population-weighted means; dashed darkred: world average reference",
x = "Year", y = "Life expectancy (years)"
) +
theme_minimal() +
theme(
text = element_text(family = "my_font"),
plot.title = element_text(face = "bold", size = 14, hjust = 0),
plot.subtitle = element_text(size = 9, color = "grey50", face = "italic", margin = margin(b = 12)),
axis.title = element_text(face = "bold", size = 10),
legend.title = element_text(face = "bold", size = 10),
panel.grid.minor = element_blank(),
plot.margin = margin(10, 18, 10, 10)
)© 华东师范大学 社会发展学院 人口研究所 | DAWN 研究组 | yzliu@soci.ecnu.edu.cn
课程负责人:刘贇喆 本章作者:刘贇喆 | 蒋娴静
最后更新:2026年03月19日 构建环境:R version 4.5.2 (2025-10-31)
基础 · 图形概念
柱状图(Bar Chart) 是用于比较分类变量在某一数值指标上的差异的标准图形。它以柱子的长度作为主要编码通道,将数值大小映射为可直接比较的视觉差异,适用于跨类别对比、排序展示与结构呈现。
1. 核心用途
2. 核心 ggplot geom
核心 ggplot geom:
geom_col()/geom_bar()
3. 函数辨析
在 ggplot2 中,柱状图常用两类几何对象,选择取决于数值是否已在数据表中:
geom_col():推荐
当数据表中已经存在要展示的数值列(如
GDP、人口、寿命)时使用:
柱高 = y 变量的实际数值
geom_bar():
当数据为原始明细记录,需要对类别进行计数(频数)时使用:
柱高 = 该类别出现次数
基础 · 代码实战
本节使用 gapminder
数据构建一个最小可复现的柱状图案例:聚焦 2007
年美洲(Americas)国家,筛选人均 GDP(gdpPercap)最高的 Top
10,并用水平柱状图呈现其排名差异。
year == 2007 且
continent == "Americas",并按 gdpPercap 取 Top
10(不含并列)。关键技巧:类别变量若不重排,通常会按名称字典序显示。为得到“排名视图”,需要按
gdpPercap 对 country 进行重排(reorder() 或
forcats::fct_reorder())。
代码逻辑:
Step 1 筛选范围(年份 × 大洲)
限定 year == 2007
且 continent == "Americas",得到目标比较集合。
Step 2 提取 Top 10(按人均 GDP 排名)
使用
slice_max(order_by = gdpPercap, n = 10, with_ties = FALSE)
获取人均 GDP 最高的 10 个国家,保证排名集合唯一且可复现。
Step 3 重排类别(避免字典序)
在映射中用
reorder(country, gdpPercap)
将国家按数值从低到高排列;配合水平柱状图即可呈现“从高到低”的视觉排序(y 轴自下而上)。
Step 4
绘制柱状图并精简背景
用 geom_col()
直接绘制已汇总好的数值型柱形;再通过 theme() 去除 y
方向主网格线以提升读数清晰度。
library(tidyverse)
library(gapminder)
# 1. 数据预处理:提取 2007 年度美洲截面样本,并筛选人均 GDP 前 10 位国家
data_bar <- gapminder %>%
filter(year == 2007, continent == "Americas") %>%
# 基于统计指标执行降序筛选,确保样本的代表性与对比价值
slice_max(order_by = gdpPercap, n = 10, with_ties = FALSE)
# 2. 绘图初始化与视觉映射逻辑构建
ggplot(
data = data_bar,
mapping = aes(
# reorder(类别, 排序依据):实施数值驱动的重排序逻辑,优化名义变量的展示可读性
x = gdpPercap,
y = reorder(country, gdpPercap)
)
) +
# 3. 几何层:构建统计柱状图
# 调用 geom_col() 以直接映射观测值(Identity mapping),而非执行频数统计
geom_col(
fill = "#4682B4",
width = 0.8
) +
# 4. 标注体系与美学净化
labs(
title = "2007 年美洲人均 GDP 领先国家分布",
subtitle = "按人均 GDP (USD) 降序排列的 Top 10 观测样本",
x = "人均 GDP (美元)",
y = NULL
) +
theme_minimal() +
# 细节优化:移除冗余的纵向分类网格线,通过提升“数据墨水比”引导读数视觉重心
theme(
panel.grid.major.y = element_blank()
)读图方法
从图中可直接读出的主要信息
进阶 · 图形概念
1. 图形定义
标准柱状图强调“类别—数值”的单一对比。堆叠柱状图(Stacked Bar Chart)在同一根柱子内叠加多个子类别,使图形在一个坐标系中同时表达总量与结构两类信息:
使用要点:堆叠柱状图适合“看总量 + 看组成”;但若目标是精确比较不同主类别间的子类别大小,需要谨慎解读(堆叠基线不同会影响比较)。
2. 场景与目标
本案例希望在一张图中同时回答两类问题:
- 各大洲的经济体量谁更大(总量比较)?
- 每个大洲的经济体量主要由哪些国家贡献(结构识别)?
3. 数据策略
以 2007 年为截面,先计算国家 GDP 总量
GDP = pop × gdpPercap(人口 × 人均
GDP)。
若将每个大洲所有国家都堆叠到一根柱内,色块与图例会急剧膨胀、可读性下降。因此采用“Top2
+ Others”的结构化汇总:
- 每洲仅保留 GDP 贡献最高的 Top1 / Top2
作为独立色块;
- 其余国家合并为该洲的 Others(Other__
- 并用“同洲同色系”的明暗变化表达
Top1/Top2/Others,使读者能快速识别洲内结构而不跳色。
进阶 · 代码实战
本节代码围绕“洲总量 + 洲内结构(Top2 + Others)”构建堆叠柱状图,并在图例中附带各色块的洲内占比,以提升读图效率。
代码逻辑:
Step 1
参数与配色锚点(continent_order +
base_cols)
设定分析年份 year_sel = 2007
与大洲显示顺序 continent_order;随后用
ggsci::pal_d3("category10")
为每个大洲分配一个基色 base_cols(作为同洲配色的“锚点”)。
同时定义颜色插值函数 mix_col() 与
shade_triplet(),将每个大洲的基色扩展为三档明暗:Top1 /
Top2 / Others(同色系一致、结构层级清晰)。
Step 2 国家层数据构造:计算
country_gdp
筛选 year == 2007,并计算
country_gdp = pop * gdpPercap(国家 GDP
总量代理);同时将 continent 设为按
continent_order
排列的因子,以固定柱子顺序并保证跨图一致性。
Step 3 洲内 Top2 识别:生成
top2_map(含 rank)
按
continent 分组,在组内按 country_gdp
降序排列并截取前 2 个国家,形成 top2_map:
- rank_in_continent = 1 → Top1
- rank_in_continent = 2 → Top2
随后拆分得到 top1 与 top2
两个清单,用于后续堆叠顺序与颜色映射。
Step 4 结构化汇总:Top1 /
Top2 / Other__洲 + 洲内占比 pct
将 top2_map
回连到国家数据:
- Top1/Top2 保留国家名作为 segment;
- 其余国家合并为 segment = "Other__<Continent>"(保证每个大洲的 Others 是独立段)。
再按 continent × segment 汇总得到
gdp_sum,并在洲内计算
pct = gdp_sum / sum(gdp_sum)(用于图例显示“洲内占比”)。
Step 5 堆叠顺序控制:Others → Top2
→ Top1(从下到上)
构造
stack_levels:先放所有
Other__<Continent>,再放各洲 Top2 国家,最后放 Top1
国家;并将 segment 设置为该顺序的因子。
同时在 geom_col() 中使用
position_stack(reverse = TRUE),使堆叠方向与因子顺序共同作用,稳定实现“底部
Others、顶部 Top1”的版式。
Step 6
颜色映射:同洲同色系(三档明暗)
遍历
continent_order,对每个大洲基色生成
shades = shade_triplet(base),并将:
- Top1 映射到 shades["top1"]
- Top2 映射到 shades["top2"]
- Other__<Continent> 映射到
shades["other"]
最终形成 fill_values,确保每个 segment
都有明确填色(不依赖默认调色)。
Step 7 图例组织:按洲分组排序 +
标签附带占比
构造
legend_df,按每个大洲依次生成图例顺序:Top1 → Top2 →
Others,并把 pct 拼接到标签文本中:
- 国家段:Country (xx%)
- Others 段:Continent: Others (xx%)
随后用 scale_fill_manual(breaks = ..., labels = ...)
显式控制图例顺序与标签内容。
Step 8 绘图输出:堆叠柱 + 单位换算
+ 字体统一
使用 geom_col()
绘制堆叠柱;scale_y_continuous(labels = label_number(scale = 1e-12, suffix = "T"))
将数量级转换为 Trillion;最后在 theme() 中统一使用
my_font,并规范标题、轴文字与图例排版以匹配全文视觉风格。
# ============================================================
# 0) Packages + 字体(统一:Noto Sans SC -> my_font)
# ============================================================
library(tidyverse)
library(gapminder)
library(ggsci)
library(scales)
library(showtext)
library(sysfonts)
# -----------------------------
# 字体:Noto Sans SC
# -----------------------------
font_add_google(name = "Noto Sans SC", family = "my_font")
showtext_auto()
options(scipen = 9999)
# ============================================================
# 1) 参数
# ============================================================
year_sel <- 2007
# 研究设定:固定大洲展示顺序(便于跨图比较)
continent_order <- c("Asia", "Americas", "Europe", "Africa", "Oceania")
# 主色:为每个大洲分配一个分类调色板的“基色”(同洲同色系锚点)
base_cols <- ggsci::pal_d3("category10")(length(continent_order))
names(base_cols) <- continent_order
bar_width <- 0.55
pct_acc <- 1 # 图例中占比精度(整数百分比)
# ------------------------------------------------------------
# 颜色工具:基色 -> 同色系明暗(Top1/Top2/Others)
# ------------------------------------------------------------
mix_col <- function(a, b, w = 0.5) {
rgb(colorRamp(c(a, b))(w) / 255)
}
shade_triplet <- function(base) {
c(
top1 = mix_col(base, "white", 0.12), # Top1:略亮(视觉主导但不跳色)
top2 = mix_col(base, "white", 0.30), # Top2:更亮(强化区分)
other = mix_col(base, "black", 0.22) # Others:略暗(压低但同色系)
)
}
# ============================================================
# 2) 数据:国家 GDP 总量(pop × gdpPercap)
# ============================================================
df_country <- gapminder %>%
filter(year == year_sel) %>%
mutate(
continent = factor(continent, levels = continent_order), # 固定绘图顺序
country = as.character(country),
country_gdp = pop * gdpPercap
)
# ============================================================
# 3) 每洲 Top2(按国家 GDP 总量贡献排序)
# ============================================================
top2_map <- df_country %>%
group_by(continent) %>%
arrange(desc(country_gdp), .by_group = TRUE) %>%
slice_head(n = 2) %>%
mutate(rank_in_continent = row_number()) %>%
ungroup() %>%
select(continent, country, rank_in_continent)
top1 <- top2_map %>% filter(rank_in_continent == 1)
top2 <- top2_map %>% filter(rank_in_continent == 2)
# ============================================================
# 4) 汇总:Top1 / Top2 / Other__洲,并计算洲内占比 pct
# ============================================================
plot_stack <- df_country %>%
left_join(top2_map, by = c("continent", "country")) %>%
mutate(
# Top2 国家保留国家名;其余合并为 “Other__<Continent>”
segment = if_else(
!is.na(rank_in_continent),
country,
paste0("Other__", as.character(continent))
)
) %>%
group_by(continent, segment) %>%
summarise(gdp_sum = sum(country_gdp), .groups = "drop") %>%
group_by(continent) %>%
mutate(pct = gdp_sum / sum(gdp_sum)) %>%
ungroup()
# ============================================================
# 5) 堆叠顺序:底部 Others -> 中间 Top2 -> 顶部 Top1
# ============================================================
stack_levels <- c(
paste0("Other__", continent_order),
top2$country,
top1$country
) %>% unique()
plot_stack <- plot_stack %>%
filter(segment %in% stack_levels) %>%
mutate(segment = factor(segment, levels = stack_levels))
# ============================================================
# 6) 颜色映射:每洲基色 -> 三档明暗(Top1/Top2/Others)
# ============================================================
fill_values <- c()
for (ct in continent_order) {
shades <- shade_triplet(base_cols[[ct]])
top1_ct <- top1 %>% filter(as.character(continent) == ct) %>% pull(country)
top2_ct <- top2 %>% filter(as.character(continent) == ct) %>% pull(country)
oth_ct <- paste0("Other__", ct)
fill_values[top1_ct] <- shades["top1"]
fill_values[top2_ct] <- shades["top2"]
fill_values[oth_ct] <- shades["other"]
}
# ============================================================
# 7) 图例:按洲顺序 Top1 -> Top2 -> "<Continent>: Others"
# ============================================================
legend_df <- map_dfr(continent_order, function(ct) {
top1_ct <- top1 %>% filter(as.character(continent) == ct) %>% pull(country)
top2_ct <- top2 %>% filter(as.character(continent) == ct) %>% pull(country)
oth_ct <- paste0("Other__", ct)
segs <- c(top1_ct, top2_ct, oth_ct)
segs <- segs[segs %in% as.character(plot_stack$segment)]
tibble(segment_chr = segs) %>%
left_join(
plot_stack %>% transmute(segment_chr = as.character(segment), pct),
by = "segment_chr"
) %>%
mutate(
label = case_when(
str_detect(segment_chr, "^Other__") ~ paste0(ct, ": Others (", percent(pct, accuracy = pct_acc), ")"),
TRUE ~ paste0(segment_chr, " (", percent(pct, accuracy = pct_acc), ")")
)
)
})
legend_breaks <- legend_df$segment_chr
legend_labels <- legend_df$label
# ============================================================
# 8) 绘图:堆叠柱(洲总量)+ 洲内 Top2 + Others
# ============================================================
ggplot(plot_stack, aes(x = continent, y = gdp_sum, fill = segment)) +
geom_col(
width = bar_width,
alpha = 0.95,
position = position_stack(reverse = TRUE)
) +
scale_fill_manual(
values = fill_values,
breaks = legend_breaks,
labels = legend_labels,
drop = FALSE,
guide = guide_legend(
byrow = TRUE,
keyheight = unit(0.55, "cm"),
keywidth = unit(0.55, "cm")
)
) +
scale_y_continuous(
labels = label_number(scale = 1e-12, suffix = "T"),
expand = expansion(mult = c(0, 0.06))
) +
labs(
title = "2007年:各大洲 GDP 总量及 Top2 贡献国",
subtitle = "同色系表示同一大洲;图例括号为洲内占比",
x = "Continent",
y = "GDP Total (Trillion USD)",
fill = "国家或地区"
) +
theme_minimal() +
theme(
# --- 字体统一 ---
plot.title = element_text(family = "my_font", face = "bold", size = 14, hjust = 0),
plot.subtitle = element_text(family = "my_font", size = 9, color = "grey50",
face = "italic", margin = margin(b = 12)),
axis.title = element_text(family = "my_font", face = "bold", size = 10),
axis.text = element_text(family = "my_font", size = 10),
legend.position = "right",
legend.text = element_text(family = "my_font", size = 9),
legend.title = element_text(family = "my_font", size = 10, face = "bold"),
legend.spacing.y = unit(0.2, "cm"),
legend.margin = margin(t = 0, b = 0, l = 0, r = 0),
panel.grid.minor = element_blank()
)© 华东师范大学 社会发展学院 人口研究所 | DAWN 研究组 | yzliu@soci.ecnu.edu.cn
课程负责人:刘贇喆 本章作者:刘贇喆 | 蒋娴静
最后更新:2026年03月19日 构建环境:R version 4.5.2 (2025-10-31)
基础 · 图形概念
直方图(Histogram) 用于刻画单个连续变量的分布形态。它将连续取值按给定组距划分为若干区间(Bins),并统计每个区间内的观测频数,从而以“柱形轮廓”近似呈现数据的整体结构与集中趋势。
1. 核心用途
2. 易混淆辨析:直方图 vs 柱状图
geom_histogram()):geom_col()
/ geom_bar()):3. 核心函数
在 ggplot2 中,直方图对应的几何对象函数为:
geom_histogram()
基础 · 代码实战
以 2007 年各国的 预期寿命
lifeExp(gapminder)
为例,使用直方图对连续变量进行分箱统计,用于快速识别预期寿命的集中区间、整体偏态特征以及是否存在长尾/极端值。
代码逻辑:
Step 1 筛选截面数据(固定分析年份)
从 gapminder
中选取 year == 2007 的观测,得到单一年份的截面数据
data_hist,用于展示该年的 lifeExp
分布形态。
Step 2 建立映射关系(连续变量 → x 轴)
在 ggplot()
中将连续变量 lifeExp 映射到 x(aes(x = lifeExp)),为后续的分箱统计与柱形绘制提供基础坐标系。
Step 3
分箱统计并绘制直方图(控制分辨率)
使用
geom_histogram() 自动完成分箱计数并绘制柱形;通过
binwidth = 2 指定组距为 2 年:
组距越小细节越多、越大则更平滑(直方图“分辨率”控制)。
Step 4 补充图形信息(标题/坐标轴/主题)
用 labs()
明确标题与坐标轴含义,其中 y = "Count"
表示柱高为每个分箱内的观测数量;最后用 theme_minimal()
统一简洁风格。
# 0) 数据:筛选 2007 年(直方图示例年)
library(tidyverse)
library(gapminder)
data_hist <- gapminder %>%
filter(year == 2007)
# 1) 直方图:lifeExp 分箱统计(组距=2 年)
ggplot(data_hist, aes(x = lifeExp)) +
geom_histogram(binwidth = 2) +
labs(
title = "Distribution of life expectancy (2007)",
x = "lifeExp (years)",
y = "Count"
) +
theme_minimal()进阶 · 代码实战
本节以 2007 年为截面考察全球各国的预期寿命分布,在同一坐标系中同时呈现:直方图的分箱结构、核密度曲线的连续轮廓、样本落点(rug),以及均值与中位数两条参考线,用于辅助识别分布的集中区间与整体偏移。
代码逻辑:
Step 1
数据筛选:锁定单一年份截面
从
gapminder 中筛选 year == 2007 得到
data_hist,确保后续所有统计量(直方图、KDE、均值/中位数)均基于同一截面数据计算,避免时间混杂。
Step 2 密度曲线预计算:KDE →
坐标表 dens_df
使用
density(data_hist$lifeExp)
计算核密度估计(KDE),并将输出的 x 与 y
组织为 dens_df(tibble)。
该步骤的目的是把“连续分布轮廓”显式转为可绘制的数据框,便于后续用
geom_area() 与 geom_line()
叠加到同一图层体系中。
Step 3 参考统计量:均值/中位数 →
ref_df
在 data_hist 上计算
mean_lifeExp 与 median_lifeExp(存入
ref_df),作为两条竖直参考线的定位依据。
这两条统计量分别刻画“均衡中心”(均值)与“稳健中心”(中位数),用于读图时对分布中心位置进行快速对照。
Step 4 统一标度:直方图切换为
density,并提取 y_top
为使直方图与 KDE 在同一 y
轴上可叠加,将直方图统计量切换为密度刻度:
aes(y = after_stat(density))(柱高表示密度而非频数)。
随后构建临时图 p_tmp 并用 ggplot_build()
提取直方图的最大密度 y_top,用于:
- 限制均值/中位数参考线的高度(避免超过绘图区);
- 提供标签放置的相对高度基准(如
0.98*y_top)。
Step 5 标签数据框:label_df
管理文本与坐标
将两条标签(Mean /
Median)及其放置坐标写入 label_df,用
geom_label() 一次性绘制。
该做法便于统一调整标签位置与字号,且避免在不同 ggplot2 版本中反复依赖
annotate() 的细节参数。
Step 6 分层绘制:Histogram → Rug →
KDE(面积+线)→ 参考线 → 标签
按信息层级依次叠加图层:
-
geom_histogram(..., y = after_stat(density)):给出分箱结构(底图)。
- geom_rug():补充样本在 x 轴上的落点密集区(细节线索)。
- geom_area() + geom_line():以统一基色
kde_col 绘制 KDE 面积与轮廓线(连续形态)。
- geom_segment():绘制均值(虚线)与中位数(实线)两条参考线,线段高度按 y_top
控制。
- geom_label():添加数值标签,并指定
family = "my_font" 以统一字体输出。
# ============================================================
# 进阶:Histogram + KDE + Rug + Mean/Median(density 标度)
# ============================================================
library(tidyverse)
library(gapminder)
library(showtext)
# -----------------------------
# 字体:Noto Sans SC
# -----------------------------
font_add_google(name = "Noto Sans SC", family = "my_font")
showtext_auto()
options(scipen = 9999)
# -----------------------------
# 0) 数据:2007 年截面(单变量分布示例)
# -----------------------------
data_hist <- gapminder %>%
filter(year == 2007)
# -----------------------------
# 1) KDE:预计算密度曲线坐标(用于面积 + 曲线叠加)
# -----------------------------
dens_df <- density(data_hist$lifeExp, na.rm = TRUE) %>%
with(tibble(x = x, y = y))
# -----------------------------
# 2) 参考统计量:均值 / 中位数(参考线与数值标签)
# -----------------------------
ref_df <- data_hist %>%
summarise(
mean_lifeExp = mean(lifeExp, na.rm = TRUE),
median_lifeExp = median(lifeExp, na.rm = TRUE)
)
# -----------------------------
# 3) y 轴上限:从 density 直方图提取最大 density(控制线段高度与标签位置)
# -----------------------------
p_tmp <- ggplot(data_hist, aes(x = lifeExp)) +
geom_histogram(aes(y = after_stat(density)), binwidth = 2)
y_top <- max(ggplot_build(p_tmp)$data[[1]]$density, na.rm = TRUE)
# -----------------------------
# 4) 统一色系:KDE 曲线与面积共用同一基色
# -----------------------------
kde_col <- "#B53A2A"
# -----------------------------
# 5) 标签数据:用数据框管理文本与坐标(便于调整)
# -----------------------------
label_df <- tibble(
x = c(ref_df$mean_lifeExp, ref_df$median_lifeExp),
y = c(0.98 * y_top, 0.90 * y_top),
lab = c(
paste0("Mean = ", round(ref_df$mean_lifeExp, 1)),
paste0("Median = ", round(ref_df$median_lifeExp, 1))
)
)
# -----------------------------
# 6) 出图:density 直方图 + KDE(面积+线)+ rug + 均值/中位数(线+数值)
# -----------------------------
ggplot(data_hist, aes(x = lifeExp)) +
# 直方图:切换到 density 标度(与 KDE 同轴叠加)
geom_histogram(
aes(y = after_stat(density)),
binwidth = 2, # 分箱组距:2 年(可按信息密度调整)
fill = "#4C78A8",
colour = "white",
linewidth = 0.35,
alpha = 0.80
) +
# rug:样本落点(补充 x 轴上的细部聚集)
geom_rug(alpha = 0.12, sides = "b") +
# KDE 面积:透明填充(避免遮挡柱形)
geom_area(
data = dens_df,
aes(x = x, y = y),
fill = kde_col,
alpha = 0.10,
inherit.aes = FALSE
) +
# KDE 曲线:分布轮廓线
geom_line(
data = dens_df,
aes(x = x, y = y),
colour = kde_col,
linewidth = 1.1,
inherit.aes = FALSE
) +
# 均值线:虚线(线段高度限制在图内)
geom_segment(
data = ref_df,
aes(x = mean_lifeExp, xend = mean_lifeExp, y = 0, yend = 0.95 * y_top),
linetype = "dashed",
linewidth = 0.8,
colour = "grey35",
inherit.aes = FALSE
) +
# 中位数线:实线(线段高度限制在图内)
geom_segment(
data = ref_df,
aes(x = median_lifeExp, xend = median_lifeExp, y = 0, yend = 0.95 * y_top),
linetype = "solid",
linewidth = 0.8,
colour = "grey35",
inherit.aes = FALSE
) +
# 数值标签:与参考线对应(必要时只需改 y 或 size)
geom_label(
data = label_df,
aes(x = x, y = y, label = lab),
inherit.aes = FALSE,
size = 3.2,
linewidth = 0.25,
label.padding = unit(0.15, "lines"),
colour = "grey20",
fill = "white",
alpha = 0.90,
family = "my_font"
) +
# 标题与坐标
labs(
title = "Life expectancy distribution (2007)",
subtitle = "Density histogram with KDE and mean/median",
x = "lifeExp (years)",
y = "Density"
) +
# 主题:字体样式
theme_minimal() +
theme(
plot.title = element_text(family = "my_font", face = "bold", size = 14, hjust = 0),
plot.subtitle = element_text(family = "my_font", size = 9, color = "grey50",
face = "italic", margin = margin(b = 12)),
axis.title = element_text(family = "my_font", face = "bold", size = 10),
axis.text = element_text(family = "my_font", size = 10),
panel.grid.minor = element_blank()
)© 华东师范大学 社会发展学院 人口研究所 | DAWN 研究组 | yzliu@soci.ecnu.edu.cn
课程负责人:刘贇喆 本章作者:刘贇喆 | 蒋娴静
最后更新:2026年03月19日 构建环境:R version 4.5.2 (2025-10-31)
基础 · 图形概念
雷达图(Radar Chart) 用于呈现多个变量在同一对象上的取值结构,并支持在同一坐标框架下对比多个对象的多维表现。它将各变量映射到从中心辐射的角向轴,并以闭合的线或多边形连接各维度取值,从而形成可直观识别的“结构画像”。
1. 核心用途
多维对比:
比较不同对象在多个指标维度上的相对水平差异。
(例如:不同国家在预期寿命、收入、人口等指标上的综合表现)
结构画像:
识别对象的优势维度与短板维度,并观察维度间的均衡性。
(Profile / Pattern)
综合评估呈现:
将多指标评分结果以同一图形呈现,便于跨对象的整体对照。
(Performance / Index-based
assessment)
2. 核心函数
在本章中,我们采用 fmsb
包绘制雷达图。其实现基于多边形连线与同心网格刻度,并提供对边框、填充与网格的直接控制。
fmsb::radarchart()
fmsb:安装与使用
本章雷达图的核心函数为:
fmsb::radarchart()
(需先安装并加载fmsb包)
fmsb 可通过 CRAN 直接安装:
radarchart():用于绘制雷达图主体(轴线 / 网格 / 多边形)。pfcol /
pcol:分别控制多边形填充色与边框色(可设置透明度)。基础 · 代码实战
使用 mtcars 数据绘制基础雷达图,用于展示单一车型(Mazda
RX4)在多个性能指标上的相对结构画像。
代码逻辑:
Step 1
设定对象与指标体系(统一比较维度)
先选定目标车型
car_name,并明确多维指标集合 vars(mpg, hp, qsec, wt, drat);同时准备
vars_zh 作为中文轴标签,仅用于展示,不参与计算。
Step 2 按指标做 min–max
标准化(把量纲压到同一尺度)
将
mtcars 转为长表后,以每个指标 var
为分组,在全样本范围内计算 min–max,并得到
score ∈ [0,1](相对位置);随后筛选目标车型并回到宽表结构,为雷达图一行多变量输入做准备。
Step 3 构造 fmsb
的专用输入(先
max/min,再放目标对象)
fmsb::radarchart()
要求数据前两行分别为上界与下界(用于固定坐标轴尺度与网格刻度),因此先构造
row_max(全 1)与
row_min(全 0),再与目标车型数据
df_radar 依次拼接为 radar_in。
Step 4 中文化轴标签并绘图
将 radar_in 的列名替换为 vars_zh
以显示中文维度;随后调用 radarchart():用
pcol/pfcol 设置轮廓与同色系透明填充,用 plwd
强化主体边界,并用 axistype 控制坐标轴刻度呈现;最后用
title() 补充主标题。
# ==============================================================================
# mtcars 单车雷达图(Mazda RX4)— fmsb 版本
# 目标:把多指标表现压到同一尺度([0,1]),再用雷达图呈现“结构画像”
# ==============================================================================
library(tidyverse)
library(fmsb)
library(showtext)
library(sysfonts)
library(scales)
# -----------------------------
# 字体:Noto Sans SC(统一 family = "my_font")
# -----------------------------
font_add_google(name = "Noto Sans SC", family = "my_font")
showtext_auto()
options(scipen = 9999)
# ==============================================================================
# 1) 分析设定:对象 + 维度(英文变量名用于计算)
# ==============================================================================
car_name <- "Mazda RX4"
vars <- c("mpg", "hp", "qsec", "wt", "drat")
# 维度中文名:仅用于轴标签显示(不参与计算)
vars_zh <- c("油耗", "马力", "加速", "重量", "传动")
# ==============================================================================
# 2) 数据准备:按“指标维度”做 min–max 标准化 → [0,1]
# 说明:以 mtcars 全样本作为参照,得到该车在每个指标上的相对位置
# ==============================================================================
df_radar <- mtcars %>%
rownames_to_column("car") %>%
select(car, all_of(vars)) %>%
pivot_longer(-car, names_to = "var", values_to = "raw") %>%
group_by(var) %>%
mutate(
score = (raw - min(raw, na.rm = TRUE)) / (max(raw, na.rm = TRUE) - min(raw, na.rm = TRUE))
) %>%
ungroup() %>%
filter(car == car_name) %>%
select(var, score) %>%
pivot_wider(names_from = var, values_from = score)
# ==============================================================================
# 3) fmsb 输入格式:需在数据前加入 max/min 两行(用于固定坐标尺度)
# ==============================================================================
row_max <- as_tibble(setNames(as.list(rep(1, length(vars))), vars)) # 上界:1
row_min <- as_tibble(setNames(as.list(rep(0, length(vars))), vars)) # 下界:0
radar_in <- bind_rows(row_max, row_min, df_radar) %>%
as.data.frame()
# 轴标签中文化:仅替换列名(不影响数值)
colnames(radar_in) <- vars_zh
# ==============================================================================
# 4) 出图:边框线 + 同色系透明填充
# ==============================================================================
op <- par(family = "my_font") # base 图形系统:指定 showtext 注册的字体 family
fmsb::radarchart(
radar_in,
# -----------------------------
# 多边形样式(主体)
# -----------------------------
pcol = "#1f78b4", # 轮廓线颜色(polygon border)
pfcol = scales::alpha("#1f78b4", 0.25), # 填充色 + 透明度(避免遮挡网格与刻度)
plwd = 2, # 轮廓线线宽(越大越强调主体边界)
# -----------------------------
# 坐标轴/刻度样式(读图辅助)
# -----------------------------
axistype = 1 # 轴刻度样式:1 = 显示刻度线/刻度文本(默认常用)
)
title(main = paste0(car_name, " (min–max scaled)"))读图方法
mtcars
全样本),环形刻度可理解为相对位置:越靠外表示在该维度上越接近样本最大值,越靠内表示越接近样本最小值。wt、qsec);因此读图时应按变量本身含义解读,而非一律将“越外越好”。从图中可直接读出的主要信息(Mazda RX4)
进阶 · 代码实战
本节以 mtcars 为例,将雷达图扩展为两类输出:
(1)多车同图叠加:
在同一坐标尺度下叠加多辆车的多维得分,用于快速识别车型之间的结构差异;
(2)单车小多图(3×3):
为每辆车分别绘制雷达图,并统一网格与刻度设置,便于逐一阅读与横向对照。
代码逻辑:
Step 1
对象与维度设定:统一比较口径
指定待比较车型集合
cars_sel(6 辆车),选定指标维度
vars(mpg, hp, qsec, wt,
drat)作为统一比较轴;同步准备 vars_zh
用于轴标签中文化(仅影响显示)。
Step 2 按维度标准化:min–max →
[0,1]
将数据转为长表后按 var
分组,在全样本范围内对每个指标做 min–max 标准化得到
score,以消除量纲差异;并通过
factor(levels = vars) 固定维度顺序,确保各图轴顺一致。
Step 3 构造 fmsb 输入:宽表 +
max/min 两行固定尺度
将标准化结果转为宽表
df_wide(行=车型,列=指标),并按
fmsb::radarchart() 规范在前两行追加上界 1
与下界 0 以固定坐标尺度;最后将列名替换为
vars_zh 作为中文轴标签。
Step 4
颜色方案:线色区分车型,填充用于单车阅读
用
RColorBrewer::brewer.pal(..., "Dark2") 生成
cols_line 区分车型;再用 scales::alpha()
构造透明填充 cols_fill,用于单车图强调形态与面积。
Step 5 输出
A:多车叠加总图
对 radar_all
直接作图:pcol = cols_line
区分车型轮廓线,pfcol = NA
关闭填充以减少遮挡;网格参数保持一致,并用 legend()
显示颜色与车型映射。
Step 6 输出
B:单车小多图(2×3;线+透明填充)
设置 par(mfrow = c(2, 3))
进行网格排版;循环构造每辆车的 radar_one(同样包含 max/min 两行以固定尺度),并用
pcol 与 pfcol
同时展示轮廓与填充,以便逐车阅读且保持严格可比性。
# ==============================================================================
# 进阶:多车同图对比 + 单车小多图(2×3)— fmsb 版本
# - 同图叠加:快速比较“结构差异”
# - 小多图:逐一阅读每辆车的多维画像(统一尺度)
# ==============================================================================
library(tidyverse)
library(fmsb)
library(showtext)
library(sysfonts)
# -----------------------------
# 0) 环境准备(含字体:可跨平台稳定显示中文)
# -----------------------------
font_add_google(name = "Noto Sans SC", family = "my_font")
showtext_auto()
options(scipen = 9999)
# ==============================================================================
# 1) 参数设置:车型集合 + 指标维度 + 中文轴标签
# ==============================================================================
cars_sel <- c("Mazda RX4", "Datsun 710", "Hornet 4 Drive",
"Valiant", "Merc 240D", "Fiat 128")
vars <- c("mpg", "hp", "qsec", "wt", "drat") # 计算用英文变量名
vars_zh <- c("油耗", "马力", "加速", "重量", "传动") # 显示用中文维度名
# ==============================================================================
# 2) 数据准备:按维度做 min–max 标准化(缩放到 [0,1])
# ==============================================================================
df_scaled <- mtcars %>%
rownames_to_column("car") %>%
select(car, all_of(vars)) %>%
pivot_longer(-car, names_to = "var", values_to = "raw") %>%
group_by(var) %>%
mutate(score = (raw - min(raw, na.rm = TRUE)) / (max(raw, na.rm = TRUE) - min(raw, na.rm = TRUE))) %>%
ungroup() %>%
filter(car %in% cars_sel) %>%
mutate(var = factor(var, levels = vars))
# ==============================================================================
# 3) 转换为 fmsb 输入:宽表 + 上界/下界两行(固定坐标尺度)
# ==============================================================================
df_wide <- df_scaled %>%
select(car, var, score) %>%
pivot_wider(names_from = var, values_from = score) %>%
column_to_rownames("car") %>%
as.data.frame()
radar_all <- rbind(
rep(1, length(vars)),
rep(0, length(vars)),
df_wide
)
colnames(radar_all) <- vars_zh
# ==============================================================================
# 4) 配色:线色 + 透明填充(分图使用)
# ==============================================================================
cols_line <- RColorBrewer::brewer.pal(n = length(cars_sel), name = "Dark2")
cols_fill <- scales::alpha(cols_line, 0.25)
# ==============================================================================
# 5) 总图:多车叠加(仅画轮廓线;避免填充造成遮挡)
# ==============================================================================
op <- par(family = "my_font")
fmsb::radarchart(
radar_all,
pcol = cols_line,
pfcol = NA,
plwd = 2,
axistype = 1,
cglcol = "grey85",
cglty = 1,
cglwd = 0.8,
vlcex = 0.9
)
title(main = "Multi-car comparison (min–max scaled)")
legend(
"topright",
legend = rownames(df_wide),
bty = "n",
col = cols_line,
lwd = 2
)# ==============================================================================
# 6) 分图:单车雷达图(2×3 小多图;启用透明填充)
# ==============================================================================
par(mfrow = c(2, 3), mar = c(2, 2, 2, 1))
for (i in seq_along(cars_sel)) {
car_i <- cars_sel[i]
radar_one <- rbind(
rep(1, length(vars)),
rep(0, length(vars)),
df_wide[car_i, , drop = FALSE]
)
colnames(radar_one) <- vars_zh
fmsb::radarchart(
radar_one,
pcol = cols_line[i],
pfcol = cols_fill[i],
plwd = 2,
axistype = 1,
cglcol = "grey85",
cglty = 1,
cglwd = 0.8,
vlcex = 0.9
)
title(main = car_i, cex.main = 0.9)
}© 华东师范大学 社会发展学院 人口研究所 | DAWN 研究组 | yzliu@soci.ecnu.edu.cn
课程负责人:刘贇喆 本章作者:刘贇喆 | 蒋娴静
最后更新:2026年03月19日 构建环境:R version 4.5.2 (2025-10-31)
基础概念
箱线图(Boxplot),又称“箱须图”,是统计学中用于展示数据分布特征的常用图形,尤其适合同时呈现离散程度与离群值信息。它基于“五数概括法”(Five-number summary),用少量图形元素概括一组数据的整体结构。
1. 适用场景
2. 如何阅读箱线图?
将数据视为已按数值大小排序,箱线图主要呈现以下关键位置:
基础 · 代码实战
使用 gapminder 2007
年数据绘制基础箱线图,用于快速比较各大洲预期寿命的分布差异。
代码逻辑:
Step 1 筛选截面数据(2007 年)
从 gapminder 中筛选
year == 2007,得到用于箱线图比较的单年截面数据
data_box。
Step 2 设定映射关系(类别 × 数值)
在 ggplot() 中将
continent 映射到 x(分类轴)、将 lifeExp 映射到
y(连续轴),从而把“洲际分组”与“寿命水平”绑定到同一张图的坐标框架。
Step 3
调用箱线图几何对象并完成五数概括绘制
叠加
geom_boxplot():该函数会基于每个 continent
组内数据自动计算中位数与四分位数,并绘制箱体、胡须以及离群点(默认按 1.5×IQR 规则识别)。
Step 4
补充文本信息与统一主题风格
用 labs()
添加标题、说明与轴标签,明确图形元素含义;最后使用
theme_minimal() 降低背景干扰,突出分布差异。
# 0. 数据筛选
library(tidyverse)
library(gapminder)
data_box <- gapminder %>%
filter(year == 2007)
# 1. 绘图初始化
ggplot(
data = data_box,
mapping = aes(
x = continent, # x轴:分类变量(大洲)
y = lifeExp # y轴:连续变量(预期寿命)
)
) +
# 2. 几何层:箱线图
# 自动计算中位数、四分位数、IQR,并绘制箱体/胡须/离群点
geom_boxplot() +
# 3. 标签与主题
labs(
title = "2007 年各大洲预期寿命分布",
subtitle = "黑线=中位数;箱体=中间 50% 数据;黑点=离群值",
x = "大洲",
y = "预期寿命 (岁)"
) +
theme_minimal()进阶 · 代码实战
为更细致地呈现分布形态与时间演变,本节使用
分面小提琴图(Faceted Violin
Plot) 展示 lifeExp
在不同大洲的分布轮廓。小提琴图基于核密度估计(KDE),能够在同一张图中同时观察各组的集中区间、偏态与可能的多峰结构。
代码逻辑:
Step 1
环境与字体:统一中文显示口径
加载
tidyverse 与 gapminder,并用
showtext 注册 Noto Sans SC 到
family = "my_font";随后启用
showtext_auto(),保证标题、坐标轴与分面条带可稳定显示中文。
Step 2 数据筛选:三期截面对照 +
分面顺序固定
从 gapminder 中筛选
1952 / 1977 / 2007 三个代表年份构成对照样本,并将
year 设为有序因子
factor(year, levels = c(1952, 1977, 2007)),确保
facet_wrap() 分面按时间顺序排列。
Step 3 美学映射:类别 × 分布变量 +
同色系边界强化
在 ggplot() 中将
continent 映射到 x(类别轴),将 lifeExp 映射到
y(连续变量);同时将
fill 与 colour 都映射为
continent,使填充与轮廓保持同色系,增强小提琴边界的辨识度。
Step 4 几何层:密度轮廓 +
中位数线(读图锚点)
使用
geom_violin()
绘制密度轮廓,并通过关键参数控制形态与可读性:
- trim = TRUE:将密度裁剪到观测范围内(不向外延展);
-
draw_quantiles = 0.5:在小提琴内部绘制中位数线,提供组内中心位置的参照;
- alpha 与
linewidth:平衡填充强度与轮廓清晰度,减少遮挡。
Step 5
分面布局:按年份分块以呈现时间演变
使用
facet_wrap(~ year, ncol = 1)
将三个年份按行排列,使同一大洲在不同年份的分布形态可纵向对照;并保持
y 轴共享尺度以支持跨年可比。
Step 6
色盘与主题:统一视觉系统并减少冗余信息
采用
scale_fill_brewer() 与 scale_colour_brewer()
的 Set2 色盘统一配色;通过 theme_minimal() +
my_font 系列设置统一排版风格,并关闭图例
legend.position = "none"(因
x 轴已直接标注大洲,避免重复)。
# ==============================================================================
# 小提琴图(Violin Plot):三期对比(1952 / 1977 / 2007)
# 目标:比较各大洲 lifeExp 的分布形态随时间的变化(密度轮廓 + 中位数线)
# ==============================================================================
library(tidyverse)
library(gapminder)
library(showtext)
# ------------------------------------------------------------------------------
# 0) 环境准备(字体:可跨平台稳定显示中文)
# ------------------------------------------------------------------------------
font_add_google(name = "Noto Sans SC", family = "my_font")
showtext_auto()
options(scipen = 9999)
# ------------------------------------------------------------------------------
# 1) 数据准备:筛选三个代表年份 + 年份设为有序因子(保证分面顺序)
# ------------------------------------------------------------------------------
data_violin <- gapminder %>%
filter(year %in% c(1952, 1977, 2007)) %>%
mutate(year = factor(year, levels = c(1952, 1977, 2007)))
# ------------------------------------------------------------------------------
# 2) 绘图:小提琴图(密度轮廓)+ 中位数线 + 分面(三行)
# ------------------------------------------------------------------------------
ggplot(
data = data_violin,
mapping = aes(
x = continent,
y = lifeExp,
fill = continent, # 填充:区分大洲
colour = continent # 轮廓:与填充同色系,增强边界可读性
)
) +
geom_violin(
alpha = 0.60, # 透明度:避免颜色过重
trim = TRUE, # 密度裁剪到观测范围(不外延)
draw_quantiles = 0.5,# 在小提琴内部画中位数线
linewidth = 0.60 # 轮廓线宽:更清晰的边界
) +
facet_wrap(~ year, ncol = 1) +
# 统一色盘
scale_fill_brewer(palette = "Set2") +
scale_colour_brewer(palette = "Set2") +
labs(
title = "1952 / 1977 / 2007:各大洲预期寿命分布演变",
subtitle = "小提琴宽度表示密度;内部横线为中位数",
x = NULL,
y = "预期寿命(岁)"
) +
# 主题:与“my_font”体系保持一致
theme_minimal() +
theme(
plot.title = element_text(family = "my_font", face = "bold", size = 14, hjust = 0),
plot.subtitle = element_text(family = "my_font", size = 9, color = "grey50",
face = "italic", margin = margin(b = 12)),
axis.title = element_text(family = "my_font", face = "bold", size = 10),
axis.text = element_text(family = "my_font", size = 10),
strip.background = element_rect(fill = "#f0f0f0", color = NA),
strip.text = element_text(family = "my_font", face = "bold", size = 11),
legend.position = "none" # x 轴已显示大洲,不重复图例
)小提琴图本质上是“镜像后的密度图”(density 的左右镜像)。
阅读时重点关注两件事:哪里最“胖”、尾部拖得多长。
胖瘦(Width)= 密度(Density)
形状对比
例如 Europe 往往在较高寿命区间更“鼓”,而 Africa
更可能在较低寿命区间更“鼓”:
同一张图里就能看出不同大洲的寿命分布中心与不均衡结构。
为什么 Oceania 看起来很奇怪?
gapminder
中,Oceania 只有 2 个国家(Australia、New Zealand)参与分布估计。警示:
当每组样本量很小(例如
<
5)时,小提琴图(或箱线图)都可能缺乏解释力。更稳妥的做法是使用抖动散点图(Jitter
Plot)直接展示观测点,必要时再配合均值/中位数标记 (参考下列代码)
进阶代码 · 抖动散点图
# ==============================================================================
# 抖动散点图(Jitter Plot):三期对比(1952 / 1977 / 2007)
# 目标:直接呈现“国家层级观测点”,并用中位数线辅助比较组间中心位置
# ==============================================================================
library(tidyverse)
library(gapminder)
library(showtext)
# ------------------------------------------------------------------------------
# 0) 环境准备(字体:可跨平台稳定显示中文)
# ------------------------------------------------------------------------------
font_add_google(name = "Noto Sans SC", family = "my_font")
showtext_auto()
options(scipen = 9999)
# ------------------------------------------------------------------------------
# 1) 数据准备:筛选三个代表年份 + 年份设为有序因子(保证分面顺序)
# ------------------------------------------------------------------------------
data_jitter <- gapminder %>%
filter(year %in% c(1952, 1977, 2007)) %>%
mutate(year = factor(year, levels = c(1952, 1977, 2007)))
# ------------------------------------------------------------------------------
# 2) 绘图:抖动散点(国家点)+ 中位数参考线 + 分面(三行)
# ------------------------------------------------------------------------------
ggplot(
data = data_jitter,
mapping = aes(
x = continent,
y = lifeExp,
colour = continent # 颜色:区分大洲
)
) +
geom_jitter(
width = 0.18, # x 方向轻微抖动:缓解重叠
height = 0, # y 方向不抖动:保持数值真实
alpha = 0.70,
size = 1.60
) +
# 组内中位数:作为“中心位置”的参考(对偏态分布更稳健)
stat_summary(
fun = median,
geom = "crossbar",
width = 0.35,
linewidth = 0.25,
colour = "black"
) +
facet_wrap(~ year, ncol = 1) +
# 统一色盘:与前述分布图一致(Set2)
scale_colour_brewer(palette = "Set2") +
labs(
title = "1952 / 1977 / 2007:各大洲预期寿命的国家观测点分布",
subtitle = "点表示国家观测值;黑色横线为组内中位数",
x = NULL,
y = "预期寿命(岁)"
) +
theme_minimal() +
theme(
plot.title = element_text(family = "my_font", face = "bold", size = 14, hjust = 0),
plot.subtitle = element_text(family = "my_font", size = 9, color = "grey50",
face = "italic", margin = margin(b = 12)),
axis.title = element_text(family = "my_font", face = "bold", size = 10),
axis.text = element_text(family = "my_font", size = 10),
strip.text = element_text(family = "my_font", face = "bold", size = 11),
legend.position = "none" # x 轴已写明大洲;避免重复图例
)© 华东师范大学 社会发展学院 人口研究所 | DAWN 研究组 | yzliu@soci.ecnu.edu.cn
课程负责人:刘贇喆 本章作者:刘贇喆 | 蒋娴静
最后更新:2026年03月19日 构建环境:R version 4.5.2 (2025-10-31)
基础概念
流向图(Sankey Diagram / Alluvial Plot)用于展示多个分类变量之间的路径结构与流量分配。图中每一列为一个阶段(axis),每个矩形为一个类别节点(stratum);连接节点的“流”(alluvium/flow)宽度编码数量或权重,便于在同一图中同时观察组成、分流与汇聚。
1. 适用场景
2. 如何阅读流向图?
3. 核心工具与函数
ggplot2 体系下常用 ggalluvial
绘制流向图,其核心几何对象为:
geom_alluvium()(绘制流线)
geom_stratum()(绘制节点)
geom_text(stat = "stratum")(节点标签)
基础 · 代码实战
使用内置的 Titanic 数据,展示“舱位等级 → 性别 →
是否幸存”的路径结构(以人数为权重)。
事件背景:
泰坦尼克号(RMS
Titanic)是英国白星航运公司建造的远洋客轮。1912 年 4 月 15
日凌晨,泰坦尼克号在北大西洋航行途中撞上冰山后沉没,造成大量人员伤亡,成为现代航海史上最著名的海难事件之一。
数据是什么:
R 语言内置的
Titanic
数据集是对该事件乘客信息的一个汇总频数表(contingency
table)。它并非逐个乘客的明细记录,而是按类别组合统计人数。
数据包含什么:
数据以四个分类维度组织:Class(舱位等级)、Sex(性别)、Age(成人/儿童)、Survived(是否幸存);对应的 Freq
表示该类别组合下的人数。
代码逻辑:
Step 1
准备数据与绘图环境(最小依赖)
加载
ggalluvial,并将内置的 Titanic
频数表转换为数据框 titanic_df。其中 Freq
表示每个类别组合对应的人数权重。
Step 2
设定多阶段轴映射(axis1/axis2/axis3)
在 aes()
中依次指定
axis1 = Class、axis2 = Sex、axis3 = Survived,并用
y = Freq 定义流线宽度的度量口径(人数)。
Step 3
绘制流线与节点(最小作图单元)
使用
geom_alluvium() 绘制流线,并以 fill = Class
按起点舱位着色以便追踪同一来源的分流;再用 geom_stratum()
绘制每一列的节点矩形,表示该阶段各类别的总量。
Step 4
固定阶段顺序并输出(保证可控)
使用
scale_x_discrete(limits = ...) 固定阶段顺序(Class → Sex → Survived),并应用
theme_bw() 作为基础主题以获得稳定、干净的输出。
# ==============================================================================
# 基础:流向图(Class → Sex → Survived)
# 目标:用流线宽度表示人数,追踪不同舱位在性别与幸存结局上的分流
# ==============================================================================
library(tidyverse) # 数据处理与 ggplot2(本例主要用到 ggplot2)
library(ggalluvial) # ggplot2 生态下的流向图/桑基图扩展包
# ------------------------------------------------------------------------------
# Step 1. 数据准备:Titanic 为四维列联表(频数表),Freq 表示人数
# 说明:流向图通常需要“分类维度 + 权重(y)”的数据结构
# ------------------------------------------------------------------------------
titanic_df <- as.data.frame(Titanic)
# ------------------------------------------------------------------------------
# Step 2. 建立坐标映射:
# - axis1/axis2/axis3:定义三列“阶段轴”(从左到右的类别序列)
# - y = Freq:定义流线宽度的度量口径(人数)
# ------------------------------------------------------------------------------
ggplot(
data = titanic_df,
mapping = aes(
axis1 = Class, # 第 1 阶段:舱位等级
axis2 = Sex, # 第 2 阶段:性别
axis3 = Survived, # 第 3 阶段:是否幸存
y = Freq # 流量/权重:该路径上的人数
)
) +
# ----------------------------------------------------------------------------
# Step 3. 绘制“流线”:
# - fill = Class:按起点舱位着色,便于追踪同一来源在后续阶段的分流
# - alpha:透明度,减轻遮挡
# - width:控制各阶段柱与流线的横向厚度
# ----------------------------------------------------------------------------
geom_alluvium(aes(fill = Class), alpha = 0.7, width = 1/8) +
# ----------------------------------------------------------------------------
# Step 4. 绘制“节点”(每一列的矩形):
# 节点高度(宽度方向的总量)由 y = Freq 聚合得到
# ----------------------------------------------------------------------------
geom_stratum(width = 1/8, alpha = 0.9) +
# ----------------------------------------------------------------------------
# Step 5. 添加节点标签:
# stat = "stratum":在每个节点中心生成统计结果
# after_stat(stratum):提取节点类别名作为标签
# ----------------------------------------------------------------------------
geom_text(
stat = "stratum",
aes(label = after_stat(stratum)),
size = 3.6
) +
# ----------------------------------------------------------------------------
# Step 6. 固定阶段顺序与留白:
# limits:锁定从左到右的阶段顺序,避免默认排序导致不确定性
# expand:控制左右边缘留白,避免图形贴边
# ----------------------------------------------------------------------------
scale_x_discrete(
limits = c("Class", "Sex", "Survived"),
expand = c(0.05, 0.05)
) +
# ----------------------------------------------------------------------------
# Step 7. 主题与版式:减少视觉干扰
# - 关闭 y 轴文字与网格:流向图主要关注路径结构,y轴刻度往往非必要
# - 图例置顶:便于快速识别舱位颜色
# ----------------------------------------------------------------------------
theme_minimal() +
theme(
legend.position = "top",
axis.text.y = element_blank(),
axis.title.y = element_blank(),
panel.grid = element_blank()
) +
# ----------------------------------------------------------------------------
# Step 8. 标题与图例标题
# ----------------------------------------------------------------------------
labs(
title = "Titanic Passenger Survival Path",
subtitle = "Class → Sex → Survived",
fill = "Class"
) 流向图的核心读法是:先看节点总量,再沿着颜色追踪分流路径(宽度代表人数)。
Class
节点宽度对比,可读出各舱位总体人数构成。Sex
节点处的分叉,展示不同舱位内部的性别构成差异。Survived
节点处的流向宽度对比,可用于判断不同路径的幸存与遇难差异。结合本图的快速观察(Class → Sex → Survived):
No
的节点明显更宽,说明总体上遇难人数多于幸存人数。Sex 到
Survived 的流线对比可见,Female → Yes
的流线相对更粗,而 Male → No
更突出,反映出明显的性别差异。1st 流向
Yes 的比例相对更高;3rd 与 Crew
流向 No
的部分更显著,提示不同舱位在幸存结果上存在差异。进阶代码
代码逻辑:
Step 1
字体与绘图环境初始化(中文渲染)
加载 showtext 并注册
Noto Sans SC 字体,将其设为全局字体
my_font,确保后续标题与节点标签可稳定显示中文。
Step 2
数据聚合与口径构造(路径占比)
将 Titanic
频数表转换为数据框,并按 Class × Sex × Survived
聚合为一条条“路径”记录,得到对应人数 Freq。同时构造
prop = Freq / sum(Freq)
作为路径占比,用于后续透明度映射;并将各类别重编码为中文标签,统一因子顺序以保证图面稳定。
Step 3
设定多阶段轴映射(axis1/axis2/axis3)
在 aes()
中指定
axis1 = Class、axis2 = Sex、axis3 = Survived,并用
y = Freq 定义流线宽度的度量口径(人数)。
Step 4
颜色与透明度的双编码(舱位 ×
占比)
geom_alluvium() 中设置
fill = Class,并用 ggsci
色板为不同舱位/船员提供一致配色;alpha = prop,使占比越高的路径越不透明,占比越低的路径越透明,以增强结构对比。Step 5
节点与标签输出(可读性)
用 geom_stratum() 绘制各阶段节点,并使用浅色底 +
黑色描边提升对比;再用 geom_text(stat="stratum")
标注节点名称,字体设置为 my_font 以保证中文显示。
Step 6
固定阶段顺序与主题排版(稳定输出)
使用
scale_x_discrete(limits = ...) 固定阶段顺序(舱位 → 性别 → 结局)并设置中文轴标签;最后用
theme_minimal() 简化背景并隐藏 y
轴元素,使读者聚焦于路径结构与分流差异。
# ==============================================================================
# 进阶:流向图(舱位 → 性别 → 结局)
# 升级点:
# 1) 使用 ggsci 配色区分舱位/船员
# 2) 用透明度编码“路径占比”(占比越高越不透明)
# 3) 使用 showtext + 中文字体,并用中文标签输出
# ==============================================================================
# ------------------------------------------------------------------------------
# 0) 字体环境(中文显示)
# ------------------------------------------------------------------------------
library(showtext)
font_add_google(name = "Noto Sans SC", family = "my_font")
showtext_auto()
options(scipen = 9999)
# ------------------------------------------------------------------------------
# 1) 依赖加载
# ------------------------------------------------------------------------------
library(tidyverse)
library(ggalluvial)
library(ggsci)
# ------------------------------------------------------------------------------
# 2) 数据准备:按「Class × Sex × Survived」聚合(避免 Age 造成重复流线)
# - prop:路径占比(用于透明度映射)
# ------------------------------------------------------------------------------
titanic_adv <- as.data.frame(Titanic) %>%
group_by(Class, Sex, Survived) %>%
summarise(Freq = sum(Freq), .groups = "drop") %>%
mutate(
prop = Freq / sum(Freq), # 路径占比:越高越不透明
# 中文标签(用于节点与图例)
Class = recode(Class,
"1st" = "一等",
"2nd" = "二等",
"3rd" = "三等",
"Crew" = "船员"),
Sex = recode(Sex,
"Male" = "男性",
"Female" = "女性"),
Survived = recode(Survived,
"Yes" = "生还",
"No" = "遇难")
) %>%
mutate(
# 固定顺序:保证图面稳定可控
Class = factor(Class, levels = c("一等", "二等", "三等", "船员")),
Sex = factor(Sex, levels = c("女性", "男性")),
Survived = factor(Survived, levels = c("遇难", "生还"))
)
# ------------------------------------------------------------------------------
# 3) 配色:ggsci 色板(为不同舱位/船员提供一致且易区分的颜色)
# ------------------------------------------------------------------------------
class_cols <- ggsci::pal_npg()(length(levels(titanic_adv$Class)))
names(class_cols) <- levels(titanic_adv$Class)
# ------------------------------------------------------------------------------
# 4) 绘图:流线颜色 = 舱位;流线透明度 = 路径占比
# ------------------------------------------------------------------------------
ggplot(
data = titanic_adv,
mapping = aes(
axis1 = Class, # 第 1 阶段:舱位
axis2 = Sex, # 第 2 阶段:性别
axis3 = Survived, # 第 3 阶段:结局
y = Freq # 流线宽度:人数
)
) +
# 流线:颜色按舱位;透明度按“路径占比”
geom_alluvium(
aes(fill = Class, alpha = prop),
width = 1/8
) +
# 节点:统一浅色底 + 黑色描边,提升中文标签可读性
geom_stratum(
width = 1/8,
fill = "grey95",
colour = "black",
linewidth = 0.7
) +
# 节点标签:直接显示中文类别
geom_text(
stat = "stratum",
aes(label = after_stat(stratum)),
family = "my_font",
size = 4
) +
# 固定阶段顺序 + 中文轴标题
scale_x_discrete(
limits = c("Class", "Sex", "Survived"),
labels = c("Class" = "舱位", "Sex" = "性别", "Survived" = "结局"),
expand = c(0.05, 0.05)
) +
# 舱位配色(ggsci)
scale_fill_manual(values = class_cols, name = "舱位") +
# 透明度映射:占比越高越不透明(不单独显示 alpha 图例)
scale_alpha_continuous(range = c(0.15, 0.7), guide = "none") +
# 标题与主题(中文字体)
labs(
title = "泰坦尼克号生还路径",
subtitle = "舱位 → 性别 → 结局(流线宽度为人数;透明度为路径占比)"
) +
theme_minimal() +
theme(
text = element_text(family = "my_font"),
legend.position = "top",
axis.title.y = element_blank(),
axis.text.y = element_blank(),
panel.grid = element_blank()
)© 华东师范大学 社会发展学院 人口研究所 | DAWN 研究组 | yzliu@soci.ecnu.edu.cn
课程负责人:刘贇喆 本章作者:刘贇喆 | 蒋娴静
最后更新:2026年03月19日 构建环境:R version 4.5.2 (2025-10-31)
基础概念
山峦图(Ridgeline Plot),又称“脊线图”,是一种用于展示多个分类组别下连续变量数据分布的图形。它通过将多个核密度估计曲线(KDE)在垂直方向上错位重叠排列,形似连绵起伏的山脉,从而在有限的垂直空间内高效对比多组数据的分布形态与演变趋势。
1. 适用场景
2. 如何阅读山峦图?
将图形视为多条独立核密度曲线的垂直排布,主要呈现以下关键特征:
3. 核心工具与函数
绘制山峦图需要配合第三方扩展包 ggridges
使用,其核心几何对象函数为: >
geom_density_ridges() >
注:需要提前安装并读取 ggridges
基础 · 代码实战
使用 gapminder 2007
年数据绘制基础山峦图,用于直观比较各大洲预期寿命的密度分布轮廓。
代码逻辑:
Step 1
准备数据与绘图环境(加载扩展包)
除了 tidyverse 与
gapminder,需额外加载 ggridges 包;筛选
year == 2007 得到用于对比的单年截面数据
data_ridge。
Step 2 设定映射关系(连续 × 类别)
在 ggplot() 中将
lifeExp 映射到 x(连续轴,决定山底的宽度)、将
continent 映射到 y(分类轴,决定各座山的基线高度),同时将
continent 映射到 fill(用颜色区分不同山体)。 >
注意:山峦图的 xy 映射方向通常与箱线图相反。
Step 3
调用脊线几何对象并控制层叠
叠加
geom_density_ridges():设置 alpha = 0.7
增加透明度以透视被遮挡的山体,并通过 scale = 1.5
参数适度放大山体的高度倍率(形成错位重叠的“山峦感”),同时加上白色描边区分边界。
Step 4
补充文本信息与统一主题风格
用 labs()
添加基本的标题与轴标签(为保证基础出图的稳定性,暂用英文避免乱码);最后使用
theme_minimal() 降低背景干扰,并通过
theme(legend.position = "none") 关闭不必要的图例(因 y
轴已标明大洲类别,保留图例属于信息冗余)。
注: 因为大洋洲(Oceania)数据中只有两个国家,在本图中暂时剔除
# ==============================================================================
# 基础:山峦图(continent × lifeExp)
# 目标:用层叠密度曲线比较各大洲预期寿命的密度分布轮廓
# ==============================================================================
# -----------------------------
# Step 1. 准备数据与绘图环境
# -----------------------------
library(tidyverse)
library(gapminder)
library(ggridges) # 绘制山峦图的核心包
# 筛选 2007 年截面数据,并剔除大洋洲 (因其样本量极小,不适合进行密度估计)
data_ridge <- gapminder %>%
filter(year == 2007, continent != "Oceania")
# -----------------------------
# Step 2 & 3 & 4. 绘图构建
# -----------------------------
ggplot(
data = data_ridge,
mapping = aes(
x = lifeExp, # x轴 (连续轴):决定山底的宽度
y = continent, # y轴 (分类轴):决定各座山的基线高度
fill = continent # 填充色:用不同颜色区分各大洲
)
) +
# Step 3. 调用脊线几何对象并控制层叠
# alpha: 增加透明度,使得被遮挡的山体轮廓依然可见
# scale: 控制山体的高度倍率,适度放大形成错位重叠的“山峦感”
geom_density_ridges(alpha = 0.7, scale = 1.5, color = "white") +
# Step 4. 补充文本信息与统一主题风格 (基础图形暂用英文避免乱码)
labs(
title = "Life Expectancy Distribution by Continent (2007)",
subtitle = "Ridgeline plot showing density of life expectancy (Oceania excluded)",
x = "Life Expectancy (years)",
y = "Continent"
) +
# 使用极简主题,并关闭图例 (因为 y 轴已经标明了大洲,保留图例属于信息冗余)
theme_minimal() +
theme(legend.position = "none")山峦图本质上是“垂直错位重叠的核密度估计曲线”(KDE)。
阅读时重点关注两个核心维度:山峰的水平位置与山体的宽窄起伏。
山峰位置(Position) = 集中水平(Central Tendency)
山体形态(Shape) = 分布结构(Distribution Structure)
从图中可直接读出的主要发现 (Findings):
补充说明:
图中排除了 Oceania(大洋洲)。在 gapminder
截面数据中该洲仅有 2
个国家,样本量极小。强行对其拟合密度曲线不仅缺乏统计意义,更会导致山体形态被平滑算法严重扭曲。这提醒我们:任何密度分布图的绘制都必须建立在充足的样本支持之上。
进阶 · 代码实战
为更细致地呈现数据的分布特征与跨时空演化,本节将基础山峦图升级为“多色阶地毯式山峦图”(Gradient Ridgeline with Rugs)。针对
ggplot2
默认单一图层仅能承载一种填充色阶的底层限制,本代码引入
ggnewscale
包实现多重标度映射:为各大洲分配独立的渐变色盘(如亚洲为紫、欧洲为蓝)。同时,图表融合了基于分位数的中位数标注与底层的地毯图(Rug
Plot),实现了宏观趋势与微观颗粒度的同步可视化。
代码逻辑:
Step 1
数据清洗与因子优化(跨时代截面)
加载
tidyverse、ggridges 及核心扩展包
ggnewscale。通过 filter 提取 1952 与 2007
年截面数据。关键操作在于使用 fct_drop()
剔除无效观测值的因子层级,并配合
scale_y_discrete(expand = ...) 参数,严格控制 Y
轴边缘留白,使大洲间的垂直间距更趋紧凑。
Step 2
多重标度叠加映射(ggnewscale)
通过循环叠加逻辑,为每个大洲单独调用
geom_density_ridges_gradient()。每完成一个大洲的颜色定义后,插入
new_scale_fill()
函数。该操作可重置绘图引擎的填充色映射,从而允许在同一坐标系内并存多套完全独立的渐变色阶。
Step 3
透明度与色阶注入(alpha 与
scale)
由于渐变几何对象会屏蔽全局 alpha
参数,本方案采用“色盘注入法”:利用
scales::alpha() 直接对底层色板进行 0.4
透明度处理。这不仅赋予了山体通透感,还确保了背景网格线在重叠区域清晰可见。此外,将
scale 提升至 4.5,强制独立渲染的山体产生错位重叠效果。
Step 4
统计标注与地毯映射(Rug
Plot)
- 中位数可视化:开启
quantile_lines 自动渲染分位线,并利用
geom_text() 在各组中点位置静态度量数值(精确至 0.1),强化信息传达的精准度。 -
分布颗粒度:启用 jittered_points
映射原始观测值,利用竖线形状 | 和垂直位移
yoffset
将散点平移至山体基线下方,形成紧凑的“地毯”效应,揭示核密度曲线背后的真实样本点分布。
Step 5 学术主题定制
应用 facet_wrap 实现跨时代对比。在 theme
层级中,加密 X 轴主刻度间距(步长为
10),并采用硬朗的网格边框与 dotted
虚线辅助线,提升图形的工程质感与学术规范性。
# ==============================================================================
# 图形构建:多色阶地毯式山峦图(Faceted Gradient Ridgeline Plot with Rugs)
# 核心技术:核密度估计 (KDE)、多重 Fill 标度映射 (ggnewscale)、透明度色阶注入
# ==============================================================================
# 1. 环境初始化与全局字体设定
library(showtext)
font_add_google(name = "Noto Sans SC", family = "my_font")
showtext_auto()
options(scipen = 9999)
# 2. 依赖加载与数据预处理
library(tidyverse)
library(gapminder)
library(ggridges)
library(ggnewscale)
data_adv <- gapminder %>%
filter(year %in% c(1952, 2007), continent != "Oceania") %>%
mutate(
year = factor(year, levels = c(1952, 2007)),
# forcats::fct_drop 彻底清除被剔除数据的冗余因子层级,防止坐标轴出现异常留白
continent = fct_drop(continent)
)
# 统计预计算:获取各洲截面的中位数,并格式化为含1位小数的字符向量,供后续文本层调用
df_median <- data_adv %>%
group_by(continent, year) %>%
summarise(med_val = median(lifeExp, na.rm = TRUE), .groups = "drop") %>%
mutate(med_label = sprintf("%.1f", med_val))
# 3. 初始化坐标系与全局参数
p <- ggplot(data_adv, aes(x = lifeExp, y = continent))
# 设定统一的核密度高度乘数 (scale),强制分离的独立图层产生向上突破基线的视觉重叠
ridge_scale <- 4.5
# ------------------------------------------------------------------------------
# 4. 核心映射:利用 ggnewscale 突破单一图层限制,逐组分配独立渐变色带
# ------------------------------------------------------------------------------
# [图层 1] 非洲 (Africa)
p <- p +
# geom_density_ridges_gradient: 允许 fill 属性沿 x 轴连续变量产生渐变映射
geom_density_ridges_gradient(
data = filter(data_adv, continent == "Africa"),
aes(fill = after_stat(x)),
scale = ridge_scale, rel_min_height = 0.01, color = "black",
quantile_lines = TRUE, quantiles = 2, vline_color = "grey90", vline_width = 0.8, vline_linetype = "solid",
jittered_points = TRUE, point_shape = "|", point_size = 3, point_alpha = 0.6,
position = position_points_jitter(width = 0, height = 0, yoffset = -0.05)
) +
# scale_fill_gradientn & scales::alpha:
# 因 gradient 几何对象底层渲染屏蔽了全局 alpha,此处通过向输入色盘直接注入透明度来规避限制
scale_fill_gradientn(
colors = scales::alpha(RColorBrewer::brewer.pal(9, "Reds"), 0.4),
name = "非洲"
) +
# new_scale_fill: 释放当前的 fill 映射,为叠加下一图层做准备
new_scale_fill()
# [图层 2] 美洲 (Americas)
p <- p +
geom_density_ridges_gradient(
data = filter(data_adv, continent == "Americas"),
aes(fill = after_stat(x)),
scale = ridge_scale, rel_min_height = 0.01, color = "black",
quantile_lines = TRUE, quantiles = 2, vline_color = "grey90", vline_width = 0.8, vline_linetype = "solid",
jittered_points = TRUE, point_shape = "|", point_size = 3, point_alpha = 0.6,
position = position_points_jitter(width = 0, height = 0, yoffset = -0.05)
) +
scale_fill_gradientn(
colors = scales::alpha(RColorBrewer::brewer.pal(9, "Greens"), 0.4),
name = "美洲"
) +
new_scale_fill()
# [图层 3] 亚洲 (Asia)
p <- p +
geom_density_ridges_gradient(
data = filter(data_adv, continent == "Asia"),
aes(fill = after_stat(x)),
scale = ridge_scale, rel_min_height = 0.01, color = "black",
quantile_lines = TRUE, quantiles = 2, vline_color = "grey90", vline_width = 0.8, vline_linetype = "solid",
jittered_points = TRUE, point_shape = "|", point_size = 3, point_alpha = 0.6,
position = position_points_jitter(width = 0, height = 0, yoffset = -0.05)
) +
scale_fill_gradientn(
colors = scales::alpha(RColorBrewer::brewer.pal(9, "Purples"), 0.4),
name = "亚洲"
) +
new_scale_fill()
# [图层 4] 欧洲 (Europe)
p <- p +
geom_density_ridges_gradient(
data = filter(data_adv, continent == "Europe"),
aes(fill = after_stat(x)),
scale = ridge_scale, rel_min_height = 0.01, color = "black",
quantile_lines = TRUE, quantiles = 2, vline_color = "grey90", vline_width = 0.8, vline_linetype = "solid",
jittered_points = TRUE, point_shape = "|", point_size = 3, point_alpha = 0.6,
position = position_points_jitter(width = 0, height = 0, yoffset = -0.05)
) +
scale_fill_gradientn(
colors = scales::alpha(RColorBrewer::brewer.pal(9, "Blues"), 0.4),
name = "欧洲"
)
# ------------------------------------------------------------------------------
# 5. 统计标注、分面排版与出版级主题定制
# ------------------------------------------------------------------------------
p +
# geom_text: 叠加预计算的中位数值,利用 nudge 控制物理偏移,确保文本浮于轴线之上
geom_text(
data = df_median,
aes(x = med_val, y = continent, label = med_label),
inherit.aes = FALSE,
nudge_x = 0.8,
nudge_y = 0.25,
hjust = 0,
size = 3.2,
color = "grey10",
family = "my_font",
fontface = "bold"
) +
# facet_wrap: 建立时间维度的平行对比切片
facet_wrap(~ year) +
# scale_x/y: 加密连续轴刻度,并严格压缩离散 Y 轴的上下边缘 (expand),提升组间紧凑度
scale_x_continuous(breaks = seq(20, 90, by = 10)) +
scale_y_discrete(expand = expansion(mult = c(0.05, 0.23))) +
labs(
title = "1952 vs 2007:全球预期寿命分布的跨时代演进",
subtitle = "各大洲独立色阶渐变,浅灰色实线及数字为中位数,底层竖线(Rugs)代表各国真实点位",
x = "预期寿命 (岁)",
y = "大洲"
) +
# theme: 定制硬朗工程质感的网格与面板边框
theme_minimal() +
theme(
text = element_text(family = "my_font"),
plot.title = element_text(face = "bold", size = 15, hjust = 0),
plot.subtitle = element_text(size = 9, color = "grey40", margin = margin(b = 15)),
axis.title = element_text(face = "bold", size = 10),
axis.text = element_text(size = 10),
panel.background = element_rect(fill = "white", color = NA),
panel.border = element_rect(color = "black", fill = NA, linewidth = 1),
panel.grid.major.y = element_line(color = "grey85", linetype = "dotted"),
panel.grid.major.x = element_line(color = "grey85", linetype = "dotted"),
panel.grid.minor = element_line(color = "grey85", linetype = "dotted"),
strip.text = element_text(face = "bold", size = 12),
strip.background = element_rect(fill = "grey90", color = "black", linewidth = 1)
)© 华东师范大学 社会发展学院 人口研究所 | DAWN 研究组 | yzliu@soci.ecnu.edu.cn
课程负责人:刘贇喆 本章作者:刘贇喆 | 蒋娴静
最后更新:2026年03月19日 构建环境:R version 4.5.2 (2025-10-31)
基础概念
热力图(Heatmap) 是一种用颜色强度编码数值大小的图形。它把二维表格中的数值映射为连续色阶,使读者能在同一视图中快速识别高值/低值区域、梯度变化与块状结构等整体格局。热力图更强调呈现模式而非逐点读数,适合用于多组、多期、多维组合下的结构对比
1. 适用场景
整体格局识别:
用于观察不同组别在不同条件下的强弱结构与变化形态。
(如:各大洲在不同年份的平均预期寿命水平差异)
多期对比压缩:
当时间点较多且需要同时比较多组对象时,热力图可在有限版面内保留完整信息,并便于横向对照。
(相比多张折线图/多组柱状图更节省空间)
结构分层呈现:
当行或列按规则排序时,热力图可更清晰地呈现梯度、分层与块状聚集的结构特征。
(Gradient / Block structure)
2. 核心技能:如何阅读热力图?
先看色标口径:
明确颜色对应的数值范围与刻度口径,确保比较建立在同一色阶之上。
(色标范围决定“差异感知”的强弱)
读行/列结构:
行方向通常承载组别差异(如 continent),列方向承载序列变化(如
year)。先读“行内随时间如何变化”,再读“同一年不同组别如何分层”。
识别连续性与块状结构:
连续的高值带/低值带通常提示稳定趋势或系统性分层;块状聚集提示阶段性结构差异或组间分化。
回到数据口径解释:
热力图展示的是聚合后的结构结果。解释模式时,应同步关注聚合方式(均值/中位数/加权均值)与样本量差异,以保证结论可追溯。
基础 · 代码实战
本节使用 gapminder
构建一张简单的热力图,用于在同一张图中同时观察时间维度与区域分组的长期变化:以
年份(year)
为横轴、大洲(continent)
为纵轴,并用颜色编码各洲在各年份的人口加权平均预期寿命
lifeExp_w。
在读图时,颜色由浅到深对应预期寿命由低到高,从而更直观地识别洲际水平差异与随时间演进的整体趋势。
代码逻辑:
Step 1 按
continent × year 聚合并计算人口加权均值
对每个“大洲—年份”组合汇总,得到人口加权平均预期寿命
lifeExp_w = Σ(lifeExp × pop) / Σ(pop)(人口加权均值),用于更贴近洲级“总体水平”。
Step 2
定义热力图的坐标与颜色编码
将 year
映射到 x、continent 映射到
y,并设置 fill = lifeExp_w
作为连续颜色标度(浅=低,深=高)。
Step 3
绘制网格单元并显示每一年刻度
用
geom_tile() 绘制规则网格;同时用
scale_x_continuous(breaks = ...)
强制显示所有年份刻度(必要时配合旋转避免重叠)。
Step 4
设定顺序色盘与图形信息
使用
scale_fill_distiller(palette = "Blues", direction = 1) 指定
RColorBrewer 顺序色盘(低值更浅,高值更深),并补充标题、轴标签等必要文本信息以支持读图。
# ==============================================================================
# 基础:热力图(continent × year → pop-weighted lifeExp)
# 目标:用颜色编码展示“时间×大洲”的长期模式(深=高,浅=低)
# ==============================================================================
library(tidyverse)
library(gapminder)
library(RColorBrewer)
# ------------------------------------------------------------------------------
# 1) 数据聚合:按 continent × year 计算人口加权平均预期寿命(lifeExp_w)
# - 加权口径:让人口规模更大的国家对洲级均值贡献更大
# ------------------------------------------------------------------------------
heat_df <- gapminder %>%
group_by(continent, year) %>%
summarise(
lifeExp_w = sum(lifeExp * pop, na.rm = TRUE) / sum(pop, na.rm = TRUE),
.groups = "drop"
)
# ------------------------------------------------------------------------------
# 2) 绘图:热力图主体使用 geom_tile()
# - x = year(连续年份轴),y = continent(分类轴)
# - fill = lifeExp_w(连续色标:浅→深表示低→高)
# ------------------------------------------------------------------------------
ggplot(heat_df, aes(x = year, y = continent, fill = lifeExp_w)) +
geom_tile() +
# x 轴:强制显示每一年刻度(年份多时需旋转以避免重叠)
scale_x_continuous(breaks = sort(unique(heat_df$year))) +
# 连续色标:RColorBrewer 顺序色盘(Blues)
# direction = 1:低=浅,高=深(符合“数值越高越深”的直觉编码)
scale_fill_distiller(
palette = "Blues",
direction = 1,
name = "LifeExp"
) +
# 文本信息:标题与轴标签(保持简洁,突出“模式”)
labs(
title = "Life expectancy heatmap (pop-weighted)",
subtitle = "Continent × year; colour encodes pop-weighted life expectancy",
x = "Year",
y = "Continent"
) +
# 主题:最小化背景干扰;旋转年份标签以保证可读性
theme_minimal() +
theme(
axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1)
)热力图的核心语义是“位置=类别/时间,颜色=数值强度”。
阅读时建议按三步走:
① 看纵向(同一大洲):
沿着某一行从左到右观察颜色变化,可判断该洲预期寿命是否持续上升、是否出现阶段性加速/放缓。
② 看横向(同一年):
固定某一年比较各行的颜色深浅,可识别不同大洲在该年份的水平分层(谁更高/谁更低)。
③ 看“梯度”与“对比”:
若整体颜色随时间逐渐变深,通常提示全球普遍改善;若某些大洲长期偏浅或变化更慢,则提示区域差异与不均衡仍然显著。
解释口径:图中数值为人口加权平均(pop-weighted mean),因此大人口国家对洲/全球水平的贡献更大,更接近“总体平均”的统计含义。
进阶 · 代码实战
基础热力图更偏向“洲 × 年”的总体趋势,会把洲内差异平均掉。本节将视角下沉到国家—年份尺度,并用相对指数刻画“各国在每一年相对世界平均水平的偏离程度”。指数口径为:
\[ \text{Index}_{c,t}=\frac{\text{lifeExp}_{c,t}}{\overline{\text{lifeExp}}_{world,t}}\times 100 \]
其中 Index = 100 表示该国与当年世界平均一致。
代码逻辑:
Step 1
构造世界基准:按年计算人口加权世界均值
先对
gapminder 按 year
分组,计算世界人口加权平均预期寿命 world_lifeExp_w:
world_lifeExp_w = Σ(lifeExp × pop) / Σ(pop)
该序列作为每一年统一的对照基准(避免小国对世界均值产生不成比例影响)。
Step 2
生成国家指数:按年对齐基准并计算 Index(World=100)
将 world_year 通过
left_join(..., by = "year")
并入国家层数据,使每个国家—年份记录都能匹配同年的世界基准;随后计算:
index = lifeExp / world_lifeExp_w * 100
从而得到“同年可比”的相对水平指标。
Step 3 确定 y 轴顺序:同洲相邻 +
洲内稳定排序
为保证读图时“同洲国家聚集”,先按
continent × country 汇总跨年平均指数
index_mean,再按
arrange(continent, desc(index_mean))
生成 country_order;最后将 country
转为按该顺序的因子。
这一策略用“跨年均值”作为排序依据,减少国家名次随年份波动造成的视觉跳动。
Step 4
固定图例关键刻度:保留多个刻度并强制包含 100
使用
pretty(index, n = 5) 生成连续色标的主刻度,并将
100 合并进刻度集合:
brks <- sort(unique(c(pretty(...), 100)))
确保图例中始终出现 “World=100”
的基准刻度,同时保留其他数值刻度用于判读梯度。
Step 5 绘制热力图主体:tile
编码指数 + 分面聚合洲块
用 geom_tile()
将每个国家—年份网格单元填充为 index;
再用
facet_grid(continent ~ ., scales = "free_y", space = "free_y")
将国家按大洲分块显示,使同洲国家自然聚合且各洲块高度自适应。
Step 6 颜色编码:发散色带以 100
为中点突出“偏离”
使用
scale_fill_gradient2(midpoint = 100) 建立发散色标:
- < 100:低于世界平均(低端色)
- ≈ 100:接近世界平均(白色)
- > 100:高于世界平均(高端色)
并将 breaks = brks 写入色标,使图例既包含基准
100,也保留判读所需的多刻度。
# ==============================================================================
# 进阶:Country × Year 相对指数热力图(World=100)
# 目标:比较“各国随时间相对世界平均的偏离程度”
# 口径:Index = lifeExp / world(pop-weighted) × 100;100 表示当年世界平均
# ==============================================================================
library(tidyverse)
library(gapminder)
library(showtext)
# 字体:统一中文渲染
font_add_google(name = "Noto Sans SC", family = "my_font")
showtext_auto()
options(scipen = 9999)
# 1) 世界基准:按年计算人口加权世界平均 lifeExp
world_year <- gapminder %>%
group_by(year) %>%
summarise(
world_lifeExp_w = sum(lifeExp * pop, na.rm = TRUE) / sum(pop, na.rm = TRUE),
.groups = "drop"
)
# 2) 国家指数:对齐同年世界基准并计算指数(World=100)
heat_country <- gapminder %>%
left_join(world_year, by = "year") %>%
mutate(index = lifeExp / world_lifeExp_w * 100)
# 3) y 轴排序:同洲相邻;洲内按跨年平均指数降序(提升整体稳定性)
country_order <- heat_country %>%
group_by(continent, country) %>%
summarise(index_mean = mean(index, na.rm = TRUE), .groups = "drop") %>%
arrange(continent, desc(index_mean)) %>%
pull(country)
heat_country <- heat_country %>%
mutate(
country = factor(country, levels = country_order),
continent = factor(continent)
)
# 4) 图例刻度:自动生成主刻度,并强制包含 100(世界基准线)
brks <- sort(unique(c(pretty(heat_country$index, n = 5), 100)))
# 5) 出图:热力图 + 洲分面
ggplot(heat_country, aes(x = year, y = country, fill = index)) +
geom_tile() +
scale_x_continuous(breaks = sort(unique(heat_country$year))) +
scale_fill_gradient2(
low = "darkred",
mid = "white",
high = "darkgreen",
midpoint = 100,
breaks = brks,
name = "Index\n(World=100)"
) +
facet_grid(continent ~ ., scales = "free_y", space = "free_y") +
labs(
title = "Life expectancy index heatmap (World=100)",
subtitle = "Country × year; colours show deviation from world average within each year",
x = "Year",
y = NULL
) +
theme_minimal(base_size = 12) +
theme(
# 字体统一
plot.title = element_text(family = "my_font", face = "bold", size = 14, hjust = 0),
plot.subtitle = element_text(family = "my_font", size = 9, color = "grey50",
face = "italic", margin = margin(b = 12)),
axis.text = element_text(family = "my_font", size = 10),
strip.text = element_text(family = "my_font", face = "bold", size = 10),
# 版面细节
axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1),
axis.text.y = element_text(size = 6),
strip.background = element_rect(fill = "grey95", colour = NA),
panel.grid = element_blank(),
legend.position = "right",
legend.text = element_text(family = "my_font", size = 9),
legend.title = element_text(family = "my_font", size = 10, face = "bold")
)© 华东师范大学 社会发展学院 人口研究所 | DAWN 研究组 | yzliu@soci.ecnu.edu.cn
课程负责人:刘贇喆 本章作者:刘贇喆 | 蒋娴静
最后更新:2026年03月19日 构建环境:R version 4.5.2 (2025-10-31)
重点
学术语境下的数据可视化更强调准确性与可解释性,其根本评价标准并非在“图是否好看” (当然,在规范的基础上进行美化会很加分),而在于图形是否能够以清晰且可核查的方式表达数据结构与比较关系。
即使图形在视觉上精致,若存在尺度误导、变量含义不清或配色语义错误等问题,也会削弱证据表达的可信度,并增加读者理解成本。
因此,本节整理一套可操作的制图规范框架,用于在每次完成图形后进行系统性核查 (暂不涉及下一章的地图制图规范)
目标 将“规范优先”作为出图的第一原则,在保证表达准确与比较有效的基础上,再讨论版式优化与视觉美化。
Figure 3.7. 数据要与图形匹配
ylim /
xlim,避免“视觉比例”替代真实差异。0 起、另一图从 50
起),这会显著放大或压缩差异,造成误读。free_y /
free_x;一旦使用,应明确标注(例如“各面板坐标范围不同”)。凡涉及“比较”,首先必须保证尺度可比。
坐标起点、范围与刻度间距一旦不一致,读者的判断容易被视觉比例牵引,从而偏离真实差异。
下一节详细介绍
按数据类型匹配色带:
连续变量(continuous):
使用顺序色带(由浅入深)编码“由小到大”的数值梯度,通常遵循深色 = 更大值的直觉规则。
例如
Blues:浅蓝表示较低水平,深蓝表示较高水平。
相对偏离/指数(diverging):
使用发散色带(颜色由中心向两端扩散并加深),并将基准值设为浅色中点(常用白色或浅色),向两侧分别表示“低于基准”与“高于基准”的方向性偏离。
例如:深蓝 — 白 — 深红。
关键 图例中需显示基准刻度与中点数值(如 0、100)
分类变量(categorical):
使用离散色板(类别之间应清晰可区分)。
当图形同时需要表达“组间对比 + 组内构成”时,可采用分组同色系策略:
- 不同“大组”用不同色系区分;
- 同一“大组”内部的子类别用同色系的明暗/饱和度变化表示(提示隶属关系,同时保持可区分)。
Figure 3.8. 数据类型与配色参考
多通道编码:不要只靠颜色传达信息:
- 在投影展示、灰度打印或存在色觉差异的场景下,可叠加其他视觉通道增强区分度,例如不同点形(散点图)、不同线型(折线图)或纹理/图案填充(柱状图/面积图),从而提升图形的稳健性与可读性。
Figure 3.9. 可视化的一致性与稳健性
组别/变量过多时,优先使用分面(facet):
当同一坐标系叠加过多组别(多条折线/多类散点/多组分布)时,图形容易退化为“线团/点团”。
更稳健的做法是用 facet_wrap() / facet_grid()
分层展示,使单幅子图的信息密度可控。
控制遮挡(overlap):
出现明显重叠时,可先用
alpha、点大小、减少标签(只标关键点)缓解;
对高密度散点可改用二维分箱或密度表达(如二维计数热力图/密度等高线)。
当目标是“比较”,组图通常优于堆叠:
若关注组间差异而非构成结构,可用共享尺度的分面或并排子图提升可比性(例如按性别分面)。
堆叠图的适用边界需明确:
堆叠柱适合表达“总量 +
构成”;但若需要比较子类别在不同主类别间的大小,通常更适合改用分组柱状图或分面(避免堆叠基线变化带来的误读)。
Figure 3.10. 可视化策略
轴标签写清楚:
轴标签应同时说明变量含义、单位与尺度处理(如对数/标准化/指数)。
log10(GDP per capita, USD) 明确指出对数变换与计量单位,避免读者误以为是线性尺度。保持刻度与范围可比性:
分面或组图用于对比时,优先保持共享的 xlim/ylim
与刻度规则,确保视觉比例一致。
若必须使用自由尺度(如
free_y/free_x),应在副标题或图注中明确提示“尺度不一致”,避免跨图误读。
术语标注要“可读”,避免缩写/编码直出:
代码变量名、缩写与分组编码(如
lifeExp_w、NO2、CI、Q5、A1)属于计算口径或内部标识,图中应转换为读者可理解的标签(必要时补充单位/分组规则)。
lifeExp_w → 人口加权平均预期寿命NO2 → 二氧化氮浓度CI Q5 → Carstairs
指数:最高贫困五分位BFL_den_cat1 → 棕地密度分组字体体系统一:
图内文字(标题、坐标轴、刻度、图例、标注)
应尽量保持字体、字号与颜色一致(同一论文/同一章节尤其如此)。
Figure 3.11. 坐标文本规范
图片标题与标注:重要
在确保图片有注释/说明的情况下 (如 Figure. x
‘…’),在最终可视化的成果上可以去掉 标题 和
副标题,保持图片台头整洁。不需要重复下方注释文字在标题中
投稿时常用
控制视觉噪声:
弱化非必要的背景元素(如过强网格线、复杂底纹),避免装饰性
3D/阴影等效果干扰数值读数与结构判断。
一图聚焦核心任务:
单幅图形宜服务于
1–2 个主要目的(趋势、对比、结构或分布);当信息维度过多,应优先使用分面或组图分层表达,避免“所有信息挤在一张图里”。
复杂度不等于信息增量:
图形设计应以降低理解成本为准则;若读者需要反复对照图例、坐标或注释才能读懂,则应考虑简化编码或拆分表达。
标注“少而准”:
仅标注关键点位(极值、拐点、阈值、显著差异);避免对每个点/每个类别都加标签造成遮挡与阅读负担。
图例与编码保持最小化:
只保留必要图例项;若图例与坐标轴信息重复(例如 x
轴已写明类别),可移除或合并,以减少认知切换。
留白用于组织,而非浪费:
通过合理边距与对齐提升层次感(标题—图体—图例分区清晰);避免图例挤压图体或信息贴边导致“读图压迫感”。
思考
图形与数据匹配:
图形类型与数据类型一致(不制造趋势/差异等错误暗示)
图的可比性:涉及对比时,坐标范围与刻度保持一致;若使用自由尺度,已明确提示(图注/副标题)
可视化布局策略:
组别/变量过多时优先
facet 或组图(减少重叠与遮挡)
配色选择:
色带类型与数据一致(顺序/发散/离散),且语义稳定(如深=高;基准=浅色中点)
颜色一致性:
同一变量/同一类别跨图配色保持一致(避免“同名换色”)
轴与单位:
轴标签写清变量含义 + 单位 +
变换(如 log10)
变量可读化:
缩写/编码/代码变量名已转为可读标签或在图注解释{.note}
图例与标注:
图例清晰且不遮挡;文字标注不过载(仅保留关键信息)
信息密度:
合理控制视觉噪声;单图聚焦 1–2
个核心任务
在通过“规范自查”之后,还需对照目标期刊/出版社的制图要求进行最终校对。常见检查项包括:
分辨率(DPI)、图幅尺寸与版面比例{.note}、字体与字号一致性{.note}、线宽/点大小可读性{.note}、颜色模式(RGB/CMYK)以及文件格式(矢量/位图:PDF/EPS/SVG 或 TIFF/PNG)。
建议遵循“先保证表达正确,再保证格式合规”的顺序,避免在技术细节上产生不必要的返工。
© 华东师范大学 社会发展学院 人口研究所 | DAWN 研究组 | yzliu@soci.ecnu.edu.cn
课程负责人:刘贇喆 本章作者:刘贇喆 | 蒋娴静
最后更新:2026年03月19日 构建环境:R version 4.5.2 (2025-10-31)
前一节已强调:学术图形的首要评价标准是准确与可解释。因此,所有“美化”都应以规范出图为前提。
在满足规范的基础上,适度的“美化”属于额外加分项:它能够降低读图成本、强化信息层次,并提升版面观感。
(本节聚焦“非地图类可视化”的美化策略)
在大多数数据驱动研究中,高质量的可视化不仅传递研究结果,也传递“研究打磨程度”。
当规范前提已满足时,版式更克制、配色更稳健、层次更清楚的图形,往往更容易获得读者与评审的注意,并提升整体呈现的专业感。
提升呈现的专业度:稳定、克制的配色能减少“随意感”,并降低高饱和原色带来的视觉刺激与误导风险。
(一般不建议大面积使用纯红/纯绿等高饱和颜色;应优先选择可读性更稳健的色板)
强化信息结构:配色不仅用于区分,也用于表达层级与方向。合理的明度/冷暖安排可突出重点,帮助读者更快抓住主结论。
降低认知负荷:直观的颜色编码能缩短“识别分组 → 对比差异 → 形成判断”的阅读路径。
保证跨图一致性:同一研究中配色体系稳定,有助于跨图对照与记忆,减少读者反复适应的成本。
顺序色带:用于连续变量。
颜色随数值单调变化(常见为浅 → 深),并尽量遵循深色 = 更大值的方向约定。
(不建议使用“彩虹色带”,其亮度跳跃容易制造伪边界,干扰梯度判断)
发散色带:用于存在基准的数据(如差值、指数、相对偏离)。
基准值设为浅色中点(常用白/浅灰),两侧用不同色相表示低于/高于基准。
(常见形式:深蓝 — 白 —
深红;图例应明确标出中点刻度,如 0 或 100)
离散色板:用于分类变量。
类别之间需可区分,同时避免某一颜色过于“抢眼”导致视觉权重失衡。
(当存在“大组 + 子组”结构,可采用“组间不同色相 +
组内同色系明暗”以提示隶属关系)
RColorBrewer
推荐访问viridis
推荐访问viridis色盘与名称
ggsci
推荐访问# install.packages('ggsci')
library("ggsci")
library("ggplot2")
library("gridExtra")
p1 <- example_scatterplot()
p2 <- example_barplot()
p1_npg <- p1 + scale_color_npg()
p2_npg <- p2 + scale_fill_npg()
grid.arrange(p1_npg, p2_npg, ncol = 2) # 展示 NPG 系列配色cols4all
推荐访问# install.packages('cols4all')
library(cols4all)
# cols4all 是一个集大成的配色包,整合了几乎所有主流色盘,并提供严格的色盲友好度评估。
# 强烈推荐:在本地 RStudio 运行下方代码,将唤出非常强大的交互式色彩仪表盘!
# c4a_gui()
# cols4all 的色盘名称采用“系列名.色盘名”的命名格式。
# 我们使用 "tableau.tableau10" 来准确调用 Tableau 经典 10 色:
pal_colors <- c4a("tableau.orange_gold", n = 10)
c4a_plot(pal_colors) # 预览该色盘rcartocolor
推荐访问colorspace
推荐访问核心原则 语义正确 → 稳健可读 → 简洁克制 → 风格统一 课外参考资料:
- https://corytophanes.github.io/BIO_BIT_Bioinformatics_209/graphics-and-colors.html - https://www.math.pku.edu.cn/teachers/lidf/docs/Rbook/html/_Rbook/graph.html —
配色并不一定需要从零开始“设计”。影视作品、海报设计与成熟的视觉系统往往已经形成了稳定的配色结构:以主色奠定基调,以辅色补足层次,再用少量强调色引导视线。对科研制图而言,这类“成熟方案”的价值主要在于帮助建立审美直觉:如何控制饱和度、如何组织冷暖关系、如何用少量对比制造重点,从而让图形在保持克制的前提下更有层次与质感。
下面给出若干影视作品的配色示例,用于观察其色彩搭配与氛围组织方式:
Figure 3.12. 配色灵感1
Figure 3.13. 配色灵感2
Figure 3.14. 配色灵感3
Figure 3.15. 配色灵感4
提醒 影视/艺术配色主要用于“审美训练与搭配借鉴”。
这类配色未必天然符合科研制图规范:例如对比度不足、色觉不友好、灰度不可辨、或与数据语义不匹配等。因此,更合适的做法是:先从作品中抽取“主—辅—强调”的组织方式与色彩关系,再回到科研语境中用语义匹配与可读性检验进行二次筛选与调整。
在 R 中,将“配色方案”真正应用到图形通常有三条路径:
(1)直接输入颜色值(最通用);
(2)从调色板函数取色(如
RColorBrewer / ggsci / viridis 等);
(3)将颜色绑定到类别(固定映射,保证跨图一致),避免“同名类别换色”造成误读。
R
中最常见的是使用十六进制颜色:#RRGGBB
也可使用 #RRGGBBAA 表达透明度(AA
为透明度通道)。
#1f78b4(蓝)、#e31a1c(红)scales::alpha()
调整:alpha("#1f78b4", 0.25)(范围
0–1)(实务建议:手动 hex 更适合“强调色 / 参考线 / 少量元素”;当类别较多时,通常更建议使用调色板函数批量取色。)
library(tidyverse)
library(gapminder)
library(scales)
df <- gapminder %>% filter(year == 2007)
base_col <- "#1f78b4"
ggplot(df, aes(x = continent)) +
geom_bar(fill = alpha(base_col, 0.70)) +
theme_minimal()当图中包含多个类别时,更稳健的做法是:
先从调色板取一组颜色向量,再用
scale_*_manual() 将颜色应用到分类变量(最常见的是
colour / fill){.note}
RColorBrewer:顺序 /
发散 / 离散色板RColorBrewer
提供经过广泛使用的配色方案,适合科研图形中的三类需求:
- 顺序色带:连续变量(由浅入深表示由小到大)
- 发散色带:存在基准值(中心浅色,两端分别表示低于/高于基准)
- 离散色板:分类变量(类别区分清晰)
library(tidyverse)
library(gapminder)
library(RColorBrewer)
# 提取 2007 年度全球跨国截面数据
df <- gapminder %>% filter(year == 2007)
# 采用 Set2 离散色板,并将颜色与因子层级(Factor Levels)显式绑定,以确保跨图表视觉映射的一致性
cols <- brewer.pal(n = 5, name = "Set2")
names(cols) <- levels(df$continent)
# 构建统计分布箱线图:实施手动填充映射,并优化离群值(Outliers)与透明度以增强数据可读性
ggplot(df, aes(x = continent, y = gdpPercap, fill = continent)) +
geom_boxplot(width = 0.7, alpha = 0.85, outlier.alpha = 0.35) +
scale_fill_manual(values = cols, name = "大洲") +
labs(
title = "2007 年全球各区域人均 GDP 分布特征",
x = NULL, y = "人均 GDP (USD)"
) +
theme_minimal() # 采用极简主题风格,聚焦分布结构主体ggsci:期刊风格离散色板
ggsci::pal_xxx()一般返回“调色板函数”,再用 (n) 取n个颜色
library(tidyverse)
library(gapminder)
library(ggsci)
# 提取 2007 年度全球跨国截面数据,并同步初始化因子能级 (Factor Levels)
df <- gapminder %>%
filter(year == 2007) %>%
mutate(continent = factor(continent))
# 确定分类变量的能级总数 (k),为色盘提取提供参数依据
k <- nlevels(df$continent)
# 调用 ggsci 提供的 Nature Publishing Group (NPG) 学术色盘函数
# 注:根据需求可切换为 pal_aaas() (Science) 或 pal_lancet() (Lancet) 等规范色盘
pal_fun <- ggsci::pal_npg("nrc")
pal <- pal_fun(k)
# 实施『颜色—类别』显式绑定 (Explicit Binding),确保多图表呈现时美学特征的绝对一致性
names(pal) <- levels(df$continent)
# 构建频数分布柱状图,并通过手动标度 (Manual Scale) 映射学术级色盘
ggplot(df, aes(x = continent, fill = continent)) +
geom_bar() +
scale_fill_manual(values = pal, name = "Continent") +
theme_minimal() # 采用极简主题,移除视觉冗余以聚焦核心统计信息当从网站(如 Coolors / Adobe Color 等)获得一组 hex 颜色时,可直接将其复制为一个向量,并绑定到类别名。
library(tidyverse)
library(gapminder)
# 提取 2007 年度全球跨国截面数据
df <- gapminder %>% filter(year == 2007)
# 预定义十六进制颜色向量,并与大洲类别显式映射,确保视觉编码在全局范围内的一致性
my_cols <- c(
Africa = "#4C78A8",
Americas = "#F58518",
Asia = "#54A24B",
Europe = "#E45756",
Oceania = "#72B7B2"
)
# 构建频数分布柱状图:实施填充 (fill) 与边框 (colour) 的手动映射
# 设置 guide = "none" 以移除冗余的颜色图例,优化图形的信息密度
ggplot(df, aes(x = continent, fill = continent, colour = continent)) +
geom_bar(linewidth = 0.4) +
scale_fill_manual(values = my_cols, name = "Continent") +
scale_colour_manual(values = my_cols, guide = "none") +
theme_minimal() # 应用极简主义风格,聚焦于统计主体分布© 华东师范大学 社会发展学院 人口研究所 | DAWN 研究组 | yzliu@soci.ecnu.edu.cn
课程负责人:刘贇喆 本章作者:刘贇喆 | 蒋娴静
最后更新:2026年03月19日 构建环境:R version 4.5.2 (2025-10-31)
实战目标
本节练习使用 fivethirtyeight 包中的
hate_crimes 州级数据进行数据可视化分析:
(1)以
gini_index 与 avg_hatecrimes_per_100k_fbi
绘制相关性散点图(线性回归趋势 +
方程 + 相关系数与 p 值);
(2)以 share_vote_trump 将州划分为“红/蓝州”,比较
median_house_inc 的分布差异,绘制小提琴 + 箱线 +
抖动点的复合分布图,并进行 t 检验(两组均值差异)。
将巩固以下能力:
-
数据处理:缺失值处理、分组变量派生(高亮/分组标签)。
-
图层组织:趋势层、点/标注层与统计注释层的叠加与遮挡控制。
- 统计呈现:回归注释(方程、相关性)与组间检验注释(t
检验)的一体化表达。
数据来源:
fivethirtyeight::hate_crimes(州级汇总数据)。
本节练习在去除缺失值后开展分析(drop_na())。
fivethirtyeight 与
hate_crimes
fivethirtyeight 是一个用于教学与复现实证分析的 R
数据包,整理并发布了新闻机构 FiveThirtyEight
报道中使用的公开数据片段(便于练习数据清洗、可视化与统计建模)。hate_crimes 为其中的州级数据集,包含与
仇恨犯罪水平、收入与不平等、以及
2016 年总统选举投票结构相关的变量(不同变量用于不同图形任务)。研究问题:
研究问题1|相关性表达:各州
贫富差距(gini_index,基尼系数) 与
仇恨犯罪率(avg_hatecrimes_per_100k_fbi;每 10
万人) 是否呈现线性相关?
本练习在散点图中叠加线性回归拟合,并以 DC 作为示例对象进行高亮标注(单独上色 + 加粗标签)。
研究问题2|组间差异(分布对比):按 2016
年总统大选结果将州划分为两组(share_vote_trump > 0.5 记为 “Trump
Won”,否则为 “Clinton Won”),两组的
家庭收入中位数(median_house_inc) 是否存在差异?
本练习使用“小提琴 + 箱线 + 抖动点”的组合呈现分布,并给出 t 检验的 p 值注释。
这是一个“照着做”的练习:先复现图形,再逐行理解每一层图形与每一个统计注释的含义。
请在你的 R Project 中新建一个 R Markdown 文档:
Lab3_ggplot_vis.Rmdfivethirtyeight、tidyverse、ggpubr、showtext(及
scales)(未安装请先安装)hate_crimes 数据集(data("hate_crimes")),并进行缺失值处理(drop_na())gini_index ×
avg_hatecrimes_per_100k_fbi,叠加回归线与相关性注释share_vote_trump
构造分组,对比 median_house_inc 的分布并给出 t
检验注释html 输出,并检查图形的基本规范(轴含义/单位、尺度、字体、配色与标注)注意
从第三章开始,练习不再提供完整的
.Rmd成品模板。
练习要求是在自建的 R Markdown 文档中自行插入代码块并运行(可参考前一章的写法)
如需新建代码块,可直接复制下方“代码框模板”。
```{r, message=FALSE, warning=FALSE, echo=TRUE}
... 在这里写 r 语言代码 ...
```
研究问题1
library(fivethirtyeight) # 注意提前安装
library(tidyverse)
library(ggpubr)
library(showtext)
# 提示:在 rmd 中可以通过全局设置读取这些包
# -----------------------------
# 1. 字体设置
# -----------------------------
font_add_google(name = "Noto Sans SC", family = "my_font")
showtext_auto()
# -----------------------------
# 2. 数据准备与处理
# -----------------------------
data("hate_crimes")
df_clean <- hate_crimes %>% drop_na()
# 创建一列用来标记 "是否为DC",用于后续单独上色
df_clean <- df_clean %>%
mutate(highlight = if_else(state_abbrev == "DC", "DC", "Normal"))
# -----------------------------
# 3. 计算回归模型 (获取精确系数)
# -----------------------------
model <- lm(avg_hatecrimes_per_100k_fbi ~ gini_index, data = df_clean)
intercept <- coef(model)[1]
slope <- coef(model)[2]
# 使用 sprintf 自动处理正负号,保留2位小数
# 格式含义:%.2f (2位小数), %+.2f (强制显示正负号)
equation_text <- sprintf("y = %.2fx %+.2f", slope, intercept)
# -----------------------------
# 4. 绘图
# -----------------------------
ggplot(df_clean, aes(x = gini_index, y = avg_hatecrimes_per_100k_fbi)) +
# --- 趋势线层 (放在最底层,不遮挡文字) ---
geom_smooth(method = "lm",
linetype = "dashed", # 回归线 虚线
color = "firebrick", # 回归线 砖红色
fill = "#ffcccc", # 回归线 浅粉色
alpha = 0.5, # 透明度 50%
se = TRUE) +
# --- 文字散点层 ---
geom_text(aes(label = state_abbrev,
color = highlight, # 根据是否为DC变色
fontface = ifelse(highlight == "DC", "bold", "plain")), # DC加粗
size = 3.5,
check_overlap = FALSE) + # 允许重叠以显示所有州
# --- 统计指标层 ---
# 1. 手动添加方程 (使用计算好的 equation_text)
annotate("text", x = 0.433, y = 8.5, # 调整位置到左上空白处
label = equation_text,
color = "firebrick", family = "my_font", size = 4.5, fontface = "bold") +
# 2. 自动添加 R 和 P值
stat_cor(aes(label = paste(after_stat(r.label), after_stat(p.label), sep = "~`,`~")),
label.x = 0.42, label.y = 7.5, # 调整位置到左上空白处
color = "firebrick",
digits = 2,
size = 4.5) +
# --- 装饰与标度 ---
# 手动设置颜色:DC用紫色,普通州用深灰;比如研究中要重点讨论 DC
scale_color_manual(values = c("DC" = "purple", "Normal" = "#2c3e50")) +
scale_x_continuous(limits = c(0.415, 0.54), breaks = seq(0.41, 0.54, 0.02)) +
labs(title = "美国各州贫富差距与仇恨犯罪的关系",
subtitle = "数据来源:FiveThirtyEight (2016) | 红色虚线为线性回归拟合",
x = "基尼系数 (Gini Index)",
y = "每10万人仇恨犯罪数 (FBI)") +
# --- 主题设置 ---
theme_classic() +
theme(
# 字体设置
text = element_text(family = "my_font"),
plot.title = element_text(face = "bold", size = 16),
plot.subtitle = element_text(size = 10, color = "grey50"),
axis.title = element_text(face = "bold"),
# 增加网格线
panel.grid.major = element_line(color = "grey90", linetype = "dotted"),
panel.grid.minor = element_blank(),
# 去掉图例
legend.position = "none"
)研究问题2
library(fivethirtyeight)
library(tidyverse)
library(ggpubr)
library(showtext)
library(scales)
# ------------------------------------------------------------------------------
# 1. 绘图环境初始化:全局字体渲染配置
# ------------------------------------------------------------------------------
font_add_google(name = "Noto Sans SC", family = "my_font")
showtext_auto()
# ------------------------------------------------------------------------------
# 2. 数据预处理:截面数据清洗与逻辑分组映射
# ------------------------------------------------------------------------------
data("hate_crimes")
# 剔除缺失观测值,并基于 2016 年大选投票份额构建二分名义变量
df_pol <- hate_crimes %>%
drop_na() %>%
mutate(
pol_group = if_else(share_vote_trump > 0.5,
"Trump Won (Red)",
"Clinton Won (Blue)")
)
# ------------------------------------------------------------------------------
# 3. 统计图形构建:分层美学映射与假设检验
# ------------------------------------------------------------------------------
ggplot(df_pol, aes(x = pol_group, y = median_house_inc, fill = pol_group)) +
# A. 分布形态层:通过小提琴图呈现连续变量的核密度估计 (KDE)
geom_violin(alpha = 0.2, color = NA, trim = FALSE) +
# B. 统计概括层:嵌套箱线图以展示中位数、四分位区间及统计分布中心
# 压缩宽度以实现与小提琴图的嵌套平衡,隐藏离群值避免与后续散点层冲突
geom_boxplot(width = 0.15, color = "#2c3e50", outlier.shape = NA, alpha = 0.8) +
# C. 原始观测层:引入抖动散点以揭示底层数据颗粒度与异质性
geom_jitter(width = 0.05, size = 1.2, color = "#2c3e50", alpha = 0.5) +
# D. 假设检验层:执行独立双样本 T 检验,量化组间差异的显著性水平
stat_compare_means(method = "t.test",
aes(label = paste0("T-test, p = ", ..p.format..)),
label.x = 1.5,
label.y = 83000,
family = "my_font", size = 4.5) +
# E. 美学标度:基于政治语义的手动配色方案
scale_fill_manual(
name = "2016 Election Winner",
values = c("Trump Won (Red)" = "#C0392B",
"Clinton Won (Blue)" = "#2980B9"),
labels = c("Clinton (Democrat)", "Trump (Republican)")
) +
# F. 坐标轴格式化:对 Y 轴执行货币化转换,增强数值传达的直观性
scale_y_continuous(labels = scales::dollar_format(scale = 1/1000, suffix = "k"),
breaks = seq(40000, 80000, 10000)) +
# G. 标签层:定义学术级标题与描述性说明
labs(title = "政治极化与经济实力:红蓝州的家庭收入对比分析",
subtitle = "统计显示:民主党获胜州 (Blue States) 的家庭收入中位数显著高于共和党获胜州 (Red States)",
x = "2016年美国总统大选获胜方 (按州统计)",
y = "家庭中位数收入 (2016年)") +
# H. 主题定制:优化视觉密度与信息层级
theme_classic() +
theme(
text = element_text(family = "my_font"),
plot.title = element_text(face = "bold", size = 16),
plot.subtitle = element_text(size = 10, color = "grey50", margin = margin(b = 10)),
# 坐标轴与文字微调:强化标题辨识度
axis.title.x = element_text(face = "bold", size = 11, margin = margin(t = 10)),
axis.title.y = element_text(face = "bold", size = 11),
axis.text = element_text(size = 10, color = "#2c3e50"),
# 网格线配置:保留辅助性纵向/横向参考线以辅助跨组数值比对
panel.grid.major.y = element_line(color = "grey95", linetype = "dotted"),
panel.grid.major.x = element_line(color = "grey95", linetype = "dotted"),
# 图例排版:采用顶部右对齐布局,提升绘图核心区域的空间利用率
legend.position = "top",
legend.justification = "right",
legend.title = element_text(face = "bold", size = 10),
legend.background = element_rect(fill = "transparent")
)进阶练习
本节不提供参考代码。任务目标是:使用 gapminder
的时序数据,独立完成一组可用于报告/论文的“研究级可视化”,重点训练
分布 × 时间 × 分组 的综合表达能力。
数据集:gapminder(国家 ×
年份的面板数据)
核心变量(建议):
- 分布变量:lifeExp 或 gdpPercap
- 分组变量:continent
- 时间变量:year
研究问题:
- 不同大洲在 1952–2007 年间,某项指标(lifeExp 或
gdpPercap)的分布形态如何随时间演变?
-
分布的中位数水平、离散程度(IQR)与离群值是否呈现系统性变化?
任务 A|时序箱线图
绘制“按年份排列的箱线图序列”:
-
横轴:year(建议转换为因子,以确保每一年都有一个箱线图)
- 纵轴:lifeExp 或
gdpPercap
- 分面:按 continent
分面(facet_wrap(~ continent, ncol = 1) 或
facet_grid(continent ~ .))
-
要求:每一个年份对应一个箱线图;每个箱线图内部包含该大洲当年所有国家的观测值。
任务 B|配色与美化
- 选择合理配色方案并说明理由:
- 若使用离散配色(按大洲),需保持跨图一致。
- 若 year 为因子且颜色映射到
continent,应避免图例冗余(可视情况关闭图例)。
-
处理可读性问题:年份多、刻度密集时,需通过字号、角度、留白或抽样刻度提升可读性。
- 图形需包含:标题、简洁副标题、轴含义与单位说明(如有变换需标注)。
knit 输出为
HTML。lifeExp 或
gdpPercap;为何采用该分面方式;配色为何合理。year
若保持数值型,箱线图会在连续轴上挤在一起;将其转为因子通常更稳健。gdpPercap,可考虑对数变换以缓解长尾分布(变换必须在轴标签中说明)。输出参考
© 华东师范大学 社会发展学院 人口研究所 | DAWN 研究组 | yzliu@soci.ecnu.edu.cn
课程负责人:刘贇喆 本章作者:刘贇喆 | 蒋娴静
最后更新:2026年03月19日 构建环境:R version 4.5.2 (2025-10-31)