# 第 08 章 可视化与图表

## 本章目标

学完本章，你将能够：

1. 理解 ggplot2 的图层语法：数据、映射（aes）、几何对象（geom）、主题（theme）；
2. 独立画出并正确解读火山图（volcano plot），并会做发表级美化；
3. 用 pheatmap 画带分组注释条和聚类树的热图（heatmap）；
4. 用 prcomp + ggplot2 画 PCA 图并加置信椭圆，判断样本整体分离；
5. 掌握论文图表的规范：字号、分辨率（dpi = 300）、图注（Figure legend）写法。

## 正文

### 1. ggplot2 绘图哲学：一图一图层

R 基础绘图（`plot()`）像“在白纸上直接涂色”，而 ggplot2 像“搭积木”：先告诉它“数据是什么、把数据的哪些列映射到图形的哪些元素”，再一层一层往上叠加几何对象和主题。它的核心是三件事：

- **数据**：一个整洁的数据框（data.frame）；
- **映射 `aes()`**：把数据列映射到图形属性——x 坐标、y 坐标、颜色、形状、大小；
- **几何对象 `geom_*()`**：决定画什么形状——点 `geom_point()`、线 `geom_line()`、柱 `geom_col()`、箱线 `geom_boxplot()` 等。

新手三步画图法：

```r
# 第一步：准备数据（通常整理成长格式：一列一个变量、一行一个观测）
# 第二步：ggplot(data, aes(x=..., y=...)) 建立画布与映射
# 第三步：+ geom_xxx() 加图层，+ theme_xxx() 调主题，+ ggsave() 存图
ggplot(data, aes(x = xvar, y = yvar, color = group)) +
  geom_point() +
  theme_bw()
```

两个常见坑：一是 `aes()` 里写的是数据框的**列名**而不是外面的向量；二是图层叠加用 **`+` 号**而不是管道符 `%>%`。记住这两点，ggplot2 就成功了一半。

### 2. 火山图：一张图看懂“谁在变”

火山图是差异表达分析的标准配图：每个点是一个基因，横轴 log2FC，纵轴 −log10(adj.P.Val)。纵轴取负对数后，p 值越小点越靠上，所以“显著且差异大”的基因分布在左右两侧高处，整张图呈火山喷发状——因此得名。

最直接的画法（输入是第 07 章的 `res` 结果表）：

```r
library(ggplot2)
res$change <- ifelse(res$adj.P.Val < 0.05 & res$logFC > 1, "up",
              ifelse(res$adj.P.Val < 0.05 & res$logFC < -1, "down", "ns"))

ggplot(res, aes(x = logFC, y = -log10(adj.P.Val))) +
  geom_point(aes(color = change), size = 0.8) +
  scale_color_manual(values = c(up = "#d7191c", down = "#2c7bb6", ns = "grey80"),
                     labels = c(up = "上调", down = "下调", ns = "不显著")) +
  geom_vline(xintercept = c(-1, 1), linetype = "dashed", linewidth = 0.4) +
  geom_hline(yintercept = -log10(0.05), linetype = "dashed", linewidth = 0.4) +
  labs(x = "log2(fold change)", y = "-log10(adjusted P)",
       title = "TCGA-LIHC 肿瘤 vs 癌旁差异表达火山图") +
  theme_bw() +
  theme(legend.position = "top")
ggsave("../figures/fig06_volcano.png", width = 6, height = 5, dpi = 300)
```

**怎么读（四象限含义）**：

- 右上象限（log2FC > 1 且 adj.P < 0.05）：显著**上调**基因；
- 左上象限（log2FC < −1 且 adj.P < 0.05）：显著**下调**基因；
- 两条竖线之间或横线以下的点：不满足任一标准，**不显著**。

想要“论文级”效果，也可以用现成包 EnhancedVolcano（基于 ggplot2，自动标注显著基因名并避开标签重叠）：

```r
# BiocManager::install("EnhancedVolcano")
library(EnhancedVolcano)
EnhancedVolcano(res,
                lab = rownames(res),      # 标注的基因名
                x = "logFC", y = "adj.P.Val",
                pCutoff = 0.05, FCcutoff = 1,
                title = "TCGA-LIHC DEGs",
                pointSize = 1.5, labSize = 3)
ggsave("../figures/fig06_volcano.png", width = 7, height = 6, dpi = 300)
```

如果只想标注重点基因（比如我们的焦亡基因），用 ggrepel 包只给它们加标签：

```r
res$label <- ifelse(rownames(res) %in% pyroptosis_genes, rownames(res), "")
ggplot(res, aes(x = logFC, y = -log10(adj.P.Val))) +
  geom_point(aes(color = change), size = 0.8) +
  ggrepel::geom_text_repel(aes(label = label), max.overlaps = 20) +
  theme_bw()
```

![图06 差异表达火山图（示例，模拟数据）](../figures/fig06_volcano.png)

### 3. 热图：表达模式的整体视图

热图把“基因 × 样本”的表达量画成颜色矩阵：行是基因、列是样本，颜色深浅代表表达高低。它适合回答“这批基因在不同样本里有没有清晰的分组模式”。用 pheatmap 画差异基因（或焦亡差异基因）的表达热图：

```r
# BiocManager::install("pheatmap")
library(pheatmap)

mat <- expr_log2[rownames(deg), ]              # 差异基因子集
mat <- t(scale(t(mat)))                        # 对每个基因做 z-score 标准化

anno <- data.frame(group = group)              # 样本注释数据框
rownames(anno) <- colnames(expr_log2)          # 行名必须与样本名一致

png("../figures/fig07_heatmap.png", width = 8, height = 6, units = "in", res = 300)
pheatmap(mat,
         scale = "row",                        # 按行（基因）标准化
         cluster_rows = TRUE, cluster_cols = TRUE,   # 行/列层次聚类
         annotation_col = anno,                # 顶部画分组注释条
         show_rownames = TRUE, show_colnames = FALSE,
         color = colorRampPalette(c("navy", "white", "firebrick3"))(100),
         main = "差异基因表达热图（示例）")
dev.off()
```

需要理解的三点：

- **z-score 标准化**：对每个基因，把表达值减去该基因的均值、再除以标准差，使每个基因在行内以 0 为中心。否则高表达基因会永远偏红、低表达基因永远偏蓝，看不出“相对变化”。（`scale = "row"` 与 `t(scale(t(mat)))` 等价，二选一即可。）
- **行/列聚类**：pheatmap 默认对行和列做层次聚类，把表达模式相近的基因/样本放在相邻位置。如果 Normal 与 Tumor 在列聚类中各自成团，说明差异表达模式清晰——这是热图要传达的核心信息。
- **注释条**：`annotation_col` 是一个数据框，行名必须与表达矩阵的列名（样本名）完全一致，列名就是注释项的名称（如 group）。颜色映射可用 `annotation_colors` 参数自定义。

![图07 差异基因表达热图（示例，模拟数据）](../figures/fig07_heatmap.png)

### 4. PCA 图：样本层面的“全家福”

PCA（主成分分析，principal component analysis）把高维表达数据压缩成少数几个“主成分”，让我们能在二维平面上一眼看出样本的整体关系。注意视角切换：差异分析看**基因**，PCA 看**样本**——它回答“肿瘤样本和癌旁样本在整体表达模式上是否分开”。

```r
pca <- prcomp(t(expr_log2), scale. = TRUE)      # 注意转置：行=样本、列=基因
pca_sum <- summary(pca)$importance               # 各主成分解释的方差比例
pca_df <- data.frame(PC1 = pca$x[, 1], PC2 = pca$x[, 2], group = group)

ggplot(pca_df, aes(x = PC1, y = PC2, color = group)) +
  geom_point(size = 2, alpha = 0.8) +
  stat_ellipse(level = 0.95) +                  # 95% 置信椭圆
  labs(x = paste0("PC1 (", round(pca_sum[2, 1] * 100, 1), "%)"),
       y = paste0("PC2 (", round(pca_sum[2, 2] * 100, 1), "%)")) +
  theme_bw()
ggsave("../figures/fig08_pca.png", width = 6, height = 5, dpi = 300)
```

要点：

- **一定要转置**：`prcomp()` 要求行是观测（样本）、列是变量（基因），而表达矩阵是“基因 × 样本”，所以先 `t()`。
- `scale. = TRUE`：对每个基因标准化，防止高表达基因主导主成分。
- 轴标签写"PC1 (xx%)"：括号里是主成分解释的方差比例，说明这张图捕捉了数据多少信息。
- `stat_ellipse(level = 0.95)`：给每组画 95% 置信椭圆，帮助判断两组是否真的分开。
- PCA 的另一个用途是**初步检查批次效应**：如果同批次的样本聚在一起、而不是按分组聚在一起，就要警惕技术性差异混入了生物学差异。

![图08 PCA 图（示例，模拟数据）](../figures/fig08_pca.png)

### 5. 图表规范：从“能看”到“能发表”

论文图表和课堂作业图的差距往往在细节：

- **字号**：正文图建议基础字号 8–12 pt（`theme_bw(base_size = 12)`），保证缩印后仍可读；图内文字不小于 7 pt。
- **分辨率**：位图保存用 `ggsave(..., dpi = 300)`，这是多数期刊的底线要求；矢量图（PDF/SVG）更好，任意缩放不模糊。
- **尺寸**：按期刊栏宽设置（单栏约 8.5 cm、双栏约 17.5 cm），先定尺寸再调字号，避免文字被压缩。
- **图注（Figure legend）**：写在图的下方，格式为“图号 + 一句话标题 + 数据与方法说明 + 统计说明”。例如："图 1. TCGA-LIHC 肿瘤与癌旁差异表达火山图。横轴为 log2 变化倍数，纵轴为 BH 校正后 p 值的负对数；红色为上调基因（|log2FC| > 1 且 adj.P < 0.05），蓝色为下调基因，灰色为不显著。" 图注要能脱离正文独立读懂。
- **一图一义**：每张图必须有独立含义，同一信息不要既画图又列表；图与图之间避免重复；配色考虑色盲友好（如 viridis 色系）。

## 本章小结

- ggplot2 的语法 = 数据 + `aes()` 映射 + `geom_*()` 图层 + `theme_*()` 主题，一层层用 `+` 叠加。
- 火山图：每点一基因，横轴 log2FC、纵轴 −log10(adj.P)，四象限对应上调/下调/不显著；EnhancedVolcano 可自动标注，ggrepel 可只标重点基因。
- 热图：pheatmap 的 `scale = "row"` 做 z-score、`cluster_rows/cols` 聚类、`annotation_col` 加分组注释条。
- PCA：`prcomp(t(expr), scale. = TRUE)` + `stat_ellipse()`，从样本层面看分离与批次。
- 发表级规范：base_size 8–12、dpi = 300、图注独立可读、一图一义。

## 思考题

1. 热图里为什么通常先做 z-score 标准化？不做会出现什么误导？
2. PCA 图里两组完全混在一起，可能有哪些原因？下一步你会怎么排查？
3. 火山图的纵轴为什么用 −log10(adj.P.Val) 而不是直接用 adj.P.Val？
4. 一篇论文配了 8 张图，审稿人说“图太多且重复”，你会如何取舍？
