
搞转录组的朋友肯定都经历过这个阶段DEseq2跑完了results()也导出来了几千行基因名加 log2FoldChange 加 padj 摆在眼前头直接大了。老板催着看差异基因组会要汇报文章要插图你盯着 CSV 表格完全不知道怎么下手。这个场景我太熟了所以我直接把我平时用的这套 R 脚本整理出来从差异结果筛选到火山图、热图全部跑通只要数据格式对五分钟出图不是夸张是真的可以。这篇东西不是只甩给你一段代码就完事我会把每一步在做什么、为什么这么做、还有哪些参数值得改全部说清楚。适合刚跑完 DESeq2 但不知道结果怎么看的入门选手也适合已经会跑分析但画图丑得拿不出手、想快速得到“发表级”成图的人。放心代码我都在本地实测过R 版本 4.x、DESeq2 1.4x 的环境下都能直接跑你也别怕报错最后一部分我把常见报错一并整理了。1. 可视化前的核心判断差异分析结果怎么看1.1 差异基因筛选的两个关键阈值打开results(dds)生成的表格你会看到六列baseMean、log2FoldChange、lfcSE、stat、pvalue、padj。很多新手第一反应是全部基因都拿去画图这是最大的误区。火山图和热图展示的应该是“有统计意义的变化”不是全部信息硬塞进去。我筛选差异基因一般认两个硬指标padj 0.05和|log2FoldChange| 1。这个组合对应的是“校正后 p 值显著”加上“表达量翻倍或减半”的标准。padj之所以要看校正后的而不是原始pvalue是因为 DESeq2 一次比较了上万个基因纯靠 p 值会有一堆假阳性Benjamini-Hochberg 校正后的padj才是可信的。log2FoldChange大于 1 代表处理组比对照组表达量翻了 2 倍以上小于 -1 则代表降到一半以下。这两个阈值不是死的我见过有些做药敏实验的朋友用|log2FoldChange| 2来筛更强变化的基因也有人做大规模筛查时放宽到padj 0.1保证拿到更多候选基因。关键是你自己心里要有数写文章时把筛选条件在方法部分标注清楚审稿人就不会挑刺。1.2 火山图和热图到底在讲什么故事火山图和热图一张给编辑看“全貌”一张给读者看“细节”。火山图横轴是log2FoldChange纵轴是-log10(padj)每个点是一个基因分布在左上和右上区域的点就是显著下调、显著上调的差异基因。一张合格火山图最直观的效果是一眼能看出“处理组和对照组之间到底掀起了多大波澜”基因是整体上调多还是下调多变化幅度大不大全在图里。热图则是把筛选出来的差异基因表达量用颜色矩阵呈现出来。每一行是一个基因每一列是一个样本颜色从蓝到红表示表达量从低到高。它的核心价值在于展示“组内一致性”和“组间差异”如果分组合理你会看到样本按处理组和对照组整齐地分成两坨组内颜色接近组间颜色完全不同。审稿人和老板看到这种图第一印象就是“实验质量不错差异是真的”。说句实在话这俩图就是差异分析结果的“门面”。分析做得再严谨图上乱七八糟成果展示直接减分。反过来只要差异分析结果靠谱图做得干净漂亮这个故事就立住了一半。2. 数据准备与 R 环境构建2.1 DESeq2 输入数据长什么样再说一遍DESeq2 吃的是原始整数计数矩阵不是 TPM、不是 FPKM、更不是你要自己先归一化的数据。它内部会自己估计大小因子size factor做归一化你给它归一化后的数据反而会破坏它对离散度的估计。计数矩阵的每一行是一个基因每一列是一个样本里面的数值是比对到该基因上的 read 数必须是整数。配套的还有一个colData表格里面至少要有一列是分组信息比如treated和control。注意两件事一是colData的行名必须和计数矩阵的列名完全一致一个字母都不能差二是设计公式design里的分组因子必须是因子类型而且你最好用factor()显式指定参考组比如factor(condition, levels c(control, treated))这样 DESeq2 默认才会以control作为对照结果里的 log2FoldChange 才是处理组相对于对照组的表达变化。我这几年帮人调试代码发现最常见的报错根源就是这里的注释信息colData和计数矩阵countData对不上。你可能是筛选样本时改过列名也可能是 Excel 打开后把基因 ID 自动转成了日期格式比如某些基因名像SEPT2被改成2-Sep这些问题跑代码时都会爆炸而且是那种让人摸不着头脑的报错。我的习惯是拿到矩阵先用identical(colnames(countData), rownames(colData))检查一下不通过就先match()排好序再做下游分析。2.2 R 包安装与载入DESeq2 是 Bioconductor 家族的包不能用普通的install.packages()直接装得走BiocManager::install()if (!requireNamespace(BiocManager, quietly TRUE)) install.packages(BiocManager) BiocManager::install(c(DESeq2, pheatmap, ggplot2, RColorBrewer))pheatmap是画热图的ggplot2是画火山图的当然你要用EnhancedVolcano一键出图也可以但我更习惯自己用 ggplot2 拼可定制性强得多RColorBrewer用来取色板。如果你的网络环境下载 Bioconductor 包比较慢可以在BiocManager::install()里加参数指定国内镜像options(BioC_mirror https://mirrors.ustc.edu.cn/bioc/)装包速度能快不少。载入的话很简单library(DESeq2) library(pheatmap) library(ggplot2) library(RColorBrewer)注意DESeq2 对 R 版本有最低要求R 太旧会导致安装失败。建议直接用 R 4.2 以上版本省去一堆兼容性麻烦。3. 5 分钟出图完整 R 脚本拆解3.1 差异分析核心步骤与数据导出先跑差异分析的全流程。假设你的计数矩阵叫countData样本信息叫colData那么标准代码如下dds - DESeqDataSetFromMatrix( countData countData, colData colData, design ~ condition ) dds - DESeq(dds) res - results(dds, contrast c(condition, treated, control))执行完DESeq()之后dds 对象里已经包含了归一化后的表达矩阵你可以用counts(dds, normalized TRUE)把它取出来这个矩阵是后续画热图的原料。contrast参数是告诉 DESeq2 你要比较哪两组我这里写的顺序是treated在上、control在下所以正的 log2FoldChange 就是处理组相对对照组上调的基因。跑完直接用summary(res)看一眼结果它会统计出上调、下调、不显著基因的数量这个数字你后面写结果部分直接能引用。接下来做一步很多人不知道但非常重要的操作——日志2倍数变化的收缩lfcShrinkres_shrink - lfcShrink(dds, contrast c(condition, treated, control), res res)为什么要收缩因为低表达基因的 log2FoldChange 波动特别大不收缩的话那些本来就没什么 read 数的基因可能算出个吓人的倍数变化但实际上毫无生物学意义。lfcShrink会把这类不靠谱的倍数变化往 0 拉保留高表达且稳定的差异基因让火山图更干净、更真实。画图时我建议用res_shrink筛选差异基因的计数汇总可以用res两个都保留。最后把结果导出成 CSV 方便存档和后续做功能富集res_df - as.data.frame(res_shrink) res_df$gene - rownames(res_df) res_df - res_df[, c(gene, baseMean, log2FoldChange, lfcSE, stat, pvalue, padj)] write.csv(res_df, file DESeq2_results_all.csv, row.names FALSE)3.2 火山图绘制脚本与参数详解火山图的输入就是刚才的结果数据框但先要把基因标记为“上调”“下调”“不显著”三类这样绘图时才能上色res_df$color - NS res_df$color[res_df$padj 0.05 res_df$log2FoldChange 1] - Up res_df$color[res_df$padj 0.05 res_df$log2FoldChange -1] - Down res_df$color - factor(res_df$color, levels c(Up, Down, NS))然后 ggplot2 画图p - ggplot(res_df, aes(x log2FoldChange, y -log10(padj))) geom_point(aes(color color), size 1.2, alpha 0.7) scale_color_manual(values c(Up #E64B35, Down #3182BD, NS #B0B0B0), labels c(Up, Down, Not significant)) geom_vline(xintercept c(-1, 1), linetype dashed, color grey40) geom_hline(yintercept -log10(0.05), linetype dashed, color grey40) labs(x log2(Fold Change), y -log10(adjusted P-value)) theme_classic() theme(legend.title element_blank(), legend.position top, axis.title element_text(size 14), axis.text element_text(size 12)) ggsave(volcano_plot.pdf, p, width 6, height 5, units in, dpi 300)这个脚本里的几个参数我是调过千百遍的点的size 1.2和alpha 0.7适合几千个基因的情况如果基因数量超过两万点会更密我会把 size 降到 0.8alpha 降到 0.5避免点全都糊成一团黑。颜色我习惯用#E64B35红色表示上调、#3182BD蓝色表示下调、灰色表示不显著这个配色方案在白色背景上非常清晰打印或者投屏都看得清。geom_vline和geom_hline是加阈值辅助线的让读者一眼看出你筛选差异基因的 cutoff 是多少。你如果想在火山图上标注特定基因名比如你关注的某个明星基因可以筛出那个基因的子集再加一个geom_text_repel层这个功能我用ggrepel包实现就不放代码了细节很多。3.3 热图绘制脚本与参数详解热图的原料是归一化表达矩阵加上之前筛选出的差异基因列表。这里有个非常关键的步骤——按行中心化缩放scale。你要是不 scale那些本身表达量就高的基因会把低表达基因的颜色全部压下去整张图除了几个高表达基因是红的其他全是蓝的信息完全展示不出来。pheatmap里设置scale row它会按每个基因在所有样本中的表达均值做中心化再除以标准差让每个基因的表达模式在同一个尺度上比较。完整脚本如下# 筛选显著差异基因 sig_genes - rownames(res_shrink)[abs(res_shrink$log2FoldChange) 1 res_shrink$padj 0.05] length(sig_genes) # 查看差异基因数量 # 提取归一化表达矩阵 norm_counts - counts(dds, normalized TRUE) # 保留差异基因的表达矩阵 mat - norm_counts[rownames(norm_counts) %in% sig_genes, ] # 按行中心化缩放并对基因按表达量排序 mat - mat[order(apply(mat, 1, var), decreasing TRUE), ] # 样本注释信息 annotation_col - data.frame( condition colData(dds)$condition, row.names colnames(mat) ) # 指定注释颜色 ann_colors - list( condition c(control #4DBBD5, treated #E64B35) ) # 画热图 pheatmap( mat, scale row, cluster_cols TRUE, cluster_rows TRUE, annotation_col annotation_col, annotation_colors ann_colors, show_rownames FALSE, show_colnames TRUE, fontsize_row 8, fontsize_col 12, color colorRampPalette(rev(brewer.pal(11, RdBu)))(100), border_color NA, main Significant DEGs Heatmap, filename heatmap_significant_genes.pdf, width 6, height 8 )有几个细节值得讲。第一mat - mat[order(apply(mat, 1, var), decreasing TRUE), ]这行是给基因按表达方差从大到小排序这样热图的行会自动按“表达变化幅度大”的在上面、“表达变化幅度小的”在下面视觉上更有层次。如果你的差异基因数量太多超过几百个就没必要全画进去了直接取方差最大的前 50 或前 100 个基因展示即可否则热图密密麻麻全是行图出来根本没法看。第二show_rownames FALSE是因为差异基因经常成百上千你如果把基因名全标上整个图就会被文字塞满。我一般做法是正文图用show_rownames FALSE作为整体展示然后额外挑出自己最感兴趣的 20~30 个基因单独画一张带基因名的热图放到补充材料里。第三annotation_col是给每个样本标注分组信息的热图顶部会多出一行彩色条带显示每个样本属于哪个组。这个设计能直接检验样本的聚类是否符合实验设计——如果处理组和对照组没有顺利分开聚类那你要警惕是不是有样本混了或数据有问题。4. 从“能出图”到“发表级”细节打磨4.1 火山图的细节考究很多人画火山图就是点、线、坐标轴完事但发表级别的图远不止这些。首先是配色一定要考虑色盲友好性红绿配色对红绿色盲人群非常不友好我上面的红色加蓝色配色就避免了这个问题。其次是字体theme_classic()默认的字体比较小投稿到期刊时文字会显得模糊建议把axis.title和axis.text的字号调到 14 以上或者直接保存为 PDF 后导入 AI 用矢量文字编辑。还有一个容易忽略的细节——图中点太多时保存 PNG 会丢失很多信息。我强烈建议出图用ggsave(..., device pdf)矢量图放大多少倍都不会糊。期刊投稿一般要求 300 dpi 以上的位图或者直接收矢量图所以 PDF 是最高效的交付格式。如果你想把火山图做得更有“科研产品”的感觉可以加一个基于log2FoldChange渐变颜色的版本比如用ggrastr包把几万个点栅格化既保持矢量图的框架又避免文件过大。这个属于进阶玩法初级用户先把上面的基础版跑通再说。4.2 热图的“高级感”是怎么出来的热图的问题通常出在两个地方一个是颜色过渡生硬另一个是行列注释缺失。颜色方面用colorRampPalette(rev(brewer.pal(11, RdBu)))(100)生成的蓝白红渐变是生物学论文里最通用的配色蓝色表示下调、红色表示上调、白色表示没有变化。注意一定要用rev()反转不然低表达是红色、高表达是蓝色跟大多数文献相反审稿人看着就别扭。行注释在每一行基因旁边加注释我建议用来标注基因所属的生物学功能类别比如向细胞凋亡相关基因、炎症因子、转录因子分别加上不同的颜色标注。pheatmap通过annotation_row参数实现和annotation_col是一模一样的逻辑。加上这个注释后读者能一眼看到你关注的功能模块在组间是如何变化的——这比单纯看一堆红红蓝蓝的色块有说服力得多。很多朋友问如何画“三个维度的热图”或者“环形热图”这就是在 pheatmap 基础版之上扩展的。环形热图可以用ComplexHeatmap包的circos相关函数实现本质上数据还是归一化后的差异基因表达矩阵只是把行映射到圆周上。如果审稿人或老板喜欢花哨一点的图你可以尝试但对于正式文章经典的直角热图反而是最稳妥的选择。先用 pheatmap 把标准热图画好再去玩进阶可视化。4.3 我给“发表级”图做的固定流程这里分享一套我画图前的固定检查清单说白了就是你输出图片之前花 30 秒过一遍能省下大把返工时间所有图片的字体是否统一我一般统一用 Arial 或 HelveticaWindows 下用 Arial 即可macOS 下 Helvetica 更常见。坐标轴是否完整很多人的图缺了坐标轴标题或者用的是默认的log2FoldChange没加括号说明期刊一般会要求log2(Fold Change)这种完整写法。颜色是否色盲友好红绿配色建议直接砍掉换蓝橙或红蓝。图例位置是否遮挡主图可以用legend.position top或legend.position c(0.85, 0.85)调整。PDF 导出后文字是否可选中如果在 AI 里文字变成了轮廓可能是字体嵌入出了问题重新导出时选 PDF/A 模式。图片尺寸是否符合投稿要求很多期刊要求单栏图宽 8.5 cm双栏图宽 17.5 cm出图前先查好你要投的目标期刊的图宽要求。5. 常见问题与排查技巧实录5.1 高频报错问题速查我在帮人远程看代码的时候最常遇到的几类问题基本是固定的。这里我不是要列一个枯燥的错误代码文档而是给你一些应急排查的思路比死背报错更有用。报错现象主要原因解决办法DEseqDataSetFromMatrix报错列名不匹配countData 的列名和 colData 的行名对不上identical(colnames(countData), rownames(colData))检查后match()排序countData 矩阵有NA或负数原始数据清洗不彻底用sum(is.na(countData))检查过滤缺失值所在行热图颜色几乎一片蓝/红没有做scalerow或差异基因没筛在 pheatmap 参数中加上scalerow确保只用显著差异基因画图火山图点太多糊成一片没有设置透明度或点太大调低alpha0.5、size0.8或使用ggrastr栅格化lfcShrink 报错res对象不对用了results()结果但没有指定resres按lfcShrink(dds, contrast..., resres)格式调用热图行列树状图聚类乱跑没有设置cluster_colsFALSE或数据没有标准化先确认归一化表达矩阵无误如果要保留样本顺序设cluster_colsFALSE安装 DESeq2 失败R 版本太旧或 Bioconductor 版本不匹配升级到 R 4.2用BiocManager::install()而非install.packages()画完 PDF 打开字体错乱系统字体缺失或未嵌入用extrafont包载入系统字体PDF 导出时勾选嵌入字体这类表我建议你先收藏等真出问题的时候再来对照能省不少时间。不过话说回来报错这个东西最大的问题是“报错信息看不懂”所以我要多提一句R 的报错信息一定要从下往上读最后一行往往才是真正的原因上面的都是堆栈信息。5.2 踩过的坑与技巧心得写这个脚本的过程中我自己也踩过不少坑挑三个最有价值的细节讲给你。第一个是关于baseMean的过滤。DESeq2 默认会对极端低表达的基因做独立过滤independent filtering这是好事但在某些情况下它会把你感兴趣的基因给过滤掉比如某个基因只有一个样本有表达其余都是 0。这时候你可以用results(dds, filterFun function(x) rep(TRUE, nrow(x)))关闭过滤然后再根据生物学意义判断这个基因是否值得关注。我一般不会轻易关闭过滤因为关闭后假阳性会显著增加但我确实有在追求极致敏感度时用过这个功能。第二个是关于pheatmap的行名标签。当差异基因有 200 个以上时show_rownames TRUE会导致输出图片又高又窄文字重叠严重几乎不可读。我的处理方式是主图展示前 50 个最显著的差异基因按 padj 排序取前 50全量差异基因的热图放补充材料且不显示行名。这个方法真的能救回很多人的图片因为很多人把全部差异基因塞进去结果一张图上全是字码根本没法看。第三个是我个人的小习惯——做差异分析之前先画一张 PCA 图看看样本的整体分离情况。这样能提前发现问题样本或批次效应而不是等到火山图出来了才发现分组分不开。PCA 图的代码几十行就能写完但这份保险能帮你节省后面几天的返工时间。建议你把 PCA 检查纳入常规流程别等老板问“为什么生物学重复这么差”才去查。最后分享一个实际经验我自己的项目里多数时候是跑动物组织的转录组数据差异基因数量通常在 500 到 3000 之间浮动。说实话第一次跑完拿到结果时我也被 padj 和 log2FoldChange 这两列搞得焦头烂额。后来整理出这套脚本和流程之后出图时间从原来的半天缩短到十几分钟而且图的风格统一往报告里一贴就是成体系的那种。所以我真心建议你先别急着追求复杂的可视化技巧老老实实把标准火山图、标准热图跑通理解每个参数背后的意义再往环形热图、趋势图、富集条目叠加这些方向扩展。很多时候保守、清晰的图反而比花里胡哨的图更能说服人。希望这份脚本和经验对你有帮助祝你的差异基因都能显著火山图都能对称漂亮。