第3章 混淆矩阵与TPR、FPR
聊完富集因子的计算,咱们得先停一下。你想想看,不管做什么分类或富集分析,最后怎么评价结果好不好?光看预测对了多少个,远远不够。我早年做过一个基因功能注释的项目,模型准确率95%,结果却一塌糊涂——后来才发现,负样本占了绝大多数,准确率根本是骗人的。
所以这一章,咱们把评价指标的老底翻一翻。混淆矩阵 是所有分类问题的基石。而 TPR(真阳性率)和 FPR(假阳性率) 更是 ROC 曲线的原料。搞不懂这俩,后面的 AUC 就是空中楼阁。
3.1 混淆矩阵:四个格子,说清全部
说白了,混淆矩阵就是一张 2×2 的表格,把真实标签和预测结果摆在一起对比。其实你平时做实验,对比真值和测量值,也是这个思路。
拿一个简单的疾病诊断来说:
- Positive (P):真实患病
- Negative (N):真实健康
- Positive (P'):模型预测患病
- Negative (N'):模型预测健康
交叉一对,就出来四个数:
| 真实 \ 预测 | 预测 P' | 预测 N' |
|---|---|---|
| 真实 P | TP (真阳性) | FN (假阴性) |
| 真实 N | FP (假阳性) | TN (真阴性) |
实际项目里,我经常收到这样的反馈:“我的模型查出来50个阳性,全是准的。” 你一问 FN 是多少,对方就愣住了。光看 TP 不看 FN,就像只报喜不报忧。
3.2 TPR 和 FPR:灵敏度与假阳性率的真面目
有了四个格子的数,就可以算出两个核心比率:
- TPR (Recall / 灵敏度 / 真阳性率) = TP / (TP + FN)
表示“真实的阳性样本里,有多少被我们揪出来了”。 - FPR (假阳性率) = FP / (FP + TN)
表示“真实的阴性样本里,有多少被我们冤枉成了阳性”。
你想想看,TPR 越大越好,FPR 越小越好。但现实是——你调高模型的灵敏度,往往就会多抓一些无辜的阴性进来,FPR 跟着涨。这就是 TPR 和 FPR 之间的博弈。
咱们用一张流程图来梳理清楚:
3.3 为什么这两个指标是 ROC 曲线的原料?
做过富集分析的朋友都知道,我们通常用 p-value 或 FDR 来卡阈值。阈值设得严,只有非常显著的基因集才被选进来——TPR 可能很低(漏了很多真的富集集),但 FPR 也低(假阳性少)。阈值放宽,TPR 上去了,FPR 也涨了。
ROC 曲线就是:遍历所有可能的阈值,算出每个阈值下的 (FPR, TPR) 点,连成一条曲线。横轴是 FPR,纵轴是 TPR。曲线越靠近左上角,说明模型越强。
sklearn.metrics.roc_curve 之前,我会先手动算一遍混淆矩阵。比如用 10 个样本模拟,一步步写出 TP、FN、FP、TN,再算 TPR、FPR。这样不管函数返回什么,我心里都有底。举个代码片段的例子,千万别直接拿去跑大数据——先在小样本上理解:
import numpy as np
# 假设真实标签和预测概率
y_true = np.array([1, 1, 0, 0, 1, 0, 1, 0, 1, 0])
y_scores = np.array([0.9, 0.8, 0.6, 0.4, 0.7, 0.3, 0.85, 0.2, 0.95, 0.1])
# 选一个阈值
threshold = 0.5
y_pred = (y_scores >= threshold).astype(int)
# 手动计算混淆矩阵
TP = np.sum((y_true == 1) & (y_pred == 1))
FN = np.sum((y_true == 1) & (y_pred == 0))
FP = np.sum((y_true == 0) & (y_pred == 1))
TN = np.sum((y_true == 0) & (y_pred == 0))
TPR = TP / (TP + FN)
FPR = FP / (FP + TN)
print(f"TPR = {TPR:.3f}, FPR = {FPR:.3f}")
# 改变阈值 0.3 再算一遍
y_pred2 = (y_scores >= 0.3).astype(int)
TP2 = np.sum((y_true == 1) & (y_pred2 == 1))
FN2 = np.sum((y_true == 1) & (y_pred2 == 0))
FP2 = np.sum((y_true == 0) & (y_pred2 == 1))
TN2 = np.sum((y_true == 0) & (y_pred2 == 0))
TPR2 = TP2 / (TP2 + FN2)
FPR2 = FP2 / (FP2 + TN2)
print(f"Threshold 0.3 -> TPR = {TPR2:.3f}, FPR = {FPR2:.3f}")
你看,阈值一降,TPR 从 0.6 可能升到 0.8,但 FPR 也跟着从 0.25 升到 0.5。这就是 ROC 曲线上的两个点。
3.4 在富集分析中到底怎么用?
做过 GSEA 的朋友应该不陌生:GSEA 用排序后的基因列表,滑窗统计每个基因集的富集分数(ES),然后算归一化的 NES 和 p-value。这里其实就隐藏了一个混淆矩阵的逻辑:
- 真实“阳性”基因集 = 在数据库里已知与表型相关的通路;
- 预测“阳性”基因集 = 被我们选出来的高富集通路;
- TP = 选出来且确实是已知通路;FP = 选出来但假阳性;
- FN = 漏掉的已知通路;TN = 没选且确实不相关。
当然实际富集分析没有绝对的金标准,但我们做模拟实验或基准测试时,就可以套用这套评价体系。我曾经参与过一个通路数据库的评估,用了 50 个已知机制模拟数据集,分别跑三种工具,最后画 ROC 曲线来对比不同工具的召回能力。那次让我深刻体会到:没有混淆矩阵思维,对比结果就像没有标尺的尺子。
3.5 本章小结
四个格子——TP、FN、FP、TN。两个比率——TPR 与 FPR。一个平衡——灵敏度与特异度(特异度 = 1 - FPR)。再后面就是怎么用这些点画出 ROC 曲线。