專家已經雙重審閱,尚未人工抽查

決策曲線分析

決策曲線問的不是模型準不準,而是「照這個模型做決定,比全部治療或都不治療好嗎」。它把偽陽性與偽陰性的相對代價編碼進一個門檻機率,再算出淨效益。這一頁在真實的外部驗證上做出那個常被提起卻很少被示範的對照——鑑別力較高的模型,在臨床上會用到的門檻附近,淨效益反而較低。

前面兩頁都沒回答的那個問題

AUC告訴你模型排序排得好不好。 校準告訴你算出來的機率數值對不對。 兩者都通過之後,還有一個問題沒被回答:

照這個模型做決定,會比現在的做法好嗎?

而「現在的做法」通常只有兩種:全部都治(或全部都做那個檢查),或都不治。 一個模型如果贏不過這兩條線裡比較好的那一條,它再準也沒有用。

決策曲線分析(decision curve analysis, DCA)就是在同一張圖上比較這三件事。

門檻機率:把臨床判斷寫成一個數字

要說「這個模型有沒有用」,必須先說偽陽性與偽陰性哪一個比較糟、糟多少。 DCA 的做法是讓你選一個門檻機率 ptp_t

風險達到 ptp_t 我就治療,低於 ptp_t 我就不治療。

這一句話已經把相對代價講完了。你願意在風險剛好 ptp_t 時開始治療,等於是說 「治療的害處」與「不治療而出事的害處」在這個風險水準上剛好打平。整理之後:

偽陽性的代價偽陰性的代價=pt1pt\frac{\text{偽陽性的代價}}{\text{偽陰性的代價}} = \frac{p_t}{1 - p_t}
門檻機率換算的代價比白話
30%0.43漏掉一個真的會出事的人,跟白治 2.3 個不會出事的人一樣糟
40%0.67漏掉一個真的會出事的人,跟白治 1.5 個不會出事的人一樣糟
50%1.00漏掉一個真的會出事的人,跟白治 1.0 個不會出事的人一樣糟
60%1.50漏掉一個真的會出事的人,跟白治 0.7 個不會出事的人一樣糟

淨效益:把偽陽性換算成真陽性再相減

淨效益(net benefit)的定義是:

NB(pt)=TPnFPn×pt1pt\mathrm{NB}(p_t) = \frac{\mathrm{TP}}{n} - \frac{\mathrm{FP}}{n} \times \frac{p_t}{1 - p_t}

第一項是「每個病人平均抓到幾個真陽性」,第二項是偽陽性按代價比折算成真陽性的當量之後扣掉。

單位因此是「每位病人的淨真陽性數」。乘以 1000 就變成 「每千位病人,用這個模型比什麼都不做多抓到幾個真陽性,而且已經扣掉多治療的代價」。

存活資料要多一步:不能直接數事件(有人在時間視野之前就設限了), 所以 TP 與 FP 的比例用被判為高風險那群人的 Kaplan-Meier 風險去估。

兩條參考線:

  • 都不治:誰都不治,沒有真陽性也沒有偽陽性,淨效益恆為 0
  • 全部治:全部都判為陽性,NB=事件率(1事件率)×pt1pt\mathrm{NB} = \text{事件率} - (1 - \text{事件率}) \times \frac{p_t}{1-p_t}。 門檻愈高它掉得愈快,在門檻等於事件率的地方穿過 0。

本頁的例子:在某些門檻上,鑑別力比較高的那個模型淨效益反而較差

survival::gbsg(686 人、299 個事件, 5 年觀察風險 50.8%)上比較三個模型, 全部都在 survival::rotterdam 開發:

模型預測變項數C-index平均預測 5 年風險
A:八個預測變項,照原樣搬過來80.661743.1%
A′:同一個模型,在此世代重新校準80.661751.5%
B:四個預測變項,在此世代重新校準40.641651.3%

模型 A 的 C-index 比模型 B 高 0.020。 但 A 沒有重新校準,它平均預測 43.1%, 而這群人實際發生 50.8%——系統性低估

左圖是決策曲線,橫軸為門檻機率、縱軸為淨效益;水平的灰線是都不治、灰色虛線是全部治。門檻低的一端,三條模型曲線與全部治重合或只高出一點;接著全部治急速下滑,在 51% 處穿過零,而三條模型曲線在整個繪製範圍內都留在零以上。右圖把病人依預測風險排序後畫出兩個模型的預測值,模型 A 的整條曲線在多數病人身上都低於實際觀察到的五年風險水平線,模型 B 的曲線則跨在那條線上下。
左:三個模型與兩條參考線的決策曲線。曲線的鋸齒來自被判為高風險那群人的 Kaplan-Meier 估計,人數少的地方抖動較大。右:兩個模型給每位病人的預測風險,依大小排序。紅色虛線是這群人實際的五年風險。產圖腳本 figures/scripts/B5-07-decision-curve.R
門檻全部治模型 A模型 A′模型 BA 判為高風險的比例B 判為高風險的比例
30%0.29770.30380.29770.297781%100%
40%0.18060.19250.21890.196644%90%
50%0.01670.09820.12840.121825%42%
60%-0.22910.06470.07190.061715%19%

誠實的部分:重新校準用掉了同一批人

上面 A′ 與 B 的重新校準都是在同一個世代上做的,然後又在同一個世代上評估。 那是樂觀的(理由與內部驗證那一頁相同)。

把驗證世代切一半:在一半上重新校準模型 B,到另一半量淨效益,重複 200 次 (模型 A 不需要重新校準,所以它兩邊都一樣):

門檻模型 A模型 B(在另一半上)全部治B 勝過 A 的比例
30%0.30440.29960.299536%
40%0.19370.18740.182747%
50%0.09740.11140.019278%
60%0.06380.0561-0.225927%

怎麼讀一張決策曲線

看什麼怎麼判斷
模型曲線在你的門檻附近,有沒有高過兩條參考線沒有的話,那個門檻下不該用這個模型——直接全部治或都不治比較好
門檻範圍畫得對不對圖的橫軸應該涵蓋臨床上真的會用到的範圍。橫軸畫到 0.9 但沒人會用那麼高的門檻,那一段是裝飾
兩個模型的曲線有沒有交叉常常會。交叉代表「哪個好」取決於門檻,不能給單一答案
「全部治」那條線什麼時候穿過零在門檻等於事件率的地方。本頁的資料在約 51% 處
曲線抖不抖抖動來自小分組的估計誤差;驗證樣本小的時候不要解讀細部起伏

動手跑一次

library(survival)
data(cancer, package = "survival")
# 與 B5-05 相同的世代調和與模型配適。攤開寫,這一段才自己跑得動。


H <- 5 * 365.25

rot <- rotterdam
rot$rfs_time  <- pmin(rot$rtime, rot$dtime)
rot$rfs_event <- as.integer(rot$recur == 1 | rot$death == 1)
rot$size_mm   <- c("<=20" = 15, "20-50" = 35, ">50" = 60)[as.character(rot$size)]
rot$grade3    <- as.integer(rot$grade >= 3)

ext <- gbsg
ext$rfs_time  <- ext$rfstime; ext$rfs_event <- ext$status
ext$size_mm   <- ext$size;    ext$grade3    <- as.integer(ext$grade >= 3)

# 兩個候選模型,與本頁產圖腳本同一份定義。
BIG   <- c("age", "meno", "size_mm", "grade3", "nodes", "log_pgr", "log_er", "hormon")
SMALL <- c("age", "size_mm", "nodes", "grade3")
rot$log_pgr <- log1p(rot$pgr); rot$log_er <- log1p(rot$er)
ext$log_pgr <- log1p(ext$pgr); ext$log_er <- log1p(ext$er)
keep <- c("rfs_time", "rfs_event", BIG)
dev  <- rot[complete.cases(rot[, keep]), keep]
ext  <- ext[complete.cases(ext[, keep]), keep]

# Breslow 的累積基線風險,直接寫出來——basehaz(centered = FALSE) 與
# predict(type = "lp") 不匹配(後者是中心化的),配錯會讓每個預測風險都少乘一個常數。
breslow_h0 <- function(time, event, lp, h = H) {
  o <- order(time); time <- time[o]; event <- event[o]; r <- exp(lp[o])
  at_risk <- rev(cumsum(rev(r)))
  sum(1 / at_risk[event == 1 & time <= h])
}
form_of <- function(v) as.formula(paste("Surv(rfs_time, rfs_event) ~",
                                        paste(v, collapse = " + ")))
fit_big   <- coxph(form_of(BIG),   data = dev)
fit_small <- coxph(form_of(SMALL), data = dev)
h0_big   <- breslow_h0(dev$rfs_time, dev$rfs_event,
                       as.numeric(as.matrix(dev[, BIG]) %*% coef(fit_big)))
lp_big   <- as.numeric(as.matrix(ext[, BIG])   %*% coef(fit_big))
lp_small <- as.numeric(as.matrix(ext[, SMALL]) %*% coef(fit_small))

# A:八個變項,原封不動搬過來。B:四個變項,再對這個世代重新校準。
pred_A <- 1 - exp(-h0_big * exp(lp_big))
sl_B   <- unname(coef(coxph(Surv(rfs_time, rfs_event) ~ lp_small, data = ext))[1])
h0_B   <- breslow_h0(ext$rfs_time, ext$rfs_event, sl_B * lp_small)
pred_B <- 1 - exp(-h0_B * exp(sl_B * lp_small))


km_risk <- function(d, h = H) {
  if (nrow(d) < 5 || sum(d$rfs_event) == 0) return(NA_real_)
  k <- survfit(Surv(rfs_time, rfs_event) ~ 1, data = d)
  1 - summary(k, times = h, extend = TRUE)$surv
}

# 淨效益:兩行算式,其餘都是把人挑出來
net_benefit <- function(pred, d, pt) sapply(pt, function(p) {
  treat  <- pred >= p
  if (sum(treat) < 10) return(0)          # 太少人就沒有可靠的 KM
  ptreat <- mean(treat)
  r      <- km_risk(d[treat, , drop = FALSE])
  r * ptreat - (1 - r) * ptreat * p / (1 - p)
})

nb_all <- function(d, pt) { r <- km_risk(d); r - (1 - r) * pt / (1 - pt) }

pt <- seq(0.05, 0.8, by = 0.01)
plot(pt, nb_all(ext, pt), type = "l", lty = 2, ylim = c(-0.05, 0.5),
     xlab = "Threshold probability", ylab = "Net benefit")
abline(h = 0)                              # 都不治
lines(pt, net_benefit(pred_A, ext, pt), col = "darkorange", lwd = 2)
lines(pt, net_benefit(pred_B, ext, pt), col = "steelblue",  lwd = 2)

# 換算成「每千位病人多幾個淨真陽性」
1000 * (net_benefit(pred_B, ext, 0.5) - net_benefit(pred_A, ext, 0.5))

驗證環境:R 4.6.0 + survival 3.8.6。這裡刻意不呼叫 dcurves 套件,把公式攤開寫——決策曲線只有兩行算式,自己寫一次比記得參數名稱有用。實務上 dcurves::dca() 可以直接吃 Surv 物件並產生同樣的曲線。

讀論文時要問的四件事

  1. 門檻範圍合理嗎? 圖的橫軸應該涵蓋臨床上真的會用到的範圍,而且作者要說明為什麼是那個範圍。
  2. 有沒有畫兩條參考線? 少了「全部治」那條,讀者無從判斷模型有沒有加值。
  3. 決策曲線是在哪一批資料上畫的? 在開發資料上畫的決策曲線帶著樂觀偏誤, 跟表面 C-index 一樣不能直接採信。
  4. 模型有沒有先校準好? 決策曲線對校準極度敏感——本頁整個例子就是這件事。

常見誤用

誤用為什麼錯
用 AUC 高低決定要不要採用模型AUC 不含臨床代價;校準不良的高 AUC 模型淨效益可能更差
決策曲線畫出來卻沒有「全部治」那條線沒有參考線就無法判斷模型有沒有加值
只報單一門檻的淨效益門檻是臨床判斷,不同醫師與病人會不同;要報一段範圍
跨研究比較淨效益的數值淨效益含事件率,族群不同就不可比
在開發資料上畫決策曲線當成證據與表面 C-index 同樣樂觀
在重新校準過的同一批人身上報告淨效益改善樂觀偏誤;要切開或另找世代
把門檻機率當成模型參數去調到最好看它是臨床代價的編碼,不是可以優化的東西
曲線交叉時硬要說哪個模型比較好交叉代表答案取決於門檻
樣本小時解讀曲線的細部起伏那些抖動來自小分組的估計誤差
把淨效益說成「治療效果」它衡量的是「用這個模型做決定」的價值,不是治療本身的效果

那重分類指標呢

決策曲線問的是「用了這個模型,每一千人能多找出幾個而不多治療幾個」。 另一族回答「新模型有沒有比較好」的指標——Brier score、NRI、IDI——問的不是同一件事, 而且在這一頁的同一組模型 A 與 B 上,它們給出的訊號與淨效益相反。 那組對照見 Brier score、NRI 與 IDI

重跑本頁的所有數字

/opt/homebrew/bin/Rscript figures/scripts/B5-07-decision-curve.R

讀讀看這張圖

答案取自產生本頁圖表的同一份統計輸出,不是另外打上去的。

決策曲線的橫軸是門檻機率。把門檻訂在四成,等於宣告了什麼?

看答案與解析

正確答案: 偽陽性與偽陰性的代價比是 0.667——漏掉一個會出事的人,跟白治一個半不會出事的人一樣糟

門檻機率換算成代價比是門檻除以一減門檻,四成的門檻給 0.667:你願意在風險剛好四成時開始治療,等於說漏掉一個會出事的人,跟白治一個半不會出事的人一樣糟。0.429 是三成門檻的代價比;門檻是臨床決定,由治療的副作用、成本、侵入性與漏掉事件的後果決定,模型的表現一個字都沒有進來。1.500 是六成門檻的代價比:門檻愈高,代表你要求愈確定才動手,被判為高風險的人愈少,不是愈多。

在門檻五成這一列,模型 A 的淨效益 0.0982 低於模型 B 的 0.1218,而 A 的 C-index 比 B 高。機制是什麼?

看答案與解析

正確答案: 模型 A 在這個門檻只把 25% 的人判為高風險,被它擋在門檻外的人裡有很多真的會出事

C-index 只看排序,看不到整條刻度被壓低了。模型 A 平均預測 43.1%,而這群人實際發生 50.8%,門檻一畫在五成,被壓低的刻度立刻把人分錯邊:它只判了 25% 的人為高風險,模型 B 判了 42%。多治療本身不會讓淨效益上升——淨效益已經把偽陽性按代價比折算成真陽性扣掉了,治得太多的模型在高門檻上會被扣到低於全部治那條線。至於 15%,那是模型 A 在六成門檻上的比例;門檻愈高、判為高風險的人愈少,是每個模型都會有的事,跟排序能力無關。

第三個模型 A′ 是模型 A 在這個世代重新校準之後的版本,C-index 與 A 完全相同,而在門檻五成上淨效益 0.1284 高於 B 的 0.1218。這說明了什麼?

看答案與解析

正確答案: A′ 平均預測 0.5148,貼近這群人實際的五年風險——贏過 A 的不是變項比較少,是校準過

重新校準不動任何一對病人的先後,所以 A′ 與 A 的 C-index 一模一樣——被改好的不是排序,是刻度:A 原本平均預測 0.4312,遠低於這群人實際的五年風險,A′ 校準後是 0.5148。B 的平均預測 0.5135 與 A′ 幾乎一樣,這正好說明差別不在變項個數。額外的預測變項確實有價值,但那個價值要等校準修好才拿得到。

把驗證世代切一半、在一半上重新校準模型 B、到另一半量淨效益,重複 200 次。結論該怎麼寫?

看答案與解析

正確答案: 五成門檻上 B 在 78.0% 的切分裡贏過 A,所以要寫「在較高的門檻附近,簡單模型的淨效益較高」

決策曲線的結論永遠要綁在門檻範圍上。五成門檻附近 B 在 78.0% 的切分裡贏,那一段站得住;四成門檻只有 46.5%,大約是擲銅板,原本那張表在這個門檻上的優勢沒有撐過去——但擲銅板的意思是未偵測到差異,推不出 B 不如 A,那是把沒有訊號讀成反方向的訊號。六成門檻是 26.5%,比五成低沒錯,可是四成又比六成高,三個數字不是單調的,門檻愈高 B 愈吃虧這句話在同一張表上就被推翻了。

決策曲線上「全部治」那條線在某個門檻穿過零。它為什麼在那裡穿過?

看答案與解析

正確答案: 它在門檻 0.510 附近穿過零,因為那裡剛好是這群人的事件率——過了事件率,全部治就是賠本生意

全部治的淨效益等於事件率減掉一減事件率再乘上代價比,門檻愈高第二項愈重,到門檻等於事件率的地方剛好歸零——這份資料在 0.510 附近。0.508 確實是這群人實際的五年風險,兩個數字幾乎重合正是上面那句話的意思,但它不是模型 A 的平均預測:A 系統性低估,它的平均預測明顯更低。至於 -0.229,那是六成門檻上全部治的淨效益,早就是負的了——穿過零的位置是變號的那一點,不是任何一個負值所在的位置。

門檻五成這一列,模型 B 比模型 A 每千位病人多 24 個淨真陽性。這個數字可以怎麼用?

看答案與解析

正確答案: 三成門檻上同一個差是 -6,方向相反——這種差值只在指定的門檻上有意義,換一個門檻要重算

淨效益差是綁在一個門檻上的量:三成門檻上模型 B 反而少,四成門檻上多一點,五成門檻上多 24 個——同一對模型、同一批病人,只是換了門檻。所以 B 比 A 好這句話沒有門檻就沒有內容。它也不能跨研究比較:淨效益的公式裡有事件率,而事件率是族群的性質,同一個模型搬到事件率不同的世代會給出不同的淨效益,那不代表模型變好或變壞。至於三個門檻的差都是正的,看一眼表就知道不是——三成那一列就是負的。

素材來源與授權

本頁為原創內容

回報內容問題

這個站的統計內容由 AI 撰寫、AI 互審,人工只做抽查。你看得出來的錯,我們不一定看得出來。

寫得越具體越修得動,例如哪一句話跟哪本教科書/哪篇論文的說法不一致。

留了才回得了信;不留也會看。

一併送出的資訊

這些是自動帶上的,每一項都可以取消。