topGO — Bioconductor でenrichment解析
Gene Ontology を利用して、 ある遺伝子セットにどんな機能が多めに含まれているかを解析する。
https://www.bioconductor.org/packages/release/bioc/html/topGO.html
Rの中からインストール
BiocManager::install("topGO")
使い方
ヒト遺伝子にランダムなスコアをつけた架空データ
library(tidyverse)
library(org.Hs.eg.db)
entrez_ids = mappedkeys(org.Hs.egGO)
scores = runif(length(entrez_ids), 0, 1) # p-value-like
# scores = rnorm(length(entrez_ids), 0, 0.4) # log2FC-like
named_scores = setNames(scores, entrez_ids)
topGOdata を作る
tg_data = new("topGOdata",
ontology="BP",
allGenes=named_scores,
geneSelectionFun=function(x) {x < 0.01},
# geneSelectionFun=function(x) {abs(x) > 1},
nodeSize=10,
annotationFun=annFUN.org,
mapping="org.Hs.eg.db",
ID="entrez")
ontologyBP,CC,MFのどれかdescription(省略可)- 説明string
allGenes- 遺伝子名を名前とするnamed vector。 値はp-valueとかlog2FCとかなんとか、好きなスコア。
- 名前は"all"だけど常に全遺伝子である必要はなくて、 むしろ目的に応じて適切な"background"を選んで渡すべき。
geneSelectionFunallGenesの数値を引数として今回興味のある遺伝子にTRUEを返すような関数。 スコアが閾値より大きい・小さいとか、上位100までとか。function(p) {p < 0.01},function(log2FC) {abs(log2FC) > 1},function(score) {rank(score) <= 100L}- 必要なのは
statistic="fisher"のときだけで、なおかつ実際に適用されるのはrunTest()実行時なのにここで入力させる、という筋の悪いデザイン。 スコアをそのまま使うはずのstatistic="ks"などでも要求され、 Significant genes という謎の結果が計算されてしまう。 fisherを使わない場合は結果の誤解を防ぐためにも一律FALSEを返す関数にしておくのが安全。function(x) {logical(length(x))} nodeSize- 紐付けられた遺伝子の数がこれより少ないGO termを除外。1以上の整数。
annotationFun- 遺伝子IDとGO termを結びつける関数。
- 後述のように
org.**.**.dbパッケージがあるようなメジャー種の遺伝子IDならannFUN.org。 チップが登録されているマイクロアレイならannFUN.db。 自作マップで頑張るならannFUN.gene2GO,annFUN.GO2genes,annFUN.file。 ...- 以降は
annotationFun()に渡す引数。 - e.g.,
annFUN.org(whichOnto, feasibleGenes, mapping, ID="entrez")mapping- IDマッピング用のBioConductorパッケージ。
BioConductor AnnotationData Packages
から探す。例えばヒトなら
org.Hs.eg.db。 IDallGenesに与えた名前の種類。entrez,genbank,alias,ensembl,symbol,genename,unigene. (annFUN.org()の中に書いてある。case-insensitive)
検定
whichAlgorithms()
whichTests()
so="increasing"
resClassicFisher = runTest(tg_data, algorithm="classic", statistic="fisher", sortOrder=so)
resElimFisher = runTest(tg_data, algorithm="elim", statistic="fisher", sortOrder=so)
resWeightFisher = runTest(tg_data, algorithm="weight", statistic="fisher", sortOrder=so)
resClassicKS = runTest(tg_data, algorithm="classic", statistic="ks", sortOrder=so)
resElimKS = runTest(tg_data, algorithm="elim", statistic="ks", sortOrder=so)
resWeightKS = runTest(tg_data, algorithm="weight01", statistic="ks", sortOrder=so)
algorithmclassic: GO termをそのまま使って計算。偽陽性多め。elim(Alexa et al. 2006): DAG上での隣接関係を考慮して補正。 下位termから検定を始め、有意なものが見つかったらそこに含まれる遺伝子を祖先ノードから除外する。 classicに比べて上位termの偽陽性が減ってconservative。cutOffオプションでこの判定基準を 0.01 から変更可能。weight(Alexa et al. 2006): elimを一般化して少しマイルドにしたような感じ。 有意な子ノードを多く持つ親ノードは生き残る。 classicよりconservativeだがelimより取りこぼさない。 計算が複雑すぎるせいか検定はfisherしかサポートされていない。weight01: “mixture between theelimand theweightalgorithms” とのことだが詳細は不明。 topGOデフォルトに据えるくらい自信あるんだろうけど。検定は全て可能。lea: ドキュメントでも論文でも言及無し。お蔵入りしたプロトタイプ?parentChild(Grossmann et al. 2007): classicよりも下位termでの偽陽性が少ない。 親を複数持つ場合の扱い2つ(union or intercection)のうちどちらを採用してるかは不明。 検定はfisherのみ。statisticfisher: 遺伝子の数に基づいて検定。セットに含まれているか否かの二値。ks(Ackermann and Strimmer 2009): 遺伝子のスコアや順位に基づいて検定。 閾値で遺伝子セットを区切らずに済む。 (普通のKSだったら上位への偏りだけを見ているとは限らないけどそのあたりは調整済み?)globaltest(Goeman and Bühlmann 2007): 統計量を挟まず生データに基づいて検定。 (topGOdataにどうやってデータ渡すんだろう?)t,sum,ks.ties: 不明。scoreOrderallGenesに与えた値が小さいほど良いP値とかならincreasing(デフォルト)。 興味のある遺伝子で値が高くなるlog2FCのようなスコアならdecreasingを指定。 ドキュメントにあんまりちゃんと載ってない?- 結局どれを使うか?
algorithmはとりあえず最もconservativeで検定も自由なelim。 もしくは作者Alexaさんを信じてweight01。デフォルト設定は論文にも書きやすい。statisticは、遺伝子セットが既に区切ってあるならfisher、 DEG解析やら何やらで遺伝子がp値やスコアを持ってるならks。globaltestを使いたい場合は 本家globaltestパッケージ を参照。
結果は topGOresult オブジェクト。
計算されるp値は多重検定の補正をしていない生の値。
“Significant genes” は geneSelectionFun(allGenes) で TRUE になったものの数で、
無関係なはずのKSでも計算されてしまって気持ち悪い。
str(resElimKS)
geneData(resElimKS)
score(resElimKS) |> head()
解釈・描画
GenTable() を使って GO term Over-representation ランキングを表示できる。
複数の runTest() 産物を好きな名前で複数並べたりすることもできる。
が、P値が桁の小さい文字列型になってたり勝手に行が削られたりして怖いので使わない。
GenTable() の実装を参考に似た形式のより良いテーブルを自分で作る。
# num_nodes=length(tg_data@graph@nodes) # GO terms >nodeSize
# tg_table = GenTable(tg_data,
# classicFisher=resClassicFisher,
# elimFisher=resElimFisher,
# classicKS=resClassicKS,
# elimKS=resElimKS,
# topNodes=num_nodes) |>
# tibble::as_tibble() |>
# print()
annoStat = termStat(tg_data, sort(tg_data@graph@nodes)) |>
tibble::rownames_to_column(var = "GO.ID") |>
tibble::as_tibble() |>
dplyr::mutate(Term=topGO:::.getTermsDefinition(terms, ontology(tg_data), 65535L)) |>
dplyr::relocate(Term, .after=GO.ID) |>
print()
tg_table = annoStat |> dplyr::mutate(
classicFisher=score(resClassicFisher, whichGO=GO.ID),
elimFisher=score(resElimFisher, whichGO=GO.ID),
weightFisher=score(resWeightFisher, whichGO=GO.ID),
classicKS=score(resClassicKS, whichGO=GO.ID),
elimKS=score(resElimKS, whichGO=GO.ID),
weightKS=score(resWeightKS, whichGO=GO.ID),
) |>
print()
あとは dplyr, tidyr, ggplot2 などを使って自由に整形、可視化する。
tg_table |> dplyr::arrange(elimKS)
tg_table |>
dplyr::select(matches("Fisher$|KS$")) |>
pairs()
p0 = ggplot(tg_table) +
geom_point(shape = 16, alpha = 0.5) +
coord_fixed() +
theme_minimal()
p0 + aes(classicFisher, classicKS)
p0 + aes(elimFisher, elimKS)
p0 + aes(classicKS, elimKS)
p0 + aes(classicKS, weightKS)
p0 + aes(elimKS, weightKS)
Rgraphviz を使ってDAG描画
# BiocManager::install("Rgraphviz")
showSigOfNodes(tg_data, score(resElimKS), firstSigNodes=6, useInfo="all")
printGraph(tg_data, resElimKS, firstSigNodes=6, fn.prefix="filename", pdfSW=TRUE)
前者はプロットだけ、後者はPDFに書き出し。 significant nodeが四角で、赤いほど低いP値。
GO terms
BPterms = ls(GOBPTerm)
MFterms = ls(GOMFTerm)
CCterms = ls(GOCCTerm)