一覧へ

モチーフ検索:トライ法を用いてプロモーター配列中の転写因子結合部位を特定する

プロモーター配列から複数の短いモチーフを一度に検索できるトライ木構造をPythonで実装します。二分探索との比較も行います。

中級
|
60
|
検証済み (2026-07)
モチーフ検索試してみる転写因子結合部位転写因子結合部位プロモーターTATAボックス短い配列のマッチング
進捗0/8 (0%)

モチーフ検索:トライブを用いてプロモーター配列中の転写因子結合部位を特定する

このトピックを終えると

教科書で学んだトライ(Trie)二分探索を組み合わせて、プロモーター配列から複数の転写因子の結合部位を一度に検索するツールを自分で作成できるようになります。Aho-Corasickのような実用的な多重パターンマッチングアルゴリズムの基礎となるデータ構造をコードで理解します。

この記事は教育用の一般的な例です。実際のモチーフ検索は、位置重み行列(PWM)、背景モデルなど、より高度な手法を用います。


「500個のモチーフ × 100kbの配列」— 単純な検索の落とし穴

マウスプロモーター100kbの配列から、既知の転写因子の結合部位である500個のモチーフ(それぞれ6〜12bp)をすべて見つけたいとします。

単純なアプローチ:

python
def naive_search(sequence: str, motifs: list[str]) -> list[tuple[int, str]]:
hits = []
for motif in motifs:
for i in range(len(sequence) - len(motif) + 1):
if sequence[i:i+len(motif)] == motif:
hits.append((i, motif))
return hits

このアプローチの問題は、モチーフごとに繰り返し探索を行うことです。時間計算量はO(モチーフ数 × 配列の長さ)= O(500 × 100,000)= 5 × 10⁷です。Pythonで数秒で済みます。

もし配列が30億bpのヒトゲノムであればどうでしょうか? 5 × 10⁵ × 3 × 10⁹ = 1.5 × 10¹⁵です。宇宙が終わるまでに終わらないでしょう。

現実的なアプローチはトライです。500個のモチーフを1つの木構造に統合し、配列を1回だけ走査しながら、木構造をたどってマッチを検出します。時間計算量はO(配列の長さ + マッチの数)に短縮されます。


ブラックボックスからパーツへ — トライを覗いてみよう

パーツ 1:トライデータ構造

トライは、各ノードが 1 つの文字を表し、ルートからリーフへのパスが 1 つの文字列を表すツリーです。

例として、モチーフのセット {"TATA", "TATT", "GCGC"} をトライで表現すると:

text
root
        /  \
       T    G
       |    |
       A    C
       |    |
       T    G
      / \   |
     A*  T* C*

* は、完結したモチーフの終端を示します。3 つのモチーフが共通の接頭辞(TA)を共有する場合、その部分はツリーに 1 回だけ保存されます。

Python による実装:

python
class TrieNode:
def __init__(self) -> None:
self.children: dict[str, TrieNode] = {}
self.matches: list[str] = []
class Trie:
def __init__(self) -> None:
self.root = TrieNode()
def insert(self, pattern: str) -> None:
node = self.root
for char in pattern:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.matches.append(pattern)

パーツ 2:トライを巡回して検索

次に、配列の各位置でトライをたどりながらマッチングを確認します。

python
def search(trie: Trie, sequence: str) -> list[tuple[int, str]]:
hits: list[tuple[int, str]] = []
for i in range(len(sequence)):
node = trie.root
j = i
while j < len(sequence) and sequence[j] in node.children:
node = node.children[sequence[j]]
for match in node.matches:
hits.append((i, match))
j += 1
return hits

各位置 i から開始し、ツリーの最大深さ(最長のモチーフの長さ)までたどります。500 個のモチーフの最大長が 12 の場合、各位置で最大 12 回の検索が行われます。全体の計算量は O(配列の長さ × 最大モチーフの長さ)です。

実践的な最適化(Aho-Corasick)では、ここで フェイルリンク を追加して、再開位置を事前に計算します。このチュートリアルでは、基本的なトライを使って概念を確立します。

二分探索との比較

トライの仕組みを理解するために、代替的な方法である「ソートされた配列 + 二分探索」と比較すると、より明確になります。

ソートされたモチーフ配列によるアプローチ:

python
def sorted_search(sequence: str, sorted_motifs: list[str], max_motif_len: int) -> list[tuple[int, str]]:
from bisect import bisect_left
hits = []
for i in range(len(sequence)):
for length in range(1, max_motif_len + 1):
substring = sequence[i:i+length]
idx = bisect_left(sorted_motifs, substring)
if idx < len(sorted_motifs) and sorted_motifs[idx] == substring:
hits.append((i, substring))
return hits

このアプローチの時間計算量:O(シーケンスの長さ × 最大モチーフ長 × log(モチーフの数))。

トライと比較すると、logの項はありますが、実際にはトライの方が高速です。その理由はキャッシュの局所性です。トライの探索は、関連するノードが連続したメモリにアクセスされるのに対し、二分探索は配列のあちこちをランダムにアクセスします。

二分探索は、モチーフの集合が頻繁に変化せず、ロード時間が重要な場合に、トライよりも有利です。トライの構築は遅いですが、検索は高速です。二分探索はソートするだけでよいため、初期ロードは高速です。これはトレードオフです。

実践的な使用例

簡単なシナリオで確認してみましょう。

python
# 既知の転写因子結合モチーフ(例)
motifs = [
"TATAAA", # TATA box
"CAAT", # CAAT box
"GGGCGG", # GC box
"CACGTG", # E-box
"TGACTCA" # AP-1
]
# 仮想プロモーター配列
promoter = "GCTATAAACCAATGGGCGGATGCACGTGCCCTGACTCAAG"
# トライを構築
trie = Trie()
for motif in motifs:
trie.insert(motif)
# 検索
hits = search(trie, promoter)
for pos, motif in sorted(hits):
print(f"位置 {pos}: {motif}")
# 出力:
# 位置 2: TATAAA
# 位置 9: CAAT
# 位置 12: GGGCGG
# 位置 21: CACGTG
# 位置 28: TGACTCA

一度の配列走査で、5つのモチーフがすべて検出されました。


フェーディング — 埋めるべき2つの空白

空白1:IUPACコードのサポート

実際の転写因子結合部位は可変性を持ちます。IUPACコード(R = A/G、Y = C/T、W = A/Tなど)で表現されます。

python
IUPAC = {
"A": {"A"}, "C": {"C"}, "G": {"G"}, "T": {"T"},
"R": {"A", "G"}, "Y": {"C", "T"}, "S": {"C", "G"},
"W": {"A", "T"}, "K": {"G", "T"}, "M": {"A", "C"},
"B": {"C", "G", "T"}, "D": {"A", "G", "T"},
"H": {"A", "C", "T"}, "V": {"A", "C", "G"},
"N": {"A", "C", "G", "T"}
}
def search_with_iupac(trie: Trie, sequence: str) -> list[tuple[int, str]]:
"""
モチーフにIUPACコードが含まれている可能性がある。
例:"TATA**W**A" はTATAAAまたはTATATAのどちらにもマッチする。
"""
# TODO:トライを探索するとき、各ノードのchildrenキーがIUPACコードの場合、拡張セットでマッチさせる。
pass

ヒント: ツリーの構築はそのままにして、検索時にsequence[j]と各子ノードのキーをIUPAC拡張セットでマッチさせる。

空白2:逆相補配列の自動検索

DNAは二重鎖であるため、モチーフは逆相補配列としても現れる可能性があります。

python
def reverse_complement(seq: str) -> str:
complement = {"A": "T", "T": "A", "G": "C", "C": "G"}
return "".join(complement.get(b, b) for b in reversed(seq))
def search_both_strands(motifs: list[str], sequence: str) -> list[tuple[int, str, str]]:
"""
各モチーフとその逆相補配列をすべてトライに入れて検索する。
返り値:(位置、モチーフ、“forward”または“reverse”)
"""
# TODO:トライを構築するとき、各モチーフの正方向と逆相補配列の両方を挿入する。
# 各エントリで元のモチーフと方向を記録する。
pass

ヒント: TrieNode.matcheslist[tuple[str, str]] (元のモチーフ、方向)に拡張する。


考察:実用的なモチーフ検索との違い

Position Weight Matrix (PWM):実際には、ほとんどの転写因子の結合部位は、厳密な文字列マッチングではなく確率的なマッチングである。各位置の各塩基に確率が割り当てられ、候補配列のスコアを計算する。JASPARのようなデータベースのモチーフは、この形式である。

背景モデル:実際には、ランダムなマッチング確率を背景モデルとして計算し、統計的に有意なマッチングのみを報告する。FIMO(MEMEスイート)が標準的なツールである。

Aho-Corasick:トライに失敗リンクを追加すると、O(配列長 + マッチ数)となり、真に線形時間になる。実用的な複数パターンマッチングの標準。

全ゲノム規模:ヒトゲノム(30億bp)の検索には、サフィックス配列、FMインデックスなどのインデックスデータ構造が代替手段となる。BWA、Bowtieなどのアライメントツールの中心部。

拡張プロジェクト

1. JASPARデータのロード: 実際のJASPARデータベースから、ヒトの転写因子100個分のPWM(位置特異的スコア行列)をダウンロードし、作成したツールでプロモーター領域のスキャンを行う。

2. 可視化: スキャン結果をプロモーターマップとして表示する。matplotlibやIGVスタイルのトラックを使用する。

3. Aho-Corasickの拡張: フェイルリンクを追加し、実際に線形時間でマッチングを行うツールを完成させる。


このセクションの構成要素

  • [F] トライ(Trie): 複数の文字列を木構造に統合。ノード=文字、パス=文字列。
  • [F] 二分探索: ソートされた配列+bisectによる代替アプローチ。トライとのトレードオフを理解する。
  • [W] ファイル入出力: FASTAファイルの解析など(完成したスクリプトとして提供)。

[F] = 自分で実装 / [W] = 完成したコードとして提供。

💬 質問・コメント

0件のコメント

ログインせずに投稿できます。ゲスト投稿は投稿者自身で編集・削除できません。

0/2000

読み込み中...