pheatmap으로 발현 히트맵 빠르게 그리기 — z-score·클러스터링·주석 (R 실전)

TL;DR — 발현 히트맵을 가장 빠르게 그리는 패키지가 pheatmap (pretty heatmaps)입니다. 핵심은 단 한 줄,
pheatmap(mat). 여기에scale = "row"(유전자별 z-score, 행 표준화)만 더하면 발현이 큰 유전자에 가려져 있던 패턴이 한눈에 드러납니다. 행·열 계층 클러스터링(hierarchical clustering), 샘플 그룹 색띠(annotation), 색·컷·저장까지 인자 몇 개로 끝납니다.이 글은 더미 발현행렬을 직접 만들어, 기본 히트맵 → 행 표준화 → 클러스터링 → 샘플 주석 → 색·
cutree·저장 순서로 복붙하면 도는 코드를 따라갑니다. pheatmap과 ComplexHeatmap의 차이, 자주 터지는 실수도 정리합니다. (코드는 pheatmap 1.0.12 기준입니다.)
🔗 관련 글 · 더 정밀한 히트맵이 필요하면: ComplexHeatmap으로 발현 히트맵 (고급) · 히트맵에 올릴 차등발현 결과는 DESeq2로 차등발현 분석 · 샘플 군집을 먼저 보려면 PCA로 샘플 QC

이 글에서 그릴 것
차등발현 분석이 끝나면 보통 다음 그림이 발현 히트맵입니다. 상위 변동 유전자 수십 개를 골라, 어떤 유전자군이 어떤 샘플에서 함께 오르내리는지 한 장으로 보여 주죠. R에서 이걸 가장 빠르게 그리는 도구가 pheatmap입니다 — 함수 하나에 인자 몇 개만 얹으면 z-score 히트맵에 주석띠까지 완성됩니다.
데모는 단순합니다. 대조군 3개·처리군 3개, 유전자 30개짜리 더미 발현행렬을 만들고(재현 가능하도록 시드 고정), scale = "row"로 유전자별 z-score를 매긴 뒤, 샘플 그룹을 색띠로 붙인 히트맵을 그립니다. 최종 결과물은 빨강(평균보다 높음)·파랑(낮음)으로 발현 패턴이 정리되고, 행·열이 클러스터링된 히트맵입니다.
library(pheatmap)
pheatmap(mat,
scale = "row", # ★유전자별 z-score — 발현 패턴 비교의 핵심
annotation_col = anno_col) # 샘플 그룹 색띠
0. 왜 행 표준화(scale = "row")인가 — 1분 직관
히트맵을 그릴 때 가장 흔한 실수가 스케일링을 건너뛰는 것입니다. 원시 발현값(vst 또는 log 발현)을 그대로 그리면, 절대 발현이 큰 유전자 몇 개가 색 범위를 독차지합니다. 항존유전자(housekeeping gene)는 늘 빨갛고, 정작 보고 싶은 저발현 전사인자의 미세한 변화는 죄다 같은 색으로 뭉개지죠.
행 표준화(z-score)는 이 문제를 해결합니다. 유전자(행)마다 평균을 빼고 표준편차로 나눠, 모든 유전자를 평균 0·표준편차 1의 같은 잣대로 맞춥니다. 그러면 절대 발현량은 사라지고 "이 샘플에서 이 유전자가 자기 평균보다 높은가 낮은가"라는 상대 패턴만 남습니다. 발현량의 크기가 아니라 패턴을 비교하는 게 히트맵의 목적이므로, 이게 핵심입니다.
pheatmap의 편한 점은 이 표준화를 scale = "row" 한 인자로 처리한다는 것입니다. (참고로 ComplexHeatmap에서는 t(scale(t(mat)))처럼 직접 z-score를 계산해 넘기는 경우가 많은데, pheatmap은 내부에서 알아서 합니다.)
1. 준비물 — 설치
pheatmap은 CRAN 패키지라 설치가 가볍습니다. 상위 변동 유전자를 고를 때 분산을 빠르게 계산하려면 matrixStats (rowVars)도 함께 있으면 편합니다.
install.packages("pheatmap") # 최초 1회
install.packages("matrixStats") # rowVars로 상위 변동 유전자 선택 (선택)
library(pheatmap) # 히트맵 함수
library(matrixStats) # rowVars (없으면 apply로 대체 가능)
pheatmap이 받는 입력은 유전자 × 샘플 수치행렬입니다. 행이 유전자, 열이 샘플이고, 값은 보통 vst (분산안정화 변환) 또는 log2 정규화 발현값입니다. 카운트 원값을 그대로 넣지는 않습니다 — 분포가 한쪽으로 쏠려 색이 무너지니, 먼저 정규화·변환을 거친 행렬을 씁니다.
2. 더미 데이터 만들기 + 상위 변동 유전자 고르기
실제로는 DESeq2의 vst()나 assay()에서 행렬을 가져오지만, 여기서는 재현 가능한 더미 행렬을 만듭니다. 대조군 3개·처리군 3개에, 일부 유전자는 군 간 차이가 나도록 설계합니다.
set.seed(42) # 재현성 고정
genes <- paste0("Gene", sprintf("%02d", 1:30))
samples <- c(paste0("Ctrl", 1:3), paste0("Trt", 1:3))
# 기본 발현 (log 스케일 가정): 평균 8, 약간의 노이즈
mat <- matrix(rnorm(30 * 6, mean = 8, sd = 1),
nrow = 30, dimnames = list(genes, samples))
# 앞쪽 10개 유전자는 처리군에서 상향 (군 차이를 일부러 부여)
mat[1:10, 4:6] <- mat[1:10, 4:6] + 2.5
dim(mat) # 30 × 6
mat[1:3, ] # 값 확인
[1] 30 6
Ctrl1 Ctrl2 Ctrl3 Trt1 Trt2 Trt3
Gene01 9.371 7.435 8.363 10.632 10.404 11.298
Gene02 7.435 8.116 7.773 10.111 10.231 10.512
Gene03 8.363 8.718 8.575 11.286 10.490 10.954
실전에서는 유전자가 수만 개라 전부 그릴 수 없습니다. 변동이 큰 상위 유전자(보통 50~100개)만 골라야 패턴이 보입니다. 분산(또는 표준편차) 기준으로 정렬해 위에서 잘라냅니다.
# 분산이 큰 순서로 상위 유전자 인덱스 (실전: top 50~100)
rv <- rowVars(mat) # matrixStats — 유전자별 분산
top <- order(rv, decreasing = TRUE)[1:20] # 데모라 20개만
mat_top <- mat[top, ]
# matrixStats가 없으면:
# rv <- apply(mat, 1, var)
rowVars로 정렬해 위쪽만 그립니다. 단, 변동 유전자 선택을 군 라벨을 보고 하면 안 됩니다(순환 논리) — 분산은 라벨과 무관하게 전체에서 계산합니다.3. 기본 히트맵 — pheatmap(mat)
먼저 아무 옵션 없이 그려 봅니다. pheatmap은 기본으로 행·열 모두 클러스터링하고, 값에 따라 색을 입힙니다.
pheatmap(mat_top) # 기본: 행·열 클러스터링 + 원시값 색
이 그림에는 문제가 있습니다. 스케일링을 안 했으니, 발현이 큰 유전자 행이 진하게 깔리고 작은 유전자는 흐릿합니다. 색이 절대 발현량을 따라가, 군 간 패턴이 잘 안 보이죠. 다음 단계에서 행 표준화를 켭니다.
4. 행 표준화 — scale = "row" (핵심)
scale = "row" 하나만 추가합니다. 이제 각 유전자(행)가 z-score로 변환돼, 자기 평균 대비 높낮이만 색으로 표현됩니다.
pheatmap(mat_top,
scale = "row") # ★유전자별 z-score (행 표준화)
이 한 줄로 그림이 완전히 달라집니다. 처리군에서 올린 앞쪽 유전자들이 일관되게 빨강, 대조군에서 파랑으로 뚜렷이 갈립니다. scale의 값은 세 가지 — "row"(유전자별, 발현 히트맵의 표준), "column"(샘플별, 드묾), "none"(원값 그대로, 기본값)입니다. 발현 패턴을 볼 때는 거의 항상 "row"입니다.
scale = "row"는 화면에 그릴 때만 z-score로 바꿔 보여 줄 뿐, 입력 행렬 자체를 바꾸지는 않습니다. 또 행에 NA나 표준편차가 0인 유전자(모든 샘플 값이 같음)가 있으면 z-score가 깨지니, 미리 걸러야 합니다.5. 클러스터링 제어 — cluster_rows / cluster_cols
pheatmap은 기본으로 행·열을 계층 클러스터링해 덴드로그램(dendrogram)을 그립니다. 거리·연결 방식을 바꾸거나, 클러스터링을 끌 수 있습니다.
pheatmap(mat_top,
scale = "row",
cluster_rows = TRUE, # 행(유전자) 클러스터링
cluster_cols = TRUE, # 열(샘플) 클러스터링
clustering_distance_rows = "correlation", # 상관 거리 (발현 패턴엔 흔히 유용)
clustering_distance_cols = "euclidean", # 유클리드 거리 (기본)
clustering_method = "complete") # 연결법: complete·average·ward.D2 등
행 클러스터링은 함께 오르내리는 유전자(공발현 유전자군)를 묶고, 열 클러스터링은 발현이 비슷한 샘플을 묶습니다. 발현 패턴의 모양을 보고 싶을 때는 거리를 "correlation"(1 − 상관계수)으로 두는 경우가 많습니다 — 절대 크기가 아니라 패턴의 유사도로 묶이기 때문입니다. 샘플 순서를 원본대로 두고 싶으면 cluster_cols = FALSE로 끄면 됩니다.
6. 샘플 주석 — annotation_col + annotation_colors
히트맵 위에 샘플이 어느 그룹인지 색띠를 붙이면 해석이 훨씬 쉬워집니다. 행 이름이 샘플 ID와 일치하는 데이터프레임을 만들어 annotation_col에 넘깁니다. 유전자(행) 쪽 주석은 annotation_row로 같은 방식입니다.
# 행 이름 = 샘플 ID (mat의 열 이름과 정확히 일치해야 함)
anno_col <- data.frame(
Condition = rep(c("Control", "Treated"), each = 3),
row.names = samples
)
# 주석 색 직접 지정 (선택)
anno_colors <- list(
Condition = c(Control = "#6BAB97", Treated = "#E76F51")
)
pheatmap(mat_top,
scale = "row",
annotation_col = anno_col, # 열 위 색띠 (샘플 그룹)
annotation_colors = anno_colors) # 색띠 색 지정
annotation_col의 행 이름이 행렬 열 이름과 어긋나면 주석이 통째로 안 붙거나 NA로 나옵니다 — all(rownames(anno_col) == colnames(mat_top))로 먼저 확인하세요. 여러 변수를 넣으면(예: Condition, Batch) 색띠가 여러 줄로 쌓입니다.

7. 색·컷·간격·저장 — 마감 손질
마지막으로 발표용 그림을 다듬습니다. 색 팔레트, 덴드로그램 자르기, 샘플 그룹 사이 간격, 그리고 파일 저장입니다.
# 파랑–흰–빨강 발산형 팔레트
my_color <- colorRampPalette(c("#2166AC", "white", "#B2182B"))(100)
pheatmap(mat_top,
scale = "row",
color = my_color, # 발산형 색 (z-score에 적합)
breaks = seq(-2, 2, length.out = 101),# 색 경계: −2~2로 고정 (이상치 포화 방지)
cutree_rows = 2, # 행 덴드로그램을 2개 군으로 절단
cutree_cols = 2, # 열도 2개 군으로 절단
show_rownames = TRUE, # 유전자 이름 표시 (많으면 FALSE 권장)
show_colnames = TRUE,
fontsize_row = 8, # 행 글자 크기
main = "상위 변동 유전자 z-score 히트맵")
각 인자의 쓸모는 이렇습니다. color + breaks는 z-score의 색 범위를 −2~2로 고정해, 극단값 하나가 색을 다 먹어 버리는 일을 막습니다. cutree_rows·cutree_cols는 덴드로그램을 k개 군으로 잘라 사이에 흰 간격을 넣어 군집을 시각적으로 분리합니다(클러스터링이 켜져 있어야 동작). show_rownames = FALSE는 유전자가 많아 이름이 겹칠 때 필수입니다. 군 사이를 수동으로 띄우려면 클러스터링을 끄고 gaps_col = c(3)처럼 위치를 직접 줄 수도 있습니다.
저장은 두 가지 길이 있습니다.
# 방법 A — filename 인자 (가장 간단, 확장자로 포맷 결정)
pheatmap(mat_top, scale = "row", annotation_col = anno_col,
filename = "heatmap.png", width = 6, height = 7)
# 방법 B — ggsave (pheatmap은 grid 객체라 $gtable을 넘김)
p <- pheatmap(mat_top, scale = "row", silent = TRUE)
ggplot2::ggsave("heatmap.pdf", p$gtable, width = 6, height = 7)
filename을 주면 화면에 띄우지 않고 바로 파일로 떨어집니다(확장자가 .png·.pdf·.tiff면 그 포맷). pheatmap의 반환값은 grid 그래픽 객체라, ggplot의 ggsave로 저장할 때는 $gtable을 넘깁니다.

8. 주요 인자 정리
| 인자 | 무엇을 하나 | 자주 쓰는 값 |
|---|---|---|
| scale | 행/열 표준화(z-score) | "row"(유전자별) · "none"(기본) |
| cluster_rows / cluster_cols | 행·열 계층 클러스터링 | TRUE(기본) · FALSE(원순서 유지) |
| clustering_distance_rows | 거리 척도 | "euclidean" · "correlation"(패턴) |
| clustering_method | 연결법 | "complete" · "average" · "ward.D2" |
| annotation_col / annotation_row | 샘플·유전자 색띠 | 행 이름이 ID와 일치하는 data.frame |
| annotation_colors | 색띠 색 지정 | 변수별 명명 벡터의 list |
| color | 히트맵 색 팔레트 | colorRampPalette(...)(100) |
| breaks | 색 경계(값 ↔ 색) | seq(-2, 2, length.out = 101) |
| cutree_rows / cutree_cols | 덴드로그램을 k군으로 절단 | 정수 (클러스터링 켜진 상태) |
| gaps_col / gaps_row | 수동 간격 위치 | c(3) (클러스터링 끈 상태) |
| show_rownames / show_colnames | 이름 표시 여부 | 유전자 많으면 show_rownames = FALSE |
| filename | 파일로 바로 저장 | "heatmap.png" (+ width·height) |
9. pheatmap vs ComplexHeatmap — 언제 무엇을
둘 다 발현 히트맵을 그리지만 결이 다릅니다. pheatmap은 함수 하나로 z-score·클러스터링·주석을 끝내는 빠르고 쉬운 도구라, 탐색 단계나 표준적인 발현 히트맵에 적합합니다. ComplexHeatmap (Gu et al. 2016)은 여러 히트맵을 가로로 잇고, 행을 임의 기준으로 분할하고, 복잡한 주석(막대그래프·점)을 얹는 정밀하고 유연한 도구라, 그림 한 장에 많은 정보를 담는 출판용 figure에 강합니다. 빠르게 패턴만 보려면 pheatmap, 정교한 조합 그림이 필요하면 ComplexHeatmap으로 넘어가면 됩니다(ComplexHeatmap 글 참고).
10. 자주 발생하는 에러 & 해결
- 고발현 유전자만 새빨갛고 나머지는 패턴이 안 보임 — 스케일링을 안 한 경우입니다.
scale = "row"를 켜 유전자별 z-score로 바꾸세요. 발현 히트맵의 1순위 점검 항목입니다. - 유전자 이름이 겹쳐 까맣게 떡짐 — 행이 너무 많은데
show_rownames = TRUE라서입니다.show_rownames = FALSE로 끄거나, 상위 변동 유전자를 더 적게(예: 50개) 고르세요. Error: 'x' must be numeric또는 NA로 색이 깨짐 — 행렬에 문자열 열이 섞였거나 NA가 있습니다.mat <- as.matrix(mat)로 수치행렬화하고,mat <- mat[complete.cases(mat), ]로 NA 행을 제거하세요. 표준편차가 0인 유전자(모든 값이 같음)도scale = "row"에서 NaN을 만드니 함께 거릅니다.- 주석 색띠가 안 붙거나 회색 NA로 나옴 —
annotation_col의 행 이름이 행렬 열 이름과 다릅니다.all(rownames(anno_col) == colnames(mat))가 TRUE인지 확인하세요(순서까지 일치해야 안전). cutree_rows가 무시됨 —cluster_rows = FALSE로 클러스터링을 끈 상태에서는 덴드로그램이 없어 자를 수 없습니다.cutree는 해당 축 클러스터링이 켜져 있을 때만 동작합니다.ggsave가 빈 파일을 만듦 — pheatmap 결과를 그대로 넘겨서입니다. 반환 객체의$gtable을 넘기거나, 가장 간단하게는filename =인자를 쓰세요.
• (정밀 버전) ComplexHeatmap으로 발현 히트맵 (고급) — 분할·복합 주석·다중 히트맵
• (샘플 구조 먼저) PCA로 샘플 QC — 히트맵 전에 군집·이상치·배치효과 점검
• (히트맵에 올릴 결과) DESeq2로 차등발현 분석 → clusterProfiler 농축분석
• (시각화 기초) ggplot2 기초 · RNA-seq 정규화란? — 히트맵에 넣을 값을 어떻게 만드나
References
- Kolde, R. (2019). pheatmap: Pretty Heatmaps. R package version 1.0.12. CRAN. (pheatmap 패키지·
scale·annotation_col·cutree) - Gu, Z., Eils, R., Schlesner, M. (2016). Complex heatmaps reveal patterns and correlations in multidimensional genomic data. Bioinformatics, 32(18), 2847–2849. (pheatmap 대비 정밀·복합 히트맵)
- Love, M. I., Huber, W., Anders, S. (2014). Moderated estimation of fold change and dispersion for RNA-seq data with DESeq2. Genome Biology, 15, 550. (vst — 히트맵 입력 변환)
- R Core Team. (2024). R: A language and environment for statistical computing. (
colorRampPalette·scale·hclust기본 함수)
Pipette & Pipeline · A bio portfolio journal
'Code Bench > R Snippets' 카테고리의 다른 글
| GSVA로 샘플별 경로 활성 점수 매기기 — 유전자에서 경로로 (R 실전) (0) | 2026.06.19 |
|---|---|
| R로 기초 통계 검정하기 — t-검정·ANOVA·상관·비모수 (R 실전) (0) | 2026.06.18 |
| dplyr·tidyr로 생물 데이터 정리하기 — 분석 전 90%는 데이터 정리 (R 실전) (0) | 2026.06.18 |
| tximport로 salmon·kallisto 정량 불러오기 — RNA-seq 카운트의 출발점 (R 실전) (0) | 2026.06.17 |
| ggplot2 기초 — 바이오 데이터 그래프 그리기 (산점도·막대·박스플롯, R 실전) (0) | 2026.06.17 |