iT邦幫忙

2026 iThome 鐵人賽

DAY 23
0
Build on Google AI

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

AlphaGenome Atlas 實戰 | Day 23 | 自動化批次標註 —— 從缺失值到結構化欄位設計

  • 分享至 

  • xImage
  •  

💡 導讀:成績單上的空白,不代表這科得了零分

​
如果你在整理一個班級的成績單。有些學生數學拿 80 分,有些拿 0 分;還有人因為尚未交卷,成績欄是空白。若把全部空白都填成 0,就會把「還不知道」誤認為「已測量且是零」。
​
在變異分析中,情況也一樣。昨天 Day 22 我們看懂了 QC 為什麼會依序檢查 FILTER、BED、REF 和 QUAL。今天只挑 Day 21 真正通過教學 QC 的三筆候選,把人工分數整理成機器可以讀、研究者可以查的標註表。重點不是哪筆比較危險,而是:每個欄位代表什麼?資料從哪裡來?沒有分數時怎麼說清楚?
​
今天還會回到最初的學習方法:先用 Google AI Studio 想像看得見的 MVP,再用 Python 拆解面板背後的資料結構。
​

一、核心概念:把「資料」「狀態」「來源」分成不同欄位

​
Day 21 的合成 VCF 有七列記錄,其中一列含兩個 ALT,展開後共有 8 筆變異候選。Day 22 逐筆追蹤 QC,結果沒有改變:
​

QC 狀態 筆數 表示什麼?
SELECTED 3 通過人工定義的教學資料檢查,才進標註表
REVIEW_REQUIRED 2 尚未正規化的 indel 或缺 QUAL,需要人工處理
REJECTED 3 在某道檢查已被排除,仍保留理由

​
通過 QC 的三筆,依 Day 21 原本的人工分數表整理如下:
​

人工變異 synthetic_score score_status review_status
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

​
這裡的 0.42、−0.17 只是在教你資料聯接,不是 AlphaGenome AVI,也不具致病性尺度。 三筆全部需要人工審閱;SELECTED 不是高風險標籤,更不是 ACMG/AMP 分類。
​

0.0 跟 None 不是同一回事

​
Python 裡,0.0 是已取得的數字;None 代表這個欄位沒有數值。轉成 JSON 後,None 才會成為 null。程式故意使用兩個欄位:
​

  • synthetic_score:保留數值或 null。
  • score_status:明確註記 SYNTHETIC_SCORE 或 MISSING_SCORE。
    ​
    匯出 CSV 時,缺值會顯示為空白儲存格;因此必須連同 score_status=MISSING_SCORE 保存,不能依靠空白格猜測生物學意義。
    ​

二、Google AI Studio:先做一張會說明「不知道」的分析面板

​
初學者很容易直接在 VS Code 面對空白檔案不知從何開始。你可以先開啟 Google AI Studio 的 Build Mode,貼上教學包內的 AI_STUDIO_DAY23_PROMPT.txt;它要求生成一個只有人工資料的標註面板:摘要卡、QC 表、三筆選入候選、缺值提示,以及 JSON/CSV 欄位預覽。
​
畫面拆回 Python 時,可以對應到下面的模組:
​

MVP 畫面 Python 函式 學到什麼
來源 QC 統計卡 Day 21 build_review_packet() 為什麼只有三筆進標註
空白與零分的比較卡 score_state() None、0.0、null 的差異
整齊的標註資料表 annotation_schema() schema_version、欄位契約與人工審閱
JSON/CSV 下載區 emit_artifacts() 同一資料的不同輸出格式
瀏覽器上的結果面板 render_html() HTML 安全顯示與資料來源說明

注意:AI Studio 產生的介面可能是 React/JavaScript;Get Code 不會自動把它變成 Python。這裡的「逆向拆解」是照著 UI 的資料需求,自己用 Python 重建同一套輸入、處理與輸出邏輯。
​

三、設計資料欄位:什麼叫「結構化」?

​
我們將 Day 21 的 review_packet 映射成 Day 23 的固定表格,使用 schema_version=day23.synthetic.annotation.v1。欄位包括:
​

欄位群 欄位 為什麼保留?
變異識別 variant_id、chrom、pos_1based、ref、alt 確定同一筆人工候選,不混用座標
QC 判斷 qc_status、qc_reason 讓下一位研究者知道選入原因
標註資料 synthetic_score、score_status、score_source 避免把缺值、人工值與真正模型輸出混淆
人工審閱 review_status 保留 REVIEW_REQUIRED,不以軟體 PASS 代替判讀
可追溯性 source_line、input_provenance、SHA-256 能找到原 VCF 行與來源檔案內容版本

SHA-256 是內容一致性指紋,無法自行證明原始來源為真,也不代表具備臨床有效性。 TOY_NOT_GRCH38 表示我們沒有使用真正的人類參考基因組;本例原本的 BED 檢查也只是 POS−1 錨點規則,不是完整 indel 區間分析。
​

💻 四、今天動手做:延續 Day 21 程式,產出 JSON+CSV+HTML

​
這一篇刻意直接匯入、重用已通過測試的 day21_offline_vcf_review_lab.py,不重新發明一套 VCF/BED/REF/QUAL 規則。程式先檢查 Day 21 的輸入是否明確標記為人工合成,再把三筆 SELECTED 變成固定 schema。檢查失敗時會拒絕來源不明、數量不一致或冒用 LIVE/正式 AVI 的資料。
​
把教學包完整解壓至同一資料夾。必須同時存在 Day 21 原程式與四份人工資料:
​

Day23 Lab/
├── day23_offline_annotation_lab.py      # 本篇完整 Python
├── test_day23_offline_annotation_lab.py # 本篇測試
├── day21_offline_vcf_review_lab.py      # 原樣保留的 Day 21 模組
├── day21_synthetic.vcf                 # 仍是人工 VCF
├── day21_toy_reference.json            # 200 個 A 的人工參考
├── day21_synthetic_regions.bed         # 人工 BED
├── day21_synthetic_scores.json         # 兩個人工分數
└── day21_review_packet_reference.json  # 可選:上篇生成的離線 JSON

​
以下為完整的 day23_offline_annotation_lab.py,只依賴 Python 3.12 標準函式庫與同目錄的 Day 21 模組,不需要 API Key。
​

"""Day 23 | OFFLINE synthetic VCF annotation schema and missing-value lesson.
​
Reuses Day21's ORIGINAL synthetic parser/QC and four byte-identical fixtures.
Does not load an AlphaGenome SDK, use a key, make a request, or interpret disease.
Python 3.12+ standard library only. NOT a clinical or production VCF engine.
"""
from __future__ import annotations
​
import argparse
import csv
import hashlib
from html import escape
import json
import math
from pathlib import Path
import re
​
from day21_offline_vcf_review_lab import (
    ASSEMBLY, QC_ORDER, VARIANT_RE, build_review_packet,
    load_toy_reference, load_toy_scores, parse_toy_bed,
)
​
EVIDENCE_CLASS = "SYNTHETIC_EDUCATIONAL_DEMO"
SCHEMA_VERSION = "day23.synthetic.annotation.v1"
ANNOTATION_FIELDS = (
    "variant_id", "chrom", "pos_1based", "ref", "alt", "qc_status",
    "qc_reason", "synthetic_score", "score_status", "score_source",
    "review_status", "source_line",
)
SHA256_RE = re.compile(r"[a-f0-9]{64}\Z")
EXPECTED_QC_ORDER = ("FILTER", "BED", "REF", "INDEL", "MISSING_QUAL", "LOW_QUAL")
ALLOWED_QC = {"SELECTED", "REVIEW_REQUIRED", "REJECTED"}
REASONS_BY_STATUS = {
    "SELECTED": {"TEACHING_QC_PASS"},
    "REVIEW_REQUIRED": {"INDEL_NOT_NORMALIZED", "MISSING_QUAL"},
    "REJECTED": {"FILTER_NOT_PASS", "OUTSIDE_TEACHING_BED", "REF_MISMATCH", "LOW_QUAL"},
}
​
​
def score_state(value):
    """None means absent; 0.0 is a PRESENT numeric teaching value."""
    if value is None:
        return "MISSING_SCORE"
    if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
        raise ValueError("Synthetic score must be finite numeric or null")
    return "SYNTHETIC_SCORE"
​
​
def split_variant(variant_id):
    if not isinstance(variant_id, str) or VARIANT_RE.fullmatch(variant_id) is None:
        raise ValueError("Invalid artificial variant ID")
    chrom, pos, alleles = variant_id.split(":")
    ref, alt = alleles.split(">")
    if ref == alt:
        raise ValueError("REF and ALT must differ")
    return chrom, int(pos), ref, alt
​
​
def validate_day21_packet(packet):
    """Accept only the narrow, explicit Day21 synthetic contract, never live/patient data."""
    if not isinstance(packet, dict):
        raise ValueError("Day21 packet must be a JSON object")
    if (packet.get("evidence_class") != EVIDENCE_CLASS
            or packet.get("mode") != "OFFLINE_ONLY"
            or packet.get("assembly") != ASSEMBLY
            or packet.get("score_source") != "SYNTHETIC_FIXTURE_ONLY"
            or packet.get("atlas_query_performed") is not False
            or packet.get("clinical_interpretation") is not False
            or packet.get("real_api_calls_this_run") != 0):
        raise ValueError("Refusing input not explicitly marked as offline synthetic teaching data")
    if tuple(QC_ORDER) != EXPECTED_QC_ORDER or packet.get("qc_order") != list(EXPECTED_QC_ORDER):
        raise ValueError("QC precedence differs from validated Day21")
    qc_rows, review_rows = packet.get("qc_rows"), packet.get("review_packet")
    if not isinstance(qc_rows, list) or not qc_rows or not isinstance(review_rows, list):
        raise ValueError("Day21 packet requires QC and annotation lists")
    qc_by_id = {}
    for row in qc_rows:
        if not isinstance(row, dict):
            raise ValueError("Malformed QC row")
        variant_id = row.get("variant_id")
        split_variant(variant_id)
        if variant_id in qc_by_id or row.get("qc_status") not in ALLOWED_QC:
            raise ValueError("Duplicate QC variant or invalid status")
        if row.get("reason") not in REASONS_BY_STATUS[row["qc_status"]]:
            raise ValueError("QC reason is not valid for its status")
        qual = row.get("qual")
        if qual is not None and (isinstance(qual, bool) or not isinstance(qual, (int, float)) or not math.isfinite(qual) or qual < 0):
            raise ValueError("Invalid QUAL in QC row")
        if (not isinstance(row.get("filter"), str) or not row["filter"]
                or not isinstance(row.get("source_line"), int)
                or isinstance(row.get("source_line"), bool)
                or row["source_line"] < 1):
            raise ValueError("QC filter and original source line required")
        qc_by_id[variant_id] = row
    counts = {
        "SELECTED": sum(r["qc_status"] == "SELECTED" for r in qc_rows),
        "REVIEW_REQUIRED": sum(r["qc_status"] == "REVIEW_REQUIRED" for r in qc_rows),
        "REJECTED": sum(r["qc_status"] == "REJECTED" for r in qc_rows),
    }
    if any(packet.get(field) != expected for field, expected in (
        ("candidate_count", len(qc_rows)), ("selected_count", counts["SELECTED"]),
        ("review_required_qc_count", counts["REVIEW_REQUIRED"]),
        ("rejected_count", counts["REJECTED"]),
    )):
        raise ValueError("QC count contract mismatch")
    reviewed = set()
    missing = 0
    for row in review_rows:
        if not isinstance(row, dict):
            raise ValueError("Malformed annotation row")
        variant_id = row.get("variant_id")
        split_variant(variant_id)
        if variant_id in reviewed or variant_id not in qc_by_id:
            raise ValueError("Duplicate or unmatched annotation")
        if qc_by_id[variant_id]["qc_status"] != "SELECTED":
            raise ValueError("Non-selected variants must not be annotated")
        value = row.get("synthetic_score")
        status = score_state(value)
        if (row.get("score_status") != status
                or row.get("review_status") != "REVIEW_REQUIRED"
                or row.get("evidence_class") != "SYNTHETIC_FIXTURE"):
            raise ValueError("Annotation status or source is inconsistent")
        missing += value is None
        reviewed.add(variant_id)
    if (reviewed != {vid for vid, row in qc_by_id.items() if row["qc_status"] == "SELECTED"}
            or packet.get("missing_score_count") != missing):
        raise ValueError("Missing, extra, or wrongly counted annotations")
    return qc_by_id
​
​
def annotation_schema(packet, input_provenance):
    """Transform verified Day21 rows into Day23's explicit analysis-ready schema."""
    qc_by_id = validate_day21_packet(packet)
    if not isinstance(input_provenance, dict) or not input_provenance:
        raise ValueError("Input provenance is required")
    for name, record in input_provenance.items():
        if not isinstance(name, str) or not isinstance(record, dict):
            raise ValueError("Malformed provenance")
        if (not isinstance(record.get("filename"), str)
                or not record["filename"] or "/" in record["filename"] or "\\" in record["filename"]
                or not isinstance(record.get("sha256"), str)
                or SHA256_RE.fullmatch(record["sha256"]) is None):
            raise ValueError("Each provenance record needs basename and SHA-256")
    annotations = []
    for row in packet["review_packet"]:
        variant_id = row["variant_id"]
        chrom, pos, ref, alt = split_variant(variant_id)
        qc = qc_by_id[variant_id]
        annotations.append({
            "variant_id": variant_id,
            "chrom": chrom,
            "pos_1based": pos,
            "ref": ref,
            "alt": alt,
            "qc_status": "SELECTED",
            "qc_reason": qc["reason"],
            "synthetic_score": row["synthetic_score"],
            "score_status": row["score_status"],
            "score_source": "SYNTHETIC_FIXTURE_ONLY",
            "review_status": "REVIEW_REQUIRED",
            "source_line": qc["source_line"],
        })
    if len(annotations) != packet["selected_count"]:
        raise ValueError("Day23 annotation count mismatch")
    return {
        "schema_version": SCHEMA_VERSION,
        "evidence_class": EVIDENCE_CLASS,
        "mode": "OFFLINE_ONLY",
        "assembly": ASSEMBLY,
        "annotation_columns": list(ANNOTATION_FIELDS),
        "candidate_count": packet["candidate_count"],
        "selected_count": packet["selected_count"],
        "review_required_qc_count": packet["review_required_qc_count"],
        "rejected_count": packet["rejected_count"],
        "annotated_count": sum(r["score_status"] == "SYNTHETIC_SCORE" for r in annotations),
        "missing_score_count": sum(r["score_status"] == "MISSING_SCORE" for r in annotations),
        "qc_order": list(QC_ORDER),
        "qc_rows": packet["qc_rows"],
        "annotations": annotations,
        "missing_data_policy": "JSON null + MISSING_SCORE; blank CSV numeric cell + MISSING_SCORE; never substitute zero",
        "input_provenance": input_provenance,
        "pipeline_source": "REUSED_UNMODIFIED_DAY21_SYNTHETIC_QC",
        "official_avi": False,
        "atlas_query_performed": False,
        "clinical_interpretation": False,
        "real_api_calls_this_run": 0,
    }
​
​
def render_html(report):
    """Create a standalone HTML table; escape all data-derived labels."""
    def h(value):
        return escape("null" if value is None else str(value), quote=True)
    annotation_rows = "\n".join(
        "<tr>" + "".join(f"<td>{h(row[column])}</td>" for column in
                           ("variant_id", "qc_status", "synthetic_score", "score_status", "score_source", "review_status")) + "</tr>"
        for row in report["annotations"]
    )
    qc_rows = "\n".join(
        "<tr>" + "".join(f"<td>{h(row.get(column))}</td>" for column in
                           ("variant_id", "qc_status", "reason")) + "</tr>"
        for row in report["qc_rows"]
    )
    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 23 | Synthetic Annotation Schema</title>
<style>:root{{font:16px/1.55 system-ui,"Noto Sans TC",sans-serif;color:#2e3649;background:#f1f1f2}}
body{{margin:auto;max-width:1140px;padding:30px 18px 65px}}h1{{font-size:1.85rem}}
.warning{{background:#fff1de;border-left:5px solid #b8743e;padding:15px;border-radius:8px}}
.stats{{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px}}
.stats div,section{{background:white;border:1px solid #dfdfdf;border-radius:12px;padding:16px;margin-top:18px}}
.num{{font-size:2rem;font-weight:800;color:#286683}}.label{{font-size:.85rem;color:#697386}}
table{{width:100%;border-collapse:collapse}}th,td{{text-align:left;padding:10px;border-bottom:1px solid #eaeaea}}
th{{background:#e9f0f5}}section{{overflow-x:auto}}.missing{{color:#a83b43}}
footer{{font-size:.85rem;color:#687386;margin-top:25px}}</style></head><body>
<h1>Day 23|從缺失值到結構化標註欄位</h1>
<div class="warning"><strong>SYNTHETIC · OFFLINE_ONLY · TOY_NOT_GRCH38</strong><br>
所有分數均為人工教學值,不是 AlphaGenome AVI,沒有患者資料或真實 API 呼叫。所有 SELECTED 仍待研究人工審閱。</div>
<div class="stats"><div><span class="num">{report['candidate_count']}</span><br><span class="label">候選</span></div>
<div><span class="num">{report['selected_count']}</span><br><span class="label">通過教學 QC</span></div>
<div><span class="num">{report['annotated_count']}</span><br><span class="label">有人工分數</span></div>
<div><span class="num">{report['missing_score_count']}</span><br><span class="label">分數缺值</span></div></div>
<section><h2>標註表|一筆候選=一列資料</h2><p>請注意 <code>synthetic_score=null</code> 表示缺分數,並非 0;分數正負號沒有臨床風險意義。</p>
<table><thead><tr><th>Variant</th><th>QC</th><th>人工分數</th><th>分數狀態</th><th>來源</th><th>審閱</th></tr></thead><tbody>{annotation_rows}</tbody></table></section>
<section><h2>全部 QC 紀錄(保留失敗原因)</h2><table><thead><tr><th>Variant</th><th>QC Status</th><th>Reason</th></tr></thead><tbody>{qc_rows}</tbody></table></section>
<footer>schema_version: {h(report['schema_version'])} · input provenance SHA-256 另見 JSON · official_avi=false · real_api_calls_this_run=0 · 研究教育用途。</footer></body></html>'''
​
​
def emit_artifacts(report, output_dir):
    """Write deterministic JSON/CSV/HTML without external services."""
    folder = Path(output_dir)
    folder.mkdir(parents=True, exist_ok=True)
    json_file = folder / "day23_annotation_report.json"
    csv_file = folder / "day23_annotation_table.csv"
    html_file = folder / "day23_annotation_panel.html"
    json_file.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2, allow_nan=False) + "\n", encoding="utf-8")
    with csv_file.open("w", encoding="utf-8", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=ANNOTATION_FIELDS, extrasaction="raise")
        writer.writeheader()
        writer.writerows(report["annotations"])
    html_file.write_text(render_html(report), encoding="utf-8")
    return {"status": "PASS", "mode": "OFFLINE_ONLY",
            "candidate_count": report["candidate_count"],
            "selected_count": report["selected_count"],
            "annotated_count": report["annotated_count"],
            "missing_score_count": report["missing_score_count"],
            "json_file": str(json_file), "csv_file": str(csv_file), "html_file": str(html_file),
            "real_api_calls_this_run": 0}
​
​
def execute_offline(vcf, reference, regions, scores, output_dir, input_day21=None):
    if input_day21 is not None:
        path = Path(input_day21)
        packet = json.loads(path.read_text(encoding="utf-8"))
        provenance = {"day21_review_packet": {
            "filename": path.name, "sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
        }}
    else:
        paths = {"vcf": Path(vcf), "reference": Path(reference),
                 "regions": Path(regions), "scores": Path(scores)}
        data = {key: path.read_text(encoding="utf-8") for key, path in paths.items()}
        packet = build_review_packet(
            data["vcf"], load_toy_reference(data["reference"]),
            parse_toy_bed(data["regions"]), load_toy_scores(data["scores"])
        )
        provenance = {
            key: {"filename": path.name, "sha256": hashlib.sha256(path.read_bytes()).hexdigest()}
            for key, path in paths.items()
        }
    return emit_artifacts(annotation_schema(packet, provenance), output_dir)
​
​
def main(argv=None):
    p = argparse.ArgumentParser(description="Day23 offline synthetic annotation and schema lab")
    p.add_argument("--mode", choices=["offline"], required=True, help="LIVE is intentionally disabled")
    p.add_argument("--input-day21", help="Optional prior Day21 synthetic JSON; excludes non-synthetic data")
    p.add_argument("--vcf", default="day21_synthetic.vcf")
    p.add_argument("--reference", default="day21_toy_reference.json")
    p.add_argument("--regions", default="day21_synthetic_regions.bed")
    p.add_argument("--scores", default="day21_synthetic_scores.json")
    p.add_argument("--output-dir", default="day23_output")
    args = p.parse_args(argv)
    result = execute_offline(args.vcf, args.reference, args.regions, args.scores,
                             args.output_dir, input_day21=args.input_day21)
    print(json.dumps(result, ensure_ascii=False, sort_keys=True))
    return result
​
​
if __name__ == "__main__":
    main()

​

▶ 執行方式(WSL Ubuntu Python 3.12)

​
先在 Windows Downloads 取得教學 ZIP,再使用 Python 內建的 zipfile 解壓,不需要另裝 unzip:
​

cd /mnt/c/Users/user/Downloads
mkdir -p ~/alphagenome-day23-qa
python3.12 -m zipfile -e AlphaGenome_Day23_Annotation_Schema_API_Bridge_Author_Review_Pack.zip ~/alphagenome-day23-qa
cd ~/alphagenome-day23-qa
python3.12 -m unittest -v test_day23_offline_annotation_lab
python3.12 day23_offline_annotation_lab.py --mode offline --output-dir day23_output

​
主程式會在 day23_output/ 生成三個檔案:day23_annotation_report.json(完整資料契約與全部八筆 QC 原因)、day23_annotation_table.csv(三筆選入候選)和 day23_annotation_panel.html(瀏覽器可開啟的人工結果面板)。
​

▶ 本次 Python 3.13.5 實際輸出

​
下列為新教學程式實際執行的 stdout,Python 3.12 本機測試仍待你重跑;本例從未呼叫真實 AlphaGenome API。
​

{"annotated_count": 2, "candidate_count": 8, "csv_file": "day23_output/day23_annotation_table.csv", "html_file": "day23_output/day23_annotation_panel.html", "json_file": "day23_output/day23_annotation_report.json", "missing_score_count": 1, "mode": "OFFLINE_ONLY", "real_api_calls_this_run": 0, "selected_count": 3, "status": "PASS"}

​
CSV 內容中的最後一筆是:
​

chr1:110:A>C,chr1,110,A,C,SELECTED,TEACHING_QC_PASS,,MISSING_SCORE,SYNTHETIC_FIXTURE_ONLY,REVIEW_REQUIRED,6

​
請注意兩個逗號之間的空白數值欄。這表示缺少分數,而不是得分為零。
​

小實驗:刻意對照真正的 0 和缺值

​
這個指令只比較欄位類型,不更動上面的八筆教學資料:
​

python3.12 -c 'from day23_offline_annotation_lab import score_state; print("0.0:", score_state(0.0)); print("None:", score_state(None))'

​
預期:
​

0.0: SYNTHETIC_SCORE
None: MISSING_SCORE

​

進階:直接讀取 Day 21 的 JSON 封包

​
如果你已完成 Day 21,也可以把那天產生的 day21_review_packet.json 放在相同資料夾,再執行:
​

python3.12 day23_offline_annotation_lab.py --mode offline --input-day21 day21_review_packet.json --output-dir day23_from_day21

​
教學 ZIP 另附 day21_review_packet_reference.json,供尚未保留 Day 21 輸出的讀者練習。輸入仍必須帶有 OFFLINE_ONLY、SYNTHETIC_EDUCATIONAL_DEMO、TOY_NOT_GRCH38 等標記;程式會拒絕未經驗證的 LIVE/臨床報告。這不是正式醫療資料解析器。
​

五、回到 AlphaGenome API Bridge:什麼時候才算真的完成標註?

​
這篇的 synthetic_score 不等於 Atlas 的 AVI。真實 API 回傳可能有不同的 scorer 名稱、AnnData 結構、模型與版本 metadata。若日後要接官方 query_variant(),應另建專用轉換器,保留 source_type、scorer、單位、版本、ontology、reference build 和 evidence_class,不能把官方 AVI 直接塞進 synthetic_score 假裝已完成相容性測試。
​
先前 Reboot 歷史查詢有真實 Atlas 衍生資料,但那是另一個歷史證據層,不是本篇八筆虛構 chr1 候選的結果。本篇沒有 LIVE 模式;之後如要驗證官方介面,須另行取得明確授權。
​

✅ 本日 Checklist

​

  • [ ] 我能說明七列 VCF 為什麼展開成八筆 ALT 候選。
  • [ ] 我能重現 QC 的 3 選入、2 待審、3 排除,並在完整 JSON 中找到每筆第一個原因。
  • [ ] 我能指出三筆標註中 2 筆有人工分數、1 筆為 null,不是把缺值填零。
  • [ ] 我知道 CSV 可能把缺值顯示為空白格,必須讀 score_status。
  • [ ] 我能在報告找到 schema_version、input_provenance、score_source、review_status。
  • [ ] 我知道這些結果全是人工教學資料,既不是官方 AVI,也不是臨床診斷。
    ​

六、科學證據與限制

​
SYNTHETIC EDUCATIONAL DEMO/OFFLINE_ONLY。本篇重用 Day 21 同一套人工 VCF、人工參考基因組、BED 和兩個人工數值;QC 是為入門教學選定的固定順序,沒有完成完整 VCF/indel/真實參考 FASTA/群體頻率檢查。
​
53 項單元測試僅驗證解析、重用 QC、缺值契約、CSV/HTML/JSON、欄位結構與拒絕 LIVE 等本地軟體行為,不是模型分子效應或臨床有效性測試。不處理真實患者資料、不呼叫 AlphaGenome/Gemini/PubMed API,也沒有自動 ACMG/AMP 判定。所有 review_status 保留 REVIEW_REQUIRED。
​

七、明日預告

​
明天 Day 24,我們將把 REF 與 ALT 的人工軌跡並排,算出兩條訊號的差值。知道資料欄位如何設計後,下一步要學會問:「訊號變化發生在哪裡?這個差分能支持什麼假說,又還不能證明什麼?」
​

延伸閱讀

​


上一篇
AlphaGenome Atlas 實戰 | Day 22 | 數據前處理與初篩 —— 使用 Python 提取與過濾候選非編碼變異
下一篇
AlphaGenome Atlas 實戰 | Day 24 | REF/ALT 軌跡差分分析 —— 從訊號變化提出機制問題
系列文
《解構生命暗物質:用 AlphaGenome Atlas 破譯 98% 非編碼基因組的 30 天實戰》 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言