# 一番簡単?な方法は`stats::heatmap()`を使う。
# パッケージをインストールする必要もない。
stats::heatmap(as.matrix(mtcars))はじめに
Rでヒートマップを作成するには、専用の関数やパッケージを使えばできる。
他のヒートマップ作成用のパッケージもある。
pheatmapComplexHeatmap
これらの関数を使用する代わりに、 ggplot2の扱いに慣れていればヒートマップをggplot2でも描くことができる。 ggplot2でヒートマップを描く利点は、 他のパッケージや関数の使用方法を新たに勉強する必要がないこと、 そして自分でプロットを細かくコントロールすることができることにある。 ここでは、ggplot2を使ってヒートマップを描く方法について、 いくつかのテクニックと共に紹介する。
Load packages
library(magrittr)
library(ggplot2)ヒートマップ作成の基本
まずはヒートマップを作成したいデータをdata.frameにする必要がある。 多くの場合、x軸あるいはy軸に遺伝子やサンプル、処理条件などを配置し、 各セルの色を何らかの値(遺伝子発現量など)にするだろう。
ここでは横軸をサンプル、縦軸を遺伝子、 セルの色を遺伝子の各サンプルにおける遺伝子の発現量としたヒートマップを描くことにする。
遺伝子発現データの準備
デモシナリオとして、何らかの処理区と非処理区のサンプルについて、 RNA-seqを行ない、得られたリードカウントデータを元にヒートマップを描くことを考える。 簡易的なシミュレーションで、擬似リードカウントデータを生成して、 プロット作成に使う。 何度かパターンを変えてヒートマップを作成したいので、簡単にデータが生成できるように、 データ生成用の関数を作っておき、それを利用する。
# 遺伝子発現量のdata.frameの元を準備
make_tbl_exp_table <- function(n_gene = 1000, n_rep = 3) {
if(n_gene < 1) stop("n_gene should be more than 1.")
DIGIT <- floor(log10(n_gene)) + 1L
if(n_rep < 1) stop("n_rep should be more than 1.")
tbl_plot <-
tidyr::crossing(
gene = sprintf(paste0("gene%0", DIGIT, "d"), seq_len(n_gene)),
condition = c("control", "treat"),
rep = seq_len(n_rep)
) %>%
dplyr::mutate(exp = NA) %>%
tidyr::pivot_wider(names_from = c(condition, rep), values_from = exp)
tbl_plot
}
make_tbl_exp_table()# A tibble: 1,000 × 7
gene control_1 control_2 control_3 treat_1 treat_2 treat_3
<chr> <lgl> <lgl> <lgl> <lgl> <lgl> <lgl>
1 gene0001 NA NA NA NA NA NA
2 gene0002 NA NA NA NA NA NA
3 gene0003 NA NA NA NA NA NA
4 gene0004 NA NA NA NA NA NA
5 gene0005 NA NA NA NA NA NA
6 gene0006 NA NA NA NA NA NA
7 gene0007 NA NA NA NA NA NA
8 gene0008 NA NA NA NA NA NA
9 gene0009 NA NA NA NA NA NA
10 gene0010 NA NA NA NA NA NA
# ℹ 990 more rows
# 基本の発現量を設定
mutate_base_exp <- function(tbl, seed = 777) {
set.seed(seed)
tbl %>%
dplyr::mutate(base = rnbinom(n = nrow(tbl), mu = 100, size = 1) + 1L,
.before = 2)
}
# 処理による発現変動(log2FC)を設定
mutate_log2fc <- function(tbl, seed = 777) {
set.seed(seed)
tbl %>%
dplyr::mutate(log2fc = rnorm(n = nrow(tbl), sd = 2), .after = base)
}
# 基本の発現量と発現変動量をもとに、各遺伝子の各サンプルにおける発現量を計算
assign_expression <- function(tbl, seed = 777) {
set.seed(seed)
tbl %>%
dplyr::mutate(dplyr::across(dplyr::starts_with("control"), \(x) {
purrr::map2_int(x, base, ~ rnbinom(n = 1, mu = .y, size = 1/.2))
})) %>%
dplyr::mutate(dplyr::across(dplyr::starts_with("treat"), \(x) {
purrr::map2_int(x, (2^log2fc)*base, ~ rnbinom(n = 1, mu = .y, size = 1/.2))
}))
}
# 関数を元に発現量のdata.frameを作成
tbl_exp <-
make_tbl_exp_table(n_gene = 200, n_rep = 5) %>%
mutate_base_exp() %>%
mutate_log2fc() %>%
assign_expression()
tbl_exp# A tibble: 200 × 13
gene base log2fc control_1 control_2 control_3 control_4 control_5 treat_1
<chr> <dbl> <dbl> <int> <int> <int> <int> <int> <int>
1 gene0… 87 3.78 117 43 48 110 146 1152
2 gene0… 212 -3.41 223 270 121 159 215 15
3 gene0… 157 0.617 196 274 106 195 356 553
4 gene0… 33 -0.259 16 25 25 50 23 36
5 gene0… 47 2.19 117 44 73 70 29 187
6 gene0… 73 0.788 85 46 33 126 113 122
7 gene0… 31 2.61 56 24 63 29 29 209
8 gene0… 210 2.33 133 186 194 251 160 2040
9 gene0… 34 -0.639 65 21 46 31 38 37
10 gene0… 83 2.17 28 98 102 84 100 273
# ℹ 190 more rows
# ℹ 4 more variables: treat_2 <int>, treat_3 <int>, treat_4 <int>,
# treat_5 <int>
最低限のヒートマップ作成
ここまで準備できたら、最低限の設定をしたヒートマップを作成していく。 基本的なコンセプトは非常に単純、
- 必要があれば
ggplot2で扱いやすいようにdata.frameを変形、変数(列)を追加 - タイル(
geom_tile)を選択して、横軸にサンプル、縦軸に遺伝子、色に発現量を指定する。
たったこれだけである。 ここでは、tidyr::pivot_longer()でdata.frameを変形して、 ggplot()でx軸にサンプルの列、y軸に遺伝子IDの列を指定し、 geom_tile()でセルの色に発現量の列を指定していく。
gp <-
tbl_exp %>%
tidyr::pivot_longer(
cols = !c(gene, base, log2fc),
names_to = "sample",
values_to = "readcount"
) %>%
ggplot(aes(x = sample, y = gene)) +
geom_tile(aes(fill = readcount + 1))
gpあとは、必要に応じてヒートマップの体裁を整えていく。
## 横軸のタイトルを消去
(gp <- gp + labs(x = ""))## 発現量がリードカウントなので、log変換して色分け
(gp <- gp + scale_fill_continuous(trans = "log10"))## ヒートマップの色を変更
(gp <- gp + scale_fill_gradient(trans = "log10", low = "grey90", high = "red"))Scale for fill is already present.
Adding another scale for fill, which will replace the existing scale.
## ヒートマップの余白&縦軸のラベル削除
gp +
scale_x_discrete(expand = expansion(0)) +
scale_y_discrete(expand = expansion(0)) +
theme(
axis.ticks.y = element_blank(),
axis.text.y = element_blank()
)(さらに)ヒートマップの体裁を整える
ggplot2でヒートマップを作成すれば、その体裁の整え方はggplot2のやり方になる。 先ほどと同様のデータに少し手を加えて、さらに体裁を整えてみる。
変動パターンを強調する
先ほどまでは色分けにリードカウントデータを使っていたが、 これだと発現が低い遺伝子ではパターンの変化が見分けにくい。 そこで、遺伝子のサンプル間における発現量の変化に注目するために、 リードカウントをRPM補正して対数変換し、 遺伝子ごとに正規化(Z変換)してヒートマップを描く。 また、サンプルの条件(対照区か処理区か)でヒートマップを区切ってみやすくする。
このような変更をggplot2だけで行うのは大変なので、 作図用のデータに手を加える(新たに列を加える)ことで対応する。
# データの修正
tbl_plot <-
tbl_exp %>%
# readcountをRPM補正する
purrr::modify_if(is.integer, ~ 1e6 * .x / sum(.x)) %>%
tidyr::pivot_longer(
cols = !c(gene, base, log2fc),
names_to = "sample",
values_to = "RPM"
) %>%
# サンプル条件の列とリピートの列を加える
dplyr::mutate(
condition =
stringr::str_extract(sample, "control|treat") %>%
stringr::str_to_title(),
rep = paste("Rep.", stringr::str_extract(sample, "[12345]"))
) %>%
# 遺伝子ごとに対数変換したRPMのZ-scoreを計算する。
dplyr::mutate(Zscore = scale(log(RPM + 1))[,1], .by = gene)
tbl_plot# A tibble: 2,000 × 8
gene base log2fc sample RPM condition rep Zscore
<chr> <dbl> <dbl> <chr> <dbl> <chr> <chr> <dbl>
1 gene001 87 3.78 control_1 5900. Control Rep. 1 -0.492
2 gene001 87 3.78 control_2 2053. Control Rep. 2 -1.54
3 gene001 87 3.78 control_3 2602. Control Rep. 3 -1.31
4 gene001 87 3.78 control_4 5691. Control Rep. 4 -0.528
5 gene001 87 3.78 control_5 7146. Control Rep. 5 -0.301
6 gene001 87 3.78 treat_1 25424. Treat Rep. 1 0.961
7 gene001 87 3.78 treat_2 13473. Treat Rep. 2 0.330
8 gene001 87 3.78 treat_3 14125. Treat Rep. 3 0.377
9 gene001 87 3.78 treat_4 35484. Treat Rep. 4 1.29
10 gene001 87 3.78 treat_5 32577. Treat Rep. 5 1.21
# ℹ 1,990 more rows
# 修正したデータを使ってヒートマップを作図
plot_heatmap <- function(tbl) {
tbl %>%
ggplot(aes(x = rep, y = gene)) +
geom_tile(aes(fill = Zscore)) +
# ここは基本編とおなじ
labs(x = "", y = "") +
scale_x_discrete(expand = expansion(0)) +
scale_y_discrete(expand = expansion(0)) +
# 以下は変更
scale_fill_gradient2() +
facet_grid(cols = vars(condition)) +
theme_linedraw(base_size = 12) +
theme(
axis.ticks = element_blank(),
axis.text.y = element_blank(),
panel.border = element_rect(color = "black", fill = NA),
strip.text = element_text(color = "black", face = "bold"),
strip.background = element_rect(color = "black", fill = NA)
) +
guides(fill = guide_colorbar(
title.theme = element_text(angle = 270),
title = "Z-score transformed RPM",
title.position = "right",
label.position = "left",
barheight = unit(5, "cm"),
barwidth = unit(.3, "cm")
))
}
tbl_plot %>% plot_heatmap()ヒートマップの並べ替え
また、遺伝子の順番を何らかの基準で並べ替えることもできる。
ggplot2では離散変数(文字列、character型)の並びは明示的に決めていない場合、 辞書順で因子型(factor型)に変換される。 つまり何も指定しなければ辞書順になる。 そこで、順番を変えたいcharacter型の変数(列)をfactor型に変更し、 明示的に順番を指定すれば良い。 こうした作業にはforcatsパッケージの関数が便利である。
まずは、簡単に発現変動量の値を元に並べ替えをしてみる。
階層型クラスタリングによる並べ替え
次に、発現パターンを元にクラスタリングを行って、その結果を使って並べ替える。 まずは階層型クラスタリングを行なってみる。 階層型クラスタリングは各要素間の距離をもとに、 アルゴリズムを用いて最も近い要素同士を 一つのクラスタにまとめる作業を繰り返すことで達成される。 Rにはデフォルトで階層型クラスタリングを行うhclust()関数があるのでこれを使うことができる。
hclust()でクラスタリングを行うためには、遺伝子間の発現パターンを元に距離を計算して、距離行列を作成する必要がある。 遺伝子の発現パターンを比較するには相関係数が良いと思うが、 Rの元々ある距離行列を求めるdist()関数ではこれは計算することができない。 そこで、amapパッケージのamap::Dist()関数を用いる。
# 階層型クラスタリング用のdata.frameの準備
df_data <-
tbl_plot %>%
dplyr::select(gene, sample, Zscore) %>%
tidyr::pivot_wider(names_from = sample, values_from = Zscore) %>%
tibble::column_to_rownames("gene")
head(df_data) control_1 control_2 control_3 control_4 control_5 treat_1
gene001 -0.49175909 -1.5416281 -1.3062846 -0.5276727 -0.3012835 0.96135918
gene002 1.01093560 1.0873486 0.7094569 0.8361411 0.9737652 -0.95850613
gene003 0.36384710 0.7501150 -0.3834417 0.3920481 1.1445993 0.65420752
gene004 0.09913786 0.5968042 0.7577385 1.5796809 0.5221213 0.07945511
gene005 0.80995359 -0.6064859 0.2616918 0.1403465 -1.1444680 0.31950414
gene006 0.38003915 -1.0725665 -1.5189545 1.2910079 0.9336676 -0.63042216
treat_2 treat_3 treat_4 treat_5
gene001 0.3295962 0.3766025 1.2930546 1.20801542
gene002 -0.8566026 -0.7540475 -1.4278261 -0.62066516
gene003 0.3846542 -2.1903781 -1.1486152 0.03296381
gene004 -0.9335277 -1.6406241 0.2311051 -1.29189111
gene005 1.2626272 1.1648417 -0.3666657 -1.84134544
gene006 1.3299753 0.3279186 -0.6690319 -0.37163340
# 距離として相関係数、アルゴリズムはWard法を使って階層型クラスタリング
hc <-
df_data %>%
amap::Dist(method = "correlation") %>%
hclust(method = "ward.D2")
# クラスタリングした順序を用いてヒートマップを並べ替え
gene_order <- hc$labels[hc$order]
tbl_plot %>%
dplyr::mutate(gene = forcats::fct_relevel(gene, gene_order)) %>%
plot_heatmap()階層型クラスタリングでは、特定のクラスタ数になるように木をある高さで切ることで、 各遺伝子をそれぞれのクラスタに分けることができる。 この操作はcutree()関数で行うことができる。 cutree()でクラスタに分割して、 その情報をもとにクラスタで分けられたヒートマップを作成してみる。
cut_into_cluster <- hc %>% cutree(3)
tbl_plot %>%
dplyr::mutate(cluster = cut_into_cluster[gene]) %>%
dplyr::arrange(cluster) %>%
dplyr::mutate(gene = forcats::fct_inorder(gene)) %>%
plot_heatmap() +
facet_grid(rows = vars(cluster), cols = vars(condition),
scales = "free", space = "free") +
theme(panel.spacing = unit(.5, "mm"))K-meanクラスタリングによる並べ替え
よく使われる非階層型クラスタリングとして、K-mean法によるクラスタリングがある。 Rではkmeans()関数で簡単にクラスタリングできるので、 これを使ってヒートマップをクラスタごとに分割してみる。 また、kmeans()で得られたクラスタの順序はお互いに関連がないので、 各クラスタの中心を元に階層型クラスタリングを行い、 クラスタの並び替えを行う。
set.seed(777)
res_kmean <-
df_data %>%
kmeans(centers = 8)
hc <- hclust(amap::Dist(res_kmean$centers, method = "correlation"))
tbl_plot_w_km_cluster <-
tbl_plot %>%
dplyr::mutate(
cluster =
res_kmean$cluster[gene] %>%
as.character() %>%
forcats::fct_relevel(hc$labels[hc$order])
) %>%
dplyr::arrange(cluster) %>%
dplyr::mutate(gene = forcats::fct_inorder(gene))
tbl_plot_w_km_cluster %>%
plot_heatmap() +
facet_grid(rows = vars(cluster), cols = vars(condition),
scales = "free", space = "free") +
theme(panel.spacing = unit(.5, "mm"))ヒートマップをさらに改造
ここまでは階層型クラスタリングに用いたamapパッケージを除いて、 Rをインストールした際に最初からインストールされているパッケージや、 tidyverseのパッケージ群を用いてヒートマップの作図を行なってきた。
これらのパッケージに加えて、 ggplot2の機能をさらに拡張することができるパッケージが多く開発されている。 ここからは、そうした追加のパッケージを用いて、 さらに凝った体裁のヒートマップを作図する方法を紹介する。
ネストされたfacet
ggh4xパッケージを使うと、ネストされたfacetを設定することができる。 これまではリピートをx軸に設定していたが、 “処理条件の中の各反復”という入れ子(ネスト)を、facetとして表現することができる。 今回の例ではそれほどネストさせる意味はないが、 たとえば異なる処理をしたサンプルを経時的に計測したデータなどでは、 ネストされたfacetでヒートマップを作成するとみやすいかもしれない。
gp_hm <-
tbl_plot_w_km_cluster %>%
plot_heatmap() +
facet_grid(rows = vars(cluster), cols = vars(condition),
scales = "free", space = "free", switch = "y") +
theme(
strip.text = element_text(color = "black", face = "bold"),
strip.text.y.left = element_text(color = "black", face = "bold", angle = 90),
strip.background = element_rect(color = NA, fill = NA),
panel.border = element_rect(color = NA, fill = NA),
panel.spacing.x = unit(1.5, "mm"),
panel.spacing.y = unit(1.5, "mm")
)
gp_hm_nested <-
gp_hm +
ggh4x::facet_nested(cols = vars(condition, rep), rows = vars(cluster),
scales = "free", space = "free", nest_line = element_line(), switch = "y") +
theme(
axis.text = element_blank(),
ggh4x.facet.nestline = element_line(colour = "black", linewidth = 1)
)
patchwork::wrap_plots(gp_hm, gp_hm_nested, nrow = 1)樹形図の追加
ggdendroパッケージを用いると、階層型クラスタリングを行なったオブジェクトから、 ggplot2で樹形図を描くためのデータを抽出することができる。 これはつまり、ggplot2で樹形図の体裁を整えることができるということである。
また、ggplot2で作成した複数のプロットを一つにまとめるパッケージはいくつかあるので、 そういったものを使えば、ヒートマップの並べ替えの根拠として階層型クラスタリングの 樹形図を組み合わせることができる。 ここでは、ggdendroパッケージとpatchworkパッケージを用いて、 ヒートマップとk-meanで求めた各クラスタの樹形図を組み合わせる方法を紹介する。
ちなみに、共通する軸を持つ複数のプロットを組み合わせる場合は、 各プロットで対応する軸の位置合わせをする必要がある。 うまく位置が合わない場合は、プロットの描画範囲や余白を調整するとよい。
cluster_x_pos <-
res_kmean$cluster %>%
table() %>%
{.[hc$labels[hc$order]]} %>%
{cumsum(.) - (. / 2)}
dend_data <- ggdendro::dendro_data(hc)
# 葉に繋がる横線
tbl_leaf <-
ggdendro::segment(dend_data) %>%
dplyr::filter(x == xend, yend == 0) %>%
dplyr::select(x, xend) %>%
dplyr::mutate(new_x = as.double(cluster_x_pos))
# ノード間の横線
tbl_temp <-
ggdendro::segment(dend_data) %>%
dplyr::filter(x == xend, yend != 0) %>%
dplyr::arrange(y) %>%
dplyr::select(x, xend)
tbl_temp <-
ggdendro::segment(dend_data) %>%
dplyr::filter(y == yend) %>%
dplyr::arrange(desc(y)) %>%
dplyr::slice_head(n = 2) %>%
dplyr::select(x, xend) %>%
unlist() %>%
unique() %>%
sort() %>%
{.[2]} %>%
{tibble::tibble(x = ., xend = .)} %>%
{dplyr::bind_rows(tbl_temp, .)}
for(i in seq_len(nrow(tbl_temp))) {
temp <-
abs(seq_len(nrow(tbl_leaf)) - tbl_temp[i, 1]) %>%
{which(. == min(.))} %>%
{mean(tbl_leaf$new_x[.])} %>%
{dplyr::mutate(tbl_temp[i, ], new_x = .)}
tbl_leaf <- dplyr::bind_rows(tbl_leaf, temp)
}gp_tree <-
ggplot() +
geom_segment(
data =
ggdendro::segment(dend_data) %>%
dplyr::left_join(dplyr::select(tbl_leaf, !xend), by = "x") %>%
dplyr::rename(nx = new_x) %>%
dplyr::left_join(dplyr::select(tbl_leaf, !x), by = "xend") %>%
dplyr::rename(nxend = new_x),
aes(x = y, y = nx, xend = yend, yend = nxend)) +
scale_x_reverse() +
scale_y_reverse(limits = c(200, 0), expand = expansion(add = .5)) +
theme_minimal()
patchwork::wrap_plots(
gp_tree +
labs(y = "K-means cluster") +
theme(
axis.text = element_blank(),
panel.grid = element_blank(),
axis.title.x= element_blank(),
axis.title.y.left = element_text(face = "bold"),
plot.margin = margin()
),
gp_hm_nested +
theme(
axis.title = element_blank(),
plot.margin = margin()
),
nrow = 1, widths = c(.05, .95)
)Session info
sessionInfo()R version 4.3.2 (2023-10-31)
Platform: aarch64-apple-darwin20 (64-bit)
Running under: macOS Ventura 13.1
Matrix products: default
BLAS: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRblas.0.dylib
LAPACK: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRlapack.dylib; LAPACK version 3.11.0
locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
time zone: Asia/Tokyo
tzcode source: internal
attached base packages:
[1] stats graphics grDevices utils datasets methods base
other attached packages:
[1] ggplot2_3.5.1 magrittr_2.0.3
loaded via a namespace (and not attached):
[1] gtable_0.3.5 jsonlite_1.8.8 dplyr_1.1.4 compiler_4.3.2
[5] tidyselect_1.2.1 stringr_1.5.1 tidyr_1.3.1 scales_1.3.0
[9] yaml_2.3.9 fastmap_1.1.1 amap_0.8-19 ggh4x_0.2.8
[13] R6_2.5.1 patchwork_1.2.0 labeling_0.4.3 generics_0.1.3
[17] knitr_1.48 MASS_7.3-60 htmlwidgets_1.6.4 forcats_1.0.0
[21] tibble_3.2.1 munsell_0.5.1 pillar_1.9.0 rlang_1.1.4
[25] utf8_1.2.4 stringi_1.8.4 xfun_0.46 cli_3.6.3
[29] withr_3.0.0 digest_0.6.34 grid_4.3.2 lifecycle_1.0.4
[33] ggdendro_0.2.0 vctrs_0.6.5 evaluate_0.24.0 glue_1.7.0
[37] farver_2.1.2 fansi_1.0.6 colorspace_2.1-1 rmarkdown_2.25
[41] purrr_1.0.2 tools_4.3.2 pkgconfig_2.0.3 htmltools_0.5.7












