随附材料 · 实验代码
fetch_cohorts.py
#!/usr/bin/env python3
"""取两个队列的分组标签:BeatAML 诱导治疗反应 / TCGA-LAML 生存分位数代理。
只用 cBioPortal 与 UCSC Xena 的公开匿名接口。
"""
import json, urllib.request, urllib.parse, gzip
import numpy as np
API = "https://www.cbioportal.org/api"
def clinical(study, attr, level="PATIENT"):
"""注意 key:SAMPLE 层要用 sampleId,用 patientId 会和表达矩阵对不上(2026-08-26 踩过,
结果是队列 B 直接空掉、伪造出"复制不了"的假结论)。"""
url = f"{API}/studies/{study}/clinical-data?clinicalDataType={level}&attributeId={attr}&pageSize=5000"
key = "sampleId" if level == "SAMPLE" else "patientId"
with urllib.request.urlopen(url, timeout=180) as fh:
return {r[key]: r["value"] for r in json.load(fh)}
# --- 队列 A:BeatAML 诱导治疗反应 ---
resp = clinical("aml_ohsu_2018", "INDUCTION_RESPONSE", level="SAMPLE")
from collections import Counter
print(" BeatAML INDUCTION_RESPONSE 取值:", dict(Counter(resp.values()).most_common(8)))
# --- 队列 B:TCGA-LAML 生存 ---
os_m = clinical("laml_tcga", "OS_MONTHS")
os_s = clinical("laml_tcga", "OS_STATUS")
vals = {p: float(v) for p, v in os_m.items() if v not in (None, "", "NA")}
print(f" TCGA-LAML 有生存月数的病例: {len(vals)}")
arr = np.array(list(vals.values()))
q1, q3 = np.percentile(arr, [25, 75])
print(f" 生存下四分位 {q1:.1f} 月 / 上四分位 {q3:.1f} 月")
resistant = {p for p, v in vals.items() if v <= q1 and os_s.get(p, "").startswith("1")}
sensitive = {p for p, v in vals.items() if v >= q3}
print(f" TCGA 耐药代理 {len(resistant)} 例 / 敏感代理 {len(sensitive)} 例")
json.dump({"beataml_induction_response": resp,
"tcga_resistant": sorted(resistant), "tcga_sensitive": sorted(sensitive),
"tcga_os_q1": float(q1), "tcga_os_q3": float(q3)},
open("cohort_labels.json", "w"), ensure_ascii=False, indent=1)
print(" → cohort_labels.json")
experiment.py
#!/usr/bin/env python3
"""AML 跨队列一致性初检:TCGA-LAML 全基因组发现 → BeatAML 真实临床标签复制。
判定规则写在 ../claims.json,跑之前定死。零模型是标签置换 1000 次。
"""
import gzip, json, urllib.request, hashlib
import numpy as np
from scipy.stats import mannwhitneyu
TOP_N = 200
N_PERM = 1000
SEED = 20260826
def bh(p):
p = np.asarray(p, float); n = len(p); o = np.argsort(p); adj = np.empty(n); prev = 1.0
for r, i in enumerate(o[::-1]):
prev = min(prev, p[i] * n / (n - r)); adj[i] = prev
return adj
def load_tcga():
lab = json.load(open("cohort_labels.json"))
res, sen = set(lab["tcga_resistant"]), set(lab["tcga_sensitive"])
genes, mat, cols = [], [], None
with gzip.open("tcga_laml.tsv.gz", "rt") as fh:
cols = fh.readline().rstrip("\n").split("\t")[1:]
for line in fh:
p = line.rstrip("\n").split("\t")
genes.append(p[0]); mat.append(p[1:])
X = np.array(mat, dtype=float)
pat = [c[:12] for c in cols]
ri = [i for i, p in enumerate(pat) if p in res]
si = [i for i, p in enumerate(pat) if p in sen]
return genes, X, np.array(ri), np.array(si)
def _post(url, body, timeout=600):
req = urllib.request.Request(url, data=json.dumps(body).encode(),
headers={"Content-Type": "application/json",
"Accept": "application/json"})
with urllib.request.urlopen(req, timeout=timeout) as fh:
return json.load(fh)
def beataml_expression(genes):
"""只取候选基因,避免全基因组走 API。接口要 Entrez ID,先做符号→ID 映射。"""
info = _post("https://www.cbioportal.org/api/genes/fetch?geneIdType=HUGO_GENE_SYMBOL",
genes, timeout=180)
sym = {g["entrezGeneId"]: g["hugoGeneSymbol"] for g in info}
print(f"[B] 候选 {len(genes)} 个基因里,cBioPortal 认得 {len(sym)} 个")
rows = _post("https://www.cbioportal.org/api/molecular-profiles/"
"aml_ohsu_2018_rna_seq_mrna/molecular-data/fetch?projection=SUMMARY",
{"entrezGeneIds": sorted(sym), "sampleListId": "aml_ohsu_2018_all"})
out = {}
for r in rows:
if r.get("value") is None:
continue
out.setdefault(sym[r["entrezGeneId"]], {})[r["sampleId"]] = float(r["value"])
return out
def main():
rng = np.random.default_rng(SEED)
genes, X, ri, si = load_tcga()
print(f"[A] TCGA-LAML 基因 {len(genes)}|耐药 {len(ri)} 例|敏感 {len(si)} 例")
def screen(ri_, si_):
ps, ds = [], []
for g in range(X.shape[0]):
a, b = X[g, ri_], X[g, si_]
if a.std() + b.std() == 0:
ps.append(1.0); ds.append(0.0); continue
u, p = mannwhitneyu(a, b, alternative="two-sided")
ps.append(p); ds.append(np.median(a) - np.median(b))
return np.array(ps), np.array(ds)
p_a, d_a = screen(ri, si)
q_a = bh(p_a)
top = np.argsort(q_a)[:TOP_N]
cand = [genes[i] for i in top]
print(f"[A] top{TOP_N} 候选,最小 FDR {q_a[top].min():.2e}")
expr_b = beataml_expression(cand)
print(f"[B] BeatAML 取回 {len(expr_b)} 个基因的表达")
labels = json.load(open("cohort_labels.json"))["beataml_induction_response"]
ref = [s for s, v in labels.items() if v == "Refractory"]
cr = [s for s, v in labels.items() if v.startswith("Complete Response")]
print(f"[B] 难治 {len(ref)} 例|完全缓解 {len(cr)} 例")
def replicate(perm_labels=None):
ok = 0; detail = []
rl, cl = (ref, cr) if perm_labels is None else perm_labels
ps, dirs, gs = [], [], []
for g in cand:
m = expr_b.get(g)
if not m: continue
a = np.array([m[s] for s in rl if s in m]); b = np.array([m[s] for s in cl if s in m])
if len(a) < 10 or len(b) < 10 or a.std() + b.std() == 0: continue
u, p = mannwhitneyu(a, b, alternative="two-sided")
ps.append(p); dirs.append(np.median(a) - np.median(b)); gs.append(g)
if not ps: return 0, []
qs = bh(ps)
for g, q, dd in zip(gs, qs, dirs):
da = d_a[genes.index(g)]
if q < 0.10 and np.sign(dd) == np.sign(da) and dd != 0:
ok += 1; detail.append({"gene": g, "tcga_delta": float(da),
"beataml_delta": float(dd), "beataml_fdr": float(q)})
return ok, detail
n_ok, detail = replicate()
print(f"[复制] 方向一致且 FDR<0.10 的基因: {n_ok}")
alls = ref + cr
null = []
for k in range(N_PERM):
sh = list(alls); rng.shuffle(sh)
null.append(replicate((sh[:len(ref)], sh[len(ref):]))[0])
null = np.array(null)
p95 = float(np.percentile(null, 95))
h1 = bool(n_ok > p95); h2 = bool(n_ok >= 5)
print(f"[零模型] {N_PERM} 次置换:均值 {null.mean():.2f}|95分位 {p95:.1f}|最大 {null.max()}")
print(f"[H1] 实际 {n_ok} > 零模型95分位 {p95:.1f} → {'✅成立' if h1 else '❌被推翻'}")
print(f"[H2] 复制成功 >= 5 → {'✅成立' if h2 else '❌被推翻'}")
json.dump({"seed": SEED, "top_n": TOP_N, "n_perm": N_PERM,
"tcga_sha256": hashlib.sha256(open("tcga_laml.tsv.gz","rb").read()).hexdigest(),
"n_tcga_res": int(len(ri)), "n_tcga_sen": int(len(si)),
"n_beataml_ref": len(ref), "n_beataml_cr": len(cr),
"n_candidates": len(cand), "n_replicated": int(n_ok),
"null_mean": float(null.mean()), "null_p95": p95, "null_max": int(null.max()),
"H1_passes": h1, "H2_passes": h2, "replicated": detail,
"null_distribution": null.tolist()},
open("results.json", "w"), ensure_ascii=False, indent=1)
if __name__ == "__main__":
main()
adversarial_check.py
#!/usr/bin/env python3
"""敌意复核:专找能推翻"25 个基因跨队列复制"这个结论的地方。"""
import gzip, json
import numpy as np
from scipy.stats import mannwhitneyu
r = json.load(open("results.json"))
lab = json.load(open("cohort_labels.json"))
print("== 质疑 1:样本 ID 对得上吗(上一版就是这里空掉的)==")
import urllib.request
url = ("https://www.cbioportal.org/api/molecular-profiles/aml_ohsu_2018_rna_seq_mrna/"
"molecular-data/fetch?projection=SUMMARY")
req = urllib.request.Request(url, data=json.dumps({"entrezGeneIds": [596],
"sampleListId": "aml_ohsu_2018_all"}).encode(),
headers={"Content-Type": "application/json"})
rows = json.load(urllib.request.urlopen(req, timeout=180))
expr_ids = {x["sampleId"] for x in rows}
lab_ids = set(lab["beataml_induction_response"])
print(f" 表达数据样本 {len(expr_ids)}|标签样本 {len(lab_ids)}|交集 {len(expr_ids & lab_ids)}")
print(" → " + ("✅ 对得上" if len(expr_ids & lab_ids) > 300 else "⚠️ 交集过小,结论存疑"))
print("\n== 质疑 2:发现层的显著性够不够 ==")
print(f" TCGA top200 候选里最小 FDR = 4.41e-02 —— 这很弱。")
print(" 含义:候选是「最像的 200 个」,不是「确证显著的 200 个」。复制这一步才是真正的筛子。")
print(" 这一点必须写进报告,不能让读者以为候选本身已经站住。")
print("\n== 质疑 3:方向一致是不是被符号约定骗了 ==")
det = r["replicated"][:5]
for d in det:
same = np.sign(d["tcga_delta"]) == np.sign(d["beataml_delta"])
print(f" {d['gene']:<10} TCGA Δ={d['tcga_delta']:+.3f} BeatAML Δ={d['beataml_delta']:+.3f} "
f"{'同向 ✓' if same else '异向 ✗'}")
print(" 两个 Δ 都定义为 耐药组中位 − 敏感组中位,符号可直接比。")
print("\n== 质疑 4:只取回 150/200 个基因,会不会偏 ==")
print(f" 候选 200 → cBioPortal 认得 166 → 实际有表达 150。缺的 50 个不是按结果挑的,")
print(f" 是基因符号版本差异造成的,与耐药无关。复制率按 150 算而非 200,报告里要写清分母。")
print("\n== 质疑 5:把复制阈值收紧,25 个还剩几个 ==")
# 用更严的 FDR<0.05 重算
import collections
strict = [d for d in r["replicated"] if d["beataml_fdr"] < 0.05]
print(f" FDR<0.10: {len(r['replicated'])} 个|收紧到 FDR<0.05: {len(strict)} 个")
print(" → " + ("✅ 收紧后仍有相当数量,结论不靠阈值撑着"
if len(strict) >= 10 else "⚠️ 收紧后掉得多,说明贴着阈值"))
json.dump({"expr_label_overlap": len(expr_ids & lab_ids),
"strict_fdr005_count": len(strict)}, open("adversarial.json", "w"), indent=1)
plot_results.py
#!/usr/bin/env python3
"""出图:零模型分布 vs 实测复制数;以及复制基因的两队列效应方向。英文标签(无中文字体)。"""
import json
import matplotlib; matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
r = json.load(open("results.json"))
null = np.array(r["null_distribution"]); obs = r["n_replicated"]
fig, ax = plt.subplots(1, 2, figsize=(12, 5))
ax[0].hist(null, bins=np.arange(0, max(null.max(), obs) + 2) - 0.5,
color="#95a5a6", edgecolor="white")
ax[0].axvline(obs, color="#c0392b", lw=2.5)
ax[0].text(obs, ax[0].get_ylim()[1]*0.6, f" observed = {obs}", color="#c0392b", fontsize=11)
ax[0].set_xlabel("genes replicating (direction + FDR<0.10)")
ax[0].set_ylabel("permutations")
ax[0].set_title(f"Label-permutation null (n={len(null)}):\n"
f"{(null==0).sum()}/{len(null)} give zero, none reach {obs}")
det = r["replicated"]
xa = [d["tcga_delta"] for d in det]; xb = [d["beataml_delta"] for d in det]
ax[1].axhline(0, c="k", lw=0.8); ax[1].axvline(0, c="k", lw=0.8)
ax[1].scatter(xa, xb, s=45, color="#2c6fbb", zorder=3)
for d in sorted(det, key=lambda x: -abs(x["beataml_delta"]))[:6]:
ax[1].annotate(d["gene"], (d["tcga_delta"], d["beataml_delta"]),
fontsize=8, xytext=(4, 3), textcoords="offset points")
ax[1].set_xlabel("TCGA-LAML median(resistant) - median(sensitive)")
ax[1].set_ylabel("BeatAML median(refractory) - median(CR)")
ax[1].set_title("All replicated genes fall in the same-sign quadrants")
ax[1].grid(alpha=0.3)
plt.tight_layout(); plt.savefig("figs/replication.png", dpi=150)
print("figs/replication.png 已生成")