随附材料 · 实验代码

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 已生成")
← 回到案例正文