iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
Build on Google AI

《解構生命暗物質:用 AlphaGenome Atlas 破譯 98% 非編碼基因組的 30 天實戰》系列 第 21 篇

AlphaGenome Atlas 實戰 | Day 21 | 從 VCF 到研究審閱封包 —— QC、註釋與證據界線

  • 分享至 

  • xImage
  •  

💡 導讀:研究助理整理八份申請表,為什麼不能只留下三份?

昨天 Day 20,我們用 1-based 與 0-based 座標判斷人工 Motif 區間是否重疊。今天,假設研究團隊拿到一個變異檔案(VCF):七列記錄,其中一列寫了兩個不同的 ALT,因此拆成八個候選。研究助理能不能只挑三筆「看起來正常」的,其他全部丟掉?

不能。

好的研究流程必須回答:哪些候選通過資料品質門檻?哪些需要人工處理?哪些被排除?每一筆為什麼得到這個結果? 對通過門檻的候選,還要能記錄「人工分數來源」以及「查無分數」,而不是隨手補上 0。

這就是 Research Review Packet(研究審閱封包)。它不是病因確診書,而是讓後續研究者能複查每一步的候選與證據整理。

一、核心概念:三個檔案與一張人工分數表

資料 用途 本篇最重要的限制
day21_synthetic.vcf 七筆八欄的人工 VCF 記錄,第二列含 G,C 兩個 ALT 沒有樣本 genotype 欄位、沒有病人資料;只是一小部分 VCF 規範
day21_synthetic_regions.bed 指定兩段人工區間:chr1:[90,115) 和 [145,165) BED 是 0-based,左含右不含
day21_toy_reference.json chr1 由 200 個 A 組成的虛構參考序列 TOY_NOT_GRCH38,絕不能當 GRCh38 FASTA
day21_synthetic_scores.json 提供兩個人工分數 0.42、−0.17 不是 AVI、沒有醫療分數含義

VCF 的 POS 是 1-based;例如第 100 個鹼基,要以 POS−1=99 比較 BED 是否包含它。本例判斷 indel 是否進入教學區間時,也只用 VCF POS 錨點,沒有進行複雜變異的範圍比對、左對齊或正規化。

QC 按順序判斷,不等於所有錯誤都已找到

程式使用明確的第一個決策理由:

FILTER → BED → REF → indel → missing QUAL → low QUAL

這個順序是我們為教學自行訂定的簡化規則,不是臨床或 VCF 官方的通用閾值。例如:

  • chr1:120:A>T 的 QUAL 是 10,但位置先被人工 BED 排除,因此輸出是 OUTSIDE_TEACHING_BED,而不是 LOW_QUAL。
  • chr1:140:G>T 的 REF 對不上人工參考序列,但同樣先在 BED 檢查被排除,輸出仍是 OUTSIDE_TEACHING_BED。
  • chr1:150:A>AT 是尚未正規化的 indel,標為 REVIEW_REQUIRED,不能當作簡單 SNV 已通過。
  • chr1:160:A>T 缺少 QUAL,保留 MISSING_QUAL,不擅自補零。

這個差異還可以銜接 Day 22:當我們另造在 BED 內的人工低 QUAL/REF mismatch 候選時,就能各自測到 LOW_QUAL、REF_MISMATCH。

二、用 Google AI Studio 先做 MVP,再反向拆成 Python

不會寫程式也沒關係。先把以下內容貼到 Google AI Studio Build Mode,做出不連後端的靜態面板,再理解每個畫面欄位在 Python 對應哪個步驟。

請用繁體中文建立「VCF → QC → 註釋 → Research Review Packet」研究教育 MVP。
所有資料必須標示 SYNTHETIC_EDUCATIONAL_DEMO,不能上傳真實病人 VCF。
使用七列、八個 ALT 候選的人工範例,呈現:
1. VCF 輸入預覽(CHROM/POS/REF/ALT/QUAL/FILTER);多 ALT 必須拆列。
2. 可見的 QC 順序 FILTER → BED → REF → indel → missing QUAL → low QUAL。
3. QC 表顯示每筆第一個決策原因:3 SELECTED、2 REVIEW_REQUIRED、3 REJECTED。
4. 第二個表格僅顯示三筆 SELECTED 的人工註釋:0.42、-0.17、null。
5. 三筆註釋都顯示 review_status=REVIEW_REQUIRED;不得將 SELECTED 解讀為高風險或致病。
6. 顯示輸入檔案來源 SHA-256 與 Evidence Class。
7. 提供 JSON 預覽和明顯的 OFFLINE_ONLY/無真實 API 提醒。
請使用完全人工資料,不做臨床診斷、不使用真實 AlphaGenome API,不要詢問 API Key。
AI Studio 面板功能 Python 端的實際函式 今天要學的技術
VCF 上傳預覽 parse_synthetic_vcf() TSV、多 ALT 拆分、欄位驗證
區間篩選 in_bed() 1-based POS 與 0-based BED
QC 判定 check_qc() 早期返回、理由優先順序
註釋與封包 build_review_packet() null、結構化 JSON、人工審查狀態
UI 表格 render_html() 不靠網路的報告展示
檔案指紋 hash_file() SHA-256 協助核對內容一致性

Google AI Studio 產生的網頁可能使用 React 或 Node.js;不是把網頁程式直接轉成 Python。我們要看懂它呈現的資料流,再用 Python 重建離線核心。

💻 三、今天動手做:完整離線 Python 程式

這次故意把資料拆成五個檔案:四份人工 Fixture + 一個主 Python。這能讓初學者從「程式碼跟資料不是同一件事」開始,逐步養成可追溯、可測試的習慣。

請下載配套 ZIP 並解壓,確認 day21_offline_vcf_review_lab.py、四份人工 Fixture 都在同一目錄。下面是主 Python 檔案的完整內容(與 ZIP 中版本一致):

"""Day 21: offline-only synthetic VCF -> QC -> annotation -> review packet.

Educational example using an invented chr1 reference, BED and annotation scores.
Not a complete VCF parser, genome annotation pipeline, or medical classifier.
No SDK, HTTP, credential loading, patient data, or real API calls.
Python 3.12+ standard library only.
"""
from __future__ import annotations

import argparse
import hashlib
from html import escape
import json
import math
from pathlib import Path
import re

VCF_FIELDS = ("#CHROM", "POS", "ID", "REF", "ALT", "QUAL", "FILTER", "INFO")
ASSEMBLY = "TOY_NOT_GRCH38"
SOURCE_FLAG = "##source=SYNTHETIC_EDUCATIONAL_DEMO"
REFERENCE_FLAG = "##reference=TOY_NOT_GRCH38"
ALLELE_RE = re.compile(r"[ACGT]+\Z")
VARIANT_RE = re.compile(r"chr[0-9XYM]+:[1-9][0-9]*:[ACGT]+>[ACGT]+\Z")
QC_ORDER = ["FILTER", "BED", "REF", "INDEL", "MISSING_QUAL", "LOW_QUAL"]


def parse_synthetic_vcf(text):
    """Parse just the documented 8-column, sample-free synthetic VCF subset."""
    if not isinstance(text, str) or SOURCE_FLAG not in text.splitlines() or REFERENCE_FLAG not in text.splitlines():
        raise ValueError("VCF must explicitly identify its synthetic source and toy reference")
    seen_header = False
    rows = []
    variant_ids = set()
    for line_number, line in enumerate(text.splitlines(), start=1):
        if not line.strip() or line.startswith("##"):
            continue
        if line.startswith("#"):
            if seen_header or tuple(line.split("\t")) != VCF_FIELDS:
                raise ValueError("Only the eight-column educational VCF header is supported")
            seen_header = True
            continue
        if not seen_header:
            raise ValueError("VCF records require the eight-column header")
        cells = line.split("\t")
        if len(cells) != len(VCF_FIELDS):
            raise ValueError(f"Line {line_number}: expected 8 VCF fields")
        chrom, pos_raw, _identifier, ref, alts, qual_raw, filt, _info = cells
        if not re.fullmatch(r"chr[0-9XYM]+", chrom):
            raise ValueError(f"Line {line_number}: invalid chromosome")
        if not pos_raw.isdecimal() or int(pos_raw) < 1:
            raise ValueError(f"Line {line_number}: invalid 1-based POS")
        pos = int(pos_raw)
        if ALLELE_RE.fullmatch(ref) is None or not alts:
            raise ValueError(f"Line {line_number}: invalid REF/ALT")
        if qual_raw == ".":
            qual = None
        else:
            try:
                qual = float(qual_raw)
            except ValueError as exc:
                raise ValueError(f"Line {line_number}: invalid QUAL") from exc
            if not math.isfinite(qual) or qual < 0:
                raise ValueError(f"Line {line_number}: QUAL must be finite and nonnegative")
        if not filt or filt == ".":
            raise ValueError(f"Line {line_number}: FILTER must be present")
        for alt in alts.split(","):
            if ALLELE_RE.fullmatch(alt) is None or alt == ref:
                raise ValueError(f"Line {line_number}: invalid ALT allele")
            vid = f"{chrom}:{pos}:{ref}>{alt}"
            if vid in variant_ids:
                raise ValueError(f"Line {line_number}: duplicate variant identifier")
            variant_ids.add(vid)
            rows.append({"variant_id": vid, "chrom": chrom, "pos_1based": pos,
                         "ref": ref, "alt": alt, "qual": qual, "filter": filt,
                         "source_line": line_number})
    if not seen_header or not rows:
        raise ValueError("Require synthetic header and at least one variant")
    return rows


def load_toy_reference(text):
    obj = json.loads(text)
    if not isinstance(obj, dict) or obj.get("data_kind") != "SYNTHETIC_FIXTURE" or obj.get("assembly") != ASSEMBLY:
        raise ValueError("Reference must be an explicit synthetic fixture")
    chromosomes = obj.get("chromosomes")
    if not isinstance(chromosomes, dict) or not chromosomes:
        raise ValueError("Reference chromosomes object is required")
    for chrom, sequence in chromosomes.items():
        if not isinstance(chrom, str) or not isinstance(sequence, str) or ALLELE_RE.fullmatch(sequence) is None:
            raise ValueError("Invalid toy reference sequence")
    return chromosomes


def parse_toy_bed(text):
    lines = [line for line in text.splitlines() if line.strip() and not line.startswith("#")]
    if not text.startswith("#SYNTHETIC_FIXTURE") or not lines:
        raise ValueError("BED must be an explicit synthetic fixture")
    intervals = []
    for line in lines:
        cells = line.split("\t")
        if len(cells) != 3 or not re.fullmatch(r"chr[0-9XYM]+", cells[0]):
            raise ValueError("BED must have chrom, start, end")
        try:
            start, end = int(cells[1]), int(cells[2])
        except ValueError as exc:
            raise ValueError("BED coordinates must be integers") from exc
        if start < 0 or end <= start:
            raise ValueError("BED requires nonempty 0-based half-open intervals")
        intervals.append((cells[0], start, end))
    return intervals


def load_toy_scores(text):
    obj = json.loads(text)
    if not isinstance(obj, dict) or obj.get("data_kind") != "SYNTHETIC_FIXTURE" or obj.get("assembly") != ASSEMBLY:
        raise ValueError("Scores must be an explicit synthetic fixture")
    scores = obj.get("scores")
    if not isinstance(scores, dict):
        raise ValueError("Score mapping must be an object")
    for variant, value in scores.items():
        if VARIANT_RE.fullmatch(variant) is None or isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
            raise ValueError("Invalid synthetic score key/value")
    return scores


def in_bed(chrom, pos_1based, intervals):
    anchor_0based = pos_1based - 1
    return any(c == chrom and start <= anchor_0based < end
               for c, start, end in intervals)


def check_qc(row, reference, intervals, min_qual=20.0):
    """Return first deciding QC status/reason (not all possible reasons)."""
    if row["filter"] != "PASS":
        return "REJECTED", "FILTER_NOT_PASS"
    if not in_bed(row["chrom"], row["pos_1based"], intervals):
        return "REJECTED", "OUTSIDE_TEACHING_BED"
    seq = reference.get(row["chrom"], "")
    start = row["pos_1based"] - 1
    if seq[start:start + len(row["ref"])] != row["ref"]:
        return "REJECTED", "REF_MISMATCH"
    if len(row["ref"]) != 1 or len(row["alt"]) != 1:
        return "REVIEW_REQUIRED", "INDEL_NOT_NORMALIZED"
    if row["qual"] is None:
        return "REVIEW_REQUIRED", "MISSING_QUAL"
    if row["qual"] < min_qual:
        return "REJECTED", "LOW_QUAL"
    return "SELECTED", "TEACHING_QC_PASS"


def build_review_packet(vcf_text, reference, intervals, scores):
    all_candidates = parse_synthetic_vcf(vcf_text)
    qc_rows = []
    review_packet = []
    for row in all_candidates:
        status, reason = check_qc(row, reference, intervals)
        qc_rows.append({"variant_id": row["variant_id"], "qc_status": status,
                        "reason": reason, "qual": row["qual"],
                        "filter": row["filter"], "source_line": row["source_line"]})
        if status == "SELECTED":
            score = scores.get(row["variant_id"])
            review_packet.append({"variant_id": row["variant_id"],
                                  "synthetic_score": score,
                                  "score_status": "MISSING_SCORE" if score is None else "SYNTHETIC_SCORE",
                                  "review_status": "REVIEW_REQUIRED",
                                  "evidence_class": "SYNTHETIC_FIXTURE"})
    report = {
        "evidence_class": "SYNTHETIC_EDUCATIONAL_DEMO",
        "mode": "OFFLINE_ONLY",
        "assembly": ASSEMBLY,
        "candidate_count": len(all_candidates),
        "selected_count": len(review_packet),
        "review_required_qc_count": sum(row["qc_status"] == "REVIEW_REQUIRED" for row in qc_rows),
        "rejected_count": sum(row["qc_status"] == "REJECTED" for row in qc_rows),
        "missing_score_count": sum(row["synthetic_score"] is None for row in review_packet),
        "qc_order": QC_ORDER,
        "qc_rows": qc_rows,
        "review_packet": review_packet,
        "score_source": "SYNTHETIC_FIXTURE_ONLY",
        "atlas_query_performed": False,
        "clinical_interpretation": False,
        "real_api_calls_this_run": 0,
    }
    if report["selected_count"] + report["review_required_qc_count"] + report["rejected_count"] != report["candidate_count"]:
        raise RuntimeError("QC group counts do not match candidates")
    return report


def render_html(report):
    """Self-contained static, offline teaching UI; escape ALL external strings."""
    def col(value):
        if value is None:
            return '<span class="missing">null</span>'
        return escape(str(value), quote=True)

    qc = "\n".join(
        '<tr><td>' + col(r["variant_id"]) + '</td><td><span class="pill">' +
        col(r["qc_status"]) + '</span></td><td>' + col(r["reason"]) +
        '</td><td>' + col(r["qual"]) + '</td></tr>' for r in report["qc_rows"])
    packets = "\n".join(
        '<tr><td>' + col(r["variant_id"]) + '</td><td>' +
        col(r["synthetic_score"]) + '</td><td>' + col(r["score_status"]) +
        '</td><td>' + col(r["review_status"]) + '</td></tr>' for r in report["review_packet"])
    return f'''<!doctype html>
<html lang="zh-Hant"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Day 21 | Synthetic VCF Review Packet</title><style>
:root{{color-scheme:light;font-family:system-ui,"Noto Sans TC",sans-serif;color:#283348;background:#f7f5f2}}
body{{margin:0 auto;padding:28px 22px 60px;max-width:1100px}}
h1{{font-size:1.8rem;margin:0 0 6px}}.intro{{color:#536378}}.warn{{background:#fff2dd;border-left:5px solid #bd783b;padding:14px 18px;margin:20px 0;border-radius:8px}}
.stats{{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin:20px 0}}
.stat{{background:#fff;border:1px solid #e1e2e5;border-radius:14px;padding:18px}}.num{{font-size:2rem;font-weight:750;color:#285b87}}.label{{font-size:.88rem;color:#5b6982}}
section{{background:#fff;border:1px solid #e1e2e5;border-radius:14px;padding:18px;margin-top:20px;overflow-x:auto}}
table{{width:100%;border-collapse:collapse;font-size:.9rem}}th,td{{text-align:left;padding:11px;border-bottom:1px solid #eee}}th{{background:#eef2fa}}.pill{{font-size:.8rem;background:#e6edf4;border-radius:7px;padding:4px 7px}}.missing{{color:#ae3b41;font-weight:700}}
footer{{font-size:.83rem;color:#5b6982;margin-top:30px}}</style></head><body>
<h1>Day 21 | 從 VCF 到研究審閱封包</h1><div class="intro">QC → Fixture 註釋 → Evidence / Human Review</div>
<div class="warn"><strong>SYNTHETIC EDUCATIONAL DEMO</strong>:所有資料人工合成,非 GRCh38、非 AlphaGenome API 分數,未進行臨床判定。OFFLINE_ONLY。</div>
<div class="stats"><div class="stat"><div class="num">{report['candidate_count']}</div><div class="label">ALT 候選</div></div>
<div class="stat"><div class="num">{report['selected_count']}</div><div class="label">SELECTED</div></div>
<div class="stat"><div class="num">{report['review_required_qc_count']}</div><div class="label">QC REVIEW_REQUIRED</div></div>
<div class="stat"><div class="num">{report['rejected_count']}</div><div class="label">REJECTED</div></div></div>
<section><h2>第一站|QC 全部候選(保留失敗理由)</h2><p>順序:{col(' → '.join(report['qc_order']))};每筆記錄第一個決定性原因。</p>
<table><thead><tr><th>Variant</th><th>QC Status</th><th>Reason</th><th>QUAL</th></tr></thead><tbody>{qc}</tbody></table></section>
<section><h2>第二站|可供人工審閱的候選封包</h2><p>只有 SELECTED 進入此表;但所有八筆仍保留在 QC 表。缺分數記為 null,不填零。</p>
<table><thead><tr><th>Variant</th><th>Artificial Score</th><th>Score Status</th><th>Review Status</th></tr></thead><tbody>{packets}</tbody></table></section>
<footer>Evidence: SYNTHETIC_FIXTURE_ONLY · assembly: {col(report['assembly'])} · atlas_query_performed: false · real_api_calls_this_run: 0 · 研究用教學,非醫療建議。</footer>
</body></html>'''


def hash_file(path):
    return hashlib.sha256(Path(path).read_bytes()).hexdigest()


def execute_offline(vcf_path, reference_path, regions_path, scores_path, output_dir):
    paths = {"vcf": Path(vcf_path), "reference": Path(reference_path),
             "regions": Path(regions_path), "scores": Path(scores_path)}
    data = {key: path.read_text(encoding="utf-8") for key, path in paths.items()}
    report = build_review_packet(data["vcf"], load_toy_reference(data["reference"]),
                                 parse_toy_bed(data["regions"]), load_toy_scores(data["scores"]))
    report["input_provenance"] = {key: {"filename": path.name, "sha256": hash_file(path)}
                                  for key, path in paths.items()}
    folder = Path(output_dir)
    folder.mkdir(parents=True, exist_ok=True)
    report_path = folder / "day21_review_packet.json"
    html_path = folder / "day21_review_panel.html"
    report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    html_path.write_text(render_html(report), encoding="utf-8")
    return {"status": "PASS", "mode": "OFFLINE_ONLY",
            "candidate_count": report["candidate_count"],
            "selected_count": report["selected_count"],
            "review_required_qc_count": report["review_required_qc_count"],
            "rejected_count": report["rejected_count"],
            "missing_score_count": report["missing_score_count"],
            "json_file": str(report_path), "html_file": str(html_path),
            "real_api_calls_this_run": 0}


def main(argv=None):
    parser = argparse.ArgumentParser(description="Offline synthetic VCF to research review packet")
    parser.add_argument("--mode", choices=["offline"], required=True, help="LIVE intentionally unavailable")
    parser.add_argument("--vcf", default="day21_synthetic.vcf")
    parser.add_argument("--reference", default="day21_toy_reference.json")
    parser.add_argument("--regions", default="day21_synthetic_regions.bed")
    parser.add_argument("--scores", default="day21_synthetic_scores.json")
    parser.add_argument("--output-dir", default="day21_output")
    args = parser.parse_args(argv)
    summary = execute_offline(args.vcf, args.reference, args.regions, args.scores, args.output_dir)
    print(json.dumps(summary, sort_keys=True, ensure_ascii=False))


if __name__ == "__main__":
    main()

▶ 在 WSL Ubuntu 執行

如果系統沒有 unzip,不用安裝任何套件,可以利用 Python 自帶的 ZIP 模組解壓。確認 ZIP 已下載到 Windows Downloads,執行:

cd /mnt/c/Users/user/Downloads
mkdir -p ~/alphagenome-day21-qa
python3.12 -m zipfile -e AlphaGenome_Day21_VCF_Review_Packet_API_Bridge_Author_Review_Pack.zip ~/alphagenome-day21-qa
cd ~/alphagenome-day21-qa
ls -l day21_offline_vcf_review_lab.py test_day21_offline_vcf_review_lab.py day21_synthetic.vcf day21_synthetic_regions.bed day21_toy_reference.json day21_synthetic_scores.json
python3.12 -m unittest -v test_day21_offline_vcf_review_lab
python3.12 day21_offline_vcf_review_lab.py --mode offline --vcf day21_synthetic.vcf --reference day21_toy_reference.json --regions day21_synthetic_regions.bed --scores day21_synthetic_scores.json --output-dir day21_output

如果你已經把六個必要檔案分別下載在 Downloads,也可以照 Day 20 的成功做法,使用 cp 複製到 ~/alphagenome-day21-qa/,不需要使用 ZIP。

▶ 預期與本次實際輸出

我在此執行環境以 Python 3.13.5 執行,得到下面的實測 stdout(你的 WSL Python 3.12 尚待驗收):

{"candidate_count": 8, "html_file": "day21_output/day21_review_panel.html", "json_file": "day21_output/day21_review_packet.json", "missing_score_count": 1, "mode": "OFFLINE_ONLY", "real_api_calls_this_run": 0, "rejected_count": 3, "review_required_qc_count": 2, "selected_count": 3, "status": "PASS"}

程式將生成 day21_output/day21_review_packet.json 和 day21_output/day21_review_panel.html。前者供機器與研究團隊複查,後者可以直接用瀏覽器開啟,不需要後端伺服器。

如何閱讀八筆 QC 結果?

人工候選 QC Status 第一個決策原因
chr1:100:A>C SELECTED TEACHING_QC_PASS
chr1:110:A>G SELECTED TEACHING_QC_PASS
chr1:110:A>C SELECTED TEACHING_QC_PASS
chr1:120:A>T REJECTED OUTSIDE_TEACHING_BED
chr1:130:A>G REJECTED FILTER_NOT_PASS
chr1:140:G>T REJECTED OUTSIDE_TEACHING_BED
chr1:150:A>AT REVIEW_REQUIRED INDEL_NOT_NORMALIZED
chr1:160:A>T REVIEW_REQUIRED MISSING_QUAL

注意兩個「REVIEW」的意思: QC 表中的兩筆 REVIEW_REQUIRED 是因 indel/缺 QUAL,尚未通過本例門檻;而 Review Packet 裡三筆 SELECTED 的 review_status=REVIEW_REQUIRED 指的是即使資料通過 QC,仍需要人類專家審閱。兩者不是同一種計數。

通過 QC 後的三筆註釋

候選 人工分數 註釋狀態 人工審閱
chr1:100:A>C 0.42 SYNTHETIC_SCORE REVIEW_REQUIRED
chr1:110:A>G −0.17 SYNTHETIC_SCORE REVIEW_REQUIRED
chr1:110:A>C null MISSING_SCORE REVIEW_REQUIRED

分數的正負不具有罹病風險解釋,null 也不等於 0。 所有資料皆為人工教學值。

四、從 OFFLINE 到 AlphaGenome Atlas API 的橋樑在哪裡?

Day 17 的 Reboot 歷史 Atlas 產物已證明:當時 SDK 0.9.0 曾成功執行 query_variant(),取得 AVI 與特徵欄位。但 Day 21 不能直接拿人工 chr1、人工 200 個 A 的參考序列去宣稱那是真正的 GRCh38 查詢。

未來在獲得授權後,要新增真實 Atlas adapter 時,至少有五個關卡:

  1. 先確認真實變異的基因組版本、REF、REF/ALT 標準化與實際 BED 註釋;人工 Fixture 不得進入 LIVE。
  2. 將 SELECTED 候選轉成有明確來源的 Atlas 查詢請求,並確認安裝版 SDK 的函式與 scorer 參數。
  3. 在另一個受控 LIVE 流程中取得官方回應,保留 scorer、版本、時間、輸出格式及缺值資訊。
  4. 把真正 Atlas 回應轉入新的 annotation adapter,明確標記 HISTORICAL_LIVE、CURRENT_LIVE 或 SYNTHETIC_FIXTURE,不可混成一個來源。
  5. 產生可追溯的 Review Packet,而非自動 ACMG/AMP 判斷或臨床報告。

這次沒有提供 LIVE adapter 的可執行功能。 CLI 只允許 --mode offline,以免誤觸金鑰或錯把人工分數稱為真實 AVI。即使電腦裡已有 API Key,這支程式也不會主動使用它。SDK 0.9.0 的歷史成功不等於 0.10.0 的現行 LIVE 相容性已驗證。

✅ 本日 Checklist

  • [ ] 我能說明「七列 VCF、八個 ALT 候選」是因為一列有兩個 ALT。
  • [ ] 我知道 VCF 1-based POS 必須轉為 POS−1 才能和 BED 比較。
  • [ ] 我能重現 3 SELECTED / 2 QC REVIEW_REQUIRED / 3 REJECTED。
  • [ ] 我能說明 chr1:120、chr1:140 為什麼先被 BED 排除。
  • [ ] 我能指出第三筆人工分數為 null,而非 0。
  • [ ] 我能打開 HTML 表格,也能在 JSON 中找到來源 SHA-256。
  • [ ] 我能區分 QC 通過、人工審閱、真實 API 回應與醫療判讀。

五、科學證據與限制

SYNTHETIC EDUCATIONAL DEMO。 這是一個最小化、僅八欄、無 genotype 的教學 VCF parser;無法代替完整 VCF/多等位基因 normalization/真實 FASTA 檢查/clinically validated variant prioritization。QUAL>=20 是本篇自行設定的教學門檻,非 AlphaGenome 規定或通用臨床篩選標準。BED 只用 VCF POS 錨點做練習,不能套用到真實複雜 indel。

輸入來源的 SHA-256 是檔案完整性線索,不證明資料來自可信實驗。這份程式不會觸及個人基因資料、真實 Atlas/Model API,也不會產生 ACMG/AMP 分類。原生 Reboot API 回應未完整留存,因此歷史 Atlas 數值是另一層獨立的衍生證據,不與本篇合成 score 併成同一次實驗。

六、明日預告

明天 Day 22,我們會把今天的 QC 再細分:專門檢查 FILTER、QUAL、REF 與 BED 的順序與邊界,讓每一個候選變異留下能複查的品質理由。

延伸閱讀與來源


上一篇
AlphaGenome Atlas 實戰 | Day 20 | 基因座區間探索 —— 從座標系統理解 Motif 標註
下一篇
AlphaGenome Atlas 實戰 | Day 22 | 數據前處理與初篩 —— 使用 Python 提取與過濾候選非編碼變異
系列文
《解構生命暗物質:用 AlphaGenome Atlas 破譯 98% 非編碼基因組的 30 天實戰》 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言