章节概述

本章以 ggplot2 为核心框架,系统梳理数据可视化的基础机制:通过映射几何对象将变量编码为图形要素,并在此基础上掌握常用标度图例主题设置,形成可复用的图形构建流程(grammar of graphics)

在实践层面,本章围绕 9 类高频统计图形构建可迁移的“图形工具箱”:散点图折线图柱状图直方图雷达图箱线图流向图山峦图热力图。各图形均结合典型应用场景,提供可直接复现的绘图语法与简要的读图要点,用于支持对分布差异关系结构路径的分析表达。

在方法与规范层面,本章归纳学术制图的关键要求,包括图形—数据匹配可比性与尺度一致单位与标注完整性、以及图例与版式管理等;并在规范基础上讨论配色与视觉层级优化,用于提升图形的可读性与发表适配性。

本章学习内容

学习导航

本章学习内容

欢迎开始 第 3 章 的学习。请点击 下方导航卡 进入相应小节:

💡 提示:学习完一个小节后,请再次点击 屏幕右下角的章节主页按钮回到本导航页

1.0 可视化导论

1.0 数据可视化导论

基本概念

数据可视化 (Data Visualisation) 是指将抽象的数据按照明确的编码规则转换为可读的图形表达:通过位置、长度、颜色、形状等视觉通道来映射变量取值,从而呈现数据的分布、结构与关系。

在学术与实证研究语境中,可视化不仅是结果呈现的手段,更是认知数据的核心工具。它强调可解释性证据力:读者应能依据图形元素准确还原变量含义、测量尺度与研究口径,而非仅仅被视觉效果所吸引。

可视化的本质是将数值信息压缩为视觉模式,以提升信息传达。


1.1 为什么要进行数据可视化?

在量化研究的全流程中,数据可视化通常承担着三个不可替代的核心职能:

1. 探索与诊断 (Exploration & Diagnosis)

在正式建立统计模型前,可视化是检验数据质量与揭示潜在特征的首要步骤。单纯的汇总统计量 (如均值、方差、相关系数) 往往会掩盖数据的真实结构 (如经典的安斯库姆四重奏,Anscombe’s quartet),而图形能够直观且客观地暴露数据的全貌。

  • 分布形态检验
    直观评估数据是否符合特定分布 (如正态分布),以及是否存在偏态 (Skewness) 或多峰 (Multimodal) 结构。
  • 异常与噪声识别
    快速定位数据记录中的离群值 (Outliers)、异常的缺失值聚集或潜在的底层编码错误。
  • 潜在模式发现
    捕捉变量间复杂的非线性关联、异方差性规律或样本的自然聚类特征。

经典案例:安斯库姆四重奏 (Anscombe’s quartet)

“数值计算是精确的,而图形展示是粗略的。”

1973年,统计学家 Frank Anscombe 针对当时学术界这一普遍存在的偏见,构建了四组特殊的数据集进行反驳。他指出:

若脱离了可视化审视,单纯依赖统计指标往往会得出具有误导性的结论

基础统计摘要的维度审视,这四组数据几乎不可区分

  • 相同的均值 \((\bar{x} = 9, \bar{y} = 7.5)\)
  • 相同的方差 \((\sigma_x^2 = 11, \sigma_y^2 \approx 4.125)\)
  • 相同的相关系数 \((r \approx 0.816)\)
  • 甚至拥有相同的线性回归方程 \((y = 3 + 0.5x)\)

然而,当我们引入可视化视角时,数据的真实结构差异便一目了然:

Figure 3.1. 经典:安斯库姆四重奏

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. 数据可视化 - 解释与推断

Figure 3.2. 数据可视化 - 解释与推断

3. 沟通与传播 (Communication & Dissemination)

在学术出版与会议报告中,高质量的图表是提升信息传递效率的核心媒介。优秀的可视化能够有效降低读者的认知负荷 (Cognitive Load),将高度抽象的统计模型推断转化为直观且具说服力的实证研究证据,进而增强学术论证的清晰度与整体影响力。

Figure 3.3. 数据可视化 - 沟通与传播

Figure 3.3. 数据可视化 - 沟通与传播


1.2 学术制图的基本原则

重点

学术图表不同于商业报表或艺术设计,其首要目标是遵循“信、达、雅”的表达规范,以确保视觉呈现准确高效不具误导性

1. 诚实原则 (Integrity) —— “信”

图形必须忠实反映数据特征,严禁通过视觉偏差扭曲客观事实。

  • 坐标基准规范
    条形图的纵轴通常要从 0 开始,慎用截断坐标轴 (Truncated Axis),以防人为夸大数值差异。
  • 呈现不确定性
    点估计 (如均值) 无法反映数据全貌。在学术制图中,通常需附带误差棒、置信区间或原始数据分布,以明确统计推断的精确度
  • 比较尺度统一
    进行多组数据对比时,应保持坐标轴范围 (Scale) 一致,确保跨图表间具有科学的可比性。

2. 自明原则 (Self-Explanatory) —— “达”

图表应具备独立表意的能力,确保读者即使脱离正文,也能准确获取核心信息。

  • 要素完备
    标题、坐标轴标签、单位与图例需完整且清晰。若数据经过特定转换 (如对数变换,Log Scale),必须在轴标签或图注中明确说明。
  • 直观引导
    建议善用文本注释 (Annotation) 直接标示关键数据点或趋势转折,从而降低读者在图例与图形间反复对照的认知负担。
  • 信息普适性
    需兼顾灰度打印效果与色盲友好性 (Color-blind Safe),确保视觉信息能准确传达给各类受众。

3. 极简原则 (Simplicity) —— “雅”

学术图表的美学建立在克制与高效之上,其核心是提升 Edward Tufte 提出的数据-墨水比 (Data-Ink Ratio)

核心理念:图形中的每一滴墨水都应服务于数据表达

  • 剔除图表垃圾
    摒弃无信息量的 3D 特效、阴影、繁杂背景或装饰性元素 (Chartjunk)
  • 优化信噪比
    最大化信号 (数据规律) 的展现,弱化或移除噪音 (如过粗的网格线、冗余的刻度线)
  • 色彩克制
    色彩应用于传递数据映射或区分变量,而非纯粹的视觉装饰;应避免滥用高饱和度的“彩虹色谱”。

1.3 工具引入:为何选择 ggplot2

在 R 语言的生态中,Base R (基础绘图系统) 虽然功能完备,但其采用的是指令式绘图逻辑,即类似“纸笔作画”,不同图形间的语法一致性较弱,且后期修改底层元素较为繁琐。为建立可迁移可复用的可视化范式,本教程将采用 ggplot2 作为核心制图工具。

ggplot2 的理论内核源自 Leland Wilkinson 的《图形语法》 (The Grammar of Graphics)。该包打破了传统“画点、画线”的机械操作,将数据可视化抽象为一套可组合的图层构建系统。它要求使用者明确数据 (Data)、美学映射 (Aesthetics)、几何对象 (Geometries) 与统计变换 (Statistics) 之间的逻辑关系,进而逐层构建图表。

在学术制图中使用 ggplot2 的核心优势体现在:

  • 语法统一
    无论是基础的散点图、条形图,还是复杂的多面板统计图,均遵循同一套底层语法,极大地降低了学习成本并提升了代码的可扩展性
  • 图层叠加
    以图层 (Layer) 为基本运算单元,支持将坐标系、几何图形、注释等元素逐一叠加 (通过 + 符号连接),使得图形的精细微调与迭代优化更加直观。
  • 高度定制
    内置成熟的标度 (Scale) 与主题 (Theme) 系统,能够精确控制坐标轴、图例、字体及色彩,轻松输出符合学术期刊规范的印刷级图表
  • 生态融合
    作为 tidyverse 的核心组件之一,ggplot2dplyrtidyr 等数据处理工具无缝衔接。在实际工作流中,通常仅需执行 library(tidyverse) 即可完成加载,实现从数据清洗到可视化的流畅闭环。
Figure 3.4. ggplot2 可视化工具与优势

Figure 3.4. ggplot2 可视化工具与优势

ggplot2 绘图能力预览

ggplot2 的强大之处在于其通用性。无论是基础统计图,还是复杂的地理空间图或多维分面图,遵循的都是同一套图层语法。以下是使用该包绘制的一些典型案例:

Figure 3.5. ggplot2 多样化绘图示例Figure 3.5. ggplot2 多样化绘图示例Figure 3.5. ggplot2 多样化绘图示例Figure 3.5. ggplot2 多样化绘图示例

Figure 3.5. ggplot2 多样化绘图示例

2.0 ggplot2 语法框架

2.0 ggplot2图形语法

工具基础

ggplot2 是 R 语言中最具影响力的可视化工具包,其命名源自统计学家 Leland Wilkinson 的经典著作 《图形语法》 (The Grammar of Graphics)。与 Excel 或 Base R 这种“预设图表类型” (如:选择‘柱状图’或‘饼图’) 的绘图逻辑不同,ggplot2 采用了一套构建式的逻辑 (见Fig3.6)

它认为任何统计图形都可以拆解为一组独立的组件 (Components)数据坐标系几何对象统计变换。用户通过将这些组件像“图层”一样叠加,即可构建出无限可能的图形组合。

Figure 3.6. ggplot2 基本语法示意图

Figure 3.6. ggplot2 基本语法示意图

在开始绘制之前,请确保已正确加载了核心工具包:

环境检查:加载 Tidyverse

注意

ggplot2tidyverse 生态系统的核心组件。在开始本章练习前,请确认当前 R 会话中已加载该包:

# 推荐:一次性加载 tidyverse 全家桶(包含 ggplot2)
library(tidyverse)

# 或者:仅加载 ggplot2
# library(ggplot2)

2.1 ggplot2 核心逻辑

理解 ggplot2 的关键在于掌握其“分层”(Layered)语法结构。

一张统计图形,本质上是由若干核心要素通过加号 + 逐层叠加而成。

Fig. 3.6 所示,一个标准的绘图语句通常包含以下三个必须要素

要素1 数据层(Data)

“画什么?” 绘图的基础必须是一个数据框(data frame / tibble)

ggplot2 通常不以“单独向量”为输入,而是要求数据以整洁数据(Tidy Data)形式组织:每一列是一个变量,每一行是一个观测。


要素2 映射层(Aesthetics Mapping)

“怎么画?” 建立数据变量视觉属性之间的对应关系。

这种对应关系称为映射(mapping),通常写在 aes() 中:

  • x / y:将哪一列映射到 x 轴与 y 轴?
  • color / fill:类别或数值差异如何通过颜色区分(线色 vs 填充色)
  • size / shape:变量差异如何通过点大小或形状体现?
  • alpha:用透明度表达密度或强调/弱化某些观测(常用于点重叠)
  • group:当绘制折线/平滑线时,用于明确“按哪一组连线/拟合”(常见于多组时间序列)


要素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():画误差棒(不确定性表达)

2.2 简单案例:使用 ggplot2gapminder

基础 · 代码实战

为练习 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, ...)

    • 指定绘图数据来源;gdpPercaplifeExpcontinent 均直接读取自该数据框。
  • 2. 映射层 (Mapping)aes(...)

    • 定义变量视觉属性的映射关系:
    • gdpPercap \(\rightarrow\) x (横坐标)
    • lifeExp \(\rightarrow\) y (纵坐标)
    • continent \(\rightarrow\) color (颜色)
  • 3. 几何层 (Geom)geom_point()

    • 指定以“”的形式呈现上述映射后的数据关系(即绘制散点图)

补充:管道写法(把 data 放在外面)

tidyverse 语境中,data部分 可以放到 ggplot() 外侧,用 %>% 把数据“送入”绘图语句,使流程更连贯:

gapminder %>% 
  filter(year == 2007) %>%
  ggplot(aes(x = gdpPercap, y = lifeExp, color = continent)) +
  geom_point()

2.3 ggplot2 实战进阶

前置准备:解决中文乱码问题

重要步骤

默认情况下,ggplot2 在渲染中文标题或标签时可能会出现乱码(显示为方框 □□,这是因为系统默认字体不支持中文字符。

为了确保图表中的中文能正常显示,我们需要引入 showtext 包。它可以帮助 R 更好地调用系统字体或加载网络字体。

请在运行绘图代码前,务必先运行以下配置代码:

# --- 1. 安装字体扩展包 (仅需运行一次) ---
# install.packages("showtext")

# --- 2. 加载并配置 (每次启动 RStudio 都需运行) ---
library(showtext)

# 自动开启字体支持
showtext_auto()

进阶 · 代码实战

为进一步理解 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)
  )


2.4 语法拆解:从图层到细节

深度解析

上一节案例虽然代码较长,但核心仍是 ggplot2分层语法:先确定数据与全局映射,再按功能叠加图层,最后通过标度与主题完成出版级表达。本节按该案例的实际写法,拆解为四个关键维度。

1. 画布与全局映射

ggplot(data = data_2007, mapping = aes(x = gdpPercap, y = lifeExp))

在最顶层的 ggplot() 中,我们只定义 x/y 的全局映射,为后续图层提供共同的坐标框架。

  • 继承机制:该映射是全局的,后续 geom_smooth()geom_point() 默认继承 xy,无需重复声明。
  • 数据准备:绘图前先用 filter(year == 2007) 构造截面数据,避免把不同年份混在同一张图里造成解释偏差。

2. 图层的堆叠顺序

ggplot2 按代码顺序绘制:先写在下,后写在上

  • 底层(Layer 1)geom_smooth(...)
    先画 LOESS 趋势线及其 95% 置信区间,使散点气泡覆盖在线条之上,避免线条穿插点形影响读图。

  • 顶层(Layer 2)geom_point(...)
    点层单独承担“主要信息编码”:

    • color = continent 用于大洲分组;
    • size = pop/10000 用于人口规模(单位换算为“万人”)。
      同时趋势线颜色被固定为 darkred,与点层的颜色映射相互隔离。
      (点随数据变,线保持统一样式)

3. 图例的定制

该案例的图例设计有两个目标:该出现的必须出现出现后的必须可读

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 图例重复呈现。

4. 标度与主题的精修

细节

  • 对数坐标(信息展开)scale_x_log10() 用于处理 GDP 的右偏分布,减少尺度压缩,使低—中收入区间的点位差异更可见。
  • 颜色标度(稳定可复用)scale_color_brewer(palette = "Set1") 提供一致的离散配色方案,便于跨图复现。
  • 大小标度(控制视觉范围)scale_size(range = c(2, 12), name = "人口规模 (万人)")
    统一气泡面积的可视范围,同时明确单位口径。
  • 字体与排版(出版一致性):通过 showtext + font_add_google() 注册并调用 family = "my_font",保证标题、轴标签与图例文本在不同系统下保持一致渲染;其余字号、加粗、边距等由 theme() 集中管理。

总结:本案例的三条操作准则

  1. 先全局、后局部:先在 ggplot() 里定义 x/y,再在各 geom_ 层补充该层独有的映射与设置。
  2. 映射 vs 设置:随数据变化写进 aes();固定样式写在 aes() 外(如回归线 color = "darkred")。
  3. 图例来自映射:图例由 scaleguides 驱动;想控制图例,优先改 scale_*(),再用 override.aes 提升可读性。

3.0 常用图形

3.0 常用图形出图

3.0 常用图形工具箱

图形导航

ggplot2 可视化图谱

“一图胜千言”。本章精选了学术与商业分析中最高频使用的 9 种统计图形。
点击下方卡片,快速跳转至对应图形的绘制语法、应用场景与美化技巧。

💡 学习建议:所有的几何对象 (geom_*) 都遵循相同的语法逻辑。
掌握前三种基础图形后,其余图形只需更换函数名即可融会贯通。

散点图
3.1 散点图:相关性

基础 · 图形概念

散点图(Scatter Plot)用于探索两个连续变量之间的关系。它将每条观测映射到二维坐标系中,通过点的空间分布呈现变量间的共变模式、分层结构与异常观测。

1. 核心用途

  • 相关性探索
    观察两变量是否呈现正相关、负相关或弱相关关系。
    (例如:人均 GDP 上升时,预期寿命是否同步上升?)

  • 结构识别
    判断数据是否存在自然分组或聚集现象。
    (Clusters)

  • 离群值发现
    识别远离主要分布区域的异常观测。
    (Outliers)

2. 核心函数

ggplot2 中,散点图对应的几何对象函数为:

geom_point()


基础 · 实战代码

本节继续使用 gapminder 数据集,并选取 2007 年 的截面数据,绘制 人均 GDP预期寿命 的散点图。每个点代表一个国家或地区,用于直观观察两变量之间的分布关系。


代码逻辑

Step 1 加载数据与绘图环境
导入 tidyversegapminder,用于数据筛选与 ggplot2 绘图。


Step 2 构建横截面样本(2007 年)
使用 filter(year == 2007) 提取单一年份数据,避免不同时期混合导致关系被“时间趋势”干扰。
(横截面图更适合做变量关系的直观初判)


Step 3 建立映射关系(x–y–分组颜色)
gdpPercap 映射到 xlifeExp 映射到 y,并将 continent 映射到 color
- xy 给出两个连续变量的二维位置关系;
- 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() # 采用极简设计原则,通过提升“数据墨水比”聚焦核心统计关联

图形解读:怎么读这张散点图?

读图方法

  • 看坐标含义
    横轴是人均 GDP,纵轴是预期寿命;每个代表一个国家或地区
  • 看点云走向
    点整体从左下向右上延展,说明两变量存在同向变化(说明:GDP 更高的国家往往寿命更长)
  • 看分布形态与异常点
    关注点云是否集中在某些区域、是否出现少量远离主体的点,这些通常提示结构差异或特殊国家或地区。

从图中可直接读出的主要信息

  • 总体格局
    国家层面的人均 GDP 与预期寿命呈明显正相关
  • “高收入区间”变化趋缓
    在图的右侧区域,点在纵向(寿命)上的提升幅度相对有限,体现出寿命随 GDP 增长的边际收益递减特征。
  • 离散度差异
    在中低 GDP 区间,寿命分布更分散;而在较高 GDP 区间,寿命更集中,国家间差异相对收敛。

进阶 · 实战代码

本节在同一张散点图中同时呈现变量关系拟合结果跨期对比:以 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()
  )


课外教程参考

折线图
3.2 折线图:时间趋势

基础 · 图形概念

折线图(Line Chart)用于呈现指标随时间有序序列变化的趋势。它通过连接相邻观测点,强调变化的方向节奏,适合表达长期上升/下降、阶段性波动与结构性拐点等动态特征。

1. 核心用途

  • 趋势刻画
    识别指标随时间演化的总体方向与增长/下降速率。
    (如:过去数十年预期寿命的变化轨迹)

  • 多组对比
    在同一时间轴上比较不同对象的变化幅度、速度与阶段差异。
    (如:不同国家的增长路径是否同步)

  • 拐点识别
    定位趋势发生明显转折或出现异常波动的时间点/阶段。
    (Turning points / shocks)

2. 核心函数

ggplot2 中,折线图对应的几何对象函数为:

geom_line()

关键逻辑:当图中包含多条线时,需要明确“哪些点属于同一条线”。最常见做法是将分组变量映射到 colour(同时完成分组);若不希望上色,也可显式设置 aes(group = ...)


基础 · 代码实战

本节使用 gapminder 数据,选取 China、United States、Japan、India 四个国家,绘制 1952—2007 年预期寿命的变化轨迹,用于对比不同国家的长期趋势与阶段性差异。


代码逻辑

Step 1 加载数据与绘图环境
导入 tidyversegapminder,确保数据处理与 ggplot2 绘图流程可用。


Step 2 筛选研究对象(四国样本)
使用 filter(country %in% ...) 仅保留四个国家的全时期记录,保证折线反映同一指标(lifeExp)的可比时间序列。


Step 3 建立映射关系(时间 × 指标 × 分组)
year 映射到 xlifeExp 映射到 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)
  )


图形解读:怎么读这张折线图?主要发现是什么?

怎么读

  • ① 起点—终点:看每条线在 1952 与 2007 的高度差,用于比较总体提升幅度
  • ② 斜率:线越陡,表示该阶段寿命提升越快;线越平,表示提升趋缓(边际改善减弱)
  • ③ 波动/拐点:若出现下探、停滞或斜率突变,通常提示该阶段存在异常冲击或结构性变化,需回到数据与背景解释。

据此读出的主要发现

  • 整体趋势:四国在 1952–2007 年间总体持续上升(长期健康水平改善)
  • 增幅对比:总体提升幅度存在明显差异:India 与 China 的提升更快(追赶型增长);Japan 与 United States 的增幅相对有限(高位区间更易“趋缓”)
  • 水平分层:Japan 全程处于高寿命水平;United States 也处于较高水平,但后期斜率更平(增长放缓)
  • 追赶与收敛:China 与 India 起点较低(早期水平偏低)但上升更快,至 2000 年后与高寿命国家差距缩小(收敛趋势)
  • 结构性变化:China 在 1950s 末至 1960s 初出现阶段性波动(短期回落/停滞),随后进入更稳定的上升通道;India 整体更平滑,呈持续累积式提升。


警惕“伪趋势”:折线图的禁区

重要

折线图的核心语义是连续变化:连线默认表示相邻点之间存在可解释的过渡关系(斜率具有实际含义)。当 x 轴不具备连续性或不代表同一对象的连续过程时,连线容易制造“伪趋势”,应避免使用。

1. 无序类别变量(Nominal Data)

  • 不推荐:x 轴为“中国、美国、日本”等类别。
  • 原因:类别之间没有连续过渡,连线会暗示不存在的变化方向与“中间状态”。
  • 替代方案:使用柱状图点图(dot plot)进行离散比较。


2. 离散区间 / 队列(Discrete Cohorts)

  • 不推荐:x 轴为年龄段(如 [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)(洲级人口加权均值)
- q25q75(洲内 25%/75% 分位数,用于 IQR 带)
随后用 geom_ribbon(ymin = q25, ymax = q75) 绘制阴影带,作为“洲内差异范围”的背景提示,并通过 show.legend = FALSE 避免图例冗余。


Step 3 合并绘图序列并区分 World / Continent(用于线型与线宽)
cont_yearworld_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)
  )


课外教程参考

柱状图
3.3 柱状图:类别比较

基础 · 图形概念

柱状图(Bar Chart) 是用于比较分类变量在某一数值指标上的差异的标准图形。它以柱子的长度作为主要编码通道,将数值大小映射为可直接比较的视觉差异,适用于跨类别对比、排序展示与结构呈现。


1. 核心用途

  • 数值比较
    对比不同类别的量级差异(如不同国家的人均 GDP)
  • 排名展示
    按数值排序,快速识别“Top N / Last N”(最高/最低组别)
  • 部分与整体
    使用堆叠柱状图展示总量的内部构成(composition)

2. 核心 ggplot geom

核心 ggplot geom:geom_col() / geom_bar()


3. 函数辨析

ggplot2 中,柱状图常用两类几何对象,选择取决于数值是否已在数据表中

  • geom_col()推荐
    当数据表中已经存在要展示的数值列(如 GDP、人口、寿命)时使用:
    柱高 = y 变量的实际数值

  • geom_bar()
    当数据为原始明细记录,需要对类别进行计数(频数)时使用:
    柱高 = 该类别出现次数


基础 · 代码实战

本节使用 gapminder 数据构建一个最小可复现的柱状图案例:聚焦 2007 年美洲(Americas)国家,筛选人均 GDPgdpPercap最高的 Top 10,并用水平柱状图呈现其排名差异。

  • 数据处理目标:限定 year == 2007continent == "Americas",并按 gdpPercap 取 Top 10(不含并列)
  • 图形表达目标:用水平柱状图展示“从高到低”的排序结构,便于快速扫描比较。

关键技巧:类别变量若不重排,通常会按名称字典序显示。为得到“排名视图”,需要按 gdpPercapcountry 进行重排reorder()forcats::fct_reorder()


代码逻辑

Step 1 筛选范围(年份 × 大洲)
限定 year == 2007continent == "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()
  )

图形解读:怎么读这张柱状图?

读图方法

  • 看坐标含义
    横轴是人均 GDP(美元),纵轴是国家/地区;每一条柱子对应一个国家,柱子的长度表示该国的人均 GDP 水平。
  • 看排序规则
    本图已按人均 GDP 降序排列,因此从上到下就是“Top 10”排名;阅读时可直接按顺序识别名次。
  • 看差距(gap)
    重点比较相邻柱子的长度差异:差距越大,说明经济水平分层越明显;差距较小,则表示该段国家之间更接近。

从图中可直接读出的主要信息

  • 头部集中
    美国与加拿大的柱长明显领先,处于该区域的人均 GDP 高位水平。
  • 断层与层级
    第二名之后出现明显“断层”(从加拿大到第三名柱长显著变短),提示高收入国家与其余国家存在清晰分层。
  • 中后段更接近
    第三名之后(如 Puerto Rico、Trinidad and Tobago、Chile 等)柱长差异相对缩小,说明这一梯队内部差距较有限。

堆叠柱状图:总量与结构

进阶 · 图形概念

1. 图形定义

标准柱状图强调“类别—数值”的单一对比。堆叠柱状图(Stacked Bar Chart)在同一根柱子内叠加多个子类别,使图形在一个坐标系中同时表达总量结构两类信息:

  • 柱子总高度:表示该主类别的总量(Total Magnitude)
  • 内部色块:表示总量的构成成分(Composition),色块高度对应各子类别的贡献大小。

使用要点:堆叠柱状图适合“看总量 + 看组成”;但若目标是精确比较不同主类别间的子类别大小,需要谨慎解读(堆叠基线不同会影响比较)


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
随后拆分得到 top1top2 两个清单,用于后续堆叠顺序与颜色映射。


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()
  )


课外教程参考

直方图
3.4 直方图:频数分布

基础 · 图形概念

直方图(Histogram) 用于刻画单个连续变量的分布形态。它将连续取值按给定组距划分为若干区间(Bins),并统计每个区间内的观测频数,从而以“柱形轮廓”近似呈现数据的整体结构与集中趋势。

1. 核心用途

  • 分布诊断
    识别近似正态、左偏/右偏(Skewness)、长尾(Heavy tail)或多峰(Multimodal)等形态特征。
  • 集中区间识别
    定位样本主要聚集的取值范围(主体区间),辅助选择描述性统计与阈值划分。
  • 极端值线索
    观察分布两端是否出现稀疏“孤立柱”(潜在离群/极端观测)

2. 易混淆辨析:直方图 vs 柱状图

  • 直方图geom_histogram()
    横轴为连续数值(如年龄、工资、lifeExp);柱体通常相邻无间隙,强调区间划分连续性
  • 柱状图geom_col() / geom_bar()
    横轴为离散类别(如国家、性别、行业);柱体通常有间隙,强调类别之间相互独立

3. 核心函数

ggplot2 中,直方图对应的几何对象函数为:

geom_histogram()


基础 · 代码实战

2007 年各国的 预期寿命 lifeExp(gapminder) 为例,使用直方图对连续变量进行分箱统计,用于快速识别预期寿命的集中区间、整体偏态特征以及是否存在长尾/极端值


代码逻辑

Step 1 筛选截面数据(固定分析年份)
gapminder 中选取 year == 2007 的观测,得到单一年份的截面数据 data_hist,用于展示该年的 lifeExp 分布形态。


Step 2 建立映射关系(连续变量 → x 轴)
ggplot() 中将连续变量 lifeExp 映射到 xaes(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),并将输出的 xy 组织为 dens_df(tibble)
该步骤的目的是把“连续分布轮廓”显式转为可绘制的数据框,便于后续用 geom_area()geom_line() 叠加到同一图层体系中。


Step 3 参考统计量:均值/中位数 → ref_df
data_hist 上计算 mean_lifeExpmedian_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()
  )


课外教程参考

雷达图
3.5 雷达图:多维对比与结构画像

基础 · 图形概念

雷达图(Radar Chart) 用于呈现多个变量在同一对象上的取值结构,并支持在同一坐标框架下对比多个对象的多维表现。它将各变量映射到从中心辐射的角向轴,并以闭合的线或多边形连接各维度取值,从而形成可直观识别的“结构画像”。

1. 核心用途

  • 多维对比
    比较不同对象在多个指标维度上的相对水平差异。
    (例如:不同国家在预期寿命、收入、人口等指标上的综合表现)

  • 结构画像
    识别对象的优势维度与短板维度,并观察维度间的均衡性。
    (Profile / Pattern)

  • 综合评估呈现
    将多指标评分结果以同一图形呈现,便于跨对象的整体对照。
    (Performance / Index-based assessment)

2. 核心函数

在本章中,我们采用 fmsb 包绘制雷达图。其实现基于多边形连线同心网格刻度,并提供对边框、填充与网格的直接控制。

fmsb::radarchart()

关于 fmsb:安装与使用

本章雷达图的核心函数为:

fmsb::radarchart()
(需先安装并加载 fmsb 包)

fmsb 可通过 CRAN 直接安装:

install.packages("fmsb")
library(fmsb)
  • 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)"))

图形解读:怎么读这张雷达图?

读图方法

  • 看维度含义
    五条辐射轴分别对应油耗、马力、加速、重量、传动;每个顶点表示该车型在该维度的取值位置。
  • 看尺度口径
    本图使用 min–max 标准化(基于 mtcars 全样本),环形刻度可理解为相对位置:越靠外表示在该维度上越接近样本最大值,越靠内表示越接近样本最小值。
  • 看形状与“短板”
    多边形越“外扩”表示整体更偏高;凹陷最明显的轴通常是该车型的相对短板维度。
  • 注意解释方向
    本例直接对原始值做缩放,未对“越小越好”的指标做反向处理(如 wtqsec;因此读图时应按变量本身含义解读,而非一律将“越外越好”。

从图中可直接读出的主要信息(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 两行以固定尺度),并用 pcolpfcol 同时展示轮廓与填充,以便逐车阅读且保持严格可比性。

# ==============================================================================
# 进阶:多车同图对比 + 单车小多图(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)
}


课外教程参考

箱线图
3.6 箱线图:离群值与分布

基础概念

箱线图(Boxplot),又称“箱须图”,是统计学中用于展示数据分布特征的常用图形,尤其适合同时呈现离散程度离群值信息。它基于“五数概括法”(Five-number summary),用少量图形元素概括一组数据的整体结构。

1. 适用场景

  • 多组分布对比
    当需要比较多个类别(如不同大洲)的分布差异时,箱线图比多张直方图更节省空间且更易横向比较。
  • 离群值检测
    快速定位远离主体分布的异常观测点(Outliers),为后续核查数据质量或解释极端个案提供线索。

2. 如何阅读箱线图?

将数据视为已按数值大小排序,箱线图主要呈现以下关键位置:

  • 箱体(Box)
    表示数据的中间 50%,范围为第 25% 分位数(\(Q_1\)) 到第 75% 分位数 (\(Q_3\))箱体越高,说明中间一半数据的波动越大
  • 中位线(Median)
    箱体中的粗线,表示中位数 \(Q_2\)(50% 分位数)
  • 胡须(Whiskers)
    从箱体上下延伸的线段,表示“非异常范围”的边界;常用规则为 \([Q_1-1.5\times IQR,\; Q_3+1.5\times IQR]\)(IQR 为四分位距)
  • 离群点(Outliers)
    落在胡须之外的点,提示其数值相对整体分布较为极端(可能是异常,也可能是具有解释价值的极端个案)

基础 · 代码实战

使用 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 环境与字体:统一中文显示口径
加载 tidyversegapminder,并用 showtext 注册 Noto Sans SCfamily = "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(连续变量);同时将 fillcolour 都映射为 continent,使填充与轮廓保持同色系,增强小提琴边界的辨识度。


Step 4 几何层:密度轮廓 + 中位数线(读图锚点)
使用 geom_violin() 绘制密度轮廓,并通过关键参数控制形态与可读性:
- trim = TRUE:将密度裁剪到观测范围内(不向外延展)
- draw_quantiles = 0.5:在小提琴内部绘制中位数线,提供组内中心位置的参照;
- alphalinewidth:平衡填充强度与轮廓清晰度,减少遮挡。


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)参与分布估计。
  • 统计原因
    小提琴图依赖核密度估计(KDE)与平滑。当样本量极少时,密度曲线不稳定,形状容易被平滑算法“撑开”,导致看起来像一条平直的宽带。

警示
当每组样本量很小(例如 < 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 轴已写明大洲;避免重复图例
  )


流向图
3.7 流向图:路径与流量

基础概念

流向图(Sankey Diagram / Alluvial Plot)用于展示多个分类变量之间的路径结构流量分配。图中每一列为一个阶段(axis),每个矩形为一个类别节点(stratum);连接节点的“流”(alluvium/flow)宽度编码数量权重,便于在同一图中同时观察组成分流汇聚

1. 适用场景

  • 路径追踪
    观察个体或样本如何从初始类别,经过若干阶段,流向最终结果
    (如:分层 → 分组 → 结局)
  • 结构对比
    对比不同起点类别在后续阶段的分流差异
    (谁更集中,谁更分散)
  • 状态转换
    展示状态/标签在多阶段下的变迁
    (如:前-中-后期的类别迁移)

2. 如何阅读流向图?

  • 节点宽度(Stratum size)
    表示该阶段某类别的总量。
  • 流线宽度(Flow width)
    表示从上一步某类别流向下一步某类别的数量或权重。
  • 分叉与汇聚(Split/Merge)
    分叉表示内部异质性(同一类别流向多个后续类别);汇聚表示不同来源在后续阶段汇合。
  • 颜色映射
    常按起点类别着色,便于追踪同一来源在各阶段的走向。

3. 核心工具与函数

ggplot2 体系下常用 ggalluvial 绘制流向图,其核心几何对象为:

geom_alluvium()(绘制流线)
geom_stratum()(绘制节点)
geom_text(stat = "stratum")(节点标签)


基础 · 代码实战

使用内置的 Titanic 数据,展示“舱位等级 → 性别 → 是否幸存”的路径结构(以人数为权重)

延伸阅读: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 = Classaxis2 = Sexaxis3 = 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 的节点明显更宽,说明总体上遇难人数多于幸存人数。
  • 性别差异:从 SexSurvived 的流线对比可见,Female → Yes 的流线相对更粗,而 Male → No 更突出,反映出明显的性别差异。
  • 舱位差异:按颜色追踪可见,1st 流向 Yes 的比例相对更高;3rdCrew 流向 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 = Classaxis2 = Sexaxis3 = 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()
  )

课外教程参考

山峦图
3.8 山峦图:脊线与层叠

基础概念

山峦图(Ridgeline Plot),又称“脊线图”,是一种用于展示多个分类组别下连续变量数据分布的图形。它通过将多个核密度估计曲线(KDE)在垂直方向上错位重叠排列,形似连绵起伏的山脉,从而在有限的垂直空间内高效对比多组数据的分布形态演变趋势

1. 适用场景

  • 类别间的形态对比
    在垂直空间受限时,比较多个类别(如不同大洲、不同班级)的密度分布。相比于将所有密度曲线画在同一水平基线上造成的严重遮挡,山峦图的错位层叠设计更具可读性。
  • 多组分布的时间演变
    展示某一连续指标随时间(如不同年份、月份)推移的分布变化趋势,山峦的“整体推移”能直观反映趋势的演进。
  • 模式与异常识别
    快速识别各组分布的峰值特征(单峰/双峰)、偏度(左偏/右偏)以及长尾特征。

2. 如何阅读山峦图?

将图形视为多条独立核密度曲线的垂直排布,主要呈现以下关键特征:

  • 山峰位置(Peaks)
    对应数据分布的众数或最集中的取值区间。山峰越靠右,说明该组在 x 轴上的整体水平越高。
  • 山体形态(Shape/Spread)
    反映数据的离散程度。山体越宽平,说明数据波动越大;越陡峭,说明数据越集中。
  • 重叠区域(Overlap)
    相邻曲线的重叠部分展示了组间数据取值的交集。重叠越少,说明组间差异越显著。

3. 核心工具与函数

绘制山峦图需要配合第三方扩展包 ggridges 使用,其核心几何对象函数为: > geom_density_ridges() > 注:需要提前安装并读取 ggridges


基础 · 代码实战

使用 gapminder 2007 年数据绘制基础山峦图,用于直观比较各大洲预期寿命的密度分布轮廓


代码逻辑

Step 1 准备数据与绘图环境(加载扩展包)
除了 tidyversegapminder,需额外加载 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)

    • 陡峭且集中:数据变异性小,组内差异小(即方差小,发展较为均衡)
    • 宽平或多峰(Multimodal):数据内部异质性强,提示组内可能存在明显的分层或极化现象。

从图中可直接读出的主要发现 (Findings):

  • Europe(欧洲):分布最为集中且山峰最靠右,呈典型的单峰结构。表明该大洲国家间预期寿命普遍较高,且内部差异极小。
  • Africa(非洲):整体分布最靠左,且山体极为宽阔(甚至隐约可见双峰)。表明其预期寿命总体偏低,且洲内国家间的健康水平存在极大的不均衡。
  • Asia(亚洲):呈现明显的“多峰”与“长左尾”特征,跨度极大。提示亚洲内部存在强烈的异质性:既有接近欧洲的高寿命国家群体,也存在部分健康水平极低的极端值国家。

补充说明
图中排除了 Oceania(大洋洲)。在 gapminder 截面数据中该洲仅有 2 个国家,样本量极小。强行对其拟合密度曲线不仅缺乏统计意义,更会导致山体形态被平滑算法严重扭曲。这提醒我们:任何密度分布图的绘制都必须建立在充足的样本支持之上


进阶 · 代码实战

为更细致地呈现数据的分布特征跨时空演化,本节将基础山峦图升级为“多色阶地毯式山峦图”(Gradient Ridgeline with Rugs)。针对 ggplot2 默认单一图层仅能承载一种填充色阶的底层限制,本代码引入 ggnewscale 包实现多重标度映射:为各大洲分配独立的渐变色盘(如亚洲为紫、欧洲为蓝)。同时,图表融合了基于分位数的中位数标注与底层的地毯图(Rug Plot),实现了宏观趋势与微观颗粒度的同步可视化。


代码逻辑

Step 1 数据清洗与因子优化(跨时代截面)
加载 tidyverseggridges 及核心扩展包 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)
  )

课外教程参考

热力图
3.9 热力图:模式识别与结构对比

基础概念

热力图(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 映射到 xcontinent 映射到 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)
  )

图形解读:如何读这张热力图(Heatmap)?

热力图的核心语义是“位置=类别/时间,颜色=数值强度”

阅读时建议按三步走:

  • ① 看纵向(同一大洲)
    沿着某一行从左到右观察颜色变化,可判断该洲预期寿命是否持续上升、是否出现阶段性加速/放缓

  • ② 看横向(同一年)
    固定某一年比较各行的颜色深浅,可识别不同大洲在该年份的水平分层(谁更高/谁更低)

  • ③ 看“梯度”与“对比”
    若整体颜色随时间逐渐变深,通常提示全球普遍改善;若某些大洲长期偏浅或变化更慢,则提示区域差异与不均衡仍然显著。


解释口径:图中数值为人口加权平均(pop-weighted mean),因此大人口国家对洲/全球水平的贡献更大,更接近“总体平均”的统计含义。


进阶 · 代码实战

基础热力图更偏向“洲 × 年”的总体趋势,会把洲内差异平均掉。本节将视角下沉到国家—年份尺度,并用相对指数刻画“各国在每一年相对世界平均水平的偏离程度”。指数口径为:

\[ \text{Index}_{c,t}=\frac{\text{lifeExp}_{c,t}}{\overline{\text{lifeExp}}_{world,t}}\times 100 \]

其中 Index = 100 表示该国与当年世界平均一致。


代码逻辑

Step 1 构造世界基准:按年计算人口加权世界均值
先对 gapminderyear 分组,计算世界人口加权平均预期寿命 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")
  )

4.0 学术可视化规范

4.0 学术制图规范

重点

学术语境下的数据可视化更强调准确性可解释性,其根本评价标准并非在“图是否好看” (当然,在规范的基础上进行美化会很加分),而在于图形是否能够以清晰可核查的方式表达数据结构与比较关系。

即使图形在视觉上精致,若存在尺度误导变量含义不清配色语义错误等问题,也会削弱证据表达的可信度,并增加读者理解成本。

因此,本节整理一套可操作的制图规范框架,用于在每次完成图形后进行系统性核查 (暂不涉及下一章的地图制图规范)

目标 将“规范优先”作为出图的第一原则,在保证表达准确比较有效的基础上,再讨论版式优化与视觉美化。


语义匹配:图形类型必须“对得上数据”

  • 数据类型需与图形编码一致
    • 连续 × 连续:
      散点图 / 折线图(横轴需具备自然顺序,如时间) / 二维密度图(样本量较大时更稳健)
    • 类别 × 连续:
      柱状图(均值/总量/比例) / 箱线图(分布与离群) / 小提琴图(密度轮廓) / 条带图(抖动散点;直接呈现观测点)
    • 类别 × 类别:
      列联表可视化(比例热力图/马赛克图) / 分组(或堆叠)柱状图(比例对比) / 网络图(关系型数据;后续章节将介绍)
    • 单变量分布:
      直方图(频数/密度) / 核密度曲线(连续轮廓) / QQ 图(正态性检验)


  • 常见“错误用法”需避免
    • 用折线图连接无序类别会制造“趋势”幻觉
    • 用面积堆叠图比较子类别的精确大小差异需谨慎(堆叠基线不一致,跨组不易对齐)
    • 用柱状图表达连续变量的分布形态并直接把柱高解读为“密度”(直方图需明确分箱与统计口径)
    • 将存在缺失/中断的时间序列用折线强行连通,可能暗示不存在的连续变化(应断线或标注缺测)
Figure 3.7. 数据要与图形匹配

Figure 3.7. 数据要与图形匹配


可比性:尺度一致

  • 坐标尺度需一致(尤其是多图/多面板对比)
    • 当比较的是同一指标(或同一单位/同一口径)时,应尽量保持相同 ylim / xlim,避免“视觉比例”替代真实差异。
    • 尤其需要避免不同图使用不同起点或截断范围(例如一图从 0 起、另一图从 50 起),这会显著放大或压缩差异,造成误读。
  • 分面与组图的尺度策略
    • 若结论依赖“跨组比较”,优先采用共享坐标尺度(常见默认分面策略),使各面板可直接对齐。
    • 仅在强调“组内形态”而非“组间水平”时才考虑 free_y / free_x;一旦使用,应明确标注(例如“各面板坐标范围不同”)
  • 时间维度的对齐
    • 同一时间轴上的刻度与间隔应一致;避免在不同图中更改 break 规则,导致读者无法对齐关键年份与阶段。

对比图的“红线”

凡涉及“比较”,首先必须保证尺度可比
坐标起点、范围与刻度间距一旦不一致,读者的判断容易被视觉比例牵引,从而偏离真实差异。


颜色规范:色带选择

下一节详细介绍

  • 按数据类型匹配色带

    • 连续变量(continuous)
      使用顺序色带(由浅入深)编码“由小到大”的数值梯度,通常遵循深色 = 更大值的直觉规则。
      例如 Blues:浅蓝表示较低水平,深蓝表示较高水平

    • 相对偏离/指数(diverging)
      使用发散色带(颜色由中心向两端扩散并加深),并将基准值设为浅色中点(常用白色或浅色),向两侧分别表示“低于基准”与“高于基准”的方向性偏离。
      例如:深蓝 — 白 — 深红

      关键 图例中需显示基准刻度与中点数值(如 0、100)

    • 分类变量(categorical)
      使用离散色板(类别之间应清晰可区分)

      当图形同时需要表达“组间对比 + 组内构成”时,可采用分组同色系策略:
      • 不同“大组”用不同色系区分;
      • 同一“大组”内部的子类别用同色系的明暗/饱和度变化表示(提示隶属关系,同时保持可区分)
Figure 3.8. 数据类型与配色参考

Figure 3.8. 数据类型与配色参考


  • 保持颜色语义一致
    • 同一变量在不同图中应避免出现“深浅含义反转”(例如某图深色=高值、另一图深色=低值),以免读者在跨图对比时产生系统性误读。
    • 连续变量,应在整篇研究内遵循一致的“方向约定”:通常采用浅色=低值、深色=高值(即使更换不同色系,也不改变深浅含义)
    • 分类变量,同一类别在不同图中应尽量保持同一颜色(例如某一组别在图 1 为紫色,则在图 2 仍为紫色);若必须更换色板,应确保读者可通过图例快速确认映射,避免“同名类别换色”造成误读。
    • 当存在“分组中的分组”(如大组=大洲,子组=国家)时,可采用同色系分层:大组使用同一色相锚定,子组用明暗/饱和度区分,以同时维持组间可辨识组内一致性


多通道编码:不要只靠颜色传达信息
- 在投影展示、灰度打印或存在色觉差异的场景下,可叠加其他视觉通道增强区分度,例如不同点形(散点图)、不同线型(折线图)纹理/图案填充(柱状图/面积图),从而提升图形的稳健性与可读性。

Figure 3.9. 可视化的一致性与稳健性

Figure 3.9. 可视化的一致性与稳健性


布局策略:减少堆叠与遮挡,优先“分层表达”

  • 组别/变量过多时,优先使用分面(facet)
    当同一坐标系叠加过多组别(多条折线/多类散点/多组分布)时,图形容易退化为“线团/点团”
    更稳健的做法是用 facet_wrap() / facet_grid() 分层展示,使单幅子图的信息密度可控。

  • 控制遮挡(overlap)
    出现明显重叠时,可先用 alpha、点大小、减少标签(只标关键点)缓解;
    对高密度散点可改用二维分箱或密度表达(如二维计数热力图/密度等高线)

  • 当目标是“比较”,组图通常优于堆叠
    若关注组间差异而非构成结构,可用共享尺度的分面或并排子图提升可比性(例如按性别分面)

  • 堆叠图的适用边界需明确
    堆叠柱适合表达“总量 + 构成”;但若需要比较子类别在不同主类别间的大小,通常更适合改用分组柱状图分面(避免堆叠基线变化带来的误读)

Figure 3.10. 可视化策略

Figure 3.10. 可视化策略


坐标与文本规范

  • 轴标签写清楚
    轴标签应同时说明变量含义单位尺度处理(如对数/标准化/指数)

    • 例:log10(GDP per capita, USD) 明确指出对数变换与计量单位,避免读者误以为是线性尺度。
  • 保持刻度与范围可比性
    分面或组图用于对比时,优先保持共享的 xlim/ylim 与刻度规则,确保视觉比例一致。
    若必须使用自由尺度(如 free_y/free_x,应在副标题或图注中明确提示“尺度不一致”,避免跨图误读。

  • 术语标注要“可读”,避免缩写/编码直出
    代码变量名、缩写与分组编码(如 lifeExp_wNO2CIQ5A1)属于计算口径或内部标识,图中应转换为读者可理解的标签(必要时补充单位/分组规则)

    • lifeExp_w人口加权平均预期寿命
    • NO2二氧化氮浓度
    • CI Q5Carstairs 指数:最高贫困五分位
    • BFL_den_cat1棕地密度分组
  • 字体体系统一
    图内文字(标题、坐标轴、刻度、图例、标注) 应尽量保持字体、字号与颜色一致(同一论文/同一章节尤其如此)

Figure 3.11. 坐标文本规范

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)
建议遵循“先保证表达正确,再保证格式合规”的顺序,避免在技术细节上产生不必要的返工。

5.0 可视化美化

5.0 制图美化与颜色选择

前一节已强调:学术图形的首要评价标准是准确可解释。因此,所有“美化”都应以规范出图为前提。

在满足规范的基础上,适度的“美化”属于额外加分项:它能够降低读图成本、强化信息层次,并提升版面观感。
(本节聚焦“非地图类可视化”的美化策略)

教师经验之谈:美化的现实价值

在大多数数据驱动研究中,高质量的可视化不仅传递研究结果,也传递“研究打磨程度”

当规范前提已满足时,版式更克制、配色更稳健、层次更清楚的图形,往往更容易获得读者与评审的注意,并提升整体呈现的专业感


配色选择:科研可视化的功能性艺术

配色的重要性
  • 提升呈现的专业度:稳定、克制的配色能减少“随意感”,并降低高饱和原色带来的视觉刺激与误导风险。
    (一般不建议大面积使用纯红/纯绿等高饱和颜色;应优先选择可读性更稳健的色板)

  • 强化信息结构:配色不仅用于区分,也用于表达层级方向。合理的明度/冷暖安排可突出重点,帮助读者更快抓住主结论。

  • 降低认知负荷:直观的颜色编码能缩短“识别分组 → 对比差异 → 形成判断”的阅读路径。

  • 保证跨图一致性:同一研究中配色体系稳定,有助于跨图对照与记忆,减少读者反复适应的成本。


什么是“好的科研配色”
1)语义匹配:色带与数据类型一致
  • 顺序色带:用于连续变量
    颜色随数值单调变化(常见为浅 → 深),并尽量遵循深色 = 更大值的方向约定。
    (不建议使用“彩虹色带”,其亮度跳跃容易制造伪边界,干扰梯度判断)

  • 发散色带:用于存在基准的数据(如差值、指数、相对偏离)。
    基准值设为浅色中点(常用白/浅灰),两侧用不同色相表示低于/高于基准。
    (常见形式:深蓝 — 白 — 深红;图例应明确标出中点刻度,如 0 或 100)

  • 离散色板:用于分类变量
    类别之间需可区分,同时避免某一颜色过于“抢眼”导致视觉权重失衡。
    (当存在“大组 + 子组”结构,可采用“组间不同色相 + 组内同色系明暗”以提示隶属关系)

2)稳健可读:不同阅读场景下仍可理解
  • 色觉差异与灰度输出:尽量避免红–绿直接对立;在灰度打印/投影场景下,仍应保持明度差异可辨。
  • 感知均匀(连续色带):优先选用感知更均匀的色带(如 viridis 系列),使视觉变化更贴近数值变化。
3)数量克制:颜色不是越多越好
  • 分类颜色过多会显著提高认知负担;当类别超出可读阈值时,优先考虑:
    • 分面/组图分层;或
    • 合并次要类别为 Other

有研究显示:人类短时记忆难以高效处理超过 7 种以上的分类颜色
建议可视化图中的颜色尽量不要超过7(分类数据)


推荐工具与资源
R 中常用配色包
# install.packages('RColorBrewer')
library(RColorBrewer)
display.brewer.all()  #展示所有颜色


viridis色盘与名称

viridis色盘与名称


# 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 系列配色


# 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) # 预览该色盘

# 运行 c4a_palettes() 可以列出所有可用的色盘名称
# install.packages('rcartocolor')
library(rcartocolor)
display_carto_all()  #展示所有颜色


# install.packages('colorspace')
library(colorspace)
hcl_palettes(plot = TRUE) #展示所有颜色


常用配色网站(排名不分先后)

核心原则 语义正确 → 稳健可读 → 简洁克制 → 风格统一 课外参考资料:
- 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.12. 配色灵感1

  • 《安娜·卡列尼娜》
Figure 3.13. 配色灵感2

Figure 3.13. 配色灵感2

  • 《楚门的世界》
Figure 3.14. 配色灵感3

Figure 3.14. 配色灵感3

  • 《布达佩斯大饭店》
Figure 3.15. 配色灵感4

Figure 3.15. 配色灵感4

提醒 影视/艺术配色主要用于“审美训练与搭配借鉴”。
这类配色未必天然符合科研制图规范:例如对比度不足、色觉不友好、灰度不可辨、或与数据语义不匹配等。

因此,更合适的做法是:先从作品中抽取“主—辅—强调”的组织方式与色彩关系,再回到科研语境中用语义匹配与可读性检验进行二次筛选与调整。


颜色如何落到代码里(hex / 调色板 / 手动映射)

在 R 中,将“配色方案”真正应用到图形通常有三条路径:
(1)直接输入颜色值(最通用)
(2)从调色板函数取色(如 RColorBrewer / ggsci / viridis 等)
(3)将颜色绑定到类别(固定映射,保证跨图一致),避免“同名类别换色”造成误读。


1)直接使用十六进制颜色(hex)

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()

2)从调色板取色并用于出图(RColorBrewer / ggsci)

当图中包含多个类别时,更稳健的做法是:
先从调色板取一组颜色向量,再用 scale_*_manual() 将颜色应用到分类变量(最常见的是 colour / fill){.note}


(a)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() # 采用极简主题风格,聚焦分布结构主体


(b)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() # 采用极简主题,移除视觉冗余以聚焦核心统计信息


3)把“网上找到的一套配色”输入到 R 并固定映射

当从网站(如 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() # 应用极简主义风格,聚焦于统计主体分布

6.0 本章练习

6.0 动手实战

实战目标

本节练习使用 fivethirtyeight 包中的 hate_crimes 州级数据进行数据可视化分析:
(1)以 gini_indexavg_hatecrimes_per_100k_fbi 绘制相关性散点图(线性回归趋势 + 方程 + 相关系数与 p 值)
(2)以 share_vote_trump 将州划分为“红/蓝州”,比较 median_house_inc 的分布差异,绘制小提琴 + 箱线 + 抖动点的复合分布图,并进行 t 检验(两组均值差异)

将巩固以下能力:
- 数据处理:缺失值处理、分组变量派生(高亮/分组标签)
- 图层组织:趋势层、点/标注层与统计注释层的叠加与遮挡控制。
- 统计呈现:回归注释(方程、相关性)与组间检验注释(t 检验)的一体化表达。


6.1 数据与研究问题设定

数据来源
fivethirtyeight::hate_crimes(州级汇总数据)
本节练习在去除缺失值后开展分析drop_na()

数据说明:fivethirtyeighthate_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.Rmd
  • 核心要求
    1. 加载 fivethirtyeighttidyverseggpubrshowtext(及 scales(未安装请先安装)
    2. 调用 hate_crimes 数据集data("hate_crimes"),并进行缺失值处理drop_na()
    3. 完成两类可视化任务:
      • 散点图gini_index × avg_hatecrimes_per_100k_fbi,叠加回归线与相关性注释
      • 分布图:按 share_vote_trump 构造分组,对比 median_house_inc 的分布并给出 t 检验注释
    4. 生成 html 输出,并检查图形的基本规范(轴含义/单位、尺度、字体、配色与标注)

6.2 可复现模板

注意

从第三章开始,练习不再提供完整的 .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") 
  )


6.3 进阶代码挑战

进阶练习

本节不提供参考代码。任务目标是:使用 gapminder 的时序数据,独立完成一组可用于报告/论文的“研究级可视化”,重点训练 分布 × 时间 × 分组 的综合表达能力。


6.3.1 数据与研究问题

数据集gapminder(国家 × 年份的面板数据)
核心变量(建议)
- 分布变量:lifeExpgdpPercap
- 分组变量:continent
- 时间变量:year

研究问题
- 不同大洲在 1952–2007 年间,某项指标(lifeExpgdpPercap)的分布形态如何随时间演变?
- 分布的中位数水平离散程度(IQR)离群值是否呈现系统性变化?


6.3.2 可视化任务

任务 A|时序箱线图
绘制“按年份排列的箱线图序列”:
- 横轴year(建议转换为因子,以确保每一年都有一个箱线图)
- 纵轴lifeExpgdpPercap
- 分面:按 continent 分面(facet_wrap(~ continent, ncol = 1)facet_grid(continent ~ .)
- 要求:每一个年份对应一个箱线图;每个箱线图内部包含该大洲当年所有国家的观测值。

任务 B|配色与美化
- 选择合理配色方案并说明理由:
- 若使用离散配色(按大洲),需保持跨图一致。
- 若 year 为因子且颜色映射到 continent,应避免图例冗余(可视情况关闭图例)。
- 处理可读性问题:年份多、刻度密集时,需通过字号、角度、留白或抽样刻度提升可读性。
- 图形需包含:标题、简洁副标题、轴含义与单位说明(如有变换需标注)。


6.3.3 技术要求
  • 使用 R Markdown 完成,并 knit 输出为 HTML
  • 文档需包含:
    1. 数据准备(筛选/变换/缺失值处理,如有)
    2. 作图代码(结构清晰、分段注释)
    3. 简短文字解读(2–4 句):描述最明显的分布差异与时间变化
  • 代码注释要求:
    • 说明关键选择:变量为何选 lifeExpgdpPercap;为何采用该分面方式;配色为何合理。

提示

  • year 若保持数值型,箱线图会在连续轴上挤在一起;将其转为因子通常更稳健。
  • 若选择 gdpPercap,可考虑对数变换以缓解长尾分布(变换必须在轴标签中说明)。
  • 年份刻度过密时,可只显示每隔若干年的标签,或旋转 x 轴文字提升可读性。
输出参考

输出参考