WGCNA로 유전자 공발현 네트워크 분석하기 — 모듈부터 허브 유전자까지 (R 실전)

TL;DR — 차등발현⁠(differential expression) 분석이 유전자를 하나씩 본다면, WGCNA (weighted gene co-expression network analysis)는 함께 켜지고 꺼지는 유전자 무리를 모듈⁠(module)로 묶어 시스템 수준에서 봅니다. 워크플로는 한 줄로 요약됩니다 — vst 발현행렬 → soft-thresholding power 선택 → TOM (topological overlap matrix) → 모듈 검출 → 모듈 고유유전자⁠(module eigengene) → 형질⁠(trait) 연관 → 허브⁠(hub) 유전자.

이 글은 DESeq2의 vst (variance-stabilizing transformation) 출력을 입력으로, 복붙하면 그대로 도는 WGCNA 코드를 따라갑니다 — goodSamplesGenespickSoftThresholdblockwiseModulesmoduleEigengenes → module–trait 상관 → signedKME(허브) → exportNetworkToCytoscape. (코드는 WGCNA 공식 튜토리얼 기준입니다.)

🔗 관련 글  ·  같이 보면 좋은 RNA-seq 분석: DESeq2로 차등발현 분석  ·  p값과 FDR (다중검정보정)  ·  edgeR 차등발현 분석
그림 1. 발현행렬⁠(vst) → soft power → TOM → 모듈⁠(색) → module–trait → 허브로 이어지는 WGCNA 파이프라인.

0. 왜 WGCNA인가 — 1분 직관

차등발현 분석은 "처리군 대비 어떤 유전자가 올라갔나/내려갔나"를 유전자별로 묻습니다. 좋은 질문이지만, 수백 개 DEG 목록을 받아 들고 "그래서 이게 무슨 생물학이냐"에서 막히기 쉽습니다.

  • WGCNA는 질문을 바꿉니다 — "어떤 유전자들이 샘플 전체에서 발현 패턴을 공유하는가?" 같이 오르내리는 유전자는 같은 경로⁠(pathway)·같은 조절자⁠(regulator) 아래 있을 가능성이 큽니다.
  • 이렇게 묶인 무리가 모듈⁠(module)입니다. 각 모듈을 하나의 대표 패턴⁠(고유유전자)으로 요약하면, 수만 개 유전자 문제가 수십 개 모듈 문제로 줄어듭니다.
  • 모듈을 형질⁠(trait, 예: 처리 여부·시간·표현형)과 상관 내면, "이 형질과 연관된 유전자 집단"을 통째로 잡을 수 있습니다. 그 모듈 안에서 가장 중심에 있는 유전자가 허브⁠(hub) — 후속 검증의 1순위 후보입니다.
📌 한 줄 요약 — 개별 유전자⁠(차등발현)가 아니라 유전자 무리⁠(공발현 모듈)를 보고, 그 무리를 형질·허브로 해석한다.

1. 준비물 — 설치

WGCNA는 CRAN에 있지만 의존 패키지 일부⁠(GO.db, impute, preprocessCore)가 Bioconductor에 있어, Bioconductor로 한 번에 까는 편이 깔끔합니다. 입력 데이터를 만들 DESeq2도 함께 설치합니다.

if (!require("BiocManager", quietly = TRUE)) install.packages("BiocManager")
BiocManager::install(c("WGCNA", "GO.db", "impute", "preprocessCore", "DESeq2"))  # 최초 1회

library(WGCNA)
library(DESeq2)
options(stringsAsFactors = FALSE)   # WGCNA 권장 전역 설정

2. 입력 데이터 — 왜 vst 발현행렬인가

WGCNA는 상관⁠(correlation) 기반이라 분산이 안정화된 연속값이 필요합니다. 원시 카운트⁠(raw count)를 그대로 넣으면 안 됩니다 — 평균이 큰 유전자에 분산이 쏠려⁠(과대산포) 상관이 왜곡됩니다. DESeq2의 vst (또는 log2(x+1), rlog)로 변환한 값을 씁니다.

또 하나 중요한 제약 — WGCNA는 샘플 수가 충분해야 합니다⁠(상관을 안정적으로 추정하려면 보통 ≥15~20개, 많을수록 좋음). 샘플이 5~6개면 모듈 자체가 불안정하니 차등발현 쪽이 낫습니다.

# 예시: DESeq2 카운트 → vst. 실제로는 본인의 RNA-seq counts/coldata로 대체
dds <- DESeqDataSetFromMatrix(countData = counts,    # 유전자 × 샘플 정수 행렬
                              colData   = coldata,    # 샘플 메타데이터⁠(형질 포함)
                              design    = ~ condition)
dds <- dds[rowSums(counts(dds)) >= 10, ]             # 거의 안 읽힌 유전자 제거
vsd <- vst(dds, blind = TRUE)                        # 분산안정화 변환
datExpr0 <- t(assay(vsd))                            # ★ WGCNA는 '샘플 × 유전자'로 전치
dim(datExpr0)                                        # 예: 24 samples × 18000 genes
# 저분산 유전자 필터 — 변화 없는 유전자는 모듈 형성에 무의미하고 메모리만 잡아먹음
vars  <- apply(datExpr0, 2, var)
datExpr0 <- datExpr0[, vars > quantile(vars, 0.25)]  # 분산 하위 25% 컷⁠(상황에 맞게 조정)
⚠️ 방향 주의 — DESeq2/edgeR은 유전자 × 샘플이지만, WGCNA는 샘플 × 유전자입니다. t()로 전치하는 걸 빠뜨리면 이후 모든 단계가 어긋납니다.

3. goodSamplesGenes — 품질 점검⁠(QC)

분석 전에 결측·무분산 유전자와 이상⁠(outlier) 샘플을 솎아 냅니다. goodSamplesGenes()가 자동 점검을 해 주고, 샘플 군집⁠(clustering)으로 눈에 띄는 이상치를 봅니다.

gsg <- goodSamplesGenes(datExpr0, verbose = 3)
gsg$allOK                                # TRUE면 통과

if (!gsg$allOK) {                        # 문제가 있으면 불량 행/열 제거
  datExpr0 <- datExpr0[gsg$goodSamples, gsg$goodGenes]
}

# 샘플 군집으로 이상치⁠(outlier) 확인 — 혼자 멀리 떨어진 가지가 있으면 의심
sampleTree <- hclust(dist(datExpr0), method = "average")
plot(sampleTree, main = "Sample clustering — outlier check", cex = 0.7)
abline(h = 120, col = "red")             # 이 높이 위로 튀는 샘플을 잘라낼 수 있음
datExpr <- datExpr0                       # 통과한 행렬을 최종 datExpr로
 Flagging genes and samples with too many missing values...
  ..step 1
 allOK: TRUE

군집도에서 한 샘플만 유난히 멀리 떨어져 있으면 cutreeStatic()으로 그 가지를 제거합니다. 이상치 하나가 전체 상관 구조를 흔들 수 있어, soft power 선택 전에 처리하는 게 중요합니다.

4. pickSoftThreshold — soft-thresholding power 선택

WGCNA의 핵심 아이디어 — 상관을 그대로 쓰지 않고 거듭제곱⁠(power, β)으로 올려 약한 상관은 더 약하게, 강한 상관은 살립니다. 이렇게 하면 네트워크가 척도 없는 위상⁠(scale-free topology)에 가까워집니다⁠(소수 허브가 많은 연결을 갖는, 실제 생물학적 네트워크의 특성). 적절한 β를 고르는 게 pickSoftThreshold()입니다.

powers <- c(1:10, seq(12, 20, by = 2))         # 후보 power들
sft <- pickSoftThreshold(datExpr,
                         powerVector = powers,
                         networkType = "signed",  # 부호 있는 네트워크⁠(아래 설명)
                         verbose = 5)

sft$powerEstimate                              # R^2 >= 0.85 를 처음 넘는 power
head(sft$fitIndices)                           # power별 적합 지표
   Power  SFT.R.sq  slope  mean.k.   median.k.  max.k.
1      1     0.020   1.85   4200      4100        6800
...
6     12     0.870  -1.42    180       120         640    <- R^2 0.85 돌파
  • SFT.R.sq ( R² )이 0.85~0.90을 넘는 가장 낮은 power를 고릅니다. R²가 높을수록 척도 없는 위상에 잘 맞는다는 뜻입니다.
  • 동시에 mean.k. (평균 연결도, k )가 너무 낮아지지 않아야 합니다 — 너무 높은 power는 네트워크를 과하게 희소하게 만듭니다.
  • pickSoftThreshold가 적당한 power를 못 찾으면⁠(어떤 power도 R²=0.85 미달), 표본 수 기준 권장값을 쓰는 게 공식 FAQ의 조언입니다.
📋 샘플 수 기준 권장 power (공식 WGCNA FAQ) — 적합이 애매할 때 이 표를 따르세요.
샘플 수 unsigned / signed hybrid signed
< 20918
20–30816
30–40714
> 40612
signed 네트워크는 같은 R²에 더 높은 power가 필요해, unsigned의 약 2배입니다.
💡 signed vs unsignedunsigned는 상관의 절댓값만 봐서 양의 상관과 음의 상관을 한 모듈에 넣습니다. signed는 부호를 살려 같은 방향으로 움직이는 유전자만 묶습니다. 생물학적 해석이 깔끔해 요즘은 signed (또는 signed hybrid)를 권장합니다.

5. blockwiseModules — 네트워크 + 모듈 한 번에

이제 인접행렬⁠(adjacency) → TOM → 군집 → 모듈 검출을 한 번에 처리합니다. 한 줄짜리 래퍼 blockwiseModules()가 표준입니다. (단계를 직접 보고 싶으면 아래 "단계별" 박스 참고.)

net <- blockwiseModules(
  datExpr,
  power            = 12,            # 4단계에서 고른 soft power
  networkType      = "signed",     # pickSoftThreshold와 동일하게
  TOMType          = "signed",
  minModuleSize    = 30,           # 모듈 최소 유전자 수
  mergeCutHeight   = 0.25,         # 비슷한 모듈 병합 기준⁠(고유유전자 상관 0.75 이상)
  numericLabels    = TRUE,         # 모듈을 숫자로⁠(나중에 색으로 변환)
  saveTOMs         = FALSE,
  maxBlockSize     = 20000,        # 메모리 한도 — 유전자 수보다 크게
  verbose          = 3
)

table(net$colors)                  # 모듈별 유전자 수 (0 = 어디에도 안 묶인 회색)
moduleColors <- labels2colors(net$colors)   # 숫자 라벨 → 색 이름
   0    1    2    3    4    5    6
 410 3200 2100 1500  980  620  410   <- 모듈 0(grey)은 미할당 유전자

각 유전자는 하나의 모듈⁠(색)에 배정됩니다. 모듈 0(grey)은 어떤 모듈에도 들지 못한 유전자라 보통 해석에서 제외합니다. 모듈 덴드로그램은 plotDendroAndColors()로 봅니다.

plotDendroAndColors(net$dendrograms[[1]],
                    moduleColors[net$blockGenes[[1]]],
                    "Module", dendroLabels = FALSE,
                    main = "Gene dendrogram and module colors")
🔧 단계별로 보고 싶다면blockwiseModules 한 줄은 사실 이 과정입니다.
adj  <- adjacency(datExpr, power = 12, type = "signed")   # 상관^power
TOM  <- TOMsimilarity(adj, TOMType = "signed")            # 위상중첩⁠(TOM)
diss <- 1 - TOM                                           # 거리 = 1 - TOM
tree <- hclust(as.dist(diss), method = "average")         # 계층 군집
mods <- cutreeDynamic(tree, distM = diss,                 # 동적 가지치기
                      deepSplit = 2, minClusterSize = 30)
merged <- mergeCloseModules(datExpr, labels2colors(mods), # 비슷한 모듈 병합
                            cutHeight = 0.25)

TOM은 단순 상관을 넘어 "두 유전자가 이웃을 얼마나 공유하는가"까지 반영해 모듈 경계를 또렷하게 만듭니다.

6. moduleEigengenes — 모듈 고유유전자⁠(ME)

각 모듈을 하나의 대표 패턴으로 요약한 게 모듈 고유유전자⁠(module eigengene, ME) — 그 모듈 발현행렬의 제1주성분⁠(first principal component)입니다. 이제 "유전자 수만 개"가 아니라 "ME 수십 개"로 형질과 연관을 따집니다.

MEs0 <- moduleEigengenes(datExpr, colors = moduleColors)$eigengenes
MEs  <- orderMEs(MEs0)                  # 비슷한 ME끼리 정렬⁠(히트맵 보기 좋게)
head(MEs[, 1:4])                        # 샘플 × 모듈ME 행렬
        MEturquoise   MEblue   MEbrown   MEgreen
Sample1      0.18      -0.22     0.05      0.31
Sample2     -0.24       0.19    -0.11     -0.08
...

ME는 샘플마다 그 모듈이 얼마나 켜졌는지를 나타내는 한 개의 값입니다. 모듈 전체 발현을 한 숫자로 압축했으니, 형질과의 상관을 바로 계산할 수 있습니다.

7. module–trait 상관 — 형질과 모듈 연관

WGCNA의 하이라이트 — 각 ME형질⁠(trait)과 상관 내어, "어떤 모듈이 어떤 표현형과 묶이는가"를 한 장의 히트맵으로 봅니다. 형질은 coldata의 수치형 변수⁠(처리 0/1, 시간, 측정값 등)입니다.

traits <- as.data.frame(colData(vsd)[, c("condition_num", "time", "score")])
nSamples <- nrow(datExpr)

moduleTraitCor  <- cor(MEs, traits, use = "p")                 # ME ↔ 형질 상관
moduleTraitPval <- corPvalueStudent(moduleTraitCor, nSamples)  # 상관의 p값

# 셀에 "상관⁠(p값)" 텍스트를 얹은 라벨 행렬
textMatrix <- paste0(signif(moduleTraitCor, 2), "\n(",
                     signif(moduleTraitPval, 1), ")")
dim(textMatrix) <- dim(moduleTraitCor)

labeledHeatmap(Matrix = moduleTraitCor,
               xLabels = colnames(traits), yLabels = names(MEs),
               ySymbols = names(MEs), colorLabels = FALSE,
               colors = blueWhiteRed(50), textMatrix = textMatrix,
               setStdMargins = FALSE, cex.text = 0.7,
               main = "Module–trait relationships")
              condition_num    time    score
MEturquoise        0.82       0.21     0.74     <- 처리·score와 강한 양의 상관
                 (3e-06)    (0.32)   (4e-05)
MEblue            -0.79      -0.15    -0.68
                 (5e-06)    (0.48)   (3e-04)

빨강⁠(양)·파랑⁠(음)이 진하고 p값이 작은 셀이 형질과 강하게 연관된 모듈입니다. 위 예라면 turquoise 모듈이 처리·score와 강한 양의 상관 — 이 모듈을 파고들 가치가 있습니다.

그림 2. 위는 유전자 덴드로그램과 모듈 색띠, 아래는 module–trait 상관 히트맵 — 색 진하고 p 작은 셀이 핵심 모듈.

8. signedKME — 허브⁠(hub) 유전자 찾기

관심 모듈을 정했으면⁠(예: turquoise), 그 안에서 가장 중심에 있는 유전자 = 허브를 뽑습니다. 기준은 모듈 소속도⁠(module membership, kME) — 각 유전자가 자기 모듈 ME와 얼마나 상관되는가입니다. signedKME()로 계산합니다.

kME <- signedKME(datExpr, MEs)            # 모든 유전자 × 모든 모듈 kME
                                          # 열 이름 예: kMEturquoise, kMEblue ...

# turquoise 모듈 유전자만 골라 kME 높은 순으로
inMod  <- moduleColors == "turquoise"
kmeTur <- kME[inMod, "kMEturquoise"]
hubs   <- sort(kmeTur, decreasing = TRUE)
head(hubs, 10)                            # 상위 10개 허브 후보
 GeneA   GeneB   GeneC   GeneD   GeneE
 0.97    0.95    0.94    0.93    0.92    <- kME 높을수록 모듈 중심⁠(허브)
  • kME (module membership)가 1에 가까울수록 그 유전자는 모듈을 대표합니다 — 허브 후보입니다.
  • 한 줄로 각 모듈 최고 허브만 뽑으려면 chooseTopHubInEachModule(datExpr, moduleColors)도 있습니다.
  • 진짜 후보를 좁히려면 kME 높음 + 형질과의 유전자 유의성⁠(gene significance) 높음을 함께 보세요⁠(cor(datExpr, trait)).

9. exportNetworkToCytoscape — 네트워크 내보내기

허브와 모듈 구조를 Cytoscape에서 시각화하려면, 모듈 내부 TOM을 엣지⁠(edge)·노드⁠(node) 표로 내보냅니다.

# 관심 모듈 유전자들의 TOM만 다시 계산⁠(메모리 절약)
TOM <- TOMsimilarityFromExpr(datExpr, power = 12, networkType = "signed")
modGenes <- colnames(datExpr)[inMod]
modTOM   <- TOM[inMod, inMod]
dimnames(modTOM) <- list(modGenes, modGenes)

exportNetworkToCytoscape(
  modTOM,
  edgeFile   = "edges_turquoise.txt",
  nodeFile   = "nodes_turquoise.txt",
  weighted   = TRUE,
  threshold  = 0.10,            # 약한 엣지는 잘라 그래프 단순화
  nodeNames  = modGenes,
  nodeAttr   = moduleColors[inMod]
)

내보낸 edges_*.txt/nodes_*.txt를 Cytoscape에서 열면, 허브가 중앙에 모인 모듈 네트워크를 그릴 수 있습니다.

그림 3. soft power로 만드는 척도 없는 위상, TOM의 이웃 공유, ME로의 모듈 압축을 한 장으로.

10. WGCNA는 언제 쓰나 — 공발현 모듈 vs 차등발현

둘은 경쟁이 아니라 보완입니다. 차등발현으로 "무엇이 변했나"를, WGCNA로 "그 변화가 어떤 시스템에 속하나"를 봅니다.

항목 WGCNA (공발현 모듈) 차등발현⁠(DESeq2 / edgeR)
보는 단위유전자 무리⁠(모듈)개별 유전자
핵심 질문함께 움직이는 유전자는?그룹 간 변한 유전자는?
그룹 라벨불필요(비지도, 형질은 사후 연결)필수(처리 vs 대조)
입력vst / log2 발현행렬원시 카운트
샘플 수많이 필요(≥15~20)적어도 됨⁠(그룹당 ≥3)
출력모듈·ME·허브·형질 연관DEG 목록⁠(logFC·FDR)
강점시스템·경로 수준 해석, 허브 발굴명확한 효과 크기·유의성
한계표본 적으면 불안정, 인과 아님개별 유전자라 맥락 약함
  • WGCNA를 쓸 때 — 샘플이 많고⁠(시계열·코호트·다조건), 표현형과 연관된 유전자 모듈/허브를 찾고 싶을 때.
  • 차등발현을 쓸 때 — 명확한 2~몇 그룹 비교에서 개별 DEG와 효과 크기가 목적일 때.
  • 함께 쓰면 최고 — DEG로 후보를 잡고, WGCNA 모듈로 그 DEG들이 어떤 공발현 시스템에 속하는지 해석.
🧭 차등발현이 ""(개별 유전자)을 본다면 WGCNA는 ""(유전자 시스템)을 봅니다 — 같은 데이터, 다른 해상도.

11. 자주 발생하는 에러 & 해결

  • 모듈이 한두 개로 뭉치거나 회색⁠(grey) 천지 — 표본이 너무 적거나⁠(<15), soft power가 부적절. 표본을 늘리거나 FAQ 권장 power로.
  • pickSoftThreshold가 power를 못 찾음⁠(R² 안 올라감)배치 효과⁠(batch effect)나 강한 이상치 신호가 흔한 원인. 군집도로 확인하고 ComBat 등으로 보정 후 재시도. 그래도 안 되면 표본 수 기준 권장 power 사용.
  • 원시 카운트를 넣음 — 상관이 평균에 끌려 모듈이 깨집니다. 반드시 vst / rlog / log2 변환값을.
  • Error: cannot allocate vector of size ... — 유전자가 너무 많음. 저분산 유전자 필터링을 강화하거나 maxBlockSize를 메모리에 맞춰 조정⁠(블록 분할).
  • 모듈–형질 상관이 다 약함 — 형질이 수치형이 아니거나⁠(요인을 as.numeric으로), 모듈이 과병합됨⁠(mergeCutHeight 낮추기).
  • 방향⁠(전치) 실수t()를 빠뜨려 유전자 × 샘플로 넣으면 전부 어긋남. dim(datExpr)샘플 × 유전자인지 확인.

12. 확장 — 모듈 → clusterProfiler 농축분석

모듈은 "함께 움직이는 유전자 목록"이라, 그대로 기능 농축분석⁠(enrichment)의 입력이 됩니다. 관심 모듈 유전자를 clusterProfiler에 넘기면 어떤 GO·KEGG 경로가 모이는지 보입니다.

library(clusterProfiler)
library(org.Hs.eg.db)

modGenes <- colnames(datExpr)[moduleColors == "turquoise"]   # 모듈 유전자⁠(SYMBOL 가정)
ego <- enrichGO(gene = modGenes, OrgDb = org.Hs.eg.db,
                keyType = "SYMBOL", ont = "BP",               # 생물학적 과정
                pAdjustMethod = "BH", qvalueCutoff = 0.05)
head(ego)                                                     # 모듈에 모인 경로

여기까지 오면 흐름이 완성됩니다 — 공발현 모듈 → 형질 연관 → 허브 + 경로 농축. 모듈 하나하나가 하나의 가설이 됩니다.

🎓 다음 학습
(입력 만들기) DESeq2로 차등발현 분석 — vst 발현행렬의 출처 · edgeR 차등발현 분석
(QC·시각화) PCA로 샘플 QC — 이상치·배치 점검 · ComplexHeatmap 히트맵
(다음 단계) clusterProfiler 농축분석 — 모듈 → GO/KEGG 경로 해석

References

  1. Langfelder, P., Horvath, S. (2008). WGCNA: an R package for weighted correlation network analysis. BMC Bioinformatics, 9, 559.
  2. Zhang, B., Horvath, S. (2005). A general framework for weighted gene co-expression network analysis. Statistical Applications in Genetics and Molecular Biology, 4, Article 17. (soft-thresholding·scale-free·TOM)
  3. WGCNA Tutorials (Langfelder & Horvath, UCLA). goodSamplesGenes·pickSoftThreshold·blockwiseModules·moduleEigengenes·labeledHeatmap·signedKME·exportNetworkToCytoscape.
  4. WGCNA FAQ (Langfelder). 표본 수 기준 권장 soft power 표 · signed vs unsigned 네트워크 · scale-free 적합 실패 시 대처.
  5. Love, M. I., Huber, W., Anders, S. (2014). Moderated estimation of fold change and dispersion for RNA-seq data with DESeq2 (vst 변환). Genome Biology, 15, 550.
  6. Yu, G., et al. (2012). clusterProfiler: an R package for comparing biological themes among gene clusters. OMICS, 16(5), 284–287.

Pipette & Pipeline · A bio portfolio journal

이 글을 쓴 사람 Yumingming

생명융합공학과 박사과정.
Microbiome · Cosmetics · RNA Therapeutics · Bioinformatics를 공부하며,
실험(Wet Lab)과 데이터(Dry Lab)를 잇는 글을 논문(article) 기반으로 씁니다.

About · 더 알아보기 →

⚕️ 이 글은 학습·정보 제공 목적이며, 의학적 진단·치료·조언을 대체하지 않습니다. 건강·질병·치료에 관한 결정은 반드시 의사 등 전문가와 상의하세요. 자세한 내용은 면책조항을 참고해 주세요.