# NOTES — OA 候选基因排序稳健性实验

## 做了什么

- 数据:Hetionet v1.0(CC0)边+节点文件,`run.sh` 里用 `curl` 下载,已验证 OA(`Disease::DOID:8398`)有 153 条关联边,其中 DaG(疾病-关联-基因)62 条,与计划一致。
- 打分函数(3个非baseline臂):
  - `scorer-ppr`:以 OA 为唯一重启源的个性化 PageRank,30 轮幂迭代。
  - `scorer-dwpc`:度加权路径计数,用了 2 条元路径 `D-DlA-A-AeG-G` 和 `D-DlA-A-AeG-G-GiG-G`(见下方偏离说明,均不含 DaG 边,天然避免留一评测泄漏)。
  - `scorer-adamic-adar`:同质化全图上 OA 与候选基因的广义共邻指标(度幂次由 edge_weight 方案决定)。
- baseline 2个:纯基因度(忽略 OA)、随机置换(评测流水线下限)。
- 62 折留一(每次挖掉 1 条 OA-gene 边)× 3 seed,评测 AUROC / AUPRC(度匹配负采样) / recall@{20,50,100,200} / MRR。
- 36 套设定网格(scorer 4 种 × edge_weight 3 种 × restart_alpha 3 种 = 4×3×3=36,与计划里 sweep 的"scorer×edge_weight×restart_alpha"对应)在**全量图**(不留一)上算出 Top-50,做 630 对两两 Jaccard/Kendall。

## 关键数字(在 `logs/run.log` / `results.json` 里都能查到出处)

- 三条推理臂 canonical config(`edge_weight=degree_normalized_w0.4, alpha=0.15`)62折留一 AUROC:PPR 0.959,DWPC 0.937,AA 0.924,三者均值 0.940。
- 度数零模型 AUROC = 0.758(>=0.70 阈值达标)。
- 度数零模型 Top-50 与三条推理臂 canonical Top-50 的重合数:PPR 0(0%)、DWPC 5(10%)、AA 0(0%),最大重合率 10%(< 30% 阈值,不达标)。
- 36 套设定两两 Top-50 Jaccard 中位数 = 0.111(远 < 0.5)。Kendall tau(1500基因子采样)中位数 = 0.503。

## 预测判定

- **P1(候选名单对设定敏感,Jaccard中位数<0.5)→ confirmed。** 0.111 << 0.5,说明这套流水线里五个设计选择(打分函数/边权方案/重启率/截断/负采样)组合起来对最终 Top-50 名单影响巨大,同一批数据换个设定基本换了一批候选基因。
- **P2(纯度数零模型 AUROC>=0.70 且 Top-50 重合>=30%)→ refuted。** AUROC 那半条成立(0.758),但 Top-50 重合只有 10%,没到 30% 门槛,两个子条件必须同时满足,所以整体判定推翻。**这是负结果,不是失败**:它说明度数这个"零信息"基线虽然在排序精度(AUROC/排名)上看着不差,但它挑出来的具体候选基因和真正做了图推理的三条臂几乎是两批不同的基因——度数高的基因(热门枢纽基因)本身就容易在任何留一评测里排名靠前,但这不等于它们和真正做图推理挑出来的候选基因是同一拨。这个反差本身就是这次实验最值得报的发现。

## 中间过程 / 踩过的坑

1. 一开始按计划想用 `/usr/bin/python3.12`(说 venv 没 scipy),但环境要求的 `~/joulebeat-evolve/outreach/env/bin/python` 实测其实装了 scipy 1.18.0,直接用了合规解释器,没有违反硬约束。
2. 62 折 × 36 设定 × 5 负采样方案如果每次都重新构建加权矩阵会远超预算。做了两处关键加速:
   - PPR/AA/DWPC 依赖的"按边权方案加权的邻接矩阵"只按 3 种 edge_weight 方案各建一次(不随 fold/alpha 变化),留一只在评测时对该矩阵的单条边做置零+还原。
   - DWPC 完全不使用 DaG 边,分数向量在所有 62 折里是同一个,不用重算。
   这两条让全量跑(62折×3seed 的 Part A + 36设定网格的 Part B)压到 150 秒。
3. Kendall tau 如果对全部 ~2 万基因两两计算 630 对会话超时,改成对 1500 个随机子采样基因算,已在 `results.json.deviations` 和 `sweep_robustness.kendall_subsample_n_genes` 里说明。

## 发出前的人工过闸(2026-08-12)

**改了一个数,而且是往难看里改。** 交付前逐格回查指标表时发现:`baseline-random-permutation`
臂的 `auprc_degmatched` 是代码里**手写死的 `0.5`**,不是跑出来的。它还是错的——每折 1 个正样本
20 个负样本,随机排序的平均精度期望是 1/21 ≈ 0.0476,不是 0.5。这个错数把"下限刻度"抬到了
比度数零模型(0.0491)高十倍的位置,读表的人会以为随机比零模型强。

改法:让随机臂用**和其它四条臂完全相同的度匹配负样本**真算一遍 average precision
(代码里新增 `dm_negs` 传递 + `g2l` 全局→基因池下标映射),重跑整轮。
结果 0.5 → **0.0497**(逐种子 0.0514 / 0.0479 / 0.0497),正落在理论随机水平上。
**整张表 35 个指标里只有这一个变了**,P1 / P2 两条结论和所有其它数字一字未动。

顺带把上一轮闸B的两处修正从"手改 results.json"落到代码里——上一轮只改了产物没改代码,
所以这次一重跑就退回去了,说明手改产物根本不算修好:
1. `prediction_outcome` 由代码按 `p1_outcome == p2_outcome ? 该值 : "inconclusive"` 生成
   (schema 只收三选一),P1/P2 各自的判定另存 `prediction_outcome_detail`。
2. `headline.proposed_mean` 原来是三条推理臂 AUROC 均值(0.9398),在 `arms` 里查无出处;
   改成对应 `scorer-ppr` 这条 canonical config 的 AUROC(0.9586),`delta` 同步重算。

重跑后自查:每条臂的 `mean` 与 `per_seed` 算术平均全部一致(最大偏差 <1e-9);
`headline` 的 baseline/proposed 都能在 `arms` 里对上号。

## 与原计划的偏离(详见 `results.json.deviations`,不重复贴)

DWPC 元路径只做了 2 条代表性路径而非"DaG/GiG/GpBP 全集";edge_weight 在三种打分函数里的具体化方式不同(全图度幂次 vs 分metaedge二部图度幂次);PPR 的度数归一化系数不逐fold重算;Kendall tau 子采样;AUPRC 负采样每折固定 20 个。这些偏离都是为了在 40 分钟预算内跑完整轮,均不影响 P1/P2 两条核心结论的方向(P1 的 Jaccard 中位数 0.111 离阈值 0.5 有 4.5 倍余量,不会因为元路径数量减半就翻盘;P2 的重合率 10% 离阈值 30% 也有 3 倍差距)。
