2×2 表、敏感度與特異度
四個格子與四個邊際各自回答什麼問題、為什麼敏感度與特異度是檢驗的性質而不是族群的性質、SpPIN / SnNOUT 這組助記在什麼時候會騙人,以及診斷研究最常見的偏誤——只有檢驗陽性的人去做 gold standard 時,敏感度與特異度會同時往相反方向跑。
這一頁在解決什麼問題
臨床上大部分的檢驗最後都會被壓成一個二元判斷:這個結果算陽性還是陰性。抽血值過了某個門檻、影像被判讀成有或沒有、快篩出現第二條線。
問題是,「這個檢驗準不準」這句話沒有單一答案。它至少要拆成四個不同的問題,而這四個問題的答案在同一份資料上可以差非常多:
- 有病的人裡,檢驗抓到幾成?
- 沒病的人裡,檢驗放行幾成?
- 檢驗說陽性的人裡,真的有病的佔幾成?
- 檢驗說陰性的人裡,真的沒病的佔幾成?
前兩個問題的答案是檢驗的性質,後兩個不是。 這一頁處理前兩個(敏感度與特異度),後兩個在預測值與盛行率。把這件事分清楚,是診斷準確度整個家族的地基。
四個格子與四個邊際
把「檢驗結果」與「參考標準(reference standard,也就是 gold standard)說的真相」交叉,就是 2×2 表:
| 真的有病 | 真的沒病 | 合計 | |
|---|---|---|---|
| 檢驗陽性 | 真陽性 TP | 偽陽性 FP | 所有陽性者 |
| 檢驗陰性 | 偽陰性 FN | 真陰性 TN | 所有陰性者 |
| 合計 | 所有病人 | 所有非病人 | 總人數 |
四個格子加起來是總人數。真正要記的是除的時候是往哪個方向除:
敏感度與特異度是沿著直行除(分母是疾病狀態的人數),預測值是沿著橫列除(分母是檢驗結果的人數)。
這一頁的例子
pROC::aSAH 是 113 位蜘蛛膜下腔出血(subarachnoid haemorrhage, SAH)病人的資料,
記錄了兩個生物標記與六個月後的神經學結果。把結果二分成好(Good)與差(Poor):
41 人結果差、72 人結果好,也就是這個世代的盛行率是 36.3%。
這一頁用 S100β(一種星狀膠細胞的蛋白質,腦損傷時會上升)當作檢驗,切點訂在 0.20 µg/L。
figures/scripts/B4-01-sens-spec.R| 結果差(有病) | 結果好(沒病) | 合計 | |
|---|---|---|---|
| S100β ≥ 0.20 | TP = 26 | FP = 14 | 40 |
| S100β < 0.20 | FN = 15 | TN = 58 | 73 |
| 合計 | 41 | 72 | 113 |
四個指標與它們的 95% 信賴區間(比例的區間用 Wilson score interval,它在接近 0 或 1 時不會跑出 [0, 1] 之外):
| 指標 | 算式 | 估計值 | 95% CI |
|---|---|---|---|
| 敏感度 Sensitivity | 26 / 41 | 63.4% | 48.1–76.4% |
| 特異度 Specificity | 58 / 72 | 80.6% | 70.0–88.0% |
| 陽性預測值 PPV | 26 / 40 | 65.0% | 49.5–77.9% |
| 陰性預測值 NPV | 58 / 73 | 79.5% | 68.8–87.1% |
信賴區間值得多看一眼:敏感度的區間是 48.1–76.4%,寬達 28.3 個百分點。診斷準確度研究的樣本數常常不足,因為它的有效樣本數是「有病的人數」與「沒病的人數」分開算的,而不是總人數;這裡的敏感度只由 41 個人決定。看到一篇診斷研究報 sensitivity 而不報區間,先假設它很寬。
動手跑一次
library(pROC)
data(aSAH, package = "pROC")
cut <- 0.20 # 先訂好的切點,不是挑出來的
pos <- aSAH$s100b >= cut
dis <- aSAH$outcome == "Poor"
tab <- table(factor(pos, c(TRUE, FALSE)), factor(dis, c(TRUE, FALSE)))
tab # 左上角就是 TP
tp <- tab[1, 1]; fp <- tab[1, 2]; fn <- tab[2, 1]; tn <- tab[2, 2]
sens <- tp / (tp + fn)
spec <- tn / (tn + fp)
c(sensitivity = sens, specificity = spec,
ppv = tp / (tp + fp), npv = tn / (tn + fn))
# 比例的信賴區間:Wilson,不要用 sens ± 1.96*SE(在接近 0/1 時會出界)
prop.test(tp, tp + fn, correct = FALSE)$conf.int
binom.test(tp, tp + fn)$conf.int # Clopper-Pearson,保守但永遠涵蓋
# 匯出給 Python 用
write.csv(aSAH, "aSAH.csv", row.names = FALSE)驗證環境:R 4.6.0 + pROC 1.19.0.1。aSAH 隨 pROC 一起安裝,不需要另外下載。
import pandas as pd
from sklearn.metrics import confusion_matrix
from statsmodels.stats.proportion import proportion_confint
d = pd.read_csv("aSAH.csv") # 由上面那行 R 產生
pos = d["s100b"] >= 0.20
dis = d["outcome"] == "Poor"
# sklearn 的順序是 [[TN, FP], [FN, TP]],跟臨床課本的擺法相反
tn, fp, fn, tp = confusion_matrix(dis, pos).ravel()
sens = tp / (tp + fn)
spec = tn / (tn + fp)
print(sens, spec, tp / (tp + fp), tn / (tn + fn))
print(proportion_confint(tp, tp + fn, method="wilson"))
print(proportion_confint(tn, tn + fp, method="wilson"))aSAH 沒有掛在 Rdatasets 上,所以 Python 這一欄從 R 匯出的 CSV 開始。sklearn 的 confusion_matrix 預設順序是 [[TN, FP], [FN, TP]],與臨床習慣的擺法上下顛倒。
敏感度與特異度不隨盛行率變——這句話的意思是什麼
課本說敏感度與特異度是「檢驗的性質」。要看清楚這句話,最乾脆的辦法是改變病人組成再算一次。
下面三個世代都是從同一批 113 位病人來的,做法是把某一類的人整批複製(不是隨機抽樣——複製才能讓數字完全乾淨,不受抽樣變異干擾):
| 病例 : 對照的複製比 | 總人數 | 盛行率 | 敏感度 | 特異度 | PPV | NPV |
|---|---|---|---|---|---|---|
| 1:4 | 329 | 12.5% | 63.4% | 80.6% | 31.7% | 93.9% |
| 1:1 | 113 | 36.3% | 63.4% | 80.6% | 65.0% | 79.5% |
| 4:1 | 236 | 69.5% | 63.4% | 80.6% | 88.1% | 49.2% |
盛行率從 12.5% 走到 69.5%,敏感度與特異度一格都沒動,而 PPV 從 31.7% 跳到 88.1%。
SpPIN 與 SnNOUT,以及它們什麼時候會騙人
臨床上流傳的助記是:
- SpPIN:Sp(specificity)高的檢驗,Positive 結果可以rule IN(確立診斷)
- SnNOUT:Sn(sensitivity)高的檢驗,Negative 結果可以rule OUT(排除診斷)
道理是對的:特異度高表示沒病的人幾乎不會被判成陽性,所以真的出現陽性,多半不是誤判。把同一個標記換三個切點,就能看到這組助記的兩端:
| 切點(µg/L) | 敏感度(95% CI) | 特異度(95% CI) | TP / FP / FN / TN | LR+ | LR− |
|---|---|---|---|---|---|
| 0.05 | 97.6%(87.4–99.6) | 6.9%(3.0–15.2) | 40 / 67 / 1 / 5 | 1.05 | 0.35 |
| 0.20 | 63.4%(48.1–76.4) | 80.6%(70.0–88.0) | 26 / 14 / 15 / 58 | 3.26 | 0.45 |
| 0.60 | 22.0%(12.0–36.7) | 100.0%(94.9–100.0) | 9 / 0 / 32 / 72 | 無法估計 | 0.78 |
最低的那個切點敏感度 97.6%,看起來是漂亮的 SnNOUT;最高的那個切點特異度 100.0%(偽陽性一個都沒有),看起來是完美的 SpPIN。兩個都有問題:
驗證性偏誤:診斷研究最常見的坑
到目前為止,本頁預設每一位病人都做了參考標準。真實的診斷研究常常不是這樣:參考標準往往是侵入性的、貴的、或有風險的(切片、血管攝影、開刀、長期追蹤),於是實務上只有檢驗陽性的人會被送去做,陰性的人就回家了。
這叫部分驗證偏誤(partial verification bias),也叫 work-up bias。後果不是「數字有點抖」,是方向固定的系統性偏誤:
把本頁的資料當成完整驗證的真相,然後假設只有一部分檢驗陰性者被送去做參考標準,剩下的人從分析中消失:
| 檢驗陰性者被驗證的比例 | 進入分析的人數 | 算出來的敏感度 | 算出來的特異度 |
|---|---|---|---|
| 100% | 113 | 63.4% | 80.6% |
| 70% | 91 | 71.2% | 74.4% |
| 50% | 77 | 77.6% | 67.4% |
| 30% | 62 | 85.2% | 55.4% |
| 10% | 47 | 94.5% | 29.3% |
figures/scripts/B4-01-sens-spec.R方向可以用算式看出來,不必背:偽陰性(FN)只出現在檢驗陰性的人裡,所以少驗證陰性者 = 少發現 FN = 敏感度的分母縮水 → 敏感度被高估;真陰性(TN)同樣只出現在陰性者裡,少驗證 = 少發現 TN,而偽陽性(FP)一個不漏地全在 → 特異度被低估。
當陰性者只有 10% 被驗證時,敏感度從真值 63.4% 被推到 94.5%,特異度從 80.6% 掉到 29.3%。一篇報告「敏感度超過九成」的診斷研究,如果沒交代陰性者怎麼被驗證,這個數字本身不能拿來用。
常見誤用
| 誤用 | 為什麼錯 |
|---|---|
| 把敏感度與 PPV 講成同一件事 | 一個沿著疾病狀態的行除、一個沿著檢驗結果的列除,分母完全不同 |
| 報敏感度不報信賴區間 | 有效樣本數是有病者人數,通常遠小於總人數,區間往往很寬 |
| 在特異度接近 1、偽陽性為 0 時宣稱「完全不會誤判」 | 零偽陽性只保證上界,區間下界仍可能明顯低於 1;此時 LR+ 也無法估計 |
| 只憑高敏感度就說陰性可以排除診斷 | 要看的是 LR−;敏感度高但特異度極低時 LR− 仍可能接近 1 |
| 把文獻的敏感度直接套到自己的病人身上 | 病情組成不同會改變測到的敏感度(光譜效應),這是要檢查的假設 |
| 只驗證檢驗陽性者卻照常報敏感度與特異度 | 部分驗證偏誤會同時高估敏感度、低估特異度 |
| 陽性與陰性用不同的參考標準卻合併分析 | 差異驗證偏誤,兩種標準定義的疾病不是同一件事 |
| 用整體正確率(accuracy)當主要指標 | 在盛行率低時,全部判陰性就能得到很高的正確率 |
| 自己從資料挑一個最好看的切點再報它的敏感度 | 樂觀偏誤,見 切點選擇與兩條 ROC 的比較 |
| 把連續的檢驗值二分之後就不提切點 | 沒有切點,敏感度與特異度沒有定義 |
重跑本頁的所有數字
/opt/homebrew/bin/Rscript figures/scripts/B4-01-sens-spec.R讀讀看這張圖
答案取自產生本頁圖表的同一份統計輸出,不是另外打上去的。
113 位病人裡有 41 位結果差。在切點 0.20 上,敏感度是 0.634、特異度是 0.806。一位讀者說「所以來做這個檢驗的人有六成三會被正確判讀」。哪一個說法對?
看答案與解析
正確答案: 不對。整批人被正確判讀的比例是 0.743,敏感度的分母只有結果差的那 41 位
敏感度的分母是那 41 位結果差的病人,不是 113 位受檢者——它沿著直行除。整批人被正確判讀的比例是 0.743,比 0.634 高,因為特異度那一側的人比較多、也被判得比較準。0.650 則是陽性預測值,沿著橫列除,回答的是「一張陽性報告代表什麼」,不是「有多少人被判對」。三個數字都在同一張表上,差別只在分母算誰;把敏感度讀成整體正確率,等於把一個沿著直行除的量當成整張表的摘要。
把同一批人的對照組整批複製,世代盛行率從 36.3% 降到 12.5%,四個指標再算一次。這張表在示範什麼?
看答案與解析
正確答案: 盛行率最低那一列的 PPV 掉到 0.317,而敏感度與特異度一格都沒動
複製對照組只改變了兩群人的相對大小,沒有改變任何一個人的檢驗結果。敏感度在病例那一群裡算、特異度在非病例那一群裡算,所以整張表上它們一格都沒動;PPV 與 NPV 的分母是檢驗陽性者與檢驗陰性者,混了兩群人,於是跟著比例移動——0.317 與 0.881 是同一個檢驗在兩個世代的 PPV。至於 0.939 這個 NPV,高是因為那個世代大多數人本來就沒病,不是因為檢驗變強了:不做檢驗、看到誰都說沒病,正確率也差不多。
切點 0.60 那一列,72 位結果好的病人裡一個偽陽性都沒有,特異度的點估計是 1.000。可以說這個切點不會誤判沒病的人嗎?
看答案與解析
正確答案: 不能。95% 信賴區間的下界是 0.949,與這份資料相容的偽陽性率仍可到五個百分點左右
零個偽陽性只是把點估計頂到上界,它不保證真值就在那裡:72 個人裡零個事件,對應的下界是 0.949,也就是與這份資料相容的偽陽性率最高仍有五個百分點左右。而且這個切點的代價並不小——敏感度只剩 0.220,多數結果差的病人會被判成陰性;同時 LR+ 的分母是零,根本估不出來。把「這份樣本沒看到誤判」讀成「這個檢驗不會誤判」,是把區間讀成保證。
切點 0.05 的敏感度是 0.976,看起來是漂亮的 SnNOUT。這個切點的陰性結果真的能排除疾病嗎?
看答案與解析
正確答案: 不能。同一列的特異度只有 0.069,這個切點幾乎把所有人都判成陽性,陰性之所以少見,是因為它幾乎不放過任何人
SnNOUT 的成立條件不是敏感度單獨高,而是陰性結果本身帶有訊息。這個切點的特異度只有 0.069,也就是結果好的人裡九成以上同樣被判成陽性——敏感度 0.976 是靠「幾乎不放過任何人」換來的。衡量陰性結果價值的量是 LR−,這裡是 0.351:它把疾病的勝算降到大約三分之一,是有用的一步,但離排除還很遠。要把陰性當成排除,通常要求 LR− 再小一個數量級。
一份診斷研究只把部分檢驗陰性者送去做參考標準。用本頁的模擬,當陰性者只有一成被驗證時,算出來的敏感度是 0.945、真值是 0.634。特異度會往哪個方向跑?
看答案與解析
正確答案: 特異度掉到 0.293——少驗證陰性者會少發現真陰性,而偽陽性一個都不會漏掉
偽陰性只出現在檢驗陰性的人裡,少驗證陰性者等於少發現偽陰性,敏感度的分母縮水,於是被高估到 0.945。真陰性同樣只出現在陰性者裡,跟著一起消失;而偽陽性全在陽性那一側,一個都不會漏掉——特異度的分子縮水、分母裡的偽陽性不動,於是被低估到 0.293。兩個指標往相反方向跑,這是固定的方向,不是隨機的抖動。0.806 是完整驗證時的特異度真值,0.776 則是另一個驗證比例下的敏感度,不是特異度。所以一篇沒交代陰性者怎麼被驗證的診斷研究,它報的敏感度不能照面值用。
敏感度是 0.634,95% 信賴區間 0.481 到 0.764,比同一張表上特異度的區間明顯寬。為什麼?
看答案與解析
正確答案: 因為敏感度只由 41 位結果差的病人決定,而特異度的分母是另外那 72 位結果好的人
診斷準確度研究的有效樣本數要分兩邊算:敏感度的分母是 41 人,特異度的分母是 72 人,兩個區間的寬度各自由這兩個數字決定,而不是由 113 這個總人數決定。40 是檢驗陽性的人數,它是 PPV 的分母,不是敏感度的。所以看到一篇診斷研究只報總收案數、不分開報兩群的人數,先假設敏感度那一側的區間很寬。
用到這個方法的章節
延伸觀看
Sensitivity and specificity – explained in 3 minutes
Calculating Sensitivity and Specificity using a 2x2 table
醫學統計 EP17 敏感度、特異度與預測值
Machine Learning Fundamentals: Sensitivity and Specificity
Principles of Epidemiology 10. Diagnosis, Tests, and Screening素材來源與授權
本頁為原創內容