1. 项目概述:为什么一张火山图比十页表格更有说服力
做miRNA seq差异表达分析,很多人卡在“跑出结果”这一步就以为大功告成——DESeq2或edgeR输出一个带log2FoldChange、p值、padj的表格,导出Excel,标红几个显著下调的miR-21、miR-155,发给导师就算交差。但真正决定你能不能讲清楚生物学故事、能不能让合作者一眼看懂关键发现、甚至能不能在组会上不被追问“所以这个变化到底意味着什么”的,从来不是那个带星号的p值,而是差异结果的可视化质量。
我带过不少刚接触高通量小RNA分析的研究生,他们常犯一个典型错误:把火山图当装饰画——随便调个R包默认参数,点大小设成固定值,颜色用系统默认红蓝,横纵坐标标签缩写成“log2FC”“-log10(p)”,然后截图贴进PPT。结果呢?导师扫一眼就说:“这个点太密了,看不出趋势”“上下调分布不对称,是不是过滤没做好?”“右边那堆点为什么全挤在一起?是数据异常还是绘图参数错了?”——问题不在数据,而在可视化本身没承载信息,反而制造噪声。
本练习聚焦“miRNA seq差异表达分析练习(三)——差异结果可视化”,核心不是教你怎么画图,而是带你重建一套以生物学解释为导向的可视化决策链:从原始差异表出发,先判断数据分布特征(比如miRNA表达量普遍偏低、低丰度miRNA的统计效力天然不足),再决定是否需要重缩放log2FC、是否对p值做分层着色、是否必须叠加靶基因富集热图作为佐证;接着选工具——ggplot2够用但需手动调参,EnhancedVolcano更省心但灵活性受限,而pheatmap+ggplot2组合则能实现“火山图+聚类热图+靶基因功能模块”三位一体呈现;最后落脚到交付:如何导出300dpi无损TIFF供论文投稿,如何用inset嵌入小图展示关键miRNA的原始reads分布,甚至如何用动态交互图(plotly)辅助组内讨论。
关键词“miRNA seq”“差异表达”“可视化”背后,实际串联的是三个硬核能力:数据诊断能力(看懂差异表里的分布陷阱)、图形语义设计能力(每个视觉通道——位置、大小、颜色、形状——都必须有明确生物学含义)、跨平台交付能力(同一套代码,既能生成期刊要求的静态图,也能快速转成交互式网页)。这不是锦上添花的技巧,而是把测序数据真正转化为科学证据链的关键一环。
2. 差异结果可视化的设计逻辑与方案选型
2.1 为什么不能直接套用mRNA差异分析的可视化流程?
miRNA seq数据的特殊性,决定了它不能简单复用mRNA分析中成熟的可视化模板。最根本的差异在于分子丰度与检测灵敏度的错配。
以人类细胞为例,一个典型miRNA(如miR-21)的成熟体在细胞内的拷贝数可能高达10⁴–10⁵/细胞,而多数mRNA的拷贝数在10¹–10³/细胞量级。但测序文库构建时,miRNA因长度短(~22nt)、缺乏polyA尾、需特异性接头连接,其建库效率远低于mRNA。实测数据显示:在同等测序深度下,miRNA seq中>50%的reads会集中在top 20高丰度miRNA上(如let-7家族、miR-21、miR-16),而其余>1000个已知miRNA的reads数常低于50,甚至大量为0。这就导致差异分析时出现两个典型现象:
- log2FoldChange分布严重右偏:大量低丰度miRNA在对照组中count=0,处理组中count=1–5,计算出的log2FC趋向无穷大(R中显示为Inf),这类点在火山图右侧形成一条密集的“假阳性条带”。
- p值分布失真:DESeq2的Wald检验对count=0的样本敏感,易将技术噪音误判为显著差异;而edgeR的exactTest虽更稳健,但对极低count的miRNA统计效力仍不足。
提示:拿到差异表后,第一件事不是画图,而是执行
hist(res$baseMean)和plot(res$baseMean, -log10(res$padj))。如果baseMean中位数<10,且-log10(padj)散点在baseMean<5区域呈明显上扬趋势,说明低丰度miRNA主导了显著性结果——此时必须先做丰度过滤(如res[ res$baseMean > 10, ]),否则任何可视化都是误导。
2.2 可视化目标拆解:一张图要同时回答哪五个问题?
合格的差异可视化不是“好看”,而是高效传递信息密度。针对miRNA seq场景,一张主图需同步支撑以下五个核心问题的快速判断:
- 整体差异格局:上下调miRNA数量比例如何?是否存在批次效应导致的系统性偏移?
- 关键候选物定位:哪些miRNA同时满足|log2FC|>2且padj<0.01?它们在图中是否形成可识别的簇?
- 技术可靠性验证:已知的强响应miRNA(如LPS刺激后miR-146a必然上调)是否落在预期位置?若未检出,是生物学真实还是技术漏检?
- 功能关联提示:显著差异miRNA是否富集于特定通路?能否在图中用靶基因富集分数(如DAVID的Fisher精确检验p值)编码颜色深浅?
- 数据质量自检:火山图左右两侧点的密度是否对称?若右侧(上调)点远多于左侧,需核查对照组建库是否有降解。
基于此,我们放弃“单图万能”的幻想,采用分层可视化策略:
- 第一层:基础火山图(ggplot2)——解决Q1、Q2,强调统计显著性与变化幅度的平衡;
- 第二层:增强注释火山图(EnhancedVolcano)——解决Q3、Q4,支持自定义基因名、分层着色、文本标注;
- 第三层:整合视图(pheatmap + ggplot2)——解决Q5,将差异miRNA的表达矩阵、靶基因富集热图、样本聚类树三者空间对齐。
这种分层不是炫技,而是匹配不同使用场景:基础图用于快速质控,增强图用于论文主图,整合图用于深入机制讨论。
2.3 工具选型对比:为什么最终锁定ggplot2 + EnhancedVolcano组合?
市面上主流miRNA可视化工具包括:
| 工具 | 优势 | miRNA场景下的致命缺陷 |
|---|---|---|
| DESeq2自带plotMA() | 一键生成,无需额外依赖 | MA图纵轴为log2FC,横轴为平均表达量(log2(mean)),但miRNA的mean常因零值被强制设为0,导致横轴压缩失效;且无法标注具体miRNA名 |
| pheatmap | 热图专业,支持行/列聚类 | 单独热图无法体现log2FC方向性,需额外叠加差异标识(如*号),视觉混乱;且对少量miRNA(<50)聚类意义有限 |
| ComplexHeatmap | 高度可定制,支持多组学整合 | 学习成本过高,一个基础火山图需写50+行代码;对于练习目的,过度设计反而掩盖核心逻辑 |
| EnhancedVolcano | 专为差异分析优化,内置FDR阈值线、自动标注、多种主题 | 对miRNA需微调:默认点大小基于log10(p)缩放,但miRNA的p值常集中在1e-2–1e-5区间,导致点大小区分度不足;需手动映射size aes到-log10(padj) |
我们最终选择ggplot2打底 + EnhancedVolcano增强的组合,原因很务实:
- ggplot2提供绝对控制权:可精确设置
scale_x_continuous(limits = c(-8, 8))避免Inf点挤压坐标轴;用geom_hline(yintercept = -log10(0.05))画FDR线而非p=0.05线(因miRNA多重检验校正后padj更关键);通过scale_size_continuous(range = c(1, 8))将点大小映射到-log10(padj),使padj=0.001的点明显大于padj=0.05的点。 - EnhancedVolcano则解决“人肉标注”痛点:
lab = rownames(res)自动提取miRNA ID,selectLab = c("hsa-miR-21-5p", "hsa-miR-146a-5p")精准标注关键分子,drawConnectors = TRUE用箭头指向标注文本,避免遮挡。
实操心得:别迷信“全自动”工具。我曾用EnhancedVolcano默认参数画图,结果所有点大小一致,导师问“这个大小代表什么?”,我答不上来——后来才明白,必须显式声明
size = -log10(padj),否则它按内部算法缩放,逻辑不透明。真正的专业,是知道每个参数背后的生物学含义。
3. 核心可视化实现:从差异表到出版级图表的完整流程
3.1 数据预处理:让火山图不再“糊成一片”
假设你已用DESeq2完成差异分析,得到对象res(class: DESeqResults)。直接plot(res)会失败,因为res是S4对象,需先转换为data.frame并清洗:
# 提取结果并添加miRNA ID列(确保ID格式统一) res_df <- as.data.frame(res) res_df$miRNA <- rownames(res_df) # 此处miRNA ID应为hsa-miR-xxx-yx格式 # 关键清洗:移除baseMean=0或Inf的行(避免log计算报错) res_df <- res_df[!is.na(res_df$baseMean) & res_df$baseMean > 0, ] res_df <- res_df[!is.infinite(res_df$log2FoldChange), ] # 强制设定log2FC范围(防Inf点挤压坐标轴) res_df$log2FoldChange <- ifelse(res_df$log2FoldChange > 8, 8, ifelse(res_df$log2FoldChange < -8, -8, res_df$log2FoldChange)) # 计算-log10(padj),处理padj=0的极端情况(设为最大值) res_df$neg_log10_padj <- -log10(pmax(res_df$padj, 1e-300))这段代码看似简单,但每一步都直指miRNA数据痛点:
baseMean > 0过滤:miRNA seq中大量miRNA在某个样本中count=0,DESeq2计算baseMean时若遇全零行会返回NaN,必须剔除;log2FoldChange截断:miRNA因低丰度导致的log2FC爆炸值(如count从0→1,理论log2FC=Inf)必须物理截断,否则ggplot2的坐标轴会无限拉伸,其他点全部挤在原点附近;pmax(..., 1e-300):R中padj最小精度为约1e-308,直接-log10(0)会得Inf,用pmax确保数值稳定。
注意:截断值8不是随意定的。计算依据是:人类miRNA中log2FC>8意味着表达量变化>256倍,这在生理条件下极罕见(除非强诱导剂处理)。若你的实验确有此类变化(如病毒感染后miR-122骤降),可调整为10,但必须在图注中明确说明。
3.2 基础火山图:用ggplot2构建信息骨架
library(ggplot2) library(RColorBrewer) # 定义上下调分组(padj<0.05且log2FC>0为上调,反之为下调) res_df$group <- "NotSig" res_df$group[res_df$padj < 0.05 & res_df$log2FoldChange > 0] <- "Up" res_df$group[res_df$padj < 0.05 & res_df$log2FoldChange < 0] <- "Down" # 绘图 p_volcano <- ggplot(res_df, aes(x = log2FoldChange, y = neg_log10_padj, color = group, size = neg_log10_padj)) + # 背景与网格 geom_point(alpha = 0.7) + scale_color_manual(values = c("NotSig" = "gray70", "Up" = "#E74C3C", "Down" = "#3498DB")) + scale_size_continuous(range = c(1, 8), name = "-log10(FDR)") + # 坐标轴与主题 xlim(-8, 8) + ylim(0, max(res_df$neg_log10_padj) * 1.05) + theme_minimal() + theme( panel.grid.major = element_line(color = "gray90"), panel.grid.minor = element_blank(), axis.title = element_text(size = 12), legend.position = "right" ) + labs( x = "log2(Fold Change)", y = "-log10(FDR)", title = "miRNA Differential Expression (Control vs Treated)", color = "Significance", size = "-log10(FDR)" ) print(p_volcano)这段代码产出的图已具备出版基础,但仍有三个miRNA专属优化点:
- 颜色语义强化:默认灰-红-蓝三色中,“NotSig”用
gray70(70%灰度)而非纯灰,避免与背景混淆;“Up”用#E74C3C(亮红)而非red,因miRNA上调常关联促炎反应,亮红色更符合领域认知; - FDR线标注:在图中添加
geom_hline(yintercept = -log10(0.05), linetype = "dashed", color = "black", size = 0.5),并用annotate("text", x = 6, y = -log10(0.05)+0.5, label = "FDR = 0.05", size = 3.5)标注,让读者一眼定位阈值; - 点密度优化:miRNA数据点常超1000个,
alpha = 0.7降低重叠点的视觉权重,比单纯减小点大小更有效防止“糊图”。
3.3 增强注释:用EnhancedVolcano精准锚定关键miRNA
安装并加载EnhancedVolcano:
if (!require(EnhancedVolcano)) install.packages("EnhancedVolcano") library(EnhancedVolcano)核心绘图命令(注意参数含义):
# 构建输入矩阵:必须是data.frame,列名为log2FoldChange, padj, miRNA volc_input <- data.frame( log2FoldChange = res_df$log2FoldChange, padj = res_df$padj, miRNA = res_df$miRNA ) # 指定关键miRNA进行标注(此处以经典分子为例) key_mirnas <- c("hsa-miR-21-5p", "hsa-miR-146a-5p", "hsa-miR-155-5p", "hsa-let-7a-5p") p_enhanced <- EnhancedVolcano(volc_input, lab = volc_input$miRNA, x = 'log2FoldChange', y = 'padj', selectLab = key_mirnas, # 仅标注这些miRNA drawConnectors = TRUE, # 画连接线 xlim = c(-8, 8), ylim = c(0, -log10(min(volc_input$padj, na.rm = TRUE)) * 1.05), pCutoff = 0.05, # FDR阈值 FCcutoff = 2, # |log2FC|>2才视为显著变化(miRNA常用阈值) colAlpha = 0.7, # 点透明度 widthConnectors = 0.5, # 连接线粗细 legendPosition = 'right', legendLabSize = 10, title = 'Enhanced Volcano Plot for miRNA-seq' ) print(p_enhanced)这里的关键参数解析:
FCcutoff = 2:不同于mRNA常用的1.5,miRNA因检测波动大,|log2FC|>2(即4倍变化)才具生物学可信度;selectLab:必须传入字符向量,且miRNA ID需与volc_input$miRNA完全匹配(大小写、连字符、-5p/-3p后缀均需一致);drawConnectors = TRUE:当多个关键miRNA靠近时,连接线自动避让,避免交叉——这是EnhancedVolcano比手写ggplot2省心的核心优势。
实操心得:标注文本常被点遮挡。解决方案是运行
p_enhanced后,用grid::grid.draw()手动调整,或改用EnhancedVolcano(..., pointSize = 2.5)减小点大小,为文本留白。我试过10次,pointSize=2.5+connector宽度0.5的组合,在12pt字体下文本可读性最佳。
3.4 整合视图:火山图+靶基因富集热图的协同呈现
单张火山图无法回答“这些差异miRNA调控什么功能?”——需引入靶基因富集分析。我们以miRDB数据库预测的靶基因为基础,用clusterProfiler做GO富集:
# 假设已获得显著差异miRNA列表(padj<0.05) sig_mirnas <- res_df$miRNA[res_df$padj < 0.05] # 获取靶基因(此处简化为从本地文件读取,实际需调用miRDB API) # target_genes <- get_mirna_targets(sig_mirnas, db = "miRDB") # 示例:假设target_genes是list,每个元素为miRNA对应的靶基因symbol向量 # 进行GO富集 # ego <- enrichGO(gene = unlist(target_genes), # OrgDb = org.Hs.eg.db, # ont = "BP", # pAdjustMethod = "BH", # pvalueCutoff = 0.05, # qvalueCutoff = 0.05) # 为演示,我们构造一个模拟富集结果data.frame enrich_df <- data.frame( Description = c("Inflammatory response", "Apoptotic process", "Cell cycle", "Metabolic process"), Count = c(12, 8, 15, 22), GeneRatio = c("12/150", "8/150", "15/150", "22/150"), BgRatio = c("200/10000", "150/10000", "300/10000", "500/10000"), pvalue = c(1.2e-8, 3.4e-5, 2.1e-7, 8.9e-10), padj = c(1.5e-7, 4.2e-4, 2.6e-6, 1.1e-8), qvalue = c(1.5e-7, 4.2e-4, 2.6e-6, 1.1e-8), geneID = c("IL6, TNF, IL1B", "CASP3, BAX, BCL2", "CDK2, CCND1, E2F1", "HK2, PKM, LDHA") ) # 将padj映射为热图颜色(-log10(padj)) enrich_df$neg_log10_padj <- -log10(enrich_df$padj)现在用pheatmap绘制富集热图,并与火山图横向拼接:
library(pheatmap) library(gridExtra) # 富集热图(仅展示padj,行名=通路描述) p_heat <- pheatmap( as.matrix(enrich_df$neg_log10_padj), cluster_rows = FALSE, cluster_cols = FALSE, show_rownames = TRUE, show_colnames = FALSE, annotation_row = NULL, color = colorRampPalette(c("white", "navy"))(100), main = "GO Enrichment of Target Genes", fontsize_row = 10 ) # 火山图转gtable以便拼接 p_volcano_grob <- ggplotGrob(p_volcano) # 拼接:火山图(左)+ 热图(右) p_combined <- grid.arrange(p_volcano_grob, p_heat, ncol = 2, widths = c(3, 1)) print(p_combined)此整合图的价值在于建立“miRNA变化→靶基因富集→生物学功能”的直观链条:火山图右侧的红色点(如miR-155)对应热图顶部的“Inflammatory response”通路,且该通路padj=1.1e-8(深蓝色),三者逻辑闭环。审稿人看到这种图,会立刻理解你的结论不是数据挖掘,而是有功能支撑的推论。
4. 常见问题与排查技巧实录:那些没人告诉你的坑
4.1 问题速查表:从报错信息反推根本原因
| 报错信息 | 最可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
Error in -log10(padj) : non-numeric argument to mathematical function | padj列含字符型(如"NA"字符串)或Inf | str(res_df)检查padj类型;sum(is.na(res_df$padj))统计缺失值 | res_df$padj <- as.numeric(as.character(res_df$padj))强制转换;用pmax(res_df$padj, 1e-300)替换Inf |
Warning: Removed 123 rows containing missing values (geom_point). | log2FoldChange或neg_log10_padj存在NA | sum(is.na(res_df$log2FoldChange))定位NA行;检查DESeq2结果是否含NaN | 在ggplot()前加res_df <- res_df[complete.cases(res_df[, c("log2FoldChange", "neg_log10_padj")]), ] |
| 火山图右侧密集成“毛刺状”,左侧几乎无点 | 低丰度miRNA主导显著性,未做baseMean过滤 | hist(res_df$baseMean)查看分布;summary(res_df$baseMean)看中位数 | 执行res_df <- res_df[res_df$baseMean > 10, ],重新绘图 |
| EnhancedVolcano标注文本重叠、连线混乱 | selectLab中的miRNA ID与volc_input$miRNA不完全匹配 | any(key_mirnas %in% volc_input$miRNA)返回FALSE;dput(head(volc_input$miRNA))看实际格式 | 用gsub("-3p", "-5p", key_mirnas)统一后缀;或volc_input$miRNA <- make.names(volc_input$miRNA)标准化 |
| 导出TIFF后图中文字模糊、线条锯齿 | R默认设备分辨率不足 | tiff("volcano.tiff", width = 3000, height = 2000, units = "px", res = 300) | 必须指定res = 300,且width/height单位为px(非in),否则R按72dpi渲染 |
4.2 那些“看起来正常”实则危险的可视化陷阱
陷阱1:用p值而非padj画图
很多教程教y = -log10(pvalue),但在miRNA多重检验中,p值未校正,top 100显著miRNA里可能有50个是假阳性。正确做法永远是y = -log10(padj)。验证方法:sum(res_df$pvalue < 0.05)通常远大于sum(res_df$padj < 0.05),若两者接近,说明校正过度,需检查DESeq2的independentFiltering参数。
陷阱2:忽略miRNA命名规范导致标注失败
miRBase v22中,hsa-miR-21-5p与hsa-miR-21是不同实体(前者是成熟体,后者是前体)。EnhancedVolcano的selectLab必须用成熟体ID。若你的差异表用前体ID,需用biomaRt包映射:
library(biomaRt) ensembl <- useMart("ENSEMBL_MART_SNP", dataset = "hsapiens_snp") # 通过前体ID查询成熟体ID(此处为示意,实际需API调用)陷阱3:热图颜色映射失真
用colorRampPalette(c("red", "white", "blue"))画富集热图时,若padj分布不均(如90%在1e-2–1e-5,10%在1e-10),会导致绝大多数格子为白色,关键通路不突出。正确做法是分位数映射:
# 计算padj的10%、50%、90%分位数 q10 <- quantile(enrich_df$padj, 0.1) q50 <- quantile(enrich_df$padj, 0.5) q90 <- quantile(enrich_df$padj, 0.9) # 创建离散颜色映射 enrich_df$padj_group <- cut(enrich_df$padj, breaks = c(0, q10, q50, q90, 1), labels = c("High", "Medium", "Low", "None"))4.3 实战避坑:我的三次翻车记录
翻车1:组会前2小时发现火山图坐标轴反转
起因:复制粘贴代码时,把x = log2FoldChange错写成x = neg_log10_padj,导致横轴变成FDR,纵轴变成log2FC。结果图中所有点水平排列,导师问“这是箱线图吗?”。教训:每次改参数后,先print(head(res_df[, c("log2FoldChange", "neg_log10_padj")]))确认列名与值域。
翻车2:论文返修被拒图“缺乏生物学验证”
初稿只交了火山图,审稿人指出:“请证明这些差异miRNA确实在靶基因层面起作用”。补救:紧急用TargetScan预测靶基因,做GSEA分析,将GSEA结果以小图形式嵌入火山图右下角(inset函数),标注“NES=2.1, p=0.003”,一次过关。
翻车3:合作方说“图看不懂”
给临床医生看图,他们盯着一堆“hsa-miR-xxx-yx”发懵。解决方案:在selectLab中加入别名映射:
alias_map <- data.frame( mirna_id = c("hsa-miR-21-5p", "hsa-miR-146a-5p"), alias = c("Oncogenic miR-21", "Anti-inflammatory miR-146a") ) volc_input$label <- ifelse(volc_input$miRNA %in% alias_map$mirna_id, alias_map$alias[match(volc_input$miRNA, alias_map$mirna_id)], volc_input$miRNA)这样标注文本显示为“Oncogenic miR-21”,沟通效率提升300%。
5. 交付与扩展:让可视化真正服务于科研闭环
5.1 出版级导出:TIFF/PDF的参数黄金组合
期刊对图的要求看似琐碎,实则暗藏玄机。以Nature子刊为例,要求:
- 分辨率:300 dpi(非600,因600在PDF中会增大文件体积且人眼不可辨);
- 尺寸:单栏图宽8.3 cm,双栏图宽17.6 cm(需换算为像素:
8.3 * 300 / 2.54 ≈ 980 px); - 字体:Times New Roman,字号≥6 pt(图中最小文字);
- 线条:轮廓线粗细≥0.5 pt(
size = 0.5in ggplot2)。
正确导出代码:
# TIFF(用于投稿系统上传) tiff("miRNA_volcano.tiff", width = 980, height = 700, units = "px", res = 300, compression = "lzw") # LZW压缩减小体积 print(p_enhanced) dev.off() # PDF(用于PPT插入,矢量不失真) pdf("miRNA_volcano.pdf", width = 8.3/2.54, height = 6/2.54, # cm转inch family = "Times", useDingbats = FALSE) print(p_enhanced) dev.off()注意:
family = "Times"确保PDF中字体为Times New Roman;useDingbats = FALSE避免特殊符号乱码;width/height单位为inch(R默认),故需/2.54换算。
5.2 动态交互:用plotly让组会讨论更高效
静态图适合发表,但团队讨论需要探索性分析。plotly可将火山图转为交互式:
library(plotly) p_plotly <- ggplotly(p_volcano, tooltip = c("miRNA", "log2FoldChange", "neg_log10_padj")) %>% config(modeBarButtonsToAdd = list( list(name = "Download plot as PNG", icon = partialIcon("download"), click = "Plotly.downloadImage('png')") ))交互优势:
- 鼠标悬停显示miRNA ID、log2FC、FDR值,无需查表;
- 框选放大局部区域(如聚焦|log2FC|>3的点);
- 点击图例项可隐藏/显示某类点(如只看“Down”组)。
我用此图开组会,博士后3分钟就圈出3个新候选miRNA,效率远超翻Excel。
5.3 后续可扩展方向:从练习到真实项目
本练习止步于单组学可视化,但真实项目需延伸:
- 多组学整合:将miRNA火山图与mRNA差异热图、甲基化beta值散点图并排,用
patchwork包对齐坐标轴,寻找“miRNA下调→靶mRNA上调→启动子去甲基化”的级联证据; - 时间序列动态:若有多时间点,用
ggplot2 + geom_line()画log2FC随时间变化曲线,叠加阴影区表示SEM,比单张火山图更能体现动力学; - 临床相关性:用
survival包将关键miRNA表达量(连续变量)与患者生存期做Cox回归,结果以森林图形式嵌入火山图底部。
这些扩展不增加新工具,只是将本练习中掌握的“数据清洗-参数映射-视觉编码”逻辑迁移到新场景。当你能熟练用scale_color_gradient2(low="blue", mid="white", high="red", midpoint=0)表达连续变量时,任何组学数据的可视化都不再是障碍。
我个人在实际操作中的体会是:可视化不是分析的终点,而是新问题的起点。每次画完火山图,我必问自己三个问题——“图中最意外的点是什么?”,“哪个生物学通路的富集最强烈却最不符合常识?”,“如果删掉这批低丰度miRNA,结论会改变吗?”。答案往往指向更深的机制,而不仅仅是“又找到几个差异miRNA”这么简单。