主题
R13 X 染色体缺陷修复与三轮核查收尾
承接 第一轮定向排查 与 第二、三轮核查 遗留的六项待办,本次全部处理完毕。
过程中撞出两条比原待办更要紧的方法学问题,见下方第一节。
一、两条新发现的方法学问题(比原待办更要紧)
1. 反向 MR 从未真正运行过
config.yaml 中 bidirectional.enabled = false,且 results/ 下没有任何反向 MR 产物。
R/12_bidirectional.R 的实现是完整的(还专门修过 out_std_full 那个会给出错误结论的坑), 但批量筛查脚本 analysis/01_screen.R 根本不调用它——只有单对流水线 run_all.R 会调, 而本课题走的是批量筛查路径。
写作红线
本课题的因果方向证据只有 Steiger 定向过滤,没有反向 MR。 方法学部分不能声称完成了 STROBE-MR item 13c。
2. Steiger 把二分类结局当连续量算 R²
R/05_harmonise.R 的 to_tsmr_outcome() 设置了 ncase / ncontrol, 但从不设置 units.outcome = "log odds" 与 prevalence.outcome (to_tsmr_exposure() 同样不设 units.exposure = "SD")。
后果:TwoSampleMR 走 get_r_from_pn()(连续假设)而非 get_r_from_lor()(二分类), 低估结局侧 R² → Steiger 更容易判定「暴露→结局方向正确」→ 过滤偏松。
这正好解释了 01_screen.R 注释里那句「实测剔除 0 个」,无需假设过滤器本身坏了。
另有一处需留意:apply_steiger() 用 safely(steiger_filtering(h), on_error = h) 包裹, 报错会被静默吞掉并返回未过滤的 h——在产物上与「跑成功且剔除 0 个」完全不可区分。
实测用规范谐化对象调用是能跑通的(AGER × Maculopathy:steiger_dir = TRUE,无 NA), 但暴露 R² = 0.0126 对结局 R² = 0.0090,只差 1.4 倍—— 代码注释写的「cis-pQTL 暴露 R² 远大于结局 R²」是夸大。
二、R13 的 X 染色体编码缺陷
病灶
| 一侧 | X 的编码 | 是否正确 |
|---|---|---|
protein_info.csv 的 CHROM | 23 | — |
FinnGen 结局文件的 #chrom | 23 | ✅ 两侧一致,疾病侧本来就对 |
| UKB-PPP tar 内部成员文件名 | chrX_ | ❌ 拿 23 去 grep 'chr23_' 永远匹配不到 |
匹配不到 → 读取器返回 NULL → 该蛋白被静默排除在共定位之外, 且因 NULL 不落盘而不留任何痕迹。三份读取器(_region_io.R、08、10)全部中招。
一个容易搞混的细节
tar 内部只有文件名用 chrX_,文件内容的 CHROM 列用的仍是 23。 所以修复只需改「拼 tar 成员名」这一处,两侧的 chr:pos 键天然对齐—— 实测 CFP 两侧交集 2608 个 SNP。
实际影响面:只有 CFP 一个蛋白
更正先前的说法
先前记录称影响 CD40LG 与 CFP 两个蛋白,这是根据全部显著表推断的,过度计数了。
CD40LG 唯一的显著结局是 T1D_gcst,而 05_hyprcoloc.R 只纳入 COMPL 声明的 5 个并发症结局——它从来就不是共定位目标。实际影响面是 CFP 一个蛋白。
修复后的结果
| 项 | 结果 |
|---|---|
| CFP × WetAMD | 检出共享簇 |
| 簇后验概率 PP | 0.6306 |
| 候选 SNP | 23:47626818(恰是 CFP 的 cis lead) |
| 共同 SNP | 2595 |
| 判定 | 证据不足(蛋白进簇但 PP 未过 0.7 阈值)→ 不构成共定位支持 |
对既有结论的影响:
R13_amdverdict 由 42 → 43 个蛋白,原有 42 个逐字节完全相同- 下游
06_network/18_candidate_shortlist/14_figure_downstream重跑后零变化 - docs 与 PPT 中没有任何数字需要修改
CFP(备解素)是补体旁路通路成分,生物学上是 AMD 很有说服力的候选。 现在它有了一份诚实的、记录在案的评估(PP = 0.63,接近但未达阈), 而不再是悄无声息地缺席。
三、PheWAS(03_phewas.R)的两处修复
1. 分批落盘 + 断点续查
原先 219 行的脚本,第一处 fwrite 在第 193 行——网络循环中途任何一批把进程带崩, 前面所有批的结果全部丢失。
改为按批落盘,批件名带「该批 SNP 集合 + 查询阈值」的 md5。
关键设计:失败绝不落盘
只有查询成功才落盘(哪怕 0 条关联);三次重试全失败不落盘, 并在循环结束后统一 stop()。
否则失败会被记成「这批查过了」而在下次运行时被跳过——那正是静默失败。
单测 10 PASS / 0 FAIL,覆盖:失败批不落盘且报错点名批号、重跑只补失败批、 全部复用时零网络调用、0 行批的边界、换 SNP 集合后指纹失效。
2. 「缓存 = 交付物」的反馈回路导致结果无法逐字节复现
phewas_all.csv 既是缓存又是交付物,每次运行都「读进来、再写回去」。
p 值里有一批处在 double 下溢边界的次正规数(约 2.2e-308), 经不起一次十进制往返——于是每跑一次就漂一点:
2.15656324105385e-308 → 2.19081854978053e-308同参数连跑两次,产物 md5 都不相同。
修法:拆出 phewas_raw.csv 作为缓存,只在真正查询后写一次, 交付物每次从它重算。往返次数固定为 1,漂移消失。
验证:修复后连跑三次,三个产物的 md5 完全一致。
不影响任何判定——关联 15,789 / Bonferroni 显著 9,107 / 覆盖 92 rsid / 蛋白归属缺失 0 / 7 个关键蛋白脱靶数(APOE 642、NOTCH2 20、APOL1 2、ERMAP 6、GALNT3 2、IFNAR1 4、WARS 24) 全部 PASS。
四、其余待办
| 项 | 结果 |
|---|---|
hyprcoloc_R13 残留目录 | 实为空壳(0 个文件),已归档 |
tests/*.bak | 5 个不是 4 个(多一个 smoke_test_position.R.bak),已归档 |
| 未排查脚本 | 实际 7 个不是 8 个(19_* 根本不存在),扫描后无实质缺陷 |
未排查脚本用「已知缺陷模式定向扫描」而非通读——这是本轮唯一奏效的方法。 命中的都是 setwd() 硬编码这类换机便利性问题,不影响正确性。 20_screenable_shortlist.R 的 file.exists 是误报:它是 「优先读新产物 → 缺失则回退旧文件并明确告警 → 都无则报错」的正确模式。
所有归档采用移动而非删除,落在 mr-pipeline-r9/_archive_20260731/,可回滚。
五、两个协作/环境坑
F 盘副本比 WSL 落后
mr-pipeline-r9 在 WSL 里不是 git 仓库,与 F 盘是两份独立副本。 昨晚修的 03 / 08 / 10 只在 WSL,F 盘还是旧版——少了别名 rsID 补丁、 退役源排除、chrX 修复。
本次已全部回灌并逐字节 cmp 校验。每次改完必须回灌,否则换机就丢。
补丁守卫的陷阱(我自己栽了两次)
用「新文本的首行是否已存在」来判断补丁幂等——而新旧文本的首行往往相同, 于是误报「已是新版」、补丁静默没打上。
守卫必须挑新增内容独有的标记(如 chr_tag、PARTS <- paste0)。
六、尚未处理
- 反向 MR 要不要真跑(需先开
bidirectional.enabled,且只有run_all.R路径支持) - Steiger 的
units/prevalence要不要补(补了会让过滤更严,可能剔除 SNP → 结果会变) 24_coloc_abf.R不校验等位(已证无偏,代码卫生项)tools/rerun_downstream.sh是分家前的旧版(仍用R9/R13而非R13_dm/R13_amd), 整篇跑会出错,需要时只跑其中对应的步骤