想像機場安檢有六道關卡:證件、目的地、資料比對、特殊行李、資料完整性、品質門檻。如果旅客在第二關就被攔下,後面四關根本還沒進行。你不能因為報表沒有記載「第五關不合格」,就認定他通過了第五關。
昨天 Day 21,我們用七列人工 VCF 拆出八筆 ALT 候選,整理成 3 筆 SELECTED、2 筆 REVIEW_REQUIRED、3 筆 REJECTED。今天不增加新患者或新變異,反而要問一個更細的問題:程式是按照什麼順序判斷的?什麼時候應停止?哪些條件其實尚未檢查?
固定的教學檢查順序是:
FILTER → BED → REF → INDEL → MISSING_QUAL → LOW_QUAL
| 關卡 | Python 實際確認的問題 | 本篇的限制 |
|---|---|---|
| FILTER | VCF 的 FILTER 欄是否為 PASS? | PASS 只表示來源變異呼叫流程的過濾狀態,不保證功能正常 |
| BED | POS−1 是否落在人工 BED 的 [start,end)? |
本篇只比對 POS 錨點,不是完整 indel 覆蓋範圍 |
| REF | VCF 的 REF 是否符合本地人工參考序列? | 人工參考是 200 個 A,不是 GRCh38 |
| INDEL | REF/ALT 是否都是單一鹼基? | Indel 標成 INDEL_NOT_NORMALIZED,交人工審閱 |
| MISSING_QUAL | QUAL 是否為缺失值 .? |
缺值不是 0 |
| LOW_QUAL | QUAL 是否達到本例人工門檻 20? | 20 是教學門檻,不是通用臨床標準 |
依照 VCF 格式,POS 是 1-based;BED 常見的區間座標是 0-based、左含右不含。例如人工 BED [90,115),用本篇的 POS 錨點演示:POS=91 對應 0-based 90(在區間內),POS=115 對應 114(仍在內),POS=116 對應 115(已在外)。這一座標制度可以從 UCSC 的官方說明獨立查證。
重要補充:用 POS−1 判斷「錨點是否在 BED」是本篇最小教學規則。真正研究 indel 或複雜變異時,還需正規化變異並檢查 REF 長度、區間覆蓋、參考基因組版本等;不能把本篇小工具當成完整 VCF/BED 分析器。
本篇直接重用 Day 21 的相同人工 VCF、人工參考與 BED,三個檔案的 SHA-256 已依 Day 21 審閱封包確認一致。七列 VCF 之中 chr1:110 有兩個 ALT,展開後共八筆候選。
| 人工候選 | QC 結果 | 讀者應注意的地方 |
|---|---|---|
chr1:100:A>C、chr1:110:A>G、chr1:110:A>C |
3 × SELECTED |
只表示通過本例門檻,不代表致病 |
chr1:120:A>T(QUAL=10) |
OUTSIDE_TEACHING_BED |
先被 BED 排除,所以未到 LOW_QUAL |
chr1:130:A>G(FILTER=q10) |
FILTER_NOT_PASS |
第一關就停止 |
chr1:140:G>T(REF=G) |
OUTSIDE_TEACHING_BED |
先被 BED 排除,所以未到 REF_MISMATCH |
chr1:150:A>AT |
INDEL_NOT_NORMALIZED |
REVIEW_REQUIRED,沒有假裝已正規化 |
chr1:160:A>T(QUAL=.) |
MISSING_QUAL |
REVIEW_REQUIRED,沒有把缺值視為 0 |
這張表告訴我們:報表的 reason 是第一個決定性原因,不是所有可能問題的完整清單。
為了讓讀者親眼看到另外兩個 QC 分支,我們另造兩筆不加入原本八筆統計的教學案例:
chr1:105:A>T、QUAL=10、FILTER=PASS、人工 REF=A → LOW_QUAL。chr1:106:G>T、QUAL=60、FILTER=PASS、人工 REF 實際是 A → REF_MISMATCH。
還記得第一次打開空白的 VS Code 嗎?我們先不急著手寫表格,而是讓 Google AI Studio Build Mode 用固定的人工 JSON 做一個查詢介面,再回頭拆解 Python 怎麼準備這些資料。
在 Google AI Studio 使用下面的提示詞;此提示詞同時附在教學包的 AI_STUDIO_DAY22_PROMPT.txt:
請建立一個「Day 22 — VCF QC 決策追蹤」中文教學 MVP,參考醫學研究儀表板的清楚資訊設計。
資料來源限定人工合成資料,不要要求或存取真實病人資料、API 金鑰、Google/AlphaGenome SDK 或外部伺服器。
介面包括:
1. 頂部 8 筆 ALT 候選、3 SELECTED、2 REVIEW_REQUIRED、3 REJECTED 的摘要卡。
2. QC 順序視覺流程:FILTER → BED → REF → INDEL → MISSING_QUAL → LOW_QUAL。
3. 逐筆 VCF QC 表:variant_id、QUAL、FILTER、status、reason;可以展開「已檢查步驟」與「未檢查步驟」。
4. 兩個獨立教學案例:chr1:105:A>T 的 LOW_QUAL,以及 chr1:106:G>T 的 REF_MISMATCH。
5. BED 端點演示:[90,115) 對應 1-based 的 POS=91–115,不含 POS=116。
6. 明顯的 SYNTHETIC EDUCATIONAL DEMO、TOY_NOT_GRCH38、No real API call 標籤。
介面數值必須能追溯至預先固定的人工 JSON;不要自行捏造 API 回傳,也不要宣稱這份 UI 已實作真實 AlphaGenome API。
提供每個 UI 區塊與 Python 資料欄位的對照表,讓初學者之後可以從 MVP 回頭理解 Python 模組。
逆向拆解時看三件事:畫面上的狀態卡來自 candidate_count 等欄位;逐筆表格來自 qc_rows;展開的 QC 過程來自 checked_steps 與 not_checked_steps。AI Studio 可能先產生 React/JavaScript 介面;我們接下來用 Python 重建資料處理模組,並不宣稱下載程式碼就會自動轉成 Python。
這是 Day 21 → Day 22 的重點:我們把既有的 day21_offline_vcf_review_lab.py 視為已測試的基礎模組,再加入 trace_qc(),讓程式顯示每一步的結果。若 Day 22 的決策與 Day 21 check_qc() 不一致,程式會主動報錯,避免兩天的同一批資料有兩種解釋。
下載教學包後,資料夾需同時存在以下內容:
Day22 Lab/
├── day22_offline_qc_explorer_lab.py # 本篇新程式
├── day21_offline_vcf_review_lab.py # Day21 已測試模組;本篇直接匯入
├── day21_synthetic.vcf # Day21 原樣人工 VCF
├── day21_toy_reference.json # Day21 原樣人工參考
├── day21_synthetic_regions.bed # Day21 原樣人工 BED
└── test_day22_offline_qc_explorer_lab.py
請勿只複製下面一支 Python 就期待完整運作:這篇刻意演示模組重用,必須一起取得 ZIP 裡的 Day21 模組及三份合成資料。整包可在沒有 API Key、沒有網路的電腦上執行。
day22_offline_qc_explorer_lab.py
"""Day 22 — inspect the first-decision FILTER/BED/REF/indel/QUAL QC chain.
This offline educational program REUSES the verified Day 21 parsing and QC
functions, on the byte-identical synthetic fixtures shipped alongside it.
It does not implement the full VCF standard, variant normalization, genome
alignment, clinical interpretation, or any AlphaGenome network call.
Python 3.12+ standard library only.
"""
from __future__ import annotations
import argparse
from collections import Counter
from html import escape
import hashlib
import json
import math
from pathlib import Path
from day21_offline_vcf_review_lab import (
ASSEMBLY, QC_ORDER, check_qc, in_bed,
load_toy_reference, parse_synthetic_vcf, parse_toy_bed,
)
EVIDENCE_CLASS = "SYNTHETIC_EDUCATIONAL_DEMO"
FAIL_REASON = {
"FILTER": ("REJECTED", "FILTER_NOT_PASS"),
"BED": ("REJECTED", "OUTSIDE_TEACHING_BED"),
"REF": ("REJECTED", "REF_MISMATCH"),
"INDEL": ("REVIEW_REQUIRED", "INDEL_NOT_NORMALIZED"),
"MISSING_QUAL": ("REVIEW_REQUIRED", "MISSING_QUAL"),
"LOW_QUAL": ("REJECTED", "LOW_QUAL"),
}
def validate_min_qual(value):
"""Validate a teaching threshold; it is NOT a clinical decision cutoff."""
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or value < 0:
raise ValueError("min_qual must be a finite nonnegative number")
return float(value)
def trace_qc(row, reference, intervals, min_qual=20.0):
"""Explain *only* the checks reached before Day21's deciding QC result.
Checks later in the chain are deliberately NOT evaluated once a prior check
makes the decision. This is central to the Day22 lesson.
"""
min_qual = validate_min_qual(min_qual)
steps = []
status = "SELECTED"
reason = "TEACHING_QC_PASS"
for step in QC_ORDER:
if step == "FILTER":
passed = row["filter"] == "PASS"
detail = "FILTER=PASS" if passed else "FILTER is not PASS"
elif step == "BED":
passed = in_bed(row["chrom"], row["pos_1based"], intervals)
detail = f"VCF POS {row['pos_1based']} -> BED anchor {row['pos_1based'] - 1}"
elif step == "REF":
seq = reference.get(row["chrom"], "")
start = row["pos_1based"] - 1
expected = seq[start:start + len(row["ref"])]
passed = expected == row["ref"]
detail = "REF matches artificial reference" if passed else "REF mismatch / absent toy reference"
elif step == "INDEL":
passed = len(row["ref"]) == 1 and len(row["alt"]) == 1
detail = "SNV teaching subset" if passed else "indel requires independent normalization/review"
elif step == "MISSING_QUAL":
passed = row["qual"] is not None
detail = "QUAL present" if passed else "QUAL missing (dot in synthetic VCF)"
elif step == "LOW_QUAL":
passed = row["qual"] >= min_qual
detail = f"QUAL compared against artificial threshold {min_qual:g}"
else:
raise RuntimeError(f"Unknown QC step in Day21 source module: {step}")
steps.append({"check": step, "passed": passed, "detail": detail})
if not passed:
status, reason = FAIL_REASON[step]
break
original_status, original_reason = check_qc(row, reference, intervals, min_qual=min_qual)
if (status, reason) != (original_status, original_reason):
raise AssertionError("Day22 trace differs from the tested Day21 QC decision")
return {"variant_id": row["variant_id"],
"qc_status": status, "reason": reason,
"bed_anchor_0based": row["pos_1based"] - 1,
"checked_steps": steps,
"not_checked_steps": QC_ORDER[len(steps):],
"source_line": row.get("source_line"),
"qual": row["qual"], "filter": row["filter"]}
def make_probe(label, pos, ref, alt, qual=60.0, filt="PASS", chrom="chr1"):
return {"label": label, "variant_id": f"{chrom}:{pos}:{ref}>{alt}",
"chrom": chrom, "pos_1based": pos, "ref": ref, "alt": alt,
"qual": qual, "filter": filt, "source_line": None}
def extra_probe_rows():
"""Explicit isolated examples that DO exercise otherwise shadowed branches."""
return [
make_probe("LOW_QUAL_INSIDE_BED", 105, "A", "T", qual=10),
make_probe("REF_MISMATCH_INSIDE_BED", 106, "G", "T"),
make_probe("LOW_QUAL_OUTSIDE_BED", 120, "A", "T", qual=10),
make_probe("REF_MISMATCH_OUTSIDE_BED", 140, "G", "T"),
make_probe("BED_FIRST_INCLUDED_POS", 91, "A", "C"),
make_probe("BED_LAST_INCLUDED_POS", 115, "A", "C"),
make_probe("BED_FIRST_EXCLUDED_POS", 116, "A", "C"),
make_probe("SECOND_BED_FIRST_INCLUDED_POS", 146, "A", "C"),
make_probe("INDEL_NEEDS_REVIEW", 150, "A", "AT"),
make_probe("MISSING_QUAL_NEEDS_REVIEW", 160, "A", "T", qual=None),
]
def qc_report(vcf_text, reference, intervals, min_qual=20.0):
validate_min_qual(min_qual)
if QC_ORDER != ["FILTER", "BED", "REF", "INDEL", "MISSING_QUAL", "LOW_QUAL"]:
raise RuntimeError("Unexpected Day21 QC order; do not silently change lesson")
input_rows = parse_synthetic_vcf(vcf_text)
rows = [trace_qc(row, reference, intervals, min_qual) for row in input_rows]
probes = [{"label": row["label"], **trace_qc(row, reference, intervals, min_qual)}
for row in extra_probe_rows()]
counts = Counter(row["qc_status"] for row in rows)
reasons = Counter(row["reason"] for row in rows)
assert sum(counts.values()) == len(input_rows)
return {
"data_kind": EVIDENCE_CLASS,
"assembly": ASSEMBLY,
"mode": "OFFLINE_ONLY",
"qc_policy": "FIRST_DECIDING_REASON_ONLY",
"qc_order": list(QC_ORDER),
"bed_policy": "VCF_1BASED_POS_MINUS_ONE_ANCHOR_FOR_TOY_BED",
"min_qual_artificial_threshold": float(min_qual),
"candidate_count": len(input_rows),
"selected_count": counts.get("SELECTED", 0),
"review_required_count": counts.get("REVIEW_REQUIRED", 0),
"rejected_count": counts.get("REJECTED", 0),
"reason_counts": dict(sorted(reasons.items())),
"qc_rows": rows,
"supplementary_probe_cases": probes,
"fixture_source": "DAY21_BYTE_IDENTICAL_SYNTHETIC_FIXTURES",
"source_parser_and_qc_module": "day21_offline_vcf_review_lab.py",
"real_genome_used": False,
"real_api_calls_this_run": 0,
"atlas_query_performed": False,
"clinical_interpretation": False,
}
def render_html(report):
"""Single-file static HTML dashboard; every data-derived string is escaped."""
def q(value):
return escape("null" if value is None else str(value), quote=True)
rows_html = "\n".join(
"<tr>" + "".join(f"<td>{q(value)}</td>" for value in (
row["variant_id"], row["qc_status"], row["reason"], row["bed_anchor_0based"],
row["qual"], " → ".join(s["check"] for s in row["checked_steps"]))) + "</tr>"
for row in report["qc_rows"]
)
probes_html = "\n".join(
"<tr>" + "".join(f"<td>{q(value)}</td>" for value in (
row["label"], row["variant_id"], row["qc_status"], row["reason"],
" → ".join(s["check"] for s in row["checked_steps"]))) + "</tr>"
for row in report["supplementary_probe_cases"]
)
details_html = "\n".join(
"<details><summary>" + q(row["variant_id"]) + " · " + q(row["reason"]) + "</summary>" +
"<ol>" + "".join("<li>" + q(s["check"]) + ":" +
("PASS" if s["passed"] else "STOP") + " — " + q(s["detail"]) + "</li>"
for s in row["checked_steps"]) + "</ol>" +
"<p>未繼續檢查:" + q(", ".join(row["not_checked_steps"]) or "無") + "</p></details>"
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>Day22 | Offline VCF QC Inspector</title>
<style>:root{{font:16px/1.6 system-ui,"Noto Sans TC",sans-serif;color:#263b51;background:#f1f4f8}}
body{{max-width:1140px;margin:auto;padding:32px 18px 64px}}h1{{margin:0;font-size:1.8rem}}.lead{{color:#526477}}
.notice{{background:#fff4dc;border-left:6px solid #bc833b;padding:16px;margin:20px 0;border-radius:8px}}
.stats{{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:12px}}
.stats div,section{{background:white;border:1px solid #dce4ed;border-radius:14px;padding:16px;margin-top:15px}}
.number{{font-size:2rem;font-weight:800;color:#346b97}}.sub{{font-size:.85rem;color:#667789}}
table{{width:100%;border-collapse:collapse}}th,td{{padding:9px;text-align:left;border-bottom:1px solid #e9ebed;vertical-align:top}}
th{{background:#e9eff5}}section{{overflow-x:auto}}details{{padding:12px;border-bottom:1px solid #dce4ed}}
summary{{cursor:pointer;font-weight:600}}code{{background:#f2f5f8;padding:2px 4px}}.warn{{color:#8d3639}}
footer{{font-size:.84rem;color:#53657b;margin-top:24px}}</style></head><body>
<h1>Day 22|VCF 資料前處理 — FILTER、QUAL、REF 與 BED QC</h1>
<p class="lead">從 Day21 同一批人工候選資料,觀察「第一個決定性原因」如何產生。</p>
<div class="notice"><strong>OFFLINE · SYNTHETIC EDUCATIONAL DEMO</strong><br>
參考序列是 200 個人工 A,基因組標記 TOY_NOT_GRCH38。沒有患者、真實模型、AlphaGenome API 或臨床判讀。</div>
<div class="stats"><div><span class="number">{report['candidate_count']}</span><br><span class="sub">ALT 候選</span></div>
<div><span class="number">{report['selected_count']}</span><br><span class="sub">SELECTED</span></div>
<div><span class="number">{report['review_required_count']}</span><br><span class="sub">QC 待審</span></div>
<div><span class="number">{report['rejected_count']}</span><br><span class="sub">REJECTED</span></div></div>
<section><h2>1|QC 主表</h2><p>判定順序:{q(' → '.join(report['qc_order']))}。</p>
<p>只記錄第一個決定性原因,不表示其他尚未執行的檢查已通過。</p>
<table><thead><tr><th>變異</th><th>狀態</th><th>原因</th><th>BED anchor</th><th>QUAL</th><th>已檢查步驟</th></tr></thead><tbody>{rows_html}</tbody></table></section>
<section><h2>2|打開 QC 決策路徑</h2>{details_html}</section>
<section><h2>3|另外建立、位於 BED 內的人工探針</h2>
<p>預設 POS 120 的低 QUAL、POS 140 的 REF mismatch 都先被 BED 排除。用 POS 105/106 才能真正檢查 LOW_QUAL 和 REF_MISMATCH。</p>
<table><thead><tr><th>探針</th><th>變異</th><th>狀態</th><th>原因</th><th>檢查路徑</th></tr></thead><tbody>{probes_html}</tbody></table></section>
<footer>資料來源:DAY21_BYTE_IDENTICAL_SYNTHETIC_FIXTURES · {q(report['assembly'])} · atlas_query_performed=false · real_api_calls_this_run=0。<br>
僅練習教學 QC;此處用 POS−1 作 BED anchor 判定,indel 仍須正常化及更嚴謹的跨區間處理。</footer></body></html>'''
def execute_offline(vcf_path, reference_path, regions_path, output_dir, min_qual=20.0):
paths = {"vcf": Path(vcf_path), "reference": Path(reference_path),
"regions": Path(regions_path)}
raw = {kind: path.read_text(encoding="utf-8") for kind, path in paths.items()}
report = qc_report(raw["vcf"], load_toy_reference(raw["reference"]),
parse_toy_bed(raw["regions"]), min_qual=min_qual)
report["input_provenance"] = {
kind: {"filename": path.name, "sha256": hashlib.sha256(path.read_bytes()).hexdigest()}
for kind, path in paths.items()
}
folder = Path(output_dir)
folder.mkdir(parents=True, exist_ok=True)
json_path = folder / "day22_qc_report.json"
html_path = folder / "day22_qc_dashboard.html"
json_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_count": report["review_required_count"],
"rejected_count": report["rejected_count"],
"low_qual_probe": next(p["reason"] for p in report["supplementary_probe_cases"]
if p["label"] == "LOW_QUAL_INSIDE_BED"),
"ref_mismatch_probe": next(p["reason"] for p in report["supplementary_probe_cases"]
if p["label"] == "REF_MISMATCH_INSIDE_BED"),
"json_file": str(json_path), "html_file": str(html_path),
"real_api_calls_this_run": 0}
def main(argv=None):
p = argparse.ArgumentParser(description="Day22 offline synthetic QC inspector (never LIVE)")
p.add_argument("--mode", choices=["offline"], required=True)
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("--output-dir", default="day22_output")
p.add_argument("--min-qual", type=float, default=20.0,
help="Artificial QC threshold for educational purposes only")
args = p.parse_args(argv)
summary = execute_offline(args.vcf, args.reference, args.regions,
args.output_dir, min_qual=args.min_qual)
print(json.dumps(summary, ensure_ascii=False, sort_keys=True))
return summary
if __name__ == "__main__":
main()
因為你先前的 WSL 沒有安裝 unzip,這裡直接使用 Python 標準函式庫內建的 zipfile,不必 apt install 任何套件。
以下程式碼貼在 Ubuntu Bash 終端機,勿貼到 Python 提示符號 >>>。
cd /mnt/c/Users/user/Downloads
mkdir -p ~/alphagenome-day22-qa
python3.12 -m zipfile -e \
AlphaGenome_Day22_VCF_QC_API_Bridge_Author_Review_Pack.zip \
~/alphagenome-day22-qa
cd ~/alphagenome-day22-qa
ls -l day22_offline_qc_explorer_lab.py \
day21_offline_vcf_review_lab.py \
test_day22_offline_qc_explorer_lab.py \
day21_synthetic.vcf day21_toy_reference.json day21_synthetic_regions.bed
python3.12 -m unittest -v test_day22_offline_qc_explorer_lab
python3.12 day22_offline_qc_explorer_lab.py \
--mode offline \
--vcf day21_synthetic.vcf \
--reference day21_toy_reference.json \
--regions day21_synthetic_regions.bed \
--output-dir day22_output
{"candidate_count": 8, "html_file": "day22_output/day22_qc_dashboard.html", "json_file": "day22_output/day22_qc_report.json", "low_qual_probe": "LOW_QUAL", "mode": "OFFLINE_ONLY", "real_api_calls_this_run": 0, "ref_mismatch_probe": "REF_MISMATCH", "rejected_count": 3, "review_required_count": 2, "selected_count": 3, "status": "PASS"}
該指令產生:
day22_output/day22_qc_report.json:八筆候選的來源與判定、每一關檢查歷程、十筆補充教學案例、輸入 SHA-256。day22_output/day22_qc_dashboard.html:可在瀏覽器打開的離線展示頁;點開每筆候選,可以看到已執行與尚未執行的 QC 關卡。\\wsl.localhost\Ubuntu-24.04\home\user\alphagenome-day22-qa\day22_outputLOW_QUAL、REF_MISMATCH 來自另外建立的補充案例;原本八筆候選的 3/2/3 統計沒有被偷偷改掉。
| 情況 | 不是什麼 | 今天真正學到的 |
|---|---|---|
FILTER=PASS |
不代表生物機制正常 | 只是本例能繼續檢查其他條件 |
OUTSIDE_TEACHING_BED |
不代表 REF、QUAL 都良好 | 表示此筆已在較早關卡停止 |
LOW_QUAL |
不等於「必然沒有功能」 | 是人工門檻下的 QC 排除原因 |
REF_MISMATCH |
不等於變異有致病性 | 需要核對 assembly、參考序列與資料來源 |
REVIEW_REQUIRED |
不等於高風險或已確認異常 | 代表資料尚有未解決的品質或格式議題 |
和真實 AlphaGenome API 的關係:今天不需要查詢 Atlas。未來進入真實 API 階段前,必須先有可追溯的 GRCh38/REF 核驗、正確變異表示方式與已獲准的查詢流程;不能將 TOY_NOT_GRCH38 的候選直接當成真實模型輸入,也不能用真實服務的回傳分數覆蓋這份人工 QC 證據。
[90,115) 為何包含 POS=115,卻不包含 POS=116。selected=3、review_required=2、rejected=3。
證據層級:SYNTHETIC EDUCATIONAL DEMO。 本篇程式不是完整 VCF reader、Genome Assembly 比對工具、變異正規化流程、族群變異註解或臨床致病性解讀。借用 Day21 的八欄、無樣本 VCF 小型子集;正式 VCF 還可包含 FORMAT、樣本基因型與其他複雜表示法。真正研究還需要可靠的參考 FASTA、assembly、複本與資料品質紀錄,以及正確的 variant normalization。
此次測試只驗證程式是否符合人工資料契約與已知邊界。本輪沒有呼叫 AlphaGenome Atlas/Model API,沒有患者資料,也沒有得到新的 AVI 或分子預測。
null、追蹤資料來源並產生結構化標註。主題是:「自動化批次標註 —— 從缺失值到結構化欄位設計」。