前阵子帮某课题组改一篇转录组文章,审稿人第一条意见就戳在痛处:WGCNA模块划分做了,但模块与性状的展示图太单薄,看不出共表达模块和实验设计之间的关联。这几乎是新手做加权基因共表达网络分析最常见的问题——流程跑通了、图片存了一堆,真正能放进文章支撑结论的“WGCNA模块与性状展示图”却没做完整,更没讲明白。
这篇东西打算把我在WGCNA模块与性状展示图上反复折腾的实操经验捋一遍。适合两类人:一类是刚跑完WGCNA、正对着一堆输出图发愁该用哪几张的人;另一类是想把图重绘得更专业、能直接投出去的人。我会把常用的展示类型、对应R代码、看图思路、以及容易踩的坑分开讲,尽量让看完的人能直接照着操作,而不是又堆一堆理论。
1. 展示图到底在展示什么:先想清楚证据链
1.1 评审逻辑:模块不是“画个圈”就完了
很多初学者跑完WGCNA,看到聚类树上一排颜色条就觉得任务完成了。但站在评审的角度,模块划分只是中间结果,真正要回答的问题是:你划分出的这些基因模块,跟哪个性状有关、相关多强、方向是正还是负、显著性够不够。
这就是模块与性状展示图存在的意义。它把“共表达模块”和“表型数据”两大块信息焊在一起,让你能从图上一眼判断:哪个模块是这篇研究的明星模块,应该继续往下做功能富集、找hub基因、做验证实验。没有这层证据,前面的聚类树只是表达了“基因之间有相似表达模式”,并不等于“这些模块对你的生物学问题有意义”。
1.2 一套完整的展示图体系包含哪些组件
WGCNA分析流程里,常见的展示图其实不止一张,它们各管一段证据:
- 聚类树加颜色条:说明模块划分的结构和规模。
- 模块-性状相关性热图:说明模块与表型的统计关联,是全文最核心的图。
- 模块显著性(GS)与模块成员(MM)散点图:说明模块内基因对表型响应的内部一致性。
- 模块特征基因(eigengene)表达趋势图:把模块从抽象的颜色块还原成可读的表达行为。
实际操作中,有的文章只放聚类树和热图,有的会再加一张散点图。我的建议是四类都做出来,哪怕正文只放其中两张,补充材料也可以放全套。这不仅是给自己留底牌,更重要的是,这些图共同构成一条完整的因果链条:先证明模块存在,再证明模块跟性状相关,再证明模块内基因的响应是协同的,最后展示模块在样本间的动态变化。缺了任何一环,审稿人都会追问。
2. 聚类树与颜色条:模块划分合理性的第一张证据
2.1 动态剪切树在做什么
WGCNA不会直接用普通层次聚类的结果当模块,而是基于不相邻度矩阵(1-TOM)做层次聚类,再用“动态剪切树算法”把树切成模块。所以聚类树的纵轴是基因之间的距离(越小越相似),不同颜色条代表不同模块,灰色代表没有被归入任何模块的基因。
有一个细节值得多说两句:软阈值β选完之后,邻接矩阵会做一次拓扑重叠(TOM)变换,再转成相异度矩阵。TOM度量的是两个基因“共享邻居基因的程度”,相比直接算表达相关,这种度量对噪声更稳健。用生活类比,两个基因不只是彼此表达相似,它们跟其他基因的“社交圈”也高度重合,才更可能属于同一个功能模块。
2.2 plotDendroAndColors代码与出图细节
假设你已经算完了网络,拿到了net对象,下面这段代码可以画出聚类树加颜色条:
library(WGCNA) options(stringsAsFactors = FALSE) # 假定 datExpr 是基因表达矩阵,行=基因,列=样本 # 假定 datTraits 是性状矩阵,行=样本,列=性状 allowWGCNAThreads() net <- blockwiseModules(datExpr, power = 6, TOMType = "unsigned", minModuleSize = 30, mergeCutHeight = 0.25, numericLabels = TRUE, verbose = 3) moduleColors <- labels2colors(net$colors) # 单个block时,直接用全部基因顺序绘图 sizeGrWindow(12, 9) plotDendroAndColors(net$dendrograms[[1]], moduleColors[net$blockGenes[[1]]], "Module colors", dendroLabels = FALSE, hang = 0.03, addGuide = TRUE, guideHang = 0.05, main = "")这里我特意打开了addGuide,在图上加了一条虚线参考线,方便你看出动态剪切的大致高度。hang = 0.03让树叶往下挂,画面更紧凑。单block情况是最常见的;如果样本量大、基因数多被自动分成多个block,就需要分别取net$dendrograms[[k]]和对应的blockGenes[[k]]循环画,再拼到一张长图里。
2.3 怎么看这张图有没有问题
拿到聚类树,我一般按三个标准快速体检:
- 灰色模块占比:灰色代表没被归类的基因。如果灰色基因太多,说明软阈值、minModuleSize或者mergeCutHeight的设置可能不合适,模块划分得太碎。理想状态下,灰色占比不至于过半。
- 模块数量与规模:几十个基因的模块可以接受,但如果出现只有五六个基因的模块,且这种小模块很多,就要考虑调大minModuleSize。
- 颜色条与树的对应是否混乱:正常情况下,一段连续树枝对应一个颜色条。如果颜色条频繁跳变,可能是mergeCutHeight太小,导致很多本应合并的模块被拆开。
还有个小经验:blockwiseModules默认把相关性接近的模块自动合并,mergeCutHeight = 0.25是常用值。如果你发现模块过多,可以适当把mergeCutHeight调大到0.3、0.35,模块会更干净;反之就调小。展示图阶段如果发现颜色条密密麻麻,多数不是绘图问题,而是参数问题,回头调整比事后修图更重要。
3. 模块-性状相关性热图:全文最常被引用的核心图
3.1 统计量的来源:模块特征基因与性状的相关系数
模块-性状相关性热图里的每一个格子,本质上是“模块特征基因(ME)”与某个性状之间的相关性。模块特征基因就是模块内所有基因表达谱的第一主成分,或者取平均表达值,相当于把整个模块抽象成每个样本上的一个综合表达值。
这里为什么要用ME而不是把所有基因都拿来算相关?因为模块内可能有几十上百个基因,如果逐个看相关性,信息量爆炸且不好归纳。用ME就等于问:这个模块的整体表达水平,跟性状的数值变化是不是同步。理解了这一点,图上的数字就有明确生物学含义:例如某个模块的ME与疾病严重程度呈正相关,可以推断该模块整体在疾病加重时被激活。
计算代码一般长这样:
nSamples <- ncol(datExpr) MEs <- orderMEs(net$MEs) moduleTraitCor <- cor(MEs, datTraits, use = "p") moduleTraitPvalue <- corPvalueStudent(moduleTraitCor, nSamples)use = "p"表示配对完整观测,遇到缺失值会自动排除对应样本对。corPvalueStudent是WGCNA自带函数,基于相关系数做t检验转换,给出P值。样本量少的时候,P值的波动会大,所以它只能作为参考,不能替代重复实验和大样本验证。
3.2 原生labeledHeatmap和pheatmap美化
WGCNA自带labeledHeatmap画热图,好处是能直接把相关系数和P值都写进格子里,坏处是样式古板、默认字体偏小。我一般先用原生函数快速看一眼结果,再用pheatmap重绘用于投稿。
# WGCNA原生版本 textMatrix <- paste(signif(moduleTraitCor, 2), "\n(", signif(moduleTraitPvalue, 1), ")", sep = "") dim(textMatrix) <- dim(moduleTraitCor) labeledHeatmap(Matrix = moduleTraitCor, xLabels = names(datTraits), yLabels = names(MEs), ySymbols = names(MEs), colorLabels = FALSE, colors = blueWhiteRed(50), textMatrix = textMatrix, setStdMargins = FALSE, cex.text = 0.5, main = "Module-trait relationships")下面是pheatmap版本,重点是加了星号标记,视觉上更贴近期刊习惯:
library(pheatmap) starMat <- matrix("", nrow = nrow(moduleTraitPvalue), ncol = ncol(moduleTraitPvalue)) starMat[moduleTraitPvalue < 0.05] <- "*" starMat[moduleTraitPvalue < 0.01] <- "**" starMat[moduleTraitPvalue < 0.001] <- "***" displayMat <- matrix(paste(sprintf("%.2f", moduleTraitCor), starMat, sep = ""), nrow = nrow(moduleTraitCor)) pheatmap(moduleTraitCor, cluster_rows = FALSE, cluster_cols = FALSE, color = colorRampPalette(c("#2166AC", "white", "#B2182B"))(100), display_numbers = displayMat, angle_col = 45, fontsize_number = 10, cellwidth = 30, cellheight = 22)这里我特意关闭了行列聚类。原因很实际:WGCNA的MEs在orderMEs之后已经按模块相关性排好序,性状也往往按实验设计顺序排列,再聚类反而会打乱读者的直觉。你要做的是保持模块顺序和聚类树的颜色顺序一致,便于交叉对照。
3.3 热图读取方法:R、P、方向、数量
看图先看行,也就是模块。每行对应一个颜色模块。再横向扫性状列,找出R的绝对值大、星号多、且不是灰色模块的那一格。
简单说,一个合格的“明星模块”要满足:跟至少一个重要性状的P值低于0.05,R的绝对值最好大于0.4;方向要跟生物学预期吻合。如果全图一片白(没有显著格子),问题往往出在性状设计与模块划分的匹配度上,而不是绘图本身。
还要小心一点:不要把多个高度相关的性状都塞进同一张热图。比如体重、体长、肝重这几个性状如果彼此强相关,会出现一整行全红或全蓝的“冗余相关”。这种格子不是假数据,但解释时要克制,最好在正文里只挑代表性性状重点讲,其余放补充材料。
4. GS与MM散点图:模块内部一致性的散点证据
4.1 两个指标的计算逻辑
基因显著性(GS,Gene Significance)度量单个基因与目标性状的相关强度。模块成员(MM,Module Membership)度量单个基因与它所在模块特征基因的相关性。计算时通常取绝对值,这样GS和MM都落在0到1之间:
trait <- datTraits$disease_stage geneSignificance <- abs(cor(datExpr, trait, use = "p")) moduleMembership <- abs(cor(datExpr, MEs, use = "p"))GS高说明这个基因的表达水平跟表型变化强相关;MM高说明这个基因是模块里的“自己人”,跟模块整体表达节奏一致。两者结合,你就有理由说:某模块里那些GS又高、MM又高的基因,是这个模块响应表型变化的核心基因。
4.2 画图和标注相关系数
挑一个你关注的模块,比如brown模块:
modGenes <- moduleColors == "brown" mmBrown <- moduleMembership[modGenes, "MEbrown"] gsBrown <- geneSignificance[modGenes] verboseScatterplot(mmBrown, gsBrown, xlab = "Module membership (MM)", ylab = "Gene significance (GS)", main = "Brown module: MM vs GS", pch = 20, cex = 1.2)画完之后,我习惯在图上补一个相关系数标注,用cor(mmBrown, gsBrown)算出来。很多文章会在图注里写“MM与GS呈显著正相关,r = 0.75,P < 2.2e-16”。这个相关系数本身也是模块质量的一个指标,比单独看图的形状更有说服力。
4.3 散点形态暗含的信息
正常情况下,GS与MM的散点不会是一团毫无规律的云,而是近似一个扇形或正相关的聚集区,越往右上角越密集、越接近一条上凸的曲线。这反映了一个基本事实:跟模块整体越“同心”的基因,越倾向于跟表型同方向变化。
如果散点图呈现出明显的两条分支,或者在高MM区域GS反而很低,就要警惕了。可能的情况有两种:一是这个模块混入了表型响应方向不一致的基因,二是模块本身包含多个子簇,分子功能并不纯粹。这种情况下,别急着在文章里下结论,先回到聚类树里看看这个模块的树枝是不是还能再切。
另外提一句:散点图的坐标轴用绝对值还是原始值,不同文章习惯不同。用绝对值的好处是方向统一、容易累加统计;缺点是丢失了正负号信息。如果模块跟性状是负相关,建议额外展示一个原始值的版本,说明负相关方向,否则容易让读者误以为模块是“上调”的。
5. 模块特征基因表达趋势图:把颜色块还原成动态表达
5.1 eigengene到底是什么
所谓模块特征基因(eigengene),就是模块内所有基因表达矩阵做主成分分析后的第一主成分。你可以把它理解成“这个模块在样本间最主流的表达变化曲线”。它比简单平均更稳健,因为它能抓住模块内部基因共同变异的主导方向,同时削弱个别离群基因的影响。
因此,ME趋势图能回答的问题和相关系数热图互补:热图告诉你模块与性状的统计关联有多强,趋势图则直观展示这个关联具体长什么样——是疾病组整体升高、恢复组回落,还是随时间稳步上升。
5.2 三种常见画法
第一种,直接看全部模块的表达热图,用pheatmap很快:
MEs_ordered <- orderMEs(net$MEs) pheatmap(t(MEs_ordered), cluster_cols = FALSE, cluster_rows = TRUE, color = colorRampPalette(c("#313695", "white", "#A50026"))(100), show_colnames = TRUE)第二种,挑重点模块画箱线图加散点,我推荐在发表级图里用:
library(reshape2) library(ggplot2) me_df <- data.frame(sampleID = rownames(MEs_ordered), MEbrown = MEs_ordered$MEbrown, MEblue = MEs_ordered$MEblue, group = sampleGroup) me_long <- melt(me_df, id.vars = c("sampleID", "group")) colnames(me_long) <- c("sampleID", "group", "module", "ME_value") ggplot(me_long, aes(group, ME_value, fill = module)) + geom_boxplot(alpha = 0.7, outlier.shape = NA) + geom_jitter(width = 0.15, size = 1, alpha = 0.6) + facet_wrap(~ module, scales = "free_y", nrow = 1) + theme_bw() + theme(legend.position = "none")第三种,更精细一点,把单基因表达趋势和模块ME一起画,双坐标或分面都行。这种方法适合讲单个明星基因,比如先展示某个hub基因在对照组和处理组间的变化,再展示它所在模块的整体趋势,证明“个别基因的变化不是孤例”。
5.3 趋势图的证据功能
趋势图在正文里往往放在热图之后。逻辑很顺:热图给出的相关性可能让人觉得是纯统计产物,趋势图则补上了生物学直观感受——这个模块在哪个条件下高、哪个条件下低,走势是否平滑、是否有离群样本。
如果趋势图显示模块在不同分组之间的差异并不明显,但热图却标了星号,通常问题出在相关系数被少数离群样本拉高。这时我建议你回去查样本,别让一两个异常样本撑起整张图。反过来,如果箱线图组间差异很清楚,而热图P值不显著,则可能是样本量太少,统计功效不足。这两类情况都需要在文章里如实处理,最好是补充样本或换用稳健统计方法。
6. 经验谈:展示图制作中容易被忽略的五个坑
6.1 性状矩阵的样本顺序错位
这是最高频的坑。几乎每个跑WGCNA的人都至少踩过一次:单独读入性状文件时,样本顺序跟表达矩阵不一致,然后cor(MEs, datTraits)照常算出结果,但配上的人名全是错的。我的习惯是读入之后立刻用match对齐一次:
sampleOrder <- match(rownames(datTraits), colnames(datExpr)) datTraits <- datTraits[sampleOrder, , drop = FALSE]别嫌这一步多余。你会感谢自己在画图前多写了这一行。
6.2 P值要不要做多重检验校正
模块-性状热图里经常有大量格子,如果模块有8个、性状有5个,就是40次相关性检验,纯按0.05阈值会期待约2个假阳性。这听起来不多,但万一你刚好挑中了假阳性模块继续做实验,代价就大了。往好了说,正文里至少要明确说明P值是未校正的P;往严谨了走,建议加BH校正后再做星号标注。对审稿人而言,“我们采用了Benjamini-Hochberg校正”这句话的观感远好于沉默。
6.3 模块颜色与调色板冲突
WGCNA的颜色名是字符串,比如“brown”“blue”“turquoise”。但默认颜色集有限,模块一多,后面会分配出“darkmagenta”这种少见色。如果你自已用RColorBrewer的Set3去重着色,很容易跟WGCNA默认颜色体系打架,导致图和聚类树对不上。
更隐蔽的问题是灰色模块。请务必确认你的展示图中没有把灰色当正常模块解读。灰色是未分配基因的垃圾桶,强行把它当模块解释是审稿人反感的低级错误。
6.4 小模块要不要展示
如果minModuleSize设成15,很容易跑出大量二三十个基因的小模块。这种模块做富集往往什么也富不出来,放进热图也只是凑格子。我的处理原则是:大于50个基因的模块作为主要分析对象;30到50的模块只在补充材料中展示;小于30的模块合并进灰色或者不看。这个阈值不是硬性规定,但能让展示图干净不少。
6.5 直接拿WGCNA原生图投稿的格式问题
WGCNA自带绘图函数的输出确实能用,但直接丢进文章会显得粗糙。常见问题有三个:字体过小、热图格子比例失衡、颜色过渡不平滑。投稿之前至少做一步:把图导出为PDF矢量格式,再在AI或Inkscape里调一遍字体和线宽。PPT拼图虽然方便,但导出位图后一旦被编辑改尺寸,细节会糊。
7. 进阶:用ggplot2重绘模块-性状图,让主图更“能打”
7.1 一个核心思路:先出数据,再画图
多次重绘之后我总结出一条思路:WGCNA的所有展示环节,最好先把相关系数、P值、MM、GS这些中间结果导出成干净的数据框,再用ggplot2自由发挥。这样做的最大好处是脱离WGCNA原生绘图函数的限制,配色、字体、分面、图注全都可以控制。
7.2 一张ggplot2版模块-性状热图的完整代码
下面这段代码是我常用的模板:
library(reshape2) library(ggplot2) library(scales) # 把相关系数和P值转成长格式数据框 cor_df <- melt(moduleTraitCor) p_df <- melt(moduleTraitPvalue) plot_df <- data.frame(module = cor_df$Var1, trait = cor_df$Var2, R = cor_df$value, P = p_df$value) plot_df$star <- cut(plot_df$P, breaks = c(-Inf, 0.001, 0.01, 0.05, Inf), labels = c("***", "**", "*", "")) plot_df$star <- as.character(plot_df$star) plot_df$label <- sprintf("%.2f %s", plot_df$R, plot_df$star) ggplot(plot_df, aes(trait, module, fill = R)) + geom_tile(color = "white", linewidth = 0.6) + geom_text(aes(label = label), size = 3.5, color = "grey20") + scale_fill_gradient2(low = "#2166AC", mid = "#F7F7F7", high = "#B2182B", midpoint = 0, limits = c(-1, 1), name = "Correlation") + theme_minimal(base_size = 12) + theme(axis.text.x = element_text(angle = 45, hjust = 1), panel.grid = element_blank()) + coord_fixed()建议把label中的星号和数值分开放:数值固定到两位小数,星号代表显著性。这样即使格子很小,读者也能先抓星号、再读数值。
7.3 组合图的版式建议
到了组图阶段,我会把聚类树颜色条、模块-性状热图、GS-MM散点图横向拼在一起:左边聚类树高度约整个横向宽度的四成,中间是热图,右边是散点图。这样做的好处是读者能从左到右依次完成“模块长什么样-模块跟谁相关-模块内部是否一致”的完整阅读路径。
几个细节值得花时间调:模块颜色条的图例要跟热图模块行一一对应,别出现颜色名和实际颜色不一致的情况;热图里的模块字体方向最好垂直或45度;整张图的字号不要小于8pt,审稿人年龄普遍偏大,这点很实在。
我自己的体会是:WGCNA模块与性状展示图做到这个程度,已经不只是“交作业”,而是真正在辅助你判断哪些基因值得下一步投入。与其花大量时间学各种花哨的可视化技巧,不如先把这张核心热图做扎实——数据对齐、P值策略想清楚、模块顺序摆合理。剩下的进阶调整,都是在给它上分。如果你正在做转录组项目,不妨把本文的代码跑一遍,应该能少走不少弯路。