プライマーライブラリの重複除去ツール — 3つの概念を組み合わせて1つのツールを作成
このトピックを終えると
教科書(DryBench)で個別に学んだハッシュテーブル、辞書、正規表現という3つの概念をまとめ、数万個のプライマーライブラリから重複をほぼ瞬時に見つけ出す実用的なツールを自分で作成できるようになります。単に「重複を削除する」だけでなく、なぜ単純な方法が遅いのか、それぞれの概念がツールのどの部分になるのかを体感的に理解できます。
この記事は教育目的の一般的な例です。特定の研究室の実際のライブラリではなく、分子生物学のあらゆる分野で繰り返される「オリゴの在庫が増えることによる問題」を題材にしています。
「あっ、これ前に注文したやつじゃない?」—私たちが抱える問題
研究室でPCR、クローニング、qPCR、シーケンシングを行うと、プライマー(短いDNAオリゴ、通常18~25nt)を継続的に注文することになります。最初はExcelシート1枚で十分でした。名前、配列、用途、注文日、Tmなど。
しかし、1年、2年が経つと、このシートが数千、数万行に膨れ上がります。複数人で使うと、以下のような問題が発生します。
- キムさんが
hGAPDH_Fとして登録した配列を、イさんが後でGAPDH_forwardという別の名前で同じ配列を登録する。 - 誰かは
ATGCGT...と大文字で、誰かはatgcgt...と小文字で入力する。 - コピー&ペーストをする際に、配列の最初と最後に空白や見えないタブが付いてしまう。
- ある配列は逆相補配列に反転させて登録したが、実際にはすでに存在するプライマーと同じ位置に一致する。
- タイプミスで
ATGXGTのようにACGT以外の文字が混ざってしまう。
その結果、同じオリゴを2回、3回注文して無駄な出費が発生し、実験中に「このプライマーが本当にそれと一致するのか?」と混乱してミスが発生する。
そこで、「重複を整理しよう」と考え、単純なコードを書いて、すべての配列を他のすべての配列と比較することにします。
# 単純な方法 — すべてのペアを比較(意図的に遅いバージョン)def find_duplicates_naive(sequences): duplicates = [] for i in range(len(sequences)): for j in range(i + 1, len(sequences)): if sequences[i] == sequences[j]: duplicates.append((i, j)) return duplicatesサンプルが100個であれば、すぐに終わります。しかし、1万個になるとどうでしょう? 比較回数は約10000 × 10000 / 2 = 5千万回。ノートパソコンで数十秒から数分かかります。10万個であれば、その100倍です。コーヒーを飲みながら待っていても終わらないでしょう。
ここで止まらず、質問を投げかけてみましょう。なぜ遅いのか? そして、これを瞬時に終わらせるためには、どのようなツールが必要なのか? この記事では、その答えをコードで実現していきます。
まずは完成品を見てみましょう(ブラックボックスを先に動かす)
空の状態でエディタを起動すると、迷ってしまうことがあります。そこで、最終的に作成するツールをまず動かしてみて、その後で蓋を開けて内部を調べてみましょう。(これは fast.ai などで使われている「トップダウン」学習法です。完成したゲームをまず1回プレイし、その後でルールを学ぶというものです。)
完成した dedup_primers.py は次のようになります。
report = dedup_primer_library("primers.csv")
print(report.summary())すると、次のようなレポートが出力されます。
=== プライマーライブラリの重複レポート ===
合計項目数:12,480
有効項目数:12,451(フォーマットエラー29件を除外)
ユニークな配列:9,832
完全な重複:2,619(同じ配列、異なる名前)
相補的な重複:174(反転すると同じ配列)
処理時間:0.31秒
─────────────────────────────────
最も重複している配列トップ3
1) ATGGCACCACAGTCCATGCC ×7(GAPDH_F、hGAPDH_forward、...)
2) GTCGACCTGCAGGCATGCAA ×5
3) CCTGCAGGTCGACTCTAGAG ×4重要な点は2つあります。第一に、0.31秒です。以前の単純なバージョンでは数分かかっていた処理を、あっという間に終わらせます。第二に、単に「重複あり/なし」だけでなく、どの配列が何回、どのような名前で重複しているのかまで教えてくれます。この2つをどのように作成するかが、この記事の主題です。
このツールはどのような部品で構成されているか(部品分解図)
私たちが組み立てる「重複排除エンジン」は、実際には3つの教科書的な概念が組み合わさった機械です。プラモデルのように、各部品がどこから来たのかを付記してみましょう。
プライマー重複排除エンジン
┌─────────────────────────────────────────────────────────┐
│ │
│ [入力] CSVファイル読み込み ──── 部品:ファイル入出力 │ ← 完成品として提供(ツール)
│ │ │
│ ▼ │
│ [精製] 配列正規化・検証 ── 部品:正規表現 │ ← 自分で作成する ★
│ │ │
│ ▼ │
│ [判定] 重複を即座に判定 ──── 部品:ハッシュセット │ ← 自分で作成する ★
│ │ │
│ ▼ │
│ [集計] 配列ごとに名前をまとめる ── 部品:辞書 │ ← 自分で作成する ★
│ │ │
│ ▼ │
│ [出力] レポート │
└─────────────────────────────────────────────────────────┘| 部品 | どこで学んだか(DryBench) | このツールで行うこと |
|---|---|---|
| ファイル入出力 | string-and-file-io | CSVから名前と配列を読み込む |
| 正規表現 | regex-pattern-matching | 配列が本当にACGTだけで構成されているか検証・精製する |
| ハッシュセット | hash-table | 「この配列は過去に見たことがあるか」をO(1)で判定する |
| 辞書 | list-and-dictionary | 配列ごとに、どのような名前が関連付けられていたかをまとめる |
📌 これらの概念を初めて学ぶ場合(上部のリンク) 以下の3つは、このチュートリアルの重要な部品です。初めて学ぶ場合は、右側のドロワー(ミニカード)でそれぞれ30秒程度予習してから戻ってきてください。ここで改めて必要に応じて解説します。
これから上から順番に一つずつ作成していきましょう。ファイル読み込み部品はすでに完成させておきます(ツールとして提供します)。皆さんには、精製・判定・集計の3つの部品に集中してもらえばよいのです。たったの3つです。人が一度に理解できる新しい概念は3つが限界なので、あえてこのように分けました。
準備段階0 — データの形式を確認する
コードを書く前に、まずデータを確認しましょう。プライマーシートをCSV形式でエクスポートすると、およそ以下のようになります。
name,sequence,purpose,date
GAPDH_F,ATGGCACCACAGTCCATGCC,qPCR,2025-01-12
GAPDH_R,GGCATGGACTGTGGTCATGAG,qPCR,2025-01-12
hGAPDH_forward, atggcaccacagtccatgcc ,qPCR,2025-06-03
cloning_F,GTCGACCTGCAGGCATGCAA,cloning,2025-02-20
bad_entry,ATGXGTNN,cloning,2025-03-01目視でも問題点がわかります。
GAPDH_FとhGAPDH_forwardは、名前は異なりますが、配列は同じです。さらに、後者は小文字で、前後に空白が付いています。bad_entryの配列ATGXGTNNは、Xが含まれているため、DNA配列ではありません(タイプミスか、何らかの省略記号)。
したがって、「重複」を正確に数えるためには、比較する前に配列を同じ形式に揃える(正規化する)必要があります。また、そもそも配列ではないものを除外する(検証する)必要があります。そうしないと、ATGGCACCACAGTCCATGCCとatggcaccacagtccatgccを異なる配列と誤認し、重複を見逃す可能性があります。
ここが重要なポイントです。**重複の排除の半分は、「比較をうまく行うこと」ではなく、「比較する前に形式を揃えること」**です。
ステップ1:ファイルの読み込み(この部分は完成した状態で提供します)
ファイルの読み込みは、このチュートリアルの学習目標ではありません。皆さんの作業領域は貴重なので、このような「補助的な」コードは、完成版として提供し、先に進むことにします。(string-and-file-ioカードで学んだ内容と同じです。もし内容が思い出せない場合は、ドローを確認してください。)
import csv
def load_primers(filepath): """CSVファイルから(名前、元の配列)のタプルのリストを読み込みます。""" records = [] with open(filepath, "r", encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: name = row["name"].strip() seq = row["sequence"] # あえてクリーンアップしません。次のステップで処理します。 records.append((name, seq)) return records
# 演習では、ファイルではなくリストで模倣します(ブラウザで直接実行できるようにするため)。raw_records = [ ("GAPDH_F", "ATGGCACCACAGTCCATGCC"), ("GAPDH_R", "GGCATGGACTGTGGTCATGAG"), ("hGAPDH_forward", " atggcaccacagtccatgcc "), ("cloning_F", "GTCGACCTGCAGGCATGCAA"), ("bad_entry", "ATGXGTNN"),]
assert len(raw_records) == 5assert raw_records[2][1] == " atggcaccacagtccatgcc " # 空白・小文字はそのまま残っています。raw_recordsはまだ整理されていない生データです。小文字、空白、不適切な配列がそのまま残っています。これを次のステップで整理します。
2段階目:配列の正規化と検証 ★(ここから実践)
✍️ ここはあなたが記述する部分です。 部品 = 正規表現。目標:配列を「標準形」に変換し、DNAではないものを除外する。
まず、「標準形」を定義しましょう。私たちのルールは次のとおりです。
- 前後の空白を削除する。
- すべて大文字に統一する。
A、C、G、Tのみで構成されているかチェックする。そうでない場合は除外する。
3番が正規表現が活躍する場所です。「文字列全体がACGTの繰り返しであるか?」は、正規表現パターン ^[ACGT]+$ だけで実現できます。
🔎 なぜ正規表現なのか?(ドロー — regex-pattern-matching)
^は文字列の開始、$は終了、[ACGT]は「この4つのいずれか」、+は「1回以上の繰り返し」。これらを組み合わせると、「最初から最後までACGTのみで構成されている」という意味になります。if文で文字を1つずつチェックすることもできますが、正規表現はそれを 1行 で表現します。
import re
# 文字列全体がA/C/G/Tのみで構成されているかチェックするパターン(事前にコンパイルして再利用)DNA_PATTERN = re.compile(r"^[ACGT]+$")
def normalize(seq): """空白の削除 + 大文字化(まだ検証前)""" return seq.strip().upper()
def is_valid_dna(seq): """正規化された配列が純粋なDNAであるかチェック。""" return DNA_PATTERN.match(seq) is not Noneそれでは確認してみましょう。
assert normalize(" atggcaccacagtccatgcc ") == "ATGGCACCACAGTCCATGCC"assert is_valid_dna("ATGGCACCACAGTCCATGCC") is Trueassert is_valid_dna("ATGXGTNN") is False # X、Nを含む → 除外assert is_valid_dna("") is False # 空文字列も除外(+のおかげで)最後の assert に注目してください。空文字列が除外されるのは、パターンに +(1回以上)を使用しているためです。もし *(0回以上)を使用した場合、空文字列が「有効なDNA」として通過してしまいます。正規表現の記号がデータの品質を左右します。
🤔 自己説明プロンプト
normalizeを最初に行い、is_valid_dnaを後で行う順序、なぜそれが正しいのでしょうか?もし検証を最初に行い、正規化を後で行った場合、atgc(小文字)を例に、何が起こるかを自分で説明してみてください。(ヒント:小文字のatgcは^[ACGT]+$に一致しますか?)
次に、生のレコードを「クリーンな配列 + 名前」のリストに変換する関数を完成させます。無効な項目は別にまとめて、レポートで「〇件除外」として報告します。
def clean_records(raw_records): cleaned = [] # (名前、標準配列) rejected = [] # (名前、元の配列) — 形式エラー for name, seq in raw_records: norm = normalize(seq) if is_valid_dna(norm): cleaned.append((name, norm)) else: rejected.append((name, seq)) return cleaned, rejected
cleaned, rejected = clean_records(raw_records)
assert len(cleaned) == 4 # bad_entryのみ除外assert len(rejected) == 1assert rejected[0][0] == "bad_entry"# 重要な点:名前は異なっていても、配列は完全に同じになったassert cleaned[0][1] == cleaned[2][1] == "ATGGCACCACAGTCCATGCC"最後の行が今回の段階の成功です。GAPDH_F と hGAPDH_forward は、これで 文字通り同じ配列 を持つようになりました。正規化前は、コンピュータの目には異なる文字列でした。これで「重複」を数える準備ができました。
ステップ 3:重複を瞬時に判定する ★(ハッシュセット)
✍️ 自分で埋めるセクション。 部品 = ハッシュセット。目標:「このシーケンスは以前に見たことがあるか?」を O(1) で答える。
最初の単純なバージョンがなぜ遅かったのか、もう一度思い出してみましょう。シーケンスをチェックするために、それまでに出現したすべてのシーケンスと一つずつ比較していたからです。n 個のシーケンスを処理する場合、比較は n² に比例します。
🔎 O(n²) 対 O(n)(図 — ビッグオー記法)
リストに inを使用してチェックする場合、リストを最初から検索します。これは、各項目に対して O(n)、全体で O(n²) です。一方、セットに in を使用すると、ハッシュを使用して位置を直接計算するため、O(1) で回答できます。これは全体で O(n) です。データが 10 倍に増えると、リスト方式は 100 倍遅くなりますが、セット方式は 10 倍遅くなるだけです。
ハッシュセットの重要な点はこれです。シーケンスをハッシュ関数に入力すると、「セル番号(アドレス)」が得られ、そのセルを直接確認すればよいので、データの量に関係なく、確認にかかる時間はほぼ一定です。図書館で分類記号を使って本をすぐに見つけるのと同じです。Python では、set がまさにこのハッシュテーブルです。
hash-table カードで学んだ原理を、ここではわずか 3 行で記述します。
def find_exact_duplicates(cleaned): """正確に同じシーケンスの重複ペア(名前)を見つける。O(n)。""" seen = set() # これまで見たシーケンス(ハッシュセット) unique = [] # 最初に見た(名前、シーケンス) duplicates = [] # 重複した(名前、シーケンス) for name, seq in cleaned: if seq in seen: # ← O(1) 判定!ここが肝心 duplicates.append((name, seq)) else: seen.add(seq) unique.append((name, seq)) return unique, duplicates
unique, dups = find_exact_duplicates(cleaned)
assert len(unique) == 3 # GAPDH_F、GAPDH_R、cloning_Fassert len(dups) == 1 # hGAPDH_forward(GAPDH_F と同じシーケンス)assert dups[0][0] == "hGAPDH_forward"assert dups[0][1] == "ATGGCACCACAGTCCATGCC"if seq in seen: — この 1 行が、数分かかる処理を 0.3 秒に短縮した立役者です。リストだった場合、seen が大きくなるにつれて、このチェックがますます遅くなりますが、セットなので、10 万個のデータが蓄積されても、チェックの速度は変わりません。
🤔 自己説明プロンプト
seenをset()ではなく[](リスト)に変更すると、コードはまだ「実行可能」です。結果も同じになります。しかし、なぜそうしないべきなのでしょうか?1 万個、10 万個の場合に何が起こるかを、O(n²) と関連付けて説明してください。
4段階 — 何回、どの名前で重複しているかをまとめてグループ化する ★(辞書)
✍️ 手動で埋める部分。 部品 = 辞書。目的:系列ごとに「この系列を使った名前」をまとめておく。
3段階まで行えば、「重複がある/ない」はわかります。しかし、実務で本当に知りたいのは**「この系列は、一体何個の名前で何回登録されているのか?」**です。GAPDH_F、hGAPDH_forward、GAPDH_qF...このように分散しているものを、一つの系列の下にまとめて整理する必要があります。
これは辞書がする仕事そのものです。キー = 系列、値 = その系列を使った名前のリスト。
🔎 なぜ辞書なのか?(ドロワー — リストと辞書) 辞書も内部はハッシュテーブルです(3段階のセットと近縁)。セットが「ある/ない」だけを記憶するのに対し、辞書は「キーに付随する値」まで記憶します。「系列 → 名前リスト」のような1対多の関連付けに最適です。
from collections import defaultdict
def group_by_sequence(cleaned): """系列をキーとして、その系列を使った名前のリストを値としてまとめる。""" groups = defaultdict(list) for name, seq in cleaned: groups[seq].append(name) # 存在しない場合は空のリストが自動的に生成される return groups
groups = group_by_sequence(cleaned)
# GAPDH系列には、2つの名前が付いている必要があるassert groups["ATGGCACCACAGTCCATGCC"] == ["GAPDH_F", "hGAPDH_forward"]assert len(groups["GGCATGGACTGTGGTCATGAG"]) == 1 # GAPDH_Rは一意assert len(groups) == 3 # 固有の系列は3種類defaultdict(list)を使ったおかげで、「キーが存在する場合は追加、存在しない場合は新しく作成する」という処理をif文で分岐することなく、groups[seq].append(name)の1行で済ませることができました。これで、このgroupsから値が2個以上のものだけを抽出すれば、「重複した系列のリスト」が得られます。
def duplicated_groups(groups): """名前が2個以上付いている(=重複している)系列だけを選び、最も重複が多い順に並べ替える。""" dups = {seq: names for seq, names in groups.items() if len(names) > 1} # 重複回数が多い順に並べ替え return sorted(dups.items(), key=lambda kv: len(kv[1]), reverse=True)
ranked = duplicated_groups(groups)
assert len(ranked) == 1assert ranked[0][0] == "ATGGCACCACAGTCCATGCC"assert ranked[0][1] == ["GAPDH_F", "hGAPDH_forward"]レポートの「最も重複が多い系列TOP3」は、このrankedから得られます。3つの部品(正規表現、ハッシュセット、辞書)がここで一つに結びつきました。
5段階で作る — さらに一歩:逆相補性の重複(発展)
これまで、私たちは「文字が同じ」重複のみを検出してきました。しかし、生物学にはもう一つの落とし穴があります。それは**逆相補性(reverse complement)**です。
DNAは二重鎖であるため、5'-ATGC-3'の反対側の鎖は3'-TACG-5'であり、これを反転させると5'-GCAT-3'になります。もし誰かがプライマーを反対側の鎖を基準に記述した場合、文字は完全に異なって見えても、実際には同じ場所に結合する同じオリゴヌクレオチドである可能性があります。
これを検出するには、「配列とその逆相補性の中で辞書順で小さい方」を代表値(canonical form)とします。そうすれば、元の配列でも逆相補性配列でも、同じ代表値に集約されます。これは、2段階で行った「正規化」の拡張版です。正規化の強みがここで再び発揮されます。
COMPLEMENT = str.maketrans("ACGT", "TGCA")
def reverse_complement(seq): return seq.translate(COMPLEMENT)[::-1]
def canonical(seq): """配列とその逆相補性の中で辞書順で小さい方を代表値とする。""" rc = reverse_complement(seq) return min(seq, rc)
# 検証:互いに逆相補性を持つ2つの配列は、同じ代表値を持つ必要があるassert reverse_complement("ATGC") == "GCAT"assert canonical("ATGC") == canonical("GCAT") # どちらも "ATGC" よりも小さい方に収束するassert canonical("AAAA") == "AAAA" # 逆相補性配列 "TTTT" よりも小さいため、自分自身になるこれで、3〜4段階でseqの代わりにcanonical(seq)を入れれば、逆相補性の重複もまとめて検出できます。新しい部品を作成したのではなく、既存の判定・集計部品に「代表値」というレンズを1つ取り付けただけです。優れたツールは、このように拡張されます。
🤔 自己説明プロンプト
canonicalを使用すると、正確な重複の数と逆相補性の重複の数をどのように区別してカウントできますか?(ヒント:seq == canonical(seq)であるかどうか、またはそうでないかが手がかりです。)
部品を一つに — 完成したエンジン
これで、最初に見たツールを五つのステップで組み立てることができます。
def dedup_primer_library(raw_records, use_canonical=False): cleaned, rejected = clean_records(raw_records)
key = canonical if use_canonical else (lambda s: s)
seen = set() groups = defaultdict(list) exact_dups = 0 for name, seq in cleaned: k = key(seq) if k in seen: exact_dups += 1 else: seen.add(k) groups[k].append(name)
ranked = sorted( ((k, names) for k, names in groups.items() if len(names) > 1), key=lambda kv: len(kv[1]), reverse=True, ) return { "total": len(raw_records), "valid": len(cleaned), "rejected": len(rejected), "unique": len(groups), "exact_duplicates": exact_dups, "top": ranked[:3], }
report = dedup_primer_library(raw_records)
assert report["total"] == 5assert report["valid"] == 4assert report["rejected"] == 1assert report["unique"] == 3assert report["exact_duplicates"] == 1assert report["top"][0][1] == ["GAPDH_F", "hGAPDH_forward"]これらの assert がすべてパスすると、教科書の概念3つを組み合わせて、実際に動作するツールを作成したことになります。おめでとうございます。
実際にどれくらい高速化されたのか(パフォーマンスの詳細分析)
「高速」と口にするだけでは意味がありません。実際に計測してみましょう。重複を含む1万個のシーケンスを作成し、単純なバージョンとハッシュ版で競わせます。
import random, time
random.seed(42)bases = "ACGT"pool = ["".join(random.choice(bases) for _ in range(20)) for _ in range(2000)]# 2000種類のシーケンスを5回繰り返す → 1万個(重複多数)big = [(f"p{i}", random.choice(pool)) for i in range(10000)]
# 単純なバージョン(O(n²)) — 最初の1500個だけでも十分に遅いsample = [s for _, s in big[:1500]]t0 = time.time()naive_pairs = find_duplicates_naive(sample)naive_time = time.time() - t0
# ハッシュ版(O(n)) — 全体の1万個t0 = time.time()report = dedup_primer_library(big)hash_time = time.time() - t0
print(f"単純なバージョン(1,500個): {naive_time:.3f} 秒")print(f"ハッシュ版(10,000個): {hash_time:.3f} 秒")
# データ量が6倍以上多いにもかかわらず、ハッシュ版の方が圧倒的に高速assert hash_time < naive_time数値はマシンによって異なりますが、傾向は常に同じです。データ量が少ない場合でも、単純なバージョンの方が遅く、データ量を増やすと差が顕著になります。これは、O(n²)とO(n)の違いを手で体感できる瞬間です。big-o-notationカードでグラフとして見ていた2つの曲線が、今、皆さんの画面のストップウォッチで分かれています。
別の道もある(マルチパスによる考察)
皆さんは、先ほどハッシュセット + 辞書を使って問題を解決しました。素晴らしいです。しかし、同じ問題を解く方法は一つだけではありません。真のスキルは、「自分の方法以外にどのような方法があり、いつそれがより優れているか」を知ることです。
- Pandas
drop_duplicates(): データがすでにDataFrameである場合、df.drop_duplicates(subset="seq")の一行で完了します。内部的にもハッシュを使用しています。私たちが手で作ったものを、ライブラリが代わりにやってくれるのです。いつ私たちの方法が優れているか? 重複の回数と名前のリストをカスタムで抽出する必要がある場合です。drop_duplicatesは重複を削除するだけで、誰が何回重複したかをレポートしてはくれません。 - Biopython: 配列の解析と逆相補性に特化したツールがすでに存在します。逆相補は
Seq(...).reverse_complement()で安全に処理できます。実務では、このような検証済みのライブラリを使用するのが適切です。ただし、原理を理解せずに使用すると、なぜ遅いのか、なぜ結果がおかしいのかをデバッグできません。そのため、私たちは最初から作り上げたのです。 - ソート後、隣接要素の比較: 配列をソート(O(n log n))した後、隣接する要素のみを比較する方法もあります。メモリをセットよりも節約できるため、データがメモリに収まらないほど大きい場合に有利です。
要点: 私たちのツールは「正解」ではなく、各方法のトレードオフを理解している人こそが正解です。ハッシュは高速ですがメモリを消費し、ソートはメモリを節約しますが少し遅く、ライブラリは便利ですが内部構造を理解していません。
次のステップへ(下部の外部リンク)
このツールをさらに発展させたい場合は、以下の概念が自然な次のステップです。
- 現在は配列をすべてメモリにロードしています。ファイルがメモリよりも大きい場合は? → 二分探索とDBインデックスを学んで、「ディスクに配置して高速に検索する」ことを学び、応用編として配列DBのインデックス化に進みます。
- 重複の判定をWeb上でリアルタイムに行う? → WebSocketを使用した応用編に進みます。
- なぜセットがO(1)なのか、より深く理解する? → ハッシュテーブルのカードに戻り、衝突の解決までマスターしましょう。
実際に試してみる(独立問題)
今度はスケルトンなしで、完全に新しい状況に皆さんのツールを適用してみましょう。構造は同じです — 正規化 → 判定 → 集計。
- 長さフィルターの追加: プライマーは通常、18〜25 ntです。この範囲外の配列も
rejectedとして処理するように、clean_recordsを拡張してください。(assertを使って、30 ntの配列がフィルターにかかることを検証すること。) - GC含量の警告: 各ユニークな配列のGC含量を計算し、40〜60%外の場合はレポートに警告を追加してください。(次の応用編 GC含量計算機 の予告です。)
- 相補配列レポート:
use_canonical=Trueで実行した場合、「正確な重複」と「相補的な重複」を 別々に 数えて、レポートに2つの行に分けて表示してください。(5段階の自己説明プロンプトのヒントを使用します。) - 挑戦: 配列にIUPAC縮約文字(
N、R、Yなど)が含まれている場合、「無効」ではなく「縮約配列」として別に分類するには、2段階の正規表現をどのように変更する必要がありますか?
各課題ごとに assert を使用して自己採点してください。すべてパスすれば、皆さんはこのツールの 所有者 です。
まとめ
私たちは、「重複を削除しよう」という些細な問題から出発し、教科書で学んだ3つの概念が、どのようにして1つの実用的なツールとして組み合わさるのかを考察しました。
- 正規表現は、比較前にデータの形式を整え、不要なものを除外しました。(精製)
- ハッシュセットは、「以前に見つけたデータかどうか?」をO(1)で判定し、処理時間を数分から0.3秒に短縮しました。(判定)
- 辞書は、データごとに名前をまとめて、どのデータが何回重複しているかを教えてくれました。(集計)
そして、これら3つは、個別に学ぶときには「これ、どこで使うんだろう?」と思うものでしたが、組み合わせることで、実際の問題を解決できるようになります。これが、応用力の真髄です。概念は個別に学び、ツールとして組み立てるのです。
繰り返しになりますが、この記事は一般的な教育の例です。実際のライブラリには、より多くのカラム、ルール、例外が存在するでしょう。詳細なバージョンは、この基本構造に、ご自身で肉付けすることで構築できます。基本構造は常に同じです。正規化、判定、集計。