DAY 2 · S7 · 下午 90 分钟
质量与证伪:亲手复现那些坑
这一节是全课的核心。四个实验,每个都让你亲手把 bug 跑出来。
前面六节都是为这一节做准备。判断力不来自知识,来自亲手见过错误发生。 下面四个实验,每一个都会在你自己的终端里把一个真实的 bug 跑出来。
LAB 3 · 亲手复现子串匹配 bug
$ python labs/lab3_substring_bug.py
你会看到三类 bug 现场发生:
- 没加词边界:HER2 的别名
neu在抗原名字段里 裸子串会命中NEURAMINIDASE、...NEUTRALIZING ANTIBODY—— 全是假阳性 (在真实规模的数据上,这一条别名就能造出上百条假配对)。 加上两侧边界断言(?<![A-Za-z0-9])neu(?![A-Za-z0-9])之后归零。这里有个自带的二级陷阱,脚本会让你踩一次: 只加左侧的(?<![A-Za-z0-9])是不够的。NEURAMINIDASE里的 NEU 正好在词首,左侧断言对它完全无效。
必须两侧都加,或者直接用\bneu\b。
这门课第一版就写错了这一条——「修好了」和「验证过修好了」是两件事。 - 没做最长匹配消歧:PD-1 的基因全名
programmed cell death 1是 PD-L1 全名programmed cell death 1 ligand 1的前缀, 所以真正的 PD-L1 抗原会被同时误判成 PD-1。 加词边界也救不了——必须做包含关系消歧:同一段文本里,长匹配压制短匹配。(注意这里用的是 HGNC 的基因全名。UniProt 的蛋白名是Programmed cell death protein 1,多了个 protein,就不构成前缀了—— 「是不是前缀」这件事本身就取决于你用的是哪个源的哪个字段。 这正是本节要教的东西。) - 语义陷阱(正则无解):
Anti-App Tag Fab是结合实验室标签肽的试剂抗体,不是抗 APP 蛋白的。 而 anti-HA tag 里的 HA 与流感的 HA 蛋白同名。 这一类靠正则解决不了,只能靠语境规则或人工规则表。
前两类是实现 bug,能修; 第三类是方法边界,只能承认并披露。 一个成熟的数据供应商会明确告诉你哪些属于第三类、占比多少; 不成熟的会假装它不存在。
LAB 4 · 亲手做一次交叉源验证 ★ 本课最重要的实验
$ python labs/lab4_cross_source.py
脚本把同一个问题问三次:一次问 HGNC(基因命名权威), 两次问 UniProt—— 用两种都很「合理」的查法。真实输出:
输入 HGNC UniProt-A 朴素 UniProt-B 严谨 CCR4 CCR4 CCR4 P51679 [Homo sapiens] NOCT Q9UK39 ← 别名劫持 HER2 ERBB2 GTF1 P53260 [Saccharomyces] ERBB2 P04626 ← 跨物种泄漏 PD-1 PDCD1 PD-1 Q9UMF3 [Homo sapiens] (查不到) OX40 TNFRSF4 - O02764 [Oryctolagus] (查不到) ← 兔子同源
两种查法都不对,而且它们以不同方式错。
「朴素」查法(全文检索、不限物种、取第一条)在 CCR4 这行反而是对的,
却把 HER2 解析成了酿酒酵母的一个基因、把 OX40 解析成兔子的同源基因——
而这些结果字段齐全、格式正确,从数据表面完全看不出来。
「严谨」查法(精确基因名 + 限人源 + 只要人工审校条目)挡住了物种泄漏,
却在 CCR4 上被别名劫持,解析到了 NOCT 上。
所以正确做法不是「换一个更好的查法」,是交叉验证。
这就是这个实验的全部结论。
用产出结果的同一个源去检查结果 = 循环论证。
必须拿另一个独立权威源来对,两边一致才算过。
这和你写测试时「不能用被测代码自己的逻辑去断言」是完全一样的道理。
更进一步:注意「查不到」那几行是不一致但不算错——
严谨查法查不到 PD-1、OX40,只是因为它们是别名不是基因符号。
把「冲突」和「查不到」分开处理,是这个实验第二重要的收获。
全部当成错误会淹没真正的冲突。
LAB 5 · 亲手从三维结构算出表位
$ python labs/lab5_compute_epitope.py
本质上是一次空间近邻查询:把抗体所有原子和抗原所有原子丢进 KD 树, 找出距离小于阈值的原子对,两侧涉及的残基分别就是互补位和表位。
原子级接触 231 对(重原子间距 < 4.5 Å) 表位(抗原一侧被抓住的残基): S202 (SEP) 接触 21 次 ← 磷酸化修饰! P203 (PRO) 接触 47 次 T205 (TPO) 接触 63 次 ← 磷酸化修饰! ... 共 8 个表位残基、18 个互补位残基
算出来的 S202;P203;G204;T205;P206;G207;S208;R209
与数据表里那一列逐字符一致——因为表里那列就是这么算出来的,不是抄的。
① 阈值和原子集合都是约定,不是真理。
「4.5 Å」这个数字必须连同算的是哪些原子一起说
(这里是重原子间距,即忽略氢原子)。
4.0 / 4.5 / 5.0 Å 三种取值都有人用,算出的表位残基数会随之变化。
所以阈值和原子集合必须随数据一起交付——不写清楚的表位列没法和别人的比较。
② 残基编号也要说明基准。下面那串 S202…R209
是按 tau 的 2N4R 亚型(441 aa)编号的。
换一个亚型,同一个残基的编号就变了——
这和 S6 第 ⑤ 个坑(跨毒株编号失效)是同一个问题。
这门课自己教了坐标基准这一课,第一版却没给自己的例子标基准。
③ 残基名会告诉你额外信息。SEP/TPO
是磷酸化修饰过的丝氨酸/苏氨酸,说明这个抗体识别的是磷酸化状态的目标蛋白,
而不是它本身。这个信息只在三维结构里有,序列里没有。
LAB 6 · 用写单元测试的方式做质检
$ python labs/lab6_assert_qc.py
质检不是「出一份统计报告」,是写一组必须成立的断言,挂了就打印具体哪几行。 跑完你会看到这几类断言:
| 断言 | 你熟悉的版本 |
|---|---|
| 主键唯一、外键无孤儿、值域合法 | 数据库约束 |
| 每个 CDR 都是所在链的子串 | 内部一致性检查 (最容易被忽略的一类) |
| 含模糊码的记录均已在 QC_flags 披露 | 注意断言的语义: 不是「不许有缺陷」,是「不许有未披露的缺陷」 |
| 抽样溯源链接可达 | 冒烟测试 ← 这里有个陷阱 |
403(反爬)、429(限流)——
这不等于链接坏了。如果断言写成「必须全部 200」,
就会把大量好链接误判成坏的,然后你会去「修」一个根本不存在的问题。教训:验证方法本身也需要被验证。 这和「测试挂了,先看是代码错了还是测试写错了」是同一件事—— 只不过在数据工程里,没有人会自动提醒你去看第二种可能。
它把「数据质量」从一个形容词变成了可执行、可回归、可进 CI 的东西。
数据变了、管线改了,重跑一遍立刻知道有没有破。
你已经知道怎么做这件事——这就是为什么程序员做数据治理有天然优势。
拿到任何一份数据,按这个顺序找问题:
① 字段匹配是不是用了裸子串/会冲突的别名?
② 归一化结果有没有用独立源复核?
③ 算出来的列(表位、CDR)有没有说明算法和阈值?
④ 缺陷清单在哪?全 OK 就是最大的红旗。
⑤ 验证方法本身有没有被验证过?
当堂自测
六道题。这一节的题最值得认真做。