拿到DESeq2的差异分析结果,不少人卡在最后一公里——表格里几万行基因,padj、log2FoldChange一堆数字,完全不知道从哪看起,更别说画出一张能放进文章里的图。其实差异分析本身只是第一步,把结果看懂、把图做出来才是真正决定论文能不能过关的关键。这篇就专门聊这个,用一套我自己一直在用的R脚本,5分钟搞定发表级火山图和热图,顺便把从结果解读到出图的坑全踩一遍给你看。
1. 拿到差异结果先别急着画图,花两分钟看懂这几列
很多人打开DESeq2的结果表格就懵了,baseMean、log2FoldChange、lfcSE、stat、pvalue、padj,六列数据谁跟谁是什么关系,完全不知道。其实你需要关注的只有三列:log2FoldChange、pvalue、padj。这三列就是你画火山图的所有数据来源,也是你筛选差异基因的标准。
log2FoldChange表示基因表达变化的倍数,取log2是为了让上调一倍和下调一倍在数值上对称。比如某个基因处理组比对照组表达量高了4倍,log2FoldChange就是2;低到原来的四分之一,就是-2。这个值只告诉你有变化,可不可信要看pvalue和padj。pvalue是统计学上的显著程度,差异分析里一般会做多重检验校正,校正后的就是padj,也是我们实际用来筛选的标准。padj越小越可信,通常以0.05为阈值。
具体的筛选逻辑是这样的:下调基因满足log2FoldChange <= -1 且 padj < 0.05,上调基因满足log2FoldChange >= 1 且 padj < 0.05,剩下的就是没有显著差异的基因。这个阈值不是死的,你可以根据自己实验的数据调,有的文章用padj < 0.01更严格,有的用log2FoldChange绝对值大于1.5甚至2,完全看你的数据量大小和文章需求。我给初学者一个建议:前期先用padj < 0.05和|log2FoldChange| > 1这个默认标准跑一遍,看看筛出来多少基因,如果太少(比如不到50个)或太多(比如上万个),再调整阈值。
还有一个很容易忽略的地方:padj列可能出现NA。这是因为基因本身的count数极低或者离散度估计有问题,DESeq2在计算时直接返回了NA。画图前一定记得过滤掉这些行,不然ggplot2会报错,或者图上出现一堆不正常的点。我用的是na.omit()直接剔除,简单粗暴,不影响结论。
# 读取DESeq2差异分析结果 res <- readRDS("dds_results.rds") res_df <- as.data.frame(res) # 过滤NA值,保留基因名 res_df <- na.omit(res_df) res_df$gene <- rownames(res_df) # 加一列差异类型标注 res_df$change <- ifelse(res_df$padj < 0.05 & abs(res_df$log2FoldChange) >= 1, ifelse(res_df$log2FoldChange > 0, "Up", "Down"), "NS") table(res_df$change)这步跑完,table()输出的三个数字就是你这次实验的上调基因数、下调基因数和不显著基因数。先记录下这个数,后面画完图用来核对,确保图上点的数量和表格里的数字对得上。
2. 环境准备:这些包装不上,后面全是白搭
画火山图和热图主要靠三个包:ggplot2、ggrepel、pheatmap。ggplot2画火山图、ggrepel用来给感兴趣的基因加标签避免文字重叠、pheatmap画热图。还有一个更省事的火山图专用包EnhancedVolcano,但它的参数封装得比较死,不如图自己控制来得灵活,所以我推荐还是用ggplot2自己画。这三个包都是R语言生态里用得最多的可视化工具,安装很简单,直接用install.packages()就能搞定。
如果你的R版本比较新,又是在比较干净的服务器环境上,装这些包一般不会出问题。但很多初学者卡在BiocManager::install("DESeq2") 这一步。DESeq2本身分析完就能导出结果,画图其实用不到DESeq2包了,但如果你还要重新跑分析,或者需要提取rlog/vst变换后的数据画热图,就绕不开它。安装时如果提示缺依赖,比如说缺RCurl、XML,先执行install.packages(c("RCurl", "XML"))再装DESeq2。
# 一次性安装所有需要的包 install.packages("ggplot2") install.packages("ggrepel") install.packages("pheatmap") install.packages("RColorBrewer") # 如果DESeq2还没装 if (!requireNamespace("BiocManager", quietly = TRUE)) install.packages("BiocManager") BiocManager::install("DESeq2")提示:服务器上如果运行时提示
library(X)找不到包,直接install.packages("X")即可,个别包提示需要编译,需要系统里有gcc。Windows用户建议直接装Rtools,macOS用户直接装Xcode Command Line Tools,不然原生安装会卡在看不懂的报错上。
3. 火山图:一张图看懂所有基因的变化趋势
火山图的美妙之处在于,它能把几万个基因的表达变化压缩在一张二维图上。横轴是log2FoldChange,越往右越上调,越往左越下调;纵轴是-log10(padj),越往上差异越显著。每个点代表一个基因,几万个点铺开来,整体形状像火山喷发,中间低两边高,所以叫火山图。
画图逻辑并不复杂:先确定一个画布,然后一层层叠加上去。基础代码很简单,但想画到能放进文章的水平,有几个参数必须调。
颜色:上调基因用红色,下调基因用蓝色,不显著用灰色。这是生命科学领域的通用配色,审稿人一看就懂。个别高分文章会用绿色表示下调,但红色和蓝色是默认选项,别搞创新。
透明度:几万个点叠在一起,最后全糊成一片黑。通过alpha参数把点的透明度降到0.5左右,重叠的区域会自然变深,单点也不会太抢眼。
阈值线:在x=1和x=-1处画虚线,在y=-log10(0.05)处画虚线。这三条线把图清晰地切分成四个区域,左上左下是显著下调,右上右下是显著上调,视觉冲击力一下就有了。
标签:圈出你真正关心的基因,比如你研究通路里的明星基因、表达量最高的Top基因。直接用geom_text会糊成一团,必须配ggrepel::geom_text_repel,它会自动把标签推开,避免重叠。
library(ggplot2) library(ggrepel) # 选一些要标注的基因,这里取差异最显著的Top10 up_genes <- res_df[res_df$change == "Up", ] down_genes <- res_df[res_df$change == "Down", ] top10_up <- head(up_genes[order(up_genes$padj), ], 5) top10_down <- head(down_genes[order(down_genes$padj), ], 5) label_genes <- rbind(top10_up, top10_down) p <- ggplot(res_df, aes(x = log2FoldChange, y = -log10(padj), color = change)) + geom_point(alpha = 0.5, size = 1.2) + scale_color_manual(values = c("Up" = "#E64B35", "Down" = "#3182BD", "NS" = "grey80")) + geom_vline(xintercept = c(-1, 1), linetype = "dashed", color = "grey40", linewidth = 0.5) + geom_hline(yintercept = -log10(0.05), linetype = "dashed", color = "grey40", linewidth = 0.5) + geom_text_repel(data = label_genes, aes(label = gene), size = 3, max.overlaps = 20) + labs(x = "log2(Fold Change)", y = "-log10(adjusted P-value)") + theme_classic(base_size = 14) + theme(legend.position = "top") ggsave("volcano_plot.png", p, width = 7, height = 6, dpi = 300)这里有个小细节,linewidth是新版ggplot2的参数,替代了旧的size。如果你用老版本,画线会报错unused argument,把它改成size = 0.5就行。ggsave输出PNG格式,dpi必须设置成300,这是期刊的最低要求,用默认72dpi的图投出去必然被编辑打回。
生成之后打开大图看一下几个关键参数:图上方有没有明显的“两翼”展开?正常样本的上调和下调基因数量应该大致均衡,如果一侧明显多于另一侧,有可能是样本分组出了问题,或者批次效应没有去除干净。这算是一个很有意思的“看图诊断”技巧。
4. 热图:差异基因表达模式一眼看穿
火山图告诉你哪些基因发生了显著变化,热图则告诉你这些变化在不同样本之间到底是什么样的模式。聚类热图的核心思想很简单:把差异基因按表达量展开成矩阵,行是基因、列是样本、颜色深浅代表表达高低,同时根据表达模式进行聚类——表达模式相近的基因聚在一起,样本表达谱相近的聚在一起。
第一步是数据准备。热图不能直接用DESeq2输出的原始count数画,因为count数和基因长度、测序深度都有关,不同基因之间不可比。必须用rlog变换或vst变换后的数据,这两个方法能把count数据的方差稳定化,让高表达和低表达的基因在热图上有可比性。
# 如果你有dds对象,可以直接提取rlog数据 rld <- rlog(dds, blind = FALSE) rld_mat <- assay(rld)第二步是确定基因集。把全部两万个基因全画进热图是灾难,主要是不显著基因会稀释模式。标准做法是取差异分析得到的显著差异基因。如果差异基因太多,取padj最小的前50或前100个,按padj排序取前N个,这样能保证热图上展示的都是最有代表性的基因。
# 按padj排序取top 50上调+top 50下调 sig_genes <- res_df[res_df$change != "NS", ] sig_genes <- sig_genes[order(sig_genes$padj), ] top_sig <- head(sig_genes, 100) # 取rlog矩阵中对应的基因 heatmap_mat <- rld_mat[rownames(rld_mat) %in% top_sig$gene, ]第三步是标准化。这一步非常关键,很多人画出来热图颜色一片红或一片蓝,原因就是没做标准化。不同基因本身的表达量基数不一样,有的基因平均表达量是几千,有的只有几十,如果不处理,高表达基因会把低表达基因的颜色完全压下去。标准做法是在热图包内对每一行做z-score标准化,即每个基因的表达值减去该基因所有样本的均值,再除以标准差。这样处理后,每个基因在所有样本中表达量均值变0,高低变化以标准差为单位,所有基因看图就公平了。
pheatmap里一行代码搞定:scale = "row"。
library(pheatmap) # 样本分组信息,替换成你自己的注释 annotation_col <- data.frame( group = factor(c(rep("Control", 3), rep("Treatment", 3))) ) rownames(annotation_col) <- colnames(heatmap_mat) pheatmap(heatmap_mat, scale = "row", clustering_method = "ward.D2", annotation_col = annotation_col, show_rownames = FALSE, show_colnames = TRUE, color = colorRampPalette(c("#3182BD", "white", "#E64B35"))(100), border_color = NA, fontsize_row = 8, width = 6, height = 8, filename = "heatmap_top100.png")热图里最容易踩的坑是列名的分组顺序和annotation顺序对不上。pheatmap在默认情况下会对样本列做聚类,这样一来样本顺序会乱掉。聚类的初衷是看样本是否按组聚在一起,但发表级热图一般更倾向于在列上不聚类,只聚类基因,列的顺序固定为Control组在前、处理组在后,这样审稿人看起来更直观。通过cluster_cols = FALSE可以停用列聚类,然后手动指定annotation_col的行名顺序来控制列排序。
另外,clustering_method默认是complete,但差异表达分析的热图用ward.D2效果更清晰——它是基于方差最小化的聚类方法,能把相似模式的基因分得更紧凑,条带更规整。想判断聚类方法是否合适,可以多试几种,看谁分的组内一致性更强。
5. 三个维度的热图进阶玩法:环形热图、聚类趋势图、富集条目图
把基础热图画熟之后,你会发现在实际投稿中,审稿人和编辑越来越喜欢“有信息量”的组合图形。最近在圈子里比较火的,就是“三个维度的热图”这个玩法——在一个图里同时展示表达谱、基因变化趋势和功能富集信息。它不是单一热图,而是一个复合图,通常左侧是传统聚类热图,中间附加展示各基因在不同样本间的表达趋势线,右侧再标注上这些基因富集到的功能条目。这样一张图下来,既能看到表达模式,又能看到趋势,还能知道这些基因在干什么,整体信息密度直接拉满。
这里推荐一个利器:ComplexHeatmap包。pheatmap能画基础热图,但画这种带多轨道注释和组合的复合热图,ComplexHeatmap才是正解。它是Bioconductor上的包,专攻复杂热图的绘制,可以很方便地在一个画布上叠加多个热图,并在热图侧面追加条形图、箱线图等注释。
环形热图则是另一种更炫酷的表达方式。把表达量矩阵映射到圆形坐标系上,基因按环形排布,样本按扇区区分,每个环代表一个样本或一个处理条件,可以直观呈现多维度的差异变化。它在展示时间序列或多组别对比时有天然优势,不过阅读门槛也高,用之前要想清楚是否真的符合你数据的展示逻辑。有的审稿人喜欢,有的觉得花哨。我的经验是:普通两组比较用矩形热图就够了,多组学数据或大型队列数据才考虑环形。
趋势分析也是很多高分文章的新宠。思路是:把差异基因按表达变化模式聚类成几类趋势——持续上升、持续下降、先升后降、先降后升、U型等。每一类趋势用一条折线或小图表示,附在热图右侧,等于把每个人的表达模式再汇总一次。这个在时间序列(0h、6h、12h、24h)或者发育阶段样本中特别好用,非常直观。R里做趋势聚类的工具有TCseq、Mfuzz,但如果你只想画出趋势图,用ggplot2按聚类类别画折线图就够了。
library(ComplexHeatmap) # 假设已经计算好了聚类分组信息,存在cluster_vector里 # 左侧是表达量热图 ht1 <- Heatmap(heatmap_mat, name = "Z-score", cluster_rows = TRUE, cluster_columns = FALSE, show_row_names = FALSE, col = colorRamp2(c(-2, 0, 2), c("#3182BD", "white", "#E64B35"))) # 右侧串联一个趋势图 trend_mat <- get_trend_matrix(heatmap_mat) # 自己封装函数计算每类基因均值 ht2 <- Heatmap(trend_mat, name = "Trend", cluster_rows = FALSE, cluster_columns = FALSE, column_names_side = "top", width = unit(2, "cm")) # 组合输出 draw(ht1 + ht2)需要注意,ComplexHeatmap的语法和pheatmap不太一样,比如颜色映射用的是colorRamp2而不是colorRampPalette,热图叠加用+号而不是放在同一个函数里。第一次用会不习惯,但画几次之后会发现它才是生信可视化的天花板。
6. 实战中躲不开的疑难杂症:从报错到出图,我把坑全踩了个遍
6.1 padj全是NA,差异基因数显示为0
最常见的一个情况,跑完DESeq2看结果,padj一列全是NA,差异分析结果让人崩溃。原因一般是过滤掉了太多低表达基因,或者数据量太小。DESeq2默认的独立过滤机制会在padj特别不显著时直接返回NA,这是正常现象,不一定是数据本身有问题。先看summary(res)输出的内容,如果提示“outliers”或者“low counts”的基因比例很高,就需要调整dds的过滤参数,或者在跑DESeq2之前过滤低表达基因时放宽标准。
6.2 热图的颜色出来是糊的
热图颜色不均匀,不是深蓝就是深红,过度生硬不柔和。原因大概率是z-score分布太极端,少数基因的表达量特别高,导致大部分基因的颜色都被压缩在一个很小的区间内。解决办法是把颜色映射改成非线性,比如用breaks参数把颜色梯度映射到数据的分位数上。pheatmap里可以直接传breaks参数,ComplexHeatmap中用colorRamp2配合自定义quantile值。
6.3 火山图的“V”字形不明显
如果你画出来的火山图没有明显的两边高中间低的形状,而是中间也特别多显著的点,就要看看你的padj阈值是不是设得太宽松了。阈值在0.05时,理论上会有很多非显著变化的基因被当成显著。另一个可能是你的padj没有真正做多重检验校正,用了原始的pvalue直接画图。画图前务必确认数据是DESeq2结果里的padj列,而不是pvalue列。
6.4 热图行数太多或者太少
如果你差出来的基因有3000个,全部画进热图,标签直接挤烂。此时裁到前100个或50个,靠的是padj排序,缺一步都会影响复现。如果筛选后只有20个基因,建议考虑缩小log2FoldChange阈值,或考虑合并处理组数据,将组内样本合并成均值,而不是全部样本都画上去。
6.5 热图的样本名顺序乱了
pheatmap默认对列也聚类。如果你需要固定顺序,提前设好cluster_cols = FALSE,再有针对性地传入annotation_col的行名顺序。很多初学在这里犯迷糊,明明注释数据是对的,画出来对不上,就是因为忘了列聚类会导致样本从原来的顺序重新排列。
提示:关于DESeq2结果文件读取,建议用
readRDS保存的dds对象,它能保留所有中间计算结果,方便后续随意提取normalized count矩阵、rlog矩阵、vst矩阵。如果你手里只有一张CSV表格,能用,但热图部分会非常受限,因为CSV里往往缺少包含样本名称关联的rlog变换矩阵。
7. 出图后的工作流:从R脚本到论文figure的全流程记录
最后完整过一遍从零到一的流程,这样你照着走就稳了。这套流程在我自己项目里跑了无数遍,每一步都是踩过坑之后沉淀下来的。
第一步,准备输入数据。你要有一个count矩阵(行是基因,列是样本)+ 一个样本信息表(至少包含样本名和分组)。用DESeq2跑出结果,保存dds对象:
# dds已经跑好的前提下 saveRDS(dds, "dds.rds") # 导出CSV形式的差异结果,方便用Excel查基因 res_df <- as.data.frame(results(dds)) write.csv(res_df, "DESeq2_results.csv")第二步,处理差异基因列表。用文章里第一部分提到的change列打标签的方式筛选上调和下调基因,统计数量,把列表存成CSV。这个CSV不仅是画图的数据源,也是后续做GO/KEGG富集分析的第一手输入。
第三步,画火山图。用ggplot2按文章第三节的代码跑一遍,输出300dpi的PNG和PDF各一份。PDF是矢量图,后续AI或Inkscape调整字体、改颜色、放大缩小都不会失真。
第四步,画热图。提取rlog矩阵,取显著差异基因的子集,用pheatmap输出。出图后仔细观察聚类树结构:同一组样本是否聚在一起?如果对照组和处理组在样本聚类上完全混在一起,说明分组之间的差异可能不是主要变异来源,需要重新审视实验设计。
第五步,进阶复合图。如果你的数据是时间序列或多组设计,可以继续用ComplexHeatmap做环形热图或组合富集热图。这里的富集信息来自你上一步做的GO/KEGG富集分析结果,可以用enrichplot包拿到富集矩阵,标注在热图右侧。
最后一步,参数统一。所有输出图上,字体大小、配色方案、边框风格要尽量统一,一篇文章里的图才是同一套体系。我个人偏好theme_classic()+ 基础字号14 + 红色/蓝色固定配色,整套图放在一起非常协调。
这个流程跑熟了之后,从拿到DESeq2结果到出完一套图,熟练的人30分钟内能搞定,新手第一次慢一点,一上午也足够走通全流程。关键在于理解每一步在干什么,而不是复制粘贴完就完事。等哪一天你拿到任何一套转录组数据都能不假思索地把这套流程跑起来,就已经完成了从“会用代码”到“会分析数据”的转变。