ROC 曲線與 AUC
ROC 是所有切點的軌跡,AUC 有一個乾淨的機率解釋(隨機取一位有病與一位沒病者,前者分數較高的機率)——這一頁把那個解釋用配對數數驗證一次。也講 AUC 看不見的兩件事:它完全不衡量校準,而且它照定義就不受盛行率影響,所以在極不平衡的資料上它不會惡化,會惡化的是 precision-recall。
一條曲線就是一整排 2×2 表
2×2 表那一頁的每一組敏感度/特異度,都綁在一個特定的切點上。換一個切點,四個格子全部重排。
把切點從最低掃到最高,每一個位置都算一次敏感度與 1 − 特異度,然後把這些點連起來——那條線就是 ROC 曲線(receiver operating characteristic curve,名字來自二戰的雷達訊號研究)。
用同一份 pROC::aSAH 的 S100β 看五個切點:
| 切點(µg/L) | TP / FP / FN / TN | 敏感度(縱軸) | 1 − 特異度(橫軸) |
|---|---|---|---|
| 0.05 | 40 / 67 / 1 / 5 | 0.976 | 0.931 |
| 0.10 | 34 / 44 / 7 / 28 | 0.829 | 0.611 |
| 0.20 | 26 / 14 / 15 / 58 | 0.634 | 0.194 |
| 0.40 | 17 / 8 / 24 / 64 | 0.415 | 0.111 |
| 0.60 | 9 / 0 / 32 / 72 | 0.220 | 0.000 |
切點愈低,愈容易判成陽性,敏感度愈高、特異度愈低——曲線上的每一點都是一組取捨,沒有哪一點天生比較對。哪一點才對,取決於偽陽性與偽陰性各自的代價,那是下一頁的題目。
figures/scripts/B4-04-roc-auc.RAUC:曲線底下的面積,以及它真正的意思
AUC(area under the curve,也寫作 C-statistic 或 concordance index)把整條曲線壓成一個數字:
| 標記 | AUC | 95% CI(DeLong) |
|---|---|---|
| S100β | 0.731 | 0.630–0.833 |
| NDKA | 0.612 | 0.501–0.723 |
面積本身不好解釋,但它等於一個非常具體的機率:
隨機抽一位結果差的病人與一位結果好的病人,前者的標記值比後者高的機率(平手算一半)。
這不是類比,是恆等式。可以直接數給自己看:41 位結果差的病人與 72 位結果好的病人,
兩兩配對共 2952 對。其中 2124 對是結果差的那位 S100β 較高,70 對平手。
把平手算一半:(2124 + 70 ÷ 2)÷ 2952 = 0.7314,
與 pROC 算出來的 AUC 0.7314 完全相同(產圖腳本裡有一行斷言在守這件事,兩者不符就不會產出檔案)。
同一個恆等式也解釋了幾件常被分開記憶的事:
- AUC 只看排序,不看數值。 把所有標記值取對數、平方、乘十,AUC 完全不動——只要順序沒變。
- AUC 等價於 Mann-Whitney U 檢定的統計量(除以樣本數乘積之後)。所以「AUC 顯著大於 0.5」與「兩組的分布位置有差異」是同一個檢定。
- AUC 0.5 是完全沒有排序能力,1 是完美排序。AUC 不會因為你收了更多沒病的人而變差,這件事在後面談 AUC 碰上不平衡世代那一節會變成一個陷阱。
AUC 看不見的第一件事:校準
AUC 衡量的是鑑別力(discrimination):模型能不能把有病的排在沒病的前面。它完全不管校準(calibration):模型說「這個人有 20% 的機率有病」時,這種人裡是不是真的有 20% 有病。
證明只要一行:把一組預測機率在 log-odds 尺度上整體平移一個常數。排序完全沒變,所以 AUC 完全不會動;但每個人的預測機率都錯了。
用 aSAH 配一個邏輯迴歸(結果差 ~ S100β + NDKA + 年齡),然後把預測值往下平移 1.50 個 log-odds:
| 原始預測 | 平移後的預測 | |
|---|---|---|
| AUC | 0.7751 | 0.7751 |
| Brier score(愈小愈好) | 0.174 | 0.222 |
| 平均預測機率 | 36.3% | 15.9% |
| 實際發生率 | 36.3% | 36.3% |
| 校準截距(0 才是對的) | 0.00 | 1.50 |
figures/scripts/B4-04-roc-auc.R平移後的模型平均預測 15.9%,而實際發生率是 36.3%——系統性地低估了風險,Brier score 從 0.174 惡化到 0.222,AUC 卻毫無反應。
Brier score 在這裡只是佐證,它本身單看沒有尺度——它同時混了鑑別力與校準, 所以一個數值要有意義得跟基準比。而有設限的存活資料直接這樣算是錯的。 完整的說明,以及它與重分類指標(NRI/IDI)的關係,見 Brier score、NRI 與 IDI。
AUC 看不見的第二件事:這群人裡有多少病人
常見的說法是「類別極不平衡時 AUC 會誤導,應該改看 PR 曲線」。這句話的結論可以用,但理由通常被講錯,值得看清楚機制。
把 aSAH 的對照組整批複製 20 份(病人維持原來那些),盛行率從 36.3% 降到 2.8%:
| 原始資料 | 對照組複製後 | |
|---|---|---|
| 總人數 | 113 | 1481 |
| 盛行率 | 36.3% | 2.8% |
| AUC | 0.7314 | 0.7314 |
| PR 曲線下的平均精確度 | 0.686 | 0.343 |
| (沒有資訊的基準線) | 0.363 | 0.028 |
figures/scripts/B4-04-roc-auc.R動手跑一次
library(pROC)
data(aSAH, package = "pROC")
r1 <- roc(aSAH$outcome, aSAH$s100b,
levels = c("Good", "Poor"), direction = "<", quiet = TRUE)
r2 <- roc(aSAH$outcome, aSAH$ndka,
levels = c("Good", "Poor"), direction = "<", quiet = TRUE)
auc(r1); ci.auc(r1, method = "delong")
plot(r1); plot(r2, add = TRUE, col = "steelblue")
# 曲線上任何一點的 2x2
coords(r1, x = c(0.05, 0.10, 0.20, 0.40, 0.60), input = "threshold",
ret = c("threshold", "sensitivity", "specificity", "tp", "fp", "fn", "tn"),
transpose = FALSE)
# AUC 的機率解釋,自己數一次
case <- aSAH$s100b[aSAH$outcome == "Poor"]
ctrl <- aSAH$s100b[aSAH$outcome == "Good"]
(sum(outer(case, ctrl, ">")) + 0.5 * sum(outer(case, ctrl, "=="))) /
(length(case) * length(ctrl))
# 排序不變 => AUC 不變
auc(roc(aSAH$outcome, log(aSAH$s100b), levels = c("Good", "Poor"),
direction = "<", quiet = TRUE))
# 匯出給下面的 Python 用
write.csv(aSAH, "aSAH.csv", row.names = FALSE)驗證環境:R 4.6.0 + pROC 1.19.0.1。pROC 的 roc() 建議明確寫出 levels 與 direction,否則它會自己猜方向並印出訊息。
import numpy as np
import pandas as pd
from sklearn.metrics import roc_auc_score, roc_curve, average_precision_score
d = pd.read_csv("aSAH.csv") # 由本頁上面那段 R 匯出
y = (d["outcome"] == "Poor").astype(int)
print(roc_auc_score(y, d["s100b"]))
print(roc_auc_score(y, d["ndka"]))
fpr, tpr, thr = roc_curve(y, d["s100b"])
# 機率解釋
case = d.loc[y == 1, "s100b"].to_numpy()
ctrl = d.loc[y == 0, "s100b"].to_numpy()
cmp = case[:, None] > ctrl[None, :]
tie = case[:, None] == ctrl[None, :]
print((cmp.sum() + 0.5 * tie.sum()) / (len(case) * len(ctrl)))
# PR 曲線下的面積,與它的基準線
print(average_precision_score(y, d["s100b"]), y.mean())sklearn 的 roc_auc_score 與 pROC 的結果一致(同樣把平手算一半);average_precision_score 對應 PR 曲線下的面積。
讀論文裡的 AUC 時要問的四件事
- 有沒有信賴區間? 本頁 S100β 的 AUC 是 0.731,區間 0.630–0.833,寬度超過 0.2。診斷研究的樣本常常不大,只報點估計會讓讀者高估精確度。
- 這個 AUC 是在哪一批資料上算的? 用來配模型的同一批資料算出來的 AUC 是樂觀的;要看內部驗證(bootstrap 或交叉驗證)修正後的值,或外部驗證的值。樂觀有多大,下一頁會用同一份資料量給你看。
- 有沒有報校準? 只有 AUC 的預測模型論文,等於只回答了一半。
- AUC 提高了多少才有臨床意義? 在既有模型上加一個新標記,AUC 常常只升千分之幾——這種幅度是否值得多抽一管血、多花一筆錢,AUC 本身回答不了。要回答它需要決策分析的工具(net benefit、decision curve analysis),或直接看在臨床決策門檻附近有多少人被重新分類。
常見誤用
| 誤用 | 為什麼錯 |
|---|---|
| 用 AUC 當「這個模型準不準」的唯一答案 | AUC 只衡量鑑別力,完全不看校準 |
| 直接比較兩篇論文的 AUC 大小 | 族群、盛行率、病情組成不同,AUC 不可跨研究直接比 |
| 看到兩個 AUC 差很多就宣稱某個標記較好 | 同一批病人的兩條曲線是相關的,要做配對 DeLong 檢定 |
| 報 AUC 不報信賴區間 | 樣本不大時區間很寬,點估計會誤導 |
| 用訓練模型的同一批資料報 AUC 卻不說 | 樂觀偏誤,要用內部或外部驗證修正 |
| 認為 AUC 在不平衡資料上會下降 | ROC 的兩軸都在疾病狀態內部算,照定義不受盛行率影響 |
| 跨盛行率直接比較 PR 曲線的面積 | PR 的基準線就是盛行率,要跟基準線一起讀 |
| 只用 AUC 決定要不要加一個新的生物標記 | AUC 的微小提升與臨床價值沒有固定關係,需要決策分析 |
| 從 ROC 上挑最好看的一點就當成建議切點 | 該點是從同一份資料選出來的,而且「最好看」本身就是一個代價假設——最後是那份資料的盛行率替你做了這個假設 |
| 把 AUC 說成「診斷正確率」 | 它是排序正確的機率,不是任何一個切點的正確率 |
| 用 covid_testing 的 Ct 值畫 ROC 教學 | Ct 值來自它要被驗證的那個 PCR 結果,是循環論證 |
重跑本頁的所有數字
/opt/homebrew/bin/Rscript figures/scripts/B4-04-roc-auc.R讀讀看這張圖
答案取自產生本頁圖表的同一份統計輸出,不是另外打上去的。
本頁這個生物標記的 AUC 是 0.731。這個數字的意思是什麼?
看答案與解析
正確答案: 隨機抽一位結果差與一位結果好的病人,前者的標記值較高的機率是 0.731,平手算一半,本頁數過 2952 對
AUC 等於一個很具體的機率:隨機抽一位結果差與一位結果好的病人,前者的標記值較高的機率,平手算一半。這不是類比而是恆等式,本頁用 2952 對配對數過一次。所以 AUC 只看排序,把所有標記值取對數或乘十都不會動它。0.634 是切點 0.20 上的敏感度,它綁在某一個操作點上,而 AUC 是整條曲線的摘要;把 AUC 說成診斷正確率,會讓人以為它綁在某個切點上。0.775 是本頁後面那個邏輯迴歸模型的 AUC,而且再高的 AUC 也不保證預測機率本身是對的——那是校準的事。
把一組預測機率在 log-odds 尺度上整體往下平移 1.5,AUC 從 0.7751 變成 0.7751,一動也沒動。這代表平移後的模型一樣好嗎?
看答案與解析
正確答案: 不一樣。平移後的平均預測機率是 0.1591,而實際發生率是 0.3628——每個人的風險都被系統性低估了
平移只改變每個人的預測值大小,不改變他們的先後順序,而 AUC 只看順序,所以它到小數點後四位都一樣。校準管的是另一件事:平移後平均預測 0.1591,實際發生率 0.3628,模型系統性低估風險。Brier score 從 0.1741 惡化到 0.2218,這個方向沒有錯——它同時吃鑑別力與校準,所以校準壞掉時它確實會變差。錯的是把這個惡化讀成鑑別力下降:這批預測沒有任何一對換過順序,AUC 到小數點後四位一模一樣,惡化的整份都來自校準。Brier 告訴你「這個模型變差了」,要知道「差在哪一塊」,得把 AUC 與平均預測對實際發生率的落差擺在一起看。只要決策綁在一個絕對機率門檻上,校準不良的模型就會系統性把病人分錯邊,而 AUC 完全看不出來。
把對照組整批複製 20 份,盛行率從 36.3% 降到 2.8%。AUC 與 PR 曲線的平均精確度各自會怎麼樣?
看答案與解析
正確答案: AUC 一格沒動,平均精確度掉到 0.3426,因為 precision 就是 PPV,它吃進盛行率
ROC 的兩個軸——敏感度與一減特異度——都是在疾病狀態內部算的比例,複製對照組不改變其中任何一個,所以 AUC 照定義不動,仍然是 0.7314。PR 曲線的縱軸 precision 就是 PPV,沿著橫列除,於是從 0.6856 掉到 0.3426。但這不表示模型變差了:沒有資訊的基準線也從 36.3% 掉到 2.8%,以相對於基準線的倍數看,第二個反而拉得更開。所以 PR 的數字同樣不能跨盛行率直接比大小,要跟自己的基準線一起讀。
本頁這個生物標記的 AUC 是 0.731(95% CI 0.630 到 0.833),另一個標記 NDKA 是 0.612(0.501 到 0.723)。可以寫「前者優於 NDKA」嗎?
看答案與解析
正確答案: 不行。前者的下界 0.630 低於 NDKA 的上界,兩個區間重疊很多,而且兩條曲線來自同一批病人
兩個點估計看起來差得不少,但前者的下界 0.630 明顯低於 NDKA 的上界 0.723,兩個區間有相當大的重疊。更重要的是這兩個標記量在同一批病人身上,兩個 AUC 估計值是相關的,不能當成兩個獨立樣本比較——正確的做法是配對的 DeLong 檢定,那在切點那一頁。NDKA 的下界 0.501 講的是另一件事:它問的是 NDKA 自己有沒有鑑別力,不是兩者有沒有差。在做完配對檢定之前,不要寫「優於」。
ROC 表上切點 0.05 那一列,敏感度 0.976、一減特異度 0.931。這一列說明這個檢驗很好嗎?
看答案與解析
正確答案: 說明不了。同一列的偽陽性率是 0.931,這一點幾乎貼在右上角
ROC 的右上角是切點低到把所有人都判成陽性,那一點任何檢驗都有,與檢驗好不好無關。切點 0.05 的座標是敏感度 0.976 配上偽陽性率 0.931,幾乎就在那個角落——敏感度高是用「幾乎不放過任何人」換來的。偽陽性率 0.000 那一列則是另一個極端,靠近左下角,它的敏感度只剩兩成出頭,同樣不是天生比較對。曲線上每一點都是一組取捨,哪一點才對取決於偽陽性與偽陰性各自的代價,不能從圖上挑最好看的那一點。
把每一位病人的標記值取對數之後重畫 ROC。AUC 會變成多少?
看答案與解析
正確答案: 還是 0.7314,取對數不改變任何一對病人的先後順序,而 AUC 只看順序
AUC 等於「隨機一位結果差的病人標記值比一位結果好的病人高」的機率,它完全由排序決定。任何嚴格遞增的變換——取對數、開根號、乘十——都不改變任何一對的先後,所以 AUC 一位小數都不會動,仍然是 0.7314。同一個性質也解釋了為什麼 AUC 等價於 Mann-Whitney U 的統計量。0.7751 是本頁那個三變項邏輯迴歸的 AUC,0.6120 是另一個標記 NDKA 的 AUC,兩個都是真的數字,但都不是這個變換會產生的結果。反過來說,AUC 不動這件事也是它的限制:整組預測機率偏掉,它同樣不會動。
用到這個方法的章節
延伸觀看
ROC and AUC, Clearly Explained!
How to interpret ROC curves
ROC Curves and Area Under the Curve (AUC) Explained
ROC 系列 1/6:ROC 曲線是什麼
ROC 系列 4/6:AUC 的兩大臨床陷阱素材來源與授權
本頁為原創內容